use std::path::Path;
use std::sync::Arc;
use self::client::McpClient;
use self::tool::{McpResourceTool, McpTool};
use crate::openai_compatible::tools::Tool;
mod client;
mod http;
mod stdio;
mod tool;
#[derive(Debug, Clone)]
pub struct McpServer {
pub name: String,
pub transport: McpTransport,
}
#[derive(Debug, Clone)]
pub enum McpTransport {
Stdio {
command: String,
args: Vec<String>,
env: Vec<(String, String)>,
},
Http {
url: String,
headers: Vec<(String, String)>,
},
}
impl McpServer {
pub fn stdio(name: impl Into<String>, command: impl Into<String>, args: Vec<String>) -> Self {
Self { name: name.into(), transport: McpTransport::Stdio { command: command.into(), args, env: Vec::new() } }
}
pub fn http(name: impl Into<String>, url: impl Into<String>) -> Self {
Self { name: name.into(), transport: McpTransport::Http { url: url.into(), headers: Vec::new() } }
}
#[must_use]
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
if let McpTransport::Stdio { env, .. } = &mut self.transport {
env.push((key.into(), value.into()));
}
self
}
#[must_use]
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
if let McpTransport::Http { headers, .. } = &mut self.transport {
headers.push((key.into(), value.into()));
}
self
}
}
pub(crate) fn connect_all(servers: &[McpServer], cwd: &Path) -> (Vec<Box<dyn Tool>>, Vec<String>) {
let mut tools: Vec<Box<dyn Tool>> = Vec::new();
let mut status = Vec::new();
for server in servers {
match McpClient::connect(server, cwd) {
Ok((client, defs)) => {
let n = defs.len();
let client = Arc::new(client);
for def in defs {
tools.push(Box::new(McpTool::new(client.clone(), &server.name, def)));
}
let resources = client.list_resources();
let r = resources.len();
if !resources.is_empty() {
tools.push(Box::new(McpResourceTool::new(client.clone(), &server.name, &resources)));
}
let res_note = if r > 0 { format!(", {r} resource{}", plural(r)) } else { String::new() };
status.push(format!("mcp: connected `{}` ({n} tool{}{res_note})", server.name, plural(n)));
}
Err(e) => status.push(format!("mcp: `{}` unavailable — {e}", server.name)),
}
}
(tools, status)
}
fn plural(n: usize) -> &'static str {
if n == 1 {
""
} else {
"s"
}
}
#[derive(Debug, Clone)]
pub struct McpPrompt {
pub server: String,
pub name: String,
pub description: String,
pub arguments: Vec<McpPromptArg>,
}
#[derive(Debug, Clone)]
pub struct McpPromptArg {
pub name: String,
pub description: String,
pub required: bool,
}
#[derive(Debug, Clone)]
pub struct PromptMessage {
pub role: String,
pub content: String,
}
pub(crate) fn list_prompts(servers: &[McpServer], cwd: &Path) -> Vec<McpPrompt> {
let mut out = Vec::new();
for server in servers {
if let Ok((client, _tools)) = McpClient::connect(server, cwd) {
for p in client.list_prompts() {
out.push(McpPrompt {
server: server.name.clone(),
name: p.name,
description: p.description,
arguments: p.arguments,
});
}
}
}
out
}
pub(crate) fn get_prompt(
servers: &[McpServer],
server: &str,
name: &str,
arguments: &[(String, String)],
cwd: &Path,
) -> Result<Vec<PromptMessage>, String> {
let cfg = servers.iter().find(|s| s.name == server).ok_or_else(|| format!("no MCP server named `{server}`"))?;
let (client, _tools) = McpClient::connect(cfg, cwd)?;
client.get_prompt(name, arguments)
}