From 26b311d18587e82585a468f84d68be7e7625eea0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:20:24 +0000 Subject: [PATCH 1/2] chore(deps): bump rmcp from 1.8.0 to 3.0.0 Bumps [rmcp](https://github.com/modelcontextprotocol/rust-sdk) from 1.8.0 to 3.0.0. - [Release notes](https://github.com/modelcontextprotocol/rust-sdk/releases) - [Changelog](https://github.com/modelcontextprotocol/rust-sdk/blob/main/release-plz.toml) - [Commits](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-v1.8.0...rmcp-v3.0.0) --- updated-dependencies: - dependency-name: rmcp dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- Cargo.lock | 11 +++++++---- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eefa3fa7..523f83f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5332,11 +5332,13 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.8.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59" +checksum = "fcd2b6dd3b18129368955f32661a7718969e8c152c7d8866434c09cf15a512e0" dependencies = [ "async-trait", + "base64 0.23.0", + "bytes", "chrono", "futures", "http 1.4.2", @@ -5356,13 +5358,14 @@ dependencies = [ "tokio-util", "tracing", "url", + "uuid", ] [[package]] name = "rmcp-macros" -version = "1.8.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aad0035b69380782d78ea95b508327e6deaa2235909053e596eea8f27b5e1d5" +checksum = "e1aa4b9345795260a43fc23d6d05e096407c8b953903f673af7c4404b49fb2d6" dependencies = [ "darling 0.23.0", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 0fe96406..212971bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ tracing-appender = "0.2" agent-client-protocol = "0.14.0" # MCP and API clients -rmcp = { version = "^1.7.0", default-features = false } +rmcp = { version = "^3.0.0", default-features = false } async-openai = { version = "^0.41.0", features = ["byot", "chat-completion", "responses"] } reqwest = { version = "^0.13.4", default-features = false, features = ["json", "query", "rustls", "http2", "stream"] } oauth2 = "5.0" From 7a71cd0901bc77eb3892f3961a09abc4f9184b00 Mon Sep 17 00:00:00 2001 From: Josh Carver Date: Wed, 29 Jul 2026 12:22:42 -0700 Subject: [PATCH 2/2] chore: gold eval patch for fix --- crates/acp-utils/src/lib.rs | 2 +- crates/acp-utils/src/notifications.rs | 8 +- crates/acp-utils/src/testing.rs | 4 +- crates/aether-auth/src/mcp/integration.rs | 2 +- crates/aether-auth/tests/mcp_oauth.rs | 67 +++++++------ crates/aether-cli/src/acp/fake_prompt_mcp.rs | 12 +-- crates/aether-cli/src/acp/session_actor.rs | 28 +++--- crates/aether-cli/src/slash_commands.rs | 4 +- .../aether-core/src/events/trace_context.rs | 10 +- crates/aether-core/src/mcp/run_mcp_task.rs | 27 +++-- crates/aether-core/src/mcp/tool_bridge.rs | 38 +++++-- .../src/testing/agent_event_builder.rs | 4 +- crates/aether-core/src/testing/fake_mcp.rs | 6 +- .../tests/mcp/trace_context_tests.rs | 16 +-- .../tests/mcp/url_elicitation_tests.rs | 51 ++++------ crates/mcp-servers/src/coding/mod.rs | 21 +--- crates/mcp-servers/src/plan/server.rs | 20 ++-- crates/mcp-servers/src/skills/server.rs | 12 +-- crates/mcp-servers/src/subagents/server.rs | 15 +-- crates/mcp-servers/src/survey/server.rs | 5 +- .../mcp-servers/tests/integration/plan_mcp.rs | 17 ++-- .../tests/integration/stdio_transport.rs | 2 +- crates/mcp-utils/src/client/manager.rs | 8 +- crates/mcp-utils/src/client/mcp_client.rs | 49 ++++++---- crates/mcp-utils/src/client/oauth_handler.rs | 4 +- crates/mcp-utils/src/client/tool_proxy.rs | 8 +- crates/wisp/src/components/app/mod.rs | 14 ++- .../src/components/conversation_screen.rs | 4 +- .../wisp/src/components/elicitation_form.rs | 98 ++++++++++++++----- crates/wisp/src/settings/overlay.rs | 1 + crates/wisp/src/test_helpers.rs | 10 +- .../app_tests/mcp_oauth_elicitation_tests.rs | 4 +- 32 files changed, 316 insertions(+), 255 deletions(-) diff --git a/crates/acp-utils/src/lib.rs b/crates/acp-utils/src/lib.rs index 74d9d5ac..27145c40 100644 --- a/crates/acp-utils/src/lib.rs +++ b/crates/acp-utils/src/lib.rs @@ -22,6 +22,6 @@ pub mod testing; // Re-export rmcp elicitation schema types so downstream crates (e.g. wisp) // don't need a direct rmcp dependency. pub use rmcp::model::{ - ConstTitle, CreateElicitationRequestParams, ElicitationSchema, EnumSchema, MultiSelectEnumSchema, PrimitiveSchema, + ConstTitle, ElicitRequestParams, ElicitationSchema, EnumSchema, MultiSelectEnumSchema, PrimitiveSchemaDefinition, SingleSelectEnumSchema, }; diff --git a/crates/acp-utils/src/notifications.rs b/crates/acp-utils/src/notifications.rs index 93428927..4ab031b9 100644 --- a/crates/acp-utils/src/notifications.rs +++ b/crates/acp-utils/src/notifications.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use agent_client_protocol::schema::AuthMethod; use agent_client_protocol::{JsonRpcNotification, JsonRpcRequest, JsonRpcResponse}; pub use mcp_utils::display_meta::{ToolDisplayMeta, ToolResultMeta}; -pub use rmcp::model::CreateElicitationRequestParams; +pub use rmcp::model::ElicitRequestParams; use serde::{Deserialize, Serialize, de::DeserializeOwned}; pub use mcp_utils::status::{McpServerAuthCapability, McpServerStatus, McpServerStatusEntry}; @@ -99,7 +99,7 @@ pub struct AuthMethodsUpdatedParams { #[request(method = "_aether/elicitation", response = ElicitationResponse)] pub struct ElicitationParams { pub server_name: String, - pub request: CreateElicitationRequestParams, + pub request: ElicitRequestParams, } pub use rmcp::model::ElicitationAction; @@ -519,7 +519,7 @@ mod tests { let params = ElicitationParams { server_name: "github".to_string(), - request: CreateElicitationRequestParams::FormElicitationParams { + request: ElicitRequestParams::FormElicitationParams { meta: None, message: "Pick a color".to_string(), requested_schema: ElicitationSchema::builder() @@ -542,7 +542,7 @@ mod tests { fn elicitation_params_url_variant_has_mode_field() { let params = ElicitationParams { server_name: "github".to_string(), - request: CreateElicitationRequestParams::UrlElicitationParams { + request: ElicitRequestParams::UrlElicitationParams { meta: None, message: "Authorize GitHub".to_string(), url: "https://github.com/login/oauth".to_string(), diff --git a/crates/acp-utils/src/testing.rs b/crates/acp-utils/src/testing.rs index b935f0e9..0caa292d 100644 --- a/crates/acp-utils/src/testing.rs +++ b/crates/acp-utils/src/testing.rs @@ -16,7 +16,7 @@ use agent_client_protocol::schema::SessionNotification; use agent_client_protocol::{ self as acp, Agent, Builder, ByteStreams, Client, ConnectionTo, HandleDispatchFrom, NullRun, Responder, }; -use rmcp::model::{CreateElicitationRequestParams, ElicitationSchema}; +use rmcp::model::{ElicitRequestParams, ElicitationSchema}; use std::collections::VecDeque; use std::sync::{Arc, Mutex}; use tokio::io::DuplexStream; @@ -196,7 +196,7 @@ pub async fn test_connection() -> (ConnectionTo, TestPeer) { fn placeholder_params() -> ElicitationParams { ElicitationParams { server_name: String::new(), - request: CreateElicitationRequestParams::FormElicitationParams { + request: ElicitRequestParams::FormElicitationParams { meta: None, message: String::new(), requested_schema: ElicitationSchema::builder().build().expect("empty schema is valid"), diff --git a/crates/aether-auth/src/mcp/integration.rs b/crates/aether-auth/src/mcp/integration.rs index 7e249c8e..d1f11dc2 100644 --- a/crates/aether-auth/src/mcp/integration.rs +++ b/crates/aether-auth/src/mcp/integration.rs @@ -51,7 +51,7 @@ pub async fn perform_oauth_flow( ); } - let metadata = manager.discover_metadata().await.map_err(rmcp_err("OAuth metadata discovery failed"))?; + let metadata = manager.resolve_metadata().await.map_err(rmcp_err("OAuth metadata discovery failed"))?.metadata; manager.set_metadata(metadata); let scopes = manager.select_scopes(None, &[]); diff --git a/crates/aether-auth/tests/mcp_oauth.rs b/crates/aether-auth/tests/mcp_oauth.rs index cbce1748..bfd4ea30 100644 --- a/crates/aether-auth/tests/mcp_oauth.rs +++ b/crates/aether-auth/tests/mcp_oauth.rs @@ -70,36 +70,47 @@ impl OAuthServer { captured_requests.lock().unwrap().push(request_line.clone()); let path = request_line.split_whitespace().nth(1).unwrap(); - let body = if path.contains("oauth-protected-resource") { - serde_json::json!({ - "resource": format!("{origin}/mcp"), - "authorization_servers": [&origin] - }) - } else if path == "/token" { - serde_json::json!({ - "access_token": "access-token", - "token_type": "Bearer", - "expires_in": 3600 - }) - } else if path == "/register" { - serde_json::json!({ - "client_id": "registered-client", - "redirect_uris": ["http://localhost:3118/"] - }) + let (status, headers, body) = if path == "/mcp" { + ( + "401 Unauthorized", + format!( + "WWW-Authenticate: Bearer resource_metadata=\"{origin}/.well-known/oauth-protected-resource/mcp\"\r\n" + ), + String::new(), + ) } else { - serde_json::json!({ - "issuer": origin, - "authorization_endpoint": format!("{origin}/authorize"), - "token_endpoint": format!("{origin}/token"), - "registration_endpoint": format!("{origin}/register"), - "response_types_supported": ["code"], - "code_challenge_methods_supported": ["S256"], - "scopes_supported": ["openid"] - }) - } - .to_string(); + let body = if path.contains("oauth-protected-resource") { + serde_json::json!({ + "resource": format!("{origin}/mcp"), + "authorization_servers": [&origin] + }) + } else if path == "/token" { + serde_json::json!({ + "access_token": "access-token", + "token_type": "Bearer", + "expires_in": 3600 + }) + } else if path == "/register" { + serde_json::json!({ + "client_id": "registered-client", + "redirect_uris": ["http://localhost:3118/"] + }) + } else { + serde_json::json!({ + "issuer": origin, + "authorization_endpoint": format!("{origin}/authorize"), + "token_endpoint": format!("{origin}/token"), + "registration_endpoint": format!("{origin}/register"), + "response_types_supported": ["code"], + "code_challenge_methods_supported": ["S256"], + "scopes_supported": ["openid"] + }) + } + .to_string(); + ("200 OK", String::new(), body) + }; let response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + "HTTP/1.1 {status}\r\nContent-Type: application/json\r\n{headers}Content-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body ); diff --git a/crates/aether-cli/src/acp/fake_prompt_mcp.rs b/crates/aether-cli/src/acp/fake_prompt_mcp.rs index d476bf3d..af55cb4a 100644 --- a/crates/aether-cli/src/acp/fake_prompt_mcp.rs +++ b/crates/aether-cli/src/acp/fake_prompt_mcp.rs @@ -1,6 +1,6 @@ use rmcp::model::{ - GetPromptRequestParams, GetPromptResult, Implementation, ListPromptsResult, PaginatedRequestParams, - Prompt as McpPrompt, PromptMessage, PromptMessageRole, ServerCapabilities, ServerInfo, + GetPromptRequestParams, GetPromptResponse, Implementation, ListPromptsResult, PaginatedRequestParams, + Prompt as McpPrompt, PromptMessage, Role, ServerCapabilities, ServerInfo, }; use rmcp::service::{DynService, RequestContext}; use rmcp::{ErrorData as McpError, RoleServer, ServerHandler}; @@ -33,18 +33,18 @@ impl ServerHandler for FakePromptMcp { _context: RequestContext, ) -> Result { let prompt = McpPrompt::new(&self.prompt_name, Some(format!("{} command", self.prompt_name)), None); - Ok(ListPromptsResult { prompts: vec![prompt], next_cursor: None, meta: None }) + Ok(ListPromptsResult::with_all_items(vec![prompt])) } async fn get_prompt( &self, request: GetPromptRequestParams, _context: RequestContext, - ) -> Result { + ) -> Result { if request.name.as_str() != self.prompt_name { return Err(McpError::invalid_params(format!("Prompt '{}' not found", request.name), None)); } - let messages = vec![PromptMessage::new_text(PromptMessageRole::User, format!("expanded {}", self.prompt_name))]; - Ok(GetPromptResult::new(messages)) + let messages = vec![PromptMessage::new_text(Role::User, format!("expanded {}", self.prompt_name))]; + Ok(rmcp::model::GetPromptResult::new(messages).into()) } } diff --git a/crates/aether-cli/src/acp/session_actor.rs b/crates/aether-cli/src/acp/session_actor.rs index 2c81c602..dcbff14e 100644 --- a/crates/aether-cli/src/acp/session_actor.rs +++ b/crates/aether-cli/src/acp/session_actor.rs @@ -11,7 +11,7 @@ use llm::catalog::LlmModel; use llm::parser::ModelProviderParser; use llm::{ChatMessage, ContentBlock, ProviderConnectionOverrides, ReasoningEffort}; use mcp_utils::client::{ElicitationRequest, McpClientEvent, McpServerStatusEntry, cancel_result}; -use rmcp::model::{CreateElicitationRequestParams, CreateElicitationResult}; +use rmcp::model::{ElicitRequestParams, ElicitResult}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::{mpsc, watch}; @@ -592,7 +592,7 @@ async fn on_elicitation_request(connection: &ConnectionTo, elicitation: .map_err(|e| AcpServerError::protocol("_aether/elicitation", e)) { Ok(response) => { - let mut result = CreateElicitationResult::new(response.action); + let mut result = ElicitResult::new(response.action); result.content = response.content; result } @@ -617,7 +617,7 @@ fn spawn_elicitation_request(connection: &ConnectionTo, elicitation: Eli } } -fn build_elicitation_params(server_name: &str, request: &CreateElicitationRequestParams) -> ElicitationParams { +fn build_elicitation_params(server_name: &str, request: &ElicitRequestParams) -> ElicitationParams { ElicitationParams { server_name: server_name.to_string(), request: request.clone() } } @@ -787,7 +787,7 @@ mod tests { #[test] fn test_build_elicitation_params_from_form() { - let elicitation = CreateElicitationRequestParams::FormElicitationParams { + let elicitation = ElicitRequestParams::FormElicitationParams { meta: None, message: "Pick a color".to_string(), requested_schema: rmcp::model::ElicitationSchema::builder().required_bool("approved").build().unwrap(), @@ -796,18 +796,19 @@ mod tests { let params = build_elicitation_params("test-server", &elicitation); assert_eq!(params.server_name, "test-server"); match ¶ms.request { - CreateElicitationRequestParams::FormElicitationParams { message, requested_schema, .. } => { + ElicitRequestParams::FormElicitationParams { message, requested_schema, .. } => { assert_eq!(message, "Pick a color"); assert_eq!(requested_schema.properties.len(), 1); assert!(requested_schema.properties.contains_key("approved")); } - CreateElicitationRequestParams::UrlElicitationParams { .. } => panic!("Expected Form, got Url"), + ElicitRequestParams::UrlElicitationParams { .. } => panic!("Expected Form, got Url"), + _ => panic!("Expected Form elicitation"), } } #[test] fn test_build_elicitation_params_from_url() { - let elicitation = CreateElicitationRequestParams::UrlElicitationParams { + let elicitation = ElicitRequestParams::UrlElicitationParams { meta: None, message: "Authorize GitHub".to_string(), url: "https://github.com/login/oauth".to_string(), @@ -817,12 +818,13 @@ mod tests { let params = build_elicitation_params("github", &elicitation); assert_eq!(params.server_name, "github"); match ¶ms.request { - CreateElicitationRequestParams::UrlElicitationParams { message, url, elicitation_id, .. } => { + ElicitRequestParams::UrlElicitationParams { message, url, elicitation_id, .. } => { assert_eq!(message, "Authorize GitHub"); assert_eq!(url, "https://github.com/login/oauth"); assert_eq!(elicitation_id, "el-123"); } - CreateElicitationRequestParams::FormElicitationParams { .. } => panic!("Expected Url, got Form"), + ElicitRequestParams::FormElicitationParams { .. } => panic!("Expected Url, got Form"), + _ => panic!("Expected URL elicitation"), } } @@ -953,7 +955,7 @@ mod tests { let (tx, rx) = oneshot::channel(); let elicitation = ElicitationRequest { server_name: "test-server".to_string(), - request: CreateElicitationRequestParams::FormElicitationParams { + request: ElicitRequestParams::FormElicitationParams { meta: None, message: "Pick a color".to_string(), requested_schema: rmcp::model::ElicitationSchema::builder() @@ -985,7 +987,7 @@ mod tests { let (tx, rx) = oneshot::channel(); let elicitation = ElicitationRequest { server_name: "github".to_string(), - request: CreateElicitationRequestParams::UrlElicitationParams { + request: ElicitRequestParams::UrlElicitationParams { meta: None, message: "Authorize".to_string(), url: "https://example.com/oauth".to_string(), @@ -1034,7 +1036,7 @@ mod tests { let (tx, rx) = oneshot::channel(); let elicitation = ElicitationRequest { server_name: "test-server".to_string(), - request: CreateElicitationRequestParams::FormElicitationParams { + request: ElicitRequestParams::FormElicitationParams { meta: None, message: "Pick a color".to_string(), requested_schema: rmcp::model::ElicitationSchema::builder() @@ -1067,7 +1069,7 @@ mod tests { let (tx, rx) = oneshot::channel(); let elicitation = ElicitationRequest { server_name: "test-server".to_string(), - request: CreateElicitationRequestParams::UrlElicitationParams { + request: ElicitRequestParams::UrlElicitationParams { meta: None, message: "Authorize".to_string(), url: "https://example.com".to_string(), diff --git a/crates/aether-cli/src/slash_commands.rs b/crates/aether-cli/src/slash_commands.rs index 42d42f95..1b090a14 100644 --- a/crates/aether-cli/src/slash_commands.rs +++ b/crates/aether-cli/src/slash_commands.rs @@ -1,6 +1,6 @@ use aether_core::mcp::run_mcp_task::McpCommand; use agent_client_protocol::schema::AvailableCommand; -use rmcp::model::{GetPromptResult, Prompt as McpPrompt, PromptMessageContent}; +use rmcp::model::{ContentBlock, GetPromptResult, Prompt as McpPrompt}; use std::collections::HashSet; use thiserror::Error; use tokio::sync::{mpsc, oneshot}; @@ -93,7 +93,7 @@ fn prompt_result_text(prompt_result: &GetPromptResult) -> Result Some(text.clone()), + ContentBlock::Text(text) => Some(text.text.clone()), _ => None, }) .ok_or(SlashCommandError::NoTextContent) diff --git a/crates/aether-core/src/events/trace_context.rs b/crates/aether-core/src/events/trace_context.rs index 89d95e50..94fe7a7e 100644 --- a/crates/aether-core/src/events/trace_context.rs +++ b/crates/aether-core/src/events/trace_context.rs @@ -1,4 +1,4 @@ -use rmcp::model::Meta; +use rmcp::model::RequestMetaObject; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -21,15 +21,15 @@ pub struct TraceContext { impl TraceContext { /// Reads the trace context a caller attached to MCP request metadata via /// [`to_meta`](Self::to_meta), if any. - pub fn from_meta(meta: &Meta) -> Option { + pub fn from_meta(meta: &RequestMetaObject) -> Option { let traceparent = meta.0.get(TRACEPARENT_KEY)?.as_str()?.to_string(); let tracestate = meta.0.get(TRACESTATE_KEY).and_then(|value| value.as_str()).map(str::to_string); Some(Self { traceparent, tracestate }) } /// Attaches the trace context to MCP request metadata. - pub fn to_meta(&self) -> Meta { - let mut meta = Meta::new(); + pub fn to_meta(&self) -> RequestMetaObject { + let mut meta = RequestMetaObject::new(); meta.0.insert(TRACEPARENT_KEY.to_string(), self.traceparent.clone().into()); if let Some(tracestate) = &self.tracestate { meta.0.insert(TRACESTATE_KEY.to_string(), tracestate.clone().into()); @@ -67,6 +67,6 @@ mod tests { #[test] fn from_meta_returns_none_without_a_traceparent() { - assert_eq!(TraceContext::from_meta(&Meta::new()), None); + assert_eq!(TraceContext::from_meta(&RequestMetaObject::new()), None); } } diff --git a/crates/aether-core/src/mcp/run_mcp_task.rs b/crates/aether-core/src/mcp/run_mcp_task.rs index 414a017b..85d3ad6d 100644 --- a/crates/aether-core/src/mcp/run_mcp_task.rs +++ b/crates/aether-core/src/mcp/run_mcp_task.rs @@ -9,8 +9,8 @@ use futures::stream::{self, StreamExt}; use llm::{ToolCallError, ToolCallRequest, ToolCallResult}; use rmcp::RoleClient; use rmcp::model::{ - CallToolRequestParams, CreateElicitationRequestParams, ErrorCode, GetPromptResult, Meta, ProgressNotificationParam, - Prompt, + CallToolRequestParams, ElicitRequestParams, ErrorCode, GetPromptResult, ProgressNotificationParam, Prompt, + RequestMetaObject, }; use rmcp::service::RunningService; use std::collections::HashSet; @@ -28,6 +28,7 @@ pub enum ToolExecutionEvent { } const MCP_AUTH_TIMEOUT: Duration = Duration::from_mins(3); +const URL_ELICITATION_REQUIRED: ErrorCode = ErrorCode(-32042); /// Commands that can be sent to the MCP manager task #[derive(Debug)] @@ -172,7 +173,7 @@ async fn execute_mcp_call( client: Arc>, request: &ToolCallRequest, params: CallToolRequestParams, - trace_meta: Option, + trace_meta: Option, timeout: Duration, tool_call_id: String, event_tx: mpsc::Sender, @@ -210,7 +211,7 @@ async fn execute_mcp_call( Ok(server_result) => server_result, Err(e) => { if let rmcp::service::ServiceError::McpError(ref error_data) = e - && error_data.code == ErrorCode::URL_ELICITATION_REQUIRED + && error_data.code == URL_ELICITATION_REQUIRED { return Err(handle_url_elicitation_required(&client, request, error_data).await); } @@ -233,7 +234,7 @@ async fn execute_mcp_call( #[derive(serde::Deserialize)] struct UrlElicitationRequiredData { - elicitations: Vec, + elicitations: Vec, } #[derive(Debug)] @@ -255,7 +256,7 @@ impl std::fmt::Display for UrlElicitationRequiredParseError { fn parse_required_url_elicitations( error_data: &rmcp::model::ErrorData, -) -> Result, UrlElicitationRequiredParseError> { +) -> Result, UrlElicitationRequiredParseError> { let data = error_data.data.as_ref().ok_or(UrlElicitationRequiredParseError::MissingData)?; let parsed: UrlElicitationRequiredData = serde_json::from_value(data.clone()).map_err(UrlElicitationRequiredParseError::InvalidData)?; @@ -263,7 +264,7 @@ fn parse_required_url_elicitations( let url_elicitations = parsed .elicitations .into_iter() - .filter(|elicitation| matches!(elicitation, CreateElicitationRequestParams::UrlElicitationParams { .. })) + .filter(|elicitation| matches!(elicitation, ElicitRequestParams::UrlElicitationParams { .. })) .collect::>(); if url_elicitations.is_empty() { @@ -318,6 +319,12 @@ async fn handle_url_elicitation_required( rmcp::model::ElicitationAction::Accept => { tracing::info!("User accepted URL elicitation for server '{server_name}'"); } + _ => { + return ToolCallError::from_request( + request, + format!("Required browser interaction for server '{server_name}' returned an unsupported response"), + ); + } } } @@ -350,14 +357,14 @@ mod tests { assert_eq!(parsed.elicitations.len(), 1); assert!(matches!( &parsed.elicitations[0], - CreateElicitationRequestParams::UrlElicitationParams { elicitation_id, .. } if elicitation_id == "el-1" + ElicitRequestParams::UrlElicitationParams { elicitation_id, .. } if elicitation_id == "el-1" )); } #[test] fn parse_required_url_elicitations_filters_to_url_only() { let error_data = rmcp::model::ErrorData { - code: rmcp::model::ErrorCode::URL_ELICITATION_REQUIRED, + code: URL_ELICITATION_REQUIRED, message: "URL elicitation required".into(), data: Some(serde_json::json!({ "elicitations": [ @@ -380,7 +387,7 @@ mod tests { assert_eq!(result.len(), 1); assert!(matches!( &result[0], - CreateElicitationRequestParams::UrlElicitationParams { elicitation_id, .. } if elicitation_id == "el-1" + ElicitRequestParams::UrlElicitationParams { elicitation_id, .. } if elicitation_id == "el-1" )); } } diff --git a/crates/aether-core/src/mcp/tool_bridge.rs b/crates/aether-core/src/mcp/tool_bridge.rs index 7dfd6b2f..b87d40aa 100644 --- a/crates/aether-core/src/mcp/tool_bridge.rs +++ b/crates/aether-core/src/mcp/tool_bridge.rs @@ -40,8 +40,15 @@ pub fn mcp_result_to_tool_call_result( mcp_result: rmcp::model::CallToolResult, ) -> Result<(ToolCallResult, Option), ToolCallError> { if mcp_result.is_error.unwrap_or(false) { - let error_msg = - mcp_result.content.first().map_or_else(|| "Unknown error".to_string(), |content| format!("{content:?}")); + let error_msg = mcp_result.content.first().map_or_else( + || "Unknown error".to_string(), + |content| { + content.as_text().map_or_else( + || serde_json::to_string(content).unwrap_or_else(|_| "Unknown error".to_string()), + |text| text.text.clone(), + ) + }, + ); Err(ToolCallError { id: request.id.clone(), name: request.name.clone(), @@ -100,7 +107,7 @@ fn maybe_spillover(tool_id: &str, result: String, max_bytes: usize, dir: &Path) fn extract_result_and_meta( structured_content: Option, - content: &[rmcp::model::Content], + content: &[rmcp::model::ContentBlock], ) -> (serde_json::Value, Option) { if let Some(mut val) = structured_content { let result_meta = extract_result_meta(&mut val); @@ -140,7 +147,7 @@ fn extract_result_meta(value: &mut serde_json::Value) -> Option mod tests { use super::*; use mcp_utils::display_meta::PlanMetaStatus; - use rmcp::model::{CallToolResult as McpCallToolResult, Content}; + use rmcp::model::{CallToolResult as McpCallToolResult, ContentBlock}; use serde::Serialize; use serde_json::json; @@ -167,7 +174,7 @@ mod tests { "_meta": { "display": { "title": "Read file", "value": "file.rs, 50 lines" } } }); let mut mcp = McpCallToolResult::structured(structured); - mcp.content = vec![Content::text("plain text fallback")]; + mcp.content = vec![ContentBlock::text("plain text fallback")]; let (result, meta) = mcp_result_to_tool_call_result(&req(), mcp).unwrap(); assert!(!result.result.contains("_meta")); @@ -237,7 +244,7 @@ mod tests { #[test] fn test_tool_call_result_falls_back_to_content() { - let mcp = McpCallToolResult::success(vec![Content::text("plain text result")]); + let mcp = McpCallToolResult::success(vec![ContentBlock::text("plain text result")]); let (result, meta) = mcp_result_to_tool_call_result(&req(), mcp).unwrap(); assert!(result.result.contains("plain text result")); assert!(meta.is_none()); @@ -313,10 +320,23 @@ mod tests { } #[test] - fn test_tool_call_result_handles_error() { - let mcp = McpCallToolResult::error(vec![Content::text("Error: file not found")]); + fn test_tool_call_result_handles_text_error_without_sdk_debug_output() { + let mcp = McpCallToolResult::error(vec![ContentBlock::text("Error: file not found")]); + let err = mcp_result_to_tool_call_result(&req(), mcp).unwrap_err(); + assert_eq!(err.error, "Tool execution error: Error: file not found"); + } + + #[test] + fn test_tool_call_result_serializes_non_text_error_content() { + let image = serde_json::from_value(serde_json::json!({ + "type": "image", + "data": "aW1hZ2U=", + "mimeType": "image/png" + })) + .unwrap(); + let mcp = McpCallToolResult::error(vec![image]); let err = mcp_result_to_tool_call_result(&req(), mcp).unwrap_err(); - assert!(err.error.contains("file not found")); + assert_eq!(err.error, r#"Tool execution error: {"type":"image","data":"aW1hZ2U=","mimeType":"image/png"}"#); } #[test] diff --git a/crates/aether-core/src/testing/agent_event_builder.rs b/crates/aether-core/src/testing/agent_event_builder.rs index 8ead91f5..d64f97f8 100644 --- a/crates/aether-core/src/testing/agent_event_builder.rs +++ b/crates/aether-core/src/testing/agent_event_builder.rs @@ -62,9 +62,7 @@ impl AgentEventBuilder { ) -> Self { let request_json = serde_json::to_string(request).expect("Failed to serialize request"); - let error_result = format!( - "Tool execution error: Annotated {{ raw: Text(RawTextContent {{ text: \"{error_message}\", meta: None }}), annotations: None }}" - ); + let error_result = format!("Tool execution error: {error_message}"); self.push_tool_call_start(tool_call_id, name); self.push_tool_call_chunk(tool_call_id, &request_json); diff --git a/crates/aether-core/src/testing/fake_mcp.rs b/crates/aether-core/src/testing/fake_mcp.rs index fe839a16..a4c098a5 100644 --- a/crates/aether-core/src/testing/fake_mcp.rs +++ b/crates/aether-core/src/testing/fake_mcp.rs @@ -1,7 +1,7 @@ use rmcp::{ ErrorData as McpError, Json, RoleServer, ServerHandler, handler::server::{router::tool::ToolRouter, wrapper::Parameters}, - model::{CallToolResult, Content, Implementation, ServerCapabilities, ServerInfo}, + model::{CallToolResult, ContentBlock, Implementation, ServerCapabilities, ServerInfo}, service::DynService, tool, tool_handler, tool_router, }; @@ -154,13 +154,13 @@ impl FakeMcpServer { let Parameters(DivideNumbersRequest { a, b }) = request; if b == 0 { - return Ok(CallToolResult::error(vec![Content::text("Division by zero")])); + return Ok(CallToolResult::error(vec![ContentBlock::text("Division by zero")])); } let result = DivideNumbersResult { quotient: a / b }; let result_json = serde_json::to_string(&result).unwrap(); - Ok(CallToolResult::success(vec![Content::text(result_json)])) + Ok(CallToolResult::success(vec![ContentBlock::text(result_json)])) } #[tool(description = "A tool that sleeps for a specified duration (for testing timeouts)")] diff --git a/crates/aether-core/tests/mcp/trace_context_tests.rs b/crates/aether-core/tests/mcp/trace_context_tests.rs index 2eaaef4b..99663b87 100644 --- a/crates/aether-core/tests/mcp/trace_context_tests.rs +++ b/crates/aether-core/tests/mcp/trace_context_tests.rs @@ -5,7 +5,7 @@ use mcp_utils::client::{McpServer, McpTransport}; use rmcp::{ RoleServer, ServerHandler, model::{ - CallToolRequestParams, CallToolResult, ErrorData, Implementation, ListToolsResult, PaginatedRequestParams, + CallToolRequestParams, CallToolResponse, ErrorData, Implementation, ListToolsResult, PaginatedRequestParams, ServerCapabilities, ServerInfo, Tool, }, service::{DynService, RequestContext}, @@ -57,19 +57,19 @@ impl ServerHandler for MetaEchoServer { _context: RequestContext, ) -> Result { let input_schema = serde_json::from_value(json!({ "type": "object", "properties": {} })).unwrap(); - Ok(ListToolsResult { - tools: vec![Tool::new("capture", "Echoes request metadata", Arc::new(input_schema))], - next_cursor: None, - meta: None, - }) + Ok(ListToolsResult::with_all_items(vec![Tool::new( + "capture", + "Echoes request metadata", + Arc::new(input_schema), + )])) } async fn call_tool( &self, _request: CallToolRequestParams, context: RequestContext, - ) -> Result { - Ok(CallToolResult::structured(serde_json::Value::Object(context.meta.0.clone()))) + ) -> Result { + Ok(rmcp::model::CallToolResult::structured(serde_json::Value::Object(context.meta.0.0.clone())).into()) } } diff --git a/crates/aether-core/tests/mcp/url_elicitation_tests.rs b/crates/aether-core/tests/mcp/url_elicitation_tests.rs index 0f52abd7..e99ac7fd 100644 --- a/crates/aether-core/tests/mcp/url_elicitation_tests.rs +++ b/crates/aether-core/tests/mcp/url_elicitation_tests.rs @@ -4,7 +4,7 @@ use mcp_utils::client::{McpServer, McpTransport}; use rmcp::{ RoleServer, ServerHandler, model::{ - CallToolRequestParams, CallToolResult, CreateElicitationRequestParams, ElicitationAction, ErrorCode, ErrorData, + CallToolRequestParams, CallToolResponse, ElicitRequestParams, ElicitationAction, ErrorCode, ErrorData, Implementation, ListToolsResult, PaginatedRequestParams, ServerCapabilities, ServerInfo, Tool, }, service::{DynService, RequestContext}, @@ -63,18 +63,18 @@ impl ServerHandler for UrlElicitationRequiredServer { "properties": {} })) .unwrap(); - Ok(ListToolsResult { - tools: vec![Tool::new("needs_browser", "Always returns URL_ELICITATION_REQUIRED", Arc::new(input_schema))], - next_cursor: None, - meta: None, - }) + Ok(ListToolsResult::with_all_items(vec![Tool::new( + "needs_browser", + "Always returns URL_ELICITATION_REQUIRED", + Arc::new(input_schema), + )])) } async fn call_tool( &self, _request: CallToolRequestParams, _context: RequestContext, - ) -> Result { + ) -> Result { let data = json!({ "elicitations": [ { @@ -85,7 +85,7 @@ impl ServerHandler for UrlElicitationRequiredServer { } ] }); - Err(ErrorData::new(ErrorCode::URL_ELICITATION_REQUIRED, "browser interaction required", Some(data))) + Err(ErrorData::new(ErrorCode(-32042), "browser interaction required", Some(data))) } } @@ -105,27 +105,19 @@ impl ServerHandler for MalformedUrlElicitationRequiredServer { "properties": {} })) .unwrap(); - Ok(ListToolsResult { - tools: vec![Tool::new( - "needs_browser", - "Returns malformed URL_ELICITATION_REQUIRED data", - Arc::new(input_schema), - )], - next_cursor: None, - meta: None, - }) + Ok(ListToolsResult::with_all_items(vec![Tool::new( + "needs_browser", + "Returns malformed URL_ELICITATION_REQUIRED data", + Arc::new(input_schema), + )])) } async fn call_tool( &self, _request: CallToolRequestParams, _context: RequestContext, - ) -> Result { - Err(ErrorData::new( - ErrorCode::URL_ELICITATION_REQUIRED, - "browser interaction required", - Some(self.data.clone()), - )) + ) -> Result { + Err(ErrorData::new(ErrorCode(-32042), "browser interaction required", Some(self.data.clone()))) } } @@ -156,7 +148,7 @@ fn call_tool_request() -> llm::ToolCallRequest { /// manager. Includes the server name and the raw request params for assertion. struct CapturedElicitation { server_name: String, - request: CreateElicitationRequestParams, + request: ElicitRequestParams, } /// Spawn an MCP manager with one fake server that always returns @@ -179,11 +171,7 @@ async fn spawn_scripted( while let Some(event) = event_rx.recv().await { if let mcp_utils::client::McpClientEvent::Elicitation(req) = event { let captured = CapturedElicitation { server_name: req.server_name, request: req.request }; - let _ = req.response_sender.send(rmcp::model::CreateElicitationResult { - action: user_action, - content: None, - meta: Option::default(), - }); + let _ = req.response_sender.send(rmcp::model::ElicitResult::new(user_action)); return Some(captured); } } @@ -241,13 +229,14 @@ async fn url_elicitation_required_accept_returns_retry_needed_error_without_url( let captured = script_handle.await.unwrap().expect("elicitation was never dispatched"); assert_eq!(captured.server_name, "browser_server"); match captured.request { - CreateElicitationRequestParams::UrlElicitationParams { url: req_url, elicitation_id, .. } => { + ElicitRequestParams::UrlElicitationParams { url: req_url, elicitation_id, .. } => { assert_eq!(req_url, url); assert_eq!(elicitation_id, "el-42"); } - CreateElicitationRequestParams::FormElicitationParams { .. } => { + ElicitRequestParams::FormElicitationParams { .. } => { panic!("expected UrlElicitationParams, got FormElicitationParams") } + _ => panic!("expected URL elicitation request"), } } diff --git a/crates/mcp-servers/src/coding/mod.rs b/crates/mcp-servers/src/coding/mod.rs index d53b4af3..ca199c63 100644 --- a/crates/mcp-servers/src/coding/mod.rs +++ b/crates/mcp-servers/src/coding/mod.rs @@ -7,7 +7,7 @@ use rmcp::{ wrapper::{Json, Parameters}, }, model::{ - CreateElicitationRequestParams, ElicitationSchema, EnumSchema, Implementation, ProgressNotificationParam, + ElicitRequestParams, ElicitationSchema, EnumSchema, Implementation, ProgressNotificationParam, ServerCapabilities, ServerInfo, }, service::RequestContext, @@ -168,15 +168,7 @@ async fn notify_preview(context: &RequestContext, meta: ToolDisplayM if let Some(token) = context.meta.get_progress_token() { let result_meta = ToolResultMeta::from(meta); let message = serde_json::to_string(&result_meta).unwrap_or_default(); - let _ = context - .peer - .notify_progress(ProgressNotificationParam { - progress_token: token, - progress: 0.0, - total: None, - message: Some(message), - }) - .await; + let _ = context.peer.notify_progress(ProgressNotificationParam::new(token, 0.0).with_message(message)).await; } } @@ -321,7 +313,7 @@ When using tools that take file paths, always use absolute paths from: let message = format!("Allow {tool_name}: {description}?"); let result = context .peer - .create_elicitation(CreateElicitationRequestParams::FormElicitationParams { + .create_elicitation(ElicitRequestParams::FormElicitationParams { meta: None, message, requested_schema: ElicitationSchema::builder() @@ -667,12 +659,7 @@ When using tools that take file paths, always use absolute paths from: if let Some(token) = progress_token { let message = format!("Search rate limited; retrying in {} seconds.", delay.as_secs()); let _ = peer - .notify_progress(ProgressNotificationParam { - progress_token: token, - progress: 0.0, - total: None, - message: Some(message), - }) + .notify_progress(ProgressNotificationParam::new(token, 0.0).with_message(message)) .await; } } diff --git a/crates/mcp-servers/src/plan/server.rs b/crates/mcp-servers/src/plan/server.rs index d19653de..c43a1325 100644 --- a/crates/mcp-servers/src/plan/server.rs +++ b/crates/mcp-servers/src/plan/server.rs @@ -10,9 +10,9 @@ use rmcp::{ wrapper::{Json, Parameters}, }, model::{ - CreateElicitationRequestParams, ElicitationAction, ElicitationSchema, EnumSchema, GetPromptRequestParams, - GetPromptResult, Implementation, ListPromptsResult, Meta, PaginatedRequestParams, Prompt, PromptArgument, - PromptMessage, PromptMessageRole, ServerCapabilities, ServerInfo, + ElicitRequestParams, ElicitationAction, ElicitationSchema, EnumSchema, GetPromptRequestParams, + GetPromptResponse, Implementation, ListPromptsResult, PaginatedRequestParams, Prompt, PromptArgument, + PromptMessage, RequestMetaObject, Role, ServerCapabilities, ServerInfo, }, service::RequestContext, tool, tool_handler, tool_router, @@ -202,10 +202,10 @@ impl PlanMcp { Ok(Json(SubmitPlanOutput { approved: false, feedback })) } - fn build_elicitation_form(plan: &Plan) -> Result { + fn build_elicitation_form(plan: &Plan) -> Result { let meta = PlanReviewElicitationMeta::new(&plan.path, &plan.content) .to_json() - .map(Meta) + .map(RequestMetaObject::from) .map_err(|e| format!("failed to serialize plan review metadata: {e}"))?; let approve = PlanReviewDecision::Approve.as_str(); @@ -216,7 +216,7 @@ impl PlanMcp { .map_err(|e| format!("failed to build decision schema: {e}"))? .build(); - Ok(CreateElicitationRequestParams::FormElicitationParams { + Ok(ElicitRequestParams::FormElicitationParams { meta: Some(meta), message: format!("Approve plan {}? Review the markdown and choose approve or deny.", plan.path.display()), requested_schema: ElicitationSchema::builder() @@ -251,14 +251,14 @@ impl ServerHandler for PlanMcp { ]), ); - Ok(ListPromptsResult { prompts: vec![prompt], next_cursor: None, meta: None }) + Ok(ListPromptsResult::with_all_items(vec![prompt])) } async fn get_prompt( &self, request: GetPromptRequestParams, _context: RequestContext, - ) -> Result { + ) -> Result { if request.name.as_str() != PROMPT_NAME { return Err(McpError::invalid_params(format!("Prompt '{}' not found", request.name), None)); } @@ -273,8 +273,8 @@ impl ServerHandler for PlanMcp { }); let content = substitute_parameters(&prompt, &arguments); - let messages = vec![PromptMessage::new_text(PromptMessageRole::User, content)]; - Ok(GetPromptResult::new(messages).with_description("Enter plan mode.".to_string())) + let messages = vec![PromptMessage::new_text(Role::User, content)]; + Ok(rmcp::model::GetPromptResult::new(messages).with_description("Enter plan mode.".to_string()).into()) } } diff --git a/crates/mcp-servers/src/skills/server.rs b/crates/mcp-servers/src/skills/server.rs index e90ef375..0cc61a12 100644 --- a/crates/mcp-servers/src/skills/server.rs +++ b/crates/mcp-servers/src/skills/server.rs @@ -6,8 +6,8 @@ use rmcp::{ wrapper::{Json, Parameters}, }, model::{ - GetPromptRequestParams, GetPromptResult, Implementation, ListPromptsResult, PaginatedRequestParams, Prompt, - PromptArgument, PromptMessage, PromptMessageRole, ServerCapabilities, ServerInfo, + GetPromptRequestParams, GetPromptResponse, Implementation, ListPromptsResult, PaginatedRequestParams, Prompt, + PromptArgument, PromptMessage, Role, ServerCapabilities, ServerInfo, }, service::RequestContext, tool, tool_handler, tool_router, @@ -281,14 +281,14 @@ impl ServerHandler for SkillsMcp { }) .collect(); - Ok(ListPromptsResult { prompts, next_cursor: None, meta: None }) + Ok(ListPromptsResult::with_all_items(prompts)) } async fn get_prompt( &self, request: GetPromptRequestParams, _context: RequestContext, - ) -> Result { + ) -> Result { let catalog = self.catalog.read().await; let spec = catalog .slash_commands() @@ -308,9 +308,9 @@ impl ServerHandler for SkillsMcp { let expander = ShellExpander::new(); let cwd = WorkspacePaths::new(self.root_dir.clone()).root().to_path_buf(); let content = expander.expand(&content, &cwd).await; - let messages = vec![PromptMessage::new_text(PromptMessageRole::User, content)]; + let messages = vec![PromptMessage::new_text(Role::User, content)]; - Ok(GetPromptResult::new(messages).with_description(spec.description.clone())) + Ok(rmcp::model::GetPromptResult::new(messages).with_description(spec.description.clone()).into()) } } diff --git a/crates/mcp-servers/src/subagents/server.rs b/crates/mcp-servers/src/subagents/server.rs index 25013aed..d048583a 100644 --- a/crates/mcp-servers/src/subagents/server.rs +++ b/crates/mcp-servers/src/subagents/server.rs @@ -192,13 +192,14 @@ impl SubAgentsMcp { tokio::spawn(async move { let _ = peer - .notify_progress(ProgressNotificationParam { - progress_token: token, - #[allow(clippy::cast_precision_loss)] - progress: counter as f64, - total: None, - message: Some(progress_data_str), - }) + .notify_progress( + ProgressNotificationParam::new(token, { + #[allow(clippy::cast_precision_loss)] + let progress = counter as f64; + progress + }) + .with_message(progress_data_str), + ) .await; }); } diff --git a/crates/mcp-servers/src/survey/server.rs b/crates/mcp-servers/src/survey/server.rs index 9dcbcb53..94b8a98b 100644 --- a/crates/mcp-servers/src/survey/server.rs +++ b/crates/mcp-servers/src/survey/server.rs @@ -5,8 +5,7 @@ use rmcp::{ wrapper::{Json, Parameters}, }, model::{ - CreateElicitationRequestParams, ElicitationAction, ElicitationSchema, Implementation, ServerCapabilities, - ServerInfo, + ElicitRequestParams, ElicitationAction, ElicitationSchema, Implementation, ServerCapabilities, ServerInfo, }, service::RequestContext, tool, tool_handler, tool_router, @@ -88,7 +87,7 @@ impl SurveyMcp { let schema = parse_schema(args.schema).map_err(|e| e.to_string())?; let result = context .peer - .create_elicitation(CreateElicitationRequestParams::FormElicitationParams { + .create_elicitation(ElicitRequestParams::FormElicitationParams { meta: None, message: args.message, requested_schema: schema, diff --git a/crates/mcp-servers/tests/integration/plan_mcp.rs b/crates/mcp-servers/tests/integration/plan_mcp.rs index f57bbda6..1e4dd324 100644 --- a/crates/mcp-servers/tests/integration/plan_mcp.rs +++ b/crates/mcp-servers/tests/integration/plan_mcp.rs @@ -3,7 +3,7 @@ use mcp_servers::file_ops::FileEdit; use mcp_servers::plan::{EditPlanInput, SubmitPlanInput, WritePlanInput}; use mcp_servers::{DEFAULT_PLAN_PROMPT, PlanMcp}; use mcp_utils::client::{McpClient, McpClientEvent}; -use rmcp::model::{CreateElicitationRequestParams, CreateElicitationResult, ElicitationAction, GetPromptRequestParams}; +use rmcp::model::{ElicitRequestParams, ElicitResult, ElicitationAction, GetPromptRequestParams}; use serde_json::json; use std::fs; use tempfile::TempDir; @@ -33,8 +33,8 @@ async fn submit_plan_raw(mcp: &TestClient, plan_name: &str) fn respond_to_elicitation_request( mut event_rx: mpsc::Receiver, - response: CreateElicitationResult, -) -> tokio::task::JoinHandle> { + response: ElicitResult, +) -> tokio::task::JoinHandle> { tokio::spawn(async move { while let Some(event) = event_rx.recv().await { if let McpClientEvent::Elicitation(req) = event { @@ -61,11 +61,7 @@ async fn submit_plan_attaches_plan_review_metadata_and_preserves_schema() -> Tes let task_handle = respond_to_elicitation_request( event_rx, - CreateElicitationResult { - action: ElicitationAction::Accept, - content: Some(json!({ "decision": "approve" })), - meta: None, - }, + ElicitResult::new(ElicitationAction::Accept).with_content(json!({ "decision": "approve" })), ); let mcp = TestClient::start_with(|| PlanMcp::new().with_plans_dir(temp_dir.path().to_path_buf()), client).await?; @@ -75,8 +71,7 @@ async fn submit_plan_attaches_plan_review_metadata_and_preserves_schema() -> Tes let plan_path = temp_dir.path().join("example-plan.md"); let elicitation_request = task_handle.await?.expect("expected elicitation request"); - let CreateElicitationRequestParams::FormElicitationParams { meta, requested_schema, .. } = elicitation_request - else { + let ElicitRequestParams::FormElicitationParams { meta, requested_schema, .. } = elicitation_request else { panic!("submit_plan should issue form elicitation request"); }; @@ -250,7 +245,7 @@ async fn get_prompt_falls_back_when_configured_file_missing() { fn extract_user_text(message: &rmcp::model::PromptMessage) -> String { match &message.content { - rmcp::model::PromptMessageContent::Text { text } => text.clone(), + rmcp::model::ContentBlock::Text(text) => text.text.clone(), other => panic!("expected text content, got {other:?}"), } } diff --git a/crates/mcp-servers/tests/integration/stdio_transport.rs b/crates/mcp-servers/tests/integration/stdio_transport.rs index c8f33e62..6c839b91 100644 --- a/crates/mcp-servers/tests/integration/stdio_transport.rs +++ b/crates/mcp-servers/tests/integration/stdio_transport.rs @@ -16,7 +16,7 @@ fn tool_names(tools: &[rmcp::model::Tool]) -> Vec<&str> { tools.iter().map(|t| t.name.as_ref()).collect() } -fn extract_text(content: &rmcp::model::Content) -> &str { +fn extract_text(content: &rmcp::model::ContentBlock) -> &str { content.as_text().expect("expected text content").text.as_str() } diff --git a/crates/mcp-utils/src/client/manager.rs b/crates/mcp-utils/src/client/manager.rs index d182dbbb..e233155f 100644 --- a/crates/mcp-utils/src/client/manager.rs +++ b/crates/mcp-utils/src/client/manager.rs @@ -16,8 +16,8 @@ use futures::future::join_all; use rmcp::{ RoleClient, model::{ - CallToolRequestParams, ClientCapabilities, ClientInfo, CreateElicitationRequestParams, CreateElicitationResult, - ElicitationAction, FormElicitationCapability, Implementation, Tool as RmcpTool, UrlElicitationCapability, + CallToolRequestParams, ClientCapabilities, ClientInfo, ElicitRequestParams, ElicitResult, ElicitationAction, + FormElicitationCapability, Implementation, Tool as RmcpTool, UrlElicitationCapability, }, service::RunningService, }; @@ -49,8 +49,8 @@ pub struct OAuthHandlerContext { #[derive(Debug)] pub struct ElicitationRequest { pub server_name: String, - pub request: CreateElicitationRequestParams, - pub response_sender: oneshot::Sender, + pub request: ElicitRequestParams, + pub response_sender: oneshot::Sender, } #[derive(Debug, Clone)] diff --git a/crates/mcp-utils/src/client/mcp_client.rs b/crates/mcp-utils/src/client/mcp_client.rs index 63670c74..d6e4bff9 100644 --- a/crates/mcp-utils/src/client/mcp_client.rs +++ b/crates/mcp-utils/src/client/mcp_client.rs @@ -3,8 +3,8 @@ use rmcp::{ ClientHandler, RoleClient, handler::client::progress::ProgressDispatcher, model::{ - ClientInfo, CreateElicitationRequestParams, CreateElicitationResult, ElicitationAction, - ElicitationResponseNotificationParam, ErrorData, ProgressNotificationParam, + ClientInfo, ConstString, CustomNotification, ElicitRequestParams, ElicitResult, ElicitationAction, + ElicitationResponseNotificationMethod, ErrorData, ProgressNotificationParam, }, service::{NotificationContext, RequestContext}, }; @@ -33,7 +33,7 @@ impl McpClient { /// /// Used by both the `create_elicitation` handler and the `-32042` /// `URL_ELICITATION_REQUIRED` error path to ensure the same user-facing flow. - pub async fn dispatch_elicitation(&self, request: CreateElicitationRequestParams) -> CreateElicitationResult { + pub async fn dispatch_elicitation(&self, request: ElicitRequestParams) -> ElicitResult { let (response_tx, response_rx) = oneshot::channel(); let elicitation_request = ElicitationRequest { server_name: self.server_name.clone(), request, response_sender: response_tx }; @@ -45,9 +45,6 @@ impl McpClient { } /// Forward a URL elicitation completion through the shared event channel. - /// - /// Split out from `on_url_elicitation_notification_complete` so it can be - /// tested without constructing a `NotificationContext`. pub async fn forward_url_elicitation_complete(&self, elicitation_id: String) { let event = McpClientEvent::UrlElicitationComplete(super::UrlElicitationCompleteParams { server_name: self.server_name.clone(), @@ -59,8 +56,8 @@ impl McpClient { } } -pub fn cancel_result() -> CreateElicitationResult { - CreateElicitationResult { action: ElicitationAction::Cancel, content: None, meta: Option::default() } +pub fn cancel_result() -> ElicitResult { + ElicitResult::new(ElicitationAction::Cancel) } impl ClientHandler for McpClient { @@ -74,18 +71,32 @@ impl ClientHandler for McpClient { async fn create_elicitation( &self, - request: CreateElicitationRequestParams, + request: ElicitRequestParams, _context: RequestContext, - ) -> Result { + ) -> Result { Ok(self.dispatch_elicitation(request).await) } - async fn on_url_elicitation_notification_complete( + async fn on_custom_notification( &self, - params: ElicitationResponseNotificationParam, + notification: CustomNotification, _context: NotificationContext, ) { - self.forward_url_elicitation_complete(params.elicitation_id).await; + if notification.method != ElicitationResponseNotificationMethod::VALUE { + return; + } + + let Some(elicitation_id) = notification + .params + .as_ref() + .and_then(|params| params.get("elicitationId")) + .and_then(serde_json::Value::as_str) + else { + tracing::warn!("URL elicitation completion notification is missing elicitationId"); + return; + }; + + self.forward_url_elicitation_complete(elicitation_id.to_string()).await; } } @@ -122,7 +133,7 @@ mod tests { let (event_tx, _) = mpsc::channel(1); let client = make_client(event_tx); - let request = CreateElicitationRequestParams::FormElicitationParams { + let request = ElicitRequestParams::FormElicitationParams { meta: None, message: "test".to_string(), requested_schema: ElicitationSchema::new(BTreeMap::new()), @@ -138,7 +149,7 @@ mod tests { let (event_tx, mut event_rx) = mpsc::channel(1); let client = make_client(event_tx); - let request = CreateElicitationRequestParams::FormElicitationParams { + let request = ElicitRequestParams::FormElicitationParams { meta: None, message: "test".to_string(), requested_schema: ElicitationSchema::new(BTreeMap::new()), @@ -162,7 +173,7 @@ mod tests { let (event_tx, mut event_rx) = mpsc::channel(1); let client = make_client(event_tx); - let request = CreateElicitationRequestParams::UrlElicitationParams { + let request = ElicitRequestParams::UrlElicitationParams { meta: None, message: "Auth".to_string(), url: "https://example.com/auth".to_string(), @@ -173,11 +184,7 @@ mod tests { let event = event_rx.recv().await.unwrap(); let elicitation = unwrap_elicitation(event); assert_eq!(elicitation.server_name, "test-server"); - let _ = elicitation.response_sender.send(CreateElicitationResult { - action: ElicitationAction::Accept, - content: None, - meta: Option::default(), - }); + let _ = elicitation.response_sender.send(ElicitResult::new(ElicitationAction::Accept)); }); let result = client.dispatch_elicitation(request).await; diff --git a/crates/mcp-utils/src/client/oauth_handler.rs b/crates/mcp-utils/src/client/oauth_handler.rs index 3a843272..568531ca 100644 --- a/crates/mcp-utils/src/client/oauth_handler.rs +++ b/crates/mcp-utils/src/client/oauth_handler.rs @@ -1,7 +1,7 @@ use crate::client::manager::{ElicitationRequest, McpClientEvent, OAuthHandlerContext, UrlElicitationCompleteParams}; use aether_auth::{OAuthCallback, OAuthError, OAuthHandler, accept_oauth_callback}; use futures::future::BoxFuture; -use rmcp::model::{CreateElicitationRequestParams, ElicitationAction}; +use rmcp::model::{ElicitRequestParams, ElicitationAction}; use std::num::NonZeroU16; use tokio::net::TcpListener; use tokio::sync::{mpsc, oneshot}; @@ -46,7 +46,7 @@ impl OAuthHandler for ElicitingOAuthHandler { let (response_sender, response_rx) = oneshot::channel(); let request = ElicitationRequest { server_name: self.server_name.clone(), - request: CreateElicitationRequestParams::UrlElicitationParams { + request: ElicitRequestParams::UrlElicitationParams { meta: None, message: "Open this URL to authorize MCP server access.".to_string(), url: auth_url, diff --git a/crates/mcp-utils/src/client/tool_proxy.rs b/crates/mcp-utils/src/client/tool_proxy.rs index 1fb51d4f..58f573d2 100644 --- a/crates/mcp-utils/src/client/tool_proxy.rs +++ b/crates/mcp-utils/src/client/tool_proxy.rs @@ -157,7 +157,13 @@ impl ToolProxy { pub fn extract_server_description(client: &RunningService, server_name: &str) -> String { client .peer_info() - .and_then(|info| info.server_info.description.as_deref().filter(|s| !s.is_empty()).map(ToString::to_string)) + .and_then(|info| { + info.server_info + .as_ref() + .and_then(|server_info| server_info.description.as_deref()) + .filter(|description| !description.is_empty()) + .map(ToString::to_string) + }) .unwrap_or_else(|| server_name.to_string()) } diff --git a/crates/wisp/src/components/app/mod.rs b/crates/wisp/src/components/app/mod.rs index b645a6e0..d1e5667b 100644 --- a/crates/wisp/src/components/app/mod.rs +++ b/crates/wisp/src/components/app/mod.rs @@ -28,9 +28,7 @@ use crate::workspace_status::WorkspaceStatus; use acp_utils::client::{AcpEvent, AcpPromptHandle}; use acp_utils::config_meta::SelectOptionMeta; use acp_utils::config_option_id::ConfigOptionId; -use acp_utils::notifications::{ - AetherCapabilities, CreateElicitationRequestParams, ElicitationAction, ElicitationResponse, -}; +use acp_utils::notifications::{AetherCapabilities, ElicitRequestParams, ElicitationAction, ElicitationResponse}; use agent_client_protocol::Responder; use agent_client_protocol::schema::{self as acp, SessionId}; use attachments::build_attachment_blocks; @@ -739,12 +737,12 @@ impl Component for App { } } -fn plan_review_meta_from_request(request: &CreateElicitationRequestParams) -> Option { +fn plan_review_meta_from_request(request: &ElicitRequestParams) -> Option { match request { - CreateElicitationRequestParams::FormElicitationParams { meta, .. } => { - PlanReviewElicitationMeta::parse(meta.as_ref().map(|meta| &meta.0)) + ElicitRequestParams::FormElicitationParams { meta, .. } => { + PlanReviewElicitationMeta::parse(meta.as_ref().map(|meta| &meta.0.0)) } - CreateElicitationRequestParams::UrlElicitationParams { .. } => None, + _ => None, } } @@ -908,7 +906,7 @@ mod tests { acp_utils::notifications::ElicitationParams { server_name: "plan-server".to_string(), - request: acp_utils::notifications::CreateElicitationRequestParams::FormElicitationParams { + request: acp_utils::notifications::ElicitRequestParams::FormElicitationParams { meta: Some( serde_json::from_value(serde_json::Value::Object(meta)) .expect("deserialize plan review metadata into rmcp meta"), diff --git a/crates/wisp/src/components/conversation_screen.rs b/crates/wisp/src/components/conversation_screen.rs index 6302cf7e..c80a3a93 100644 --- a/crates/wisp/src/components/conversation_screen.rs +++ b/crates/wisp/src/components/conversation_screen.rs @@ -10,7 +10,7 @@ use crate::components::session_picker::{SessionEntry, SessionPicker, SessionPick use crate::components::tool_call_statuses::{PromptTermination, ToolCallStatuses}; use crate::components::workspace_picker::{WorkspacePicker, WorkspacePickerMessage}; use crate::keybindings::Keybindings; -use acp_utils::CreateElicitationRequestParams; +use acp_utils::ElicitRequestParams; use acp_utils::notifications::{ AetherCapabilities, ElicitationResponse, PromptSearchParams, PromptSearchResponse, SessionPreviewResponse, WorkspaceEntry, WorkspaceMoveTarget, @@ -309,7 +309,7 @@ impl ConversationScreen { params: acp_utils::notifications::ElicitationParams, responder: Responder, ) { - if let CreateElicitationRequestParams::UrlElicitationParams { elicitation_id, .. } = ¶ms.request { + if let ElicitRequestParams::UrlElicitationParams { elicitation_id, .. } = ¶ms.request { self.pending_url_elicitations.insert((params.server_name.clone(), elicitation_id.clone())); } self.active_modal = Some(Modal::Elicitation(ElicitationForm::from_params(params, responder))); diff --git a/crates/wisp/src/components/elicitation_form.rs b/crates/wisp/src/components/elicitation_form.rs index ed15c613..ef9dabc1 100644 --- a/crates/wisp/src/components/elicitation_form.rs +++ b/crates/wisp/src/components/elicitation_form.rs @@ -1,9 +1,8 @@ use acp_utils::notifications::{ - CreateElicitationRequestParams, ElicitationAction, ElicitationParams, ElicitationResponse, - UrlElicitationCompleteParams, + ElicitRequestParams, ElicitationAction, ElicitationParams, ElicitationResponse, UrlElicitationCompleteParams, }; use acp_utils::{ - ConstTitle, ElicitationSchema, EnumSchema, MultiSelectEnumSchema, PrimitiveSchema, SingleSelectEnumSchema, + ConstTitle, ElicitationSchema, EnumSchema, MultiSelectEnumSchema, PrimitiveSchemaDefinition, SingleSelectEnumSchema, }; use agent_client_protocol::Responder; use std::io::Write; @@ -26,6 +25,11 @@ pub enum ElicitationMessage { pub enum ElicitationUi { Form(Form), Url(UrlPrompt), + Unsupported(UnsupportedPrompt), +} + +pub struct UnsupportedPrompt { + pub message: String, } pub struct UrlPrompt { @@ -174,6 +178,16 @@ impl Component for ElicitationForm { } } } + ElicitationUi::Unsupported(_) => { + let Event::Key(key) = event else { + return Some(vec![]); + }; + if key.code != KeyCode::Esc { + return Some(vec![]); + } + let _ = self.responder.take().map(|r| r.respond(Self::cancel())); + Some(vec![ElicitationMessage::Responded]) + } } } @@ -181,6 +195,7 @@ impl Component for ElicitationForm { match &mut self.ui { ElicitationUi::Form(form) => form.render(ctx), ElicitationUi::Url(prompt) => render_url_prompt(prompt, ctx), + ElicitationUi::Unsupported(prompt) => render_unsupported_prompt(prompt, ctx), } } } @@ -212,13 +227,16 @@ impl ElicitationForm { U: Fn(&str) -> Result<(), UrlHandlerError> + Send + Sync + 'static, { let ui = match params.request { - CreateElicitationRequestParams::FormElicitationParams { message, requested_schema, .. } => { + ElicitRequestParams::FormElicitationParams { message, requested_schema, .. } => { let fields = parse_schema(&requested_schema); ElicitationUi::Form(Form::new(message, fields)) } - CreateElicitationRequestParams::UrlElicitationParams { message, url, elicitation_id, .. } => { + ElicitRequestParams::UrlElicitationParams { message, url, elicitation_id, .. } => { ElicitationUi::Url(UrlPrompt::new(params.server_name, elicitation_id, message, url)) } + _ => ElicitationUi::Unsupported(UnsupportedPrompt { + message: "This server requested an unsupported type of input.".to_string(), + }), }; Self { ui, @@ -234,6 +252,7 @@ impl ElicitationForm { ElicitationResponse { action: ElicitationAction::Accept, content: Some(form.to_json()) } } ElicitationUi::Url(_) => ElicitationResponse { action: ElicitationAction::Accept, content: None }, + ElicitationUi::Unsupported(_) => Self::cancel(), } } @@ -302,6 +321,17 @@ pub fn render_url_prompt(prompt: &UrlPrompt, ctx: &ViewContext) -> Frame { Frame::new(lines) } +pub fn render_unsupported_prompt(prompt: &UnsupportedPrompt, ctx: &ViewContext) -> Frame { + use tui::{Line, Style}; + + Frame::new(vec![ + Line::default(), + Line::with_style(&prompt.message, Style::fg(ctx.theme.error())), + Line::default(), + Line::with_style("Press Esc to close.", Style::fg(ctx.theme.text_secondary())), + ]) +} + fn is_local_http_url(url: &url::Url) -> bool { if url.scheme() != "http" { return false; @@ -397,81 +427,92 @@ fn parse_schema(schema: &ElicitationSchema) -> Vec { schema .properties .iter() - .map(|(name, prop)| { + .filter_map(|(name, prop)| { + let Some(kind) = parse_field_kind(prop) else { + tracing::warn!(field = name, "Ignoring unsupported elicitation schema field"); + return None; + }; let (title, description) = extract_metadata(prop); - FormField { + Some(FormField { name: name.clone(), label: title.unwrap_or_else(|| name.clone()), description, required: required.iter().any(|r| r == name), - kind: parse_field_kind(prop), - } + kind, + }) }) .collect() } -fn parse_field_kind(prop: &PrimitiveSchema) -> FormFieldKind { +fn parse_field_kind(prop: &PrimitiveSchemaDefinition) -> Option { match prop { - PrimitiveSchema::Boolean(b) => FormFieldKind::Boolean(Checkbox::new(b.default.unwrap_or(false))), - PrimitiveSchema::Integer(_) => FormFieldKind::Number(NumberField::new(String::new(), true)), - PrimitiveSchema::Number(_) => FormFieldKind::Number(NumberField::new(String::new(), false)), - PrimitiveSchema::String(_) => FormFieldKind::Text(TextField::new(String::new())), - PrimitiveSchema::Enum(e) => parse_enum_field(e), + PrimitiveSchemaDefinition::Boolean(b) => { + Some(FormFieldKind::Boolean(Checkbox::new(b.default.unwrap_or(false)))) + } + PrimitiveSchemaDefinition::Integer(_) => Some(FormFieldKind::Number(NumberField::new(String::new(), true))), + PrimitiveSchemaDefinition::Number(_) => Some(FormFieldKind::Number(NumberField::new(String::new(), false))), + PrimitiveSchemaDefinition::String(_) => Some(FormFieldKind::Text(TextField::new(String::new()))), + PrimitiveSchemaDefinition::Enum(e) => parse_enum_field(e), + _ => None, } } -fn parse_enum_field(e: &EnumSchema) -> FormFieldKind { +fn parse_enum_field(e: &EnumSchema) -> Option { match e { EnumSchema::Single(s) => match s { SingleSelectEnumSchema::Untitled(u) => { let options = options_from_strings(&u.enum_); let default_idx = u.default.as_ref().and_then(|d| options.iter().position(|o| o.value == *d)).unwrap_or(0); - FormFieldKind::SingleSelect(RadioSelect::new(options, default_idx)) + Some(FormFieldKind::SingleSelect(RadioSelect::new(options, default_idx))) } SingleSelectEnumSchema::Titled(t) => { let options = options_from_const_titles(&t.one_of); let default_idx = t.default.as_ref().and_then(|d| options.iter().position(|o| o.value == *d)).unwrap_or(0); - FormFieldKind::SingleSelect(RadioSelect::new(options, default_idx)) + Some(FormFieldKind::SingleSelect(RadioSelect::new(options, default_idx))) } + _ => None, }, EnumSchema::Multi(m) => match m { MultiSelectEnumSchema::Untitled(u) => { let options = options_from_strings(&u.items.enum_); let defaults = u.default.as_deref().unwrap_or(&[]); let selected: Vec = options.iter().map(|o| defaults.contains(&o.value)).collect(); - FormFieldKind::MultiSelect(MultiSelect::new(options, selected)) + Some(FormFieldKind::MultiSelect(MultiSelect::new(options, selected))) } MultiSelectEnumSchema::Titled(t) => { let options = options_from_const_titles(&t.items.any_of); let defaults = t.default.as_deref().unwrap_or(&[]); let selected: Vec = options.iter().map(|o| defaults.contains(&o.value)).collect(); - FormFieldKind::MultiSelect(MultiSelect::new(options, selected)) + Some(FormFieldKind::MultiSelect(MultiSelect::new(options, selected))) } + _ => None, }, EnumSchema::Legacy(l) => { let options = options_from_strings(&l.enum_); - FormFieldKind::SingleSelect(RadioSelect::new(options, 0)) + Some(FormFieldKind::SingleSelect(RadioSelect::new(options, 0))) } + _ => None, } } -fn extract_metadata(prop: &PrimitiveSchema) -> (Option, Option) { +fn extract_metadata(prop: &PrimitiveSchemaDefinition) -> (Option, Option) { match prop { - PrimitiveSchema::String(s) => { + PrimitiveSchemaDefinition::String(s) => { (s.title.as_ref().map(ToString::to_string), s.description.as_ref().map(ToString::to_string)) } - PrimitiveSchema::Number(n) => { + PrimitiveSchemaDefinition::Number(n) => { (n.title.as_ref().map(ToString::to_string), n.description.as_ref().map(ToString::to_string)) } - PrimitiveSchema::Integer(i) => { + PrimitiveSchemaDefinition::Integer(i) => { (i.title.as_ref().map(ToString::to_string), i.description.as_ref().map(ToString::to_string)) } - PrimitiveSchema::Boolean(b) => { + PrimitiveSchemaDefinition::Boolean(b) => { (b.title.as_ref().map(ToString::to_string), b.description.as_ref().map(ToString::to_string)) } - PrimitiveSchema::Enum(e) => extract_enum_metadata(e), + PrimitiveSchemaDefinition::Enum(e) => extract_enum_metadata(e), + _ => (None, None), } } @@ -484,6 +525,7 @@ fn extract_enum_metadata(e: &EnumSchema) -> (Option, Option) { SingleSelectEnumSchema::Titled(t) => { (t.title.as_ref().map(ToString::to_string), t.description.as_ref().map(ToString::to_string)) } + _ => (None, None), }, EnumSchema::Multi(m) => match m { MultiSelectEnumSchema::Untitled(u) => { @@ -492,10 +534,12 @@ fn extract_enum_metadata(e: &EnumSchema) -> (Option, Option) { MultiSelectEnumSchema::Titled(t) => { (t.title.as_ref().map(ToString::to_string), t.description.as_ref().map(ToString::to_string)) } + _ => (None, None), }, EnumSchema::Legacy(l) => { (l.title.as_ref().map(ToString::to_string), l.description.as_ref().map(ToString::to_string)) } + _ => (None, None), } } diff --git a/crates/wisp/src/settings/overlay.rs b/crates/wisp/src/settings/overlay.rs index 5e47014d..d3de897e 100644 --- a/crates/wisp/src/settings/overlay.rs +++ b/crates/wisp/src/settings/overlay.rs @@ -189,6 +189,7 @@ impl SettingsOverlay { context, ), ElicitationUi::Form(_) => shortcut_footer(&[("[Enter]", " Submit "), ("[Esc]", " Cancel")], context), + ElicitationUi::Unsupported(_) => shortcut_footer(&[("[Esc]", " Close")], context), }; } let text = match &self.active_pane { diff --git a/crates/wisp/src/test_helpers.rs b/crates/wisp/src/test_helpers.rs index 1d5ce5d5..a5aaf8a3 100644 --- a/crates/wisp/src/test_helpers.rs +++ b/crates/wisp/src/test_helpers.rs @@ -1,6 +1,6 @@ use crate::settings::WISP_HOME_ENV_MUTEX; use acp_utils::ElicitationSchema; -use acp_utils::notifications::{CreateElicitationRequestParams, ElicitationParams}; +use acp_utils::notifications::{ElicitRequestParams, ElicitationParams}; use std::path::Path; use tui::{Event, KeyCode, KeyEvent, KeyModifiers}; @@ -27,11 +27,7 @@ pub fn elicitation_params( ) -> ElicitationParams { ElicitationParams { server_name: server.into(), - request: CreateElicitationRequestParams::FormElicitationParams { - meta: None, - message: message.into(), - requested_schema, - }, + request: ElicitRequestParams::FormElicitationParams { meta: None, message: message.into(), requested_schema }, } } @@ -51,7 +47,7 @@ pub fn url_elicitation_params_with_message( ) -> ElicitationParams { ElicitationParams { server_name: server.into(), - request: CreateElicitationRequestParams::UrlElicitationParams { + request: ElicitRequestParams::UrlElicitationParams { meta: None, message: message.into(), url: url.into(), diff --git a/crates/wisp/tests/app_tests/mcp_oauth_elicitation_tests.rs b/crates/wisp/tests/app_tests/mcp_oauth_elicitation_tests.rs index 8ee34437..3d378680 100644 --- a/crates/wisp/tests/app_tests/mcp_oauth_elicitation_tests.rs +++ b/crates/wisp/tests/app_tests/mcp_oauth_elicitation_tests.rs @@ -1,7 +1,7 @@ use super::common::*; use acp_utils::{ notifications::{ - CreateElicitationRequestParams, ElicitationAction, ElicitationParams, McpNotification, McpServerAuthCapability, + ElicitRequestParams, ElicitationAction, ElicitationParams, McpNotification, McpServerAuthCapability, McpServerStatus, McpServerStatusEntry, UrlElicitationCompleteParams, }, testing::test_connection, @@ -99,7 +99,7 @@ fn url_elicitation_params( ) -> ElicitationParams { ElicitationParams { server_name: server_name.into(), - request: CreateElicitationRequestParams::UrlElicitationParams { + request: ElicitRequestParams::UrlElicitationParams { meta: None, message: message.into(), url: url.into(),