MCP Builder — Build MCP Servers
4-phase guide for building production-quality MCP (Model Context Protocol) servers.
When to Use
- Building a new MCP server for a tool/API integration
- Wrapping an existing API as MCP tools
- Need guidance on MCP best practices
Phase 1: Research
- Read the MCP spec: https://modelcontextprotocol.io/specification
- Check existing MCP servers for similar functionality
- Identify the tools, resources, and prompts needed
- Choose Python (FastMCP) or TypeScript (MCP SDK)
Phase 2: Implement
Python (FastMCP + Pydantic)
from fastmcp import FastMCP
from pydantic import BaseModel, Field
mcp = FastMCP("my-server", description="What it does")
class SearchParams(BaseModel):
query: str = Field(description="Search query")
limit: int = Field(default=10, description="Max results")
@mcp.tool()
def search(params: SearchParams) -> str:
"""Search for things."""
# Implementation here
return json.dumps(results)
if __name__ == "__main__":
mcp.run()
TypeScript (MCP SDK + Zod)
import { Server } from "@modelcontextprotocol/sdk/server";
import { z } from "zod";
const server = new Server({ name: "my-server", version: "1.0.0" });
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
// Handle tool calls
});
Phase 3: Review
- Each tool has a clear, actionable description
- Error messages include what went wrong AND how to fix it
- Workflow-oriented design (not just CRUD wrappers)
- Input validation with helpful error messages
- No hardcoded credentials (use env vars)
Phase 4: Evaluate
# Test compilation
python -m py_compile server.py
# Test startup (should start and accept stdio)
timeout 5s python server.py
# Test with mcporter
mcporter config add my-server python server.py
mcporter call --server my-server --tool search '{"query": "test"}'
Design Principles
- Workflow-oriented tools — not just data access, but task completion
- Actionable errors — tell the user what to do, not just what failed
- Sensible defaults — minimize required parameters
- Composable — tools should work together naturally
- Idempotent — safe to retry on failure
MCP SDK Docs
- Python: https://github.com/modelcontextprotocol/python-sdk
- TypeScript: https://github.com/modelcontextprotocol/typescript-sdk
- FastMCP: https://github.com/jlowin/fastmcp
When NOT to Use
- Wrapping a trivial single-function API — a direct tool call or shell command is simpler; don't build an MCP server for one endpoint
- When the client already has a native plugin/extension — building a redundant MCP layer adds maintenance overhead with no benefit
Pitfalls
- stdio vs HTTP transport confusion — FastMCP's
mcp.run()defaults to stdio (for Claude Desktop / mcporter); passingtransport="http"starts an HTTP server that Claude Desktop cannot connect to. Only use HTTP if you're targeting a remote deployment - FastMCP version conflicts — FastMCP < 1.0 and ≥ 1.0 have incompatible APIs (
@mcp.tool()decorator changed behavior); pin the version inrequirements.txtand checkfastmcp.__version__if tool registration silently fails - Returning raw Python objects — MCP tools must return strings (or structured content dicts); returning a dict or list directly causes a serialization error at runtime; always
json.dumps()complex results - Missing
if __name__ == "__main__"guard — without it, importing the module for testing also starts the server, blocking the test runner - Credential leakage in error messages — actionable errors are good, but never include env var values or API keys in error strings; log them server-side only
- TypeScript: forgetting to call
server.connect(transport)— the server object is initialized but never starts listening;tools/callhandler is registered but never reachable
Verification
# Python: syntax check then startup test
python -m py_compile server.py && echo "Syntax OK"
timeout 5s python server.py && echo "Startup OK" || echo "Check exit code"
# Confirm a tool is callable end-to-end
mcporter config add my-server python server.py
mcporter call --server my-server --tool <tool_name> '{"key": "value"}'
# TypeScript: compile check
npx tsc --noEmit && echo "TS OK"
- Confirm the returned JSON matches the expected schema before declaring the server ready