A visual developer's guide to MCP.
Everything you need to build, debug, and reason about MCP — from the JSON-RPC envelope to the host-side orchestration loop.
Written by Blijnder — makers of Stedt, the platform powering strong regional communities.
tools · resources · prompts
connect → result
stdio · streamable HTTP
JSON-RPC 2.0
Dotted words like JSON-RPC open a definition popover when clicked. Orange-highlighted segments inside code blocks are annotated — click them to learn what that field does. Diagrams are clickable. Use the table of contents on the left to jump around.
One host, many servers, one model.
MCP defines how an AI app (the host) talks to external capabilities. The host runs locally on your machine; it spawns one MCP client per MCP server you've configured. Each server is its own process (or remote URL) exposing tools, resources, and prompts. The model — the thing actually generating text — runs server-side at Anthropic and never speaks to your MCP servers directly. The host is the broker.
Click any element below to see what it does.
Click any box in the diagram above to see what it is and what it does.
Three takeaways from this picture
One client per server. The host doesn't multiplex servers over a shared connection. If Stedt and Moneybird are both configured, the host spawns two clients, each holding its own pipe. This keeps server crashes isolated and lets the host implement per-server permissions, timeouts, and logging.
Servers are dumb processes, by design. A server exposes capabilities. It doesn't know which model is running, what other servers exist, or what the user asked. It receives a tools/call, runs the function, returns content. That's it. The intelligence sits in the host's orchestration loop and the model.
The model is remote. Even when every server is local, the actual inference call is an HTTPS request to api.anthropic.com. Latency-wise, a single tool call typically involves: one round trip to the API to get a tool_use, one local IPC to the server, and one more API round trip to feed the result back.
Every MCP session moves through five stages.
From the moment the host decides to use a server until the moment the user sees an answer, MCP follows a predictable script. Click any stage to expand it.
1
Connect
Spawn the server process or open the HTTP stream
›
For stdio servers, the host reads its config file, finds the command and args, and uses the OS to spawn that as a child process. It connects its own pipe to the child's stdin and stdout. Nothing has been sent yet — the wire is just open.
For HTTP servers, the host opens a POST connection to the server's URL and keeps it warm. The server may also open a long-lived SSE stream the other direction for server-initiated messages (sampling, notifications).
{
"mcpServers": {
"stedt": {
"command": "uvx",
"args": ["stedt-mcp"],
"env": { "STEDT_API_KEY": "sk-..." }
},
"moneybird-remote": {
"url": "https://mcp.moneybird.com/v1",
"headers": { "Authorization": "Bearer ..." }
}
}
}
2
Initialize
Negotiate protocol version and capabilities
›
The client sends an initialize request stating which spec version it speaks and what it supports (sampling, roots, etc.). The server replies with its own version and a list of which primitives it exposes. Both sides then know exactly what's on the table for this session.
After receiving the response, the client sends a one-way notification called notifications/initialized. No reply. From this point on, normal traffic can flow.
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {
"sampling": {},
"roots": { "listChanged": true }
},
"clientInfo": { "name": "claude-desktop", "version": "0.9.2" }
}
}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"prompts": { "listChanged": false },
"logging": {}
},
"serverInfo": { "name": "stedt", "version": "2.4.1" }
}
}
3
Discover
tools/list · resources/list · prompts/list
›
Now the host asks the server what's actually available. Three list methods: tools/list, resources/list, prompts/list. Each returns an array. Crucially, every tool ships with a JSON Schema describing its arguments — the host hands that schema (and the description) straight to the model as the tools[] parameter in the next API call.
{
"jsonrpc": "2.0", "id": 2,
"result": {
"tools": [
{
"name": "community_search",
"description": "Full-text search across Stedt regional communities. Returns community IDs and one-line summaries — use community_retrieve for full member rosters and event feeds.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Free-text query" },
"limit": { "type": "integer", "default": 10, "maximum": 50 },
"region": { "type": "string", "enum": ["north", "central", "south"] }
},
"required": ["query"]
}
}
],
"nextCursor": null
}
}
List responses are paginated via a cursor field. If nextCursor is non-null, the host calls tools/list again with params.cursor set to that value.
4
Invoke
tools/call with name and arguments
›
The model has produced a tool_use. The host validates the arguments against the schema it cached at discovery time, then sends a tools/call request over the open pipe. The id on this JSON-RPC envelope is independent of the model's tool_use.id — the host keeps a map between them.
{
"jsonrpc": "2.0",
"id": 17,
"method": "tools/call",
"params": {
"name": "community_search",
"arguments": { "query": "NHG", "limit": 5 }
}
}
If the server supports progress reporting and the request is expected to be slow, the client can include a _meta.progressToken in params; the server then emits notifications/progress events as it works.
5
Result
Content blocks flow back into the LLM context
›
The server replies with an array of content blocks. Each block has a type — text, image, audio, or resource — plus the appropriate payload. If something went wrong, isError is true and the text content explains what.
{
"jsonrpc": "2.0", "id": 17,
"result": {
"content": [
{ "type": "text", "text": "Found 3 communities matching NHG:\n1. ..." },
{ "type": "image", "data": "iVBORw0KGgo...", "mimeType": "image/png" }
],
"isError": false
}
}
The host wraps these blocks in a tool_result and ships them back to the API in the next request. The model now sees them as part of its conversation history.
Every message is a JSON-RPC 2.0 envelope.
If you've used JSON-RPC before, MCP holds no surprises. There are three message shapes: request (has an id, expects a response), response (carries the matching id plus a result or error), and notification (no id, no reply). Servers can send any of these to clients and vice versa.
The orange highlights below are clickable — they explain each field.
A request
{
"jsonrpc": "2.0",
"id": 17,
"method": "tools/call",
"params": {
"name": "community_search",
"arguments": { "query": "NHG", "limit": 5 }
}
}
A successful response
{
"jsonrpc": "2.0",
"id": 17,
"result": {
"content": [
{ "type": "text",
"text": "Found 3 communities matching NHG..." }
],
"isError": false
}
}
An error response
Errors use the JSON-RPC error object with a numeric code and human-readable message. The MCP spec adds a handful of conventions on top of the standard JSON-RPC error codes.
{
"jsonrpc": "2.0",
"id": 17,
"error": {
"code": -32602,
"message": "Invalid params: 'query' is required",
"data": { "field": "query" }
}
}
A notification
Notifications carry no id. The other side processes them and sends no reply. Used for events: tool list changed, resource list changed, progress updates, log messages.
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}
JSON-RPC doesn't define ordering guarantees across separate requests. If you fire tools/call #1 and tools/call #2 back-to-back, the server is free to return #2 first. Always pair responses to requests by id, never by arrival order.
Tools, resources, prompts.
Everything an MCP server can offer falls into one of three buckets. The differences come down to who controls them and whether they have side effects.
What they are
Tools are callable functions — possibly with side effects. The model decides when to invoke them based on the conversation. Think send_email, create_invoice, search_database.
Schema shape
{
"name": "send_email",
"description": "Send an email to one recipient. Returns the message ID on success.",
"inputSchema": {
"type": "object",
"properties": {
"to": { "type": "string", "format": "email" },
"subject": { "type": "string", "maxLength": 200 },
"body": { "type": "string" }
},
"required": ["to", "subject", "body"]
}
}
Design tips
Tool names are visible to the model — keep them concise, lowercase, snake_case. The description is the single most important piece of prompt engineering on the server side: it's literally the prompt the model uses to decide whether to call your tool. Spell out preconditions, expected output, edge cases. Don't be cute.
Schemas should be conservative. If limit is optional with a default, say so. If category is an enum, enumerate it — the model is much more reliable when constrained.
What they are
Resources are read-only data, addressed by URI. They behave like files: the host (or the user) can attach them to the context; the model doesn't call them like tools. Examples: file://notes.md, db://users/42, github://issue/123.
Schema shape
{
"uri": "file:///Users/jonathan/notes/2026-05-12.md",
"name": "Today's notes",
"description": "Running notes from today",
"mimeType": "text/markdown"
}
The crucial distinction
Tools are model-controlled: the model decides when to invoke them. Resources are application-controlled: the host (or the user via the UI) decides when to include them. A "send_email" capability is a tool. A "the current open file in my editor" is a resource.
Servers can also support resources/subscribe so the host gets notified when a resource changes — useful for things that mutate during a session (an open IDE buffer, a database row being edited).
What they are
Prompts are user-controlled templates. They show up as commands in the host's UI (Claude Desktop renders them as slash commands). The user picks one, fills in any arguments, and the server returns a pre-built message list that gets seeded into the conversation.
Schema shape
{
"name": "review-pr",
"description": "Generate a careful PR review",
"arguments": [
{ "name": "diff", "description": "The unified diff", "required": true },
{ "name": "style", "description": "tone (strict|gentle)", "required": false }
]
}
When to reach for them
Use prompts for repeated, structured workflows where the user knows in advance what they want. They beat re-typing the same instructions every time. They're not a substitute for tools — the model can't call a prompt, it can only call a tool.
Tools
The model picks them, based on the conversation. Have side effects. The thing you probably want most of the time.
Resources
The application picks them — often via the user. Read-only. Attached to context like a file.
Prompts
The user picks them explicitly from the host's UI. Templates with arguments that seed structured conversations.
Two transports cover everything.
The MCP spec defines two official transports. The shape of the messages is identical in both — only the framing differs.
stdio
The host spawns your server as a child process and exchanges newline-delimited JSON over its stdin and stdout. stderr is reserved for logging.
Use it when
- The server is a local tool (filesystem, git, local DB)
- You want zero auth — the OS isolates the process
- Distribution is via package managers (
uvx,npx)
One golden rule
Don't write to stdout unless it's a JSON-RPC frame. A stray print() corrupts the stream and the client will drop the connection. Log to stderr.
Streamable HTTP
Single endpoint that accepts POST requests for client→server messages, and optionally upgrades to SSE for streaming responses and server→client traffic. Replaces the older HTTP+SSE design from the 2024 spec.
Use it when
- The server is remote (SaaS, cloud)
- Multiple clients should share one server
- You need standard web auth (OAuth, bearer tokens)
One detail
The server identifies a session via the Mcp-Session-Id response header, which the client echoes back on subsequent requests.
POST /messages + GET /sse). It still works against existing servers, but new code should use Streamable HTTP.
One tool call, six messages.
This is the most important diagram on the page. Step through the six messages of a single tool call. Notice that the host is in the middle of every exchange — the model never talks to the server directly.
POST https://api.anthropic.com/v1/messages
{
"model": "claude-sonnet-4-6",
"system": "You are a helpful assistant…",
"messages": [
{ "role": "user", "content": "What do we know about the NHG community?" }
],
"tools": [
{
"name": "community_search",
"description": "Full-text search across Stedt regional communities…",
"input_schema": { "type": "object", "properties": { … }, "required": ["query"] }
}
]
}
A complete server in 20 lines.
Hover or click any line of the Python code below to see what it does. The right panel updates with the explanation.
1# pip install "mcp[cli]"2from mcp.server.fastmcp import FastMCP34mcp = FastMCP("weather")56@mcp.tool()7def get_weather(city: str) -> str:8 """Return the current weather for a city."""9 return f"It is 18°C and partly cloudy in {city}."1011@mcp.resource("notes://daily")12def daily_notes() -> str:13 with open("notes.txt") as f:14 return f.read()1516@mcp.prompt()17def summarize(text: str) -> str:18 return f"Summarize in 3 bullets:\n{text}"1920if __name__ == "__main__":21 mcp.run()
Run it
Save the file as server.py and test it locally with the MCP Inspector:
$ pip install "mcp[cli]"
$ npx @modelcontextprotocol/inspector python server.py
The Inspector is a small web UI that talks JSON-RPC to your server and lets you call tools, fetch resources, and see every byte on the wire. It's the single most valuable debugging tool while building.
Wire it into Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (on macOS) and add an entry:
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}
Restart the app. Claude Desktop will spawn your process, run the handshake, and surface get_weather to the model. Ask it about the weather in Amsterdam — if everything's wired up, it'll call your tool.
Lessons learned the hard way.
print() from a stdio server. Every byte you write to stdout outside a JSON-RPC frame corrupts the stream. The client will silently drop the connection and you'll have no idea why. Log to stderr (Python's logging.basicConfig(stream=sys.stderr)).
description field. A vague description means random tool selection. A precise one — including preconditions, return shape, edge cases — means reliable calls.
limit has a maximum, set it. If category is one of three values, make it an enum. The model is much more reliable when the schema constrains its options.
_meta.progressToken for long calls. Anything taking more than a couple of seconds should emit progress notifications. Hosts use them to keep the UI alive and to let the user cancel.
"Always sends — do not retry on uncertain failure.") or implement an idempotency key.
resources or prompts in the initialize response. Clients use that to decide whether to even call the list methods.
nextCursor. Don't dump the whole list in one response — hosts will pass that array straight into the model's context and you'll blow through token budgets.
Vocabulary, alphabetized.
Click any card to read the full entry. Every dotted term elsewhere on this page opens the same popover.
Where to go next.
The official spec →
The canonical reference. Every method, every field, every error code. Versioned by date.
Reference servers →
Real-world implementations: filesystem, git, Postgres, Slack, GitHub. Best source for non-trivial patterns.
Python SDK →
The package this guide's FastMCP examples use. Mature, well-documented.
TypeScript SDK →
The right pick for remote servers you want to deploy to a worker or Lambda. Streamable HTTP works out of the box.
MCP Inspector →
Browser UI for poking at any MCP server. npx @modelcontextprotocol/inspector ... and you're in.
JSON-RPC 2.0 spec →
The transport-agnostic protocol MCP is built on. A short, readable spec; worth knowing cold.