1use super::transport::{InboundHandler, McpTransport, stdio::StdioTransport};
9use super::types::{
10 JsonRpcNotification, JsonRpcRequest, McpCallResult, McpContent, McpToolDef, RawJsonRpcMessage,
11 ServerInfo,
12};
13use anyhow::{Context, Result};
14use std::collections::HashMap;
15
16const MCP_PROTOCOL_VERSION: &str = "2025-03-26";
18
19#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21pub struct McpPrompt {
22 pub name: String,
24 #[serde(default)]
26 pub description: Option<String>,
27 #[serde(default)]
29 pub arguments: Vec<McpPromptArgument>,
30}
31
32#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
34pub struct McpPromptArgument {
35 pub name: String,
37 #[serde(default)]
39 pub description: Option<String>,
40 #[serde(default)]
42 pub required: bool,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum McpLogLevel {
48 Debug,
50 Info,
52 Notice,
54 Warning,
56 Error,
58 Critical,
60 Alert,
62 Emergency,
64}
65
66impl McpLogLevel {
67 pub fn as_str(&self) -> &'static str {
69 match self {
70 McpLogLevel::Debug => "debug",
71 McpLogLevel::Info => "info",
72 McpLogLevel::Notice => "notice",
73 McpLogLevel::Warning => "warning",
74 McpLogLevel::Error => "error",
75 McpLogLevel::Critical => "critical",
76 McpLogLevel::Alert => "alert",
77 McpLogLevel::Emergency => "emergency",
78 }
79 }
80}
81
82#[derive(Debug, Clone, serde::Serialize)]
84pub struct McpSamplingRequest {
85 pub messages: Vec<serde_json::Value>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub system_prompt: Option<String>,
90 pub max_tokens: u32,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub temperature: Option<f32>,
95}
96
97pub struct McpClient {
99 transport: Box<dyn McpTransport>,
101 next_id: u64,
103 pub server_info: ServerInfo,
105}
106
107impl std::fmt::Debug for McpClient {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.debug_struct("McpClient")
110 .field("server_info", &self.server_info)
111 .field("next_id", &self.next_id)
112 .field("connected", &self.transport.is_connected())
113 .finish()
114 }
115}
116
117impl McpClient {
118 pub async fn connect(
126 command: &str,
127 args: &[String],
128 env: &HashMap<String, String>,
129 cwd: Option<&str>,
130 debug: bool,
131 ) -> Result<Self> {
132 let transport: Box<dyn McpTransport> =
133 Box::new(StdioTransport::spawn(command, args, env, cwd, debug, None)?);
134 Self::connect_with_transport(transport).await
135 }
136
137 pub async fn connect_with_transport(mut transport: Box<dyn McpTransport>) -> Result<Self> {
143 transport.set_inbound_handler(default_inbound_handler());
144 let mut client = Self {
145 transport,
146 next_id: 1,
147 server_info: ServerInfo {
148 name: String::new(),
149 version: None,
150 protocol_version: String::new(),
151 },
152 };
153 client.initialize().await?;
154 Ok(client)
155 }
156
157 async fn initialize(&mut self) -> Result<()> {
159 let params = serde_json::json!({
160 "protocolVersion": MCP_PROTOCOL_VERSION,
161 "capabilities": {},
162 "clientInfo": {
163 "name": "oxicode-mcp",
164 "version": env!("CARGO_PKG_VERSION")
165 }
166 });
167
168 let result = self
169 .send_request("initialize", Some(params))
170 .await
171 .context("MCP initialize failed")?;
172
173 if let Some(info) = result.get("serverInfo") {
174 self.server_info.name = info
175 .get("name")
176 .and_then(|v| v.as_str())
177 .unwrap_or("unknown")
178 .to_string();
179 self.server_info.version = info
180 .get("version")
181 .and_then(|v| v.as_str())
182 .map(String::from);
183 }
184 if let Some(version) = result.get("protocolVersion").and_then(|v| v.as_str()) {
185 self.server_info.protocol_version = version.to_string();
186 }
187
188 let notification = JsonRpcNotification {
189 jsonrpc: "2.0",
190 method: "notifications/initialized".to_string(),
191 params: None,
192 };
193 let json = serde_json::to_string(¬ification)?;
194 self.transport
195 .notify(&json)
196 .await
197 .context("Failed to send notifications/initialized")?;
198
199 Ok(())
200 }
201
202 pub async fn list_tools(&mut self) -> Result<Vec<McpToolDef>> {
204 let result = self
205 .send_request("tools/list", None)
206 .await
207 .context("MCP tools/list failed")?;
208
209 let tools = result
210 .get("tools")
211 .cloned()
212 .and_then(|v| serde_json::from_value::<Vec<McpToolDef>>(v).ok())
213 .unwrap_or_else(|| {
214 tracing::warn!(
215 "MCP: failed to parse tools/list response from '{}'",
216 self.server_info.name
217 );
218 Vec::new()
219 });
220
221 Ok(tools)
222 }
223
224 pub async fn call_tool(
226 &mut self,
227 name: &str,
228 args: serde_json::Value,
229 ) -> Result<McpCallResult> {
230 let params = serde_json::json!({
231 "name": name,
232 "arguments": args
233 });
234
235 let result = self
236 .send_request("tools/call", Some(params))
237 .await
238 .with_context(|| format!("MCP tools/call '{}' failed", name))?;
239
240 let is_error = result
241 .get("isError")
242 .and_then(|v| v.as_bool())
243 .unwrap_or(false);
244
245 let content = result
246 .get("content")
247 .cloned()
248 .and_then(|v| serde_json::from_value::<Vec<McpContent>>(v).ok())
249 .unwrap_or_default();
250
251 Ok(McpCallResult { content, is_error })
252 }
253
254 pub async fn list_resources(&mut self) -> Result<Vec<serde_json::Value>> {
256 let result = self
257 .send_request("resources/list", None)
258 .await
259 .context("MCP resources/list failed")?;
260
261 Ok(result
262 .get("resources")
263 .and_then(|v| v.as_array())
264 .cloned()
265 .unwrap_or_default())
266 }
267
268 pub async fn read_resource(&mut self, uri: &str) -> Result<Vec<McpContent>> {
270 let params = serde_json::json!({ "uri": uri });
271 let result = self
272 .send_request("resources/read", Some(params))
273 .await
274 .with_context(|| format!("MCP resources/read '{}' failed", uri))?;
275
276 let contents = result
277 .get("contents")
278 .and_then(|v| v.as_array())
279 .cloned()
280 .unwrap_or_default();
281
282 let mut content = Vec::new();
283 for item in contents {
284 if let Some(text) = item.get("text").and_then(|t| t.as_str()) {
285 content.push(McpContent::Text {
286 text: text.to_string(),
287 });
288 } else if item.get("blob").is_some() {
289 content.push(McpContent::Text {
290 text: format!(
291 "[Binary data: {}]",
292 item.get("mimeType")
293 .and_then(|m| m.as_str())
294 .unwrap_or("unknown")
295 ),
296 });
297 }
298 }
299 Ok(content)
300 }
301
302 pub async fn list_prompts(&mut self) -> Result<Vec<McpPrompt>> {
304 let result = self.send_request("prompts/list", None).await?;
305 let prompts = result
306 .get("prompts")
307 .cloned()
308 .unwrap_or(serde_json::Value::Array(vec![]));
309 serde_json::from_value(prompts)
310 .map_err(|e| anyhow::anyhow!("Failed to parse prompts/list response: {}", e))
311 }
312
313 pub async fn get_prompt(
315 &mut self,
316 name: &str,
317 args: HashMap<String, String>,
318 ) -> Result<Vec<serde_json::Value>> {
319 let params = serde_json::json!({
320 "name": name,
321 "arguments": args
322 });
323 let result = self.send_request("prompts/get", Some(params)).await?;
324 let messages = result
325 .get("messages")
326 .cloned()
327 .unwrap_or(serde_json::Value::Array(vec![]));
328 Ok(serde_json::from_value(messages).unwrap_or_default())
329 }
330
331 pub async fn set_log_level(&mut self, level: McpLogLevel) -> Result<()> {
333 let params = serde_json::json!({ "level": level.as_str() });
334 self.send_request("logging/setLevel", Some(params)).await?;
335 Ok(())
336 }
337
338 pub async fn create_sample(
340 &mut self,
341 request: McpSamplingRequest,
342 ) -> Result<serde_json::Value> {
343 let params = serde_json::to_value(&request)
344 .map_err(|e| anyhow::anyhow!("Failed to serialize sampling request: {}", e))?;
345 self.send_request("sampling/createMessage", Some(params))
346 .await
347 }
348
349 pub async fn ping(&mut self) -> Result<()> {
351 self.send_request("ping", None).await?;
352 Ok(())
353 }
354
355 pub fn is_connected(&self) -> bool {
357 self.transport.is_connected()
358 }
359
360 pub fn set_inbound_handler(&mut self, handler: InboundHandler) {
365 self.transport.set_inbound_handler(handler);
366 }
367
368 pub async fn close(&mut self) -> Result<()> {
370 self.transport.close().await
371 }
372
373 async fn send_request(
378 &mut self,
379 method: &str,
380 params: Option<serde_json::Value>,
381 ) -> Result<serde_json::Value> {
382 let id = self.next_id;
383 self.next_id += 1;
384
385 let request = JsonRpcRequest {
386 jsonrpc: "2.0",
387 id,
388 method: method.to_string(),
389 params,
390 };
391
392 let json = serde_json::to_string(&request)?;
393 let resp = self
394 .transport
395 .request(id, &json)
396 .await
397 .with_context(|| format!("MCP request '{}' failed", method))?;
398
399 if let Some(error) = resp.error {
400 return Err(anyhow::anyhow!(
401 "JSON-RPC error {}: {}",
402 error.code,
403 error.message
404 ));
405 }
406 Ok(resp.result.unwrap_or(serde_json::Value::Null))
407 }
408}
409
410fn default_inbound_handler() -> InboundHandler {
421 Box::new(|msg: RawJsonRpcMessage| -> Option<serde_json::Value> {
422 let id = msg.id?;
424 let method = msg.method.as_deref()?;
425 Some(match method {
426 "ping" => serde_json::json!({"jsonrpc": "2.0", "id": id, "result": {}}),
427 "roots/list" => serde_json::json!({
428 "jsonrpc": "2.0",
429 "id": id,
430 "result": {"roots": []}
431 }),
432 _ => serde_json::json!({
433 "jsonrpc": "2.0",
434 "id": id,
435 "error": {"code": -32601, "message": "Method not found"}
436 }),
437 })
438 })
439}