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. 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 https://modelcontextprotocol.io/specification/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 : python /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: php 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.