The handshake
# grade this stage (from your course directory)
npx theinference test --stage initialize --watchAn MCP connection opens with a handshake, and nothing — no tool list, no call — happens before it completes. The host sends initialize: here's the protocol version I speak, here's who I am, and here's what I'm capable of. The server replies with the mirror image: the version it agreed on and the capabilities it offers. From that reply you learn whether the server even has tools to list.
This step you build the request side of the handshake and parse the server's capabilities out of its response.
host → {"jsonrpc":"2.0","id":1,"method":"initialize","params":{
protocolVersion:"2025-11-25",
capabilities:{ roots:{}, sampling:{} },
clientInfo:{ name:"claude-code", version:"1.0.0" } }}
server → {"jsonrpc":"2.0","id":1,"result":{ capabilities:{ tools:{ listChanged:false } }, ... }}
└── parse this out ──┘
What to build
Implement initializeRequest and parse server capabilities
In src/mcp.ts, build the handshake request:
initializeRequest(id)— return a JSON-RPC request withmethod: 'initialize'andparamscarrying three fields:protocolVersion(thePROTOCOL_VERSIONconstant,'2025-11-25'),capabilities(what the host offers —{ roots: {}, sampling: {} }), andclientInfo({ name: 'claude-code', version: '1.0.0' }). Setjsonrpc: '2.0'and the givenid.- Then, from the server's response, pull out its capabilities:
serverResponse.result.capabilities(ornullif there's no response).
Graded by program.ts mcp-init, which calls initializeRequest(id) from the case fixture, reads serverResponse.result.capabilities, and prints { request, capabilities }.
export const PROTOCOL_VERSION = '2025-11-25'
export function initializeRequest(id: number): JsonRpcRequest {
// -> { jsonrpc:'2.0', id, method:'initialize',
// params: { protocolVersion, capabilities: { roots:{}, sampling:{} }, clientInfo: { name, version } } }
}
// In program.ts the parse is just: serverResponse?.result?.capabilities ?? null
The handshake is symmetric. The host advertises what it can do for the server — here roots (it can expose filesystem roots) and sampling (it can run model completions on the server's behalf). The server advertises what it offers the host — tools, resources, prompts. Each side reads the other's set and only uses features both agreed on. For this chapter the field that matters in the reply is tools: its presence is your signal that tools/list is worth sending next.
Two protocol rules to respect. The response must carry the same id as your request — that's what lets you match it; don't hard-code or drop it. And initialize gates everything: a real host sends no tools/list or tools/call until the server's initialize response arrives. The grader doesn't enforce ordering here, but build the mental model now — the next two steps assume the handshake already happened.
Process mode: your_program.sh mcp-init runs program.ts mcp-init on the case fixture and prints { request, capabilities }. The grader matches fixtures/expected.json exactly: request must be the full initialize frame (protocolVersion:"2025-11-25", capabilities:{roots:{},sampling:{}}, clientInfo:{name:"claude-code",version:"1.0.0"}, matching id), and capabilities must be the server set you parsed out ({"tools":{"listChanged":false}}). A wrong protocol version or a missing client field diffs on the spot.