Model Context Protocol (MCP)
Build, call and serve stateless MCP 2026-07-28 servers over stdio and Streamable HTTP.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Defaults and numbers
- Troubleshooting
- Questions and tips
Tasks#
| Task | Code |
|---|---|
| Serve over stdio | python3 main.py |
| Run the stdio demo | python3 main.py --demo |
| Serve Streamable HTTP | python3 main.py --serve |
| Probe the HTTP server | python3 main.py --probe |
| Discover a server | make_request(1, |
| List tools | make_request(2, |
| Call a tool | make_request(5, |
| Add a legacy peer | client.add_server("<name>", |
From Lesson 13.07, 13.07 code/main.py, 13.08 code/main.py, Lesson 13.09, 13.09 code/main.py
Define and run a tool#
TOOLS = [{ "name": "notes_search",name the model sees "description": "Search note titles and bodies by keyword.", "inputSchema": {"type": "object",JSON Schema of arguments "properties": {"query": {"type": "string"}}, "required": ["query"]}, "annotations": {"readOnlyHint": True},hint only}]def handle_tools_call(params): executor = TOOL_EXECUTORS.get(params.get("name")) if executor is None:unknown tool return complete({"content": [...], "isError": True}) content = executor(params.get("arguments", {})) return complete({"content": content, "isError": False})From 13.07 code/main.py
One tools/call over HTTP#
POST /mcpone message per POSTAccept: application/json, text/event-streamMCP-Protocol-Version: 2026-07-28equals the _meta versionMcp-Method: tools/callequals the JSON-RPC methodMcp-Name: notes_searchequals params.name{"jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": {"name": "notes_search", "arguments": {"query": "MCP"}, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}}}From Lesson 13.09, Lesson 13.06
Architecture#
From Lesson 13.08, 13.08 code/main.py, 13.07 code/main.py, Lesson 13.09, Lesson 13.16
Discover, list, call#
Tips#
- Sort list items. Stable order gives stable cache keys and model context.
- Bind local servers to
127.0.0.1. Origin checks are not authentication. - Match origins exactly. Prefix checks accept attacker suffixes.
- Send the bearer token in
Authorization. Never in the URL. - Return an explicit state handle. Sticky routing breaks on failover.
- On a tool name collision, prefix or reject. Never overwrite.
Methods#
| Command | What it does |
|---|---|
server/ | Versions, capabilities, instructions, server identity |
tools/ | Lists tools. Cacheable |
tools/ | Runs one tool. Returns content blocks |
resources/ | Lists URI-addressed data. Cacheable |
resources/ | Reads one resource by URI |
prompts/list, prompts/get | Lists and renders prompt templates |
subscriptions/ | Long-lived POST stream for change events |
notifications/ | Cancels a request on stdio. No response |
From Lesson 13.06, Lesson 13.09, Lesson 13.29
Key concepts#
- Stateless core
- Every request carries the metadata to interpret it.
- Request metadata
- Version and capabilities are required.
clientInfois recommended. - Complete result
- Every success has
resultTypeset to complete. - Cacheable result
- Lists, reads and discovery carry
ttlMsandcacheScope. - Tool annotations
readOnlyHint,destructiveHintare hints. The server enforces.- stdio
- Client-launched subprocess. JSON on stdout, logs on stderr.
- Streamable HTTP
- One
/mcpendpoint. Each message is a new POST. - Legacy era
- Up to
2025-11-25,initializethennotifications/initialized.
From Lesson 13.06, Lesson 13.07, Lesson 13.09
Defaults#
| Setting | Default | Note |
|---|---|---|
PROTOCOL_VERSION | 2026-07-28 | Modern revision in the lessons |
server/discover ttl | 3_600_000 | ms, cacheScope public |
tools/list ttl | 60_000 | ms, cacheScope public |
resources/read ttl | 5_000 | ms, cacheScope private |
cache_scope | private | complete() default |
| HTTP port | 8017 | --serve binds 127.0.0.1 |
MAX_REQUEST_BYTES | 1_048_576 | HTTP body cap |
legacy_probe_timeout_ms | 1_000 | Deadline for one initialize probe |
From 13.07 code/main.py, 13.08 code/main.py, 13.09 code/main.py
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
-32022 | Version not supported | Pick from data.supported. Retry, new id |
-32602 | Missing request metadata | Stamp _meta on every request |
HTTP 400 -32020 | Header disagrees with body | Mirror version, method and name exactly |
| HTTP 403 on POST | Origin not allowed | Add the exact origin to the allowlist |
| HTTP 405 | GET or DELETE on /mcp | POST, and subscriptions/listen for events |
| stdio client fails to parse | Logs on stdout | Write diagnostics to stderr |
| HTTP 401 | Invalid, expired or wrong-audience token | Get a token for this resource |
403 insufficient_scope | Tool needs more scope | Step up with the challenge scope |
From Lesson 13.06, Lesson 13.07, Lesson 13.08, Lesson 13.09, Lesson 13.16
Glossary#
- JSON-RPC 2.0
- Envelope of every MCP request, response and notification.
- SSE
text/event-streamresponse. Notifications, then the final response.- MRTR
- Multi Round-Trip Request.
input_required, then a fresh retry. - PKCE
- Verifier and S256 challenge. Protects an intercepted code.
- CIMD
- HTTPS metadata document whose URL is the client id.
- DCR
- Deprecated dynamic client enrollment for compatibility.
- RFC 9728
- Protected-resource metadata. Names the authorization servers.
Questions and answers#
- Tool error or protocol error?
isError: truefor a valid call that fails. JSON-RPC error for a bad request.- Can a server call the client?
- No. It returns
input_required. The client retries with a new id. - How do I cancel?
- stdio: send
notifications/cancelled. HTTP: close the response stream. - Does stdio need OAuth?
- No. Authorization applies to HTTP transports.
- When do I send
initialize? - Only to an allowlisted legacy peer. Never by default.
From Lesson 13.07, Lesson 13.08, Lesson 13.16, Lesson 13.29, Lesson 13.06