{"slug": "handshake-which-mcp-clients-will-break-your-server-after-the-2026-07-28-spec", "title": "Handshake: which MCP clients will break your server after the 2026-07-28 spec split?", "summary": "A developer built Handshake, an agent that checks whether a Model Context Protocol (MCP) server will keep working with a given client after the 2026-07-28 spec revision, which removes initialize, Mcp-Session-Id, ping, logging/setLevel and resources/subscribe while adding server/discover, per-request _meta, a required resultType and cache hints. The tool joins stored spec, version and client-support facts in Sanity via Sanity Context MCP, with a deterministic function rather than the model assigning each verdict color; on 18 held-out labeled cells it scored 16 correct versus 6 for keyword search.", "body_md": "*This is a submission for the [Sanity Challenge, Path One: Ship an Agent That Queries Real Content](https://dev.to/challenges/sanity-2026-09-16)*\n\n**Will my MCP server still work with client X after the 2026-07-28 spec split?** Paste a manifest into Handshake and you get a grid of verdicts, one per feature and client, each with its sources. The spec and client-support content lives in Sanity, and the agent reads it through Sanity Context MCP (GROQ over a dataset, plus a Knowledge Base). A plain function picks each colour, not the model. On 18 held-out cells I labeled from stored quotes, Handshake gets 16 right. Keyword search gets 6.\n\nOn 28 July 2026 the Model Context Protocol removed its own handshake: revision `2026-07-28` drops `initialize`, `Mcp-Session-Id`, `ping`, `logging/setLevel`, and `resources/subscribe`, and adds `server/discover`, per-request `_meta`, a required `resultType`, and cache hints on list results. Roots, Sampling, and Logging are deprecated, and the earliest removal date on the registry is 28 July 2027. Client docs didn't all keep up: Cursor's MCP page still lists Roots as Supported, and still lists SSE, which the spec deprecated in March 2025.\n\nHandshake is for people who ship MCP servers. Paste a `server/discover` result, an `initialize` result, or a short manifest. Rows are the features the server depends on, columns are clients, and each cell gets a colour:\n\n`compute_verdict` picks the colour by joining three stored facts: where the feature sits in the spec, which protocol versions the server advertises, and a support claim with a URL and the date I read it. Gemini only writes the paragraph under the grid. If it tries to relabel a cell, the page still shows the function's answer.\n\nThe demo has eight scenarios. These four make the case.\n\n`initialize` and `logging/setLevel`: it still speaks the legacy era, which has those methods. Claude is amber because Anthropic's 28 July post says modern support is rolling out, not finished. Cursor is grey on `supportedVersions` to `2026-07-28` but still requires `Mcp-Session-Id`, and There's no hosted copy of the Next app, so the quickest way in is the public dataset (project `hmeltser`, no login): [first five feature documents, as JSON](https://hmeltser.api.sanity.io/v2025-01-01/data/query/production?query=*%5B_type%3D%3D%22feature%22%5D%5B0...5%5D%7Bkey%2Cname%7D). The [Sanity Studio](https://handshake-mcp.sanity.studio/) needs a Sanity login; more links are at the end.\n\nThe grid needs no API key: without `SANITY_CONTEXT_TOKEN`, lookups use the same fixture data seeded into the public dataset. That's what the screenshots show, and the colours match the seeded documents.\n\nTwo GIFs: that manifest turning into the grid, and a cell's sources plus the migration advice.\n\nThe source isn't public yet, so the parts that matter are quoted inline below. Copyright 2026 Manoj Kumar.\n\nThe core is `lib/verdict/compute.ts`. Around it:\n\n`data/`: revisions, features, clients, and claims. `data/heldout.ts` is the quote-labeled set; `data/golden.ts` is a rules-consistency file whose score I don't treat as accuracy.`lib/manifest/parse.ts` turns a paste into feature keys plus a flag for \"this server advertises only 2026-07-28\".`lib/agent/loop.ts` is an eight-step tool loop; `lib/agent/gemini.ts` calls the free Gemini API.\n`npm test` covers the claims, held-out quotes, consistency file, mock loop, and Gemini retry. `npm run eval` prints the held-out comparison first. I wrote the components myself; button styling uses `class-variance-authority`, `clsx`, and `tailwind-merge`.\n\nThree excerpts, copied verbatim from the source.\n\n**1. The agent reads Sanity through Context MCP.** With a token set, the agent's `groq_query` calls the `groq_query` tool on the `handshake-data` endpoint. From `lib/agent/execute.ts`:\n\n``` js\nasync function liveGroq(query: string): Promise<string> {\n  try {\n    const tools = await listMcpTools(contextDataUrl(), contextToken())\n    const tool = tools.find((item) => item.name === \"groq_query\")\n    if (!tool) {\n      return JSON.stringify({\n        error: \"handshake-data did not expose groq_query\",\n        tools: tools.map((item) => item.name),\n        endpoint: \"handshake-data\",\n      })\n    }\n    const text = await callMcpTool(contextDataUrl(), contextToken(), tool.name, queryArgs(tool, query))\n    return text.slice(0, 2500)\n  } catch (error) {\n    return JSON.stringify({ error: errorText(error), endpoint: \"handshake-data\" })\n  }\n}\n```\n\n`callMcpTool` in `lib/sanity/mcp.ts` refuses any tool name that looks like semantic search or embeddings, so a stray call can't eat the monthly quota.\n\n**2. The colour is a rule, not a vibe.** The branch behind scenario 2, where the server advertises only `2026-07-28` but still leans on a removed feature. From `lib/verdict/compute.ts`:\n\n```\n  if (server.rejectsLegacy && modernDisposition === \"removed\") {\n    if (modern === \"supported\") {\n      return {\n        ...base,\n        verdict: \"breaks\",\n        rationale: `The server advertises only ${server.advertisedVersions.join(\", \")}, and ${feature.name} was removed in that revision. ${clientName(catalog, clientSlug)} documents modern-era support, so there is no legacy version on this server to fall back to.`,\n      }\n    }\n    if (modern === \"partial\" || legacy === \"supported\" || legacy === \"partial\") {\n      return {\n        ...base,\n        verdict: \"legacy-only\",\n        rationale: `${feature.name} was removed in 2026-07-28. The server does not advertise a legacy version. ${eraPhrase(catalog, clientSlug, legacy, modern)}`,\n      }\n    }\n    return {\n      ...base,\n      verdict: \"unknown\",\n      rationale: `${feature.name} was removed in 2026-07-28 and this server advertises only the modern revision. ${clientName(catalog, clientSlug)} has no sourced era claim, so a break is likely but not proven.`,\n    }\n  }\n```\n\nGemini can't overrule this. The system prompt says so: \"You choose what to look up. You do not relabel a cell.\"\n\n**3. Free-tier Gemini with a fallback.** A 429 or 503 retries the same model (500ms, then 1500ms backoff), then falls back from `gemini-3.8-flash` to `gemini-3.5-flash`. I don't call `gemini-2.5-flash`: new AI Studio keys get a 404 for that id. From `lib/agent/gemini.ts`:\n\n``` js\n  async generate(request: LlmRequest): Promise<LlmResponse> {\n    const models = [this.options.model, this.options.fallbackModel]\n    let lastError: Error | null = null\n    for (const model of models) {\n      for (let attempt = 0; attempt < GEMINI_ATTEMPTS_PER_MODEL; attempt += 1) {\n        try {\n          const result = await this.call(model, request)\n          this.id = model\n          return result\n        } catch (error) {\n          if (!(error instanceof Error) || !isRetryable(error)) throw error\n          lastError = error\n          const retriesLeft = attempt < GEMINI_ATTEMPTS_PER_MODEL - 1\n          if (retriesLeft) await this.sleep(geminiBackoffMs(attempt))\n        }\n      }\n    }\n    throw lastError ?? new Error(\"Gemini request failed\")\n  }\n```\n\nSanity holds everything the verdict joins, and the agent reaches it through two Sanity Context MCP endpoints: GROQ over the dataset, and a Knowledge Base. A feature points at the revisions where it arrived, was deprecated, and sometimes was removed. A client has claims, each with a status, an era, and a source. A server has the versions it advertises. A keyword search returns a paragraph: it doesn't bind those rows, or know that a server listing only `2026-07-28` has refused the legacy fallback.\n\nSix document types:\n\n`specRevision`: every published revision I could verify: `2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25`, and `feature`: a protocol feature or official extension, with detector strings for manifests and a migration sentence.`client`: a product. Twelve of them: Claude Desktop, Claude on the web, Claude Code, Cursor, VS Code GitHub Copilot, Microsoft 365 Copilot, Goose, ChatGPT, MCP Inspector, Postman, fast-agent, and Archestra.AI. I left out clients I couldn't tie to a page.`supportClaim`: one sourced statement: supported, partial, or unsupported (no fake unknown rows). Each has a URL, a short quote, a source type, and `retrievedAt` of 2026-09-27.`gotcha`: a conflict worth showing next to a cell, like Cursor's Roots row.`sampleServer`: a demo manifest.\nDeployed schema id: `_.schemas.handshake`. Studio runs on the free host at `handshake-mcp`, app id `cfp4wjaipfhkyfhjfo47800v`.\n\nOne Context MCP endpoint can't serve both a dataset and a Knowledge Base. Give it both and the dataset wins; the Knowledge Bases are ignored with no error. So I created two.\n\n| Endpoint | Mode | URL | \n|---|---|---|\n| `handshake-data` | GROQ, dataset `production` , filter on the six types | `https://api.sanity.io/v1/context/organizations/oaj2t5f91/mcp/handshake-data` | \n| `handshake-kb` | Knowledge Base `MCP spec & client docs` | `https://api.sanity.io/v1/context/organizations/oaj2t5f91/mcp/handshake-kb` | \n\n`handshake-data` stayed NOT READY until the schema descriptor existed. After `sanity schemas deploy`, its tools are `initial_context`, `groq_query`, `schema_explorer`, and `array_field_reader`. `handshake-kb` was Ready as soon as the Knowledge Base was built.\n\nThe agent uses these for lookups; `parse_manifest` and `compute_verdict` stay in the app, with the same rules as the seeded documents. I skip semantic search: the Free plan allows 500 of those queries a month, and a refresh shouldn't spend them.\n\nFifteen markdown files: changelogs, the era definitions, the deprecated registry, the extension matrix, Cursor's docs, Anthropic's rollout post, and the Inspector README. The cap is 150 indexed documents per organization. I didn't point it at a domain or at the dataset.\n\nTwo pairs of sources disagree. `compute_verdict` applies both resolutions, and I pasted these instructions into the Knowledge Base so a rebuild keeps the same reading.\n\n**HTTP+SSE.** The 2025-03-26 changelog says Streamable HTTP replaced it; the deprecated registry says it's Deprecated since that date, not Removed. A client that still documents SSE (Cursor does) gets amber.\n\n```\nWhen the 2025-03-26 changelog says HTTP+SSE was replaced, and the deprecated registry says it is Deprecated, follow the registry for lifecycle and the changelog for the date the deprecation started.\n\nDo not tell a server author that HTTP+SSE is already gone. Do not tell them it is a current transport. A client doc that still lists SSE, such as Cursor's transport table, can be true as a support claim and still produce an amber cell.\n```\n\n**Claude enterprise auth.** Anthropic's 28 July 2026 post says enterprise-managed auth shipped for Claude. The extension matrix has a check for Archestra.AI and none for Claude, so the claim stays partial.\n\n```\nWhen Anthropic's 28 July 2026 post says enterprise-managed auth shipped for Claude, and the extension support matrix has no Enterprise Auth check for Claude (web) or Claude Desktop, record Claude as partial.\n\nDo not treat the blog post as a green cell. Do not treat the empty matrix cell as unsupported. The claim stays partial until those sources agree. Archestra.AI is the client with an Enterprise Auth check.\n```\n\nCaveat: the Knowledge Base Issues view showed 0 conflicts, because each note already tells both sides and the resolution in one file, so the indexer sees one story. To make Issues flag the pairs, I'd add the changelog, the deprecated registry, the Anthropic post, and the extension matrix as separate URL sources.\n\nThe number I quote is from a held-out set of 18 cases, each labeled from a stored quote. I didn't call `compute_verdict` while labeling.\n\n|  | Cells correct | Share | \n|---|---|---|\n| Keyword search | 6 of 18 | 33% | \n| Knowledge Base text only | 2 of 18 | 11% | \n| Handshake | 16 of 18 | 89% | \n\nHandshake has a citation on all 18 and misses two. Both are labeled unknown, because the Anthropic post never says Claude implements `ping` or `logging/setLevel`. The engine treats \"rolling out soon\" as enough for amber. I left that disagreement in the table.\n\nA second file of 25 cases matches Handshake on every label, but those labels came from the same rules as the function. That's a consistency check, not an accuracy number, so I don't quote it.\n\n`gemini-3.8-flash` returned 503 (\"high demand\") once in a live smoke and passed on retry, hence the retry and fallback in excerpt 3. The smoke passed on both Flash models for the held-out Postman MCP Apps cell: local verdict `works`, Gemini repeated `hmeltser`\n`production` (public, Free plan)`_.schemas.handshake`\nAgent session transcript: [Handshake build session](https://dev.to/agent_sessions/handshake-build-session-sanity-challenge-yc5jrj)\n\nIt's a sanitized build summary (`docs/AGENT_SESSION.md`): the two endpoints, the schema deploy, the held-out numbers, and the Gemini retry. It has no keys or tokens. The uploader's redaction misses some strings, so I checked it before publishing.\n\nDisclosure, set in the DEV editor: **AI-Assisted**. I used a coding agent to draft the app and this post, and I'm publishing it under my name.", "url": "https://wpnews.pro/news/handshake-which-mcp-clients-will-break-your-server-after-the-2026-07-28-spec", "canonical_source": "https://dev.to/manoj07ar/handshake-which-mcp-clients-will-break-your-server-after-the-2026-07-28-spec-split-12ga", "published_at": "2026-09-28 08:32:17+00:00", "updated_at": "2026-09-28 08:49:26.626596+00:00", "lang": "en", "topics": ["agent-protocols", "ai-agents", "ai-tools", "developer-tools"], "entities": ["Handshake", "Model Context Protocol", "Sanity", "Gemini", "Cursor", "Anthropic", "Manoj Kumar"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/handshake-which-mcp-clients-will-break-your-server-after-the-2026-07-28-spec", "markdown": "https://wpnews.pro/news/handshake-which-mcp-clients-will-break-your-server-after-the-2026-07-28-spec.md", "text": "https://wpnews.pro/news/handshake-which-mcp-clients-will-break-your-server-after-the-2026-07-28-spec.txt", "jsonld": "https://wpnews.pro/news/handshake-which-mcp-clients-will-break-your-server-after-the-2026-07-28-spec.jsonld"}}