Thomas Piacentini
← Atelier

Fiche

Building an MCP Server

Texte original · anglais

Validate, dispatch, wrap and cache a stateless MCP 2026-07-28 server. Tools, resources, prompts, policy.

Run the lesson servers

TaskCode
Serve on stdiopython3 main.py
Run the finite demopython3 main.py --demo
Run the TypeScript portnpx tsx main.ts --demo
Run the lesson testspython3 -m unittest discover tests -v
Build a request with _metamake_request(2, "tools/list")
Run the capstone modelpython3 code/main.py
Check the capstone TypeScriptnpm run typecheck
Sources

The dispatch loop

  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.
Sources

Validate and wrap every request

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_scopedefault "private"
Sources

Register a tool and its executor

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})
Sources

Tool, resource or prompt

FactorToolResourcePrompt
IntentPerform an operationRead content at a URIStart a message workflow
ChooserModel or applicationHost, application or userUser through host UI
ResultStructured action resultText or binary contentPrompt messages
Methodstools/list, tools/callresources/list, resources/readprompts/list, prompts/get
Exampledelete_notenotes://note-1review_note
Sources

Tips

  • 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.
Sources

Cache hints in the lessons

SettingDefaultNote
server/discover ttl3_600_000ms, cacheScope public
tools/list ttl60_000public. Capstone uses private
resources/list ttl10_000Lesson 07, private
resources/read ttl5_000Lesson 07, private
prompts/list ttl600_000Lesson 10, public
cache_scopeprivatecomplete() default in lesson 07
limit of notes_search10Integer from 1 through 50
MAX_TOOL_LIST_PAGES100Lesson 28 client page cap
Sources

Error codes

SymptomCauseFix
-32700 Parse errorLine is not JSONSend one JSON object per line
-32600 Invalid RequestNo jsonrpc 2.0 or no methodFix the envelope
-32602 invalid paramsparams._meta missingSend version and capabilities each time
-32022Unsupported version stringRetry with a version from supported
-32601Method not in HANDLERSAdd the handler or fix the name
-32602 on resources/readUnknown or invalid URINever return an empty read
HTTP 400 and -32020Mcp-Param-* header differs from bodyReject before dispatch
Client parse failsLogs on stdoutWrite diagnostics to stderr
Sources

Tool contracts

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.
Sources

Memory server project

CommandWhat it does
project_test.py --init <workspace>Creates your workspace. A fresh starter fails
project_test.py --all --path <workspace> --strictGrades all four stages
cli.ts --data-dir <dir> --put <file>Appends revision 1 to the JSONL log
cli.ts --data-dir <dir> --query "<text>"0.6 lexical plus 0.4 cosine score
cli.ts --data-dir <dir> --history <id>Every revision with its source
--put <file> --revision 1Appends revision 2. Revision 0 conflicts
cli.ts --data-dir <dir> --serve --port 8788REST and MCP on one store. Needs MEMORY_TOKEN
Sources

Capstone policy gate

remote URLtools/callallowallowapprovalRegistry APIserver.jsonstoreMCP clientserver/discover,tools/listclientpolicy_decideissuer, audience, scopeprocessRead-only replicasno affinityprocessApproval recordarguments_digeststoreState-changing replicasdestructive toolsprocessAudit sinkredacted entriesstore
Nodes and connections
NodeRoleKind
Registry APIserver.jsonstore
MCP clientserver/discover, tools/listclient
policy_decideissuer, audience, scopeprocess
Read-only replicasno affinityprocess
Approval recordarguments_digeststore
State-changing replicasdestructive toolsprocess
Audit sinkredacted entriesstore
FromToConnection
Registry APIMCP clientremote URL
MCP clientpolicy_decidetools/call
policy_decideRead-only replicasallow
policy_decideState-changing replicasallow
Approval recordState-changing replicasapproval
Read-only replicasAudit sink
State-changing replicasAudit sink
Sources

Questions

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.
Sources
Original source
---
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 <workspace>` | Creates your workspace. A fresh starter fails |
| `project_test.py --all --path <workspace> --strict` | Grades all four stages |
| `cli.ts --data-dir <dir> --put <file>` | Appends revision 1 to the JSONL log |
| `cli.ts --data-dir <dir> --query "<text>"` | 0.6 lexical plus 0.4 cosine score |
| `cli.ts --data-dir <dir> --history <id>` | Every revision with its source |
| `--put <file> --revision 1` | Appends revision 2. Revision 0 conflicts |
| `cli.ts --data-dir <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.
Code et livrables 1
Ce que vous en retenez

Enregistré dans votre carnet sur cet appareil.

Sources et limites

Les repères de lecture sont déclaratifs. Les résultats de tests sont enregistrés séparément ; ils valent pour les cas exécutés.

AI Engineering from Scratch · 3f9715c · MIT

Télécharger ce fichier ↓