Skip to content

MCP Servers Cheat Sheet

I want to…Use
Create a server (TS)new McpServer({ name, version })
Create a server (Python)FastMCP("name")
Expose a callable functionTool (tools/list, tools/call)
Expose read-only dataResource (resources/read)
Expose an interaction templatePrompt (prompts/get)
Connect locallystdio transport
Connect remotelyStreamable HTTP transport
Test interactivelynpx @modelcontextprotocol/inspector
Configure in Claude Code.mcp.json at project or ~/.claude/.mcp.json

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)
  1. Client sends initialize with its capabilities and protocol version
  2. Server responds with its capabilities (tools, resources, prompts)
  3. Client sends notifications/initialized
  4. Normal operation: list/call/read exchanges
  5. Client closes connection or terminates subprocess
{
"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.

Three building blocks define what a server exposes.

PrimitivePurposeControl ModelDiscoveryExecution
ToolExecutable functionModel-driventools/listtools/call
ResourceRead-only dataApp-drivenresources/listresources/read
PromptReusable interaction templateUser-drivenprompts/listprompts/get

Tools let the LLM perform actions — query a database, call an API, modify a file.

{
"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"]
}
}

Results return a content array supporting multiple types.

Content TypeFieldUse
texttextPlain text responses
imagedataBase64-encoded image
audiodataBase64-encoded audio
resource_linkuriLink to a server resource
resourceresourceEmbedded 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 expose data the application reads for context — file contents, database schemas, API responses.

{
"uri": "db://schema/users",
"name": "users-table-schema",
"description": "Column definitions for the users table",
"mimeType": "application/json"
}

Parameterized URIs using RFC 6570 templates.

{
"uriTemplate": "db://tables/{table}/schema",
"name": "table-schema",
"description": "Schema for any database table",
"mimeType": "application/json"
}
SchemeUse
file://Filesystem-like resources
https://Web resources clients can fetch directly
git://Version control integration
CustomDomain-specific (db://, slack://, etc.)

Prompts define parameterized templates that generate structured messages.

{
"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
}
]
}

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"
}
}
]
}
Terminal window
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript
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 automatically
server.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.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.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}`,
},
},
],
}),
);
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.

Terminal window
uv add "mcp[cli]"
# or
pip install "mcp[cli]"
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.

@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)
@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}"

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 results
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)
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.

The client spawns the server as a subprocess. Messages flow over stdin/stdout as newline-delimited JSON-RPC.

AttributeDetail
LaunchClient spawns server process
LatencyMinimal (no network)
AuthInherits OS-level process permissions
ScalingOne client per server process
Best forLocal tools, CLI integrations, dev/test

The server runs as an HTTP service. Client sends POST requests; server may respond with JSON or open an SSE stream.

AttributeDetail
LaunchServer runs independently
LatencyNetwork round-trip
AuthBearer tokens, API keys, OAuth
ScalingMultiple clients per server
Best forRemote APIs, shared services, production deploy
# Python: Streamable HTTP
mcp.run(transport="streamable-http")
# Python: Mount on existing ASGI app
from starlette.applications import Starlette
from starlette.routing import Mount
app = Starlette(routes=[Mount("/mcp", app=mcp.streamable_http_app())])
ScenarioTransport
Local filesystem or database accessstdio
Running inside Docker on same hoststdio
Shared team service behind authStreamable HTTP
Public API integrationStreamable HTTP
Development and testingstdio

Interactive browser-based tool for exercising all server capabilities.

Terminal window
# Test a local TypeScript server
npx @modelcontextprotocol/inspector node build/index.js
# Test a local Python server
npx @modelcontextprotocol/inspector uv --directory ./myserver run server.py
# Test an npm package
npx @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem /tmp
# Test a PyPI package
npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/repo

The Inspector provides tabs for tools, resources, prompts, and a notification pane for logs.

Test tool logic independently of the transport layer.

import pytest
from server import search_issues
@pytest.mark.asyncio
async 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"));
});
});
  1. Unit test each handler function in isolation
  2. Use the Inspector to verify protocol compliance
  3. Integration test with a real client (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": "..." }
}
}
}
FileScopeGitPurpose
.mcp.jsonProjectYesTeam-shared servers
~/.claude/.mcp.jsonPersonalNoPersonal tool servers

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}" }
}
}
}
LevelMechanismWhen
Protocol errorJSON-RPC error responseUnknown tool, invalid params, server bug
Tool execution errorisError: true in resultAPI failure, bad input, business logic
CodeMeaning
-32600Invalid request
-32601Method not found
-32602Invalid params
-32603Internal error
-32002Resource not found
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,
};
}
},
);
  • 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
  • Declare only the primitives your server needs
  • Scope tools narrowly — one action per tool, not a god-tool that does everything
  • Use annotations.audience to control which content reaches the user vs. the model
  • Pass secrets via environment variables, never in source code
  • Use env in .mcp.json for injection at launch time
  • For Streamable HTTP, use OAuth or bearer tokens — never embed keys in URLs
  • 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
PatternTransportUse Case
Local processstdioDev tools, personal utilities
Docker containerstdioIsolated local tools, reproducible envs
HTTP serviceStreamable HTTPShared team servers, cloud deployment
Sidecar containerstdioK8s pods, co-located with app
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY build/ ./build/
USER node
ENTRYPOINT ["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.

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER nobody
EXPOSE 8000
CMD ["python", "server.py"]
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-patternFix
God-tool with 15+ parametersSplit into focused tools with 2-5 params each
Missing or vague descriptionsWrite descriptions the LLM uses to decide when to call
No error messages in tool resultsReturn isError: true with a human-readable explanation
console.log() in stdio serversUse console.error() — stdout is the JSON-RPC channel
Secrets hard-coded in sourceInject via env in .mcp.json or environment variables
Returning raw stack tracesCatch errors, return sanitized messages
No input validationUse Zod (TS) or Pydantic (Python) on every tool
Exposing all data as toolsUse resources for read-only data, tools for actions
Blocking sync calls in PythonUse async def handlers with httpx or aiohttp
No pagination on large listsImplement cursor-based pagination for */list methods
  • Claude Code Extensibility — Using MCP servers from the client side, configuration, and .mcp.json setup
  • AI CLI Patterns — Claude Code CLI workflows and prompting patterns
  • TypeScript — TypeScript language reference for SDK development
  • Docker — Containerizing MCP servers for isolated deployment