converge_mcp/client/
mod.rs1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use crate::types::MCP_PROTOCOL_VERSION;
9
10#[derive(Debug, Clone)]
12pub enum McpTransport {
13 Stdio {
15 command: String,
16 args: Vec<String>,
17 env: HashMap<String, String>,
18 },
19 Http {
21 base_url: String,
22 auth_header: Option<String>,
23 },
24}
25
26impl McpTransport {
27 #[must_use]
28 pub fn stdio(command: impl Into<String>, args: &[&str]) -> Self {
29 Self::Stdio {
30 command: command.into(),
31 args: args.iter().map(|s| (*s).to_string()).collect(),
32 env: HashMap::new(),
33 }
34 }
35
36 #[must_use]
37 pub fn stdio_with_env(
38 command: impl Into<String>,
39 args: &[&str],
40 env: HashMap<String, String>,
41 ) -> Self {
42 Self::Stdio {
43 command: command.into(),
44 args: args.iter().map(|s| (*s).to_string()).collect(),
45 env,
46 }
47 }
48
49 #[must_use]
50 pub fn http(base_url: impl Into<String>) -> Self {
51 Self::Http {
52 base_url: base_url.into(),
53 auth_header: None,
54 }
55 }
56
57 #[must_use]
58 pub fn http_with_auth(base_url: impl Into<String>, auth_header: impl Into<String>) -> Self {
59 Self::Http {
60 base_url: base_url.into(),
61 auth_header: Some(auth_header.into()),
62 }
63 }
64
65 #[must_use]
66 pub fn to_uri(&self) -> String {
67 match self {
68 Self::Stdio { command, args, .. } => format!("stdio:{command} {}", args.join(" ")),
69 Self::Http { base_url, .. } => base_url.clone(),
70 }
71 }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct McpServerInfo {
77 pub name: String,
78 pub version: String,
79 #[serde(default)]
80 pub protocol_version: String,
81}
82
83#[derive(Debug, Deserialize)]
85pub struct McpToolDefinition {
86 pub name: String,
87 #[serde(default)]
88 pub description: Option<String>,
89 #[serde(default, rename = "inputSchema")]
90 pub input_schema: Option<serde_json::Value>,
91}
92
93#[derive(Debug)]
98pub struct McpClient {
99 name: String,
100 transport: McpTransport,
101 server_info: Option<McpServerInfo>,
102 connected: bool,
103}
104
105impl McpClient {
106 #[must_use]
107 pub fn new(name: impl Into<String>, transport: McpTransport) -> Self {
108 Self {
109 name: name.into(),
110 transport,
111 server_info: None,
112 connected: false,
113 }
114 }
115
116 #[must_use]
117 pub fn name(&self) -> &str {
118 &self.name
119 }
120
121 #[must_use]
122 pub fn transport(&self) -> &McpTransport {
123 &self.transport
124 }
125
126 #[must_use]
127 pub fn is_connected(&self) -> bool {
128 self.connected
129 }
130
131 pub fn connect(&mut self) -> Result<&McpServerInfo, McpClientError> {
133 self.server_info = Some(McpServerInfo {
134 name: self.name.clone(),
135 version: "1.0.0".to_string(),
136 protocol_version: MCP_PROTOCOL_VERSION.to_string(),
137 });
138 self.connected = true;
139 Ok(self.server_info.as_ref().expect("just set"))
140 }
141
142 pub fn disconnect(&mut self) {
144 self.connected = false;
145 self.server_info = None;
146 }
147}
148
149#[derive(Debug, Default)]
151pub struct McpClientBuilder {
152 name: Option<String>,
153 transport: Option<McpTransport>,
154}
155
156impl McpClientBuilder {
157 #[must_use]
158 pub fn new() -> Self {
159 Self::default()
160 }
161
162 #[must_use]
163 pub fn name(mut self, name: impl Into<String>) -> Self {
164 self.name = Some(name.into());
165 self
166 }
167
168 #[must_use]
169 pub fn stdio(mut self, command: impl Into<String>, args: &[&str]) -> Self {
170 self.transport = Some(McpTransport::stdio(command, args));
171 self
172 }
173
174 #[must_use]
175 pub fn http(mut self, base_url: impl Into<String>) -> Self {
176 self.transport = Some(McpTransport::http(base_url));
177 self
178 }
179
180 pub fn build(self) -> Result<McpClient, McpClientError> {
181 let name = self
182 .name
183 .ok_or_else(|| McpClientError::Config("name required".to_string()))?;
184 let transport = self
185 .transport
186 .ok_or_else(|| McpClientError::Config("transport required".to_string()))?;
187 Ok(McpClient::new(name, transport))
188 }
189}
190
191#[derive(Debug, thiserror::Error)]
193pub enum McpClientError {
194 #[error("configuration error: {0}")]
195 Config(String),
196 #[error("connection failed: {0}")]
197 ConnectionFailed(String),
198 #[error("not connected")]
199 NotConnected,
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_transport_stdio() {
208 let transport = McpTransport::stdio("npx", &["-y", "mcp-server"]);
209 assert!(transport.to_uri().starts_with("stdio:npx"));
210 }
211
212 #[test]
213 fn test_client_connect() {
214 let mut client = McpClient::new("test", McpTransport::stdio("echo", &[]));
215 let info = client.connect().unwrap();
216 assert_eq!(info.name, "test");
217 assert!(client.is_connected());
218 }
219
220 #[test]
221 fn test_builder() {
222 let client = McpClientBuilder::new()
223 .name("test")
224 .stdio("echo", &[])
225 .build()
226 .unwrap();
227 assert_eq!(client.name(), "test");
228 }
229}