stdio ではホストが子プロセスを起こします。ホストが 3 つあれば、同じサーバーが 3 つ立ちます。ウィンドウを 2 枚開けば 2 倍になります。
| 困ること | 中身 |
|---|---|
| 資源 | Node のプロセスが 1 本あたり数十 MB。10 本立てば無視できない |
| 記憶が分かれる | 記憶の中に持った状態は共有されない。片方で覚えたことを他方は知らない |
| 重い初期化 | 索引の構築や接続の確立を、起動のたびに繰り返す |
| 外部の上限 | API のレート制限や DB の接続数を、プロセスの数だけ食う |
まず、本当に共有が要るか考えてください。メモサーバーのように状態をファイルに持つなら、プロセスが増えても結果は揃います。共有が効くのは、起動が重いか、記憶の中に状態を持つ場合です。
| やり方 | 仕組み | 難所 |
|---|---|---|
| localhost の HTTP | 常駐を 1 本立て、各ホストは HTTP で繋ぐ | ホスト側の設定を HTTP に変える必要がある |
| stdio ブリッジ | 各ホストには軽い stdio のプロセスを起こさせ、それが常駐へ中継する | ブリッジを自分で書く |
| 名前付きパイプ | stdio と同じ形式をパイプで運ぶ | Windows 固有。SDK に既製品が無い |
07 章で作ったものが、そのまま使えます。1 本立てて、各ホストから繋ぐだけです。
# 常駐させる(1 回だけ)
$env:PORT = "3333"
node docs/samples/07-http/server.mjs
# 各ホストから繋ぐ
claude mcp add --transport http memo http://localhost:3333/mcp
codex mcp add memo --url http://localhost:3333/mcp
listen(PORT) はすべてのネットワークで待ち受けます。同じ PC だけに限るなら、明示します。
// 127.0.0.1 に限ると、外のマシンからは繋がらない
http.listen(PORT, '127.0.0.1', () => { /* ... */ });
認証の無いサーバーを外へ出さないでください。MCP サーバーはツールを実行する仕組みです。誰でも叩ける場所に置けば、誰でもコマンドを走らせられます。共有はあくまで同じ PC の中に留めるのが、この章の前提です。
listen は既定で全方位に開く。共有目的なら必ず絞る。「ホスト側の設定は stdio のままにしたい」「HTTP を知らない人にも配りたい」。そのときはブリッジを挟みます。
やることは単純です。stdin で受けた行を HTTP に投げ、返ってきたものを stdout に書くだけ。samples/10-shared/bridge.mjs の全文は 70 行ほどです。
const TARGET = process.env.MCP_TARGET ?? 'http://localhost:3333/mcp';
async function forward(line) {
const res = await fetch(TARGET, {
method: 'POST',
headers: {
'content-type': 'application/json',
// どちらの形で返してもよい、と伝える
accept: 'application/json, text/event-stream',
},
body: line,
});
// 通知を中継したときは本文が無い(202 が返る)
if (res.status === 202) return;
const body = await res.text();
for (const message of extractMessages(res.headers.get('content-type') ?? '', body)) {
send(message);
}
}
rl.on('line', (line) => {
if (!line.trim()) return;
forward(line).catch((err) => { /* エラーを返す(後述) */ });
});
本体が落ちていると fetch が失敗します。何も返さないとホストは待ち続けます。エラーを返して知らせます。
forward(line).catch((err) => {
log('中継に失敗:', err.message);
// 応答を待っているホストを固まらせないよう、エラーを返す。
// 通知(id 無し)には返さない
let id;
try { id = JSON.parse(line).id; } catch { /* 壊れた行は無視 */ }
if (id === undefined) return;
send(JSON.stringify({
jsonrpc: '2.0',
id,
error: { code: -32603, message: `本体へ中継できない: ${err.message}` },
}));
});
2 本のブリッジから同じ本体を見ていることを、テストで確かめました。
test('2 本のブリッジが同じ本体を見ている(片方で書いた内容が他方で読める)', async () => {
const a = await connect(sample('10-shared/bridge.mjs'), { env: { MCP_TARGET: TARGET } });
const b = await connect(sample('10-shared/bridge.mjs'), { env: { MCP_TARGET: TARGET } });
try {
const mark = `共有の確認 ${Date.now()}`;
await a.callTool({ name: 'memo_add', arguments: { text: mark } });
// 別のブリッジから探しても見つかる
const found = await b.callTool({ name: 'memo_search', arguments: { query: '共有の確認' } });
assert.match(textOf(found), new RegExp(mark));
} finally {
await a.close();
await b.close();
}
});
✔ ブリッジ越しでもツールの一覧が取れる (843.454ms)
✔ 2 本のブリッジが同じ本体を見ている(片方で書いた内容が他方で読める) (353.484ms)
✔ 本体が落ちているときは接続の時点で失敗する(固まらない) (151.9163ms)
共有の形にすると、本体を誰が起こすかという問題が出ます。stdio ではホストがやってくれていた仕事です。
| やり方 | 向く場面 | 注意 |
|---|---|---|
| 手で起動 | 試すとき | 閉じ忘れ・起動し忘れが起きる |
| スタートアップ | 自分の PC | ログオンしないと動かない |
| Windows サービス | 常に動かしたい | ユーザープロファイルを見られない場合がある |
| ブリッジが起こす | 起動忘れを無くしたい | 同時に起きたときの競合を自分で捌く |
| 状況 | お勧め |
|---|---|
| 状態を持たない・起動が軽い | 共有しない。stdio のままでよい |
| 起動が重い(索引・接続) | localhost の HTTP |
| 設定を変えたくない・配りたい | stdio ブリッジ |
| ポートを使いたくない | 名前付きパイプ(自分で書く) |
最後に、ここまでのものを 1 つにまとめます。コマンドを実行し、進み具合を知らせ、終わったら画面にも出すサーバーを作ります。