Files
operations/docs/architecture/response-envelopes.md
glm-5.1 92936f4232 feat: implement ADR-007 subscription transport — PendingRequestMap.subscribe(), CallHandler dispatch, SSE AsyncGenerator handlers
Add remote subscription support so spokes can consume streaming operations
over pubsub transports (WebSocket, Redis). Extract checkAccess to access.ts
to break circular dep between call.ts and subscribe.ts.
2026-05-16 06:03:21 +00:00

29 KiB

status, last_updated
status last_updated
stable 2026-05-16

Response Envelopes

Types, factory functions, integration points, and constraints for the ResponseEnvelope system. See ADR-005 for the design rationale.

Problem

Only locally-defined operations return types matching their declared outputSchema. MCP operations return ContentBlock[] (or structuredContent when the tool declares outputSchema), OpenAPI operations return raw HTTP response data, and there is no way for consumers to:

  • Know what source produced a result
  • Access transport metadata (HTTP status, MCP error flags)
  • Compose operations across sources with a uniform interface
  • Access typed data from MCP tools that declare outputSchema (spec 2025-06-18+)

See ADR-005 for the full problem statement, composability analysis, and rationale.

Types

ResponseEnvelope

interface ResponseEnvelope<T = unknown> {
  data: T
  meta: ResponseMeta
}

Universal result wrapper. data holds the operation output. meta carries transport-specific metadata, discriminated on meta.source.

ResponseMeta (discriminated union)

type ResponseMeta = LocalResponseMeta | HTTPResponseMeta | MCPResponseMeta
type ResponseSource = "local" | "http" | "mcp"

LocalResponseMeta

interface LocalResponseMeta {
  source: "local"
  operationId: string
  timestamp: number  // Unix epoch milliseconds (Date.now())
}

Minimal metadata for in-process operations. operationId is the full namespace.name key. timestamp is set at wrap time.

HTTPResponseMeta

interface HTTPResponseMeta {
  source: "http"
  statusCode: number
  headers: Record<string, string>
  contentType: string
}

Preserves HTTP response metadata. Multi-value headers are joined with , per the fetch Headers specification. This does not preserve Set-Cookie as a separate header — known limitation, can be extended to Record<string, string | string[]> if needed.

MCPResponseMeta

interface MCPResponseMeta {
  source: "mcp"
  isError: boolean
  content: MCPContentBlock[]
  structuredContent?: Record<string, unknown>
  _meta?: Record<string, unknown>
}

Mirrors the MCP CallToolResult structure. Key semantics:

  • isError: true means the tool ran but returned an error result — this is NOT a CallError exception. The handler wraps the result in an envelope. Consumers check envelope.meta.isError to distinguish error results from success results.
  • structuredContent is the MCP structured output (spec version 2025-03-26+). When present, it is placed in envelope.data as the primary output.
  • content always holds the full content block array, available in meta for rendering or fallback.
  • _meta carries MCP protocol extensions.

MCPContentBlock

type MCPContentBlock =
  | { type: "text"; text: string; annotations?: MCPAnnotations }
  | { type: "image"; data: string; mimeType: string; annotations?: MCPAnnotations }
  | { type: "audio"; data: string; mimeType: string; annotations?: MCPAnnotations }
  | { type: "resource"; resource: MCPResourceContent; annotations?: MCPAnnotations }
  | { type: "resource_link"; uri: string; name: string; description?: string; mimeType?: string }

interface MCPResourceContent {
  uri: string
  mimeType?: string
  text?: string
  blob?: string
}

interface MCPAnnotations {
  audience?: Array<"user" | "assistant">
  priority?: number
  lastModified?: string
}

These types are our own definitions, decoupled from @modelcontextprotocol/sdk. The MCP adapter maps from SDK types to these types. This avoids coupling the main barrel to the MCP SDK runtime dependency.

Detection

isResponseEnvelope()

const RESPONSE_SOURCES = ["local", "http", "mcp"] as const

function isResponseEnvelope(value: unknown): value is ResponseEnvelope {
  if (typeof value !== "object" || value === null) return false
  const obj = value as Record<string, unknown>
  if (!("data" in obj) || !("meta" in obj)) return false
  if (typeof obj.meta !== "object" || obj.meta === null) return false
  return RESPONSE_SOURCES.includes((obj.meta as ResponseMeta).source as ResponseSource)
}

Used by execute() and CallHandler to detect whether a handler return value is already an envelope. Detection by meta.source discriminant is:

  • JSON-serializable (works across pubsub transport boundaries)
  • Cross-realm safe (no Symbol.for() iframe/Worker issues)
  • Low false-positive risk (the known source strings are under our control)

If false positives become a concern, a versioned brand like __envelopeVersion: 1 can be added to the envelope shape. New sources must be registered in RESPONSE_SOURCES.

Factory Functions

Adapters and infrastructure use factory functions to construct envelopes. Adapters do not construct envelope objects directly.

function localEnvelope<T>(data: T, operationId: string): ResponseEnvelope<T> {
  return { data, meta: { source: "local", operationId, timestamp: Date.now() } }
}

function httpEnvelope<T>(data: T, meta: Omit<HTTPResponseMeta, "source">): ResponseEnvelope<T> {
  return { data, meta: { source: "http", ...meta } }
}

function mcpEnvelope<T>(data: T, meta: Omit<MCPResponseMeta, "source">): ResponseEnvelope<T> {
  return { data, meta: { source: "mcp", ...meta } }
}
  • localEnvelope is used by execute() and CallHandler when wrapping raw handler return values
  • httpEnvelope is used by the OpenAPI adapter handler
  • mcpEnvelope is used by the MCP adapter handler
  • T resolves to undefined for void handlers — localEnvelope(undefined, opId) is valid

Utility

unwrap()

function unwrap<T>(envelope: ResponseEnvelope<T>): T {
  return envelope.data
}

Convenience for the common case where metadata is not needed. Equivalent to envelope.data. Named for clarity at call sites and as a search target for envelope-unwrapping patterns.

Schema Constants

TypeBox schemas for use in validation, spec definitions, and call protocol events:

const ResponseEnvelopeSchema = Type.Object({
  data: Type.Unknown({ description: "Operation output" }),
  meta: ResponseMetaSchema,
})

const ResponseMetaSchema = Type.Union([
  LocalResponseMetaSchema,
  HTTPResponseMetaSchema,
  MCPResponseMetaSchema,
])

Individual meta schemas (LocalResponseMetaSchema, HTTPResponseMetaSchema, MCPResponseMetaSchema, MCPContentBlockSchema) are defined inline in the source. See src/response-envelope.ts for the exact definitions.

Integration Points

Shared Result Pipeline

Both OperationRegistry.execute() and CallHandler follow the same result processing pipeline after obtaining a handler result. This ensures consistent behavior whether the operation is invoked directly or through the call protocol:

  1. Detect envelope: If result isResponseEnvelope() → pass through as-is (adapter handlers return pre-built envelopes)
  2. Wrap: Otherwise → wrap in localEnvelope(result, operationId)
  3. Normalize: If spec.outputSchema is not Type.Unknown(), apply Value.Cast(spec.outputSchema, envelope.data) — strips excess properties, fills defaults, upcasts values
  4. Validate: Check envelope.data against spec.outputSchema with collectErrors()warning-only, logged but not thrown

The order matters: normalization happens before validation, so warnings reflect the normalized data shape. Value.Cast() may silently fix data that would otherwise fail validation (e.g., filling defaults for optional fields), and the validation step catches any remaining mismatches.

Result Pipeline Differences

Aspect execute() CallHandler
Access control Not checked (trusted internal calls) Checks AccessControl before dispatch
Error handling Throws CallError directly Publishes call.error via pubsub
Publishing Returns envelope directly Publishes call.responded via callMap.respond()
Context Direct call, no requestId Has requestId, parentRequestId, identity, deadline

OperationRegistry.execute()

Returns Promise<ResponseEnvelope<TOutput>> instead of Promise<TOutput>.

Flow:

  1. Look up spec and handler (existing)
  2. Validate input with validateOrThrow (existing)
  3. Await handler result
  4. Apply shared result pipeline (detect → wrap → normalize → validate)
  5. Return envelope

Note: isResponseEnvelope() does not validate that the envelope's source matches the operation's origin. An MCP handler that explicitly returns a localEnvelope(...) passes through as-is. Handlers that explicitly construct envelopes take responsibility for their metadata.

CallHandler

Takes full ownership of publishing call.responded. Handlers return values; they do NOT publish events.

Flow:

  1. Look up spec by operationId from the registry via getSpec()
  2. If not found, throw CallError(OPERATION_NOT_FOUND, ...)
  3. Look up handler by operationId via getHandler()
  4. If not found, throw CallError(OPERATION_NOT_FOUND, "No handler registered for operation: ...")
  5. Check access control (existing)
  6. Validate input with validateOrThrow (existing)
  7. Call handler and await result
  8. Apply shared result pipeline (detect → wrap → normalize → validate)
  9. Publish call.responded via callMap.respond(requestId, envelope)
  10. On handler exception → publish call.error (existing). Note: an envelope with meta.isError: true does not trigger call.error. Only thrown exceptions do.

PendingRequestMap.respond()

Enforces that output is a ResponseEnvelope via the isResponseEnvelope() type guard (not full schema validation). If called with a non-envelope value, throws. This prevents any code bypassing CallHandler's envelope wrapping. A future iteration may make respond() internal (not exported on the public API surface) to further enforce this invariant.

PendingRequestMap.call()

Resolves with the ResponseEnvelope from call.responded.output instead of raw unknown.

subscribe()

Each yielded value is wrapped in ResponseEnvelope. SubscriptionHandler implementations still yield raw values — subscribe() wraps each yield, consistent with how local handlers return raw values and execute() wraps them. If a yielded value isResponseEnvelope(), it passes through. Otherwise, localEnvelope(value, operationId) with a fresh timestamp per yield.

buildEnv()

Each function in the returned OperationEnv returns Promise<ResponseEnvelope> instead of Promise<unknown>.

MCP Adapter (from_mcp.ts)

data shape and composability

MCP tool results have two shapes depending on whether the tool declares outputSchema:

  • With outputSchema (MCP spec 2025-06-18+): structuredContent contains the typed output matching the tool's declared schema. The from_mcp adapter converts the tool's JSON Schema outputSchema to a TypeBox schema via FromSchema and sets it as the operation's outputSchema. At call time, envelope.data is structuredContent, which matches outputSchema. These operations are fully composable with local operations — the consumer gets typed data just like calling a local handler.

  • Without outputSchema: No typed output is available. The operation's outputSchema is Type.Unknown(). envelope.data is MCPContentBlock[] (the raw content blocks). These operations are not composable — the consumer must inspect content blocks heuristically. Some MCP servers return JSON.stringify'd text in content blocks rather than structured output, making programmatic consumption even harder.

When structuredContent is present, it is the primary data path. When absent, content is the only data path. This means envelope.data has a different shape depending on the MCP server version and tool configuration. Consumers that need a consistent shape should check meta.source === "mcp" and meta.structuredContent to determine the access pattern.

outputSchema extraction

The from_mcp adapter SHOULD extract outputSchema from MCP tool definitions (when available) and convert it to a TypeBox schema:

// In createMCPClient, when building the operation spec:
outputSchema: tool.outputSchema
  ? FromSchema(tool.outputSchema)   // Convert JSON Schema to TypeBox
  : Type.Unknown()                   // No schema available

This enables outputSchema validation on envelope.data for MCP operations that declare their output schema, making them composable with local operations.

Envelope stripping with Value.Cast()

When an MCP operation has outputSchema and returns structuredContent, the structuredContent data can be cast against the TypeBox schema to strip excess properties and normalize it:

import { Value } from "@alkdev/typebox/value"

// In the MCP handler, after getting structuredContent:
const data = result.structuredContent
  ? Value.Cast(spec.outputSchema, result.structuredContent)
  : mapMCPContentBlocks(result.content)

Value.Cast() upcasts the structuredContent value into the target type, retaining matching properties, filling defaults for missing ones, and stripping properties not in the schema. This ensures envelope.data reliably matches outputSchema — the same guarantee that local operations provide.

For MCP operations without outputSchema (where outputSchema is Type.Unknown()), Value.Cast() against Type.Unknown() is a no-op (accepts any value), so the data shape remains unpredictable. The composability gap exists only for these operations.

This same pattern applies to OpenAPI responses: the parsed JSON can be passed through Value.Cast(spec.outputSchema, data) to normalize it against the operation's outputSchema.

Handler behavior change

handler: async (input, context) => {
  const result = await client.callTool({ name, arguments: input })
  // MCP isError: true means the tool ran but returned an error result.
  // Wrap in envelope — consumer checks meta.isError.
  // Transport-level errors (tool not found, connection failure) still throw CallError.
  return mcpEnvelope(
    result.structuredContent ?? mapMCPContentBlocks(result.content),
    {
      isError: result.isError ?? false,
      content: mapMCPContentBlocks(result.content),
      structuredContent: result.structuredContent as Record<string, unknown> | undefined,
      _meta: result._meta as Record<string, unknown> | undefined,
    },
  )
}

Key differences from current behavior:

  • isError: true no longer throws. Error content is accessible via envelope.data and envelope.meta.content. The consumer checks envelope.meta.isError.
  • structuredContent is preferred as data when available (MCP spec 2025-03-26+)
  • Raw content blocks are always available in meta.content
  • mapMCPContentBlocks() maps SDK ContentBlock[] to our MCPContentBlock[] (1:1 field mapping, decoupling us from the MCP SDK at the interface level). Signature: (sdkBlocks: SDKContentBlock[]) => MCPContentBlock[]. Unknown content block types (from newer MCP spec versions that our MCPContentBlock union doesn't yet cover) are mapped to { type: "text", text: JSON.stringify(block) } as a fallback — preserving the data without crashing.

OpenAPI Adapter (from_openapi.ts)

QUERY / MUTATION handler

Handler behavior for single-return operations:

handler: async (input, context) => {
  // ... existing URL construction and fetch logic ...
  const response = await fetch(url, fetchOptions)

  if (!response.ok) {
    throw new CallError("EXECUTION_ERROR", `HTTP ${response.status}: ${response.statusText}`)
  }

  const contentType = response.headers.get("Content-Type") || ""
  let data: unknown
  if (contentType.includes("application/json")) {
    data = await response.json()
  } else if (contentType.includes("text/")) {
    data = await response.text()
  } else {
    data = await response.arrayBuffer()
  }

  return httpEnvelope(data, {
    statusCode: response.status,
    headers: Object.fromEntries(response.headers.entries()),
    contentType,
  })
}
  • On HTTP error status → throw CallError (not an envelope — these are transport errors)
  • Success responses → wrapped in httpEnvelope with full HTTP metadata
  • Headers are a Record<string, string> snapshot (multi-value headers joined with , per fetch spec)

SUBSCRIPTION handler (SSE)

For SUBSCRIPTION-type operations (detected by text/event-stream in response content type), the handler is an AsyncGenerator that:

handler: async function* (input, context) => {
  // ... URL construction and fetch logic (same as QUERY/MUTATION) ...
  const response = await fetch(url, fetchOptions)

  if (!response.ok) {
    throw new CallError("EXECUTION_ERROR", `HTTP ${response.status}: ${response.statusText}`)
  }

  // Parse the SSE stream
  const reader = response.body!.getReader()
  const decoder = new TextDecoder()
  let buffer = ""
  let eventType = ""
  let data = ""
  let lastEventId = ""

  try {
    while (true) {
      const { done, value } = await reader.read()
      if (done) break

      buffer += decoder.decode(value, { stream: true })
      // Parse SSE frames from buffer
      // ... (see adapters.md for full parsing rules) ...

      // When a complete event is dispatched:
      yield httpEnvelope(parsedData, {
        statusCode: response.status,
        headers: Object.fromEntries(response.headers.entries()),
        contentType: "text/event-stream",
      })
    }
  } finally {
    reader.releaseLock()
  }
}
  • Each SSE event is yielded as a ResponseEnvelope with meta.contentType: "text/event-stream"
  • The SSE event type and id fields are not carried in the envelope — a future SSEResponseMeta source type may be added if per-event metadata is needed
  • On HTTP error status → throw CallError from the generator body before first yield
  • On stream parse error → log warning, skip malformed frame, continue
  • finally block closes the ReadableStream reader

Value.Cast() for Data Normalization

The shared result pipeline (defined above) includes Value.Cast() normalization as step 3. This section provides additional context on how Value.Cast() works and why it matters for each source:

  • Local operations: Value.Cast() provides normalization against the declared outputSchema, stripping excess properties and filling defaults — a stronger guarantee than Value.Check() validation alone (which only warns).
  • MCP operations with outputSchema: Value.Cast() normalizes structuredContent against the TypeBox-converted schema, stripping any extra properties the MCP server may have added beyond the declared schema. This makes MCP data composable with local operations.
  • OpenAPI operations: Value.Cast() normalizes the parsed HTTP response body against the operation's outputSchema, ensuring consistent data shapes.

Operations with Type.Unknown() outputSchema (typically MCP tools without outputSchema) are excluded — Value.Cast() against Type.Unknown() is a no-op (accepts any value).

CallEventSchema

call.responded.output changes from Type.Unknown() to ResponseEnvelopeSchema:

"call.responded": Type.Object({
  requestId: Type.String(),
  output: ResponseEnvelopeSchema,
})

Type Erasure at Runtime Boundaries

ResponseEnvelope<T> carries a compile-time type parameter T, but at the call protocol boundary (pubsub serialization, WebSocket transport), T is erased to unknown. This means:

  • registry.execute<TInput, TOutput>() returns Promise<ResponseEnvelope<TOutput>> — type-safe at compile time
  • PendingRequestMap.call() returns Promise<ResponseEnvelope>TOutput is not available (the caller doesn't know the operation's output type without a spec lookup)
  • subscribe() yields AsyncGenerator<ResponseEnvelope, void, unknown> — similarly untyped

The runtime guarantee for envelope.data shape is outputSchema + Value.Cast() normalization (step 3 of the shared result pipeline). When outputSchema is Type.Unknown(), no runtime shape guarantee exists — the consumer must handle arbitrary data.

outputSchema and Validation`

outputSchema on OperationSpec validates envelope.data, not the full ResponseEnvelope. The envelope has its own ResponseEnvelopeSchema for call protocol validation. This keeps outputSchema focused on business data shape — no envelope schema bloat, and existing outputSchema definitions don't need changes.

The shared result pipeline (see Shared Result Pipeline) applies two steps to envelope.data after wrapping:

  1. Normalization (Value.Cast): When outputSchema is meaningful (not Type.Unknown()), Value.Cast(spec.outputSchema, envelope.data) normalizes the data — strips excess properties, fills defaults for missing ones, upcasts values to match the declared type. Normalization happens before validation.
  2. Warning validation (collectErrors + formatValueErrors): Checks envelope.data against spec.outputSchema and logs warnings on mismatch. Validation happens after normalization, so it checks the normalized data shape.

The normalization step is optional: if outputSchema is Type.Unknown(), normalization is skipped (any value is accepted). This preserves the current behavior for MCP tools without outputSchema.

Constraints

  1. No runtime envelope overhead for local handlers — local handlers return raw values; wrapping happens in infrastructure (execute(), CallHandler), not in user code
  2. Envelope always present at call protocol boundarycall.responded always carries ResponseEnvelope, never raw values. PendingRequestMap.respond() enforces this.
  3. Adapters use factory functionslocalEnvelope(), httpEnvelope(), mcpEnvelope(). Adapters do not construct envelope objects directly.
  4. MCPContentBlock types are our own — not MCP SDK types. Avoids runtime dependency on @modelcontextprotocol/sdk in the main barrel.
  5. Breaking change: single-release, no transitional APIexecute() return type and call.responded shape change in one release. Package is pre-1.0, call protocol is draft, consumers are coordinated.
  6. unwrap() is the simple-path API — for consumers that don't need metadata
  7. outputSchema validates envelope.dataResponseEnvelopeSchema validates the envelope structure; outputSchema validates business data
  8. isResponseEnvelope() is the sole detection mechanism — no Symbol brands, no duck-typing beyond the closed source discriminant set

Open Questions

  1. Client abstraction — Envelopes provide metadata and unwrap() provides simple access, but real usage may reveal patterns that justify a higher-level abstraction: e.g., a client that auto-handles MCP isError results (throwing on error content vs. returning it), or one that extracts pagination cursors from HTTP headers. Deferred until usage patterns from alkhub and opencode confirm whether unwrap() + meta.source dispatch is sufficient or whether a typed "client" per source adds real value.

  2. SSEResponseMeta — SSE events currently use httpEnvelope() with contentType: "text/event-stream". The SSE event type and id fields are dropped by the parser — they are not available in the ResponseEnvelope. The data field value (typically JSON) is the primary envelope.data payload. A future SSEResponseMeta with source: "sse", eventType: string, lastEventId: string could carry this per-event metadata if usage patterns confirm the need. See ADR-007.

  3. respond() visibility — Resolved: respond() remains public on PendingRequestMap. The call protocol is the integration surface for spoke/hub SDKs (see amended ADR-006), which means spokes need respond() for publishing call.responded events back to the hub. The envelope invariant is still enforced by the isResponseEnvelope() guard.

  4. Envelope directionality (serving) — The current design covers consuming remote operations (MCP, OpenAPI) and wrapping their results. The reverse direction — serving local operations via MCP or OpenAPI — is not yet addressed. When exposing operations as an MCP server, results must be wrapped in MCP's CallToolResult format (structuredContent + content). When exposing as OpenAPI, results must be serialized as HTTP response bodies. How envelopes interact with this serving direction is an open design question.

  5. JSON.stringify in MCP content blocks — Some MCP servers return JSON.stringify'd data in text content blocks instead of using structuredContent. The from_mcp adapter could attempt JSON.parse() on text content when structuredContent is absent, but this is fragile (not all text content is JSON). If the operation has a meaningful outputSchema, Value.Cast() could normalize the parsed result against the schema, making the consumer's experience consistent regardless of whether the MCP server uses structuredContent or stringified JSON. Whether this JSON.parse heuristic belongs in the adapter or in consumer code is unresolved.

  6. MCP outputSchema extraction completeness — The MCP spec (2025-06-18+) allows tools to declare outputSchema, but many existing servers don't. The adapter's FromSchema conversion of MCP outputSchema to TypeBox may encounter JSON Schema features that FromSchema doesn't handle. The fallback is Type.Unknown(), same as the no-schema case.

Migration Checklist

The following documentation changes have been completed:

Document Section Change Status
call-protocol.md CallHandler Handler no longer publishes call.responded; returns values. CallHandler wraps and publishes.
call-protocol.md PendingRequestMap respond() validates envelope via isResponseEnvelope(); resolves with ResponseEnvelope instead of unknown
call-protocol.md CallEventMap call.responded.output changes from Type.Unknown() to ResponseEnvelopeSchema
api-surface.md execute() Return type: Promise<TOutput>Promise<ResponseEnvelope<TOutput>>
api-surface.md PendingRequestMap.call() Resolve type: unknownResponseEnvelope
api-surface.md subscribe() Yield type: unknownResponseEnvelope
api-surface.md OperationEnv Inner function return type: Promise<unknown>Promise<ResponseEnvelope>
api-surface.md CallHandler Wraps handler result, publishes call.responded. No longer "handler publishes" model.
call-protocol.md PendingRequestMap.respond() Now enforces isResponseEnvelope() check — throws on raw values.
api-surface.md PendingRequestMap.respond() respond() now requires ResponseEnvelope argument.
adapters.md from_mcp Handler returns mcpEnvelope(). MCP isError: true no longer throws.
adapters.md from_mcp outputSchema extracted when available, via FromSchema. Falls back to Type.Unknown().
adapters.md from_openapi Handler returns httpEnvelope(). Error on HTTP error status still throws CallError.

The following documentation changes are pending for the subscription transport feature:

Document Section Change Status
adapters.md FromOpenAPI SSE handlers Subscription handler as AsyncGenerator, SSE parsing, per-yield envelope (doc updated)
call-protocol.md PendingRequestMap Add subscribe() method for remote subscriptions (doc updated)
call-protocol.md CallHandler Dispatch on operation type: execute() for QUERY/MUTATION, subscribe() for SUBSCRIPTION (doc updated)
call-protocol.md Transport Mapping Add WebSocket topology diagram, mention subscribe() over transport (doc updated)
api-surface.md PendingRequestMap Add subscribe() method to the table (doc updated)
api-surface.md Subscribe Add SSE operations note, mention PendingRequestMap.subscribe() for remote (doc updated)
decisions/007-subscription-transport.md New ADR SSE subscription handler, PendingRequestMap.subscribe(), CallHandler dispatch (doc created)
response-envelopes.md OpenAPI adapter Separate QUERY/MUTATION handler from SUBSCRIPTION handler (SSE) (doc updated)
response-envelopes.md Open Questions Replace subscription envelopes question with SSEResponseMeta question (doc updated)

The following code changes are pending:

Code Change Status
src/from_openapi.ts Generate SubscriptionHandler (AsyncGenerator) for SUBSCRIPTION operations, parse SSE stream, yield per-event Implemented
src/call.ts Add PendingRequestMap.subscribe() method using Repeater from @alkdev/pubsub Implemented
src/call.ts Update CallHandler to dispatch on operation type Implemented
src/subscribe.ts Ensure subscribe() handles httpEnvelope detection for SSE yields Already handles envelopes

References