MCP
Reusable Rust client support for the Model Context Protocol. The workspace provides a tools-only
client, stdio and Streamable HTTP transports, OAuth, a strict named local registry, and a standalone
b10x-mcp operator CLI.
Both transports are exercised here rather than only in a consumer:
crates/b10x-mcp-stdio/tests/transport.rs and crates/b10x-mcp-http/tests/transport.rs drive a real
pipe and a real loopback socket against the controlled server b10x-mcp-testkit compiles from
standard-library source, and assert the negotiated version, the frozen snapshot and one tool call on
each.
The client prefers MCP 2026-07-28 and falls back to 2025-11-25. Consumers retain authority:
Harness supplies envelopes and approvals; Connectors supplies catalog, grants, egress, and hosted
credential custody. MCP server annotations never grant either consumer anything.
cargo xtask bootstrap-tools # explicit Linux x86_64 setup, into .cache/tools
cargo xtask gate
cargo run -p b10x-mcp-cli -- connections list
The gate requires the ESS 0.50.0 and AEP 0.68.0 tools pinned in
toolchain.json. bootstrap-tools verifies release archive
SHA256 digests before installing the exact binary members; it requires curl and
tar. It does not change globally installed tools. Alternatively set ESS_BIN
and AEP_BIN to matching executables, or provide those versions on PATH when no
repository-local tools exist. Explicit paths take priority, then .cache/tools,
then PATH; a wrong selected version fails without fallback or automatic download.
task check invokes the same gate. It validates ESS and AEP, checks the exact
partial constructor suite and remaining synthesis refusals, then runs formatting,
workspace tests (including the real constructor target), Clippy and Rustdoc.
cargo xtask specification performs only the specification checks. Passing those
checks does not establish full MCP conformance; see coverage.
The default registry is $XDG_CONFIG_HOME/b10x/mcp.toml (falling back to the XDG location below
HOME). OAuth material lives separately under $XDG_STATE_HOME/b10x/mcp, with owner-only
permissions. A minimal registry looks like this:
[connections.local_files]
transport = "stdio"
program = "/absolute/path/to/mcp-server"
args = ["--stdio"]
cwd = "/absolute/working/directory"
inherit-env = []
[connections.remote]
transport = "http"
url = "https://mcp.example.com/mcp"
[connections.remote.auth]
kind = "oauth"
resource-url = "https://mcp.example.com/mcp"
redirect-uri = "http://127.0.0.1:38123/callback"
client-name = "b10x MCP client"
scopes = []
application-type = "native"
Use b10x-mcp auth login remote, then b10x-mcp tools snapshot remote. A bearer token may instead
come from an explicitly named environment variable or JSON file/pointer; inline credentials and
ambient credential discovery are not part of the schema.
Consumer boundary
The library returns lossless descriptors and results. It does not convert MCP annotations into permissions, risk, idempotency, grants, or approvals. An embedding consumer must review and attach those facts itself. See the boundary design.
Hosts with their own egress and secret boundary can use connect_http_with_client; the supplied
client performs every HTTP exchange while the foundation retains protocol negotiation, discovery,
snapshotting, bounds, and calls.
The opt-in strict-http feature adds strict_connection::connect in
b10x-mcp-client. Supply a bodyless admitted POST template, an HTTP builder and
ESS-generated http_exchange::McpHttpConnectionSetupInput to establish one configured revision. Modern setup uses
discovery; legacy setup verifies initialization and its acknowledgement, retaining
any assigned session privately. The handle exposes the peer description and one
raw family exchange at a time. strict_discovery::discover adds bounded complete
tool, resource or prompt catalogs using generated McpHttpDiscoveryFamily and
McpHttpDiscoveryListLimits. Pages share one deadline; empty cursors remain valid
continuations. Catalogs preserve raw metadata and do not grant invocation authority
or validate arbitrary JSON Schema semantics. strict_invocation::InvocationClient
owns that connection and obtains its own selected catalogs before typed tool
calls, resource reads and prompt retrieval. It preserves ordered content, opaque
fields, business errors and structured-output presence. Invalid modern parameter
header annotations exclude the affected tool; valid annotations mirror arguments
into safely encoded Mcp-Param-* headers.
Supply schema_worker::SchemaWorker with an admitted absolute path to the
b10x-mcp-schema-worker binary and an explicit IPC byte ceiling. Build that binary
with cargo build --locked -p b10x-mcp-client --features strict-http --bin b10x-mcp-schema-worker. Tool input and declared output schemas execute offline
JSON Schema 2020-12 in a one-shot child sharing the invocation deadline. Unsupported
dialects or external references refuse before business dispatch. Awaited failures
kill and reap the worker; dropping the future initiates termination without an
observed reap barrier. This internal validator does not select MCP stdio ownership.
For directly controlled validation, SchemaWorker::run_cancellable takes a
Cancellation signal plus separate absolute operation and teardown deadlines.
The returned observation distinguishes an actual reap from a kill request.
If teardown expires, the worker retains the child and refuses both execution APIs
until reap_pending(deadline) successfully waits for exit. Dropping the controlled
future also requests termination and retains ownership for that explicit reap.
Dropping the worker itself supplies no observed exit barrier. The original run
API retains its existing cleanup behavior.
The conformance/schema-lifecycle suite executes actual child processes on Linux
with test-schema-worker enabled, including /proc checks after reaping. The
repository's Linux gate enables all features and verifies its 15-case inventory.
Other platform/feature combinations do not execute this OS-specific selection;
the strict HTTP lifecycle suite remains independent of that fixture feature.
Await shutdown(teardown_deadline) on the strict connection or invocation client
to consume the handle and close its pool. A legacy session receives one DELETE
attempt;405 is permitted and other failures retain bounded observations. Modern
and sessionless connections send no DELETE. The explicit teardown deadline is
capped by the existing execution/provider limits and does not extend a business
operation's deadline. Dropping an exchange closes its local stream but proves no
legacy cancellation or rollback. The handle then refuses another exchange until
shutdown, retaining the abandoned request ID with unknown effects. Reconnect from
the caller's unchanged configuration. Strict connections retain ordered SSE
messages and separate control observations under exchange.observation.stream.
Legacy ping receives an empty result POST; unadvertised server methods receive
method-not-found. Modern server requests are refused. Shared byte limits cover
the original response and all retained messages/control bodies. Control I/O runs
concurrently within the exchange future under the same deadline; pending control
futures are dropped before return or when the exchange is dropped. An earlier business
terminal survives a delayed control acknowledgement. To opt into progress on a
strict connection, supply a string or integer params._meta.progressToken.
Ordered progress observations retain exact numbers and untrusted server text.
Unknown tokens remain unmatched; equal or decreasing matched values refuse.
Progress shares the response byte budget and never extends the deadline or
becomes a business result.
StrictConnection::exchange_cancellable takes an explicit Cancellation signal
and a separate absolute teardown deadline. Await it to observe either a finished
exchange or cancellation. Modern closes its stream; legacy attempts one
notifications/cancelled POST after an attempted business send. The notification
uses only the remaining response-byte allowance. Its acknowledgement proves no
remote rollback. An already observed terminal wins over later cancellation during
side I/O. Unread late bytes are not reported as observed. Raw exchange retains
its existing timeout behavior.
InvocationClient exposes call_tool_cancellable, read_resource_cancellable,
get_prompt_cancellable and discover_cancellable with the same explicit signal
and separate absolute teardown deadline. Typed interruptions name the phase and
retain actual worker or HTTP observations. Stopping output validation after a
business response preserves that response without sending a new cancellation
notification for it. Discovery interruption keeps prior page observations but
returns no partial catalog. A refresh invalidates the old selected-family catalog
before awaiting any I/O, including when the refresh future is later dropped.
Use reap_schema_worker(deadline) to observe exit of a retained schema child
through the owning invocation client. Await this before consuming shutdown when
an observed worker-exit barrier is required; shutdown closes the HTTP connection,
and dropping the worker itself only initiates termination. A dropped HTTP exchange
still requires shutdown and fresh setup. Repeated cancellation uses one monotonic
signal; explicitly start a fresh signal for a subsequent operation.
The caller supplies authority and network policy; the builder must contain no hidden MCP protocol/session/routing header defaults. The existing tools-only constructors retain their separate compatibility behavior.
License
Apache-2.0.