Skip to main content

Versioning & breaking changes

EDITED MCP evolves continuously — new tools, new parameters, occasionally a breaking change. There's no release train to subscribe to: one deployment moves forward, and every change that affects callers lands as a dated entry in the changelog, with breaking changes flagged ⚠ Breaking and migration steps. What the surface promises at any moment is carried by a contract version and a fingerprint you can read straight off the connection.

The short answer​

You cannot tie a hosted endpoint to a specific build. What you get instead:

  • a stable major surface at a stable URL that changes additively only;
  • the gateway build version, the surface contract version, and a fingerprint of the entire tool surface returned on every initialize, so you can log them and assert on them in your own CI;
  • a changelog entry for anything breaking — and from 1.0, 30 days' notice and a published cutover date with it;
  • and a versioned URL, /mcp/v1, to pin to — which declares the major you expect, and lets you be held on it where a break can be served in parallel.

Assert on the fingerprint. It catches surface drift the day it happens, which is stronger than reading a version number.

Which version am I talking to?​

Every initialize response carries the answer in _meta:

{
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {},
"prompts": {},
"resources": { "subscribe": false, "listChanged": false }
},
"serverInfo": { "name": "msv-mcp-runtime", "version": "0.2.0" },
"_meta": {
"com.edited/gateway": {
"version": "0.2.0",
"build": "dbe50346574179e033886cde2d865c590d4c2273"
},
"com.edited/surface": {
"version": "1.0",
"fingerprint": "sha256:7d83ef929d1e185f",
"backends": {
"mdmcp": { "contract": "1.0", "fingerprint": "sha256:a47d44aae3ae2fb3" },
"researchmcp": { "contract": "1.0", "fingerprint": "sha256:05e26cf6549e659a" },
"messagingmcp": { "contract": "1.0", "fingerprint": "sha256:b984c7037f2f4d99" }
}
}
}
}

Four numbers, four jobs. They're easy to conflate, and conflating them is what makes a version useless to you — a build number read as a contract moves on every deploy and gives you nothing stable to build against.

NumberWhat it tells you
protocolVersionWhich revision of the MCP spec answered your initialize. See Protocol version below.
serverInfo.versionWhich build of the gateway you reached, also at _meta → com.edited/gateway.version. Support identity, not a contract — quote it in a bug report; don't branch on it.
com.edited/surface.versionThe aggregate tool surface. This is the one to assert on.
com.edited/surface.backends[…].contractEach backend's own declared contract version, for provenance — which part of the surface moved, and whether the team that moved it said so. It is not the number to act on; that's com.edited/surface.version above.

Plus the two with the most practical value for a builder, covered in detail below: com.edited/surface.fingerprint, a hash over the whole published catalogue, and the matching per-backend fingerprint under com.edited/surface.backends.

_meta sits on the result, which the MCP spec defines on every Result, and clients that don't read it ignore it safely. Nothing about your existing integration changes because it appeared.

com.edited/gateway.build is the full commit SHA, not a short one. Log it verbatim; don't parse or truncate it.

Read these defensively — two of them are optional. A backend's contract may be absent, and a backend's entry in backends may be missing entirely. In both cases absence means "not known", never "unchanged" — so use a defaulting read rather than direct indexing, and don't treat a vanished key as a signal that nothing moved.

The fingerprint​

A hash over every tool, prompt and resource in our published catalogue: each one's name, plus a tool's input and output schemas, a prompt's arguments, and a resource's media type. It deliberately excludes descriptions and titles at every depth, including the ones on individual arguments and response fields, so wording improvements don't move it and you don't learn to ignore it.

It is the only value here that is derived rather than asserted: it moves when the surface moves, independently of any version number.

Treat the whole string as an opaque token. Compare it for equality; don't parse it, slice it, or validate its shape. The sha256: prefix names the algorithm, but what follows is a truncated digest — 16 hex characters, not the 64 of a full SHA-256 — so a client validating against something like ^sha256:[0-9a-f]{64}$ rejects every value we have ever sent. If you need a pattern at all, match the prefix and leave the rest alone.

Two things it is not, both worth knowing before you rely on it:

It describes the whole catalogue, not your subset. Every caller gets the same fingerprint, whatever their key is entitled to. If your key is scoped to some backends and not others, the fingerprint still covers all of them — so it can move for a change to a tool you cannot call. That's deliberate: a per-caller fingerprint would mean two clients seeing different values for the same gateway on the same day, which makes a support conversation harder rather than easier. Compare tools/list against your own last-known list when you want a change scoped to what you actually use.

It is a statement about shape, not availability. When a backend fails repeatedly we stop listing its tools, so you don't call something certain to fail — but the published catalogue hasn't changed, so the fingerprint doesn't move. A shorter tools/list with an unchanged fingerprint means "briefly degraded", not "the surface changed". The inverse holds too: an unchanged fingerprint is not a promise that every tool in it is answering right now. Handle a missing or failing tool at call time regardless of what the fingerprint says.

A fingerprint change with a shorter tools/list is degradation, not a release

If the gateway restarts while a backend is unreachable, that backend's tools are missing from the catalogue it builds, so the fingerprint moves even though the surface didn't change. Re-read both before acting on the change. A real surface change moves the fingerprint while the catalogue stays the same size or grows.

Assert on the fingerprint in your CI​

Record it once, compare on every run, and re-read tools/list when it changes. That catches surface drift on your side the day it happens:

EXPECTED = "sha256:7d83ef929d1e185f"

result = await session.initialize()
surface = (result.meta or {}).get("com.edited/surface", {})
actual = surface.get("fingerprint")
assert actual == EXPECTED, f"EDITED MCP surface changed: {actual}"

The per-backend fingerprints narrow it further: if only mdmcp moved, nothing your agent does against researchmcp needs re-checking.

Read it as a release-time signal — "has the contract moved since we last looked?" — rather than a runtime health check, which is the one job it doesn't do.

Protocol version​

The MCP protocol revision is negotiated in each initialize call. Your client offers a revision in params.protocolVersion; the gateway answers with the revision it will speak in result.protocolVersion. Read that field if you need to know which one you got.

You offerWe answer
2024-11-052024-11-05
2025-03-262025-03-26
2025-06-182025-06-18
anything else — a newer revision, an unrecognised value, or nothing at all2025-06-18

An offer we don't recognise gets our newest revision rather than an error: initialize is the one call a confused client still needs answered.

2025-06-18 is the newest revision we speak. That number can go up; it won't go down. Removing one of the older revisions from the list above would be a breaking change, and would be announced in the changelog before it happened.

Negotiating an older revision doesn't narrow what you receive​

The gateway is stateless: it keeps nothing from your initialize call, so there is no stored revision it could tailor later responses to. It relays what the backends produce. A client that offers 2024-11-05 still receives every field that later revisions introduced.

FieldIntroduced in
structuredContent on tool results2025-06-18
outputSchema on tools2025-06-18
top-level title on tools, prompts and resources2025-06-18
annotations / readOnlyHint2025-03-26

The negotiated revision doesn't filter the catalogue: tools/list returns the same tools carrying the same fields whichever revision you negotiate.

The practical rule: don't gate your parsing on the negotiated revision. A client that negotiates 2024-11-05 and therefore declines to read structuredContent is throwing away the machine-readable payload that is sitting right there. Read the fields you find; ignore the ones you don't recognise.

The MCP-Protocol-Version header​

2025-06-18 added an MCP-Protocol-Version request header, for clients to restate the negotiated revision on the calls that follow initialize. The spec is firm about it and this gateway is more lenient than the spec requires — both halves matter:

  • The spec requires you to send it. "If using HTTP, the client MUST include the MCP-Protocol-Version: <protocol-version> HTTP header on all subsequent requests to the MCP server." The official SDKs do this for you.
  • The spec requires a server to reject a bad one. "If the server receives a request with an invalid or unsupported MCP-Protocol-Version, it MUST respond with 400 Bad Request." Omitting it is different: a server that never sees the header, and has nothing else to identify the revision by, SHOULD assume 2025-03-26 — a silent downgrade, not a rejection.
  • This gateway does none of that. It doesn't require the header, doesn't validate it, and doesn't use it to choose a revision — that happens in initialize and nowhere else. A missing or unrecognised value is ignored, not rejected.

Two things follow. Don't reach for this header when diagnosing a failure here: a missing or wrong MCP-Protocol-Version has never been the cause of one. But don't carry that leniency to other servers — a conforming one rejects a value it doesn't support, and treats a missing header as 2025-03-26, which may not be the revision you negotiated. Keep sending the negotiated value. If we tighten this to match the spec, the changelog will say so first.

What counts as breaking​

ChangeBreaking?
New tool, resource, or promptno
New optional parameterno
New fields in result objectsno
Improved descriptions, titles, or documentationno
Tool, resource, or prompt renamed or removedyes
Parameter type changed or made requiredyes
New required parameteryes
Parameter removedyes
A published response property removed, or its type narrowedyes
Response shape changedyes

The parameter rows apply to prompt arguments too — a prompt's arguments are its parameters, and the drift rules treat them the same.

Everything in the "no" column happens regularly and without notice. Write clients that tolerate unknown fields and don't treat a longer tool list as an error.

What we guarantee at a breaking change​

From 1.0, breaking changes come with:

  • 30 days' notice before the change reaches production.
  • A changelog entry describing what moves and what to do about it.
  • A published cutover date, so the migration window is a date you can plan against rather than an open question.
  • Where practical, a deprecation window in which the old name keeps working — a renamed tool keeps its old name as an alias, and the deprecation is stated in the tool's own description, so an agent reading the catalogue sees it too.

Notice arrives out of band — release notes and email — not over the protocol. MCP's mechanism for pushing a catalogue change (notifications/tools/list_changed) requires a persistent connection, and this gateway is stateless request/response by design. If you hold a cached tool list, nothing will arrive over the wire to tell you it changed — re-read it with a fresh tools/list after a notice.

Versioned URLs​

URLUse
https://mcp.edited.com/mcpThe endpoint used throughout these docs. Alias for the current stable major: the URL never goes away, but when a v2 becomes current it moves with it.
https://mcp.edited.com/mcp/v1Available, and the one to pin to. Same surface, explicit about which major you expect.

Both serve the same surface today, so moving costs you nothing. What pinning /mcp/v1 changes is what your client has declared: a client on /mcp has said nothing about which major it expects, so it moves with everyone else, while a client on /mcp/v1 can be held on v1's behaviour if a future major arrives at /mcp/v2. The rest of these docs still show the bare /mcp — that's the alias doing its job, not a different endpoint.

Neither form takes a trailing slash. /mcp/ and /mcp/v1/ both answer 307 with a redirect to a plain-http URL, which most clients either refuse to follow or follow in a way that drops the request body — see Transports → Endpoint.

Where a break can practically be served in parallel — the old surface at /mcp/v1 alongside the new one at /mcp/v2 — the notice will say so. Treat parallel majors as per-change rather than guaranteed: the gateway relays backend responses verbatim with no translation layer, so serving both shapes depends on the backends behind it being able to, which is not true of every change. Plan a migration on the notice period and the cutover date, which you always get, rather than on an overlap you might.

Build for change​

Agents that follow MCP's grain barely notice non-breaking changes:

  • Discover, don't hardcode. Read the catalogue with tools/list at runtime instead of baking tool names and schemas into your code — that's what the catalogue is for.
  • Ignore unknown fields. Result objects gain fields over time; parse what you need and skip the rest.
  • Watch the changelog before upgrading or redeploying agents that call EDITED MCP — breaking entries include what changed and what callers need to do.

If the surface looks wrong​

A tool that has changed shape, a version that reads oddly, a fingerprint that moved when you expected it not to — none of these produce an error envelope, so there's nothing to paste. Quote serverInfo.version and the surface fingerprint from your initialize response instead, and see Errors → Getting help for the full list.