cheat sheet

MCP Frameworks

Model Context Protocol (MCP) framework overview. Covers client/server architecture, stdio vs SSE vs streamable HTTP transports, FastMCP, mcp-go, the Python and TypeScript SDKs, and comparison with custom tool servers.

MCP Frameworks — Servers, Clients, and Transports

What it is

The Model Context Protocol (MCP) is an open JSON-RPC protocol introduced by Anthropic in late 2024 for exposing tools, resources, and prompts to LLM clients. Instead of every LLM application reimplementing tool integrations, an MCP server publishes its capabilities once and any MCP client (Claude Code, Claude Desktop, Codex CLI, Cursor, ChatGPT desktop, custom agents) can connect, list, and invoke them. The protocol is transport-agnostic — it runs over stdio, Server-Sent Events (SSE), and the newer Streamable HTTP transport.

This page covers the frameworks ecosystem around MCP: the official Python and TypeScript SDKs (with their FastMCP ergonomic wrappers), the Go server library mcp-go, Rust and Java SDKs, and where MCP fits relative to home-grown tool servers like OpenAI function-calling endpoints or custom REST/gRPC APIs.

For client-side configuration of MCP servers inside specific tools, see the claude-code MCP page and codex MCP page.

Install

The official Python SDK:

bash
pip install "mcp[cli]"

Output:

text
Successfully installed mcp-1.x.x ...

TypeScript SDK:

bash
npm install @modelcontextprotocol/sdk

Output:

text
added 12 packages in 3s

Go SDK (mark3labs/mcp-go):

bash
go get github.com/mark3labs/mcp-go

Output:

text
go: added github.com/mark3labs/mcp-go v0.x.x

Quick example — a FastMCP server in Python

FastMCP is the high-level decorator-driven API in the official Python SDK. Three decorators cover most servers: @mcp.tool(), @mcp.resource(...), and @mcp.prompt().

python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("demo-server")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two integers and return the result."""
    return a + b

@mcp.tool()
def greet(name: str) -> str:
    """Return a friendly greeting for the given name."""
    return f"Hello, {name}!"

@mcp.resource("config://app")
def app_config() -> str:
    """Static application configuration."""
    return '{"version": "1.0", "feature_flags": {"beta": true}}'

@mcp.prompt()
def code_review(language: str, code: str) -> str:
    """Prompt template for reviewing code in the given language."""
    return f"Review the following {language} code for bugs and style:\n\n{code}"

if __name__ == "__main__":
    mcp.run()

Run it:

bash
python server.py

Output:

text
[INFO] FastMCP server "demo-server" running on stdio

By default mcp.run() listens on stdio — the transport every MCP client supports out of the box. Configure Claude Desktop, Claude Code, or Codex to launch this process and they will discover add, greet, config://app, and code_review automatically.

When / why to use it

  • You have a tool, dataset, or workflow that should be available to multiple LLM clients — Claude Desktop, Claude Code, Cursor, and custom agents — without reimplementing per-client.
  • You want a clean transport boundary: the LLM runs in one process, your tools in another, isolated by a JSON-RPC contract.
  • You need to expose resources (read-only content the LLM can reference, like file system roots, database tables, or knowledge bases) in addition to tools.
  • You want prompt templates that clients can offer to users as slash commands or quick actions.
  • You are integrating with a vendor-specific MCP server (GitHub, Notion, Linear, Slack, Sentry) and need to understand the protocol so you can debug.

Common pitfalls

stdio servers must not print to stdout — stdio is the JSON-RPC transport. Any stray print() corrupts the protocol stream. Log to stderr or to a file. FastMCP does this correctly by default; custom raw servers must be careful.

SSE transport is being deprecated — the original SSE transport is replaced by Streamable HTTP (single endpoint, bi-directional, supports resumability). New servers should publish Streamable HTTP; SSE remains supported for older clients.

Type hints are the schemaFastMCP reads Python type hints to generate the JSON schema clients send to the LLM. def f(x) without a hint exposes x as untyped — the LLM may pass anything. Always annotate.

Tool side effects run on every call — there is no idempotency layer. Use a tool_call_id argument or external dedup if the underlying action is non-idempotent (sending email, charging cards).

Use mcp dev path/to/server.py to launch the MCP Inspector — a web UI that lets you call tools, list resources, and inspect prompts without an LLM client. Indispensable while developing servers.

Wrap tools that hit external APIs with timeouts. MCP clients usually impose their own (Claude Code: 60s by default), but tools that hang block the entire client agent loop.

Architecture — client, server, transport

text
+----------------+       JSON-RPC 2.0        +-----------------+
|   MCP client   | <-----------------------> |   MCP server    |
| (Claude Code,  |   stdio / SSE / HTTP      | (FastMCP, mcp-  |
|  Cursor, etc.) |                           |  go, custom)    |
+----------------+                           +-----------------+
       |                                              |
       | initialize → list_tools → call_tool ...      |
       v                                              v
+----------------+                           +-----------------+
|   Host LLM     |                           |  Your business  |
|   (Claude,     |                           |  logic / APIs   |
|   GPT, etc.)   |                           |                 |
+----------------+                           +-----------------+

The client is whichever LLM-driving application the user is interacting with. The server exposes capabilities. The transport carries JSON-RPC frames between them. Servers never call LLMs themselves — they only return data the client passes to the LLM.

Lifecycle methods

MethodDirectionPurpose
initializeclient → serverNegotiate protocol version and capabilities.
tools/listclient → serverEnumerate available tools.
tools/callclient → serverExecute a tool by name with arguments.
resources/listclient → serverEnumerate resources.
resources/readclient → serverFetch resource contents.
prompts/listclient → serverEnumerate prompt templates.
prompts/getclient → serverRender a prompt template.
notifications/*bidirectionalProgress, log, resource-changed events.

Transports — stdio, SSE, Streamable HTTP

stdio

Default for local servers. The client launches the server as a subprocess and exchanges JSON-RPC frames over stdin/stdout. Simplest to deploy, no networking, no auth required.

python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("local")
if __name__ == "__main__":
    mcp.run(transport="stdio")

Output:

text
[INFO] FastMCP server "local" running on stdio

Client config (Claude Desktop / Claude Code):

json
{
  "mcpServers": {
    "local": {
      "command": "python",
      "args": ["/abs/path/to/server.py"]
    }
  }
}

Server-Sent Events (SSE) — legacy

Two HTTP endpoints: a long-lived GET /sse event stream and a POST /messages write endpoint. Works through HTTPS and reverse proxies; supported by all clients today but deprecated in favour of Streamable HTTP.

python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("remote-sse")
if __name__ == "__main__":
    mcp.run(transport="sse", host="0.0.0.0", port=8000)

Output:

text
[INFO] FastMCP server "remote-sse" listening at http://0.0.0.0:8000
[INFO] SSE endpoint:    http://0.0.0.0:8000/sse
[INFO] Message endpoint: http://0.0.0.0:8000/messages

Streamable HTTP — current standard

A single POST /mcp endpoint that supports both request/response and server-pushed events using chunked transfer or SSE in the response body. Adds session IDs, optional resumability via Last-Event-ID, and a clean auth story.

python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("remote-http")
if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)

Output:

text
[INFO] FastMCP server "remote-http" listening at http://0.0.0.0:8000/mcp
TransportLocalRemoteAuthResumableStatus
stdioyesnon/a (subprocess)norecommended for local
SSEnoyesbearer / cookienodeprecated
Streamable HTTPnoyesbearer / OAuthyes (via session ID)recommended for remote

FastMCP — Python decorators

Tools

python
from typing import Annotated
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("github")

@mcp.tool()
def open_issue(
    repo: Annotated[str, "owner/name"],
    title: Annotated[str, "Issue title"],
    body:  Annotated[str, "Issue body in Markdown"] = "",
) -> str:
    """Open a new GitHub issue and return its URL."""
    ...
    return f"https://github.com/{repo}/issues/123"

Type hints become the JSON schema. Annotated[T, "description"] adds the parameter description the LLM sees. Default values mark a parameter as optional.

Resources — static and templated

python
@mcp.resource("docs://intro")
def docs_intro() -> str:
    """The intro page of the docs."""
    return "Welcome to the docs..."

@mcp.resource("file:///{path}")
def file_read(path: str) -> str:
    """Read an arbitrary file by absolute path."""
    with open(path) as f:
        return f.read()

Resources are read-only by convention — the LLM may reference them by URI but does not invoke side effects. Templated URIs let one definition cover many resources.

Prompts

python
@mcp.prompt()
def summarise(language: str) -> str:
    """Summarise a document in the given language."""
    return f"Summarise the attached document in {language}. Keep it under 200 words."

Clients surface prompts as slash commands (/summarise) or quick-action buttons.

Async tools

python
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather")

@mcp.tool()
async def get_weather(city: str) -> str:
    """Return current weather for a city."""
    async with httpx.AsyncClient(timeout=10) as client:
        r = await client.get(f"https://wttr.in/{city}?format=3")
        return r.text

FastMCP runs async tools on its event loop and pushes blocking sync tools to a thread pool.

Context — progress and logs

python
from mcp.server.fastmcp import Context, FastMCP

mcp = FastMCP("indexer")

@mcp.tool()
async def reindex(directory: str, ctx: Context) -> str:
    """Re-index all files in a directory, reporting progress."""
    files = list_files(directory)
    for i, f in enumerate(files):
        await ctx.info(f"indexing {f}")
        await ctx.report_progress(progress=i + 1, total=len(files))
        process(f)
    return f"Indexed {len(files)} files."

ctx.info / warning / error ship log lines to the client; ctx.report_progress updates the client's progress UI.

TypeScript SDK

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: "ts-demo", version: "1.0.0" });

server.registerTool(
  "multiply",
  {
    title: "Multiply",
    description: "Multiply two numbers.",
    inputSchema: { a: z.number(), b: z.number() },
  },
  async ({ a, b }) => ({
    content: [{ type: "text", text: String(a * b) }],
  }),
);

server.registerResource(
  "version",
  "app://version",
  { mimeType: "text/plain" },
  async () => ({ contents: [{ uri: "app://version", text: "1.0.0" }] }),
);

const transport = new StdioServerTransport();
await server.connect(transport);

zod schemas drive the JSON schema the client sends to the LLM. For Streamable HTTP, swap in StreamableHTTPServerTransport.

Go SDK — mcp-go

go
package main

import (
    "context"
    "fmt"

    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    s := server.NewMCPServer("go-demo", "1.0.0",
        server.WithToolCapabilities(true),
    )

    s.AddTool(mcp.NewTool("multiply",
        mcp.WithDescription("Multiply two integers."),
        mcp.WithNumber("a", mcp.Required(), mcp.Description("first operand")),
        mcp.WithNumber("b", mcp.Required(), mcp.Description("second operand")),
    ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        a := req.GetFloat("a", 0)
        b := req.GetFloat("b", 0)
        return mcp.NewToolResultText(fmt.Sprintf("%g", a*b)), nil
    })

    server.ServeStdio(s)
}

Build and run:

bash
go build -o go-mcp .
./go-mcp

Output:

text
(silent — stdio server waiting for client frames)

mcp-go is particularly attractive for embedding a server inside an existing Go service (Kubernetes operator, gRPC backend) without spinning up a Python process.

Building an MCP client

Most engineers will consume MCP servers through existing tools (Claude Code, Cursor, etc.), but building a client is useful for custom agents.

python
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    params = StdioServerParameters(command="python", args=["server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print([t.name for t in tools.tools])
            result = await session.call_tool("add", {"a": 2, "b": 3})
            print(result.content[0].text)

asyncio.run(main())

Output:

text
['add', 'greet']
5

For Streamable HTTP servers, swap stdio_client for streamablehttp_client(url="https://...") from mcp.client.streamable_http.

Auth for remote servers

Streamable HTTP servers accept bearer tokens via the Authorization header. The 2025 specification also defines an OAuth 2.1 flow for end-user delegation.

python
from mcp.server.fastmcp import FastMCP
from starlette.requests import Request

mcp = FastMCP("authed")

@mcp.tool()
async def whoami(ctx) -> str:
    """Return the authenticated user from the request token."""
    request: Request = ctx.request
    token = request.headers.get("authorization", "").removeprefix("Bearer ")
    return verify_jwt(token).sub

Client side, pass headers when constructing the transport:

python
from mcp.client.streamable_http import streamablehttp_client

async with streamablehttp_client(
    url="https://example.com/mcp",
    headers={"Authorization": f"Bearer {token}"},
) as (read, write, _):
    ...

stdio servers run as the client user with the client's environment — there is no auth boundary. Anything sensitive in env vars (API keys) is visible to the server process.

When to use MCP vs a custom tool server

DecisionChoose MCPChoose custom
Multiple LLM clients will use the toolsyesno — one client
Want tools surfaced as a marketplace listingyesno
Need fine-grained per-client UI affordances (rich cards, file pickers)partly — capability evolvingyes, full control
Tool latency budget is sub-millisecondno — JSON-RPC overhead mattersyes
Tools are stateless HTTP APIs that LLMs hit via OpenAI tool callingoptionalacceptable
Want OAuth-delegated remote toolsyes (Streamable HTTP)bespoke
Need server-pushed notifications / progressyesbespoke

Rule of thumb: if you envision more than one LLM client consuming the tool, write an MCP server. If the tool is internal to a single agent and latency matters, embed it directly as a function in your tool-calling code.

Real-world recipes

Recipe — MCP server in front of a Postgres database

python
import os
import psycopg
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("postgres")
DSN = os.environ["DATABASE_URL"]

@mcp.tool()
def list_tables() -> list[str]:
    """List public tables in the connected Postgres database."""
    with psycopg.connect(DSN) as cn, cn.cursor() as cur:
        cur.execute("select table_name from information_schema.tables where table_schema = 'public'")
        return [r[0] for r in cur.fetchall()]

@mcp.tool()
def query(sql: str) -> list[dict]:
    """Run a read-only SQL query and return rows as dicts."""
    if not sql.lstrip().lower().startswith("select"):
        raise ValueError("Only SELECT queries are allowed.")
    with psycopg.connect(DSN) as cn, cn.cursor(row_factory=psycopg.rows.dict_row) as cur:
        cur.execute(sql)
        return cur.fetchmany(100)

if __name__ == "__main__":
    mcp.run()

Add a SQL allow-list and row caps; LLMs that read this server can now answer questions about the database.

Recipe — wrap an existing REST API

python
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("jira")
BASE = os.environ["JIRA_BASE_URL"]
TOKEN = os.environ["JIRA_TOKEN"]

client = httpx.AsyncClient(base_url=BASE, headers={"Authorization": f"Bearer {TOKEN}"})

@mcp.tool()
async def search_issues(jql: str) -> list[dict]:
    """Search Jira issues using JQL."""
    r = await client.get("/rest/api/3/search", params={"jql": jql, "maxResults": 20})
    return r.json().get("issues", [])

if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8001)

Deploy behind your auth layer; point any MCP client at the URL.

Recipe — combine multiple MCP servers in one client

A client agent connects to several servers and exposes their union to the LLM.

python
from mcp.client.stdio import stdio_client
from mcp import ClientSession, StdioServerParameters
import asyncio

async def gather_tools():
    servers = {
        "fs":     StdioServerParameters(command="python", args=["./fs_server.py"]),
        "github": StdioServerParameters(command="python", args=["./gh_server.py"]),
        "db":     StdioServerParameters(command="python", args=["./db_server.py"]),
    }
    sessions = {}
    all_tools = []
    for name, params in servers.items():
        read, write = await stdio_client(params).__aenter__()
        session = ClientSession(read, write)
        await session.__aenter__()
        await session.initialize()
        tools = (await session.list_tools()).tools
        for t in tools:
            t.name = f"{name}.{t.name}"
        all_tools.extend(tools)
        sessions[name] = session
    return sessions, all_tools

Namespacing tools by server name (fs.read, github.open_issue) avoids collisions and helps the LLM reason about provenance.

Recipe — typed errors with content blocks

python
from mcp.server.fastmcp import FastMCP
from mcp.types import TextContent, ToolError

mcp = FastMCP("billing")

@mcp.tool()
def charge(customer_id: str, amount_cents: int) -> list[TextContent]:
    """Charge a customer's saved payment method."""
    if amount_cents <= 0:
        raise ToolError("amount_cents must be positive")
    txn_id = stripe_charge(customer_id, amount_cents)
    return [TextContent(type="text", text=f"Charged. Transaction {txn_id}.")]

Throwing ToolError returns an isError: true result the LLM can recover from; uncaught exceptions become protocol errors.

Recipe — local dev with MCP Inspector

bash
pip install "mcp[cli]"
mcp dev server.py

Output:

text
Inspector running at http://localhost:5173
Server stdio attached.

Open the URL, list tools, invoke them with form inputs, and inspect raw JSON-RPC frames — the fastest dev loop for a new server.

Quick reference

TaskCode
Install Python SDKpip install "mcp[cli]"
Install TS SDKnpm install @modelcontextprotocol/sdk
Install Go SDKgo get github.com/mark3labs/mcp-go
Create server (Py)mcp = FastMCP("name")
Add tool@mcp.tool() on a typed function
Add resource@mcp.resource("scheme://path")
Add prompt@mcp.prompt() on a function returning the prompt text
Stdio runmcp.run() or mcp.run(transport="stdio")
SSE runmcp.run(transport="sse", host=..., port=...)
Streamable HTTP runmcp.run(transport="streamable-http", host=..., port=...)
Inspectormcp dev server.py
Progress / logsctx.report_progress, ctx.info, ctx.warning, ctx.error
Errorsraise ToolError("...")
Client (stdio)async with stdio_client(params) as (r, w): ClientSession(r, w)
Client (HTTP)async with streamablehttp_client(url=..., headers=...) as ...
List toolsawait session.list_tools()
Call toolawait session.call_tool(name, args)
List resourcesawait session.list_resources()
Read resourceawait session.read_resource(uri)
Render promptawait session.get_prompt(name, args)