07. HTTP に載せ替える

同じサーバーを Streamable HTTP で動かす

📅 作成: 2026-09-07 / 更新: 2026-09-07

この資料の内容

  1. 何が変わるか
  2. 載せ替える
  3. セッションを持つか
  4. 動かす
  5. どちらを選ぶか

何が変わるか

ツールの中身は変わりません。変わるのは誰がプロセスを起こすかです。

項目stdioStreamable HTTP
起動する人ホスト(子プロセスとして)自分(先に立てておく)
寿命ホストと一緒に終わる止めるまで生きている
共有ホストごとに 1 本ずつ立つ1 本を何人でも使える
置き場所同じ PC のみ別のマシンでもよい
console.log禁止使える
手間登録するだけ起動と終了を自分で見る
stdio Claude Code Codex Antigravity サーバー 1 サーバー 2 サーバー 3 同じものが 3 つ動く 状態は共有されない Streamable HTTP Claude Code Codex Antigravity サーバー 1 本 :3333 1 つで足りる 記憶も繋ぎ先も 1 つにまとまる
3 つのホストから使うなら、プロセスの数が 3 対 1 になる。10 章で詳しく扱う。

載せ替える

samples/07-http/server.mjs の全文です。memo-tools.mjs はそのまま使います

import { createServer } from 'node:http';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createMemoServer } from '../06-memo/memo-tools.mjs';

const PORT = Number(process.env.PORT ?? 3333);

const http = createServer(async (req, res) => {
	// MCP のエンドポイントは 1 つ。ここに POST が来る
	if (!req.url?.startsWith('/mcp')) {
		res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
		res.end('/mcp へ POST してください\n');
		return;
	}

	// セッションを持たない形。リクエストごとに使い捨てる
	const server = createMemoServer();
	const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });

	// 応答が終わったら片付ける。閉じ忘れると接続が溜まる
	res.on('close', () => {
		transport.close();
		server.close();
	});

	try {
		await server.connect(transport);
		await transport.handleRequest(req, res);
	} catch (err) {
		console.error('[http] 失敗:', err);
		if (!res.headersSent) {
			res.writeHead(500, { 'content-type': 'application/json' });
			res.end(JSON.stringify({
				jsonrpc: '2.0',
				error: { code: -32603, message: 'Internal server error' },
				id: null,
			}));
		}
	}
});

http.listen(PORT, () => {
	// stdio 版と違い、標準出力に書いても壊れない
	console.log(`メモサーバー(HTTP): http://localhost:${PORT}/mcp`);
});

Web のフレームワークは要りません。Node の node:http だけで足ります。transport.handleRequest(req, res) に投げれば、あとは SDK が処理します。

POST /mcp JSON-RPC が本文 node:http req, res transport .handleRequest(req, res) memo-tools.mjs 06 章と同じもの 書き換えたのは左半分だけ
トランスポートを差し替えるだけで済むのは、06 章で中身を分けておいたおかげ。

セッションを持つか

sessionIdGenerator をどうするかで、2 つの作りに分かれます。

書き方ふるまい向くもの
undefinedセッションを持たない。1 回のリクエストで完結する状態をファイルや DB に持つサーバー
() => randomUUID()セッション ID を発行し、記憶の中に状態を持つ接続ごとの状態が要るサーバー

メモサーバーは持たない側にしました。状態はファイルにあるので、リクエストごとに作り直しても困りません。

リクエスト 1 リクエスト 2 リクエスト 3 その場で作って捨てる その場で作って捨てる その場で作って捨てる memo.jsonl 状態はここだけにある サーバーの側に記憶を持たせないと、作り直しても平気になる プロセスを再起動しても、複数台に増やしても、同じように動く
状態を外に出しておくのは、共有や再起動を考えると効いてくる。

後片付けを忘れないでください。res.on('close', ...)transport.close()server.close() を呼びます。リクエストごとに作るのに閉じないと、使われないオブジェクトが溜まり続けます。

動かす

$env:PORT = "3333"
node docs/samples/07-http/server.mjs
メモサーバー(HTTP): http://localhost:3333/mcp

叩いてみる

$body = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"手","version":"1.0"}}}'

Invoke-WebRequest -Uri http://localhost:3333/mcp -Method Post `
	-Body $body -ContentType 'application/json' `
	-Headers @{ Accept = 'application/json, text/event-stream' } -UseBasicParsing

返ってきたものです。

HTTP 200
event: message
data: {"result":{"protocolVersion":"2025-11-25","capabilities":{...},"serverInfo":{"name":"memo","version":"1.0.0"}},"jsonrpc":"2.0","id":1}

Accept ヘッダを忘れない

これが最初の関門です。応答は「ただの JSON」か「SSE のストリーム」のどちらかで返ります。両方を受け取れると伝えないと、サーバーは断ります

Accept: application/json, text/event-stream
両方を書く Accept: application/json, text/event-stream どちらで返っても受けられる 片方だけ Accept: application/json 406 で断られる サーバーが SSE で返したいときに、受け取れる相手か確かめている
curl や fetch を手で書くときの引っかかりどころ。SDK のクライアントを使えば自動で付く。

SSE の形

SSE で返る場合、本文はこうなります。data: の後ろが JSON-RPC のメッセージです。

event: message
data: {"result":{...},"jsonrpc":"2.0","id":1}

手で解く場合はこう取り出します(10 章のブリッジで使います)。

function extractMessages(contentType, body) {
	if (contentType.includes('text/event-stream')) {
		return body.split('\n')
			.filter((line) => line.startsWith('data:'))
			.map((line) => line.slice(5).trim())
			.filter(Boolean);
	}
	return body.trim() ? [body.trim()] : [];
}

ホストに登録する

claude mcp add --transport http memo http://localhost:3333/mcp
codex mcp add memo --url http://localhost:3333/mcp

どちらを選ぶか

どちらにするか 別のマシンに置く必要がある? はい HTTP いいえ 複数のホストで 1 本を共有したい? はい HTTP いいえ stdio 起動も終了もホスト任せで済む
迷ったら stdio。共有したくなってから HTTP に移せばよい。中身は変えずに済む。

HTTP を選んだときに増える仕事

増えること対処
起動と終了常駐させる仕掛けが要る(サービス登録など)
ポートの管理他と重ならない番号を決める
落ちたときの検知ホストからは「繋がらない」としか見えない
誰でも繋げる同じ PC の他のプログラムからも叩ける。127.0.0.1 に限定する

外に出すなら認証が要ります。この資料の例は localhost の中だけで使う前提です。ネットワークに出す場合は、MCP の認可(OAuth)が別に決まっています。認証なしのサーバーを社内ネットワークに置かないでください。ツールは実行される側であり、誰でも叩ける状態は危険です。

次の章へ

ここまでは「訊かれたら答える」だけでした。次はサーバーの側から伝える方法と、その限界を扱います。