Cheat sheets

Building an MCP Server

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

  • Concepts and terms
  • Tasks and commands
  • Annotated code
  • Diagrams
  • Comparisons
  • Defaults and numbers
  • Troubleshooting
  • Questions and tips

Source

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

The dispatch loop#

1serve_stdioReads one line from stdin. Bad JSON gives-32700.2dispatchNo id means a notification. The server sends noresponse.3validate_requestChecks params._meta for version andcapabilities.4HANDLERSRoutes by method. An unknown method gives-32601.5completeAdds resultType and serverInfo to the handlerresult.6serve_stdioWrites one JSON line to stdout, flushes, forgetsthe metadata.

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"

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

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

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.

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

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

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.

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

Capstone policy gate#

remote URLtools/callallowallowapprovalstoreRegistry APIserver.jsonclientMCP clientserver/discover,tools/listprocesspolicy_decideissuer, audience,scopeprocessRead-onlyreplicasno affinitystoreApproval recordarguments_digestprocessState-changingreplicasdestructive toolsstoreAudit sinkredacted entriesremote URLtools/callallowallowapprovalstoreRegistry APIserver.jsonclientMCP clientserver/discover,tools/listprocesspolicy_decideissuer, audience,scopeprocessRead-only replicasno affinitystoreApproval recordarguments_digestprocessState-changingreplicasdestructive toolsstoreAudit sinkredacted entries

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.