MCP Servers Cheat Sheet
Quick Reference
Section titled “Quick Reference”| I want to… | Use |
|---|---|
| Create a server (TS) | new McpServer({ name, version }) |
| Create a server (Python) | FastMCP("name") |
| Expose a callable function | Tool (tools/list, tools/call) |
| Expose read-only data | Resource (resources/read) |
| Expose an interaction template | Prompt (prompts/get) |
| Connect locally | stdio transport |
| Connect remotely | Streamable HTTP transport |
| Test interactively | npx @modelcontextprotocol/inspector |
| Configure in Claude Code | .mcp.json at project or ~/.claude/.mcp.json |
Protocol Overview
Section titled “Protocol Overview”MCP follows a client-server architecture over JSON-RPC 2.0. A host (Claude Code, Claude Desktop, VS Code) creates one client per server. Each client maintains a dedicated connection to its server.
Host (AI Application)├── Client 1 ──── Server A (local, stdio)├── Client 2 ──── Server B (local, stdio)└── Client 3 ──── Server C (remote, HTTP)Lifecycle
Section titled “Lifecycle”- Client sends
initializewith its capabilities and protocol version - Server responds with its capabilities (tools, resources, prompts)
- Client sends
notifications/initialized - Normal operation: list/call/read exchanges
- Client closes connection or terminates subprocess
Capability Negotiation
Section titled “Capability Negotiation”{ "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": true, "listChanged": true }, "prompts": { "listChanged": true } }}Declare only the primitives your server supports. listChanged enables dynamic
notifications when available items change.
Server Primitives
Section titled “Server Primitives”Three building blocks define what a server exposes.
| Primitive | Purpose | Control Model | Discovery | Execution |
|---|---|---|---|---|
| Tool | Executable function | Model-driven | tools/list | tools/call |
| Resource | Read-only data | App-driven | resources/list | resources/read |
| Prompt | Reusable interaction template | User-driven | prompts/list | prompts/get |
Tools let the LLM perform actions — query a database, call an API, modify a file.
Tool Definition (JSON Schema)
Section titled “Tool Definition (JSON Schema)”{ "name": "search_issues", "description": "Search project issues by keyword and status", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "Search keyword" }, "status": { "type": "string", "enum": ["open", "closed", "all"], "description": "Filter by issue status" } }, "required": ["query"] }}Tool Result
Section titled “Tool Result”Results return a content array supporting multiple types.
| Content Type | Field | Use |
|---|---|---|
text | text | Plain text responses |
image | data | Base64-encoded image |
audio | data | Base64-encoded audio |
resource_link | uri | Link to a server resource |
resource | resource | Embedded resource content |
Set isError: true in the result to signal a tool execution failure without
raising a protocol-level error.
{ "content": [ { "type": "text", "text": "Rate limit exceeded. Retry after 60s." } ], "isError": true}Resources
Section titled “Resources”Resources expose data the application reads for context — file contents, database schemas, API responses.
Resource Definition
Section titled “Resource Definition”{ "uri": "db://schema/users", "name": "users-table-schema", "description": "Column definitions for the users table", "mimeType": "application/json"}Resource Templates
Section titled “Resource Templates”Parameterized URIs using RFC 6570 templates.
{ "uriTemplate": "db://tables/{table}/schema", "name": "table-schema", "description": "Schema for any database table", "mimeType": "application/json"}Common URI Schemes
Section titled “Common URI Schemes”| Scheme | Use |
|---|---|
file:// | Filesystem-like resources |
https:// | Web resources clients can fetch directly |
git:// | Version control integration |
| Custom | Domain-specific (db://, slack://, etc.) |
Prompts
Section titled “Prompts”Prompts define parameterized templates that generate structured messages.
Prompt Definition
Section titled “Prompt Definition”{ "name": "code_review", "description": "Review code for quality and suggest improvements", "arguments": [ { "name": "code", "description": "The code to review", "required": true }, { "name": "language", "description": "Programming language", "required": false } ]}Prompt Result
Section titled “Prompt Result”prompts/get returns an array of messages with roles.
{ "messages": [ { "role": "user", "content": { "type": "text", "text": "Review this Python code for clarity and correctness:\n\ndef add(a, b):\n return a + b" } } ]}Building a Server in TypeScript
Section titled “Building a Server in TypeScript”npm install @modelcontextprotocol/sdk zodnpm install -D @types/node typescriptServer with Tools
Section titled “Server with Tools”import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { z } from "zod";
const server = new McpServer({ name: "my-server", version: "1.0.0",});
// Register a tool — Zod schemas define inputSchema automaticallyserver.registerTool( "search_issues", { description: "Search project issues by keyword", inputSchema: { query: z.string().describe("Search keyword"), status: z .enum(["open", "closed", "all"]) .default("open") .describe("Filter by status"), }, }, async ({ query, status }) => { const results = await searchIssues(query, status); return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; },);Server with Resources
Section titled “Server with Resources”server.registerResource( "schema", "db://schema/users", { description: "Users table schema", mimeType: "application/json" }, async () => ({ contents: [ { uri: "db://schema/users", mimeType: "application/json", text: JSON.stringify(getUsersSchema()), }, ], }),);Server with Prompts
Section titled “Server with Prompts”server.registerPrompt( "code_review", { description: "Review code for quality issues", arguments: [ { name: "code", description: "Code to review", required: true }, ], }, async ({ code }) => ({ messages: [ { role: "user", content: { type: "text", text: `Review this code for clarity, correctness, and style:\n\n${code}`, }, }, ], }),);Start with stdio Transport
Section titled “Start with stdio Transport”async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP server running on stdio");}
main().catch((error) => { console.error("Fatal error:", error); process.exit(1);});Logging rule for stdio: Never use console.log() — it writes to stdout and
corrupts JSON-RPC messages. Use console.error() for all diagnostic output.
Building a Server in Python
Section titled “Building a Server in Python”uv add "mcp[cli]"# orpip install "mcp[cli]"Server with Tools
Section titled “Server with Tools”from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool()async def search_issues(query: str, status: str = "open") -> str: """Search project issues by keyword.
Args: query: Search keyword status: Filter by status (open, closed, all) """ results = await do_search(query, status) return json.dumps(results, indent=2)FastMCP reads type hints and docstrings to generate inputSchema automatically.
No manual JSON Schema needed.
Server with Resources
Section titled “Server with Resources”@mcp.resource("db://schema/{table}")def get_table_schema(table: str) -> str: """Return the schema for a database table.""" schema = load_schema(table) return json.dumps(schema)Server with Prompts
Section titled “Server with Prompts”@mcp.prompt()def code_review(code: str, language: str = "python") -> str: """Review code for quality issues.""" return f"Review this {language} code for clarity and correctness:\n\n{code}"Context Object
Section titled “Context Object”Inject Context for logging, progress, and sampling.
from mcp.server.fastmcp import Context
@mcp.tool()async def long_analysis(repo: str, ctx: Context) -> str: """Analyze a repository for security issues.""" await ctx.info(f"Starting analysis of {repo}") await ctx.report_progress(progress=0, total=100)
results = await analyze(repo)
await ctx.report_progress(progress=100, total=100) return resultsStructured Output with Pydantic
Section titled “Structured Output with Pydantic”from pydantic import BaseModel
class AnalysisResult(BaseModel): score: float issues: list[str] passed: bool
@mcp.tool()def analyze_code(code: str) -> AnalysisResult: """Run static analysis on code.""" return AnalysisResult(score=8.5, issues=["unused import"], passed=True)Start with stdio Transport
Section titled “Start with stdio Transport”if __name__ == "__main__": mcp.run(transport="stdio")Logging rule for stdio: Never use print() — it writes to stdout. Use
print(..., file=sys.stderr) or the logging module.
Transport Layers
Section titled “Transport Layers”The client spawns the server as a subprocess. Messages flow over stdin/stdout as newline-delimited JSON-RPC.
| Attribute | Detail |
|---|---|
| Launch | Client spawns server process |
| Latency | Minimal (no network) |
| Auth | Inherits OS-level process permissions |
| Scaling | One client per server process |
| Best for | Local tools, CLI integrations, dev/test |
Streamable HTTP
Section titled “Streamable HTTP”The server runs as an HTTP service. Client sends POST requests; server may respond with JSON or open an SSE stream.
| Attribute | Detail |
|---|---|
| Launch | Server runs independently |
| Latency | Network round-trip |
| Auth | Bearer tokens, API keys, OAuth |
| Scaling | Multiple clients per server |
| Best for | Remote APIs, shared services, production deploy |
# Python: Streamable HTTPmcp.run(transport="streamable-http")# Python: Mount on existing ASGI appfrom starlette.applications import Starlettefrom starlette.routing import Mount
app = Starlette(routes=[Mount("/mcp", app=mcp.streamable_http_app())])Transport Decision Guide
Section titled “Transport Decision Guide”| Scenario | Transport |
|---|---|
| Local filesystem or database access | stdio |
| Running inside Docker on same host | stdio |
| Shared team service behind auth | Streamable HTTP |
| Public API integration | Streamable HTTP |
| Development and testing | stdio |
Testing
Section titled “Testing”MCP Inspector
Section titled “MCP Inspector”Interactive browser-based tool for exercising all server capabilities.
# Test a local TypeScript servernpx @modelcontextprotocol/inspector node build/index.js
# Test a local Python servernpx @modelcontextprotocol/inspector uv --directory ./myserver run server.py
# Test an npm packagenpx @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem /tmp
# Test a PyPI packagenpx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/repoThe Inspector provides tabs for tools, resources, prompts, and a notification pane for logs.
Unit Testing Handlers
Section titled “Unit Testing Handlers”Test tool logic independently of the transport layer.
import pytest
from server import search_issues
@pytest.mark.asyncioasync def test_search_issues(): result = await search_issues("authentication", status="open") parsed = json.loads(result) assert len(parsed) > 0 assert all(issue["status"] == "open" for issue in parsed)import { describe, it, expect } from "vitest";import { searchIssues } from "./handlers.js";
describe("search_issues", () => { it("filters by status", async () => { const result = await searchIssues("auth", "open"); expect(result.length).toBeGreaterThan(0); result.forEach((issue) => expect(issue.status).toBe("open")); });});Testing Workflow
Section titled “Testing Workflow”- Unit test each handler function in isolation
- Use the Inspector to verify protocol compliance
- Integration test with a real client (Claude Code, Claude Desktop)
Client Configuration
Section titled “Client Configuration”Claude Code / Claude Desktop
Section titled “Claude Code / Claude Desktop”{ "mcpServers": { "my-server": { "type": "stdio", "command": "node", "args": ["./build/index.js"] }, "remote-api": { "type": "sse", "url": "https://mcp.example.com/sse", "env": { "API_KEY": "..." } } }}| File | Scope | Git | Purpose |
|---|---|---|---|
.mcp.json | Project | Yes | Team-shared servers |
~/.claude/.mcp.json | Personal | No | Personal tool servers |
Environment Variables
Section titled “Environment Variables”Pass secrets through env — never hard-code them in server source.
{ "mcpServers": { "github": { "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } } }}Error Handling
Section titled “Error Handling”Two Error Levels
Section titled “Two Error Levels”| Level | Mechanism | When |
|---|---|---|
| Protocol error | JSON-RPC error response | Unknown tool, invalid params, server bug |
| Tool execution error | isError: true in result | API failure, bad input, business logic |
Standard JSON-RPC Error Codes
Section titled “Standard JSON-RPC Error Codes”| Code | Meaning |
|---|---|
-32600 | Invalid request |
-32601 | Method not found |
-32602 | Invalid params |
-32603 | Internal error |
-32002 | Resource not found |
Validation Pattern (TypeScript)
Section titled “Validation Pattern (TypeScript)”server.registerTool( "create_issue", { description: "Create a new issue", inputSchema: { title: z.string().min(1).max(200).describe("Issue title"), priority: z.enum(["low", "medium", "high"]).describe("Priority level"), labels: z.array(z.string()).max(10).optional().describe("Labels"), }, }, async ({ title, priority, labels }) => { try { const issue = await createIssue({ title, priority, labels }); return { content: [{ type: "text", text: `Created issue #${issue.id}` }], }; } catch (err) { return { content: [{ type: "text", text: `Failed: ${err.message}` }], isError: true, }; } },);Security
Section titled “Security”Input Validation
Section titled “Input Validation”- Validate every tool input — use Zod (TS) or Pydantic (Python) for schema enforcement
- Sanitize string inputs before passing to shell commands, SQL, or file paths
- Reject unexpected fields; never pass raw input to
eval()or template engines
Capability Scoping
Section titled “Capability Scoping”- Declare only the primitives your server needs
- Scope tools narrowly — one action per tool, not a god-tool that does everything
- Use
annotations.audienceto control which content reaches the user vs. the model
Secrets Management
Section titled “Secrets Management”- Pass secrets via environment variables, never in source code
- Use
envin.mcp.jsonfor injection at launch time - For Streamable HTTP, use OAuth or bearer tokens — never embed keys in URLs
Stdio Isolation
Section titled “Stdio Isolation”- Stdio servers inherit the host process’s permissions — scope filesystem access
- Validate all file paths against an allow-list
- Run containerized servers for untrusted workloads
Deployment Patterns
Section titled “Deployment Patterns”| Pattern | Transport | Use Case |
|---|---|---|
| Local process | stdio | Dev tools, personal utilities |
| Docker container | stdio | Isolated local tools, reproducible envs |
| HTTP service | Streamable HTTP | Shared team servers, cloud deployment |
| Sidecar container | stdio | K8s pods, co-located with app |
Docker with stdio
Section titled “Docker with stdio”FROM node:22-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --productionCOPY build/ ./build/USER nodeENTRYPOINT ["node", "build/index.js"]{ "mcpServers": { "my-server": { "type": "stdio", "command": "docker", "args": ["run", "-i", "--rm", "my-mcp-server:latest"] } }}The -i flag keeps stdin open for JSON-RPC communication. Skip -t — TTY mode
corrupts the binary stream.
Docker with Streamable HTTP
Section titled “Docker with Streamable HTTP”FROM python:3.12-slimWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY . .USER nobodyEXPOSE 8000CMD ["python", "server.py"]mcp = FastMCP("my-server")# ... register tools, resources, prompts ...
if __name__ == "__main__": mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)Anti-patterns
Section titled “Anti-patterns”| Anti-pattern | Fix |
|---|---|
| God-tool with 15+ parameters | Split into focused tools with 2-5 params each |
| Missing or vague descriptions | Write descriptions the LLM uses to decide when to call |
| No error messages in tool results | Return isError: true with a human-readable explanation |
console.log() in stdio servers | Use console.error() — stdout is the JSON-RPC channel |
| Secrets hard-coded in source | Inject via env in .mcp.json or environment variables |
| Returning raw stack traces | Catch errors, return sanitized messages |
| No input validation | Use Zod (TS) or Pydantic (Python) on every tool |
| Exposing all data as tools | Use resources for read-only data, tools for actions |
| Blocking sync calls in Python | Use async def handlers with httpx or aiohttp |
| No pagination on large lists | Implement cursor-based pagination for */list methods |
See Also
Section titled “See Also”- Claude Code Extensibility — Using MCP servers from the
client side, configuration, and
.mcp.jsonsetup - AI CLI Patterns — Claude Code CLI workflows and prompting patterns
- TypeScript — TypeScript language reference for SDK development
- Docker — Containerizing MCP servers for isolated deployment