Interop (MCP)
indusagi::interopis the bidirectional Model Context Protocol bridge of the Rust edition, built on the official Rust MCP SDK (rmcp). As a client it dials external MCP servers, normalizes their tools, and grafts them into the capabilities kernel under collision-freeserver__toolnames; as a provider host it stands up an MCP server over the agent's ownToolBoxso outside clients can enumerate and call our tools. Reached asindusagi::interop::*(one barrel re-exporting the wholeprotocol_bridgemodule), gated behind themcpcargo feature.
The interop layer is indusagi's seam to the wider MCP ecosystem, and it works in
both directions over the same ToolRegistry / ToolBox abstractions the
Capabilities kernel uses for local tools. As a
client it spawns or connects to external MCP servers (a stdio subprocess or a
streamable-HTTP endpoint), enumerates their tools, normalizes their untrusted
JSON schemas, and registers each remote tool as a kernel tool the model can call
like any built-in. As a provider host it stands up an rmcp server that answers
tools/list and tools/call against a Runtime
ToolBox. Conceptually it is the inverse of the kernel: capabilities defines
local tools; interop grafts remote tools in and publishes local tools out. The
module is the 100%-Rust port of the TypeScript interop/ subsystem; parity
behaviour (the __ qualifier, the byte-for-byte schema normalizer, the
six-variant fault model, the endpoint lifecycle machine, pre-flight-only invoke
cancellation) is preserved deliberately and pinned by tests.
Table of Contents
- Cargo feature and dependency
- Public surface
- Module map
- Key concepts
- The bridge contract
- Mounting external servers
- Driving a single endpoint
- The server fleet
- The provider host
- Schema normalization
- The fault model
- Design invariants
- Relationship to neighbors
Cargo feature and dependency
The entire module is gated behind the mcp cargo feature (on by default). The
feature pulls in rmcp (the official Rust MCP SDK) and the http header-type
crate its transport config uses:
[features]
default = ["rustls", "mcp", "tui"]
# `mcp` — the MCP protocol bridge surface. Gates the `interop` module (its `rmcp`
# dependency) plus the facade's legacy `indusagi/mcp` vocabulary shim.
mcp = ["dep:rmcp", "dep:http"]
[dependencies]
rmcp = { version = "1.7.0", default-features = false, features = [
"client", "server", "transport-io", "transport-async-rw",
"transport-child-process", "transport-streamable-http-client",
"transport-streamable-http-client-reqwest", "reqwest",
], optional = true }
http = { version = "1", optional = true }
Both the client (client, transport-child-process,
transport-streamable-http-client) and server (server) halves of rmcp are
enabled, so the bridge can act as either party. With --no-default-features and
no mcp, the interop module and its rmcp dependency vanish entirely.
Public surface
interop/mod.rs re-exports the whole protocol_bridge barrel to the crate root
(pub use protocol_bridge::*), mirroring the TS interop/index.ts re-export.
Everything below is reachable as indusagi::interop::<Name>.
| Name | Kind | Source | Purpose |
|---|---|---|---|
mount_protocol_bridge |
async fn |
bridge.rs |
The join point: start a fleet, list each ready endpoint's tools, wrap each as a kernel tool, return a MountedProtocolBridge (ToolBox + live fleet) |
MountedProtocolBridge |
struct | bridge.rs |
The mount return value: box_ (ToolBox) plus fleet (Arc<dyn ServerFleet>) |
create_server_endpoint |
fn |
endpoint.rs |
Factory for one ServerEndpointImpl over a ServerConfig, with an optional injected transport |
ServerEndpointImpl |
struct | endpoint.rs |
The one concrete client endpoint: a single live rmcp connection driving the phase machine |
InjectedTransport |
enum | endpoint.rs |
A pre-built transport injected at construction (the in-process / test seam); variant Duplex(DuplexStream) |
CLIENT_NAME / CLIENT_VERSION |
const &str |
endpoint.rs |
The intended client identity ("indus-protocol-bridge" / "0.1.0"), documented for parity with the TS CLIENT_INFO; the rmcp () client handler currently advertises its own default name, so these document the intent and reserve it for future handler customization |
start_server_fleet |
async fn |
fleet.rs |
Construct and start() a ServerFleetImpl from a BridgeConfig, returning it after every connection has settled |
ServerFleetImpl |
struct | fleet.rs |
The one concrete fleet: mints one endpoint per ServerConfig, connects them in parallel with per-server failure isolation |
create_provider_host |
fn |
host.rs |
Provider-host factory: build a ProviderHost over an Arc<ToolBox> |
ProviderHost |
struct | host.rs |
The agent-as-MCP-server: connect(transport) binds an rmcp ServerHandler and serves |
RunningProviderHost |
struct | host.rs |
A live host session; holding it keeps the server task alive, waiting()/cancel() end it |
ProviderHostInfo |
struct | host.rs |
Identity (name, version) the host advertises to connecting clients |
normalize_schema |
fn |
schema.rs |
Reshape an untrusted remote inputSchema into a draft-agnostic subset every provider accepts |
ProtocolFault |
struct | contract.rs |
The normalized, discriminable error raised everywhere in the bridge; class on .kind, cause on the Error::source chain |
protocol_fault |
fn |
contract.rs |
Terse constructor protocol_fault(kind, message, cause) |
is_protocol_fault |
fn |
contract.rs |
Downcasting guard narrowing a dyn Error to ProtocolFault |
is_usable_phase |
fn |
contract.rs |
True only when phase == EndpointPhase::Ready (can accept tool calls) |
is_terminal_phase |
fn |
contract.rs |
True when phase is Closed or Faulted |
qualify_tool_name |
fn |
contract.rs |
Build the agent-facing "<server>__<tool>" from a RemoteToolRef |
QUALIFIER |
const &str |
contract.rs |
The "__" delimiter joining server id and tool name |
ServerEndpoint |
trait | contract.rs |
A live single-server connection: config/phase plus open/list_tools/invoke/status/close |
ServerFleet |
trait | contract.rs |
A name-keyed endpoint collection: spin_up/tear_down/endpoint/endpoints/status |
EndpointPhase |
enum | contract.rs |
Lifecycle: Idle, Connecting, Ready, Closing, Closed, Faulted |
FaultKind |
enum | contract.rs |
Six-variant fault class: Transport, Protocol, Timeout, ToolError, NotConnected, SpawnFailed |
FaultCause |
type alias | contract.rs |
Arc<dyn std::error::Error + Send + Sync + 'static> — the cause chain |
TransportKind |
enum | contract.rs |
Stdio, Sse, StreamableHttp |
ServerConfig |
enum | contract.rs |
Per-server connection description, discriminated by transport: Stdio/Sse/StreamableHttp |
BridgeConfig |
struct | contract.rs |
The set of ServerConfigs to connect on start: servers: Vec<ServerConfig> |
RemoteToolRef |
struct | contract.rs |
A { server, name } pair identifying one tool on one server |
RemoteTool |
struct | contract.rs |
A RemoteToolRef (r#ref) paired with the model-facing ToolDescriptor |
RemoteCallResult |
struct | contract.rs |
Raw invocation result: content: Vec<Value> (MCP blocks) plus is_error: bool |
EndpointStatus |
struct | contract.rs |
Point-in-time health: server, phase, tool_count, optional fault |
FleetStatus |
type alias | contract.rs |
IndexMap<String, EndpointStatus> keyed by server id, in config order |
Module map
| Module | Holds |
|---|---|
protocol_bridge::contract |
The frozen vocabulary and declares no behaviour: the fault model (ProtocolFault/FaultKind), the endpoint lifecycle (EndpointPhase), per-server config shapes (ServerConfig/BridgeConfig), the server__tool grammar (QUALIFIER/qualify_tool_name), the shared handles (RemoteTool/RemoteCallResult/EndpointStatus), and the two behavioural traits (ServerEndpoint, ServerFleet). |
protocol_bridge::endpoint |
ServerEndpointImpl — the single-server client connection over the rmcp client; the transport builders, the phase machine, single-flight connect, and the tool-listing cache. |
protocol_bridge::fleet |
ServerFleetImpl — parallel multi-server lifecycle with per-server fault isolation. |
protocol_bridge::bridge |
mount_protocol_bridge — grafts remote tools into the kernel ToolRegistry; the inert remote backends, content projection, and remote-tool wrapper. |
protocol_bridge::host |
ProviderHost — the agent-as-MCP-server provider (the rmcp ServerHandler). |
protocol_bridge::schema |
normalize_schema — the byte-for-byte JSON-Schema normalizer. |
contract.rs is the single source of truth: every behavioural module imports its
nouns from there. bridge.rs is the only module that crosses into the
Capabilities kernel; the rest is self-contained
over rmcp.
Key concepts
Qualified tool name. Remote tool names are unique only within their own
server, so each is grafted under "<server_id>__<tool_name>". The QUALIFIER is
"__", kept a legal identifier across providers. The endpoint stamps the
qualified name onto each ToolDescriptor (via qualify_tool_name) while keeping
the unqualified server-side name on the RemoteToolRef; the wrapper advertises
the qualified name to the model but routes invocations back under the unqualified
r#ref.name.
The EndpointPhase machine. An endpoint advertises an explicit lifecycle —
idle → connecting → ready → closing → closed, with any failure short-circuiting
to faulted — instead of scattered boolean flags. is_usable_phase
(== Ready) gates tool calls; is_terminal_phase is Closed | Faulted.
Discriminated fault model. Every bridge error is a single ProtocolFault
discriminated by .kind (a FaultKind), with the original triggering value on
the standard std::error::Error::source chain (a FaultCause = Arc<dyn Error + Send + Sync>). Callers branch on the kind rather than parsing message text. The
fault is Clone (the Arc cause makes it cheap to share — single-flight connect
memoizes and hands the same fault to concurrent callers).
Failure isolation. The fleet connects all endpoints in parallel via
futures::future::join_all (deliberately not try_join_all, which would
short-circuit). A server that fails to spawn or handshake just stays faulted,
records its own fault, and contributes no tools, while healthy peers serve.
mount_protocol_bridge skips non-Ready endpoints and any whose list_tools
errs.
Fault classification by call site. Faults are classified by where they
occur, not by inspecting the rmcp error type — so the six-kind contract
survives a different SDK's error taxonomy. stdio connect → SpawnFailed; http
connect → Transport; list_tools failure → Protocol; invoke SDK throw →
ToolError; any operation before Ready → NotConnected.
Single-flight connect. A second open() while a connect is in flight
serializes on the endpoint's connect_lock mutex rather than starting a second
attempt, matching the TS shared-promise semantics.
Injected-transport seam. create_server_endpoint accepts an optional
InjectedTransport. When present, config-derived transport construction is
bypassed entirely. The one variant, InjectedTransport::Duplex(DuplexStream),
wires an endpoint to the client half of a tokio::io::duplex pair so in-process
callers and tests can connect a host to an endpoint with no process spawned (the
in-memory round-trip the mod.rs doc-comment describes). It mirrors the TS
InMemoryTransport.
Pre-flight-only invoke cancellation (W-7). invoke performs a pre-flight
readiness check only; an in-flight MCP call_tool is never raced against a
cancel token. Threading cancel into the rmcp call would be a behavioral change
versus the TS/Python editions, so it is deliberately not done.
The bridge contract
contract.rs declares the two behavioural traits that the endpoint, fleet, host,
and mount pass between one another. Both are async_trait and Send + Sync.
A ServerEndpoint is one live connection to one external MCP server. Every
failure surfaces as a ProtocolFault; open/list_tools/invoke are fallible,
while close is infallible (it swallows errors and always lands at closed, or
leaves a faulted terminal untouched):
#[async_trait]
pub trait ServerEndpoint: Send + Sync {
fn config(&self) -> &ServerConfig;
fn phase(&self) -> EndpointPhase;
async fn open(&self) -> Result<(), ProtocolFault>;
async fn list_tools(&self) -> Result<Vec<RemoteTool>, ProtocolFault>;
async fn invoke(&self, name: &str, args: Value) -> Result<RemoteCallResult, ProtocolFault>;
fn status(&self) -> EndpointStatus;
async fn close(&self);
}
A ServerFleet is a name-keyed collection of endpoints with a unified lifecycle;
its defining responsibility is per-server failure isolation:
#[async_trait]
pub trait ServerFleet: Send + Sync {
async fn spin_up(&self) -> Result<(), ProtocolFault>;
async fn tear_down(&self);
fn endpoint(&self, server: &str) -> Option<Arc<dyn ServerEndpoint>>;
fn endpoints(&self) -> Vec<Arc<dyn ServerEndpoint>>;
fn status(&self) -> FleetStatus;
}
A server's connection description is a discriminated union on its transport, so a
stdio server and an http server cannot be confused at the type level. The
StreamableHttp variant is reserved for the SSE → Streamable HTTP migration
(interop.md R7); reserving it from day one keeps the union and fault contracts
stable:
pub enum ServerConfig {
Stdio { id: String, command: String, args: Vec<String>, env: IndexMap<String, String> },
Sse { id: String, url: String, headers: IndexMap<String, String> },
StreamableHttp { id: String, url: String, headers: IndexMap<String, String> },
}
Convenience constructors ServerConfig::stdio(id, command) and
ServerConfig::sse(id, url) build the no-args / no-headers forms, and
.id() / .kind() read the shared discriminants across all variants.
Mounting external servers
mount_protocol_bridge is the high-level entry point — the join between the two
halves of the bridge:
pub async fn mount_protocol_bridge(config: BridgeConfig) -> MountedProtocolBridge
It starts a ServerFleetImpl from the BridgeConfig, iterates the endpoints,
skips any failing is_usable_phase, lists the rest's tools (skipping any whose
list_tools errs), wraps each remote tool as a kernel DefinedTool (routing
run back to the owning endpoint under the unqualified name), registers them
in a fresh ToolRegistry, and returns the boxed tools alongside the live fleet:
pub struct MountedProtocolBridge {
pub box_: ToolBox,
pub fleet: Arc<dyn ServerFleet>,
}
use std::sync::Arc;
use indusagi::interop::{BridgeConfig, ServerConfig, mount_protocol_bridge};
use indexmap::IndexMap;
#[tokio::main]
async fn main() {
let config = BridgeConfig::new(vec![
ServerConfig::Stdio {
id: "fs".into(),
command: "npx".into(),
args: vec![
"-y".into(),
"@modelcontextprotocol/server-filesystem".into(),
"/tmp".into(),
],
env: IndexMap::new(),
},
ServerConfig::Sse {
id: "remote".into(),
url: "https://example.com/mcp".into(),
headers: IndexMap::new(),
},
]);
let mounted = mount_protocol_bridge(config).await;
// Every grafted tool, qualified: e.g. "fs__read_file", "remote__search".
for descriptor in mounted.box_.descriptors() {
println!("{}", descriptor.name);
}
// Which servers came up ready vs. faulted.
for (server_id, status) in mounted.fleet.status() {
println!("{server_id} {} tools={} fault={:?}",
status.phase, status.tool_count, status.fault.map(|f| f.kind));
}
mounted.fleet.tear_down().await;
}
Each remote tool's inputSchema is run through normalize_schema before it is
shown to the model, and each RemoteCallResult is projected onto a kernel
ToolResult — a {type:"text", text} block becomes a ToolContentBlock::text,
everything else (images, resources, embedded data, malformed) becomes
ToolContentBlock::json so nothing is dropped on the way to the model, and
is_error is carried straight through. Because remote tools do no local I/O, the
registry is boxed against an inert ToolContext whose UnavailableFs and
UnavailableShell backends io::Error::other(...) only if a tool incorrectly
reaches for them.
Driving a single endpoint
For finer control, drive a ServerEndpoint directly. It owns one transport,
advances through the phase machine, and caches its tool listing after the first
successful list_tools. invoke takes the unqualified server-side tool name:
use indusagi::interop::{
create_server_endpoint, ServerConfig, ServerEndpoint, EndpointPhase,
is_usable_phase, is_protocol_fault,
};
use serde_json::json;
#[tokio::main]
async fn main() {
let endpoint = create_server_endpoint(
ServerConfig::stdio("echo", "python"),
None, // no injected transport — build one from the config on connect
);
assert_eq!(endpoint.phase(), EndpointPhase::Idle);
match endpoint.open().await { // idle -> connecting -> ready
Ok(()) if is_usable_phase(endpoint.phase()) => {
for tool in endpoint.list_tools().await.unwrap() {
println!("{} -> {}", tool.descriptor.name, tool.r#ref.name);
}
let result = endpoint.invoke("echo", json!({ "text": "hi" })).await.unwrap();
println!("is_error={} content={:?}", result.is_error, result.content);
}
Ok(()) => {}
Err(fault) => {
if is_protocol_fault(&fault) {
eprintln!("bridge fault: {} {}", fault.kind, fault);
}
}
}
endpoint.close().await; // -> closing -> closed
}
The endpoint is single-shot: once it reaches a terminal phase (Closed or
Faulted) it is not reusable — open() on a Closed/Closing endpoint returns
a NotConnected fault, and on a Faulted endpoint returns the stored fault.
The mutable interior (phase, the live rmcp RunningService, the tool cache,
the recorded fault, the injected transport) is guarded by one tokio::sync::Mutex
so the &self trait methods can mutate it; phase()/status() read it through a
cheap try_lock.
The server fleet
ServerFleetImpl is the multi-server form. Construct it empty, then start(config)
mints one endpoint per ServerConfig and connects them concurrently. Endpoints
are keyed by ServerConfig::id in an IndexMap, which gives O(1) lookup while
preserving declaration order in every snapshot (interop.md R4 — a BTreeMap or
HashMap would re-sort and break ordering parity). endpoints() and status()
return fresh owned snapshots; tear_down() closes everything in parallel.
use indusagi::interop::{start_server_fleet, BridgeConfig, ServerConfig, ServerFleet};
#[tokio::main]
async fn main() {
let fleet = start_server_fleet(BridgeConfig::new(vec![
ServerConfig::stdio("a", "node"),
ServerConfig::stdio("b", "python"),
])).await;
// Every connection already attempted, in parallel, with failure isolation.
for (id, status) in fleet.status() {
println!("{id}: {}", status.phase);
}
fleet.tear_down().await;
}
Each endpoint is registered before any connect, so even one that fails to
connect appears in endpoints()/status() carrying its fault. Connects run via
join_all (never try_join_all): a single failed open() is absorbed — the
endpoint records its own ProtocolFault and moves to Faulted — and never
propagates. A later duplicate id silently supersedes an earlier one
(last-id-wins), matching the registry's last-registration-wins convention.
spin_up() (the trait's vocabulary alias) re-runs start against the last
config, or returns a NotConnected fault if called before any start.
The provider host
The host is the mirror image: it makes the agent an MCP server.
create_provider_host(box_, info) builds a ProviderHost over an Arc<ToolBox>
and an optional ProviderHostInfo; when info is None the host advertises the
default identity (name: "indus-provider-host", version: "0.1.0").
The host is a thin, stateless rmcp ServerHandler that closes over the box in
two directions:
- Catalog.
list_toolsmapsbox_.descriptors()to MCPTools — each descriptor's JSON-Schemaparametersis the client-facinginputSchema, so the host does not re-normalize. - Dispatch.
call_toolroutes a request straight intobox_.runner.run(...)with a fresh, never-cancelledCancellationToken(a clienttools/callcarries none), then projects theToolOutcomeonto aCallToolResultof shape{ content: [{ type: "text", text }], isError }.
connect(transport) binds the handler to a transport and serves; it resolves
once the MCP session is established and returns a RunningProviderHost that must
be kept alive for the connection's lifetime (waiting() awaits the server task,
cancel() tears it down). The host is transport-agnostic — the transport governs
how a client reaches it (stdio pipe, http stream, or an in-memory duplex pair):
use std::sync::Arc;
use indusagi::interop::{create_provider_host, ProviderHostInfo};
use indusagi::capabilities::ToolBox;
async fn serve(box_: Arc<ToolBox>) {
let host = create_provider_host(
box_,
Some(ProviderHostInfo::new("my-agent", "1.0")),
);
let (client_io, server_io) = tokio::io::duplex(8 * 1024);
let running = host.connect(server_io).await.expect("host serves");
// An MCP client on `client_io` can now enumerate and call our tools.
running.waiting().await;
}
connect is generic over rmcp::transport::IntoTransport<RoleServer, E, A>, so
a tokio::io::duplex half, a stdio transport, or an http stream all bind through
the same call. A failure to start the server surfaces as a Transport
ProtocolFault.
Schema normalization
normalize_schema is a pure, table-driven, total function ported byte-for-byte
from the TS source. A remote MCP server authors each tool's inputSchema to its
own taste; before it can become the model-facing parameters it must be reshaped
into a small, defensive, draft-agnostic subset every provider accepts.
pub fn normalize_schema(input: &Value) -> Value
It reads the declared type and dispatches per type
(object/array/string/number/integer/boolean/null), falling back to
a closed { "type": "object", "properties": {} } when the type is absent,
non-string, or unrecognized (including the array-of-types form). A relax
combinator strips, at every level, the keywords no provider validates against:
| Stripped keyword | Why |
|---|---|
$schema, $id, $ref, $defs, definitions |
Draft plumbing / cross-references no provider resolves |
$comment, deprecated, readOnly, writeOnly |
Annotations the validator ignores |
default, examples |
Sample data, not constraints |
contentEncoding, contentMediaType |
Content metadata outside the accepted subset |
The object normalizer guarantees a record-shaped properties with every child
recursively normalized, filters required to keys that actually exist (dropping
it when empty), and normalizes additionalProperties when it is a schema (a
boolean is forwarded untouched). The array normalizer collapses a tuple-form
items (an array of schemas) to its first element so the result stays within the
single-schema subset providers reliably accept. The argument is never mutated;
key insertion order is preserved (serde_json under preserve_order).
use indusagi::interop::normalize_schema;
use serde_json::json;
let clean = normalize_schema(&json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"path": { "type": "string", "default": "/tmp" },
"tags": { "type": "array", "items": [{ "type": "string" }, { "type": "number" }] },
},
"required": ["path", "missing"],
}));
// `$schema` and `default` stripped; `required` filtered to declared keys (["path"]);
// tuple-form `items` collapsed to the first element ({ "type": "string" }).
The fault model
Every failure surfaces as a ProtocolFault, and the fault's .kind is
site-determined:
| Site | FaultKind |
|---|---|
| Opening a stdio transport / spawning the subprocess | SpawnFailed |
Building a stdio transport (TokioChildProcess::new) |
Transport |
| Opening an SSE / Streamable-HTTP transport / handshake | Transport |
list_tools failure |
Protocol |
invoke SDK failure |
ToolError |
Operating on a non-Ready endpoint |
NotConnected |
| Provider host fails to start | Transport |
The Timeout kind is reserved in the enum (FaultKind::ALL enumerates the
closed set of six). Branch on the kind, and read the underlying cause from the
standard Error::source chain:
use indusagi::interop::{FaultKind, is_protocol_fault};
use std::error::Error;
fn handle(fault: &(dyn Error + 'static)) {
if is_protocol_fault(fault) {
let pf = fault.downcast_ref::<indusagi::interop::ProtocolFault>().unwrap();
match pf.kind {
FaultKind::SpawnFailed => { /* the subprocess could not be started */ }
FaultKind::ToolError => { /* the remote tool ran and reported failure */ }
_ => {}
}
if let Some(cause) = pf.source() {
eprintln!("caused by: {cause}");
}
}
}
ProtocolFault is Clone (its cause is an Arc<dyn Error + Send + Sync>), so
the single-flight connect can memoize one fault and hand it to every concurrent
caller. Display writes the message only — the discriminant lives on .kind,
exactly as the TS Error.message carries no kind prefix.
Design invariants
- One vocabulary, one source.
contract.rsowns every noun and declares no behaviour; the endpoint, fleet, bridge, and host all import from it, so the fault model, lifecycle, and config shapes have a single definition. - Discriminate at the type level.
ServerConfigis a discriminated union on transport,ProtocolFaultdiscriminates onFaultKind, andEndpointPhaseis an explicit state machine — no boolean flags, no string codes, nokindinferred from an SDK error type. - Order preservation.
IndexMapis used forenv,headers,by_id, andFleetStatusso configuration order survives every snapshot (interop.md R4). - Failure isolation everywhere.
join_all(nevertry_join_all) on both connect and teardown means one bad server never sinks its peers; the mount skips non-Readyendpoints andlist_toolserrors silently. - Faithful content passthrough. MCP content blocks ride back as opaque
Vec<Value>so images and resource blocks reach the model untouched (R9); a non-array result defaults to[](R10). - Behavioural parity, not SDK parity. Fault classification by call site,
the
__qualifier, the byte-for-bytenormalize_schema, and pre-flight-only invoke cancellation (W-7) are pinned to the TS/Python editions regardless ofrmcp's own shapes.
Relationship to neighbors
Interop depends inward on the Capabilities
kernel — it pulls ToolDescriptor into the contract, and in bridge.rs it uses
ToolRegistry, DefinedTool, define_tool, Tool, ToolBox, ToolContext,
ToolResult, ToolContentBlock, the Fs/Shell backend traits, and
standard_budget to graft remote tools and box them. The provider host routes
into ToolBox::runner with a ToolCall and reads ToolBox::descriptors(). It
takes CancellationToken from Core, and ProtocolFault
is one of the subsystem error types that wraps the shared CoreError base.
Externally it sits on top of the official rmcp SDK (ServiceExt,
RunningService, RoleClient/RoleServer, ServerHandler,
TokioChildProcess, StreamableHttpClientTransport, CallToolRequestParams,
CallToolResult, Content, Tool, ListToolsResult) and tokio::io::duplex
for the in-memory transport.
This module is the Rust counterpart of the
Python interop bridge, preserving its directional
design (client graft + provider host), its __ qualifier, and its discriminated
fault model on top of a different MCP SDK. The legacy indusagi/mcp facade (also
gated by the mcp feature) is a thin camelCase vocabulary shim over this module
for migrating code. See the Architecture overview for where
the layer sits in the stack.
