use std::collections::{HashMap, HashSet};
use std::pin::Pin;
use std::sync::Arc;
use async_stream::try_stream;
use deepstrike_core::types::message::{ToolCall, ToolSchema};
use futures::StreamExt;
use futures::stream::Stream;
use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::sync::Mutex;
use crate::run_event::RunEvent;
use crate::runtime::credential_vault::CredentialVault;
use crate::runtime::execution_plane::{ExecutionPlane, LocalExecutionPlane, RunContext};
use crate::tools::RegisteredTool;
use crate::{Error, Result};
pub struct McpServerConfig {
pub command: String,
pub args: Vec<String>,
pub credential_keys: Vec<String>,
pub env: HashMap<String, String>,
}
struct Inner {
stdin: tokio::process::ChildStdin,
reader: BufReader<tokio::process::ChildStdout>,
next_id: u64,
}
struct McpConnection {
server_name: String,
inner: Mutex<Inner>,
child: Mutex<Option<tokio::process::Child>>,
schemas: Vec<ToolSchema>,
}
async fn do_request(
inner: &mut Inner,
server_name: &str,
method: &str,
params: Option<Value>,
) -> Result<Value> {
let id = inner.next_id;
inner.next_id += 1;
let mut msg = serde_json::json!({ "jsonrpc": "2.0", "method": method, "id": id });
if let Some(p) = params {
msg["params"] = p;
}
let line = serde_json::to_string(&msg).map_err(|e| Error::Other(e.to_string()))? + "\n";
inner
.stdin
.write_all(line.as_bytes())
.await
.map_err(|e| Error::Other(e.to_string()))?;
inner
.stdin
.flush()
.await
.map_err(|e| Error::Other(e.to_string()))?;
loop {
let mut buf = String::new();
let n = inner
.reader
.read_line(&mut buf)
.await
.map_err(|e| Error::Other(e.to_string()))?;
if n == 0 {
return Err(Error::Other(format!(
"MCP server '{server_name}' disconnected"
)));
}
let trimmed = buf.trim();
if trimmed.is_empty() {
continue;
}
let Ok(resp) = serde_json::from_str::<Value>(trimmed) else {
continue;
};
let Some(resp_id) = resp.get("id").and_then(Value::as_u64) else {
continue;
};
if resp_id != id {
continue;
}
if let Some(err) = resp.get("error") {
return Err(Error::Other(format!("MCP({server_name}) error: {err}")));
}
return Ok(resp.get("result").cloned().unwrap_or(Value::Null));
}
}
async fn do_notify(inner: &mut Inner, method: &str) {
let msg = serde_json::json!({ "jsonrpc": "2.0", "method": method });
if let Ok(s) = serde_json::to_string(&msg) {
inner.stdin.write_all((s + "\n").as_bytes()).await.ok();
inner.stdin.flush().await.ok();
}
}
impl McpConnection {
async fn start(
server_name: &str,
config: &McpServerConfig,
vault: &dyn CredentialVault,
) -> Result<Arc<Self>> {
let mut env: HashMap<String, String> = std::env::vars().collect();
for (k, v) in &config.env {
env.insert(k.clone(), v.clone());
}
for key in &config.credential_keys {
if let Some(val) = vault.get(key).await {
env.insert(key.clone(), val);
}
}
let mut child = tokio::process::Command::new(&config.command)
.args(&config.args)
.envs(&env)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.spawn()
.map_err(|e| {
Error::Other(format!("failed to spawn MCP server '{server_name}': {e}"))
})?;
let stdin = child
.stdin
.take()
.ok_or_else(|| Error::Other(format!("MCP server '{server_name}': no stdin handle")))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| Error::Other(format!("MCP server '{server_name}': no stdout handle")))?;
let mut inner = Inner {
stdin,
reader: BufReader::new(stdout),
next_id: 1,
};
do_request(
&mut inner,
server_name,
"initialize",
Some(serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": { "tools": {} },
"clientInfo": { "name": "deepstrike", "version": "0.1.0" },
})),
)
.await?;
do_notify(&mut inner, "notifications/initialized").await;
let list = do_request(&mut inner, server_name, "tools/list", None).await?;
let raw_tools = list
.get("tools")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let mut schemas = Vec::new();
for t in &raw_tools {
let name = t.get("name").and_then(Value::as_str).unwrap_or("");
if name.is_empty() {
continue;
}
let description = t.get("description").and_then(Value::as_str).unwrap_or(name);
let parameters = t
.get("inputSchema")
.cloned()
.unwrap_or_else(|| serde_json::json!({ "type": "object", "properties": {} }));
schemas.push(ToolSchema {
name: name.into(),
description: description.to_string(),
parameters,
});
}
Ok(Arc::new(McpConnection {
server_name: server_name.to_string(),
inner: Mutex::new(inner),
child: Mutex::new(Some(child)),
schemas,
}))
}
async fn execute(&self, call: &ToolCall) -> (String, bool) {
let params = serde_json::json!({
"name": call.name.as_str(),
"arguments": call.arguments,
});
let mut inner = self.inner.lock().await;
match do_request(&mut inner, &self.server_name, "tools/call", Some(params)).await {
Ok(result) => {
let text = result
.get("content")
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter(|c| c.get("type").and_then(Value::as_str) == Some("text"))
.filter_map(|c| c.get("text").and_then(Value::as_str))
.collect::<Vec<_>>()
.join("\n")
})
.unwrap_or_default();
let is_error = result
.get("isError")
.and_then(Value::as_bool)
.unwrap_or(false);
let output = if text.is_empty() {
serde_json::to_string(&result).unwrap_or_default()
} else {
text
};
(output, is_error)
}
Err(e) => (e.to_string(), true),
}
}
async fn stop(&self) {
if let Some(mut child) = self.child.lock().await.take() {
child.kill().await.ok();
child.wait().await.ok();
}
}
}
pub struct McpProxyPlane {
server_configs: HashMap<String, McpServerConfig>,
vault: Arc<dyn CredentialVault>,
connections: Vec<Arc<McpConnection>>,
tool_to_conn: HashMap<String, Arc<McpConnection>>,
local: LocalExecutionPlane,
local_names: HashSet<String>,
}
impl McpProxyPlane {
pub fn new(vault: Arc<dyn CredentialVault>) -> Self {
Self {
server_configs: HashMap::new(),
vault,
connections: Vec::new(),
tool_to_conn: HashMap::new(),
local: LocalExecutionPlane::new(),
local_names: HashSet::new(),
}
}
pub fn add_server(&mut self, name: impl Into<String>, config: McpServerConfig) -> &mut Self {
self.server_configs.insert(name.into(), config);
self
}
pub async fn connect(&mut self) -> Result<()> {
for (name, config) in &self.server_configs {
let conn = McpConnection::start(name, config, self.vault.as_ref()).await?;
for schema in &conn.schemas {
self.tool_to_conn
.insert(schema.name.to_string(), Arc::clone(&conn));
}
self.connections.push(conn);
}
Ok(())
}
pub async fn disconnect(&mut self) {
for conn in &self.connections {
conn.stop().await;
}
self.connections.clear();
self.tool_to_conn.clear();
}
pub fn register(&mut self, tool: RegisteredTool) -> &mut Self {
self.local_names.insert(tool.schema.name.to_string());
self.local.register(tool);
self
}
pub fn unregister(&mut self, name: &str) -> &mut Self {
self.local_names.remove(name);
self.local.unregister(name);
self
}
}
impl ExecutionPlane for McpProxyPlane {
fn schemas(&self) -> Vec<ToolSchema> {
let mut result = self.local.schemas();
for conn in &self.connections {
result.extend(conn.schemas.iter().cloned());
}
result
}
fn execute_all<'a>(
&'a self,
calls: &'a [ToolCall],
ctx: RunContext<'a>,
) -> Pin<Box<dyn Stream<Item = Result<RunEvent>> + Send + 'a>> {
Box::pin(try_stream! {
let RunContext {
agent_id, skill_dir, dream_store, knowledge_source, governance, on_tool_suspend, on_permission_request,
} = ctx;
let local_calls: Vec<_> = calls
.iter()
.filter(|c| self.local_names.contains(c.name.as_str()))
.cloned()
.collect();
let mcp_calls: Vec<_> = calls
.iter()
.filter(|c| !self.local_names.contains(c.name.as_str()))
.cloned()
.collect();
if !local_calls.is_empty() {
let local_ctx = RunContext {
agent_id,
skill_dir,
dream_store,
knowledge_source,
governance: governance.clone(),
on_tool_suspend: on_tool_suspend.clone(),
on_permission_request: on_permission_request.clone(),
};
let mut s = self.local.execute_all(&local_calls, local_ctx);
while let Some(evt) = s.next().await {
yield evt?;
}
}
let mut groups: HashMap<usize, (Arc<McpConnection>, Vec<ToolCall>)> = HashMap::new();
let mut unknown: Vec<ToolCall> = Vec::new();
for call in &mcp_calls {
if let Some(conn) = self.tool_to_conn.get(call.name.as_str()) {
let key = Arc::as_ptr(conn) as usize;
let entry = groups.entry(key).or_insert_with(|| (Arc::clone(conn), Vec::new()));
entry.1.push(call.clone());
} else {
unknown.push(call.clone());
}
}
for call in &unknown {
yield RunEvent::ToolResult {
call_id: call.id.to_string(),
content: format!("unknown MCP tool: {}", call.name),
is_error: true,
is_fatal: false,
error_kind: None,
};
}
let futs: Vec<_> = groups
.into_values()
.map(|(conn, group_calls)| async move {
let mut results = Vec::new();
for call in &group_calls {
let (output, is_error) = conn.execute(call).await;
results.push((call.id.to_string(), output, is_error));
}
results
})
.collect();
let all_results = futures::future::join_all(futs).await;
for group in all_results {
for (call_id, content, is_error) in group {
yield RunEvent::ToolResult {
call_id,
content,
is_error,
is_fatal: false,
error_kind: None,
};
}
}
})
}
}