
# Protocol Eras

> One endpoint, two incompatible revisions of the spec.

MCP `2026-07-28` is a breaking revision. It removes the `initialize` handshake, protocol-level sessions, the standalone `GET`/SSE endpoint, `ping`, and `logging/setLevel`, and carries version, identity, and capabilities in per-request `_meta` instead. It is **not wire-compatible with `2025-11-25` in either direction**.

Following the spec's own [terminology](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning#terminology), `h3-mcp` calls these:

- **Modern** — revisions carrying `_meta` per request: `2026-07-28`.
- **Legacy** — revisions that negotiate through `initialize`: `2025-11-25`, `2025-06-18`, `2025-03-26`.
- **Dual-era** — an implementation serving both. This is the default.

## Dispatch

Because the two eras cannot be told apart by URL, the handler decides per request:

| Signal on the incoming request                            | Era    | Behavior                         |
| --------------------------------------------------------- | ------ | -------------------------------- |
| `params._meta["io.modelcontextprotocol/protocolVersion"]` | modern | stateless `2026-07-28` path      |
| `method: "initialize"`                                    | legacy | handshake + optional session     |
| `Mcp-Session-Id` header, no `_meta` version               | legacy | existing session path            |
| `GET` / `DELETE`                                          | legacy | modern-only servers answer `405` |

Both eras serve the same tools, resources, resource templates, and prompts — you write a definition once and every client can reach it.

## Narrowing at Runtime

Set `era` to switch off the surface you don't want:

```ts
defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  era: "dual", // "dual" (default) | "modern" | "legacy"
  tools: [/* ... */],
});
```

With `era: "modern"`, the legacy surface is gone: `GET`/`DELETE` return `405`, `Mcp-Session-Id` and `Last-Event-ID` are ignored, and a legacy `initialize` is answered with `-32022` listing the versions the server does support.

With `era: "legacy"`, a modern request is rejected the same way — `-32022` naming the legacy versions — because legacy clients have no fall-forward mechanism of their own.

## Entrypoints

`era` narrows _behavior_, but both implementations are still bundled. To also drop the era you don't serve, import it directly:

| Import          | Serves             | Min   | Gzip  |
| --------------- | ------------------ | ----- | ----- |
| `h3-mcp`        | both (`era` picks) | ~34kb | ~11kb |
| `h3-mcp/modern` | `2026-07-28` only  | ~28kb | ~9kb  |
| `h3-mcp/legacy` | `≤2025-11-25` only | ~19kb | ~6kb  |

```ts
// Stateless 2026-07-28 only: no sessions, no GET/SSE, no initialize handshake.
import { defineMcpHandler } from "h3-mcp/modern";

// initialize-based revisions only: no per-request _meta, no server/discover.
import { defineMcpHandler } from "h3-mcp/legacy";
```

Each subpath takes the same options and behaves exactly like the default entrypoint with `era` set — except the other era's implementation is _absent_ rather than merely disabled. The `era` option is therefore ignored on the subpaths, and each entrypoint re-exports the full public type surface, so you never need to import from two paths at once.

> [!TIP]
> Serving one era from a subpath is the right default for a new server that only needs to reach modern clients, or for an existing deployment frozen on the legacy revisions. Reach for the dual entrypoint when your clients are mixed — the extra ~15kb buys you compatibility with both.

## Which Methods Exist Where

Shared by both eras:

`tools/list` · `tools/call` · `resources/list` · `resources/read` · `resources/templates/list` · `prompts/list` · `prompts/get` · `completion/complete`

Modern only:

`server/discover` · `subscriptions/listen`

Legacy only:

`initialize` · `notifications/initialized` · `ping` · `notifications/cancelled` · `notifications/progress` · `resources/subscribe` · `resources/unsubscribe` · `logging/setLevel`

:read-more{to="/guide/api/methods" title="Full method reference"}

## Version Constants

Each era entrypoint exports the version it serves, so you never have to hardcode a date string:

```ts
import { MCP_PROTOCOL_VERSION } from "h3-mcp/modern"; // "2026-07-28"
import { MCP_LEGACY_PROTOCOL_VERSION } from "h3-mcp/legacy"; // "2025-11-25", reported by `initialize`
```

The version negotiated for the request in flight is always on the request context as `event.context.mcp.protocolVersion`, whichever entrypoint you mounted.

:read-more{to="/guide/api/request-context" title="Request context"}
