docs(http): pre-decomposition sanity check fixes — /subscribe POST, direct-call cleanup, from_mcp output handling

Three issues found in the http crate spec sanity check that would have
caused problems during task decomposition, now fixed:

C1 — /subscribe GET→POST: the gateway's /subscribe is an invoke endpoint
carrying { operation, input } in the body, but was listed as GET (which
has no body). Flipped to POST with Accept: text/event-stream negotiating
the SSE response, consistent with /call's flat-JSON-body invariant.
Browsers using EventSource can't POST but use WebSocket for the
bidirectional path; the HTTP gateway's /subscribe is for non-browser
HTTP clients (fetch + ReadableStream). Touches ADR-042, ADR-047,
ADR-048, http-adapters.md, http-server.md.

C2 — stale direct-call references: three spots contradicted ADR-047
(which removed the POST /{service}/{op} direct-call surface) and
ADR-046 §3 (which states /{service}/{op} is no longer reserved).
Cleaned up in http-server.md (custom-routes intro + collision list) and
ADR-046 §6 (default-surface list).

W2 — from_mcp output handling: the spec's fallback for tools without
outputSchema was Type.Unknown(), but the correct fallback is the MCP
ContentBlock union (text|image|audio|resource|resource_link) — a
well-defined MCP type, not Unknown. Fixed http-mcp.md with the full
structuredContent-preferred-over-content-blocks logic (matching the TS
adapter and rmcp SDK), enriched references with specific rmcp source
files. Also added shared-dispatch-spine notes to http-mcp.md and
http-adapters.md cross-referencing the new research findings.

Research (docs/research/alknet-http-gateway-factoring/findings.md):
to_mcp and to_openapi share a dispatch spine (resolve → invoke → map).
Recommendation: extract a thin shared struct now, not a GatewayDispatch
trait — the server-integration layers (axum routes vs rmcp
StreamableHttpService) and wire-framing stay per-gateway. A third
gateway is not on the horizon; if one appears its server-integration
needs its own shape anyway.

Minor: WS route precedence note (websocket.md), OpenAPISpec
shared-type-not-shape clarification (http-adapters.md), date bumps.
This commit is contained in:
2026-07-01 05:41:07 +00:00
parent 3edc42e3b4
commit e0c6f61e6a
9 changed files with 770 additions and 31 deletions

View File

@@ -87,7 +87,7 @@ The gateway endpoint set (initial, two-way-door extensible):
| `/schema` | `services/schema` | `GET` | Get an operation's full `OperationSpec` (input/output JSON Schemas, error schemas). |
| `/call` | `call.requested` (Query/Mutation) | `POST` | Invoke an operation by name with a JSON input. Returns the output or a typed error (ADR-023). |
| `/batch` | multiple `call.requested` | `POST` | Invoke multiple operations in one request (correlated request IDs, OQ-14). Returns an array of results. |
| `/subscribe` | `call.requested` (Subscription) | `GET` (SSE) | Invoke a streaming operation. Returns `text/event-stream` — each `call.responded` is an SSE frame, `call.completed` closes the stream. |
| `/subscribe` | `call.requested` (Subscription) | `POST` (SSE) | Invoke a streaming operation. Body `{ operation, input }` (same shape as `/call`); response is `text/event-stream` — each `call.responded` is an SSE frame, `call.completed` closes the stream. |
Five endpoints. The client calls `/search` to find operations, `/schema`
to learn the input shape, `/call` (or `/subscribe` for streaming) to
@@ -99,13 +99,24 @@ as JSON. No path/query/body split to reverse-engineer.
The OpenAPI gateway includes `subscribe` (which the MCP gateway excludes
— ADR-041, MCP tool calls are request/response). The `subscribe`
endpoint maps `Subscription` operations onto SSE: `GET /subscribe` with
endpoint maps `Subscription` operations onto SSE: `POST /subscribe` with
a `{ operation, input }` JSON body (same shape as `/call`) and
`Accept: text/event-stream`, each `call.responded` event is an SSE
`data:` frame, `call.completed` closes the stream, `call.aborted` closes
with an error frame. This is the same SSE projection ADR-036 describes
for `h2`/`http/1.1` clients — the gateway's `subscribe` endpoint is the
single SSE entry point instead of per-operation SSE streams.
`POST` (not `GET`) is used because `/subscribe` is an invoke endpoint
that carries `{ operation, input }` in the request body, the same flat
JSON body shape the rest of the gateway uses. A `GET` request has no
body, so it cannot carry the operation name and input. The SSE response
is negotiated via `Accept: text/event-stream` on the `POST`, not via the
method. (Browsers using `EventSource` cannot `POST`, but browsers use
WebSocket for the bidirectional path — ADR-044; the HTTP gateway's
`/subscribe` is for non-browser HTTP clients, and `fetch` +
`ReadableStream` handles POST-SSE cleanly.)
### 3. The generated OpenAPI doc is per-caller (AccessControl-filtered)
The `/search` endpoint's results are filtered by the caller's

View File

@@ -154,10 +154,11 @@ it wants.
### 6. This does not change the default surface
A deployment that constructs `HttpAdapter` with no extra routes gets
exactly the behavior documented in `http-server.md`direct-call,
gateway, `/healthz`, `/openapi.json`, MCP (feature-gated), decoy. The
exactly the behavior documented in `http-server.md`gateway,
`/healthz`, `/openapi.json`, MCP (feature-gated), decoy. The
extension point is purely additive. The default surface remains the
published contract (ADR-036, ADR-042, ADR-045); custom routes are a
published contract (ADR-042, ADR-045; ADR-036's routing decision is
superseded by ADR-047); custom routes are a
deployment-specific addition on top, not a modification of it.
## Consequences

View File

@@ -112,7 +112,7 @@ ADR-036's `OperationType` → HTTP method mapping (`Query`→`GET`,
at the HTTP path level, because there are no per-operation HTTP paths.
The gateway endpoints have fixed methods (ADR-042's table):
`/search` `GET`, `/schema` `GET`, `/call` `POST`, `/batch` `POST`,
`/subscribe` `GET` (SSE). The `OperationType` of the *called operation*
`/subscribe` `POST` (SSE). The `OperationType` of the *called operation*
is carried in the request/result, not expressed in the HTTP verb — the
client calls `/call` with the operation name; the operation's type is
the registry's concern, not the HTTP method's. A `Query` operation and a

View File

@@ -51,7 +51,7 @@ own explicit decision record:
| Invoke shape | `POST /call` with `{ "operation": "/fs/readFile", "input": {...} }` | `call.requested` event with `{ operation, input }` payload (the call protocol's native shape) |
| Discovery | `GET /search` (gateway endpoint) | `services/list` as an ordinary call-protocol op |
| Schema | `GET /schema` (gateway endpoint) | `services/schema` as an ordinary call-protocol op |
| Streaming | `GET /subscribe` (SSE frames) | `call.responded` events as binary WS messages (no SSE) |
| Streaming | `POST /subscribe` (SSE frames) | `call.responded` events as binary WS messages (no SSE) |
| Dispatcher | axum route handler → `OperationRegistry::invoke()` | shared `Dispatcher` (ADR-012, stream-agnostic) |
| Multiplexing | HTTP/2 native; HTTP/1.1 sequential | By request ID (ADR-012), not by stream |