Hero Background

Power Your Software Testing with AI Agents and Cloud

The Native AI-Agentic Cloud Platform to Supercharge Quality Engineering. Test Intelligently and Ship Faster.

AIMCPRegression Testing

Stateless MCP Migration: How to Regression Test Your Server

Stateless MCP drops the initialize handshake and sessions. Baseline your server on the old spec, migrate, then diff tools, schemas, errors and agent behavior.

Published on:

You upgrade the SDK, opt your server into stateless MCP with the 2026-07-28 revision, and point MCP Inspector at it. The connection goes green, tools/list returns every tool, and the migration looks done.

Then a client pinned to 2026-07-28 calls a tool that asks the user to confirm an action, and the call errors. Legacy is the Inspector's default era, per its protocol eras docs, so it had connected with a plain initialize and the green check only exercised the old path your server still serves. On a 2026-07-28 connection, a server that calls server.elicitInput errors, because server-to-client requests are not allowed there.

The 2026-07-28 specification release post, published on July 28, 2026, says all four Tier 1 SDKs (TypeScript, Python, Go and C#) speak the new revision.

Treat the move like any other regression testing job: freeze the legacy output, change one layer at a time, and finish with the agents that call your server. TestMu AI's Agent Assurance fits that last step, since it tests agents rather than the wire protocol.

Overview

Stateless MCP removes the initialize handshake and sessions from the Model Context Protocol: in the 2026-07-28 revision, every request carries its own protocol version and client capabilities in _meta. To regression test an MCP server across the change, capture a legacy baseline, migrate one layer at a time, then diff normalized output against that baseline.

Five Regression Checks for a Stateless MCP Migration

  • Legacy baseline: Before any dependency moves in a stateless MCP migration, capture the full tools/list, one passing and one failing golden tools/call per tool, and the error envelopes, with MCP Inspector pinned to the legacy era, because a v2 software development kit can change the legacy wire shape too.
  • Two-era matrix: Run every scenario against the migrated MCP server once per protocol era, legacy and 2026-07-28, and read back which era each connection negotiated. MCP Inspector defaults to the legacy era, so a matrix that never sets protocolEra runs one cell and reports green.
  • Removed-feature tests: A legacy MCP baseline never calls the replacement paths, so each feature the 2026-07-28 revision removed needs a named test: replay a legacy Mcp-Session-Id on a 2026-07-28 request and confirm the server ignores it, drive a Multi Round-Trip Requests exchange to completion, and expect a rejection when one byte of requestState changes.
  • Normalized diff: Before diffing 2026-07-28 output against the legacy MCP baseline, normalize era-only fields: treat a missing resultType as complete, drop ttlMs, cacheScope and the self-reported serverInfo, and sort tools/list by tool name. In the 2026-07-28 spec, resource not found moves from -32002 to -32602, but v1 software development kits differed, so re-baseline that error once.
  • Agent-level regressions: An MCP server can pass every conformance scenario and still change what an agent does. Agent Assurance by TestMu AI reruns its own scenarios, derived once from the agent's code, against the agent that calls the migrated server, and in Rook CLI 0.1.5 its discovery asks each approved stdio MCP server what tools it really has.

What Does Stateless MCP Mean?

Stateless MCP means every request stands on its own. In the 2026-07-28 revision, each request carries its protocol version and client capabilities in _meta, the initialize handshake is gone, and servers MUST implement server/discover to advertise their supported versions, capabilities and identity, according to the 2026-07-28 changelog.

Spec changeWhat breaksRegression test
initialize removed: version and capabilities travel in _meta on every requestA legacy client against a modern-only server fails: an initialize error on stdio, 400 Bad Request on HTTPConnect a legacy client to the migrated build and expect it to be served, or told which versions work
Sessions removed: no Mcp-Session-Id; cross-call state moves to server-minted handles passed as tool argumentsCode that kept per-session state under the headerReplay a captured session header and confirm no state rides on it
server/discover required: returns supported versions, capabilities and server identityA client in auto mode probes it first, so a missing handler breaks era detectionCall server/discover and assert that 2026-07-28 is listed
ping and logging/setLevel removed: log level is set per request in _metaHealth checks that ping, and log pipelines that relied on setLevelCall a tool without the logLevel key and assert that no log notification arrives
Multi Round-Trip Requests: a tool returns resultType input_required and the client retriesTools that sent roots/list, sampling/createMessage or elicitation/create mid-requestDrive a full input_required round trip, then tamper with requestState
New result fields: resultType on every result, ttlMs and cacheScope on list and read resultsSnapshot diffs against legacy outputNormalize those fields before diffing
Error codes: resource not found moves from -32002 to -32602Assertions written against the old codeRe-baseline the error envelopes
subscriptions/listen: replaces the HTTP GET stream and resources/subscribe; Last-Event-ID resumability is goneResource watchers and stream reconnect logicSubscribe through subscriptions/listen, then break a stream mid-request
Tasks extension: tasks move to io.modelcontextprotocol/tasks; tasks/result and tasks/list are removedClients that block on tasks/resultPoll tasks/get to completion
HTTP headers: Streamable HTTP POST requests must carry Mcp-Method, plus Mcp-Name on tools/call, resources/read and prompts/getGateways and proxies that drop headers they do not knowSend a tools/call through your gateway and check that both headers arrive
Authorization: iss validation, application_type at registration, credentials keyed by issuerClient authorization flowsThe authorization checks later in this guide
Deprecations: Roots, Sampling, Logging and Dynamic Client Registration are deprecated but still workNothing yetKeep legacy-path tests until you migrate off them

The discover result can also carry an optional instructions field, which the spec describes as natural-language guidance for LLMs on how to use the server. That text is written for the model, so give it a security review of its own, separate from this migration.

Capture a Baseline on the Legacy Spec

Record what your server does today before any dependency moves, because a v2 SDK can change the legacy wire shape too: a TypeScript v2 server advertises differently shaped inputSchema entries in tools/list, the Python v2 client adds a _meta envelope to every outbound request, and a Python v2 server answers unknown methods and missing resources with new error codes. Run every capture with the MCP Inspector pinned to the legacy era, so the snapshot matches what 2025-11-25 clients see.

Give the same server one entry per era in a config file. The catalog format in the Inspector configuration docs takes a protocolEra field on each server, and it defaults to legacy.

{
  "mcpServers": {
    "orders-legacy": {
      "command": "node",
      "args": ["build/index.js"],
      "protocolEra": "legacy"
    },
    "orders-auto": {
      "command": "node",
      "args": ["build/index.js"],
      "protocolEra": "auto"
    },
    "orders-modern": {
      "command": "node",
      "args": ["build/index.js"],
      "protocolEra": "modern"
    }
  }
}

Capture these on the legacy entry before you change anything:

  • tools/list - the full list, with every inputSchema and outputSchema.
  • Golden tool calls - one passing and one failing tools/call per tool, saved exactly as returned.
  • Error envelopes - an unknown tool, invalid arguments and a missing resource, with the JSON-RPC code each one returns.
  • Resources and prompts - resources/list, resources/read and prompts/list, if your server offers them.
  • Stateful flows - any sequence where one call depends on an earlier one, since that is where session removal bites.

The MCP Inspector CLI reference documents every flag below. With --format json it writes a single JSON object to stdout with no banners, so each capture is a file you can diff.

set -euo pipefail
mkdir -p baseline

npx @modelcontextprotocol/inspector@2.8.0 --cli --config ./mcp.json --server orders-legacy \
  --method tools/list --format json > baseline/tools-list.legacy.json

# One golden call per tool; get_order and its argument stand in for your own tools
npx @modelcontextprotocol/inspector@2.8.0 --cli --config ./mcp.json --server orders-legacy \
  --method tools/call --tool-name get_order --tool-args-json '{"order_id":"A-1001"}' \
  --format json > baseline/get_order.legacy.json

Failing calls need their own handling. The MCP Inspector CLI reference maps each outcome to an Inspector exit code:

  • Exit 0 - success.
  • Exit 1 - a usage or unexpected error.
  • Exit 3 - the server requires authentication.
  • Exit 4 - the server is unreachable.
  • Exit 5 - a tool error, so a tools/call that returns isError: true still prints its payload while the Inspector CLI exits 5.

Every non-zero exit also writes one JSON line to stderr, so end these calls with || true and keep both streams:

# A failing call; the order ID stands in for one your server rejects
npx @modelcontextprotocol/inspector@2.8.0 --cli --config ./mcp.json --server orders-legacy \
  --method tools/call --tool-name get_order --tool-args-json '{"order_id":"does-not-exist"}' \
  --format json > baseline/get_order.fail.legacy.json 2> baseline/get_order.fail.legacy.err.json || true

# orders://missing stands in for a URI your server does not have
npx @modelcontextprotocol/inspector@2.8.0 --cli --config ./mcp.json --server orders-legacy \
  --method resources/read --uri orders://missing --format json \
  > baseline/missing-resource.legacy.json 2> baseline/missing-resource.legacy.err.json || true

The stderr line names the CLI's own failure class and carries the server's message, but not the JSON-RPC error code. Assert the codes in a test that uses your SDK client, where the TypeScript client's ProtocolError exposes the number as code, and diff the stderr files as they are.

Then freeze the conformance result at the legacy revision. The MCP conformance README describes --requirements as running exactly what a spec revision requires, frozen at its release, and lists 2025-11-25 and 2026-07-28. Its server examples target a URL, so run it against the Streamable HTTP build of your server.

npx @modelcontextprotocol/conformance@0.2.0-alpha.11 server \
  --url http://localhost:3000/mcp --requirements 2025-11-25 \
  --expected-failures ./conformance-baseline-2025.yml

List the scenarios that fail on your server today in the baseline file, in the format the README shows. The two entries below are the README's own examples, so replace them, because a listed scenario that passes makes the run exit 1:

server:
  - tools-call-with-progress
  - resources-subscribe

Keep the baseline trustworthy:

  • Verbatim arguments - use --tool-args-json for golden calls. It passes the argument object verbatim, while --tool-arg coerces each value by JSON-parsing it, so "012" becomes the number 12 and the baseline records a call you never meant to make.
  • Conformance version - on September 29, 2026, the npm latest tag pointed to 0.1.16, whose README has no --requirements flag, while the alpha tag pointed to 0.2.0-alpha.11, which has it. Pin the version you ran.
  • Inspector version - the commands in this guide pin MCP Inspector 2.8.0, the npm latest tag on September 29, 2026, so a later release cannot change the capture format under you. Recapture the baseline whenever you move a pin.
  • Storage - commit the baseline folder and the conformance baseline file next to the server code, so every later run diffs against the same pre-migration capture.

Upgrade the SDK Before the Protocol

Change one variable at a time. The maintainers of the official reference servers track their own migration in issue #4857 of the modelcontextprotocol/servers repository on GitHub, Tracker: 2026-07-28 Spec Refactor, opened on September 26, 2026, and they order the work in waves.

  • Wave 1 - "Tests come first so we can prove the supposedly transparent SDK migrations really are transparent." For your server, that is the legacy baseline from the previous section.
  • Wave 2 - "No wire behavior changes; the Wave 1 suites must pass with only import and type changes." Bump the SDK, keep serving 2025-11-25, and rerun the baseline.
  • Wave 3 - "Serve 2026-07-28 and legacy clients from the same server, and extend the test suites to cover the new spec." Opt in to the new revision and build the two-era matrix.

Neither SDK bump leaves the wire untouched. TypeScript v2 keeps the 2025-era protocol until you opt in, but it reshapes the tool schemas you advertise, and a Python v2 server picks up the new revision on upgrade. Both SDKs also have client-side catches:

  • TypeScript default - the TypeScript SDK 2026-07-28 support guide states that "Nothing in v2 puts a 2026-07-28 byte on the wire by default", so a hand-constructed Client, Server or McpServer keeps speaking the 2025-era protocol until you opt in.
  • TypeScript schemas - the TypeScript upgrade-to-v2 guide warns that "Your advertised tool schemas change shape on the wire": the same registerTool calls produce tools/list inputSchema entries that differ from v1 (JSON Schema 2020-12 idioms, different additionalProperties handling, no execution.taskSupport member), so re-baseline golden tools/list snapshots after the bump.
  • Python server - the Python SDK's Serving legacy clients page says "Both eras are always on", with no option to reject or disable an era, so a v2 server answers 2026-07-28 requests alongside initialize from the moment you bump. For a Python server, run the modern matrix cells in wave 2, not wave 3.
  • Python default - the Python SDK v2 migration guide says the v2 Client defaults to mode='auto', which probes server/discover and lands on 2026-07-28 against a v2 server. A v1 test that drove ctx.elicit(), create_message() or list_roots() through the in-process client now fails with NoBackChannelError, even with the callbacks set.
  • Python renames and codes - FastMCP becomes MCPServer, unknown request methods now return -32601 instead of -32602, and a missing resource returns -32602 where v1 returned code 0 with no data, so some error assertions change in wave 2 with no protocol opt-in at all.
  • Python wire shape - mode="legacy" reproduces the initialize handshake, but the guide notes that the per-request wire shape still differs from v1, because every outbound request now carries a _meta envelope.

The Python guide shows both forms of the v2 client. Pin the legacy era in the tests you carry through wave 2:

from mcp.client import Client

# v2 default: mode='auto' negotiates 2026-07-28 against a v2 server,
# where server-initiated requests raise NoBackChannelError
async with Client(server) as client:
    result = await client.call_tool("my_tool", {"x": 1})

# A v1-style test that drives sampling, elicitation or roots stays on the legacy era
async with Client(server, mode="legacy", sampling_callback=..., elicitation_callback=..., list_roots_callback=...) as client:
    ...
  • Wave 2 tests - keep mode="legacy" on every existing test until the SDK bump is merged, so the suite still exercises the path your current clients use.
  • Wave 3 tests - add new tests with mode='2026-07-28'; the guide says that value pins a version without probing.
  • Accepted diffs - after the bump, rerun the baseline and accept only the diffs you can trace to the SDK migration guides, such as the TypeScript inputSchema reshaping and the Python unknown-method and missing-resource codes.

Build a Two-Era Test Matrix

Wave 3 opts a TypeScript server in; a Python v2 server is already dual-era. The TypeScript guide shows createMcpHandler as the HTTP entry point, and its default legacy: 'stateless' option serves 2026-07-28 and 2025 clients per request. For stdio, serveStdio also serves 2025 clients unless you pass legacy: 'reject'.

import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';

// Default legacy: 'stateless' serves 2026-07-28 and 2025 clients per request
const handler = createMcpHandler(() => {
  const server = new McpServer(
    { name: 'my-server', version: '1.0.0' },
    { capabilities: { tools: {} } }
  );
  return server;
});

Then run every scenario once per era. The spec page on versioning and compatibility gives the expected outcome for each client and server pairing, and these are the cells a migrating server needs:

ClientServerExpected outcome in the specHow to run the cell
LegacyYour dual-era buildWorks: the server answers initialize and serves the negotiated legacy revisionInspector protocolEra legacy, or Python mode="legacy"
ModernYour dual-era buildWorks (the spec's modern-server row, since a dual-era build serves the modern era): server/discover is optional, and a version mismatch returns UnsupportedProtocolVersionErrorInspector protocolEra modern, a TypeScript pin, or Python mode='2026-07-28'
Dual-eraYour pre-migration buildWorks: on stdio the probe returns a non-modern error or times out; on HTTP the modern request gets a 4xx without a modern error body. Either way the client falls back to initializeInspector protocolEra auto against the pre-migration build
LegacyA modern-only buildFails: stdio rejects initialize with a JSON-RPC error, and HTTP answers 400 Bad RequestOnly if you ship the TypeScript legacy: 'reject' option (a Python v2 server cannot reject an era); assert the error names the versions you support
ModernYour pre-migration buildFails: the server may reject, stay silent, or process an ambiguous method under legacy rulesA pinned client must fail loudly instead of half working

Run the matrix with these rules:

  • Set the era explicitly - the Inspector's legacy default is deliberate, because its docs say a debugging tool must not auto-probe. A matrix that never sets protocolEra runs one cell and reports green.
  • Budget for the probe - the Inspector docs warn that a server/discover probe stalls against silent legacy stdio servers, and the stdio transport spec treats no answer within a reasonable timeout as a legacy server, so the auto cell against the pre-migration build should pass once the client falls back to initialize. The CLI's --connect-timeout, in milliseconds, bounds that whole connection attempt, so set it longer than the probe timeout; a connect timeout on that cell means the fallback never finished.
  • Assert the era - read back which era was negotiated once the connection opens. The TypeScript client reports it through getProtocolEra().
  • Expect loud failures - a TypeScript client pinned to 2026-07-28 rejects with SdkError(EraNegotiationFailed) against your pre-migration build (the guide's 2025-only server), which is the failure the last cell expects.
  • Test in process - the TypeScript guide points a StreamableHTTPClientTransport at handler.fetch, so the 2026 path runs without opening a socket.
  • Legacy push features - the TypeScript guide says createMcpHandler's default legacy: 'stateless' builds a fresh instance per request with no return path for server-to-client requests, so a tool rewritten in the inputRequired form that needs elicitation, sampling or roots from a 2025 HTTP client gets a clean capability refusal, returned as an isError result. A sessionful v1 server keeps those requests working by routing legacy traffic with isLegacyRequest() to its existing handler in front of a legacy: 'reject' entry, so add a legacy-client elicitation test to the matrix.
  • Conformance at both revisions - the README says "Scenarios run at their revision's wire version", so --requirements 2025-11-25 exercises the initialize path and --requirements 2026-07-28 the stateless one, against the same build.

The TypeScript guide pins a modern client this way:

// Matrix cell: modern client, no fallback
const client = new Client(
  { name: 'my-client', version: '1.0.0' },
  { versionNegotiation: { mode: { pin: '2026-07-28' } } }
);
await client.connect(transport);
client.getProtocolEra(); // returns 'modern' | 'legacy'

Test Each Removed and Deprecated Feature

The baseline proves that what stayed still works, but nothing in it calls the replacement paths. Every removal in the changelog needs a named test of its own.

Sessions, Ping and Logging

  • Stale session replay - capture an Mcp-Session-Id from a legacy session, send it on a modern request, and confirm the modern path does not read it. The changelog moves cross-call state into server-minted handles passed as ordinary tool arguments.
  • Handle ownership - test that one user cannot use a handle another user created. The spec states that binding rule for MRTR state rather than for handles, but the replay risk is the same.
  • Silent logs - call a tool without io.modelcontextprotocol/logLevel in _meta and assert that no notifications/message arrives, since servers MUST NOT emit one for a request that did not opt in. Then set the key and assert the logs ride that request's response stream. If you drive this test with MCP Inspector, add "modernLogLevel": "off" to the modern entry first, because the Inspector stamps debug on every modern request by default.
  • Health checks - ping and logging/setLevel are gone in 2026-07-28, and the Python guide deprecates Client.send_ping() for that reason. Move liveness probes to a method both eras serve.

Multi Round-Trip Requests

MRTR replaces server-initiated requests. The spec's Multi Round-Trip Requests page says the server processing a retry does not need any information beyond what is directly present in the retry request, and that servers MUST treat requestState as attacker-controlled input. Each requirement on that page maps to a test:

  • Round trip - a tool returns resultType input_required with inputRequests, and the retry carries inputResponses and the echoed requestState under a new JSON-RPC id, then completes.
  • Tampered state - change one byte of requestState and expect a rejection. Servers MUST reject state that fails verification when it influences authorization, resource access or business logic.
  • Another principal - present one user's requestState under a second user's credentials and expect a rejection, which servers SHOULD enforce.
  • Expired state - retry after the TTL you set and expect a rejection.
  • Wrong request - replay the state on a different method or different arguments and expect a rejection.
  • Second replica - route the retry to another instance behind your load balancer and expect it to complete.
  • One-time actions - if a state must be used at most once, replay it twice and expect the second attempt to fail. The spec warns that expiry and binding do not by themselves guarantee single use.
  • Legacy elicitation - a server that calls server.elicitInput errors on a 2026-07-28 connection, so every tool that used it needs an MRTR test.

Subscriptions and Tasks

  • subscriptions/listen - the Python guide says a 2026-07-28 server answers resources/subscribe with -32601. Subscribe through subscriptions/listen instead, and wait for notifications/subscriptions/acknowledged before you assert on updates.
  • Broken streams - Last-Event-ID resumability is removed, so cut a response stream mid-request and assert that the client re-issues the request with a new request ID.
  • Tasks - if you used the experimental tasks feature, poll tasks/get to completion and send input through tasks/update, because tasks/result and tasks/list are gone from the extension.

Deprecated Features

Roots, Sampling, Logging and Dynamic Client Registration are deprecated in 2026-07-28 but still work. The spec's deprecated features registry gives those four the same earliest removal, the first revision released on or after 2027-07-28, and says the actual removal is a Core Maintainer decision.

  • Roots - pass directories or files through tool parameters, resource URIs or server configuration.
  • Sampling - integrate directly with LLM provider APIs.
  • Logging - log to stderr for stdio transports, or use OpenTelemetry.
  • Dynamic Client Registration - move to Client ID Metadata Documents.
  • HTTP+SSE transport - move to Streamable HTTP. The registry lists this transport as deprecated since 2025-03-26, with its own earliest removal of three months after SEP-2596 reaches Final, so check it before you plan around the 2027-07-28 date.

Keep legacy-era tests for each deprecated feature you still serve, and add the replacement's test before you remove the old path.

Add Authorization Regression Checks

The authorization changes land mostly on clients and authorization servers, so they matter if your team ships either one alongside the MCP server. MCP security explains why MCP authorization is harder to get right than API authorization.

  • Issuer check - authorization servers SHOULD include iss in the authorization response per RFC 9207, which exists to defend against mix-up attacks, and clients MUST validate a present iss against the recorded issuer before redeeming the code. Send a response with the wrong iss and expect the client to stop before redemption.
  • application_type - clients must specify an appropriate application_type during Dynamic Client Registration. Capture the registration request and assert the field is present.
  • Issuer-keyed credentials - clients MUST key stored credentials by issuer, MUST NOT reuse them with a different authorization server, and MUST re-register when the server changes. Point the client at a second authorization server and assert a fresh registration.
  • CIMD and DCR - Dynamic Client Registration is deprecated in favor of Client ID Metadata Documents but stays available for authorization servers that do not support them, so test both paths. The MCP Inspector CLI takes a --client-metadata-url flag for the CIMD path.

Diff Stateless MCP Output Against the Baseline

A raw diff of 2026-07-28 output against the legacy baseline goes red on fields that were never regressions. Normalize the era-specific ones first; generic schema drift checks, such as a parameter entering the required array, are covered in MCP testing.

Field or codeLegacy baseline2026-07-28Normalizer action
resultTypeAbsentRequired: "complete" or "input_required" in the core spec ("task" under the tasks extension)Treat a missing value as "complete", as clients MUST
ttlMs, cacheScopeAbsentRequired on list and read results; the TypeScript SDK defaults to ttlMs 0 and cacheScope 'private'Drop both
serverInfoIn the initialize resultIn the result _meta, self-reported by the serverDrop it from _meta
tools/list orderNot specifiedServers SHOULD return a deterministic orderSort by tool name
Resource not found-32002 in the 2025-11-25 spec, but SDKs differed: Python v1 sent code 0, and the TypeScript upgrade guide says v1.x already sent -32602-32602Re-baseline once; for Python the diff arrives with the v2 bump in wave 2
Unknown method (Python v2)-32602-32601Re-baseline in wave 2
New error codesNone-32020 HeaderMismatch, -32021 MissingRequiredClientCapability, -32022 UnsupportedProtocolVersionUpdate tests written against the draft numbers -32001, -32003 and -32004

This Node script applies the first four rows to a successful MCP Inspector CLI snapshot and sorts keys, so the only lines left in a diff are real changes:

// normalize.mjs: strip era-only fields from an MCP Inspector CLI --format json snapshot
import { readFileSync } from "node:fs";

const SERVER_INFO = "io.modelcontextprotocol/serverInfo";
const sortKeys = (v) =>
  Array.isArray(v) ? v.map(sortKeys)
  : v && typeof v === "object"
    ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortKeys(v[k])]))
    : v;

const snapshot = JSON.parse(readFileSync(process.argv[2], "utf8"));
if (!snapshot.result) throw new Error(process.argv[2] + " holds no result; diff error snapshots as they are");
const result = { ...snapshot.result };

delete result.ttlMs;       // 2026-07-28 cache hints, absent from legacy results
delete result.cacheScope;
if (result._meta) {
  const { [SERVER_INFO]: _selfReported, ...rest } = result._meta;
  if (Object.keys(rest).length) result._meta = rest;
  else delete result._meta;
}
result.resultType ??= "complete";   // legacy results omit it
if (Array.isArray(result.tools)) {
  result.tools = [...result.tools].sort((a, b) => a.name.localeCompare(b.name));
}

process.stdout.write(JSON.stringify(sortKeys(result), null, 2) + "\n");

Schema diffs still need a human. The changelog loosens inputSchema and outputSchema to allow any JSON Schema 2020-12 keywords, and structuredContent to allow any JSON value, so a schema that gains a $ref can be a legitimate change. TestMu AI's JSON compare tool shows two normalized files side by side when you need to read a diff before accepting it.

Gate CI on the conformance run as well as the diff:

  • Stale entries fail too - with --expected-failures, the conformance runner exits 1 on an unexpected regression and also on a stale entry, a baselined scenario that now passes, so the file shrinks as you fix things.
  • One file per revision - keep conformance-baseline-2025.yml and conformance-baseline-2026.yml apart, because a scenario can fail on one wire and pass on the other.
set -euo pipefail

# --stored-auth-only fails fast with auth_required instead of waiting on a browser callback
npx @modelcontextprotocol/inspector@2.8.0 --cli --config ./mcp.json --server orders-modern \
  --stored-auth-only --method tools/list --format json > tools-list.modern.json

node normalize.mjs baseline/tools-list.legacy.json > legacy.norm.json
node normalize.mjs tools-list.modern.json > modern.norm.json
diff -u legacy.norm.json modern.norm.json

npx @modelcontextprotocol/conformance@0.2.0-alpha.11 server \
  --url http://localhost:3000/mcp --requirements 2026-07-28 \
  --expected-failures ./conformance-baseline-2026.yml

Catch Agent Regressions After the MCP Migration With Agent Assurance

A server can pass every conformance scenario and still change what an agent does, because a reordered tool list, a reworded description or a new schema keyword all reach the model on its next call.

Agent Assurance, which you run as Rook CLI, checks what the run changed: it invokes the agent that calls your server for real, so point it at staging. If the confirmation tool from the opening errors, the agent may still say the action went through; that claim never counts as proof.

For its own discovery and verifier calls, Rook CLI 0.1.5 connects only to stdio MCP servers and lists HTTP, SSE and WebSocket definitions as unsupported-transport, so reaching a Streamable HTTP build that way needs a stdio bridge; see how to configure MCP servers in Agent Assurance. The Agent Assurance docs do not name the MCP revision Rook CLI speaks, so keep the server dual-era for these runs.

Rook CLI 0.1.5 terminal showing the output of /help mcp: the list, get, add, remove, enable, disable and approve subcommands, with add taking scope, transport (stdio, http, sse or ws), url, env and header options

Rook CLI /help mcp: a help screen from a saved demo project, Rook CLI 0.1.5

For the calls your agent sends through the migrated server, Agent Assurance checks:

  • Calls to the migrated server - each call the agent makes to your server is held against the agent's declared tool surface, provided the profile's hooks return the calls they saw; otherwise those criteria cannot be decided and come back Unable to Verify.
  • Real tool lists - discovery asks each approved stdio server what tools it really has rather than trusting the config, and a project or discovered definition whose command, arguments or environment references change goes back to pending approval.
  • No unexpected writes - an order lookup scenario can assert that a write tool was not_called, which again depends on hooks that report observed calls.
  • Read-only verifiers - a scenario can require an MCP verifier such as get_order, which judges use under instructions to leave the order untouched; if that verifier's server cannot be called, the skip reason names it.

Compared with a typical eval tool's defaults:

After the migrationTypical eval toolAgent Assurance
What the rerun testsYour existing cases, rerun after the switchThe same scenario IDs, derived once from the agent's code and declared tools, run again
Proof an action went throughA graded reply or traceThe order state a read-only MCP tool such as get_order returns; model judges still read the reply, but a success message sent after the confirmation call errored proves nothing
Calls through your serverMatched to a tool list you write or pass in; confirming what a write through your server changed takes a per-task script, since it is not built in by defaultHeld against the tool surface the agent declares, with effects read from files, artifacts and read-only probes
An effect no probe could confirmThe case errors, or is skipped if you opted inUnable to Verify, listed apart from the pass rate on both sides of the migration

Read the eval column as that category's usual setup, not any one product: several eval tools score tool calls, and a custom scorer can check what a write changed.

To run it against the migrated agent, install Rook CLI from npm:

npm install -g @testmuai/rook
rook --version

It needs Node 22 or newer. Start rook from the codebase of the agent that calls your server; for other install routes, see the Rook CLI install guide.

To drive it from Claude Code, set up the rook skill:

npx @testmuai/rook-skill@latest install --agent claude-code

Then, in Claude Code, ask for a bounded run:

/rook Use the staging profile for the agent that calls our orders MCP server. Show the MCP servers it declares and their approval state, then propose three scenarios for the 2026-07-28 migration: an order lookup that must not call a write tool, an action that needs the user's confirmation, and a missing order. List every write those scenarios could make through the server, and do not generate them or invoke the agent until I approve.
Note

Note: Each Agent Assurance run snapshots the agent definition, profile and scenarios it used, so rerunning the same scenarios after the migration lets the report tell a regression from a change to those definitions.

Conclusion

Record the legacy baseline this week, while your server still speaks only 2025-11-25: tools/list, golden calls and error envelopes captured with protocolEra set to legacy, plus a conformance run at that revision.

Then bump the SDK (TypeScript keeps the 2025 protocol but reshapes the tool schemas you advertise, while a Python v2 server starts answering 2026-07-28 too), opt in behind a dual-era handler, and run the two-era matrix before any client pins 2026-07-28. Add the named tests for sessions, logging, MRTR and subscriptions, and diff against the normalized baseline in CI.

Finish a stateless MCP migration at the agent layer, where Agent Assurance grades each criterion against the evidence a run leaves, such as files changed and tool calls made. Put the deprecated paths on a calendar too, since Roots, Sampling and Logging become eligible for removal with the first revision released on or after 2027-07-28.

Author

...

Anubhav Singhmaar

Blogs: 41

  • Linkedin

Anubhav Singhmaar is an AI Product Manager at TestMu AI driving Kane CLI, the command-line tool that brings browser automation to the terminal, turning natural-language flows into runs in a real Chrome browser that return pass or fail with shareable proof. He owns the roadmap and prioritization and works with engineering to ship developer-facing features. Before TestMu AI, he spent over four years at Sprinklr owning enterprise voice AI across APAC and EMEA. A mechanical engineer turned product manager, he grounds guidance in real QA workflows.

Reviewer

...

Srinivasan Sekar

Reviewer

  • Linkedin

Srinivasan Sekar is Director of Engineering at TestMu AI (formerly LambdaTest), where he leads engineering and open-source initiatives behind the Selenium and Appium automation grid and owns TestMu AI's MCP Server. A committer to Appium and a contributor to Selenium, WebdriverIO, Taiko, and AppiumTestDistribution, he brings over 15 years of experience in quality engineering and open-source technologies. He is the author of the Apress book 'The MCP Standard: A Developer's Guide to Building Universal AI Tools with the Model Context Protocol,' a Certified Kubernetes and Cloud Native Associate, and an international conference speaker. Before TestMu AI he spent over eight years at Thoughtworks as a Principal Consultant and Quality Architect. Srinivasan holds a B.Tech in Information Technology from Anna University.

Add to Google preferred sources

Summarise with AI

Copied to Clipboard!
...

3000+ Browsers. One Platform.

See exactly how your site performs everywhere.

Try it free
...

Write Tests in Plain English with KaneAI

Create, debug, and evolve tests using natural language.

Try for free

Stateless MCP Migration FAQs

Did you find this page helpful?

More Related Blogs

TestMu AI forEnterprise

Get access to solutions built on Enterprise
grade security, privacy, & compliance

  • Advanced access controls
  • Advanced data retention rules
  • Advanced Local Testing
  • Premium Support options
  • Early access to beta features
  • Private Slack Channel
  • Unlimited Manual Accessibility DevTools Tests