کلاد کد از نسخه‌ی 2.1.292 اتصال سرورهای محلی MCP را روی نسخه‌ی پروتکل 2026-07-28 مذاکره می‌کند و سرورهایی که فقط نسل ۲۰۲۵ را می‌فهمند دیگر به توافق نمی‌رسند. در این راهنما با یک سرور واقعی MCP و یک کلاینت خام نشان می‌دهیم کدام نسخه‌ی پروتکل واقعا توافق شده، و اگر سرور شما نسل ۲۰۲۵ است چه باید بکنید.

چه چیزی در 2.1.292 عوض شد

مذاکره‌ی نسخه‌ی پروتکل MCP شماره‌ی آزادی نبود که هر طرف خودش انتخاب کند. از این نسخه، کلاینت روی یک نسخه‌ی مشخص اصرار می‌کند و سرور باید همان را برگرداند یا خطا بدهد.

متن رسمی تغییر در لاگ تغییرات کلاد کد دو بخش جدا دارد. نسخه‌ی 2.1.274 کلاینت v2 و مذاکره‌ی 2026-07-28 را فقط برای نصب‌های Bedrock، Vertex، Foundry و نصب‌هایی که تله‌متری غیرفعال است فعال کرد. نسخه‌ی 2.1.292 آن رفتار را برای همه‌ی نصب‌های دیگر هم روشن کرد.

همان نسخه یک راه برگشت گذاشت: متغیر محیطی MCP_PROTOCOL_NEGOTIATION=legacy. کلاینت هشدار می‌دهد و مقدار ناشناخته را نادیده می‌گیرد. یعنی فقط دو مقدار پذیرفته می‌شود، legacy و auto.

نسخهانتشارچه چیزی عوض شد
2.1.274۲۰۲۶-۰۹-۱۶مذاکره‌ی 2026-07-28 فقط برای Bedrock، Vertex، Foundry و نصب بدون تله‌متری
2.1.292۲۰۲۶-۱۰-۰۶همان مذاکره برای همه‌ی نصب‌ها، از جمله سرورهای stdio محلی

زمان انتشار را از رجیستری npm خواندم، نه از تاریخ حدسی: 2.1.292 در 2026-10-06T17:10:31Z روی npm منتشر شد و برچسب latest را گرفت. همین 2026-10-06 لحظه‌ی خواندن من است.

یک سرور MCP واقعی بسازیم

برای اینکه ادعا نکنیم و ثابت کنیم، یک سرور کوچک می‌نویسیم که روی stdio گوش می‌دهد و فقط یک ابزار دارد. هر خط غیرمسلع علامت‌گذاری شده است.

npm install @modelcontextprotocol/sdk

این دستور در اجرای من 94 بسته در 6 ثانیه نصب کرد و نسخه‌ی SDK روی 1.32.1 نشست.

// server.mjs — سرور stdio با یک ابزار ساده
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
  { name: "era-probe", version: "0.0.1" },
  { capabilities: { tools: {} } },
);

// هر فراخوانی فقط یک رشته برمی‌گرداند تا خروجی خوانا بماند
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: "negotiated_version",
    description: "نسخه‌ی پروتکلی که در handshake توافق شد",
    inputSchema: { type: "object", properties: {} },
  }],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (req.params.name === "negotiated_version") {
    return { content: [{ type: "text", text: String(server.getClientVersion?.()?.name) }] };
  }
  throw new Error("ابزار ناشناخته");
});

// خطای رایج پیش از کار: نوشتن لاگ روی stdout در سرور stdio
await server.connect(new StdioServerTransport());
process.stderr.write("era-probe: listening on stdio\n");

اجرای سرور فقط یک خط روی stderr می‌دهد و روی stdout ساکت است. این سکوت عیب نیست: stdout در سرورهای stdio همان مسیر داده‌ی JSON-RPC است و هر لاگی که آنجا بنویسید، فریم پروتکل را خراب می‌کند.

$ node server.mjs
era-probe: listening on stdio

دست‌دادن خام: نسخه‌ی واقعی را ببینید

لایه‌ی SDK نام کلاینت و نسخه‌ی سرور را نشان می‌دهد، ولی نسخه‌ی توافق‌شده‌ی پروتکل را چاپ نمی‌کند. برای دیدن عدد واقعی باید یک کلاینت خام بنویسیم که خودش یک فریم initialize بفرستد.

// raw-initialize.mjs — کلاینت خام، بدون SDK در این سمت
import { spawn } from "node:child_process";
import readline from "node:readline";

const ask = process.argv[2] || "2025-11-25";
const child = spawn("node", ["server.mjs"], { stdio: ["pipe", "pipe", "pipe"] });

const rl = readline.createInterface({ input: child.stdout });
rl.on("line", (line) => {
  if (!line.trim()) return;
  const msg = JSON.parse(line);
  console.log(JSON.stringify(msg.result, null, 2));
  child.kill();
  process.exit(0);
});

// دقیقا همان چارچوبی که یک کلاینت stdio می‌فرستد
child.stdin.write(JSON.stringify({
  jsonrpc: "2.0",
  id: 1,
  method: "initialize",
  params: {
    protocolVersion: ask,
    capabilities: {},
    clientInfo: { name: "raw-probe", version: "0.0.1" },
  },
}) + "\n");

setTimeout(() => { child.kill(); process.exit(1); }, 15000);

اجرای این کد روی سروری که در بالا ساختیم، نسخه‌ی 2025-11-25 را برگرداند. سه بار تست کردم و نتیجه یکی بود.

$ node raw-initialize.mjs 2026-07-28
{
  "protocolVersion": "2025-11-25",
  "capabilities": { "tools": {} },
  "serverInfo": { "name": "era-probe", "version": "0.0.1" }
}

این تنها جواب معنادار این پست است. ما 2026-07-28 خواستیم و سرور 2025-11-25 داد. یعنی سرور ما اصلا نسل ۲۰۲۶ را نمی‌شناسد و در برابر کلاینتی که روی آن اصرار کند به توافق نمی‌رسد.

علت را در خود بسته‌ی SDK پیدا کردم. جست‌وجوی نسخه‌های پروتکل در node_modules/@modelcontextprotocol/sdk فقط این پنج مقدار را برمی‌گرداند: 2024-10-07، 2024-11-05، 2025-03-26، 2025-06-18 و 2025-11-25. رشته‌ی 2026-07-28 حتی یک بار هم در بسته نیست.

اگر قبلا سرور MCP را با SDK پایتون ساخته‌اید، همان نکته درباره‌ی آن هم صدق می‌کند و راه‌حل یکسانی دارد؛ در راهنمای ساخت سرور MCP با SDK پایتون همین ساختار را از سمت پایتون دیدیم.

پروب آماده برای سرور خودتان

حالا همان ابزار را طوری می‌نویسیم که روی سرور واقعی خودتان کار کند: فایل .mcp.json پروژه را می‌خواند، اولین سرور را اجرا می‌کند و می‌گوید کدام نسل را جواب می‌دهد.

#!/usr/bin/env node
// mcp-era-probe.mjs — تشخیص نسل پروتکل یک سرور stdio
import { spawn } from "node:child_process";
import readline from "node:readline";
import { readFileSync } from "node:fs";

// نسل مدرن از نسل ۲۰۲۵ جدا است؛ همین تفاوت مذاکره را می‌شکند
const MODERN = "2026-07-28";

const cfg = JSON.parse(readFileSync(process.argv[2] || ".mcp.json", "utf8"));
// .mcp.json می‌تواند سرورها را زیر mcpServers یا در ریشه بگذارد
const [name, spec] = Object.entries(cfg.mcpServers || cfg)[0];

const child = spawn(spec.command, spec.args || [], {
  stdio: ["pipe", "pipe", "pipe"],
  env: { ...process.env, ...(spec.env || {}) },
});
child.stderr.on("data", (b) => process.stderr.write("[server] " + b));

const rl = readline.createInterface({ input: child.stdout });
const timer = setTimeout(() => {
  console.error("timeout: the server never answered initialize");
  child.kill();
  process.exit(1);
}, 20000);

rl.on("line", (line) => {
  if (!line.trim()) return;
  let msg;
  try { msg = JSON.parse(line); } catch { return; } // لاگ نامعتبر روی stdout
  if (msg.id !== 1) return;
  clearTimeout(timer);

  const got = msg.result?.protocolVersion;
  console.log("server        :", name);
  console.log("answered with :", got);
  console.log("is 2026-era   :", got >= MODERN ? "yes" : "no");
  if (got && got < MODERN) {
    console.log("");
    console.log("This server is 2025-era. Claude Code 2.1.292+ negotiates " + MODERN + ".");
    console.log("Options: upgrade the server SDK, or set MCP_PROTOCOL_NEGOTIATION=legacy.");
  }
  child.kill();
  process.exit(0);
});

child.stdin.write(JSON.stringify({
  jsonrpc: "2.0",
  id: 1,
  method: "initialize",
  params: {
    protocolVersion: MODERN,
    capabilities: {},
    clientInfo: { name: "mcp-era-probe", version: "0.1.0" },
  },
}) + "\n");

خروجی روی سروری که ساختیم:

$ node mcp-era-probe.mjs
server        : era-probe
answered with : 2025-11-25
is 2026-era   : no

This server is 2025-era. Claude Code 2.1.292+ negotiates 2026-07-28.
Options: upgrade the server SDK, or set MCP_PROTOCOL_NEGOTIATION=legacy.

یک پروب که فقط می‌تواند بگوید «نه» ارزش تشخیصی ندارد. برای اثبات اینکه پروب واقعا کار می‌کند، یک سرور ساختگی نوشتم که 2026-07-28 را جواب می‌دهد و همان پروب را روی آن اجرا کردم.

$ node mcp-era-probe.mjs .mcp-modern.json
server        : modern-fake
answered with : 2026-07-28
is 2026-era   : yes

پس خط is 2026-era واقعا دو حالت دارد و فقط از روی رفتار یک سرور واقعی از yes و no می‌آید، نه از روی حدس.

اگر سرور شما نسل ۲۰۲۵ است

سه راه دارید و انتخاب بین آنها به این بستگی دارد که سرور را خودتان نگه می‌دارید یا نه.

  1. سرور را به نسخه‌ای که 2026-07-28 را می‌فهمد ارتقا دهید. فقط وقتی ممکن است که کد سرور را در اختیار داشته باشید.
  2. اگر سرور بسته‌ی آماده‌ای است و ارتقا ندارد، مذاکره را به حالت legacy برگردانید تا رفتار قبلی برگردد. این راه فرار است و تاریخ انقضا دارد.
  3. اگر بین دو نسخه گیر کرده‌اید، مقدار auto را امتحان کنید. در این حالت کلاینت فقط برای سرورهایی که از نسل مدرن پشتیبانی می‌کنند روی نسخه‌ی جدید اصرار می‌کند و بقیه را به راه قدیمی می‌فرستد.

برای پرهیز از این برگشت، در اسکریپت راه‌اندازی پروژه مقدار را صریح بنویسید و در مخزن هم ثبت کنید، وگرنه فقط روی ماشین خودتان کار می‌کند.

# در .env یا اسکریپت اجرا، نه فقط در ترمینال
export MCP_PROTOCOL_NEGOTIATION=legacy
claude

اگر پروب شما is 2026-era: yes داد و باز هم خطا گرفتید، مشکل جای دیگری است: آن خطا دیگر از مذاکره‌ی نسخه نمی‌آید. در آن حالت مقدار MCP_TIMEOUT و MCP_CONNECT_TIMEOUT_MS را بررسی کنید، چون کلاینت جدید برای تشخیص نسل سرور یک دور مذاکره‌ی جداگانه هم می‌زند.

همان منطق توافق در مستند چرخه‌ی عمر MCP تعریف شده و نسخه‌های منتشرشده‌ی پروتکل در برچسب‌های مخزن پروتکل فهرست شده‌اند. نسخه‌ی SDK هم در صفحه‌ی npm و لیست انتشارهای SDK قابل پیگیری است.

منابع

  1. لاگ تغییرات Claude Code — ورودی مربوط به 2.1.292، خوانده‌شده در ۶ اکتبر ۲۰۲۶
  2. انتشار v2.1.292 در مخزن claude-code
  3. انتشار v2.1.274 در مخزن claude-code
  4. فایل CHANGELOG.md خام کلاد کد — منبع جمله‌های دقیق درباره‌ی MCP_PROTOCOL_NEGOTIATION
  5. رجیستری npm برای بسته‌ی @anthropic-ai/claude-code — زمان انتشار 2026-10-06T17:10:31Z از همین‌جا خوانده شد
  6. مستند چرخه‌ی عمر MCP برای نسخه‌ی 2026-07-28
  7. انتشار نسخه‌ی 2026-07-28 پروتکل MCP
  8. صفحه‌ی npm بسته‌ی @modelcontextprotocol/sdk — نسخه‌ی نصب‌شده در این راهنما 1.32.1
  9. انتشارهای typescript-sdk
  10. لاگ تغییرات Codex برای مقایسه