
# Request Context

> `event.context.mcp` — everything about the request in flight.

Every handler receives the H3 event, and `event.context.mcp` carries the MCP state for that request. The property is typed via h3's `H3EventContext` augmentation, and it is optional (`mcp?`) because the same event type is used outside MCP routes — use `event.context.mcp!` inside a handler, or optional calls (`mcp.progress?.()`) if you prefer no assertions.

```ts
defineMcpTool({
  name: "context",
  handler: (event) => {
    const mcp = event.context.mcp!;
    return {
      content: [
        { type: "text", text: `${mcp.era} · ${mcp.protocolVersion} · ${mcp.clientInfo?.name}` },
      ],
    };
  },
});
```

## Fields

| Field                | Type                                      | Available                | Notes                                                                        |
| -------------------- | ----------------------------------------- | ------------------------ | ---------------------------------------------------------------------------- |
| `era`                | `"modern" \| "legacy"`                    | both                     | Which path served this request                                               |
| `protocolVersion`    | `string`                                  | both                     | Negotiated version (legacy: header; modern: `_meta`)                         |
| `requestId`          | `string \| number`                        | both                     | JSON-RPC id                                                                  |
| `signal`             | `AbortSignal`                             | both                     | Cancellation for this request                                                |
| `progressToken`      | `string \| number`                        | when the client sent one | Opts the request into a stream                                               |
| `options`            | `McpResolvedOptions`                      | both                     | The resolved handler options                                                 |
| `sessionId`          | `string`                                  | legacy, sessions on      | Current `Mcp-Session-Id`                                                     |
| `lastEventId`        | `string`                                  | legacy `GET`             | From `Last-Event-ID` or `?lastEventId=`                                      |
| `clientInfo`         | `McpImplementation`                       | modern                   | `_meta["io.modelcontextprotocol/clientInfo"]`                                |
| `clientCapabilities` | `McpClientCapabilities`                   | modern                   | Declared per request                                                         |
| `logLevel`           | `McpLogLevel`                             | both, if requested       | Minimum level the client wants (modern: `_meta`; legacy: `logging/setLevel`) |
| `trace`              | `{ traceparent?, tracestate?, baggage? }` | modern                   | W3C trace context propagated through `_meta`                                 |
| `inputResponses`     | `McpInputResponses`                       | modern, on an MRTR retry | Answers keyed by request name                                                |
| `requestState`       | `string`                                  | modern, on an MRTR retry | **Attacker-controlled** — verify it                                          |
| `transportSignal`    | `AbortSignal`                             | both, streaming          | Fires when the response stream closes; linked into `signal`                  |
| `cacheHints`         | `McpCacheHints`                           | modern                   | Hints recorded for the result decorator                                      |

## Methods

| Method                              | Purpose                                                             |
| ----------------------------------- | ------------------------------------------------------------------- |
| `progress(progress, total?, msg?)`  | Emit `notifications/progress` for this request's `progressToken`    |
| `log(level, data, logger?)`         | Emit `notifications/message`, gated on the client's requested level |
| `notify(method, params?)`           | Emit any notification on this request's stream                      |
| `requireClientCapability(...names)` | Throw `-32021` unless every capability was declared                 |

All four are optional properties, and the first three **no-op when the request has no stream**. Both eras have one; they differ only in how the client opts in:

| Era    | Opt-in                                                                                                        |
| ------ | ------------------------------------------------------------------------------------------------------------- |
| modern | `_meta.progressToken` and/or `_meta["io.modelcontextprotocol/logLevel"]` on the request                       |
| legacy | `_meta.progressToken` on the request, and/or an earlier `logging/setLevel` — plus `Accept: text/event-stream` |

So the same handler streams on both eras, and falls back to a plain JSON response on both when the client asked for nothing.

:read-more{to="/guide/modern/streaming" title="Request-scoped streaming"}

## Era Differences

Nothing in the context is a lie about the other era — fields simply stay `undefined` where they do not apply. That makes era-agnostic handlers easy:

```ts
// Works on both eras
const mcp = event.context.mcp!;
if (mcp.signal?.aborted) return;
mcp.progress?.(50, 100); // delivered on either era if the client opted in, otherwise dropped

// Era-specific
if (mcp.era === "legacy" && mcp.sessionId) {
  await touchSession(mcp.sessionId);
}
```

> [!IMPORTANT]
> Two fields come from the client and must never be trusted: `requestState` (validated only as a bounded string) and anything inside `inputResponses`. Verify them exactly as you would a request body.

:read-more{to="/guide/modern/mrtr" title="Multi round-trip requests"}
