agent_sdk/mcp/
client.rs

1//! MCP client implementation.
2
3use anyhow::{Context, Result, bail};
4use serde_json::{Value, json};
5use std::sync::Arc;
6
7use super::protocol::JsonRpcRequest;
8use super::protocol::{
9    ClientCapabilities, ClientInfo, InitializeParams, InitializeResult, McpToolCallResult,
10    McpToolDefinition, ToolCallParams, ToolsListResult,
11};
12use super::transport::McpTransport;
13
14/// MCP protocol version.
15pub const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
16
17/// MCP client for communicating with MCP servers.
18///
19/// The client handles the MCP protocol, including initialization,
20/// tool discovery, and tool execution.
21///
22/// # Example
23///
24/// ```ignore
25/// use agent_sdk::mcp::{McpClient, StdioTransport};
26///
27/// // Spawn server and create client
28/// let transport = StdioTransport::spawn("npx", &["-y", "mcp-server"]).await?;
29/// let client = McpClient::new(transport, "my-server".to_string()).await?;
30///
31/// // List available tools
32/// let tools = client.list_tools().await?;
33///
34/// // Call a tool
35/// let result = client.call_tool("tool_name", json!({"arg": "value"})).await?;
36/// ```
37pub struct McpClient<T: McpTransport> {
38    transport: Arc<T>,
39    server_name: String,
40    server_info: Option<InitializeResult>,
41}
42
43impl<T: McpTransport> McpClient<T> {
44    /// Create a new MCP client and initialize the connection.
45    ///
46    /// # Arguments
47    ///
48    /// * `transport` - The transport to use for communication
49    /// * `server_name` - A name to identify this server connection
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if initialization fails.
54    pub async fn new(transport: Arc<T>, server_name: String) -> Result<Self> {
55        let mut client = Self {
56            transport,
57            server_name,
58            server_info: None,
59        };
60
61        client.initialize().await?;
62
63        Ok(client)
64    }
65
66    /// Create a client without initialization.
67    ///
68    /// Use this if you need to control when initialization happens.
69    #[must_use]
70    pub const fn new_uninitialized(transport: Arc<T>, server_name: String) -> Self {
71        Self {
72            transport,
73            server_name,
74            server_info: None,
75        }
76    }
77
78    /// Initialize the MCP connection.
79    ///
80    /// This must be called before using other methods.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if the server rejects initialization.
85    pub async fn initialize(&mut self) -> Result<&InitializeResult> {
86        let params = InitializeParams {
87            protocol_version: MCP_PROTOCOL_VERSION.to_string(),
88            capabilities: ClientCapabilities::default(),
89            client_info: ClientInfo {
90                name: "agent-sdk".to_string(),
91                version: env!("CARGO_PKG_VERSION").to_string(),
92            },
93        };
94
95        let request = JsonRpcRequest::new("initialize", Some(serde_json::to_value(&params)?), 0);
96
97        let response = self.transport.send(request).await?;
98
99        let result: InitializeResult = response
100            .result
101            .map(serde_json::from_value)
102            .transpose()
103            .context("Failed to parse initialize response")?
104            .context("Initialize response missing result")?;
105
106        // Send initialized notification
107        let notification = JsonRpcRequest::new("notifications/initialized", None, 0);
108        // Notifications don't expect a response, but we send through the same channel
109        let _ = self.transport.send(notification).await;
110
111        self.server_info = Some(result);
112
113        self.server_info
114            .as_ref()
115            .context("Server info not available")
116    }
117
118    /// Get the server name.
119    #[must_use]
120    pub fn server_name(&self) -> &str {
121        &self.server_name
122    }
123
124    /// Get server info if initialized.
125    #[must_use]
126    pub const fn server_info(&self) -> Option<&InitializeResult> {
127        self.server_info.as_ref()
128    }
129
130    /// List available tools from the server.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if the request fails.
135    pub async fn list_tools(&self) -> Result<Vec<McpToolDefinition>> {
136        let request = JsonRpcRequest::new("tools/list", None, 0);
137
138        let response = self.transport.send(request).await?;
139
140        let result: ToolsListResult = response
141            .result
142            .map(serde_json::from_value)
143            .transpose()
144            .context("Failed to parse tools/list response")?
145            .context("tools/list response missing result")?;
146
147        Ok(result.tools)
148    }
149
150    /// Call a tool on the server.
151    ///
152    /// # Arguments
153    ///
154    /// * `name` - Tool name to call
155    /// * `arguments` - Tool arguments as JSON
156    ///
157    /// # Errors
158    ///
159    /// Returns an error if the tool call fails.
160    pub async fn call_tool(&self, name: &str, arguments: Value) -> Result<McpToolCallResult> {
161        let params = ToolCallParams {
162            name: name.to_string(),
163            arguments: Some(arguments),
164        };
165
166        let request = JsonRpcRequest::new("tools/call", Some(serde_json::to_value(&params)?), 0);
167
168        let response = self.transport.send(request).await?;
169
170        if let Some(ref error) = response.error {
171            bail!("Tool call failed: {} (code {})", error.message, error.code);
172        }
173
174        let result: McpToolCallResult = response
175            .result
176            .map(serde_json::from_value)
177            .transpose()
178            .context("Failed to parse tools/call response")?
179            .context("tools/call response missing result")?;
180
181        Ok(result)
182    }
183
184    /// Call a tool with raw Value arguments.
185    ///
186    /// # Arguments
187    ///
188    /// * `name` - Tool name to call
189    /// * `arguments` - Tool arguments as optional JSON
190    ///
191    /// # Errors
192    ///
193    /// Returns an error if the tool call fails.
194    pub async fn call_tool_raw(
195        &self,
196        name: &str,
197        arguments: Option<Value>,
198    ) -> Result<McpToolCallResult> {
199        let args = arguments.unwrap_or_else(|| json!({}));
200        self.call_tool(name, args).await
201    }
202
203    /// Close the client connection.
204    ///
205    /// # Errors
206    ///
207    /// Returns an error if the transport fails to close.
208    pub async fn close(&self) -> Result<()> {
209        self.transport.close().await
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_mcp_protocol_version() {
219        assert!(!MCP_PROTOCOL_VERSION.is_empty());
220    }
221
222    #[test]
223    fn test_client_info() {
224        let info = ClientInfo {
225            name: "test".to_string(),
226            version: "1.0.0".to_string(),
227        };
228
229        let json = serde_json::to_string(&info).expect("serialize");
230        assert!(json.contains("test"));
231        assert!(json.contains("1.0.0"));
232    }
233}