Files
alknet/tasks/http/adapters/from-mcp.md
glm-5.2 e855c8c7eb docs(http): decompose alknet-http spec into 19 implementation tasks
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).
2026-07-01 07:11:17 +00:00

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
http/client/shared-http-client
http/gateway/error-mapping
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:

  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, ADR-029 §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 Subscription mapping would be added.)
    • spec.visibility = Internal (adapter-registered, ADR-015).
    • spec.input_schema = the tool's inputSchema (JSON Schema).
    • spec.output_schema = depends on whether the tool declares outputSchema (MCP 2025-06-18+):
      • outputSchema presentoutput_schema = the declared schema (converted from JSON Schema). The result arrives in CallToolResult.structured_content and is composable with local operations.
      • 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>. See "Output handling" below.
    • spec.error_schemas = the MCP tool's error description mapped to ErrorDefinition (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).
  4. 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:

  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 CallAdapterArc<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_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.

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

  • FromMCP struct with endpoint, auth_token, namespace
  • OperationAdapter impl: 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 (or namespace/tool_name with prefix)
  • spec.namespace = configured namespace
  • spec.op_type = Mutation (MCP tools are call/response)
  • spec.visibility = Internal (ADR-015)
  • spec.input_schema = tool's inputSchema
  • spec.output_schema = declared outputSchema if present, else ContentBlock union
  • spec.error_schemas from 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_content present → use as result (validated against output_schema)
  • structured_content absent → map content: Vec<ContentBlock> to ContentBlock union
  • No heuristic JSON.parse of text blocks (carry as ContentBlock)
  • isError: trueCallError with MCP error content
  • rmcp client connection maintained for registration lifetime
  • No-env-vars: handler reads context.capabilities, never std::env::var (ADR-014)
  • Feature-gated behind mcp (no compile without mcp feature)
  • stdio transport NOT built (ADR-037 — streamable HTTP only)
  • Unit test: import() with mock MCP server → HandlerRegistration bundles
  • Unit test: outputSchema present → output_schema = declared schema
  • Unit test: outputSchema absent → output_schema = ContentBlock union
  • Unit test: structured_content present → used as result
  • Unit test: structured_content absent → content blocks mapped to union
  • Unit test: isError: trueCallError
  • Integration test: forwarding handler calls remote MCP tool via rmcp
  • Integration test: no std::env::var reads in the forwarding handler
  • cargo test -p alknet-http --features mcp succeeds
  • cargo clippy -p alknet-http --features mcp --all-targets succeeds with no warnings
  • cargo check -p alknet-http (no mcp feature) 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