use std::path::{Path, PathBuf};
use std::sync::Arc;
use mentra::{mcp::McpManager, tool::AudienceToolRegistration, tool::ToolAudience};
use crate::runtime::Runtime;
use super::{ConfiguredServer, McpServer};
pub(crate) struct McpConnections {
runtime: Arc<Runtime>,
manager: Option<McpManager>,
claimed: Vec<String>,
bridged: Vec<AudienceToolRegistration>,
root: PathBuf,
names: Vec<String>,
}
impl McpConnections {
pub(crate) async fn connect(
runtime: Arc<Runtime>,
audience: &ToolAudience,
root: &Path,
servers: Vec<ConfiguredServer>,
) -> Self {
let mut manager = McpManager::new();
let mut claimed = Vec::new();
let mut bridged = Vec::new();
let mut names = Vec::new();
for ConfiguredServer {
server,
sse_inferred,
} in servers
{
let effective = runtime.claim_mcp_server(server.name(), root);
claimed.push(effective.clone());
let outcome = match server {
McpServer::Stdio(mut config) => {
config.name = effective.clone();
manager.connect(&config).await.map_err(|e| e.to_string())
}
McpServer::Sse(mut config) => {
config.name = effective.clone();
manager
.connect_sse(&config)
.await
.map_err(|e| e.to_string())
}
McpServer::Http(mut config) => {
config.name = effective.clone();
manager
.connect_streamable_http(&config)
.await
.map_err(|e| e.to_string())
}
};
match outcome {
Ok(tools) => {
let registered = bridge(&runtime, audience, &effective, tools);
runtime.record_bridged_tools(&effective, root, registered_names(®istered));
bridged.extend(registered);
}
Err(error) => {
eprintln!("{}", connect_warning(&effective, &error, sse_inferred));
}
}
names.push(effective);
}
Self {
runtime,
manager: Some(manager),
claimed,
bridged,
root: root.to_path_buf(),
names,
}
}
pub(crate) fn names(&self) -> &[String] {
&self.names
}
}
fn connect_warning(name: &str, error: &str, sse_inferred: bool) -> String {
let mut warning = format!("Warning: MCP server '{name}' failed to connect: {error}");
if sse_inferred {
warning.push_str(
"; basis inferred the HTTP+SSE transport from a bare `url` — if this server \
speaks Streamable HTTP, say `type: \"http\"`",
);
}
warning
}
fn bridge<T>(
runtime: &Runtime,
audience: &ToolAudience,
server: &str,
tools: Vec<T>,
) -> Vec<AudienceToolRegistration>
where
T: mentra::tool::ExecutableTool + 'static,
{
let mut registered = Vec::new();
for tool in tools {
match runtime
.mentra_runtime()
.try_register_tool_for_audience(audience.clone(), tool)
{
Ok(registration) => registered.push(registration),
Err(collision) => eprintln!(
"Warning: MCP server '{server}' offers a tool called '{}', which this runtime \
already answers to; it was not bridged",
collision.name
),
}
}
registered
}
fn registered_names(registered: &[AudienceToolRegistration]) -> Vec<String> {
registered
.iter()
.map(|registration| registration.descriptor().provider.name.clone())
.collect()
}
impl Drop for McpConnections {
fn drop(&mut self) {
self.bridged.clear();
for name in self.claimed.drain(..) {
self.runtime.release_mcp_claim(&name, &self.root);
}
if let Some(mut manager) = self.manager.take() {
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn(async move {
manager.shutdown_all().await;
});
}
}
}
}
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use mentra::tool::{
ParallelToolContext, RuntimeToolDescriptor, ToolDefinition, ToolExecutor, ToolResult,
};
use serde_json::{Value, json};
use super::*;
struct Bridged(&'static str);
impl ToolDefinition for Bridged {
fn descriptor(&self) -> RuntimeToolDescriptor {
RuntimeToolDescriptor::builder(self.0)
.description("a bridged tool")
.input_schema(json!({"type": "object"}))
.build()
}
}
#[async_trait]
impl ToolExecutor for Bridged {
async fn execute(&self, _ctx: ParallelToolContext, _input: Value) -> ToolResult {
Ok("bridged".to_string())
}
}
fn runtime() -> Arc<Runtime> {
Arc::new(
Runtime::builder()
.with_base_url("http://127.0.0.1:1/v1")
.with_api_key("test-key")
.with_ephemeral_history()
.build()
.expect("builds"),
)
}
fn root() -> &'static Path {
Path::new("/repo")
}
fn audience() -> ToolAudience {
crate::runtime::probe::audience_for(root())
}
fn named(registered: &[AudienceToolRegistration]) -> Vec<&str> {
registered
.iter()
.map(|registration| registration.descriptor().provider.name.as_str())
.collect()
}
fn answers(runtime: &Runtime, name: &str) -> bool {
crate::runtime::probe::answers(runtime, root(), name)
}
#[test]
fn a_failed_inferred_transport_names_the_inference_and_the_fix() {
let warning = connect_warning("api", "404 Not Found", true);
assert!(
warning.contains("failed to connect: 404 Not Found"),
"{warning}"
);
assert!(warning.contains("bare `url`"), "{warning}");
assert!(warning.contains("type: \"http\""), "{warning}");
let explicit = connect_warning("api", "404 Not Found", false);
assert!(
!explicit.contains("inferred"),
"an explicit choice gets no lecture: {explicit}"
);
}
#[test]
fn every_tool_a_server_offers_reaches_the_registry() {
let runtime = runtime();
let registered = bridge(
&runtime,
&audience(),
"docs",
vec![Bridged("mcp__docs__search"), Bridged("mcp__docs__fetch")],
);
assert_eq!(
named(®istered),
["mcp__docs__search", "mcp__docs__fetch"]
);
assert!(answers(&runtime, "mcp__docs__search"));
assert!(answers(&runtime, "mcp__docs__fetch"));
}
#[test]
fn a_name_the_runtime_already_answers_to_is_left_where_it_is() {
let runtime = runtime();
runtime
.mentra_runtime()
.register_tool(Bridged("mcp__docs__search"));
let registered = bridge(
&runtime,
&audience(),
"docs",
vec![Bridged("mcp__docs__search"), Bridged("mcp__docs__fetch")],
);
assert_eq!(
named(®istered),
["mcp__docs__fetch"],
"the collision is skipped and the rest of the server still bridges"
);
}
#[test]
fn what_a_workspace_bridged_comes_off_when_it_drops() {
let runtime = runtime();
let connections = McpConnections {
runtime: Arc::clone(&runtime),
manager: None,
claimed: vec!["docs".to_string()],
bridged: bridge(
&runtime,
&audience(),
"docs",
vec![Bridged("mcp__docs__search")],
),
root: PathBuf::from("/repo"),
names: vec!["docs".to_string()],
};
assert!(answers(&runtime, "mcp__docs__search"));
drop(connections);
assert!(
!answers(&runtime, "mcp__docs__search"),
"a registry a host keeps for its whole process must not grow by one \
server's worth of tools per workspace open"
);
}
}