use std::path::{Path, PathBuf};
use std::sync::Arc;
use mentra::mcp::McpManager;
use crate::runtime::Runtime;
use super::{ConfiguredServer, McpServer};
pub(crate) struct McpConnections {
runtime: Arc<Runtime>,
manager: Option<McpManager>,
claimed: Vec<String>,
bridged: Vec<String>,
root: PathBuf,
names: Vec<String>,
}
impl McpConnections {
pub(crate) fn empty(runtime: Arc<Runtime>, root: &Path) -> Self {
Self {
runtime,
manager: None,
claimed: Vec::new(),
bridged: Vec::new(),
root: root.to_path_buf(),
names: Vec::new(),
}
}
pub(crate) async fn connect(
runtime: Arc<Runtime>,
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) => bridged.extend(bridge(&runtime, &effective, tools)),
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, server: &str, tools: Vec<T>) -> Vec<String>
where
T: mentra::tool::ExecutableTool + 'static,
{
let mut registered = Vec::new();
for tool in tools {
let name = tool.descriptor().provider.name;
match runtime.mentra_runtime().try_register_tool(tool) {
Ok(()) => registered.push(name),
Err(collision) => eprintln!(
"Warning: MCP server '{server}' offers a tool called '{}', which this runtime \
already answers to; it was not bridged",
collision.name
),
}
}
registered
}
impl Drop for McpConnections {
fn drop(&mut self) {
for name in self.bridged.drain(..) {
self.runtime.mentra_runtime().unregister_tool(&name);
}
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 registers(runtime: &Runtime, name: &str) -> bool {
runtime
.mentra_runtime()
.tools()
.iter()
.any(|tool| tool.provider.name == 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 names = bridge(
&runtime,
"docs",
vec![Bridged("mcp__docs__search"), Bridged("mcp__docs__fetch")],
);
assert_eq!(names, ["mcp__docs__search", "mcp__docs__fetch"]);
assert!(registers(&runtime, "mcp__docs__search"));
assert!(registers(&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 names = bridge(
&runtime,
"docs",
vec![Bridged("mcp__docs__search"), Bridged("mcp__docs__fetch")],
);
assert_eq!(
names,
["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, "docs", vec![Bridged("mcp__docs__search")]),
root: PathBuf::from("/repo"),
names: vec!["docs".to_string()],
};
assert!(registers(&runtime, "mcp__docs__search"));
drop(connections);
assert!(
!registers(&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"
);
}
#[test]
fn an_empty_owner_has_no_manager_claims_tools_or_names() {
let runtime = runtime();
let connections = McpConnections::empty(Arc::clone(&runtime), Path::new("/repo"));
assert!(connections.manager.is_none());
assert!(connections.claimed.is_empty());
assert!(connections.bridged.is_empty());
assert!(connections.names().is_empty());
drop(connections);
assert!(
runtime
.mentra_runtime()
.tools()
.iter()
.all(|tool| !tool.provider.name.starts_with("mcp__"))
);
}
}