use crate::client::McpError;
use crate::inbound;
use crate::rpc;
use crate::wire::{Implementation, Prompt, ReadResourceResult, Resource, ServerCapabilities, Tool};
use serde_json::{Value, json};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use rmcp::model::{
CallToolRequestParams, ClientCapabilities, ClientInfo, ElicitRequestParams, ElicitResult,
ElicitationAction, ElicitationCapability, Implementation as RmcpImpl, ProtocolVersion,
ReadResourceRequestParams, SubscriptionFilter,
};
use rmcp::service::{RoleClient, RunningService};
use rmcp::transport::StreamableHttpClientTransport;
use rmcp::{ClientHandler, ServiceExt};
#[derive(Clone)]
struct Inbound {
caps: inbound::Capabilities,
handler: Option<Arc<dyn inbound::Handler>>,
}
#[derive(Clone)]
struct Handler {
info: ClientInfo,
inbound: Inbound,
queue: Arc<Mutex<Vec<rpc::Notification>>>,
}
impl Handler {
fn queue(&self, method: &str, params: Value) {
self.queue
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(rpc::Notification::new(method, Some(params)));
}
}
fn declined() -> ElicitResult {
ElicitResult::new(ElicitationAction::Decline)
}
impl ClientHandler for Handler {
fn get_info(&self) -> ClientInfo {
self.info.clone()
}
async fn create_elicitation(
&self,
params: ElicitRequestParams,
_ctx: rmcp::service::RequestContext<RoleClient>,
) -> Result<ElicitResult, rmcp::ErrorData> {
let (message, requested_schema) = match ¶ms {
ElicitRequestParams::FormElicitationParams {
message,
requested_schema,
..
} => (
message.clone(),
serde_json::to_value(requested_schema).unwrap_or_else(|_| json!({})),
),
_ => return Ok(declined()),
};
if !self.inbound.caps.elicitation {
return Ok(declined());
}
let answer = self.inbound.handler.as_ref().and_then(|h| {
h.handle(inbound::Inbound::Elicit {
message,
requested_schema,
})
});
Ok(match answer {
Some(inbound::Answer::Accept(content)) => {
ElicitResult::new(ElicitationAction::Accept).with_content(content)
}
Some(inbound::Answer::Decline) => declined(),
_ => ElicitResult::new(ElicitationAction::Cancel),
})
}
async fn on_resource_updated(
&self,
params: rmcp::model::ResourceUpdatedNotificationParam,
_ctx: rmcp::service::NotificationContext<RoleClient>,
) {
self.queue(
"notifications/resources/updated",
serde_json::to_value(¶ms).unwrap_or_else(|_| json!({})),
);
}
async fn on_resource_list_changed(&self, _ctx: rmcp::service::NotificationContext<RoleClient>) {
self.queue("notifications/resources/list_changed", json!({}));
}
async fn on_tool_list_changed(&self, _ctx: rmcp::service::NotificationContext<RoleClient>) {
self.queue("notifications/tools/list_changed", json!({}));
}
async fn on_prompt_list_changed(&self, _ctx: rmcp::service::NotificationContext<RoleClient>) {
self.queue("notifications/prompts/list_changed", json!({}));
}
#[allow(deprecated)]
async fn on_logging_message(
&self,
params: rmcp::model::LoggingMessageNotificationParam,
_ctx: rmcp::service::NotificationContext<RoleClient>,
) {
self.queue(
"notifications/message",
serde_json::to_value(¶ms).unwrap_or_else(|_| json!({})),
);
}
async fn on_progress(
&self,
params: rmcp::model::ProgressNotificationParam,
_ctx: rmcp::service::NotificationContext<RoleClient>,
) {
self.queue(
"notifications/progress",
serde_json::to_value(¶ms).unwrap_or_else(|_| json!({})),
);
}
}
pub struct RmcpClient {
name: String,
rt: tokio::runtime::Runtime,
service: RunningService<RoleClient, Handler>,
caps: ServerCapabilities,
protocol_version: Option<String>,
timeout: Duration,
tool_meta: Option<Value>,
notifications: Arc<Mutex<Vec<rpc::Notification>>>,
uris: Mutex<std::collections::BTreeSet<String>>,
pump: Mutex<Option<tokio::task::JoinHandle<()>>>,
}
pub struct RmcpBuilder {
name: String,
endpoint: String,
headers: Vec<(String, String)>,
timeout: Duration,
client_info: Implementation,
inbound: Inbound,
http: Option<Arc<crate::http::HttpTransport>>,
}
impl RmcpBuilder {
pub fn new(
name: &str,
endpoint: &str,
headers: Vec<(String, String)>,
timeout: Duration,
) -> Self {
RmcpBuilder {
name: name.to_string(),
endpoint: endpoint.to_string(),
headers,
timeout,
client_info: Implementation {
name: "agentd".into(),
version: env!("CARGO_PKG_VERSION").into(),
title: None,
},
inbound: Inbound {
caps: inbound::Capabilities::default(),
handler: None,
},
http: None,
}
}
pub fn with_http(mut self, http: Arc<crate::http::HttpTransport>) -> Self {
self.http = Some(http);
self
}
pub fn with_client_info(mut self, info: Implementation) -> Self {
self.client_info = info;
self
}
pub fn with_elicitation(mut self, handler: Arc<dyn inbound::Handler>) -> Self {
self.inbound.caps.elicitation = true;
self.inbound.handler = Some(handler);
self
}
pub fn connect(self) -> Result<RmcpClient, McpError> {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.thread_name("agentd-mcp")
.build()
.map_err(|e| {
McpError::Transport(format!("mcp server '{}': runtime: {e}", self.name))
})?;
let mut config =
rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig::with_uri(
self.endpoint.clone(),
);
for (k, v) in &self.headers {
if let (Ok(name), Ok(value)) = (
http::HeaderName::from_bytes(k.as_bytes()),
http::HeaderValue::from_str(v),
) {
config.custom_headers.insert(name, value);
}
}
let mut caps = ClientCapabilities::default();
if self.inbound.caps.elicitation {
caps.elicitation = Some(ElicitationCapability::new());
}
let notifications: Arc<Mutex<Vec<rpc::Notification>>> = Arc::default();
let mut implementation = RmcpImpl::new(
self.client_info.name.clone(),
self.client_info.version.clone(),
);
implementation.title = self.client_info.title.clone();
let handler = Handler {
queue: Arc::clone(¬ifications),
info: ClientInfo::new(caps, implementation)
.with_protocol_version(ProtocolVersion::default()),
inbound: self.inbound.clone(),
};
let name = self.name.clone();
let socket = match &self.http {
Some(h) => Arc::clone(h),
None => Arc::new(crate::http::HttpTransport::new(
crate::http::McpEndpoint::parse(&self.endpoint)
.map_err(|e| McpError::Transport(format!("mcp server '{name}': {e}")))?,
self.headers.clone(),
)),
};
let client = crate::rmcp_transport::AgentdHttp::new(socket, self.timeout);
let service = rt
.block_on(async move {
let transport = StreamableHttpClientTransport::with_client(client, config);
handler.serve(transport).await
})
.map_err(|e| McpError::Transport(format!("mcp server '{name}': {e}")))?;
let info = service.peer_info();
let protocol_version = info.as_ref().map(|i| i.protocol_version.to_string());
let info_json = info
.as_ref()
.and_then(|i| serde_json::to_value(i.as_ref()).ok());
let caps = server_capabilities(info_json.as_ref());
Ok(RmcpClient {
name: self.name,
rt,
service,
caps,
protocol_version,
timeout: self.timeout,
tool_meta: None,
notifications,
uris: Mutex::new(std::collections::BTreeSet::new()),
pump: Mutex::new(None),
})
}
}
fn server_capabilities(info: Option<&serde_json::Value>) -> ServerCapabilities {
info.and_then(|v| v.get("capabilities"))
.and_then(|c| serde_json::from_value(c.clone()).ok())
.unwrap_or_default()
}
fn rpc_err(name: &str, op: &str, e: impl std::fmt::Display) -> McpError {
McpError::Transport(format!("mcp server '{name}': {op}: {e}"))
}
impl RmcpClient {
pub fn name(&self) -> &str {
&self.name
}
pub fn capabilities(&self) -> &ServerCapabilities {
&self.caps
}
pub fn protocol_version(&self) -> Option<&str> {
self.protocol_version.as_deref()
}
pub fn set_tool_meta(&mut self, meta: Value) {
self.tool_meta = Some(meta);
}
fn convert<T: serde::de::DeserializeOwned>(
&self,
v: &impl serde::Serialize,
what: &str,
) -> Result<T, McpError> {
let json = serde_json::to_value(v).map_err(|e| rpc_err(&self.name, what, e))?;
serde_json::from_value(json).map_err(|e| rpc_err(&self.name, what, e))
}
pub fn list_tools(&self) -> Result<Vec<Tool>, McpError> {
let res = self
.rt
.block_on(self.service.list_all_tools())
.map_err(|e| rpc_err(&self.name, "tools/list", e))?;
self.convert(&res, "tools/list")
}
pub fn call_tool(&self, name: &str, args: Option<Value>) -> Result<Value, McpError> {
self.call_tool_with_meta(name, args, None)
}
pub fn call_tool_with_meta(
&self,
name: &str,
args: Option<Value>,
extra_meta: Option<Value>,
) -> Result<Value, McpError> {
let mut arguments = match args {
Some(Value::Object(m)) => m,
_ => serde_json::Map::new(),
};
if let Some(m) = merge_meta(self.tool_meta.as_ref(), extra_meta) {
arguments.insert("_meta".into(), m);
}
let param = CallToolRequestParams::new(name.to_string()).with_arguments(arguments);
let res = self
.rt
.block_on(self.service.call_tool(param))
.map_err(|e| rpc_err(&self.name, &format!("tools/call {name}"), e))?;
serde_json::to_value(&res).map_err(|e| rpc_err(&self.name, "tools/call", e))
}
pub fn list_resources(&self) -> Result<Vec<Resource>, McpError> {
let res = self
.rt
.block_on(self.service.list_all_resources())
.map_err(|e| rpc_err(&self.name, "resources/list", e))?;
self.convert(&res, "resources/list")
}
pub fn read_resource(&self, uri: &str) -> Result<ReadResourceResult, McpError> {
let res = self
.rt
.block_on(
self.service
.read_resource(ReadResourceRequestParams::new(uri.to_string())),
)
.map_err(|e| rpc_err(&self.name, &format!("resources/read {uri}"), e))?;
self.convert(&res, "resources/read")
}
pub fn list_prompts(&self) -> Result<Vec<Prompt>, McpError> {
let res = self
.rt
.block_on(self.service.list_all_prompts())
.map_err(|e| rpc_err(&self.name, "prompts/list", e))?;
self.convert(&res, "prompts/list")
}
pub fn subscribe(&self, uri: &str) -> Result<(), McpError> {
{
let mut uris = self.uris.lock().unwrap_or_else(|e| e.into_inner());
if !uris.insert(uri.to_string()) {
return Ok(()); }
}
self.relisten()
}
#[allow(deprecated)]
pub fn unsubscribe(&self, uri: &str) -> Result<(), McpError> {
{
let mut uris = self.uris.lock().unwrap_or_else(|e| e.into_inner());
if !uris.remove(uri) {
return Ok(());
}
}
if !self.modern() {
return self
.rt
.block_on(
self.service
.unsubscribe(rmcp::model::UnsubscribeRequestParams::new(uri.to_string())),
)
.map_err(|e| rpc_err(&self.name, &format!("resources/unsubscribe {uri}"), e));
}
self.relisten()
}
fn relisten(&self) -> Result<(), McpError> {
if !self.modern() {
return self.legacy_subscribe_all();
}
let uris: Vec<String> = self
.uris
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.cloned()
.collect();
*self.pump.lock().unwrap_or_else(|e| e.into_inner()) = None;
if uris.is_empty() {
return Ok(());
}
let mut filter = SubscriptionFilter::builder().resources_list_changed();
for u in &uris {
filter = filter.resource_subscription(u.clone());
}
let filter = filter.build();
let peer = self.service.peer().clone();
let mut subscription = self
.rt
.block_on(peer.listen(filter))
.map_err(|e| rpc_err(&self.name, "subscriptions/listen", e))?;
let queue = Arc::clone(&self.notifications);
let handle = self.rt.spawn(async move {
while let Ok(Some(note)) = subscription.next().await {
if let Ok(v) = serde_json::to_value(¬e)
&& let Ok(n) = serde_json::from_value::<rpc::Notification>(v)
{
queue.lock().unwrap_or_else(|e| e.into_inner()).push(n);
}
}
});
*self.pump.lock().unwrap_or_else(|e| e.into_inner()) = Some(handle);
Ok(())
}
fn modern(&self) -> bool {
self.protocol_version
.as_deref()
.map(|v| matches!(crate::version::era_of(v), crate::version::Era::Modern))
.unwrap_or(false)
}
#[allow(deprecated)]
fn legacy_subscribe_all(&self) -> Result<(), McpError> {
let uris: Vec<String> = self
.uris
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.cloned()
.collect();
for uri in uris {
self.rt
.block_on(
self.service
.subscribe(rmcp::model::SubscribeRequestParams::new(uri.clone())),
)
.map_err(|e| rpc_err(&self.name, &format!("resources/subscribe {uri}"), e))?;
}
Ok(())
}
pub fn drain_notifications(&self) -> Vec<rpc::Notification> {
std::mem::take(&mut *self.notifications.lock().unwrap_or_else(|e| e.into_inner()))
}
pub fn timeout(&self) -> Duration {
self.timeout
}
}
fn merge_meta(base: Option<&Value>, extra: Option<Value>) -> Option<Value> {
match (base, extra) {
(None, None) => None,
(Some(b), None) => Some(b.clone()),
(None, Some(e)) => Some(e),
(Some(b), Some(e)) => {
let mut m = b.as_object().cloned().unwrap_or_default();
if let Some(eo) = e.as_object() {
for (k, v) in eo {
m.insert(k.clone(), v.clone());
}
}
Some(Value::Object(m))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn meta_overlay_wins_without_mutating_the_base() {
let base = json!({"agent/run_id": "r1", "traceparent": "tp"});
let merged = merge_meta(Some(&base), Some(json!({"traceparent": "tp2", "k": 1}))).unwrap();
assert_eq!(merged["agent/run_id"], "r1");
assert_eq!(merged["traceparent"], "tp2");
assert_eq!(merged["k"], 1);
assert_eq!(base["traceparent"], "tp");
assert!(merge_meta(None, None).is_none());
}
#[test]
fn we_ask_for_the_newest_revision_we_know_not_rmcps_conservative_default() {
let ours = ProtocolVersion::V_2026_07_28;
assert_eq!(ours.to_string(), crate::version::LATEST_MODERN_VERSION);
assert_ne!(ours.to_string(), ProtocolVersion::LATEST.to_string());
}
}