Break the alknet-http architecture spec into atomic, dependency-ordered tasks in tasks/http/, following the taskgraph frontmatter conventions used by the call/core/vault crates. Tasks span 7 phases across 5 module subdirectories (server/, gateway/, client/, adapters/, websocket/): - Phase 0: crate-init (foundation) - Phase 1: gateway-dispatch-spine, error-mapping, shared-http-client (shared infrastructure) - Phase 2: http-adapter, bearer-auth-middleware, gateway-endpoints, healthz-decoy (HTTP server surface) - Phase 3: to-openapi (OpenAPI gateway projection) - Phase 4: from-openapi (OpenAPI adapter, reqwest forwarding) - Phase 5: dispatcher-transport-abstraction, upgrade-handler, connection-overlay (WebSocket browser bidirectional path) - Phase 6: from-mcp, to-mcp (MCP adapters, feature-gated) - Phase 7: review-http, review-websocket, review-mcp, review-http-final (quality checkpoints) The gateway-dispatch-spine task implements the thin shared core recommended by the gateway-factoring research (concrete struct, not a trait). The dispatcher-transport-abstraction task is a cross-crate change to alknet-call (exposes EventEnvelope-level dispatch API for non-QUIC transports) — the highest-risk task. WebTransport/h3 is deferred per ADR-044 and has no tasks; from_wss is out of scope. Validated: 19 tasks, no cycles, 8 parallel generations, critical path length 8 (through the WebSocket strand).
12 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | ||
|---|---|---|---|---|---|---|---|---|---|
| http/adapters/from-mcp | Implement from_mcp adapter (rmcp streamable HTTP client, tools/list discovery, structuredContent handling) | pending |
|
broad | medium | component | implementation |
Description
Implement from_mcp in src/adapters/from_mcp.rs (feature-gated behind
mcp). This is the MCP-direction adapter: it 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). Implements
OperationAdapter (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.
The adapter (http-mcp.md §"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= depends on whether the tool declaresoutputSchema(MCP 2025-06-18+):outputSchemapresent →output_schema= the declared schema (converted from JSON Schema). The result arrives inCallToolResult.structured_contentand is composable with local operations.outputSchemaabsent (older MCP servers) →output_schema= the MCPContentBlockunion (text | image | audio | resource | resource_link— a well-defined MCP type, notType.Unknown()). The result arrives inCallToolResult.contentas aVec<ContentBlock>. See "Output handling" below.
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 (http-mcp.md §"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 the result from the
CallToolResult, following thestructuredContent-preferred-over-content-blocks rule (see "Output handling" below), 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.
Output handling: structuredContent vs content blocks (http-mcp.md §"Output handling")
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_contentpresent (tool declaredoutputSchema): the handler usesstructured_contentas the result, validated/cast against the declaredoutput_schema. This is the composable case — the data matches the declared type, so a composing handler can use it as a typed value.structured_contentabsent (older server, nooutputSchema): the handler mapscontent: Vec<ContentBlock>to theContentBlock-unionoutput_schema(text/image/audio/resource/ resource_link). The TSmapMCPContentBlocksshows the mapping; the RustContentBlockenum (rmcp/src/model/content.rs) is the same shape. The common sub-case is a singleTextblock — older servers often JSON-stringify structured data into thetextfield. The adapter does not attempt toJSON.parsethe text heuristically (fragile, not the adapter's concern); it carries theContentBlockunion 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.
No-Env-Vars (http-mcp.md §"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).
Acceptance Criteria
FromMCPstruct withendpoint,auth_token,namespaceOperationAdapterimpl:async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>- Connects via rmcp
StreamableHttpClientTransport::from_uri(endpoint) - Connection failure →
AdapterError::DiscoveryFailed - 401 →
AdapterError::Unauthorized - Calls
tools/list→ MCP tools (name, description, inputSchema, outputSchema) - For each tool: constructs
HandlerRegistration spec.name= tool name (ornamespace/tool_namewith prefix)spec.namespace= configurednamespacespec.op_type=Mutation(MCP tools are call/response)spec.visibility=Internal(ADR-015)spec.input_schema= tool'sinputSchemaspec.output_schema= declaredoutputSchemaif present, elseContentBlockunionspec.error_schemasfrom MCP tool error descriptions (ADR-023)spec.access_control=AccessControl::default()provenance=FromMCP,composition_authority: None,scoped_env: None(ADR-022)capabilities= bearer token for MCP server (injected at registration)- Forwarding handler calls
client.call_tool({ name, arguments }) structured_contentpresent → use as result (validated againstoutput_schema)structured_contentabsent → mapcontent: Vec<ContentBlock>toContentBlockunion- No heuristic
JSON.parseof text blocks (carry asContentBlock) isError: true→CallErrorwith MCP error content- rmcp client connection maintained for registration lifetime
- No-env-vars: handler reads
context.capabilities, neverstd::env::var(ADR-014) - Feature-gated behind
mcp(no compile withoutmcpfeature) - stdio transport NOT built (ADR-037 — streamable HTTP only)
- Unit test:
import()with mock MCP server →HandlerRegistrationbundles - Unit test:
outputSchemapresent →output_schema= declared schema - Unit test:
outputSchemaabsent →output_schema=ContentBlockunion - Unit test:
structured_contentpresent → used as result - Unit test:
structured_contentabsent →contentblocks mapped to union - Unit test:
isError: true→CallError - Integration test: forwarding handler calls remote MCP tool via rmcp
- Integration test: no
std::env::varreads in the forwarding handler cargo test -p alknet-http --features mcpsucceedscargo clippy -p alknet-http --features mcp --all-targetssucceeds with no warningscargo check -p alknet-http(nomcpfeature) succeeds — from_mcp not compiled
References
- docs/architecture/crates/http/http-mcp.md — from_mcp (full spec)
- docs/architecture/decisions/037-mcp-stdio-transport-exclusion.md — ADR-037 (streamable HTTP only)
- docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md — ADR-017 §5 (OperationAdapter)
- docs/architecture/decisions/015-privilege-model-and-authority-context.md — ADR-015 (Internal)
- docs/architecture/decisions/022-handler-registration-provenance-and-composition-authority.md — ADR-022 (leaf)
- docs/architecture/decisions/023-operation-error-schemas.md — ADR-023 (error fidelity)
- docs/architecture/decisions/014-secret-material-flow-and-capability-injection.md — ADR-014 (no env vars)
- /workspace/rust-sdk/crates/rmcp/src/model.rs — CallToolResult (content + structured_content)
- /workspace/rust-sdk/crates/rmcp/src/model/content.rs — ContentBlock enum
- /workspace/rust-sdk/examples/clients/src/streamable_http.rs — streamable HTTP MCP client pattern
- /workspace/@alkdev/operations/src/from_mcp.ts — TypeScript prior art (mapMCPContentBlocks, structuredContent logic)
Notes
from_mcp is feature-gated behind mcp (rmcp dependency). Streamable HTTP only — stdio is NOT built (ADR-037). The output handling follows the structuredContent-preferred-over-content-blocks rule (same as the TS adapter and rmcp's into_typed). The adapter does NOT heuristically JSON.parse text blocks — it carries the ContentBlock union as the typed result; a downstream consumer that knows the text is JSON can parse it. The no-env-vars invariant applies (handler reads context.capabilities, not std::env::var). The rmcp client connection is maintained for the registration lifetime (persistent streamable HTTP endpoint, not per-call). The handler is opaque to CallAdapter (Arc); alknet-call never sees rmcp.
Summary
To be filled on completion