diff --git a/Cargo.toml b/Cargo.toml index 5154456ad..ee9b43a15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ edition = "2024" version = "1.2.0" authors = ["4t145 "] license = "Apache-2.0" -license-file = "LICENSE" repository = "https://github.com/modelcontextprotocol/rust-sdk/" description = "Rust SDK for Model Context Protocol" keywords = ["mcp", "sdk", "tokio", "modelcontextprotocol"] diff --git a/conformance/src/bin/client.rs b/conformance/src/bin/client.rs index 422435840..b9c8cea9d 100644 --- a/conformance/src/bin/client.rs +++ b/conformance/src/bin/client.rs @@ -1,5 +1,3 @@ -use std::future::Future; - use rmcp::{ ClientHandler, ErrorData, RoleClient, ServiceExt, model::*, @@ -55,82 +53,76 @@ impl ClientHandler for ElicitationDefaultsClientHandler { info } - fn create_elicitation( + async fn create_elicitation( &self, request: CreateElicitationRequestParams, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async move { - let content = match &request { - CreateElicitationRequestParams::FormElicitationParams { - requested_schema, .. - } => { - let mut defaults = serde_json::Map::new(); - for (name, prop) in &requested_schema.properties { - match prop { - PrimitiveSchema::String(s) => { - if let Some(d) = &s.default { - defaults.insert(name.clone(), Value::String(d.clone())); - } + ) -> Result { + let content = match &request { + CreateElicitationRequestParams::FormElicitationParams { + requested_schema, .. + } => { + let mut defaults = serde_json::Map::new(); + for (name, prop) in &requested_schema.properties { + match prop { + PrimitiveSchema::String(s) => { + if let Some(d) = &s.default { + defaults.insert(name.clone(), Value::String(d.clone())); } - PrimitiveSchema::Number(n) => { - if let Some(d) = n.default { - defaults.insert(name.clone(), json!(d)); - } + } + PrimitiveSchema::Number(n) => { + if let Some(d) = n.default { + defaults.insert(name.clone(), json!(d)); } - PrimitiveSchema::Integer(i) => { - if let Some(d) = i.default { - defaults.insert(name.clone(), json!(d)); - } + } + PrimitiveSchema::Integer(i) => { + if let Some(d) = i.default { + defaults.insert(name.clone(), json!(d)); } - PrimitiveSchema::Boolean(b) => { - if let Some(d) = b.default { - defaults.insert(name.clone(), Value::Bool(d)); - } + } + PrimitiveSchema::Boolean(b) => { + if let Some(d) = b.default { + defaults.insert(name.clone(), Value::Bool(d)); } - PrimitiveSchema::Enum(e) => { - let val = match e { - EnumSchema::Single(SingleSelectEnumSchema::Untitled(u)) => { - u.default.as_ref().map(|d| Value::String(d.clone())) - } - EnumSchema::Single(SingleSelectEnumSchema::Titled(t)) => { - t.default.as_ref().map(|d| Value::String(d.clone())) - } - EnumSchema::Multi(MultiSelectEnumSchema::Untitled(u)) => { - u.default.as_ref().map(|d| { - Value::Array( - d.iter() - .map(|s| Value::String(s.clone())) - .collect(), - ) - }) - } - EnumSchema::Multi(MultiSelectEnumSchema::Titled(t)) => { - t.default.as_ref().map(|d| { - Value::Array( - d.iter() - .map(|s| Value::String(s.clone())) - .collect(), - ) - }) - } - EnumSchema::Legacy(_) => None, - }; - if let Some(v) = val { - defaults.insert(name.clone(), v); + } + PrimitiveSchema::Enum(e) => { + let val = match e { + EnumSchema::Single(SingleSelectEnumSchema::Untitled(u)) => { + u.default.as_ref().map(|d| Value::String(d.clone())) + } + EnumSchema::Single(SingleSelectEnumSchema::Titled(t)) => { + t.default.as_ref().map(|d| Value::String(d.clone())) + } + EnumSchema::Multi(MultiSelectEnumSchema::Untitled(u)) => { + u.default.as_ref().map(|d| { + Value::Array( + d.iter().map(|s| Value::String(s.clone())).collect(), + ) + }) + } + EnumSchema::Multi(MultiSelectEnumSchema::Titled(t)) => { + t.default.as_ref().map(|d| { + Value::Array( + d.iter().map(|s| Value::String(s.clone())).collect(), + ) + }) } + EnumSchema::Legacy(_) => None, + }; + if let Some(v) = val { + defaults.insert(name.clone(), v); } } } - Some(Value::Object(defaults)) } - _ => Some(json!({})), - }; - Ok(CreateElicitationResult { - action: ElicitationAction::Accept, - content, - }) - } + Some(Value::Object(defaults)) + } + _ => Some(json!({})), + }; + Ok(CreateElicitationResult { + action: ElicitationAction::Accept, + content, + }) } } @@ -149,44 +141,40 @@ impl ClientHandler for FullClientHandler { info } - fn create_message( + async fn create_message( &self, params: CreateMessageRequestParams, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async move { - let prompt_text = params - .messages - .first() - .and_then(|m| m.content.first()) - .and_then(|c| c.as_text()) - .map(|t| t.text.clone()) - .unwrap_or_default(); - Ok(CreateMessageResult::new( - SamplingMessage::new( - Role::Assistant, - SamplingMessageContent::text(format!( - "This is a mock LLM response to: {}", - prompt_text - )), - ), - "mock-model".into(), - ) - .with_stop_reason("endTurn")) - } + ) -> Result { + let prompt_text = params + .messages + .first() + .and_then(|m| m.content.first()) + .and_then(|c| c.as_text()) + .map(|t| t.text.clone()) + .unwrap_or_default(); + Ok(CreateMessageResult::new( + SamplingMessage::new( + Role::Assistant, + SamplingMessageContent::text(format!( + "This is a mock LLM response to: {}", + prompt_text + )), + ), + "mock-model".into(), + ) + .with_stop_reason("endTurn")) } - fn create_elicitation( + async fn create_elicitation( &self, _request: CreateElicitationRequestParams, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async move { - Ok(CreateElicitationResult { - action: ElicitationAction::Accept, - content: Some(json!({"username": "testuser", "email": "test@example.com"})), - }) - } + ) -> Result { + Ok(CreateElicitationResult { + action: ElicitationAction::Accept, + content: Some(json!({"username": "testuser", "email": "test@example.com"})), + }) } } @@ -761,9 +749,7 @@ fn build_tool_arguments(tool: &Tool) -> Option> { }) .unwrap_or_default(); - let Some(properties) = properties else { - return None; - }; + let properties = properties?; if properties.is_empty() && required.is_empty() { return None; } diff --git a/conformance/src/bin/server.rs b/conformance/src/bin/server.rs index 4cfde48c6..bfa98f42c 100644 --- a/conformance/src/bin/server.rs +++ b/conformance/src/bin/server.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, future::Future, sync::Arc}; +use std::{collections::HashSet, sync::Arc}; use rmcp::{ ErrorData, RoleServer, ServerHandler, @@ -42,793 +42,764 @@ impl ConformanceServer { } impl ServerHandler for ConformanceServer { - fn initialize( + async fn initialize( &self, _request: InitializeRequestParams, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async { - Ok(InitializeResult::new( - ServerCapabilities::builder() - .enable_prompts() - .enable_resources() - .enable_tools() - .enable_logging() - .build(), - ) - .with_server_info(Implementation::new("rust-conformance-server", "0.1.0")) - .with_instructions("Rust MCP conformance test server")) - } + ) -> Result { + Ok(InitializeResult::new( + ServerCapabilities::builder() + .enable_prompts() + .enable_resources() + .enable_tools() + .enable_logging() + .build(), + ) + .with_server_info(Implementation::new("rust-conformance-server", "0.1.0")) + .with_instructions("Rust MCP conformance test server")) } - fn ping( - &self, - _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async { Ok(()) } + async fn ping(&self, _cx: RequestContext) -> Result<(), ErrorData> { + Ok(()) } - fn list_tools( + async fn list_tools( &self, _request: Option, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async { - let tools = vec![ - Tool::new( - "test_simple_text", - "Returns simple text content", - json_object(json!({ - "type": "object", - "properties": {} - })), - ), - Tool::new( - "test_image_content", - "Returns image content", - json_object(json!({ - "type": "object", - "properties": {} - })), - ), - Tool::new( - "test_audio_content", - "Returns audio content", - json_object(json!({ - "type": "object", - "properties": {} - })), - ), - Tool::new( - "test_embedded_resource", - "Returns embedded resource content", - json_object(json!({ - "type": "object", - "properties": {} - })), - ), - Tool::new( - "test_multiple_content_types", - "Returns multiple content types", - json_object(json!({ - "type": "object", - "properties": {} - })), - ), - Tool::new( - "test_tool_with_logging", - "Sends logging notifications during execution", - json_object(json!({ - "type": "object", - "properties": {} - })), - ), - Tool::new( - "test_error_handling", - "Always returns an error", - json_object(json!({ - "type": "object", - "properties": {} - })), - ), - Tool::new( - "test_tool_with_progress", - "Reports progress notifications", - json_object(json!({ - "type": "object", - "properties": {} - })), - ), - Tool::new( - "test_sampling", - "Requests LLM sampling from client", - json_object(json!({ - "type": "object", - "properties": { - "prompt": { "type": "string", "description": "The prompt to send" } - }, - "required": ["prompt"] - })), - ), - Tool::new( - "test_elicitation", - "Requests user input from client", - json_object(json!({ - "type": "object", - "properties": { - "message": { "type": "string", "description": "The message to show" } - }, - "required": ["message"] - })), - ), - Tool::new( - "test_elicitation_sep1034_defaults", - "Tests elicitation with default values (SEP-1034)", - json_object(json!({ - "type": "object", - "properties": {} - })), - ), - Tool::new( - "test_elicitation_sep1330_enums", - "Tests enum schema improvements (SEP-1330)", - json_object(json!({ - "type": "object", - "properties": {} - })), - ), - Tool::new( - "json_schema_2020_12_tool", - "Tool with JSON Schema 2020-12 features", - json_object(json!({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "$defs": { - "address": { - "type": "object", - "properties": { - "street": { "type": "string" }, - "city": { "type": "string" } - } + ) -> Result { + let tools = vec![ + Tool::new( + "test_simple_text", + "Returns simple text content", + json_object(json!({ + "type": "object", + "properties": {} + })), + ), + Tool::new( + "test_image_content", + "Returns image content", + json_object(json!({ + "type": "object", + "properties": {} + })), + ), + Tool::new( + "test_audio_content", + "Returns audio content", + json_object(json!({ + "type": "object", + "properties": {} + })), + ), + Tool::new( + "test_embedded_resource", + "Returns embedded resource content", + json_object(json!({ + "type": "object", + "properties": {} + })), + ), + Tool::new( + "test_multiple_content_types", + "Returns multiple content types", + json_object(json!({ + "type": "object", + "properties": {} + })), + ), + Tool::new( + "test_tool_with_logging", + "Sends logging notifications during execution", + json_object(json!({ + "type": "object", + "properties": {} + })), + ), + Tool::new( + "test_error_handling", + "Always returns an error", + json_object(json!({ + "type": "object", + "properties": {} + })), + ), + Tool::new( + "test_tool_with_progress", + "Reports progress notifications", + json_object(json!({ + "type": "object", + "properties": {} + })), + ), + Tool::new( + "test_sampling", + "Requests LLM sampling from client", + json_object(json!({ + "type": "object", + "properties": { + "prompt": { "type": "string", "description": "The prompt to send" } + }, + "required": ["prompt"] + })), + ), + Tool::new( + "test_elicitation", + "Requests user input from client", + json_object(json!({ + "type": "object", + "properties": { + "message": { "type": "string", "description": "The message to show" } + }, + "required": ["message"] + })), + ), + Tool::new( + "test_elicitation_sep1034_defaults", + "Tests elicitation with default values (SEP-1034)", + json_object(json!({ + "type": "object", + "properties": {} + })), + ), + Tool::new( + "test_elicitation_sep1330_enums", + "Tests enum schema improvements (SEP-1330)", + json_object(json!({ + "type": "object", + "properties": {} + })), + ), + Tool::new( + "json_schema_2020_12_tool", + "Tool with JSON Schema 2020-12 features", + json_object(json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "$defs": { + "address": { + "type": "object", + "properties": { + "street": { "type": "string" }, + "city": { "type": "string" } } - }, - "properties": { - "name": { "type": "string" }, - "address": { "$ref": "#/$defs/address" } - }, - "additionalProperties": false - })), - ), - Tool::new( - "test_reconnection", - "Tests SSE reconnection behavior", - json_object(json!({ - "type": "object", - "properties": {} - })), - ), - ]; - Ok(ListToolsResult { - meta: None, - tools, - next_cursor: None, - }) - } + } + }, + "properties": { + "name": { "type": "string" }, + "address": { "$ref": "#/$defs/address" } + }, + "additionalProperties": false + })), + ), + Tool::new( + "test_reconnection", + "Tests SSE reconnection behavior", + json_object(json!({ + "type": "object", + "properties": {} + })), + ), + ]; + Ok(ListToolsResult { + meta: None, + tools, + next_cursor: None, + }) } - fn call_tool( + async fn call_tool( &self, request: CallToolRequestParams, cx: RequestContext, - ) -> impl Future> + Send + '_ { - async move { - let args = request.arguments.unwrap_or_default(); - match request.name.as_ref() { - "test_simple_text" => Ok(CallToolResult::success(vec![Content::text( - "This is a simple text response for testing.", - )])), - - "test_image_content" => Ok(CallToolResult::success(vec![Content::image( - TEST_IMAGE_DATA, - "image/png", - )])), - - "test_audio_content" => { - // No Content::audio() helper, construct manually - let audio = RawContent::Audio(RawAudioContent { - data: TEST_AUDIO_DATA.into(), - mime_type: "audio/wav".into(), - }) - .no_annotation(); - Ok(CallToolResult::success(vec![audio])) + ) -> Result { + let args = request.arguments.unwrap_or_default(); + match request.name.as_ref() { + "test_simple_text" => Ok(CallToolResult::success(vec![Content::text( + "This is a simple text response for testing.", + )])), + + "test_image_content" => Ok(CallToolResult::success(vec![Content::image( + TEST_IMAGE_DATA, + "image/png", + )])), + + "test_audio_content" => { + let audio = RawContent::Audio(RawAudioContent { + data: TEST_AUDIO_DATA.into(), + mime_type: "audio/wav".into(), + }) + .no_annotation(); + Ok(CallToolResult::success(vec![audio])) + } + + "test_embedded_resource" => Ok(CallToolResult::success(vec![Content::resource( + ResourceContents::TextResourceContents { + uri: "test://embedded-resource".into(), + mime_type: Some("text/plain".into()), + text: "This is an embedded resource content.".into(), + meta: None, + }, + )])), + + "test_multiple_content_types" => Ok(CallToolResult::success(vec![ + Content::text("Multiple content types test:"), + Content::image(TEST_IMAGE_DATA, "image/png"), + Content::resource(ResourceContents::TextResourceContents { + uri: "test://mixed-content-resource".into(), + mime_type: Some("application/json".into()), + text: r#"{"test":"data","value":123}"#.into(), + meta: None, + }), + ])), + + "test_tool_with_logging" => { + for msg in [ + "Tool execution started", + "Tool processing data", + "Tool execution completed", + ] { + let _ = cx + .peer + .notify_logging_message(LoggingMessageNotificationParam { + level: LoggingLevel::Info, + logger: Some("conformance-server".into()), + data: json!(msg), + }) + .await; + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; } - "test_embedded_resource" => Ok(CallToolResult::success(vec![Content::resource( - ResourceContents::TextResourceContents { - uri: "test://embedded-resource".into(), - mime_type: Some("text/plain".into()), - text: "This is an embedded resource content.".into(), - meta: None, - }, - )])), - - "test_multiple_content_types" => Ok(CallToolResult::success(vec![ - Content::text("Multiple content types test:"), - Content::image(TEST_IMAGE_DATA, "image/png"), - Content::resource(ResourceContents::TextResourceContents { - uri: "test://mixed-content-resource".into(), - mime_type: Some("application/json".into()), - text: r#"{"test":"data","value":123}"#.into(), - meta: None, - }), - ])), - - "test_tool_with_logging" => { - for msg in [ - "Tool execution started", - "Tool processing data", - "Tool execution completed", - ] { + Ok(CallToolResult::success(vec![Content::text( + "Logging test completed", + )])) + } + + "test_error_handling" => Ok(CallToolResult::error(vec![Content::text( + "This tool intentionally returns an error for testing", + )])), + + "test_tool_with_progress" => { + let progress_token = cx.meta.get_progress_token(); + + for (progress, message) in + [(0.0, "Starting"), (50.0, "Halfway"), (100.0, "Complete")] + { + if let Some(token) = &progress_token { let _ = cx .peer - .notify_logging_message(LoggingMessageNotificationParam { - level: LoggingLevel::Info, - logger: Some("conformance-server".into()), - data: json!(msg), + .notify_progress(ProgressNotificationParam { + progress_token: token.clone(), + progress, + total: Some(100.0), + message: Some(message.into()), }) .await; - tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; } - - Ok(CallToolResult::success(vec![Content::text( - "Logging test completed", - )])) + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; } - "test_error_handling" => Ok(CallToolResult::error(vec![Content::text( - "This tool intentionally returns an error for testing", - )])), - - "test_tool_with_progress" => { - let progress_token = cx.meta.get_progress_token(); - - for (progress, message) in - [(0.0, "Starting"), (50.0, "Halfway"), (100.0, "Complete")] - { - if let Some(token) = &progress_token { - let _ = cx - .peer - .notify_progress(ProgressNotificationParam { - progress_token: token.clone(), - progress, - total: Some(100.0), - message: Some(message.into()), - }) - .await; - } - tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; - } - - Ok(CallToolResult::success(vec![Content::text( - "Progress test completed", - )])) - } - - "test_sampling" => { - let prompt = args - .get("prompt") - .and_then(|v| v.as_str()) - .unwrap_or("Hello"); + Ok(CallToolResult::success(vec![Content::text( + "Progress test completed", + )])) + } - match cx - .peer - .create_message(CreateMessageRequestParams::new( - vec![SamplingMessage::user_text(prompt)], - 100, - )) - .await - { - Ok(result) => { - let text = result - .message - .content - .first() - .and_then(|c| c.as_text()) - .map(|t| t.text.clone()) - .unwrap_or_else(|| "No text response".into()); - Ok(CallToolResult::success(vec![Content::text(format!( - "LLM response: {}", - text - ))])) - } - Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( - "Sampling error: {}", - e - ))])), + "test_sampling" => { + let prompt = args + .get("prompt") + .and_then(|v| v.as_str()) + .unwrap_or("Hello"); + + match cx + .peer + .create_message(CreateMessageRequestParams::new( + vec![SamplingMessage::user_text(prompt)], + 100, + )) + .await + { + Ok(result) => { + let text = result + .message + .content + .first() + .and_then(|c| c.as_text()) + .map(|t| t.text.clone()) + .unwrap_or_else(|| "No text response".into()); + Ok(CallToolResult::success(vec![Content::text(format!( + "LLM response: {}", + text + ))])) } + Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( + "Sampling error: {}", + e + ))])), } + } - "test_elicitation" => { - let message = args - .get("message") - .and_then(|v| v.as_str()) - .unwrap_or("Please provide your information"); - - let schema_json = json!({ - "type": "object", - "properties": { - "username": { - "type": "string", - "description": "User's response" - }, - "email": { - "type": "string", - "description": "User's email address" - } + "test_elicitation" => { + let message = args + .get("message") + .and_then(|v| v.as_str()) + .unwrap_or("Please provide your information"); + + let schema_json = json!({ + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "User's response" }, - "required": ["username", "email"] - }); + "email": { + "type": "string", + "description": "User's email address" + } + }, + "required": ["username", "email"] + }); - let schema: ElicitationSchema = serde_json::from_value(schema_json).unwrap(); + let schema: ElicitationSchema = serde_json::from_value(schema_json).unwrap(); - match cx - .peer - .create_elicitation(CreateElicitationRequestParams::FormElicitationParams { - meta: None, - message: message.into(), - requested_schema: schema, - }) - .await - { - Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!( - "User response: action={}, content={:?}", - match result.action { - ElicitationAction::Accept => "accept", - ElicitationAction::Decline => "decline", - ElicitationAction::Cancel => "cancel", - }, - result.content - ))])), - Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( - "Elicitation error: {}", - e - ))])), - } + match cx + .peer + .create_elicitation(CreateElicitationRequestParams::FormElicitationParams { + meta: None, + message: message.into(), + requested_schema: schema, + }) + .await + { + Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!( + "User response: action={}, content={:?}", + match result.action { + ElicitationAction::Accept => "accept", + ElicitationAction::Decline => "decline", + ElicitationAction::Cancel => "cancel", + }, + result.content + ))])), + Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( + "Elicitation error: {}", + e + ))])), } + } - "test_elicitation_sep1034_defaults" => { - let schema_json = json!({ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "User's name", - "default": "John Doe" - }, - "age": { - "type": "integer", - "description": "User's age", - "default": 30 - }, - "score": { - "type": "number", - "description": "User's score", - "default": 95.5 - }, - "status": { - "type": "string", - "description": "User's status", - "enum": ["active", "inactive", "pending"], - "default": "active" - }, - "verified": { - "type": "boolean", - "description": "Whether user is verified", - "default": true - } + "test_elicitation_sep1034_defaults" => { + let schema_json = json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "User's name", + "default": "John Doe" + }, + "age": { + "type": "integer", + "description": "User's age", + "default": 30 + }, + "score": { + "type": "number", + "description": "User's score", + "default": 95.5 + }, + "status": { + "type": "string", + "description": "User's status", + "enum": ["active", "inactive", "pending"], + "default": "active" + }, + "verified": { + "type": "boolean", + "description": "Whether user is verified", + "default": true } - }); + } + }); - let schema: ElicitationSchema = serde_json::from_value(schema_json).unwrap(); + let schema: ElicitationSchema = serde_json::from_value(schema_json).unwrap(); - match cx - .peer - .create_elicitation(CreateElicitationRequestParams::FormElicitationParams { - meta: None, - message: "Please provide values (all have defaults)".into(), - requested_schema: schema, - }) - .await - { - Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!( - "Elicitation completed: action={}, content={:?}", - match result.action { - ElicitationAction::Accept => "accept", - ElicitationAction::Decline => "decline", - ElicitationAction::Cancel => "cancel", - }, - result.content - ))])), - Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( - "Elicitation error: {}", - e - ))])), - } + match cx + .peer + .create_elicitation(CreateElicitationRequestParams::FormElicitationParams { + meta: None, + message: "Please provide values (all have defaults)".into(), + requested_schema: schema, + }) + .await + { + Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!( + "Elicitation completed: action={}, content={:?}", + match result.action { + ElicitationAction::Accept => "accept", + ElicitationAction::Decline => "decline", + ElicitationAction::Cancel => "cancel", + }, + result.content + ))])), + Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( + "Elicitation error: {}", + e + ))])), } + } - "test_elicitation_sep1330_enums" => { - let schema_json = json!({ - "type": "object", - "properties": { - "untitledSingle": { + "test_elicitation_sep1330_enums" => { + let schema_json = json!({ + "type": "object", + "properties": { + "untitledSingle": { + "type": "string", + "enum": ["option1", "option2", "option3"] + }, + "titledSingle": { + "type": "string", + "oneOf": [ + { "const": "value1", "title": "First Option" }, + { "const": "value2", "title": "Second Option" }, + { "const": "value3", "title": "Third Option" } + ] + }, + "legacyEnum": { + "type": "string", + "enum": ["opt1", "opt2", "opt3"], + "enumNames": ["Option One", "Option Two", "Option Three"] + }, + "untitledMulti": { + "type": "array", + "items": { "type": "string", "enum": ["option1", "option2", "option3"] - }, - "titledSingle": { - "type": "string", - "oneOf": [ - { "const": "value1", "title": "First Option" }, - { "const": "value2", "title": "Second Option" }, - { "const": "value3", "title": "Third Option" } + } + }, + "titledMulti": { + "type": "array", + "items": { + "anyOf": [ + { "const": "value1", "title": "First Choice" }, + { "const": "value2", "title": "Second Choice" }, + { "const": "value3", "title": "Third Choice" } ] - }, - "legacyEnum": { - "type": "string", - "enum": ["opt1", "opt2", "opt3"], - "enumNames": ["Option One", "Option Two", "Option Three"] - }, - "untitledMulti": { - "type": "array", - "items": { - "type": "string", - "enum": ["option1", "option2", "option3"] - } - }, - "titledMulti": { - "type": "array", - "items": { - "anyOf": [ - { "const": "value1", "title": "First Choice" }, - { "const": "value2", "title": "Second Choice" }, - { "const": "value3", "title": "Third Choice" } - ] - } } } - }); - - let schema: ElicitationSchema = serde_json::from_value(schema_json).unwrap(); - - match cx - .peer - .create_elicitation(CreateElicitationRequestParams::FormElicitationParams { - meta: None, - message: "Test enum schema improvements".into(), - requested_schema: schema, - }) - .await - { - Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!( - "Enum elicitation completed: action={}", - match result.action { - ElicitationAction::Accept => "accept", - ElicitationAction::Decline => "decline", - ElicitationAction::Cancel => "cancel", - } - ))])), - Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( - "Elicitation error: {}", - e - ))])), } - } + }); - "json_schema_2020_12_tool" => { - let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("world"); - Ok(CallToolResult::success(vec![Content::text(format!( - "Hello, {}!", - name - ))])) - } + let schema: ElicitationSchema = serde_json::from_value(schema_json).unwrap(); - "test_reconnection" => { - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - Ok(CallToolResult::success(vec![Content::text( - "Reconnection test completed", - )])) + match cx + .peer + .create_elicitation(CreateElicitationRequestParams::FormElicitationParams { + meta: None, + message: "Test enum schema improvements".into(), + requested_schema: schema, + }) + .await + { + Ok(result) => Ok(CallToolResult::success(vec![Content::text(format!( + "Enum elicitation completed: action={}", + match result.action { + ElicitationAction::Accept => "accept", + ElicitationAction::Decline => "decline", + ElicitationAction::Cancel => "cancel", + } + ))])), + Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( + "Elicitation error: {}", + e + ))])), } + } - _ => Err(ErrorData::invalid_params( - format!("Unknown tool: {}", request.name), - None, - )), + "json_schema_2020_12_tool" => { + let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("world"); + Ok(CallToolResult::success(vec![Content::text(format!( + "Hello, {}!", + name + ))])) } + + "test_reconnection" => { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + Ok(CallToolResult::success(vec![Content::text( + "Reconnection test completed", + )])) + } + + _ => Err(ErrorData::invalid_params( + format!("Unknown tool: {}", request.name), + None, + )), } } - fn list_resources( + async fn list_resources( &self, _request: Option, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async { - Ok(ListResourcesResult { - meta: None, - resources: vec![ - RawResource { - uri: "test://static-text".into(), - name: "Static Text Resource".into(), - title: None, - description: Some("A static text resource for testing".into()), - mime_type: Some("text/plain".into()), - size: None, - icons: None, - meta: None, - } - .no_annotation(), - RawResource { - uri: "test://static-binary".into(), - name: "Static Binary Resource".into(), - title: None, - description: Some("A static binary/blob resource for testing".into()), - mime_type: Some("image/png".into()), - size: None, - icons: None, - meta: None, - } - .no_annotation(), - ], - next_cursor: None, - }) - } + ) -> Result { + Ok(ListResourcesResult { + meta: None, + resources: vec![ + RawResource { + uri: "test://static-text".into(), + name: "Static Text Resource".into(), + title: None, + description: Some("A static text resource for testing".into()), + mime_type: Some("text/plain".into()), + size: None, + icons: None, + meta: None, + } + .no_annotation(), + RawResource { + uri: "test://static-binary".into(), + name: "Static Binary Resource".into(), + title: None, + description: Some("A static binary/blob resource for testing".into()), + mime_type: Some("image/png".into()), + size: None, + icons: None, + meta: None, + } + .no_annotation(), + ], + next_cursor: None, + }) } - fn read_resource( + async fn read_resource( &self, request: ReadResourceRequestParams, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async move { - let uri = request.uri.as_str(); - match uri { - "test://static-text" => Ok(ReadResourceResult::new(vec![ - ResourceContents::TextResourceContents { - uri: uri.into(), - mime_type: Some("text/plain".into()), - text: "This is the content of the static text resource.".into(), - meta: None, - }, - ])), - "test://static-binary" => Ok(ReadResourceResult::new(vec![ - ResourceContents::BlobResourceContents { - uri: uri.into(), - mime_type: Some("image/png".into()), - blob: TEST_IMAGE_DATA.into(), - meta: None, - }, - ])), - _ => { - // Check if it matches template: test://template/{id}/data - if uri.starts_with("test://template/") && uri.ends_with("/data") { - let id = uri - .strip_prefix("test://template/") - .and_then(|s| s.strip_suffix("/data")) - .unwrap_or("unknown"); - Ok(ReadResourceResult::new(vec![ - ResourceContents::TextResourceContents { - uri: uri.into(), - mime_type: Some("application/json".into()), - text: format!( - r#"{{"id":"{}","templateTest":true,"data":"Data for ID: {}"}}"#, - id, id - ), - meta: None, - }, - ])) - } else { - Err(ErrorData::resource_not_found( - format!("Resource not found: {}", uri), - None, - )) - } + ) -> Result { + let uri = request.uri.as_str(); + match uri { + "test://static-text" => Ok(ReadResourceResult::new(vec![ + ResourceContents::TextResourceContents { + uri: uri.into(), + mime_type: Some("text/plain".into()), + text: "This is the content of the static text resource.".into(), + meta: None, + }, + ])), + "test://static-binary" => Ok(ReadResourceResult::new(vec![ + ResourceContents::BlobResourceContents { + uri: uri.into(), + mime_type: Some("image/png".into()), + blob: TEST_IMAGE_DATA.into(), + meta: None, + }, + ])), + _ => { + if uri.starts_with("test://template/") && uri.ends_with("/data") { + let id = uri + .strip_prefix("test://template/") + .and_then(|s| s.strip_suffix("/data")) + .unwrap_or("unknown"); + Ok(ReadResourceResult::new(vec![ + ResourceContents::TextResourceContents { + uri: uri.into(), + mime_type: Some("application/json".into()), + text: format!( + r#"{{"id":"{}","templateTest":true,"data":"Data for ID: {}"}}"#, + id, id + ), + meta: None, + }, + ])) + } else { + Err(ErrorData::resource_not_found( + format!("Resource not found: {}", uri), + None, + )) } } } } - fn list_resource_templates( + async fn list_resource_templates( &self, _request: Option, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async { - Ok(ListResourceTemplatesResult { - meta: None, - resource_templates: vec![ - RawResourceTemplate { - uri_template: "test://template/{id}/data".into(), - name: "Dynamic Resource".into(), - title: None, - description: Some("A dynamic resource with parameter substitution".into()), - mime_type: Some("application/json".into()), - icons: None, - } - .no_annotation(), - ], - next_cursor: None, - }) - } + ) -> Result { + Ok(ListResourceTemplatesResult { + meta: None, + resource_templates: vec![ + RawResourceTemplate { + uri_template: "test://template/{id}/data".into(), + name: "Dynamic Resource".into(), + title: None, + description: Some("A dynamic resource with parameter substitution".into()), + mime_type: Some("application/json".into()), + icons: None, + } + .no_annotation(), + ], + next_cursor: None, + }) } - fn subscribe( + async fn subscribe( &self, request: SubscribeRequestParams, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async move { - let mut subs = self.subscriptions.lock().await; - subs.insert(request.uri.to_string()); - Ok(()) - } + ) -> Result<(), ErrorData> { + let mut subs = self.subscriptions.lock().await; + subs.insert(request.uri.to_string()); + Ok(()) } - fn unsubscribe( + async fn unsubscribe( &self, request: UnsubscribeRequestParams, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async move { - let mut subs = self.subscriptions.lock().await; - subs.remove(request.uri.as_str()); - Ok(()) - } + ) -> Result<(), ErrorData> { + let mut subs = self.subscriptions.lock().await; + subs.remove(request.uri.as_str()); + Ok(()) } - fn list_prompts( + async fn list_prompts( &self, _request: Option, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async { - Ok(ListPromptsResult { - meta: None, - prompts: vec![ - Prompt::new( - "test_simple_prompt", - Some("A simple test prompt with no arguments"), - None, - ), - Prompt::new( - "test_prompt_with_arguments", - Some("A test prompt that accepts arguments"), - Some(vec![ - PromptArgument::new("name") - .with_description("The name to greet") - .with_required(true), - PromptArgument::new("style") - .with_description("The greeting style") - .with_required(false), - ]), - ), - Prompt::new( - "test_prompt_with_embedded_resource", - Some("A test prompt that includes an embedded resource"), - None, - ), - Prompt::new( - "test_prompt_with_image", - Some("A test prompt that includes an image"), - None, - ), - ], - next_cursor: None, - }) - } + ) -> Result { + Ok(ListPromptsResult { + meta: None, + prompts: vec![ + Prompt::new( + "test_simple_prompt", + Some("A simple test prompt with no arguments"), + None, + ), + Prompt::new( + "test_prompt_with_arguments", + Some("A test prompt that accepts arguments"), + Some(vec![ + PromptArgument::new("name") + .with_description("The name to greet") + .with_required(true), + PromptArgument::new("style") + .with_description("The greeting style") + .with_required(false), + ]), + ), + Prompt::new( + "test_prompt_with_embedded_resource", + Some("A test prompt that includes an embedded resource"), + None, + ), + Prompt::new( + "test_prompt_with_image", + Some("A test prompt that includes an image"), + None, + ), + ], + next_cursor: None, + }) } - fn get_prompt( + async fn get_prompt( &self, request: GetPromptRequestParams, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async move { - match request.name.as_str() { - "test_simple_prompt" => Ok(GetPromptResult::new(vec![PromptMessage::new_text( + ) -> Result { + match request.name.as_str() { + "test_simple_prompt" => Ok(GetPromptResult::new(vec![PromptMessage::new_text( + PromptMessageRole::User, + "This is a simple test prompt.", + )]) + .with_description("A simple test prompt")), + "test_prompt_with_arguments" => { + let args = request.arguments.unwrap_or_default(); + let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("World"); + let style = args + .get("style") + .and_then(|v| v.as_str()) + .unwrap_or("friendly"); + Ok(GetPromptResult::new(vec![PromptMessage::new_text( PromptMessageRole::User, - "This is a simple test prompt.", + format!("Please greet {} in a {} style.", name, style), )]) - .with_description("A simple test prompt")), - "test_prompt_with_arguments" => { - let args = request.arguments.unwrap_or_default(); - let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("World"); - let style = args - .get("style") - .and_then(|v| v.as_str()) - .unwrap_or("friendly"); - Ok(GetPromptResult::new(vec![PromptMessage::new_text( - PromptMessageRole::User, - format!("Please greet {} in a {} style.", name, style), - )]) - .with_description("A prompt with arguments")) - } - "test_prompt_with_embedded_resource" => Ok(GetPromptResult::new(vec![ - PromptMessage::new_text(PromptMessageRole::User, "Here is a resource:"), - PromptMessage::new_resource( + .with_description("A prompt with arguments")) + } + "test_prompt_with_embedded_resource" => Ok(GetPromptResult::new(vec![ + PromptMessage::new_text(PromptMessageRole::User, "Here is a resource:"), + PromptMessage::new_resource( + PromptMessageRole::User, + "test://static-text".into(), + Some("text/plain".into()), + Some("Resource content for prompt".into()), + None, + None, + None, + ), + ]) + .with_description("A prompt with an embedded resource")), + "test_prompt_with_image" => { + let image_content = RawImageContent { + data: TEST_IMAGE_DATA.into(), + mime_type: "image/png".into(), + meta: None, + }; + Ok(GetPromptResult::new(vec![ + PromptMessage::new_text(PromptMessageRole::User, "Here is an image:"), + PromptMessage::new( PromptMessageRole::User, - "test://static-text".into(), - Some("text/plain".into()), - Some("Resource content for prompt".into()), - None, - None, - None, + PromptMessageContent::Image { + image: image_content.no_annotation(), + }, ), ]) - .with_description("A prompt with an embedded resource")), - "test_prompt_with_image" => { - let image_content = RawImageContent { - data: TEST_IMAGE_DATA.into(), - mime_type: "image/png".into(), - meta: None, - }; - Ok(GetPromptResult::new(vec![ - PromptMessage::new_text(PromptMessageRole::User, "Here is an image:"), - PromptMessage::new( - PromptMessageRole::User, - PromptMessageContent::Image { - image: image_content.no_annotation(), - }, - ), - ]) - .with_description("A prompt with an image")) - } - _ => Err(ErrorData::invalid_params( - format!("Unknown prompt: {}", request.name), - None, - )), + .with_description("A prompt with an image")) } + _ => Err(ErrorData::invalid_params( + format!("Unknown prompt: {}", request.name), + None, + )), } } - fn complete( + async fn complete( &self, request: CompleteRequestParams, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async move { - let values = match &request.r#ref { - Reference::Resource(_) => { - if request.argument.name == "id" { - vec!["1".into(), "2".into(), "3".into()] - } else { - vec![] - } + ) -> Result { + let values = match &request.r#ref { + Reference::Resource(_) => { + if request.argument.name == "id" { + vec!["1".into(), "2".into(), "3".into()] + } else { + vec![] } - Reference::Prompt(prompt_ref) => { - if request.argument.name == "name" { - vec!["Alice".into(), "Bob".into(), "Charlie".into()] - } else if request.argument.name == "style" { - vec!["friendly".into(), "formal".into(), "casual".into()] - } else { - vec![prompt_ref.name.clone()] - } + } + Reference::Prompt(prompt_ref) => { + if request.argument.name == "name" { + vec!["Alice".into(), "Bob".into(), "Charlie".into()] + } else if request.argument.name == "style" { + vec!["friendly".into(), "formal".into(), "casual".into()] + } else { + vec![prompt_ref.name.clone()] } - }; - Ok(CompleteResult::new( - CompletionInfo::new(values).map_err(|e| ErrorData::internal_error(e, None))?, - )) - } + } + }; + Ok(CompleteResult::new( + CompletionInfo::new(values).map_err(|e| ErrorData::internal_error(e, None))?, + )) } - fn set_level( + async fn set_level( &self, request: SetLevelRequestParams, _cx: RequestContext, - ) -> impl Future> + Send + '_ { - async move { - let mut level = self.log_level.lock().await; - *level = request.level; - Ok(()) - } + ) -> Result<(), ErrorData> { + let mut level = self.log_level.lock().await; + *level = request.level; + Ok(()) } } diff --git a/crates/rmcp-macros/Cargo.toml b/crates/rmcp-macros/Cargo.toml index b59929265..8413e5d93 100644 --- a/crates/rmcp-macros/Cargo.toml +++ b/crates/rmcp-macros/Cargo.toml @@ -3,7 +3,6 @@ [package] name = "rmcp-macros" license = { workspace = true } -license-file = { workspace = true } version = { workspace = true } edition = { workspace = true } repository = { workspace = true } diff --git a/crates/rmcp/Cargo.toml b/crates/rmcp/Cargo.toml index 31e05acab..6ac4b02c0 100644 --- a/crates/rmcp/Cargo.toml +++ b/crates/rmcp/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "rmcp" license = { workspace = true } -license-file = { workspace = true } version = { workspace = true } edition = { workspace = true } repository = { workspace = true } diff --git a/examples/servers/src/cimd_auth_streamhttp.rs b/examples/servers/src/cimd_auth_streamhttp.rs index 7b402c9fb..6a634d883 100644 --- a/examples/servers/src/cimd_auth_streamhttp.rs +++ b/examples/servers/src/cimd_auth_streamhttp.rs @@ -18,7 +18,7 @@ use rmcp::transport::{ StreamableHttpServerConfig, streamable_http_server::{session::local::LocalSessionManager, tower::StreamableHttpService}, }; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use serde_json::Value; use tokio::sync::RwLock; use tower_http::cors::{Any, CorsLayer}; @@ -35,8 +35,8 @@ const BIND_ADDRESS: &str = "127.0.0.1:3000"; /// In-memory authorization code record #[derive(Clone, Debug)] struct AuthCodeRecord { - client_id: String, - redirect_uri: String, + _client_id: String, + _redirect_uri: String, expires_at: SystemTime, } @@ -368,8 +368,8 @@ async fn handle_authorize( codes.insert( code.clone(), AuthCodeRecord { - client_id: client_id_url, - redirect_uri: redirect_uri.to_string(), + _client_id: client_id_url, + _redirect_uri: redirect_uri.to_string(), expires_at, }, ); diff --git a/examples/servers/src/common/counter.rs b/examples/servers/src/common/counter.rs index 1806a9fa3..91b4a7bc1 100644 --- a/examples/servers/src/common/counter.rs +++ b/examples/servers/src/common/counter.rs @@ -1,7 +1,6 @@ #![allow(dead_code)] use std::{any::Any, sync::Arc}; -use chrono::Utc; use rmcp::{ ErrorData as McpError, RoleServer, ServerHandler, handler::server::{ @@ -12,14 +11,11 @@ use rmcp::{ prompt, prompt_handler, prompt_router, schemars, service::RequestContext, task_handler, - task_manager::{ - OperationDescriptor, OperationMessage, OperationProcessor, OperationResultTransport, - }, + task_manager::{OperationProcessor, OperationResultTransport}, tool, tool_handler, tool_router, }; use serde_json::json; use tokio::sync::Mutex; -use tracing::info; struct ToolCallOperationResult { id: String, diff --git a/examples/servers/src/elicitation_enum_inference.rs b/examples/servers/src/elicitation_enum_inference.rs index 27bde508e..328fb8c88 100644 --- a/examples/servers/src/elicitation_enum_inference.rs +++ b/examples/servers/src/elicitation_enum_inference.rs @@ -5,7 +5,7 @@ //! - Use `#[schemars(inline)]` to ensure the enum is inlined in the schema. //! - Use `#[schemars(extend("type" = "string"))]` to manually add the required type field, since `schemars` does not provide it for enums. //! - Optionally, use `#[schemars(title = "...")]` to provide titles for enum variants. -//! For more details, see: https://docs.rs/schemars/latest/schemars/ +//! For more details, see: https://docs.rs/schemars/latest/schemars/ use std::{ fmt::{Display, Formatter}, sync::Arc,