1use std::collections::HashMap;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, Mutex};
8use std::time::Duration;
9
10use serde::Deserialize;
11use serde_json::{Map, Value, json};
12use tokio::sync::{Notify, mpsc, oneshot};
13use tokio_util::sync::CancellationToken;
14use yapi_types::sync::lock;
15
16use super::jsonrpc::{Incoming, METHOD_NOT_FOUND, McpError, classify, id_key, is_id};
17use super::transport::{Event, Transport};
18
19pub const LATEST_PROTOCOL_VERSION: &str = "2025-11-25";
21pub const SUPPORTED_PROTOCOL_VERSIONS: [&str; 4] = [
24 LATEST_PROTOCOL_VERSION,
25 "2025-06-18",
26 "2025-03-26",
27 "2024-11-05",
28];
29const MAX_LIST_PAGES: usize = 1_000;
30
31#[derive(Clone, Debug, PartialEq)]
33pub struct Root {
34 pub uri: String,
36 pub name: Option<String>,
38}
39
40#[derive(Clone, Debug, PartialEq)]
42pub struct ClientOptions {
43 pub name: String,
45 pub version: String,
47 pub request_timeout: Duration,
49 pub roots: Vec<Root>,
52}
53
54pub type ProgressListener = Arc<dyn Fn(&Value) + Send + Sync>;
56
57#[derive(Clone, Default)]
59pub struct RequestOptions {
60 pub cancel: Option<CancellationToken>,
62 pub timeout: Option<Duration>,
64 pub on_progress: Option<ProgressListener>,
66}
67
68#[derive(Clone, Debug, PartialEq, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct Tool {
72 pub name: String,
74 #[serde(default)]
76 pub title: Option<String>,
77 #[serde(default)]
79 pub description: Option<String>,
80 pub input_schema: Map<String, Value>,
82 #[serde(default)]
84 pub output_schema: Option<Map<String, Value>>,
85 #[serde(default)]
87 pub annotations: Option<Map<String, Value>>,
88}
89
90#[derive(Clone, Debug, PartialEq)]
92pub struct ServerInfo {
93 pub protocol_version: String,
95 pub capabilities: Map<String, Value>,
97 pub instructions: Option<String>,
99}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102enum State {
103 Connecting,
104 Connected,
105 Closed,
106}
107
108struct Pending {
109 reply: oneshot::Sender<Result<Value, McpError>>,
110 progress: Option<ProgressListener>,
111 touched: Arc<Notify>,
112}
113
114type Listener = Arc<dyn Fn(&Value) + Send + Sync>;
115
116struct Inner {
117 options: ClientOptions,
118 transport: Transport,
119 state: Mutex<State>,
120 next_id: AtomicU64,
121 pending: Mutex<HashMap<String, Pending>>,
122 server: Mutex<Option<ServerInfo>>,
123 listeners: Mutex<Vec<(String, Listener)>>,
124 close_listeners: Mutex<Vec<Arc<dyn Fn() + Send + Sync>>>,
125}
126
127#[derive(Clone)]
129pub struct McpClient {
130 inner: Arc<Inner>,
131}
132
133fn validate_initialize(value: &Value) -> Result<ServerInfo, McpError> {
134 let invalid = || McpError::invalid("Invalid MCP initialize result");
135 let object = value.as_object().ok_or_else(invalid)?;
136 let version = object
137 .get("protocolVersion")
138 .and_then(Value::as_str)
139 .ok_or_else(invalid)?;
140 let capabilities = object
141 .get("capabilities")
142 .and_then(Value::as_object)
143 .ok_or_else(invalid)?;
144 let info = object
145 .get("serverInfo")
146 .and_then(Value::as_object)
147 .ok_or_else(invalid)?;
148 if !info.get("name").is_some_and(Value::is_string)
149 || !info.get("version").is_some_and(Value::is_string)
150 {
151 return Err(invalid());
152 }
153 let instructions = match object.get("instructions") {
154 None => None,
155 Some(Value::String(text)) => Some(text.clone()),
156 Some(_) => return Err(invalid()),
157 };
158 Ok(ServerInfo {
159 protocol_version: version.to_owned(),
160 capabilities: capabilities.clone(),
161 instructions,
162 })
163}
164
165fn list_page(
167 method: &str,
168 key: &str,
169 value: &Value,
170 valid: fn(&Map<String, Value>) -> bool,
171) -> Result<(Vec<Value>, Option<String>), McpError> {
172 let items = value
173 .get(key)
174 .and_then(Value::as_array)
175 .ok_or_else(|| McpError::invalid(format!("Invalid MCP {method} result")))?;
176 for item in items {
177 if !item.as_object().is_some_and(valid) {
178 return Err(McpError::invalid(format!(
179 "Invalid entry in MCP {method} result"
180 )));
181 }
182 }
183 let cursor = match value.get("nextCursor") {
185 None | Some(Value::Null) => None,
186 Some(Value::String(text)) if text.is_empty() => None,
187 Some(Value::String(text)) => Some(text.clone()),
188 Some(_) => {
189 return Err(McpError::invalid(format!("Invalid MCP {method} cursor")));
190 }
191 };
192 Ok((items.clone(), cursor))
193}
194
195fn is_tool(tool: &Map<String, Value>) -> bool {
196 tool.get("name").is_some_and(Value::is_string)
197 && tool.get("inputSchema").is_some_and(Value::is_object)
198}
199
200fn is_resource(resource: &Map<String, Value>) -> bool {
202 resource.get("uri").is_some_and(Value::is_string)
203 && resource.get("name").is_none_or(Value::is_string)
204}
205
206fn is_resource_template(template: &Map<String, Value>) -> bool {
207 template.get("uriTemplate").is_some_and(Value::is_string)
208 && template.get("name").is_none_or(Value::is_string)
209}
210
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
213pub enum ResourceKind {
214 Resources,
216 Templates,
218}
219
220impl ResourceKind {
221 fn method(self) -> &'static str {
222 match self {
223 ResourceKind::Resources => "resources/list",
224 ResourceKind::Templates => "resources/templates/list",
225 }
226 }
227
228 pub fn key(self) -> &'static str {
230 match self {
231 ResourceKind::Resources => "resources",
232 ResourceKind::Templates => "resourceTemplates",
233 }
234 }
235
236 fn uri_field(self) -> &'static str {
238 match self {
239 ResourceKind::Resources => "uri",
240 ResourceKind::Templates => "uriTemplate",
241 }
242 }
243
244 fn valid(self) -> fn(&Map<String, Value>) -> bool {
245 match self {
246 ResourceKind::Resources => is_resource,
247 ResourceKind::Templates => is_resource_template,
248 }
249 }
250}
251
252fn with_name(mut item: Value, field: &str) -> Value {
254 if item.get("name").is_none()
255 && let Some(fallback) = item.get(field).cloned()
256 && let Some(object) = item.as_object_mut()
257 {
258 object.insert("name".into(), fallback);
259 }
260 item
261}
262
263fn named(items: Vec<Value>, field: &str) -> Vec<Value> {
265 items
266 .into_iter()
267 .map(|item| with_name(item, field))
268 .collect()
269}
270
271impl McpClient {
272 pub async fn connect(
274 options: ClientOptions,
275 transport: Transport,
276 ) -> Result<McpClient, McpError> {
277 Self::connect_with_stderr(options, transport)
278 .await
279 .map_err(|failure| failure.0)
280 }
281
282 pub async fn connect_with_stderr(
285 options: ClientOptions,
286 transport: Transport,
287 ) -> Result<McpClient, Box<(McpError, Option<String>)>> {
288 let (events, receiver) = mpsc::unbounded_channel();
289 let client = McpClient {
290 inner: Arc::new(Inner {
291 options,
292 transport,
293 state: Mutex::new(State::Connecting),
294 next_id: AtomicU64::new(1),
295 pending: Mutex::new(HashMap::new()),
296 server: Mutex::new(None),
297 listeners: Mutex::new(Vec::new()),
298 close_listeners: Mutex::new(Vec::new()),
299 }),
300 };
301 tokio::spawn(client.clone().dispatch(receiver));
302 match client.handshake(events).await {
303 Ok(()) => Ok(client),
304 Err(error) => {
305 client.close().await;
306 Err(Box::new((error, client.stderr())))
307 }
308 }
309 }
310
311 async fn handshake(&self, events: super::transport::Events) -> Result<(), McpError> {
312 self.inner.transport.start(events).await?;
313 let mut capabilities = Map::new();
314 if !self.inner.options.roots.is_empty() {
315 capabilities.insert("roots".into(), json!({}));
316 }
317 let params = json!({
318 "protocolVersion": LATEST_PROTOCOL_VERSION,
319 "capabilities": capabilities,
320 "clientInfo": {"name": self.inner.options.name, "version": self.inner.options.version},
321 });
322 let result = self
323 .request_internal("initialize", Some(params), RequestOptions::default(), true)
324 .await?;
325 let info = validate_initialize(&result)?;
326 if !SUPPORTED_PROTOCOL_VERSIONS.contains(&info.protocol_version.as_str()) {
327 return Err(McpError::Other(format!(
328 "MCP server selected unsupported protocol version {}",
329 info.protocol_version
330 )));
331 }
332 self.inner
333 .transport
334 .set_protocol_version(&info.protocol_version);
335 *lock(&self.inner.server) = Some(info);
336 self.notify_internal("notifications/initialized", None, true)
337 .await?;
338 *lock(&self.inner.state) = State::Connected;
339 Ok(())
340 }
341
342 pub fn same(&self, other: &McpClient) -> bool {
344 Arc::ptr_eq(&self.inner, &other.inner)
345 }
346
347 pub fn is_connected(&self) -> bool {
349 *lock(&self.inner.state) == State::Connected
350 }
351
352 pub fn server(&self) -> Option<ServerInfo> {
354 lock(&self.inner.server).clone()
355 }
356
357 pub fn stderr(&self) -> Option<String> {
359 self.inner.transport.stderr()
360 }
361
362 pub fn on_notification(&self, method: &str, listener: impl Fn(&Value) + Send + Sync + 'static) {
364 lock(&self.inner.listeners).push((method.to_owned(), Arc::new(listener)));
365 }
366
367 pub fn on_close(&self, listener: impl Fn() + Send + Sync + 'static) {
369 lock(&self.inner.close_listeners).push(Arc::new(listener));
370 }
371
372 pub async fn request(
374 &self,
375 method: &str,
376 params: Option<Value>,
377 options: RequestOptions,
378 ) -> Result<Value, McpError> {
379 self.request_internal(method, params, options, false).await
380 }
381
382 pub async fn list_tools(&self, options: RequestOptions) -> Result<Vec<Tool>, McpError> {
384 let items = self
385 .list_all("tools/list", "tools", is_tool, options)
386 .await?;
387 items
388 .into_iter()
389 .map(|item| {
390 serde_json::from_value(item)
391 .map_err(|_| McpError::invalid("Invalid entry in MCP tools/list result"))
392 })
393 .collect()
394 }
395
396 pub async fn list_resources(
398 &self,
399 kind: ResourceKind,
400 options: RequestOptions,
401 ) -> Result<Vec<Value>, McpError> {
402 let items = self
403 .list_all(kind.method(), kind.key(), kind.valid(), options)
404 .await?;
405 Ok(named(items, kind.uri_field()))
406 }
407
408 pub async fn list_resources_page(
411 &self,
412 kind: ResourceKind,
413 cursor: Option<String>,
414 options: RequestOptions,
415 ) -> Result<(Vec<Value>, Option<String>), McpError> {
416 let (items, next) = self
417 .list_page(kind.method(), kind.key(), kind.valid(), cursor, options)
418 .await?;
419 Ok((named(items, kind.uri_field()), next))
420 }
421
422 pub async fn read_resource(
424 &self,
425 uri: &str,
426 options: RequestOptions,
427 ) -> Result<Value, McpError> {
428 let result = self
429 .request("resources/read", Some(json!({ "uri": uri })), options)
430 .await?;
431 let contents = result
432 .get("contents")
433 .and_then(Value::as_array)
434 .ok_or_else(|| McpError::invalid("Invalid MCP resources/read result"))?;
435 for item in contents {
436 let valid = item.get("uri").is_some_and(Value::is_string)
437 && (item.get("text").is_some_and(Value::is_string)
438 || item.get("blob").is_some_and(Value::is_string));
439 if !valid {
440 return Err(McpError::invalid(
441 "Invalid contents in MCP resources/read result",
442 ));
443 }
444 }
445 Ok(result)
446 }
447
448 pub async fn call_tool(
451 &self,
452 name: &str,
453 arguments: Value,
454 options: RequestOptions,
455 ) -> Result<Value, McpError> {
456 let mut params = json!({ "name": name });
457 if !arguments.is_null() {
458 params["arguments"] = arguments;
459 }
460 let mut result = self.request("tools/call", Some(params), options).await?;
461 let object = result
462 .as_object_mut()
463 .filter(|object| object.get("content").is_none_or(Value::is_array))
464 .ok_or_else(|| McpError::invalid("Invalid MCP tools/call result"))?;
465 if object
466 .get("structuredContent")
467 .is_some_and(|value| !value.is_object())
468 {
469 return Err(McpError::invalid(
470 "Invalid MCP tools/call structured content",
471 ));
472 }
473 if !object.contains_key("content") {
474 object.insert("content".into(), json!([]));
475 }
476 Ok(result)
477 }
478
479 pub async fn close(&self) {
481 self.mark_closed();
482 self.inner.transport.close().await;
483 }
484
485 async fn list_page(
486 &self,
487 method: &str,
488 key: &str,
489 valid: fn(&Map<String, Value>) -> bool,
490 cursor: Option<String>,
491 options: RequestOptions,
492 ) -> Result<(Vec<Value>, Option<String>), McpError> {
493 let params = cursor.map(|cursor| json!({ "cursor": cursor }));
494 let result = self.request(method, params, options).await?;
495 list_page(method, key, &result, valid)
496 }
497
498 async fn list_all(
499 &self,
500 method: &str,
501 key: &str,
502 valid: fn(&Map<String, Value>) -> bool,
503 options: RequestOptions,
504 ) -> Result<Vec<Value>, McpError> {
505 let mut items = Vec::new();
506 let mut seen = std::collections::HashSet::new();
507 let mut cursor = None;
508 for _ in 0..MAX_LIST_PAGES {
509 let (page, next) = self
510 .list_page(method, key, valid, cursor, options.clone())
511 .await?;
512 items.extend(page);
513 let Some(next) = next else {
514 return Ok(items);
515 };
516 if !seen.insert(next.clone()) {
517 return Err(McpError::Other(format!(
518 "MCP {method} returned duplicate cursor: {next}"
519 )));
520 }
521 cursor = Some(next);
522 }
523 Err(McpError::Other(format!(
524 "MCP {method} exceeded {MAX_LIST_PAGES} pages"
525 )))
526 }
527
528 fn require(&self, allow_connecting: bool) -> Result<(), McpError> {
529 let state = *lock(&self.inner.state);
530 match state {
531 State::Connected => Ok(()),
532 State::Connecting if allow_connecting => Ok(()),
533 State::Connecting => Err(McpError::Closed("MCP client is connecting".into())),
534 State::Closed => Err(McpError::Closed("MCP client is closed".into())),
535 }
536 }
537
538 async fn request_internal(
539 &self,
540 method: &str,
541 params: Option<Value>,
542 options: RequestOptions,
543 allow_connecting: bool,
544 ) -> Result<Value, McpError> {
545 self.require(allow_connecting)?;
546 let cancel = options.cancel.clone().unwrap_or_default();
547 if cancel.is_cancelled() {
548 return Err(McpError::Aborted);
549 }
550 let id = self.inner.next_id.fetch_add(1, Ordering::SeqCst);
551 let key = id_key(&json!(id));
552 let params = match (&options.on_progress, params) {
553 (Some(_), params) => {
554 let mut params = params.unwrap_or_else(|| json!({}));
555 let mut meta = params
556 .get("_meta")
557 .and_then(Value::as_object)
558 .cloned()
559 .unwrap_or_default();
560 meta.insert("progressToken".into(), json!(id));
561 params["_meta"] = Value::Object(meta);
562 Some(params)
563 }
564 (None, params) => params,
565 };
566 let mut message = json!({"jsonrpc": "2.0", "id": id, "method": method});
567 if let Some(params) = params {
568 message["params"] = params;
569 }
570 let (reply, mut result) = oneshot::channel();
571 let touched = Arc::new(Notify::new());
572 lock(&self.inner.pending).insert(
573 key.clone(),
574 Pending {
575 reply,
576 progress: options.on_progress.clone(),
577 touched: Arc::clone(&touched),
578 },
579 );
580 let client = self.clone();
584 let sent = key.clone();
585 tokio::spawn(async move {
586 if let Err(error) = client.inner.transport.send(&message).await
587 && let Some(entry) = client.remove_pending(&sent)
588 {
589 let _ = entry.reply.send(Err(error));
590 }
591 });
592 let timeout = options
593 .timeout
594 .unwrap_or(self.inner.options.request_timeout);
595 let cancellable = method != "initialize";
597 loop {
598 let expired = async {
599 if timeout.is_zero() {
600 std::future::pending::<()>().await;
601 }
602 tokio::time::sleep(timeout).await;
603 };
604 tokio::select! {
605 outcome = &mut result => {
606 return outcome.unwrap_or_else(|_| Err(McpError::closed()));
607 }
608 () = touched.notified() => {}
609 () = expired => {
610 self.remove_pending(&key);
611 if cancellable {
612 self.send_cancelled(id, "Request timed out");
613 }
614 return Err(McpError::Timeout(timeout.as_millis() as u64));
615 }
616 () = cancel.cancelled() => {
617 self.remove_pending(&key);
618 if cancellable {
619 self.send_cancelled(id, "AbortError: This operation was aborted");
620 }
621 return Err(McpError::Aborted);
622 }
623 }
624 }
625 }
626
627 fn send_cancelled(&self, id: u64, reason: &str) {
628 let client = self.clone();
629 let message = json!({
630 "jsonrpc": "2.0",
631 "method": "notifications/cancelled",
632 "params": {"requestId": id, "reason": reason},
633 });
634 tokio::spawn(async move {
635 if !matches!(*lock(&client.inner.state), State::Closed) {
636 let _ = client.inner.transport.send(&message).await;
637 }
638 });
639 }
640
641 async fn notify_internal(
642 &self,
643 method: &str,
644 params: Option<Value>,
645 allow_connecting: bool,
646 ) -> Result<(), McpError> {
647 self.require(allow_connecting)?;
648 let mut message = json!({"jsonrpc": "2.0", "method": method});
649 if let Some(params) = params {
650 message["params"] = params;
651 }
652 self.inner.transport.send(&message).await
653 }
654
655 fn remove_pending(&self, key: &str) -> Option<Pending> {
656 lock(&self.inner.pending).remove(key)
657 }
658
659 fn mark_closed(&self) {
661 let was = std::mem::replace(&mut *lock(&self.inner.state), State::Closed);
662 let pending: Vec<Pending> = lock(&self.inner.pending).drain().map(|(_, p)| p).collect();
663 for entry in pending {
664 let _ = entry.reply.send(Err(McpError::closed()));
665 }
666 if was != State::Closed {
667 let listeners = lock(&self.inner.close_listeners).clone();
668 for listener in listeners {
669 listener();
670 }
671 }
672 }
673
674 async fn dispatch(self, mut events: mpsc::UnboundedReceiver<Event>) {
675 while let Some(event) = events.recv().await {
676 match event {
677 Event::Message(message) => self.handle(&message),
678 Event::Error(_) => {}
680 Event::Closed => {
681 self.mark_closed();
682 break;
683 }
684 }
685 }
686 }
687
688 fn handle(&self, message: &Value) {
689 match classify(message) {
690 Some(Incoming::Response { id, outcome }) => {
691 let Some(entry) = self.remove_pending(&id_key(id)) else {
692 return;
693 };
694 let _ = entry.reply.send(outcome.cloned());
695 }
696 Some(Incoming::Request { id, method, .. }) => {
697 let result = match method {
698 "ping" => Ok(json!({})),
699 "roots/list" if !self.inner.options.roots.is_empty() => {
700 let roots: Vec<Value> = self
701 .inner
702 .options
703 .roots
704 .iter()
705 .map(|root| match &root.name {
706 Some(name) => json!({"uri": root.uri, "name": name}),
707 None => json!({"uri": root.uri}),
708 })
709 .collect();
710 Ok(json!({ "roots": roots }))
711 }
712 other => Err(json!({
713 "code": METHOD_NOT_FOUND,
714 "message": format!("Method not found: {other}"),
715 })),
716 };
717 let response = match result {
718 Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
719 Err(error) => json!({"jsonrpc": "2.0", "id": id, "error": error}),
720 };
721 let client = self.clone();
722 tokio::spawn(async move {
723 let _ = client.inner.transport.send(&response).await;
724 });
725 }
726 Some(Incoming::Notification { method, params }) => {
727 let params = params.cloned().unwrap_or(Value::Null);
728 if method == "notifications/progress" {
729 self.progress(¶ms);
730 }
731 let listeners: Vec<Listener> = lock(&self.inner.listeners)
732 .iter()
733 .filter(|(name, _)| name == method)
734 .map(|(_, listener)| Arc::clone(listener))
735 .collect();
736 for listener in listeners {
737 listener(¶ms);
738 }
739 }
740 None => {}
741 }
742 }
743
744 fn progress(&self, params: &Value) {
745 let Some(token) = params.get("progressToken").filter(|token| is_id(token)) else {
746 return;
747 };
748 if !params.get("progress").is_some_and(Value::is_number) {
749 return;
750 }
751 let (touched, listener) = match lock(&self.inner.pending).get(&id_key(token)) {
753 Some(Pending {
754 touched,
755 progress: Some(listener),
756 ..
757 }) => (Arc::clone(touched), Arc::clone(listener)),
758 _ => return,
759 };
760 touched.notify_one();
761 listener(params);
762 }
763}