๐ฏ MCP Explained โ Step by Step with a Live Example (draw.io)¶
A hands-on walkthrough of how the Model Context Protocol works, using a draw.io diagramming server as the concrete example. Current as of spec 2026-07-28 and Python SDK v2; the server code below runs as written.
30-second version
The user asks the host for a diagram. The host already fetched the server's tool list and gave those tool definitions to the model. The model answers with a tool call; the host turns it into a JSON-RPC tools/call, the server runs a Python function and returns content (plus structuredContent if the tool has an output schema), and the host feeds that back to the model as a tool result. Repeat until the model answers the user. MCP is only the host โ server leg.
1. THE BIG PICTURE¶
Before the code, here is where MCP fits:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ USER (You) โ
โ "Create a class diagram for a parking lot system" โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MCP HOST (Claude Desktop / Claude Code / agent) โ
โ โ
โ 0. At startup: launch/connect to servers, call tools/list โ
โ 1. Send the user message + tool definitions to the LLM โ
โ 2. LLM replies with a tool call: create_diagram(...) โ
โ 3. Host sends tools/call to the server (after any user approval) โ
โ 4. Server executes, returns a result โ
โ 5. Host gives the result to the LLM; LLM calls more tools or โ
โ answers the user โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ MCP: JSON-RPC 2.0 over stdio or Streamable HTTP
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MCP SERVER (drawio-diagrammer) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โข create_diagram(title) โ new diagram id โ โ
โ โ โข add_class(diagram_id, name, attrs, methods) โ UML class box โ โ
โ โ โข add_relationship(diagram_id, from, to, type) โ arrow โ โ
โ โ โข export_diagram(diagram_id) โ .drawio XML โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The model never talks to the server and never sees JSON-RPC. It sees tool definitions and tool results in its own API format; the host translates.
Key Concept: MCP Decouples Intent from Execution¶
| Component | Role | Example |
|---|---|---|
| You | Express intent | "Draw a class diagram" |
| LLM | Chooses tools and arguments | "Call create_diagram, then add_class ร5" |
| MCP Host | Owns the model loop, user approval, and the MCP clients | Translates tool calls to tools/call |
| MCP Server | Executes the work | Builds the diagram |
| draw.io | Renders the result | Opens the .drawio file |
2. THE LIVE EXAMPLE: draw.io MCP Server¶
2.1 Server Implementation¶
A .drawio file is just mxGraph XML, so the server builds it directly; no draw.io library is needed. Save as drawio_mcp_server.py, pip install "mcp>=2", and run it under the Inspector (npx @modelcontextprotocol/inspector python drawio_mcp_server.py).
# drawio_mcp_server.py
"""A draw.io MCP server: builds UML class diagrams as .drawio (mxGraph XML) files.
No draw.io binding is needed: a .drawio file is XML that diagrams.net opens directly.
"""
import sys
import uuid
from typing import Literal, TypedDict
from xml.sax.saxutils import escape
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
mcp = MCPServer("drawio-diagrammer")
# In-memory store keyed by a server-minted handle. Fine for one stdio process;
# a multi-replica HTTP deployment would keep diagrams in Redis/a database.
diagrams: dict[str, dict] = {}
class DiagramRef(TypedDict):
"""A TypedDict (or Pydantic model) return type becomes the tool's outputSchema.
A bare `dict` has no known shape, so it is returned as text only."""
diagram_id: str
title: str
EDGE_STYLES = {
"inheritance": "endArrow=block;endFill=0;",
"composition": "endArrow=diamondThin;endFill=1;startArrow=none;",
"aggregation": "endArrow=diamondThin;endFill=0;",
"dependency": "endArrow=open;dashed=1;",
"association": "endArrow=open;",
}
def _get(diagram_id: str) -> dict:
if diagram_id not in diagrams:
known = ", ".join(diagrams) or "none"
raise ToolError(f"Diagram {diagram_id!r} not found. Known diagrams: {known}")
return diagrams[diagram_id]
@mcp.tool()
def create_diagram(title: str) -> DiagramRef:
"""Create a new, empty UML class diagram and return its diagram_id."""
diagram_id = f"d_{uuid.uuid4().hex[:8]}"
diagrams[diagram_id] = {"title": title, "classes": {}, "edges": []}
return DiagramRef(diagram_id=diagram_id, title=title)
@mcp.tool()
def add_class(diagram_id: str, class_name: str,
attributes: list[str], methods: list[str]) -> dict:
"""Add a UML class box.
attributes: e.g. ["-floors: list[Floor]"]; methods: e.g. ["+park(v: Vehicle): Ticket"]
"""
d = _get(diagram_id)
if class_name in d["classes"]:
raise ToolError(f"Class {class_name!r} already exists in {diagram_id}")
d["classes"][class_name] = {"attributes": attributes, "methods": methods,
"index": len(d["classes"])}
return {"status": "added", "class": class_name,
"attributes": len(attributes), "methods": len(methods)}
@mcp.tool()
def add_relationship(diagram_id: str, from_class: str, to_class: str,
relationship_type: Literal["inheritance", "composition", "aggregation",
"dependency", "association"]) -> dict:
"""Connect two existing classes. For inheritance, from_class is the subclass."""
d = _get(diagram_id)
missing = [c for c in (from_class, to_class) if c not in d["classes"]]
if missing:
raise ToolError(f"Unknown class(es) {missing}; add them with add_class first")
d["edges"].append((from_class, to_class, relationship_type))
return {"status": "added", "from": from_class, "to": to_class, "type": relationship_type}
@mcp.tool()
def export_diagram(diagram_id: str) -> str:
"""Return the diagram as .drawio XML (open it in diagrams.net or the draw.io app)."""
d = _get(diagram_id)
cells, ids = ['<mxCell id="0"/>', '<mxCell id="1" parent="0"/>'], {}
for name, c in d["classes"].items():
ids[name] = cid = f"c{c['index']}"
body = "<hr>".join([escape("<br>".join(c["attributes"])), escape("<br>".join(c["methods"]))])
label = escape(f"<b>{escape(name)}</b><hr>{body}")
x, y = 40 + (c["index"] % 4) * 240, 40 + (c["index"] // 4) * 220
cells.append(f'<mxCell id="{cid}" value="{label}" style="rounded=0;whiteSpace=wrap;html=1;'
f'align=left;verticalAlign=top;" vertex="1" parent="1">'
f'<mxGeometry x="{x}" y="{y}" width="200" height="160" as="geometry"/></mxCell>')
for i, (src, dst, kind) in enumerate(d["edges"]):
cells.append(f'<mxCell id="e{i}" style="{EDGE_STYLES[kind]}html=1;" edge="1" parent="1" '
f'source="{ids[src]}" target="{ids[dst]}"><mxGeometry relative="1" as="geometry"/></mxCell>')
return (f'<mxfile><diagram name="{escape(d["title"])}"><mxGraphModel><root>'
+ "".join(cells) + "</root></mxGraphModel></diagram></mxfile>")
if __name__ == "__main__":
print("drawio-diagrammer starting (stdio)", file=sys.stderr)
mcp.run()
Things this small server demonstrates:
| Detail | Why it matters |
|---|---|
Literal[...] on relationship_type |
Becomes an enum in the JSON Schema, so the model sees the valid values and bad values are rejected before your code runs |
-> DiagramRef (a TypedDict) |
Becomes the tool's outputSchema; results carry structuredContent. A bare dict return has no schema and comes back as text only |
ToolError with a helpful message |
Comes back as isError: true. "Known diagrams: d_1a2b..." lets the model fix its own mistake |
Server-minted diagram_id |
Cross-call state is an explicit handle passed as a tool argument, the pattern the 2026-07-28 spec recommends now that sessions are gone |
In-memory diagrams dict |
Fine for one stdio process. Behind a load balancer, put diagrams in Redis or a database, because the next call may hit another replica |
| Log to stderr | stdout is the protocol channel |
๐ DEEP DIVE: What Does mcp.run(transport="stdio") Actually Do?¶
mcp.run() (stdio is the default) starts the server and blocks until the host closes the pipe.
1. The Call Chain¶
mcp.run(transport="stdio") # synchronous entry point
โ
โผ
anyio.run(mcp.run_stdio_async) # start an event loop
โ
โโโ 1. stdio_server()
โ โข wraps fd 0 (stdin) as the read stream, fd 1 (stdout) as the write stream
โ โข one JSON-RPC message per line, UTF-8
โ
โโโ 2. low-level Server.run(read, write, ...)
โ โข tools/resources/prompts were registered when the
โ @mcp.tool()/@mcp.resource()/@mcp.prompt() decorators ran at import
โ โข a dispatcher reads messages, routes by method, runs handlers
โ (sync tools run on a worker thread so they don't block the loop)
โ
โโโ 3. Shutdown
โข stdin EOF (host closed the pipe) โ loop ends โ process exits
โข hosts follow up with SIGTERM, then SIGKILL, if it doesn't
2. What Happens at Each Level¶
Level 1: MCPServer.run() โ The High-Level Entry¶
run() validates the transport name and starts the matching async runner: run_stdio_async, or run_streamable_http_async for HTTP (which builds an ASGI app served by uvicorn). Decorators did the registration earlier, so run() only wires a transport to the already-built server.
Level 2: StdioServerTransport โ The I/O Layer¶
Conceptually (the SDK's real code uses anyio streams):
import json
import sys
for line in sys.stdin: # blocks until the host writes a line; EOF ends the loop
message = json.loads(line) # one JSON-RPC message per line, no embedded newlines
response = dispatch(message) # None for notifications
if response is not None:
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush() # stdout to a pipe is block-buffered: flush every message
This is also why print() in a stdio server breaks things: that text lands in the protocol stream and the host fails to parse it.
Level 3: Server โ The Protocol Handler¶
Routes each message by method and wraps the outcome as a JSON-RPC result or error:
| Incoming | Handler result |
|---|---|
server/discover (2026-07-28) |
Supported versions, capabilities, server info |
initialize (legacy clients) |
Negotiated version, capabilities; the SDK serves both eras |
tools/list, resources/list, prompts/list |
Definitions generated from your decorated functions |
tools/call |
Validate arguments โ call the function โ content (+ structuredContent), or isError |
| Unknown method | JSON-RPC error -32601 Method not found |
3. The Complete stdio Transport Lifecycle¶
HOST (Claude Desktop) SERVER (Python process)
โโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
1. Spawns the server as a child process:
$ python drawio_mcp_server.py
imports module, registers tools,
blocks reading stdin
2. Probe (dual-era client):
{"method":"server/discover","id":1}
โโโบ
โโโ {"result":{"supportedVersions":[...],
"capabilities":{"tools":{...}}},"id":1}
(a legacy server would reject this; the client then falls back to initialize)
3. {"method":"tools/list","id":2,"params":{"_meta":{...}}}
โโโบ
โโโ {"result":{"tools":[...]},"id":2}
4. {"method":"tools/call","id":3,"params":{"name":"create_diagram",...}}
โโโบ runs create_diagram()
โโโ {"result":{"content":[...],
"structuredContent":{...}},"id":3}
... one tools/call per model tool call ...
5. Closes the server's stdin
โโโบ EOF โ exits
4. Technical Details & Timing¶
| Aspect | Detail |
|---|---|
| Process model | One child process per configured server, started by the host |
| IPC | Anonymous pipes on stdin/stdout. No sockets, no ports |
| Message format | Newline-delimited JSON-RPC 2.0, UTF-8 |
| Latency | Pipe overhead is negligible (well under a millisecond); tool work and the model's turn dominate |
| Memory | A Python process plus the SDK and your dependencies: tens of MB at least, far more if you load ML models |
| Lifecycle | Lives as long as the host keeps it; the host closes stdin, then signals |
| Logging | stderr only: print("debug", file=sys.stderr) or logging (which defaults to stderr) |
| Credentials | From the environment the host passes in; no OAuth over stdio |
5. What This Means in Practice¶
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("my-server")
@mcp.tool()
def my_tool(x: int) -> int:
"""Double a number."""
return x * 2
if __name__ == "__main__":
mcp.run()
# 1. Python starts; the decorator registers my_tool with schema {"x": integer}
# 2. The process blocks on stdin
# 3. Host: server/discover (or initialize, for older hosts) โ capabilities
# 4. Host: tools/list โ [{"name": "my_tool", "inputSchema": {...}, "outputSchema": {...}}]
# 5. Host: tools/call {"name": "my_tool", "arguments": {"x": 5}}
# 6. Server: {"content": [{"type": "text", "text": "10"}], "structuredContent": {"result": 10}}
# 7. Host closes stdin โ EOF โ process exits
6. The "stdin/stdout" Architecture Diagram¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ HOST PROCESS โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ MCP Client โ โ
โ โ write(child.stdin, json_request + "\n") โ โ
โ โ read_line(child.stdout) โ json_response โ โ
โ โ read(child.stderr) โ host's log file โ โ
โ โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ stdin โโโโโโโโ JSON-RPC โโโโโโโโโบ
โ stdout โโโโโโโโ JSON-RPC โโโโโโโโโ
โ stderr โโโโโโโโ logs only โโโโโโโโบ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โผ SERVER PROCESS (child) โ
โ for line in stdin: parse โ dispatch โ write line + flush โ
โ EOF โ exit โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
7. Why No Network Port?¶
mcp.run(transport="stdio") opens no network port; it uses the file descriptors every process already has:
| File descriptor | Direction | Used for |
|---|---|---|
stdin (fd 0) |
Host โ Server | JSON-RPC requests (and responses to anything the server asked) |
stdout (fd 1) |
Server โ Host | JSON-RPC responses and notifications |
stderr (fd 2) |
Server โ Host's logs | Logging, debug output |
What this does and doesn't buy you: there's no remote attack surface, so no firewall rules or listener to secure. But the server is not sandboxed: it runs as you, with your files, network and credentials. A malicious or prompt-injected stdio server can do anything you can. Treat installing one like installing any program, and run untrusted ones in a container or sandbox.
3. STEP-BY-STEP: WHAT HAPPENS WHEN YOU SAY "DRAW A PARKING LOT CLASS DIAGRAM"¶
Step 1: User Sends a Request¶
Step 2: LLM Receives the Request¶
The host sends the conversation and the tool definitions (converted from tools/list) to the model. Shown here in a generic chat-completions style; each provider's format differs slightly:
{
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "Create a class diagram for a parking lot system" }
],
"tools": [
{
"type": "function",
"function": {
"name": "create_diagram",
"description": "Create a new, empty UML class diagram and return its diagram_id.",
"parameters": { "type": "object", "properties": { "title": { "type": "string" } }, "required": ["title"] }
}
}
]
}
(Other tools omitted. Hosts with many servers often prefix names, e.g. drawio__create_diagram, to avoid collisions.)
Step 3: LLM Decides to Use a Tool¶
{
"role": "assistant",
"content": "I'll start by creating the diagram.",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": { "name": "create_diagram", "arguments": "{\"title\": \"Parking Lot System\"}" }
}
]
}
Step 4: Host Sends JSON-RPC Request to MCP Server¶
If the tool needs approval, the host asks the user first. Then:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "create_diagram",
"arguments": { "title": "Parking Lot System" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": { "name": "claude-desktop", "version": "x.y.z" }
}
}
}
This is the MCP message on the wire: a line on the server's stdin, or the body of a POST to /mcp (with Mcp-Method: tools/call and Mcp-Name: create_diagram headers) over Streamable HTTP.
Step 5: MCP Server Processes the Request¶
- Parse the JSON-RPC message and look up the tool.
- Validate
argumentsagainst the tool's input schema (generated from type hints:titlemust be a string;relationship_typemust be one of theLiteralvalues). - Call the Python function.
- Convert the return value: text content for the model, plus
structuredContentwhen there's an output schema. AToolErrorbecomesisError: true.
Step 6: Server Sends JSON-RPC Response¶
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resultType": "complete",
"content": [
{ "type": "text", "text": "{\"diagram_id\": \"d_1a2b3c4d\", \"title\": \"Parking Lot System\"}" }
],
"structuredContent": { "diagram_id": "d_1a2b3c4d", "title": "Parking Lot System" },
"isError": false
}
}
Step 7: LLM Receives the Result and Plans Next Steps¶
The host appends the result as a tool message:
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"diagram_id\": \"d_1a2b3c4d\", \"title\": \"Parking Lot System\"}"
}
and calls the model again, which continues:
{
"role": "assistant",
"content": "Now the main classes.",
"tool_calls": [
{
"id": "call_def456",
"type": "function",
"function": {
"name": "add_class",
"arguments": "{\"diagram_id\": \"d_1a2b3c4d\", \"class_name\": \"ParkingLot\", \"attributes\": [\"-floors: list[ParkingFloor]\"], \"methods\": [\"+park_vehicle(v: Vehicle): Ticket\", \"+unpark_vehicle(t: Ticket): bool\"]}"
}
}
]
}
Models can emit several independent tool calls in one turn; hosts may run them in parallel.
Step 8: Multiple Tool Calls Build the Complete Diagram¶
Each step is a model turn plus a tools/call round trip:
LLM: add_class("ParkingLot", ...) โ Server: added
LLM: add_class("Vehicle", ...) โ Server: added
LLM: add_class("Car", ...) โ Server: added
LLM: add_relationship("Car", "Vehicle", "inheritance") โ Server: added
LLM: add_class("ParkingSpot", ...) โ Server: added
LLM: add_relationship("ParkingLot", "ParkingSpot", "composition") โ Server: added
LLM: export_diagram("d_1a2b3c4d") โ Server: .drawio XML
Seven model turns is slow and costly. A better-designed server would offer create_class_diagram(classes: list[ClassSpec], relationships: list[RelSpec]) and do it in one call. Fewer, coarser tools is the most common MCP design improvement.
Step 9: Final Response to User¶
{
"role": "assistant",
"content": "I've created the Parking Lot class diagram (saved as parking_lot.drawio; open it in diagrams.net). It includes ParkingLot, ParkingFloor, ParkingSpot, an abstract Vehicle with Car/Truck/Motorcycle subclasses, ParkingTicket and FeeCalculator, with inheritance, composition and dependency relationships."
}
4. THE COMPLETE JSON-RPC FLOW (DIAGRAM)¶
sequenceDiagram
participant U as User
participant H as MCP Host + LLM
participant S as draw.io MCP Server
Note over H,S: at startup
H->>S: server/discover (or initialize, legacy)
S-->>H: versions, capabilities
H->>S: tools/list
S-->>H: create_diagram, add_class, add_relationship, export_diagram
U->>H: Draw a parking lot class diagram
Note over H: LLM chooses create_diagram
H->>S: tools/call create_diagram
S-->>H: diagram_id d_1a2b3c4d
loop one per LLM tool call
H->>S: tools/call add_class or add_relationship
S-->>H: added
end
H->>S: tools/call export_diagram
S-->>H: .drawio XML
H-->>U: Diagram ready
5. WHERE IS MCP ACTUALLY?¶
MCP is a protocol: it lives in the messages between the host's MCP client and the server. Not in the model, and not between the host and the model.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ YOUR COMPUTER โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ CLAUDE DESKTOP (Host) โ โ
โ โ โ โ
โ โ conversation + tool definitions โ โ
โ โ โ HTTPS to the model provider's API โ โ
โ โ โผ (NOT MCP: the provider's own tool format) โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ LLM (remote API) โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ tool call โ โ
โ โ โผ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ MCP CLIENT โ โ โ
โ โ โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ MCP: JSON-RPC 2.0 over stdin/stdout โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ MCP SERVER (drawio_mcp_server.py) โ โ
โ โ MCPServer (Python SDK): parse, validate, dispatch โ โ
โ โ Your functions: create_diagram, add_class, ... โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
It's not a library or a framework but a contract both sides speak; the SDKs are just convenient implementations of it.
6. WHAT REQUESTS DO WE SEND? WHAT RESPONSES DO WE GET?¶
6.1 Request Types¶
| Method | Purpose | When |
|---|---|---|
server/discover |
Supported versions, capabilities, identity (2026-07-28) | Optional, before other requests |
initialize + notifications/initialized |
Handshake (legacy, โค 2025-11-25) | Connection start, older servers |
tools/list / tools/call |
Discover / execute tools | Startup / when the model asks |
resources/list / resources/read |
Discover / read data | When the host or user attaches context |
resources/templates/list |
Parameterised resource URIs | Startup |
prompts/list / prompts/get |
Discover / expand templates | Startup / when the user picks one (slash command) |
subscriptions/listen |
Long-lived stream of opted-in change notifications (2026-07-28) | When the host wants list_changed / resource updates |
completion/complete |
Autocomplete for prompt or resource-template arguments | While the user types |
6.2 Response Types¶
| Type | Structure | Example |
|---|---|---|
| Result | {"jsonrpc":"2.0","id":1,"result":{"resultType":"complete",...}} |
Tool result (possibly isError: true), resource contents |
| Input required | {"result":{"resultType":"input_required","inputRequests":{...},"requestState":"..."}} |
Server needs elicitation before finishing (2026-07-28 MRTR) |
| Error | {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"..."}} |
Unknown tool, malformed params |
| Notification | {"jsonrpc":"2.0","method":"notifications/progress","params":{...}} (no id) |
Progress, list changes |
6.3 Concrete Request/Response Pair¶
Request (Host โ Server):
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "add_class",
"arguments": {
"diagram_id": "d_1a2b3c4d",
"class_name": "ParkingTicket",
"attributes": ["-ticket_id: str", "-entry_time: datetime", "-exit_time: datetime | None"],
"methods": ["+close(fee_calculator: FeeCalculator): float"]
},
"_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }
}
}
Response (Server โ Host):
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"resultType": "complete",
"content": [
{ "type": "text", "text": "{\"status\": \"added\", \"class\": \"ParkingTicket\", \"attributes\": 3, \"methods\": 1}" }
],
"isError": false
}
}
(add_class returns a plain dict, so there's no structuredContent; compare create_diagram.)
6.4 Error Response Example¶
Two different failures, two different shapes:
The tool ran and failed (unknown diagram id): a normal result with isError, which the model sees and can act on:
{
"jsonrpc": "2.0",
"id": 8,
"result": {
"resultType": "complete",
"isError": true,
"content": [
{ "type": "text", "text": "Error executing tool add_class: Diagram 'd_99' not found. Known diagrams: d_1a2b3c4d" }
]
}
}
The request itself is bad (no such tool, malformed params): the spec's answer is a JSON-RPC error, handled by the host and usually not shown to the model. (SDKs vary at the edges: Python SDK v2 reports an unknown tool as an isError result so the model can see it.)
7. MCP IN PRODUCTION¶
Local tools (files, git, the IDE) stay on stdio in production: they need the user's machine. Shared tools that many users or agents call become remote MCP servers over Streamable HTTP, deployed like any stateless web service:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PRODUCTION ARCHITECTURE โ
โ โ
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ User โโโโโโบโ AI host / โโโโโโบโ Gateway / LB โ โ
โ โ โ โ agent โ โ โข TLS โ โ
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ โข coarse limits โ โ
โ OAuth token โ โข round-robin โ โ
โ โโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโ โ
โ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ draw.io MCP server โ โ DB MCP server โ โ
โ โ N replicas, stateless โ โ N replicas โ โ
โ โ POST /mcp โ โ POST /mcp โ โ
โ โ diagrams in Redis/DB โ โ read-only role โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ traces, metrics, audit logs โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ OpenTelemetry โ metrics / logs / traces โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Production vs Development¶
| Aspect | Local server (stdio) | Remote server (Streamable HTTP) |
|---|---|---|
| Who runs it | The host, as a child process | You, as a service |
| Transport | Pipes | HTTPS POST; JSON or a per-request SSE stream |
| Overhead | Negligible | A network round trip (more across regions) |
| Scaling | One process per host | Many clients, many stateless replicas |
| Auth | Environment credentials | OAuth 2.1 bearer token per request, audience-checked |
| State | Process memory is fine | Externalise it (handles + Redis/DB) |
| Deployment | Package on PyPI/npm, or a binary | Container โ Kubernetes / serverless |
| Failure | Process dies, host restarts it | Health checks, retries, circuit breakers |
Production Deployment Steps¶
# 1. Build and push a versioned image (never deploy :latest)
docker build -t myregistry/mcp-drawio-server:1.0.0 .
docker push myregistry/mcp-drawio-server:1.0.0
# 2. Deploy (Deployment + Service + HPA; full manifest in 04_MCP_PRODUCTION_ARCHITECTURE.md)
kubectl apply -f k8s/mcp-drawio-server.yaml
# 3. Register the URL in the host, e.g. https://mcp.company.com/drawio/mcp
# The host discovers the auth server from the 401 + protected-resource metadata.
Kubernetes config (abridged; see Production Architecture ยง1.2 for probes, security context and autoscaling):
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-drawio-server
spec:
replicas: 3
selector:
matchLabels:
app: mcp-drawio-server
template:
metadata:
labels:
app: mcp-drawio-server
spec:
containers:
- name: server
image: myregistry/mcp-drawio-server:1.0.0
ports:
- containerPort: 8000
env:
- name: MAX_DIAGRAM_BYTES
value: "10485760"
- name: REDIS_URL
valueFrom:
secretKeyRef: { name: drawio-redis, key: url }
resources:
requests: { memory: "256Mi", cpu: "250m" }
limits: { memory: "512Mi" }
---
apiVersion: v1
kind: Service
metadata:
name: mcp-drawio-service
spec:
selector:
app: mcp-drawio-server
ports:
- port: 80
targetPort: 8000
8. COMPLETE FLOW SUMMARY¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE COMPLETE MCP FLOW โ
โ โ
โ PHASE 1: CONNECTION โ
โ stdio: host spawns the server. HTTP: nothing to open. โ
โ Optional server/discover โ versions + capabilities โ
โ (legacy servers: initialize โ result โ notifications/initialized)โ
โ โ
โ PHASE 2: DISCOVERY โ
โ tools/list โ [{"name":"create_diagram","inputSchema":...}, ...] โ
โ host converts these into the model's tool definitions โ
โ โ
โ PHASE 3: EXECUTION (repeats) โ
โ model emits a tool call โ host (user approval if needed) โ
โ โ tools/call {"name":"add_class","arguments":{...}} โ
โ โ server validates, runs the function โ
โ โ {"content":[...], "structuredContent":{...}, "isError":false} โ
โ โ host feeds the result back โ model calls more tools or answers โ
โ โ
โ PHASE 4: SHUTDOWN โ
โ stdio: host closes stdin โ server exits. HTTP: nothing to close.โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
9. KEY TAKEAWAYS¶
- MCP is a protocol, not a product: JSON-RPC 2.0 messages between a host's MCP client and a server.
- The model doesn't know MCP exists: it sees tool definitions and results in its provider's API format; the host translates both ways.
- The host owns the loop and the user: tool selection happens in the model, execution approval and routing in the host.
- Round trips are model turns: each tool call usually costs a full LLM turn. Prefer fewer, coarser tools.
- Schemas guide, servers enforce: type hints and
Literals produce schemas that steer the model and reject bad types; business validation and security checks stay in your code. - Both transports are production transports: stdio for local tools, Streamable HTTP for shared remote ones. HTTP+SSE is deprecated.
- The draw.io example generalises: the same pattern works for databases, filesystems, SaaS APIs and internal services.
Next: See MCP Fundamentals for protocol mechanics, and MCP Production Architecture for production deployment.