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