From c37b9f58c237bafe734b8e543ad6df02e952ad1b Mon Sep 17 00:00:00 2001 From: Alex Hancock Date: Tue, 3 Mar 2026 17:23:09 -0500 Subject: [PATCH] fix: conformance syntax changes --- conformance/src/bin/client.rs | 111 +++------- conformance/src/bin/server.rs | 388 +++++++++++++--------------------- 2 files changed, 178 insertions(+), 321 deletions(-) diff --git a/conformance/src/bin/client.rs b/conformance/src/bin/client.rs index 53a44d9e..42243584 100644 --- a/conformance/src/bin/client.rs +++ b/conformance/src/bin/client.rs @@ -5,8 +5,7 @@ use rmcp::{ model::*, service::RequestContext, transport::{ - AuthClient, AuthorizationManager, StreamableHttpClientTransport, - auth::{OAuthClientConfig, OAuthState}, + AuthClient, AuthorizationManager, StreamableHttpClientTransport, auth::OAuthState, streamable_http_client::StreamableHttpClientTransportConfig, }, }; @@ -17,9 +16,6 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[derive(Debug, Default, serde::Deserialize)] struct ConformanceContext { - #[serde(default)] - name: Option, - // pre-registration / client-credentials-basic #[serde(default)] client_id: Option, #[serde(default)] @@ -29,15 +25,6 @@ struct ConformanceContext { private_key_pem: Option, #[serde(default)] signing_algorithm: Option, - // cross-app-access - #[serde(default)] - idp_client_id: Option, - #[serde(default)] - idp_id_token: Option, - #[serde(default)] - idp_issuer: Option, - #[serde(default)] - idp_token_endpoint: Option, } fn load_context() -> ConformanceContext { @@ -175,17 +162,17 @@ impl ClientHandler for FullClientHandler { .and_then(|c| c.as_text()) .map(|t| t.text.clone()) .unwrap_or_default(); - Ok(CreateMessageResult { - message: SamplingMessage::new( + Ok(CreateMessageResult::new( + SamplingMessage::new( Role::Assistant, SamplingMessageContent::text(format!( "This is a mock LLM response to: {}", prompt_text )), ), - model: "mock-model".into(), - stop_reason: Some("endTurn".into()), - }) + "mock-model".into(), + ) + .with_stop_reason("endTurn")) } } @@ -216,7 +203,7 @@ const REDIRECT_URI: &str = "http://localhost:3000/callback"; /// 4. Return an `AuthClient` wrapping `reqwest::Client` async fn perform_oauth_flow( server_url: &str, - ctx: &ConformanceContext, + _ctx: &ConformanceContext, ) -> anyhow::Result> { let mut oauth = OAuthState::new(server_url, None).await?; @@ -335,12 +322,7 @@ async fn run_auth_client(server_url: &str, ctx: &ConformanceContext) -> anyhow:: for tool in &tools.tools { let args = build_tool_arguments(tool); let _ = client - .call_tool(CallToolRequestParams { - meta: None, - name: tool.name.clone(), - arguments: args, - task: None, - }) + .call_tool(call_tool_params(tool.name.clone(), args)) .await; } @@ -352,7 +334,7 @@ async fn run_auth_client(server_url: &str, ctx: &ConformanceContext) -> anyhow:: /// then call tool which triggers 403 → re-auth with expanded scopes → retry. async fn run_auth_scope_step_up_client( server_url: &str, - ctx: &ConformanceContext, + _ctx: &ConformanceContext, ) -> anyhow::Result<()> { // First auth let mut oauth = OAuthState::new(server_url, None).await?; @@ -388,12 +370,7 @@ async fn run_auth_scope_step_up_client( for tool in &tools.tools { let args = build_tool_arguments(tool); match client - .call_tool(CallToolRequestParams { - meta: None, - name: tool.name.clone(), - arguments: args.clone(), - task: None, - }) + .call_tool(call_tool_params(tool.name.clone(), args.clone())) .await { Ok(_) => { @@ -428,12 +405,7 @@ async fn run_auth_scope_step_up_client( ); let client2 = BasicClientHandler.serve(transport2).await?; let _ = client2 - .call_tool(CallToolRequestParams { - meta: None, - name: tool.name.clone(), - arguments: args, - task: None, - }) + .call_tool(call_tool_params(tool.name.clone(), args)) .await; client2.cancel().await.ok(); return Ok(()); @@ -481,12 +453,7 @@ async fn run_auth_scope_retry_limit_client( for tool in &tools.tools { let args = build_tool_arguments(tool); match client - .call_tool(CallToolRequestParams { - meta: None, - name: tool.name.clone(), - arguments: args, - task: None, - }) + .call_tool(call_tool_params(tool.name.clone(), args)) .await { Ok(_) => {} @@ -539,12 +506,7 @@ async fn run_auth_preregistered_client( for tool in &tools.tools { let args = build_tool_arguments(tool); let _ = client - .call_tool(CallToolRequestParams { - meta: None, - name: tool.name.clone(), - arguments: args, - task: None, - }) + .call_tool(call_tool_params(tool.name.clone(), args)) .await; } client.cancel().await?; @@ -597,12 +559,7 @@ async fn run_client_credentials_basic( for tool in &tools.tools { let args = build_tool_arguments(tool); let _ = client - .call_tool(CallToolRequestParams { - meta: None, - name: tool.name.clone(), - arguments: args, - task: None, - }) + .call_tool(call_tool_params(tool.name.clone(), args)) .await; } client.cancel().await?; @@ -667,12 +624,7 @@ async fn run_client_credentials_jwt( for tool in &tools.tools { let args = build_tool_arguments(tool); let _ = client - .call_tool(CallToolRequestParams { - meta: None, - name: tool.name.clone(), - arguments: args, - task: None, - }) + .call_tool(call_tool_params(tool.name.clone(), args)) .await; } client.cancel().await?; @@ -783,6 +735,18 @@ async fn headless_authorize(auth_url: &str) -> anyhow::Result<(String, String)> Ok((code, state)) } +/// Build a `CallToolRequestParams` for a tool, optionally with arguments. +fn call_tool_params( + name: std::borrow::Cow<'static, str>, + arguments: Option>, +) -> CallToolRequestParams { + let mut p = CallToolRequestParams::new(name); + if let Some(a) = arguments { + p = p.with_arguments(a); + } + p +} + /// Build arguments for a tool based on its input schema. fn build_tool_arguments(tool: &Tool) -> Option> { let schema = &tool.input_schema; @@ -840,12 +804,7 @@ async fn run_tools_call_client(server_url: &str) -> anyhow::Result<()> { for tool in &tools.tools { let args = build_tool_arguments(tool); let _ = client - .call_tool(CallToolRequestParams { - meta: None, - name: tool.name.clone(), - arguments: args, - task: None, - }) + .call_tool(call_tool_params(tool.name.clone(), args)) .await?; } client.cancel().await?; @@ -862,12 +821,7 @@ async fn run_elicitation_defaults_client(server_url: &str) -> anyhow::Result<()> }); if let Some(tool) = test_tool { let _ = client - .call_tool(CallToolRequestParams { - meta: None, - name: tool.name.clone(), - arguments: None, - task: None, - }) + .call_tool(call_tool_params(tool.name.clone(), None)) .await?; } client.cancel().await?; @@ -884,12 +838,7 @@ async fn run_sse_retry_client(server_url: &str) -> anyhow::Result<()> { .find(|t| t.name.as_ref() == "test_reconnection") { let _ = client - .call_tool(CallToolRequestParams { - meta: None, - name: tool.name.clone(), - arguments: None, - task: None, - }) + .call_tool(call_tool_params(tool.name.clone(), None)) .await?; } client.cancel().await?; diff --git a/conformance/src/bin/server.rs b/conformance/src/bin/server.rs index 97bfbcdc..4cfde48c 100644 --- a/conformance/src/bin/server.rs +++ b/conformance/src/bin/server.rs @@ -48,24 +48,16 @@ impl ServerHandler for ConformanceServer { _cx: RequestContext, ) -> impl Future> + Send + '_ { async { - Ok(InitializeResult { - server_info: Implementation { - name: "rust-conformance-server".into(), - title: None, - version: "0.1.0".into(), - description: None, - icons: None, - website_url: None, - }, - capabilities: ServerCapabilities::builder() + Ok(InitializeResult::new( + ServerCapabilities::builder() .enable_prompts() .enable_resources() .enable_tools() .enable_logging() .build(), - instructions: Some("Rust MCP conformance test server".into()), - ..Default::default() - }) + ) + .with_server_info(Implementation::new("rust-conformance-server", "0.1.0")) + .with_instructions("Rust MCP conformance test server")) } } @@ -232,19 +224,14 @@ impl ServerHandler for ConformanceServer { async move { let args = request.arguments.unwrap_or_default(); match request.name.as_ref() { - "test_simple_text" => Ok(CallToolResult { - content: vec![Content::text("This is a simple text response for testing.")], - structured_content: None, - is_error: None, - meta: None, - }), - - "test_image_content" => Ok(CallToolResult { - content: vec![Content::image(TEST_IMAGE_DATA, "image/png")], - structured_content: None, - is_error: None, - meta: None, - }), + "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 @@ -253,41 +240,28 @@ impl ServerHandler for ConformanceServer { mime_type: "audio/wav".into(), }) .no_annotation(); - Ok(CallToolResult { - content: vec![audio], - structured_content: None, - is_error: None, - meta: None, - }) + Ok(CallToolResult::success(vec![audio])) } - "test_embedded_resource" => Ok(CallToolResult { - content: vec![Content::resource(ResourceContents::TextResourceContents { + "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, - })], - structured_content: None, - is_error: None, - meta: None, - }), - - "test_multiple_content_types" => Ok(CallToolResult { - content: 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, - }), - ], - structured_content: None, - is_error: None, - 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 [ @@ -306,22 +280,14 @@ impl ServerHandler for ConformanceServer { tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; } - Ok(CallToolResult { - content: vec![Content::text("Logging test completed")], - structured_content: None, - is_error: None, - meta: None, - }) + Ok(CallToolResult::success(vec![Content::text( + "Logging test completed", + )])) } - "test_error_handling" => Ok(CallToolResult { - content: vec![Content::text( - "This tool intentionally returns an error for testing", - )], - structured_content: None, - is_error: Some(true), - meta: None, - }), + "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(); @@ -343,12 +309,9 @@ impl ServerHandler for ConformanceServer { tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; } - Ok(CallToolResult { - content: vec![Content::text("Progress test completed")], - structured_content: None, - is_error: None, - meta: None, - }) + Ok(CallToolResult::success(vec![Content::text( + "Progress test completed", + )])) } "test_sampling" => { @@ -359,20 +322,10 @@ impl ServerHandler for ConformanceServer { match cx .peer - .create_message(CreateMessageRequestParams { - meta: None, - task: None, - messages: vec![SamplingMessage::user_text(prompt)], - max_tokens: 100, - model_preferences: None, - system_prompt: None, - include_context: None, - temperature: None, - stop_sequences: None, - metadata: None, - tools: None, - tool_choice: None, - }) + .create_message(CreateMessageRequestParams::new( + vec![SamplingMessage::user_text(prompt)], + 100, + )) .await { Ok(result) => { @@ -383,19 +336,15 @@ impl ServerHandler for ConformanceServer { .and_then(|c| c.as_text()) .map(|t| t.text.clone()) .unwrap_or_else(|| "No text response".into()); - Ok(CallToolResult { - content: vec![Content::text(format!("LLM response: {}", text))], - structured_content: None, - is_error: None, - meta: None, - }) + Ok(CallToolResult::success(vec![Content::text(format!( + "LLM response: {}", + text + ))])) } - Err(e) => Ok(CallToolResult { - content: vec![Content::text(format!("Sampling error: {}", e))], - structured_content: None, - is_error: Some(true), - meta: None, - }), + Err(e) => Ok(CallToolResult::error(vec![Content::text(format!( + "Sampling error: {}", + e + ))])), } } @@ -431,26 +380,19 @@ impl ServerHandler for ConformanceServer { }) .await { - Ok(result) => Ok(CallToolResult { - content: vec![Content::text(format!( - "User response: action={}, content={:?}", - match result.action { - ElicitationAction::Accept => "accept", - ElicitationAction::Decline => "decline", - ElicitationAction::Cancel => "cancel", - }, - result.content - ))], - structured_content: None, - is_error: None, - meta: None, - }), - Err(e) => Ok(CallToolResult { - content: vec![Content::text(format!("Elicitation error: {}", e))], - structured_content: None, - is_error: Some(true), - meta: None, - }), + 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 + ))])), } } @@ -498,26 +440,19 @@ impl ServerHandler for ConformanceServer { }) .await { - Ok(result) => Ok(CallToolResult { - content: vec![Content::text(format!( - "Elicitation completed: action={}, content={:?}", - match result.action { - ElicitationAction::Accept => "accept", - ElicitationAction::Decline => "decline", - ElicitationAction::Cancel => "cancel", - }, - result.content - ))], - structured_content: None, - is_error: None, - meta: None, - }), - Err(e) => Ok(CallToolResult { - content: vec![Content::text(format!("Elicitation error: {}", e))], - structured_content: None, - is_error: Some(true), - meta: None, - }), + 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 + ))])), } } @@ -573,46 +508,34 @@ impl ServerHandler for ConformanceServer { }) .await { - Ok(result) => Ok(CallToolResult { - content: vec![Content::text(format!( - "Enum elicitation completed: action={}", - match result.action { - ElicitationAction::Accept => "accept", - ElicitationAction::Decline => "decline", - ElicitationAction::Cancel => "cancel", - } - ))], - structured_content: None, - is_error: None, - meta: None, - }), - Err(e) => Ok(CallToolResult { - content: vec![Content::text(format!("Elicitation error: {}", e))], - structured_content: None, - is_error: Some(true), - meta: None, - }), + 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 { - content: vec![Content::text(format!("Hello, {}!", name))], - structured_content: None, - is_error: None, - meta: None, - }) + Ok(CallToolResult::success(vec![Content::text(format!( + "Hello, {}!", + name + ))])) } "test_reconnection" => { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - Ok(CallToolResult { - content: vec![Content::text("Reconnection test completed")], - structured_content: None, - is_error: None, - meta: None, - }) + Ok(CallToolResult::success(vec![Content::text( + "Reconnection test completed", + )])) } _ => Err(ErrorData::invalid_params( @@ -668,22 +591,22 @@ impl ServerHandler for ConformanceServer { async move { let uri = request.uri.as_str(); match uri { - "test://static-text" => Ok(ReadResourceResult { - contents: vec![ResourceContents::TextResourceContents { + "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 { - contents: vec![ResourceContents::BlobResourceContents { + }, + ])), + "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") { @@ -691,8 +614,8 @@ impl ServerHandler for ConformanceServer { .strip_prefix("test://template/") .and_then(|s| s.strip_suffix("/data")) .unwrap_or("unknown"); - Ok(ReadResourceResult { - contents: vec![ResourceContents::TextResourceContents { + Ok(ReadResourceResult::new(vec![ + ResourceContents::TextResourceContents { uri: uri.into(), mime_type: Some("application/json".into()), text: format!( @@ -700,8 +623,8 @@ impl ServerHandler for ConformanceServer { id, id ), meta: None, - }], - }) + }, + ])) } else { Err(ErrorData::resource_not_found( format!("Resource not found: {}", uri), @@ -779,18 +702,12 @@ impl ServerHandler for ConformanceServer { "test_prompt_with_arguments", Some("A test prompt that accepts arguments"), Some(vec![ - PromptArgument { - name: "name".into(), - title: None, - description: Some("The name to greet".into()), - required: Some(true), - }, - PromptArgument { - name: "style".into(), - title: None, - description: Some("The greeting style".into()), - required: Some(false), - }, + 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( @@ -816,13 +733,11 @@ impl ServerHandler for ConformanceServer { ) -> impl Future> + Send + '_ { async move { match request.name.as_str() { - "test_simple_prompt" => Ok(GetPromptResult { - description: Some("A simple test prompt".into()), - messages: vec![PromptMessage::new_text( - PromptMessageRole::User, - "This is a simple test prompt.", - )], - }), + "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"); @@ -830,47 +745,41 @@ impl ServerHandler for ConformanceServer { .get("style") .and_then(|v| v.as_str()) .unwrap_or("friendly"); - Ok(GetPromptResult { - description: Some("A prompt with arguments".into()), - messages: vec![PromptMessage::new_text( - PromptMessageRole::User, - format!("Please greet {} in a {} style.", name, style), - )], - }) + 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 { - description: Some("A prompt with an embedded resource".into()), - messages: 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, - ), - ], - }), + "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 { - description: Some("A prompt with an image".into()), - messages: vec![ - PromptMessage::new_text(PromptMessageRole::User, "Here is an image:"), - PromptMessage { - role: PromptMessageRole::User, - content: PromptMessageContent::Image { - image: image_content.no_annotation(), - }, + 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), @@ -904,10 +813,9 @@ impl ServerHandler for ConformanceServer { } } }; - Ok(CompleteResult { - completion: 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))?, + )) } }