First speccing pass for alknet-http (HTTP interface crate: h2/http1.1/h3 server + from_openapi/to_openapi/from_mcp/to_mcp adapters). Specs (crates/http/): - README.md, overview.md — crate index, two-roles-in-one-crate framing, adapter location map, feature gates (h3, mcp), no-env-vars invariant - http-server.md — HttpAdapter for h2/http1.1, axum over QUIC stream, Bearer auth, SSE projection for subscriptions, /healthz, stealth decoy - http-adapters.md — from_openapi (reqwest) and to_openapi (projection), error fidelity (HTTP_<status> per ADR-023), type definitions - http-mcp.md — from_mcp/to_mcp (feature-gated), streamable-HTTP-only - webtransport.md — h3/WebTransport handler, browser streaming path, HTTP/3 request vs WebTransport session distinguished at framing layer ADRs: - ADR-036 HTTP-to-Call Operation Mapping (Proposed) — direct path mapping; to_openapi is projection, not router (the load-bearing one-way door from Phase 0 DH-3) - ADR-037 MCP Stdio Transport Exclusion (Proposed) — streamable HTTP only; stdio is not built (RCE-vector security position) - ADR-038 HTTP/3 and WebTransport as First-Class HTTP Transports (Proposed) — corrects the Phase 0 DH-2 deferral framing; h3 is in scope, not deferred, per ADR-009 §'What this framework is NOT' - ADR-039 HTTP Server and Client Host Colocated in alknet-http (Proposed) — one crate for server + client host (shared HTTP deps, shared operation-spec->HTTP mapping) - ADR-003 Amendment 1 — clarifies alknet-call is a protocol-foundation crate (the alknet-http -> alknet-call dependency edge) Open questions (OQ-38, OQ-39, OQ-40 added under 'Theme: alknet-http'): - OQ-38 WebTransport relay-as-proxy scope (genuine scope question, not a deferral — the decision is made when the use case becomes concrete) - OQ-39 to_openapi published-spec versioning (one-way after first publication) - OQ-40 reqwest client config and connection pooling (two-way-door) Architecture README and overview updated with doc table, ADR table (036-039), current-state note, and crate graph (alknet-http -> alknet-call edge). Reviewed by architecture-reviewer subagent: 3 critical, 4 warning, 5 suggestion issues found and fixed (missing ADR-039, WebTransport stream routing conflation, undefined types, stale OQ-37 deferral language, README OQ table completeness, Bearer-only attribution, cross-references, ADR-038 ALPN quote, feature-gate placeholder, MCP temporal language).
12 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-29 |
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 alknet-http, both behind the mcp feature gate:
from_mcp— discovers remote MCP tools via the MCPtools/listcall over streamable HTTP, and registers each as aHandlerRegistrationbundle with a forwarding handler that calls the remote tool viatools/call. Uses rmcp'sStreamableHttpClientTransport(reqwest-based). Provenance isFromMCP(leaf,composition_authority: None,scoped_env: None,Internalby default — ADR-015/022). ImplementsOperationAdapter.to_mcp— exposes the local registry'sExternaloperations as MCP tools over streamable HTTP, using rmcp'sStreamableHttpService(an axum-compatible tower service). An external MCP client (an editor, an AI tool) discovers and calls alknet operations through the MCP protocol. A pure projection (consumes the registry, does not produce entries — ADR-017 §5).
Streamable HTTP only (ADR-037)
MCP defines two transports: streamable HTTP and stdio. alknet-http
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 alknet. The bridge is where the RCE risk lives, explicitly in the operator's hands. See ADR-037.
from_mcp
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:
- Connects to the MCP server's streamable HTTP endpoint using rmcp's
StreamableHttpClientTransport::from_uri(endpoint)(the rmcpstreamable_http.rsclient example shows the pattern:client_info .serve(transport).await, thenclient.list_tools(),client.call_tool()). On connection failure, returnsAdapterError::DiscoveryFailed; on 401,AdapterError::Unauthorized. - Calls
tools/list→ the list of MCP tools (name, description,inputSchema, optionaloutputSchema). - For each tool, constructs a
HandlerRegistration:spec.name= the tool name (ornamespace/tool_nameif a namespace prefix is configured — same local-naming sugar asfrom_call'sFromCallConfig::namespace_prefix, ADR-029 §5).spec.namespace= the configurednamespace.spec.op_type=Mutation(MCP tools are call/response; the MCP spec doesn't have a native streaming/tool-subscription distinction —tools/callreturns a result. If MCP adds a streaming-tool extension, aSubscriptionmapping would be added.)spec.visibility=Internal(adapter-registered, ADR-015).spec.input_schema= the tool'sinputSchema(JSON Schema).spec.output_schema= the tool'soutputSchema, orType.Unknown()if absent (the TSfrom_mcp.tsshows this fallback).spec.error_schemas= the MCP tool's error description mapped toErrorDefinition(ADR-023 — 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 — ADR-022).capabilities= the bearer token for the MCP server (injected by the assembly layer at registration — see No-Env-Vars below).
- Returns the bundles. The caller (the assembly layer) registers them
in the
OperationRegistry.
Forwarding handler
At call time, the from_mcp forwarding handler:
- Reads the call input (
serde_json::Value— the tool arguments). - Calls
client.call_tool({ name: tool_name, arguments: input })via the rmcp client (thestreamable_http.rsexample showsclient.call_tool(CallToolRequestParams::new(name).with_arguments(...))). - On success: extracts
structuredContent(if present) or maps thecontentblocks (the TSmapMCPContentBlocksshows the mapping: text/image/audio/resource/resource_link →MCPContentBlock), wraps in aResponseEnvelope, returns. - On
result.isError: maps to aCallErrorwith the MCP error content (the TSfrom_mcp.tshandler shows the error mapping), returns. - 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 — Arc<dyn Handler> the
registry dispatches. alknet-call never sees rmcp.
to_mcp
pub fn to_mcp_service(
registry: Arc<OperationRegistry>,
identity_provider: Arc<dyn IdentityProvider>,
) -> StreamableHttpService<...>;
to_mcp exposes the local registry's External operations as MCP tools
over streamable HTTP, using rmcp's StreamableHttpService (an
axum-compatible tower service). The rmcp
simple_auth_streamhttp.rs server example shows the pattern:
// 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));
alknet-http's to_mcp follows the same pattern: the local operations
are exposed as an MCP server (an rmcp Service impl that wraps the
OperationRegistry), the StreamableHttpService nests into the axum
Router at /mcp, and a Bearer auth middleware gates access (the
simple_auth_streamhttp.rs auth_middleware + extract_token pattern).
The to_mcp service:
- On MCP
tools/list: returns the local registry'sExternaloperations as MCP tools (name, description,inputSchema). - On MCP
tools/call: dispatches to theOperationRegistry::invoke()— the same dispatch path the HTTP server uses for HTTP requests (ADR-036). The MCP tool call becomes acall.requestedinternally. The result is mapped back to the MCPtools/callresponse shape (structuredContentorcontentblocks). - Auth: the Bearer middleware resolves the token via
IdentityProvider::resolve_from_token(), same as the HTTP server's auth (ADR-004). The MCP client authenticates by bearer token; noPeerId(browsers and MCP clients are not alknet peers — ADR-034 §4).
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, overview.md).
Why
MCP is the protocol editors and AI tools use to discover and call tools.
from_mcp lets alknet 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 alknet
operations through the MCP protocol, without those clients needing to
speak EventEnvelope.
The streamable-HTTP-only constraint (ADR-037) is a security position: alknet does not import the MCP stdio RCE vector. The streamable HTTP path is network-isolated, auth-gatable, and runs under alknet'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
mcpfeature pulls in rmcp with streamable HTTP transport features only. from_mcp-registered ops areInternalby default. Composition material, not directly callable from the wire (ADR-015).from_mcphandlers read credentials fromOperationContext.capabilities. No env vars (ADR-014).to_mcpis a pure projection. Consumes the registry, does not produce entries. Not anOperationAdapter.- MCP clients are not alknet peers. A browser or MCP client
connecting to
to_mcpauthenticates by bearer token, gets noPeerId, is not in the peer graph (ADR-034 §4). - The
mcpfeature is optional. A deployment that doesn't need MCP doesn't compile rmcp. The default feature set ish2+http1.
Design Decisions
| Decision | ADR | Summary |
|---|---|---|
| MCP stdio transport excluded | ADR-037 | Streamable HTTP only; stdio is not built |
from_mcp is an OperationAdapter |
ADR-017 | Async trait; produces HandlerRegistration bundles |
to_mcp is a projection |
ADR-017 | Consumes the registry, doesn't produce entries |
Adapter-registered ops are Internal |
ADR-015 | from_mcp ops are composition material |
from_mcp provenance is a leaf |
ADR-022 | composition_authority: None, scoped_env: None |
| Error fidelity | ADR-023 | MCP tool errors mapped to ErrorDefinitions |
| No-env-vars credential injection | ADR-014 | Handler reads context.capabilities, not env vars |
| MCP clients are not alknet peers | ADR-034 | Bearer token, no PeerId |
Open Questions
See open-questions.md for full details.
- OQ-40 (open): reqwest client config — the shared
reqwest::Clientused byfrom_mcp(same client asfrom_openapi).
References
- ADR-037 — the stdio exclusion this document enforces
- overview.md — adapter location map, feature gates
- ../call/client-and-adapters.md —
OperationAdaptertrait,AdapterErrorvariants /workspace/rust-sdk/— MCP Rust SDK (rmcp v1.8.0); streamable HTTP transport/workspace/rust-sdk/examples/servers/src/simple_auth_streamhttp.rs— streamable HTTP MCP server with Bearer auth (theto_mcppattern)/workspace/rust-sdk/examples/clients/src/streamable_http.rs— streamable HTTP MCP client (thefrom_mcppattern)/workspace/@alkdev/operations/src/from_mcp.ts— TypeScript prior art (createMCPClient,mapMCPContentBlocks, theMCPClientLoader)