Skip to content
AI agents

Migrate an MCP server to protocol 2026-07-28

Move an MCP server to the stateless 2026-07-28 revision on the official SDKs: no sessions or handshake, server/discover, input_required, then a raw stdio probe.

Verified install Not agent-tested v1.0.0 MIT updated mcptypescriptnodejspython
Equips
  • 1 1 Rules
  • 1 1 Skills
  • – 0 MCP

MCP revision 2026-07-28 removed the initialize handshake and sessions. Every request now carries its own _meta envelope, and a tool asks the user for input by returning input_required instead of pushing a request mid-call. Upgrading the SDK is not enough on its own: a TypeScript v2 server still speaks 2025 until it is served through serveStdio, and session-keyed state or elicitInput calls survive the official codemod. This loadout gives your agent the changelog as a fix list, plus a scanner for removed patterns and a dependency-free stdio probe. The probe sends server/discover and stateless calls, runs one input_required round trip, and checks -32022 for unknown versions and dual-era fallback. The verifier builds a TypeScript v2 server and probes it alongside a Python SDK 2.3.0 server.

What's in it

  • Changes checked against the official 2026-07-28 changelog and spec pages
  • Scanner for sessions, pushed elicitation and sampling, setLevel, subscribe and v1 wiring
  • stdio probe: server/discover, resultType, stable tools/list, -32022, dual-era check
  • input_required round trip in place of elicitInput, with a working TypeScript v2 example
  • Exact pins: @modelcontextprotocol/server 2.3.1, mcp 2.3.0, go-sdk v1.8.0

You'll need

  • Tools: nodejs, npm
Level 1 · automated

Verification

Verified install

Checked at commit 08c3710 · 9 passed, 0 failed, 1 not applicable

CheckResultWhat we found
Listinglisting.yaml matches the schema and declares every file in the folder. Pass
Schema valid; 8 files declared, none missing or extra
  • tested.status: untested (agent tests are separate from Level 1)
Files parseJSON parses; SKILL.md and .mdc front matter is valid YAML with the fields each agent needs; code fences are closed. Pass All 8 files parse; front matter valid
AdaptersCLAUDE.md imports @AGENTS.md, GEMINI.md imports @./AGENTS.md, and both match the generator. Pass
CLAUDE.md and GEMINI.md import AGENTS.md and are up to date
  • CLAUDE.md: matches the generator
  • GEMINI.md: matches the generator
Install pathsEvery file lands where each agent documents it, with no collisions, and the laid-out project is re-discovered correctly. Pass
Correct paths for all 6 agents; layouts re-discovered
  • Claude Code: 7 files, 1 skill in .claude/skills/
  • Codex CLI: 6 files, 1 skill in .agents/skills/
  • Cursor: 6 files, 1 skill in .agents/skills/
  • Gemini CLI: 7 files, 1 skill in .agents/skills/
  • Antigravity CLI: 6 files, 1 skill in .agents/skills/
  • GitHub Copilot: 6 files, 1 skill in .agents/skills/
MCP configmcp.json is structurally valid, converts to each agent's format, declares no literal secrets, and referenced packages exist on npm/PyPI. Not applicable No MCP servers in this loadout
Safety scanThe intake scanner finds no credentials, risky shell, hidden Unicode or prompt-injection wording, and the listing's network flag is honest. Pass
Clean: no credentials, risky shell or hidden text
  • Scanner intake-precheck/1: verdict clean; network no (listing says yes)
ReferencesLinks and file paths between the loadout's files resolve, including for each agent's installed layout. Pass 2 references between files resolve for every agent
Size limitsFile count and sizes are within the submission limits; SKILL.md bodies stay under 500 lines. Pass
8 files, 28.2 KB total; largest skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs 8.2 KB
  • Limits: 30 files, 32 KB per file, 128 KB total, 500 lines per skill or rule
LicenceA recognised SPDX licence, with the licence text available. Pass MIT, by Agent Loadout; text in the repository LICENSE
Starter buildA minimal fixture project with the loadout applied installs and builds or type-checks, and the loadout's own code examples compile. Pass
The mcp-2026-migration skill's TypeScript v2 server builds and passes the skill's 2026-07-28 stdio probe, including an input_required round trip and legacy initialize. The same probe passes a Python mcp 2.3.0 server. The scanner flags a v1 server and still flags it after the official codemod.
  • Applied 13 loadout files at every agent's paths
  • ✓ place the skill's server and scripts: bash -c mkdir -p src && cp .agents/skills/mcp-2026-migration/references/server.ts src/server.ts && cp .agents/skills/mcp-2026-migration/scripts/*.mjs . && echo placed
  • ✓ install (exact pins): npm ci --no-audit --no-fund
  • ✓ type-check and build (tsc): npm run build
  • ✓ probe the TypeScript v2 server: node probe_mcp_2026.mjs --call delete_note {"id":"n1"} --answer {"confirm":{"action":"accept","content":{"confirm":true}}} -- node dist/server.js
  • ✓ probe a Python mcp 2.3.0 server: node probe_mcp_2026.mjs --call lookup_term {"term":"skill"} -- uv run -q --no-project --python 3.12 --with mcp==2.3.0 python py_server.py
  • ✓ scanner is clean on the migrated server: node scan_mcp_legacy.mjs src
  • ✓ scanner flags the v1 server: bash -c node scan_mcp_legacy.mjs legacy; echo "exit=$?"
  • ✓ official codemod leaves the protocol work to do: bash -c mkdir -p cm/src && cp legacy/server.ts cm/src/ && printf '%s' '{"name":"cm","type":"module","dependencies":{"@modelcontextprotocol/sdk":"1.32.1"}}' > cm/package.json && npx -y @modelcontextprotocol/codemod@2.3.1 v1-to-v2 cm >/dev/null && node scan_mcp_legacy.mjs cm; echo "exit=$?"
  • Covers: serveStdio(buildServer) on @modelcontextprotocol/server 2.3.1 answers server/discover and stateless tools/list with resultType and serverInfo
  • Covers: A destructive tool asks through input_required and finishes on the retry with inputResponses
  • Covers: Unknown protocol version gets -32022 with the supported list; legacy initialize still works (dual-era)
  • Covers: The same probe passes the official Python SDK 2.3.0
  • Covers: The scanner finds sessions, pushed elicitation, protocol logging, resources/subscribe and v1 stdio wiring, and the codemod alone does not fix them

What this means: a script checked the files themselves: they parse, they install where each agent looks, any MCP config is sound, the safety scan is clean, references between files resolve, and, where a free and repeatable fixture exists, a small starter project builds with the loadout applied. No AI agent was run. Per-agent results ("tested with agent") are separate and stay untested until an agent actually runs.

Install

Choose your agent. Run the commands from your project root, or download a zip already laid out in the right folders.

Claude Code reads CLAUDE.md (imports AGENTS.md with @AGENTS.md) and skills from .claude/skills/.

Download zip
curl -fsSL --create-dirs -o AGENTS.md https://loadout.jnw-it.com/raw/mcp-2026-07-28-migration/1.0.0/AGENTS.md
curl -fsSL --create-dirs -o CLAUDE.md https://loadout.jnw-it.com/raw/mcp-2026-07-28-migration/1.0.0/CLAUDE.md
curl -fsSL --create-dirs -o .claude/skills/mcp-2026-migration/SKILL.md https://loadout.jnw-it.com/raw/mcp-2026-07-28-migration/1.0.0/skills/mcp-2026-migration/SKILL.md
curl -fsSL --create-dirs -o .claude/skills/mcp-2026-migration/references/changes.md https://loadout.jnw-it.com/raw/mcp-2026-07-28-migration/1.0.0/skills/mcp-2026-migration/references/changes.md
curl -fsSL --create-dirs -o .claude/skills/mcp-2026-migration/references/server.ts https://loadout.jnw-it.com/raw/mcp-2026-07-28-migration/1.0.0/skills/mcp-2026-migration/references/server.ts
curl -fsSL --create-dirs -o .claude/skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs https://loadout.jnw-it.com/raw/mcp-2026-07-28-migration/1.0.0/skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs
curl -fsSL --create-dirs -o .claude/skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs https://loadout.jnw-it.com/raw/mcp-2026-07-28-migration/1.0.0/skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs
FileGoes to
AGENTS.mdAGENTS.md
CLAUDE.mdCLAUDE.md
skills/mcp-2026-migration/SKILL.md.claude/skills/mcp-2026-migration/SKILL.md
skills/mcp-2026-migration/references/changes.md.claude/skills/mcp-2026-migration/references/changes.md
skills/mcp-2026-migration/references/server.ts.claude/skills/mcp-2026-migration/references/server.ts
skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs.claude/skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs
skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs.claude/skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs

Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want.

Files

Rules AGENTS.md Rules for moving an MCP server to the stateless 2026-07-28 revision through the official SDKs. 29 lines
Download Raw
# AGENTS.md: MCP server migrating to protocol 2026-07-28

This MCP server is moving to protocol revision 2026-07-28: stateless requests with a per-request `_meta` envelope,
`server/discover`, and `input_required` results instead of server-to-client requests. The spec at
https://modelcontextprotocol.io/specification/2026-07-28 is the authority. Do not rely on memory of older revisions,
which had sessions and an `initialize` handshake.

## How to work
- Start with the `mcp-2026-migration` skill. Scan first, then change code, then prove it with the skill's probe
  against the built server.
- Upgrade through the official SDK, never by hand-writing JSON-RPC: TypeScript `@modelcontextprotocol/server`
  2.3.1, Python `mcp==2.3.0`, Go `github.com/modelcontextprotocol/go-sdk` v1.8.0. Pin exactly and commit the
  lockfile.
- TypeScript v1 (`@modelcontextprotocol/sdk`): run the official codemod first, then fix what it marks.
- Keep 2025-era clients working (dual-era) unless the user decides otherwise. Ask before going modern-only.

## Rules
- No state keyed on a session or connection. Anything a later call needs goes back to the client as a handle or,
  within one call, as signed `requestState`.
- `inputResponses` and `requestState` come from the client: validate them, and never let them grant access.
- stdout is the protocol on stdio. Logs go to stderr.
- Register tools in a fixed order. Return `-32602` for an unknown resource.
- Destructive tools ask for confirmation through `input_required`, not by assuming the client will prompt.

## Done means
- The skill's scanner is clean, or each remaining finding is explained.
- The probe prints `RESULT: speaks 2026-07-28`.
- Type-check, lint and the existing tests pass.
Adapter CLAUDE.md Imports AGENTS.md for Claude Code. 11 lines
Download Raw
# CLAUDE.md

@AGENTS.md

## Claude Code notes

- Skills from this loadout live in `.claude/skills/`: `mcp-2026-migration`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From the Agent Loadout "Migrate an MCP server to protocol 2026-07-28" v1.0.0 — https://loadout.jnw-it.com/loadouts/mcp-2026-07-28-migration/ -->
Adapter GEMINI.md Imports AGENTS.md for Gemini CLI. 11 lines
Download Raw
# GEMINI.md

@./AGENTS.md

## Gemini CLI notes

- Skills from this loadout live in `.agents/skills/`: `mcp-2026-migration`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From the Agent Loadout "Migrate an MCP server to protocol 2026-07-28" v1.0.0 — https://loadout.jnw-it.com/loadouts/mcp-2026-07-28-migration/ -->
Skill skills/mcp-2026-migration/SKILL.md Scan, upgrade, rewrite removed patterns and probe the result. 85 lines
Download Raw
---
name: mcp-2026-migration
description: Move an MCP server to protocol revision 2026-07-28 (stateless requests, server/discover, input_required) on the official TypeScript, Python or Go SDK, then prove it with a raw stdio probe. Use when upgrading an MCP SDK, removing sessions or initialize handling, or when a 2026 client cannot talk to the server.
---

# Migrate an MCP server to 2026-07-28

Revision 2026-07-28 makes MCP stateless: no `initialize` handshake, no sessions, every request carries its own
`_meta` envelope, and servers can no longer push requests to the client mid-call. `references/changes.md` lists
every change with its fix. Read it before editing.

## 1. Find what has to change

```bash
node scripts/scan_mcp_legacy.mjs .
```

Each finding names the line, what the revision changed and the fix. Lockfiles, `node_modules`, `dist` and comments
are skipped. Work through the findings; do not delete a feature to make the scan clean without asking.

## 2. Upgrade the SDK (exact pins)

| SDK | 2026-07-28 from | Pin | Serving entry |
| --- | --- | --- | --- |
| TypeScript | v2 packages | `@modelcontextprotocol/server@2.3.1` | `serveStdio(() => buildServer())`, `createMcpHandler(factory)` for HTTP |
| Python | `mcp` 2.x | `mcp==2.3.0` | `MCPServer(...).run()`, dual-era by default |
| Go | go-sdk v1.7.0 | `github.com/modelcontextprotocol/go-sdk@v1.8.0` | `mcp.NewServer` + `server.Run(ctx, &mcp.StdioTransport{})` |

TypeScript v1 (`@modelcontextprotocol/sdk`) never speaks 2026-07-28. Run the official codemod at the package root,
then fix what it marks:

```bash
npx @modelcontextprotocol/codemod@2.3.1 v1-to-v2 .
grep -rn '@mcp-codemod-error' . --exclude-dir=node_modules
npx tsc --noEmit
```

On TypeScript v2 a server wired with `server.connect(new StdioServerTransport())` still speaks only the 2025 era.
Switching to `serveStdio(buildServer)` is the opt-in. `references/server.ts` is a complete example.

## 3. Rewrite the removed patterns

- **Session state** becomes explicit: return a handle (an id the server minted) and take it back as a tool
  argument. Inside one multi-round-trip call, carry state in `requestState`, and sign it (the TypeScript SDK's
  `createRequestStateCodec`), because it comes back from the client.
- **elicitInput / createMessage / listRoots** inside a tool become an `input_required` result. The handler returns
  `inputRequired({ inputRequests: { key: inputRequired.elicit({...}) } })`, the client retries the same call with
  `inputResponses`, and the handler runs again and reads the answer (`acceptedContent(...)` in TypeScript).
  Treat `inputResponses` as untrusted and validate them.
- **Logging**: write to stderr. `logging/setLevel` is gone; a modern request may carry
  `io.modelcontextprotocol/logLevel`, and when it does not, protocol log notifications are not sent.
- **Subscriptions**: `resources/subscribe` becomes `subscriptions/listen`.
- **Errors**: unknown resource is `-32602`. Unsupported version is `-32022`, and the SDK sends it.
- **Ordering**: register tools in a fixed order. `tools/list` must be deterministic.

Keep 2025-era clients working unless the user says otherwise. All three SDKs serve both eras from one server by
default. Modern-only is an opt-in (`{ legacy: 'reject' }` in TypeScript).

## 4. Prove it on the wire

Build, then probe the real server command over stdio:

```bash
node scripts/probe_mcp_2026.mjs -- node dist/server.js
node scripts/probe_mcp_2026.mjs --call delete_note '{"id":"n1"}' \
  --answer '{"confirm":{"action":"accept","content":{"confirm":true}}}' -- node dist/server.js
```

The probe sends `server/discover` and stateless `tools/list` calls with the `_meta` envelope. It checks `resultType`,
`serverInfo`, stable tool order, `-32022` for an unknown version, and that stdout is pure JSON-RPC. It runs one
`input_required` round trip if asked, and reports whether a legacy `initialize` still works (dual-era) or not. `FAIL`
lines are MUST-level and the exit code is 1.

On an established stdio connection the official TypeScript and Go SDKs pin the era, so a request with an unknown
version mid-connection is served rather than rejected. The probe reports that as `WARN`. The Go SDK v1.8.0 also
answers an unknown-version opening `server/discover` with method-not-found, not `-32022`, and the probe fails it.
That is SDK behaviour: note it in the PR and check the go-sdk issue tracker. Do not hand-roll the wire format.

## Done means

- The scan is clean, or every remaining finding is explained in the PR.
- The SDK is pinned exactly and the lockfile is committed.
- The probe prints `RESULT: speaks 2026-07-28`.
- The existing tests pass, including legacy-client tests if the server stays dual-era.
Skill file skills/mcp-2026-migration/references/changes.md Every 2026-07-28 change that affects servers, with the fix. 48 lines
Download Raw
# MCP 2026-07-28: what changed for servers

Source: the official changelog at https://modelcontextprotocol.io/specification/2026-07-28/changelog and the
versioning, discover, multi-round-trip and stdio pages of the same spec. Check there before relying on a detail.

## Removed or replaced

| 2025-11-25 and earlier | 2026-07-28 | Server action |
| --- | --- | --- |
| `initialize` / `notifications/initialized` handshake | Every request carries `_meta` with `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` (clientInfo SHOULD) | Use the SDK's 2026 entry point; no handshake code |
| Version negotiated once | Per request; unsupported version gets `UnsupportedProtocolVersionError` `-32022` with `data.supported` | Let the SDK answer |
| (none) | `server/discover` returns `supportedVersions`, `capabilities`, `instructions`, serverInfo in result `_meta` | Servers MUST implement it (SDKs do) |
| `Mcp-Session-Id` and session state | No sessions | Handles in tool arguments; `requestState` within one call |
| Server-to-client `sampling/createMessage`, `elicitation/create`, `roots/list` | `InputRequiredResult` (`resultType: "input_required"`, `inputRequests`, optional `requestState`); client retries with `inputResponses` | Only on `tools/call`, `prompts/get`, `resources/read` |
| `ping` | Removed | Delete handlers |
| `logging/setLevel` | Per-request `io.modelcontextprotocol/logLevel`; logging deprecated | stderr or OpenTelemetry |
| `notifications/roots/list_changed` | Removed (roots deprecated) | Paths as tool arguments |
| `resources/subscribe`, `resources/unsubscribe`, HTTP GET stream | `subscriptions/listen` | Move listeners |
| SSE resumability (`Last-Event-ID`) | Removed | Drop event stores |
| Tasks in core | `io.modelcontextprotocol/tasks` extension | Advertise under `capabilities.extensions` |
| `notifications/elicitation/complete` | Removed | Delete |

## Changed

- Every result has `resultType`: `"complete"` or `"input_required"`. Clients treat a missing field as complete.
- `tools/list` (and other lists) must come back in a deterministic order.
- List and read results may carry `ttlMs` and `cacheScope` cache hints.
- Resource not found: `-32602` (was `-32002`).
- New error codes: HeaderMismatch `-32020`, MissingRequiredClientCapability `-32021`, UnsupportedProtocolVersion
  `-32022`.
- Streamable HTTP: `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name` headers; servers validate that they match
  the body.
- `capabilities.extensions` advertises optional extensions by prefixed identifier.
- Auth: clients validate `iss` (RFC 9207); Client ID Metadata Documents are preferred over dynamic client
  registration, which is deprecated.

## Deprecated (still on the wire for now)

Roots, sampling, protocol logging, the HTTP+SSE transport, `includeContext` values `thisServer`/`allServers`, and
dynamic client registration.

## Backward compatibility

A dual-era server answers `initialize` with legacy semantics for that stdio process or HTTP session, and serves
requests that carry modern `_meta` statelessly. A modern-only server SHOULD name its supported versions in the error
it returns to `initialize`, because legacy clients have no other diagnostic. On stdio, dual-era clients probe with
`server/discover` first and fall back on any error that is not a recognised modern error.
Skill file skills/mcp-2026-migration/references/server.ts TypeScript v2 dual-era stdio server with an input_required tool. 79 lines
Download Raw
// A stdio MCP server on the TypeScript SDK v2 that speaks protocol 2026-07-28
// and still serves 2025-era clients from the same factory.
// Before (v1): new McpServer(...) + server.connect(new StdioServerTransport())
// After  (v2): serveStdio(() => buildServer())
import type { CallToolResult, InputRequiredResult } from "@modelcontextprotocol/server";
import { acceptedContent, inputRequired, McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

const GLOSSARY: Record<string, { definition: string; related: string[] }> = {
  loadout: { definition: "A bundle of agent instructions, skills and config for one kind of project.", related: ["skill", "fixture"] },
  skill: { definition: "A folder with a SKILL.md that an agent loads when the task matches.", related: ["loadout"] },
  fixture: { definition: "A small sample project the verifier runs a loadout against.", related: ["loadout"] },
};

// Notes live in the process for the demo. No state is keyed on a session:
// 2026-07-28 has no sessions, so anything a later call needs is passed back in
// the tool's own arguments (here, the note id).
const notes = new Map<string, string>([["n1", "Rotate the staging API key"], ["n2", "Book the retro"]]);

const confirmSchema = z.object({ confirm: z.boolean().meta({ title: "Delete this note?" }) });

function buildServer(): McpServer {
  const server = new McpServer(
    { name: "notes", version: "1.0.0" },
    { capabilities: { tools: {} }, instructions: "Glossary lookups and a small notes list." },
  );

  // Tools are registered in a fixed order; tools/list order must be deterministic.
  server.registerTool(
    "lookup_term",
    {
      description: "Look up a glossary term. Returns the definition and related terms. Read-only.",
      inputSchema: z.object({ term: z.string().min(1).max(64).describe("Glossary term, case-insensitive") }),
      annotations: { readOnlyHint: true },
    },
    async ({ term }): Promise<CallToolResult> => {
      const entry = GLOSSARY[term.toLowerCase()];
      if (!entry) {
        return { isError: true, content: [{ type: "text", text: `Unknown term "${term}". Known: ${Object.keys(GLOSSARY).join(", ")}` }] };
      }
      return { content: [{ type: "text", text: JSON.stringify({ term, ...entry }) }] };
    },
  );

  // v1 called ctx.mcpReq.elicitInput() here (a server-to-client request). That
  // channel is gone in 2026-07-28: return input_required and let the client
  // retry the same call with the answer (multi-round-trip requests).
  server.registerTool(
    "delete_note",
    {
      description: "Delete a note by id after the user confirms. Destructive.",
      inputSchema: z.object({ id: z.string().min(1).max(16).describe("Note id, for example n1") }),
      annotations: { destructiveHint: true },
    },
    async ({ id }, ctx): Promise<CallToolResult | InputRequiredResult> => {
      const text = notes.get(id);
      if (text === undefined) {
        return { isError: true, content: [{ type: "text", text: `No note "${id}". Ids: ${[...notes.keys()].join(", ")}` }] };
      }
      const answer = acceptedContent(ctx.mcpReq.inputResponses, "confirm", confirmSchema);
      if (answer === undefined) {
        return inputRequired({
          inputRequests: { confirm: inputRequired.elicit({ message: `Delete "${text}"?`, requestedSchema: confirmSchema }) },
        });
      }
      if (!answer.confirm) return { content: [{ type: "text", text: `Kept ${id}.` }] };
      notes.delete(id);
      return { content: [{ type: "text", text: `Deleted ${id}.` }] };
    },
  );

  return server;
}

const handle = serveStdio(buildServer);
console.error("notes MCP server on stdio (2026-07-28 and 2025-era clients)");
process.on("SIGINT", () => void handle.close());
Skill file skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs Dependency-free stdio probe for 2026-07-28 behaviour. 169 lines
Download Raw
#!/usr/bin/env node
// Probe a stdio MCP server for protocol revision 2026-07-28 behaviour.
// Usage: node probe_mcp_2026.mjs [--call <tool> '<json args>' [--answer '<inputResponses json>']] -- <command> [args...]
// --answer: if the call returns input_required, retry once with these inputResponses
// (the probe declares the elicitation capability on the call so the server may ask).
// No dependencies. Exit 1 if a MUST-level check fails.
import { spawn } from "node:child_process";

const MODERN = "2026-07-28";
const argv = process.argv.slice(2);
const sep = argv.indexOf("--");
if (sep === -1 || sep === argv.length - 1) {
  console.error("usage: probe_mcp_2026.mjs [--call <tool> '<json>'] -- <command> [args...]");
  process.exit(2);
}
const opts = argv.slice(0, sep);
const cmd = argv.slice(sep + 1);
let call = null;
const ci = opts.indexOf("--call");
if (ci !== -1) call = { name: opts[ci + 1], args: JSON.parse(opts[ci + 2] ?? "{}") };
const ai = opts.indexOf("--answer");
const answer = ai !== -1 ? JSON.parse(opts[ai + 1]) : null;

const envelope = (version = MODERN, caps = {}) => ({
  "io.modelcontextprotocol/protocolVersion": version,
  "io.modelcontextprotocol/clientInfo": { name: "probe-mcp-2026", version: "1.0.0" },
  "io.modelcontextprotocol/clientCapabilities": caps,
});

let failures = 0;
const pass = (m) => console.log(`PASS ${m}`);
const fail = (m) => { failures++; console.log(`FAIL ${m}`); };
const warn = (m) => console.log(`WARN ${m}`);
const info = (m) => console.log(`INFO ${m}`);

function startServer() {
  const child = spawn(cmd[0], cmd.slice(1), { stdio: ["pipe", "pipe", "pipe"] });
  const pending = new Map();
  const nonJson = [];
  let buf = "";
  child.stdout.setEncoding("utf8");
  child.stdout.on("data", (chunk) => {
    buf += chunk;
    let i;
    while ((i = buf.indexOf("\n")) !== -1) {
      const line = buf.slice(0, i).trim();
      buf = buf.slice(i + 1);
      if (!line) continue;
      let msg;
      try { msg = JSON.parse(line); } catch { nonJson.push(line); continue; }
      if (msg.id !== undefined && pending.has(msg.id)) {
        pending.get(msg.id)(msg);
        pending.delete(msg.id);
      }
    }
  });
  child.stderr.resume();
  let next = 1;
  const request = (method, params, timeoutMs = 15000) =>
    new Promise((resolve) => {
      const id = next++;
      const timer = setTimeout(() => { pending.delete(id); resolve({ timeout: true }); }, timeoutMs);
      pending.set(id, (msg) => { clearTimeout(timer); resolve(msg); });
      child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
    });
  const stop = () => { child.stdin.end(); setTimeout(() => child.kill(), 500).unref(); };
  return { request, stop, nonJson };
}

function checkMismatch(label, msg, onMiss = fail) {
  if (msg.error?.code === -32022 && Array.isArray(msg.error.data?.supported))
    pass(`${label} -> -32022, supported ${JSON.stringify(msg.error.data.supported)}`);
  else {
    const got = msg.timeout ? "no answer" : JSON.stringify(msg.error ?? { result: Object.keys(msg.result ?? {}) });
    onMiss(`${label}: expected error -32022 with data.supported, got ${got.slice(0, 160)}`);
  }
}

const names = (msg) => (msg.result?.tools ?? []).map((t) => t.name);

async function main() {
  const s = startServer();

  // 1. server/discover (servers MUST implement it)
  const d = await s.request("server/discover", { _meta: envelope() });
  if (d.timeout) fail("server/discover: no answer (a legacy-only server, or it waits for initialize)");
  else if (d.error) fail(`server/discover: error ${d.error.code} ${d.error.message}`);
  else {
    const r = d.result;
    if (r.resultType === "complete") pass("server/discover: resultType complete");
    else fail(`server/discover: resultType is ${JSON.stringify(r.resultType)}, expected "complete"`);
    const versions = r.supportedVersions ?? [];
    if (versions.includes(MODERN)) pass(`server/discover: supportedVersions ${JSON.stringify(versions)}`);
    else fail(`server/discover: supportedVersions ${JSON.stringify(versions)} does not include ${MODERN}`);
    const si = r._meta?.["io.modelcontextprotocol/serverInfo"];
    if (si?.name) pass(`server/discover: serverInfo ${si.name} ${si.version ?? ""}`.trim());
    else warn("server/discover: no io.modelcontextprotocol/serverInfo in result _meta (SHOULD)");
    info(`capabilities: ${Object.keys(r.capabilities ?? {}).join(", ") || "none"}`);
  }

  // 2. stateless tools/list, twice: no handshake, deterministic order
  const l1 = await s.request("tools/list", { _meta: envelope() });
  const l2 = await s.request("tools/list", { _meta: envelope() });
  if (l1.timeout || l1.error) fail(`tools/list with _meta and no initialize: ${l1.error ? l1.error.message : "no answer"}`);
  else {
    if (l1.result.resultType === "complete") pass(`tools/list stateless: ${names(l1).join(", ") || "(no tools)"}`);
    else fail(`tools/list: resultType is ${JSON.stringify(l1.result.resultType)}, expected "complete"`);
    if (JSON.stringify(names(l1)) === JSON.stringify(names(l2))) pass("tools/list order is stable across calls");
    else fail("tools/list order changed between two identical calls (MUST be deterministic)");
    if ("ttlMs" in l1.result || "cacheScope" in l1.result) info(`cache hints: ttlMs=${l1.result.ttlMs} cacheScope=${l1.result.cacheScope}`);
    else info("no ttlMs/cacheScope cache hints on tools/list (optional)");
  }

  // 3. optional tools/call, with one multi-round-trip retry if --answer is given
  if (call) {
    const caps = { elicitation: {} };
    const params = { name: call.name, arguments: call.args, _meta: envelope(MODERN, caps) };
    let c = await s.request("tools/call", params);
    if (!c.timeout && !c.error && c.result.resultType === "input_required") {
      const keys = Object.keys(c.result.inputRequests ?? {});
      pass(`tools/call ${call.name}: input_required asks for ${keys.join(", ") || "(requestState only)"}`);
      if (answer) {
        const retry = { ...params, inputResponses: answer, _meta: envelope(MODERN, caps) };
        if (c.result.requestState !== undefined) retry.requestState = c.result.requestState;
        c = await s.request("tools/call", retry);
      }
    }
    if (c.timeout || c.error) fail(`tools/call ${call.name}: ${c.error ? c.error.message : "no answer"}`);
    else if (c.result.resultType === "input_required") info(`tools/call ${call.name}: still input_required`);
    else if (c.result.resultType === "complete" && !c.result.isError)
      pass(`tools/call ${call.name}: complete: ${(c.result.content?.[0]?.text ?? "").slice(0, 80)}`);
    else fail(`tools/call ${call.name}: resultType ${c.result.resultType} isError ${c.result.isError}`);
  }

  // 4. unknown version on an established connection. The spec says every request
  // carries its version and an unsupported one MUST get -32022, but the official
  // TypeScript and Go SDKs pin the era per stdio connection, so this is a warning.
  const bad = await s.request("tools/list", { _meta: envelope("1900-01-01") });
  checkMismatch("unknown version mid-connection", bad, warn);
  s.stop();
  if (s.nonJson.length) fail(`stdout carried ${s.nonJson.length} non-JSON line(s), first: ${s.nonJson[0].slice(0, 80)}`);
  else pass("stdout carried only JSON-RPC");

  // 5. legacy initialize in a fresh process: dual-era or modern-only?
  const l = startServer();
  const init = await l.request("initialize", {
    protocolVersion: "2025-11-25",
    capabilities: {},
    clientInfo: { name: "probe-mcp-2026-legacy", version: "1.0.0" },
  }, 8000);
  l.stop();
  if (init.result?.protocolVersion) info(`era: dual-era (legacy initialize negotiated ${init.result.protocolVersion})`);
  else if (init.error) {
    info("era: modern-only (legacy initialize rejected)");
    if (!/20\d\d-\d\d-\d\d/.test(init.error.message + JSON.stringify(init.error.data ?? ""))) warn("initialize error does not name the supported versions (SHOULD)");
  } else warn("legacy initialize got no answer");

  // 6. unknown version on the opening probe of a fresh process MUST also be -32022
  const f = startServer();
  const fresh = await f.request("server/discover", { _meta: envelope("1900-01-01") }, 8000);
  f.stop();
  checkMismatch("unknown version on the opening server/discover", fresh);

  console.log(failures ? `RESULT: ${failures} failure(s)` : `RESULT: speaks ${MODERN}`);
  process.exit(failures ? 1 : 0);
}

main();
Skill file skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs Finds code using removed or changed protocol features. 94 lines
Download Raw
#!/usr/bin/env node
// Find MCP server code that protocol revision 2026-07-28 removed or changed.
// Usage: node scan_mcp_legacy.mjs [dir ...]   (default: .)
// Prints file:line, what changed and what to do. Exit 1 if anything is found.
// Patterns are plain strings joined at runtime so this file does not flag itself.
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, extname, basename } from "node:path";

const j = (...parts) => parts.join("");
const RULES = [
  { re: new RegExp(j("Mcp-Session", "-Id|session", "IdGenerator|\\bsession", "Id\\b"), "i"),
    what: "Sessions are gone; there is no Mcp-Session-Id.",
    fix: "Pass state the next call needs in tool arguments (a server-minted handle) or, inside one multi-round-trip call, in an integrity-protected requestState." },
  { re: new RegExp(j("[\"']notifications/", "initialized[\"']|on", "initialized\\b|[\"']initi", "alize[\"']")),
    what: "The initialize/initialized handshake is removed; every request carries _meta with protocolVersion and clientCapabilities.",
    fix: "Let the SDK's 2026 entry point (serveStdio / createMcpHandler, MCPServer v2, go-sdk >= v1.7.0) handle eras. Do not hand-roll handshake handlers." },
  { re: new RegExp(j("[\"']pi", "ng[\"']")), what: "ping is removed.", fix: "Delete ping handlers and keep-alive pings." },
  { re: new RegExp(j("logging/set", "Level|set", "LoggingLevel|send", "LoggingMessage")),
    what: "logging/setLevel is removed and protocol logging is deprecated.",
    fix: "Log to stderr (stdio) or OpenTelemetry. If you keep protocol logs, read the per-request io.modelcontextprotocol/logLevel." },
  { re: new RegExp(j("sampling/create", "Message|\\.create", "Message\\(|request", "Sampling\\(")),
    what: "Server-to-client sampling requests are gone (sampling is deprecated).",
    fix: "Return input_required with a sampling/createMessage entry in inputRequests, or drop the dependency on client-side LLM calls." },
  { re: new RegExp(j("elicit", "Input\\(|(?<!input", "Required)\\.elicit\\(")),
    what: "Server-pushed elicitation is gone.",
    fix: "Return input_required with an elicitation/create entry in inputRequests and read inputResponses on the retry." },
  { re: new RegExp(j("roots/li", "st|list", "Roots\\(|notifications/roots/list_", "changed")),
    what: "Roots are deprecated and roots/list_changed is removed.",
    fix: "Take the paths a tool needs as tool arguments, checked against an allowed root." },
  { re: new RegExp(j("resources/sub", "scribe|resources/unsub", "scribe")),
    what: "resources/subscribe and resources/unsubscribe are replaced.",
    fix: "Use subscriptions/listen (the long-lived stream that also replaced the HTTP GET endpoint)." },
  { re: new RegExp(j("Last-Event", "-ID|event", "Store\\b|Event", "Store\\b"), "i"),
    what: "SSE resumability (Last-Event-ID, event stores) is removed.",
    fix: "Drop the event store; long work belongs in the tasks extension or a handle the client can poll." },
  { re: new RegExp(j("-3200", "2\\b")),
    what: "Resource not found is now -32602 (invalid params), not -32002.",
    fix: "Return -32602 for an unknown resource URI." },
  { re: new RegExp(j("SSEServer", "Transport|sse_", "app\\(")),
    what: "The HTTP+SSE transport is deprecated.",
    fix: "Serve Streamable HTTP (createMcpHandler in TypeScript v2)." },
  { re: new RegExp(j("Stdio", "ServerTransport\\(")),
    what: "A v2 TypeScript McpServer connected straight to StdioServerTransport only speaks 2025-era protocol.",
    fix: "Replace server.connect(new StdioServerTransport()) with serveStdio(() => buildServer())." },
  { re: new RegExp(j("@modelcontext", "protocol/sdk\\b")),
    what: "@modelcontextprotocol/sdk is the v1 TypeScript SDK; v1 never speaks 2026-07-28.",
    fix: "Run npx @modelcontextprotocol/codemod@2.3.1 v1-to-v2 . then pin @modelcontextprotocol/server 2.3.1." },
  { re: new RegExp(j("mcp\\.server\\.fast", "mcp|\\bmcp\\s*(==|~=|<)\\s*1\\.")),
    what: "MCP Python SDK v1 (FastMCP) does not speak 2026-07-28.",
    fix: "Pin mcp==2.3.0 and use from mcp.server import MCPServer." },
  { re: new RegExp(j("modelcontextprotocol/go-", "sdk v(0\\.|1\\.[0-6]\\.)")),
    what: "go-sdk before v1.7.0 does not speak 2026-07-28.",
    fix: "go get github.com/modelcontextprotocol/go-sdk@v1.8.0" },
  { re: new RegExp(j("[\"']tasks/(get|list|cancel|res", "ult)[\"']")),
    what: "Tasks moved out of core into the io.modelcontextprotocol/tasks extension.",
    fix: "Advertise the extension under capabilities.extensions and follow its spec, or drop task support." },
];

const EXT = new Set([".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".py", ".go", ".rs", ".json", ".toml", ".txt", ".mod"]);
const SKIP = new Set(["node_modules", ".git", "dist", "build", ".venv", "venv", "__pycache__", "vendor", "target"]);
const self = basename(new URL(import.meta.url).pathname);
const lockfiles = new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "uv.lock", "go.sum", "Cargo.lock"]);

function* walk(p) {
  const st = statSync(p);
  if (st.isFile()) { yield p; return; }
  for (const name of readdirSync(p)) {
    if (SKIP.has(name)) continue;
    yield* walk(join(p, name));
  }
}

let hits = 0;
const roots = process.argv.slice(2).length ? process.argv.slice(2) : ["."];
for (const root of roots) {
  for (const file of walk(root)) {
    const name = basename(file);
    if (name === self || lockfiles.has(name)) continue;
    if (!EXT.has(extname(file)) && name !== "requirements.txt") continue;
    const lines = readFileSync(file, "utf8").split("\n");
    lines.forEach((line, i) => {
      if (/^\s*(\/\/|#|\*|\/\*)/.test(line)) return; // comments describe the old way; skip them
      for (const r of RULES) {
        if (r.re.test(line)) {
          hits++;
          console.log(`${file}:${i + 1}: ${r.what}\n    fix: ${r.fix}`);
        }
      }
    });
  }
}
console.log(hits ? `${hits} finding(s)` : "no 2025-era MCP patterns found");
process.exit(hits ? 1 : 0);

All files: source zip · Licence MIT · By Agent Loadout · First listed

Installing and testing

How do I install Migrate an MCP server to protocol 2026-07-28 in Claude Code?

From your project root, put AGENTS.md, CLAUDE.md, skills/mcp-2026-migration/SKILL.md at .claude/skills/mcp-2026-migration/SKILL.md, skills/mcp-2026-migration/references/changes.md at .claude/skills/mcp-2026-migration/references/changes.md, skills/mcp-2026-migration/references/server.ts at .claude/skills/mcp-2026-migration/references/server.ts, skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs at .claude/skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs, skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs at .claude/skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs. The "Claude Code" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/mcp-2026-07-28-migration/claude-code.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Migrate an MCP server to protocol 2026-07-28 in Codex CLI?

From your project root, put AGENTS.md, skills/mcp-2026-migration/SKILL.md at .agents/skills/mcp-2026-migration/SKILL.md, skills/mcp-2026-migration/references/changes.md at .agents/skills/mcp-2026-migration/references/changes.md, skills/mcp-2026-migration/references/server.ts at .agents/skills/mcp-2026-migration/references/server.ts, skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs at .agents/skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs, skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs at .agents/skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs. The "Codex CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/mcp-2026-07-28-migration/codex.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Migrate an MCP server to protocol 2026-07-28 in Cursor?

From your project root, put AGENTS.md, skills/mcp-2026-migration/SKILL.md at .agents/skills/mcp-2026-migration/SKILL.md, skills/mcp-2026-migration/references/changes.md at .agents/skills/mcp-2026-migration/references/changes.md, skills/mcp-2026-migration/references/server.ts at .agents/skills/mcp-2026-migration/references/server.ts, skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs at .agents/skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs, skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs at .agents/skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs. The "Cursor" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/mcp-2026-07-28-migration/cursor.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Migrate an MCP server to protocol 2026-07-28 in Gemini CLI?

From your project root, put AGENTS.md, GEMINI.md, skills/mcp-2026-migration/SKILL.md at .agents/skills/mcp-2026-migration/SKILL.md, skills/mcp-2026-migration/references/changes.md at .agents/skills/mcp-2026-migration/references/changes.md, skills/mcp-2026-migration/references/server.ts at .agents/skills/mcp-2026-migration/references/server.ts, skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs at .agents/skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs, skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs at .agents/skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs. The "Gemini CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/mcp-2026-07-28-migration/gemini-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Migrate an MCP server to protocol 2026-07-28 in Antigravity CLI?

From your project root, put AGENTS.md, skills/mcp-2026-migration/SKILL.md at .agents/skills/mcp-2026-migration/SKILL.md, skills/mcp-2026-migration/references/changes.md at .agents/skills/mcp-2026-migration/references/changes.md, skills/mcp-2026-migration/references/server.ts at .agents/skills/mcp-2026-migration/references/server.ts, skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs at .agents/skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs, skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs at .agents/skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs. The "Antigravity CLI" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/mcp-2026-07-28-migration/antigravity-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Migrate an MCP server to protocol 2026-07-28 in GitHub Copilot?

From your project root, put AGENTS.md, skills/mcp-2026-migration/SKILL.md at .agents/skills/mcp-2026-migration/SKILL.md, skills/mcp-2026-migration/references/changes.md at .agents/skills/mcp-2026-migration/references/changes.md, skills/mcp-2026-migration/references/server.ts at .agents/skills/mcp-2026-migration/references/server.ts, skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs at .agents/skills/mcp-2026-migration/scripts/probe_mcp_2026.mjs, skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs at .agents/skills/mcp-2026-migration/scripts/scan_mcp_legacy.mjs. The "GitHub Copilot" tab on this page has the curl commands, or download https://loadout.jnw-it.com/download/mcp-2026-07-28-migration/copilot.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

Has Migrate an MCP server to protocol 2026-07-28 been tested?

It passed the Level 1 automated install checks on 2026-10-17: the files parse, install to each agent's documented paths, and pass the safety scan. No AI agent has run it yet, so no agent pass or fail is claimed.

More for Claude Code, Codex, Cursor, Gemini CLI, Antigravity CLI and GitHub Copilot.