Building a Custom MCP Server
Writing your own MCP server with the official SDK to expose internal tools/resources to Claude Code — the minimal shape, the wiring, and the stdio rules that trip people up.
Why it matters
The off-the-shelf servers cover popular SaaS, but your internal systems — a deploy API, a feature-flag service, a domain-specific query layer — have no public server. A 40-line custom server turns any function into a model-callable tool, reusable across every MCP client your team uses, with one auth and one schema instead of N bespoke integrations. It’s the highest-leverage way to extend the agent past the local repo.
How it works
The @modelcontextprotocol/sdk (TS) and mcp (Python, incl. FastMCP) handle the JSON-RPC; you register handlers and pick a transport.
- Define a tool: a name, a one-line description (the model’s only doc), a JSON-Schema input, and an async handler returning content.
- Transport:
stdiofor a local child process (simplest — Claude spawns it); HTTP for a shared remote service. See stdio-vs-sse-http-servers. - Wire it: register with claude mcp add pointing at the launch command (stdio) or URL (HTTP).
- Golden stdio rule: JSON-RPC owns stdout — send all logs to stderr; a stray
print/console.logcorrupts the protocol stream. - Inside the SDK you can also expose resources and prompts; for in-process tools defined within a host app, see custom-tools-mcp-in-the-sdk.
Example
A minimal Python stdio server with one tool:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("deploy")
@mcp.tool()
def rollback(service: str, to_sha: str) -> str:
"""Roll a service back to a given commit SHA."""
# ...call internal deploy API...
return f"{service} rolled back to {to_sha}"
if __name__ == "__main__":
mcp.run() # stdio transportRegister it: claude mcp add deploy -- python server.py. The model can now call mcp__deploy__rollback, gated by your permissions and auth.
Pitfalls
- Logging to stdout. The #1 bug: any non-JSON byte on stdout breaks the stream. Log to stderr; test with the MCP Inspector before wiring into Claude.
- Fat tool surface. Exposing 30 fine-grained tools bloats every turn’s
tools/listcontext — design a few well-described, coarse tools. - No input validation. Treat tool args as untrusted model output; validate against the schema and guard side effects.
- Blocking the event loop. A synchronous slow call freezes the whole server; do real I/O
asyncand consider timeouts.