Skip to main content

tycode_core/mcp/
client.rs

1use std::process::Stdio;
2
3use crate::settings::config::McpServerConfig;
4use rmcp::{
5    model::{CallToolRequestParam, CallToolResult, Tool},
6    service::{RunningService, ServiceExt},
7    transport::{ConfigureCommandExt, TokioChildProcess},
8    ClientHandler,
9};
10use tokio::process::Command;
11use tracing::{debug, info};
12
13/// MCP is pretty basic - we aren't parsing any out of bound messages from MCP
14/// servers so we use this simple handler that no-ops all messages.
15#[derive(Clone, Debug, Default)]
16struct SimpleClientHandler;
17
18impl ClientHandler for SimpleClientHandler {}
19
20pub struct McpClient {
21    name: String,
22    client_handle: RunningService<rmcp::RoleClient, SimpleClientHandler>,
23}
24
25impl McpClient {
26    pub async fn new(name: String, config: McpServerConfig) -> anyhow::Result<Self> {
27        info!(client_name = %name, "Initializing MCP client");
28
29        let cmd = Command::new(&config.command).configure(|c| {
30            c.args(&config.args);
31            c.envs(config.env.iter());
32            c.stderr(Stdio::null());
33            #[cfg(unix)]
34            c.process_group(0);
35            #[cfg(windows)]
36            {
37                const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
38                c.creation_flags(CREATE_NEW_PROCESS_GROUP);
39            }
40        });
41
42        let transport = TokioChildProcess::new(cmd)
43            .map_err(|e| anyhow::anyhow!("Failed to create MCP transport: {e:?}"))?;
44
45        let client: RunningService<rmcp::RoleClient, SimpleClientHandler> = SimpleClientHandler
46            .serve(transport)
47            .await
48            .map_err(|e| anyhow::anyhow!("Failed to serve MCP client: {e:?}"))?;
49
50        debug!(client_name = %name, "MCP client initialized successfully");
51
52        Ok(Self {
53            name,
54            client_handle: client,
55        })
56    }
57
58    pub async fn list_tools(&mut self) -> anyhow::Result<Vec<Tool>> {
59        let tools_response = self
60            .client_handle
61            .list_tools(Default::default())
62            .await
63            .map_err(|e| anyhow::anyhow!("Failed to list MCP tools: {e:?}"))?;
64
65        Ok(tools_response.tools)
66    }
67
68    pub async fn call_tool(
69        &mut self,
70        name: &str,
71        arguments: Option<serde_json::Value>,
72    ) -> anyhow::Result<CallToolResult> {
73        let request = CallToolRequestParam {
74            name: name.to_string().into(),
75            arguments: arguments.as_ref().and_then(|v| v.as_object().cloned()),
76        };
77
78        self.client_handle
79            .call_tool(request)
80            .await
81            .map_err(|e| anyhow::anyhow!("Failed to call MCP tool '{name}': {e:?}"))
82    }
83
84    pub async fn close(self) -> anyhow::Result<()> {
85        info!(client_name = %self.name, "Closing MCP client");
86
87        self.client_handle
88            .cancel()
89            .await
90            .map_err(|e| anyhow::anyhow!("Failed to cancel MCP client: {e:?}"))?;
91
92        debug!(client_name = %self.name, "MCP client closed successfully");
93
94        Ok(())
95    }
96}