CLAUDE LABEN
MCP — 7月28日のMCP仕様リリース候補でMcp-Session-Idヘッダーが廃止されステートレス化します。リモートMCPサーバーはスティッキーセッションなしで運用できますAPPS — 同じ仕様でMCP Apps(サーバー側でレンダリングするUI)と、長時間処理向けのTasks拡張が加わりますMEMORY — Python 0.116.0・TypeScript 0.110.0・Go 1.56.0など各SDKが、メモリストア呼び出しにagent-memory-2026-07-22ヘッダーを送るようになりましたSPILL — agent_toolsetやMCPツールの出力が10万文字を超えると自動でサンドボックス内のファイルへ退避され、モデルには切り詰めたプレビューが渡りますBG — 2分を超えるMCPツール呼び出しが自動でバックグラウンドへ移り、セッションを止めなくなりました。CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますRESUME — エージェントビューで/resumeを打つと過去セッションのピッカーが開き、選んだものをバックグラウンドセッションとして再開できますMCP — 7月28日のMCP仕様リリース候補でMcp-Session-Idヘッダーが廃止されステートレス化します。リモートMCPサーバーはスティッキーセッションなしで運用できますAPPS — 同じ仕様でMCP Apps(サーバー側でレンダリングするUI)と、長時間処理向けのTasks拡張が加わりますMEMORY — Python 0.116.0・TypeScript 0.110.0・Go 1.56.0など各SDKが、メモリストア呼び出しにagent-memory-2026-07-22ヘッダーを送るようになりましたSPILL — agent_toolsetやMCPツールの出力が10万文字を超えると自動でサンドボックス内のファイルへ退避され、モデルには切り詰めたプレビューが渡りますBG — 2分を超えるMCPツール呼び出しが自動でバックグラウンドへ移り、セッションを止めなくなりました。CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますRESUME — エージェントビューで/resumeを打つと過去セッションのピッカーが開き、選んだものをバックグラウンドセッションとして再開できます
記事一覧/API & SDK
API & SDK/2026-07-26上級

Mcp-Session-Id を前提にしない MCP サーバーへ — 状態の置き場所を作り直す

MCP 仕様のリリース候補でセッションヘッダーが廃止されます。自作リモート MCP サーバーの結合を監査スクリプトで数え、署名付きカーソルへ移すまでの実測記録です。

MCP47リモートMCPステートレスアーキテクチャ6Claude Code204

プレミアム記事

MCP 仕様の 2026-07-28 リリース候補に、Mcp-Session-Id ヘッダーの廃止が入りました。差分としては一行に見えて、リモート MCP サーバーの土台に触れる変更です。

私自身、個人開発の自動化まわりで小さな MCP サーバーを1台だけ動かしておりまして、最初は遠い話のつもりで読んでいました。ところが手元のコードを開いて数えてみると、セッションIDを鍵にした Map が三箇所。そのうち一つは、ページングのオフセットを抱えていました。

負荷分散の背後に2台目を置いた瞬間に崩れる構造。しかも1台で動かしている限り、テストは全部通ります。

以下は、その依存を数え、署名付きカーソルへ移すまでの手元の記録です。仕様はリリース候補の段階ですので、実際の移行前にMCP 仕様の一次情報を必ずご確認ください。

半分だけ失敗する、という壊れ方

先に、壊れ方を目で見える形にしておきたいと思いました。依存パッケージなしの Node で、最小の再現を書きます。

同じデータ源を見る「インスタンス」を複数用意し、クライアントはラウンドロビンで呼びます。セッション常駐型は、オフセットを自分のメモリに持ちます。

// roundrobin-repro.mjs — node roundrobin-repro.mjs で実行
import crypto from "node:crypto";
 
const ITEMS = Array.from({ length: 500 }, (_, i) => `item-${i}`);
const PAGE = 20;
 
// (A) セッション常駐型: オフセットをインスタンスのメモリに置く
function makeStatefulInstance(name) {
  const sessions = new Map(); // sessionId -> offset
  return {
    name,
    open(sessionId) { sessions.set(sessionId, 0); },
    listItems(sessionId) {
      if (!sessions.has(sessionId)) {
        const e = new Error("session not found");
        e.code = 404;                       // 実際のログではここが並びます
        throw e;
      }
      const off = sessions.get(sessionId);
      sessions.set(sessionId, off + PAGE);
      return ITEMS.slice(off, off + PAGE);
    },
  };
}
 
// (B) ステートレス型: 署名付きの不透明カーソルを往復させる
const SECRET = crypto.randomBytes(32);
const sign = (body) =>
  crypto.createHmac("sha256", SECRET).update(body).digest("base64url");
 
function encode(state) {
  const body = Buffer.from(JSON.stringify(state)).toString("base64url");
  return `${body}.${sign(body)}`;
}
function decode(token) {
  const [body, mac] = String(token).split(".");
  if (!body || !mac) throw new Error("malformed cursor");
  const a = Buffer.from(mac), b = Buffer.from(sign(body));
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) throw new Error("bad signature");
  return JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
}
function makeStatelessInstance(name) {
  return {
    name,
    listItems(cursor) {
      const off = cursor ? decode(cursor).off : 0;
      const slice = ITEMS.slice(off, off + PAGE);
      const next = off + PAGE < ITEMS.length ? encode({ off: off + PAGE }) : null;
      return { items: slice, nextCursor: next };
    },
  };
}
 
const CALLS = 200;
function runStateful(n) {
  const pool = Array.from({ length: n }, (_, i) => makeStatefulInstance(`i${i}`));
  const sid = crypto.randomUUID();
  pool[0].open(sid);                        // 初期化要求は1台目に届いた前提
  let fail = 0;
  for (let k = 0; k < CALLS; k++) {
    try { pool[k % n].listItems(sid); } catch { fail++; }
  }
  return fail;
}
function runStateless(n) {
  const pool = Array.from({ length: n }, (_, i) => makeStatelessInstance(`i${i}`));
  let cursor = null, fail = 0;
  for (let k = 0; k < CALLS; k++) {
    try {
      const r = pool[k % n].listItems(cursor);
      cursor = r.nextCursor;                // null になったら先頭へ戻る
    } catch { fail++; }
  }
  return fail;
}
 
for (const n of [1, 2, 3, 4]) {
  const s = runStateful(n), l = runStateless(n);
  console.log(
    `instances=${n} stateful_fail=${s}/${CALLS} (${(s / CALLS * 100).toFixed(1)}%)` +
    `  stateless_fail=${l}/${CALLS} (${(l / CALLS * 100).toFixed(1)}%)`
  );
}

手元での結果です。

インスタンス数セッション常駐型の失敗ステートレス型の失敗
10 / 200(0.0%0 / 200(0.0%)
2100 / 200(50.0%0 / 200(0.0%)
3133 / 200(66.5%0 / 200(0.0%)
4150 / 200(75.0%0 / 200(0.0%)

失敗率は素直に (n-1)/n を辿ります。ここで目を引いたのは、むしろ1行目のほうでした。

1台構成では失敗が一度も出ません。つまり、開発機でもステージングでも、インスタンスを1つしか立てていない限り、この不具合は永久に姿を見せない設計になっています。スティッキーセッションを外した本番でだけ、それも「半分だけ」現れます。

私はこの数字を見てから、移行の順番を組み直しました。コードを直す前に、まず何台構成で回っているかを確かめる、という順番です。

セッションへの結合を機械で数える

目視のレビューは、こういう横断的な依存を取りこぼします。sessionId という変数名だけを追っても、transportspagerState のような別名で同じ結合が生まれているためです。

そこで、結合そのものを重み付きで数えるスクリプトを書きました。

#!/usr/bin/env bash
# mcp-session-audit.sh — リモート MCP サーバーのセッション結合を洗い出す
# 使い方: ./mcp-session-audit.sh <src ディレクトリ>
set -uo pipefail
ROOT="${1:-src}"
[ -d "$ROOT" ] || { echo "no such dir: $ROOT" >&2; exit 2; }
 
SCORE=0
hit() { # hit <重み> <ラベル> <正規表現>
  local w="$1" label="$2" re="$3" out n
  out=$(grep -rInE "$re" "$ROOT" \
        --include='*.ts' --include='*.js' --include='*.mjs' --include='*.py' 2>/dev/null)
  n=$(printf '%s' "$out" | grep -c .)
  if [ "$n" -gt 0 ]; then
    SCORE=$((SCORE + w * n))
    printf '\n[%s] %s件 (重み%s)\n' "$label" "$n" "$w"
    printf '%s\n' "$out" | sed 's/^/  /' | head -12
  fi
}
 
hit 3 "セッションヘッダーの直接参照" 'mcp-session-id|Mcp-Session-Id|MCP_SESSION_ID'
hit 3 "セッションIDを鍵にしたメモリ保持" 'sessionIdGenerator|(transports|sessions|sessionStore)\s*[:=]\s*(new Map|\{)'
hit 2 "セッション単位のインメモリ状態" '\[[a-zA-Z_]*[Ss]essionId\]|\.get\(\s*sessionId|\.set\(\s*sessionId'
hit 2 "セッション終了に依存した後始末" 'onsessionclosed|onclose|DELETE\s+/mcp|session.*(delete|close|terminate)'
hit 1 "スティッキー前提のインフラ設定" 'sticky|affinity|ip_hash|sessionAffinity'
 
printf '\n=== 結合スコア: %s ===\n' "$SCORE"
if [ "$SCORE" -eq 0 ]; then
  echo "✅ セッション結合は検出されませんでした"
elif [ "$SCORE" -le 6 ]; then
  echo "⚠️  軽度: 参照箇所を個別に潰せば移行できます"
else
  echo "🛑 重度: 状態の置き場所そのものを設計し直す必要があります"
fi

動作を確かめるため、よくある書き方を詰め込んだ17行のサンプルに当てました。

[セッションヘッダーの直接参照] 1件 (重み3)
  sample/src/server.ts:4:  const sessionId = req.headers["mcp-session-id"] as string | undefined;
 
[セッションIDを鍵にしたメモリ保持] 2件 (重み3)
  sample/src/server.ts:2:const transports = new Map<string, StreamableHTTPServerTransport>();
  sample/src/server.ts:8:      sessionIdGenerator: () => randomUUID(),
 
[セッション単位のインメモリ状態] 3件 (重み2)
  sample/src/server.ts:5:  let transport = sessionId ? transports.get(sessionId) : undefined;
  sample/src/server.ts:15:  const offset = pagerState[sessionId] ?? 0;
  sample/src/server.ts:16:  pagerState[sessionId] = offset + 20;
 
=== 結合スコア: 15 ===
🛑 重度: 状態の置き場所そのものを設計し直す必要があります

17行で15点。行数あたりの密度としては、公式のサンプルをそのまま伸ばした構成ほど高く出ます。逆に言えば、点数の絶対値よりも「移行後に0へ落ちたか」を見る道具として使うのが向いています。

重みの根拠も書いておきます。ヘッダー参照とトランスポート保持(重み3)は、ヘッダーが消えた時点で確実に壊れます。セッション単位の状態(重み2)は静かに壊れます。インフラのスティッキー設定(重み1)は、消しても機能は壊れません。壊れ方の派手さではなく、気づきにくさで重みを付けています。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
セッション結合を数える監査スクリプト(17行のサンプルコードで結合スコア15を検出)
HMAC 署名付きカーソルの実装 — 88バイト・署名3.8μs/検証5.1μs の実測付き
インスタンス2台で50%・4台で75%が失敗する再現条件と、1台では再現しない理由
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

API & SDK2026-07-24
共通エージェント定義をセッション単位で差し替える運用設計
Managed Agents の agent_with_overrides を使い、1つの土台エージェントをセッションごとにモデル・プロンプト・ツール・MCP・スキルまで安全に差し替える設計パターンを、検証コードと実運用の落とし穴とともにまとめました。
API & SDK2026-07-08
MCP コネクタを申請・無人運用に回す前に、各ツールを契約テストで確かめる
手元で動いた MCP コネクタをそのまま無人ジョブに繋ぐと、応答形状の誤読や書き込みの二重発火で静かに壊れます。ツール記述・応答契約・冪等性・レイテンシを機械検証する小さなハーネスの作り方を、実測値とともにまとめました。
API & SDK2026-07-04
Claude apps gateway の発表を読んで、個人開発の管制面を作り直した話
セルフホスト型 Claude apps gateway の設計を管制面と実行面の分離として読み解き、個人開発の規模に縮約します。アプリ別コスト帰属・モデル許可リスト・fail-closed の支出上限を Cloudflare Workers で実装した記録です。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →