はじめに
Model Context Protocol(MCP)の次期仕様「2026-07-28 Release Candidate」で、プロトコルの根幹に関わる変更が入った。initialize / notifications/initialized のハンドシェイクが廃止され(SEP-2575)、Mcp-Session-Id ヘッダーも廃止された(SEP-2567)。つまりサーバーは「ステートレス」に振る舞えるようになる。
この記事では、この変更に対応した TypeScript SDK v2 beta(@modelcontextprotocol/server / client / node)を実際にインストールし、initialize を一切送らずに tools/call を直接 POST して、本当に 200 OK が返ってくるのかをこのセッション内で実機検証する。
この記事で学べること
- MCP TypeScript SDK v1 → v2 beta の API 差分(
.tool()→registerTool()、パッケージ分割) -
initializeハンドシェイクなしでtools/callが通ることを、curl の生ログで確認する手順 - v2 beta を試す上でのハマりどころ
対象読者
- MCP サーバー・クライアントを実装している、または実装予定の方
- v1 → v2 移行の影響を事前に把握したい方
前提環境
- Claude Code のクラウド実行環境(Node.js v22.22.2 / npm 10.9.7)
-
@modelcontextprotocol/server@beta/@modelcontextprotocol/client@beta/@modelcontextprotocol/node@beta(いずれも2.0.0-beta.4)
TL;DR
- MCP 2026-07-28 Release Candidate で
initialize/notifications/initializedハンドシェイクが廃止された(SEP-2575) - 同時に
Mcp-Session-Idヘッダーも廃止された(SEP-2567) - TypeScript SDK v2 beta で実際に
initializeを送らずtools/callを直接 POST したところ、Mcp-Session-Idヘッダーなしで HTTP 200 OK が返ってきた - v1 の
.tool()は v2 でregisterTool()に変わり、パッケージも@modelcontextprotocol/sdkからserver/client/nodeに分割された
背景・課題
MCP は当初、TCP のようなコネクション指向のプロトコルとして設計されていた。クライアントはまず initialize を送り、サーバーがセッション ID を発行し、それ以降のリクエストにそのセッション ID を付ける、という手順が必須だった。
この設計は「1 リクエストごとに独立して処理したい」サーバーレス環境やロードバランサー配下の水平スケール環境と相性が悪い。MCP Specification Blog の Release Candidate 記事では、この課題意識のもとハンドシェイクとセッション ID をプロトコルの必須要件から外し、サーバーが「ステートレスに応答してよい」選択肢を持てるようにしたと説明されている。
本サイトでは既に仕様解説記事(#357, #476)を公開済みだが、今回は「実際に v2 beta SDK をインストールして動かし、ステートレスな挙動を実機ログで確認する」ハンズオンとして書く。
やったこと
ステップ1: v2 beta パッケージのインストール
npm でベータ版のバージョン一覧を確認してからインストールした。
$ npm view @modelcontextprotocol/server versions --json | tail -5
"2.0.0-beta.1","2.0.0-beta.2","2.0.0-beta.3","2.0.0-beta.4"
$ npm install @modelcontextprotocol/server@beta @modelcontextprotocol/client@beta
added 14 packages, and audited 15 packages in 2s
found 0 vulnerabilities
v1 では単一パッケージ @modelcontextprotocol/sdk(最新 1.29.0)だったが、v2 では server / client / node に分割されている。
ステップ2: InMemoryTransport でサーバー・クライアントを直結する
まずネットワークを介さず、同一プロセス内でサーバーとクライアントを繋いで動作確認した。
import { McpServer, InMemoryTransport } from '@modelcontextprotocol/server';
import { Client } from '@modelcontextprotocol/client';
import { z } from 'zod';
const server = new McpServer({ name: 'demo-server', version: '1.0.0' });
server.registerTool('add', {
description: '2つの数値を足す',
inputSchema: z.object({ a: z.number(), b: z.number() })
}, async ({ a, b }) => {
return { content: [{ type: 'text', text: String(a + b) }] };
});
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: 'demo-client', version: '1.0.0' });
await Promise.all([
server.connect(serverTransport),
client.connect(clientTransport),
]);
console.log('接続完了(v2はステートレス core: initialize/initialized ハンドシェイクなし)');
const tools = await client.listTools();
console.log('listTools():', JSON.stringify(tools, null, 2));
const result = await client.callTool({ name: 'add', arguments: { a: 3, b: 4 } });
console.log('callTool(add, {a:3,b:4}):', JSON.stringify(result, null, 2));
v1 の server.tool(name, schema, handler) は v2 では server.registerTool(name, config, handler) に変わっている点に注意。config に description と inputSchema をまとめて渡す形になった。
実行結果(このセッション内で実際に node で実行した生ログ):
接続完了(v2はステートレス core: initialize/initialized ハンドシェイクなし)
listTools(): {
"tools": [
{
"name": "add",
"description": "2つの数値を足す",
"inputSchema": {
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["a", "b"],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
}
]
}
callTool(add, {a:3,b:4}): {
"content": [
{ "type": "text", "text": "7" }
]
}
client.connect() の内部でハンドシェイクらしきログは一切出ない。listTools() / callTool() がそのまま通っている。
ステップ3: HTTP トランスポートで initialize を送らず直接 POST する
次に、実際のネットワーク越しの挙動を確認するため、@modelcontextprotocol/node@beta の NodeStreamableHTTPServerTransport を使い、sessionIdGenerator: undefined を指定して HTTP サーバーを立てた。
import { createServer } from 'node:http';
import { localhostHostValidation, localhostOriginValidation, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';
const server = new McpServer({ name: 'http-demo-server', version: '1.0.0' });
server.registerTool('echo', {
description: '入力をそのまま返す',
inputSchema: z.object({ text: z.string() })
}, async ({ text }) => ({ content: [{ type: 'text', text: `echo: ${text}` }] }));
const validateHost = localhostHostValidation();
const validateOrigin = localhostOriginValidation();
createServer(async (req, res) => {
if (!validateHost(req, res) || !validateOrigin(req, res)) return;
const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await transport.handleRequest(req, res);
}).listen(3939, '127.0.0.1', () => console.log('listening on 3939'));
サーバーを起動した状態で、initialize を一切送らずに tools/call をいきなり curl で POST した。
$ curl -s -i -X POST http://127.0.0.1:3939/ \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hello v2"}}}'
HTTP/1.1 200 OK
cache-control: no-cache
connection: keep-alive
content-type: text/event-stream
Date: Wed, 15 Jul 2026 03:25:56 GMT
Transfer-Encoding: chunked
event: message
data: {"result":{"content":[{"type":"text","text":"echo: hello v2"}]},"jsonrpc":"2.0","id":1}
レスポンスヘッダーに Mcp-Session-Id は含まれておらず、initialize なしでいきなり 200 OK が返ってきた。tools/list も同様に確認した。
$ curl -s -i -X POST http://127.0.0.1:3939/ \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
HTTP/1.1 200 OK
cache-control: no-cache
connection: keep-alive
content-type: text/event-stream
content-length: 277
Date: Wed, 15 Jul 2026 03:25:59 GMT
event: message
data: {"result":{"tools":[{"name":"echo","description":"入力をそのまま返す","inputSchema":{"type":"object","$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"text":{"type":"string"}},"required":["text"]}}]},"jsonrpc":"2.0","id":2}
v1 の Streamable HTTP トランスポートでは、initialize を経てセッション ID を払い出してもらう必要があった。v2 beta では sessionIdGenerator: undefined を指定するだけで、そのステップを完全に省略できる。
ハマりポイント
パッケージ名が変わっている
v1 の @modelcontextprotocol/sdk に慣れていると、npm install @modelcontextprotocol/sdk@2 のようなコマンドを打ちたくなるが存在しない。v2 beta は server / client / node の3パッケージに分割されているため、用途に応じて個別にインストールする必要がある。
registerTool() への書き換えが必要
v1 の server.tool(name, description, schema, handler) をそのまま v2 に持ち込むと引数の形が合わずエラーになる。server.registerTool(name, { description, inputSchema }, handler) という形に書き換える必要がある。
著者視点の発見ポイント
今回いちばん印象的だったのは、NodeStreamableHTTPServerTransport に sessionIdGenerator: undefined を渡すだけでステートレス化が完結した点。v1 のときはセッション管理用のストア実装まで踏み込む必要があったが、v2 beta ではオプション1行で「セッションという概念そのものを持たない」サーバーが書けるようになっている。
一方で InMemoryTransport を使った検証(ステップ2)ではハンドシェイクの有無が見えづらく、実際にネットワーク越しの HTTP リクエストを curl で直接叩いて初めて「本当にヘッダーが無い」ことを目視で確認できた。仕様変更の影響を確認するときは、SDK 内部のトランスポート層だけでなく生の HTTP レスポンスまで見るのが確実だと感じた。
まとめ
- MCP 2026-07-28 Release Candidate はプロトコルをステートレス化する大きな改訂(SEP-2575, SEP-2567)
- TypeScript SDK v2 beta で実際に
initializeなしのtools/callを直接 POST し、200 OK かつMcp-Session-Idヘッダーなしで応答することを確認した - v1 → v2 移行では
.tool()→registerTool()の書き換えとパッケージ分割への追従が必要 - まだ beta 版なので、本番導入前には正式リリースでの API 安定性を確認したい
参考リンク
- MCP Specification Blog: 2026-07-28 Release Candidate — ステートレス化の設計意図
- @modelcontextprotocol/server on npm — v2 beta パッケージのバージョン一覧