Files
alkhttp/docs/architecture/http-mcp.md
T
glm-5.3-flash 320ea87b08 docs: port architecture specs and ADRs from alknet-http; write new alkhttp ADRs 067-070
Phase 1 (SDD) — architecture documentation:

Ported specs (adapted for alkcall, producer/consumer terms, 6-endpoint
gateway, channels-over-WS, Sub/Pub operation types):
- overview.md, http-server.md, http-adapters.md, http-mcp.md
- README.md index (rewritten for alkhttp)

New ADRs:
- 067: WebSocket carries the channels protocol (8-byte chunk demux,
  channel 0 = alk/call, upgrade path /alk/channels)
- 068: gateway /publish endpoint for Pub operations (NDJSON body)
- 069: WebTransport out of scope in alkhttp (alknet concern)
- 070: from_wss consumer adapter (wss feature, tokio-tungstenite)

Ported ADRs (25, same numbers, port notes + amendments where the
extraction changed facts): 001-004, 010, 014, 015, 017, 022, 023, 027,
034, 036, 037, 039, 041, 042, 044, 045, 046, 047, 048, 049, 051, 066.

websocket.md rewritten for the channels session; open-questions.md
seeded (OQ-01 WS byte-stream adapter, OQ-02 /publish framing,
OQ-03 from_wss reconnect, OQ-04 browser client ownership).

Verified: cargo test, clippy -D warnings, fmt, doc --no-deps.
2026-08-27 14:19:24 +00:00

481 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: draft
last_updated: 2026-08-27
---
# HTTP MCP — from_mcp and to_mcp
The MCP-direction adapters (feature-gated behind `mcp`): `from_mcp`
imports remote MCP tools as call-protocol operations over streamable
HTTP (reqwest client), and `to_mcp` exposes local operations as MCP
tools over streamable HTTP (axum server). This document covers both, the
rmcp integration, and the stdio exclusion (ADR-037).
## What
Two adapters, both in `alkhttp`, both behind the `mcp` feature gate:
1. **`from_mcp`** — discovers remote MCP tools via the MCP
`tools/list` call over streamable HTTP, and registers each as a
`HandlerRegistration` bundle with a forwarding handler that calls the
remote tool via `tools/call`. Uses rmcp's
`StreamableHttpClientTransport` (reqwest-based). Provenance is
`FromMCP` (leaf, `composition_authority: None`, `scoped_env: None`,
`Internal` by default — alkcall ADR-017/018). Implements
`OperationAdapter` (the async trait from `alkcall::client`).
2. **`to_mcp`** — exposes the local registry's `External` operations as
MCP tools over streamable HTTP, using rmcp's `StreamableHttpService`
(an axum-compatible tower service). An external MCP client (an editor,
an AI tool) discovers and calls the local deployment's operations
through the MCP protocol. A pure projection (consumes the registry,
does not produce entries — alkcall ADR-022 §5).
### Streamable HTTP only (ADR-037)
MCP defines two transports: streamable HTTP and stdio. **alkhttp
supports only streamable HTTP.** Stdio is not built — it is the spawn-
arbitrary-executable RCE vector that the rest of the architecture is
designed to avoid (ADR-037). The `mcp` feature gate pulls in rmcp with
the streamable HTTP transport features only; the stdio transport
(`transport-child-process`) is not a dependency, not optional, not
behind a separate feature.
If an operator wants a stdio-only MCP server, they run a small
streamable-HTTP-to-stdio bridge themselves, outside alkhttp. The bridge
is where the RCE risk lives, explicitly in the operator's hands. See
[ADR-037](decisions/037-mcp-stdio-transport-exclusion.md).
### from_mcp
```rust
pub struct FromMCP {
/// The MCP server's streamable HTTP endpoint URL.
endpoint: String,
/// Bearer token for the MCP server (from Capabilities at registration).
auth_token: Option<String>,
/// The importing deployment's name for this MCP server (becomes the
/// operation namespace).
namespace: String,
}
#[async_trait]
impl OperationAdapter for FromMCP {
async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>;
}
```
The adapter:
1. Connects to the MCP server's streamable HTTP endpoint using rmcp's
`StreamableHttpClientTransport::from_uri(endpoint)` (the rmcp
`streamable_http.rs` client example shows the pattern: `client_info
.serve(transport).await`, then `client.list_tools()`,
`client.call_tool()`). On connection failure, returns
`AdapterError::DiscoveryFailed`; on 401, `AdapterError::Unauthorized`.
2. Calls `tools/list` → the list of MCP tools (name, description,
`inputSchema`, optional `outputSchema`).
3. For each tool, constructs a `HandlerRegistration`:
- `spec.name` = the tool name (or `namespace/tool_name` if a
namespace prefix is configured — same local-naming sugar as
`from_call`'s `FromCallConfig::namespace_prefix`, alkcall ADR-024
§5).
- `spec.namespace` = the configured `namespace`.
- `spec.op_type` = `Mutation` (MCP tools are call/response; the MCP
spec doesn't have a native streaming/tool-subscription distinction
`tools/call` returns a result. If MCP adds a streaming-tool
extension, a `Sub` mapping would be added.) All `from_mcp`
handlers are `HandlerKind::Once` (alkcall ADR-021); `from_mcp`
never produces a `StreamingHandler` (nor a `HandlerKind::Sink`
the `Pub` type, alkcall ADR-046, has no MCP representation).
- `spec.visibility` = `Internal` (adapter-registered, alkcall
ADR-017).
- `spec.input_schema` = the tool's `inputSchema` (JSON Schema).
- `spec.output_schema` = depends on whether the tool declares
`outputSchema` (MCP 2025-06-18+):
- **`outputSchema` present** → `output_schema` = the declared
schema (converted from JSON Schema). The result arrives in
`CallToolResult.structured_content` and is composable with
local operations (the data matches the declared type).
- **`outputSchema` absent** (older MCP servers) → `output_schema`
= the MCP `ContentBlock` union (`text | image | audio |
resource | resource_link` — a well-defined MCP type, *not*
`Type.Unknown()`). The result arrives in
`CallToolResult.content` as a `Vec<ContentBlock>`. The common
sub-case is a single `Text` block (which older servers often
fill with JSON-stringified data), but the *type* is the
`ContentBlock` union regardless of what the text contains.
See "Output handling" below.
- `spec.error_schemas` = the MCP tool's error description mapped to
`ErrorDefinition` (alkcall ADR-016 — MCP tool definitions carry
error descriptions; the adapter maps them).
- `spec.access_control` = `AccessControl::default()`.
- `handler` = a forwarding handler (see Forwarding Handler below).
- `provenance` = `FromMCP`, `composition_authority: None`,
`scoped_env: None` (leaf — alkcall ADR-018).
- `capabilities` = the bearer token for the MCP server (injected by
the assembly layer at registration — see No-Env-Vars below).
4. Returns the bundles. The caller (the assembly layer) registers them
in the `OperationRegistry`.
### Forwarding handler
At call time, the `from_mcp` forwarding handler:
1. Reads the call input (`serde_json::Value` — the tool arguments).
2. Calls `client.call_tool({ name: tool_name, arguments: input })` via
the rmcp client (the `streamable_http.rs` example shows
`client.call_tool(CallToolRequestParams::new(name).with_arguments(...))`).
3. On success: extracts the result from the `CallToolResult`, following
the `structuredContent`-preferred-over-content-blocks rule (see
"Output handling" below), wraps in a `ResponseEnvelope`, returns.
4. On `result.isError`: maps to a `CallError` with the MCP error content
(the TS `from_mcp.ts` handler shows the error mapping), returns.
5. The rmcp client connection is maintained for the lifetime of the
registration (the MCP server is a persistent streamable HTTP
endpoint, not a per-call connection).
The handler is opaque to the `CallAdapter` — a `HandlerKind::Once`
wrapping an `Arc<dyn Handler>` that the registry dispatches. `alkcall`
never sees rmcp.
### Output handling (structuredContent vs content blocks)
MCP `CallToolResult` (rmcp `model.rs`) carries two result fields:
`content: Vec<ContentBlock>` (always present, defaults to `[]`) and
`structured_content: Option<Value>` (present when the tool declared
`outputSchema`). The `from_mcp` handler follows the same rule the TS
adapter (`@alkdev/operations/src/from_mcp.ts`) and the rmcp SDK
(`CallToolResult::into_typed`) use:
- **`structured_content` present** (tool declared `outputSchema`): the
handler uses `structured_content` as the result, validated/cast
against the declared `output_schema`. This is the composable case —
the data matches the declared type, so a composing handler can use it
as a typed value.
- **`structured_content` absent** (older server, no `outputSchema`):
the handler maps `content: Vec<ContentBlock>` to the
`ContentBlock`-union `output_schema` (text/image/audio/resource/
resource_link). The TS `mapMCPContentBlocks` shows the mapping; the
Rust `ContentBlock` enum (`rmcp/src/model/content.rs`) is the same
shape. The common sub-case is a single `Text` block — older servers
often JSON-stringify structured data into the `text` field. The
adapter does *not* attempt to `JSON.parse` the text heuristically
(fragile, not the adapter's concern); it carries the `ContentBlock`
union as the typed result. A consumer that knows the text is JSON can
parse it downstream.
The `isError: true` case is handled separately (step 4 above) — it
maps to a `CallError`, not to the output handling path.
### to_mcp
```rust
pub fn to_mcp_service(
registry: Arc<OperationRegistry>,
identity_provider: Arc<dyn IdentityProvider>,
) -> StreamableHttpService<...>;
```
`to_mcp` exposes the local registry's operations as a **fixed gateway
tool set** over streamable HTTP — not one MCP tool per operation. This
is the tool-gateway pattern (ADR-041): the LLM has a few tools in
context (search, schema, call, batch), not hundreds, and discovers
operations on demand through the gateway. See
[ADR-041](decisions/041-mcp-tool-gateway-pattern.md) for the
rationale (the tool-bloat problem, the `memory`/`worktree` tool pattern
that informed the design).
The rmcp `simple_auth_streamhttp.rs` server example shows the
streamable-HTTP-service-into-axum-`Router` pattern:
```rust
// From the rmcp example:
let mcp_service: StreamableHttpService<Counter, LocalSessionManager> =
StreamableHttpService::new(
|| Ok(Counter::new()),
LocalSessionManager::default().into(),
StreamableHttpServerConfig::default(),
);
let protected_mcp_router = Router::new()
.nest_service("/mcp", mcp_service)
.layer(middleware::from_fn_with_state(token_store, auth_middleware));
```
`alkhttp`'s `to_mcp` follows the same axum integration pattern,
but the rmcp `Service` impl is a gateway service (4 fixed tools) rather
than a per-operation tool registry.
#### The gateway tool set
`to_mcp` exposes four MCP tools that gate access to the full operation
registry:
| MCP tool | Call protocol operation | Purpose |
|----------|------------------------|---------|
| `search` | `services/list` | List/search available operations (filtered by the caller's `AccessControl`). Returns names + descriptions, not full schemas. |
| `schema` | `services/schema` | Get an operation's full `OperationSpec` (input/output JSON Schemas, error schemas). |
| `call` | `call.requested` (Query/Mutation) | Invoke an operation by name with a JSON input. Returns the output or a typed error (alkcall ADR-016). |
| `batch` | multiple `call.requested` | Invoke multiple operations in one tool call (correlated request IDs, OQ-14). |
The LLM calls `search` to discover operations, `schema` to learn an
operation's input shape, `call` to invoke. Same pattern as `man
<command>` — discover on demand, don't preload. See ADR-041 for the
rationale.
#### `Sub` and `Pub` exclusion
The gateway exposes only `Query` and `Mutation` operations
(request/response). `Sub` operations (streaming responses — many
`call.responded` events) and `Pub` operations (streaming requests —
the consumer publishes chunks to a `HandlerKind::Sink` handler,
alkcall ADR-046) are both filtered out of `search` results and cannot
be invoked via `call` — MCP tool calls are request/response by
protocol design; neither a streaming response nor a client-side
publish fits the LLM tool-call pattern. This is unaffected by the
streaming handler work (alkcall ADR-021): the `StreamingHandler` type
and `invoke_streaming()` dispatch path exist in alkcall and are used
by `to_openapi`'s `/subscribe` endpoint (and `invoke_sink()` by
`/publish` — [ADR-068](decisions/068-gateway-publish-endpoint.md)),
but `to_mcp` does not expose them — it filters by `op_type` and only
dispatches `Query`/`Mutation` via `invoke()`. See
[ADR-041](decisions/041-mcp-tool-gateway-pattern.md) §2.
#### `to_mcp` service behavior
1. On MCP `tools/list`: returns the fixed gateway tool set (4 tools:
`search`, `schema`, `call`, `batch`), not the registry's
operations. The gateway tools have stable names and schemas; the
registry's operations are discovered through `search`.
2. On MCP `tools/call`:
- `search` → dispatches `services/list` (filtered by the caller's
`AccessControl`), returns operation names + descriptions.
- `schema` → dispatches `services/schema`, returns the
`OperationSpec`.
- `call` → dispatches `OperationRegistry::invoke()` (the same
dispatch spine the HTTP gateway uses — the gateway surface per
[ADR-047](decisions/047-remove-direct-call-http-surface.md), the
HTTP-to-call mapping per
[ADR-036](decisions/036-http-to-call-operation-mapping.md)). The
result is mapped to an MCP `CallToolResult`
(`structuredContent` for the output, or `isError: true` for a
`CallError` with typed `details` per alkcall ADR-016).
- `batch` → dispatches multiple `call.requested` events, returns
an array of results.
3. Auth: the Bearer middleware resolves the token via
`IdentityProvider::resolve_from_token()`, same as the HTTP server's
auth ([ADR-004](decisions/004-auth-as-shared-core.md)). The MCP
client authenticates by bearer token; no `PeerId` (browsers and MCP
clients are not alk peers —
[ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md)
§4). `AccessControl` gates `search` results and `call` dispatch — the
LLM sees only what it's authorized to call.
#### Shared dispatch spine with `to_openapi`
`to_mcp`'s `call` tool and `to_openapi`'s `/call` endpoint share the
same dispatch spine: resolve caller identity (Bearer →
`IdentityProvider::resolve_from_token`) → build a root
`OperationContext``OperationRegistry::invoke()` → map the
`ResponseEnvelope` to the gateway's wire shape (`CallToolResult` for
MCP, HTTP JSON for OpenAPI). The wire framing, discovery listing
(`tools/list` vs `/search`), streaming (excluded in `to_mcp` vs
`to_openapi`'s `/subscribe` SSE and `/publish` NDJSON — the OpenAPI
gateway is 6 endpoints per
[ADR-068](decisions/068-gateway-publish-endpoint.md), while `to_mcp`
stays the 4 fixed tools), and server integration (rmcp
`StreamableHttpService` tower service vs axum route handlers) are
genuinely per-gateway and are not shared. See also
[http-adapters.md](http-adapters.md) §"Shared dispatch spine with
`to_mcp`".
Research findings
(`/workspace/@alkdev/alknet/docs/research/alknet-http-gateway-factoring/findings.md`)
recommend extracting a **thin shared spine** (the concrete
`GatewayDispatch` struct holding `Arc<OperationRegistry>` +
`Arc<dyn IdentityProvider>` with a `resolve + build_context + invoke`
method returning a `ResponseEnvelope`, named in alkcall ADR-021 and
extended with `invoke_streaming()` for the streaming path), **not** a
trait or gateway abstraction. The spine is small (~1530 lines per
endpoint), but it is the one place where a divergence bug (identity
resolved differently, `OperationContext.internal` set inconsistently,
`CallError` mapped asymmetrically) would be a security/correctness
issue. The server-integration and wire-framing layers stay
per-gateway; a third gateway (GraphQL, gRPC) is not on the horizon,
and if one appears its server-integration layer needs its own shape
anyway. This is an implementation factoring note, not an ADR — the
decision is internal to `alkhttp` and does not cross crate boundaries.
### No-Env-Vars
The `from_mcp` forwarding handler reads the MCP server's bearer token
from `context.capabilities` (the same injection path as `from_openapi`),
not from `std::env::var`. The assembly layer injects the token at
registration; the handler reads it per-call. This is the no-env-vars
invariant ([ADR-014](decisions/014-secret-material-flow-and-capability-injection.md),
[overview.md](overview.md)).
## Why
MCP is the protocol editors and AI tools use to discover and call tools.
`from_mcp` lets the alk stack compose external MCP servers (a remote tool
server, a third-party MCP endpoint) into the call protocol — the same
composition pattern as `from_openapi` and `from_call`. `to_mcp` lets
external MCP clients (an editor, an AI tool) discover and call the local
deployment's operations through the MCP protocol, without those clients
needing to speak EventEnvelope.
`to_mcp` uses the **tool-gateway pattern** (ADR-041): a fixed set of
meta-tools (`search`, `schema`, `call`, `batch`) gates access to the
full operation registry, so the LLM has a few tools in context instead
of hundreds. This addresses the tool-bloat problem — an LLM connecting
to a node with 200 operations gets 4 MCP tools, not 200, and discovers
operations on demand through `search` + `schema`. Same pattern as the
`memory` and `worktree` tools (one entry point, large dataset behind
it), and the same principle as Linux's `man` command (don't preload all
documentation; query on demand).
The streamable-HTTP-only constraint (ADR-037) is a security position:
alkhttp does not import the MCP stdio RCE vector. The streamable HTTP
path is network-isolated, auth-gatable, and runs under alkhttp's
auth/identity/capabilities machinery — the same machinery that gates
every other HTTP request.
## Constraints
- **Streamable HTTP only.** Stdio is not built (ADR-037). The `mcp`
feature pulls in rmcp with streamable HTTP transport features only.
- **`from_mcp`-registered ops are `Internal` by default.** Composition
material, not directly callable from the wire (alkcall ADR-017).
- **`from_mcp` handlers read credentials from
`OperationContext.capabilities`.** No env vars
([ADR-014](decisions/014-secret-material-flow-and-capability-injection.md)).
- **`to_mcp` is a pure projection.** Consumes the registry, does not
produce entries. Not an `OperationAdapter`.
- **MCP clients are not alk peers.** A browser or MCP client
connecting to `to_mcp` authenticates by bearer token, gets no
`PeerId`, is not in the peer graph
([ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md)
§4).
- **The `mcp` feature is optional.** A deployment that doesn't need MCP
doesn't compile rmcp. The default feature set is `h2` + `http1`.
## Design Decisions
| Decision | ADR | Summary |
|----------|-----|---------|
| MCP stdio transport excluded | [ADR-037](decisions/037-mcp-stdio-transport-exclusion.md) | Streamable HTTP only; stdio is not built |
| `to_mcp` tool-gateway pattern | [ADR-041](decisions/041-mcp-tool-gateway-pattern.md) | 4 fixed gateway tools (search/schema/call/batch), not one tool per operation; `Sub` and `Pub` excluded |
| `from_mcp` is an `OperationAdapter` | alkcall ADR-022 (Call Protocol Client and Adapter Contract) | Async trait (`alkcall::client`); produces `HandlerRegistration` bundles |
| `to_mcp` is a projection | alkcall ADR-022 (Call Protocol Client and Adapter Contract) | Consumes the registry, doesn't produce entries |
| Adapter-registered ops are `Internal` | alkcall ADR-017 (Privilege Model and Authority Context) | `from_mcp` ops are composition material |
| `from_mcp` provenance is a leaf | alkcall ADR-018 (Handler Registration, Provenance, and Composition Authority) | `composition_authority: None`, `scoped_env: None` |
| Error fidelity | alkcall ADR-016 (Operation Error Schemas) | MCP tool errors mapped to `ErrorDefinition`s |
| No-env-vars credential injection | [ADR-014](decisions/014-secret-material-flow-and-capability-injection.md) | Handler reads `context.capabilities`, not env vars |
| MCP clients are not alk peers | [ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md) §4 | Bearer token, no `PeerId` |
| Streaming handler for subscriptions | alkcall ADR-021 (Streaming Handler for Subscription Operations) | `from_mcp` handlers are always `HandlerKind::Once` (MCP tools are request/response); `to_mcp` excludes `Sub` (and `Pub`) ops (unchanged by the streaming handler) |
## Open Questions
See [open-questions.md](open-questions.md) for full details.
- **OQ-40** (resolved): reqwest client config — the shared
`ClientWithMiddleware` used by `from_mcp` (same client as
`from_openapi`).
## References
- [ADR-037](decisions/037-mcp-stdio-transport-exclusion.md) — the
stdio exclusion this document enforces
- [overview.md](overview.md) — adapter location map, feature gates
- the alkcall crate's `docs/architecture/client-and-adapters.md`
`OperationAdapter` trait, `AdapterError` variants
- `/workspace/rust-sdk/` — MCP Rust SDK (rmcp v1.8.0); streamable HTTP
transport
- `/workspace/rust-sdk/crates/rmcp/src/model/tool.rs``Tool` with
`output_schema: Option<Arc<JsonObject>>` (the `outputSchema` field)
- `/workspace/rust-sdk/crates/rmcp/src/model/content.rs``ContentBlock`
enum (text/image/audio/resource/resource_link — the fallback
`output_schema` type when `outputSchema` is absent)
- `/workspace/rust-sdk/crates/rmcp/src/model.rs` (~line 2868) —
`CallToolResult` with `content: Vec<ContentBlock>` and
`structured_content: Option<Value>` (the two result fields); see also
`into_typed` (~line 3057) for the SDK's own
structured-content-preferred-over-text-block fallback logic
- `/workspace/rust-sdk/examples/servers/src/simple_auth_streamhttp.rs`
— streamable HTTP MCP server with Bearer auth (the `to_mcp` pattern)
- `/workspace/rust-sdk/examples/clients/src/streamable_http.rs`
streamable HTTP MCP client (the `from_mcp` pattern)
- `/workspace/@alkdev/operations/src/from_mcp.ts` — TypeScript prior art
(`createMCPClient`, `mapMCPContentBlocks`, the `MCPClientLoader`; the
`structuredContent`-preferred-over-content-blocks logic)
- `/workspace/@alkdev/operations/docs/architecture/adapters.md`
TypeScript adapter architecture doc (the `from_mcp` `outputSchema`/
`structuredContent` handling, the `MUTATION` tool-type decision)
- `/workspace/@alkdev/alknet/docs/research/alknet-http-gateway-factoring/findings.md`
— research on the shared dispatch spine between `to_mcp` and
`to_openapi` (recommendation: thin shared struct, not a trait)
## Port notes
Corrections applied during the alknet → alkhttp port, beyond mechanical
crate renames (`alknet-http``alkhttp`; `alknet-call`/`alknet-core`
`alkcall`, with the adapter contract in `alkcall::client`, registry
types in `alkcall::registry`, core in `alkcall::core`):
1. **`OperationType::Subscription` renamed `Sub`** (alkcall rename);
`OperationType::Pub` exists (producer→consumer streaming,
`HandlerKind::Sink`, wire event `call.published` — alkcall ADR-046).
The MCP gateway exclusion was a "Subscription excluded" claim in the
source; extended to cover both `Sub` (streaming responses) and `Pub`
(streaming requests) — MCP tool calls are request/response, so
neither fits. The section is retitled "`Sub` and `Pub` exclusion".
`to_mcp` stays 4 fixed tools.
2. **The OpenAPI gateway now has 6 endpoints.** `/publish` (Pub, NDJSON,
alkhttp ADR-068) joined the original five. The shared-dispatch-spine
cross-reference updated accordingly ("streaming (excluded in
`to_mcp` vs `to_openapi`'s `/subscribe` SSE and `/publish`
NDJSON)"; the `/subscribe` mention in the Sub-exclusion section now
cites `/publish` too). `to_mcp` has no `/publish` analog — the 4
tool set is unchanged.
3. **ADR cross-reference remapping.** Call-protocol-internal decisions
now live in the alkcall crate and are cited textually (no relative
links across crates): old ADR-017 adapter contract → alkcall ADR-022
(§5 mapping preserved); old ADR-022 handler registration → alkcall
ADR-018; old ADR-023 error schemas → alkcall ADR-016; old ADR-015
privilege model → alkcall ADR-017; old ADR-049 streaming handler →
alkcall ADR-021 (the `GatewayDispatch` spine clause is alkcall ADR-021
§7); Pub/`HandlerKind::Sink` → alkcall ADR-046. The old ADR-029 §5
citation for `FromCallConfig::namespace_prefix` (peer-graph collision
rule) → alkcall ADR-024 §5 (the alkcall peer-graph routing model;
alkcall's ADR-029 is a different, later decision — aggregated peer-env
wiring).
4. **alkhttp-owned ADRs keep their numbers and are linked relatively:**
014, 034, 004 (ported files exist); 037, 041 (being ported in
parallel — linked per instruction); 036, 047, 068 (linked by
inference — their ports are assumed, consistent with
http-adapters.md/http-server.md practice). The old ADR-036 "same
dispatch path the HTTP server uses" citation was extended with
ADR-047 (the gateway is the sole invoke path; ADR-047 supersedes
ADR-036's direct-call surface) so the reference reflects the current
architecture.
5. **Terminology.** "MCP clients are not alknet peers" → "not alk
peers"; "outside alknet" → "outside alkhttp"; "alknet's
auth/identity/capabilities machinery" → "alkhttp's". No call-protocol
server/client framing existed in the source; the remaining
server/client wording (rmcp streamable HTTP server/client, axum
server, remote MCP server, stdio-only MCP server) is MCP's inherent
transport directionality and is deliberately retained.
6. **Project naming.** "lets alknet compose external MCP servers" →
"lets the alk stack compose"; "calls alknet operations" → "the local
deployment's operations" in the Why prose.
7. **Links.** `../../decisions/``decisions/`;
`../../open-questions.md``open-questions.md` (sibling, pending
port); `../call/client-and-adapters.md` → textual reference to the
alkcall crate's `docs/architecture/client-and-adapters.md`; the
mono-repo research findings path is kept as a textual absolute path
(the findings doc was not ported). `overview.md` and
`http-adapters.md` are sibling links (overview.md pending port).
OQ-14/OQ-40 references kept as-is (OQ-14 is a call-protocol OQ — the
alkcall crate's `docs/architecture/open-questions.md`; OQ-40 is the
shared reqwest client config, resolved).