initialize + tools/list
# grade this stage (from your course directory)
npx theinference test --stage server-handlers --watchFor three chapters you've been the host — your client launched a server, shook hands, and called its tools. Now flip the wire around. In this chapter you are the server: the thing on the other end of the stdio pipe that answers JSON-RPC and exposes tools for someone else's agent to drive.
A server's life starts with two requests. initialize is the handshake — the client asks who you are and what you can do, and you answer with a protocol version, your capabilities, and your name. tools/list is discovery — the client asks for your menu, and you hand back the tool definitions it'll call later. Get these two right and a real MCP host can connect to you; the rest of the chapter just fills in tools/call.
→ { jsonrpc:"2.0", id:1, method:"initialize", params:{} }
← { jsonrpc:"2.0", id:1, result:{ protocolVersion, capabilities:{ tools:{listChanged:false} }, serverInfo } }
→ { jsonrpc:"2.0", id:2, method:"tools/list", params:{} }
← { jsonrpc:"2.0", id:2, result:{ tools:[ { name, description, inputSchema } ] } }
What to build
Implement MCPServer.handle for initialize + tools/list
In src/mcp.ts, implement handle(request) on the MCPServer class for the first two methods. The server is constructed with a list of tools (each { name, description }); your job is to answer requests against them.
initialize→ return aresultthat declares atoolscapability:protocolVersion: '2025-11-25',capabilities: { tools: { listChanged: false } }, andserverInfo: { name: 'example-mcp', version: '1.0.0' }. Declaring the capability is what tells the client it may sendtools/listandtools/callat all.tools/list→ return{ tools: [...] }, one entry per configured tool, each withname,description, and aninputSchemaof{ type: 'object', properties: {} }.
Every response echoes the request's id back unchanged and carries jsonrpc: '2.0'. Graded by program.ts mcp-server.
import type { JsonRpcRequest, JsonRpcResponse, ToolDef } from './types.ts'
export class MCPServer {
constructor(private tools: ToolDef[]) {}
handle(req: JsonRpcRequest): JsonRpcResponse {
// 'initialize' -> result: { protocolVersion: '2025-11-25',
// capabilities: { tools: { listChanged: false } },
// serverInfo: { name: 'example-mcp', version: '1.0.0' } }
// 'tools/list' -> result: { tools: this.tools.map(t => ({
// name: t.name, description: t.description,
// inputSchema: { type: 'object', properties: {} } })) }
// every response: { jsonrpc: '2.0', id: req.id, result }
}
}
capabilities.tools isn't a label — it's the contract. A client that doesn't see a tools capability in your initialize result is entitled to never call tools/list or tools/call. That's why it comes first: the handshake gates everything downstream. listChanged: false says you won't emit notifications/tools/list_changed — your tool set is fixed for the session, which is exactly true here.
JSON-RPC pairs responses to requests by id. Copy req.id onto the response verbatim — request id: 1 gets response id: 1, request id: 2 gets id: 2. The grader feeds both requests in order and compares each response by its id; a hard-coded or off-by-one id is the most common way this step goes red even when the result body is perfect.
Process mode. program.ts mcp-server constructs an MCPServer from case.json's serverTools (one echo tool), feeds it the requests array — an initialize then a tools/list — and prints { responses: [...] }. Each response is compared to fixtures/expected.json exactly: the initialize result with its declared tools capability and serverInfo, and the tools/list result with echo carrying inputSchema: { type: 'object', properties: {} }. Same id, same result — byte for byte.