pub use ::mcp::client;
pub use ::mcp::http;
pub mod pace {
use crate::supervisor::tree::TokenBucket;
use std::collections::HashMap;
use std::sync::Mutex;
type Reg = HashMap<String, (String, String)>; static REG: Mutex<Option<Reg>> = Mutex::new(None);
static BUCKETS: Mutex<Option<HashMap<String, TokenBucket>>> = Mutex::new(None);
pub fn register(server: &str, service: &str, rate: &str) {
let mut g = REG.lock().unwrap_or_else(|e| e.into_inner());
g.get_or_insert_with(HashMap::new)
.insert(server.to_string(), (service.to_string(), rate.to_string()));
}
pub fn take(server: &str) -> Result<(), String> {
let (service, rate) = {
let g = REG.lock().unwrap_or_else(|e| e.into_inner());
match g.as_ref().and_then(|m| m.get(server)) {
Some((s, r)) => (s.clone(), r.clone()),
None => return Ok(()),
}
};
let (burst, per_s) = crate::supervisor::tree::parse_rate(&rate)
.map_err(|e| format!("services.{service}.rate: {e}"))?;
let mut g = BUCKETS.lock().unwrap_or_else(|e| e.into_inner());
let b = g
.get_or_insert_with(HashMap::new)
.entry(service.clone())
.or_insert_with(|| TokenBucket::new(burst, f64::from(burst) / per_s));
if b.try_take() {
Ok(())
} else {
let retry = (per_s / f64::from(burst.max(1))).ceil().max(1.0) as u32;
Err(format!(
"service '{service}' rate exceeded (services.{service}.rate: {rate} paces this process); retry in ~{retry}s"
))
}
}
}
pub fn from_spec(
spec: &crate::config::McpServerSpec,
timeout: std::time::Duration,
) -> Result<client::McpClient, client::McpError> {
use client::{McpClient, McpError};
if spec.endpoint.trim().is_empty() {
return Err(McpError::Transport(format!(
"mcp server '{}' has no endpoint",
spec.name
)));
}
let headers = auth::resolve_headers(&spec.headers).map_err(McpError::Transport)?;
#[cfg(feature = "aauth")]
let aauth_signer = if spec.aauth == Some(false) {
None
} else {
let s = crate::aauth::signer();
if s.is_some()
&& let Some(client) = crate::aauth::installed()
{
let authority = ::mcp::http::authority_of(&spec.endpoint);
client.discover(&authority, &spec.endpoint);
}
s
};
#[cfg(not(feature = "aauth"))]
let aauth_signer: Option<std::sync::Arc<dyn ::mcp::http::RequestSigner>> = None;
#[cfg(feature = "oauth")]
let signer: Option<std::sync::Arc<dyn ::mcp::http::RequestSigner>> = if let Some(a) = &spec.auth
{
let target = match &spec.service {
Some(svc) => format!("service:{svc}"),
None => format!("mcp:{}", spec.name),
};
crate::auth::device::signer_for(a, &target, timeout).map_err(McpError::Transport)?
} else if let Some(o) = &spec.oauth {
Some(
std::sync::Arc::new(oauth::OAuthBearerSigner::new(o.clone(), timeout))
as std::sync::Arc<dyn ::mcp::http::RequestSigner>,
)
} else {
aauth_signer
};
#[cfg(not(feature = "oauth"))]
let signer = aauth_signer;
if let (Some(service), Some(rate)) = (&spec.service, &spec.rate) {
pace::register(&spec.name, service, rate);
}
let client = McpClient::connect_signed(&spec.name, &spec.endpoint, headers, timeout, signer)?
.with_client_info(::mcp::wire::Implementation {
name: "agentd".into(),
version: crate::VERSION.into(),
title: None,
});
#[cfg(feature = "tls")]
let client = match spiffe_x509_identity(spec)? {
Some(id) => client.with_identity(id),
None => client,
};
Ok(client)
}
#[cfg(feature = "tls")]
fn spiffe_x509_identity(
spec: &crate::config::McpServerSpec,
) -> Result<Option<crate::net::tls::ClientIdentity>, client::McpError> {
use client::McpError;
let Some(a) = &spec.auth else {
return Ok(None);
};
if a.kind != "spiffe" || a.svid.as_deref() != Some("x509") {
return Ok(None);
}
let cert_path = a
.svid_file
.as_deref()
.ok_or_else(|| McpError::Transport("spiffe x509: svid_file is required".into()))?;
let key_path = a
.key_file
.as_deref()
.ok_or_else(|| McpError::Transport("spiffe x509: key_file is required".into()))?;
let cert = std::fs::read(cert_path)
.map_err(|e| McpError::Transport(format!("spiffe svid_file: {e}")))?;
let key = std::fs::read(key_path)
.map_err(|e| McpError::Transport(format!("spiffe key_file: {e}")))?;
crate::net::tls::ClientIdentity::from_pem(&cert, &key)
.map(Some)
.map_err(|e| McpError::Transport(format!("spiffe svid: {e}")))
}
pub mod auth;
pub mod elicit;
#[cfg(feature = "oauth")]
pub mod oauth;
#[cfg(any(feature = "internal-mocks", debug_assertions))]
pub mod mock_http;
#[cfg(feature = "a2a")]
pub mod a2a_client;