use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::{broadcast, oneshot, RwLock};
use super::json_rpc::RequestId;
static ELICITATION_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
fn generate_elicitation_id() -> String {
let id = ELICITATION_ID_COUNTER.fetch_add(1, Ordering::SeqCst);
format!("elicit-{}", id)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum ProtocolVersion {
#[default]
V2024_11_05,
V2025_03_26,
V2025_06_18,
}
impl ProtocolVersion {
pub fn from_str(s: &str) -> Option<Self> {
match s {
"2024-11-05" => Some(Self::V2024_11_05),
"2025-03-26" => Some(Self::V2025_03_26),
"2025-06-18" => Some(Self::V2025_06_18),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::V2024_11_05 => "2024-11-05",
Self::V2025_03_26 => "2025-03-26",
Self::V2025_06_18 => "2025-06-18",
}
}
pub fn supports_roots(&self) -> bool {
*self >= Self::V2025_03_26
}
pub fn supports_elicitation(&self) -> bool {
*self >= Self::V2025_06_18
}
pub fn supports_progress(&self) -> bool {
*self >= Self::V2025_03_26
}
pub fn supports_structured_output(&self) -> bool {
*self >= Self::V2025_06_18
}
pub fn all_versions() -> &'static [ProtocolVersion] {
&[Self::V2024_11_05, Self::V2025_03_26, Self::V2025_06_18]
}
pub fn latest() -> Self {
Self::V2025_06_18
}
}
impl std::fmt::Display for ProtocolVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct VersionNegotiator {
latest: ProtocolVersion,
}
impl VersionNegotiator {
pub fn new() -> Self {
Self {
latest: ProtocolVersion::latest(),
}
}
pub fn with_latest(latest: ProtocolVersion) -> Self {
Self { latest }
}
pub fn try_negotiate(&self, requested: &str) -> Option<ProtocolVersion> {
ProtocolVersion::from_str(requested).filter(|version| *version <= self.latest)
}
pub fn negotiate(&self, requested: &str) -> ProtocolVersion {
self.try_negotiate(requested).unwrap_or_default()
}
pub fn latest(&self) -> ProtocolVersion {
self.latest
}
}
impl Default for VersionNegotiator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Root {
pub uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl Root {
pub fn new(uri: impl Into<String>) -> Self {
Self {
uri: uri.into(),
name: None,
}
}
pub fn with_name(uri: impl Into<String>, name: impl Into<String>) -> Self {
Self {
uri: uri.into(),
name: Some(name.into()),
}
}
}
pub struct RootsRegistry {
roots: RwLock<Vec<Root>>,
change_tx: broadcast::Sender<()>,
}
impl RootsRegistry {
pub fn new() -> Self {
let (change_tx, _) = broadcast::channel(16);
Self {
roots: RwLock::new(Vec::new()),
change_tx,
}
}
pub async fn add_root(&self, root: Root) {
self.roots.write().await.push(root);
let _ = self.change_tx.send(());
}
pub async fn remove_root(&self, uri: &str) -> bool {
let mut roots = self.roots.write().await;
let len_before = roots.len();
roots.retain(|r| r.uri != uri);
let removed = roots.len() < len_before;
if removed {
let _ = self.change_tx.send(());
}
removed
}
pub async fn list(&self) -> Vec<Root> {
self.roots.read().await.clone()
}
pub async fn allows_uri(&self, uri: &str) -> bool {
if !uri.starts_with("file://") {
return true;
}
let roots = self.roots.read().await;
if roots.is_empty() {
return false;
}
let Some(uri_path) = normalize_file_uri_path(uri) else {
return false;
};
roots.iter().any(|root| {
let Some(root_path) = normalize_file_uri_path(&root.uri) else {
return false;
};
is_path_within_root(&uri_path, &root_path)
})
}
pub async fn clear(&self) {
let mut roots = self.roots.write().await;
if !roots.is_empty() {
roots.clear();
let _ = self.change_tx.send(());
}
}
pub async fn len(&self) -> usize {
self.roots.read().await.len()
}
pub async fn is_empty(&self) -> bool {
self.roots.read().await.is_empty()
}
pub fn subscribe(&self) -> broadcast::Receiver<()> {
self.change_tx.subscribe()
}
pub fn subscriber_count(&self) -> usize {
self.change_tx.receiver_count()
}
}
fn normalize_file_uri_path(uri: &str) -> Option<String> {
let raw_path = uri.strip_prefix("file://")?;
if raw_path.is_empty() || raw_path.contains('%') || raw_path.contains('\\') {
return None;
}
let mut parts = Vec::new();
for part in raw_path.split('/') {
match part {
"" | "." => {}
".." => {
parts.pop()?;
}
value => parts.push(value),
}
}
if parts.is_empty() {
Some("/".to_string())
} else {
Some(format!("/{}", parts.join("/")))
}
}
fn is_path_within_root(path: &str, root: &str) -> bool {
root == "/"
|| path == root
|| path
.strip_prefix(root)
.is_some_and(|suffix| suffix.starts_with('/'))
}
impl Default for RootsRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitationRequest {
pub message: String,
#[serde(rename = "requestedSchema", skip_serializing_if = "Option::is_none")]
pub requested_schema: Option<ElicitationSchema>,
}
impl ElicitationRequest {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
requested_schema: None,
}
}
pub fn with_schema(message: impl Into<String>, schema: ElicitationSchema) -> Self {
Self {
message: message.into(),
requested_schema: Some(schema),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ElicitationSchema {
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, PropertySchema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
}
impl ElicitationSchema {
pub fn object() -> Self {
Self {
schema_type: "object".to_string(),
properties: Some(HashMap::new()),
required: None,
}
}
pub fn with_property(mut self, name: impl Into<String>, schema: PropertySchema) -> Self {
self.properties
.get_or_insert_with(HashMap::new)
.insert(name.into(), schema);
self
}
pub fn with_required(mut self, name: impl Into<String>) -> Self {
self.required.get_or_insert_with(Vec::new).push(name.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PropertySchema {
#[serde(rename = "type")]
pub prop_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<String>>,
}
impl PropertySchema {
pub fn string() -> Self {
Self {
prop_type: "string".to_string(),
..Default::default()
}
}
pub fn number() -> Self {
Self {
prop_type: "number".to_string(),
..Default::default()
}
}
pub fn boolean() -> Self {
Self {
prop_type: "boolean".to_string(),
..Default::default()
}
}
pub fn enumeration(values: Vec<String>) -> Self {
Self {
prop_type: "string".to_string(),
enum_values: Some(values),
..Default::default()
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
pub fn with_minimum(mut self, min: f64) -> Self {
self.minimum = Some(min);
self
}
pub fn with_maximum(mut self, max: f64) -> Self {
self.maximum = Some(max);
self
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ElicitationAction {
Accept,
Decline,
Cancel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitationResponse {
pub action: ElicitationAction,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Value>,
}
impl ElicitationResponse {
pub fn accept(content: Value) -> Self {
Self {
action: ElicitationAction::Accept,
content: Some(content),
}
}
pub fn decline() -> Self {
Self {
action: ElicitationAction::Decline,
content: None,
}
}
pub fn cancel() -> Self {
Self {
action: ElicitationAction::Cancel,
content: None,
}
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum ElicitationError {
#[error("elicitation cancelled")]
Cancelled,
#[error("elicitation timeout")]
Timeout,
#[error("validation failed: {0}")]
ValidationFailed(String),
}
pub struct ElicitationHandler {
pending: RwLock<HashMap<String, oneshot::Sender<ElicitationResponse>>>,
timeout_secs: u64,
}
impl ElicitationHandler {
pub fn new() -> Self {
Self {
pending: RwLock::new(HashMap::new()),
timeout_secs: 300,
}
}
pub fn with_timeout(timeout_secs: u64) -> Self {
Self {
pending: RwLock::new(HashMap::new()),
timeout_secs,
}
}
pub async fn create(
&self,
request: ElicitationRequest,
) -> Result<ElicitationResponse, ElicitationError> {
let id = generate_elicitation_id();
let (tx, rx) = oneshot::channel();
self.pending.write().await.insert(id.clone(), tx);
let result =
tokio::time::timeout(std::time::Duration::from_secs(self.timeout_secs), rx).await;
self.pending.write().await.remove(&id);
match result {
Ok(Ok(response)) => {
if let (Some(schema), Some(content)) =
(&request.requested_schema, &response.content)
{
self.validate_response(schema, content)?;
}
Ok(response)
}
Ok(Err(_)) => Err(ElicitationError::Cancelled),
Err(_) => Err(ElicitationError::Timeout),
}
}
pub async fn respond(&self, id: &str, response: ElicitationResponse) -> bool {
if let Some(tx) = self.pending.write().await.remove(id) {
tx.send(response).is_ok()
} else {
false
}
}
fn validate_response(
&self,
schema: &ElicitationSchema,
content: &Value,
) -> Result<(), ElicitationError> {
if let Some(required) = &schema.required {
for field in required {
if content.get(field).is_none() {
return Err(ElicitationError::ValidationFailed(format!(
"missing required field: {}",
field
)));
}
}
}
if let (Some(properties), Some(obj)) = (&schema.properties, content.as_object()) {
for (name, prop_schema) in properties {
if let Some(value) = obj.get(name) {
self.validate_property_type(name, prop_schema, value)?;
}
}
}
Ok(())
}
fn validate_property_type(
&self,
name: &str,
schema: &PropertySchema,
value: &Value,
) -> Result<(), ElicitationError> {
match schema.prop_type.as_str() {
"string" => {
if !value.is_string() {
return Err(ElicitationError::ValidationFailed(format!(
"field '{}' must be a string",
name
)));
}
if let (Some(enum_values), Some(s)) = (&schema.enum_values, value.as_str()) {
if !enum_values.contains(&s.to_string()) {
return Err(ElicitationError::ValidationFailed(format!(
"field '{}' must be one of: {:?}",
name, enum_values
)));
}
}
}
"number" => {
if let Some(n) = value.as_f64() {
if let Some(min) = schema.minimum {
if n < min {
return Err(ElicitationError::ValidationFailed(format!(
"field '{}' must be >= {}",
name, min
)));
}
}
if let Some(max) = schema.maximum {
if n > max {
return Err(ElicitationError::ValidationFailed(format!(
"field '{}' must be <= {}",
name, max
)));
}
}
} else {
return Err(ElicitationError::ValidationFailed(format!(
"field '{}' must be a number",
name
)));
}
}
"boolean" => {
if !value.is_boolean() {
return Err(ElicitationError::ValidationFailed(format!(
"field '{}' must be a boolean",
name
)));
}
}
_ => {}
}
Ok(())
}
pub async fn pending_count(&self) -> usize {
self.pending.read().await.len()
}
}
impl Default for ElicitationHandler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ResourceTemplate {
#[serde(rename = "uriTemplate")]
pub uri_template: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
impl ResourceTemplate {
pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
Self {
uri_template: uri_template.into(),
name: name.into(),
description: None,
mime_type: None,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_mime_type(mut self, mime: impl Into<String>) -> Self {
self.mime_type = Some(mime.into());
self
}
pub fn placeholders(&self) -> Vec<String> {
let mut placeholders = Vec::new();
let mut chars = self.uri_template.chars().peekable();
while let Some(c) = chars.next() {
if c == '{' {
let mut name = String::new();
while let Some(&next) = chars.peek() {
if next == '}' {
chars.next();
break;
}
name.push(chars.next().unwrap());
}
if !name.is_empty() {
placeholders.push(name);
}
}
}
placeholders
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TemplateParam {
pub name: String,
pub value: String,
}
pub struct ResourceTemplateRegistry {
templates: RwLock<Vec<ResourceTemplate>>,
}
impl ResourceTemplateRegistry {
pub fn new() -> Self {
Self {
templates: RwLock::new(Vec::new()),
}
}
pub async fn register(&self, template: ResourceTemplate) {
self.templates.write().await.push(template);
}
pub async fn unregister(&self, uri_template: &str) -> bool {
let mut templates = self.templates.write().await;
let len_before = templates.len();
templates.retain(|t| t.uri_template != uri_template);
templates.len() < len_before
}
pub async fn list(&self) -> Vec<ResourceTemplate> {
self.templates.read().await.clone()
}
pub async fn match_uri(&self, uri: &str) -> Option<(ResourceTemplate, Vec<TemplateParam>)> {
let templates = self.templates.read().await;
for template in templates.iter() {
if let Some(params) = Self::extract_params(uri, &template.uri_template) {
return Some((template.clone(), params));
}
}
None
}
fn extract_params(uri: &str, template: &str) -> Option<Vec<TemplateParam>> {
let mut params = Vec::new();
let mut uri_chars = uri.chars().peekable();
let mut template_chars = template.chars().peekable();
while let Some(tc) = template_chars.next() {
if tc == '{' {
let mut name = String::new();
while let Some(&next) = template_chars.peek() {
if next == '}' {
template_chars.next();
break;
}
name.push(template_chars.next().unwrap());
}
let next_literal = template_chars.peek().copied();
let mut value = String::new();
while let Some(&uc) = uri_chars.peek() {
if Some(uc) == next_literal {
break;
}
value.push(uri_chars.next().unwrap());
}
if !name.is_empty() {
params.push(TemplateParam { name, value });
}
} else {
match uri_chars.next() {
Some(uc) if uc == tc => continue,
_ => return None,
}
}
}
if uri_chars.next().is_some() {
return None;
}
Some(params)
}
pub async fn len(&self) -> usize {
self.templates.read().await.len()
}
pub async fn is_empty(&self) -> bool {
self.templates.read().await.is_empty()
}
}
impl Default for ResourceTemplateRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct Annotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<f64>,
}
impl Annotations {
pub fn new() -> Self {
Self::default()
}
pub fn with_audience(mut self, audience: Vec<String>) -> Self {
self.audience = Some(audience);
self
}
pub fn with_priority(mut self, priority: f64) -> Self {
self.priority = Some(priority.clamp(0.0, 1.0));
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum Content {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image {
data: String,
#[serde(rename = "mimeType")]
mime_type: String,
},
#[serde(rename = "resource")]
Resource { uri: String },
}
impl Content {
pub fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self::Image {
data: data.into(),
mime_type: mime_type.into(),
}
}
pub fn resource(uri: impl Into<String>) -> Self {
Self::Resource { uri: uri.into() }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AnnotatedContent {
#[serde(flatten)]
pub content: Content,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
impl AnnotatedContent {
pub fn new(content: Content) -> Self {
Self {
content,
annotations: None,
}
}
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
self.annotations = Some(annotations);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnhancedToolResult {
pub content: Vec<AnnotatedContent>,
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
}
impl EnhancedToolResult {
pub fn success(content: Vec<AnnotatedContent>) -> Self {
Self {
content,
is_error: None,
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
content: vec![AnnotatedContent {
content: Content::text(message),
annotations: Some(Annotations::new().with_priority(1.0)),
}],
is_error: Some(true),
}
}
pub fn text(text: impl Into<String>) -> Self {
Self::success(vec![AnnotatedContent::new(Content::text(text))])
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProgressNotification {
#[serde(rename = "progressToken")]
pub progress_token: String,
pub progress: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancellationNotification {
#[serde(rename = "requestId")]
pub request_id: RequestId,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "method", content = "params")]
pub enum Notification {
#[serde(rename = "notifications/tools/list_changed")]
ToolsListChanged,
#[serde(rename = "notifications/resources/list_changed")]
ResourcesListChanged,
#[serde(rename = "notifications/prompts/list_changed")]
PromptsListChanged,
#[serde(rename = "notifications/roots/list_changed")]
RootsListChanged,
#[serde(rename = "notifications/progress")]
Progress(ProgressNotification),
#[serde(rename = "notifications/cancelled")]
Cancelled(CancellationNotification),
}
impl Notification {
pub fn to_json_rpc(&self) -> String {
let json = match self {
Notification::ToolsListChanged => serde_json::json!({
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}),
Notification::ResourcesListChanged => serde_json::json!({
"jsonrpc": "2.0",
"method": "notifications/resources/list_changed"
}),
Notification::PromptsListChanged => serde_json::json!({
"jsonrpc": "2.0",
"method": "notifications/prompts/list_changed"
}),
Notification::RootsListChanged => serde_json::json!({
"jsonrpc": "2.0",
"method": "notifications/roots/list_changed"
}),
Notification::Progress(p) => serde_json::json!({
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": p
}),
Notification::Cancelled(c) => serde_json::json!({
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": c
}),
};
serde_json::to_string(&json).unwrap_or_default()
}
}
pub struct NotificationManager {
tx: broadcast::Sender<Notification>,
}
impl NotificationManager {
pub fn new() -> Self {
let (tx, _) = broadcast::channel(256);
Self { tx }
}
pub fn notify(&self, notification: Notification) {
let _ = self.tx.send(notification);
}
pub fn subscribe(&self) -> broadcast::Receiver<Notification> {
self.tx.subscribe()
}
pub fn subscriber_count(&self) -> usize {
self.tx.receiver_count()
}
pub fn notify_tools_changed(&self) {
self.notify(Notification::ToolsListChanged);
}
pub fn notify_resources_changed(&self) {
self.notify(Notification::ResourcesListChanged);
}
pub fn notify_prompts_changed(&self) {
self.notify(Notification::PromptsListChanged);
}
pub fn notify_roots_changed(&self) {
self.notify(Notification::RootsListChanged);
}
pub fn notify_progress(&self, token: impl Into<String>, progress: f64, total: Option<u64>) {
self.notify(Notification::Progress(ProgressNotification {
progress_token: token.into(),
progress: progress.clamp(0.0, 1.0),
total,
}));
}
pub fn notify_cancelled(&self, request_id: RequestId, reason: Option<String>) {
self.notify(Notification::Cancelled(CancellationNotification {
request_id,
reason,
}));
}
}
impl Default for NotificationManager {
fn default() -> Self {
Self::new()
}
}
pub struct SubscriptionTracker {
max_subscribers_per_resource: usize,
subscriptions: RwLock<HashMap<String, Vec<String>>>,
}
impl SubscriptionTracker {
pub const DEFAULT_MAX_SUBSCRIBERS_PER_RESOURCE: usize = 1024;
pub fn new() -> Self {
Self::with_max_subscribers_per_resource(Self::DEFAULT_MAX_SUBSCRIBERS_PER_RESOURCE)
}
pub fn with_max_subscribers_per_resource(max_subscribers_per_resource: usize) -> Self {
Self {
max_subscribers_per_resource,
subscriptions: RwLock::new(HashMap::new()),
}
}
pub async fn subscribe(&self, uri: &str, subscriber_id: &str) -> bool {
let mut subs = self.subscriptions.write().await;
let subscribers = subs.entry(uri.to_string()).or_insert_with(Vec::new);
if subscribers.iter().any(|id| id == subscriber_id) {
return true;
}
if subscribers.len() >= self.max_subscribers_per_resource {
return false;
}
subscribers.push(subscriber_id.to_string());
true
}
pub async fn unsubscribe(&self, uri: &str, subscriber_id: &str) -> bool {
let mut subs = self.subscriptions.write().await;
let should_remove;
let removed;
if let Some(subscribers) = subs.get_mut(uri) {
let len_before = subscribers.len();
subscribers.retain(|id| id != subscriber_id);
removed = subscribers.len() < len_before;
should_remove = subscribers.is_empty();
} else {
return true;
}
if should_remove {
subs.remove(uri);
}
removed
}
pub async fn is_subscribed(&self, uri: &str, subscriber_id: &str) -> bool {
let subs = self.subscriptions.read().await;
subs.get(uri)
.map(|subscribers| subscribers.iter().any(|id| id == subscriber_id))
.unwrap_or(false)
}
pub async fn get_subscribers(&self, uri: &str) -> Vec<String> {
let subs = self.subscriptions.read().await;
subs.get(uri).cloned().unwrap_or_default()
}
pub async fn subscription_count(&self, uri: &str) -> usize {
let subs = self.subscriptions.read().await;
subs.get(uri).map(|s| s.len()).unwrap_or(0)
}
pub async fn clear(&self) {
self.subscriptions.write().await.clear();
}
}
impl Default for SubscriptionTracker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CancellationState {
Active,
Cancelled,
Completed,
}
#[derive(Debug)]
pub struct CancellationToken {
request_id: RequestId,
state: std::sync::atomic::AtomicU8,
reason: RwLock<Option<String>>,
}
impl CancellationToken {
pub fn new(request_id: RequestId) -> Self {
Self {
request_id,
state: std::sync::atomic::AtomicU8::new(0), reason: RwLock::new(None),
}
}
pub fn request_id(&self) -> &RequestId {
&self.request_id
}
pub fn is_cancelled(&self) -> bool {
self.state.load(Ordering::SeqCst) == 1
}
pub fn is_completed(&self) -> bool {
self.state.load(Ordering::SeqCst) == 2
}
pub fn state(&self) -> CancellationState {
match self.state.load(Ordering::SeqCst) {
1 => CancellationState::Cancelled,
2 => CancellationState::Completed,
_ => CancellationState::Active,
}
}
pub async fn cancel(&self, reason: Option<String>) -> bool {
let result = self.state.compare_exchange(
0, 1, Ordering::SeqCst,
Ordering::SeqCst,
);
if result.is_ok() {
*self.reason.write().await = reason;
true
} else {
false
}
}
pub fn complete(&self) -> bool {
self.state
.compare_exchange(
0, 2, Ordering::SeqCst,
Ordering::SeqCst,
)
.is_ok()
}
pub async fn reason(&self) -> Option<String> {
self.reason.read().await.clone()
}
}
pub struct CancellationManager {
tokens: RwLock<HashMap<String, Arc<CancellationToken>>>,
}
impl CancellationManager {
pub fn new() -> Self {
Self {
tokens: RwLock::new(HashMap::new()),
}
}
pub async fn create_token(&self, request_id: RequestId) -> Arc<CancellationToken> {
let token = Arc::new(CancellationToken::new(request_id.clone()));
let key = Self::request_id_to_key(&request_id);
self.tokens.write().await.insert(key, Arc::clone(&token));
token
}
pub async fn get_token(&self, request_id: &RequestId) -> Option<Arc<CancellationToken>> {
let key = Self::request_id_to_key(request_id);
self.tokens.read().await.get(&key).cloned()
}
pub async fn cancel(&self, request_id: &RequestId, reason: Option<String>) -> bool {
if let Some(token) = self.get_token(request_id).await {
token.cancel(reason).await
} else {
false
}
}
pub async fn remove_token(&self, request_id: &RequestId) {
let key = Self::request_id_to_key(request_id);
self.tokens.write().await.remove(&key);
}
pub async fn is_cancelled(&self, request_id: &RequestId) -> bool {
if let Some(token) = self.get_token(request_id).await {
token.is_cancelled()
} else {
false
}
}
pub async fn active_count(&self) -> usize {
self.tokens.read().await.len()
}
fn request_id_to_key(request_id: &RequestId) -> String {
match request_id {
RequestId::Number(n) => format!("n:{}", n),
RequestId::String(s) => format!("s:{}", s),
RequestId::Null => "null".to_string(),
RequestId::Missing => "missing".to_string(),
}
}
}
impl Default for CancellationManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ProgressState {
pub token: String,
pub progress: f64,
pub total: Option<u64>,
pub completed: bool,
}
pub struct ProgressTracker {
states: RwLock<HashMap<String, ProgressState>>,
notification_tx: broadcast::Sender<ProgressNotification>,
}
impl ProgressTracker {
pub fn new() -> Self {
let (notification_tx, _) = broadcast::channel(256);
Self {
states: RwLock::new(HashMap::new()),
notification_tx,
}
}
pub async fn start(&self, token: impl Into<String>, total: Option<u64>) {
let token = token.into();
self.states.write().await.insert(
token.clone(),
ProgressState {
token,
progress: 0.0,
total,
completed: false,
},
);
}
pub async fn update(&self, token: &str, progress: f64) -> bool {
let mut states = self.states.write().await;
if let Some(state) = states.get_mut(token) {
if !state.completed {
state.progress = progress.clamp(0.0, 1.0);
let _ = self.notification_tx.send(ProgressNotification {
progress_token: token.to_string(),
progress: state.progress,
total: state.total,
});
return true;
}
}
false
}
pub async fn complete(&self, token: &str) -> bool {
let mut states = self.states.write().await;
if let Some(state) = states.get_mut(token) {
if !state.completed {
state.progress = 1.0;
state.completed = true;
let _ = self.notification_tx.send(ProgressNotification {
progress_token: token.to_string(),
progress: 1.0,
total: state.total,
});
return true;
}
}
false
}
pub async fn get(&self, token: &str) -> Option<ProgressState> {
self.states.read().await.get(token).cloned()
}
pub async fn remove(&self, token: &str) {
self.states.write().await.remove(token);
}
pub fn subscribe(&self) -> broadcast::Receiver<ProgressNotification> {
self.notification_tx.subscribe()
}
pub async fn is_tracking(&self, token: &str) -> bool {
self.states.read().await.contains_key(token)
}
pub async fn active_count(&self) -> usize {
self.states.read().await.len()
}
}
impl Default for ProgressTracker {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct PingConfig {
pub timeout_secs: u64,
}
impl Default for PingConfig {
fn default() -> Self {
Self { timeout_secs: 30 }
}
}
pub struct ExtendedMcpAdapter {
version_negotiator: VersionNegotiator,
negotiated_version: RwLock<ProtocolVersion>,
roots: Arc<RootsRegistry>,
elicitation: Arc<ElicitationHandler>,
resource_templates: Arc<ResourceTemplateRegistry>,
subscriptions: Arc<SubscriptionTracker>,
notifications: Arc<NotificationManager>,
cancellation: Arc<CancellationManager>,
progress: Arc<ProgressTracker>,
ping_config: PingConfig,
}
impl ExtendedMcpAdapter {
pub fn new() -> Self {
Self {
version_negotiator: VersionNegotiator::new(),
negotiated_version: RwLock::new(ProtocolVersion::default()),
roots: Arc::new(RootsRegistry::new()),
elicitation: Arc::new(ElicitationHandler::new()),
resource_templates: Arc::new(ResourceTemplateRegistry::new()),
subscriptions: Arc::new(SubscriptionTracker::new()),
notifications: Arc::new(NotificationManager::new()),
cancellation: Arc::new(CancellationManager::new()),
progress: Arc::new(ProgressTracker::new()),
ping_config: PingConfig::default(),
}
}
pub fn with_ping_timeout(mut self, timeout_secs: u64) -> Self {
self.ping_config.timeout_secs = timeout_secs;
self
}
pub fn with_elicitation_timeout(mut self, timeout_secs: u64) -> Self {
self.elicitation = Arc::new(ElicitationHandler::with_timeout(timeout_secs));
self
}
pub fn version_negotiator(&self) -> &VersionNegotiator {
&self.version_negotiator
}
pub async fn negotiated_version(&self) -> ProtocolVersion {
*self.negotiated_version.read().await
}
pub async fn set_negotiated_version(&self, version: ProtocolVersion) {
*self.negotiated_version.write().await = version;
}
pub fn roots(&self) -> Arc<RootsRegistry> {
Arc::clone(&self.roots)
}
pub fn elicitation(&self) -> Arc<ElicitationHandler> {
Arc::clone(&self.elicitation)
}
pub fn resource_templates(&self) -> Arc<ResourceTemplateRegistry> {
Arc::clone(&self.resource_templates)
}
pub fn subscriptions(&self) -> Arc<SubscriptionTracker> {
Arc::clone(&self.subscriptions)
}
pub fn notifications(&self) -> Arc<NotificationManager> {
Arc::clone(&self.notifications)
}
pub fn cancellation(&self) -> Arc<CancellationManager> {
Arc::clone(&self.cancellation)
}
pub fn progress(&self) -> Arc<ProgressTracker> {
Arc::clone(&self.progress)
}
pub fn ping_config(&self) -> &PingConfig {
&self.ping_config
}
pub async fn negotiate_version(&self, requested: &str) -> ProtocolVersion {
let version = self.version_negotiator.negotiate(requested);
*self.negotiated_version.write().await = version;
version
}
pub async fn supports_roots(&self) -> bool {
self.negotiated_version.read().await.supports_roots()
}
pub async fn supports_elicitation(&self) -> bool {
self.negotiated_version.read().await.supports_elicitation()
}
pub async fn supports_progress(&self) -> bool {
self.negotiated_version.read().await.supports_progress()
}
pub async fn supports_structured_output(&self) -> bool {
self.negotiated_version
.read()
.await
.supports_structured_output()
}
pub async fn build_capabilities(&self) -> serde_json::Value {
let version = *self.negotiated_version.read().await;
let mut capabilities = serde_json::json!({
"tools": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"prompts": { "listChanged": true },
"logging": {}
});
if version.supports_roots() {
capabilities["roots"] = serde_json::json!({ "listChanged": true });
}
if version.supports_elicitation() {
capabilities["elicitation"] = serde_json::json!({});
}
capabilities
}
pub async fn is_method_available(&self, method: &str) -> bool {
let version = *self.negotiated_version.read().await;
match method {
"roots/list" => version.supports_roots(),
"elicitation/create" => version.supports_elicitation(),
_ => true,
}
}
pub async fn create_cancellation_token(&self, request_id: RequestId) -> Arc<CancellationToken> {
self.cancellation.create_token(request_id).await
}
pub async fn start_progress(&self, token: &str, total: Option<u64>) {
self.progress.start(token, total).await;
}
pub async fn update_progress(&self, token: &str, progress: f64) -> bool {
self.progress.update(token, progress).await
}
pub async fn complete_progress(&self, token: &str) -> bool {
self.progress.complete(token).await
}
pub fn notify_tools_changed(&self) {
self.notifications.notify_tools_changed();
}
pub fn notify_resources_changed(&self) {
self.notifications.notify_resources_changed();
}
pub fn notify_prompts_changed(&self) {
self.notifications.notify_prompts_changed();
}
pub fn notify_roots_changed(&self) {
self.notifications.notify_roots_changed();
}
pub fn subscribe_notifications(&self) -> broadcast::Receiver<Notification> {
self.notifications.subscribe()
}
}
impl Default for ExtendedMcpAdapter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_protocol_version_parsing() {
assert_eq!(
ProtocolVersion::from_str("2024-11-05"),
Some(ProtocolVersion::V2024_11_05)
);
assert_eq!(
ProtocolVersion::from_str("2025-03-26"),
Some(ProtocolVersion::V2025_03_26)
);
assert_eq!(
ProtocolVersion::from_str("2025-06-18"),
Some(ProtocolVersion::V2025_06_18)
);
assert_eq!(ProtocolVersion::from_str("invalid"), None);
}
#[test]
fn test_protocol_version_as_str() {
assert_eq!(ProtocolVersion::V2024_11_05.as_str(), "2024-11-05");
assert_eq!(ProtocolVersion::V2025_03_26.as_str(), "2025-03-26");
assert_eq!(ProtocolVersion::V2025_06_18.as_str(), "2025-06-18");
}
#[test]
fn test_protocol_version_ordering() {
assert!(ProtocolVersion::V2024_11_05 < ProtocolVersion::V2025_03_26);
assert!(ProtocolVersion::V2025_03_26 < ProtocolVersion::V2025_06_18);
}
#[test]
fn test_protocol_version_features() {
let v1 = ProtocolVersion::V2024_11_05;
assert!(!v1.supports_roots());
assert!(!v1.supports_elicitation());
assert!(!v1.supports_progress());
let v2 = ProtocolVersion::V2025_03_26;
assert!(v2.supports_roots());
assert!(!v2.supports_elicitation());
assert!(v2.supports_progress());
let v3 = ProtocolVersion::V2025_06_18;
assert!(v3.supports_roots());
assert!(v3.supports_elicitation());
assert!(v3.supports_progress());
}
#[test]
fn test_version_negotiator() {
let negotiator = VersionNegotiator::new();
assert_eq!(
negotiator.negotiate("2024-11-05"),
ProtocolVersion::V2024_11_05
);
assert_eq!(
negotiator.negotiate("2025-03-26"),
ProtocolVersion::V2025_03_26
);
assert_eq!(
negotiator.negotiate("2025-06-18"),
ProtocolVersion::V2025_06_18
);
assert_eq!(
negotiator.negotiate("invalid"),
ProtocolVersion::V2024_11_05
);
assert_eq!(
negotiator.negotiate("2099-01-01"),
ProtocolVersion::V2024_11_05
);
}
#[tokio::test]
async fn test_roots_registry() {
let registry = RootsRegistry::new();
assert!(registry.is_empty().await);
registry.add_root(Root::new("file:///home/user")).await;
assert_eq!(registry.len().await, 1);
registry
.add_root(Root::with_name("file:///project", "Project"))
.await;
assert_eq!(registry.len().await, 2);
let roots = registry.list().await;
assert_eq!(roots.len(), 2);
assert_eq!(roots[0].uri, "file:///home/user");
assert_eq!(roots[1].name, Some("Project".to_string()));
}
#[tokio::test]
async fn test_roots_remove() {
let registry = RootsRegistry::new();
registry.add_root(Root::new("file:///a")).await;
registry.add_root(Root::new("file:///b")).await;
assert!(registry.remove_root("file:///a").await);
assert_eq!(registry.len().await, 1);
assert!(!registry.remove_root("file:///nonexistent").await);
}
#[test]
fn test_elicitation_schema() {
let schema = ElicitationSchema::object()
.with_property(
"name",
PropertySchema::string().with_description("User name"),
)
.with_property(
"age",
PropertySchema::number()
.with_minimum(0.0)
.with_maximum(150.0),
)
.with_required("name");
assert_eq!(schema.schema_type, "object");
assert!(schema.properties.as_ref().unwrap().contains_key("name"));
assert!(schema
.required
.as_ref()
.unwrap()
.contains(&"name".to_string()));
}
#[test]
fn test_elicitation_response() {
let accept = ElicitationResponse::accept(serde_json::json!({"name": "test"}));
assert_eq!(accept.action, ElicitationAction::Accept);
assert!(accept.content.is_some());
let decline = ElicitationResponse::decline();
assert_eq!(decline.action, ElicitationAction::Decline);
assert!(decline.content.is_none());
let cancel = ElicitationResponse::cancel();
assert_eq!(cancel.action, ElicitationAction::Cancel);
}
#[test]
fn test_resource_template_placeholders() {
let template = ResourceTemplate::new("file:///{path}", "File");
assert_eq!(template.placeholders(), vec!["path"]);
let template2 = ResourceTemplate::new("db:///{table}/{id}", "Database");
assert_eq!(template2.placeholders(), vec!["table", "id"]);
}
#[tokio::test]
async fn test_resource_template_matching() {
let registry = ResourceTemplateRegistry::new();
registry
.register(ResourceTemplate::new("file:///{path}", "File"))
.await;
let result = registry.match_uri("file:///test.txt").await;
assert!(result.is_some());
let (template, params) = result.unwrap();
assert_eq!(template.name, "File");
assert_eq!(params.len(), 1);
assert_eq!(params[0].name, "path");
assert_eq!(params[0].value, "test.txt");
}
#[test]
fn test_extract_params() {
let params = ResourceTemplateRegistry::extract_params("file:///test.txt", "file:///{path}");
assert!(params.is_some());
let params = params.unwrap();
assert_eq!(params.len(), 1);
assert_eq!(params[0].value, "test.txt");
let params =
ResourceTemplateRegistry::extract_params("http://example.com", "file:///{path}");
assert!(params.is_none());
}
#[test]
fn test_annotations() {
let ann = Annotations::new()
.with_audience(vec!["user".to_string(), "admin".to_string()])
.with_priority(0.8);
assert_eq!(
ann.audience,
Some(vec!["user".to_string(), "admin".to_string()])
);
assert_eq!(ann.priority, Some(0.8));
}
#[test]
fn test_annotations_priority_clamping() {
let ann = Annotations::new().with_priority(1.5);
assert_eq!(ann.priority, Some(1.0));
let ann = Annotations::new().with_priority(-0.5);
assert_eq!(ann.priority, Some(0.0));
}
#[test]
fn test_enhanced_tool_result() {
let success = EnhancedToolResult::text("Hello");
assert!(success.is_error.is_none());
let error = EnhancedToolResult::error("Something went wrong");
assert_eq!(error.is_error, Some(true));
}
#[test]
fn test_notification_json_rpc() {
let notif = Notification::ToolsListChanged;
let json = notif.to_json_rpc();
assert!(json.contains("notifications/tools/list_changed"));
let progress = Notification::Progress(ProgressNotification {
progress_token: "token-1".to_string(),
progress: 0.5,
total: Some(100),
});
let json = progress.to_json_rpc();
assert!(json.contains("notifications/progress"));
assert!(json.contains("token-1"));
}
#[tokio::test]
async fn test_subscription_tracker() {
let tracker = SubscriptionTracker::new();
tracker.subscribe("file:///test.txt", "client-1").await;
assert!(tracker.is_subscribed("file:///test.txt", "client-1").await);
assert!(!tracker.is_subscribed("file:///test.txt", "client-2").await);
tracker.subscribe("file:///test.txt", "client-2").await;
assert_eq!(tracker.subscription_count("file:///test.txt").await, 2);
assert!(tracker.unsubscribe("file:///test.txt", "client-1").await);
assert!(!tracker.is_subscribed("file:///test.txt", "client-1").await);
assert_eq!(tracker.subscription_count("file:///test.txt").await, 1);
}
#[tokio::test]
async fn test_subscription_tracker_idempotent() {
let tracker = SubscriptionTracker::new();
assert!(tracker.unsubscribe("file:///nonexistent", "client-1").await);
}
}