The problem
An MCP (Model Context Protocol) server offers tools to a language model. You will build a minimal one: a class that stores tools, and answers the three methods of the protocol: server/discover (who are you?), tools/list (what tools do you have?) and tools/call (run this one). It is stateless: each request is handled on its own.
The class
class ToolServer(name, version, supported=["2026-07-28"]):
add_tool(self, name, description, input_schema, fn): register a tool, whose function isfn. Tools are listed in registration order.handle(self, message): process one parsed JSON-RPC message and return the response dict, orNonewhen no response must be sent.
handle, in this order
- A message without
"id"(a notification): returnNone. - A method other than
server/discover,tools/list,tools/call: error-32601, message"Method not found". check_meta(given, from the previous exercise) fails: return its error response.- Otherwise return
{"jsonrpc": "2.0", "id": <id>, "result": <result>}where<result>always has"resultType": "complete", plus:server/discover:"supportedVersions"(the list),"capabilities": {"tools": {}}, and"_meta"holding{"name": ..., "version": ...}under the keyserverInfoprefixed withio.modelcontextprotocol/;tools/list:"tools", a list of{"name", "description", "inputSchema"}, plus the caching hints"ttlMs": 300000and"cacheScope": "public";tools/call: look upparams["name"]. An unknown tool is a protocol error: error-32602, message"Unknown tool: <name>". Otherwise callfn(**params.get("arguments", {})). On success the result has"content": [{"type": "text", "text": str(value)}]and"isError": False. If the tool raises, it is a tool execution error: same shape, with the exception's text and"isError": True.
Two kinds of failure, on purpose: a protocol error means the request itself was wrong (the tool does not exist), and is a JSON-RPC error. A tool execution error means the request was fine but the tool failed: it is an ordinary result with isError: true, and the text tells the model what went wrong, so that it can correct itself and try again.
Example. A server
ToolServer("demo", "0.1")with the toolsadd(addsaandb) andboom(always raisesValueError("city not found")). Every request below carries a valid_meta, and everyresulthas"resultType": "complete".
server/discover:resultwithsupportedVersions["2026-07-28"],capabilities{"tools": {}}, and_metaholding the server's namedemoand version0.1under itsserverInfokey.tools/list:resultwithtools(addandboom, each with name, description andinputSchema),ttlMs300000 andcacheScope"public".tools/calladdwitha=2,b=3:contentwith the text5, andisErrorfalse.tools/callboom:contentwith the textcity not found, andisErrortrue.tools/callnope:errorwith code-32602and message "Unknown tool: nope".resources/list:errorwith code-32601and message "Method not found".- Any notification (no
id):None.
What to implement
ToolServer with add_tool and handle.
Requirements
- The server is stateless: it keeps no information between requests besides its registered tools.