--- id: mcp-server-build title: Building an MCP Server code: BM subtitle: Validate, dispatch, wrap and cache a stateless MCP 2026-07-28 server. Tools, resources, prompts, policy. hue: 172 sources: phases/13-tools-and-protocols/07-building-an-mcp-server, phases/13-tools-and-protocols/10-mcp-resources-and-prompts, phases/13-tools-and-protocols/28-mcp-tool-contracts-and-content, phases/19-capstone-projects/13-mcp-server-with-registry, projects/memory-server source_hash: 04ba5f070c3f22cc --- ## Run the lesson servers type: task-table from: phases/13-tools-and-protocols/07-building-an-mcp-server/docs/en.md, phases/13-tools-and-protocols/07-building-an-mcp-server/code/main.py, phases/13-tools-and-protocols/10-mcp-resources-and-prompts/docs/en.md, phases/13-tools-and-protocols/28-mcp-tool-contracts-and-content/docs/en.md, phases/19-capstone-projects/13-mcp-server-with-registry/docs/en.md | Task | Code | |---|---| | Serve on stdio | `python3 main.py` | | Run the finite demo | `python3 main.py --demo` | | Run the TypeScript port | `npx tsx main.ts --demo` | | Run the lesson tests | `python3 -m unittest discover tests -v` | | Build a request with `_meta` | `make_request(2, "tools/list")` | | Run the capstone model | `python3 code/main.py` | | Check the capstone TypeScript | `npm run typecheck` | ## The dispatch loop type: flow from: phases/13-tools-and-protocols/07-building-an-mcp-server/docs/en.md, phases/13-tools-and-protocols/07-building-an-mcp-server/code/main.py 1. **serve_stdio**: Reads one line from stdin. Bad JSON gives `-32700`. 2. **dispatch**: No `id` means a notification. The server sends no response. 3. **validate_request**: Checks `params._meta` for version and capabilities. 4. **HANDLERS**: Routes by `method`. An unknown method gives `-32601`. 5. **complete**: Adds `resultType` and serverInfo to the handler result. 6. **serve_stdio**: Writes one JSON line to stdout, flushes, forgets the metadata. ## Validate and wrap every request type: annotated-code lang: python from: phases/13-tools-and-protocols/07-building-an-mcp-server/code/main.py ```python meta = params.get("_meta") # -32602 when missing requested = meta.get(VERSION_KEY) # protocolVersion key if requested not in SUPPORTED_VERSIONS: raise RpcProblem(-32022, "Unsupported protocol version", {"requested": requested, "supported": SUPPORTED_VERSIONS.copy()}) if not isinstance(meta.get(CAPABILITIES_KEY), dict): raise RpcProblem(-32602, f"{CAPABILITIES_KEY} is required") result = {"resultType": "complete", **payload, "_meta": {SERVER_INFO_KEY: SERVER_INFO.copy()}} if ttl_ms is not None: # lists, reads, discovery result["ttlMs"] = ttl_ms result["cacheScope"] = cache_scope # default "private" ``` ## Register a tool and its executor type: annotated-code lang: python from: phases/13-tools-and-protocols/07-building-an-mcp-server/code/main.py ```python TOOL_EXECUTORS = {"notes_search": exec_notes_search} # name to function def handle_tools_list(params): return complete({"tools": sorted(TOOLS, key=lambda tool: tool["name"])}, ttl_ms=60_000, cache_scope="public") def handle_tools_call(params): executor = TOOL_EXECUTORS.get(name) if executor is None: # result, not RPC error return complete({"content": [...], "isError": True}) try: return complete({"content": executor(arguments), "isError": False}) except (KeyError, TypeError, ValueError) as exc: return complete({"content": [...], "isError": True}) ``` ## Tool, resource or prompt type: comparison from: phases/13-tools-and-protocols/10-mcp-resources-and-prompts/docs/en.md | Factor | Tool | Resource | Prompt | |---|---|---|---| | Intent | Perform an operation | Read content at a URI | Start a message workflow | | Chooser | Model or application | Host, application or user | User through host UI | | Result | Structured action result | Text or binary content | Prompt messages | | Methods | `tools/list`, `tools/call` | `resources/list`, `resources/read` | `prompts/list`, `prompts/get` | | Example | `delete_note` | `notes://note-1` | `review_note` | ## Tips type: tips from: phases/13-tools-and-protocols/07-building-an-mcp-server/docs/en.md, phases/13-tools-and-protocols/10-mcp-resources-and-prompts/docs/en.md, phases/13-tools-and-protocols/28-mcp-tool-contracts-and-content/docs/en.md, phases/19-capstone-projects/13-mcp-server-with-registry/docs/en.md, projects/memory-server/solution - Use `cacheScope: "private"` for user data. Use `ttlMs: 0` for secrets. - Reject descriptors that mirror `password`, `secret`, `token` or `authorization`. - Cap request bodies. The memory server sends 413 above 50000 bytes. ## Cache hints in the lessons type: defaults span: 1 from: phases/13-tools-and-protocols/07-building-an-mcp-server/code/main.py, phases/13-tools-and-protocols/10-mcp-resources-and-prompts/code/main.py, phases/13-tools-and-protocols/28-mcp-tool-contracts-and-content/code/main.py, phases/19-capstone-projects/13-mcp-server-with-registry/code/main.py | Setting | Default | Note | |---|---|---| | `server/discover` ttl | 3_600_000 | ms, `cacheScope` public | | `tools/list` ttl | 60_000 | public. Capstone uses private | | `resources/list` ttl | 10_000 | Lesson 07, private | | `resources/read` ttl | 5_000 | Lesson 07, private | | `prompts/list` ttl | 600_000 | Lesson 10, public | | `cache_scope` | private | `complete()` default in lesson 07 | | `limit` of `notes_search` | 10 | Integer from 1 through 50 | | `MAX_TOOL_LIST_PAGES` | 100 | Lesson 28 client page cap | ## Error codes type: troubleshooting from: phases/13-tools-and-protocols/07-building-an-mcp-server/docs/en.md, phases/13-tools-and-protocols/07-building-an-mcp-server/code/main.py, phases/13-tools-and-protocols/10-mcp-resources-and-prompts/docs/en.md, phases/13-tools-and-protocols/28-mcp-tool-contracts-and-content/docs/en.md | Symptom | Cause | Fix | |---|---|---| | `-32700` Parse error | Line is not JSON | Send one JSON object per line | | `-32600` Invalid Request | No `jsonrpc` 2.0 or no method | Fix the envelope | | `-32602` invalid params | `params._meta` missing | Send version and capabilities each time | | `-32022` | Unsupported version string | Retry with a version from `supported` | | `-32601` | Method not in `HANDLERS` | Add the handler or fix the name | | `-32602` on `resources/read` | Unknown or invalid URI | Never return an empty read | | HTTP `400` and `-32020` | `Mcp-Param-*` header differs from body | Reject before dispatch | | Client parse fails | Logs on stdout | Write diagnostics to stderr | ## Tool contracts type: concepts from: phases/13-tools-and-protocols/28-mcp-tool-contracts-and-content/docs/en.md, phases/13-tools-and-protocols/28-mcp-tool-contracts-and-content/code/main.py - **inputSchema**: JSON Schema. With no `$schema`, the dialect is 2020-12. - **No arguments**: `{"type": "object", "additionalProperties": false}`. - **outputSchema**: Optional. Once published, every result carries conforming `structuredContent`. - **structuredContent**: Any JSON value. Also send the JSON as a text block. - **Content blocks**: `text`, `image`, `audio`, `resource_link`, `resource`. - **x-mcp-header**: Mirrors a top-level argument to `Mcp-Param-{name}`. String, integer or boolean only. - **Header encoding**: Non-ASCII or padded values use `=?base64?{Base64UTF8}?=`. - **Cursors**: Opaque. An empty `nextCursor` is valid. Stop only on `None`. ## Memory server project type: command-table from: projects/memory-server/README.md, projects/memory-server/API.md, projects/memory-server/project.json, projects/memory-server/solution | Command | What it does | |---|---| | `project_test.py --init ` | Creates your workspace. A fresh starter fails | | `project_test.py --all --path --strict` | Grades all four stages | | `cli.ts --data-dir --put ` | Appends revision 1 to the JSONL log | | `cli.ts --data-dir --query ""` | 0.6 lexical plus 0.4 cosine score | | `cli.ts --data-dir --history ` | Every revision with its source | | `--put --revision 1` | Appends revision 2. Revision 0 conflicts | | `cli.ts --data-dir --serve --port 8788` | REST and MCP on one store. Needs MEMORY_TOKEN | ## Capstone policy gate type: architecture from: phases/19-capstone-projects/13-mcp-server-with-registry/docs/en.md, phases/19-capstone-projects/13-mcp-server-with-registry/code/main.py ```arch node | reg | 1,1 | Registry API | server.json | store node | client | 2,1 | MCP client | server/discover, tools/list | client node | gw | 3,1 | policy_decide | issuer, audience, scope | process node | ro | 4,1 | Read-only replicas | no affinity | process node | appr | 2,2 | Approval record | arguments_digest | store node | rw | 3,2 | State-changing replicas | destructive tools | process node | audit | 4,2 | Audit sink | redacted entries | store edge | reg | client | remote URL edge | client | gw | tools/call edge | gw | ro | allow edge | gw | rw | allow edge | appr | rw | approval edge | ro | audit | edge | rw | audit | ``` ## Questions type: qa from: phases/13-tools-and-protocols/07-building-an-mcp-server/docs/en.md, phases/13-tools-and-protocols/10-mcp-resources-and-prompts/docs/en.md, phases/13-tools-and-protocols/28-mcp-tool-contracts-and-content/docs/en.md, phases/19-capstone-projects/13-mcp-server-with-registry/docs/en.md, projects/memory-server/project.json - **Tool error or protocol error?** A failed valid call returns `isError: true`. A bad envelope returns a JSON-RPC error. - **Does discovery unlock the server?** No. `tools/list` carries the same request metadata. - **Can the server ask the client for input?** It returns `input_required`. It never sends its own request. - **How does a client learn a resource changed?** It opens `subscriptions/listen`. It then calls `resources/read` again. - **Do annotations authorize?** No. `readOnlyHint` and `destructiveHint` are hints only.