use tracing::{debug, info};
use super::transport::McpTransportConnection;
use super::types::*;
pub struct McpClient {
config: McpServerConfig,
transport: Option<McpTransportConnection>,
tools: Vec<McpTool>,
resources: Vec<McpResource>,
status: McpConnectionStatus,
}
impl McpClient {
pub fn new(config: McpServerConfig) -> Self {
Self {
config,
transport: None,
tools: Vec::new(),
resources: Vec::new(),
status: McpConnectionStatus::Disconnected,
}
}
pub fn name(&self) -> &str {
&self.config.name
}
pub fn status(&self) -> &McpConnectionStatus {
&self.status
}
pub fn tools(&self) -> &[McpTool] {
&self.tools
}
pub fn resources(&self) -> &[McpResource] {
&self.resources
}
pub async fn connect(&mut self) -> Result<(), String> {
self.status = McpConnectionStatus::Connecting;
let transport = match &self.config.transport {
McpTransport::Stdio { command, args } => {
McpTransportConnection::connect_stdio(command, args, &self.config.env).await?
}
McpTransport::Sse { url } => McpTransportConnection::connect_sse(url).await?,
};
let init_result = transport
.request(
"initialize",
Some(serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": {}
},
"clientInfo": {
"name": "agent-code",
"version": env!("CARGO_PKG_VERSION")
}
})),
)
.await?;
debug!("MCP server initialized: {:?}", init_result);
transport.notify("notifications/initialized", None).await?;
self.transport = Some(transport);
self.status = McpConnectionStatus::Connected;
self.discover_tools().await?;
self.discover_resources().await?;
info!(
"MCP server '{}' connected: {} tools, {} resources",
self.config.name,
self.tools.len(),
self.resources.len()
);
Ok(())
}
async fn discover_tools(&mut self) -> Result<(), String> {
let transport = self.transport.as_ref().ok_or("Not connected")?;
let result = transport.request("tools/list", None).await?;
if let Some(tools) = result.get("tools").and_then(|v| v.as_array()) {
self.tools = tools
.iter()
.filter_map(|t| serde_json::from_value(t.clone()).ok())
.collect();
}
Ok(())
}
async fn discover_resources(&mut self) -> Result<(), String> {
let transport = self.transport.as_ref().ok_or("Not connected")?;
match transport.request("resources/list", None).await {
Ok(result) => {
if let Some(resources) = result.get("resources").and_then(|v| v.as_array()) {
self.resources = resources
.iter()
.filter_map(|r| serde_json::from_value(r.clone()).ok())
.collect();
}
}
Err(e) => {
debug!(
"MCP server '{}' doesn't support resources: {e}",
self.config.name
);
}
}
Ok(())
}
pub async fn call_tool(
&self,
tool_name: &str,
arguments: serde_json::Value,
) -> Result<McpToolResult, String> {
let transport = self.transport.as_ref().ok_or("Not connected")?;
let result = transport
.request(
"tools/call",
Some(serde_json::json!({
"name": tool_name,
"arguments": arguments,
})),
)
.await?;
serde_json::from_value(result).map_err(|e| format!("Invalid tool result: {e}"))
}
pub async fn read_resource(&self, uri: &str) -> Result<String, String> {
let transport = self.transport.as_ref().ok_or("Not connected")?;
let result = transport
.request("resources/read", Some(serde_json::json!({ "uri": uri })))
.await?;
if let Some(contents) = result.get("contents").and_then(|v| v.as_array()) {
let text: Vec<&str> = contents
.iter()
.filter_map(|c| c.get("text").and_then(|t| t.as_str()))
.collect();
Ok(text.join("\n"))
} else {
Ok(result.to_string())
}
}
pub async fn disconnect(&mut self) {
if let Some(transport) = self.transport.take() {
transport.shutdown().await;
}
self.tools.clear();
self.resources.clear();
self.status = McpConnectionStatus::Disconnected;
}
}