h3-mcp logoh3-mcp

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

FieldTypeAvailableNotes
era"modern" \| "legacy"bothWhich path served this request
protocolVersionstringbothNegotiated version (legacy: header; modern: _meta)
requestIdstring \| numberbothJSON-RPC id
signalAbortSignalbothCancellation for this request
progressTokenstring \| numberwhen the client sent oneOpts the request into a stream
optionsMcpResolvedOptionsbothThe resolved handler options
sessionIdstringlegacy, sessions onCurrent Mcp-Session-Id
lastEventIdstringlegacy GETFrom Last-Event-ID or ?lastEventId=
clientInfoMcpImplementationmodern_meta["io.modelcontextprotocol/clientInfo"]
clientCapabilitiesMcpClientCapabilitiesmodernDeclared per request
logLevelMcpLogLevelboth, if requestedMinimum level the client wants (modern: _meta; legacy: logging/setLevel)
trace{ traceparent?, tracestate?, baggage? }modernW3C trace context propagated through _meta
inputResponsesMcpInputResponsesmodern, on an MRTR retryAnswers keyed by request name
requestStatestringmodern, on an MRTR retryAttacker-controlled — verify it
transportSignalAbortSignalboth, streamingFires when the response stream closes; linked into signal
cacheHintsMcpCacheHintsmodernHints recorded for the result decorator

#Methods

MethodPurpose
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:

EraOpt-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 in 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:

// 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 in Multi round-trip requests.
h3-mcp logo

h3-mcp  MCP servers, built on H3.