From ddc0607b9048a39f82cc281cdcf22b4875a90cfd Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Mon, 11 May 2026 02:55:13 +0000 Subject: [PATCH] docs: clean up ADR-005 architecture docs after envelope implementation Remove stale ADR-005 drift tables across all architecture docs since ResponseEnvelope types, factories, detection, and integration points are now fully implemented in source code. Key changes: - api-surface.md: Remove ADR-005 drift table (all items implemented), retain ADR-006 drift table without execute() return type (now done) - call-protocol.md: Remove ADR-005 drift table, update ADR-006 table, fix CallHandlerConfig to show callMap? (current source) - adapters.md: Remove 'current source state' and 'implementation changes needed' tables for from_mcp and from_openapi, replace with current-accurate descriptions of envelope behavior - response-envelopes.md: Remove 'current source state' blocks, update migration checklist to show all code changes completed - 005-response-envelopes.md: Change status from Draft to Implemented - 006-unified-invocation-path.md: Update Prerequisites section to note ADR-005 is now implemented - build-distribution.md: Add response-envelope.ts to source layout - architecture.md: Add response-envelopes.md link and ADR-005/006 entries to design decisions table - README.md: Add response-envelopes.md to documents table - Update last_updated dates on all changed docs --- docs/architecture.md | 5 +- docs/architecture/README.md | 3 +- docs/architecture/adapters.md | 30 +++------- docs/architecture/api-surface.md | 20 +------ docs/architecture/build-distribution.md | 3 +- docs/architecture/call-protocol.md | 20 +------ .../decisions/005-response-envelopes.md | 2 +- .../decisions/006-unified-invocation-path.md | 4 +- docs/architecture/response-envelopes.md | 60 ++++++------------- 9 files changed, 40 insertions(+), 107 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 6c18f73..fe6e699 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -7,6 +7,7 @@ | [architecture/README.md](architecture/README.md) | Overview, why this exists, what it provides, consumer context, threat model | | [architecture/api-surface.md](architecture/api-surface.md) | All public types, registry API, call protocol API, subscribe, env, adapters | | [architecture/call-protocol.md](architecture/call-protocol.md) | PendingRequestMap, CallHandler, call≡subscribe semantics, events, error model, access control | +| [architecture/response-envelopes.md](architecture/response-envelopes.md) | Response envelope types, factory functions, detection, schemas, integration points | | [architecture/adapters.md](architecture/adapters.md) | from_schema, from_openapi, from_mcp, scanner — how they work, how to add new adapters | | [architecture/build-distribution.md](architecture/build-distribution.md) | Dependencies, project structure, sub-path exports, peer deps, build tooling | @@ -17,4 +18,6 @@ | [001](architecture/decisions/001-logger-direct-import.md) | Direct @logtape/logtape import instead of wrapper module | | [002](architecture/decisions/002-fs-injection.md) | Inject filesystem dependencies for runtime agnosticism | | [003](architecture/decisions/003-peer-dep-adapters.md) | Peer dependencies for adapter isolation (MCP SDK, @std/path) | -| [004](architecture/decisions/004-schema-const-naming.md) | Schema const naming convention (AccessControlSchema + AccessControl type) | \ No newline at end of file +| [004](architecture/decisions/004-schema-const-naming.md) | Schema const naming convention (AccessControlSchema + AccessControl type) | +| [005](architecture/decisions/005-response-envelopes.md) | Response Envelopes for transport-aware results | +| [006](architecture/decisions/006-unified-invocation-path.md) | Unified Invocation Path (execute as single entry point) | \ No newline at end of file diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1523e3e..4b759fb 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-05-09 +last_updated: 2026-05-11 --- # @alkdev/operations Architecture @@ -74,6 +74,7 @@ Spokes will import `@alkdev/operation` for operation definitions and `@alkdev/pu |----------|---------| | [api-surface.md](api-surface.md) | All public types, registry, call protocol, subscribe, env, adapters | | [call-protocol.md](call-protocol.md) | PendingRequestMap, CallHandler, call≡subscribe, events, error model, access control | +| [response-envelopes.md](response-envelopes.md) | Response envelope types, factory functions, detection, schemas, integration points | | [adapters.md](adapters.md) | from_schema, from_openapi, from_mcp, scanner — how they work, how to add new adapters | | [build-distribution.md](build-distribution.md) | Dependencies, project structure, sub-path exports, peer deps, build tooling | diff --git a/docs/architecture/adapters.md b/docs/architecture/adapters.md index 2dd1ed9..1e54da7 100644 --- a/docs/architecture/adapters.md +++ b/docs/architecture/adapters.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-05-09 +last_updated: 2026-05-11 --- # Adapters @@ -79,13 +79,7 @@ Processes all paths in the spec. For each path and method combination: - Applies auth headers from config - Returns JSON, text, or `ArrayBuffer` based on response content type -**Current source state** (`src/from_openapi.ts`): The handler currently returns raw response data — `response.json()`, `response.text()`, or `response.arrayBuffer()` (lines 273-279). It does NOT wrap the result in `httpEnvelope()`. Error handling throws a plain `Error` (line 268) instead of `CallError`. The response-envelopes spec requires wrapping in `httpEnvelope()` and throwing `CallError` on HTTP errors. The `Value.Cast()` normalization step against `outputSchema` is also not yet implemented. See [response-envelopes.md](response-envelopes.md) for the full specification. - -| What | Current source (`src/from_openapi.ts`) | Target (per response-envelopes spec) | -|------|------------------------------------------|---------------------------------------| -| Handler return value | Returns raw `response.json()` / `.text()` / `.arrayBuffer()` (lines 273-279) | Returns `httpEnvelope(data, { statusCode, headers, contentType })` | -| Error handling | Throws `Error(\`HTTP ${status}\`)` (line 268) | Throws `CallError("EXECUTION_ERROR", ...)` | -| `Value.Cast()` | Not used | If `outputSchema !== Unknown`, cast `Value.Cast(outputSchema, data)` | +The handler wraps results in `httpEnvelope()` with HTTP metadata (status code, headers, content type). On HTTP error status, it throws `CallError("EXECUTION_ERROR", ...)`. `Value.Cast()` normalization against `outputSchema` is applied by `registry.execute()` and `CallHandler` as part of the shared result pipeline — see [response-envelopes.md](response-envelopes.md#shared-result-pipeline). ### `FromOpenAPIFile(path, config, fs?)` @@ -179,11 +173,11 @@ async function createMCPClient( - `namespace`: the `name` parameter (used as grouping) - `type`: `MUTATION` (all MCP tools are mutations) ### `FromSchema(tool.inputSchema)` (converts JSON Schema to TypeBox) - - `outputSchema`: `tool.outputSchema ? FromSchema(tool.outputSchema) : Type.Unknown()` (MCP spec 2025-06-18+ provides `outputSchema`; older tools lack it) - - `handler`: calls `client.callTool({ name, arguments })`, wraps result in `mcpEnvelope()` - - `accessControl`: `{ requiredScopes: [] }` (no auth by default) + - `outputSchema`: `tool.outputSchema ? FromSchema(tool.outputSchema) : Type.Unknown()` (MCP spec 2025-06-18+ provides `outputSchema`; older tools lack it) + - `handler`: calls `client.callTool({ name, arguments })`, wraps result in `mcpEnvelope()` + - `accessControl`: `{ requiredScopes: [] }` (no auth by default) -**Current source state** (`src/from_mcp.ts` line 66): `outputSchema: Type.Unknown()` for all tools. The `tool.outputSchema` property is not used. The handler currently throws on `isError` (line 76) and returns `result.content` directly (line 79) instead of wrapping in `mcpEnvelope()`. The `structuredContent` field on `CallToolResult` is not used. See [response-envelopes.md](response-envelopes.md) for the full specification of what needs to change. +The handler returns pre-built `ResponseEnvelope` instances via `mcpEnvelope()`. `isError: true` results are wrapped in the envelope (not thrown), so consumers check `envelope.meta.isError`. `structuredContent` is preferred as `envelope.data` when available; otherwise `mapMCPContentBlocks(result.content)` is used. `Value.Cast()` normalization against `outputSchema` is applied by `registry.execute()` and `CallHandler` as part of the shared result pipeline. ### outputSchema and structuredContent @@ -203,17 +197,7 @@ When a tool does NOT declare `outputSchema`: See [response-envelopes.md](response-envelopes.md) for the full envelope specification and envelope stripping with `Value.Cast()`. -**Implementation changes needed** (tracking spec vs. current source): - -| What | Current source (`src/from_mcp.ts`) | Target (per response-envelopes spec) | -|------|--------------------------------------|---------------------------------------| -| `outputSchema` at discovery | `Type.Unknown()` for all tools (line 66) | `tool.outputSchema ? FromSchema(tool.outputSchema) : Type.Unknown()` | -| Handler return value | Returns `result.content` (line 79) | Returns `mcpEnvelope(data, meta)` | -| `structuredContent` | Not used | Prefer as `data` when present; fall back to `mapMCPContentBlocks(result.content)` | -| `isError` handling | Throws `Error` (line 76) | Wraps in envelope with `meta.isError: true`, does NOT throw | -| `Value.Cast()` | Not used | If `structuredContent && outputSchema !== Unknown`, cast `Value.Cast(outputSchema, structuredContent)` | - -The `CallToolResult` type in the installed SDK (`@modelcontextprotocol/sdk` DRAFT-2026-v1) already includes `structuredContent?: { [key: string]: unknown }` and the `Tool` type already includes `outputSchema?`. No SDK upgrade needed — only the adapter code needs updating. +The `CallToolResult` type in the installed SDK (`@modelcontextprotocol/sdk` DRAFT-2026-v1) includes `structuredContent?: { [key: string]: unknown }` and the `Tool` type includes `outputSchema?`. The adapter code extracts `outputSchema` at discovery time and uses `structuredContent` at call time. ### `MCPClientConfig` diff --git a/docs/architecture/api-surface.md b/docs/architecture/api-surface.md index 3cdae3e..818dc88 100644 --- a/docs/architecture/api-surface.md +++ b/docs/architecture/api-surface.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-05-10 +last_updated: 2026-05-11 --- # API Surface @@ -373,23 +373,7 @@ See [adapters.md](adapters.md) for detailed adapter documentation. ## Source vs. Spec Drift -This section documents differences between the architecture spec (this document) and the current source code. Items marked **ADR-005** or **ADR-006** are planned changes not yet implemented. - -### ADR-005 (Response Envelopes) — not yet implemented - -| What | Spec says | Source currently does | -|------|----------|----------------------| -| `ResponseEnvelope`, `ResponseMeta`, factory functions, `isResponseEnvelope()`, `unwrap()` | Exported from `src/response-envelope.ts` | None of these types or functions exist in source | -| `execute()` return type | `Promise>` | `Promise` | -| `execute()` result pipeline | Detect → wrap → normalize → validate | Returns raw `result`, validates raw output with `collectErrors` | -| `OperationEnv` inner function return type | `Promise` | `Promise` | -| `PendingRequestMap.call()` return type | `Promise` | `Promise` | -| `PendingRequestMap.respond()` validation | Enforces `isResponseEnvelope()`, throws on raw values | Accepts `unknown`, no validation | -| `subscribe()` yield type | `AsyncGenerator` | `AsyncGenerator` | -| `CallRespondedEvent.output` | `ResponseEnvelope` | `unknown` | -| `CallHandler` description | Wraps handler result, applies pipeline, publishes `call.responded` | Discards handler return value; handler publishes `call.responded` itself | -| `from_mcp` handler | Returns `mcpEnvelope()`, uses `structuredContent`, extracts `outputSchema` | Returns `result.content`, types `outputSchema` as `Type.Unknown()`, throws on `isError` | -| `from_openapi` handler | Returns `httpEnvelope()` with HTTP metadata | Returns raw response data, throws on HTTP error status | +This section documents differences between the architecture spec and the current source code. ADR-005 (Response Envelopes) has been fully implemented — all envelope types, factories, detection, and integration points are in source and match the spec. ADR-006 (Unified Invocation Path) is not yet implemented. ### ADR-006 (Unified Invocation Path) — not yet implemented diff --git a/docs/architecture/build-distribution.md b/docs/architecture/build-distribution.md index a50ae64..537693b 100644 --- a/docs/architecture/build-distribution.md +++ b/docs/architecture/build-distribution.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-05-09 +last_updated: 2026-05-11 --- # Build & Distribution @@ -43,6 +43,7 @@ Dependencies, project structure, sub-path exports, peer deps, and build tooling. types.ts # Core types: IOperationDefinition, OperationSpec, OperationType, etc. registry.ts # OperationRegistry: registerSpec, registerHandler, execute, get, list validation.ts # assertIsSchema, validateOrThrow, collectErrors, formatValueErrors + response-envelope.ts # ResponseEnvelope types, factories, detection, schemas, unwrap call.ts # PendingRequestMap, buildCallHandler, CallEventMap, event types subscribe.ts # subscribe(): direct AsyncGenerator execution env.ts # buildEnv(): namespace-keyed env with direct/call-protocol modes diff --git a/docs/architecture/call-protocol.md b/docs/architecture/call-protocol.md index 90b76dc..ddc9ea5 100644 --- a/docs/architecture/call-protocol.md +++ b/docs/architecture/call-protocol.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-05-10 +last_updated: 2026-05-11 --- # Call Protocol @@ -159,7 +159,7 @@ function buildCallHandler(config: CallHandlerConfig): CallHandler interface CallHandlerConfig { registry: OperationRegistry - eventTarget?: EventTarget + callMap?: PendingRequestMap } type CallHandler = (event: CallRequestedEvent) => Promise @@ -309,20 +309,7 @@ This allows spec-only registration for scenarios where handlers are provided sep ## Source vs. Spec Drift -This section documents differences between the architecture spec (this document) and the current source code. Items are planned changes not yet implemented. - -### ADR-005 (Response Envelopes) — not yet implemented - -| What | Spec says | Source currently does | -|------|----------|----------------------| -| `CallEventSchema["call.responded"].output` | `ResponseEnvelopeSchema` | `Type.Unknown()` | -| `CallHandler` behavior | Wraps handler return value, publishes `call.responded` | Discards handler return value; handler must publish itself | -| `CallHandler` error handling | Publishes `call.error` via pubsub | Re-throws `CallError` (does not publish) | -| `call()` return type | `Promise` | `Promise` | -| `call()` resolution | Resolves with `ResponseEnvelope` from `output` field | Resolves with raw `unknown` from `output` | -| `respond()` validation | Enforces `isResponseEnvelope()` guard, throws on raw values | Accepts `unknown`, no validation | -| `subscribe()` yield type | `AsyncGenerator`, wraps yields | `AsyncGenerator`, yields raw values | -| `buildEnv()` return types | `Promise` per function | `Promise` per function | +This section documents differences between the architecture spec (this document) and the current source code. ADR-005 (Response Envelopes) has been fully implemented — `CallEventSchema["call.responded"].output` uses `ResponseEnvelopeSchema`, `CallHandler` wraps handler return values and publishes `call.responded`, `call()` returns `Promise`, `respond()` enforces `isResponseEnvelope()`, `subscribe()` yields `ResponseEnvelope`, and `buildEnv()` returns `Promise` per function. ADR-006 (Unified Invocation Path) is not yet implemented. ### ADR-006 (Unified Invocation Path) — not yet implemented @@ -333,7 +320,6 @@ This section documents differences between the architecture spec (this document) | `CallHandler` calls `execute()` | Thin adapter that calls `registry.execute()` internally | Reimplements lookup, validation, and access control independently | | `buildEnv()` | Always uses `execute()`, no `callMap` option | Toggles between `execute()` and `callMap.call()` via `if (callMap)` | | `OperationContext.trusted` | New field for nested call bypass | Does not exist | -| `execute()` return type | `Promise>` | `Promise` | | `execute()` error type | Throws `CallError` | Throws plain `Error` | ## References diff --git a/docs/architecture/decisions/005-response-envelopes.md b/docs/architecture/decisions/005-response-envelopes.md index 6c860ec..a8a8292 100644 --- a/docs/architecture/decisions/005-response-envelopes.md +++ b/docs/architecture/decisions/005-response-envelopes.md @@ -1,6 +1,6 @@ # ADR-005: Response Envelopes for Transport-Aware Results -**Status**: Draft +**Status**: Implemented **Date**: 2026-05-10 ## Context diff --git a/docs/architecture/decisions/006-unified-invocation-path.md b/docs/architecture/decisions/006-unified-invocation-path.md index 2165e8a..5ea009f 100644 --- a/docs/architecture/decisions/006-unified-invocation-path.md +++ b/docs/architecture/decisions/006-unified-invocation-path.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-05-10 +last_updated: 2026-05-11 --- # ADR-006: Unified Invocation Path @@ -31,7 +31,7 @@ Meanwhile `execute()` is a **domain call** — same-process, same-trust, rich ty ### Prerequisites -This ADR depends on ADR-005 (response envelopes) being implemented in source first. The unified `execute()` requires `ResponseEnvelope` types, `isResponseEnvelope()`, and factory functions that don't exist in source yet. +This ADR depends on ADR-005 (response envelopes) being implemented in source first. **ADR-005 is now implemented** — `ResponseEnvelope` types, `isResponseEnvelope()`, and factory functions exist in `src/response-envelope.ts`. The prerequisite is met. ## Decision diff --git a/docs/architecture/response-envelopes.md b/docs/architecture/response-envelopes.md index c08cfd2..1a21cd5 100644 --- a/docs/architecture/response-envelopes.md +++ b/docs/architecture/response-envelopes.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-05-10 +last_updated: 2026-05-11 --- # Response Envelopes @@ -221,15 +221,6 @@ Flow: **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. -**Current source state** (`src/registry.ts` lines 80-105): `execute()` returns `Promise` directly. It does not wrap results in envelopes, does not call `isResponseEnvelope()`, and validates raw `result` against `outputSchema`. The `Value.Cast()` normalization step is not implemented. Changes needed: - -| What | Current source | Target | -|------|---------------|--------| -| Return type | `Promise` | `Promise>` | -| Wrapping | None — returns raw `result` | `isResponseEnvelope(result) ? result : localEnvelope(result, operationId)` | -| Validation | `collectErrors(spec.outputSchema, result)` — on raw value | `collectErrors(spec.outputSchema, envelope.data)` — on envelope data | -| `Value.Cast()` | Not used | If `outputSchema !== Unknown`, `envelope.data = Value.Cast(spec.outputSchema, envelope.data)` | - ### `CallHandler` Takes full ownership of publishing `call.responded`. Handlers return values; they do NOT publish events. @@ -246,29 +237,14 @@ Flow: 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. -**Current source state** (`src/call.ts` lines 182-233): `buildCallHandler` calls `handler(input, context)` (line 226) but does not use the return value. The handler is expected to publish `call.responded` itself. Changes needed: - -| What | Current source | Target | -|------|---------------|--------| -| Handler model | Handler publishes `call.responded` itself; return value ignored | Handler returns value; `CallHandler` wraps and publishes | -| Return value | `await handler(input, context)` called but result discarded | Result captured, wrapped in envelope if needed, then published | -| Envelope detection | Not applicable | `isResponseEnvelope(result)` check before wrapping | -| Result pipeline | None | Detect → wrap → normalize → validate → publish | -| `call.responded.output` | `Type.Unknown()` | `ResponseEnvelopeSchema` | -| `PendingRequestMap.respond()` | Accepts any `unknown` value | Must enforce `isResponseEnvelope()` guard | - ### `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. -**Current source state** (`src/call.ts` lines 151-156): `respond()` publishes `call.responded` with `output: unknown`. No envelope validation. - ### `PendingRequestMap.call()` Resolves with the `ResponseEnvelope` from `call.responded.output` instead of raw `unknown`. -**Current source state** (`src/call.ts` lines 120-149): `call()` returns `Promise`. The type should become `Promise`. - ### `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. @@ -455,26 +431,24 @@ The following documentation changes have been completed: | `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. | ✅ (previous) | -| `adapters.md` | `from_mcp` | `outputSchema` extracted when available, via `FromSchema`. Falls back to `Type.Unknown()`. | ✅ (previous) | -| `adapters.md` | `from_openapi` | Handler returns `httpEnvelope()`. Error on HTTP error status still throws `CallError`. | ✅ (previous) | +| `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 **code** changes are still needed: +The following **code** changes have been completed: -| Code | Change | -|------|--------| -| `src/registry.ts` | `execute()` returns `Promise>` | -| `src/call.ts` | `CallHandler` captures return value, wraps in envelope, publishes `call.responded` | -| `src/call.ts` | `CallEventSchema` `output` field changes to `ResponseEnvelopeSchema` | -| `src/call.ts` | `PendingRequestMap.respond()` adds `isResponseEnvelope()` guard | -| `src/call.ts` | `PendingRequestMap.call()` resolves with `ResponseEnvelope` | -| `src/subscribe.ts` | `subscribe()` wraps yields in `ResponseEnvelope` | -| `src/env.ts` | `buildEnv()` functions return `Promise` | -| `src/response-envelope.ts` | New file: types, factory functions, detection, schemas | -| `src/from_mcp.ts` | Handler returns `mcpEnvelope()`, extracts `outputSchema`, uses `structuredContent` | -| `src/from_openapi.ts` | Handler returns `httpEnvelope()` | - -Additionally, any code subscribing to `"call.responded"` events via the pubsub system (not just `PendingRequestMap`, but any direct pubsub consumer) must expect `ResponseEnvelope` instead of `unknown` in the event payload. +| Code | Change | Status | +|------|--------|--------| +| `src/response-envelope.ts` | New file: types, factory functions, detection, schemas | ✅ | +| `src/registry.ts` | `execute()` returns `Promise>` | ✅ | +| `src/call.ts` | `CallHandler` captures return value, wraps in envelope, publishes `call.responded` | ✅ | +| `src/call.ts` | `CallEventSchema` `output` field changes to `ResponseEnvelopeSchema` | ✅ | +| `src/call.ts` | `PendingRequestMap.respond()` adds `isResponseEnvelope()` guard | ✅ | +| `src/call.ts` | `PendingRequestMap.call()` resolves with `ResponseEnvelope` | ✅ | +| `src/subscribe.ts` | `subscribe()` wraps yields in `ResponseEnvelope` | ✅ | +| `src/env.ts` | `buildEnv()` functions return `Promise` | ✅ | +| `src/from_mcp.ts` | Handler returns `mcpEnvelope()`, extracts `outputSchema`, uses `structuredContent` | ✅ | +| `src/from_openapi.ts` | Handler returns `httpEnvelope()` | ✅ | ## References