cd /news/developer-tools/audit-your-codebase-before-migrating… · home topics developer-tools article
[ARTICLE · art-79860] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Audit Your Codebase Before Migrating to Stateless MCP

A developer built a dependency scanner to audit codebases before migrating to the stateless MCP 2026-07-28 protocol, which removes session IDs and initialization handshakes. The tool classifies matches by migration risk and produces a staged rollout checklist, emphasizing that stateless protocol does not mean the application has no state.

read6 min views1 publishedJul 30, 2026

MCP 2026-07-28 removes protocol-level sessions, the required initialization handshake, and Mcp-Session-Id

. Before deleting those code paths, find the application state they were hiding. This tutorial builds a dependency scanner, classifies each match by migration risk, and turns the results into a staged rollout checklist.

This is a static audit, not proof that a deployment is compatible. It finds likely legacy assumptions; you still need runtime tests across clients, servers, gateways, and replicas.

What changed?

The official MCP 2026-07-28 changelog makes several changes that affect production architecture:

Mcp-Session-Id

is removed from Streamable HTTP.initialize

and notifications/initialized

are no longer required._meta

.server/discover

for version and capability discovery.Mcp-Method

and Mcp-Name

support HTTP routing and policy.ttlMs

and cacheScope

.includeContext

values, and Dynamic Client Registration are deprecated rather than immediately removed.The risky misunderstanding is that “stateless protocol” means “the application has no state.” A workspace, durable task, OAuth credential, or pending approval can still span calls. The dependency must simply be explicit and available to any compatible worker that receives the next request.

The following Node.js script searches a repository for older MCP assumptions. It uses only Node’s standard library and produces both a readable report and an optional JSON report for CI.

Create audit-mcp-2026.mjs

:

#!/usr/bin/env node

import { promises as fs } from "node:fs";
import path from "node:path";
import process from "node:process";

const rules = [
  {
    id: "protocol-session",
    risk: "high",
    pattern: /Mcp-Session-Id|ctx\.sessionId|extra\.sessionId|sessionId/g,
    action: "Map the state behind the session, then use an explicit handle or shared storage.",
  },
  {
    id: "initialization-gate",
    risk: "medium",
    pattern: /notifications\/initialized|\binitialize\b/g,
    action: "Remove gates for modern requests and add server/discover plus version negotiation.",
  },
  {
    id: "sticky-routing",
    risk: "high",
    pattern: /sticky[_-]?session|sessionAffinity|affinity:\s*(?:ClientIP|cookie)/gi,
    action: "Test consecutive calls on different replicas before removing affinity.",
  },
  {
    id: "server-initiated-request",
    risk: "high",
    pattern: /elicitation\/create|sampling\/createMessage|roots\/list/g,
    action: "Move interaction to MRTR and make retried side effects idempotent.",
  },
  {
    id: "legacy-resource-subscription",
    risk: "medium",
    pattern: /resources\/(?:subscribe|unsubscribe)|Last-Event-ID/g,
    action: "Plan subscriptions/listen and test broken-stream behavior.",
  },
  {
    id: "legacy-task",
    risk: "medium",
    pattern: /tasks\/(?:result|list)/g,
    action: "Adopt the io.modelcontextprotocol/tasks extension contract.",
  },
  {
    id: "legacy-logging",
    risk: "low",
    pattern: /logging\/setLevel|notifications\/message/g,
    action: "Review per-request log metadata and production observability.",
  },
];

const ignoredDirectories = new Set([
  ".git",
  "node_modules",
  "dist",
  "build",
  "coverage",
  "vendor",
]);

const textExtensions = new Set([
  ".cjs", ".conf", ".go", ".java", ".js", ".json", ".jsx",
  ".md", ".mjs", ".py", ".rb", ".rs", ".sh", ".toml",
  ".ts", ".tsx", ".yaml", ".yml",
]);

function parseArguments(argv) {
  const result = { root: ".", json: false };

  for (const argument of argv) {
    if (argument === "--json") result.json = true;
    else if (argument.startsWith("-")) {
      throw new Error(`Unknown option: ${argument}`);
    } else result.root = argument;
  }

  return result;
}

async function* walk(directory) {
  const entries = await fs.readdir(directory, { withFileTypes: true });

  for (const entry of entries) {
    if (ignoredDirectories.has(entry.name)) continue;

    const fullPath = path.join(directory, entry.name);
    if (entry.isDirectory()) yield* walk(fullPath);
    else if (entry.isFile() && textExtensions.has(path.extname(entry.name))) {
      yield fullPath;
    }
  }
}

function scanLine(line, file, lineNumber) {
  const findings = [];

  for (const rule of rules) {
    rule.pattern.lastIndex = 0;
    const matches = [...line.matchAll(rule.pattern)];
    for (const match of matches) {
      findings.push({
        file,
        line: lineNumber,
        column: (match.index ?? 0) + 1,
        match: match[0],
        rule: rule.id,
        risk: rule.risk,
        action: rule.action,
      });
    }
  }

  return findings;
}

async function audit(root) {
  const absoluteRoot = path.resolve(root);
  const findings = [];

  for await (const file of walk(absoluteRoot)) {
    const content = await fs.readFile(file, "utf8");
    const lines = content.split(/\r?\n/);

    lines.forEach((line, index) => {
      findings.push(
        ...scanLine(line, path.relative(absoluteRoot, file), index + 1),
      );
    });
  }

  return findings;
}

function printText(findings) {
  if (findings.length === 0) {
    console.log("No known legacy MCP patterns found. Runtime testing is still required.");
    return;
  }

  const weight = { high: 0, medium: 1, low: 2 };
  findings.sort((a, b) => weight[a.risk] - weight[b.risk]);

  for (const finding of findings) {
    console.log(
      `[${finding.risk.toUpperCase()}] ${finding.file}:${finding.line}:${finding.column} ` +
      `${finding.rule} (${finding.match})`,
    );
    console.log(`  ${finding.action}`);
  }

  const counts = findings.reduce(
    (summary, item) => ({ ...summary, [item.risk]: summary[item.risk] + 1 }),
    { high: 0, medium: 0, low: 0 },
  );
  console.log(`\nSummary: ${counts.high} high, ${counts.medium} medium, ${counts.low} low`);
}

async function main() {
  const options = parseArguments(process.argv.slice(2));
  const findings = await audit(options.root);

  if (options.json) console.log(JSON.stringify({ findings }, null, 2));
  else printText(findings);

  process.exitCode = findings.some((item) => item.risk === "high") ? 2 : 0;
}

main().catch((error) => {
  console.error(error instanceof Error ? error.message : error);
  process.exitCode = 1;
});

The script requires Node.js 20 or later. Run it from any directory and pass the repository path:

node audit-mcp-2026.mjs /path/to/your/repository

Use JSON output in CI or to create migration tickets:

node audit-mcp-2026.mjs /path/to/your/repository --json > mcp-audit.json

The exit codes are:

Code Meaning
0
No high-risk static match was found
1
The scanner itself failed
2
At least one high-risk pattern was found

Do not fail a production release merely because a string appears in documentation or a compatibility test. Treat each finding as a review item. The script intentionally favors visibility over certainty.

A grep result is not a migration plan. For each high-risk match, record:

The common replacement patterns are explicit handles and shared storage. An explicit handle makes the dependency visible in the tool schema. Shared storage makes the state accessible across replicas and restarts. Long-running work belongs in a durable task system rather than the memory of a request worker.

Static scanning cannot detect whether a retry is safe or whether an authorization boundary is correct. Add integration tests that deliberately exercise the new architecture.

input_required

before the side effect.requestState

; both cases must fail closed.Send a request whose Mcp-Name

identifies a low-risk tool while the JSON-RPC body calls a restricted tool. The request should be rejected and the mismatch should appear in security telemetry.

Fetch a private tool or resource list as tenant A, then request the same method as tenant B. The second tenant must never receive A’s entry, regardless of the remaining TTL.

A safe rollout keeps modern and legacy behavior observable at the same time. Test these combinations explicitly:

modern client + modern server    -> MCP 2026-07-28
modern client + legacy server    -> tested fallback, if supported
legacy client + dual server      -> legacy path
unsupported combination         -> clear protocol-version error

Track protocol version, discovery success, fallback rate, missing or invalid headers, header/body mismatches, MRTR completion and timeout, request-state verification, duplicate-operation prevention, cache hits by scope, OAuth issuer failures, legacy HTTP+SSE traffic, and deprecated-method traffic.

Retire a compatibility path because the telemetry says it is unused—not because a calendar says the upgrade should be complete.

MCP and a model API solve different problems. MCP connects an agent to tools, resources, approvals, and tasks. A model API connects an application to inference providers, credentials, usage, and billing.

When migrating away from the deprecated Sampling capability, an application can call a model provider directly or use a unified model layer such as CometAPI. That model layer does not replace MCP authorization, tool contracts, application state, or MRTR handling. Keeping the interfaces separate simply allows the protocol and inference providers to evolve independently.

server/discover

and version negotiation.requestState

.MCP 2026-07-28 removes sessions from the protocol contract, not from every application requirement. The migration succeeds when any compatible worker can process the next request from explicit inputs, every retry is authorized and idempotent, and the legacy path can be retired from evidence.

── more in #developer-tools 4 stories · sorted by recency
── more on @mcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/audit-your-codebase-…] indexed:0 read:6min 2026-07-30 ·