E9 · discover
5 minRun the server/discover block.
Done when: the output has "supportedVersions":["2026-07-28"] and ends with saída: 0. Got saída: 1? Check that port 3333 is free (ss -ltn | grep 3333).
Lesson 4 · 20 min
For the service to handle many clients at one URL, the transport is Streamable HTTP. In the 2026-07-28 revision it became stateless: no session to keep, each request carries what it needs. In this lesson you bring up the same server over HTTP and talk to it with curl, even making mistakes on purpose.
Start the lesson
Goal: serve the same server over stateless Streamable HTTP and read server/discover and the Mcp-Method and Mcp-Name headers in a request made by hand.
Click each step.
2025 tutorials teach SSE as its own transport, an initialize at the start, and the Mcp-Session-Id header to tie the requests of a session together. In 2026-07-28 that is gone: no session, no initialize, and the GET/SSE used to receive notices became subscriptions/listen (official changelog, read on 10/19/2026).
Stateless, the server scales like any API: two instances behind a load balancer answer the same. Whatever needs to survive between requests is your data, kept by you.
Same factory as lesson 2. It listens only on 127.0.0.1.
// http.mjs — serves the classes server over Streamable HTTP, stateless, on 127.0.0.1.
// Each request builds a new McpServer (createMcpHandler): the protocol keeps no session.
// Your service's data (here, TURMAS in the module) stays yours: database, cache, pool live outside the factory.
// The Host and Origin guards block DNS rebinding (a malicious page talking to your localhost).
import { createServer as criarHttp } from 'node:http';
import { createMcpHandler } from '@modelcontextprotocol/server';
import { toNodeHandler, localhostHostValidation, localhostOriginValidation } from '@modelcontextprotocol/node';
import { createServer } from './servidor.mjs';
const PORTA = Number(process.env.PORTA ?? 3333);
const handler = createMcpHandler(createServer);
const node = toNodeHandler(handler);
const hostOk = localhostHostValidation();
const origemOk = localhostOriginValidation();
const http = criarHttp((req, res) => {
if (!hostOk(req, res) || !origemOk(req, res)) return; // replies 403 on its own
if (!req.url.startsWith('/mcp')) { res.writeHead(404).end(); return; }
void node(req, res);
});
http.listen(PORTA, '127.0.0.1', () => console.error(`turmas: MCP em http://127.0.0.1:${PORTA}/mcp`));
process.on('SIGINT', async () => { await handler.close(); http.close(); process.exit(0); });
process.on('SIGTERM', async () => { await handler.close(); http.close(); process.exit(0); });server/discover done by hand. The block starts the server, asks, and shuts it down.
cd ~/projetos/formacao-webmcp-mcp-v72/kit
PORTA=3333 node http.mjs & PID=$!; sleep 1
curl -s -X POST http://127.0.0.1:3333/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},
"io.modelcontextprotocol/clientCapabilities":{}}}}' | grep '"supportedVersions"'
S=$?; kill $PID; wait $PID 2>/dev/null; echo "saída: $S"turmas: MCP em http://127.0.0.1:3333/mcp
{"result":{"supportedVersions":["2026-07-28"],"capabilities":{"tools":{"listChanged":true},"resources":{"listChanged":true},"prompts":{"listChanged":true}},"resultType":"complete","ttlMs":0,"cacheScope":"private","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"turmas","version":"1.0.0"}}},"jsonrpc":"2.0","id":1}
saída: 0supportedVersions lists the revisions it speaks; capabilities, what it offers. ttlMs and cacheScope are the cache hints of the new revision.
The second request is the same, without the header. The server refuses it because the header and the body don't match.
cd ~/projetos/formacao-webmcp-mcp-v72/kit
PORTA=3333 node http.mjs & PID=$!; sleep 1
META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}'
chamar() { # $1 = value of the Mcp-Name header (empty = without the header)
curl -s -w ' [HTTP %{http_code}]\n' -X POST http://127.0.0.1:3333/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/call' ${1:+-H "Mcp-Name: $1"} \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"buscar_turmas","arguments":{"termo":"mcp"},'"$META"'}}'
}
echo "## com Mcp-Name"; chamar buscar_turmas
echo "## sem Mcp-Name"; chamar ""
kill $PID; wait $PID 2>/dev/nullturmas: MCP em http://127.0.0.1:3333/mcp
## com Mcp-Name
{"result":{"content":[{"type":"text","text":"mcp-01 · Servidor MCP em uma tarde · 1 vagas\nmcp-02 · MCP com autorização · 0 vagas"}],"structuredContent":{"turmas":[{"id":"mcp-01","titulo":"Servidor MCP em uma tarde","vagas":1},{"id":"mcp-02","titulo":"MCP com autorização","vagas":0}]},"resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"turmas","version":"1.0.0"}}},"jsonrpc":"2.0","id":2} [HTTP 200]
## sem Mcp-Name
{"jsonrpc":"2.0","error":{"code":-32020,"message":"Bad Request: the request headers and body disagree: the body carries params.name=\"buscar_turmas\" but the required Mcp-Name header is absent","data":{"mismatch":{"header":"(missing)","body":"the body carries params.name=\"buscar_turmas\" but the required Mcp-Name header is absent"}}},"id":2} [HTTP 400]A client built with the SDK sends all three headers on its own. The -32020 error shows up in a hand-written client or behind a proxy that strips headers.
An initialize request from the 2025-11-25 revision against the same server.
cd ~/projetos/formacao-webmcp-mcp-v72/kit
PORTA=3333 node http.mjs & PID=$!; sleep 1
C=${TMPDIR:-/tmp}/cabecalhos-mcp.txt
# a client on the 2025-11-25 revision still opens with initialize; the same server answers
curl -s -D "$C" -X POST http://127.0.0.1:3333/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"antigo","version":"1.0.0"}}}' \
| grep -o '"protocolVersion":"[^"]*"'
echo "Mcp-Session-Id na resposta: $(grep -ci '^mcp-session-id' "$C")"
rm -f "$C"; kill $PID; wait $PID 2>/dev/nullturmas: MCP em http://127.0.0.1:3333/mcp "protocolVersion":"2025-11-25" Mcp-Session-Id na resposta: 0
The SDK v2 handler serves the stateless 2025 era and does not return Mcp-Session-Id. You don't need two servers during the transition.
Three requests by hand and one tweak of your own.
Run the server/discover block.
Done when: the output has "supportedVersions":["2026-07-28"] and ends with saída: 0. Got saída: 1? Check that port 3333 is free (ss -ltn | grep 3333).
Run the tools/call by hand block.
Done when: first call [HTTP 200] with "structuredContent"; second [HTTP 400] with "code":-32020.
Start the server with PORTA=4000 node http.mjs and repeat the discover, changing the port in curl.
Done when: curl at http://127.0.0.1:4000/mcp returns "supportedVersions".
Question 1 of 4
In the 2026-07-28 revision, how does the server know the client's version and capabilities?skill
_meta. initialize and Mcp-Session-Id belong to the 2025 era._meta. initialize and Mcp-Session-Id belong to the 2025 era.Review: One request, from curl to the responseQuestion 2 of 4
What result does a tools/call without the Mcp-Name header come back with?skill
Mcp-Name is missing on a method that has a name, it refuses with 400 and -32020 (header and body disagree).Mcp-Name is missing on a method that has a name, it refuses with 400 and -32020 (header and body disagree).Review: The kit's HTTP serverQuestion 3 of 4
What are the Host and Origin guards for on the server at 127.0.0.1?
Question 4 of 4
An old client sends initialize (2025-11-25) to the kit's server. What happens?
protocolVersion 2025-11-25 and zero Mcp-Session-Id.protocolVersion 2025-11-25 and zero Mcp-Session-Id.Review: The kit's HTTP serverPick an option. The explanation appears after you try.