> ## Documentation Index
> Fetch the complete documentation index at: https://docs.impellabs.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Protocol revisions

> The server answers two eras of MCP at once. Which one you are in, why an old client needs to do nothing, and what a modern client must send.

**Preview.** This server is **dual-era**: it answers the current MCP revision
and the three handshake-era ones, on the same URL, at the same time. Serving
both is explicitly permitted by the spec, and here it is the only honest
option — every shipping client today speaks `2025-06-18`, and a cutover would
break all of them on the day of deploy.

## The two eras

|                          | Handshake era                                  | Modern era                             |
| ------------------------ | ---------------------------------------------- | -------------------------------------- |
| **Revisions**            | `2025-06-18`, `2025-03-26`, `2024-11-05`       | `2026-07-28`                           |
| **Opens with**           | `initialize`, then `notifications/initialized` | Nothing. There is no handshake.        |
| **Version is agreed**    | Once, for the connection                       | Never. Every request declares its own. |
| **"What do you speak?"** | `initialize`                                   | `server/discover`                      |
| **Sessions**             | A MAY, and we issue none                       | Removed from the spec entirely         |

The authoritative list is `mcpserver/protocol.py: SUPPORTED_PROTOCOL_VERSIONS`,
and `server/discover` reads it out over the wire — the section below shows how
to ask. Adding a revision there is a decision about behaviour, not a string
appended to a tuple, so that list is short on purpose.

## Which era you are in

**The `MCP-Protocol-Version` request header decides, and nothing else does.**

That is deliberate: the era is known before the body is parsed, before the
credential is touched, and therefore before anything can run under an
assumption that turns out not to hold. The body's own declaration is then
checked *against* the header, rather than being a second, competing source of
the answer.

| `MCP-Protocol-Version`                   | Era                        | What happens                                                                                                                 |
| ---------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Absent                                   | Handshake, as `2025-03-26` | The spec's own compatibility rule: the header is newer than that revision, so a client omitting it is older than the header. |
| `2025-06-18`, `2025-03-26`, `2024-11-05` | Handshake                  | Served byte for byte as it was before `2026-07-28` was implemented.                                                          |
| `2026-07-28`                             | Modern                     | Held to everything in [What a modern client must send](#what-a-modern-client-must-send).                                     |
| Anything else                            | —                          | **400** with `-32022`, carrying the list to retry from.                                                                      |

<Note>
  **`2026-07-28` permits a server to reject a request with no
  `MCP-Protocol-Version` header at all. This one does not.** Keeping the
  fallback is most of the backward-compatibility guarantee, in one line: a
  client that has never sent that header is served today exactly as it was
  yesterday.
</Note>

## If your client already works, do nothing

There is no migration here, and that is a property of the code rather than an
intention. Nothing in the modern era's rules can be reached by a request that
did not declare `2026-07-28` in the header:

* **`initialize` still works**, and still negotiates. A client asking for a
  revision we speak is answered in it; one asking for anything else is
  answered with the newest *handshake* revision rather than being refused over
  a string. It is never answered `2026-07-28` — that would name a revision in
  which `initialize` does not exist, to a client that just proved it expects
  one in which it does.
* **`notifications/initialized` is still 202 with an empty body**, in either
  era, and notifications are held to none of the header rules below.
* **Handshake-era results are unchanged.** No `resultType`, no `_meta` server
  identity, no cache hints — a result that grew fields for everybody would
  still parse for most clients, which is exactly why nothing would notice
  until one strict client somewhere stopped connecting.

## What a modern client must send

Every request. There is no handshake in which any of it could have been said
once.

<Steps>
  <Step title="The MCP-Protocol-Version header">
    `2026-07-28`. A request carrying the modern `_meta` but no header is
    **refused, not downgraded** — served silently it would be answered as a
    `2025-03-26` client.
  </Step>

  <Step title="Two required _meta keys, under params">
    `io.modelcontextprotocol/protocolVersion` — the revision this one request
    is written against — and `io.modelcontextprotocol/clientCapabilities`,
    what the client can be asked to do. This server asks nothing of a client,
    so capabilities are read only to enforce their presence.
    `io.modelcontextprotocol/clientInfo` is optional, and self-reported: it is
    logged and shown, never trusted, because the bearer token is what says who
    this is.
  </Step>

  <Step title="Mirrored headers">
    `Mcp-Method` on every request, matching the body's `method`. `Mcp-Name` on
    `tools/call`, matching `params.name`. Both are validated against the body
    and a mismatch is refused — see [Why the headers are
    mirrored](#why-the-headers-are-mirrored).
  </Step>
</Steps>

<Note>
  **Header values are ASCII-only, so a name that is not gets a Base64
  sentinel.** `Mcp-Name: =?base64?ZWNobw==?=` is decoded before it is compared
  to the body, so a tool name with an accent or leading whitespace round-trips
  intact. The prefix and suffix are exact and case-sensitive; a wrapper that
  does not decode is compared as-is and therefore refused.
</Note>

### A modern request, beside the one that works today

<CodeGroup>
  ```bash 2025-06-18 theme={null}
  # The handshake era. Not one byte of this changed when 2026-07-28 landed.
  curl https://api.impellabs.tech/mcp \
    -H "Authorization: Bearer tgcc_xxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -H "MCP-Protocol-Version: 2025-06-18" \
    -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
  ```

  ```bash 2026-07-28 theme={null}
  # No handshake ran before this. The request says everything about itself.
  curl https://api.impellabs.tech/mcp \
    -H "Authorization: Bearer tgcc_xxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -H "MCP-Protocol-Version: 2026-07-28" \
    -H "Mcp-Method: tools/list" \
    -d '{
      "jsonrpc": "2.0",
      "id": 1,
      "method": "tools/list",
      "params": {
        "_meta": {
          "io.modelcontextprotocol/protocolVersion": "2026-07-28",
          "io.modelcontextprotocol/clientCapabilities": {}
        }
      }
    }'
  ```

  ```bash 2026-07-28 tools/call theme={null}
  # `Mcp-Name` as well, and it must be the tool the body names.
  curl https://api.impellabs.tech/mcp \
    -H "Authorization: Bearer tgcc_xxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -H "MCP-Protocol-Version: 2026-07-28" \
    -H "Mcp-Method: tools/call" \
    -H "Mcp-Name: list_agents" \
    -d '{
      "jsonrpc": "2.0",
      "id": 2,
      "method": "tools/call",
      "params": {
        "name": "list_agents",
        "arguments": {},
        "_meta": {
          "io.modelcontextprotocol/protocolVersion": "2026-07-28",
          "io.modelcontextprotocol/clientCapabilities": {}
        }
      }
    }'
  ```
</CodeGroup>

## `server/discover`

The modern way to ask what this server speaks, and the method that replaces
the half of `initialize`'s job that survived: which revisions, what
capabilities, who we are. It is a MUST in `2026-07-28`.

**It agrees nothing.** There is no session to agree it in. A client may call
it, or skip it and let a wrong version come back as `-32022` instead.

```bash theme={null}
curl https://api.impellabs.tech/mcp \
  -H "Authorization: Bearer tgcc_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: server/discover" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "server/discover",
    "params": {
      "_meta": {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientCapabilities": {}
      }
    }
  }'
```

The result carries `supportedVersions` — every revision this server will
answer on, which is the live reading of `SUPPORTED_PROTOCOL_VERSIONS` — plus
`capabilities` and the same `instructions` the handshake era gets from
`initialize`.

<Note>
  **This server answers `server/discover` in *both* eras, deliberately.** A
  handshake-era client has no reason to call it and none will — but a modern
  client that forgot the `MCP-Protocol-Version` header is read here as a
  `2025-03-26` client, and refusing it would answer "what do you speak?" with
  "no such method". Answering says only what a public spec page says, and is
  the one reply that gets that client unstuck.

  `supportedVersions` lists the handshake revisions too, for the same reason:
  a client that discovers us and finds it cannot speak `2026-07-28` learns
  from that list that `initialize` is still an option here.
</Note>

It needs a credential like every other method. An unauthenticated
`server/discover` is **401** with a `WWW-Authenticate` challenge — which is a
better answer than handing a passer-by this server's capability list.

## What a modern result looks like

The era changes the **shape** of an answer and never its behaviour. No
authority, billing or tenancy decision anywhere in this server branches on the
revision a request declared.

| Field                                         | On                                      | What it is                                                                                                                                                                           |
| --------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `resultType`                                  | Every modern result                     | Always `"complete"` here. The other value belongs to a server asking the *client* for input mid-request, and no tool here asks a client for anything.                                |
| `_meta["io.modelcontextprotocol/serverInfo"]` | Every modern result                     | Who answered — there is no handshake to have said it. Merged into `_meta`, so a tool error keeps its own `impellabs.tech/error` code beside it.                                      |
| `ttlMs`, `cacheScope`                         | `tools/list` and `server/discover` only | Freshness hints, required on those two in this revision. `cacheScope` is `"public"`, because `tools/list` is deliberately unfiltered and the catalogue is the same for every caller. |

<Note>
  Those hints are worth having rather than ceremonial. The server declares
  `listChanged: false` — the registry is fixed at import, so there is no
  notification it could ever send — which makes the TTL the only thing
  standing between a client and a `tools/list` on every turn.
</Note>

## Errors this revision added

`2026-07-28` partitions JSON-RPC's implementation-defined range and reserves
`-32020` to `-32099` for the spec's own meanings, so these are not numbers this
server was free to choose. The two it emits both travel on **HTTP 400**.

| Code     | Name                              | When                                                                                                                                                                                                                   |
| -------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-32020` | `HeaderMismatch`                  | A mirrored header disagrees with the body, or a required one is missing.                                                                                                                                               |
| `-32021` | `MissingRequiredClientCapability` | The request needs something of the client it did not declare. **Never emitted here** — no tool on this server samples, elicits or reads roots.                                                                         |
| `-32022` | `UnsupportedProtocolVersion`      | The revision this request declared is not one we implement. `data.supported` is the list to retry from; `data.requested` is what we were asked for, echoed back because an intermediary may have rewritten the header. |

Two more refusals are worth knowing:

* **A missing required `_meta` field is `-32602`, not `-32020`**, and the
  error names the field. The spec splits them by what is missing: a missing
  *header* is a mirroring failure, a missing required `_meta` field is a
  malformed request.
* **A body that names a different revision than the header is `-32020` in
  either era.** A handshake-era client never writes that key, so the rule
  cannot affect it — but a body saying `2026-07-28` behind a header saying
  `2025-06-18` is precisely the split-brain the mirroring exists to catch.

### Unknown methods are 404 for a modern client

A method this server does not implement is **HTTP 404** carrying `-32601`, so
a client can tell "this MCP server has no such method" from a bare 404 served
by something that is not an MCP endpoint at all. A handshake-era client still
gets `-32601` on a **200**, because that is what it was built to read and a
404 would look to it like a missing endpoint.

`initialize` is one of those methods once you have declared `2026-07-28`. It
is refused rather than quietly answered — a client that got a plausible-looking
handshake result would carry on believing it had a negotiated session, and
this server keeps none. The refusal names `server/discover` and names the
revisions in which `initialize` still works.

## Why the headers are mirrored

`Mcp-Method` and `Mcp-Name` exist so a gateway can route, rate-limit or
authorise **without parsing the body**. The moment the header and the body
disagree, that gateway and this server are enforcing policy on two different
requests — a proxy authorising a `tools/list` in front of a server executing a
`tools/call`. There is no way to tell which one is the client's intent, so
refusing is the only answer that cannot be wrong.

<Note>
  There is no `Mcp-Param-*` check. Those headers mirror tool arguments a
  server asked for with `x-mcp-header` in its `inputSchema`, and no tool in
  this registry asks — so an unrecognised `Mcp-Param-*` is one an intermediary
  added, which RFC 9110 says to forward and ignore.
</Note>

## Still stateless, in both eras

`2026-07-28` removed sessions as a concept, which changed nothing here: this
endpoint has never minted an `Mcp-Session-Id`. One arriving from an older
client is **ignored and never echoed**, which is what that revision tells a
server to do with it. `GET /mcp` and `DELETE /mcp` are **405** in both eras.
See [Limits](/connect/limits).

JSON-RPC batching is not accepted in either era — it was removed in
`2025-06-18` and did not come back. One message per POST.

<Warning>
  **The platform's MCP *client* deliberately stayed on `2025-06-18`.** When the
  platform calls a tool server *you* run, it sends
  `MCP-Protocol-Version: 2025-06-18` and runs `initialize` — that is
  `core/mcp.py: PROTOCOL_VERSION`, and it is a different half of the protocol
  from everything on this page. Nothing here asks you to implement
  `2026-07-28` on a server you host. See [Building a tool
  server](/mcp/overview).
</Warning>

<CardGroup cols={2}>
  <Card title="Any MCP client" icon="plug" href="/connect/clients">
    The four facts that connect anything, and what an SDK on either era does
    when it meets this server.
  </Card>

  <Card title="Limits" icon="gauge" href="/connect/limits">
    Rate limits, and the transport answers that look like faults and are not.
  </Card>
</CardGroup>
