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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user