Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

The MCP initialize handshake

After this lesson: I can answer an initialize request and say what my server can do.

70 min lesson8 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

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

WordWhat it meansExample
Handshakethe first exchange that sets up a session
Protocol versionthe date that names a version of the rules2024-11-05
Capabilitya group of features the server says it hastools
Statewhat the server remembers: new, waiting or ready
Negotiateagree 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 forServer speaksServer answers
2024-11-052025-03-26, 2024-11-05A result using 2024-11-05
2025-03-262025-03-26, 2024-11-05A result using 2025-03-26
1999-01-012025-03-26, 2024-11-05Error -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

Command
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

Command
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

Command
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

Command
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

Command
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

Command
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

Command
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

Command
docker exec -w /home/node/project agent node client2.js

Shown 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

RequestAnswer
1 tools/listError -32600, Not initialised
2 initialize, 1999-01-01Error -32602, with the two supported versions
3 initialize, 2024-11-05A result with tools announced and the server name
4 tools/listError -32600, still not ready
5 tools/listA result with an empty list
6 resources/listError -32601, never announced

The cells students fill in are left blank in this preview.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.