How an MCP server answers initialize
The handshake is the first exchange between an MCP client and a server. The client sends initialize with the version it wants. The server answers with the version it will use and what it can do.
It matters because a client only uses what the server says it offers. A server that claims too much breaks the client.
By the end of this lesson
Note
I can answer an initialize request and say what my server can do.
Key words
| Word | What it means | Example |
|---|---|---|
| Handshake | the first exchange that sets up a session | |
| Protocol version | the date that names a version of the rules | 2024-11-05 |
| Capability | a group of features the server says it has | tools |
| State | what the server remembers: new, waiting or ready | |
| Negotiate | agree on one version both sides speak |
The cells students fill in are left blank in this preview.
The server picks a version it knows
The client names a version. The server keeps a list of the versions it speaks. If the one asked for is on the list, the server answers with it.
If not, this server refuses with code -32602. It puts the version asked for and its own list in the error, so the client can try again.
Worked example: three clients ask
| Client asks for | Server speaks | Server answers |
|---|---|---|
| 2024-11-05 | 2025-03-26, 2024-11-05 | A result using 2024-11-05 |
| 2025-03-26 | 2025-03-26, 2024-11-05 | A result using 2025-03-26 |
| 1999-01-01 | 2025-03-26, 2024-11-05 | Error -32602 with both lists |
The cells students fill in are left blank in this preview.
A server announces only what it does
The result of initialize holds capabilities. Each one is a group of methods the server has: tools, resources, prompts or logging.
A capability can carry flags. listChanged says the server will tell the client when its list changes. This server never does, so it says false. A method from a group it did not announce is answered with -32601.
A common mistake
Warning
Many people announce every capability "just in case". The client then calls methods the server does not have. Announce only what you have written and tested. A claim is a promise.
Write the model helper
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", { method: \"POST\", headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" }, body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 80 }), signal: AbortSignal.timeout(30000) });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const data = await res.json();' ' return { text: data.choices[0].message.content, recorded: false };' ' } catch (error) {' ' console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' ' return { text: sample, recorded: true };' ' }' '}' 'module.exports = { ask };' > model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the server, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const SUPPORTED = [\"2025-03-26\", \"2024-11-05\"];' 'let state = \"new\";' 'function send(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function reply(id, result) { send({ id: id, result: result }); }' 'function fail(id, code, text, data) { send({ id: id, error: { code: code, message: text, data: data } }); }' > server2.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the server, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function initialise(msg) {' ' const asked = (msg.params || {}).protocolVersion;' ' if (SUPPORTED.indexOf(asked) < 0) return fail(msg.id, -32602, \"Unsupported protocol version\", { requested: asked, supported: SUPPORTED });' ' state = \"waiting\";' ' reply(msg.id, { protocolVersion: asked, capabilities: { tools: { listChanged: false } }, serverInfo: { name: \"lunch\", version: \"0.2\" } });' '}' >> server2.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the server, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function handle(msg) {' ' if (msg.id === undefined) {' ' if (msg.method === \"notifications/initialized\" && state === \"waiting\") state = \"ready\";' ' return;' ' }' ' if (msg.method === \"ping\") return reply(msg.id, {});' ' if (msg.method === \"initialize\") return state === \"new\" ? initialise(msg) : fail(msg.id, -32600, \"Already initialised\");' ' if (state === \"new\" || state === \"waiting\") return fail(msg.id, -32600, \"Not initialised\");' ' if (msg.method === \"tools/list\") return reply(msg.id, { tools: [] });' ' fail(msg.id, -32601, \"Method not found: \" + msg.method);' '}' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (line) {' ' let msg;' ' try { msg = JSON.parse(line); } catch (error) { return fail(null, -32700, \"Parse error\"); }' ' handle(msg);' '});' >> server2.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the scripted client, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const server = require(\"child_process\").spawn(\"node\", [\"server2.js\"]);' 'const init = function (id, version) { return { id: id, method: \"initialize\", params: { protocolVersion: version, capabilities: {}, clientInfo: { name: \"demo\", version: \"0.1\" } } }; };' 'const script = [' ' { id: 1, method: \"tools/list\" },' ' init(2, \"1999-01-01\"),' ' init(3, \"2024-11-05\"),' ' { id: 4, method: \"tools/list\" },' ' { method: \"notifications/initialized\" },' ' { id: 5, method: \"tools/list\" },' ' { id: 6, method: \"resources/list\" },' '];' > client2.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the scripted client, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const seen = [];' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (line) {' ' console.log(\"<- \" + line);' ' seen.push(JSON.parse(line));' ' if (seen.length === 6) server.stdin.end();' '});' 'script.forEach(function (m) {' ' const full = Object.assign({ jsonrpc: \"2.0\" }, m);' ' console.log(\"-> \" + JSON.stringify(full));' ' server.stdin.write(JSON.stringify(full) + \"\n\");' '});' >> client2.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the scripted client, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'server.on(\"close\", async function () {' ' const caps = JSON.stringify(seen[2].result.capabilities);' ' const r = await ask([{ role: \"user\", content: \"In one sentence, say what a server with these capabilities can do: \" + caps }], \"It can list tools, and it does not say that its list will change.\", 60);' ' console.log(\"model: \" + r.text);' '});' >> client2.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run the client against the server
docker exec -w /home/node/project agent node client2.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
What the script sends
Seven messages go in a fixed order. First a list too early and a bad version. Then a good version and a second list too early.
Then the ready notification, a list that works and a method the server never announced.
A short model call at the end words what the capabilities mean. With no model it prints a Recorded sample.
The six answers
| Request | Answer |
|---|---|
| 1 tools/list | Error -32600, Not initialised |
| 2 initialize, 1999-01-01 | Error -32602, with the two supported versions |
| 3 initialize, 2024-11-05 | A result with tools announced and the server name |
| 4 tools/list | Error -32600, still not ready |
| 5 tools/list | A result with an empty list |
| 6 resources/list | Error -32601, never announced |
The cells students fill in are left blank in this preview.