Build your own Claude Code
MCP server·22 min

initialize + tools/list

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage server-handlers --watch

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

request in, response out — one id at a time
→  { 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

Your task

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 a result that declares a tools capability: protocolVersion: '2025-11-25', capabilities: { tools: { listChanged: false } }, and serverInfo: { name: 'example-mcp', version: '1.0.0' }. Declaring the capability is what tells the client it may send tools/list and tools/call at all.
  • tools/list → return { tools: [...] }, one entry per configured tool, each with name, description, and an inputSchema of { type: 'object', properties: {} }.

Every response echoes the request's id back unchanged and carries jsonrpc: '2.0'. Graded by program.ts mcp-server.

src/mcp.ts
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 }
  }
}
The capability is a promise, not decoration

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.

Echo the id, never invent one

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.

How it's graded

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.