1use crate::client::McpError;
33use crate::inbound;
34use crate::rpc;
35use crate::wire::{Implementation, Prompt, ReadResourceResult, Resource, ServerCapabilities, Tool};
36use serde_json::{Value, json};
37use std::sync::{Arc, Mutex};
38use std::time::Duration;
39
40use rmcp::model::{
41 CallToolRequestParams, ClientCapabilities, ClientInfo, ElicitRequestParams, ElicitResult,
42 ElicitationAction, ElicitationCapability, Implementation as RmcpImpl, ProtocolVersion,
43 ReadResourceRequestParams, SubscriptionFilter,
44};
45use rmcp::service::{RoleClient, RunningService};
46use rmcp::transport::StreamableHttpClientTransport;
47use rmcp::{ClientHandler, ServiceExt};
48
49#[derive(Clone)]
51struct Inbound {
52 caps: inbound::Capabilities,
53 handler: Option<Arc<dyn inbound::Handler>>,
54}
55
56#[derive(Clone)]
65struct Handler {
66 info: ClientInfo,
67 inbound: Inbound,
68 queue: Arc<Mutex<Vec<rpc::Notification>>>,
70}
71
72impl Handler {
73 fn queue(&self, method: &str, params: Value) {
74 self.queue
75 .lock()
76 .unwrap_or_else(|e| e.into_inner())
77 .push(rpc::Notification::new(method, Some(params)));
78 }
79}
80
81fn declined() -> ElicitResult {
82 ElicitResult::new(ElicitationAction::Decline)
83}
84
85impl ClientHandler for Handler {
86 fn get_info(&self) -> ClientInfo {
87 self.info.clone()
88 }
89
90 async fn create_elicitation(
91 &self,
92 params: ElicitRequestParams,
93 _ctx: rmcp::service::RequestContext<RoleClient>,
94 ) -> Result<ElicitResult, rmcp::ErrorData> {
95 let (message, requested_schema) = match ¶ms {
98 ElicitRequestParams::FormElicitationParams {
99 message,
100 requested_schema,
101 ..
102 } => (
103 message.clone(),
104 serde_json::to_value(requested_schema).unwrap_or_else(|_| json!({})),
105 ),
106 _ => return Ok(declined()),
107 };
108 if !self.inbound.caps.elicitation {
109 return Ok(declined());
110 }
111 let answer = self.inbound.handler.as_ref().and_then(|h| {
112 h.handle(inbound::Inbound::Elicit {
113 message,
114 requested_schema,
115 })
116 });
117 Ok(match answer {
118 Some(inbound::Answer::Accept(content)) => {
119 ElicitResult::new(ElicitationAction::Accept).with_content(content)
120 }
121 Some(inbound::Answer::Decline) => declined(),
122 _ => ElicitResult::new(ElicitationAction::Cancel),
125 })
126 }
127
128 async fn on_resource_updated(
131 &self,
132 params: rmcp::model::ResourceUpdatedNotificationParam,
133 _ctx: rmcp::service::NotificationContext<RoleClient>,
134 ) {
135 self.queue(
136 "notifications/resources/updated",
137 serde_json::to_value(¶ms).unwrap_or_else(|_| json!({})),
138 );
139 }
140
141 async fn on_resource_list_changed(&self, _ctx: rmcp::service::NotificationContext<RoleClient>) {
142 self.queue("notifications/resources/list_changed", json!({}));
143 }
144
145 async fn on_tool_list_changed(&self, _ctx: rmcp::service::NotificationContext<RoleClient>) {
146 self.queue("notifications/tools/list_changed", json!({}));
147 }
148
149 async fn on_prompt_list_changed(&self, _ctx: rmcp::service::NotificationContext<RoleClient>) {
150 self.queue("notifications/prompts/list_changed", json!({}));
151 }
152
153 #[allow(deprecated)]
156 async fn on_logging_message(
157 &self,
158 params: rmcp::model::LoggingMessageNotificationParam,
159 _ctx: rmcp::service::NotificationContext<RoleClient>,
160 ) {
161 self.queue(
162 "notifications/message",
163 serde_json::to_value(¶ms).unwrap_or_else(|_| json!({})),
164 );
165 }
166
167 async fn on_progress(
168 &self,
169 params: rmcp::model::ProgressNotificationParam,
170 _ctx: rmcp::service::NotificationContext<RoleClient>,
171 ) {
172 self.queue(
173 "notifications/progress",
174 serde_json::to_value(¶ms).unwrap_or_else(|_| json!({})),
175 );
176 }
177}
178
179pub struct RmcpClient {
181 name: String,
182 rt: tokio::runtime::Runtime,
183 service: RunningService<RoleClient, Handler>,
184 caps: ServerCapabilities,
185 protocol_version: Option<String>,
186 timeout: Duration,
187 tool_meta: Option<Value>,
188 notifications: Arc<Mutex<Vec<rpc::Notification>>>,
189 uris: Mutex<std::collections::BTreeSet<String>>,
191 pump: Mutex<Option<tokio::task::JoinHandle<()>>>,
193}
194
195pub struct RmcpBuilder {
198 name: String,
199 endpoint: String,
200 headers: Vec<(String, String)>,
201 timeout: Duration,
202 client_info: Implementation,
203 inbound: Inbound,
204 http: Option<Arc<crate::http::HttpTransport>>,
208}
209
210impl RmcpBuilder {
211 pub fn new(
212 name: &str,
213 endpoint: &str,
214 headers: Vec<(String, String)>,
215 timeout: Duration,
216 ) -> Self {
217 RmcpBuilder {
218 name: name.to_string(),
219 endpoint: endpoint.to_string(),
220 headers,
221 timeout,
222 client_info: Implementation {
223 name: "agentd".into(),
224 version: env!("CARGO_PKG_VERSION").into(),
225 title: None,
226 },
227 inbound: Inbound {
228 caps: inbound::Capabilities::default(),
229 handler: None,
230 },
231 http: None,
232 }
233 }
234
235 pub fn with_http(mut self, http: Arc<crate::http::HttpTransport>) -> Self {
238 self.http = Some(http);
239 self
240 }
241
242 pub fn with_client_info(mut self, info: Implementation) -> Self {
243 self.client_info = info;
244 self
245 }
246
247 pub fn with_elicitation(mut self, handler: Arc<dyn inbound::Handler>) -> Self {
250 self.inbound.caps.elicitation = true;
251 self.inbound.handler = Some(handler);
252 self
253 }
254
255 pub fn connect(self) -> Result<RmcpClient, McpError> {
257 let rt = tokio::runtime::Builder::new_multi_thread()
264 .worker_threads(1)
265 .enable_all()
266 .thread_name("agentd-mcp")
267 .build()
268 .map_err(|e| {
269 McpError::Transport(format!("mcp server '{}': runtime: {e}", self.name))
270 })?;
271
272 let mut config =
273 rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig::with_uri(
274 self.endpoint.clone(),
275 );
276 for (k, v) in &self.headers {
277 if let (Ok(name), Ok(value)) = (
278 http::HeaderName::from_bytes(k.as_bytes()),
279 http::HeaderValue::from_str(v),
280 ) {
281 config.custom_headers.insert(name, value);
282 }
283 }
284
285 let mut caps = ClientCapabilities::default();
286 if self.inbound.caps.elicitation {
287 caps.elicitation = Some(ElicitationCapability::new());
288 }
289
290 let notifications: Arc<Mutex<Vec<rpc::Notification>>> = Arc::default();
293 let mut implementation = RmcpImpl::new(
294 self.client_info.name.clone(),
295 self.client_info.version.clone(),
296 );
297 implementation.title = self.client_info.title.clone();
298
299 let handler = Handler {
300 queue: Arc::clone(¬ifications),
301 info: ClientInfo::new(caps, implementation)
305 .with_protocol_version(ProtocolVersion::default()),
306 inbound: self.inbound.clone(),
307 };
308
309 let name = self.name.clone();
310 let socket = match &self.http {
313 Some(h) => Arc::clone(h),
314 None => Arc::new(crate::http::HttpTransport::new(
315 crate::http::McpEndpoint::parse(&self.endpoint)
316 .map_err(|e| McpError::Transport(format!("mcp server '{name}': {e}")))?,
317 self.headers.clone(),
318 )),
319 };
320 let client = crate::rmcp_transport::AgentdHttp::new(socket, self.timeout);
321 let service = rt
322 .block_on(async move {
323 let transport = StreamableHttpClientTransport::with_client(client, config);
324 handler.serve(transport).await
325 })
326 .map_err(|e| McpError::Transport(format!("mcp server '{name}': {e}")))?;
327
328 let info = service.peer_info();
329 let protocol_version = info.as_ref().map(|i| i.protocol_version.to_string());
330 let info_json = info
331 .as_ref()
332 .and_then(|i| serde_json::to_value(i.as_ref()).ok());
333 let caps = server_capabilities(info_json.as_ref());
334
335 Ok(RmcpClient {
336 name: self.name,
337 rt,
338 service,
339 caps,
340 protocol_version,
341 timeout: self.timeout,
342 tool_meta: None,
343 notifications,
344 uris: Mutex::new(std::collections::BTreeSet::new()),
345 pump: Mutex::new(None),
346 })
347 }
348}
349
350fn server_capabilities(info: Option<&serde_json::Value>) -> ServerCapabilities {
356 info.and_then(|v| v.get("capabilities"))
357 .and_then(|c| serde_json::from_value(c.clone()).ok())
358 .unwrap_or_default()
359}
360
361fn rpc_err(name: &str, op: &str, e: impl std::fmt::Display) -> McpError {
362 McpError::Transport(format!("mcp server '{name}': {op}: {e}"))
363}
364
365impl RmcpClient {
366 pub fn name(&self) -> &str {
367 &self.name
368 }
369
370 pub fn capabilities(&self) -> &ServerCapabilities {
371 &self.caps
372 }
373
374 pub fn protocol_version(&self) -> Option<&str> {
375 self.protocol_version.as_deref()
376 }
377
378 pub fn set_tool_meta(&mut self, meta: Value) {
379 self.tool_meta = Some(meta);
380 }
381
382 fn convert<T: serde::de::DeserializeOwned>(
386 &self,
387 v: &impl serde::Serialize,
388 what: &str,
389 ) -> Result<T, McpError> {
390 let json = serde_json::to_value(v).map_err(|e| rpc_err(&self.name, what, e))?;
391 serde_json::from_value(json).map_err(|e| rpc_err(&self.name, what, e))
392 }
393
394 pub fn list_tools(&self) -> Result<Vec<Tool>, McpError> {
395 let res = self
396 .rt
397 .block_on(self.service.list_all_tools())
398 .map_err(|e| rpc_err(&self.name, "tools/list", e))?;
399 self.convert(&res, "tools/list")
400 }
401
402 pub fn call_tool(&self, name: &str, args: Option<Value>) -> Result<Value, McpError> {
403 self.call_tool_with_meta(name, args, None)
404 }
405
406 pub fn call_tool_with_meta(
409 &self,
410 name: &str,
411 args: Option<Value>,
412 extra_meta: Option<Value>,
413 ) -> Result<Value, McpError> {
414 let mut arguments = match args {
415 Some(Value::Object(m)) => m,
416 _ => serde_json::Map::new(),
417 };
418 if let Some(m) = merge_meta(self.tool_meta.as_ref(), extra_meta) {
419 arguments.insert("_meta".into(), m);
420 }
421 let param = CallToolRequestParams::new(name.to_string()).with_arguments(arguments);
422 let res = self
423 .rt
424 .block_on(self.service.call_tool(param))
425 .map_err(|e| rpc_err(&self.name, &format!("tools/call {name}"), e))?;
426 serde_json::to_value(&res).map_err(|e| rpc_err(&self.name, "tools/call", e))
427 }
428
429 pub fn list_resources(&self) -> Result<Vec<Resource>, McpError> {
430 let res = self
431 .rt
432 .block_on(self.service.list_all_resources())
433 .map_err(|e| rpc_err(&self.name, "resources/list", e))?;
434 self.convert(&res, "resources/list")
435 }
436
437 pub fn read_resource(&self, uri: &str) -> Result<ReadResourceResult, McpError> {
438 let res = self
439 .rt
440 .block_on(
441 self.service
442 .read_resource(ReadResourceRequestParams::new(uri.to_string())),
443 )
444 .map_err(|e| rpc_err(&self.name, &format!("resources/read {uri}"), e))?;
445 self.convert(&res, "resources/read")
446 }
447
448 pub fn list_prompts(&self) -> Result<Vec<Prompt>, McpError> {
449 let res = self
450 .rt
451 .block_on(self.service.list_all_prompts())
452 .map_err(|e| rpc_err(&self.name, "prompts/list", e))?;
453 self.convert(&res, "prompts/list")
454 }
455
456 pub fn subscribe(&self, uri: &str) -> Result<(), McpError> {
470 {
471 let mut uris = self.uris.lock().unwrap_or_else(|e| e.into_inner());
472 if !uris.insert(uri.to_string()) {
473 return Ok(()); }
475 }
476 self.relisten()
477 }
478
479 #[allow(deprecated)]
480 pub fn unsubscribe(&self, uri: &str) -> Result<(), McpError> {
481 {
482 let mut uris = self.uris.lock().unwrap_or_else(|e| e.into_inner());
483 if !uris.remove(uri) {
484 return Ok(());
485 }
486 }
487 if !self.modern() {
492 return self
493 .rt
494 .block_on(
495 self.service
496 .unsubscribe(rmcp::model::UnsubscribeRequestParams::new(uri.to_string())),
497 )
498 .map_err(|e| rpc_err(&self.name, &format!("resources/unsubscribe {uri}"), e));
499 }
500 self.relisten()
501 }
502
503 fn relisten(&self) -> Result<(), McpError> {
506 if !self.modern() {
510 return self.legacy_subscribe_all();
511 }
512 let uris: Vec<String> = self
513 .uris
514 .lock()
515 .unwrap_or_else(|e| e.into_inner())
516 .iter()
517 .cloned()
518 .collect();
519 *self.pump.lock().unwrap_or_else(|e| e.into_inner()) = None;
521 if uris.is_empty() {
522 return Ok(());
523 }
524
525 let mut filter = SubscriptionFilter::builder().resources_list_changed();
526 for u in &uris {
527 filter = filter.resource_subscription(u.clone());
528 }
529 let filter = filter.build();
530
531 let peer = self.service.peer().clone();
532 let mut subscription = self
533 .rt
534 .block_on(peer.listen(filter))
535 .map_err(|e| rpc_err(&self.name, "subscriptions/listen", e))?;
536
537 let queue = Arc::clone(&self.notifications);
538 let handle = self.rt.spawn(async move {
539 while let Ok(Some(note)) = subscription.next().await {
540 if let Ok(v) = serde_json::to_value(¬e)
541 && let Ok(n) = serde_json::from_value::<rpc::Notification>(v)
542 {
543 queue.lock().unwrap_or_else(|e| e.into_inner()).push(n);
544 }
545 }
546 });
547 *self.pump.lock().unwrap_or_else(|e| e.into_inner()) = Some(handle);
548 Ok(())
549 }
550
551 fn modern(&self) -> bool {
553 self.protocol_version
554 .as_deref()
555 .map(|v| matches!(crate::version::era_of(v), crate::version::Era::Modern))
556 .unwrap_or(false)
557 }
558
559 #[allow(deprecated)]
562 fn legacy_subscribe_all(&self) -> Result<(), McpError> {
563 let uris: Vec<String> = self
564 .uris
565 .lock()
566 .unwrap_or_else(|e| e.into_inner())
567 .iter()
568 .cloned()
569 .collect();
570 for uri in uris {
571 self.rt
572 .block_on(
573 self.service
574 .subscribe(rmcp::model::SubscribeRequestParams::new(uri.clone())),
575 )
576 .map_err(|e| rpc_err(&self.name, &format!("resources/subscribe {uri}"), e))?;
577 }
578 Ok(())
579 }
580
581 pub fn drain_notifications(&self) -> Vec<rpc::Notification> {
584 std::mem::take(&mut *self.notifications.lock().unwrap_or_else(|e| e.into_inner()))
585 }
586
587 pub fn timeout(&self) -> Duration {
589 self.timeout
590 }
591}
592
593fn merge_meta(base: Option<&Value>, extra: Option<Value>) -> Option<Value> {
595 match (base, extra) {
596 (None, None) => None,
597 (Some(b), None) => Some(b.clone()),
598 (None, Some(e)) => Some(e),
599 (Some(b), Some(e)) => {
600 let mut m = b.as_object().cloned().unwrap_or_default();
601 if let Some(eo) = e.as_object() {
602 for (k, v) in eo {
603 m.insert(k.clone(), v.clone());
604 }
605 }
606 Some(Value::Object(m))
607 }
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614
615 #[test]
616 fn meta_overlay_wins_without_mutating_the_base() {
617 let base = json!({"agent/run_id": "r1", "traceparent": "tp"});
618 let merged = merge_meta(Some(&base), Some(json!({"traceparent": "tp2", "k": 1}))).unwrap();
619 assert_eq!(merged["agent/run_id"], "r1");
620 assert_eq!(merged["traceparent"], "tp2");
621 assert_eq!(merged["k"], 1);
622 assert_eq!(base["traceparent"], "tp");
623 assert!(merge_meta(None, None).is_none());
624 }
625
626 #[test]
627 fn we_ask_for_the_newest_revision_we_know_not_rmcps_conservative_default() {
628 let ours = ProtocolVersion::V_2026_07_28;
631 assert_eq!(ours.to_string(), crate::version::LATEST_MODERN_VERSION);
632 assert_ne!(ours.to_string(), ProtocolVersion::LATEST.to_string());
633 }
634}