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.
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.
#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:
// 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.