From 9713a34772a837a21452cdf8bc846cc6bf27cbef Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 22 Apr 2026 19:38:45 -0700 Subject: [PATCH] Use proxy bridge for OAuth HttpClient routing Co-authored-by: Codex --- codex-rs/Cargo.lock | 2 + codex-rs/Cargo.toml | 3 +- codex-rs/rmcp-client/src/oauth.rs | 4 + codex-rs/rmcp-client/src/oauth_http_client.rs | 413 +- codex-rs/rmcp-client/src/rmcp_client.rs | 8 +- codex-rs/vendor/rmcp/Cargo.toml | 486 --- codex-rs/vendor/rmcp/README.md | 310 -- codex-rs/vendor/rmcp/build.rs | 23 - codex-rs/vendor/rmcp/src/error.rs | 61 - codex-rs/vendor/rmcp/src/handler.rs | 6 - codex-rs/vendor/rmcp/src/handler/client.rs | 376 -- .../rmcp/src/handler/client/progress.rs | 100 - codex-rs/vendor/rmcp/src/handler/server.rs | 570 --- .../vendor/rmcp/src/handler/server/common.rs | 271 -- .../vendor/rmcp/src/handler/server/prompt.rs | 367 -- .../rmcp/src/handler/server/resource.rs | 1 - .../vendor/rmcp/src/handler/server/router.rs | 138 - .../rmcp/src/handler/server/router/prompt.rs | 213 -- .../rmcp/src/handler/server/router/tool.rs | 285 -- .../vendor/rmcp/src/handler/server/tool.rs | 333 -- .../handler/server/tool_name_validation.rs | 261 -- .../vendor/rmcp/src/handler/server/wrapper.rs | 4 - .../rmcp/src/handler/server/wrapper/json.rs | 54 - .../src/handler/server/wrapper/parameters.rs | 55 - codex-rs/vendor/rmcp/src/lib.rs | 52 - codex-rs/vendor/rmcp/src/model.rs | 3352 ----------------- codex-rs/vendor/rmcp/src/model/annotated.rs | 224 -- .../vendor/rmcp/src/model/capabilities.rs | 731 ---- codex-rs/vendor/rmcp/src/model/content.rs | 364 -- .../rmcp/src/model/elicitation_schema.rs | 2010 ---------- codex-rs/vendor/rmcp/src/model/extension.rs | 337 -- codex-rs/vendor/rmcp/src/model/meta.rs | 289 -- codex-rs/vendor/rmcp/src/model/prompt.rs | 271 -- codex-rs/vendor/rmcp/src/model/resource.rs | 188 - codex-rs/vendor/rmcp/src/model/serde_impl.rs | 373 -- codex-rs/vendor/rmcp/src/model/task.rs | 79 - codex-rs/vendor/rmcp/src/model/tool.rs | 260 -- codex-rs/vendor/rmcp/src/service.rs | 944 ----- codex-rs/vendor/rmcp/src/service/client.rs | 543 --- codex-rs/vendor/rmcp/src/service/server.rs | 895 ----- codex-rs/vendor/rmcp/src/service/tower.rs | 54 - codex-rs/vendor/rmcp/src/task_manager.rs | 299 -- codex-rs/vendor/rmcp/src/transport.rs | 277 -- .../vendor/rmcp/src/transport/async_rw.rs | 557 --- codex-rs/vendor/rmcp/src/transport/auth.rs | 1974 ---------- .../rmcp/src/transport/child_process.rs | 309 -- codex-rs/vendor/rmcp/src/transport/common.rs | 19 - .../vendor/rmcp/src/transport/common/auth.rs | 3 - .../common/auth/streamable_http_client.rs | 61 - .../src/transport/common/client_side_sse.rs | 310 -- .../rmcp/src/transport/common/http_header.rs | 4 - .../rmcp/src/transport/common/reqwest.rs | 3 - .../common/reqwest/streamable_http_client.rs | 199 - .../src/transport/common/server_side_http.rs | 170 - codex-rs/vendor/rmcp/src/transport/io.rs | 6 - .../vendor/rmcp/src/transport/sink_stream.rs | 78 - .../src/transport/streamable_http_client.rs | 774 ---- .../src/transport/streamable_http_server.rs | 8 - .../streamable_http_server/session.rs | 53 - .../streamable_http_server/session/local.rs | 1043 ----- .../streamable_http_server/session/never.rs | 107 - .../transport/streamable_http_server/tower.rs | 521 --- codex-rs/vendor/rmcp/src/transport/worker.rs | 208 - codex-rs/vendor/rmcp/src/transport/ws.rs | 1 - 64 files changed, 327 insertions(+), 21967 deletions(-) delete mode 100644 codex-rs/vendor/rmcp/Cargo.toml delete mode 100644 codex-rs/vendor/rmcp/README.md delete mode 100644 codex-rs/vendor/rmcp/build.rs delete mode 100644 codex-rs/vendor/rmcp/src/error.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/client.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/client/progress.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server/common.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server/prompt.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server/resource.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server/router.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server/router/prompt.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server/router/tool.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server/tool.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server/tool_name_validation.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server/wrapper.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server/wrapper/json.rs delete mode 100644 codex-rs/vendor/rmcp/src/handler/server/wrapper/parameters.rs delete mode 100644 codex-rs/vendor/rmcp/src/lib.rs delete mode 100644 codex-rs/vendor/rmcp/src/model.rs delete mode 100644 codex-rs/vendor/rmcp/src/model/annotated.rs delete mode 100644 codex-rs/vendor/rmcp/src/model/capabilities.rs delete mode 100644 codex-rs/vendor/rmcp/src/model/content.rs delete mode 100644 codex-rs/vendor/rmcp/src/model/elicitation_schema.rs delete mode 100644 codex-rs/vendor/rmcp/src/model/extension.rs delete mode 100644 codex-rs/vendor/rmcp/src/model/meta.rs delete mode 100644 codex-rs/vendor/rmcp/src/model/prompt.rs delete mode 100644 codex-rs/vendor/rmcp/src/model/resource.rs delete mode 100644 codex-rs/vendor/rmcp/src/model/serde_impl.rs delete mode 100644 codex-rs/vendor/rmcp/src/model/task.rs delete mode 100644 codex-rs/vendor/rmcp/src/model/tool.rs delete mode 100644 codex-rs/vendor/rmcp/src/service.rs delete mode 100644 codex-rs/vendor/rmcp/src/service/client.rs delete mode 100644 codex-rs/vendor/rmcp/src/service/server.rs delete mode 100644 codex-rs/vendor/rmcp/src/service/tower.rs delete mode 100644 codex-rs/vendor/rmcp/src/task_manager.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/async_rw.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/auth.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/child_process.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/common.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/common/auth.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/common/auth/streamable_http_client.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/common/client_side_sse.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/common/http_header.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/common/reqwest.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/common/reqwest/streamable_http_client.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/common/server_side_http.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/io.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/sink_stream.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/streamable_http_client.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/streamable_http_server.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/streamable_http_server/session.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/streamable_http_server/session/local.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/streamable_http_server/session/never.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/streamable_http_server/tower.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/worker.rs delete mode 100644 codex-rs/vendor/rmcp/src/transport/ws.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 0cdc85fcb1..0fedac11e6 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -10559,6 +10559,8 @@ dependencies = [ [[package]] name = "rmcp" version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bef41ebc9ebed2c1b1d90203e9d1756091e8a00bbc3107676151f39868ca0ee" dependencies = [ "async-trait", "axum", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 54eb994ed7..648c184ec8 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -478,7 +478,8 @@ ratatui = { git = "https://github.com/nornagon/ratatui", rev = "9b2ad1298408c459 tokio-tungstenite = { git = "https://github.com/openai-oss-forks/tokio-tungstenite", rev = "132f5b39c862e3a970f731d709608b3e6276d5f6" } tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" } -rmcp = { path = "vendor/rmcp" } +# Uncomment to debug local changes. +# rmcp = { path = "../../rust-sdk/crates/rmcp" } [patch."ssh://git@github.com/openai-oss-forks/tungstenite-rs.git"] tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" } diff --git a/codex-rs/rmcp-client/src/oauth.rs b/codex-rs/rmcp-client/src/oauth.rs index 5adfae9347..5f4a681609 100644 --- a/codex-rs/rmcp-client/src/oauth.rs +++ b/codex-rs/rmcp-client/src/oauth.rs @@ -48,6 +48,7 @@ use codex_keyring_store::KeyringStore; use rmcp::transport::auth::AuthorizationManager; use tokio::sync::Mutex; +use crate::oauth_http_client::OAuthHttpProxy; use codex_utils_home_dir::find_codex_home; const KEYRING_SERVICE: &str = "Codex MCP Credentials"; @@ -258,6 +259,7 @@ struct OAuthPersistorInner { server_name: String, url: String, authorization_manager: Arc>, + _oauth_http_proxy: Option, store_mode: OAuthCredentialsStoreMode, last_credentials: Mutex>, } @@ -267,6 +269,7 @@ impl OAuthPersistor { server_name: String, url: String, authorization_manager: Arc>, + oauth_http_proxy: Option, store_mode: OAuthCredentialsStoreMode, initial_credentials: Option, ) -> Self { @@ -275,6 +278,7 @@ impl OAuthPersistor { server_name, url, authorization_manager, + _oauth_http_proxy: oauth_http_proxy, store_mode, last_credentials: Mutex::new(initial_credentials), }), diff --git a/codex-rs/rmcp-client/src/oauth_http_client.rs b/codex-rs/rmcp-client/src/oauth_http_client.rs index a4631f32ce..83b193b2ab 100644 --- a/codex-rs/rmcp-client/src/oauth_http_client.rs +++ b/codex-rs/rmcp-client/src/oauth_http_client.rs @@ -1,121 +1,291 @@ -//! OAuth bootstrap helpers built on the shared `HttpClient` capability. +//! OAuth bootstrap helpers that route non-browser HTTP through the shared +//! `HttpClient` capability. //! -//! Browser launch, callback handling, and token persistence remain -//! orchestrator-owned. This module covers the non-browser OAuth HTTP work -//! needed for discovery, registration, token exchange, and refresh so those -//! requests follow the same local-or-remote placement as the MCP transport. +//! The browser-facing part of MCP OAuth stays on the orchestrator, but the +//! HTTP requests used for discovery, registration, token exchange, and refresh +//! need to follow the selected MCP placement. This module discovers OAuth +//! metadata through `HttpClient` and exposes token/registration endpoints +//! through a small localhost proxy so RMCP's OAuth manager can keep using its +//! existing `reqwest`-based internals. +use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; +use std::thread; use anyhow::Result; use anyhow::anyhow; -use async_trait::async_trait; use codex_exec_server::HttpClient; use codex_exec_server::HttpHeader; use codex_exec_server::HttpRequestParams; -use oauth2::HttpRequest; -use oauth2::HttpResponse; -use oauth2::http::HeaderName as OAuthHeaderName; -use oauth2::http::HeaderValue as OAuthHeaderValue; use reqwest::Method; +use reqwest::StatusCode; use reqwest::Url; use reqwest::header::HeaderMap; use rmcp::transport::auth::AuthError; use rmcp::transport::auth::AuthorizationManager; -use rmcp::transport::auth::OAuthHttpClient; +use rmcp::transport::auth::AuthorizationMetadata; +use rmcp::transport::auth::CredentialStore; use rmcp::transport::auth::StoredCredentials; +use tiny_http::Header; +use tiny_http::Response; +use tiny_http::Server; -const DEFAULT_TIMEOUT_MS: u64 = 30_000; +const DISCOVERY_TIMEOUT_MS: u64 = 30_000; +const MCP_PROTOCOL_VERSION_HEADER: &str = "MCP-Protocol-Version"; +const MCP_PROTOCOL_VERSION: &str = "2024-11-05"; +const TOKEN_PROXY_PATH: &str = "/oauth/token"; +const REGISTRATION_PROXY_PATH: &str = "/oauth/register"; -pub(crate) async fn create_oauth_authorization_manager( +/// The OAuth manager plus any bridge state needed to keep its non-browser +/// HTTP requests on the selected `HttpClient`. +pub(crate) struct OAuthHttpSetup { + pub(crate) authorization_manager: AuthorizationManager, + pub(crate) proxy: Option, +} + +/// Builds an OAuth manager whose metadata discovery and token flows follow the +/// selected `HttpClient`, while still leaving browser launch and callback +/// handling on the orchestrator. +pub(crate) async fn create_oauth_http_setup( url: &str, - default_headers: HeaderMap, + default_headers: &HeaderMap, http_client: Arc, initial_credentials: Option, -) -> Result { +) -> Result { + let metadata = discover_authorization_metadata(url, default_headers, Arc::clone(&http_client)) + .await + .map_err(|error| anyhow!(error))?; + // RMCP's OAuth manager still issues token and registration requests through + // reqwest. Rewrite just those endpoints through a tiny local bridge so the + // actual network hop still follows the selected `HttpClient`. + let proxy = OAuthHttpProxy::new(Arc::clone(&http_client), default_headers, &metadata)?; + let mut authorization_manager = AuthorizationManager::new(url) .await .map_err(|error| anyhow!(error))?; - authorization_manager - .with_oauth_http_client(HttpClientOAuthAdapter::new(http_client, default_headers)) - .map_err(|error| anyhow!(error))?; + authorization_manager.set_metadata(proxy.rewrite_metadata(&metadata)?); if let Some(initial_credentials) = initial_credentials { + let client_id = initial_credentials.client_id.clone(); authorization_manager.set_credential_store(StaticCredentialStore::new(initial_credentials)); authorization_manager - .initialize_from_store() - .await + .configure_client_id(&client_id) .map_err(|error| anyhow!(error))?; } - Ok(authorization_manager) + Ok(OAuthHttpSetup { + authorization_manager, + proxy: Some(proxy), + }) } -#[derive(Clone)] -struct HttpClientOAuthAdapter { +async fn discover_authorization_metadata( + url: &str, + default_headers: &HeaderMap, http_client: Arc, - default_headers: HeaderMap, -} +) -> Result { + let base_url = Url::parse(url).map_err(|error| AuthError::OAuthError(error.to_string()))?; -impl HttpClientOAuthAdapter { - fn new(http_client: Arc, default_headers: HeaderMap) -> Self { - Self { - http_client, - default_headers, - } - } -} - -#[async_trait] -impl OAuthHttpClient for HttpClientOAuthAdapter { - async fn execute(&self, request: HttpRequest) -> Result { - let method = Method::from_bytes(request.method().as_str().as_bytes()) - .map_err(|error| AuthError::InternalError(error.to_string()))?; - let url = Url::parse(&request.uri().to_string()).map_err(AuthError::UrlError)?; - let mut headers = protocol_headers(&self.default_headers); - for (name, value) in request.headers() { - headers.push(HttpHeader { - name: name.as_str().to_string(), - value: value - .to_str() - .map_err(|error| AuthError::InternalError(error.to_string()))? - .to_string(), - }); - } - - let response = self - .http_client + for discovery_url in generate_discovery_urls(&base_url) { + let response = http_client .http_request(HttpRequestParams { - method: method.as_str().to_string(), - url: url.to_string(), - headers, - body: Some(request.body().clone().into()), - timeout_ms: Some(DEFAULT_TIMEOUT_MS), - request_id: next_request_id("oauth-http"), + method: Method::GET.as_str().to_string(), + url: discovery_url.to_string(), + headers: discovery_headers(default_headers)?, + body: None, + timeout_ms: Some(DISCOVERY_TIMEOUT_MS), + request_id: next_request_id("oauth-discovery"), stream_response: false, }) .await .map_err(|error| AuthError::OAuthError(error.to_string()))?; - let mut http_response = HttpResponse::new(response.body.into_inner()); - *http_response.status_mut() = reqwest::StatusCode::from_u16(response.status) - .map_err(|error| AuthError::InternalError(error.to_string()))?; - for header in response.headers { - let header_name = header - .name - .parse::() - .map_err(|error| AuthError::InternalError(error.to_string()))?; - let header_value = header - .value - .parse::() - .map_err(|error| AuthError::InternalError(error.to_string()))?; - http_response - .headers_mut() - .insert(header_name, header_value); + if response.status != StatusCode::OK.as_u16() { + continue; } - Ok(http_response) + + let metadata = serde_json::from_slice::(&response.body.into_inner()) + .map_err(|error| AuthError::OAuthError(error.to_string()))?; + return Ok(metadata); + } + + Err(AuthError::NoAuthorizationSupport) +} + +fn generate_discovery_urls(base_url: &Url) -> Vec { + let mut candidates = Vec::new(); + let trimmed = base_url + .path() + .trim_start_matches('/') + .trim_end_matches('/'); + let mut push_candidate = |discovery_path: String| { + let mut discovery_url = base_url.clone(); + discovery_url.set_query(None); + discovery_url.set_fragment(None); + discovery_url.set_path(&discovery_path); + candidates.push(discovery_url); + }; + + if trimmed.is_empty() { + push_candidate("/.well-known/oauth-authorization-server".to_string()); + push_candidate("/.well-known/openid-configuration".to_string()); + } else { + push_candidate(format!("/.well-known/oauth-authorization-server/{trimmed}")); + push_candidate(format!("/.well-known/openid-configuration/{trimmed}")); + push_candidate(format!("/{trimmed}/.well-known/openid-configuration")); + push_candidate("/.well-known/oauth-authorization-server".to_string()); + } + + candidates +} + +fn discovery_headers(default_headers: &HeaderMap) -> Result, AuthError> { + let mut headers = protocol_headers(default_headers); + headers.push(HttpHeader { + name: MCP_PROTOCOL_VERSION_HEADER.to_string(), + value: MCP_PROTOCOL_VERSION.to_string(), + }); + Ok(headers) +} + +/// A tiny localhost bridge that lets RMCP keep using its internal reqwest +/// OAuth flows while the real network requests still go through `HttpClient`. +pub(crate) struct OAuthHttpProxy { + server: Arc, + base_url: String, + routes: Arc>, +} + +impl OAuthHttpProxy { + fn new( + http_client: Arc, + default_headers: &HeaderMap, + metadata: &AuthorizationMetadata, + ) -> Result { + let server = Arc::new(Server::http("127.0.0.1:0").map_err(|error| anyhow!(error))?); + let address = match server.server_addr() { + tiny_http::ListenAddr::IP(address) => address, + #[cfg(not(target_os = "windows"))] + _ => return Err(anyhow!("unable to determine OAuth HTTP proxy bind address")), + }; + let base_url = format!("http://{address}"); + + let mut routes = HashMap::new(); + routes.insert( + TOKEN_PROXY_PATH.to_string(), + metadata.token_endpoint.clone(), + ); + if let Some(registration_endpoint) = metadata.registration_endpoint.clone() { + routes.insert(REGISTRATION_PROXY_PATH.to_string(), registration_endpoint); + } + let routes = Arc::new(routes); + + let server_for_thread = Arc::clone(&server); + let routes_for_thread = Arc::clone(&routes); + let default_headers = default_headers.clone(); + let runtime = tokio::runtime::Handle::current(); + thread::spawn(move || { + while let Ok(mut request) = server_for_thread.recv() { + let request_url = request.url().to_string(); + let route_key = request_url + .split_once('?') + .map(|(path, _)| path) + .unwrap_or(request_url.as_str()) + .to_string(); + let Some(target_url) = routes_for_thread.get(&route_key).cloned() else { + let _ = request.respond(Response::empty(StatusCode::NOT_FOUND.as_u16())); + continue; + }; + + let mut target = target_url; + if let Some((_, query)) = request_url.split_once('?') { + target.push('?'); + target.push_str(query); + } + + let method = request.method().as_str().to_string(); + let mut body = Vec::new(); + if request.as_reader().read_to_end(&mut body).is_err() { + let _ = request.respond(Response::empty(StatusCode::BAD_REQUEST.as_u16())); + continue; + } + + // Preserve the original request shape as much as we can, but + // let the selected `HttpClient` own the outbound HTTP call. + let mut headers = protocol_headers(&default_headers); + for header in request.headers() { + let field = header.field.as_str().to_string(); + if field.eq_ignore_ascii_case("host") + || field.eq_ignore_ascii_case("content-length") + { + continue; + } + headers.push(HttpHeader { + name: field, + value: header.value.to_string(), + }); + } + + let response = runtime.block_on(http_client.http_request(HttpRequestParams { + method, + url: target, + headers, + body: Some(body.into()), + timeout_ms: Some(DISCOVERY_TIMEOUT_MS), + request_id: next_request_id("oauth-proxy"), + stream_response: false, + })); + + let response = match response { + Ok(response) => response, + Err(_) => { + let _ = request.respond(Response::empty(StatusCode::BAD_GATEWAY.as_u16())); + continue; + } + }; + + let mut proxy_response = Response::from_data(response.body.into_inner()) + .with_status_code(response.status); + for header in response.headers { + if let Ok(header) = Header::from_bytes(header.name.as_bytes(), header.value) { + proxy_response.add_header(header); + } + } + let _ = request.respond(proxy_response); + } + }); + + Ok(Self { + server, + base_url, + routes, + }) + } + + fn rewrite_metadata(&self, metadata: &AuthorizationMetadata) -> Result { + let mut rewritten = metadata.clone(); + rewritten.token_endpoint = self.proxied_url(TOKEN_PROXY_PATH)?; + rewritten.registration_endpoint = if self.routes.contains_key(REGISTRATION_PROXY_PATH) { + Some(self.proxied_url(REGISTRATION_PROXY_PATH)?) + } else { + None + }; + Ok(rewritten) + } + + fn proxied_url(&self, path: &str) -> Result { + let mut url = Url::parse(&self.base_url).map_err(|error| anyhow!(error))?; + url.set_path(path); + Ok(url.to_string()) + } +} + +impl Drop for OAuthHttpProxy { + fn drop(&mut self) { + // Wake the background thread so it can notice shutdown and exit. + self.server.unblock(); } } @@ -132,8 +302,8 @@ impl StaticCredentialStore { } } -#[async_trait] -impl rmcp::transport::auth::CredentialStore for StaticCredentialStore { +#[async_trait::async_trait] +impl CredentialStore for StaticCredentialStore { async fn load(&self) -> Result, AuthError> { Ok(self.stored.lock().await.clone()) } @@ -176,9 +346,6 @@ mod tests { use codex_exec_server::HttpRequestResponse; use futures::FutureExt; use futures::future::BoxFuture; - use oauth2::http::Method as OAuthMethod; - use oauth2::http::Uri; - use oauth2::http::header::CONTENT_TYPE; use pretty_assertions::assert_eq; use super::*; @@ -231,36 +398,84 @@ mod tests { } #[tokio::test] - async fn oauth_http_adapter_uses_http_client() -> Result<()> { + async fn discover_authorization_metadata_uses_http_client() -> Result<()> { let http_client = TestHttpClient::default(); http_client.push_response(Ok(HttpRequestResponse { - status: reqwest::StatusCode::OK.as_u16(), + status: StatusCode::OK.as_u16(), + headers: Vec::new(), + body: serde_json::to_vec(&AuthorizationMetadata { + authorization_endpoint: "https://example.com/authorize".to_string(), + token_endpoint: "https://example.com/token".to_string(), + registration_endpoint: None, + issuer: None, + jwks_uri: None, + scopes_supported: None, + response_types_supported: None, + additional_fields: HashMap::new(), + })? + .into(), + })); + + let metadata = discover_authorization_metadata( + "https://example.com/mcp", + &HeaderMap::new(), + Arc::new(http_client.clone()), + ) + .await?; + + assert_eq!( + metadata.authorization_endpoint, + "https://example.com/authorize" + ); + assert_eq!(metadata.token_endpoint, "https://example.com/token"); + assert_eq!( + http_client.requests()[0].url, + "https://example.com/.well-known/oauth-authorization-server/mcp" + ); + Ok(()) + } + + #[tokio::test] + async fn oauth_http_proxy_forwards_token_requests() -> Result<()> { + let http_client = TestHttpClient::default(); + http_client.push_response(Ok(HttpRequestResponse { + status: StatusCode::OK.as_u16(), headers: vec![HttpHeader { name: "content-type".to_string(), value: "application/json".to_string(), }], - body: br#"{"ok":true}"#.to_vec().into(), + body: br#"{"access_token":"abc","token_type":"Bearer"}"#.to_vec().into(), })); - let adapter = HttpClientOAuthAdapter::new(Arc::new(http_client.clone()), HeaderMap::new()); - let mut request = HttpRequest::new(b"grant_type=refresh_token".to_vec()); - *request.method_mut() = OAuthMethod::POST; - *request.uri_mut() = "https://example.com/token" - .parse::() - .map_err(|error| anyhow!(error))?; - request.headers_mut().insert( - CONTENT_TYPE, - OAuthHeaderValue::from_static("application/x-www-form-urlencoded"), - ); + let proxy = OAuthHttpProxy::new( + Arc::new(http_client.clone()), + &HeaderMap::new(), + &AuthorizationMetadata { + authorization_endpoint: "https://example.com/authorize".to_string(), + token_endpoint: "https://remote.example.com/token".to_string(), + registration_endpoint: None, + issuer: None, + jwks_uri: None, + scopes_supported: None, + response_types_supported: None, + additional_fields: HashMap::new(), + }, + )?; - let response = adapter.execute(request).await?; + let response = reqwest::Client::new() + .post(proxy.proxied_url(TOKEN_PROXY_PATH)?) + .header("content-type", "application/x-www-form-urlencoded") + .body("grant_type=refresh_token") + .send() + .await?; - assert_eq!(response.status(), reqwest::StatusCode::OK); - assert_eq!(response.body().as_slice(), br#"{"ok":true}"#); - assert_eq!(http_client.requests().len(), 1); - assert_eq!(http_client.requests()[0].url, "https://example.com/token"); + assert_eq!(response.status(), StatusCode::OK); + + let requests = http_client.requests(); + assert_eq!(requests.len(), 1); + assert_eq!(requests[0].url, "https://remote.example.com/token"); assert_eq!( - String::from_utf8(http_client.requests()[0].body.clone().unwrap().into_inner())?, + String::from_utf8(requests[0].body.clone().unwrap().into_inner())?, "grant_type=refresh_token" ); Ok(()) diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index b1cc2a27ea..530f46e32f 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -63,7 +63,7 @@ use crate::http_client_adapter::StreamableHttpClientAdapterError; use crate::load_oauth_tokens; use crate::oauth::OAuthPersistor; use crate::oauth::StoredOAuthTokens; -use crate::oauth_http_client::create_oauth_authorization_manager; +use crate::oauth_http_client::create_oauth_http_setup; use crate::stdio_server_launcher::StdioServerCommand; use crate::stdio_server_launcher::StdioServerLauncher; use crate::stdio_server_launcher::StdioServerTransport; @@ -933,9 +933,9 @@ async fn create_oauth_transport_and_runtime( StreamableHttpClientTransport>, OAuthPersistor, )> { - let manager = create_oauth_authorization_manager( + let oauth_http_setup = create_oauth_http_setup( url, - default_headers.clone(), + &default_headers, http_client.clone(), Some(StoredCredentials { client_id: initial_tokens.client_id.clone(), @@ -943,6 +943,7 @@ async fn create_oauth_transport_and_runtime( }), ) .await?; + let manager = oauth_http_setup.authorization_manager; let auth_client = AuthClient::new( StreamableHttpClientAdapter::new(http_client, default_headers), @@ -959,6 +960,7 @@ async fn create_oauth_transport_and_runtime( server_name.to_string(), url.to_string(), auth_manager, + oauth_http_setup.proxy, credentials_store, Some(initial_tokens), ); diff --git a/codex-rs/vendor/rmcp/Cargo.toml b/codex-rs/vendor/rmcp/Cargo.toml deleted file mode 100644 index 0376fa653b..0000000000 --- a/codex-rs/vendor/rmcp/Cargo.toml +++ /dev/null @@ -1,486 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2024" -name = "rmcp" -version = "0.15.0" -build = "build.rs" -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Rust SDK for Model Context Protocol" -homepage = "https://github.com/modelcontextprotocol/rust-sdk" -documentation = "https://docs.rs/rmcp" -readme = "README.md" -license = "Apache-2.0" -repository = "https://github.com/modelcontextprotocol/rust-sdk/" -resolver = "2" - -[package.metadata.docs.rs] -all-features = true -rustdoc-args = [ - "--cfg", - "docsrs", -] - -[features] -__reqwest = ["dep:reqwest"] -auth = [ - "dep:oauth2", - "__reqwest", - "dep:http", - "dep:url", -] -client = ["dep:tokio-stream"] -client-side-sse = [ - "dep:sse-stream", - "dep:http", -] -default = [ - "base64", - "macros", - "server", -] -elicitation = ["dep:url"] -macros = [ - "dep:rmcp-macros", - "dep:pastey", -] -reqwest = [ - "__reqwest", - "reqwest?/rustls-tls", -] -reqwest-native-tls = [ - "__reqwest", - "reqwest?/native-tls", -] -reqwest-tls-no-provider = [ - "__reqwest", - "reqwest?/rustls-tls-no-provider", -] -schemars = ["dep:schemars"] -server = [ - "transport-async-rw", - "dep:schemars", - "dep:pastey", -] -server-side-http = [ - "uuid", - "dep:rand", - "dep:tokio-stream", - "dep:http", - "dep:http-body", - "dep:http-body-util", - "dep:bytes", - "dep:sse-stream", - "dep:axum", - "tower", -] -tower = ["dep:tower-service"] -transport-async-rw = [ - "tokio/io-util", - "tokio-util/codec", -] -transport-child-process = [ - "transport-async-rw", - "tokio/process", - "dep:process-wrap", -] -transport-io = [ - "transport-async-rw", - "tokio/io-std", -] -transport-streamable-http-client = [ - "client-side-sse", - "transport-worker", -] -transport-streamable-http-client-reqwest = [ - "transport-streamable-http-client", - "__reqwest", -] -transport-streamable-http-server = [ - "transport-streamable-http-server-session", - "server-side-http", - "transport-worker", -] -transport-streamable-http-server-session = [ - "transport-async-rw", - "dep:tokio-stream", -] -transport-worker = ["dep:tokio-stream"] - -[lib] -name = "rmcp" -path = "src/lib.rs" - -[[test]] -name = "test_client_initialization" -path = "tests/test_client_initialization.rs" - -[[test]] -name = "test_close_connection" -path = "tests/test_close_connection.rs" -required-features = [ - "server", - "client", -] - -[[test]] -name = "test_completion" -path = "tests/test_completion.rs" - -[[test]] -name = "test_complex_schema" -path = "tests/test_complex_schema.rs" - -[[test]] -name = "test_custom_request" -path = "tests/test_custom_request.rs" -required-features = [ - "server", - "client", -] - -[[test]] -name = "test_deserialization" -path = "tests/test_deserialization.rs" - -[[test]] -name = "test_elicitation" -path = "tests/test_elicitation.rs" -required-features = [ - "elicitation", - "client", - "server", -] - -[[test]] -name = "test_embedded_resource_meta" -path = "tests/test_embedded_resource_meta.rs" - -[[test]] -name = "test_handler_wrappers" -path = "tests/test_handler_wrappers.rs" - -[[test]] -name = "test_json_schema_detection" -path = "tests/test_json_schema_detection.rs" - -[[test]] -name = "test_logging" -path = "tests/test_logging.rs" -required-features = [ - "server", - "client", -] - -[[test]] -name = "test_message_protocol" -path = "tests/test_message_protocol.rs" -required-features = ["client"] - -[[test]] -name = "test_message_schema" -path = "tests/test_message_schema.rs" -required-features = [ - "server", - "client", - "schemars", -] - -[[test]] -name = "test_notification" -path = "tests/test_notification.rs" -required-features = [ - "server", - "client", -] - -[[test]] -name = "test_progress_subscriber" -path = "tests/test_progress_subscriber.rs" -required-features = [ - "server", - "client", - "macros", -] - -[[test]] -name = "test_prompt_handler" -path = "tests/test_prompt_handler.rs" - -[[test]] -name = "test_prompt_macro_annotations" -path = "tests/test_prompt_macro_annotations.rs" - -[[test]] -name = "test_prompt_macros" -path = "tests/test_prompt_macros.rs" -required-features = [ - "server", - "client", -] - -[[test]] -name = "test_prompt_routers" -path = "tests/test_prompt_routers.rs" - -[[test]] -name = "test_resource_link" -path = "tests/test_resource_link.rs" - -[[test]] -name = "test_resource_link_integration" -path = "tests/test_resource_link_integration.rs" - -[[test]] -name = "test_sampling" -path = "tests/test_sampling.rs" -required-features = [ - "server", - "client", -] - -[[test]] -name = "test_streamable_http_priming" -path = "tests/test_streamable_http_priming.rs" -required-features = [ - "server", - "client", - "transport-streamable-http-server", - "reqwest", -] - -[[test]] -name = "test_structured_output" -path = "tests/test_structured_output.rs" - -[[test]] -name = "test_task" -path = "tests/test_task.rs" -required-features = [ - "server", - "client", - "macros", -] - -[[test]] -name = "test_task_support_validation" -path = "tests/test_task_support_validation.rs" - -[[test]] -name = "test_tool_builder_methods" -path = "tests/test_tool_builder_methods.rs" - -[[test]] -name = "test_tool_handler" -path = "tests/test_tool_handler.rs" - -[[test]] -name = "test_tool_macro_annotations" -path = "tests/test_tool_macro_annotations.rs" - -[[test]] -name = "test_tool_macros" -path = "tests/test_tool_macros.rs" -required-features = [ - "server", - "client", -] - -[[test]] -name = "test_tool_result_meta" -path = "tests/test_tool_result_meta.rs" - -[[test]] -name = "test_tool_routers" -path = "tests/test_tool_routers.rs" - -[[test]] -name = "test_with_js" -path = "tests/test_with_js.rs" -required-features = [ - "server", - "client", - "transport-child-process", - "transport-streamable-http-server", - "transport-streamable-http-client", - "__reqwest", -] - -[[test]] -name = "test_with_python" -path = "tests/test_with_python.rs" -required-features = [ - "reqwest", - "server", - "client", - "transport-child-process", -] - -[dependencies.async-trait] -version = "0.1.89" - -[dependencies.axum] -version = "0.8" -features = [] -optional = true - -[dependencies.base64] -version = "0.22" -optional = true - -[dependencies.bytes] -version = "1" -optional = true - -[dependencies.futures] -version = "0.3" - -[dependencies.http] -version = "1" -optional = true - -[dependencies.http-body] -version = "1" -optional = true - -[dependencies.http-body-util] -version = "0.1" -optional = true - -[dependencies.oauth2] -version = "5.0" -features = ["reqwest"] -optional = true -default-features = false - -[dependencies.pastey] -version = "0.2.0" -optional = true - -[dependencies.pin-project-lite] -version = "0.2" - -[dependencies.process-wrap] -version = "9.0" -features = ["tokio1"] -optional = true - -[dependencies.rand] -version = "0.9" -optional = true - -[dependencies.reqwest] -version = "0.12" -features = [ - "json", - "stream", -] -optional = true -default-features = false - -[dependencies.rmcp-macros] -version = "0.15.0" -optional = true - -[dependencies.schemars] -version = "1.0" -features = ["chrono04"] -optional = true - -[dependencies.serde] -version = "1.0" -features = [ - "derive", - "rc", -] - -[dependencies.serde_json] -version = "1.0" - -[dependencies.sse-stream] -version = "0.2" -optional = true - -[dependencies.thiserror] -version = "2" - -[dependencies.tokio] -version = "1" -features = [ - "sync", - "macros", - "rt", - "time", -] - -[dependencies.tokio-stream] -version = "0.1" -optional = true - -[dependencies.tokio-util] -version = "0.7" - -[dependencies.tower-service] -version = "0.3" -optional = true - -[dependencies.tracing] -version = "0.1" - -[dependencies.url] -version = "2.4" -optional = true - -[dependencies.uuid] -version = "1" -features = ["v4"] -optional = true - -[dev-dependencies.anyhow] -version = "1.0" - -[dev-dependencies.async-trait] -version = "0.1" - -[dev-dependencies.schemars] -version = "1.1.0" -features = ["chrono04"] - -[dev-dependencies.tokio] -version = "1" -features = ["full"] - -[dev-dependencies.tracing-subscriber] -version = "0.3" -features = [ - "env-filter", - "std", - "fmt", -] - -[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies.chrono] -version = "0.4.38" -features = [ - "serde", - "clock", - "std", - "oldtime", -] -default-features = false - -[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies.chrono] -version = "0.4.38" -features = ["serde"] diff --git a/codex-rs/vendor/rmcp/README.md b/codex-rs/vendor/rmcp/README.md deleted file mode 100644 index 54b27b9102..0000000000 --- a/codex-rs/vendor/rmcp/README.md +++ /dev/null @@ -1,310 +0,0 @@ - - -
- -# RMCP: Rust Model Context Protocol - -[![Crates.io](https://img.shields.io/crates/v/rmcp.svg)](https://crates.io/crates/rmcp) -[![Documentation](https://docs.rs/rmcp/badge.svg)](https://docs.rs/rmcp) - -
- -`rmcp` is the official Rust implementation of the Model Context Protocol (MCP), a protocol designed for AI assistants to communicate with other services. This library can be used to build both servers that expose capabilities to AI assistants and clients that interact with such servers. - -## Quick Start - -### Server Implementation - -Creating a server with tools is simple using the `#[tool]` macro: - -```rust,no_run -use rmcp::{ - ServerHandler, ServiceExt, - handler::server::tool::ToolRouter, - model::*, - tool, tool_handler, tool_router, - transport::stdio, - ErrorData as McpError, -}; -use std::sync::Arc; -use tokio::sync::Mutex; - -#[derive(Clone)] -pub struct Counter { - counter: Arc>, - tool_router: ToolRouter, -} - -#[tool_router] -impl Counter { - fn new() -> Self { - Self { - counter: Arc::new(Mutex::new(0)), - tool_router: Self::tool_router(), - } - } - - #[tool(description = "Increment the counter by 1")] - async fn increment(&self) -> Result { - let mut counter = self.counter.lock().await; - *counter += 1; - Ok(CallToolResult::success(vec![Content::text( - counter.to_string(), - )])) - } - - #[tool(description = "Get the current counter value")] - async fn get(&self) -> Result { - let counter = self.counter.lock().await; - Ok(CallToolResult::success(vec![Content::text( - counter.to_string(), - )])) - } -} - -// Implement the server handler -#[tool_handler] -impl ServerHandler for Counter { - fn get_info(&self) -> ServerInfo { - ServerInfo { - instructions: Some("A simple counter that tallies the number of times the increment tool has been used".into()), - capabilities: ServerCapabilities::builder().enable_tools().build(), - ..Default::default() - } - } -} - -// Run the server -#[tokio::main] -async fn main() -> Result<(), Box> { - // Create and run the server with STDIO transport - let service = Counter::new().serve(stdio()).await.inspect_err(|e| { - println!("Error starting server: {}", e); - })?; - service.waiting().await?; - Ok(()) -} -``` - -### Structured Output - -Tools can return structured JSON data with schemas. Use the [`Json`] wrapper: - -```rust -# use rmcp::{tool, tool_router, handler::server::{tool::ToolRouter, wrapper::Parameters}, Json}; -# use schemars::JsonSchema; -# use serde::{Serialize, Deserialize}; -# -#[derive(Serialize, Deserialize, JsonSchema)] -struct CalculationRequest { - a: i32, - b: i32, - operation: String, -} - -#[derive(Serialize, Deserialize, JsonSchema)] -struct CalculationResult { - result: i32, - operation: String, -} - -# #[derive(Clone)] -# struct Calculator { -# tool_router: ToolRouter, -# } -# -# #[tool_router] -# impl Calculator { -#[tool(name = "calculate", description = "Perform a calculation")] -async fn calculate(&self, params: Parameters) -> Result, String> { - let result = match params.0.operation.as_str() { - "add" => params.0.a + params.0.b, - "multiply" => params.0.a * params.0.b, - _ => return Err("Unknown operation".to_string()), - }; - - Ok(Json(CalculationResult { result, operation: params.0.operation })) -} -# } -``` - -The `#[tool]` macro automatically generates an output schema from the `CalculationResult` type. - -## Tasks - -RMCP implements the task lifecycle from SEP-1686 so long-running or asynchronous tool calls can be queued and polled safely. - -- **Create:** set the `task` field on `CallToolRequestParam` to ask the server to enqueue the tool call. The response is a `CreateTaskResult` that includes the generated `task.task_id`. -- **Inspect:** use `tasks/get` (`GetTaskInfoRequest`) to retrieve metadata such as status, timestamps, TTL, and poll interval. -- **Await results:** call `tasks/result` (`GetTaskResultRequest`) to block until the task completes and receive either the final `CallToolResult` payload or a protocol error. -- **Cancel:** call `tasks/cancel` (`CancelTaskRequest`) to request termination of a running task. - -To expose task support, enable the `tasks` capability when building `ServerCapabilities`. The `#[task_handler]` macro and `OperationProcessor` utility provide reference implementations for enqueuing, tracking, and collecting task results. - -### Client Implementation - -Creating a client to interact with a server: - -```rust,no_run -use rmcp::{ - ServiceExt, - model::CallToolRequestParams, - transport::{ConfigureCommandExt, TokioChildProcess}, -}; -use tokio::process::Command; - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Connect to a server running as a child process - let service = () - .serve(TokioChildProcess::new(Command::new("uvx").configure( - |cmd| { - cmd.arg("mcp-server-git"); - }, - ))?) - .await?; - - // Get server information - let server_info = service.peer_info(); - println!("Connected to server: {server_info:#?}"); - - // List available tools - let tools = service.list_tools(Default::default()).await?; - println!("Available tools: {tools:#?}"); - - // Call a tool - let result = service - .call_tool(CallToolRequestParams { - meta: None, - name: "git_status".into(), - arguments: serde_json::json!({ "repo_path": "." }).as_object().cloned(), - task: None, - }) - .await?; - println!("Result: {result:#?}"); - - // Gracefully close the connection - service.cancel().await?; - Ok(()) -} -``` - -For more examples, see the [examples directory](https://github.com/anthropics/mcp-rust-sdk/tree/main/examples) in the repository. - -## Transport Options - -RMCP supports multiple transport mechanisms, each suited for different use cases: - -### `transport-async-rw` -Low-level interface for asynchronous read/write operations. This is the foundation for many other transports. - -### `transport-io` -For working directly with I/O streams (`tokio::io::AsyncRead` and `tokio::io::AsyncWrite`). - -### `transport-child-process` -Run MCP servers as child processes and communicate via standard I/O. - -Example: -```rust,ignore -use rmcp::transport::TokioChildProcess; -use tokio::process::Command; - -let transport = TokioChildProcess::new(Command::new("mcp-server"))?; -let service = client.serve(transport).await?; -``` - -## Access with peer interface when handling message - -You can get the [`Peer`](crate::service::Peer) struct from [`NotificationContext`](crate::service::NotificationContext) and [`RequestContext`](crate::service::RequestContext). - -```rust, ignore -# use rmcp::{ -# ServerHandler, -# model::{LoggingLevel, LoggingMessageNotificationParam, ProgressNotificationParam}, -# service::{NotificationContext, RoleServer}, -# }; -# pub struct Handler; - -impl ServerHandler for Handler { - async fn on_progress( - &self, - notification: ProgressNotificationParam, - context: NotificationContext, - ) { - let peer = context.peer; - let _ = peer - .notify_logging_message(LoggingMessageNotificationParam { - level: LoggingLevel::Info, - logger: None, - data: serde_json::json!({ - "message": format!("Progress: {}", notification.progress), - }), - }) - .await; - } -} -``` - - -## Manage Multi Services - -For many cases you need to manage several service in a collection, you can call `into_dyn` to convert services into the same type. -```rust, ignore -let service = service.into_dyn(); -``` - -## Feature Flags - -RMCP uses feature flags to control which components are included: - -- `client`: Enable client functionality -- `server`: Enable server functionality and the tool system -- `macros`: Enable the `#[tool]` macro (enabled by default) -- Transport-specific features: - - `transport-async-rw`: Async read/write support - - `transport-io`: I/O stream support - - `transport-child-process`: Child process support - - `transport-streamable-http-client` / `transport-streamable-http-server`: HTTP streaming (client agnostic, see [`StreamableHttpClientTransport`](crate::transport::StreamableHttpClientTransport) for details) - - `transport-streamable-http-client-reqwest`: a default `reqwest` implementation of the streamable http client -- `auth`: OAuth2 authentication support -- `schemars`: JSON Schema generation (for tool definitions) -- TLS backend options (for HTTP transports): - - `reqwest`: Uses rustls (pure Rust TLS, recommended default) - - `reqwest-native-tls`: Uses platform native TLS (OpenSSL on Linux, Secure Transport on macOS, SChannel on Windows) - - `reqwest-tls-no-provider`: Uses rustls without a default crypto provider (bring your own) - - -## Transports - -- `transport-io`: Server stdio transport -- `transport-child-process`: Client stdio transport -- `transport-streamable-http-server` streamable http server transport -- `transport-streamable-http-client` streamable http client transport - -
-Transport - -The transport type must implement the [`Transport`](crate::transport::Transport) trait, which allows it to send messages concurrently and receive messages sequentially. -There are 2 pairs of standard transport types: - -| transport | client | server | -|:---------------:|:-----------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------:| -| std IO | [`TokioChildProcess`](crate::transport::TokioChildProcess) | [`stdio`](crate::transport::stdio) | -| streamable http | [`StreamableHttpClientTransport`](crate::transport::StreamableHttpClientTransport) | [`StreamableHttpService`](crate::transport::StreamableHttpService) | - -#### [`IntoTransport`](crate::transport::IntoTransport) trait -[`IntoTransport`](crate::transport::IntoTransport) is a helper trait that implicitly converts a type into a transport type. - -These types automatically implement [`IntoTransport`](crate::transport::IntoTransport): -1. A type that implements both `futures::Sink` and `futures::Stream`, or a tuple `(Tx, Rx)` where `Tx` is `futures::Sink` and `Rx` is `futures::Stream`. -2. A type that implements both `tokio::io::AsyncRead` and `tokio::io::AsyncWrite`, or a tuple `(R, W)` where `R` is `tokio::io::AsyncRead` and `W` is `tokio::io::AsyncWrite`. -3. A type that implements the [`Worker`](crate::transport::worker::Worker) trait. -4. A type that implements the [`Transport`](crate::transport::Transport) trait. - -
- -## License - -This project is licensed under the terms specified in the repository's LICENSE file. diff --git a/codex-rs/vendor/rmcp/build.rs b/codex-rs/vendor/rmcp/build.rs deleted file mode 100644 index 05702b405b..0000000000 --- a/codex-rs/vendor/rmcp/build.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Install git hooks on build -fn main() { - // Only run in the workspace root (not when building as a dependency) - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let workspace_root = std::path::Path::new(&manifest_dir) - .parent() - .and_then(|p| p.parent()); - - if let Some(root) = workspace_root { - let githooks_dir = root.join(".githooks"); - let git_dir = root.join(".git"); - - // Only configure if we're in the actual workspace (not a dependency) - // and git directory exists - if githooks_dir.exists() && git_dir.exists() { - // Configure git to use our hooks directory - let _ = std::process::Command::new("git") - .args(["config", "core.hooksPath", ".githooks"]) - .current_dir(root) - .output(); - } - } -} diff --git a/codex-rs/vendor/rmcp/src/error.rs b/codex-rs/vendor/rmcp/src/error.rs deleted file mode 100644 index c7901f4b5e..0000000000 --- a/codex-rs/vendor/rmcp/src/error.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::{borrow::Cow, fmt::Display}; - -pub use crate::model::ErrorData; -#[deprecated( - note = "Use `rmcp::ErrorData` instead, `rmcp::ErrorData` could become `RmcpError` in the future." -)] -pub type Error = ErrorData; -impl Display for ErrorData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}: {}", self.code.0, self.message)?; - if let Some(data) = &self.data { - write!(f, "({})", data)?; - } - Ok(()) - } -} - -impl std::error::Error for ErrorData {} - -/// This is an unified error type for the errors could be returned by the service. -#[derive(Debug, thiserror::Error)] -#[allow(clippy::large_enum_variant)] -pub enum RmcpError { - #[cfg(any(feature = "client", feature = "server"))] - #[error("Service error: {0}")] - Service(#[from] crate::ServiceError), - #[cfg(feature = "client")] - #[error("Client initialization error: {0}")] - ClientInitialize(#[from] crate::service::ClientInitializeError), - #[cfg(feature = "server")] - #[error("Server initialization error: {0}")] - ServerInitialize(#[from] crate::service::ServerInitializeError), - #[error("Runtime error: {0}")] - Runtime(#[from] tokio::task::JoinError), - #[error("Transport creation error: {error}")] - // TODO: Maybe we can introduce something like `TryIntoTransport` to auto wrap transport type, - // but it could be an breaking change, so we could do it in the future. - TransportCreation { - into_transport_type_name: Cow<'static, str>, - into_transport_type_id: std::any::TypeId, - #[source] - error: Box, - }, - // and cancellation shouldn't be an error? - - // TODO: add more error variants as needed - #[error("Task error: {0}")] - TaskError(String), -} - -impl RmcpError { - pub fn transport_creation( - error: impl Into>, - ) -> Self { - RmcpError::TransportCreation { - into_transport_type_id: std::any::TypeId::of::(), - into_transport_type_name: std::any::type_name::().into(), - error: error.into(), - } - } -} diff --git a/codex-rs/vendor/rmcp/src/handler.rs b/codex-rs/vendor/rmcp/src/handler.rs deleted file mode 100644 index c2b9737b8b..0000000000 --- a/codex-rs/vendor/rmcp/src/handler.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[cfg(feature = "client")] -#[cfg_attr(docsrs, doc(cfg(feature = "client")))] -pub mod client; -#[cfg(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -pub mod server; diff --git a/codex-rs/vendor/rmcp/src/handler/client.rs b/codex-rs/vendor/rmcp/src/handler/client.rs deleted file mode 100644 index eeb79309e7..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/client.rs +++ /dev/null @@ -1,376 +0,0 @@ -pub mod progress; -use std::sync::Arc; - -use crate::{ - error::ErrorData as McpError, - model::*, - service::{NotificationContext, RequestContext, RoleClient, Service, ServiceRole}, -}; - -impl Service for H { - async fn handle_request( - &self, - request: ::PeerReq, - context: RequestContext, - ) -> Result<::Resp, McpError> { - match request { - ServerRequest::PingRequest(_) => self.ping(context).await.map(ClientResult::empty), - ServerRequest::CreateMessageRequest(request) => self - .create_message(request.params, context) - .await - .map(Box::new) - .map(ClientResult::CreateMessageResult), - ServerRequest::ListRootsRequest(_) => self - .list_roots(context) - .await - .map(ClientResult::ListRootsResult), - ServerRequest::CreateElicitationRequest(request) => self - .create_elicitation(request.params, context) - .await - .map(ClientResult::CreateElicitationResult), - ServerRequest::CustomRequest(request) => self - .on_custom_request(request, context) - .await - .map(ClientResult::CustomResult), - } - } - - async fn handle_notification( - &self, - notification: ::PeerNot, - context: NotificationContext, - ) -> Result<(), McpError> { - match notification { - ServerNotification::CancelledNotification(notification) => { - self.on_cancelled(notification.params, context).await - } - ServerNotification::ProgressNotification(notification) => { - self.on_progress(notification.params, context).await - } - ServerNotification::LoggingMessageNotification(notification) => { - self.on_logging_message(notification.params, context).await - } - ServerNotification::ResourceUpdatedNotification(notification) => { - self.on_resource_updated(notification.params, context).await - } - ServerNotification::ResourceListChangedNotification(_notification_no_param) => { - self.on_resource_list_changed(context).await - } - ServerNotification::ToolListChangedNotification(_notification_no_param) => { - self.on_tool_list_changed(context).await - } - ServerNotification::PromptListChangedNotification(_notification_no_param) => { - self.on_prompt_list_changed(context).await - } - ServerNotification::ElicitationCompletionNotification(notification) => { - self.on_url_elicitation_notification_complete(notification.params, context) - .await - } - ServerNotification::CustomNotification(notification) => { - self.on_custom_notification(notification, context).await - } - }; - Ok(()) - } - - fn get_info(&self) -> ::Info { - self.get_info() - } -} - -#[allow(unused_variables)] -pub trait ClientHandler: Sized + Send + Sync + 'static { - fn ping( - &self, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Ok(())) - } - - fn create_message( - &self, - params: CreateMessageRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Err( - McpError::method_not_found::(), - )) - } - - fn list_roots( - &self, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Ok(ListRootsResult::default())) - } - - /// Handle an elicitation request from a server asking for user input. - /// - /// This method is called when a server needs interactive input from the user - /// during tool execution. Implementations should present the message to the user, - /// collect their input according to the requested schema, and return the result. - /// - /// # Arguments - /// * `request` - The elicitation request with message and schema - /// * `context` - The request context - /// - /// # Returns - /// The user's response including action (accept/decline/cancel) and optional data - /// - /// # Default Behavior - /// The default implementation automatically declines all elicitation requests. - /// Real clients should override this to provide user interaction. - /// - /// # Example - /// ```rust,ignore - /// use rmcp::model::CreateElicitationRequestParam; - /// use rmcp::{ - /// model::ErrorData as McpError, - /// model::*, - /// service::{NotificationContext, RequestContext, RoleClient, Service, ServiceRole}, - /// }; - /// use rmcp::ClientHandler; - /// - /// impl ClientHandler for MyClient { - /// async fn create_elicitation( - /// &self, - /// request: CreateElicitationRequestParam, - /// context: RequestContext, - /// ) -> Result { - /// match request { - /// CreateElicitationRequestParam::FormElicitationParam {meta, message, requested_schema,} => { - /// // Display message to user and collect input according to requested_schema - /// let user_input = get_user_input(message, requested_schema).await?; - /// Ok(CreateElicitationResult { - /// action: ElicitationAction::Accept, - /// content: Some(user_input), - /// }) - /// } - /// CreateElicitationRequestParam::UrlElicitationParam {meta, message, url, elicitation_id,} => { - /// // Open URL in browser for user to complete elicitation - /// open_url_in_browser(url).await?; - /// Ok(CreateElicitationResult { - /// action: ElicitationAction::Accept, - /// content: None, - /// }) - /// } - /// } - /// } - /// } - /// ``` - fn create_elicitation( - &self, - request: CreateElicitationRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - // Default implementation declines all requests - real clients should override this - let _ = (request, context); - std::future::ready(Ok(CreateElicitationResult { - action: ElicitationAction::Decline, - content: None, - })) - } - - fn on_custom_request( - &self, - request: CustomRequest, - context: RequestContext, - ) -> impl Future> + Send + '_ { - let CustomRequest { method, .. } = request; - let _ = context; - std::future::ready(Err(McpError::new( - ErrorCode::METHOD_NOT_FOUND, - method, - None, - ))) - } - - fn on_cancelled( - &self, - params: CancelledNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - std::future::ready(()) - } - fn on_progress( - &self, - params: ProgressNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - std::future::ready(()) - } - fn on_logging_message( - &self, - params: LoggingMessageNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - std::future::ready(()) - } - fn on_resource_updated( - &self, - params: ResourceUpdatedNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - std::future::ready(()) - } - fn on_resource_list_changed( - &self, - context: NotificationContext, - ) -> impl Future + Send + '_ { - std::future::ready(()) - } - fn on_tool_list_changed( - &self, - context: NotificationContext, - ) -> impl Future + Send + '_ { - std::future::ready(()) - } - fn on_prompt_list_changed( - &self, - context: NotificationContext, - ) -> impl Future + Send + '_ { - std::future::ready(()) - } - - fn on_url_elicitation_notification_complete( - &self, - params: ElicitationResponseNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - std::future::ready(()) - } - fn on_custom_notification( - &self, - notification: CustomNotification, - context: NotificationContext, - ) -> impl Future + Send + '_ { - let _ = (notification, context); - std::future::ready(()) - } - - fn get_info(&self) -> ClientInfo { - ClientInfo::default() - } -} - -/// Do nothing, with default client info. -impl ClientHandler for () {} - -/// Do nothing, with a specific client info. -impl ClientHandler for ClientInfo { - fn get_info(&self) -> ClientInfo { - self.clone() - } -} - -macro_rules! impl_client_handler_for_wrapper { - ($wrapper:ident) => { - impl ClientHandler for $wrapper { - fn ping( - &self, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).ping(context) - } - - fn create_message( - &self, - params: CreateMessageRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).create_message(params, context) - } - - fn list_roots( - &self, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).list_roots(context) - } - - fn create_elicitation( - &self, - request: CreateElicitationRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).create_elicitation(request, context) - } - - fn on_custom_request( - &self, - request: CustomRequest, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).on_custom_request(request, context) - } - - fn on_cancelled( - &self, - params: CancelledNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_cancelled(params, context) - } - - fn on_progress( - &self, - params: ProgressNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_progress(params, context) - } - - fn on_logging_message( - &self, - params: LoggingMessageNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_logging_message(params, context) - } - - fn on_resource_updated( - &self, - params: ResourceUpdatedNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_resource_updated(params, context) - } - - fn on_resource_list_changed( - &self, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_resource_list_changed(context) - } - - fn on_tool_list_changed( - &self, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_tool_list_changed(context) - } - - fn on_prompt_list_changed( - &self, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_prompt_list_changed(context) - } - - fn on_custom_notification( - &self, - notification: CustomNotification, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_custom_notification(notification, context) - } - - fn get_info(&self) -> ClientInfo { - (**self).get_info() - } - } - }; -} - -impl_client_handler_for_wrapper!(Box); -impl_client_handler_for_wrapper!(Arc); diff --git a/codex-rs/vendor/rmcp/src/handler/client/progress.rs b/codex-rs/vendor/rmcp/src/handler/client/progress.rs deleted file mode 100644 index 04f31610ff..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/client/progress.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use futures::{Stream, StreamExt}; -use tokio::sync::RwLock; -use tokio_stream::wrappers::ReceiverStream; - -use crate::model::{ProgressNotificationParam, ProgressToken}; -type Dispatcher = - Arc>>>; - -/// A dispatcher for progress notifications. -#[derive(Debug, Clone, Default)] -pub struct ProgressDispatcher { - pub(crate) dispatcher: Dispatcher, -} - -impl ProgressDispatcher { - const CHANNEL_SIZE: usize = 16; - pub fn new() -> Self { - Self::default() - } - - /// Handle a progress notification by sending it to the appropriate subscriber - pub async fn handle_notification(&self, notification: ProgressNotificationParam) { - let token = ¬ification.progress_token; - if let Some(sender) = self.dispatcher.read().await.get(token).cloned() { - let send_result = sender.send(notification).await; - if let Err(e) = send_result { - tracing::warn!("Failed to send progress notification: {e}"); - } - } - } - - /// Subscribe to progress notifications for a specific token. - /// - /// If you drop the returned `ProgressSubscriber`, it will automatically unsubscribe from notifications for that token. - pub async fn subscribe(&self, progress_token: ProgressToken) -> ProgressSubscriber { - let (sender, receiver) = tokio::sync::mpsc::channel(Self::CHANNEL_SIZE); - self.dispatcher - .write() - .await - .insert(progress_token.clone(), sender); - let receiver = ReceiverStream::new(receiver); - ProgressSubscriber { - progress_token, - receiver, - dispatcher: self.dispatcher.clone(), - } - } - - /// Unsubscribe from progress notifications for a specific token. - pub async fn unsubscribe(&self, token: &ProgressToken) { - self.dispatcher.write().await.remove(token); - } - - /// Clear all dispatcher. - pub async fn clear(&self) { - let mut dispatcher = self.dispatcher.write().await; - dispatcher.clear(); - } -} - -pub struct ProgressSubscriber { - pub(crate) progress_token: ProgressToken, - pub(crate) receiver: ReceiverStream, - pub(crate) dispatcher: Dispatcher, -} - -impl ProgressSubscriber { - pub fn progress_token(&self) -> &ProgressToken { - &self.progress_token - } -} - -impl Stream for ProgressSubscriber { - type Item = ProgressNotificationParam; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.receiver.poll_next_unpin(cx) - } - - fn size_hint(&self) -> (usize, Option) { - self.receiver.size_hint() - } -} - -impl Drop for ProgressSubscriber { - fn drop(&mut self) { - let token = self.progress_token.clone(); - self.receiver.close(); - let dispatcher = self.dispatcher.clone(); - tokio::spawn(async move { - let mut dispatcher = dispatcher.write_owned().await; - dispatcher.remove(&token); - }); - } -} diff --git a/codex-rs/vendor/rmcp/src/handler/server.rs b/codex-rs/vendor/rmcp/src/handler/server.rs deleted file mode 100644 index 86773d8789..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server.rs +++ /dev/null @@ -1,570 +0,0 @@ -use std::sync::Arc; - -use crate::{ - error::ErrorData as McpError, - model::{TaskSupport, *}, - service::{NotificationContext, RequestContext, RoleServer, Service, ServiceRole}, -}; - -pub mod common; -pub mod prompt; -mod resource; -pub mod router; -pub mod tool; -pub mod tool_name_validation; -pub mod wrapper; - -impl Service for H { - async fn handle_request( - &self, - request: ::PeerReq, - context: RequestContext, - ) -> Result<::Resp, McpError> { - match request { - ClientRequest::InitializeRequest(request) => self - .initialize(request.params, context) - .await - .map(ServerResult::InitializeResult), - ClientRequest::PingRequest(_request) => { - self.ping(context).await.map(ServerResult::empty) - } - ClientRequest::CompleteRequest(request) => self - .complete(request.params, context) - .await - .map(ServerResult::CompleteResult), - ClientRequest::SetLevelRequest(request) => self - .set_level(request.params, context) - .await - .map(ServerResult::empty), - ClientRequest::GetPromptRequest(request) => self - .get_prompt(request.params, context) - .await - .map(ServerResult::GetPromptResult), - ClientRequest::ListPromptsRequest(request) => self - .list_prompts(request.params, context) - .await - .map(ServerResult::ListPromptsResult), - ClientRequest::ListResourcesRequest(request) => self - .list_resources(request.params, context) - .await - .map(ServerResult::ListResourcesResult), - ClientRequest::ListResourceTemplatesRequest(request) => self - .list_resource_templates(request.params, context) - .await - .map(ServerResult::ListResourceTemplatesResult), - ClientRequest::ReadResourceRequest(request) => self - .read_resource(request.params, context) - .await - .map(ServerResult::ReadResourceResult), - ClientRequest::SubscribeRequest(request) => self - .subscribe(request.params, context) - .await - .map(ServerResult::empty), - ClientRequest::UnsubscribeRequest(request) => self - .unsubscribe(request.params, context) - .await - .map(ServerResult::empty), - ClientRequest::CallToolRequest(request) => { - let is_task = request.params.task.is_some(); - - // Validate task support mode per MCP specification - if let Some(tool) = self.get_tool(&request.params.name) { - match (tool.task_support(), is_task) { - // If taskSupport is "required", clients MUST invoke the tool as a task. - // Servers MUST return a -32601 (Method not found) error if they don't. - (TaskSupport::Required, false) => { - return Err(McpError::new( - ErrorCode::METHOD_NOT_FOUND, - "Tool requires task-based invocation", - None, - )); - } - // If taskSupport is "forbidden" (default), clients MUST NOT invoke as a task. - (TaskSupport::Forbidden, true) => { - return Err(McpError::invalid_params( - "Tool does not support task-based invocation", - None, - )); - } - _ => {} - } - } - - if is_task { - tracing::info!("Enqueueing task for tool call: {}", request.params.name); - self.enqueue_task(request.params, context.clone()) - .await - .map(ServerResult::CreateTaskResult) - } else { - self.call_tool(request.params, context) - .await - .map(ServerResult::CallToolResult) - } - } - ClientRequest::ListToolsRequest(request) => self - .list_tools(request.params, context) - .await - .map(ServerResult::ListToolsResult), - ClientRequest::CustomRequest(request) => self - .on_custom_request(request, context) - .await - .map(ServerResult::CustomResult), - ClientRequest::ListTasksRequest(request) => self - .list_tasks(request.params, context) - .await - .map(ServerResult::ListTasksResult), - ClientRequest::GetTaskInfoRequest(request) => self - .get_task_info(request.params, context) - .await - .map(ServerResult::GetTaskInfoResult), - ClientRequest::GetTaskResultRequest(request) => self - .get_task_result(request.params, context) - .await - .map(ServerResult::TaskResult), - ClientRequest::CancelTaskRequest(request) => self - .cancel_task(request.params, context) - .await - .map(ServerResult::empty), - } - } - - async fn handle_notification( - &self, - notification: ::PeerNot, - context: NotificationContext, - ) -> Result<(), McpError> { - match notification { - ClientNotification::CancelledNotification(notification) => { - self.on_cancelled(notification.params, context).await - } - ClientNotification::ProgressNotification(notification) => { - self.on_progress(notification.params, context).await - } - ClientNotification::InitializedNotification(_notification) => { - self.on_initialized(context).await - } - ClientNotification::RootsListChangedNotification(_notification) => { - self.on_roots_list_changed(context).await - } - ClientNotification::CustomNotification(notification) => { - self.on_custom_notification(notification, context).await - } - }; - Ok(()) - } - - fn get_info(&self) -> ::Info { - self.get_info() - } -} - -#[allow(unused_variables)] -pub trait ServerHandler: Sized + Send + Sync + 'static { - fn enqueue_task( - &self, - _request: CallToolRequestParams, - _context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Err(McpError::internal_error( - "Task processing not implemented".to_string(), - None, - ))) - } - fn ping( - &self, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Ok(())) - } - // handle requests - fn initialize( - &self, - request: InitializeRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - if context.peer.peer_info().is_none() { - context.peer.set_peer_info(request); - } - std::future::ready(Ok(self.get_info())) - } - fn complete( - &self, - request: CompleteRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Ok(CompleteResult::default())) - } - fn set_level( - &self, - request: SetLevelRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Err(McpError::method_not_found::())) - } - fn get_prompt( - &self, - request: GetPromptRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Err(McpError::method_not_found::())) - } - fn list_prompts( - &self, - request: Option, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Ok(ListPromptsResult::default())) - } - fn list_resources( - &self, - request: Option, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Ok(ListResourcesResult::default())) - } - fn list_resource_templates( - &self, - request: Option, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Ok(ListResourceTemplatesResult::default())) - } - fn read_resource( - &self, - request: ReadResourceRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Err( - McpError::method_not_found::(), - )) - } - fn subscribe( - &self, - request: SubscribeRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Err(McpError::method_not_found::())) - } - fn unsubscribe( - &self, - request: UnsubscribeRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Err(McpError::method_not_found::())) - } - fn call_tool( - &self, - request: CallToolRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Err(McpError::method_not_found::())) - } - fn list_tools( - &self, - request: Option, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Ok(ListToolsResult::default())) - } - /// Get a tool definition by name. - /// - /// The default implementation returns `None`, which bypasses validation. - /// When using `#[tool_handler]`, this method is automatically implemented. - fn get_tool(&self, _name: &str) -> Option { - None - } - fn on_custom_request( - &self, - request: CustomRequest, - context: RequestContext, - ) -> impl Future> + Send + '_ { - let CustomRequest { method, .. } = request; - let _ = context; - std::future::ready(Err(McpError::new( - ErrorCode::METHOD_NOT_FOUND, - method, - None, - ))) - } - - fn on_cancelled( - &self, - notification: CancelledNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - std::future::ready(()) - } - fn on_progress( - &self, - notification: ProgressNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - std::future::ready(()) - } - fn on_initialized( - &self, - context: NotificationContext, - ) -> impl Future + Send + '_ { - tracing::info!("client initialized"); - std::future::ready(()) - } - fn on_roots_list_changed( - &self, - context: NotificationContext, - ) -> impl Future + Send + '_ { - std::future::ready(()) - } - fn on_custom_notification( - &self, - notification: CustomNotification, - context: NotificationContext, - ) -> impl Future + Send + '_ { - let _ = (notification, context); - std::future::ready(()) - } - - fn get_info(&self) -> ServerInfo { - ServerInfo::default() - } - - fn list_tasks( - &self, - request: Option, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Err(McpError::method_not_found::())) - } - - fn get_task_info( - &self, - request: GetTaskInfoParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Err(McpError::method_not_found::())) - } - - fn get_task_result( - &self, - request: GetTaskResultParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - let _ = (request, context); - std::future::ready(Err(McpError::method_not_found::())) - } - - fn cancel_task( - &self, - request: CancelTaskParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - let _ = (request, context); - std::future::ready(Err(McpError::method_not_found::())) - } -} - -macro_rules! impl_server_handler_for_wrapper { - ($wrapper:ident) => { - impl ServerHandler for $wrapper { - fn enqueue_task( - &self, - request: CallToolRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).enqueue_task(request, context) - } - - fn ping( - &self, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).ping(context) - } - - fn initialize( - &self, - request: InitializeRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).initialize(request, context) - } - - fn complete( - &self, - request: CompleteRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).complete(request, context) - } - - fn set_level( - &self, - request: SetLevelRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).set_level(request, context) - } - - fn get_prompt( - &self, - request: GetPromptRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).get_prompt(request, context) - } - - fn list_prompts( - &self, - request: Option, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).list_prompts(request, context) - } - - fn list_resources( - &self, - request: Option, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).list_resources(request, context) - } - - fn list_resource_templates( - &self, - request: Option, - context: RequestContext, - ) -> impl Future> + Send + '_ - { - (**self).list_resource_templates(request, context) - } - - fn read_resource( - &self, - request: ReadResourceRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).read_resource(request, context) - } - - fn subscribe( - &self, - request: SubscribeRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).subscribe(request, context) - } - - fn unsubscribe( - &self, - request: UnsubscribeRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).unsubscribe(request, context) - } - - fn call_tool( - &self, - request: CallToolRequestParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).call_tool(request, context) - } - - fn list_tools( - &self, - request: Option, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).list_tools(request, context) - } - - fn get_tool(&self, name: &str) -> Option { - (**self).get_tool(name) - } - - fn on_custom_request( - &self, - request: CustomRequest, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).on_custom_request(request, context) - } - - fn on_cancelled( - &self, - notification: CancelledNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_cancelled(notification, context) - } - - fn on_progress( - &self, - notification: ProgressNotificationParam, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_progress(notification, context) - } - - fn on_initialized( - &self, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_initialized(context) - } - - fn on_roots_list_changed( - &self, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_roots_list_changed(context) - } - - fn on_custom_notification( - &self, - notification: CustomNotification, - context: NotificationContext, - ) -> impl Future + Send + '_ { - (**self).on_custom_notification(notification, context) - } - - fn get_info(&self) -> ServerInfo { - (**self).get_info() - } - - fn list_tasks( - &self, - request: Option, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).list_tasks(request, context) - } - - fn get_task_info( - &self, - request: GetTaskInfoParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).get_task_info(request, context) - } - - fn get_task_result( - &self, - request: GetTaskResultParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).get_task_result(request, context) - } - - fn cancel_task( - &self, - request: CancelTaskParams, - context: RequestContext, - ) -> impl Future> + Send + '_ { - (**self).cancel_task(request, context) - } - } - }; -} - -impl_server_handler_for_wrapper!(Box); -impl_server_handler_for_wrapper!(Arc); diff --git a/codex-rs/vendor/rmcp/src/handler/server/common.rs b/codex-rs/vendor/rmcp/src/handler/server/common.rs deleted file mode 100644 index 4f344a86f1..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server/common.rs +++ /dev/null @@ -1,271 +0,0 @@ -//! Common utilities shared between tool and prompt handlers - -use std::{any::TypeId, collections::HashMap, sync::Arc}; - -use schemars::JsonSchema; - -use crate::{ - RoleServer, model::JsonObject, schemars::generate::SchemaSettings, service::RequestContext, -}; - -/// Generates a JSON schema for a type -pub fn schema_for_type() -> Arc { - thread_local! { - static CACHE_FOR_TYPE: std::sync::RwLock>> = Default::default(); - }; - CACHE_FOR_TYPE.with(|cache| { - if let Some(x) = cache - .read() - .expect("schema cache lock poisoned") - .get(&TypeId::of::()) - { - x.clone() - } else { - // explicitly to align json schema version to official specifications. - // refer to https://github.com/modelcontextprotocol/modelcontextprotocol/pull/655 for details. - let mut settings = SchemaSettings::draft2020_12(); - settings.transforms = vec![Box::new(schemars::transform::AddNullable::default())]; - let generator = settings.into_generator(); - let schema = generator.into_root_schema_for::(); - let object = serde_json::to_value(schema).expect("failed to serialize schema"); - let object = match object { - serde_json::Value::Object(object) => object, - _ => panic!( - "Schema serialization produced non-object value: expected JSON object but got {:?}", - object - ), - }; - let schema = Arc::new(object); - cache - .write() - .expect("schema cache lock poisoned") - .insert(TypeId::of::(), schema.clone()); - - schema - } - }) -} - -/// Generate and validate a JSON schema for outputSchema (must have root type "object"). -pub fn schema_for_output() -> Result, String> { - thread_local! { - static CACHE_FOR_OUTPUT: std::sync::RwLock, String>>> = Default::default(); - }; - - CACHE_FOR_OUTPUT.with(|cache| { - // Try to get from cache first - if let Some(result) = cache - .read() - .expect("output schema cache lock poisoned") - .get(&TypeId::of::()) - { - return result.clone(); - } - - // Generate and validate schema - let schema = schema_for_type::(); - let result = match schema.get("type") { - Some(serde_json::Value::String(t)) if t == "object" => Ok(schema.clone()), - Some(serde_json::Value::String(t)) => Err(format!( - "MCP specification requires tool outputSchema to have root type 'object', but found '{}'.", - t - )), - None => Err( - "Schema is missing 'type' field. MCP specification requires outputSchema to have root type 'object'.".to_string() - ), - Some(other) => Err(format!( - "Schema 'type' field has unexpected format: {:?}. Expected \"object\".", - other - )), - }; - - // Cache the result (both success and error cases) - cache - .write() - .expect("output schema cache lock poisoned") - .insert(TypeId::of::(), result.clone()); - - result - }) -} - -/// Trait for extracting parts from a context, unifying tool and prompt extraction -pub trait FromContextPart: Sized { - fn from_context_part(context: &mut C) -> Result; -} - -/// Common extractors that can be used by both tool and prompt handlers -impl FromContextPart for RequestContext -where - C: AsRequestContext, -{ - fn from_context_part(context: &mut C) -> Result { - Ok(context.as_request_context().clone()) - } -} - -impl FromContextPart for tokio_util::sync::CancellationToken -where - C: AsRequestContext, -{ - fn from_context_part(context: &mut C) -> Result { - Ok(context.as_request_context().ct.clone()) - } -} - -impl FromContextPart for crate::model::Extensions -where - C: AsRequestContext, -{ - fn from_context_part(context: &mut C) -> Result { - Ok(context.as_request_context().extensions.clone()) - } -} - -pub struct Extension(pub T); - -impl FromContextPart for Extension -where - C: AsRequestContext, - T: Send + Sync + 'static + Clone, -{ - fn from_context_part(context: &mut C) -> Result { - let extension = context - .as_request_context() - .extensions - .get::() - .cloned() - .ok_or_else(|| { - crate::ErrorData::invalid_params( - format!("missing extension {}", std::any::type_name::()), - None, - ) - })?; - Ok(Extension(extension)) - } -} - -impl FromContextPart for crate::Peer -where - C: AsRequestContext, -{ - fn from_context_part(context: &mut C) -> Result { - Ok(context.as_request_context().peer.clone()) - } -} - -impl FromContextPart for crate::model::Meta -where - C: AsRequestContext, -{ - fn from_context_part(context: &mut C) -> Result { - let request_context = context.as_request_context_mut(); - let mut meta = crate::model::Meta::default(); - std::mem::swap(&mut meta, &mut request_context.meta); - Ok(meta) - } -} - -pub struct RequestId(pub crate::model::RequestId); - -impl FromContextPart for RequestId -where - C: AsRequestContext, -{ - fn from_context_part(context: &mut C) -> Result { - Ok(RequestId(context.as_request_context().id.clone())) - } -} - -/// Trait for types that can provide access to RequestContext -pub trait AsRequestContext { - fn as_request_context(&self) -> &RequestContext; - fn as_request_context_mut(&mut self) -> &mut RequestContext; -} - -#[cfg(test)] -mod tests { - use super::*; - - #[derive(serde::Serialize, serde::Deserialize, JsonSchema)] - struct TestObject { - value: i32, - } - - #[derive(serde::Serialize, serde::Deserialize, JsonSchema)] - struct AnotherTestObject { - value: i32, - } - - #[test] - fn test_schema_for_type_handles_primitive() { - let schema = schema_for_type::(); - - assert_eq!(schema.get("type"), Some(&serde_json::json!("integer"))); - } - - #[test] - fn test_schema_for_type_handles_array() { - let schema = schema_for_type::>(); - - assert_eq!(schema.get("type"), Some(&serde_json::json!("array"))); - let items = schema.get("items").and_then(|v| v.as_object()); - assert_eq!( - items.unwrap().get("type"), - Some(&serde_json::json!("integer")) - ); - } - - #[test] - fn test_schema_for_type_handles_struct() { - let schema = schema_for_type::(); - - assert_eq!(schema.get("type"), Some(&serde_json::json!("object"))); - let properties = schema.get("properties").and_then(|v| v.as_object()); - assert!(properties.unwrap().contains_key("value")); - } - - #[test] - fn test_schema_for_type_caches_primitive_types() { - let schema1 = schema_for_type::(); - let schema2 = schema_for_type::(); - - assert!(Arc::ptr_eq(&schema1, &schema2)); - } - - #[test] - fn test_schema_for_type_caches_struct_types() { - let schema1 = schema_for_type::(); - let schema2 = schema_for_type::(); - - assert!(Arc::ptr_eq(&schema1, &schema2)); - } - - #[test] - fn test_schema_for_type_different_types_different_schemas() { - let schema1 = schema_for_type::(); - let schema2 = schema_for_type::(); - - assert!(!Arc::ptr_eq(&schema1, &schema2)); - } - - #[test] - fn test_schema_for_type_arc_can_be_shared() { - let schema = schema_for_type::(); - let cloned = schema.clone(); - - assert!(Arc::ptr_eq(&schema, &cloned)); - } - - #[test] - fn test_schema_for_output_rejects_primitive() { - let result = schema_for_output::(); - assert!(result.is_err(),); - } - - #[test] - fn test_schema_for_output_accepts_object() { - let result = schema_for_output::(); - assert!(result.is_ok(),); - } -} diff --git a/codex-rs/vendor/rmcp/src/handler/server/prompt.rs b/codex-rs/vendor/rmcp/src/handler/server/prompt.rs deleted file mode 100644 index 826bee0dfd..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server/prompt.rs +++ /dev/null @@ -1,367 +0,0 @@ -//! Prompt handling infrastructure for MCP servers -//! -//! This module provides the core types and traits for implementing prompt handlers -//! in MCP servers. Prompts allow servers to provide reusable templates for LLM -//! interactions with customizable arguments. - -use std::{future::Future, marker::PhantomData}; - -use futures::future::{BoxFuture, FutureExt}; -use serde::de::DeserializeOwned; - -use super::common::{AsRequestContext, FromContextPart}; -pub use super::common::{Extension, RequestId}; -use crate::{ - RoleServer, - handler::server::wrapper::Parameters, - model::{GetPromptResult, PromptMessage}, - service::RequestContext, -}; - -/// Context for prompt retrieval operations -pub struct PromptContext<'a, S> { - pub server: &'a S, - pub name: String, - pub arguments: Option>, - pub context: RequestContext, -} - -impl<'a, S> PromptContext<'a, S> { - pub fn new( - server: &'a S, - name: String, - arguments: Option>, - context: RequestContext, - ) -> Self { - Self { - server, - name, - arguments, - context, - } - } -} - -impl AsRequestContext for PromptContext<'_, S> { - fn as_request_context(&self) -> &RequestContext { - &self.context - } - - fn as_request_context_mut(&mut self) -> &mut RequestContext { - &mut self.context - } -} - -/// Trait for handling prompt retrieval -pub trait GetPromptHandler { - fn handle( - self, - context: PromptContext<'_, S>, - ) -> BoxFuture<'_, Result>; -} - -/// Type alias for dynamic prompt handlers -pub type DynGetPromptHandler = dyn for<'a> Fn(PromptContext<'a, S>) -> BoxFuture<'a, Result> - + Send - + Sync; - -/// Adapter type for async methods that return `Vec` -pub struct AsyncMethodAdapter(PhantomData); - -/// Adapter type for async methods with parameters that return `Vec` -pub struct AsyncMethodWithArgsAdapter(PhantomData); - -/// Adapter types for macro-generated implementations -#[allow(clippy::type_complexity)] -pub struct AsyncPromptAdapter(PhantomData fn(Fut) -> R>); -pub struct SyncPromptAdapter(PhantomData R>); -pub struct AsyncPromptMethodAdapter(PhantomData R>); -pub struct SyncPromptMethodAdapter(PhantomData R>); - -/// Trait for types that can be converted into GetPromptResult -pub trait IntoGetPromptResult { - fn into_get_prompt_result(self) -> Result; -} - -impl IntoGetPromptResult for GetPromptResult { - fn into_get_prompt_result(self) -> Result { - Ok(self) - } -} - -impl IntoGetPromptResult for Vec { - fn into_get_prompt_result(self) -> Result { - Ok(GetPromptResult { - description: None, - messages: self, - }) - } -} - -impl IntoGetPromptResult for Result { - fn into_get_prompt_result(self) -> Result { - self.and_then(|v| v.into_get_prompt_result()) - } -} - -// Future wrapper that automatically handles IntoGetPromptResult conversion -pin_project_lite::pin_project! { - #[project = IntoGetPromptResultFutProj] - pub enum IntoGetPromptResultFut { - Pending { - #[pin] - fut: F, - _marker: PhantomData, - }, - Ready { - #[pin] - result: futures::future::Ready>, - } - } -} - -impl Future for IntoGetPromptResultFut -where - F: Future, - R: IntoGetPromptResult, -{ - type Output = Result; - - fn poll( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - match self.project() { - IntoGetPromptResultFutProj::Pending { fut, _marker } => fut - .poll(cx) - .map(IntoGetPromptResult::into_get_prompt_result), - IntoGetPromptResultFutProj::Ready { result } => result.poll(cx), - } - } -} - -// Prompt-specific extractor for prompt name -pub struct PromptName(pub String); - -impl FromContextPart> for PromptName { - fn from_context_part(context: &mut PromptContext) -> Result { - Ok(Self(context.name.clone())) - } -} - -// Special implementation for Parameters that handles prompt arguments -impl FromContextPart> for Parameters

-where - P: DeserializeOwned, -{ - fn from_context_part(context: &mut PromptContext) -> Result { - let params = if let Some(args_map) = context.arguments.take() { - let args_value = serde_json::Value::Object(args_map); - serde_json::from_value::

(args_value).map_err(|e| { - crate::ErrorData::invalid_params(format!("Failed to parse parameters: {}", e), None) - })? - } else { - // Try to deserialize from empty object for optional fields - serde_json::from_value::

(serde_json::json!({})).map_err(|e| { - crate::ErrorData::invalid_params( - format!("Missing required parameters: {}", e), - None, - ) - })? - }; - Ok(Parameters(params)) - } -} - -// Macro to generate GetPromptHandler implementations for various parameter combinations -macro_rules! impl_prompt_handler_for { - ($($T: ident)*) => { - impl_prompt_handler_for!([] [$($T)*]); - }; - // finished - ([$($Tn: ident)*] []) => { - impl_prompt_handler_for!(@impl $($Tn)*); - }; - ([$($Tn: ident)*] [$Tn_1: ident $($Rest: ident)*]) => { - impl_prompt_handler_for!(@impl $($Tn)*); - impl_prompt_handler_for!([$($Tn)* $Tn_1] [$($Rest)*]); - }; - (@impl $($Tn: ident)*) => { - // Implementation for async methods (transformed by #[prompt] macro) - impl<$($Tn,)* S, F, R> GetPromptHandler for F - where - $( - $Tn: for<'a> FromContextPart> + Send, - )* - F: FnOnce(&S, $($Tn,)*) -> BoxFuture<'_, R> + Send, - R: IntoGetPromptResult + Send + 'static, - S: Send + Sync + 'static, - { - #[allow(unused_variables, non_snake_case, unused_mut)] - fn handle( - self, - mut context: PromptContext<'_, S>, - ) -> BoxFuture<'_, Result> - { - $( - let result = $Tn::from_context_part(&mut context); - let $Tn = match result { - Ok(value) => value, - Err(e) => return std::future::ready(Err(e)).boxed(), - }; - )* - let service = context.server; - let fut = self(service, $($Tn,)*); - async move { - let result = fut.await; - result.into_get_prompt_result() - }.boxed() - } - } - - - // Implementation for sync methods - impl<$($Tn,)* S, F, R> GetPromptHandler> for F - where - $( - $Tn: for<'a> FromContextPart> + Send, - )* - F: FnOnce(&S, $($Tn,)*) -> R + Send, - R: IntoGetPromptResult + Send, - S: Send + Sync, - { - #[allow(unused_variables, non_snake_case, unused_mut)] - fn handle( - self, - mut context: PromptContext<'_, S>, - ) -> BoxFuture<'_, Result> - { - $( - let result = $Tn::from_context_part(&mut context); - let $Tn = match result { - Ok(value) => value, - Err(e) => return std::future::ready(Err(e)).boxed(), - }; - )* - let service = context.server; - let result = self(service, $($Tn,)*); - std::future::ready(result.into_get_prompt_result()).boxed() - } - } - - - // AsyncPromptAdapter - for standalone functions returning GetPromptResult - impl<$($Tn,)* S, F, Fut, R> GetPromptHandler> for F - where - $( - $Tn: for<'a> FromContextPart> + Send + 'static, - )* - F: FnOnce($($Tn,)*) -> Fut + Send + 'static, - Fut: Future> + Send + 'static, - R: IntoGetPromptResult + Send + 'static, - S: Send + Sync + 'static, - { - #[allow(unused_variables, non_snake_case, unused_mut)] - fn handle( - self, - mut context: PromptContext<'_, S>, - ) -> BoxFuture<'_, Result> - { - // Extract all parameters before moving into the async block - $( - let result = $Tn::from_context_part(&mut context); - let $Tn = match result { - Ok(value) => value, - Err(e) => return std::future::ready(Err(e)).boxed(), - }; - )* - - // Since we're dealing with standalone functions that don't take &S, - // we can return a 'static future - Box::pin(async move { - let result = self($($Tn,)*).await?; - result.into_get_prompt_result() - }) - } - } - - - // SyncPromptAdapter - for standalone sync functions returning Result - impl<$($Tn,)* S, F, R> GetPromptHandler> for F - where - $( - $Tn: for<'a> FromContextPart> + Send + 'static, - )* - F: FnOnce($($Tn,)*) -> Result + Send + 'static, - R: IntoGetPromptResult + Send + 'static, - S: Send + Sync, - { - #[allow(unused_variables, non_snake_case, unused_mut)] - fn handle( - self, - mut context: PromptContext<'_, S>, - ) -> BoxFuture<'_, Result> - { - $( - let result = $Tn::from_context_part(&mut context); - let $Tn = match result { - Ok(value) => value, - Err(e) => return std::future::ready(Err(e)).boxed(), - }; - )* - let result = self($($Tn,)*); - std::future::ready(result.and_then(|r| r.into_get_prompt_result())).boxed() - } - } - - }; -} - -// Invoke the macro to generate implementations for up to 16 parameters -impl_prompt_handler_for!(T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14 T15); - -/// Extract prompt arguments from a type's JSON schema -/// This function analyzes the schema of a type and extracts the properties -/// as PromptArgument entries with name, description, and required status -pub fn cached_arguments_from_schema() --> Option> { - let schema = super::common::schema_for_type::(); - let schema_value = serde_json::Value::Object((*schema).clone()); - - let properties = schema_value.get("properties").and_then(|p| p.as_object()); - - if let Some(props) = properties { - let required = schema_value - .get("required") - .and_then(|r| r.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str()) - .collect::>() - }) - .unwrap_or_default(); - - let mut arguments = Vec::new(); - for (name, prop_schema) in props { - let description = prop_schema - .get("description") - .and_then(|d| d.as_str()) - .map(|s| s.to_string()); - - arguments.push(crate::model::PromptArgument { - name: name.clone(), - title: None, - description, - required: Some(required.contains(name.as_str())), - }); - } - - if arguments.is_empty() { - None - } else { - Some(arguments) - } - } else { - None - } -} diff --git a/codex-rs/vendor/rmcp/src/handler/server/resource.rs b/codex-rs/vendor/rmcp/src/handler/server/resource.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server/resource.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/codex-rs/vendor/rmcp/src/handler/server/router.rs b/codex-rs/vendor/rmcp/src/handler/server/router.rs deleted file mode 100644 index 1f34ba5b2a..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server/router.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::sync::Arc; - -use prompt::{IntoPromptRoute, PromptRoute}; -use tool::{IntoToolRoute, ToolRoute}; - -use super::ServerHandler; -use crate::{ - RoleServer, Service, - model::{ClientRequest, ListPromptsResult, ListToolsResult, ServerResult}, - service::NotificationContext, -}; - -pub mod prompt; -pub mod tool; - -pub struct Router { - pub tool_router: tool::ToolRouter, - pub prompt_router: prompt::PromptRouter, - pub service: Arc, -} - -impl Router -where - S: ServerHandler, -{ - pub fn new(service: S) -> Self { - Self { - tool_router: tool::ToolRouter::new(), - prompt_router: prompt::PromptRouter::new(), - service: Arc::new(service), - } - } - - pub fn with_tool(mut self, route: R) -> Self - where - R: IntoToolRoute, - { - self.tool_router.add_route(route.into_tool_route()); - self - } - - pub fn with_tools(mut self, routes: impl IntoIterator>) -> Self { - for route in routes { - self.tool_router.add_route(route); - } - self - } - - pub fn with_prompt(mut self, route: R) -> Self - where - R: IntoPromptRoute, - { - self.prompt_router.add_route(route.into_prompt_route()); - self - } - - pub fn with_prompts(mut self, routes: impl IntoIterator>) -> Self { - for route in routes { - self.prompt_router.add_route(route); - } - self - } -} - -impl Service for Router -where - S: ServerHandler, -{ - async fn handle_notification( - &self, - notification: ::PeerNot, - context: NotificationContext, - ) -> Result<(), crate::ErrorData> { - self.service - .handle_notification(notification, context) - .await - } - async fn handle_request( - &self, - request: ::PeerReq, - context: crate::service::RequestContext, - ) -> Result<::Resp, crate::ErrorData> { - match request { - ClientRequest::CallToolRequest(request) => { - if self.tool_router.has_route(request.params.name.as_ref()) - || !self.tool_router.transparent_when_not_found - { - let tool_call_context = crate::handler::server::tool::ToolCallContext::new( - self.service.as_ref(), - request.params, - context, - ); - let result = self.tool_router.call(tool_call_context).await?; - Ok(ServerResult::CallToolResult(result)) - } else { - self.service - .handle_request(ClientRequest::CallToolRequest(request), context) - .await - } - } - ClientRequest::ListToolsRequest(_) => { - let tools = self.tool_router.list_all(); - Ok(ServerResult::ListToolsResult(ListToolsResult { - tools, - ..Default::default() - })) - } - ClientRequest::GetPromptRequest(request) => { - if self.prompt_router.has_route(request.params.name.as_ref()) { - let prompt_context = crate::handler::server::prompt::PromptContext::new( - self.service.as_ref(), - request.params.name, - request.params.arguments, - context, - ); - let result = self.prompt_router.get_prompt(prompt_context).await?; - Ok(ServerResult::GetPromptResult(result)) - } else { - self.service - .handle_request(ClientRequest::GetPromptRequest(request), context) - .await - } - } - ClientRequest::ListPromptsRequest(_) => { - let prompts = self.prompt_router.list_all(); - Ok(ServerResult::ListPromptsResult(ListPromptsResult { - prompts, - ..Default::default() - })) - } - rest => self.service.handle_request(rest, context).await, - } - } - - fn get_info(&self) -> ::Info { - ServerHandler::get_info(&self.service) - } -} diff --git a/codex-rs/vendor/rmcp/src/handler/server/router/prompt.rs b/codex-rs/vendor/rmcp/src/handler/server/router/prompt.rs deleted file mode 100644 index a48d2ad7db..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server/router/prompt.rs +++ /dev/null @@ -1,213 +0,0 @@ -use std::{borrow::Cow, sync::Arc}; - -use futures::future::BoxFuture; - -use crate::{ - handler::server::prompt::{DynGetPromptHandler, GetPromptHandler, PromptContext}, - model::{GetPromptResult, Prompt}, -}; - -pub struct PromptRoute { - #[allow(clippy::type_complexity)] - pub get: Arc>, - pub attr: crate::model::Prompt, -} - -impl std::fmt::Debug for PromptRoute { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PromptRoute") - .field("name", &self.attr.name) - .field("description", &self.attr.description) - .field("arguments", &self.attr.arguments) - .finish() - } -} - -impl Clone for PromptRoute { - fn clone(&self) -> Self { - Self { - get: self.get.clone(), - attr: self.attr.clone(), - } - } -} - -impl PromptRoute { - pub fn new(attr: impl Into, handler: H) -> Self - where - H: GetPromptHandler + Send + Sync + Clone + 'static, - { - Self { - get: Arc::new(move |context: PromptContext| { - let handler = handler.clone(); - handler.handle(context) - }), - attr: attr.into(), - } - } - - pub fn new_dyn(attr: impl Into, handler: H) -> Self - where - H: for<'a> Fn( - PromptContext<'a, S>, - ) -> BoxFuture<'a, Result> - + Send - + Sync - + 'static, - { - Self { - get: Arc::new(handler), - attr: attr.into(), - } - } - - pub fn name(&self) -> &str { - &self.attr.name - } -} - -pub trait IntoPromptRoute { - fn into_prompt_route(self) -> PromptRoute; -} - -impl IntoPromptRoute for (P, H) -where - S: Send + Sync + 'static, - A: 'static, - H: GetPromptHandler + Send + Sync + Clone + 'static, - P: Into, -{ - fn into_prompt_route(self) -> PromptRoute { - PromptRoute::new(self.0.into(), self.1) - } -} - -impl IntoPromptRoute for PromptRoute -where - S: Send + Sync + 'static, -{ - fn into_prompt_route(self) -> PromptRoute { - self - } -} - -/// Adapter for functions generated by the #\[prompt\] macro -pub struct PromptAttrGenerateFunctionAdapter; - -impl IntoPromptRoute for F -where - S: Send + Sync + 'static, - F: Fn() -> PromptRoute, -{ - fn into_prompt_route(self) -> PromptRoute { - (self)() - } -} - -#[derive(Debug)] -pub struct PromptRouter { - #[allow(clippy::type_complexity)] - pub map: std::collections::HashMap, PromptRoute>, -} - -impl Default for PromptRouter { - fn default() -> Self { - Self { - map: std::collections::HashMap::new(), - } - } -} - -impl Clone for PromptRouter { - fn clone(&self) -> Self { - Self { - map: self.map.clone(), - } - } -} - -impl IntoIterator for PromptRouter { - type Item = PromptRoute; - type IntoIter = std::collections::hash_map::IntoValues, PromptRoute>; - - fn into_iter(self) -> Self::IntoIter { - self.map.into_values() - } -} - -impl PromptRouter -where - S: Send + Sync + 'static, -{ - pub fn new() -> Self { - Self { - map: std::collections::HashMap::new(), - } - } - - pub fn with_route(mut self, route: R) -> Self - where - R: IntoPromptRoute, - { - self.add_route(route.into_prompt_route()); - self - } - - pub fn add_route(&mut self, item: PromptRoute) { - self.map.insert(item.attr.name.clone().into(), item); - } - - pub fn merge(&mut self, other: PromptRouter) { - for item in other.map.into_values() { - self.add_route(item); - } - } - - pub fn remove_route(&mut self, name: &str) { - self.map.remove(name); - } - - pub fn has_route(&self, name: &str) -> bool { - self.map.contains_key(name) - } - - pub async fn get_prompt( - &self, - context: PromptContext<'_, S>, - ) -> Result { - let item = self.map.get(context.name.as_str()).ok_or_else(|| { - crate::ErrorData::invalid_params( - format!("prompt '{}' not found", context.name), - Some(serde_json::json!({ - "available_prompts": self.list_all().iter().map(|p| &p.name).collect::>() - })), - ) - })?; - (item.get)(context).await - } - - pub fn list_all(&self) -> Vec { - self.map.values().map(|item| item.attr.clone()).collect() - } -} - -impl std::ops::Add> for PromptRouter -where - S: Send + Sync + 'static, -{ - type Output = Self; - - fn add(mut self, other: PromptRouter) -> Self::Output { - self.merge(other); - self - } -} - -impl std::ops::AddAssign> for PromptRouter -where - S: Send + Sync + 'static, -{ - fn add_assign(&mut self, other: PromptRouter) { - self.merge(other); - } -} diff --git a/codex-rs/vendor/rmcp/src/handler/server/router/tool.rs b/codex-rs/vendor/rmcp/src/handler/server/router/tool.rs deleted file mode 100644 index 72b7f2e26c..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server/router/tool.rs +++ /dev/null @@ -1,285 +0,0 @@ -use std::{borrow::Cow, sync::Arc}; - -use futures::{FutureExt, future::BoxFuture}; -use schemars::JsonSchema; - -use crate::{ - handler::server::{ - tool::{CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type}, - tool_name_validation::validate_and_warn_tool_name, - }, - model::{CallToolResult, Tool, ToolAnnotations}, -}; - -pub struct ToolRoute { - #[allow(clippy::type_complexity)] - pub call: Arc>, - pub attr: crate::model::Tool, -} - -impl std::fmt::Debug for ToolRoute { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ToolRoute") - .field("name", &self.attr.name) - .field("description", &self.attr.description) - .field("input_schema", &self.attr.input_schema) - .finish() - } -} - -impl Clone for ToolRoute { - fn clone(&self) -> Self { - Self { - call: self.call.clone(), - attr: self.attr.clone(), - } - } -} - -impl ToolRoute { - pub fn new(attr: impl Into, call: C) -> Self - where - C: CallToolHandler + Send + Sync + Clone + 'static, - { - Self { - call: Arc::new(move |context: ToolCallContext| { - let call = call.clone(); - context.invoke(call).boxed() - }), - attr: attr.into(), - } - } - pub fn new_dyn(attr: impl Into, call: C) -> Self - where - C: for<'a> Fn( - ToolCallContext<'a, S>, - ) -> BoxFuture<'a, Result> - + Send - + Sync - + 'static, - { - Self { - call: Arc::new(call), - attr: attr.into(), - } - } - pub fn name(&self) -> &str { - &self.attr.name - } -} - -pub trait IntoToolRoute { - fn into_tool_route(self) -> ToolRoute; -} - -impl IntoToolRoute for (T, C) -where - S: Send + Sync + 'static, - C: CallToolHandler + Send + Sync + Clone + 'static, - T: Into, -{ - fn into_tool_route(self) -> ToolRoute { - ToolRoute::new(self.0.into(), self.1) - } -} - -impl IntoToolRoute for ToolRoute -where - S: Send + Sync + 'static, -{ - fn into_tool_route(self) -> ToolRoute { - self - } -} - -pub struct ToolAttrGenerateFunctionAdapter; -impl IntoToolRoute for F -where - S: Send + Sync + 'static, - F: Fn() -> ToolRoute, -{ - fn into_tool_route(self) -> ToolRoute { - (self)() - } -} - -pub trait CallToolHandlerExt: Sized -where - Self: CallToolHandler + Send + Sync + Clone + 'static, -{ - fn name(self, name: impl Into>) -> WithToolAttr; -} - -impl CallToolHandlerExt for C -where - C: CallToolHandler + Send + Sync + Clone + 'static, -{ - fn name(self, name: impl Into>) -> WithToolAttr { - WithToolAttr { - attr: Tool::new( - name.into(), - "", - schema_for_type::(), - ), - call: self, - _marker: std::marker::PhantomData, - } - } -} - -pub struct WithToolAttr -where - C: CallToolHandler + Send + Sync + Clone + 'static, -{ - pub attr: crate::model::Tool, - pub call: C, - pub _marker: std::marker::PhantomData, -} - -impl IntoToolRoute for WithToolAttr -where - C: CallToolHandler + Send + Sync + Clone + 'static, - S: Send + Sync + 'static, -{ - fn into_tool_route(self) -> ToolRoute { - ToolRoute::new(self.attr, self.call) - } -} - -impl WithToolAttr -where - C: CallToolHandler + Send + Sync + Clone + 'static, -{ - pub fn description(mut self, description: impl Into>) -> Self { - self.attr.description = Some(description.into()); - self - } - pub fn parameters(mut self) -> Self { - self.attr.input_schema = schema_for_type::(); - self - } - pub fn parameters_value(mut self, schema: serde_json::Value) -> Self { - self.attr.input_schema = crate::model::object(schema).into(); - self - } - pub fn annotation(mut self, annotation: impl Into) -> Self { - self.attr.annotations = Some(annotation.into()); - self - } -} -#[derive(Debug)] -pub struct ToolRouter { - #[allow(clippy::type_complexity)] - pub map: std::collections::HashMap, ToolRoute>, - - pub transparent_when_not_found: bool, -} - -impl Default for ToolRouter { - fn default() -> Self { - Self { - map: std::collections::HashMap::new(), - transparent_when_not_found: false, - } - } -} -impl Clone for ToolRouter { - fn clone(&self) -> Self { - Self { - map: self.map.clone(), - transparent_when_not_found: self.transparent_when_not_found, - } - } -} - -impl IntoIterator for ToolRouter { - type Item = ToolRoute; - type IntoIter = std::collections::hash_map::IntoValues, ToolRoute>; - - fn into_iter(self) -> Self::IntoIter { - self.map.into_values() - } -} - -impl ToolRouter -where - S: Send + Sync + 'static, -{ - pub fn new() -> Self { - Self { - map: std::collections::HashMap::new(), - transparent_when_not_found: false, - } - } - pub fn with_route(mut self, route: R) -> Self - where - R: IntoToolRoute, - { - self.add_route(route.into_tool_route()); - self - } - - pub fn add_route(&mut self, item: ToolRoute) { - let new_name = &item.attr.name; - validate_and_warn_tool_name(new_name); - self.map.insert(new_name.clone(), item); - } - - pub fn merge(&mut self, other: ToolRouter) { - for item in other.map.into_values() { - self.add_route(item); - } - } - - pub fn remove_route(&mut self, name: &str) { - self.map.remove(name); - } - pub fn has_route(&self, name: &str) -> bool { - self.map.contains_key(name) - } - pub async fn call( - &self, - context: ToolCallContext<'_, S>, - ) -> Result { - let item = self - .map - .get(context.name()) - .ok_or_else(|| crate::ErrorData::invalid_params("tool not found", None))?; - - let result = (item.call)(context).await?; - - Ok(result) - } - - pub fn list_all(&self) -> Vec { - self.map.values().map(|item| item.attr.clone()).collect() - } - - /// Get a tool definition by name. - /// - /// Returns the tool if found, or `None` if no tool with the given name exists. - pub fn get(&self, name: &str) -> Option<&crate::model::Tool> { - self.map.get(name).map(|r| &r.attr) - } -} - -impl std::ops::Add> for ToolRouter -where - S: Send + Sync + 'static, -{ - type Output = Self; - - fn add(mut self, other: ToolRouter) -> Self::Output { - self.merge(other); - self - } -} - -impl std::ops::AddAssign> for ToolRouter -where - S: Send + Sync + 'static, -{ - fn add_assign(&mut self, other: ToolRouter) { - self.merge(other); - } -} diff --git a/codex-rs/vendor/rmcp/src/handler/server/tool.rs b/codex-rs/vendor/rmcp/src/handler/server/tool.rs deleted file mode 100644 index c98aef0d57..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server/tool.rs +++ /dev/null @@ -1,333 +0,0 @@ -use std::{ - borrow::Cow, - future::{Future, Ready}, - marker::PhantomData, -}; - -use futures::future::{BoxFuture, FutureExt}; -use serde::de::DeserializeOwned; - -use super::common::{AsRequestContext, FromContextPart}; -pub use super::{ - common::{Extension, RequestId, schema_for_output, schema_for_type}, - router::tool::{ToolRoute, ToolRouter}, -}; -use crate::{ - RoleServer, - handler::server::wrapper::Parameters, - model::{CallToolRequestParams, CallToolResult, IntoContents, JsonObject}, - service::RequestContext, -}; - -/// Deserialize a JSON object into a type -pub fn parse_json_object(input: JsonObject) -> Result { - serde_json::from_value(serde_json::Value::Object(input)).map_err(|e| { - crate::ErrorData::invalid_params( - format!("failed to deserialize parameters: {error}", error = e), - None, - ) - }) -} -pub struct ToolCallContext<'s, S> { - pub request_context: RequestContext, - pub service: &'s S, - pub name: Cow<'static, str>, - pub arguments: Option, - pub task: Option, -} - -impl<'s, S> ToolCallContext<'s, S> { - pub fn new( - service: &'s S, - CallToolRequestParams { - meta: _, - name, - arguments, - task, - }: CallToolRequestParams, - request_context: RequestContext, - ) -> Self { - Self { - request_context, - service, - name, - arguments, - task, - } - } - pub fn name(&self) -> &str { - &self.name - } - pub fn request_context(&self) -> &RequestContext { - &self.request_context - } -} - -impl AsRequestContext for ToolCallContext<'_, S> { - fn as_request_context(&self) -> &RequestContext { - &self.request_context - } - - fn as_request_context_mut(&mut self) -> &mut RequestContext { - &mut self.request_context - } -} - -pub trait IntoCallToolResult { - fn into_call_tool_result(self) -> Result; -} - -impl IntoCallToolResult for T { - fn into_call_tool_result(self) -> Result { - Ok(CallToolResult::success(self.into_contents())) - } -} - -impl IntoCallToolResult for Result { - fn into_call_tool_result(self) -> Result { - match self { - Ok(value) => Ok(CallToolResult::success(value.into_contents())), - Err(error) => Ok(CallToolResult::error(error.into_contents())), - } - } -} - -impl IntoCallToolResult for Result { - fn into_call_tool_result(self) -> Result { - match self { - Ok(value) => value.into_call_tool_result(), - Err(error) => Err(error), - } - } -} - -pin_project_lite::pin_project! { - #[project = IntoCallToolResultFutProj] - pub enum IntoCallToolResultFut { - Pending { - #[pin] - fut: F, - _marker: PhantomData, - }, - Ready { - #[pin] - result: Ready>, - } - } -} - -impl Future for IntoCallToolResultFut -where - F: Future, - R: IntoCallToolResult, -{ - type Output = Result; - - fn poll( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - match self.project() { - IntoCallToolResultFutProj::Pending { fut, _marker } => { - fut.poll(cx).map(IntoCallToolResult::into_call_tool_result) - } - IntoCallToolResultFutProj::Ready { result } => result.poll(cx), - } - } -} - -impl IntoCallToolResult for Result { - fn into_call_tool_result(self) -> Result { - self - } -} - -pub trait CallToolHandler { - fn call( - self, - context: ToolCallContext<'_, S>, - ) -> BoxFuture<'_, Result>; -} - -pub type DynCallToolHandler = dyn for<'s> Fn(ToolCallContext<'s, S>) -> BoxFuture<'s, Result> - + Send - + Sync; - -// Tool-specific extractor for tool name -pub struct ToolName(pub Cow<'static, str>); - -impl FromContextPart> for ToolName { - fn from_context_part(context: &mut ToolCallContext) -> Result { - Ok(Self(context.name.clone())) - } -} - -// Special implementation for Parameters that handles tool arguments -impl FromContextPart> for Parameters

-where - P: DeserializeOwned, -{ - fn from_context_part(context: &mut ToolCallContext) -> Result { - let arguments = context.arguments.take().unwrap_or_default(); - let value: P = - serde_json::from_value(serde_json::Value::Object(arguments)).map_err(|e| { - crate::ErrorData::invalid_params( - format!("failed to deserialize parameters: {error}", error = e), - None, - ) - })?; - Ok(Parameters(value)) - } -} - -// Special implementation for JsonObject that takes tool arguments -impl FromContextPart> for JsonObject { - fn from_context_part(context: &mut ToolCallContext) -> Result { - let object = context.arguments.take().unwrap_or_default(); - Ok(object) - } -} - -impl<'s, S> ToolCallContext<'s, S> { - pub fn invoke(self, h: H) -> BoxFuture<'s, Result> - where - H: CallToolHandler, - { - h.call(self) - } -} -#[allow(clippy::type_complexity)] -pub struct AsyncAdapter(PhantomData fn(Fut) -> R>); -pub struct SyncAdapter(PhantomData R>); -// #[allow(clippy::type_complexity)] -pub struct AsyncMethodAdapter(PhantomData R>); -pub struct SyncMethodAdapter(PhantomData R>); - -macro_rules! impl_for { - ($($T: ident)*) => { - impl_for!([] [$($T)*]); - }; - // finished - ([$($Tn: ident)*] []) => { - impl_for!(@impl $($Tn)*); - }; - ([$($Tn: ident)*] [$Tn_1: ident $($Rest: ident)*]) => { - impl_for!(@impl $($Tn)*); - impl_for!([$($Tn)* $Tn_1] [$($Rest)*]); - }; - (@impl $($Tn: ident)*) => { - impl<$($Tn,)* S, F, R> CallToolHandler> for F - where - $( - $Tn: for<'a> FromContextPart> , - )* - F: FnOnce(&S, $($Tn,)*) -> BoxFuture<'_, R>, - - // Need RTN support here(I guess), https://github.com/rust-lang/rust/pull/138424 - // Fut: Future + Send + 'a, - R: IntoCallToolResult + Send + 'static, - S: Send + Sync + 'static, - { - #[allow(unused_variables, non_snake_case, unused_mut)] - fn call( - self, - mut context: ToolCallContext<'_, S>, - ) -> BoxFuture<'_, Result>{ - $( - let result = $Tn::from_context_part(&mut context); - let $Tn = match result { - Ok(value) => value, - Err(e) => return std::future::ready(Err(e)).boxed(), - }; - )* - let service = context.service; - let fut = self(service, $($Tn,)*); - async move { - let result = fut.await; - result.into_call_tool_result() - }.boxed() - } - } - - impl<$($Tn,)* S, F, Fut, R> CallToolHandler> for F - where - $( - $Tn: for<'a> FromContextPart> , - )* - F: FnOnce($($Tn,)*) -> Fut + Send + , - Fut: Future + Send + 'static, - R: IntoCallToolResult + Send + 'static, - S: Send + Sync, - { - #[allow(unused_variables, non_snake_case, unused_mut)] - fn call( - self, - mut context: ToolCallContext, - ) -> BoxFuture<'static, Result>{ - $( - let result = $Tn::from_context_part(&mut context); - let $Tn = match result { - Ok(value) => value, - Err(e) => return std::future::ready(Err(e)).boxed(), - }; - )* - let fut = self($($Tn,)*); - async move { - let result = fut.await; - result.into_call_tool_result() - }.boxed() - } - } - - impl<$($Tn,)* S, F, R> CallToolHandler> for F - where - $( - $Tn: for<'a> FromContextPart> + , - )* - F: FnOnce(&S, $($Tn,)*) -> R + Send + , - R: IntoCallToolResult + Send + , - S: Send + Sync, - { - #[allow(unused_variables, non_snake_case, unused_mut)] - fn call( - self, - mut context: ToolCallContext, - ) -> BoxFuture<'static, Result> { - $( - let result = $Tn::from_context_part(&mut context); - let $Tn = match result { - Ok(value) => value, - Err(e) => return std::future::ready(Err(e)).boxed(), - }; - )* - std::future::ready(self(context.service, $($Tn,)*).into_call_tool_result()).boxed() - } - } - - impl<$($Tn,)* S, F, R> CallToolHandler> for F - where - $( - $Tn: for<'a> FromContextPart> + , - )* - F: FnOnce($($Tn,)*) -> R + Send + , - R: IntoCallToolResult + Send + , - S: Send + Sync, - { - #[allow(unused_variables, non_snake_case, unused_mut)] - fn call( - self, - mut context: ToolCallContext, - ) -> BoxFuture<'static, Result> { - $( - let result = $Tn::from_context_part(&mut context); - let $Tn = match result { - Ok(value) => value, - Err(e) => return std::future::ready(Err(e)).boxed(), - }; - )* - std::future::ready(self($($Tn,)*).into_call_tool_result()).boxed() - } - } - }; -} -impl_for!(T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14 T15); diff --git a/codex-rs/vendor/rmcp/src/handler/server/tool_name_validation.rs b/codex-rs/vendor/rmcp/src/handler/server/tool_name_validation.rs deleted file mode 100644 index 8ae04b6264..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server/tool_name_validation.rs +++ /dev/null @@ -1,261 +0,0 @@ -//! Tool name validation utilities according to SEP: Specify Format for Tool Names -//! -//! Tool names SHOULD be between 1 and 128 characters in length (inclusive). -//! Tool names are case-sensitive. -//! Allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits -//! (0-9), underscore (_), dash (-), and dot (.). -//! Tool names SHOULD NOT contain spaces, commas, or other special characters. - -use std::collections::HashSet; - -/// Result of tool name validation containing validation status and warnings. -#[derive(Debug, Clone, PartialEq, Eq)] -struct ToolNameValidationResult { - /// Whether the tool name is valid according to the specification - is_valid: bool, - /// Array of warning messages about non-conforming aspects of the tool name - warnings: Vec, -} - -impl ToolNameValidationResult { - /// Create a new validation result - fn new(is_valid: bool, warnings: Vec) -> Self { - Self { is_valid, warnings } - } -} - -/// Validates a tool name according to the SEP specification. -fn validate_tool_name(name: &str) -> ToolNameValidationResult { - let mut warnings = Vec::new(); - - // Check length - if name.is_empty() { - return ToolNameValidationResult::new(false, vec!["Tool name cannot be empty".to_string()]); - } - - if name.len() > 128 { - return ToolNameValidationResult::new( - false, - vec![format!( - "Tool name exceeds maximum length of 128 characters (current: {})", - name.len() - )], - ); - } - - // Check for specific problematic patterns (these are warnings, not validation failures) - if name.contains(' ') { - warnings.push("Tool name contains spaces, which may cause parsing issues".to_string()); - } - - if name.contains(',') { - warnings.push("Tool name contains commas, which may cause parsing issues".to_string()); - } - - // Check for potentially confusing patterns (leading/trailing dashes, dots, slashes) - if name.starts_with('-') || name.ends_with('-') { - warnings.push( - "Tool name starts or ends with a dash, which may cause parsing issues in some contexts" - .to_string(), - ); - } - - if name.starts_with('.') || name.ends_with('.') { - warnings.push( - "Tool name starts or ends with a dot, which may cause parsing issues in some contexts" - .to_string(), - ); - } - - // Check for invalid characters - let mut invalid_chars = HashSet::new(); - let valid_chars: HashSet = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-" - .chars() - .collect(); - - for ch in name.chars() { - if !valid_chars.contains(&ch) { - invalid_chars.insert(ch); - } - } - - if !invalid_chars.is_empty() { - let invalid_chars_list: Vec = - invalid_chars.iter().map(|c| format!("\"{}\"", c)).collect(); - warnings.push(format!( - "Tool name contains invalid characters: {}", - invalid_chars_list.join(", ") - )); - warnings.push( - "Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)" - .to_string(), - ); - - return ToolNameValidationResult::new(false, warnings); - } - - // Verify the pattern matches (double check with character-by-character validation) - // We've already validated characters above, just need to verify length is within bounds - if name.is_empty() || name.len() > 128 { - return ToolNameValidationResult::new( - false, - vec!["Tool name length must be between 1 and 128 characters".to_string()], - ); - } - - ToolNameValidationResult::new(true, warnings) -} - -/// Issues warnings for non-conforming tool names. -fn issue_tool_name_warning(name: &str, warnings: &[String]) { - tracing::warn!("Tool name validation warning for \"{}\":", name); - for warning in warnings { - tracing::warn!(" - {}", warning); - } - tracing::warn!("Tool registration will proceed, but this may cause compatibility issues."); - tracing::warn!("Consider updating the tool name to conform to the MCP tool naming standard."); - tracing::warn!( - "See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details." - ); -} - -/// Validates a tool name and issues warnings for non-conforming names. -pub fn validate_and_warn_tool_name(name: &str) -> bool { - let result = validate_tool_name(name); - - if !result.warnings.is_empty() { - issue_tool_name_warning(name, &result.warnings); - } - - result.is_valid -} -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_valid_tool_names() { - let max_length_name = "a".repeat(128); - let valid_names = vec![ - "my_tool", - "MyTool", - "my-tool", - "my.tool", - "tool123", - "a", - max_length_name.as_str(), // Maximum length - ]; - - for name in valid_names { - let result = validate_tool_name(name); - assert!(result.is_valid, "Tool name '{}' should be valid", name); - } - } - - #[test] - fn test_empty_tool_name() { - let result = validate_tool_name(""); - assert!(!result.is_valid); - assert!( - result - .warnings - .contains(&"Tool name cannot be empty".to_string()) - ); - } - - #[test] - fn test_too_long_tool_name() { - let name = "a".repeat(129); - let result = validate_tool_name(&name); - assert!(!result.is_valid); - assert!(result.warnings[0].contains("exceeds maximum length")); - } - - #[test] - fn test_tool_name_with_spaces() { - let result = validate_tool_name("my tool"); - assert!(!result.is_valid); - assert!( - result - .warnings - .iter() - .any(|w| w.contains("contains spaces")) - ); - } - - #[test] - fn test_tool_name_with_commas() { - let result = validate_tool_name("my,tool"); - assert!(!result.is_valid); - assert!( - result - .warnings - .iter() - .any(|w| w.contains("contains commas")) - ); - } - - #[test] - fn test_tool_name_starting_with_dash() { - let result = validate_tool_name("-tool"); - assert!(result.is_valid); // Still valid, but has warning - assert!( - result - .warnings - .iter() - .any(|w| w.contains("starts or ends with a dash")) - ); - } - - #[test] - fn test_tool_name_ending_with_dot() { - let result = validate_tool_name("tool."); - assert!(result.is_valid); // Still valid, but has warning - assert!( - result - .warnings - .iter() - .any(|w| w.contains("starts or ends with a dot")) - ); - } - - #[test] - fn test_tool_name_with_invalid_characters() { - let result = validate_tool_name("my@tool"); - assert!(!result.is_valid); - assert!( - result - .warnings - .iter() - .any(|w| w.contains("contains invalid characters")) - ); - } - - #[test] - fn test_tool_name_all_special_characters_allowed() { - let valid_chars = vec!['_', '-', '.']; - for ch in valid_chars { - let name = format!("tool{}", ch); - let result = validate_tool_name(&name); - assert!( - result.is_valid, - "Tool name with character '{}' should be valid", - ch - ); - } - } - - #[test] - fn test_minimum_length() { - let result = validate_tool_name("a"); - assert!(result.is_valid); - } - - #[test] - fn test_maximum_length() { - let name = "a".repeat(128); - let result = validate_tool_name(&name); - assert!(result.is_valid); - } -} diff --git a/codex-rs/vendor/rmcp/src/handler/server/wrapper.rs b/codex-rs/vendor/rmcp/src/handler/server/wrapper.rs deleted file mode 100644 index d9f2c86d97..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server/wrapper.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod json; -mod parameters; -pub use json::*; -pub use parameters::*; diff --git a/codex-rs/vendor/rmcp/src/handler/server/wrapper/json.rs b/codex-rs/vendor/rmcp/src/handler/server/wrapper/json.rs deleted file mode 100644 index 8eae302688..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server/wrapper/json.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::borrow::Cow; - -use schemars::JsonSchema; -use serde::Serialize; - -use crate::{ - handler::server::tool::IntoCallToolResult, - model::{CallToolResult, IntoContents}, -}; - -/// Json wrapper for structured output -/// -/// When used with tools, this wrapper indicates that the value should be -/// serialized as structured JSON content with an associated schema. -/// The framework will place the JSON in the `structured_content` field -/// of the tool result rather than the regular `content` field. -pub struct Json(pub T); - -// Implement JsonSchema for Json to delegate to T's schema -impl JsonSchema for Json { - fn schema_name() -> Cow<'static, str> { - T::schema_name() - } - - fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - T::json_schema(generator) - } -} - -// Implementation for Json to create structured content -impl IntoCallToolResult for Json { - fn into_call_tool_result(self) -> Result { - let value = serde_json::to_value(self.0).map_err(|e| { - crate::ErrorData::internal_error( - format!("Failed to serialize structured content: {}", e), - None, - ) - })?; - - Ok(CallToolResult::structured(value)) - } -} - -// Implementation for Result, E> -impl IntoCallToolResult - for Result, E> -{ - fn into_call_tool_result(self) -> Result { - match self { - Ok(value) => value.into_call_tool_result(), - Err(error) => Ok(CallToolResult::error(error.into_contents())), - } - } -} diff --git a/codex-rs/vendor/rmcp/src/handler/server/wrapper/parameters.rs b/codex-rs/vendor/rmcp/src/handler/server/wrapper/parameters.rs deleted file mode 100644 index 9de73dd668..0000000000 --- a/codex-rs/vendor/rmcp/src/handler/server/wrapper/parameters.rs +++ /dev/null @@ -1,55 +0,0 @@ -use schemars::JsonSchema; - -/// Parameter extractor for tools and prompts -/// -/// When used in tool and prompt handlers, this wrapper extracts and deserializes -/// parameters from the incoming request. The framework will automatically parse -/// the JSON arguments from tool calls or prompt arguments and deserialize them -/// into the specified type `P`. -/// -/// The `#[serde(transparent)]` attribute ensures that the wrapper doesn't add -/// an extra layer in the JSON structure - it directly delegates serialization -/// and deserialization to the inner type `P`. -/// -/// # Usage -/// -/// Use `Parameters` as a parameter in your tool or prompt handler functions: -/// -/// ```rust -/// # use rmcp::handler::server::wrapper::Parameters; -/// # use schemars::JsonSchema; -/// # use serde::{Deserialize, Serialize}; -/// #[derive(Deserialize, JsonSchema)] -/// struct CalculationRequest { -/// operation: String, -/// a: f64, -/// b: f64, -/// } -/// -/// // In a tool handler -/// async fn calculate(params: Parameters) -> Result { -/// let request = params.0; // Extract the inner value -/// match request.operation.as_str() { -/// "add" => Ok((request.a + request.b).to_string()), -/// _ => Err("Unknown operation".to_string()), -/// } -/// } -/// ``` -/// -/// The framework handles the extraction automatically: -/// - For tools: Parses the `arguments` field from tool call requests -/// - For prompts: Parses the `arguments` field from prompt requests -/// - Returns appropriate error responses if deserialization fails -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(transparent)] -pub struct Parameters

(pub P); - -impl JsonSchema for Parameters

{ - fn schema_name() -> std::borrow::Cow<'static, str> { - P::schema_name() - } - - fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - P::json_schema(generator) - } -} diff --git a/codex-rs/vendor/rmcp/src/lib.rs b/codex-rs/vendor/rmcp/src/lib.rs deleted file mode 100644 index 7a2ea49f38..0000000000 --- a/codex-rs/vendor/rmcp/src/lib.rs +++ /dev/null @@ -1,52 +0,0 @@ -#![cfg_attr(docsrs, feature(doc_cfg))] -#![cfg_attr(docsrs, allow(unused_attributes))] -#![doc = include_str!("../README.md")] - -mod error; -#[allow(deprecated)] -pub use error::{Error, ErrorData, RmcpError}; - -/// Basic data types in MCP specification -pub mod model; -#[cfg(any(feature = "client", feature = "server"))] -#[cfg_attr(docsrs, doc(cfg(any(feature = "client", feature = "server"))))] -pub mod service; -#[cfg(feature = "client")] -#[cfg_attr(docsrs, doc(cfg(feature = "client")))] -pub use handler::client::ClientHandler; -#[cfg(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -pub use handler::server::ServerHandler; -#[cfg(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -pub use handler::server::wrapper::Json; -#[cfg(any(feature = "client", feature = "server"))] -#[cfg_attr(docsrs, doc(cfg(any(feature = "client", feature = "server"))))] -pub use service::{Peer, Service, ServiceError, ServiceExt}; -#[cfg(feature = "client")] -#[cfg_attr(docsrs, doc(cfg(feature = "client")))] -pub use service::{RoleClient, serve_client}; -#[cfg(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -pub use service::{RoleServer, serve_server}; - -pub mod handler; -pub mod task_manager; -pub mod transport; - -// re-export -#[cfg(all(feature = "macros", feature = "server"))] -#[cfg_attr(docsrs, doc(cfg(all(feature = "macros", feature = "server"))))] -pub use pastey::paste; -#[cfg(all(feature = "macros", feature = "server"))] -#[cfg_attr(docsrs, doc(cfg(all(feature = "macros", feature = "server"))))] -pub use rmcp_macros::*; -#[cfg(any(feature = "macros", feature = "server"))] -#[cfg_attr(docsrs, doc(cfg(any(feature = "macros", feature = "server"))))] -pub use schemars; -#[cfg(feature = "macros")] -#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] -pub use serde; -#[cfg(feature = "macros")] -#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] -pub use serde_json; diff --git a/codex-rs/vendor/rmcp/src/model.rs b/codex-rs/vendor/rmcp/src/model.rs deleted file mode 100644 index 00c51bcb3d..0000000000 --- a/codex-rs/vendor/rmcp/src/model.rs +++ /dev/null @@ -1,3352 +0,0 @@ -use std::{borrow::Cow, sync::Arc}; -mod annotated; -mod capabilities; -mod content; -mod elicitation_schema; -mod extension; -mod meta; -mod prompt; -mod resource; -mod serde_impl; -mod task; -mod tool; -pub use annotated::*; -pub use capabilities::*; -pub use content::*; -pub use elicitation_schema::*; -pub use extension::*; -pub use meta::*; -pub use prompt::*; -pub use resource::*; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; -use serde_json::Value; -pub use task::*; -pub use tool::*; - -/// A JSON object type alias for convenient handling of JSON data. -/// -/// You can use [`crate::object!`] or [`crate::model::object`] to create a json object quickly. -/// This is commonly used for storing arbitrary JSON data in MCP messages. -pub type JsonObject = serde_json::Map; - -/// unwrap the JsonObject under [`serde_json::Value`] -/// -/// # Panic -/// This will panic when the value is not a object in debug mode. -pub fn object(value: serde_json::Value) -> JsonObject { - debug_assert!(value.is_object()); - match value { - serde_json::Value::Object(map) => map, - _ => JsonObject::default(), - } -} - -/// Use this macro just like [`serde_json::json!`] -#[cfg(feature = "macros")] -#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] -#[macro_export] -macro_rules! object { - ({$($tt:tt)*}) => { - $crate::model::object(serde_json::json! { - {$($tt)*} - }) - }; -} - -/// This is commonly used for representing empty objects in MCP messages. -/// -/// without returning any specific data. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Copy, Eq)] -#[serde(deny_unknown_fields)] -#[cfg_attr(feature = "server", derive(schemars::JsonSchema))] -pub struct EmptyObject {} - -pub trait ConstString: Default { - const VALUE: &str; - fn as_str(&self) -> &'static str { - Self::VALUE - } -} -#[macro_export] -macro_rules! const_string { - ($name:ident = $value:literal) => { - #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] - pub struct $name; - - impl ConstString for $name { - const VALUE: &str = $value; - } - - impl serde::Serialize for $name { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - $value.serialize(serializer) - } - } - - impl<'de> serde::Deserialize<'de> for $name { - fn deserialize(deserializer: D) -> Result<$name, D::Error> - where - D: serde::Deserializer<'de>, - { - let s: String = serde::Deserialize::deserialize(deserializer)?; - if s == $value { - Ok($name) - } else { - Err(serde::de::Error::custom(format!(concat!( - "expect const string value \"", - $value, - "\"" - )))) - } - } - } - - #[cfg(feature = "schemars")] - impl schemars::JsonSchema for $name { - fn schema_name() -> Cow<'static, str> { - Cow::Borrowed(stringify!($name)) - } - - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - use serde_json::{Map, json}; - - let mut schema_map = Map::new(); - schema_map.insert("type".to_string(), json!("string")); - schema_map.insert("format".to_string(), json!("const")); - schema_map.insert("const".to_string(), json!($value)); - - schemars::Schema::from(schema_map) - } - } - }; -} - -const_string!(JsonRpcVersion2_0 = "2.0"); - -// ============================================================================= -// CORE PROTOCOL TYPES -// ============================================================================= - -/// Represents the MCP protocol version used for communication. -/// -/// This ensures compatibility between clients and servers by specifying -/// which version of the Model Context Protocol is being used. -#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ProtocolVersion(Cow<'static, str>); - -impl Default for ProtocolVersion { - fn default() -> Self { - Self::LATEST - } -} - -impl std::fmt::Display for ProtocolVersion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl ProtocolVersion { - pub const V_2025_06_18: Self = Self(Cow::Borrowed("2025-06-18")); - pub const V_2025_03_26: Self = Self(Cow::Borrowed("2025-03-26")); - pub const V_2024_11_05: Self = Self(Cow::Borrowed("2024-11-05")); - // Keep LATEST at 2025-03-26 until full 2025-06-18 compliance and automated testing are in place. - pub const LATEST: Self = Self::V_2025_03_26; -} - -impl Serialize for ProtocolVersion { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.0.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for ProtocolVersion { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s: String = Deserialize::deserialize(deserializer)?; - #[allow(clippy::single_match)] - match s.as_str() { - "2024-11-05" => return Ok(ProtocolVersion::V_2024_11_05), - "2025-03-26" => return Ok(ProtocolVersion::V_2025_03_26), - "2025-06-18" => return Ok(ProtocolVersion::V_2025_06_18), - _ => {} - } - Ok(ProtocolVersion(Cow::Owned(s))) - } -} - -/// A flexible identifier type that can be either a number or a string. -/// -/// This is commonly used for request IDs and other identifiers in JSON-RPC -/// where the specification allows both numeric and string values. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum NumberOrString { - /// A numeric identifier - Number(i64), - /// A string identifier - String(Arc), -} - -impl NumberOrString { - pub fn into_json_value(self) -> Value { - match self { - NumberOrString::Number(n) => Value::Number(serde_json::Number::from(n)), - NumberOrString::String(s) => Value::String(s.to_string()), - } - } -} - -impl std::fmt::Display for NumberOrString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - NumberOrString::Number(n) => n.fmt(f), - NumberOrString::String(s) => s.fmt(f), - } - } -} - -impl Serialize for NumberOrString { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - NumberOrString::Number(n) => n.serialize(serializer), - NumberOrString::String(s) => s.serialize(serializer), - } - } -} - -impl<'de> Deserialize<'de> for NumberOrString { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value: Value = Deserialize::deserialize(deserializer)?; - match value { - Value::Number(n) => { - if let Some(i) = n.as_i64() { - Ok(NumberOrString::Number(i)) - } else if let Some(u) = n.as_u64() { - // Handle large unsigned numbers that fit in i64 - if u <= i64::MAX as u64 { - Ok(NumberOrString::Number(u as i64)) - } else { - Err(serde::de::Error::custom("Number too large for i64")) - } - } else { - Err(serde::de::Error::custom("Expected an integer")) - } - } - Value::String(s) => Ok(NumberOrString::String(s.into())), - _ => Err(serde::de::Error::custom("Expect number or string")), - } - } -} - -#[cfg(feature = "schemars")] -impl schemars::JsonSchema for NumberOrString { - fn schema_name() -> Cow<'static, str> { - Cow::Borrowed("NumberOrString") - } - - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - use serde_json::{Map, json}; - - let mut number_schema = Map::new(); - number_schema.insert("type".to_string(), json!("number")); - - let mut string_schema = Map::new(); - string_schema.insert("type".to_string(), json!("string")); - - let mut schema_map = Map::new(); - schema_map.insert("oneOf".to_string(), json!([number_schema, string_schema])); - - schemars::Schema::from(schema_map) - } -} - -/// Type alias for request identifiers used in JSON-RPC communication. -pub type RequestId = NumberOrString; - -/// A token used to track the progress of long-running operations. -/// -/// Progress tokens allow clients and servers to associate progress notifications -/// with specific requests, enabling real-time updates on operation status. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Hash, Eq)] -#[serde(transparent)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ProgressToken(pub NumberOrString); - -// ============================================================================= -// JSON-RPC MESSAGE STRUCTURES -// ============================================================================= - -/// Represents a JSON-RPC request with method, parameters, and extensions. -/// -/// This is the core structure for all MCP requests, containing: -/// - `method`: The name of the method being called -/// - `params`: The parameters for the method -/// - `extensions`: Additional context data (similar to HTTP headers) -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Request { - pub method: M, - pub params: P, - /// extensions will carry anything possible in the context, including [`Meta`] - /// - /// this is similar with the Extensions in `http` crate - #[cfg_attr(feature = "schemars", schemars(skip))] - pub extensions: Extensions, -} - -impl Request { - pub fn new(params: P) -> Self { - Self { - method: Default::default(), - params, - extensions: Extensions::default(), - } - } -} - -impl GetExtensions for Request { - fn extensions(&self) -> &Extensions { - &self.extensions - } - fn extensions_mut(&mut self) -> &mut Extensions { - &mut self.extensions - } -} - -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct RequestOptionalParam { - pub method: M, - // #[serde(skip_serializing_if = "Option::is_none")] - pub params: Option

, - /// extensions will carry anything possible in the context, including [`Meta`] - /// - /// this is similar with the Extensions in `http` crate - #[cfg_attr(feature = "schemars", schemars(skip))] - pub extensions: Extensions, -} - -impl RequestOptionalParam { - pub fn with_param(params: P) -> Self { - Self { - method: Default::default(), - params: Some(params), - extensions: Extensions::default(), - } - } -} - -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct RequestNoParam { - pub method: M, - /// extensions will carry anything possible in the context, including [`Meta`] - /// - /// this is similar with the Extensions in `http` crate - #[cfg_attr(feature = "schemars", schemars(skip))] - pub extensions: Extensions, -} - -impl GetExtensions for RequestNoParam { - fn extensions(&self) -> &Extensions { - &self.extensions - } - fn extensions_mut(&mut self) -> &mut Extensions { - &mut self.extensions - } -} -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Notification { - pub method: M, - pub params: P, - /// extensions will carry anything possible in the context, including [`Meta`] - /// - /// this is similar with the Extensions in `http` crate - #[cfg_attr(feature = "schemars", schemars(skip))] - pub extensions: Extensions, -} - -impl Notification { - pub fn new(params: P) -> Self { - Self { - method: Default::default(), - params, - extensions: Extensions::default(), - } - } -} - -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct NotificationNoParam { - pub method: M, - /// extensions will carry anything possible in the context, including [`Meta`] - /// - /// this is similar with the Extensions in `http` crate - #[cfg_attr(feature = "schemars", schemars(skip))] - pub extensions: Extensions, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct JsonRpcRequest { - pub jsonrpc: JsonRpcVersion2_0, - pub id: RequestId, - #[serde(flatten)] - pub request: R, -} - -type DefaultResponse = JsonObject; -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct JsonRpcResponse { - pub jsonrpc: JsonRpcVersion2_0, - pub id: RequestId, - pub result: R, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct JsonRpcError { - pub jsonrpc: JsonRpcVersion2_0, - pub id: RequestId, - pub error: ErrorData, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct JsonRpcNotification { - pub jsonrpc: JsonRpcVersion2_0, - #[serde(flatten)] - pub notification: N, -} - -/// Standard JSON-RPC error codes used throughout the MCP protocol. -/// -/// These codes follow the JSON-RPC 2.0 specification and provide -/// standardized error reporting across all MCP implementations. -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] -#[serde(transparent)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ErrorCode(pub i32); - -impl ErrorCode { - pub const RESOURCE_NOT_FOUND: Self = Self(-32002); - pub const INVALID_REQUEST: Self = Self(-32600); - pub const METHOD_NOT_FOUND: Self = Self(-32601); - pub const INVALID_PARAMS: Self = Self(-32602); - pub const INTERNAL_ERROR: Self = Self(-32603); - pub const PARSE_ERROR: Self = Self(-32700); - pub const URL_ELICITATION_REQUIRED: Self = Self(-32042); -} - -/// Error information for JSON-RPC error responses. -/// -/// This structure follows the JSON-RPC 2.0 specification for error reporting, -/// providing a standardized way to communicate errors between clients and servers. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ErrorData { - /// The error type that occurred (using standard JSON-RPC error codes) - pub code: ErrorCode, - - /// A short description of the error. The message SHOULD be limited to a concise single sentence. - pub message: Cow<'static, str>, - - /// Additional information about the error. The value of this member is defined by the - /// sender (e.g. detailed error information, nested errors etc.). - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, -} - -impl ErrorData { - pub fn new( - code: ErrorCode, - message: impl Into>, - data: Option, - ) -> Self { - Self { - code, - message: message.into(), - data, - } - } - pub fn resource_not_found(message: impl Into>, data: Option) -> Self { - Self::new(ErrorCode::RESOURCE_NOT_FOUND, message, data) - } - pub fn parse_error(message: impl Into>, data: Option) -> Self { - Self::new(ErrorCode::PARSE_ERROR, message, data) - } - pub fn invalid_request(message: impl Into>, data: Option) -> Self { - Self::new(ErrorCode::INVALID_REQUEST, message, data) - } - pub fn method_not_found() -> Self { - Self::new(ErrorCode::METHOD_NOT_FOUND, M::VALUE, None) - } - pub fn invalid_params(message: impl Into>, data: Option) -> Self { - Self::new(ErrorCode::INVALID_PARAMS, message, data) - } - pub fn internal_error(message: impl Into>, data: Option) -> Self { - Self::new(ErrorCode::INTERNAL_ERROR, message, data) - } - pub fn url_elicitation_required( - message: impl Into>, - data: Option, - ) -> Self { - Self::new(ErrorCode::URL_ELICITATION_REQUIRED, message, data) - } -} - -/// Represents any JSON-RPC message that can be sent or received. -/// -/// This enum covers all possible message types in the JSON-RPC protocol: -/// individual requests/responses, notifications, and errors. -/// It serves as the top-level message container for MCP communication. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(untagged)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum JsonRpcMessage { - /// A single request expecting a response - Request(JsonRpcRequest), - /// A response to a previous request - Response(JsonRpcResponse), - /// A one-way notification (no response expected) - Notification(JsonRpcNotification), - /// An error response - Error(JsonRpcError), -} - -impl JsonRpcMessage { - #[inline] - pub const fn request(request: Req, id: RequestId) -> Self { - JsonRpcMessage::Request(JsonRpcRequest { - jsonrpc: JsonRpcVersion2_0, - id, - request, - }) - } - #[inline] - pub const fn response(response: Resp, id: RequestId) -> Self { - JsonRpcMessage::Response(JsonRpcResponse { - jsonrpc: JsonRpcVersion2_0, - id, - result: response, - }) - } - #[inline] - pub const fn error(error: ErrorData, id: RequestId) -> Self { - JsonRpcMessage::Error(JsonRpcError { - jsonrpc: JsonRpcVersion2_0, - id, - error, - }) - } - #[inline] - pub const fn notification(notification: Not) -> Self { - JsonRpcMessage::Notification(JsonRpcNotification { - jsonrpc: JsonRpcVersion2_0, - notification, - }) - } - pub fn into_request(self) -> Option<(Req, RequestId)> { - match self { - JsonRpcMessage::Request(r) => Some((r.request, r.id)), - _ => None, - } - } - pub fn into_response(self) -> Option<(Resp, RequestId)> { - match self { - JsonRpcMessage::Response(r) => Some((r.result, r.id)), - _ => None, - } - } - pub fn into_notification(self) -> Option { - match self { - JsonRpcMessage::Notification(n) => Some(n.notification), - _ => None, - } - } - pub fn into_error(self) -> Option<(ErrorData, RequestId)> { - match self { - JsonRpcMessage::Error(e) => Some((e.error, e.id)), - _ => None, - } - } - pub fn into_result(self) -> Option<(Result, RequestId)> { - match self { - JsonRpcMessage::Response(r) => Some((Ok(r.result), r.id)), - JsonRpcMessage::Error(e) => Some((Err(e.error), e.id)), - - _ => None, - } - } -} - -// ============================================================================= -// INITIALIZATION AND CONNECTION SETUP -// ============================================================================= - -/// # Empty result -/// A response that indicates success but carries no data. -pub type EmptyResult = EmptyObject; - -impl From<()> for EmptyResult { - fn from(_value: ()) -> Self { - EmptyResult {} - } -} - -impl From for () { - fn from(_value: EmptyResult) {} -} - -/// A catch-all response either side can use for custom requests. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(transparent)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CustomResult(pub Value); - -impl CustomResult { - pub fn new(result: Value) -> Self { - Self(result) - } - - /// Deserialize the result into a strongly-typed structure. - pub fn result_as(&self) -> Result { - serde_json::from_value(self.0.clone()) - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CancelledNotificationParam { - pub request_id: RequestId, - pub reason: Option, -} - -const_string!(CancelledNotificationMethod = "notifications/cancelled"); - -/// # Cancellation -/// This notification can be sent by either side to indicate that it is cancelling a previously-issued request. -/// -/// The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. -/// -/// This notification indicates that the result will be unused, so any associated processing SHOULD cease. -/// -/// A client MUST NOT attempt to cancel its `initialize` request. -pub type CancelledNotification = - Notification; - -/// A catch-all notification either side can use to send custom messages to its peer. -/// -/// This preserves the raw `method` name and `params` payload so handlers can -/// deserialize them into domain-specific types. -#[derive(Debug, Clone)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CustomNotification { - pub method: String, - pub params: Option, - /// extensions will carry anything possible in the context, including [`Meta`] - /// - /// this is similar with the Extensions in `http` crate - #[cfg_attr(feature = "schemars", schemars(skip))] - pub extensions: Extensions, -} - -impl CustomNotification { - pub fn new(method: impl Into, params: Option) -> Self { - Self { - method: method.into(), - params, - extensions: Extensions::default(), - } - } - - /// Deserialize `params` into a strongly-typed structure. - pub fn params_as(&self) -> Result, serde_json::Error> { - self.params - .as_ref() - .map(|params| serde_json::from_value(params.clone())) - .transpose() - } -} - -/// A catch-all request either side can use to send custom messages to its peer. -/// -/// This preserves the raw `method` name and `params` payload so handlers can -/// deserialize them into domain-specific types. -#[derive(Debug, Clone)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CustomRequest { - pub method: String, - pub params: Option, - /// extensions will carry anything possible in the context, including [`Meta`] - /// - /// this is similar with the Extensions in `http` crate - #[cfg_attr(feature = "schemars", schemars(skip))] - pub extensions: Extensions, -} - -impl CustomRequest { - pub fn new(method: impl Into, params: Option) -> Self { - Self { - method: method.into(), - params, - extensions: Extensions::default(), - } - } - - /// Deserialize `params` into a strongly-typed structure. - pub fn params_as(&self) -> Result, serde_json::Error> { - self.params - .as_ref() - .map(|params| serde_json::from_value(params.clone())) - .transpose() - } -} - -const_string!(InitializeResultMethod = "initialize"); -/// # Initialization -/// This request is sent from the client to the server when it first connects, asking it to begin initialization. -pub type InitializeRequest = Request; - -const_string!(InitializedNotificationMethod = "notifications/initialized"); -/// This notification is sent from the client to the server after initialization has finished. -pub type InitializedNotification = NotificationNoParam; - -/// Parameters sent by a client when initializing a connection to an MCP server. -/// -/// This contains the client's protocol version, capabilities, and implementation -/// information, allowing the server to understand what the client supports. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct InitializeRequestParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - /// The MCP protocol version this client supports - pub protocol_version: ProtocolVersion, - /// The capabilities this client supports (sampling, roots, etc.) - pub capabilities: ClientCapabilities, - /// Information about the client implementation - pub client_info: Implementation, -} - -impl RequestParamsMeta for InitializeRequestParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -/// Deprecated: Use [`InitializeRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use InitializeRequestParams instead")] -pub type InitializeRequestParam = InitializeRequestParams; - -/// The server's response to an initialization request. -/// -/// Contains the server's protocol version, capabilities, and implementation -/// information, along with optional instructions for the client. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct InitializeResult { - /// The MCP protocol version this server supports - pub protocol_version: ProtocolVersion, - /// The capabilities this server provides (tools, resources, prompts, etc.) - pub capabilities: ServerCapabilities, - /// Information about the server implementation - pub server_info: Implementation, - /// Optional human-readable instructions about using this server - #[serde(skip_serializing_if = "Option::is_none")] - pub instructions: Option, -} - -pub type ServerInfo = InitializeResult; -pub type ClientInfo = InitializeRequestParams; - -#[allow(clippy::derivable_impls)] -impl Default for ServerInfo { - fn default() -> Self { - ServerInfo { - protocol_version: ProtocolVersion::default(), - capabilities: ServerCapabilities::default(), - server_info: Implementation::from_build_env(), - instructions: None, - } - } -} - -#[allow(clippy::derivable_impls)] -impl Default for ClientInfo { - fn default() -> Self { - ClientInfo { - meta: None, - protocol_version: ProtocolVersion::default(), - capabilities: ClientCapabilities::default(), - client_info: Implementation::from_build_env(), - } - } -} - -/// A URL pointing to an icon resource or a base64-encoded data URI. -/// -/// Clients that support rendering icons MUST support at least the following MIME types: -/// - image/png - PNG images (safe, universal compatibility) -/// - image/jpeg (and image/jpg) - JPEG images (safe, universal compatibility) -/// -/// Clients that support rendering icons SHOULD also support: -/// - image/svg+xml - SVG images (scalable but requires security precautions) -/// - image/webp - WebP images (modern, efficient format) -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Icon { - /// A standard URI pointing to an icon resource - pub src: String, - /// Optional override if the server's MIME type is missing or generic - #[serde(skip_serializing_if = "Option::is_none")] - pub mime_type: Option, - /// Size specification, each string should be in WxH format (e.g., `\"48x48\"`, `\"96x96\"`) or `\"any\"` for scalable formats like SVG - #[serde(skip_serializing_if = "Option::is_none")] - pub sizes: Option>, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Implementation { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - pub version: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub icons: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub website_url: Option, -} - -impl Default for Implementation { - fn default() -> Self { - Self::from_build_env() - } -} - -impl Implementation { - pub fn from_build_env() -> Self { - Implementation { - name: env!("CARGO_CRATE_NAME").to_owned(), - title: None, - version: env!("CARGO_PKG_VERSION").to_owned(), - description: None, - icons: None, - website_url: None, - } - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct PaginatedRequestParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cursor: Option, -} - -impl RequestParamsMeta for PaginatedRequestParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -/// Deprecated: Use [`PaginatedRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use PaginatedRequestParams instead")] -pub type PaginatedRequestParam = PaginatedRequestParams; -// ============================================================================= -// PROGRESS AND PAGINATION -// ============================================================================= - -const_string!(PingRequestMethod = "ping"); -pub type PingRequest = RequestNoParam; - -const_string!(ProgressNotificationMethod = "notifications/progress"); -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ProgressNotificationParam { - pub progress_token: ProgressToken, - /// The progress thus far. This should increase every time progress is made, even if the total is unknown. - pub progress: f64, - /// Total number of items to process (or total progress required), if known - #[serde(skip_serializing_if = "Option::is_none")] - pub total: Option, - /// An optional message describing the current progress. - #[serde(skip_serializing_if = "Option::is_none")] - pub message: Option, -} - -pub type ProgressNotification = Notification; - -pub type Cursor = String; - -macro_rules! paginated_result { - ($t:ident { - $i_item: ident: $t_item: ty - }) => { - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] - #[serde(rename_all = "camelCase")] - #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] - pub struct $t { - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub next_cursor: Option, - pub $i_item: $t_item, - } - - impl $t { - pub fn with_all_items( - items: $t_item, - ) -> Self { - Self { - meta: None, - next_cursor: None, - $i_item: items, - } - } - } - }; -} - -// ============================================================================= -// RESOURCE MANAGEMENT -// ============================================================================= - -const_string!(ListResourcesRequestMethod = "resources/list"); -/// Request to list all available resources from a server -pub type ListResourcesRequest = - RequestOptionalParam; - -paginated_result!(ListResourcesResult { - resources: Vec -}); - -const_string!(ListResourceTemplatesRequestMethod = "resources/templates/list"); -/// Request to list all available resource templates from a server -pub type ListResourceTemplatesRequest = - RequestOptionalParam; - -paginated_result!(ListResourceTemplatesResult { - resource_templates: Vec -}); - -const_string!(ReadResourceRequestMethod = "resources/read"); -/// Parameters for reading a specific resource -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ReadResourceRequestParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - /// The URI of the resource to read - pub uri: String, -} - -impl RequestParamsMeta for ReadResourceRequestParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -/// Deprecated: Use [`ReadResourceRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use ReadResourceRequestParams instead")] -pub type ReadResourceRequestParam = ReadResourceRequestParams; - -/// Result containing the contents of a read resource -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ReadResourceResult { - /// The actual content of the resource - pub contents: Vec, -} - -/// Request to read a specific resource -pub type ReadResourceRequest = Request; - -const_string!(ResourceListChangedNotificationMethod = "notifications/resources/list_changed"); -/// Notification sent when the list of available resources changes -pub type ResourceListChangedNotification = - NotificationNoParam; - -const_string!(SubscribeRequestMethod = "resources/subscribe"); -/// Parameters for subscribing to resource updates -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct SubscribeRequestParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - /// The URI of the resource to subscribe to - pub uri: String, -} - -impl RequestParamsMeta for SubscribeRequestParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -/// Deprecated: Use [`SubscribeRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use SubscribeRequestParams instead")] -pub type SubscribeRequestParam = SubscribeRequestParams; - -/// Request to subscribe to resource updates -pub type SubscribeRequest = Request; - -const_string!(UnsubscribeRequestMethod = "resources/unsubscribe"); -/// Parameters for unsubscribing from resource updates -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct UnsubscribeRequestParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - /// The URI of the resource to unsubscribe from - pub uri: String, -} - -impl RequestParamsMeta for UnsubscribeRequestParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -/// Deprecated: Use [`UnsubscribeRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use UnsubscribeRequestParams instead")] -pub type UnsubscribeRequestParam = UnsubscribeRequestParams; - -/// Request to unsubscribe from resource updates -pub type UnsubscribeRequest = Request; - -const_string!(ResourceUpdatedNotificationMethod = "notifications/resources/updated"); -/// Parameters for a resource update notification -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ResourceUpdatedNotificationParam { - /// The URI of the resource that was updated - pub uri: String, -} -/// Notification sent when a subscribed resource is updated -pub type ResourceUpdatedNotification = - Notification; - -// ============================================================================= -// PROMPT MANAGEMENT -// ============================================================================= - -const_string!(ListPromptsRequestMethod = "prompts/list"); -/// Request to list all available prompts from a server -pub type ListPromptsRequest = - RequestOptionalParam; - -paginated_result!(ListPromptsResult { - prompts: Vec -}); - -const_string!(GetPromptRequestMethod = "prompts/get"); -/// Parameters for retrieving a specific prompt -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct GetPromptRequestParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub arguments: Option, -} - -impl RequestParamsMeta for GetPromptRequestParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -/// Deprecated: Use [`GetPromptRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use GetPromptRequestParams instead")] -pub type GetPromptRequestParam = GetPromptRequestParams; - -/// Request to get a specific prompt -pub type GetPromptRequest = Request; - -const_string!(PromptListChangedNotificationMethod = "notifications/prompts/list_changed"); -/// Notification sent when the list of available prompts changes -pub type PromptListChangedNotification = NotificationNoParam; - -const_string!(ToolListChangedNotificationMethod = "notifications/tools/list_changed"); -/// Notification sent when the list of available tools changes -pub type ToolListChangedNotification = NotificationNoParam; - -// ============================================================================= -// LOGGING -// ============================================================================= - -/// Logging levels supported by the MCP protocol -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Copy)] -#[serde(rename_all = "lowercase")] //match spec -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum LoggingLevel { - Debug, - Info, - Notice, - Warning, - Error, - Critical, - Alert, - Emergency, -} - -const_string!(SetLevelRequestMethod = "logging/setLevel"); -/// Parameters for setting the logging level -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct SetLevelRequestParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - /// The desired logging level - pub level: LoggingLevel, -} - -impl RequestParamsMeta for SetLevelRequestParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -/// Deprecated: Use [`SetLevelRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use SetLevelRequestParams instead")] -pub type SetLevelRequestParam = SetLevelRequestParams; - -/// Request to set the logging level -pub type SetLevelRequest = Request; - -const_string!(LoggingMessageNotificationMethod = "notifications/message"); -/// Parameters for a logging message notification -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct LoggingMessageNotificationParam { - /// The severity level of this log message - pub level: LoggingLevel, - /// Optional logger name that generated this message - #[serde(skip_serializing_if = "Option::is_none")] - pub logger: Option, - /// The actual log data - pub data: Value, -} -/// Notification containing a log message -pub type LoggingMessageNotification = - Notification; - -// ============================================================================= -// SAMPLING (LLM INTERACTION) -// ============================================================================= - -const_string!(CreateMessageRequestMethod = "sampling/createMessage"); -pub type CreateMessageRequest = Request; - -/// Represents the role of a participant in a conversation or message exchange. -/// -/// Used in sampling and chat contexts to distinguish between different -/// types of message senders in the conversation flow. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum Role { - /// A human user or client making a request - User, - /// An AI assistant or server providing a response - Assistant, -} - -/// Tool selection mode (SEP-1577). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum ToolChoiceMode { - /// Model decides whether to use tools - Auto, - /// Model must use at least one tool - Required, - /// Model must not use tools - None, -} - -impl Default for ToolChoiceMode { - fn default() -> Self { - Self::Auto - } -} - -/// Tool choice configuration (SEP-1577). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ToolChoice { - #[serde(skip_serializing_if = "Option::is_none")] - pub mode: Option, -} - -impl ToolChoice { - pub fn auto() -> Self { - Self { - mode: Some(ToolChoiceMode::Auto), - } - } - - pub fn required() -> Self { - Self { - mode: Some(ToolChoiceMode::Required), - } - } - - pub fn none() -> Self { - Self { - mode: Some(ToolChoiceMode::None), - } - } -} - -/// Single or array content wrapper (SEP-1577). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum SamplingContent { - Single(T), - Multiple(Vec), -} - -impl SamplingContent { - /// Convert to a Vec regardless of whether it's single or multiple - pub fn into_vec(self) -> Vec { - match self { - SamplingContent::Single(item) => vec![item], - SamplingContent::Multiple(items) => items, - } - } - - /// Check if the content is empty - pub fn is_empty(&self) -> bool { - match self { - SamplingContent::Single(_) => false, - SamplingContent::Multiple(items) => items.is_empty(), - } - } - - /// Get the number of content items - pub fn len(&self) -> usize { - match self { - SamplingContent::Single(_) => 1, - SamplingContent::Multiple(items) => items.len(), - } - } -} - -impl Default for SamplingContent { - fn default() -> Self { - SamplingContent::Multiple(Vec::new()) - } -} - -impl SamplingContent { - /// Get the first item if present - pub fn first(&self) -> Option<&T> { - match self { - SamplingContent::Single(item) => Some(item), - SamplingContent::Multiple(items) => items.first(), - } - } - - /// Iterate over all content items - pub fn iter(&self) -> impl Iterator { - let items: Vec<&T> = match self { - SamplingContent::Single(item) => vec![item], - SamplingContent::Multiple(items) => items.iter().collect(), - }; - items.into_iter() - } -} - -impl SamplingMessageContent { - /// Get the text content if this is a Text variant - pub fn as_text(&self) -> Option<&RawTextContent> { - match self { - SamplingMessageContent::Text(text) => Some(text), - _ => None, - } - } - - /// Get the tool use content if this is a ToolUse variant - pub fn as_tool_use(&self) -> Option<&ToolUseContent> { - match self { - SamplingMessageContent::ToolUse(tool_use) => Some(tool_use), - _ => None, - } - } - - /// Get the tool result content if this is a ToolResult variant - pub fn as_tool_result(&self) -> Option<&ToolResultContent> { - match self { - SamplingMessageContent::ToolResult(tool_result) => Some(tool_result), - _ => None, - } - } -} - -impl From for SamplingContent { - fn from(item: T) -> Self { - SamplingContent::Single(item) - } -} - -impl From> for SamplingContent { - fn from(items: Vec) -> Self { - SamplingContent::Multiple(items) - } -} - -/// A message in a sampling conversation, containing a role and content. -/// -/// This represents a single message in a conversation flow, used primarily -/// in LLM sampling requests where the conversation history is important -/// for generating appropriate responses. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct SamplingMessage { - /// The role of the message sender (User or Assistant) - pub role: Role, - /// The actual content of the message (text, image, audio, tool use, or tool result) - pub content: SamplingContent, - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, -} - -/// Content types for sampling messages (SEP-1577). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum SamplingMessageContent { - Text(RawTextContent), - Image(RawImageContent), - Audio(RawAudioContent), - /// Assistant only - ToolUse(ToolUseContent), - /// User only - ToolResult(ToolResultContent), -} - -impl SamplingMessageContent { - /// Create a text content - pub fn text(text: impl Into) -> Self { - Self::Text(RawTextContent { - text: text.into(), - meta: None, - }) - } - - pub fn tool_use(id: impl Into, name: impl Into, input: JsonObject) -> Self { - Self::ToolUse(ToolUseContent::new(id, name, input)) - } - - pub fn tool_result(tool_use_id: impl Into, content: Vec) -> Self { - Self::ToolResult(ToolResultContent::new(tool_use_id, content)) - } -} - -impl SamplingMessage { - pub fn new(role: Role, content: impl Into) -> Self { - Self { - role, - content: SamplingContent::Single(content.into()), - meta: None, - } - } - - pub fn new_multiple(role: Role, contents: Vec) -> Self { - Self { - role, - content: SamplingContent::Multiple(contents), - meta: None, - } - } - - pub fn user_text(text: impl Into) -> Self { - Self::new(Role::User, SamplingMessageContent::text(text)) - } - - pub fn assistant_text(text: impl Into) -> Self { - Self::new(Role::Assistant, SamplingMessageContent::text(text)) - } - - pub fn user_tool_result(tool_use_id: impl Into, content: Vec) -> Self { - Self::new( - Role::User, - SamplingMessageContent::tool_result(tool_use_id, content), - ) - } - - pub fn assistant_tool_use( - id: impl Into, - name: impl Into, - input: JsonObject, - ) -> Self { - Self::new( - Role::Assistant, - SamplingMessageContent::tool_use(id, name, input), - ) - } -} - -// Conversion from RawTextContent to SamplingMessageContent -impl From for SamplingMessageContent { - fn from(text: RawTextContent) -> Self { - SamplingMessageContent::Text(text) - } -} - -// Conversion from String to SamplingMessageContent (as text) -impl From for SamplingMessageContent { - fn from(text: String) -> Self { - SamplingMessageContent::text(text) - } -} - -impl From<&str> for SamplingMessageContent { - fn from(text: &str) -> Self { - SamplingMessageContent::text(text) - } -} - -// Backward compatibility: Convert Content to SamplingMessageContent -// Note: Resource and ResourceLink variants are not supported in sampling messages -impl TryFrom for SamplingMessageContent { - type Error = &'static str; - - fn try_from(content: Content) -> Result { - match content.raw { - RawContent::Text(text) => Ok(SamplingMessageContent::Text(text)), - RawContent::Image(image) => Ok(SamplingMessageContent::Image(image)), - RawContent::Audio(audio) => Ok(SamplingMessageContent::Audio(audio)), - RawContent::Resource(_) => { - Err("Resource content is not supported in sampling messages") - } - RawContent::ResourceLink(_) => { - Err("ResourceLink content is not supported in sampling messages") - } - } - } -} - -// Backward compatibility: Convert Content to SamplingContent -impl TryFrom for SamplingContent { - type Error = &'static str; - - fn try_from(content: Content) -> Result { - Ok(SamplingContent::Single(content.try_into()?)) - } -} - -/// Specifies how much context should be included in sampling requests. -/// -/// This allows clients to control what additional context information -/// should be provided to the LLM when processing sampling requests. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum ContextInclusion { - /// Include context from all connected MCP servers - #[serde(rename = "allServers")] - AllServers, - /// Include no additional context - #[serde(rename = "none")] - None, - /// Include context only from the requesting server - #[serde(rename = "thisServer")] - ThisServer, -} - -/// Parameters for creating a message through LLM sampling. -/// -/// This structure contains all the necessary information for a client to -/// generate an LLM response, including conversation history, model preferences, -/// and generation parameters. -/// -/// This implements `TaskAugmentedRequestParamsMeta` as sampling requests can be -/// long-running and may benefit from task-based execution. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CreateMessageRequestParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - /// Task metadata for async task management (SEP-1319) - #[serde(skip_serializing_if = "Option::is_none")] - pub task: Option, - /// The conversation history and current messages - pub messages: Vec, - /// Preferences for model selection and behavior - #[serde(skip_serializing_if = "Option::is_none")] - pub model_preferences: Option, - /// System prompt to guide the model's behavior - #[serde(skip_serializing_if = "Option::is_none")] - pub system_prompt: Option, - /// How much context to include from MCP servers - #[serde(skip_serializing_if = "Option::is_none")] - pub include_context: Option, - /// Temperature for controlling randomness (0.0 to 1.0) - #[serde(skip_serializing_if = "Option::is_none")] - pub temperature: Option, - /// Maximum number of tokens to generate - pub max_tokens: u32, - /// Sequences that should stop generation - #[serde(skip_serializing_if = "Option::is_none")] - pub stop_sequences: Option>, - /// Additional metadata for the request - #[serde(skip_serializing_if = "Option::is_none")] - pub metadata: Option, - /// Tools available for the model to call (SEP-1577) - #[serde(skip_serializing_if = "Option::is_none")] - pub tools: Option>, - /// Tool selection behavior (SEP-1577) - #[serde(skip_serializing_if = "Option::is_none")] - pub tool_choice: Option, -} - -impl RequestParamsMeta for CreateMessageRequestParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -impl TaskAugmentedRequestParamsMeta for CreateMessageRequestParams { - fn task(&self) -> Option<&JsonObject> { - self.task.as_ref() - } - fn task_mut(&mut self) -> &mut Option { - &mut self.task - } -} - -impl CreateMessageRequestParams { - /// Validate the sampling request parameters per SEP-1577 spec requirements. - /// - /// Checks: - /// - ToolUse content is only allowed in assistant messages - /// - ToolResult content is only allowed in user messages - /// - Messages with tool result content MUST NOT contain other content types - /// - Every assistant ToolUse must be balanced with a corresponding user ToolResult - pub fn validate(&self) -> Result<(), String> { - for msg in &self.messages { - for content in msg.content.iter() { - // ToolUse only in assistant messages, ToolResult only in user messages - match content { - SamplingMessageContent::ToolUse(_) if msg.role != Role::Assistant => { - return Err("ToolUse content is only allowed in assistant messages".into()); - } - SamplingMessageContent::ToolResult(_) if msg.role != Role::User => { - return Err("ToolResult content is only allowed in user messages".into()); - } - _ => {} - } - } - - // Tool result messages MUST NOT contain other content types - let contents: Vec<_> = msg.content.iter().collect(); - let has_tool_result = contents - .iter() - .any(|c| matches!(c, SamplingMessageContent::ToolResult(_))); - if has_tool_result - && contents - .iter() - .any(|c| !matches!(c, SamplingMessageContent::ToolResult(_))) - { - return Err( - "SamplingMessage with tool result content MUST NOT contain other content types" - .into(), - ); - } - } - - // Every assistant ToolUse must be balanced with a user ToolResult - self.validate_tool_use_result_balance()?; - - Ok(()) - } - - fn validate_tool_use_result_balance(&self) -> Result<(), String> { - let mut pending_tool_use_ids: Vec = Vec::new(); - for msg in &self.messages { - if msg.role == Role::Assistant { - for content in msg.content.iter() { - if let SamplingMessageContent::ToolUse(tu) = content { - pending_tool_use_ids.push(tu.id.clone()); - } - } - } else if msg.role == Role::User { - for content in msg.content.iter() { - if let SamplingMessageContent::ToolResult(tr) = content { - if !pending_tool_use_ids.contains(&tr.tool_use_id) { - return Err(format!( - "ToolResult with toolUseId '{}' has no matching ToolUse", - tr.tool_use_id - )); - } - pending_tool_use_ids.retain(|id| id != &tr.tool_use_id); - } - } - } - } - if !pending_tool_use_ids.is_empty() { - return Err(format!( - "ToolUse with id(s) {:?} not balanced with ToolResult", - pending_tool_use_ids - )); - } - Ok(()) - } -} - -/// Deprecated: Use [`CreateMessageRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use CreateMessageRequestParams instead")] -pub type CreateMessageRequestParam = CreateMessageRequestParams; - -/// Preferences for model selection and behavior in sampling requests. -/// -/// This allows servers to express their preferences for which model to use -/// and how to balance different priorities when the client has multiple -/// model options available. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ModelPreferences { - /// Specific model names or families to prefer (e.g., "claude", "gpt") - #[serde(skip_serializing_if = "Option::is_none")] - pub hints: Option>, - /// Priority for cost optimization (0.0 to 1.0, higher = prefer cheaper models) - #[serde(skip_serializing_if = "Option::is_none")] - pub cost_priority: Option, - /// Priority for speed/latency (0.0 to 1.0, higher = prefer faster models) - #[serde(skip_serializing_if = "Option::is_none")] - pub speed_priority: Option, - /// Priority for intelligence/capability (0.0 to 1.0, higher = prefer more capable models) - #[serde(skip_serializing_if = "Option::is_none")] - pub intelligence_priority: Option, -} - -/// A hint suggesting a preferred model name or family. -/// -/// Model hints are advisory suggestions that help clients choose appropriate -/// models. They can be specific model names or general families like "claude" or "gpt". -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ModelHint { - /// The suggested model name or family identifier - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, -} - -// ============================================================================= -// COMPLETION AND AUTOCOMPLETE -// ============================================================================= - -/// Context for completion requests providing previously resolved arguments. -/// -/// This enables context-aware completion where subsequent argument completions -/// can take into account the values of previously resolved arguments. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CompletionContext { - /// Previously resolved argument values that can inform completion suggestions - #[serde(skip_serializing_if = "Option::is_none")] - pub arguments: Option>, -} - -impl CompletionContext { - /// Create a new empty completion context - pub fn new() -> Self { - Self::default() - } - - /// Create a completion context with the given arguments - pub fn with_arguments(arguments: std::collections::HashMap) -> Self { - Self { - arguments: Some(arguments), - } - } - - /// Get a specific argument value by name - pub fn get_argument(&self, name: &str) -> Option<&String> { - self.arguments.as_ref()?.get(name) - } - - /// Check if the context has any arguments - pub fn has_arguments(&self) -> bool { - self.arguments.as_ref().is_some_and(|args| !args.is_empty()) - } - - /// Get all argument names - pub fn argument_names(&self) -> impl Iterator { - self.arguments - .as_ref() - .into_iter() - .flat_map(|args| args.keys()) - .map(|k| k.as_str()) - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CompleteRequestParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - pub r#ref: Reference, - pub argument: ArgumentInfo, - /// Optional context containing previously resolved argument values - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option, -} - -impl RequestParamsMeta for CompleteRequestParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -/// Deprecated: Use [`CompleteRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use CompleteRequestParams instead")] -pub type CompleteRequestParam = CompleteRequestParams; - -pub type CompleteRequest = Request; - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CompletionInfo { - pub values: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub total: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub has_more: Option, -} - -impl CompletionInfo { - /// Maximum number of completion values allowed per response according to MCP specification - pub const MAX_VALUES: usize = 100; - - /// Create a new CompletionInfo with validation for maximum values - pub fn new(values: Vec) -> Result { - if values.len() > Self::MAX_VALUES { - return Err(format!( - "Too many completion values: {} (max: {})", - values.len(), - Self::MAX_VALUES - )); - } - Ok(Self { - values, - total: None, - has_more: None, - }) - } - - /// Create CompletionInfo with all values and no pagination - pub fn with_all_values(values: Vec) -> Result { - let completion = Self::new(values)?; - Ok(Self { - total: Some(completion.values.len() as u32), - has_more: Some(false), - ..completion - }) - } - - /// Create CompletionInfo with pagination information - pub fn with_pagination( - values: Vec, - total: Option, - has_more: bool, - ) -> Result { - let completion = Self::new(values)?; - Ok(Self { - total, - has_more: Some(has_more), - ..completion - }) - } - - /// Check if this completion response indicates more results are available - pub fn has_more_results(&self) -> bool { - self.has_more.unwrap_or(false) - } - - /// Get the total number of available completions, if known - pub fn total_available(&self) -> Option { - self.total - } - - /// Validate that the completion info complies with MCP specification - pub fn validate(&self) -> Result<(), String> { - if self.values.len() > Self::MAX_VALUES { - return Err(format!( - "Too many completion values: {} (max: {})", - self.values.len(), - Self::MAX_VALUES - )); - } - Ok(()) - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CompleteResult { - pub completion: CompletionInfo, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(tag = "type")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum Reference { - #[serde(rename = "ref/resource")] - Resource(ResourceReference), - #[serde(rename = "ref/prompt")] - Prompt(PromptReference), -} - -impl Reference { - /// Create a prompt reference - pub fn for_prompt(name: impl Into) -> Self { - // Not accepting `title` currently as it'll break the API - // Until further decision, keep it `None`, modify later - // if required, add `title` to the API - Self::Prompt(PromptReference { - name: name.into(), - title: None, - }) - } - - /// Create a resource reference - pub fn for_resource(uri: impl Into) -> Self { - Self::Resource(ResourceReference { uri: uri.into() }) - } - - /// Get the reference type as a string - pub fn reference_type(&self) -> &'static str { - match self { - Self::Prompt(_) => "ref/prompt", - Self::Resource(_) => "ref/resource", - } - } - - /// Extract prompt name if this is a prompt reference - pub fn as_prompt_name(&self) -> Option<&str> { - match self { - Self::Prompt(prompt_ref) => Some(&prompt_ref.name), - _ => None, - } - } - - /// Extract resource URI if this is a resource reference - pub fn as_resource_uri(&self) -> Option<&str> { - match self { - Self::Resource(resource_ref) => Some(&resource_ref.uri), - _ => None, - } - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ResourceReference { - pub uri: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct PromptReference { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, -} - -const_string!(CompleteRequestMethod = "completion/complete"); -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ArgumentInfo { - pub name: String, - pub value: String, -} - -// ============================================================================= -// ROOTS AND WORKSPACE MANAGEMENT -// ============================================================================= - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Root { - pub uri: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, -} - -const_string!(ListRootsRequestMethod = "roots/list"); -pub type ListRootsRequest = RequestNoParam; - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ListRootsResult { - pub roots: Vec, -} - -const_string!(RootsListChangedNotificationMethod = "notifications/roots/list_changed"); -pub type RootsListChangedNotification = NotificationNoParam; - -// ============================================================================= -// ELICITATION (INTERACTIVE USER INPUT) -// ============================================================================= - -// Method constants for elicitation operations. -// Elicitation allows servers to request interactive input from users during tool execution. -const_string!(ElicitationCreateRequestMethod = "elicitation/create"); -const_string!(ElicitationResponseNotificationMethod = "notifications/elicitation/response"); -const_string!(ElicitationCompletionNotificationMethod = "notifications/elicitation/complete"); - -/// Represents the possible actions a user can take in response to an elicitation request. -/// -/// When a server requests user input through elicitation, the user can: -/// - Accept: Provide the requested information and continue -/// - Decline: Refuse to provide the information but continue the operation -/// - Cancel: Stop the entire operation -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum ElicitationAction { - /// User accepts the request and provides the requested information - Accept, - /// User declines to provide the information but allows the operation to continue - Decline, - /// User cancels the entire operation - Cancel, -} - -/// Helper enum for deserializing CreateElicitationRequestParam with backward compatibility. -/// When mode is missing, it defaults to FormElicitationParam. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(tag = "mode")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -enum CreateElicitationRequestParamDeserializeHelper { - #[serde(rename = "form", rename_all = "camelCase")] - FormElicitationParam { - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option, - message: String, - requested_schema: ElicitationSchema, - }, - #[serde(rename = "url", rename_all = "camelCase")] - UrlElicitationParam { - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option, - message: String, - url: String, - elicitation_id: String, - }, - #[serde(untagged, rename_all = "camelCase")] - FormElicitationParamBackwardsCompat { - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option, - message: String, - requested_schema: ElicitationSchema, - }, -} - -impl TryFrom for CreateElicitationRequestParams { - type Error = serde_json::Error; - - fn try_from( - value: CreateElicitationRequestParamDeserializeHelper, - ) -> Result { - match value { - CreateElicitationRequestParamDeserializeHelper::FormElicitationParam { - meta, - message, - requested_schema, - } - | CreateElicitationRequestParamDeserializeHelper::FormElicitationParamBackwardsCompat { - meta, - message, - requested_schema, - } => Ok(CreateElicitationRequestParams::FormElicitationParams { - meta, - message, - requested_schema, - }), - CreateElicitationRequestParamDeserializeHelper::UrlElicitationParam { - meta, - message, - url, - elicitation_id, - } => Ok(CreateElicitationRequestParams::UrlElicitationParams { - meta, - message, - url, - elicitation_id, - }), - } - } -} - -/// Parameters for creating an elicitation request to gather user input. -/// -/// This structure contains everything needed to request interactive input from a user: -/// - A human-readable message explaining what information is needed -/// - A type-safe schema defining the expected structure of the response -/// -/// # Example -/// 1. Form-based elicitation request -/// ```rust -/// use rmcp::model::*; -/// -/// let params = CreateElicitationRequestParams::FormElicitationParams { -/// meta: None, -/// message: "Please provide your email".to_string(), -/// requested_schema: ElicitationSchema::builder() -/// .required_email("email") -/// .build() -/// .unwrap(), -/// }; -/// ``` -/// 2. URL-based elicitation request -/// ```rust -/// use rmcp::model::*; -/// let params = CreateElicitationRequestParams::UrlElicitationParams { -/// meta: None, -/// message: "Please provide your feedback at the following URL".to_string(), -/// url: "https://example.com/feedback".to_string(), -/// elicitation_id: "unique-id-123".to_string(), -/// }; -/// ``` -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde( - tag = "mode", - try_from = "CreateElicitationRequestParamDeserializeHelper" -)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum CreateElicitationRequestParams { - #[serde(rename = "form", rename_all = "camelCase")] - FormElicitationParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option, - /// Human-readable message explaining what input is needed from the user. - /// This should be clear and provide sufficient context for the user to understand - /// what information they need to provide. - message: String, - - /// Type-safe schema defining the expected structure and validation rules for the user's response. - /// This enforces the MCP 2025-06-18 specification that elicitation schemas must be objects - /// with primitive-typed properties. - requested_schema: ElicitationSchema, - }, - #[serde(rename = "url", rename_all = "camelCase")] - UrlElicitationParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option, - /// Human-readable message explaining what input is needed from the user. - /// This should be clear and provide sufficient context for the user to understand - /// what information they need to provide. - message: String, - - /// The URL where the user can provide the requested information. - /// The client should direct the user to this URL to complete the elicitation. - url: String, - /// The unique identifier for this elicitation request. - elicitation_id: String, - }, -} - -impl RequestParamsMeta for CreateElicitationRequestParams { - fn meta(&self) -> Option<&Meta> { - match self { - CreateElicitationRequestParams::FormElicitationParams { meta, .. } => meta.as_ref(), - CreateElicitationRequestParams::UrlElicitationParams { meta, .. } => meta.as_ref(), - } - } - fn meta_mut(&mut self) -> &mut Option { - match self { - CreateElicitationRequestParams::FormElicitationParams { meta, .. } => meta, - CreateElicitationRequestParams::UrlElicitationParams { meta, .. } => meta, - } - } -} - -/// Deprecated: Use [`CreateElicitationRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use CreateElicitationRequestParams instead")] -pub type CreateElicitationRequestParam = CreateElicitationRequestParams; - -/// The result returned by a client in response to an elicitation request. -/// -/// Contains the user's decision (accept/decline/cancel) and optionally their input data -/// if they chose to accept the request. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CreateElicitationResult { - /// The user's decision on how to handle the elicitation request - pub action: ElicitationAction, - - /// The actual data provided by the user, if they accepted the request. - /// Must conform to the JSON schema specified in the original request. - /// Only present when action is Accept. - #[serde(skip_serializing_if = "Option::is_none")] - pub content: Option, -} - -/// Request type for creating an elicitation to gather user input -pub type CreateElicitationRequest = - Request; - -/// Notification parameters for an url elicitation completion notification. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ElicitationResponseNotificationParam { - pub elicitation_id: String, -} - -/// Notification sent when an url elicitation process is completed. -pub type ElicitationCompletionNotification = - Notification; - -// ============================================================================= -// TOOL EXECUTION RESULTS -// ============================================================================= - -/// The result of a tool call operation. -/// -/// Contains the content returned by the tool execution and an optional -/// flag indicating whether the operation resulted in an error. -#[derive(Debug, Serialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CallToolResult { - /// The content returned by the tool (text, images, etc.) - pub content: Vec, - /// An optional JSON object that represents the structured result of the tool call - #[serde(skip_serializing_if = "Option::is_none")] - pub structured_content: Option, - /// Whether this result represents an error condition - #[serde(skip_serializing_if = "Option::is_none")] - pub is_error: Option, - /// Optional protocol-level metadata for this result - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, -} - -impl CallToolResult { - /// Create a successful tool result with unstructured content - pub fn success(content: Vec) -> Self { - CallToolResult { - content, - structured_content: None, - is_error: Some(false), - meta: None, - } - } - /// Create an error tool result with unstructured content - pub fn error(content: Vec) -> Self { - CallToolResult { - content, - structured_content: None, - is_error: Some(true), - meta: None, - } - } - /// Create a successful tool result with structured content - /// - /// # Example - /// - /// ```rust,ignore - /// use rmcp::model::CallToolResult; - /// use serde_json::json; - /// - /// let result = CallToolResult::structured(json!({ - /// "temperature": 22.5, - /// "humidity": 65, - /// "description": "Partly cloudy" - /// })); - /// ``` - pub fn structured(value: Value) -> Self { - CallToolResult { - content: vec![Content::text(value.to_string())], - structured_content: Some(value), - is_error: Some(false), - meta: None, - } - } - /// Create an error tool result with structured content - /// - /// # Example - /// - /// ```rust,ignore - /// use rmcp::model::CallToolResult; - /// use serde_json::json; - /// - /// let result = CallToolResult::structured_error(json!({ - /// "error_code": "INVALID_INPUT", - /// "message": "Temperature value out of range", - /// "details": { - /// "min": -50, - /// "max": 50, - /// "provided": 100 - /// } - /// })); - /// ``` - pub fn structured_error(value: Value) -> Self { - CallToolResult { - content: vec![Content::text(value.to_string())], - structured_content: Some(value), - is_error: Some(true), - meta: None, - } - } - - /// Convert the `structured_content` part of response into a certain type. - /// - /// # About json schema validation - /// Since rust is a strong type language, we don't need to do json schema validation here. - /// - /// But if you do have to validate the response data, you can use [`jsonschema`](https://crates.io/crates/jsonschema) crate. - pub fn into_typed(self) -> Result - where - T: DeserializeOwned, - { - let raw_text = match (self.structured_content, &self.content.first()) { - (Some(value), _) => return serde_json::from_value(value), - (None, Some(contents)) => { - if let Some(text) = contents.as_text() { - let text = &text.text; - Some(text) - } else { - None - } - } - (None, None) => None, - }; - if let Some(text) = raw_text { - return serde_json::from_str(text); - } - serde_json::from_value(serde_json::Value::Null) - } -} - -// Custom deserialize implementation to validate mutual exclusivity -impl<'de> Deserialize<'de> for CallToolResult { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct CallToolResultHelper { - #[serde(skip_serializing_if = "Option::is_none")] - content: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - structured_content: Option, - #[serde(skip_serializing_if = "Option::is_none")] - is_error: Option, - /// Accept `_meta` during deserialization - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - meta: Option, - } - - let helper = CallToolResultHelper::deserialize(deserializer)?; - let result = CallToolResult { - content: helper.content.unwrap_or_default(), - structured_content: helper.structured_content, - is_error: helper.is_error, - meta: helper.meta, - }; - - // Validate mutual exclusivity - if result.content.is_empty() && result.structured_content.is_none() { - return Err(serde::de::Error::custom( - "CallToolResult must have either content or structured_content", - )); - } - - Ok(result) - } -} - -const_string!(ListToolsRequestMethod = "tools/list"); -/// Request to list all available tools from a server -pub type ListToolsRequest = RequestOptionalParam; - -paginated_result!( - ListToolsResult { - tools: Vec - } -); - -const_string!(CallToolRequestMethod = "tools/call"); -/// Parameters for calling a tool provided by an MCP server. -/// -/// Contains the tool name and optional arguments needed to execute -/// the tool operation. -/// -/// This implements `TaskAugmentedRequestParamsMeta` as tool calls can be -/// long-running and may benefit from task-based execution. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CallToolRequestParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - /// The name of the tool to call - pub name: Cow<'static, str>, - /// Arguments to pass to the tool (must match the tool's input schema) - #[serde(skip_serializing_if = "Option::is_none")] - pub arguments: Option, - /// Task metadata for async task management (SEP-1319) - #[serde(skip_serializing_if = "Option::is_none")] - pub task: Option, -} - -impl RequestParamsMeta for CallToolRequestParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -impl TaskAugmentedRequestParamsMeta for CallToolRequestParams { - fn task(&self) -> Option<&JsonObject> { - self.task.as_ref() - } - fn task_mut(&mut self) -> &mut Option { - &mut self.task - } -} - -/// Deprecated: Use [`CallToolRequestParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use CallToolRequestParams instead")] -pub type CallToolRequestParam = CallToolRequestParams; - -/// Request to call a specific tool -pub type CallToolRequest = Request; - -/// Result of sampling/createMessage (SEP-1577). -/// The result of a sampling/createMessage request containing the generated response. -/// -/// This structure contains the generated message along with metadata about -/// how the generation was performed and why it stopped. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CreateMessageResult { - /// The identifier of the model that generated the response - pub model: String, - /// The reason why generation stopped (e.g., "endTurn", "maxTokens") - #[serde(skip_serializing_if = "Option::is_none")] - pub stop_reason: Option, - /// The generated message with role and content - #[serde(flatten)] - pub message: SamplingMessage, -} - -impl CreateMessageResult { - pub const STOP_REASON_END_TURN: &str = "endTurn"; - pub const STOP_REASON_END_SEQUENCE: &str = "stopSequence"; - pub const STOP_REASON_END_MAX_TOKEN: &str = "maxTokens"; - pub const STOP_REASON_TOOL_USE: &str = "toolUse"; - - /// Validate the result per SEP-1577: role must be "assistant". - pub fn validate(&self) -> Result<(), String> { - if self.message.role != Role::Assistant { - return Err("CreateMessageResult role must be 'assistant'".into()); - } - Ok(()) - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct GetPromptResult { - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - pub messages: Vec, -} - -// ============================================================================= -// TASK MANAGEMENT -// ============================================================================= - -const_string!(GetTaskInfoMethod = "tasks/get"); -pub type GetTaskInfoRequest = Request; - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct GetTaskInfoParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - pub task_id: String, -} - -impl RequestParamsMeta for GetTaskInfoParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -/// Deprecated: Use [`GetTaskInfoParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use GetTaskInfoParams instead")] -pub type GetTaskInfoParam = GetTaskInfoParams; - -const_string!(ListTasksMethod = "tasks/list"); -pub type ListTasksRequest = RequestOptionalParam; - -const_string!(GetTaskResultMethod = "tasks/result"); -pub type GetTaskResultRequest = Request; - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct GetTaskResultParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - pub task_id: String, -} - -impl RequestParamsMeta for GetTaskResultParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -/// Deprecated: Use [`GetTaskResultParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use GetTaskResultParams instead")] -pub type GetTaskResultParam = GetTaskResultParams; - -const_string!(CancelTaskMethod = "tasks/cancel"); -pub type CancelTaskRequest = Request; - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CancelTaskParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - pub task_id: String, -} - -impl RequestParamsMeta for CancelTaskParams { - fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() - } - fn meta_mut(&mut self) -> &mut Option { - &mut self.meta - } -} - -/// Deprecated: Use [`CancelTaskParams`] instead (SEP-1319 compliance). -#[deprecated(since = "0.13.0", note = "Use CancelTaskParams instead")] -pub type CancelTaskParam = CancelTaskParams; -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[serde(deny_unknown_fields)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct GetTaskInfoResult { - #[serde(skip_serializing_if = "Option::is_none")] - pub task: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ListTasksResult { - pub tasks: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub next_cursor: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub total: Option, -} - -// ============================================================================= -// MESSAGE TYPE UNIONS -// ============================================================================= - -macro_rules! ts_union { - ( - export type $U:ident = - $($rest:tt)* - ) => { - ts_union!(@declare $U { $($rest)* }); - ts_union!(@impl_from $U { $($rest)* }); - }; - (@declare $U:ident { $($variant:tt)* }) => { - ts_union!(@declare_variant $U { } {$($variant)*} ); - }; - (@declare_variant $U:ident { $($declared:tt)* } {$(|)? box $V:ident $($rest:tt)*}) => { - ts_union!(@declare_variant $U { $($declared)* $V(Box<$V>), } {$($rest)*}); - }; - (@declare_variant $U:ident { $($declared:tt)* } {$(|)? $V:ident $($rest:tt)*}) => { - ts_union!(@declare_variant $U { $($declared)* $V($V), } {$($rest)*}); - }; - (@declare_variant $U:ident { $($declared:tt)* } { ; }) => { - ts_union!(@declare_end $U { $($declared)* } ); - }; - (@declare_end $U:ident { $($declared:tt)* }) => { - #[derive(Debug, Serialize, Deserialize, Clone)] - #[serde(untagged)] - #[allow(clippy::large_enum_variant)] - #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] - pub enum $U { - $($declared)* - } - }; - (@impl_from $U: ident {$(|)? box $V:ident $($rest:tt)*}) => { - impl From<$V> for $U { - fn from(value: $V) -> Self { - $U::$V(Box::new(value)) - } - } - ts_union!(@impl_from $U {$($rest)*}); - }; - (@impl_from $U: ident {$(|)? $V:ident $($rest:tt)*}) => { - impl From<$V> for $U { - fn from(value: $V) -> Self { - $U::$V(value) - } - } - ts_union!(@impl_from $U {$($rest)*}); - }; - (@impl_from $U: ident { ; }) => {}; - (@impl_from $U: ident { }) => {}; -} - -ts_union!( - export type ClientRequest = - | PingRequest - | InitializeRequest - | CompleteRequest - | SetLevelRequest - | GetPromptRequest - | ListPromptsRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscribeRequest - | UnsubscribeRequest - | CallToolRequest - | ListToolsRequest - | GetTaskInfoRequest - | ListTasksRequest - | GetTaskResultRequest - | CancelTaskRequest - | CustomRequest; -); - -impl ClientRequest { - pub fn method(&self) -> &str { - match &self { - ClientRequest::PingRequest(r) => r.method.as_str(), - ClientRequest::InitializeRequest(r) => r.method.as_str(), - ClientRequest::CompleteRequest(r) => r.method.as_str(), - ClientRequest::SetLevelRequest(r) => r.method.as_str(), - ClientRequest::GetPromptRequest(r) => r.method.as_str(), - ClientRequest::ListPromptsRequest(r) => r.method.as_str(), - ClientRequest::ListResourcesRequest(r) => r.method.as_str(), - ClientRequest::ListResourceTemplatesRequest(r) => r.method.as_str(), - ClientRequest::ReadResourceRequest(r) => r.method.as_str(), - ClientRequest::SubscribeRequest(r) => r.method.as_str(), - ClientRequest::UnsubscribeRequest(r) => r.method.as_str(), - ClientRequest::CallToolRequest(r) => r.method.as_str(), - ClientRequest::ListToolsRequest(r) => r.method.as_str(), - ClientRequest::GetTaskInfoRequest(r) => r.method.as_str(), - ClientRequest::ListTasksRequest(r) => r.method.as_str(), - ClientRequest::GetTaskResultRequest(r) => r.method.as_str(), - ClientRequest::CancelTaskRequest(r) => r.method.as_str(), - ClientRequest::CustomRequest(r) => r.method.as_str(), - } - } -} - -ts_union!( - export type ClientNotification = - | CancelledNotification - | ProgressNotification - | InitializedNotification - | RootsListChangedNotification - | CustomNotification; -); - -ts_union!( - export type ClientResult = - box CreateMessageResult - | ListRootsResult - | CreateElicitationResult - | EmptyResult - | CustomResult; -); - -impl ClientResult { - pub fn empty(_: ()) -> ClientResult { - ClientResult::EmptyResult(EmptyResult {}) - } -} - -pub type ClientJsonRpcMessage = JsonRpcMessage; - -ts_union!( - export type ServerRequest = - | PingRequest - | CreateMessageRequest - | ListRootsRequest - | CreateElicitationRequest - | CustomRequest; -); - -ts_union!( - export type ServerNotification = - | CancelledNotification - | ProgressNotification - | LoggingMessageNotification - | ResourceUpdatedNotification - | ResourceListChangedNotification - | ToolListChangedNotification - | PromptListChangedNotification - | ElicitationCompletionNotification - | CustomNotification; -); - -ts_union!( - export type ServerResult = - | InitializeResult - | CompleteResult - | GetPromptResult - | ListPromptsResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | CallToolResult - | ListToolsResult - | CreateElicitationResult - | EmptyResult - | CreateTaskResult - | ListTasksResult - | GetTaskInfoResult - | TaskResult - | CustomResult - ; -); - -impl ServerResult { - pub fn empty(_: ()) -> ServerResult { - ServerResult::EmptyResult(EmptyResult {}) - } -} - -pub type ServerJsonRpcMessage = JsonRpcMessage; - -impl TryInto for ServerNotification { - type Error = ServerNotification; - fn try_into(self) -> Result { - if let ServerNotification::CancelledNotification(t) = self { - Ok(t) - } else { - Err(self) - } - } -} - -impl TryInto for ClientNotification { - type Error = ClientNotification; - fn try_into(self) -> Result { - if let ClientNotification::CancelledNotification(t) = self { - Ok(t) - } else { - Err(self) - } - } -} - -// ============================================================================= -// TESTS -// ============================================================================= - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - - #[test] - fn test_notification_serde() { - let raw = json!( { - "jsonrpc": JsonRpcVersion2_0, - "method": InitializedNotificationMethod, - }); - let message: ClientJsonRpcMessage = - serde_json::from_value(raw.clone()).expect("invalid notification"); - match &message { - ClientJsonRpcMessage::Notification(JsonRpcNotification { - notification: ClientNotification::InitializedNotification(_n), - .. - }) => {} - _ => panic!("Expected Notification"), - } - let json = serde_json::to_value(message).expect("valid json"); - assert_eq!(json, raw); - } - - #[test] - fn test_custom_client_notification_roundtrip() { - let raw = json!( { - "jsonrpc": JsonRpcVersion2_0, - "method": "notifications/custom", - "params": {"foo": "bar"}, - }); - - let message: ClientJsonRpcMessage = - serde_json::from_value(raw.clone()).expect("invalid notification"); - match &message { - ClientJsonRpcMessage::Notification(JsonRpcNotification { - notification: ClientNotification::CustomNotification(notification), - .. - }) => { - assert_eq!(notification.method, "notifications/custom"); - assert_eq!( - notification - .params - .as_ref() - .and_then(|p| p.get("foo")) - .expect("foo present"), - "bar" - ); - } - _ => panic!("Expected custom client notification"), - } - - let json = serde_json::to_value(message).expect("valid json"); - assert_eq!(json, raw); - } - - #[test] - fn test_custom_server_notification_roundtrip() { - let raw = json!( { - "jsonrpc": JsonRpcVersion2_0, - "method": "notifications/custom-server", - "params": {"hello": "world"}, - }); - - let message: ServerJsonRpcMessage = - serde_json::from_value(raw.clone()).expect("invalid notification"); - match &message { - ServerJsonRpcMessage::Notification(JsonRpcNotification { - notification: ServerNotification::CustomNotification(notification), - .. - }) => { - assert_eq!(notification.method, "notifications/custom-server"); - assert_eq!( - notification - .params - .as_ref() - .and_then(|p| p.get("hello")) - .expect("hello present"), - "world" - ); - } - _ => panic!("Expected custom server notification"), - } - - let json = serde_json::to_value(message).expect("valid json"); - assert_eq!(json, raw); - } - - #[test] - fn test_custom_request_roundtrip() { - let raw = json!( { - "jsonrpc": JsonRpcVersion2_0, - "id": 42, - "method": "requests/custom", - "params": {"foo": "bar"}, - }); - - let message: ClientJsonRpcMessage = - serde_json::from_value(raw.clone()).expect("invalid request"); - match &message { - ClientJsonRpcMessage::Request(JsonRpcRequest { id, request, .. }) => { - assert_eq!(id, &RequestId::Number(42)); - match request { - ClientRequest::CustomRequest(custom) => { - let expected_request = json!({ - "method": "requests/custom", - "params": {"foo": "bar"}, - }); - let actual_request = - serde_json::to_value(custom).expect("serialize custom request"); - assert_eq!(actual_request, expected_request); - } - other => panic!("Expected custom request, got: {other:?}"), - } - } - other => panic!("Expected request, got: {other:?}"), - } - - let json = serde_json::to_value(message).expect("valid json"); - assert_eq!(json, raw); - } - - #[test] - fn test_request_conversion() { - let raw = json!( { - "jsonrpc": JsonRpcVersion2_0, - "id": 1, - "method": "request", - "params": {"key": "value"}, - }); - let message: JsonRpcMessage = serde_json::from_value(raw.clone()).expect("invalid request"); - - match &message { - JsonRpcMessage::Request(r) => { - assert_eq!(r.id, RequestId::Number(1)); - assert_eq!(r.request.method, "request"); - assert_eq!( - &r.request.params, - json!({"key": "value"}) - .as_object() - .expect("should be an object") - ); - } - _ => panic!("Expected Request"), - } - let json = serde_json::to_value(&message).expect("valid json"); - assert_eq!(json, raw); - } - - #[test] - fn test_initial_request_response_serde() { - let request = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "initialize", - "params": { - "protocolVersion": "2024-11-05", - "capabilities": { - "roots": { - "listChanged": true - }, - "sampling": {} - }, - "clientInfo": { - "name": "ExampleClient", - "version": "1.0.0" - } - } - }); - let raw_response_json = json!({ - "jsonrpc": "2.0", - "id": 1, - "result": { - "protocolVersion": "2024-11-05", - "capabilities": { - "logging": {}, - "prompts": { - "listChanged": true - }, - "resources": { - "subscribe": true, - "listChanged": true - }, - "tools": { - "listChanged": true - } - }, - "serverInfo": { - "name": "ExampleServer", - "version": "1.0.0" - } - } - }); - let request: ClientJsonRpcMessage = - serde_json::from_value(request.clone()).expect("invalid request"); - let (request, id) = request.into_request().expect("should be a request"); - assert_eq!(id, RequestId::Number(1)); - #[allow(deprecated)] - match request { - ClientRequest::InitializeRequest(Request { - method: _, - params: - InitializeRequestParam { - meta: _, - protocol_version: _, - capabilities, - client_info, - }, - .. - }) => { - assert_eq!(capabilities.roots.unwrap().list_changed, Some(true)); - let sampling = capabilities.sampling.unwrap(); - assert_eq!(sampling.tools, None); - assert_eq!(sampling.context, None); - assert_eq!(client_info.name, "ExampleClient"); - assert_eq!(client_info.version, "1.0.0"); - } - _ => panic!("Expected InitializeRequest"), - } - let server_response: ServerJsonRpcMessage = - serde_json::from_value(raw_response_json.clone()).expect("invalid response"); - let (response, id) = server_response - .clone() - .into_response() - .expect("expect response"); - assert_eq!(id, RequestId::Number(1)); - match response { - ServerResult::InitializeResult(InitializeResult { - protocol_version: _, - capabilities, - server_info, - instructions, - }) => { - assert_eq!(capabilities.logging.unwrap().len(), 0); - assert_eq!(capabilities.prompts.unwrap().list_changed, Some(true)); - assert_eq!( - capabilities.resources.as_ref().unwrap().subscribe, - Some(true) - ); - assert_eq!(capabilities.resources.unwrap().list_changed, Some(true)); - assert_eq!(capabilities.tools.unwrap().list_changed, Some(true)); - assert_eq!(server_info.name, "ExampleServer"); - assert_eq!(server_info.version, "1.0.0"); - assert_eq!(server_info.icons, None); - assert_eq!(instructions, None); - } - other => panic!("Expected InitializeResult, got {other:?}"), - } - - let server_response_json: Value = serde_json::to_value(&server_response).expect("msg"); - - assert_eq!(server_response_json, raw_response_json); - } - - #[test] - fn test_negative_and_large_request_ids() { - // Test negative ID - let negative_id_json = json!({ - "jsonrpc": "2.0", - "id": -1, - "method": "test", - "params": {} - }); - - let message: JsonRpcMessage = - serde_json::from_value(negative_id_json.clone()).expect("Should parse negative ID"); - - match &message { - JsonRpcMessage::Request(r) => { - assert_eq!(r.id, RequestId::Number(-1)); - } - _ => panic!("Expected Request"), - } - - // Test roundtrip serialization - let serialized = serde_json::to_value(&message).expect("Should serialize"); - assert_eq!(serialized, negative_id_json); - - // Test large negative ID - let large_negative_json = json!({ - "jsonrpc": "2.0", - "id": -9007199254740991i64, // JavaScript's MIN_SAFE_INTEGER - "method": "test", - "params": {} - }); - - let message: JsonRpcMessage = serde_json::from_value(large_negative_json.clone()) - .expect("Should parse large negative ID"); - - match &message { - JsonRpcMessage::Request(r) => { - assert_eq!(r.id, RequestId::Number(-9007199254740991i64)); - } - _ => panic!("Expected Request"), - } - - // Test large positive ID (JavaScript's MAX_SAFE_INTEGER) - let large_positive_json = json!({ - "jsonrpc": "2.0", - "id": 9007199254740991i64, - "method": "test", - "params": {} - }); - - let message: JsonRpcMessage = serde_json::from_value(large_positive_json.clone()) - .expect("Should parse large positive ID"); - - match &message { - JsonRpcMessage::Request(r) => { - assert_eq!(r.id, RequestId::Number(9007199254740991i64)); - } - _ => panic!("Expected Request"), - } - - // Test zero ID - let zero_id_json = json!({ - "jsonrpc": "2.0", - "id": 0, - "method": "test", - "params": {} - }); - - let message: JsonRpcMessage = - serde_json::from_value(zero_id_json.clone()).expect("Should parse zero ID"); - - match &message { - JsonRpcMessage::Request(r) => { - assert_eq!(r.id, RequestId::Number(0)); - } - _ => panic!("Expected Request"), - } - } - - #[test] - fn test_protocol_version_order() { - let v1 = ProtocolVersion::V_2024_11_05; - let v2 = ProtocolVersion::V_2025_03_26; - assert!(v1 < v2); - } - - #[test] - fn test_icon_serialization() { - let icon = Icon { - src: "https://example.com/icon.png".to_string(), - mime_type: Some("image/png".to_string()), - sizes: Some(vec!["48x48".to_string()]), - }; - - let json = serde_json::to_value(&icon).unwrap(); - assert_eq!(json["src"], "https://example.com/icon.png"); - assert_eq!(json["mimeType"], "image/png"); - assert_eq!(json["sizes"][0], "48x48"); - - // Test deserialization - let deserialized: Icon = serde_json::from_value(json).unwrap(); - assert_eq!(deserialized, icon); - } - - #[test] - fn test_icon_minimal() { - let icon = Icon { - src: "data:image/svg+xml;base64,PHN2Zy8+".to_string(), - mime_type: None, - sizes: None, - }; - - let json = serde_json::to_value(&icon).unwrap(); - assert_eq!(json["src"], "data:image/svg+xml;base64,PHN2Zy8+"); - assert!(json.get("mimeType").is_none()); - assert!(json.get("sizes").is_none()); - } - - #[test] - fn test_implementation_with_icons() { - let implementation = Implementation { - name: "test-server".to_string(), - title: Some("Test Server".to_string()), - version: "1.0.0".to_string(), - description: Some("A test server for unit testing".to_string()), - icons: Some(vec![ - Icon { - src: "https://example.com/icon.png".to_string(), - mime_type: Some("image/png".to_string()), - sizes: Some(vec!["48x48".to_string()]), - }, - Icon { - src: "https://example.com/icon.svg".to_string(), - mime_type: Some("image/svg+xml".to_string()), - sizes: Some(vec!["any".to_string()]), - }, - ]), - website_url: Some("https://example.com".to_string()), - }; - - let json = serde_json::to_value(&implementation).unwrap(); - assert_eq!(json["name"], "test-server"); - assert_eq!(json["description"], "A test server for unit testing"); - assert_eq!(json["websiteUrl"], "https://example.com"); - assert!(json["icons"].is_array()); - assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png"); - assert_eq!(json["icons"][0]["sizes"][0], "48x48"); - assert_eq!(json["icons"][1]["mimeType"], "image/svg+xml"); - assert_eq!(json["icons"][1]["sizes"][0], "any"); - } - - #[test] - fn test_backward_compatibility() { - // Test that old JSON without icons still deserializes correctly - let old_json = json!({ - "name": "legacy-server", - "version": "0.9.0" - }); - - let implementation: Implementation = serde_json::from_value(old_json).unwrap(); - assert_eq!(implementation.name, "legacy-server"); - assert_eq!(implementation.version, "0.9.0"); - assert_eq!(implementation.description, None); - assert_eq!(implementation.icons, None); - assert_eq!(implementation.website_url, None); - } - - #[test] - fn test_initialize_with_icons() { - let init_result = InitializeResult { - protocol_version: ProtocolVersion::default(), - capabilities: ServerCapabilities::default(), - server_info: Implementation { - name: "icon-server".to_string(), - title: None, - version: "2.0.0".to_string(), - description: None, - icons: Some(vec![Icon { - src: "https://example.com/server.png".to_string(), - mime_type: Some("image/png".to_string()), - sizes: Some(vec!["48x48".to_string()]), - }]), - website_url: Some("https://docs.example.com".to_string()), - }, - instructions: None, - }; - - let json = serde_json::to_value(&init_result).unwrap(); - assert!(json["serverInfo"]["icons"].is_array()); - assert_eq!( - json["serverInfo"]["icons"][0]["src"], - "https://example.com/server.png" - ); - assert_eq!(json["serverInfo"]["icons"][0]["sizes"][0], "48x48"); - assert_eq!(json["serverInfo"]["websiteUrl"], "https://docs.example.com"); - } - - #[test] - fn test_elicitation_deserialization_untagged() { - // Test deserialization without the "type" field (should default to FormElicitationParam) - let json_data_without_tag = json!({ - "message": "Please provide more details.", - "requestedSchema": { - "title": "User Details", - "type": "object", - "properties": { - "name": { "type": "string" }, - "age": { "type": "integer" } - }, - "required": ["name", "age"] - } - }); - let elicitation: CreateElicitationRequestParams = - serde_json::from_value(json_data_without_tag).expect("Deserialization failed"); - if let CreateElicitationRequestParams::FormElicitationParams { - meta, - message, - requested_schema, - } = elicitation - { - assert_eq!(meta, None); - assert_eq!(message, "Please provide more details."); - assert_eq!(requested_schema.title, Some(Cow::from("User Details"))); - assert_eq!(requested_schema.type_, ObjectTypeConst); - } else { - panic!("Expected FormElicitationParam"); - } - } - - #[test] - fn test_elicitation_deserialization() { - let json_data_form = json!({ - "_meta": { "meta_form_key_1": "meta form value 1" }, - "mode": "form", - "message": "Please provide more details.", - "requestedSchema": { - "title": "User Details", - "type": "object", - "properties": { - "name": { "type": "string" }, - "age": { "type": "integer" } - }, - "required": ["name", "age"] - } - }); - let elicitation_form: CreateElicitationRequestParams = - serde_json::from_value(json_data_form).expect("Deserialization failed"); - if let CreateElicitationRequestParams::FormElicitationParams { - meta, - message, - requested_schema, - } = elicitation_form - { - assert_eq!( - meta, - Some(Meta(object!({ "meta_form_key_1": "meta form value 1" }))) - ); - assert_eq!(message, "Please provide more details."); - assert_eq!(requested_schema.title, Some(Cow::from("User Details"))); - assert_eq!(requested_schema.type_, ObjectTypeConst); - } else { - panic!("Expected FormElicitationParam"); - } - - let json_data_url = json!({ - "_meta": { "meta_url_key_1": "meta url value 1" }, - "mode": "url", - "message": "Please fill out the form at the following URL.", - "url": "https://example.com/form", - "elicitationId": "elicitation-123" - }); - let elicitation_url: CreateElicitationRequestParams = - serde_json::from_value(json_data_url).expect("Deserialization failed"); - if let CreateElicitationRequestParams::UrlElicitationParams { - meta, - message, - url, - elicitation_id, - } = elicitation_url - { - assert_eq!( - meta, - Some(Meta(object!({ "meta_url_key_1": "meta url value 1" }))) - ); - assert_eq!(message, "Please fill out the form at the following URL."); - assert_eq!(url, "https://example.com/form"); - assert_eq!(elicitation_id, "elicitation-123"); - } else { - panic!("Expected UrlElicitationParam"); - } - } - - #[test] - fn test_elicitation_serialization() { - let form_elicitation = CreateElicitationRequestParams::FormElicitationParams { - meta: Some(Meta(object!({ "meta_form_key_1": "meta form value 1" }))), - message: "Please provide more details.".to_string(), - requested_schema: ElicitationSchema::builder() - .title("User Details") - .string_property("name", |s| s) - .build() - .expect("Valid schema"), - }; - let json_form = serde_json::to_value(&form_elicitation).expect("Serialization failed"); - let expected_form_json = json!({ - "_meta": { "meta_form_key_1": "meta form value 1" }, - "mode": "form", - "message": "Please provide more details.", - "requestedSchema": { - "title":"User Details", - "type":"object", - "properties":{ - "name": { "type": "string" }, - }, - } - }); - assert_eq!(json_form, expected_form_json); - - let url_elicitation = CreateElicitationRequestParams::UrlElicitationParams { - meta: Some(Meta(object!({ "meta_url_key_1": "meta url value 1" }))), - message: "Please fill out the form at the following URL.".to_string(), - url: "https://example.com/form".to_string(), - elicitation_id: "elicitation-123".to_string(), - }; - let json_url = serde_json::to_value(&url_elicitation).expect("Serialization failed"); - let expected_url_json = json!({ - "_meta": { "meta_url_key_1": "meta url value 1" }, - "mode": "url", - "message": "Please fill out the form at the following URL.", - "url": "https://example.com/form", - "elicitationId": "elicitation-123" - }); - assert_eq!(json_url, expected_url_json); - } -} diff --git a/codex-rs/vendor/rmcp/src/model/annotated.rs b/codex-rs/vendor/rmcp/src/model/annotated.rs deleted file mode 100644 index f9921146a4..0000000000 --- a/codex-rs/vendor/rmcp/src/model/annotated.rs +++ /dev/null @@ -1,224 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -use super::{ - RawAudioContent, RawContent, RawEmbeddedResource, RawImageContent, RawResource, - RawResourceTemplate, RawTextContent, Role, -}; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Annotations { - #[serde(skip_serializing_if = "Option::is_none")] - pub audience: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub priority: Option, - #[serde(skip_serializing_if = "Option::is_none", rename = "lastModified")] - pub last_modified: Option>, -} - -impl Annotations { - /// Creates a new Annotations instance specifically for resources - /// optional priority, and a timestamp (defaults to now if None) - pub fn for_resource(priority: f32, timestamp: DateTime) -> Self { - assert!( - (0.0..=1.0).contains(&priority), - "Priority {priority} must be between 0.0 and 1.0" - ); - Annotations { - priority: Some(priority), - last_modified: Some(timestamp), - audience: None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Annotated { - #[serde(flatten)] - pub raw: T, - #[serde(skip_serializing_if = "Option::is_none")] - pub annotations: Option, -} - -impl Deref for Annotated { - type Target = T; - fn deref(&self) -> &Self::Target { - &self.raw - } -} - -impl DerefMut for Annotated { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.raw - } -} - -impl Annotated { - pub fn new(raw: T, annotations: Option) -> Self { - Self { raw, annotations } - } - pub fn remove_annotation(&mut self) -> Option { - self.annotations.take() - } - pub fn audience(&self) -> Option<&Vec> { - self.annotations.as_ref().and_then(|a| a.audience.as_ref()) - } - pub fn priority(&self) -> Option { - self.annotations.as_ref().and_then(|a| a.priority) - } - pub fn timestamp(&self) -> Option> { - self.annotations.as_ref().and_then(|a| a.last_modified) - } - pub fn with_audience(self, audience: Vec) -> Annotated - where - Self: Sized, - { - if let Some(annotations) = self.annotations { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - audience: Some(audience), - ..annotations - }), - } - } else { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - audience: Some(audience), - priority: None, - last_modified: None, - }), - } - } - } - pub fn with_priority(self, priority: f32) -> Annotated - where - Self: Sized, - { - if let Some(annotations) = self.annotations { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - priority: Some(priority), - ..annotations - }), - } - } else { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - priority: Some(priority), - last_modified: None, - audience: None, - }), - } - } - } - pub fn with_timestamp(self, timestamp: DateTime) -> Annotated - where - Self: Sized, - { - if let Some(annotations) = self.annotations { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - last_modified: Some(timestamp), - ..annotations - }), - } - } else { - Annotated { - raw: self.raw, - annotations: Some(Annotations { - last_modified: Some(timestamp), - priority: None, - audience: None, - }), - } - } - } - pub fn with_timestamp_now(self) -> Annotated - where - Self: Sized, - { - self.with_timestamp(Utc::now()) - } -} - -mod sealed { - pub trait Sealed {} -} -macro_rules! annotate { - ($T: ident) => { - impl sealed::Sealed for $T {} - impl AnnotateAble for $T {} - }; -} - -annotate!(RawContent); -annotate!(RawTextContent); -annotate!(RawImageContent); -annotate!(RawAudioContent); -annotate!(RawEmbeddedResource); -annotate!(RawResource); -annotate!(RawResourceTemplate); - -pub trait AnnotateAble: sealed::Sealed { - fn optional_annotate(self, annotations: Option) -> Annotated - where - Self: Sized, - { - Annotated::new(self, annotations) - } - fn annotate(self, annotations: Annotations) -> Annotated - where - Self: Sized, - { - Annotated::new(self, Some(annotations)) - } - fn no_annotation(self) -> Annotated - where - Self: Sized, - { - Annotated::new(self, None) - } - fn with_audience(self, audience: Vec) -> Annotated - where - Self: Sized, - { - self.annotate(Annotations { - audience: Some(audience), - ..Default::default() - }) - } - fn with_priority(self, priority: f32) -> Annotated - where - Self: Sized, - { - self.annotate(Annotations { - priority: Some(priority), - ..Default::default() - }) - } - fn with_timestamp(self, timestamp: DateTime) -> Annotated - where - Self: Sized, - { - self.annotate(Annotations { - last_modified: Some(timestamp), - ..Default::default() - }) - } - fn with_timestamp_now(self) -> Annotated - where - Self: Sized, - { - self.with_timestamp(Utc::now()) - } -} diff --git a/codex-rs/vendor/rmcp/src/model/capabilities.rs b/codex-rs/vendor/rmcp/src/model/capabilities.rs deleted file mode 100644 index d0f8e1b2e2..0000000000 --- a/codex-rs/vendor/rmcp/src/model/capabilities.rs +++ /dev/null @@ -1,731 +0,0 @@ -use std::{collections::BTreeMap, marker::PhantomData}; - -use pastey::paste; -use serde::{Deserialize, Serialize}; - -use super::JsonObject; -pub type ExperimentalCapabilities = BTreeMap; - -/// MCP extension capabilities map. -/// -/// Keys are extension identifiers in the format `{vendor-prefix}/{extension-name}` -/// (e.g., `io.modelcontextprotocol/ui`, `io.modelcontextprotocol/oauth-client-credentials`). -/// Values are per-extension settings objects. An empty object indicates support with no settings. -/// -/// # Example -/// -/// ```rust -/// use rmcp::model::ExtensionCapabilities; -/// use serde_json::json; -/// -/// let mut extensions = ExtensionCapabilities::new(); -/// extensions.insert( -/// "io.modelcontextprotocol/ui".to_string(), -/// serde_json::from_value(json!({ -/// "mimeTypes": ["text/html;profile=mcp-app"] -/// })).unwrap() -/// ); -/// ``` -pub type ExtensionCapabilities = BTreeMap; - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct PromptsCapability { - #[serde(skip_serializing_if = "Option::is_none")] - pub list_changed: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ResourcesCapability { - #[serde(skip_serializing_if = "Option::is_none")] - pub subscribe: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub list_changed: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ToolsCapability { - #[serde(skip_serializing_if = "Option::is_none")] - pub list_changed: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct RootsCapabilities { - #[serde(skip_serializing_if = "Option::is_none")] - pub list_changed: Option, -} - -/// Task capabilities shared by client and server. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct TasksCapability { - #[serde(skip_serializing_if = "Option::is_none")] - pub requests: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub list: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cancel: Option, -} - -/// Request types that support task-augmented execution. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct TaskRequestsCapability { - #[serde(skip_serializing_if = "Option::is_none")] - pub sampling: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub elicitation: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub tools: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct SamplingTaskCapability { - #[serde(skip_serializing_if = "Option::is_none")] - pub create_message: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ElicitationTaskCapability { - #[serde(skip_serializing_if = "Option::is_none")] - pub create: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ToolsTaskCapability { - #[serde(skip_serializing_if = "Option::is_none")] - pub call: Option, -} - -impl TasksCapability { - /// Default client tasks capability with sampling and elicitation support. - pub fn client_default() -> Self { - Self { - list: Some(JsonObject::new()), - cancel: Some(JsonObject::new()), - requests: Some(TaskRequestsCapability { - sampling: Some(SamplingTaskCapability { - create_message: Some(JsonObject::new()), - }), - elicitation: Some(ElicitationTaskCapability { - create: Some(JsonObject::new()), - }), - tools: None, - }), - } - } - - /// Default server tasks capability with tools/call support. - pub fn server_default() -> Self { - Self { - list: Some(JsonObject::new()), - cancel: Some(JsonObject::new()), - requests: Some(TaskRequestsCapability { - sampling: None, - elicitation: None, - tools: Some(ToolsTaskCapability { - call: Some(JsonObject::new()), - }), - }), - } - } - - pub fn supports_list(&self) -> bool { - self.list.is_some() - } - - pub fn supports_cancel(&self) -> bool { - self.cancel.is_some() - } - - pub fn supports_tools_call(&self) -> bool { - self.requests - .as_ref() - .and_then(|r| r.tools.as_ref()) - .and_then(|t| t.call.as_ref()) - .is_some() - } - - pub fn supports_sampling_create_message(&self) -> bool { - self.requests - .as_ref() - .and_then(|r| r.sampling.as_ref()) - .and_then(|s| s.create_message.as_ref()) - .is_some() - } - - pub fn supports_elicitation_create(&self) -> bool { - self.requests - .as_ref() - .and_then(|r| r.elicitation.as_ref()) - .and_then(|e| e.create.as_ref()) - .is_some() - } -} - -/// Capability for handling elicitation requests from servers. -/// Elicitation allows servers to request interactive input from users during tool execution. -/// This capability indicates that a client can handle elicitation requests and present -/// appropriate UI to users for collecting the requested information. -/// -/// Capability for form mode elicitation. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct FormElicitationCapability { - /// Whether the client supports JSON Schema validation for elicitation responses. - /// When true, the client will validate user input against the requested_schema - /// before sending the response back to the server. - #[serde(skip_serializing_if = "Option::is_none")] - pub schema_validation: Option, -} - -/// Capability for URL mode elicitation. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct UrlElicitationCapability {} - -/// Elicitation allows servers to request interactive input from users during tool execution. -/// This capability indicates that a client can handle elicitation requests and present -/// appropriate UI to users for collecting the requested information. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ElicitationCapability { - /// Whether client supports form-based elicitation. - #[serde(skip_serializing_if = "Option::is_none")] - pub form: Option, - /// Whether client supports URL-based elicitation. - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, -} - -/// Sampling capability with optional sub-capabilities (SEP-1577). -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct SamplingCapability { - /// Support for `tools` and `toolChoice` parameters - #[serde(skip_serializing_if = "Option::is_none")] - pub tools: Option, - /// Support for `includeContext` (soft-deprecated) - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option, -} - -/// -/// # Builder -/// ```rust -/// # use rmcp::model::ClientCapabilities; -/// let cap = ClientCapabilities::builder() -/// .enable_experimental() -/// .enable_roots() -/// .enable_roots_list_changed() -/// .build(); -/// ``` -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ClientCapabilities { - #[serde(skip_serializing_if = "Option::is_none")] - pub experimental: Option, - /// Optional MCP extensions that the client supports (SEP-1724). - /// Keys are extension identifiers (e.g., `"io.modelcontextprotocol/ui"`), - /// values are per-extension settings objects. An empty object indicates - /// support with no settings. - #[serde(skip_serializing_if = "Option::is_none")] - pub extensions: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub roots: Option, - /// Capability for LLM sampling requests (SEP-1577) - #[serde(skip_serializing_if = "Option::is_none")] - pub sampling: Option, - /// Capability to handle elicitation requests from servers for interactive user input - #[serde(skip_serializing_if = "Option::is_none")] - pub elicitation: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub tasks: Option, -} - -/// -/// ## Builder -/// ```rust -/// # use rmcp::model::ServerCapabilities; -/// let cap = ServerCapabilities::builder() -/// .enable_logging() -/// .enable_experimental() -/// .enable_prompts() -/// .enable_resources() -/// .enable_tools() -/// .enable_tool_list_changed() -/// .build(); -/// ``` -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ServerCapabilities { - #[serde(skip_serializing_if = "Option::is_none")] - pub experimental: Option, - /// Optional MCP extensions that the server supports (SEP-1724). - /// Keys are extension identifiers (e.g., `"io.modelcontextprotocol/apps"`), - /// values are per-extension settings objects. An empty object indicates - /// support with no settings. - #[serde(skip_serializing_if = "Option::is_none")] - pub extensions: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub logging: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub completions: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub prompts: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub resources: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub tools: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub tasks: Option, -} - -macro_rules! builder { - ($Target: ident {$($f: ident: $T: ty),* $(,)?}) => { - paste! { - #[derive(Default, Clone, Copy, Debug)] - pub struct [<$Target BuilderState>]< - $(const [<$f:upper>]: bool = false,)* - >; - #[derive(Debug, Default)] - pub struct [<$Target Builder>]]> { - $(pub $f: Option<$T>,)* - pub state: PhantomData - } - impl $Target { - #[doc = "Create a new [`" $Target "`] builder."] - pub fn builder() -> [<$Target Builder>] { - <[<$Target Builder>]>::default() - } - } - impl [<$Target Builder>] { - pub fn build(self) -> $Target { - $Target { - $( $f: self.$f, )* - } - } - } - impl From<[<$Target Builder>]> for $Target { - fn from(builder: [<$Target Builder>]) -> Self { - builder.build() - } - } - } - builder!($Target @toggle $($f: $T,) *); - - }; - ($Target: ident @toggle $f0: ident: $T0: ty, $($f: ident: $T: ty,)*) => { - builder!($Target @toggle [][$f0: $T0][$($f: $T,)*]); - }; - ($Target: ident @toggle [$($ff: ident: $Tf: ty,)*][$fn: ident: $TN: ty][$fn_1: ident: $Tn_1: ty, $($ft: ident: $Tt: ty,)*]) => { - builder!($Target @impl_toggle [$($ff: $Tf,)*][$fn: $TN][$fn_1: $Tn_1, $($ft:$Tt,)*]); - builder!($Target @toggle [$($ff: $Tf,)* $fn: $TN,][$fn_1: $Tn_1][$($ft:$Tt,)*]); - }; - ($Target: ident @toggle [$($ff: ident: $Tf: ty,)*][$fn: ident: $TN: ty][]) => { - builder!($Target @impl_toggle [$($ff: $Tf,)*][$fn: $TN][]); - }; - ($Target: ident @impl_toggle [$($ff: ident: $Tf: ty,)*][$fn: ident: $TN: ty][$($ft: ident: $Tt: ty,)*]) => { - paste! { - impl< - $(const [<$ff:upper>]: bool,)* - $(const [<$ft:upper>]: bool,)* - > [<$Target Builder>]<[<$Target BuilderState>]< - $([<$ff:upper>],)* - false, - $([<$ft:upper>],)* - >> { - pub fn [](self) -> [<$Target Builder>]<[<$Target BuilderState>]< - $([<$ff:upper>],)* - true, - $([<$ft:upper>],)* - >> { - [<$Target Builder>] { - $( $ff: self.$ff, )* - $fn: Some($TN::default()), - $( $ft: self.$ft, )* - state: PhantomData - } - } - pub fn [](self, $fn: $TN) -> [<$Target Builder>]<[<$Target BuilderState>]< - $([<$ff:upper>],)* - true, - $([<$ft:upper>],)* - >> { - [<$Target Builder>] { - $( $ff: self.$ff, )* - $fn: Some($fn), - $( $ft: self.$ft, )* - state: PhantomData - } - } - } - // do we really need to disable some thing in builder? - // impl< - // $(const [<$ff:upper>]: bool,)* - // $(const [<$ft:upper>]: bool,)* - // > [<$Target Builder>]<[<$Target BuilderState>]< - // $([<$ff:upper>],)* - // true, - // $([<$ft:upper>],)* - // >> { - // pub fn [](self) -> [<$Target Builder>]<[<$Target BuilderState>]< - // $([<$ff:upper>],)* - // false, - // $([<$ft:upper>],)* - // >> { - // [<$Target Builder>] { - // $( $ff: self.$ff, )* - // $fn: None, - // $( $ft: self.$ft, )* - // state: PhantomData - // } - // } - // } - } - } -} - -builder! { - ServerCapabilities { - experimental: ExperimentalCapabilities, - extensions: ExtensionCapabilities, - logging: JsonObject, - completions: JsonObject, - prompts: PromptsCapability, - resources: ResourcesCapability, - tools: ToolsCapability, - tasks: TasksCapability - } -} - -impl< - const E: bool, - const EXT: bool, - const L: bool, - const C: bool, - const P: bool, - const R: bool, - const TASKS: bool, -> ServerCapabilitiesBuilder> -{ - pub fn enable_tool_list_changed(mut self) -> Self { - if let Some(c) = self.tools.as_mut() { - c.list_changed = Some(true); - } - self - } -} - -impl< - const E: bool, - const EXT: bool, - const L: bool, - const C: bool, - const R: bool, - const T: bool, - const TASKS: bool, -> ServerCapabilitiesBuilder> -{ - pub fn enable_prompts_list_changed(mut self) -> Self { - if let Some(c) = self.prompts.as_mut() { - c.list_changed = Some(true); - } - self - } -} - -impl< - const E: bool, - const EXT: bool, - const L: bool, - const C: bool, - const P: bool, - const T: bool, - const TASKS: bool, -> ServerCapabilitiesBuilder> -{ - pub fn enable_resources_list_changed(mut self) -> Self { - if let Some(c) = self.resources.as_mut() { - c.list_changed = Some(true); - } - self - } - - pub fn enable_resources_subscribe(mut self) -> Self { - if let Some(c) = self.resources.as_mut() { - c.subscribe = Some(true); - } - self - } -} - -builder! { - ClientCapabilities{ - experimental: ExperimentalCapabilities, - extensions: ExtensionCapabilities, - roots: RootsCapabilities, - sampling: SamplingCapability, - elicitation: ElicitationCapability, - tasks: TasksCapability, - } -} - -impl - ClientCapabilitiesBuilder> -{ - pub fn enable_roots_list_changed(mut self) -> Self { - if let Some(c) = self.roots.as_mut() { - c.list_changed = Some(true); - } - self - } -} - -impl - ClientCapabilitiesBuilder> -{ - /// Enable tool calling in sampling requests - pub fn enable_sampling_tools(mut self) -> Self { - if let Some(c) = self.sampling.as_mut() { - c.tools = Some(JsonObject::default()); - } - self - } - - /// Enable context inclusion in sampling (soft-deprecated) - pub fn enable_sampling_context(mut self) -> Self { - if let Some(c) = self.sampling.as_mut() { - c.context = Some(JsonObject::default()); - } - self - } -} - -#[cfg(feature = "elicitation")] -impl - ClientCapabilitiesBuilder> -{ - /// Enable JSON Schema validation for elicitation responses in form mode. - /// When enabled, the client will validate user input against the requested_schema - /// before sending responses back to the server. - pub fn enable_elicitation_schema_validation(mut self) -> Self { - if let Some(c) = self.elicitation.as_mut() { - c.form = Some(FormElicitationCapability { - schema_validation: Some(true), - }); - } - self - } -} - -#[cfg(test)] -mod test { - use super::*; - #[test] - fn test_builder() { - let builder = ::default() - .enable_logging() - .enable_experimental() - .enable_prompts() - .enable_resources() - .enable_tools() - .enable_tool_list_changed(); - assert_eq!(builder.logging, Some(JsonObject::default())); - assert_eq!(builder.prompts, Some(PromptsCapability::default())); - assert_eq!(builder.resources, Some(ResourcesCapability::default())); - assert_eq!( - builder.tools, - Some(ToolsCapability { - list_changed: Some(true), - }) - ); - assert_eq!( - builder.experimental, - Some(ExperimentalCapabilities::default()) - ); - let client_builder = ::default() - .enable_experimental() - .enable_roots() - .enable_roots_list_changed() - .enable_sampling(); - assert_eq!( - client_builder.experimental, - Some(ExperimentalCapabilities::default()) - ); - assert_eq!( - client_builder.roots, - Some(RootsCapabilities { - list_changed: Some(true), - }) - ); - } - - #[test] - fn test_task_capabilities_deserialization() { - // Test deserializing from the MCP spec format - let json = serde_json::json!({ - "list": {}, - "cancel": {}, - "requests": { - "tools": { "call": {} } - } - }); - - let tasks: TasksCapability = serde_json::from_value(json).unwrap(); - assert!(tasks.list.is_some()); - assert!(tasks.cancel.is_some()); - assert!(tasks.requests.is_some()); - let requests = tasks.requests.unwrap(); - assert!(requests.tools.is_some()); - assert!(requests.tools.unwrap().call.is_some()); - } - - #[test] - fn test_tasks_capability_client_default() { - let tasks = TasksCapability::client_default(); - - // Verify structure - assert!(tasks.supports_list()); - assert!(tasks.supports_cancel()); - assert!(tasks.supports_sampling_create_message()); - assert!(tasks.supports_elicitation_create()); - assert!(!tasks.supports_tools_call()); - - // Verify serialization matches expected format - let json = serde_json::to_value(&tasks).unwrap(); - assert_eq!(json["list"], serde_json::json!({})); - assert_eq!(json["cancel"], serde_json::json!({})); - assert_eq!( - json["requests"]["sampling"]["createMessage"], - serde_json::json!({}) - ); - assert_eq!( - json["requests"]["elicitation"]["create"], - serde_json::json!({}) - ); - } - - #[test] - fn test_tasks_capability_server_default() { - let tasks = TasksCapability::server_default(); - - // Verify structure - assert!(tasks.supports_list()); - assert!(tasks.supports_cancel()); - assert!(tasks.supports_tools_call()); - assert!(!tasks.supports_sampling_create_message()); - assert!(!tasks.supports_elicitation_create()); - - // Verify serialization matches expected format - let json = serde_json::to_value(&tasks).unwrap(); - assert_eq!(json["list"], serde_json::json!({})); - assert_eq!(json["cancel"], serde_json::json!({})); - assert_eq!(json["requests"]["tools"]["call"], serde_json::json!({})); - } - - #[test] - fn test_client_extensions_capability() { - // Test building ClientCapabilities with extensions (MCP Apps support) - let mut extensions = ExtensionCapabilities::new(); - extensions.insert( - "io.modelcontextprotocol/ui".to_string(), - serde_json::from_value(serde_json::json!({ - "mimeTypes": ["text/html;profile=mcp-app"] - })) - .unwrap(), - ); - - let capabilities = ClientCapabilities::builder() - .enable_extensions_with(extensions) - .enable_sampling() - .build(); - - // Verify serialization matches MCP Apps spec format - let json = serde_json::to_value(&capabilities).unwrap(); - assert_eq!( - json["extensions"]["io.modelcontextprotocol/ui"]["mimeTypes"], - serde_json::json!(["text/html;profile=mcp-app"]) - ); - assert!(json["sampling"].is_object()); - } - - #[test] - fn test_server_extensions_capability() { - // Test building ServerCapabilities with extensions - let mut extensions = ExtensionCapabilities::new(); - extensions.insert( - "io.modelcontextprotocol/apps".to_string(), - serde_json::from_value(serde_json::json!({})).unwrap(), - ); - - let capabilities = ServerCapabilities::builder() - .enable_extensions_with(extensions) - .enable_tools() - .build(); - - // Verify serialization - let json = serde_json::to_value(&capabilities).unwrap(); - assert!(json["extensions"]["io.modelcontextprotocol/apps"].is_object()); - assert!(json["tools"].is_object()); - } - - #[test] - fn test_extensions_deserialization() { - // Test deserializing capabilities with extensions from JSON - let json = serde_json::json!({ - "extensions": { - "io.modelcontextprotocol/ui": { - "mimeTypes": ["text/html;profile=mcp-app"] - } - }, - "sampling": {} - }); - - let capabilities: ClientCapabilities = serde_json::from_value(json).unwrap(); - assert!(capabilities.extensions.is_some()); - let extensions = capabilities.extensions.unwrap(); - assert!(extensions.contains_key("io.modelcontextprotocol/ui")); - let ui_ext = extensions.get("io.modelcontextprotocol/ui").unwrap(); - assert!(ui_ext.contains_key("mimeTypes")); - } - - #[test] - fn test_extensions_empty_settings() { - // Test that empty extension settings work (indicates support with no settings) - let mut extensions = ExtensionCapabilities::new(); - extensions.insert( - "io.modelcontextprotocol/oauth-client-credentials".to_string(), - JsonObject::new(), - ); - - let capabilities = ClientCapabilities::builder() - .enable_extensions_with(extensions) - .build(); - - let json = serde_json::to_value(&capabilities).unwrap(); - assert_eq!( - json["extensions"]["io.modelcontextprotocol/oauth-client-credentials"], - serde_json::json!({}) - ); - } -} diff --git a/codex-rs/vendor/rmcp/src/model/content.rs b/codex-rs/vendor/rmcp/src/model/content.rs deleted file mode 100644 index beb4d9f5df..0000000000 --- a/codex-rs/vendor/rmcp/src/model/content.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! Content sent around agents, extensions, and LLMs -//! The various content types can be display to humans but also understood by models -//! They include optional annotations used to help inform agent usage -use serde::{Deserialize, Serialize}; -use serde_json::json; - -use super::{AnnotateAble, Annotated, resource::ResourceContents}; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct RawTextContent { - pub text: String, - /// Optional protocol-level metadata for this content block - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, -} -pub type TextContent = Annotated; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct RawImageContent { - /// The base64-encoded image - pub data: String, - pub mime_type: String, - /// Optional protocol-level metadata for this content block - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, -} - -pub type ImageContent = Annotated; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct RawEmbeddedResource { - /// Optional protocol-level metadata for this content block - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, - pub resource: ResourceContents, -} -pub type EmbeddedResource = Annotated; - -impl EmbeddedResource { - pub fn get_text(&self) -> String { - match &self.resource { - ResourceContents::TextResourceContents { text, .. } => text.clone(), - _ => String::new(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct RawAudioContent { - pub data: String, - pub mime_type: String, -} - -pub type AudioContent = Annotated; - -/// Tool call request from assistant (SEP-1577). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ToolUseContent { - /// Unique identifier for this tool call - pub id: String, - /// Name of the tool to call - pub name: String, - /// Input arguments for the tool - pub input: super::JsonObject, - /// Optional metadata (preserved for caching) - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, -} - -/// Tool execution result in user message (SEP-1577). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ToolResultContent { - /// Optional metadata - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, - /// ID of the corresponding tool use - pub tool_use_id: String, - /// Content blocks returned by the tool - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub content: Vec, - /// Optional structured result - #[serde(skip_serializing_if = "Option::is_none")] - pub structured_content: Option, - /// Whether tool execution failed - #[serde(skip_serializing_if = "Option::is_none")] - pub is_error: Option, -} - -impl ToolUseContent { - pub fn new(id: impl Into, name: impl Into, input: super::JsonObject) -> Self { - Self { - id: id.into(), - name: name.into(), - input, - meta: None, - } - } -} - -impl ToolResultContent { - pub fn new(tool_use_id: impl Into, content: Vec) -> Self { - Self { - meta: None, - tool_use_id: tool_use_id.into(), - content, - structured_content: None, - is_error: None, - } - } - - pub fn error(tool_use_id: impl Into, content: Vec) -> Self { - Self { - meta: None, - tool_use_id: tool_use_id.into(), - content, - structured_content: None, - is_error: Some(true), - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum RawContent { - Text(RawTextContent), - Image(RawImageContent), - Resource(RawEmbeddedResource), - Audio(RawAudioContent), - ResourceLink(super::resource::RawResource), -} - -pub type Content = Annotated; - -impl RawContent { - pub fn json(json: S) -> Result { - let json = serde_json::to_string(&json).map_err(|e| { - crate::ErrorData::internal_error( - "fail to serialize response to json", - Some(json!( - {"reason": e.to_string()} - )), - ) - })?; - Ok(RawContent::text(json)) - } - - pub fn text>(text: S) -> Self { - RawContent::Text(RawTextContent { - text: text.into(), - meta: None, - }) - } - - pub fn image, T: Into>(data: S, mime_type: T) -> Self { - RawContent::Image(RawImageContent { - data: data.into(), - mime_type: mime_type.into(), - meta: None, - }) - } - - pub fn resource(resource: ResourceContents) -> Self { - RawContent::Resource(RawEmbeddedResource { - meta: None, - resource, - }) - } - - pub fn embedded_text, T: Into>(uri: S, content: T) -> Self { - RawContent::Resource(RawEmbeddedResource { - meta: None, - resource: ResourceContents::TextResourceContents { - uri: uri.into(), - mime_type: Some("text".to_string()), - text: content.into(), - meta: None, - }, - }) - } - - /// Get the text content if this is a TextContent variant - pub fn as_text(&self) -> Option<&RawTextContent> { - match self { - RawContent::Text(text) => Some(text), - _ => None, - } - } - - /// Get the image content if this is an ImageContent variant - pub fn as_image(&self) -> Option<&RawImageContent> { - match self { - RawContent::Image(image) => Some(image), - _ => None, - } - } - - /// Get the resource content if this is an ImageContent variant - pub fn as_resource(&self) -> Option<&RawEmbeddedResource> { - match self { - RawContent::Resource(resource) => Some(resource), - _ => None, - } - } - - /// Get the resource link if this is a ResourceLink variant - pub fn as_resource_link(&self) -> Option<&super::resource::RawResource> { - match self { - RawContent::ResourceLink(link) => Some(link), - _ => None, - } - } - - /// Create a resource link content - pub fn resource_link(resource: super::resource::RawResource) -> Self { - RawContent::ResourceLink(resource) - } -} - -impl Content { - pub fn text>(text: S) -> Self { - RawContent::text(text).no_annotation() - } - - pub fn image, T: Into>(data: S, mime_type: T) -> Self { - RawContent::image(data, mime_type).no_annotation() - } - - pub fn resource(resource: ResourceContents) -> Self { - RawContent::resource(resource).no_annotation() - } - - pub fn embedded_text, T: Into>(uri: S, content: T) -> Self { - RawContent::embedded_text(uri, content).no_annotation() - } - - pub fn json(json: S) -> Result { - RawContent::json(json).map(|c| c.no_annotation()) - } - - /// Create a resource link content - pub fn resource_link(resource: super::resource::RawResource) -> Self { - RawContent::resource_link(resource).no_annotation() - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct JsonContent(S); -/// Types that can be converted into a list of contents -pub trait IntoContents { - fn into_contents(self) -> Vec; -} - -impl IntoContents for Content { - fn into_contents(self) -> Vec { - vec![self] - } -} - -impl IntoContents for String { - fn into_contents(self) -> Vec { - vec![Content::text(self)] - } -} - -impl IntoContents for () { - fn into_contents(self) -> Vec { - vec![] - } -} - -#[cfg(test)] -mod tests { - use serde_json; - - use super::*; - - #[test] - fn test_image_content_serialization() { - let image_content = RawImageContent { - data: "base64data".to_string(), - mime_type: "image/png".to_string(), - meta: None, - }; - - let json = serde_json::to_string(&image_content).unwrap(); - println!("ImageContent JSON: {}", json); - - // Verify it contains mimeType (camelCase) not mime_type (snake_case) - assert!(json.contains("mimeType")); - assert!(!json.contains("mime_type")); - } - - #[test] - fn test_audio_content_serialization() { - let audio_content = RawAudioContent { - data: "base64audiodata".to_string(), - mime_type: "audio/wav".to_string(), - }; - - let json = serde_json::to_string(&audio_content).unwrap(); - println!("AudioContent JSON: {}", json); - - // Verify it contains mimeType (camelCase) not mime_type (snake_case) - assert!(json.contains("mimeType")); - assert!(!json.contains("mime_type")); - } - - #[test] - fn test_resource_link_serialization() { - use super::super::resource::RawResource; - - let resource_link = RawContent::ResourceLink(RawResource { - uri: "file:///test.txt".to_string(), - name: "test.txt".to_string(), - title: None, - description: Some("A test file".to_string()), - mime_type: Some("text/plain".to_string()), - size: Some(100), - icons: None, - meta: None, - }); - - let json = serde_json::to_string(&resource_link).unwrap(); - println!("ResourceLink JSON: {}", json); - - // Verify it contains the correct type tag - assert!(json.contains("\"type\":\"resource_link\"")); - assert!(json.contains("\"uri\":\"file:///test.txt\"")); - assert!(json.contains("\"name\":\"test.txt\"")); - } - - #[test] - fn test_resource_link_deserialization() { - let json = r#"{ - "type": "resource_link", - "uri": "file:///example.txt", - "name": "example.txt", - "description": "Example file", - "mimeType": "text/plain" - }"#; - - let content: RawContent = serde_json::from_str(json).unwrap(); - - if let RawContent::ResourceLink(resource) = content { - assert_eq!(resource.uri, "file:///example.txt"); - assert_eq!(resource.name, "example.txt"); - assert_eq!(resource.description, Some("Example file".to_string())); - assert_eq!(resource.mime_type, Some("text/plain".to_string())); - } else { - panic!("Expected ResourceLink variant"); - } - } -} diff --git a/codex-rs/vendor/rmcp/src/model/elicitation_schema.rs b/codex-rs/vendor/rmcp/src/model/elicitation_schema.rs deleted file mode 100644 index a02e07f4c1..0000000000 --- a/codex-rs/vendor/rmcp/src/model/elicitation_schema.rs +++ /dev/null @@ -1,2010 +0,0 @@ -//! Type-safe schema definitions for MCP elicitation requests. -//! -//! This module provides strongly-typed schema definitions for elicitation requests -//! that comply with the MCP 2025-06-18 specification. Elicitation schemas must be -//! objects with primitive-typed properties. -//! -//! # Example -//! -//! ```rust -//! use rmcp::model::*; -//! -//! let schema = ElicitationSchema::builder() -//! .required_email("email") -//! .required_integer("age", 0, 150) -//! .optional_bool("newsletter", false) -//! .build(); -//! ``` - -use std::{borrow::Cow, collections::BTreeMap, marker::PhantomData}; - -use serde::{Deserialize, Serialize}; - -use crate::{const_string, model::ConstString}; - -// ============================================================================= -// CONST TYPES FOR JSON SCHEMA TYPE FIELD -// ============================================================================= - -const_string!(ObjectTypeConst = "object"); -const_string!(StringTypeConst = "string"); -const_string!(NumberTypeConst = "number"); -const_string!(IntegerTypeConst = "integer"); -const_string!(BooleanTypeConst = "boolean"); -const_string!(EnumTypeConst = "string"); -const_string!(ArrayTypeConst = "array"); - -// ============================================================================= -// PRIMITIVE SCHEMA DEFINITIONS -// ============================================================================= - -/// Primitive schema definition for elicitation properties. -/// -/// According to MCP 2025-06-18 specification, elicitation schemas must have -/// properties of primitive types only (string, number, integer, boolean, enum). -/// -/// Note: Put Enum as the first variant to avoid ambiguity during deserialization. -/// This is due to the fact that EnumSchema can contain StringSchema internally and serde -/// uses first match wins strategy when deserializing untagged enums. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(untagged)] -pub enum PrimitiveSchema { - /// Enum property (explicit enum schema) - Enum(EnumSchema), - /// String property (with optional enum constraint) - String(StringSchema), - /// Number property (with optional enum constraint) - Number(NumberSchema), - /// Integer property (with optional enum constraint) - Integer(IntegerSchema), - /// Boolean property - Boolean(BooleanSchema), -} - -// ============================================================================= -// STRING SCHEMA -// ============================================================================= - -/// String format types allowed by the MCP specification. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(rename_all = "kebab-case")] -pub enum StringFormat { - /// Email address format - Email, - /// URI format - Uri, - /// Date format (YYYY-MM-DD) - Date, - /// Date-time format (ISO 8601) - DateTime, -} - -/// Schema definition for string properties. -/// -/// Compliant with MCP 2025-06-18 specification for elicitation schemas. -/// Supports only the fields allowed by the MCP spec: -/// - format limited to: "email", "uri", "date", "date-time" -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(rename_all = "camelCase")] -pub struct StringSchema { - /// Type discriminator - #[serde(rename = "type")] - pub type_: StringTypeConst, - - /// Optional title for the schema - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option>, - - /// Human-readable description - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - - /// Minimum string length - #[serde(skip_serializing_if = "Option::is_none")] - pub min_length: Option, - - /// Maximum string length - #[serde(skip_serializing_if = "Option::is_none")] - pub max_length: Option, - - /// String format - limited to: "email", "uri", "date", "date-time" - #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option, -} - -impl Default for StringSchema { - fn default() -> Self { - Self { - type_: StringTypeConst, - title: None, - description: None, - min_length: None, - max_length: None, - format: None, - } - } -} - -impl StringSchema { - /// Create a new string schema - pub fn new() -> Self { - Self::default() - } - - /// Create an email string schema - pub fn email() -> Self { - Self { - format: Some(StringFormat::Email), - ..Default::default() - } - } - - /// Create a URI string schema - pub fn uri() -> Self { - Self { - format: Some(StringFormat::Uri), - ..Default::default() - } - } - - /// Create a date string schema - pub fn date() -> Self { - Self { - format: Some(StringFormat::Date), - ..Default::default() - } - } - - /// Create a date-time string schema - pub fn date_time() -> Self { - Self { - format: Some(StringFormat::DateTime), - ..Default::default() - } - } - - /// Set title - pub fn title(mut self, title: impl Into>) -> Self { - self.title = Some(title.into()); - self - } - - /// Set description - pub fn description(mut self, description: impl Into>) -> Self { - self.description = Some(description.into()); - self - } - - /// Set minimum and maximum length - pub fn with_length(mut self, min: u32, max: u32) -> Result { - if min > max { - return Err("min_length must be <= max_length"); - } - self.min_length = Some(min); - self.max_length = Some(max); - Ok(self) - } - - /// Set minimum and maximum length (panics on invalid input) - pub fn length(mut self, min: u32, max: u32) -> Self { - assert!(min <= max, "min_length must be <= max_length"); - self.min_length = Some(min); - self.max_length = Some(max); - self - } - - /// Set minimum length - pub fn min_length(mut self, min: u32) -> Self { - self.min_length = Some(min); - self - } - - /// Set maximum length - pub fn max_length(mut self, max: u32) -> Self { - self.max_length = Some(max); - self - } - - /// Set format (limited to: "email", "uri", "date", "date-time") - pub fn format(mut self, format: StringFormat) -> Self { - self.format = Some(format); - self - } -} - -// ============================================================================= -// NUMBER SCHEMA -// ============================================================================= - -/// Schema definition for number properties (floating-point). -/// -/// Compliant with MCP 2025-06-18 specification for elicitation schemas. -/// Supports only the fields allowed by the MCP spec. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(rename_all = "camelCase")] -pub struct NumberSchema { - /// Type discriminator - #[serde(rename = "type")] - pub type_: NumberTypeConst, - - /// Optional title for the schema - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option>, - - /// Human-readable description - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - - /// Minimum value (inclusive) - #[serde(skip_serializing_if = "Option::is_none")] - pub minimum: Option, - - /// Maximum value (inclusive) - #[serde(skip_serializing_if = "Option::is_none")] - pub maximum: Option, -} - -impl Default for NumberSchema { - fn default() -> Self { - Self { - type_: NumberTypeConst, - title: None, - description: None, - minimum: None, - maximum: None, - } - } -} - -impl NumberSchema { - /// Create a new number schema - pub fn new() -> Self { - Self::default() - } - - /// Set minimum and maximum (inclusive) - pub fn with_range(mut self, min: f64, max: f64) -> Result { - if min > max { - return Err("minimum must be <= maximum"); - } - self.minimum = Some(min); - self.maximum = Some(max); - Ok(self) - } - - /// Set minimum and maximum (panics on invalid input) - pub fn range(mut self, min: f64, max: f64) -> Self { - assert!(min <= max, "minimum must be <= maximum"); - self.minimum = Some(min); - self.maximum = Some(max); - self - } - - /// Set minimum (inclusive) - pub fn minimum(mut self, min: f64) -> Self { - self.minimum = Some(min); - self - } - - /// Set maximum (inclusive) - pub fn maximum(mut self, max: f64) -> Self { - self.maximum = Some(max); - self - } - - /// Set title - pub fn title(mut self, title: impl Into>) -> Self { - self.title = Some(title.into()); - self - } - - /// Set description - pub fn description(mut self, description: impl Into>) -> Self { - self.description = Some(description.into()); - self - } -} - -// ============================================================================= -// INTEGER SCHEMA -// ============================================================================= - -/// Schema definition for integer properties. -/// -/// Compliant with MCP 2025-06-18 specification for elicitation schemas. -/// Supports only the fields allowed by the MCP spec. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(rename_all = "camelCase")] -pub struct IntegerSchema { - /// Type discriminator - #[serde(rename = "type")] - pub type_: IntegerTypeConst, - - /// Optional title for the schema - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option>, - - /// Human-readable description - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - - /// Minimum value (inclusive) - #[serde(skip_serializing_if = "Option::is_none")] - pub minimum: Option, - - /// Maximum value (inclusive) - #[serde(skip_serializing_if = "Option::is_none")] - pub maximum: Option, -} - -impl Default for IntegerSchema { - fn default() -> Self { - Self { - type_: IntegerTypeConst, - title: None, - description: None, - minimum: None, - maximum: None, - } - } -} - -impl IntegerSchema { - /// Create a new integer schema - pub fn new() -> Self { - Self::default() - } - - /// Set minimum and maximum (inclusive) - pub fn with_range(mut self, min: i64, max: i64) -> Result { - if min > max { - return Err("minimum must be <= maximum"); - } - self.minimum = Some(min); - self.maximum = Some(max); - Ok(self) - } - - /// Set minimum and maximum (panics on invalid input) - pub fn range(mut self, min: i64, max: i64) -> Self { - assert!(min <= max, "minimum must be <= maximum"); - self.minimum = Some(min); - self.maximum = Some(max); - self - } - - /// Set minimum (inclusive) - pub fn minimum(mut self, min: i64) -> Self { - self.minimum = Some(min); - self - } - - /// Set maximum (inclusive) - pub fn maximum(mut self, max: i64) -> Self { - self.maximum = Some(max); - self - } - - /// Set title - pub fn title(mut self, title: impl Into>) -> Self { - self.title = Some(title.into()); - self - } - - /// Set description - pub fn description(mut self, description: impl Into>) -> Self { - self.description = Some(description.into()); - self - } -} - -// ============================================================================= -// BOOLEAN SCHEMA -// ============================================================================= - -/// Schema definition for boolean properties. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(rename_all = "camelCase")] -pub struct BooleanSchema { - /// Type discriminator - #[serde(rename = "type")] - pub type_: BooleanTypeConst, - - /// Optional title for the schema - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option>, - - /// Human-readable description - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - - /// Default value - #[serde(skip_serializing_if = "Option::is_none")] - pub default: Option, -} - -impl Default for BooleanSchema { - fn default() -> Self { - Self { - type_: BooleanTypeConst, - title: None, - description: None, - default: None, - } - } -} - -impl BooleanSchema { - /// Create a new boolean schema - pub fn new() -> Self { - Self::default() - } - - /// Set title - pub fn title(mut self, title: impl Into>) -> Self { - self.title = Some(title.into()); - self - } - - /// Set description - pub fn description(mut self, description: impl Into>) -> Self { - self.description = Some(description.into()); - self - } - - /// Set default value - pub fn with_default(mut self, default: bool) -> Self { - self.default = Some(default); - self - } -} - -// ============================================================================= -// ENUM SCHEMA -// ============================================================================= - -/// Schema definition for enum properties. -/// -/// Represent single entry for titled item -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ConstTitle { - #[serde(rename = "const")] - pub const_: String, - pub title: String, -} - -/// Legacy enum schema, keep for backward compatibility -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct LegacyEnumSchema { - #[serde(rename = "type")] - pub type_: StringTypeConst, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - #[serde(rename = "enum")] - pub enum_: Vec, - pub enum_names: Option>, -} - -/// Untitled single-select -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct UntitledSingleSelectEnumSchema { - #[serde(rename = "type")] - pub type_: StringTypeConst, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - #[serde(rename = "enum")] - pub enum_: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub default: Option, -} - -/// Titled single-select -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct TitledSingleSelectEnumSchema { - #[serde(rename = "type")] - pub type_: StringTypeConst, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - #[serde(rename = "oneOf")] - pub one_of: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub default: Option, -} - -/// Combined single-select -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(untagged)] -pub enum SingleSelectEnumSchema { - Untitled(UntitledSingleSelectEnumSchema), - Titled(TitledSingleSelectEnumSchema), -} - -/// Items for untitled multi-select options -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct UntitledItems { - #[serde(rename = "type")] - pub type_: StringTypeConst, - #[serde(rename = "enum")] - pub enum_: Vec, -} - -/// Items for titled multi-select options -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct TitledItems { - // MCP spec requires "anyOf" for multi-select enums (allows any combination) - // Alias "oneOf" for compatibility with schemars - #[serde(rename = "anyOf", alias = "oneOf")] - pub any_of: Vec, -} - -/// Multi-select untitled options -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(rename_all = "camelCase")] -pub struct UntitledMultiSelectEnumSchema { - #[serde(rename = "type")] - pub type_: ArrayTypeConst, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub min_items: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_items: Option, - pub items: UntitledItems, - #[serde(skip_serializing_if = "Option::is_none")] - pub default: Option>, -} - -/// Multi-select titled options -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(rename_all = "camelCase")] -pub struct TitledMultiSelectEnumSchema { - #[serde(rename = "type")] - pub type_: ArrayTypeConst, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub min_items: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_items: Option, - pub items: TitledItems, - #[serde(skip_serializing_if = "Option::is_none")] - pub default: Option>, -} - -/// Multi-select enum options -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(untagged)] -pub enum MultiSelectEnumSchema { - Untitled(UntitledMultiSelectEnumSchema), - Titled(TitledMultiSelectEnumSchema), -} - -/// Compliant with MCP 2025-06-18 specification for elicitation schemas. -/// Enums must have string type for values and can optionally include human-readable names. -/// -/// # Example -/// -/// ```rust -/// use rmcp::model::*; -/// -/// let enum_schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) -/// .multiselect() -/// .min_items(1u64).expect("Min items should be correct value") -/// .max_items(4u64).expect("Max items should be correct value") -/// .description("Country code") -/// .build(); -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(untagged)] -pub enum EnumSchema { - Single(SingleSelectEnumSchema), - Multi(MultiSelectEnumSchema), - Legacy(LegacyEnumSchema), -} - -/// Marker type for single-select enum builder -#[derive(Debug)] -pub struct SingleSelect; -/// Marker type for multi-select enum builder -#[derive(Debug)] -pub struct MultiSelect; -/// Builder for EnumSchema -/// Allows to create various enum schema types (single/multi select, titled/untitled) -/// with validation of provided parameters -/// -/// # Example -/// ```rust -/// use rmcp::model::*; -/// let enum_schema = EnumSchema::builder(vec!["Red".to_string(), "Green".to_string(), "Blue".to_string()]) -/// .multiselect() -/// .enum_titles(vec!["Red Color".to_string(), "Green Color".to_string(), "Blue Color".to_string()]) -/// .expect("Number of titles should match number of values") -/// .min_items(1u64).expect("Min items should be correct value") -/// .max_items(3u64).expect("Max items should be correct value") -/// .description("Select your favorite colors") -/// .build(); -/// ``` -#[derive(Debug)] -pub struct EnumSchemaBuilder { - /// Enum values - enum_values: Vec, - /// If true generate Titled EnumSchema, UnTitled otherwise - titled: bool, - /// Title of EnumSchema - title: Option>, - /// Description of EnumSchema - description: Option>, - /// Titles of given enum values - enum_titles: Vec, - /// Minimum number of items to choose for MultiSelect - min_items: Option, - /// Maximum number of items to choose for MultiSelect - max_items: Option, - /// Default values for enum - default: Vec, - select_type: PhantomData, -} - -/// Default implementation for single-select enum builder -impl Default for EnumSchemaBuilder { - fn default() -> Self { - Self { - title: None, - description: None, - titled: false, - enum_titles: Vec::new(), - enum_values: Vec::new(), - min_items: None, - max_items: None, - default: Vec::new(), - select_type: PhantomData, - } - } -} - -/// Common enum schema builder methods -impl EnumSchemaBuilder { - /// Set title of enum schema - pub fn title(mut self, value: impl Into>) -> Self { - self.title = Some(value.into()); - self - } - - /// Set description of enum schema - pub fn description(mut self, value: impl Into>) -> Self { - self.description = Some(value.into()); - self - } - - /// Set enum as untitled - /// Clears any previously set titles - pub fn untitled(mut self) -> Self { - self.enum_titles = Vec::new(); - self.titled = false; - self - } - - /// Set titles to enum values. Also, implicitly set this enum schema as titled - pub fn enum_titles(mut self, titles: Vec) -> Result, String> { - if titles.len() != self.enum_values.len() { - return Err(format!( - "Provided number of titles do not match number of values: expected {}, but got {}", - self.enum_values.len(), - titles.len() - )); - } - self.titled = true; - self.enum_titles = titles; - Ok(self) - } -} - -/// Enum selection builder for single-select enums -impl EnumSchemaBuilder { - pub fn new(values: Vec) -> EnumSchemaBuilder { - EnumSchemaBuilder { - enum_values: values, - ..Default::default() - } - } - - /// Transition to multi-select enum builder. - /// - /// Clears any previously set default values and resets min/max items. - /// After this transition, you can use `min_items()`, `max_items()`, and - /// `with_default()` for multi-select semantics. - pub fn multiselect(self) -> EnumSchemaBuilder { - EnumSchemaBuilder { - enum_values: self.enum_values, - titled: self.titled, - title: self.title, - description: self.description, - enum_titles: self.enum_titles, - min_items: None, - max_items: None, - default: Vec::new(), // Clear default for multi-select - select_type: PhantomData, - } - } - - /// Set default value - pub fn with_default( - mut self, - default_value: impl Into, - ) -> Result, String> { - let value: String = default_value.into(); - if !self.enum_values.contains(&value) { - return Err("Provided default value is not in enum values".to_string()); - } - self.default = vec![value]; - Ok(self) - } - - /// Build enum schema - pub fn build(mut self) -> EnumSchema { - match self.titled { - false => EnumSchema::Single(SingleSelectEnumSchema::Untitled( - UntitledSingleSelectEnumSchema { - type_: StringTypeConst, - title: self.title, - description: self.description, - enum_: self.enum_values, - default: self.default.pop(), - }, - )), - true => EnumSchema::Single(SingleSelectEnumSchema::Titled( - TitledSingleSelectEnumSchema { - type_: StringTypeConst, - title: self.title, - description: self.description, - one_of: self - .enum_titles - .into_iter() - .zip(self.enum_values) - .map(|(title, const_)| ConstTitle { const_, title }) - .collect(), - default: self.default.pop(), - }, - )), - } - } -} - -/// Enum selection builder for multi-select enums -impl EnumSchemaBuilder { - /// Set enum as single-select - /// If it was multi-select, clear default values - pub fn single_select(self) -> EnumSchemaBuilder { - EnumSchemaBuilder { - enum_values: self.enum_values, - titled: self.titled, - title: self.title, - description: self.description, - enum_titles: self.enum_titles, - min_items: None, - max_items: None, - default: Vec::new(), // Clear default for single-select - select_type: PhantomData, - } - } - - /// Set default values - pub fn with_default( - mut self, - default_values: Vec, - ) -> Result, String> { - for value in &default_values { - if !self.enum_values.contains(value) { - return Err("One of the provided default values is not in enum values".to_string()); - } - } - if let Some(min) = self.min_items { - if (default_values.len() as u64) < min { - return Err("Number of provided default values is less than min_items".to_string()); - } - } - if let Some(max) = self.max_items { - if (default_values.len() as u64) > max { - return Err( - "Number of provided default values is greater than max_items".to_string(), - ); - } - } - self.default = default_values; - Ok(self) - } - - /// Set minimal number of items for multi-select enum options - pub fn min_items(mut self, value: u64) -> Result, String> { - if let Some(max) = self.max_items - && value > max - { - return Err("Provided value is greater than max_items".to_string()); - } - self.min_items = Some(value); - Ok(self) - } - - /// Set maximal number of items for multi-select enum options - pub fn max_items(mut self, value: u64) -> Result, String> { - if let Some(min) = self.min_items - && value < min - { - return Err("Provided value is less than min_items".to_string()); - } - self.max_items = Some(value); - Ok(self) - } - - /// Build enum schema - pub fn build(self) -> EnumSchema { - match self.titled { - false => EnumSchema::Multi(MultiSelectEnumSchema::Untitled( - UntitledMultiSelectEnumSchema { - type_: ArrayTypeConst, - title: self.title, - description: self.description, - min_items: self.min_items, - max_items: self.max_items, - items: UntitledItems { - type_: StringTypeConst, - enum_: self.enum_values, - }, - default: if self.default.is_empty() { - None - } else { - Some(self.default) - }, - }, - )), - true => EnumSchema::Multi(MultiSelectEnumSchema::Titled(TitledMultiSelectEnumSchema { - type_: ArrayTypeConst, - title: self.title, - description: self.description, - min_items: self.min_items, - max_items: self.max_items, - items: TitledItems { - any_of: self - .enum_titles - .into_iter() - .zip(self.enum_values) - .map(|(title, const_)| ConstTitle { const_, title }) - .collect(), - }, - default: if self.default.is_empty() { - None - } else { - Some(self.default) - }, - })), - } - } -} - -impl EnumSchema { - /// Creates a new `EnumSchemaBuilder` with the given enum values. - /// - /// This convenience method allows you to construct an enum schema by specifying - /// the possible string values for the enum. Use the returned builder to further - /// configure the schema before building it. - /// - /// # Arguments - /// - /// * `values` - A vector of strings representing the allowed enum values. - /// - /// # Example - /// - /// ``` - /// use rmcp::model::*; - /// - /// let enum_schema = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]). - /// with_default("A"). - /// expect("Default value should be valid"). - /// enum_titles(vec!["Option A".to_string(), "Option B".to_string()]). - /// expect("Number of titles should match number of values"). - /// build(); - /// ``` - pub fn builder(values: Vec) -> EnumSchemaBuilder { - EnumSchemaBuilder::new(values) - } -} - -// ============================================================================= -// ELICITATION SCHEMA -// ============================================================================= - -/// Type-safe elicitation schema for requesting structured user input. -/// -/// This enforces the MCP 2025-06-18 specification that elicitation schemas -/// must be objects with primitive-typed properties. -/// -/// # Example -/// -/// ```rust -/// use rmcp::model::*; -/// -/// let schema = ElicitationSchema::builder() -/// .required_email("email") -/// .required_integer("age", 0, 150) -/// .optional_bool("newsletter", false) -/// .build(); -/// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(rename_all = "camelCase")] -pub struct ElicitationSchema { - /// Always "object" for elicitation schemas - #[serde(rename = "type")] - pub type_: ObjectTypeConst, - - /// Optional title for the schema - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option>, - - /// Property definitions (must be primitive types) - pub properties: BTreeMap, - - /// List of required property names - #[serde(skip_serializing_if = "Option::is_none")] - pub required: Option>, - - /// Optional description of what this schema represents - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, -} - -impl ElicitationSchema { - /// Create a new elicitation schema with the given properties - pub fn new(properties: BTreeMap) -> Self { - Self { - type_: ObjectTypeConst, - title: None, - properties, - required: None, - description: None, - } - } - - /// Convert from a JSON Schema object (typically generated by schemars) - /// - /// This allows converting from JsonObject to ElicitationSchema, which is useful - /// when working with automatically generated schemas from types. - /// - /// # Example - /// - /// ```rust,ignore - /// use rmcp::model::*; - /// - /// let json_schema = schema_for_type::(); - /// let elicitation_schema = ElicitationSchema::from_json_schema(json_schema)?; - /// ``` - /// - /// # Errors - /// - /// Returns a [`serde_json::Error`] if the JSON object cannot be deserialized - /// into a valid ElicitationSchema. - pub fn from_json_schema(schema: crate::model::JsonObject) -> Result { - serde_json::from_value(serde_json::Value::Object(schema)) - } - - /// Generate an ElicitationSchema from a Rust type that implements JsonSchema - /// - /// This is a convenience method that combines schema generation and conversion. - /// It uses the same schema generation settings as the rest of the MCP SDK. - /// - /// # Example - /// - /// ```rust,ignore - /// use rmcp::model::*; - /// use schemars::JsonSchema; - /// use serde::{Deserialize, Serialize}; - /// - /// #[derive(JsonSchema, Serialize, Deserialize)] - /// struct UserInput { - /// name: String, - /// age: u32, - /// } - /// - /// let schema = ElicitationSchema::from_type::()?; - /// ``` - /// - /// # Errors - /// - /// Returns a [`serde_json::Error`] if the generated schema cannot be converted - /// to a valid ElicitationSchema. - #[cfg(feature = "schemars")] - pub fn from_type() -> Result - where - T: schemars::JsonSchema, - { - use crate::schemars::generate::SchemaSettings; - - let mut settings = SchemaSettings::draft07(); - settings.transforms = vec![Box::new(schemars::transform::AddNullable::default())]; - let generator = settings.into_generator(); - let schema = generator.into_root_schema_for::(); - let object = serde_json::to_value(schema).expect("failed to serialize schema"); - match object { - serde_json::Value::Object(object) => Self::from_json_schema(object), - _ => panic!( - "Schema serialization produced non-object value: expected JSON object but got {:?}", - object - ), - } - } - - /// Set the required fields - pub fn with_required(mut self, required: Vec) -> Self { - self.required = Some(required); - self - } - - /// Set the title - pub fn with_title(mut self, title: impl Into>) -> Self { - self.title = Some(title.into()); - self - } - - /// Set the description - pub fn with_description(mut self, description: impl Into>) -> Self { - self.description = Some(description.into()); - self - } - - /// Create a builder for constructing elicitation schemas fluently - pub fn builder() -> ElicitationSchemaBuilder { - ElicitationSchemaBuilder::new() - } -} - -// ============================================================================= -// BUILDER -// ============================================================================= - -/// Fluent builder for constructing elicitation schemas. -/// -/// # Example -/// -/// ```rust -/// use rmcp::model::*; -/// -/// let schema = ElicitationSchema::builder() -/// .required_email("email") -/// .required_integer("age", 0, 150) -/// .optional_bool("newsletter", false) -/// .description("User registration") -/// .build(); -/// ``` -#[derive(Debug, Default)] -pub struct ElicitationSchemaBuilder { - pub properties: BTreeMap, - pub required: Vec, - pub title: Option>, - pub description: Option>, -} - -impl ElicitationSchemaBuilder { - /// Create a new builder - pub fn new() -> Self { - Self::default() - } - - /// Add a property to the schema - pub fn property(mut self, name: impl Into, schema: PrimitiveSchema) -> Self { - self.properties.insert(name.into(), schema); - self - } - - /// Add a required property to the schema - pub fn required_property(mut self, name: impl Into, schema: PrimitiveSchema) -> Self { - let name_str = name.into(); - self.required.push(name_str.clone()); - self.properties.insert(name_str, schema); - self - } - - // =========================================================================== - // TYPED PROPERTY METHODS - Cleaner API without PrimitiveSchema wrapper - // =========================================================================== - - /// Add a string property with custom builder (required) - pub fn string_property( - mut self, - name: impl Into, - f: impl FnOnce(StringSchema) -> StringSchema, - ) -> Self { - self.properties - .insert(name.into(), PrimitiveSchema::String(f(StringSchema::new()))); - self - } - - /// Add a required string property with custom builder - pub fn required_string_property( - mut self, - name: impl Into, - f: impl FnOnce(StringSchema) -> StringSchema, - ) -> Self { - let name_str = name.into(); - self.required.push(name_str.clone()); - self.properties - .insert(name_str, PrimitiveSchema::String(f(StringSchema::new()))); - self - } - - /// Add a number property with custom builder - pub fn number_property( - mut self, - name: impl Into, - f: impl FnOnce(NumberSchema) -> NumberSchema, - ) -> Self { - self.properties - .insert(name.into(), PrimitiveSchema::Number(f(NumberSchema::new()))); - self - } - - /// Add a required number property with custom builder - pub fn required_number_property( - mut self, - name: impl Into, - f: impl FnOnce(NumberSchema) -> NumberSchema, - ) -> Self { - let name_str = name.into(); - self.required.push(name_str.clone()); - self.properties - .insert(name_str, PrimitiveSchema::Number(f(NumberSchema::new()))); - self - } - - /// Add an integer property with custom builder - pub fn integer_property( - mut self, - name: impl Into, - f: impl FnOnce(IntegerSchema) -> IntegerSchema, - ) -> Self { - self.properties.insert( - name.into(), - PrimitiveSchema::Integer(f(IntegerSchema::new())), - ); - self - } - - /// Add a required integer property with custom builder - pub fn required_integer_property( - mut self, - name: impl Into, - f: impl FnOnce(IntegerSchema) -> IntegerSchema, - ) -> Self { - let name_str = name.into(); - self.required.push(name_str.clone()); - self.properties - .insert(name_str, PrimitiveSchema::Integer(f(IntegerSchema::new()))); - self - } - - /// Add a boolean property with custom builder - pub fn bool_property( - mut self, - name: impl Into, - f: impl FnOnce(BooleanSchema) -> BooleanSchema, - ) -> Self { - self.properties.insert( - name.into(), - PrimitiveSchema::Boolean(f(BooleanSchema::new())), - ); - self - } - - /// Add a required boolean property with custom builder - pub fn required_bool_property( - mut self, - name: impl Into, - f: impl FnOnce(BooleanSchema) -> BooleanSchema, - ) -> Self { - let name_str = name.into(); - self.required.push(name_str.clone()); - self.properties - .insert(name_str, PrimitiveSchema::Boolean(f(BooleanSchema::new()))); - self - } - - // =========================================================================== - // CONVENIENCE METHODS - Simple common cases - // =========================================================================== - - /// Add a required string property - pub fn required_string(self, name: impl Into) -> Self { - self.required_property(name, PrimitiveSchema::String(StringSchema::new())) - } - - /// Add an optional string property - pub fn optional_string(self, name: impl Into) -> Self { - self.property(name, PrimitiveSchema::String(StringSchema::new())) - } - - /// Add a required email property - pub fn required_email(self, name: impl Into) -> Self { - self.required_property(name, PrimitiveSchema::String(StringSchema::email())) - } - - /// Add an optional email property - pub fn optional_email(self, name: impl Into) -> Self { - self.property(name, PrimitiveSchema::String(StringSchema::email())) - } - - /// Add a required string property with custom builder - pub fn required_string_with( - self, - name: impl Into, - f: impl FnOnce(StringSchema) -> StringSchema, - ) -> Self { - self.required_property(name, PrimitiveSchema::String(f(StringSchema::new()))) - } - - /// Add an optional string property with custom builder - pub fn optional_string_with( - self, - name: impl Into, - f: impl FnOnce(StringSchema) -> StringSchema, - ) -> Self { - self.property(name, PrimitiveSchema::String(f(StringSchema::new()))) - } - - // Convenience methods for numbers - - /// Add a required number property with range - pub fn required_number(self, name: impl Into, min: f64, max: f64) -> Self { - self.required_property( - name, - PrimitiveSchema::Number(NumberSchema::new().range(min, max)), - ) - } - - /// Add an optional number property with range - pub fn optional_number(self, name: impl Into, min: f64, max: f64) -> Self { - self.property( - name, - PrimitiveSchema::Number(NumberSchema::new().range(min, max)), - ) - } - - /// Add a required number property with custom builder - pub fn required_number_with( - self, - name: impl Into, - f: impl FnOnce(NumberSchema) -> NumberSchema, - ) -> Self { - self.required_property(name, PrimitiveSchema::Number(f(NumberSchema::new()))) - } - - /// Add an optional number property with custom builder - pub fn optional_number_with( - self, - name: impl Into, - f: impl FnOnce(NumberSchema) -> NumberSchema, - ) -> Self { - self.property(name, PrimitiveSchema::Number(f(NumberSchema::new()))) - } - - // Convenience methods for integers - - /// Add a required integer property with range - pub fn required_integer(self, name: impl Into, min: i64, max: i64) -> Self { - self.required_property( - name, - PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)), - ) - } - - /// Add an optional integer property with range - pub fn optional_integer(self, name: impl Into, min: i64, max: i64) -> Self { - self.property( - name, - PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)), - ) - } - - /// Add a required integer property with custom builder - pub fn required_integer_with( - self, - name: impl Into, - f: impl FnOnce(IntegerSchema) -> IntegerSchema, - ) -> Self { - self.required_property(name, PrimitiveSchema::Integer(f(IntegerSchema::new()))) - } - - /// Add an optional integer property with custom builder - pub fn optional_integer_with( - self, - name: impl Into, - f: impl FnOnce(IntegerSchema) -> IntegerSchema, - ) -> Self { - self.property(name, PrimitiveSchema::Integer(f(IntegerSchema::new()))) - } - - // Convenience methods for booleans - - /// Add a required boolean property - pub fn required_bool(self, name: impl Into) -> Self { - self.required_property(name, PrimitiveSchema::Boolean(BooleanSchema::new())) - } - - /// Add an optional boolean property with default value - pub fn optional_bool(self, name: impl Into, default: bool) -> Self { - self.property( - name, - PrimitiveSchema::Boolean(BooleanSchema::new().with_default(default)), - ) - } - - /// Add a required boolean property with custom builder - pub fn required_bool_with( - self, - name: impl Into, - f: impl FnOnce(BooleanSchema) -> BooleanSchema, - ) -> Self { - self.required_property(name, PrimitiveSchema::Boolean(f(BooleanSchema::new()))) - } - - /// Add an optional boolean property with custom builder - pub fn optional_bool_with( - self, - name: impl Into, - f: impl FnOnce(BooleanSchema) -> BooleanSchema, - ) -> Self { - self.property(name, PrimitiveSchema::Boolean(f(BooleanSchema::new()))) - } - - // Enum convenience methods - - /// Add a required enum property using EnumSchema - pub fn required_enum_schema(self, name: impl Into, enum_schema: EnumSchema) -> Self { - self.required_property(name, PrimitiveSchema::Enum(enum_schema)) - } - - /// Add an optional enum property using EnumSchema - pub fn optional_enum_schema(self, name: impl Into, enum_schema: EnumSchema) -> Self { - self.property(name, PrimitiveSchema::Enum(enum_schema)) - } - - /// Add a required enum property using values. Creates an untitled single-select enum. - #[deprecated( - since = "0.13.0", - note = "Use ElicitationSchemaBuilder::required_enum_schema with EnumSchema::builder instead" - )] - pub fn required_enum(self, name: impl Into, values: Vec) -> Self { - self.required_property( - name, - PrimitiveSchema::Enum(EnumSchema::Legacy(LegacyEnumSchema { - type_: StringTypeConst, - title: None, - description: None, - enum_: values, - enum_names: None, - })), - ) - } - - /// Add an optional enum property using values. Creates an untitled single-select enum. - #[deprecated( - since = "0.13.0", - note = "Use ElicitationSchemaBuilder::optional_enum_schema with EnumSchema::builder instead" - )] - pub fn optional_enum(self, name: impl Into, values: Vec) -> Self { - self.property( - name, - PrimitiveSchema::Enum(EnumSchema::Legacy(LegacyEnumSchema { - type_: StringTypeConst, - title: None, - description: None, - enum_: values, - enum_names: None, - })), - ) - } - - /// Mark an existing property as required - pub fn mark_required(mut self, name: impl Into) -> Self { - self.required.push(name.into()); - self - } - - /// Set the schema title - pub fn title(mut self, title: impl Into>) -> Self { - self.title = Some(title.into()); - self - } - - /// Set the schema description - pub fn description(mut self, description: impl Into>) -> Self { - self.description = Some(description.into()); - self - } - - /// Build the elicitation schema with validation - /// - /// # Errors - /// - /// Returns an error if: - /// - Required fields reference non-existent properties - /// - No properties are defined (empty schema) - pub fn build(self) -> Result { - // Validate that all required fields exist in properties - if !self.required.is_empty() { - for field_name in &self.required { - if !self.properties.contains_key(field_name) { - return Err("Required field does not exist in properties"); - } - } - } - - Ok(ElicitationSchema { - type_: ObjectTypeConst, - title: self.title, - properties: self.properties, - required: if self.required.is_empty() { - None - } else { - Some(self.required) - }, - description: self.description, - }) - } - - /// Build the elicitation schema without validation (panics on invalid schema) - /// - /// # Panics - /// - /// Panics if required fields reference non-existent properties - pub fn build_unchecked(self) -> ElicitationSchema { - self.build().expect("Invalid elicitation schema") - } -} - -#[cfg(test)] -mod tests { - use anyhow::anyhow; - use serde_json::json; - - use super::*; - - #[test] - fn test_string_schema_serialization() { - let schema = StringSchema::email().description("Email address"); - let json = serde_json::to_value(&schema).unwrap(); - - assert_eq!(json["type"], "string"); - assert_eq!(json["format"], "email"); - assert_eq!(json["description"], "Email address"); - } - - #[test] - fn test_number_schema_serialization() { - let schema = NumberSchema::new() - .range(0.0, 100.0) - .description("Percentage"); - let json = serde_json::to_value(&schema).unwrap(); - - assert_eq!(json["type"], "number"); - assert_eq!(json["minimum"], 0.0); - assert_eq!(json["maximum"], 100.0); - } - - #[test] - fn test_integer_schema_serialization() { - let schema = IntegerSchema::new().range(0, 150); - let json = serde_json::to_value(&schema).unwrap(); - - assert_eq!(json["type"], "integer"); - assert_eq!(json["minimum"], 0); - assert_eq!(json["maximum"], 150); - } - - #[test] - fn test_boolean_schema_serialization() { - let schema = BooleanSchema::new().with_default(true); - let json = serde_json::to_value(&schema).unwrap(); - - assert_eq!(json["type"], "boolean"); - assert_eq!(json["default"], true); - } - - #[test] - fn test_enum_schema_untitled_single_select_serialization() { - let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) - .description("Country code") - .build(); - let json = serde_json::to_value(&schema).unwrap(); - - assert_eq!(json["type"], "string"); - assert_eq!(json["enum"], json!(["US", "UK"])); - assert_eq!(json["description"], "Country code"); - } - - #[test] - fn test_enum_schema_untitled_multi_select_serialization() -> anyhow::Result<()> { - let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) - .multiselect() - .min_items(1u64) - .map_err(|e| anyhow!("{e}"))? - .max_items(4u64) - .map_err(|e| anyhow!("{e}"))? - .description("Country code") - .build(); - let json = serde_json::to_value(&schema)?; - - assert_eq!(json["type"], "array"); - assert_eq!(json["minItems"], 1u64); - assert_eq!(json["maxItems"], 4u64); - assert_eq!(json["items"], json!({"type":"string", "enum":["US", "UK"]})); - assert_eq!(json["description"], "Country code"); - Ok(()) - } - - #[test] - fn test_enum_schema_titled_single_select_serialization() -> anyhow::Result<()> { - let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) - .enum_titles(vec![ - "United States".to_string(), - "United Kingdom".to_string(), - ]) - .map_err(|e| anyhow!("{e}"))? - .description("Country code") - .build(); - let json = serde_json::to_value(&schema)?; - - assert_eq!(json["type"], "string"); - assert_eq!( - json["oneOf"], - json!([ - {"const": "US", "title":"United States"}, - {"const": "UK", "title":"United Kingdom"} - ]) - ); - assert_eq!(json["description"], "Country code"); - Ok(()) - } - - #[test] - fn test_enum_schema_legacy_serialization() -> anyhow::Result<()> { - let schema = EnumSchema::Legacy(LegacyEnumSchema { - type_: StringTypeConst, - title: Some("Legacy Enum".into()), - description: Some("A legacy enum schema".into()), - enum_: vec!["A".to_string(), "B".to_string()], - enum_names: Some(vec!["Option A".to_string(), "Option B".to_string()]), - }); - let json = serde_json::to_value(&schema)?; - - assert_eq!(json["type"], "string"); - assert_eq!(json["title"], "Legacy Enum"); - assert_eq!(json["description"], "A legacy enum schema"); - assert_eq!(json["enum"], json!(["A", "B"])); - assert_eq!(json["enumNames"], json!(["Option A", "Option B"])); - Ok(()) - } - - #[test] - fn test_enum_schema_titled_multi_select_serialization() -> anyhow::Result<()> { - let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) - .enum_titles(vec![ - "United States".to_string(), - "United Kingdom".to_string(), - ]) - .map_err(|e| anyhow!("{e}"))? - .multiselect() - .min_items(1u64) - .map_err(|e| anyhow!("{e}"))? - .max_items(4u64) - .map_err(|e| anyhow!("{e}"))? - .description("Country code") - .build(); - let json = serde_json::to_value(&schema)?; - - assert_eq!(json["type"], "array"); - assert_eq!(json["minItems"], 1u64); - assert_eq!(json["maxItems"], 4u64); - assert_eq!( - json["items"], - json!({"anyOf":[ - {"const":"US","title":"United States"}, - {"const":"UK","title":"United Kingdom"} - ]}) - ); - assert_eq!(json["description"], "Country code"); - Ok(()) - } - - #[test] - fn test_enum_schema_single_select_with_default() -> anyhow::Result<()> { - let schema = EnumSchema::builder(vec![ - "red".to_string(), - "green".to_string(), - "blue".to_string(), - ]) - .with_default("green") - .map_err(|e| anyhow!("{e}"))? - .description("Favorite color") - .build(); - - let json = serde_json::to_value(&schema)?; - - assert_eq!(json["type"], "string"); - assert_eq!(json["enum"], json!(["red", "green", "blue"])); - assert_eq!(json["default"], "green"); - assert_eq!(json["description"], "Favorite color"); - Ok(()) - } - - #[test] - fn test_enum_schema_multi_select_with_default() -> anyhow::Result<()> { - let schema = EnumSchema::builder(vec![ - "red".to_string(), - "green".to_string(), - "blue".to_string(), - ]) - .multiselect() - .with_default(vec!["red".to_string(), "blue".to_string()]) - .map_err(|e| anyhow!("{e}"))? - .min_items(1) - .map_err(|e| anyhow!("{e}"))? - .max_items(3) - .map_err(|e| anyhow!("{e}"))? - .build(); - - let json = serde_json::to_value(&schema)?; - - assert_eq!(json["type"], "array"); - assert_eq!(json["items"]["enum"], json!(["red", "green", "blue"])); - assert_eq!(json["default"], json!(["red", "blue"])); - assert_eq!(json["minItems"], 1); - assert_eq!(json["maxItems"], 3); - Ok(()) - } - - #[test] - fn test_enum_schema_transition_clears_defaults() -> anyhow::Result<()> { - // Start with single-select with default - let builder = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]) - .with_default("A") - .map_err(|e| anyhow!("{e}"))?; - - // Transition to multi-select should clear the default - let schema = builder.multiselect().build(); - let json = serde_json::to_value(&schema)?; - - assert_eq!(json["type"], "array"); - assert!(json["default"].is_null()); - Ok(()) - } - - #[test] - fn test_enum_schema_multi_to_single_transition() -> anyhow::Result<()> { - // Start with multi-select with defaults - let builder = EnumSchema::builder(vec!["A".to_string(), "B".to_string(), "C".to_string()]) - .multiselect() - .with_default(vec!["A".to_string(), "B".to_string()]) - .map_err(|e| anyhow!("{e}"))? - .min_items(1) - .map_err(|e| anyhow!("{e}"))?; - - // Transition back to single-select should clear defaults and min/max items - let schema = builder.single_select().build(); - let json = serde_json::to_value(&schema)?; - - assert_eq!(json["type"], "string"); - assert!(json["default"].is_null()); - assert!(json["minItems"].is_null()); - assert!(json["maxItems"].is_null()); - Ok(()) - } - - #[test] - fn test_enum_schema_invalid_single_default() { - let result = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]).with_default("C"); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - "Provided default value is not in enum values" - ); - } - - #[test] - fn test_enum_schema_invalid_multi_default() { - let result = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]) - .multiselect() - .with_default(vec!["A".to_string(), "C".to_string()]); - - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - "One of the provided default values is not in enum values" - ); - } - - #[test] - fn test_enum_schema_titled_with_default() -> anyhow::Result<()> { - let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) - .enum_titles(vec![ - "United States".to_string(), - "United Kingdom".to_string(), - ]) - .map_err(|e| anyhow!("{e}"))? - .with_default("UK") - .map_err(|e| anyhow!("{e}"))? - .build(); - - let json = serde_json::to_value(&schema)?; - - assert_eq!(json["type"], "string"); - assert_eq!(json["default"], "UK"); - assert_eq!( - json["oneOf"], - json!([ - {"const": "US", "title": "United States"}, - {"const": "UK", "title": "United Kingdom"} - ]) - ); - Ok(()) - } - - #[test] - fn test_enum_schema_untitled_after_titled() -> anyhow::Result<()> { - let schema = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]) - .enum_titles(vec!["Option A".to_string(), "Option B".to_string()]) - .map_err(|e| anyhow!("{e}"))? - .untitled() - .build(); - - let json = serde_json::to_value(&schema)?; - - assert_eq!(json["type"], "string"); - assert_eq!(json["enum"], json!(["A", "B"])); - assert!(json["oneOf"].is_null()); - Ok(()) - } - - #[test] - fn test_primitive_schema_enum_deserialization() { - // Test that enum schemas deserialize as Enum variant, not String - let json = json!({ - "type": "string", - "enum": ["a", "b"] - }); - let schema: PrimitiveSchema = serde_json::from_value(json).unwrap(); - assert!(matches!(schema, PrimitiveSchema::Enum(_))); - // Test that string schemas deserialize as String variant - let json = json!({ - "type": "string" - }); - let schema: PrimitiveSchema = serde_json::from_value(json).unwrap(); - assert!(matches!(schema, PrimitiveSchema::String(_))); - } - - #[test] - fn test_elicitation_schema_builder_simple() { - let schema = ElicitationSchema::builder() - .required_email("email") - .optional_bool("newsletter", false) - .build() - .unwrap(); - - assert_eq!(schema.properties.len(), 2); - assert!(schema.properties.contains_key("email")); - assert!(schema.properties.contains_key("newsletter")); - assert_eq!(schema.required, Some(vec!["email".to_string()])); - } - - #[test] - fn test_elicitation_schema_builder_complex() { - let enum_schema = - EnumSchema::builder(vec!["US".to_string(), "UK".to_string(), "CA".to_string()]).build(); - let schema = ElicitationSchema::builder() - .required_string_with("name", |s| s.length(1, 100)) - .required_integer("age", 0, 150) - .optional_bool("newsletter", false) - .required_enum_schema("country", enum_schema) - .description("User registration") - .build() - .unwrap(); - - assert_eq!(schema.properties.len(), 4); - assert_eq!( - schema.required, - Some(vec![ - "name".to_string(), - "age".to_string(), - "country".to_string() - ]) - ); - assert_eq!(schema.description.as_deref(), Some("User registration")); - } - - #[test] - fn test_elicitation_schema_serialization() { - let schema = ElicitationSchema::builder() - .required_string_with("name", |s| s.length(1, 100)) - .build() - .unwrap(); - - let json = serde_json::to_value(&schema).unwrap(); - - assert_eq!(json["type"], "object"); - assert!(json["properties"]["name"].is_object()); - assert_eq!(json["required"], json!(["name"])); - } - - #[test] - #[should_panic(expected = "minimum must be <= maximum")] - fn test_integer_range_validation() { - IntegerSchema::new().range(10, 5); // Should panic - } - - #[test] - #[should_panic(expected = "min_length must be <= max_length")] - fn test_string_length_validation() { - StringSchema::new().length(10, 5); // Should panic - } - - #[test] - fn test_integer_range_validation_with_result() { - let result = IntegerSchema::new().with_range(10, 5); - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), "minimum must be <= maximum"); - } - - #[cfg(feature = "schemars")] - mod schemars_tests { - use anyhow::Result; - use schemars::JsonSchema; - use serde::{Deserialize, Serialize}; - use serde_json::json; - - use crate::model::ElicitationSchema; - - #[derive(Debug, Serialize, Deserialize, JsonSchema, Default)] - #[schemars(inline)] - #[schemars(extend("type" = "string"))] - enum TitledEnum { - #[schemars(title = "Title for the first value")] - #[default] - FirstValue, - #[schemars(title = "Title for the second value")] - SecondValue, - } - - #[derive(Debug, Serialize, Deserialize, JsonSchema)] - #[schemars(inline)] - enum UntitledEnum { - First, - Second, - Third, - } - - fn default_untitled_multi_select() -> Vec { - vec![UntitledEnum::Second, UntitledEnum::Third] - } - - #[derive(Debug, Serialize, Deserialize, JsonSchema)] - #[schemars(description = "User information")] - struct UserInfo { - #[schemars(description = "User's name")] - pub name: String, - pub single_select_untitled: UntitledEnum, - #[schemars( - title = "Single Select Titled", - description = "Description for single select enum", - default - )] - pub single_select_titled: TitledEnum, - #[serde(default = "default_untitled_multi_select")] - pub multi_select_untitled: Vec, - #[schemars( - title = "Multi Select Titled", - description = "Multi Select Description" - )] - pub multi_select_titled: Vec, - } - - #[test] - fn test_schema_inference_for_enum_fields() -> Result<()> { - let schema = ElicitationSchema::from_type::()?; - - let json = serde_json::to_value(&schema)?; - assert_eq!(json["type"], "object"); - assert_eq!(json["description"], "User information"); - assert_eq!( - json["required"], - json!(["name", "single_select_untitled", "multi_select_titled"]) - ); - let properties = match json.get("properties") { - Some(serde_json::Value::Object(map)) => map, - _ => panic!("Schema does not have 'properties' field or it is not object type"), - }; - - assert_eq!(properties.len(), 5); - assert_eq!( - properties["name"], - json!({"description":"User's name", "type":"string"}) - ); - - assert_eq!( - properties["single_select_untitled"], - serde_json::json!({ - "type":"string", - "enum":["First", "Second", "Third"] - }) - ); - - assert_eq!( - properties["single_select_titled"], - json!({ - "type":"string", - "title":"Single Select Titled", - "description":"Description for single select enum", - "oneOf":[ - {"const":"FirstValue", "title":"Title for the first value"}, - {"const":"SecondValue", "title":"Title for the second value"} - ], - "default":"FirstValue" - }) - ); - assert_eq!( - properties["multi_select_untitled"], - serde_json::json!({ - "type":"array", - "items" : { - "type":"string", - "enum":["First", "Second", "Third"] - }, - "default":["Second", "Third"] - }) - ); - assert_eq!( - properties["multi_select_titled"], - serde_json::json!({ - "type":"array", - "title":"Multi Select Titled", - "description":"Multi Select Description", - "items":{ - "anyOf":[ - {"const":"FirstValue", "title":"Title for the first value"}, - {"const":"SecondValue", "title":"Title for the second value"} - ] - } - }) - ); - Ok(()) - } - } -} diff --git a/codex-rs/vendor/rmcp/src/model/extension.rs b/codex-rs/vendor/rmcp/src/model/extension.rs deleted file mode 100644 index 039fdf2eb7..0000000000 --- a/codex-rs/vendor/rmcp/src/model/extension.rs +++ /dev/null @@ -1,337 +0,0 @@ -//! A container for those extra data could be carried on request or notification. -//! -//! This file is copied and modified from crate [http](https://github.com/hyperium/http). -//! -//! - Original code license: -//! - Original code: -use std::{ - any::{Any, TypeId}, - collections::HashMap, - fmt, - hash::{BuildHasherDefault, Hasher}, -}; - -type AnyMap = HashMap, BuildHasherDefault>; - -// With TypeIds as keys, there's no need to hash them. They are already hashes -// themselves, coming from the compiler. The IdHasher just holds the u64 of -// the TypeId, and then returns it, instead of doing any bit fiddling. -#[derive(Default)] -struct IdHasher(u64); - -impl Hasher for IdHasher { - fn write(&mut self, _: &[u8]) { - unreachable!("TypeId calls write_u64"); - } - - #[inline] - fn write_u64(&mut self, id: u64) { - self.0 = id; - } - - #[inline] - fn finish(&self) -> u64 { - self.0 - } -} - -/// A type map of protocol extensions. -/// -/// `Extensions` can be used by `Request` `Notification` and `Response` to store -/// extra data derived from the underlying protocol. -#[derive(Clone, Default)] -pub struct Extensions { - // If extensions are never used, no need to carry around an empty HashMap. - // That's 3 words. Instead, this is only 1 word. - map: Option>, -} - -impl Extensions { - /// Create an empty `Extensions`. - #[inline] - pub const fn new() -> Extensions { - Extensions { map: None } - } - - /// Insert a type into this `Extensions`. - /// - /// If a extension of this type already existed, it will - /// be returned and replaced with the new one. - /// - /// # Example - /// - /// ``` - /// # use rmcp::model::Extensions; - /// let mut ext = Extensions::new(); - /// assert!(ext.insert(5i32).is_none()); - /// assert!(ext.insert(4u8).is_none()); - /// assert_eq!(ext.insert(9i32), Some(5i32)); - /// ``` - pub fn insert(&mut self, val: T) -> Option { - self.map - .get_or_insert_with(Box::default) - .insert(TypeId::of::(), Box::new(val)) - .and_then(|boxed| boxed.into_any().downcast().ok().map(|boxed| *boxed)) - } - - /// Get a reference to a type previously inserted on this `Extensions`. - /// - /// # Example - /// - /// ``` - /// # use rmcp::model::Extensions; - /// let mut ext = Extensions::new(); - /// assert!(ext.get::().is_none()); - /// ext.insert(5i32); - /// - /// assert_eq!(ext.get::(), Some(&5i32)); - /// ``` - pub fn get(&self) -> Option<&T> { - self.map - .as_ref() - .and_then(|map| map.get(&TypeId::of::())) - .and_then(|boxed| (**boxed).as_any().downcast_ref()) - } - - /// Get a mutable reference to a type previously inserted on this `Extensions`. - /// - /// # Example - /// - /// ``` - /// # use rmcp::model::Extensions; - /// let mut ext = Extensions::new(); - /// ext.insert(String::from("Hello")); - /// ext.get_mut::().unwrap().push_str(" World"); - /// - /// assert_eq!(ext.get::().unwrap(), "Hello World"); - /// ``` - pub fn get_mut(&mut self) -> Option<&mut T> { - self.map - .as_mut() - .and_then(|map| map.get_mut(&TypeId::of::())) - .and_then(|boxed| (**boxed).as_any_mut().downcast_mut()) - } - - /// Get a mutable reference to a type, inserting `value` if not already present on this - /// `Extensions`. - /// - /// # Example - /// - /// ``` - /// # use rmcp::model::Extensions; - /// let mut ext = Extensions::new(); - /// *ext.get_or_insert(1i32) += 2; - /// - /// assert_eq!(*ext.get::().unwrap(), 3); - /// ``` - pub fn get_or_insert(&mut self, value: T) -> &mut T { - self.get_or_insert_with(|| value) - } - - /// Get a mutable reference to a type, inserting the value created by `f` if not already present - /// on this `Extensions`. - /// - /// # Example - /// - /// ``` - /// # use rmcp::model::Extensions; - /// let mut ext = Extensions::new(); - /// *ext.get_or_insert_with(|| 1i32) += 2; - /// - /// assert_eq!(*ext.get::().unwrap(), 3); - /// ``` - pub fn get_or_insert_with T>( - &mut self, - f: F, - ) -> &mut T { - let out = self - .map - .get_or_insert_with(Box::default) - .entry(TypeId::of::()) - .or_insert_with(|| Box::new(f())); - (**out).as_any_mut().downcast_mut().unwrap() - } - - /// Get a mutable reference to a type, inserting the type's default value if not already present - /// on this `Extensions`. - /// - /// # Example - /// - /// ``` - /// # use rmcp::model::Extensions; - /// let mut ext = Extensions::new(); - /// *ext.get_or_insert_default::() += 2; - /// - /// assert_eq!(*ext.get::().unwrap(), 2); - /// ``` - pub fn get_or_insert_default(&mut self) -> &mut T { - self.get_or_insert_with(T::default) - } - - /// Remove a type from this `Extensions`. - /// - /// If a extension of this type existed, it will be returned. - /// - /// # Example - /// - /// ``` - /// # use rmcp::model::Extensions; - /// let mut ext = Extensions::new(); - /// ext.insert(5i32); - /// assert_eq!(ext.remove::(), Some(5i32)); - /// assert!(ext.get::().is_none()); - /// ``` - pub fn remove(&mut self) -> Option { - self.map - .as_mut() - .and_then(|map| map.remove(&TypeId::of::())) - .and_then(|boxed| boxed.into_any().downcast().ok().map(|boxed| *boxed)) - } - - /// Clear the `Extensions` of all inserted extensions. - /// - /// # Example - /// - /// ``` - /// # use rmcp::model::Extensions; - /// let mut ext = Extensions::new(); - /// ext.insert(5i32); - /// ext.clear(); - /// - /// assert!(ext.get::().is_none()); - /// ``` - #[inline] - pub fn clear(&mut self) { - if let Some(ref mut map) = self.map { - map.clear(); - } - } - - /// Check whether the extension set is empty or not. - /// - /// # Example - /// - /// ``` - /// # use rmcp::model::Extensions; - /// let mut ext = Extensions::new(); - /// assert!(ext.is_empty()); - /// ext.insert(5i32); - /// assert!(!ext.is_empty()); - /// ``` - #[inline] - pub fn is_empty(&self) -> bool { - self.map.as_ref().is_none_or(|map| map.is_empty()) - } - - /// Get the number of extensions available. - /// - /// # Example - /// - /// ``` - /// # use rmcp::model::Extensions; - /// let mut ext = Extensions::new(); - /// assert_eq!(ext.len(), 0); - /// ext.insert(5i32); - /// assert_eq!(ext.len(), 1); - /// ``` - #[inline] - pub fn len(&self) -> usize { - self.map.as_ref().map_or(0, |map| map.len()) - } - - /// Extends `self` with another `Extensions`. - /// - /// If an instance of a specific type exists in both, the one in `self` is overwritten with the - /// one from `other`. - /// - /// # Example - /// - /// ``` - /// # use rmcp::model::Extensions; - /// let mut ext_a = Extensions::new(); - /// ext_a.insert(8u8); - /// ext_a.insert(16u16); - /// - /// let mut ext_b = Extensions::new(); - /// ext_b.insert(4u8); - /// ext_b.insert("hello"); - /// - /// ext_a.extend(ext_b); - /// assert_eq!(ext_a.len(), 3); - /// assert_eq!(ext_a.get::(), Some(&4u8)); - /// assert_eq!(ext_a.get::(), Some(&16u16)); - /// assert_eq!(ext_a.get::<&'static str>().copied(), Some("hello")); - /// ``` - pub fn extend(&mut self, other: Self) { - if let Some(other) = other.map { - if let Some(map) = &mut self.map { - map.extend(*other); - } else { - self.map = Some(other); - } - } - } -} - -impl fmt::Debug for Extensions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Extensions").finish() - } -} - -trait AnyClone: Any { - fn clone_box(&self) -> Box; - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - fn into_any(self: Box) -> Box; -} - -impl AnyClone for T { - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn into_any(self: Box) -> Box { - self - } -} - -impl Clone for Box { - fn clone(&self) -> Self { - (**self).clone_box() - } -} - -#[test] -fn test_extensions() { - #[derive(Clone, Debug, PartialEq)] - struct MyType(i32); - - let mut extensions = Extensions::new(); - - extensions.insert(5i32); - extensions.insert(MyType(10)); - - assert_eq!(extensions.get(), Some(&5i32)); - assert_eq!(extensions.get_mut(), Some(&mut 5i32)); - - let ext2 = extensions.clone(); - - assert_eq!(extensions.remove::(), Some(5i32)); - assert!(extensions.get::().is_none()); - - // clone still has it - assert_eq!(ext2.get(), Some(&5i32)); - assert_eq!(ext2.get(), Some(&MyType(10))); - - assert_eq!(extensions.get::(), None); - assert_eq!(extensions.get(), Some(&MyType(10))); -} diff --git a/codex-rs/vendor/rmcp/src/model/meta.rs b/codex-rs/vendor/rmcp/src/model/meta.rs deleted file mode 100644 index c60762a357..0000000000 --- a/codex-rs/vendor/rmcp/src/model/meta.rs +++ /dev/null @@ -1,289 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use super::{ - ClientNotification, ClientRequest, CustomNotification, CustomRequest, Extensions, JsonObject, - JsonRpcMessage, NumberOrString, ProgressToken, ServerNotification, ServerRequest, -}; - -pub trait GetMeta { - fn get_meta_mut(&mut self) -> &mut Meta; - fn get_meta(&self) -> &Meta; -} - -pub trait GetExtensions { - fn extensions(&self) -> &Extensions; - fn extensions_mut(&mut self) -> &mut Extensions; -} - -/// Trait for request params that contain the `_meta` field. -/// -/// Per the MCP 2025-11-25 spec, all request params should have an optional `_meta` -/// field that can contain a `progressToken` for tracking long-running operations. -pub trait RequestParamsMeta { - /// Get a reference to the meta field - fn meta(&self) -> Option<&Meta>; - /// Get a mutable reference to the meta field - fn meta_mut(&mut self) -> &mut Option; - /// Set the meta field - fn set_meta(&mut self, meta: Meta) { - *self.meta_mut() = Some(meta); - } - /// Get the progress token from meta, if present - fn progress_token(&self) -> Option { - self.meta().and_then(|m| m.get_progress_token()) - } - /// Set a progress token in meta - fn set_progress_token(&mut self, token: ProgressToken) { - match self.meta_mut() { - Some(meta) => meta.set_progress_token(token), - none => { - let mut meta = Meta::new(); - meta.set_progress_token(token); - *none = Some(meta); - } - } - } -} - -/// Trait for task-augmented request params that contain both `_meta` and `task` fields. -/// -/// Per the MCP 2025-11-25 spec, certain requests (like `tools/call` and `sampling/createMessage`) -/// can include a `task` field to signal that the caller wants task-augmented execution. -pub trait TaskAugmentedRequestParamsMeta: RequestParamsMeta { - /// Get a reference to the task field - fn task(&self) -> Option<&JsonObject>; - /// Get a mutable reference to the task field - fn task_mut(&mut self) -> &mut Option; - /// Set the task field - fn set_task(&mut self, task: JsonObject) { - *self.task_mut() = Some(task); - } -} - -impl GetExtensions for CustomNotification { - fn extensions(&self) -> &Extensions { - &self.extensions - } - fn extensions_mut(&mut self) -> &mut Extensions { - &mut self.extensions - } -} - -impl GetMeta for CustomNotification { - fn get_meta_mut(&mut self) -> &mut Meta { - self.extensions_mut().get_or_insert_default() - } - fn get_meta(&self) -> &Meta { - self.extensions() - .get::() - .unwrap_or(Meta::static_empty()) - } -} - -impl GetExtensions for CustomRequest { - fn extensions(&self) -> &Extensions { - &self.extensions - } - fn extensions_mut(&mut self) -> &mut Extensions { - &mut self.extensions - } -} - -impl GetMeta for CustomRequest { - fn get_meta_mut(&mut self) -> &mut Meta { - self.extensions_mut().get_or_insert_default() - } - fn get_meta(&self) -> &Meta { - self.extensions() - .get::() - .unwrap_or(Meta::static_empty()) - } -} - -macro_rules! variant_extension { - ( - $Enum: ident { - $($variant: ident)* - } - ) => { - impl GetExtensions for $Enum { - fn extensions(&self) -> &Extensions { - match self { - $( - $Enum::$variant(v) => &v.extensions, - )* - } - } - fn extensions_mut(&mut self) -> &mut Extensions { - match self { - $( - $Enum::$variant(v) => &mut v.extensions, - )* - } - } - } - impl GetMeta for $Enum { - fn get_meta_mut(&mut self) -> &mut Meta { - self.extensions_mut().get_or_insert_default() - } - fn get_meta(&self) -> &Meta { - self.extensions().get::().unwrap_or(Meta::static_empty()) - } - } - }; -} - -variant_extension! { - ClientRequest { - PingRequest - InitializeRequest - CompleteRequest - SetLevelRequest - GetPromptRequest - ListPromptsRequest - ListResourcesRequest - ListResourceTemplatesRequest - ReadResourceRequest - SubscribeRequest - UnsubscribeRequest - CallToolRequest - ListToolsRequest - CustomRequest - GetTaskInfoRequest - ListTasksRequest - GetTaskResultRequest - CancelTaskRequest - } -} - -variant_extension! { - ServerRequest { - PingRequest - CreateMessageRequest - ListRootsRequest - CreateElicitationRequest - CustomRequest - } -} - -variant_extension! { - ClientNotification { - CancelledNotification - ProgressNotification - InitializedNotification - RootsListChangedNotification - CustomNotification - } -} - -variant_extension! { - ServerNotification { - CancelledNotification - ProgressNotification - LoggingMessageNotification - ResourceUpdatedNotification - ResourceListChangedNotification - ToolListChangedNotification - PromptListChangedNotification - ElicitationCompletionNotification - CustomNotification - } -} -#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[serde(transparent)] -pub struct Meta(pub JsonObject); -const PROGRESS_TOKEN_FIELD: &str = "progressToken"; -impl Meta { - pub fn new() -> Self { - Self(JsonObject::new()) - } - - /// Create a new Meta with a progress token set - pub fn with_progress_token(token: ProgressToken) -> Self { - let mut meta = Self::new(); - meta.set_progress_token(token); - meta - } - - pub(crate) fn static_empty() -> &'static Self { - static EMPTY: std::sync::OnceLock = std::sync::OnceLock::new(); - EMPTY.get_or_init(Default::default) - } - - pub fn get_progress_token(&self) -> Option { - self.0.get(PROGRESS_TOKEN_FIELD).and_then(|v| match v { - Value::String(s) => Some(ProgressToken(NumberOrString::String(s.to_string().into()))), - Value::Number(n) => { - if let Some(i) = n.as_i64() { - Some(ProgressToken(NumberOrString::Number(i))) - } else if let Some(u) = n.as_u64() { - if u <= i64::MAX as u64 { - Some(ProgressToken(NumberOrString::Number(u as i64))) - } else { - None - } - } else { - None - } - } - _ => None, - }) - } - - pub fn set_progress_token(&mut self, token: ProgressToken) { - match token.0 { - NumberOrString::String(ref s) => self.0.insert( - PROGRESS_TOKEN_FIELD.to_string(), - Value::String(s.to_string()), - ), - NumberOrString::Number(n) => self - .0 - .insert(PROGRESS_TOKEN_FIELD.to_string(), Value::Number(n.into())), - }; - } - - pub fn extend(&mut self, other: Meta) { - for (k, v) in other.0.into_iter() { - self.0.insert(k, v); - } - } -} - -impl Deref for Meta { - type Target = JsonObject; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Meta { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl JsonRpcMessage -where - Req: GetExtensions, - Noti: GetExtensions, -{ - pub fn insert_extension(&mut self, value: T) { - match self { - JsonRpcMessage::Request(json_rpc_request) => { - json_rpc_request.request.extensions_mut().insert(value); - } - JsonRpcMessage::Notification(json_rpc_notification) => { - json_rpc_notification - .notification - .extensions_mut() - .insert(value); - } - _ => {} - } - } -} diff --git a/codex-rs/vendor/rmcp/src/model/prompt.rs b/codex-rs/vendor/rmcp/src/model/prompt.rs deleted file mode 100644 index f90aff1992..0000000000 --- a/codex-rs/vendor/rmcp/src/model/prompt.rs +++ /dev/null @@ -1,271 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::{ - AnnotateAble, Annotations, Icon, Meta, RawEmbeddedResource, - content::{EmbeddedResource, ImageContent}, - resource::ResourceContents, -}; - -/// A prompt that can be used to generate text from a model -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Prompt { - /// The name of the prompt - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - /// Optional description of what the prompt does - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - /// Optional arguments that can be passed to customize the prompt - #[serde(skip_serializing_if = "Option::is_none")] - pub arguments: Option>, - /// Optional list of icons for the prompt - #[serde(skip_serializing_if = "Option::is_none")] - pub icons: Option>, - /// Optional additional metadata for this prompt - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, -} - -impl Prompt { - /// Create a new prompt with the given name, description and arguments - pub fn new( - name: N, - description: Option, - arguments: Option>, - ) -> Self - where - N: Into, - D: Into, - { - Prompt { - name: name.into(), - title: None, - description: description.map(Into::into), - arguments, - icons: None, - meta: None, - } - } -} - -/// Represents a prompt argument that can be passed to customize the prompt -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct PromptArgument { - /// The name of the argument - pub name: String, - /// A human-readable title for the argument - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - /// A description of what the argument is used for - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - /// Whether this argument is required - #[serde(skip_serializing_if = "Option::is_none")] - pub required: Option, -} - -/// Represents the role of a message sender in a prompt conversation -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum PromptMessageRole { - User, - Assistant, -} - -/// Content types that can be included in prompt messages -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum PromptMessageContent { - /// Plain text content - Text { text: String }, - /// Image content with base64-encoded data - Image { - #[serde(flatten)] - image: ImageContent, - }, - /// Embedded server-side resource - Resource { resource: EmbeddedResource }, - /// A link to a resource that can be fetched separately - ResourceLink { - #[serde(flatten)] - link: super::resource::Resource, - }, -} - -impl PromptMessageContent { - pub fn text(text: impl Into) -> Self { - Self::Text { text: text.into() } - } - - /// Create a resource link content - pub fn resource_link(resource: super::resource::Resource) -> Self { - Self::ResourceLink { link: resource } - } -} - -/// A message in a prompt conversation -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct PromptMessage { - /// The role of the message sender - pub role: PromptMessageRole, - /// The content of the message - pub content: PromptMessageContent, -} - -impl PromptMessage { - /// Create a new text message with the given role and text content - pub fn new_text>(role: PromptMessageRole, text: S) -> Self { - Self { - role, - content: PromptMessageContent::Text { text: text.into() }, - } - } - - /// Create a new image message. `meta` and `annotations` are optional. - #[cfg(feature = "base64")] - pub fn new_image( - role: PromptMessageRole, - data: &[u8], - mime_type: &str, - meta: Option, - annotations: Option, - ) -> Self { - use base64::{Engine, prelude::BASE64_STANDARD}; - - let base64 = BASE64_STANDARD.encode(data); - Self { - role, - content: PromptMessageContent::Image { - image: crate::model::RawImageContent { - data: base64, - mime_type: mime_type.into(), - meta, - } - .optional_annotate(annotations), - }, - } - } - - /// Create a new resource message. `resource_meta`, `resource_content_meta`, and `annotations` are optional. - pub fn new_resource( - role: PromptMessageRole, - uri: String, - mime_type: Option, - text: Option, - resource_meta: Option, - resource_content_meta: Option, - annotations: Option, - ) -> Self { - let resource_contents = match text { - Some(t) => ResourceContents::TextResourceContents { - uri, - mime_type, - text: t, - meta: resource_content_meta, - }, - None => ResourceContents::BlobResourceContents { - uri, - mime_type, - blob: String::new(), - meta: resource_content_meta, - }, - }; - Self { - role, - content: PromptMessageContent::Resource { - resource: RawEmbeddedResource { - meta: resource_meta, - resource: resource_contents, - } - .optional_annotate(annotations), - }, - } - } - - /// Note: PromptMessage text content does not carry protocol-level _meta per current schema. - /// This function exists for API symmetry but ignores the meta parameter. - pub fn new_text_with_meta>( - role: PromptMessageRole, - text: S, - _meta: Option, - ) -> Self { - Self::new_text(role, text) - } - - /// Create a new resource link message - pub fn new_resource_link(role: PromptMessageRole, resource: super::resource::Resource) -> Self { - Self { - role, - content: PromptMessageContent::ResourceLink { link: resource }, - } - } -} - -#[cfg(test)] -mod tests { - use serde_json; - - use super::*; - - #[test] - fn test_prompt_message_image_serialization() { - let image_content = crate::model::RawImageContent { - data: "base64data".to_string(), - mime_type: "image/png".to_string(), - meta: None, - }; - - let json = serde_json::to_string(&image_content).unwrap(); - println!("PromptMessage ImageContent JSON: {}", json); - - // Verify it contains mimeType (camelCase) not mime_type (snake_case) - assert!(json.contains("mimeType")); - assert!(!json.contains("mime_type")); - } - - #[test] - fn test_prompt_message_resource_link_serialization() { - use super::super::resource::RawResource; - - let resource = RawResource::new("file:///test.txt", "test.txt"); - let message = - PromptMessage::new_resource_link(PromptMessageRole::User, resource.no_annotation()); - - let json = serde_json::to_string(&message).unwrap(); - println!("PromptMessage with ResourceLink JSON: {}", json); - - // Verify it contains the correct type tag - assert!(json.contains("\"type\":\"resource_link\"")); - assert!(json.contains("\"uri\":\"file:///test.txt\"")); - assert!(json.contains("\"name\":\"test.txt\"")); - } - - #[test] - fn test_prompt_message_content_resource_link_deserialization() { - let json = r#"{ - "type": "resource_link", - "uri": "file:///example.txt", - "name": "example.txt", - "description": "Example file", - "mimeType": "text/plain" - }"#; - - let content: PromptMessageContent = serde_json::from_str(json).unwrap(); - - if let PromptMessageContent::ResourceLink { link } = content { - assert_eq!(link.uri, "file:///example.txt"); - assert_eq!(link.name, "example.txt"); - assert_eq!(link.description, Some("Example file".to_string())); - assert_eq!(link.mime_type, Some("text/plain".to_string())); - } else { - panic!("Expected ResourceLink variant"); - } - } -} diff --git a/codex-rs/vendor/rmcp/src/model/resource.rs b/codex-rs/vendor/rmcp/src/model/resource.rs deleted file mode 100644 index cf3a1071fc..0000000000 --- a/codex-rs/vendor/rmcp/src/model/resource.rs +++ /dev/null @@ -1,188 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::{Annotated, Icon, Meta}; - -/// Represents a resource in the extension with metadata -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct RawResource { - /// URI representing the resource location (e.g., "file:///path/to/file" or "str:///content") - pub uri: String, - /// Name of the resource - pub name: String, - /// Human-readable title of the resource - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - /// Optional description of the resource - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - /// MIME type of the resource content ("text" or "blob") - #[serde(skip_serializing_if = "Option::is_none")] - pub mime_type: Option, - - /// The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. - /// - /// This can be used by Hosts to display file sizes and estimate context window us - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, - /// Optional list of icons for the resource - #[serde(skip_serializing_if = "Option::is_none")] - pub icons: Option>, - /// Optional additional metadata for this resource - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, -} - -pub type Resource = Annotated; - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct RawResourceTemplate { - pub uri_template: String, - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub mime_type: Option, - /// Optional list of icons for the resource template - #[serde(skip_serializing_if = "Option::is_none")] - pub icons: Option>, -} - -pub type ResourceTemplate = Annotated; - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(untagged)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum ResourceContents { - #[serde(rename_all = "camelCase")] - TextResourceContents { - uri: String, - #[serde(skip_serializing_if = "Option::is_none")] - mime_type: Option, - text: String, - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - meta: Option, - }, - #[serde(rename_all = "camelCase")] - BlobResourceContents { - uri: String, - #[serde(skip_serializing_if = "Option::is_none")] - mime_type: Option, - blob: String, - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - meta: Option, - }, -} - -impl ResourceContents { - pub fn text(text: impl Into, uri: impl Into) -> Self { - Self::TextResourceContents { - uri: uri.into(), - mime_type: Some("text".into()), - text: text.into(), - meta: None, - } - } -} - -impl RawResource { - /// Creates a new Resource from a URI with explicit mime type - pub fn new(uri: impl Into, name: impl Into) -> Self { - Self { - uri: uri.into(), - name: name.into(), - title: None, - description: None, - mime_type: None, - size: None, - icons: None, - meta: None, - } - } -} - -#[cfg(test)] -mod tests { - use serde_json; - - use super::*; - - #[test] - fn test_resource_serialization() { - let resource = RawResource { - uri: "file:///test.txt".to_string(), - title: None, - name: "test".to_string(), - description: Some("Test resource".to_string()), - mime_type: Some("text/plain".to_string()), - size: Some(100), - icons: None, - meta: None, - }; - - let json = serde_json::to_string(&resource).unwrap(); - println!("Serialized JSON: {}", json); - - // Verify it contains mimeType (camelCase) not mime_type (snake_case) - assert!(json.contains("mimeType")); - assert!(!json.contains("mime_type")); - } - - #[test] - fn test_resource_contents_serialization() { - let text_contents = ResourceContents::TextResourceContents { - uri: "file:///test.txt".to_string(), - mime_type: Some("text/plain".to_string()), - text: "Hello world".to_string(), - meta: None, - }; - - let json = serde_json::to_string(&text_contents).unwrap(); - println!("ResourceContents JSON: {}", json); - - // Verify it contains mimeType (camelCase) not mime_type (snake_case) - assert!(json.contains("mimeType")); - assert!(!json.contains("mime_type")); - } - - #[test] - fn test_resource_template_with_icons() { - let resource_template = RawResourceTemplate { - uri_template: "file:///{path}".to_string(), - name: "template".to_string(), - title: Some("Test Template".to_string()), - description: Some("A test resource template".to_string()), - mime_type: Some("text/plain".to_string()), - icons: Some(vec![Icon { - src: "https://example.com/icon.png".to_string(), - mime_type: Some("image/png".to_string()), - sizes: Some(vec!["48x48".to_string()]), - }]), - }; - - let json = serde_json::to_value(&resource_template).unwrap(); - assert!(json["icons"].is_array()); - assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png"); - assert_eq!(json["icons"][0]["sizes"][0], "48x48"); - } - - #[test] - fn test_resource_template_without_icons() { - let resource_template = RawResourceTemplate { - uri_template: "file:///{path}".to_string(), - name: "template".to_string(), - title: None, - description: None, - mime_type: None, - icons: None, - }; - - let json = serde_json::to_value(&resource_template).unwrap(); - assert!(json.get("icons").is_none()); - } -} diff --git a/codex-rs/vendor/rmcp/src/model/serde_impl.rs b/codex-rs/vendor/rmcp/src/model/serde_impl.rs deleted file mode 100644 index 8b88b5e01e..0000000000 --- a/codex-rs/vendor/rmcp/src/model/serde_impl.rs +++ /dev/null @@ -1,373 +0,0 @@ -use std::borrow::Cow; - -use serde::{Deserialize, Serialize}; - -use super::{ - CustomNotification, CustomRequest, Extensions, Meta, Notification, NotificationNoParam, - Request, RequestNoParam, RequestOptionalParam, -}; -#[derive(Serialize, Deserialize)] -struct WithMeta<'a, P> { - #[serde(skip_serializing_if = "Option::is_none")] - _meta: Option>, - #[serde(flatten)] - _rest: P, -} - -#[derive(Serialize, Deserialize)] -struct Proxy<'a, M, P> { - method: M, - params: WithMeta<'a, P>, -} - -#[derive(Serialize, Deserialize)] -struct ProxyOptionalParam<'a, M, P> { - method: M, - params: Option>, -} - -#[derive(Serialize, Deserialize)] -struct ProxyNoParam { - method: M, -} - -impl Serialize for Request -where - M: Serialize, - R: Serialize, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let extensions = &self.extensions; - let _meta = extensions.get::().map(Cow::Borrowed); - Proxy::serialize( - &Proxy { - method: &self.method, - params: WithMeta { - _rest: &self.params, - _meta, - }, - }, - serializer, - ) - } -} - -impl<'de, M, R> Deserialize<'de> for Request -where - M: Deserialize<'de>, - R: Deserialize<'de>, -{ - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let body = Proxy::deserialize(deserializer)?; - let _meta = body.params._meta.map(|m| m.into_owned()); - let mut extensions = Extensions::new(); - if let Some(meta) = _meta { - extensions.insert(meta); - } - Ok(Request { - extensions, - method: body.method, - params: body.params._rest, - }) - } -} - -impl Serialize for RequestOptionalParam -where - M: Serialize, - R: Serialize, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let extensions = &self.extensions; - let _meta = extensions.get::().map(Cow::Borrowed); - Proxy::serialize( - &Proxy { - method: &self.method, - params: WithMeta { - _rest: &self.params, - _meta, - }, - }, - serializer, - ) - } -} - -impl<'de, M, R> Deserialize<'de> for RequestOptionalParam -where - M: Deserialize<'de>, - R: Deserialize<'de>, -{ - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let body = ProxyOptionalParam::<'_, _, Option>::deserialize(deserializer)?; - let mut params = None; - let mut _meta = None; - if let Some(body_params) = body.params { - params = body_params._rest; - _meta = body_params._meta.map(|m| m.into_owned()); - } - let mut extensions = Extensions::new(); - if let Some(meta) = _meta { - extensions.insert(meta); - } - Ok(RequestOptionalParam { - extensions, - method: body.method, - params, - }) - } -} - -impl Serialize for RequestNoParam -where - M: Serialize, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let extensions = &self.extensions; - let _meta = extensions.get::().map(Cow::Borrowed); - ProxyNoParam::serialize( - &ProxyNoParam { - method: &self.method, - }, - serializer, - ) - } -} - -impl<'de, M> Deserialize<'de> for RequestNoParam -where - M: Deserialize<'de>, -{ - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let body = ProxyNoParam::<_>::deserialize(deserializer)?; - let extensions = Extensions::new(); - Ok(RequestNoParam { - extensions, - method: body.method, - }) - } -} - -impl Serialize for Notification -where - M: Serialize, - R: Serialize, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let extensions = &self.extensions; - let _meta = extensions.get::().map(Cow::Borrowed); - Proxy::serialize( - &Proxy { - method: &self.method, - params: WithMeta { - _rest: &self.params, - _meta, - }, - }, - serializer, - ) - } -} - -impl<'de, M, R> Deserialize<'de> for Notification -where - M: Deserialize<'de>, - R: Deserialize<'de>, -{ - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let body = Proxy::deserialize(deserializer)?; - let _meta = body.params._meta.map(|m| m.into_owned()); - let mut extensions = Extensions::new(); - if let Some(meta) = _meta { - extensions.insert(meta); - } - Ok(Notification { - extensions, - method: body.method, - params: body.params._rest, - }) - } -} - -impl Serialize for NotificationNoParam -where - M: Serialize, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let extensions = &self.extensions; - let _meta = extensions.get::().map(Cow::Borrowed); - ProxyNoParam::serialize( - &ProxyNoParam { - method: &self.method, - }, - serializer, - ) - } -} - -impl<'de, M> Deserialize<'de> for NotificationNoParam -where - M: Deserialize<'de>, -{ - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let body = ProxyNoParam::<_>::deserialize(deserializer)?; - let extensions = Extensions::new(); - Ok(NotificationNoParam { - extensions, - method: body.method, - }) - } -} - -impl Serialize for CustomRequest { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let extensions = &self.extensions; - let _meta = extensions.get::().map(Cow::Borrowed); - let params = self.params.as_ref(); - - let params = if _meta.is_some() || params.is_some() { - Some(WithMeta { - _meta, - _rest: &self.params, - }) - } else { - None - }; - - ProxyOptionalParam::serialize( - &ProxyOptionalParam { - method: &self.method, - params, - }, - serializer, - ) - } -} - -impl<'de> Deserialize<'de> for CustomRequest { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let body = - ProxyOptionalParam::<'_, _, Option>::deserialize(deserializer)?; - let mut params = None; - let mut _meta = None; - if let Some(body_params) = body.params { - params = body_params._rest; - _meta = body_params._meta.map(|m| m.into_owned()); - } - let mut extensions = Extensions::new(); - if let Some(meta) = _meta { - extensions.insert(meta); - } - Ok(CustomRequest { - extensions, - method: body.method, - params, - }) - } -} - -impl Serialize for CustomNotification { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let extensions = &self.extensions; - let _meta = extensions.get::().map(Cow::Borrowed); - let params = self.params.as_ref(); - - let params = if _meta.is_some() || params.is_some() { - Some(WithMeta { - _meta, - _rest: &self.params, - }) - } else { - None - }; - - ProxyOptionalParam::serialize( - &ProxyOptionalParam { - method: &self.method, - params, - }, - serializer, - ) - } -} - -impl<'de> Deserialize<'de> for CustomNotification { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let body = - ProxyOptionalParam::<'_, _, Option>::deserialize(deserializer)?; - let mut params = None; - let mut _meta = None; - if let Some(body_params) = body.params { - params = body_params._rest; - _meta = body_params._meta.map(|m| m.into_owned()); - } - let mut extensions = Extensions::new(); - if let Some(meta) = _meta { - extensions.insert(meta); - } - Ok(CustomNotification { - extensions, - method: body.method, - params, - }) - } -} - -#[cfg(test)] -mod test { - use serde_json::json; - - use crate::model::ListToolsRequest; - - #[test] - fn test_deserialize_lost_tools_request() { - let _req: ListToolsRequest = serde_json::from_value(json!( - { - "method": "tools/list", - } - )) - .unwrap(); - } -} diff --git a/codex-rs/vendor/rmcp/src/model/task.rs b/codex-rs/vendor/rmcp/src/model/task.rs deleted file mode 100644 index 8cb0ee583d..0000000000 --- a/codex-rs/vendor/rmcp/src/model/task.rs +++ /dev/null @@ -1,79 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -/// Canonical task lifecycle status as defined by SEP-1686. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum TaskStatus { - /// The receiver accepted the request and is currently working on it. - #[default] - Working, - /// The receiver requires additional input before work can continue. - InputRequired, - /// The underlying operation completed successfully and the result is ready. - Completed, - /// The underlying operation failed and will not continue. - Failed, - /// The task was cancelled and will not continue processing. - Cancelled, -} - -/// Final result for a succeeded task (returned from `tasks/result`). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct TaskResult { - /// MIME type or custom content-type identifier. - pub content_type: String, - /// The actual result payload, matching the underlying request's schema. - pub value: Value, - /// Optional short summary for UI surfaces. - #[serde(skip_serializing_if = "Option::is_none")] - pub summary: Option, -} - -/// Primary Task object that surfaces metadata during the task lifecycle. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Task { - /// Unique task identifier generated by the receiver. - pub task_id: String, - /// Current lifecycle status (see [`TaskStatus`]). - pub status: TaskStatus, - /// Optional human-readable status message for UI surfaces. - #[serde(skip_serializing_if = "Option::is_none")] - pub status_message: Option, - /// ISO-8601 creation timestamp. - pub created_at: String, - /// ISO-8601 timestamp for the most recent status change. - #[serde(skip_serializing_if = "Option::is_none")] - pub last_updated_at: Option, - /// Retention window in milliseconds that the receiver agreed to honor. - #[serde(skip_serializing_if = "Option::is_none")] - pub ttl: Option, - /// Suggested polling interval (milliseconds). - #[serde(skip_serializing_if = "Option::is_none")] - pub poll_interval: Option, -} - -/// Wrapper returned by task-augmented requests (CreateTaskResult in SEP-1686). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CreateTaskResult { - pub task: Task, -} - -/// Paginated list of tasks -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct TaskList { - pub tasks: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub next_cursor: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub total: Option, -} diff --git a/codex-rs/vendor/rmcp/src/model/tool.rs b/codex-rs/vendor/rmcp/src/model/tool.rs deleted file mode 100644 index cb2cfda371..0000000000 --- a/codex-rs/vendor/rmcp/src/model/tool.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::{borrow::Cow, sync::Arc}; - -use schemars::JsonSchema; -/// Tools represent a routine that a server can execute -/// Tool calls represent requests from the client to execute one -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use super::{Icon, JsonObject, Meta}; - -/// A tool that can be used by a model. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct Tool { - /// The name of the tool - pub name: Cow<'static, str>, - /// A human-readable title for the tool - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - /// A description of what the tool does - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - /// A JSON Schema object defining the expected parameters for the tool - pub input_schema: Arc, - /// An optional JSON Schema object defining the structure of the tool's output - #[serde(skip_serializing_if = "Option::is_none")] - pub output_schema: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - /// Optional additional tool information. - pub annotations: Option, - /// Execution-related configuration including task support mode. - #[serde(skip_serializing_if = "Option::is_none")] - pub execution: Option, - /// Optional list of icons for the tool - #[serde(skip_serializing_if = "Option::is_none")] - pub icons: Option>, - /// Optional additional metadata for this tool - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, -} - -/// Per-tool task support mode as defined in the MCP specification. -/// -/// This enum indicates whether a tool supports task-based invocation, -/// allowing clients to know how to properly call the tool. -/// -/// See [Tool-Level Negotiation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiation). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "lowercase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum TaskSupport { - /// Clients MUST NOT invoke this tool as a task (default behavior). - #[default] - Forbidden, - /// Clients MAY invoke this tool as either a task or a normal call. - Optional, - /// Clients MUST invoke this tool as a task. - Required, -} - -/// Execution-related configuration for a tool. -/// -/// This struct contains settings that control how a tool should be executed, -/// including task support configuration. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ToolExecution { - /// Indicates whether this tool supports task-based invocation. - /// - /// When not present or set to `Forbidden`, clients MUST NOT invoke this tool as a task. - /// When set to `Optional`, clients MAY invoke this tool as a task or normal call. - /// When set to `Required`, clients MUST invoke this tool as a task. - #[serde(skip_serializing_if = "Option::is_none")] - pub task_support: Option, -} - -impl ToolExecution { - /// Create a new empty ToolExecution configuration. - pub fn new() -> Self { - Self::default() - } - - /// Set the task support mode. - pub fn with_task_support(mut self, task_support: TaskSupport) -> Self { - self.task_support = Some(task_support); - self - } -} - -/// Additional properties describing a Tool to clients. -/// -/// NOTE: all properties in ToolAnnotations are **hints**. -/// They are not guaranteed to provide a faithful description of -/// tool behavior (including descriptive properties like `title`). -/// -/// Clients should never make tool use decisions based on ToolAnnotations -/// received from untrusted servers. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ToolAnnotations { - /// A human-readable title for the tool. - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - - /// If true, the tool does not modify its environment. - /// - /// Default: false - #[serde(skip_serializing_if = "Option::is_none")] - pub read_only_hint: Option, - - /// If true, the tool may perform destructive updates to its environment. - /// If false, the tool performs only additive updates. - /// - /// (This property is meaningful only when `readOnlyHint == false`) - /// - /// Default: true - /// A human-readable description of the tool's purpose. - #[serde(skip_serializing_if = "Option::is_none")] - pub destructive_hint: Option, - - /// If true, calling the tool repeatedly with the same arguments - /// will have no additional effect on the its environment. - /// - /// (This property is meaningful only when `readOnlyHint == false`) - /// - /// Default: false. - #[serde(skip_serializing_if = "Option::is_none")] - pub idempotent_hint: Option, - - /// If true, this tool may interact with an "open world" of external - /// entities. If false, the tool's domain of interaction is closed. - /// For example, the world of a web search tool is open, whereas that - /// of a memory tool is not. - /// - /// Default: true - #[serde(skip_serializing_if = "Option::is_none")] - pub open_world_hint: Option, -} - -impl ToolAnnotations { - pub fn new() -> Self { - Self::default() - } - pub fn with_title(title: T) -> Self - where - T: Into, - { - ToolAnnotations { - title: Some(title.into()), - ..Self::default() - } - } - pub fn read_only(self, read_only: bool) -> Self { - ToolAnnotations { - read_only_hint: Some(read_only), - ..self - } - } - pub fn destructive(self, destructive: bool) -> Self { - ToolAnnotations { - destructive_hint: Some(destructive), - ..self - } - } - pub fn idempotent(self, idempotent: bool) -> Self { - ToolAnnotations { - idempotent_hint: Some(idempotent), - ..self - } - } - pub fn open_world(self, open_world: bool) -> Self { - ToolAnnotations { - open_world_hint: Some(open_world), - ..self - } - } - - /// If not set, defaults to true. - pub fn is_destructive(&self) -> bool { - self.destructive_hint.unwrap_or(true) - } - - /// If not set, defaults to false. - pub fn is_idempotent(&self) -> bool { - self.idempotent_hint.unwrap_or(false) - } -} - -impl Tool { - /// Create a new tool with the given name and description - pub fn new(name: N, description: D, input_schema: S) -> Self - where - N: Into>, - D: Into>, - S: Into>, - { - Tool { - name: name.into(), - title: None, - description: Some(description.into()), - input_schema: input_schema.into(), - output_schema: None, - annotations: None, - execution: None, - icons: None, - meta: None, - } - } - - pub fn annotate(self, annotations: ToolAnnotations) -> Self { - Tool { - annotations: Some(annotations), - ..self - } - } - - /// Set the execution configuration for this tool. - pub fn with_execution(self, execution: ToolExecution) -> Self { - Tool { - execution: Some(execution), - ..self - } - } - - /// Returns the task support mode for this tool. - /// - /// Returns `TaskSupport::Forbidden` if not explicitly set. - pub fn task_support(&self) -> TaskSupport { - self.execution - .as_ref() - .and_then(|e| e.task_support) - .unwrap_or_default() - } - - /// Set the output schema using a type that implements JsonSchema - /// - /// # Panics - /// - /// Panics if the generated schema does not have root type "object" as required by MCP specification. - pub fn with_output_schema(mut self) -> Self { - let schema = crate::handler::server::tool::schema_for_output::() - .unwrap_or_else(|e| panic!("Invalid output schema for tool '{}': {}", self.name, e)); - self.output_schema = Some(schema); - self - } - - /// Set the input schema using a type that implements JsonSchema - pub fn with_input_schema(mut self) -> Self { - self.input_schema = crate::handler::server::tool::schema_for_type::(); - self - } - - /// Get the schema as json value - pub fn schema_as_json_value(&self) -> Value { - Value::Object(self.input_schema.as_ref().clone()) - } -} diff --git a/codex-rs/vendor/rmcp/src/service.rs b/codex-rs/vendor/rmcp/src/service.rs deleted file mode 100644 index e0fd76425b..0000000000 --- a/codex-rs/vendor/rmcp/src/service.rs +++ /dev/null @@ -1,944 +0,0 @@ -use futures::{FutureExt, future::BoxFuture}; -use thiserror::Error; - -use crate::{ - error::ErrorData as McpError, - model::{ - CancelledNotification, CancelledNotificationParam, Extensions, GetExtensions, GetMeta, - JsonRpcError, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, Meta, - NumberOrString, ProgressToken, RequestId, ServerJsonRpcMessage, - }, - transport::{DynamicTransportError, IntoTransport, Transport}, -}; -#[cfg(feature = "client")] -#[cfg_attr(docsrs, doc(cfg(feature = "client")))] -mod client; -#[cfg(feature = "client")] -#[cfg_attr(docsrs, doc(cfg(feature = "client")))] -pub use client::*; -#[cfg(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -mod server; -#[cfg(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -pub use server::*; -#[cfg(feature = "tower")] -#[cfg_attr(docsrs, doc(cfg(feature = "tower")))] -mod tower; -use tokio_util::sync::{CancellationToken, DropGuard}; -#[cfg(feature = "tower")] -#[cfg_attr(docsrs, doc(cfg(feature = "tower")))] -pub use tower::*; -use tracing::{Instrument as _, instrument}; -#[derive(Error, Debug)] -#[non_exhaustive] -pub enum ServiceError { - #[error("Mcp error: {0}")] - McpError(McpError), - #[error("Transport send error: {0}")] - TransportSend(DynamicTransportError), - #[error("Transport closed")] - TransportClosed, - #[error("Unexpected response type")] - UnexpectedResponse, - #[error("task cancelled for reason {}", reason.as_deref().unwrap_or(""))] - Cancelled { reason: Option }, - #[error("request timeout after {}", chrono::Duration::from_std(*timeout).unwrap_or_default())] - Timeout { timeout: Duration }, -} - -trait TransferObject: - std::fmt::Debug + Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static -{ -} - -impl TransferObject for T where - T: std::fmt::Debug - + serde::Serialize - + serde::de::DeserializeOwned - + Send - + Sync - + 'static - + Clone -{ -} - -#[allow(private_bounds, reason = "there's no the third implementation")] -pub trait ServiceRole: std::fmt::Debug + Send + Sync + 'static + Copy + Clone { - type Req: TransferObject + GetMeta + GetExtensions; - type Resp: TransferObject; - type Not: TryInto - + From - + TransferObject; - type PeerReq: TransferObject + GetMeta + GetExtensions; - type PeerResp: TransferObject; - type PeerNot: TryInto - + From - + TransferObject - + GetMeta - + GetExtensions; - type InitializeError; - const IS_CLIENT: bool; - type Info: TransferObject; - type PeerInfo: TransferObject; -} - -pub type TxJsonRpcMessage = - JsonRpcMessage<::Req, ::Resp, ::Not>; -pub type RxJsonRpcMessage = JsonRpcMessage< - ::PeerReq, - ::PeerResp, - ::PeerNot, ->; - -pub trait Service: Send + Sync + 'static { - fn handle_request( - &self, - request: R::PeerReq, - context: RequestContext, - ) -> impl Future> + Send + '_; - fn handle_notification( - &self, - notification: R::PeerNot, - context: NotificationContext, - ) -> impl Future> + Send + '_; - fn get_info(&self) -> R::Info; -} - -pub trait ServiceExt: Service + Sized { - /// Convert this service to a dynamic boxed service - /// - /// This could be very helpful when you want to store the services in a collection - fn into_dyn(self) -> Box> { - Box::new(self) - } - fn serve( - self, - transport: T, - ) -> impl Future, R::InitializeError>> + Send - where - T: IntoTransport, - E: std::error::Error + Send + Sync + 'static, - Self: Sized, - { - Self::serve_with_ct(self, transport, Default::default()) - } - fn serve_with_ct( - self, - transport: T, - ct: CancellationToken, - ) -> impl Future, R::InitializeError>> + Send - where - T: IntoTransport, - E: std::error::Error + Send + Sync + 'static, - Self: Sized; -} - -impl Service for Box> { - fn handle_request( - &self, - request: R::PeerReq, - context: RequestContext, - ) -> impl Future> + Send + '_ { - DynService::handle_request(self.as_ref(), request, context) - } - - fn handle_notification( - &self, - notification: R::PeerNot, - context: NotificationContext, - ) -> impl Future> + Send + '_ { - DynService::handle_notification(self.as_ref(), notification, context) - } - - fn get_info(&self) -> R::Info { - DynService::get_info(self.as_ref()) - } -} - -pub trait DynService: Send + Sync { - fn handle_request( - &self, - request: R::PeerReq, - context: RequestContext, - ) -> BoxFuture<'_, Result>; - fn handle_notification( - &self, - notification: R::PeerNot, - context: NotificationContext, - ) -> BoxFuture<'_, Result<(), McpError>>; - fn get_info(&self) -> R::Info; -} - -impl> DynService for S { - fn handle_request( - &self, - request: R::PeerReq, - context: RequestContext, - ) -> BoxFuture<'_, Result> { - Box::pin(self.handle_request(request, context)) - } - fn handle_notification( - &self, - notification: R::PeerNot, - context: NotificationContext, - ) -> BoxFuture<'_, Result<(), McpError>> { - Box::pin(self.handle_notification(notification, context)) - } - fn get_info(&self) -> R::Info { - self.get_info() - } -} - -use std::{ - collections::{HashMap, VecDeque}, - ops::Deref, - sync::{Arc, atomic::AtomicU64}, - time::Duration, -}; - -use tokio::sync::mpsc; - -pub trait RequestIdProvider: Send + Sync + 'static { - fn next_request_id(&self) -> RequestId; -} - -pub trait ProgressTokenProvider: Send + Sync + 'static { - fn next_progress_token(&self) -> ProgressToken; -} - -pub type AtomicU32RequestIdProvider = AtomicU32Provider; -pub type AtomicU32ProgressTokenProvider = AtomicU32Provider; - -#[derive(Debug, Default)] -pub struct AtomicU32Provider { - id: AtomicU64, -} - -impl RequestIdProvider for AtomicU32Provider { - fn next_request_id(&self) -> RequestId { - let id = self.id.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - // Safe conversion: we start at 0 and increment by 1, so we won't overflow i64::MAX in practice - RequestId::Number(id as i64) - } -} - -impl ProgressTokenProvider for AtomicU32Provider { - fn next_progress_token(&self) -> ProgressToken { - let id = self.id.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - ProgressToken(NumberOrString::Number(id as i64)) - } -} - -type Responder = tokio::sync::oneshot::Sender; - -/// A handle to a remote request -/// -/// You can cancel it by call [`RequestHandle::cancel`] with a reason, -/// -/// or wait for response by call [`RequestHandle::await_response`] -#[derive(Debug)] -pub struct RequestHandle { - pub rx: tokio::sync::oneshot::Receiver>, - pub options: PeerRequestOptions, - pub peer: Peer, - pub id: RequestId, - pub progress_token: ProgressToken, -} - -impl RequestHandle { - pub const REQUEST_TIMEOUT_REASON: &str = "request timeout"; - pub async fn await_response(self) -> Result { - if let Some(timeout) = self.options.timeout { - let timeout_result = tokio::time::timeout(timeout, async move { - self.rx.await.map_err(|_e| ServiceError::TransportClosed)? - }) - .await; - match timeout_result { - Ok(response) => response, - Err(_) => { - let error = Err(ServiceError::Timeout { timeout }); - // cancel this request - let notification = CancelledNotification { - params: CancelledNotificationParam { - request_id: self.id, - reason: Some(Self::REQUEST_TIMEOUT_REASON.to_owned()), - }, - method: crate::model::CancelledNotificationMethod, - extensions: Default::default(), - }; - let _ = self.peer.send_notification(notification.into()).await; - error - } - } - } else { - self.rx.await.map_err(|_e| ServiceError::TransportClosed)? - } - } - - /// Cancel this request - pub async fn cancel(self, reason: Option) -> Result<(), ServiceError> { - let notification = CancelledNotification { - params: CancelledNotificationParam { - request_id: self.id, - reason, - }, - method: crate::model::CancelledNotificationMethod, - extensions: Default::default(), - }; - self.peer.send_notification(notification.into()).await?; - Ok(()) - } -} - -#[derive(Debug)] -pub(crate) enum PeerSinkMessage { - Request { - request: R::Req, - id: RequestId, - responder: Responder>, - }, - Notification { - notification: R::Not, - responder: Responder>, - }, -} - -/// An interface to fetch the remote client or server -/// -/// For general purpose, call [`Peer::send_request`] or [`Peer::send_notification`] to send message to remote peer. -/// -/// To create a cancellable request, call [`Peer::send_request_with_option`]. -#[derive(Clone)] -pub struct Peer { - tx: mpsc::Sender>, - request_id_provider: Arc, - progress_token_provider: Arc, - info: Arc>, -} - -impl std::fmt::Debug for Peer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PeerSink") - .field("tx", &self.tx) - .field("is_client", &R::IS_CLIENT) - .finish() - } -} - -type ProxyOutbound = mpsc::Receiver>; - -#[derive(Debug, Default)] -pub struct PeerRequestOptions { - pub timeout: Option, - pub meta: Option, -} - -impl PeerRequestOptions { - pub fn no_options() -> Self { - Self::default() - } -} - -impl Peer { - const CLIENT_CHANNEL_BUFFER_SIZE: usize = 1024; - pub(crate) fn new( - request_id_provider: Arc, - peer_info: Option, - ) -> (Peer, ProxyOutbound) { - let (tx, rx) = mpsc::channel(Self::CLIENT_CHANNEL_BUFFER_SIZE); - ( - Self { - tx, - request_id_provider, - progress_token_provider: Arc::new(AtomicU32ProgressTokenProvider::default()), - info: Arc::new(tokio::sync::OnceCell::new_with(peer_info)), - }, - rx, - ) - } - pub async fn send_notification(&self, notification: R::Not) -> Result<(), ServiceError> { - let (responder, receiver) = tokio::sync::oneshot::channel(); - self.tx - .send(PeerSinkMessage::Notification { - notification, - responder, - }) - .await - .map_err(|_m| ServiceError::TransportClosed)?; - receiver.await.map_err(|_e| ServiceError::TransportClosed)? - } - pub async fn send_request(&self, request: R::Req) -> Result { - self.send_request_with_option(request, PeerRequestOptions::no_options()) - .await? - .await_response() - .await - } - - pub async fn send_cancellable_request( - &self, - request: R::Req, - options: PeerRequestOptions, - ) -> Result, ServiceError> { - self.send_request_with_option(request, options).await - } - - pub async fn send_request_with_option( - &self, - mut request: R::Req, - options: PeerRequestOptions, - ) -> Result, ServiceError> { - let id = self.request_id_provider.next_request_id(); - let progress_token = self.progress_token_provider.next_progress_token(); - request - .get_meta_mut() - .set_progress_token(progress_token.clone()); - if let Some(meta) = options.meta.clone() { - request.get_meta_mut().extend(meta); - } - let (responder, receiver) = tokio::sync::oneshot::channel(); - self.tx - .send(PeerSinkMessage::Request { - request, - id: id.clone(), - responder, - }) - .await - .map_err(|_m| ServiceError::TransportClosed)?; - Ok(RequestHandle { - id, - rx: receiver, - progress_token, - options, - peer: self.clone(), - }) - } - pub fn peer_info(&self) -> Option<&R::PeerInfo> { - self.info.get() - } - - pub fn set_peer_info(&self, info: R::PeerInfo) { - if self.info.initialized() { - tracing::warn!("trying to set peer info, which is already initialized"); - } else { - let _ = self.info.set(info); - } - } - - pub fn is_transport_closed(&self) -> bool { - self.tx.is_closed() - } -} - -#[derive(Debug)] -pub struct RunningService> { - service: Arc, - peer: Peer, - handle: Option>, - cancellation_token: CancellationToken, - dg: DropGuard, -} -impl> Deref for RunningService { - type Target = Peer; - - fn deref(&self) -> &Self::Target { - &self.peer - } -} - -impl> RunningService { - #[inline] - pub fn peer(&self) -> &Peer { - &self.peer - } - #[inline] - pub fn service(&self) -> &S { - self.service.as_ref() - } - #[inline] - pub fn cancellation_token(&self) -> RunningServiceCancellationToken { - RunningServiceCancellationToken(self.cancellation_token.clone()) - } - - /// Returns true if the service has been closed or cancelled. - #[inline] - pub fn is_closed(&self) -> bool { - self.handle.is_none() || self.cancellation_token.is_cancelled() - } - - /// Wait for the service to complete. - /// - /// This will block until the service loop terminates (either due to - /// cancellation, transport closure, or an error). - #[inline] - pub async fn waiting(mut self) -> Result { - match self.handle.take() { - Some(handle) => handle.await, - None => Ok(QuitReason::Closed), - } - } - - /// Gracefully close the connection and wait for cleanup to complete. - /// - /// This method cancels the service, waits for the background task to finish - /// (which includes calling `transport.close()`), and ensures all cleanup - /// operations complete before returning. - /// - /// Unlike [`cancel`](Self::cancel), this method takes `&mut self` and can be - /// called without consuming the `RunningService`. After calling this method, - /// the service is considered closed and subsequent operations will fail. - /// - /// # Example - /// - /// ```rust,ignore - /// let mut client = ().serve(transport).await?; - /// // ... use the client ... - /// client.close().await?; - /// ``` - pub async fn close(&mut self) -> Result { - if let Some(handle) = self.handle.take() { - // Disarm the drop guard so it doesn't try to cancel again - // We need to cancel manually and wait for completion - self.cancellation_token.cancel(); - handle.await - } else { - // Already closed - Ok(QuitReason::Closed) - } - } - - /// Gracefully close the connection with a timeout. - /// - /// Similar to [`close`](Self::close), but returns after the specified timeout - /// if the cleanup doesn't complete in time. This is useful for ensuring - /// a bounded shutdown time. - /// - /// Returns `Ok(Some(reason))` if shutdown completed within the timeout, - /// `Ok(None)` if the timeout was reached, or `Err` if there was a join error. - pub async fn close_with_timeout( - &mut self, - timeout: Duration, - ) -> Result, tokio::task::JoinError> { - if let Some(handle) = self.handle.take() { - self.cancellation_token.cancel(); - match tokio::time::timeout(timeout, handle).await { - Ok(result) => result.map(Some), - Err(_elapsed) => { - tracing::warn!( - "close_with_timeout: cleanup did not complete within {:?}", - timeout - ); - Ok(None) - } - } - } else { - Ok(Some(QuitReason::Closed)) - } - } - - /// Cancel the service and wait for cleanup to complete. - /// - /// This consumes the `RunningService` and ensures the connection is properly - /// closed. For a non-consuming alternative, see [`close`](Self::close). - pub async fn cancel(mut self) -> Result { - // Disarm the drop guard since we're handling cancellation explicitly - let _ = std::mem::replace(&mut self.dg, self.cancellation_token.clone().drop_guard()); - self.close().await - } -} - -impl> Drop for RunningService { - fn drop(&mut self) { - if self.handle.is_some() && !self.cancellation_token.is_cancelled() { - tracing::debug!( - "RunningService dropped without explicit close(). \ - The connection will be closed asynchronously. \ - For guaranteed cleanup, call close() or cancel() before dropping." - ); - } - // The DropGuard will handle cancellation - } -} - -// use a wrapper type so we can tweak the implementation if needed -pub struct RunningServiceCancellationToken(CancellationToken); - -impl RunningServiceCancellationToken { - pub fn cancel(self) { - self.0.cancel(); - } -} - -#[derive(Debug)] -pub enum QuitReason { - Cancelled, - Closed, - JoinError(tokio::task::JoinError), -} - -/// Request execution context -#[derive(Debug, Clone)] -pub struct RequestContext { - /// this token will be cancelled when the [`CancelledNotification`] is received. - pub ct: CancellationToken, - pub id: RequestId, - pub meta: Meta, - pub extensions: Extensions, - /// An interface to fetch the remote client or server - pub peer: Peer, -} - -/// Request execution context -#[derive(Debug, Clone)] -pub struct NotificationContext { - pub meta: Meta, - pub extensions: Extensions, - /// An interface to fetch the remote client or server - pub peer: Peer, -} - -/// Use this function to skip initialization process -pub fn serve_directly( - service: S, - transport: T, - peer_info: Option, -) -> RunningService -where - R: ServiceRole, - S: Service, - T: IntoTransport, - E: std::error::Error + Send + Sync + 'static, -{ - serve_directly_with_ct(service, transport, peer_info, Default::default()) -} - -/// Use this function to skip initialization process -pub fn serve_directly_with_ct( - service: S, - transport: T, - peer_info: Option, - ct: CancellationToken, -) -> RunningService -where - R: ServiceRole, - S: Service, - T: IntoTransport, - E: std::error::Error + Send + Sync + 'static, -{ - let (peer, peer_rx) = Peer::new(Arc::new(AtomicU32RequestIdProvider::default()), peer_info); - serve_inner(service, transport.into_transport(), peer, peer_rx, ct) -} - -#[instrument(skip_all)] -fn serve_inner( - service: S, - transport: T, - peer: Peer, - mut peer_rx: tokio::sync::mpsc::Receiver>, - ct: CancellationToken, -) -> RunningService -where - R: ServiceRole, - S: Service, - T: Transport + 'static, -{ - const SINK_PROXY_BUFFER_SIZE: usize = 64; - let (sink_proxy_tx, mut sink_proxy_rx) = - tokio::sync::mpsc::channel::>(SINK_PROXY_BUFFER_SIZE); - let peer_info = peer.peer_info(); - if R::IS_CLIENT { - tracing::info!(?peer_info, "Service initialized as client"); - } else { - tracing::info!(?peer_info, "Service initialized as server"); - } - - let mut local_responder_pool = - HashMap::>>::new(); - let mut local_ct_pool = HashMap::::new(); - let shared_service = Arc::new(service); - // for return - let service = shared_service.clone(); - - // let message_sink = tokio::sync:: - // let mut stream = std::pin::pin!(stream); - let serve_loop_ct = ct.child_token(); - let peer_return: Peer = peer.clone(); - let current_span = tracing::Span::current(); - let handle = tokio::spawn(async move { - let mut transport = transport.into_transport(); - let mut batch_messages = VecDeque::>::new(); - let mut send_task_set = tokio::task::JoinSet::::new(); - #[derive(Debug)] - enum SendTaskResult { - Request { - id: RequestId, - result: Result<(), DynamicTransportError>, - }, - Notification { - responder: Responder>, - cancellation_param: Option, - result: Result<(), DynamicTransportError>, - }, - } - #[derive(Debug)] - enum Event { - ProxyMessage(PeerSinkMessage), - PeerMessage(RxJsonRpcMessage), - ToSink(TxJsonRpcMessage), - SendTaskResult(SendTaskResult), - } - - let quit_reason = loop { - let evt = if let Some(m) = batch_messages.pop_front() { - Event::PeerMessage(m) - } else { - tokio::select! { - m = sink_proxy_rx.recv(), if !sink_proxy_rx.is_closed() => { - if let Some(m) = m { - Event::ToSink(m) - } else { - continue - } - } - m = transport.receive() => { - if let Some(m) = m { - Event::PeerMessage(m) - } else { - // input stream closed - tracing::info!("input stream terminated"); - break QuitReason::Closed - } - } - m = peer_rx.recv(), if !peer_rx.is_closed() => { - if let Some(m) = m { - Event::ProxyMessage(m) - } else { - continue - } - } - m = send_task_set.join_next(), if !send_task_set.is_empty() => { - let Some(result) = m else { - continue - }; - match result { - Err(e) => { - // join error, which is serious, we should quit. - tracing::error!(%e, "send request task encounter a tokio join error"); - break QuitReason::JoinError(e) - } - Ok(result) => { - Event::SendTaskResult(result) - } - } - } - _ = serve_loop_ct.cancelled() => { - tracing::info!("task cancelled"); - break QuitReason::Cancelled - } - } - }; - - tracing::trace!(?evt, "new event"); - match evt { - Event::SendTaskResult(SendTaskResult::Request { id, result }) => { - if let Err(e) = result { - if let Some(responder) = local_responder_pool.remove(&id) { - let _ = responder.send(Err(ServiceError::TransportSend(e))); - } - } - } - Event::SendTaskResult(SendTaskResult::Notification { - responder, - result, - cancellation_param, - }) => { - let response = if let Err(e) = result { - Err(ServiceError::TransportSend(e)) - } else { - Ok(()) - }; - let _ = responder.send(response); - if let Some(param) = cancellation_param { - if let Some(responder) = local_responder_pool.remove(¶m.request_id) { - tracing::info!(id = %param.request_id, reason = param.reason, "cancelled"); - let _response_result = responder.send(Err(ServiceError::Cancelled { - reason: param.reason.clone(), - })); - } - } - } - // response and error - Event::ToSink(m) => { - if let Some(id) = match &m { - JsonRpcMessage::Response(response) => Some(&response.id), - JsonRpcMessage::Error(error) => Some(&error.id), - _ => None, - } { - if let Some(ct) = local_ct_pool.remove(id) { - ct.cancel(); - } - let send = transport.send(m); - let current_span = tracing::Span::current(); - tokio::spawn(async move { - let send_result = send.await; - if let Err(error) = send_result { - tracing::error!(%error, "fail to response message"); - } - }.instrument(current_span)); - } - } - Event::ProxyMessage(PeerSinkMessage::Request { - request, - id, - responder, - }) => { - local_responder_pool.insert(id.clone(), responder); - let send = transport.send(JsonRpcMessage::request(request, id.clone())); - { - let id = id.clone(); - let current_span = tracing::Span::current(); - send_task_set.spawn(send.map(move |r| SendTaskResult::Request { - id, - result: r.map_err(DynamicTransportError::new::), - }).instrument(current_span)); - } - } - Event::ProxyMessage(PeerSinkMessage::Notification { - notification, - responder, - }) => { - // catch cancellation notification - let mut cancellation_param = None; - let notification = match notification.try_into() { - Ok::(cancelled) => { - cancellation_param.replace(cancelled.params.clone()); - cancelled.into() - } - Err(notification) => notification, - }; - let send = transport.send(JsonRpcMessage::notification(notification)); - let current_span = tracing::Span::current(); - send_task_set.spawn(send.map(move |result| SendTaskResult::Notification { - responder, - cancellation_param, - result: result.map_err(DynamicTransportError::new::), - }).instrument(current_span)); - } - Event::PeerMessage(JsonRpcMessage::Request(JsonRpcRequest { - id, - mut request, - .. - })) => { - tracing::debug!(%id, ?request, "received request"); - { - let service = shared_service.clone(); - let sink = sink_proxy_tx.clone(); - let request_ct = serve_loop_ct.child_token(); - let context_ct = request_ct.child_token(); - local_ct_pool.insert(id.clone(), request_ct); - let mut extensions = Extensions::new(); - let mut meta = Meta::new(); - // avoid clone - // swap meta firstly, otherwise progress token will be lost - std::mem::swap(&mut meta, request.get_meta_mut()); - std::mem::swap(&mut extensions, request.extensions_mut()); - let context = RequestContext { - ct: context_ct, - id: id.clone(), - peer: peer.clone(), - meta, - extensions, - }; - let current_span = tracing::Span::current(); - tokio::spawn(async move { - let result = service - .handle_request(request, context) - .await; - let response = match result { - Ok(result) => { - tracing::debug!(%id, ?result, "response message"); - JsonRpcMessage::response(result, id) - } - Err(error) => { - tracing::warn!(%id, ?error, "response error"); - JsonRpcMessage::error(error, id) - } - }; - let _send_result = sink.send(response).await; - }.instrument(current_span)); - } - } - Event::PeerMessage(JsonRpcMessage::Notification(JsonRpcNotification { - notification, - .. - })) => { - tracing::info!(?notification, "received notification"); - // catch cancelled notification - let mut notification = match notification.try_into() { - Ok::(cancelled) => { - if let Some(ct) = local_ct_pool.remove(&cancelled.params.request_id) { - tracing::info!(id = %cancelled.params.request_id, reason = cancelled.params.reason, "cancelled"); - ct.cancel(); - } - cancelled.into() - } - Err(notification) => notification, - }; - { - let service = shared_service.clone(); - let mut extensions = Extensions::new(); - let mut meta = Meta::new(); - // avoid clone - std::mem::swap(&mut extensions, notification.extensions_mut()); - std::mem::swap(&mut meta, notification.get_meta_mut()); - let context = NotificationContext { - peer: peer.clone(), - meta, - extensions, - }; - let current_span = tracing::Span::current(); - tokio::spawn(async move { - let result = service.handle_notification(notification, context).await; - if let Err(error) = result { - tracing::warn!(%error, "Error sending notification"); - } - }.instrument(current_span)); - } - } - Event::PeerMessage(JsonRpcMessage::Response(JsonRpcResponse { - result, - id, - .. - })) => { - if let Some(responder) = local_responder_pool.remove(&id) { - let response_result = responder.send(Ok(result)); - if let Err(_error) = response_result { - tracing::warn!(%id, "Error sending response"); - } - } - } - Event::PeerMessage(JsonRpcMessage::Error(JsonRpcError { error, id, .. })) => { - if let Some(responder) = local_responder_pool.remove(&id) { - let _response_result = responder.send(Err(ServiceError::McpError(error))); - if let Err(_error) = _response_result { - tracing::warn!(%id, "Error sending response"); - } - } - } - } - }; - let sink_close_result = transport.close().await; - if let Err(e) = sink_close_result { - tracing::error!(%e, "fail to close sink"); - } - tracing::info!(?quit_reason, "serve finished"); - quit_reason - }.instrument(current_span)); - RunningService { - service, - peer: peer_return, - handle: Some(handle), - cancellation_token: ct.clone(), - dg: ct.drop_guard(), - } -} diff --git a/codex-rs/vendor/rmcp/src/service/client.rs b/codex-rs/vendor/rmcp/src/service/client.rs deleted file mode 100644 index 837fafeffb..0000000000 --- a/codex-rs/vendor/rmcp/src/service/client.rs +++ /dev/null @@ -1,543 +0,0 @@ -use std::borrow::Cow; - -use thiserror::Error; - -use super::*; -use crate::{ - model::{ - ArgumentInfo, CallToolRequest, CallToolRequestParams, CallToolResult, - CancelledNotification, CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage, - ClientNotification, ClientRequest, ClientResult, CompleteRequest, CompleteRequestParams, - CompleteResult, CompletionContext, CompletionInfo, ErrorData, GetPromptRequest, - GetPromptRequestParams, GetPromptResult, InitializeRequest, InitializedNotification, - JsonRpcResponse, ListPromptsRequest, ListPromptsResult, ListResourceTemplatesRequest, - ListResourceTemplatesResult, ListResourcesRequest, ListResourcesResult, ListToolsRequest, - ListToolsResult, PaginatedRequestParams, ProgressNotification, ProgressNotificationParam, - ReadResourceRequest, ReadResourceRequestParams, ReadResourceResult, Reference, RequestId, - RootsListChangedNotification, ServerInfo, ServerJsonRpcMessage, ServerNotification, - ServerRequest, ServerResult, SetLevelRequest, SetLevelRequestParams, SubscribeRequest, - SubscribeRequestParams, UnsubscribeRequest, UnsubscribeRequestParams, - }, - transport::DynamicTransportError, -}; - -/// It represents the error that may occur when serving the client. -/// -/// if you want to handle the error, you can use `serve_client_with_ct` or `serve_client` with `Result, ClientError>` -#[derive(Error, Debug)] -pub enum ClientInitializeError { - #[error("expect initialized response, but received: {0:?}")] - ExpectedInitResponse(Option), - - #[error("expect initialized result, but received: {0:?}")] - ExpectedInitResult(Option), - - #[error("conflict initialized response id: expected {0}, got {1}")] - ConflictInitResponseId(RequestId, RequestId), - - #[error("connection closed: {0}")] - ConnectionClosed(String), - - #[error("Send message error {error}, when {context}")] - TransportError { - error: DynamicTransportError, - context: Cow<'static, str>, - }, - - #[error("JSON-RPC error: {0}")] - JsonRpcError(ErrorData), - - #[error("Cancelled")] - Cancelled, -} - -impl ClientInitializeError { - pub fn transport + 'static>( - error: T::Error, - context: impl Into>, - ) -> Self { - Self::TransportError { - error: DynamicTransportError::new::(error), - context: context.into(), - } - } -} - -/// Helper function to get the next message from the stream -async fn expect_next_message( - transport: &mut T, - context: &str, -) -> Result -where - T: Transport, -{ - transport - .receive() - .await - .ok_or_else(|| ClientInitializeError::ConnectionClosed(context.to_string())) -} - -/// Helper function to expect a response from the stream -async fn expect_response( - transport: &mut T, - context: &str, - service: &S, - peer: Peer, -) -> Result<(ServerResult, RequestId), ClientInitializeError> -where - T: Transport, - S: Service, -{ - loop { - let message = expect_next_message(transport, context).await?; - match message { - // Expected message to complete the initialization - ServerJsonRpcMessage::Response(JsonRpcResponse { id, result, .. }) => { - break Ok((result, id)); - } - // Handle JSON-RPC error responses - ServerJsonRpcMessage::Error(error) => { - break Err(ClientInitializeError::JsonRpcError(error.error)); - } - // Server could send logging messages before handshake - ServerJsonRpcMessage::Notification(mut notification) => { - let ServerNotification::LoggingMessageNotification(logging) = - &mut notification.notification - else { - tracing::warn!(?notification, "Received unexpected message"); - continue; - }; - - let mut context = NotificationContext { - peer: peer.clone(), - meta: Meta::default(), - extensions: Extensions::default(), - }; - - if let Some(meta) = logging.extensions.get_mut::() { - std::mem::swap(&mut context.meta, meta); - } - std::mem::swap(&mut context.extensions, &mut logging.extensions); - - if let Err(error) = service - .handle_notification(notification.notification, context) - .await - { - tracing::warn!(?error, "Handle logging before handshake failed."); - } - } - // Server could send pings before handshake - ServerJsonRpcMessage::Request(ref request) - if matches!(request.request, ServerRequest::PingRequest(_)) => - { - tracing::trace!("Received ping request. Ignored.") - } - // Server SHOULD NOT send any other messages before handshake. We ignore them anyway - _ => tracing::warn!(?message, "Received unexpected message"), - } - } -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct RoleClient; - -impl ServiceRole for RoleClient { - type Req = ClientRequest; - type Resp = ClientResult; - type Not = ClientNotification; - type PeerReq = ServerRequest; - type PeerResp = ServerResult; - type PeerNot = ServerNotification; - type Info = ClientInfo; - type PeerInfo = ServerInfo; - type InitializeError = ClientInitializeError; - const IS_CLIENT: bool = true; -} - -pub type ServerSink = Peer; - -impl> ServiceExt for S { - fn serve_with_ct( - self, - transport: T, - ct: CancellationToken, - ) -> impl Future, ClientInitializeError>> + Send - where - T: IntoTransport, - E: std::error::Error + Send + Sync + 'static, - Self: Sized, - { - serve_client_with_ct(self, transport, ct) - } -} - -pub async fn serve_client( - service: S, - transport: T, -) -> Result, ClientInitializeError> -where - S: Service, - T: IntoTransport, - E: std::error::Error + Send + Sync + 'static, -{ - serve_client_with_ct(service, transport, Default::default()).await -} - -pub async fn serve_client_with_ct( - service: S, - transport: T, - ct: CancellationToken, -) -> Result, ClientInitializeError> -where - S: Service, - T: IntoTransport, - E: std::error::Error + Send + Sync + 'static, -{ - tokio::select! { - result = serve_client_with_ct_inner(service, transport.into_transport(), ct.clone()) => { result } - _ = ct.cancelled() => { - Err(ClientInitializeError::Cancelled) - } - } -} - -async fn serve_client_with_ct_inner( - service: S, - transport: T, - ct: CancellationToken, -) -> Result, ClientInitializeError> -where - S: Service, - T: Transport + 'static, -{ - let mut transport = transport.into_transport(); - let id_provider = >::default(); - - // service - let id = id_provider.next_request_id(); - let init_request = InitializeRequest { - method: Default::default(), - params: service.get_info(), - extensions: Default::default(), - }; - transport - .send(ClientJsonRpcMessage::request( - ClientRequest::InitializeRequest(init_request), - id.clone(), - )) - .await - .map_err(|error| ClientInitializeError::TransportError { - error: DynamicTransportError::new::(error), - context: "send initialize request".into(), - })?; - - let (peer, peer_rx) = Peer::new(id_provider, None); - - let (response, response_id) = expect_response( - &mut transport, - "initialize response", - &service, - peer.clone(), - ) - .await?; - - if id != response_id { - return Err(ClientInitializeError::ConflictInitResponseId( - id, - response_id, - )); - } - - let ServerResult::InitializeResult(initialize_result) = response else { - return Err(ClientInitializeError::ExpectedInitResult(Some(response))); - }; - peer.set_peer_info(initialize_result); - - // send notification - let notification = ClientJsonRpcMessage::notification( - ClientNotification::InitializedNotification(InitializedNotification { - method: Default::default(), - extensions: Default::default(), - }), - ); - transport.send(notification).await.map_err(|error| { - ClientInitializeError::transport::(error, "send initialized notification") - })?; - Ok(serve_inner(service, transport, peer, peer_rx, ct)) -} - -macro_rules! method { - (peer_req $method:ident $Req:ident() => $Resp: ident ) => { - pub async fn $method(&self) -> Result<$Resp, ServiceError> { - let result = self - .send_request(ClientRequest::$Req($Req { - method: Default::default(), - })) - .await?; - match result { - ServerResult::$Resp(result) => Ok(result), - _ => Err(ServiceError::UnexpectedResponse), - } - } - }; - (peer_req $method:ident $Req:ident($Param: ident) => $Resp: ident ) => { - pub async fn $method(&self, params: $Param) -> Result<$Resp, ServiceError> { - let result = self - .send_request(ClientRequest::$Req($Req { - method: Default::default(), - params, - extensions: Default::default(), - })) - .await?; - match result { - ServerResult::$Resp(result) => Ok(result), - _ => Err(ServiceError::UnexpectedResponse), - } - } - }; - (peer_req $method:ident $Req:ident($Param: ident)? => $Resp: ident ) => { - pub async fn $method(&self, params: Option<$Param>) -> Result<$Resp, ServiceError> { - let result = self - .send_request(ClientRequest::$Req($Req { - method: Default::default(), - params, - extensions: Default::default(), - })) - .await?; - match result { - ServerResult::$Resp(result) => Ok(result), - _ => Err(ServiceError::UnexpectedResponse), - } - } - }; - (peer_req $method:ident $Req:ident($Param: ident)) => { - pub async fn $method(&self, params: $Param) -> Result<(), ServiceError> { - let result = self - .send_request(ClientRequest::$Req($Req { - method: Default::default(), - params, - extensions: Default::default(), - })) - .await?; - match result { - ServerResult::EmptyResult(_) => Ok(()), - _ => Err(ServiceError::UnexpectedResponse), - } - } - }; - - (peer_not $method:ident $Not:ident($Param: ident)) => { - pub async fn $method(&self, params: $Param) -> Result<(), ServiceError> { - self.send_notification(ClientNotification::$Not($Not { - method: Default::default(), - params, - extensions: Default::default(), - })) - .await?; - Ok(()) - } - }; - (peer_not $method:ident $Not:ident) => { - pub async fn $method(&self) -> Result<(), ServiceError> { - self.send_notification(ClientNotification::$Not($Not { - method: Default::default(), - extensions: Default::default(), - })) - .await?; - Ok(()) - } - }; -} - -impl Peer { - method!(peer_req complete CompleteRequest(CompleteRequestParams) => CompleteResult); - method!(peer_req set_level SetLevelRequest(SetLevelRequestParams)); - method!(peer_req get_prompt GetPromptRequest(GetPromptRequestParams) => GetPromptResult); - method!(peer_req list_prompts ListPromptsRequest(PaginatedRequestParams)? => ListPromptsResult); - method!(peer_req list_resources ListResourcesRequest(PaginatedRequestParams)? => ListResourcesResult); - method!(peer_req list_resource_templates ListResourceTemplatesRequest(PaginatedRequestParams)? => ListResourceTemplatesResult); - method!(peer_req read_resource ReadResourceRequest(ReadResourceRequestParams) => ReadResourceResult); - method!(peer_req subscribe SubscribeRequest(SubscribeRequestParams) ); - method!(peer_req unsubscribe UnsubscribeRequest(UnsubscribeRequestParams)); - method!(peer_req call_tool CallToolRequest(CallToolRequestParams) => CallToolResult); - method!(peer_req list_tools ListToolsRequest(PaginatedRequestParams)? => ListToolsResult); - - method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam)); - method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam)); - method!(peer_not notify_initialized InitializedNotification); - method!(peer_not notify_roots_list_changed RootsListChangedNotification); -} - -impl Peer { - /// A wrapper method for [`Peer::list_tools`]. - /// - /// This function will call [`Peer::list_tools`] multiple times until all tools are listed. - pub async fn list_all_tools(&self) -> Result, ServiceError> { - let mut tools = Vec::new(); - let mut cursor = None; - loop { - let result = self - .list_tools(Some(PaginatedRequestParams { meta: None, cursor })) - .await?; - tools.extend(result.tools); - cursor = result.next_cursor; - if cursor.is_none() { - break; - } - } - Ok(tools) - } - - /// A wrapper method for [`Peer::list_prompts`]. - /// - /// This function will call [`Peer::list_prompts`] multiple times until all prompts are listed. - pub async fn list_all_prompts(&self) -> Result, ServiceError> { - let mut prompts = Vec::new(); - let mut cursor = None; - loop { - let result = self - .list_prompts(Some(PaginatedRequestParams { meta: None, cursor })) - .await?; - prompts.extend(result.prompts); - cursor = result.next_cursor; - if cursor.is_none() { - break; - } - } - Ok(prompts) - } - - /// A wrapper method for [`Peer::list_resources`]. - /// - /// This function will call [`Peer::list_resources`] multiple times until all resources are listed. - pub async fn list_all_resources(&self) -> Result, ServiceError> { - let mut resources = Vec::new(); - let mut cursor = None; - loop { - let result = self - .list_resources(Some(PaginatedRequestParams { meta: None, cursor })) - .await?; - resources.extend(result.resources); - cursor = result.next_cursor; - if cursor.is_none() { - break; - } - } - Ok(resources) - } - - /// A wrapper method for [`Peer::list_resource_templates`]. - /// - /// This function will call [`Peer::list_resource_templates`] multiple times until all resource templates are listed. - pub async fn list_all_resource_templates( - &self, - ) -> Result, ServiceError> { - let mut resource_templates = Vec::new(); - let mut cursor = None; - loop { - let result = self - .list_resource_templates(Some(PaginatedRequestParams { meta: None, cursor })) - .await?; - resource_templates.extend(result.resource_templates); - cursor = result.next_cursor; - if cursor.is_none() { - break; - } - } - Ok(resource_templates) - } - - /// Convenient method to get completion suggestions for a prompt argument - /// - /// # Arguments - /// * `prompt_name` - Name of the prompt being completed - /// * `argument_name` - Name of the argument being completed - /// * `current_value` - Current partial value of the argument - /// * `context` - Optional context with previously resolved arguments - /// - /// # Returns - /// CompletionInfo with suggestions for the specified prompt argument - pub async fn complete_prompt_argument( - &self, - prompt_name: impl Into, - argument_name: impl Into, - current_value: impl Into, - context: Option, - ) -> Result { - let request = CompleteRequestParams { - meta: None, - r#ref: Reference::for_prompt(prompt_name), - argument: ArgumentInfo { - name: argument_name.into(), - value: current_value.into(), - }, - context, - }; - - let result = self.complete(request).await?; - Ok(result.completion) - } - - /// Convenient method to get completion suggestions for a resource URI argument - /// - /// # Arguments - /// * `uri_template` - URI template pattern being completed - /// * `argument_name` - Name of the URI parameter being completed - /// * `current_value` - Current partial value of the parameter - /// * `context` - Optional context with previously resolved arguments - /// - /// # Returns - /// CompletionInfo with suggestions for the specified resource URI argument - pub async fn complete_resource_argument( - &self, - uri_template: impl Into, - argument_name: impl Into, - current_value: impl Into, - context: Option, - ) -> Result { - let request = CompleteRequestParams { - meta: None, - r#ref: Reference::for_resource(uri_template), - argument: ArgumentInfo { - name: argument_name.into(), - value: current_value.into(), - }, - context, - }; - - let result = self.complete(request).await?; - Ok(result.completion) - } - - /// Simple completion for a prompt argument without context - /// - /// This is a convenience wrapper around `complete_prompt_argument` for - /// simple completion scenarios that don't require context awareness. - pub async fn complete_prompt_simple( - &self, - prompt_name: impl Into, - argument_name: impl Into, - current_value: impl Into, - ) -> Result, ServiceError> { - let completion = self - .complete_prompt_argument(prompt_name, argument_name, current_value, None) - .await?; - Ok(completion.values) - } - - /// Simple completion for a resource URI argument without context - /// - /// This is a convenience wrapper around `complete_resource_argument` for - /// simple completion scenarios that don't require context awareness. - pub async fn complete_resource_simple( - &self, - uri_template: impl Into, - argument_name: impl Into, - current_value: impl Into, - ) -> Result, ServiceError> { - let completion = self - .complete_resource_argument(uri_template, argument_name, current_value, None) - .await?; - Ok(completion.values) - } -} diff --git a/codex-rs/vendor/rmcp/src/service/server.rs b/codex-rs/vendor/rmcp/src/service/server.rs deleted file mode 100644 index 5f54f3dcd6..0000000000 --- a/codex-rs/vendor/rmcp/src/service/server.rs +++ /dev/null @@ -1,895 +0,0 @@ -use std::borrow::Cow; -#[cfg(feature = "elicitation")] -use std::collections::HashSet; - -use thiserror::Error; -#[cfg(feature = "elicitation")] -use url::Url; - -use super::*; -#[cfg(feature = "elicitation")] -use crate::model::{ - CreateElicitationRequest, CreateElicitationRequestParams, CreateElicitationResult, - ElicitationAction, ElicitationCompletionNotification, ElicitationResponseNotificationParam, -}; -use crate::{ - model::{ - CancelledNotification, CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage, - ClientNotification, ClientRequest, ClientResult, CreateMessageRequest, - CreateMessageRequestParams, CreateMessageResult, ErrorData, ListRootsRequest, - ListRootsResult, LoggingMessageNotification, LoggingMessageNotificationParam, - ProgressNotification, ProgressNotificationParam, PromptListChangedNotification, - ProtocolVersion, ResourceListChangedNotification, ResourceUpdatedNotification, - ResourceUpdatedNotificationParam, ServerInfo, ServerNotification, ServerRequest, - ServerResult, ToolListChangedNotification, - }, - transport::DynamicTransportError, -}; - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct RoleServer; - -impl ServiceRole for RoleServer { - type Req = ServerRequest; - type Resp = ServerResult; - type Not = ServerNotification; - type PeerReq = ClientRequest; - type PeerResp = ClientResult; - type PeerNot = ClientNotification; - type Info = ServerInfo; - type PeerInfo = ClientInfo; - - type InitializeError = ServerInitializeError; - const IS_CLIENT: bool = false; -} - -/// It represents the error that may occur when serving the server. -/// -/// if you want to handle the error, you can use `serve_server_with_ct` or `serve_server` with `Result, ServerError>` -#[derive(Error, Debug)] -pub enum ServerInitializeError { - #[error("expect initialized request, but received: {0:?}")] - ExpectedInitializeRequest(Option), - - #[error("expect initialized notification, but received: {0:?}")] - ExpectedInitializedNotification(Option), - - #[error("connection closed: {0}")] - ConnectionClosed(String), - - #[error("unexpected initialize result: {0:?}")] - UnexpectedInitializeResponse(ServerResult), - - #[error("initialize failed: {0}")] - InitializeFailed(ErrorData), - - #[error("unsupported protocol version: {0}")] - UnsupportedProtocolVersion(ProtocolVersion), - - #[error("Send message error {error}, when {context}")] - TransportError { - error: DynamicTransportError, - context: Cow<'static, str>, - }, - - #[error("Cancelled")] - Cancelled, -} - -impl ServerInitializeError { - pub fn transport + 'static>( - error: T::Error, - context: impl Into>, - ) -> Self { - Self::TransportError { - error: DynamicTransportError::new::(error), - context: context.into(), - } - } -} -pub type ClientSink = Peer; - -impl> ServiceExt for S { - fn serve_with_ct( - self, - transport: T, - ct: CancellationToken, - ) -> impl Future, ServerInitializeError>> + Send - where - T: IntoTransport, - E: std::error::Error + Send + Sync + 'static, - Self: Sized, - { - serve_server_with_ct(self, transport, ct) - } -} - -pub async fn serve_server( - service: S, - transport: T, -) -> Result, ServerInitializeError> -where - S: Service, - T: IntoTransport, - E: std::error::Error + Send + Sync + 'static, -{ - serve_server_with_ct(service, transport, CancellationToken::new()).await -} - -/// Helper function to get the next message from the stream -async fn expect_next_message( - transport: &mut T, - context: &str, -) -> Result -where - T: Transport, -{ - transport - .receive() - .await - .ok_or_else(|| ServerInitializeError::ConnectionClosed(context.to_string())) -} - -/// Helper function to expect a request from the stream -async fn expect_request( - transport: &mut T, - context: &str, -) -> Result<(ClientRequest, RequestId), ServerInitializeError> -where - T: Transport, -{ - let msg = expect_next_message(transport, context).await?; - let msg_clone = msg.clone(); - msg.into_request() - .ok_or(ServerInitializeError::ExpectedInitializeRequest(Some( - msg_clone, - ))) -} - -/// Helper function to expect a notification from the stream -async fn expect_notification( - transport: &mut T, - context: &str, -) -> Result -where - T: Transport, -{ - let msg = expect_next_message(transport, context).await?; - let msg_clone = msg.clone(); - msg.into_notification() - .ok_or(ServerInitializeError::ExpectedInitializedNotification( - Some(msg_clone), - )) -} - -pub async fn serve_server_with_ct( - service: S, - transport: T, - ct: CancellationToken, -) -> Result, ServerInitializeError> -where - S: Service, - T: IntoTransport, - E: std::error::Error + Send + Sync + 'static, -{ - tokio::select! { - result = serve_server_with_ct_inner(service, transport.into_transport(), ct.clone()) => { result } - _ = ct.cancelled() => { - Err(ServerInitializeError::Cancelled) - } - } -} - -async fn serve_server_with_ct_inner( - service: S, - transport: T, - ct: CancellationToken, -) -> Result, ServerInitializeError> -where - S: Service, - T: Transport + 'static, -{ - let mut transport = transport.into_transport(); - let id_provider = >::default(); - - // Get initialize request - let (request, id) = expect_request(&mut transport, "initialized request").await?; - - let ClientRequest::InitializeRequest(peer_info) = &request else { - return Err(ServerInitializeError::ExpectedInitializeRequest(Some( - ClientJsonRpcMessage::request(request, id), - ))); - }; - let (peer, peer_rx) = Peer::new(id_provider, Some(peer_info.params.clone())); - let context = RequestContext { - ct: ct.child_token(), - id: id.clone(), - meta: request.get_meta().clone(), - extensions: request.extensions().clone(), - peer: peer.clone(), - }; - // Send initialize response - let init_response = service.handle_request(request.clone(), context).await; - let mut init_response = match init_response { - Ok(ServerResult::InitializeResult(init_response)) => init_response, - Ok(result) => { - return Err(ServerInitializeError::UnexpectedInitializeResponse(result)); - } - Err(e) => { - transport - .send(ServerJsonRpcMessage::error(e.clone(), id)) - .await - .map_err(|error| { - ServerInitializeError::transport::(error, "sending error response") - })?; - return Err(ServerInitializeError::InitializeFailed(e)); - } - }; - let peer_protocol_version = peer_info.params.protocol_version.clone(); - let protocol_version = match peer_protocol_version - .partial_cmp(&init_response.protocol_version) - .ok_or(ServerInitializeError::UnsupportedProtocolVersion( - peer_protocol_version, - ))? { - std::cmp::Ordering::Less => peer_info.params.protocol_version.clone(), - _ => init_response.protocol_version, - }; - init_response.protocol_version = protocol_version; - transport - .send(ServerJsonRpcMessage::response( - ServerResult::InitializeResult(init_response), - id, - )) - .await - .map_err(|error| { - ServerInitializeError::transport::(error, "sending initialize response") - })?; - - // Wait for initialize notification - let notification = expect_notification(&mut transport, "initialize notification").await?; - let ClientNotification::InitializedNotification(_) = notification else { - return Err(ServerInitializeError::ExpectedInitializedNotification( - Some(ClientJsonRpcMessage::notification(notification)), - )); - }; - let context = NotificationContext { - meta: notification.get_meta().clone(), - extensions: notification.extensions().clone(), - peer: peer.clone(), - }; - let _ = service.handle_notification(notification, context).await; - // Continue processing service - Ok(serve_inner(service, transport, peer, peer_rx, ct)) -} - -macro_rules! method { - (peer_req $method:ident $Req:ident() => $Resp: ident ) => { - pub async fn $method(&self) -> Result<$Resp, ServiceError> { - let result = self - .send_request(ServerRequest::$Req($Req { - method: Default::default(), - extensions: Default::default(), - })) - .await?; - match result { - ClientResult::$Resp(result) => Ok(result), - _ => Err(ServiceError::UnexpectedResponse), - } - } - }; - (peer_req $method:ident $Req:ident($Param: ident) => $Resp: ident ) => { - pub async fn $method(&self, params: $Param) -> Result<$Resp, ServiceError> { - let result = self - .send_request(ServerRequest::$Req($Req { - method: Default::default(), - params, - extensions: Default::default(), - })) - .await?; - match result { - ClientResult::$Resp(result) => Ok(result), - _ => Err(ServiceError::UnexpectedResponse), - } - } - }; - (peer_req $method:ident $Req:ident($Param: ident)) => { - pub fn $method( - &self, - params: $Param, - ) -> impl Future> + Send + '_ { - async move { - let result = self - .send_request(ServerRequest::$Req($Req { - method: Default::default(), - params, - })) - .await?; - match result { - ClientResult::EmptyResult(_) => Ok(()), - _ => Err(ServiceError::UnexpectedResponse), - } - } - } - }; - - (peer_not $method:ident $Not:ident($Param: ident)) => { - pub async fn $method(&self, params: $Param) -> Result<(), ServiceError> { - self.send_notification(ServerNotification::$Not($Not { - method: Default::default(), - params, - extensions: Default::default(), - })) - .await?; - Ok(()) - } - }; - (peer_not $method:ident $Not:ident) => { - pub async fn $method(&self) -> Result<(), ServiceError> { - self.send_notification(ServerNotification::$Not($Not { - method: Default::default(), - extensions: Default::default(), - })) - .await?; - Ok(()) - } - }; - - // Timeout-only variants (base method should be created separately with peer_req) - (peer_req_with_timeout $method_with_timeout:ident $Req:ident() => $Resp: ident) => { - pub async fn $method_with_timeout( - &self, - timeout: Option, - ) -> Result<$Resp, ServiceError> { - let request = ServerRequest::$Req($Req { - method: Default::default(), - extensions: Default::default(), - }); - let options = crate::service::PeerRequestOptions { - timeout, - meta: None, - }; - let result = self - .send_request_with_option(request, options) - .await? - .await_response() - .await?; - match result { - ClientResult::$Resp(result) => Ok(result), - _ => Err(ServiceError::UnexpectedResponse), - } - } - }; - - (peer_req_with_timeout $method_with_timeout:ident $Req:ident($Param: ident) => $Resp: ident) => { - pub async fn $method_with_timeout( - &self, - params: $Param, - timeout: Option, - ) -> Result<$Resp, ServiceError> { - let request = ServerRequest::$Req($Req { - method: Default::default(), - params, - extensions: Default::default(), - }); - let options = crate::service::PeerRequestOptions { - timeout, - meta: None, - }; - let result = self - .send_request_with_option(request, options) - .await? - .await_response() - .await?; - match result { - ClientResult::$Resp(result) => Ok(result), - _ => Err(ServiceError::UnexpectedResponse), - } - } - }; -} - -impl Peer { - /// Check if the client supports sampling tools capability. - pub fn supports_sampling_tools(&self) -> bool { - if let Some(client_info) = self.peer_info() { - client_info - .capabilities - .sampling - .as_ref() - .and_then(|s| s.tools.as_ref()) - .is_some() - } else { - false - } - } - - pub async fn create_message( - &self, - params: CreateMessageRequestParams, - ) -> Result { - // MUST throw error when tools/toolChoice provided without capability - if (params.tools.is_some() || params.tool_choice.is_some()) - && !self.supports_sampling_tools() - { - return Err(ServiceError::McpError(ErrorData::invalid_params( - "tools or toolChoice provided but client does not support sampling tools capability", - None, - ))); - } - // Validate message structure - params - .validate() - .map_err(|e| ServiceError::McpError(ErrorData::invalid_params(e, None)))?; - let result = self - .send_request(ServerRequest::CreateMessageRequest(CreateMessageRequest { - method: Default::default(), - params, - extensions: Default::default(), - })) - .await?; - match result { - ClientResult::CreateMessageResult(result) => Ok(*result), - _ => Err(ServiceError::UnexpectedResponse), - } - } - method!(peer_req list_roots ListRootsRequest() => ListRootsResult); - #[cfg(feature = "elicitation")] - method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult); - #[cfg(feature = "elicitation")] - method!(peer_req_with_timeout create_elicitation_with_timeout CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult); - #[cfg(feature = "elicitation")] - method!(peer_not notify_url_elicitation_completed ElicitationCompletionNotification(ElicitationResponseNotificationParam)); - - method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam)); - method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam)); - method!(peer_not notify_logging_message LoggingMessageNotification(LoggingMessageNotificationParam)); - method!(peer_not notify_resource_updated ResourceUpdatedNotification(ResourceUpdatedNotificationParam)); - method!(peer_not notify_resource_list_changed ResourceListChangedNotification); - method!(peer_not notify_tool_list_changed ToolListChangedNotification); - method!(peer_not notify_prompt_list_changed PromptListChangedNotification); -} - -// ============================================================================= -// ELICITATION CONVENIENCE METHODS -// These methods are specific to server role and provide typed elicitation functionality -// ============================================================================= - -/// Errors that can occur during typed elicitation operations -#[cfg(feature = "elicitation")] -#[derive(Error, Debug)] -pub enum ElicitationError { - /// The elicitation request failed at the service level - #[error("Service error: {0}")] - Service(#[from] ServiceError), - - /// User explicitly declined to provide the requested information - /// This indicates a conscious decision by the user to reject the request - /// (e.g., clicked "Reject", "Decline", "No", etc.) - #[error("User explicitly declined the request")] - UserDeclined, - - /// User dismissed the request without making an explicit choice - /// This indicates the user cancelled without explicitly declining - /// (e.g., closed dialog, clicked outside, pressed Escape, etc.) - #[error("User cancelled/dismissed the request")] - UserCancelled, - - /// The response data could not be parsed into the requested type - #[error("Failed to parse response data: {error}\nReceived data: {data}")] - ParseError { - error: serde_json::Error, - data: serde_json::Value, - }, - - /// No response content was provided by the user - #[error("No response content provided")] - NoContent, - - /// Client does not support elicitation capability - #[error("Client does not support elicitation - capability not declared during initialization")] - CapabilityNotSupported, -} - -/// Marker trait to ensure that elicitation types generate object-type JSON schemas. -/// -/// This trait provides compile-time safety to ensure that types used with -/// `elicit()` methods will generate JSON schemas of type "object", which -/// aligns with MCP client expectations for structured data input. -/// -/// # Type Safety Rationale -/// -/// MCP clients typically expect JSON objects for elicitation schemas to -/// provide structured forms and validation. This trait prevents common -/// mistakes like: -/// -/// ```compile_fail -/// // These would not compile due to missing ElicitationSafe bound: -/// let name: String = server.elicit("Enter name").await?; // Primitive -/// let items: Vec = server.elicit("Enter items").await?; // Array -/// ``` -#[cfg(feature = "elicitation")] -pub trait ElicitationSafe: schemars::JsonSchema {} - -/// Macro to mark types as safe for elicitation by verifying they generate object schemas. -/// -/// This macro automatically implements the `ElicitationSafe` trait for struct types -/// that should be used with `elicit()` methods. -/// -/// # Example -/// -/// ```rust -/// use rmcp::elicit_safe; -/// use schemars::JsonSchema; -/// use serde::{Deserialize, Serialize}; -/// -/// #[derive(Serialize, Deserialize, JsonSchema)] -/// struct UserProfile { -/// name: String, -/// email: String, -/// } -/// -/// elicit_safe!(UserProfile); -/// -/// // Now safe to use in async context: -/// // let profile: UserProfile = server.elicit("Enter profile").await?; -/// ``` -#[cfg(feature = "elicitation")] -#[macro_export] -macro_rules! elicit_safe { - ($($t:ty),* $(,)?) => { - $( - impl $crate::service::ElicitationSafe for $t {} - )* - }; -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ElicitationMode { - Form, - Url, -} - -#[cfg(feature = "elicitation")] -impl Peer { - /// Check if the client supports elicitation capability - /// - /// Returns true if the client declared elicitation capability during initialization, - /// false otherwise. According to MCP 2025-06-18 specification, clients that support - /// elicitation MUST declare the capability during initialization. - pub fn supported_elicitation_modes(&self) -> HashSet { - if let Some(client_info) = self.peer_info() { - if let Some(elicit_capability) = &client_info.capabilities.elicitation { - let mut modes = HashSet::new(); - // Backward compatibility: if neither form nor url is specified, assume form - if elicit_capability.form.is_none() && elicit_capability.url.is_none() { - modes.insert(ElicitationMode::Form); - } else { - if elicit_capability.form.is_some() { - modes.insert(ElicitationMode::Form); - } - if elicit_capability.url.is_some() { - modes.insert(ElicitationMode::Url); - } - } - modes - } else { - HashSet::new() - } - } else { - HashSet::new() - } - } - - /// Request typed data from the user with automatic schema generation. - /// - /// This method automatically generates the JSON schema from the Rust type using `schemars`, - /// eliminating the need to manually create schemas. The response is automatically parsed - /// into the requested type. - /// - /// **Requires the `elicitation` feature to be enabled.** - /// - /// # Type Requirements - /// The type `T` must implement: - /// - `schemars::JsonSchema` - for automatic schema generation - /// - `serde::Deserialize` - for parsing the response - /// - /// # Arguments - /// * `message` - The prompt message for the user - /// - /// # Returns - /// * `Ok(Some(data))` if user provided valid data that matches type T - /// * `Err(ElicitationError::UserDeclined)` if user explicitly declined the request - /// * `Err(ElicitationError::UserCancelled)` if user cancelled/dismissed the request - /// * `Err(ElicitationError::ParseError { .. })` if response data couldn't be parsed into type T - /// * `Err(ElicitationError::NoContent)` if no response content was provided - /// * `Err(ElicitationError::Service(_))` if the underlying service call failed - /// - /// # Example - /// - /// Add to your `Cargo.toml`: - /// ```toml - /// [dependencies] - /// rmcp = { version = "0.3", features = ["elicitation"] } - /// serde = { version = "1.0", features = ["derive"] } - /// schemars = "1.0" - /// ``` - /// - /// ```rust,no_run - /// # use rmcp::*; - /// # use rmcp::service::ElicitationError; - /// # use serde::{Deserialize, Serialize}; - /// # use schemars::JsonSchema; - /// # - /// #[derive(Debug, Serialize, Deserialize, JsonSchema)] - /// struct UserProfile { - /// #[schemars(description = "Full name")] - /// name: String, - /// #[schemars(description = "Email address")] - /// email: String, - /// #[schemars(description = "Age")] - /// age: u8, - /// } - /// - /// // Mark as safe for elicitation (generates object schema) - /// rmcp::elicit_safe!(UserProfile); - /// - /// # async fn example(peer: Peer) -> Result<(), Box> { - /// match peer.elicit::("Please enter your profile information").await { - /// Ok(Some(profile)) => { - /// println!("Name: {}, Email: {}, Age: {}", profile.name, profile.email, profile.age); - /// } - /// Ok(None) => { - /// println!("User provided no content"); - /// } - /// Err(ElicitationError::UserDeclined) => { - /// println!("User explicitly declined to provide information"); - /// // Handle explicit decline - perhaps offer alternatives - /// } - /// Err(ElicitationError::UserCancelled) => { - /// println!("User cancelled the request"); - /// // Handle cancellation - perhaps prompt again later - /// } - /// Err(ElicitationError::ParseError { error, data }) => { - /// println!("Failed to parse response: {}\nData: {}", error, data); - /// } - /// Err(e) => return Err(e.into()), - /// } - /// # Ok(()) - /// # } - /// ``` - #[cfg(all(feature = "schemars", feature = "elicitation"))] - pub async fn elicit(&self, message: impl Into) -> Result, ElicitationError> - where - T: ElicitationSafe + for<'de> serde::Deserialize<'de>, - { - self.elicit_with_timeout(message, None).await - } - - /// Request typed data from the user with custom timeout. - /// - /// Same as `elicit()` but allows specifying a custom timeout for the request. - /// If the user doesn't respond within the timeout, the request will be cancelled. - /// - /// # Arguments - /// * `message` - The prompt message for the user - /// * `timeout` - Optional timeout duration. If None, uses default timeout behavior - /// - /// # Returns - /// Same as `elicit()` but may also return `ServiceError::Timeout` if timeout expires - /// - /// # Example - /// ```rust,no_run - /// # use rmcp::*; - /// # use rmcp::service::ElicitationError; - /// # use serde::{Deserialize, Serialize}; - /// # use schemars::JsonSchema; - /// # use std::time::Duration; - /// # - /// #[derive(Debug, Serialize, Deserialize, JsonSchema)] - /// struct QuickResponse { - /// answer: String, - /// } - /// - /// // Mark as safe for elicitation - /// rmcp::elicit_safe!(QuickResponse); - /// - /// # async fn example(peer: Peer) -> Result<(), Box> { - /// // Give user 30 seconds to respond - /// let timeout = Some(Duration::from_secs(30)); - /// match peer.elicit_with_timeout::( - /// "Quick question - what's your answer?", - /// timeout - /// ).await { - /// Ok(Some(response)) => println!("Got answer: {}", response.answer), - /// Ok(None) => println!("User provided no content"), - /// Err(ElicitationError::UserDeclined) => { - /// println!("User explicitly declined"); - /// // Handle explicit decline - /// } - /// Err(ElicitationError::UserCancelled) => { - /// println!("User cancelled/dismissed"); - /// // Handle cancellation - /// } - /// Err(ElicitationError::Service(ServiceError::Timeout { .. })) => { - /// println!("User didn't respond in time"); - /// } - /// Err(e) => return Err(e.into()), - /// } - /// # Ok(()) - /// # } - /// ``` - #[cfg(all(feature = "schemars", feature = "elicitation"))] - pub async fn elicit_with_timeout( - &self, - message: impl Into, - timeout: Option, - ) -> Result, ElicitationError> - where - T: ElicitationSafe + for<'de> serde::Deserialize<'de>, - { - // Check if client supports form elicitation capability - if !self - .supported_elicitation_modes() - .contains(&ElicitationMode::Form) - { - return Err(ElicitationError::CapabilityNotSupported); - } - - // Generate schema automatically from type - let schema = crate::model::ElicitationSchema::from_type::().map_err(|e| { - ElicitationError::Service(ServiceError::McpError(crate::ErrorData::invalid_params( - format!( - "Invalid schema for type {}: {}", - std::any::type_name::(), - e - ), - None, - ))) - })?; - - let response = self - .create_elicitation_with_timeout( - CreateElicitationRequestParams::FormElicitationParams { - meta: None, - message: message.into(), - requested_schema: schema, - }, - timeout, - ) - .await?; - - match response.action { - crate::model::ElicitationAction::Accept => { - if let Some(value) = response.content { - match serde_json::from_value::(value.clone()) { - Ok(parsed) => Ok(Some(parsed)), - Err(error) => Err(ElicitationError::ParseError { error, data: value }), - } - } else { - Err(ElicitationError::NoContent) - } - } - crate::model::ElicitationAction::Decline => Err(ElicitationError::UserDeclined), - crate::model::ElicitationAction::Cancel => Err(ElicitationError::UserCancelled), - } - } - - /// Request the user to visit a URL and confirm completion. - /// - /// This method sends a URL elicitation request to the client, prompting the user - /// to visit the specified URL and confirm completion. It returns the user's action - /// (accept/decline/cancel) without any additional data. - /// **Requires the `elicitation` feature to be enabled.** - /// - /// # Arguments - /// * `message` - The prompt message for the user - /// * `url` - The URL the user is requested to visit - /// * `elicitation_id` - A unique identifier for this elicitation request - /// # Returns - /// * `Ok(action)` indicating the user's response action - /// * `Err(ElicitationError::CapabilityNotSupported)` if client does not support elicitation via URL - /// * `Err(ElicitationError::Service(_))` if the underlying service call failed - /// # Example - /// ```rust,no_run - /// # use rmcp::*; - /// # use rmcp::model::ElicitationAction; - /// # use url::Url; - /// - /// async fn example(peer: Peer) -> Result<(), Box> { - /// let elicit_result = peer.elicit_url("Please visit the following URL to complete the action", - /// Url::parse("https://example.com/complete_action")?, "elicit_123").await?; - /// match elicit_result { - /// ElicitationAction::Accept => { - /// println!("User accepted and confirmed completion"); - /// } - /// ElicitationAction::Decline => { - /// println!("User declined the request"); - /// } - /// ElicitationAction::Cancel => { - /// println!("User cancelled/dismissed the request"); - /// } - /// } - /// Ok(()) - /// } - /// ``` - #[cfg(feature = "elicitation")] - pub async fn elicit_url( - &self, - message: impl Into, - url: impl Into, - elicitation_id: impl Into, - ) -> Result { - self.elicit_url_with_timeout(message, url, elicitation_id, None) - .await - } - - /// Request the user to visit a URL and confirm completion. - /// - /// Same as `elicit_url()` but allows specifying a custom timeout for the request. - /// - /// # Arguments - /// * `message` - The prompt message for the user - /// * `url` - The URL the user is requested to visit - /// * `elicitation_id` - A unique identifier for this elicitation request - /// * `timeout` - Optional timeout duration. If None, uses default timeout behavior - /// # Returns - /// * `Ok(action)` indicating the user's response action - /// * `Err(ElicitationError::CapabilityNotSupported)` if client does not support elicitation via URL - /// * `Err(ElicitationError::Service(_))` if the underlying service call failed - /// # Example - /// ```rust,no_run - /// # use std::time::Duration; - /// use rmcp::*; - /// # use rmcp::model::ElicitationAction; - /// # use url::Url; - /// - /// async fn example(peer: Peer) -> Result<(), Box> { - /// let elicit_result = peer.elicit_url_with_timeout("Please visit the following URL to complete the action", - /// Url::parse("https://example.com/complete_action")?, - /// "elicit_123", - /// Some(Duration::from_secs(30))).await?; - /// match elicit_result { - /// ElicitationAction::Accept => { - /// println!("User accepted and confirmed completion"); - /// } - /// ElicitationAction::Decline => { - /// println!("User declined the request"); - /// } - /// ElicitationAction::Cancel => { - /// println!("User cancelled/dismissed the request"); - /// } - /// } - /// Ok(()) - /// } - /// ``` - #[cfg(feature = "elicitation")] - pub async fn elicit_url_with_timeout( - &self, - message: impl Into, - url: impl Into, - elicitation_id: impl Into, - timeout: Option, - ) -> Result { - // Check if client supports url elicitation - if !self - .supported_elicitation_modes() - .contains(&ElicitationMode::Url) - { - return Err(ElicitationError::CapabilityNotSupported); - } - - let action = self - .create_elicitation_with_timeout( - CreateElicitationRequestParams::UrlElicitationParams { - meta: None, - message: message.into(), - url: url.into().to_string(), - elicitation_id: elicitation_id.into(), - }, - timeout, - ) - .await? - .action; - Ok(action) - } -} diff --git a/codex-rs/vendor/rmcp/src/service/tower.rs b/codex-rs/vendor/rmcp/src/service/tower.rs deleted file mode 100644 index ac4a66f00d..0000000000 --- a/codex-rs/vendor/rmcp/src/service/tower.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::{future::poll_fn, marker::PhantomData}; - -use tower_service::Service as TowerService; - -use super::NotificationContext; -use crate::service::{RequestContext, Service, ServiceRole}; - -pub struct TowerHandler { - pub service: S, - pub info: R::Info, - role: PhantomData, -} - -impl TowerHandler { - pub fn new(service: S, info: R::Info) -> Self { - Self { - service, - role: PhantomData, - info, - } - } -} - -impl Service for TowerHandler -where - S: TowerService + Sync + Send + Clone + 'static, - S::Error: Into, - S::Future: Send, -{ - async fn handle_request( - &self, - request: R::PeerReq, - _context: RequestContext, - ) -> Result { - let mut service = self.service.clone(); - poll_fn(|cx| service.poll_ready(cx)) - .await - .map_err(Into::into)?; - let resp = service.call(request).await.map_err(Into::into)?; - Ok(resp) - } - - fn handle_notification( - &self, - _notification: R::PeerNot, - _context: NotificationContext, - ) -> impl Future> + Send + '_ { - std::future::ready(Ok(())) - } - - fn get_info(&self) -> R::Info { - self.info.clone() - } -} diff --git a/codex-rs/vendor/rmcp/src/task_manager.rs b/codex-rs/vendor/rmcp/src/task_manager.rs deleted file mode 100644 index 774c542f86..0000000000 --- a/codex-rs/vendor/rmcp/src/task_manager.rs +++ /dev/null @@ -1,299 +0,0 @@ -use std::{any::Any, collections::HashMap, pin::Pin}; - -use futures::Future; -use tokio::{ - sync::mpsc, - time::{Duration, timeout}, -}; - -use crate::{ - RoleServer, - error::{ErrorData as McpError, RmcpError as Error}, - model::{CallToolResult, ClientRequest}, - service::RequestContext, -}; - -/// Boxed future that represents an asynchronous operation managed by the processor. -pub type OperationFuture = - Pin, Error>> + Send>>; - -/// Describes metadata associated with an enqueued task. -#[derive(Debug, Clone)] -pub struct OperationDescriptor { - pub operation_id: String, - pub name: String, - pub client_request: Option, - pub context: Option>, - pub ttl: Option, -} - -impl OperationDescriptor { - pub fn new(operation_id: impl Into, name: impl Into) -> Self { - Self { - operation_id: operation_id.into(), - name: name.into(), - client_request: None, - context: None, - ttl: None, - } - } - - pub fn with_client_request(mut self, request: ClientRequest) -> Self { - self.client_request = Some(request); - self - } - - pub fn with_context(mut self, context: RequestContext) -> Self { - self.context = Some(context); - self - } - - pub fn with_ttl(mut self, ttl: u64) -> Self { - self.ttl = Some(ttl); - self - } -} - -/// Operation message describing a unit of asynchronous work. -pub struct OperationMessage { - pub descriptor: OperationDescriptor, - pub future: OperationFuture, -} - -impl OperationMessage { - pub fn new(descriptor: OperationDescriptor, future: OperationFuture) -> Self { - Self { descriptor, future } - } -} - -/// Trait for operation result transport -pub trait OperationResultTransport: Send + Sync + 'static { - fn operation_id(&self) -> &String; - fn as_any(&self) -> &dyn std::any::Any; -} - -// ===== Operation Processor ===== -pub const DEFAULT_TASK_TIMEOUT_SECS: u64 = 300; // 5 minutes -/// Operation processor that coordinates extractors and handlers -pub struct OperationProcessor { - /// Currently running tasks keyed by id - running_tasks: HashMap, - /// Completed results waiting to be collected - completed_results: Vec, - task_result_receiver: mpsc::UnboundedReceiver, - task_result_sender: mpsc::UnboundedSender, -} - -struct RunningTask { - task_handle: tokio::task::JoinHandle<()>, - started_at: std::time::Instant, - timeout: Option, - descriptor: OperationDescriptor, -} - -pub struct TaskResult { - pub descriptor: OperationDescriptor, - pub result: Result, Error>, -} - -/// Helper to generate an ISO 8601 timestamp for task metadata. -pub fn current_timestamp() -> String { - chrono::Utc::now().to_rfc3339() -} - -/// Result transport for tool calls executed as tasks. -pub struct ToolCallTaskResult { - id: String, - pub result: Result, -} - -impl ToolCallTaskResult { - pub fn new(id: impl Into, result: Result) -> Self { - Self { - id: id.into(), - result, - } - } -} - -impl OperationResultTransport for ToolCallTaskResult { - fn operation_id(&self) -> &String { - &self.id - } - - fn as_any(&self) -> &dyn Any { - self - } -} - -impl Default for OperationProcessor { - fn default() -> Self { - Self::new() - } -} - -impl OperationProcessor { - pub fn new() -> Self { - let (task_result_sender, task_result_receiver) = mpsc::unbounded_channel(); - Self { - running_tasks: HashMap::new(), - completed_results: Vec::new(), - task_result_receiver, - task_result_sender, - } - } - - /// Submit an operation for asynchronous execution. - #[allow(clippy::result_large_err)] - pub fn submit_operation(&mut self, message: OperationMessage) -> Result<(), Error> { - if self - .running_tasks - .contains_key(&message.descriptor.operation_id) - { - return Err(Error::TaskError(format!( - "Operation with id {} is already running", - message.descriptor.operation_id - ))); - } - self.spawn_async_task(message); - Ok(()) - } - - fn spawn_async_task(&mut self, message: OperationMessage) { - let OperationMessage { descriptor, future } = message; - let task_id = descriptor.operation_id.clone(); - let timeout_secs = descriptor.ttl.or(Some(DEFAULT_TASK_TIMEOUT_SECS)); - let sender = self.task_result_sender.clone(); - let descriptor_for_result = descriptor.clone(); - - let timed_future = async move { - if let Some(secs) = timeout_secs { - match timeout(Duration::from_secs(secs), future).await { - Ok(result) => result, - Err(_) => Err(Error::TaskError("Operation timed out".to_string())), - } - } else { - future.await - } - }; - - let handle = tokio::spawn(async move { - let result = timed_future.await; - let task_result = TaskResult { - descriptor: descriptor_for_result, - result, - }; - let _ = sender.send(task_result); - }); - let running_task = RunningTask { - task_handle: handle, - started_at: std::time::Instant::now(), - timeout: timeout_secs, - descriptor, - }; - self.running_tasks.insert(task_id, running_task); - } - - /// Collect completed results from running tasks and remove them from the running tasks map. - fn collect_completed_results(&mut self) { - while let Ok(result) = self.task_result_receiver.try_recv() { - self.running_tasks.remove(&result.descriptor.operation_id); - self.completed_results.push(result); - } - } - - /// Check for tasks that have exceeded their timeout and handle them appropriately. - pub fn check_timeouts(&mut self) { - self.collect_completed_results(); - let now = std::time::Instant::now(); - let mut timed_out_tasks = Vec::new(); - - for (task_id, task) in &self.running_tasks { - if let Some(timeout_duration) = task.timeout { - if now.duration_since(task.started_at).as_secs() > timeout_duration { - task.task_handle.abort(); - timed_out_tasks.push(task_id.clone()); - } - } - } - - for task_id in timed_out_tasks { - if let Some(task) = self.running_tasks.remove(&task_id) { - let timeout_result = TaskResult { - descriptor: task.descriptor, - result: Err(Error::TaskError("Operation timed out".to_string())), - }; - self.completed_results.push(timeout_result); - } - } - } - - /// Get the number of running tasks. - pub fn running_task_count(&mut self) -> usize { - self.collect_completed_results(); - self.running_tasks.len() - } - - /// Cancel all running tasks. - pub fn cancel_all_tasks(&mut self) { - for (_, task) in self.running_tasks.drain() { - task.task_handle.abort(); - } - while self.task_result_receiver.try_recv().is_ok() {} - self.completed_results.clear(); - } - - /// List running task ids. - pub fn list_running(&mut self) -> Vec { - self.collect_completed_results(); - self.running_tasks.keys().cloned().collect() - } - - /// Returns a snapshot of completed task results. - pub fn peek_completed(&mut self) -> &[TaskResult] { - self.collect_completed_results(); - &self.completed_results - } - - /// Fetch the metadata for a running or recently completed task. - pub fn task_descriptor(&self, task_id: &str) -> Option<&OperationDescriptor> { - if let Some(task) = self.running_tasks.get(task_id) { - return Some(&task.descriptor); - } - self.completed_results - .iter() - .rev() - .find(|result| result.descriptor.operation_id == task_id) - .map(|result| &result.descriptor) - } - - /// Attempt to cancel a running task. - pub fn cancel_task(&mut self, task_id: &str) -> bool { - self.collect_completed_results(); - if let Some(task) = self.running_tasks.remove(task_id) { - task.task_handle.abort(); - // Insert a cancelled result so callers can observe the terminal state. - let cancel_result = TaskResult { - descriptor: task.descriptor, - result: Err(Error::TaskError("Operation cancelled".to_string())), - }; - self.completed_results.push(cancel_result); - return true; - } - false - } - - /// Retrieve a completed task result if available. - pub fn take_completed_result(&mut self, task_id: &str) -> Option { - self.collect_completed_results(); - if let Some(position) = self - .completed_results - .iter() - .position(|result| result.descriptor.operation_id == task_id) - { - Some(self.completed_results.remove(position)) - } else { - None - } - } -} diff --git a/codex-rs/vendor/rmcp/src/transport.rs b/codex-rs/vendor/rmcp/src/transport.rs deleted file mode 100644 index 5b9318d961..0000000000 --- a/codex-rs/vendor/rmcp/src/transport.rs +++ /dev/null @@ -1,277 +0,0 @@ -//! # Transport -//! The transport type must implemented [`Transport`] trait, which allow it send message concurrently and receive message sequentially. -//! -//! ## Standard Transport Types -//! There are 2 pairs of standard transport types: -//! -//! | transport | client | server | -//! |:-: |:-: |:-: | -//! | std IO | [`child_process::TokioChildProcess`] | [`io::stdio`] | -//! | streamable http | [`streamable_http_client::StreamableHttpClientTransport`] | [`streamable_http_server::StreamableHttpService`] | -//! -//!## Helper Transport Types -//! Thers are several helper transport types that can help you to create transport quickly. -//! -//! ### [Worker Transport](`worker::WorkerTransport`) -//! Which allows you to run a worker and process messages in another tokio task. -//! -//! ### [Async Read/Write Transport](`async_rw::AsyncRwTransport`) -//! You need to enable `transport-async-rw` feature to use this transport. -//! -//! This transport is used to create a transport from a byte stream which implemented [`tokio::io::AsyncRead`] and [`tokio::io::AsyncWrite`]. -//! -//! This could be very helpful when you want to create a transport from a byte stream, such as a file or a tcp connection. -//! -//! ### [Sink/Stream Transport](`sink_stream::SinkStreamTransport`) -//! This transport is used to create a transport from a sink and a stream. -//! -//! This could be very helpful when you want to create a transport from a duplex object stream, such as a websocket connection. -//! -//! ## [IntoTransport](`IntoTransport`) trait -//! [`IntoTransport`] is a helper trait that implicitly convert a type into a transport type. -//! -//! ### These types is automatically implemented [`IntoTransport`] trait -//! 1. A type that already implement both [`futures::Sink`] and [`futures::Stream`] trait, or a tuple `(Tx, Rx)` where `Tx` is [`futures::Sink`] and `Rx` is [`futures::Stream`]. -//! 2. A type that implement both [`tokio::io::AsyncRead`] and [`tokio::io::AsyncWrite`] trait. or a tuple `(R, W)` where `R` is [`tokio::io::AsyncRead`] and `W` is [`tokio::io::AsyncWrite`]. -//! 3. A type that implement [Worker](`worker::Worker`) trait. -//! 4. A type that implement [`Transport`] trait. -//! -//! ## Examples -//! -//! ```rust -//! # use rmcp::{ -//! # ServiceExt, serve_server, -//! # }; -//! #[cfg(feature = "client")] -//! #[cfg_attr(docsrs, doc(cfg(feature = "client")))] -//! # use rmcp::serve_client; -//! -//! // create transport from tcp stream -//! #[cfg(feature = "client")] -//! #[cfg_attr(docsrs, doc(cfg(feature = "client")))] -//! async fn client() -> Result<(), Box> { -//! let stream = tokio::net::TcpSocket::new_v4()? -//! .connect("127.0.0.1:8001".parse()?) -//! .await?; -//! let client = ().serve(stream).await?; -//! let tools = client.peer().list_tools(Default::default()).await?; -//! println!("{:?}", tools); -//! Ok(()) -//! } -//! -//! // create transport from std io -//! #[cfg(feature = "client")] -//! #[cfg_attr(docsrs, doc(cfg(feature = "client")))] -//! async fn io() -> Result<(), Box> { -//! let client = ().serve((tokio::io::stdin(), tokio::io::stdout())).await?; -//! let tools = client.peer().list_tools(Default::default()).await?; -//! println!("{:?}", tools); -//! Ok(()) -//! } -//! ``` - -use std::{borrow::Cow, sync::Arc}; - -use crate::service::{RxJsonRpcMessage, ServiceRole, TxJsonRpcMessage}; - -pub mod sink_stream; - -#[cfg(feature = "transport-async-rw")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-async-rw")))] -pub mod async_rw; - -#[cfg(feature = "transport-worker")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-worker")))] -pub mod worker; -#[cfg(feature = "transport-worker")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-worker")))] -pub use worker::WorkerTransport; - -#[cfg(feature = "transport-child-process")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-child-process")))] -pub mod child_process; -#[cfg(feature = "transport-child-process")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-child-process")))] -pub use child_process::{ConfigureCommandExt, TokioChildProcess}; - -#[cfg(feature = "transport-io")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-io")))] -pub mod io; -#[cfg(feature = "transport-io")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-io")))] -pub use io::stdio; - -#[cfg(feature = "auth")] -#[cfg_attr(docsrs, doc(cfg(feature = "auth")))] -pub mod auth; -#[cfg(feature = "auth")] -#[cfg_attr(docsrs, doc(cfg(feature = "auth")))] -pub use auth::{ - AuthError, AuthorizationManager, AuthorizationSession, AuthorizedHttpClient, CredentialStore, - InMemoryCredentialStore, InMemoryStateStore, StateStore, StoredAuthorizationState, - StoredCredentials, -}; - -// #[cfg(feature = "transport-ws")] -// #[cfg_attr(docsrs, doc(cfg(feature = "transport-ws")))] -// pub mod ws; -#[cfg(feature = "transport-streamable-http-server-session")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-server-session")))] -pub mod streamable_http_server; -#[cfg(feature = "transport-streamable-http-server")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-server")))] -pub use streamable_http_server::tower::{StreamableHttpServerConfig, StreamableHttpService}; - -#[cfg(feature = "transport-streamable-http-client")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-client")))] -pub mod streamable_http_client; -#[cfg(feature = "transport-streamable-http-client")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-client")))] -pub use streamable_http_client::StreamableHttpClientTransport; - -/// Common use codes -pub mod common; - -pub trait Transport: Send -where - R: ServiceRole, -{ - type Error: std::error::Error + Send + Sync + 'static; - fn name() -> Cow<'static, str> { - std::any::type_name::().into() - } - /// Send a message to the transport - /// - /// Notice that the future returned by this function should be `Send` and `'static`. - /// It's because the sending message could be executed concurrently. - /// - fn send( - &mut self, - item: TxJsonRpcMessage, - ) -> impl Future> + Send + 'static; - - /// Receive a message from the transport, this operation is sequential. - fn receive(&mut self) -> impl Future>> + Send; - - /// Close the transport - fn close(&mut self) -> impl Future> + Send; -} - -pub trait IntoTransport: Send + 'static -where - R: ServiceRole, - E: std::error::Error + Send + 'static, -{ - fn into_transport(self) -> impl Transport + 'static; -} - -pub enum TransportAdapterIdentity {} -impl IntoTransport for T -where - T: Transport + Send + 'static, - R: ServiceRole, - E: std::error::Error + Send + Sync + 'static, -{ - fn into_transport(self) -> impl Transport + 'static { - self - } -} - -/// A transport that can send a single message and then close itself -pub struct OneshotTransport -where - R: ServiceRole, -{ - message: Option>, - sender: tokio::sync::mpsc::Sender>, - termination: Arc, -} - -impl OneshotTransport -where - R: ServiceRole, -{ - pub fn new( - message: RxJsonRpcMessage, - ) -> (Self, tokio::sync::mpsc::Receiver>) { - let (sender, receiver) = tokio::sync::mpsc::channel(16); - ( - Self { - message: Some(message), - sender, - termination: Arc::new(tokio::sync::Semaphore::new(0)), - }, - receiver, - ) - } -} - -impl Transport for OneshotTransport -where - R: ServiceRole, -{ - type Error = tokio::sync::mpsc::error::SendError>; - - fn send( - &mut self, - item: TxJsonRpcMessage, - ) -> impl Future> + Send + 'static { - let sender = self.sender.clone(); - let terminate = matches!(item, TxJsonRpcMessage::::Response(_)) - || matches!(item, TxJsonRpcMessage::::Error(_)); - let termination = self.termination.clone(); - async move { - sender.send(item).await?; - if terminate { - termination.add_permits(1); - } - Ok(()) - } - } - - async fn receive(&mut self) -> Option> { - if let Some(msg) = self.message.take() { - return Some(msg); - } - let _ = self.termination.acquire().await; - None - } - - fn close(&mut self) -> impl Future> + Send { - self.message.take(); - std::future::ready(Ok(())) - } -} - -#[derive(Debug, thiserror::Error)] -#[error("Transport [{transport_name}] error: {error}")] -pub struct DynamicTransportError { - pub transport_name: Cow<'static, str>, - pub transport_type_id: std::any::TypeId, - #[source] - pub error: Box, -} - -impl DynamicTransportError { - pub fn new + 'static, R: ServiceRole>(e: T::Error) -> Self { - Self { - transport_name: T::name(), - transport_type_id: std::any::TypeId::of::(), - error: Box::new(e), - } - } - pub fn downcast + 'static, R: ServiceRole>(self) -> Result { - if !self.is::() { - Err(self) - } else { - Ok(self - .error - .downcast::() - .map(|e| *e) - .expect("type is checked")) - } - } - pub fn is + 'static, R: ServiceRole>(&self) -> bool { - self.error.is::() && self.transport_type_id == std::any::TypeId::of::() - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/async_rw.rs b/codex-rs/vendor/rmcp/src/transport/async_rw.rs deleted file mode 100644 index acd1b4f65b..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/async_rw.rs +++ /dev/null @@ -1,557 +0,0 @@ -use std::{marker::PhantomData, sync::Arc}; - -// use crate::schema::*; -use futures::{SinkExt, StreamExt}; -use serde::{Serialize, de::DeserializeOwned}; -use thiserror::Error; -use tokio::{ - io::{AsyncRead, AsyncWrite}, - sync::Mutex, -}; -use tokio_util::{ - bytes::{Buf, BufMut, BytesMut}, - codec::{Decoder, Encoder, FramedRead, FramedWrite}, -}; - -use super::{IntoTransport, Transport}; -use crate::service::{RxJsonRpcMessage, ServiceRole, TxJsonRpcMessage}; - -pub enum TransportAdapterAsyncRW {} - -impl IntoTransport for (R, W) -where - Role: ServiceRole, - R: AsyncRead + Send + 'static + Unpin, - W: AsyncWrite + Send + 'static + Unpin, -{ - fn into_transport(self) -> impl Transport + 'static { - AsyncRwTransport::new(self.0, self.1) - } -} - -pub enum TransportAdapterAsyncCombinedRW {} -impl IntoTransport for S -where - Role: ServiceRole, - S: AsyncRead + AsyncWrite + Send + 'static, -{ - fn into_transport(self) -> impl Transport + 'static { - IntoTransport::::into_transport( - tokio::io::split(self), - ) - } -} - -pub type TransportWriter = FramedWrite>>; - -pub struct AsyncRwTransport { - read: FramedRead>>, - write: Arc>>>, -} - -impl AsyncRwTransport -where - R: Send + AsyncRead + Unpin, - W: Send + AsyncWrite + Unpin + 'static, -{ - pub fn new(read: R, write: W) -> Self { - let read = FramedRead::new( - read, - JsonRpcMessageCodec::>::default(), - ); - let write = Arc::new(Mutex::new(Some(FramedWrite::new( - write, - JsonRpcMessageCodec::>::default(), - )))); - Self { read, write } - } -} - -#[cfg(feature = "client")] -#[cfg_attr(docsrs, doc(cfg(feature = "client")))] -impl AsyncRwTransport -where - R: Send + AsyncRead + Unpin, - W: Send + AsyncWrite + Unpin + 'static, -{ - pub fn new_client(read: R, write: W) -> Self { - Self::new(read, write) - } -} - -#[cfg(feature = "server")] -#[cfg_attr(docsrs, doc(cfg(feature = "server")))] -impl AsyncRwTransport -where - R: Send + AsyncRead + Unpin, - W: Send + AsyncWrite + Unpin + 'static, -{ - pub fn new_server(read: R, write: W) -> Self { - Self::new(read, write) - } -} - -impl Transport for AsyncRwTransport -where - R: Send + AsyncRead + Unpin, - W: Send + AsyncWrite + Unpin + 'static, -{ - type Error = std::io::Error; - - fn send( - &mut self, - item: TxJsonRpcMessage, - ) -> impl Future> + Send + 'static { - let lock = self.write.clone(); - async move { - let mut write = lock.lock().await; - if let Some(ref mut write) = *write { - write.send(item).await.map_err(Into::into) - } else { - Err(std::io::Error::new( - std::io::ErrorKind::NotConnected, - "Transport is closed", - )) - } - } - } - - fn receive(&mut self) -> impl Future>> { - let next = self.read.next(); - async { - next.await.and_then(|e| { - e.inspect_err(|e| { - tracing::error!("Error reading from stream: {}", e); - }) - .ok() - }) - } - } - - async fn close(&mut self) -> Result<(), Self::Error> { - let mut write = self.write.lock().await; - drop(write.take()); - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub struct JsonRpcMessageCodec { - _marker: PhantomData T>, - next_index: usize, - max_length: usize, - is_discarding: bool, -} - -impl Default for JsonRpcMessageCodec { - fn default() -> Self { - Self::new() - } -} - -impl JsonRpcMessageCodec { - pub fn new() -> Self { - Self { - _marker: PhantomData, - next_index: 0, - max_length: usize::MAX, - is_discarding: false, - } - } - - pub fn new_with_max_length(max_length: usize) -> Self { - Self { - max_length, - ..Self::new() - } - } - - pub fn max_length(&self) -> usize { - self.max_length - } -} - -fn without_carriage_return(s: &[u8]) -> &[u8] { - if let Some(&b'\r') = s.last() { - &s[..s.len() - 1] - } else { - s - } -} - -/// Check if a method is a standard MCP method (request, response, or notification). -/// This includes both requests and notifications defined in the MCP specification. -/// -/// Based on MCP specification 2025-06-18: https://modelcontextprotocol.io/specification/2025-06-18 -fn is_standard_method(method: &str) -> bool { - matches!( - method, - "initialize" - | "ping" - | "prompts/get" - | "prompts/list" - | "resources/list" - | "resources/read" - | "resources/subscribe" - | "resources/unsubscribe" - | "resources/templates/list" - | "tools/call" - | "tools/list" - | "completion/complete" - | "logging/setLevel" - | "roots/list" - | "sampling/createMessage" - ) || is_standard_notification(method) -} - -fn is_standard_notification(method: &str) -> bool { - matches!( - method, - "notifications/cancelled" - | "notifications/initialized" - | "notifications/message" - | "notifications/progress" - | "notifications/prompts/list_changed" - | "notifications/resources/list_changed" - | "notifications/resources/updated" - | "notifications/roots/list_changed" - | "notifications/tools/list_changed" - ) -} - -/// Determines if a notification should be ignored for compatibility. -fn should_ignore_notification(json_value: &serde_json::Value, method: &str) -> bool { - let is_notification = json_value.get("id").is_none(); - - // Ignore non-MCP notifications (like LSP messages) for compatibility - if is_notification && !is_standard_method(method) { - tracing::trace!( - "Ignoring non-MCP notification '{}' for compatibility", - method - ); - return true; - } - - // Ignore non-standard MCP notifications - matches!( - ( - method.starts_with("notifications/"), - is_standard_notification(method) - ), - (true, false) - ) -} - -/// Try to parse a message with compatibility handling for non-standard notifications -fn try_parse_with_compatibility( - line: &[u8], - context: &str, -) -> Result, JsonRpcMessageCodecError> { - if let Ok(line_str) = std::str::from_utf8(line) { - match serde_json::from_slice(line) { - Ok(item) => Ok(Some(item)), - Err(e) => { - // Check if this is a notification that should be ignored for compatibility - if let Ok(json_value) = serde_json::from_str::(line_str) { - if let Some(method) = - json_value.get("method").and_then(serde_json::Value::as_str) - { - if should_ignore_notification(&json_value, method) { - return Ok(None); - } - } - } - - tracing::debug!( - "Failed to parse message {}: {} | Error: {}", - context, - line_str, - e - ); - Err(JsonRpcMessageCodecError::Serde(e)) - } - } - } else { - serde_json::from_slice(line) - .map(Some) - .map_err(JsonRpcMessageCodecError::Serde) - } -} - -#[derive(Debug, Error)] -pub enum JsonRpcMessageCodecError { - #[error("max line length exceeded")] - MaxLineLengthExceeded, - #[error("serde error {0}")] - Serde(#[from] serde_json::Error), - #[error("io error {0}")] - Io(#[from] std::io::Error), -} - -impl From for std::io::Error { - fn from(value: JsonRpcMessageCodecError) -> Self { - match value { - JsonRpcMessageCodecError::MaxLineLengthExceeded => { - std::io::Error::new(std::io::ErrorKind::InvalidData, value) - } - JsonRpcMessageCodecError::Serde(e) => e.into(), - JsonRpcMessageCodecError::Io(e) => e, - } - } -} - -impl Decoder for JsonRpcMessageCodec { - type Item = T; - - type Error = JsonRpcMessageCodecError; - - fn decode( - &mut self, - buf: &mut BytesMut, - ) -> Result, JsonRpcMessageCodecError> { - loop { - // Determine how far into the buffer we'll search for a newline. If - // there's no max_length set, we'll read to the end of the buffer. - let read_to = std::cmp::min(self.max_length.saturating_add(1), buf.len()); - - let newline_offset = buf[self.next_index..read_to] - .iter() - .position(|b| *b == b'\n'); - - match (self.is_discarding, newline_offset) { - (true, Some(offset)) => { - // If we found a newline, discard up to that offset and - // then stop discarding. On the next iteration, we'll try - // to read a line normally. - buf.advance(offset + self.next_index + 1); - self.is_discarding = false; - self.next_index = 0; - } - (true, None) => { - // Otherwise, we didn't find a newline, so we'll discard - // everything we read. On the next iteration, we'll continue - // discarding up to max_len bytes unless we find a newline. - buf.advance(read_to); - self.next_index = 0; - if buf.is_empty() { - return Ok(None); - } - } - (false, Some(offset)) => { - // Found a line! - let newline_index = offset + self.next_index; - self.next_index = 0; - let line = buf.split_to(newline_index + 1); - let line = &line[..line.len() - 1]; - let line = without_carriage_return(line); - - // Use compatibility handling function - let item = match try_parse_with_compatibility(line, "decode")? { - Some(item) => item, - None => return Ok(None), // Skip non-standard message - }; - return Ok(Some(item)); - } - (false, None) if buf.len() > self.max_length => { - // Reached the maximum length without finding a - // newline, return an error and start discarding on the - // next call. - self.is_discarding = true; - return Err(JsonRpcMessageCodecError::MaxLineLengthExceeded); - } - (false, None) => { - // We didn't find a line or reach the length limit, so the next - // call will resume searching at the current offset. - self.next_index = read_to; - return Ok(None); - } - } - } - } - - fn decode_eof(&mut self, buf: &mut BytesMut) -> Result, JsonRpcMessageCodecError> { - Ok(match self.decode(buf)? { - Some(frame) => Some(frame), - None => { - self.next_index = 0; - // No terminating newline - return remaining data, if any - if buf.is_empty() || buf == &b"\r"[..] { - None - } else { - let line = buf.split_to(buf.len()); - let line = without_carriage_return(&line); - - // Use compatibility handling function - let item = match try_parse_with_compatibility(line, "decode_eof")? { - Some(item) => item, - None => return Ok(None), // Skip non-standard message - }; - Some(item) - } - } - }) - } -} - -impl Encoder for JsonRpcMessageCodec { - type Error = JsonRpcMessageCodecError; - - fn encode(&mut self, item: T, buf: &mut BytesMut) -> Result<(), JsonRpcMessageCodecError> { - serde_json::to_writer(buf.writer(), &item)?; - buf.put_u8(b'\n'); - Ok(()) - } -} - -#[cfg(test)] -mod test { - use futures::{Sink, Stream}; - - use super::*; - fn from_async_read(reader: R) -> impl Stream { - FramedRead::new(reader, JsonRpcMessageCodec::::default()).filter_map(|result| { - if let Err(e) = &result { - tracing::error!("Error reading from stream: {}", e); - } - futures::future::ready(result.ok()) - }) - } - - fn from_async_write( - writer: W, - ) -> impl Sink { - FramedWrite::new(writer, JsonRpcMessageCodec::::default()).sink_map_err(Into::into) - } - #[tokio::test] - async fn test_decode() { - use futures::StreamExt; - use tokio::io::BufReader; - - let data = r#"{"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":1} - {"jsonrpc":"2.0","method":"subtract","params":[23,42],"id":2} - {"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":3} - {"jsonrpc":"2.0","method":"subtract","params":[23,42],"id":4} - {"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":5} - {"jsonrpc":"2.0","method":"subtract","params":[23,42],"id":6} - {"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":7} - {"jsonrpc":"2.0","method":"subtract","params":[23,42],"id":8} - {"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":9} - {"jsonrpc":"2.0","method":"subtract","params":[23,42],"id":10} - - "#; - - let mut cursor = BufReader::new(data.as_bytes()); - let mut stream = from_async_read::(&mut cursor); - - for i in 1..=10 { - let item = stream.next().await.unwrap(); - assert_eq!( - item, - serde_json::json!({ - "jsonrpc": "2.0", - "method": "subtract", - "params": if i % 2 != 0 { [42, 23] } else { [23, 42] }, - "id": i, - }) - ); - } - } - - #[tokio::test] - async fn test_encode() { - let test_messages = vec![ - serde_json::json!({ - "jsonrpc": "2.0", - "method": "subtract", - "params": [42, 23], - "id": 1, - }), - serde_json::json!({ - "jsonrpc": "2.0", - "method": "subtract", - "params": [23, 42], - "id": 2, - }), - ]; - - // Create a buffer to write to - let mut buffer = Vec::new(); - let mut writer = from_async_write(&mut buffer); - - // Write the test messages - for message in test_messages.iter() { - writer.send(message.clone()).await.unwrap(); - } - writer.close().await.unwrap(); - drop(writer); - // Parse the buffer back into lines and check each one - let output = String::from_utf8_lossy(&buffer); - let mut lines = output.lines(); - - for expected_message in test_messages { - let line = lines.next().unwrap(); - let parsed_message: serde_json::Value = serde_json::from_str(line).unwrap(); - assert_eq!(parsed_message, expected_message); - } - - // Make sure there are no extra lines - assert!(lines.next().is_none()); - } - - #[test] - fn test_standard_notification_check() { - // Test that all standard notifications are recognized - assert!(is_standard_notification("notifications/cancelled")); - assert!(is_standard_notification("notifications/initialized")); - assert!(is_standard_notification("notifications/progress")); - assert!(is_standard_notification( - "notifications/resources/list_changed" - )); - assert!(is_standard_notification("notifications/resources/updated")); - assert!(is_standard_notification( - "notifications/prompts/list_changed" - )); - assert!(is_standard_notification("notifications/tools/list_changed")); - assert!(is_standard_notification("notifications/message")); - assert!(is_standard_notification("notifications/roots/list_changed")); - - // Test that non-standard notifications are not recognized - assert!(!is_standard_notification("notifications/stderr")); - assert!(!is_standard_notification("notifications/custom")); - assert!(!is_standard_notification("notifications/debug")); - assert!(!is_standard_notification("some/other/method")); - } - - #[test] - fn test_compatibility_function() { - // Test the compatibility function directly - let stderr_message = - r#"{"method":"notifications/stderr","params":{"content":"stderr message"}}"#; - let custom_message = r#"{"method":"notifications/custom","params":{"data":"custom"}}"#; - let standard_message = - r#"{"method":"notifications/message","params":{"level":"info","data":"standard"}}"#; - let progress_message = r#"{"method":"notifications/progress","params":{"progressToken":"token","progress":50}}"#; - - // Test with valid JSON - all should parse successfully - let result1 = - try_parse_with_compatibility::(stderr_message.as_bytes(), "test"); - let result2 = - try_parse_with_compatibility::(custom_message.as_bytes(), "test"); - let result3 = - try_parse_with_compatibility::(standard_message.as_bytes(), "test"); - let result4 = - try_parse_with_compatibility::(progress_message.as_bytes(), "test"); - - // All should parse successfully since they're valid JSON - assert!(result1.is_ok()); - assert!(result2.is_ok()); - assert!(result3.is_ok()); - assert!(result4.is_ok()); - - // Standard notifications should return Some(value) - assert!(result3.unwrap().is_some()); - assert!(result4.unwrap().is_some()); - - println!("Standard notifications are preserved, non-standard are handled gracefully"); - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/auth.rs b/codex-rs/vendor/rmcp/src/transport/auth.rs deleted file mode 100644 index 53248c929b..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/auth.rs +++ /dev/null @@ -1,1974 +0,0 @@ -use std::{collections::HashMap, sync::Arc, time::Duration}; - -use async_trait::async_trait; -use oauth2::{ - AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, - EmptyExtraTokenFields, HttpRequest, HttpResponse, - PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, RefreshToken, RequestTokenError, Scope, - StandardTokenResponse, TokenResponse, TokenUrl, - basic::{BasicClient, BasicTokenType}, -}; -use reqwest::{ - Client as HttpClient, IntoUrl, StatusCode, Url, - header::{AUTHORIZATION, WWW_AUTHENTICATE}, -}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use tokio::sync::{Mutex, RwLock}; -use tracing::{debug, warn}; - -const DEFAULT_EXCHANGE_URL: &str = "http://localhost"; - -/// Stored credentials for OAuth2 authorization -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StoredCredentials { - pub client_id: String, - pub token_response: Option, -} - -/// Trait for storing and retrieving OAuth2 credentials -/// -/// Implementations of this trait can provide custom storage backends -/// for OAuth2 credentials, such as file-based storage, keychain integration, -/// or database storage. -#[async_trait] -pub trait CredentialStore: Send + Sync { - async fn load(&self) -> Result, AuthError>; - - async fn save(&self, credentials: StoredCredentials) -> Result<(), AuthError>; - - async fn clear(&self) -> Result<(), AuthError>; -} - -/// In-memory credential store (default implementation) -/// -/// This store keeps credentials in memory only and does not persist them -/// between application restarts. This is the default behavior when no -/// custom credential store is provided. -#[derive(Debug, Default, Clone)] -pub struct InMemoryCredentialStore { - credentials: Arc>>, -} - -impl InMemoryCredentialStore { - pub fn new() -> Self { - Self { - credentials: Arc::new(RwLock::new(None)), - } - } -} - -#[async_trait::async_trait] -impl CredentialStore for InMemoryCredentialStore { - async fn load(&self) -> Result, AuthError> { - Ok(self.credentials.read().await.clone()) - } - - async fn save(&self, credentials: StoredCredentials) -> Result<(), AuthError> { - *self.credentials.write().await = Some(credentials); - Ok(()) - } - - async fn clear(&self) -> Result<(), AuthError> { - *self.credentials.write().await = None; - Ok(()) - } -} - -/// Stored authorization state for OAuth2 PKCE flow -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StoredAuthorizationState { - pub pkce_verifier: String, - pub csrf_token: String, - pub created_at: u64, -} - -impl StoredAuthorizationState { - pub fn new(pkce_verifier: &PkceCodeVerifier, csrf_token: &CsrfToken) -> Self { - Self { - pkce_verifier: pkce_verifier.secret().to_string(), - csrf_token: csrf_token.secret().to_string(), - created_at: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0), - } - } - - pub fn into_pkce_verifier(self) -> PkceCodeVerifier { - PkceCodeVerifier::new(self.pkce_verifier) - } -} - -/// Trait for storing and retrieving OAuth2 authorization state -/// -/// Implementations of this trait can provide custom storage backends -/// for OAuth2 PKCE flow state, such as Redis or database storage. -/// -/// Implementors are responsible for expiring stale states (e.g., abandoned -/// authorization flows). Use [`StoredAuthorizationState::created_at`] for -/// TTL-based expiration. -#[async_trait] -pub trait StateStore: Send + Sync { - async fn save( - &self, - csrf_token: &str, - state: StoredAuthorizationState, - ) -> Result<(), AuthError>; - - async fn load(&self, csrf_token: &str) -> Result, AuthError>; - - async fn delete(&self, csrf_token: &str) -> Result<(), AuthError>; -} - -/// HTTP capability used by OAuth discovery, registration, token exchange, and -/// refresh flows. -/// -/// Implementations should execute a single HTTP request and return the full -/// response body without applying higher-level OAuth or MCP behavior on top. -#[async_trait] -pub trait OAuthHttpClient: Send + Sync { - async fn execute(&self, request: HttpRequest) -> Result; -} - -/// In-memory state store (default implementation) -/// -/// This store keeps authorization state in memory only and does not persist -/// between application restarts or across multiple server instances. -#[derive(Debug, Default, Clone)] -pub struct InMemoryStateStore { - states: Arc>>, -} - -impl InMemoryStateStore { - pub fn new() -> Self { - Self { - states: Arc::new(RwLock::new(HashMap::new())), - } - } -} - -#[async_trait] -impl StateStore for InMemoryStateStore { - async fn save( - &self, - csrf_token: &str, - state: StoredAuthorizationState, - ) -> Result<(), AuthError> { - self.states - .write() - .await - .insert(csrf_token.to_string(), state); - Ok(()) - } - - async fn load(&self, csrf_token: &str) -> Result, AuthError> { - Ok(self.states.read().await.get(csrf_token).cloned()) - } - - async fn delete(&self, csrf_token: &str) -> Result<(), AuthError> { - self.states.write().await.remove(csrf_token); - Ok(()) - } -} - -/// HTTP client with OAuth 2.0 authorization -#[derive(Clone)] -pub struct AuthClient { - pub http_client: C, - pub auth_manager: Arc>, -} - -impl std::fmt::Debug for AuthClient { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("AuthorizedClient") - .field("http_client", &self.http_client) - .field("auth_manager", &"...") - .finish() - } -} - -impl AuthClient { - /// Create a new authorized HTTP client - pub fn new(http_client: C, auth_manager: AuthorizationManager) -> Self { - Self { - http_client, - auth_manager: Arc::new(Mutex::new(auth_manager)), - } - } -} - -impl AuthClient { - pub fn get_access_token(&self) -> impl Future> + Send { - let auth_manager = self.auth_manager.clone(); - async move { auth_manager.lock().await.get_access_token().await } - } -} - -/// Auth error -#[derive(Debug, Error)] -pub enum AuthError { - #[error("OAuth authorization required")] - AuthorizationRequired, - - #[error("OAuth authorization failed: {0}")] - AuthorizationFailed(String), - - #[error("OAuth token exchange failed: {0}")] - TokenExchangeFailed(String), - - #[error("OAuth token refresh failed: {0}")] - TokenRefreshFailed(String), - - #[error("HTTP error: {0}")] - HttpError(#[from] reqwest::Error), - - #[error("OAuth error: {0}")] - OAuthError(String), - - #[error("Metadata error: {0}")] - MetadataError(String), - - #[error("URL parse error: {0}")] - UrlError(#[from] url::ParseError), - - #[error("No authorization support detected")] - NoAuthorizationSupport, - - #[error("Internal error: {0}")] - InternalError(String), - - #[error("Invalid token type: {0}")] - InvalidTokenType(String), - - #[error("Token expired")] - TokenExpired, - - #[error("Invalid scope: {0}")] - InvalidScope(String), - - #[error("Registration failed: {0}")] - RegistrationFailed(String), -} - -/// oauth2 metadata -#[derive(Debug, Clone, Deserialize, Serialize, Default)] -pub struct AuthorizationMetadata { - pub authorization_endpoint: String, - pub token_endpoint: String, - pub registration_endpoint: Option, - pub issuer: Option, - pub jwks_uri: Option, - pub scopes_supported: Option>, - pub response_types_supported: Option>, - // allow additional fields - #[serde(flatten)] - pub additional_fields: HashMap, -} - -#[derive(Debug, Clone, Deserialize)] -struct ResourceServerMetadata { - authorization_server: Option, - authorization_servers: Option>, -} - -/// oauth2 client config -#[derive(Debug, Clone)] -pub struct OAuthClientConfig { - pub client_id: String, - pub client_secret: Option, - pub scopes: Vec, - pub redirect_uri: String, -} - -// add type aliases for oauth2 types -type OAuthErrorResponse = oauth2::StandardErrorResponse; -pub type OAuthTokenResponse = StandardTokenResponse; -type OAuthTokenIntrospection = - oauth2::StandardTokenIntrospectionResponse; -type OAuthRevocableToken = oauth2::StandardRevocableToken; -type OAuthRevocationError = oauth2::StandardErrorResponse; -type OAuthClient = oauth2::Client< - OAuthErrorResponse, - OAuthTokenResponse, - OAuthTokenIntrospection, - OAuthRevocableToken, - OAuthRevocationError, - oauth2::EndpointSet, - oauth2::EndpointNotSet, - oauth2::EndpointNotSet, - oauth2::EndpointNotSet, - oauth2::EndpointSet, ->; -type Credentials = (String, Option); - -#[derive(Clone)] -enum AuthorizationHttpClient { - Reqwest(HttpClient), - Custom(Arc), -} - -/// oauth2 auth manager -pub struct AuthorizationManager { - http_client: AuthorizationHttpClient, - metadata: Option, - oauth_client: Option, - credential_store: Arc, - state_store: Arc, - base_url: Url, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ClientRegistrationRequest { - pub client_name: String, - pub redirect_uris: Vec, - pub grant_types: Vec, - pub token_endpoint_auth_method: String, - pub response_types: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ClientRegistrationResponse { - pub client_id: String, - pub client_secret: Option, - pub client_name: Option, - pub redirect_uris: Vec, - // allow additional fields - #[serde(flatten)] - pub additional_fields: HashMap, -} - -impl AuthorizationHttpClient { - async fn execute(&self, request: HttpRequest) -> Result { - match self { - AuthorizationHttpClient::Reqwest(client) => execute_reqwest_request(client, request).await, - AuthorizationHttpClient::Custom(client) => client.execute(request).await, - } - } -} - -async fn execute_reqwest_request( - client: &HttpClient, - request: HttpRequest, -) -> Result { - let (parts, body) = request.into_parts(); - let url = parts - .uri - .to_string() - .parse::() - .map_err(AuthError::UrlError)?; - let mut reqwest_request = client.request(parts.method, url).body(body); - for (name, value) in &parts.headers { - reqwest_request = reqwest_request.header(name, value); - } - - let response = reqwest_request.send().await.map_err(AuthError::HttpError)?; - let status = response.status(); - let headers = response.headers().clone(); - let body = response.bytes().await.map_err(AuthError::HttpError)?.to_vec(); - - let mut response = HttpResponse::new(body); - *response.status_mut() = status; - for (name, value) in &headers { - response.headers_mut().insert(name, value.clone()); - } - Ok(response) -} - -/// SEP-991: URL-based Client IDs -/// Validate that the client_id is a valid URL with https scheme and non-root pathname -fn is_https_url(value: &str) -> bool { - Url::parse(value) - .ok() - .map(|url| url.scheme() == "https" && url.path() != "/" && url.host_str().is_some()) - .unwrap_or(false) -} - -impl AuthorizationManager { - fn well_known_paths(base_path: &str, resource: &str) -> Vec { - let trimmed = base_path.trim_start_matches('/').trim_end_matches('/'); - let mut candidates = Vec::new(); - - let mut push_candidate = |candidate: String| { - if !candidates.contains(&candidate) { - candidates.push(candidate); - } - }; - - let canonical = format!("/.well-known/{resource}"); - - if trimmed.is_empty() { - push_candidate(canonical); - return candidates; - } - - // This follows the RFC 8414 recommendation for well-known URI discovery - push_candidate(format!("{canonical}/{trimmed}")); - // This is a common pattern used by some identity providers - push_candidate(format!("/{trimmed}/.well-known/{resource}")); - // The canonical path should always be the last fallback - push_candidate(canonical); - - candidates - } - - /// create new auth manager with base url - pub async fn new(base_url: U) -> Result { - let base_url = base_url.into_url()?; - let http_client = HttpClient::builder() - .timeout(Duration::from_secs(30)) - .build() - .map_err(|e| AuthError::InternalError(e.to_string()))?; - - let manager = Self { - http_client: AuthorizationHttpClient::Reqwest(http_client), - metadata: None, - oauth_client: None, - credential_store: Arc::new(InMemoryCredentialStore::new()), - state_store: Arc::new(InMemoryStateStore::new()), - base_url, - }; - - Ok(manager) - } - - /// Set a custom credential store - /// - /// This allows you to provide your own implementation of credential storage, - /// such as file-based storage, keychain integration, or database storage. - /// This should be called before any operations that need credentials. - pub fn set_credential_store(&mut self, store: S) { - self.credential_store = Arc::new(store); - } - - /// Set a custom state store for OAuth2 authorization flow state - /// - /// This should be called before initiating the authorization flow. - pub fn set_state_store(&mut self, store: S) { - self.state_store = Arc::new(store); - } - - /// Set OAuth2 authorization metadata - /// - /// This should be called after discovering metadata via `discover_metadata()` - /// and before creating an `AuthorizationSession`. - pub fn set_metadata(&mut self, metadata: AuthorizationMetadata) { - self.metadata = Some(metadata); - } - - /// Initialize from stored credentials if available - /// - /// This will load credentials from the credential store and configure - /// the client if credentials are found. - pub async fn initialize_from_store(&mut self) -> Result { - if let Some(stored) = self.credential_store.load().await? { - if stored.token_response.is_some() { - if self.metadata.is_none() { - let metadata = self.discover_metadata().await?; - self.metadata = Some(metadata); - } - - self.configure_client_id(&stored.client_id)?; - return Ok(true); - } - } - Ok(false) - } - - pub fn with_client(&mut self, http_client: HttpClient) -> Result<(), AuthError> { - self.http_client = AuthorizationHttpClient::Reqwest(http_client); - Ok(()) - } - - pub fn with_oauth_http_client(&mut self, http_client: C) -> Result<(), AuthError> - where - C: OAuthHttpClient + 'static, - { - self.http_client = AuthorizationHttpClient::Custom(Arc::new(http_client)); - Ok(()) - } - - async fn send_request(&self, request: HttpRequest) -> Result { - self.http_client.execute(request).await - } - - fn build_request( - &self, - method: reqwest::Method, - url: &Url, - body: Vec, - headers: &[(&str, &str)], - ) -> Result { - let mut request = HttpRequest::new(body); - *request.method_mut() = method; - let uri = url - .as_str() - .parse::() - .map_err(|error| AuthError::InternalError(error.to_string()))?; - *request.uri_mut() = uri; - for (name, value) in headers { - let header_name = (*name) - .parse::() - .map_err(|error| AuthError::InternalError(error.to_string()))?; - let header_value = value - .parse::() - .map_err(|error| AuthError::InternalError(error.to_string()))?; - request.headers_mut().insert(header_name, header_value); - } - Ok(request) - } - - /// discover oauth2 metadata - pub async fn discover_metadata(&self) -> Result { - if let Some(metadata) = self.try_discover_oauth_server(&self.base_url).await? { - return Ok(metadata); - } - - if let Some(metadata) = self.discover_oauth_server_via_resource_metadata().await? { - return Ok(metadata); - } - - // No valid authorization metadata found - return error instead of guessing - // OAuth endpoints must be discovered from the server, not constructed by the client - Err(AuthError::NoAuthorizationSupport) - } - - /// get client id and credentials - pub async fn get_credentials(&self) -> Result { - let client_id = self - .oauth_client - .as_ref() - .ok_or_else(|| AuthError::InternalError("OAuth client not configured".to_string()))? - .client_id(); - - let stored = self.credential_store.load().await?; - let token_response = stored.and_then(|s| s.token_response); - - Ok((client_id.to_string(), token_response)) - } - - /// configure oauth2 client with client credentials - pub fn configure_client(&mut self, config: OAuthClientConfig) -> Result<(), AuthError> { - if self.metadata.is_none() { - return Err(AuthError::NoAuthorizationSupport); - } - - let metadata = self.metadata.as_ref().unwrap(); - - let auth_url = AuthUrl::new(metadata.authorization_endpoint.clone()) - .map_err(|e| AuthError::OAuthError(format!("Invalid authorization URL: {}", e)))?; - - let token_url = TokenUrl::new(metadata.token_endpoint.clone()) - .map_err(|e| AuthError::OAuthError(format!("Invalid token URL: {}", e)))?; - - let client_id = ClientId::new(config.client_id); - let redirect_url = RedirectUrl::new(config.redirect_uri.clone()) - .map_err(|e| AuthError::OAuthError(format!("Invalid re URL: {}", e)))?; - - let mut client_builder = BasicClient::new(client_id.clone()) - .set_auth_uri(auth_url) - .set_token_uri(token_url) - .set_redirect_uri(redirect_url); - - if let Some(secret) = config.client_secret { - client_builder = client_builder.set_client_secret(ClientSecret::new(secret)); - } - - self.oauth_client = Some(client_builder); - Ok(()) - } - /// validate if the server support the response type - fn validate_response_supported(&self, response_type: &str) -> Result<(), AuthError> { - if let Some(metadata) = self.metadata.as_ref() { - if let Some(response_types_supported) = metadata.response_types_supported.as_ref() { - if !response_types_supported.contains(&response_type.to_string()) { - return Err(AuthError::InvalidScope(response_type.to_string())); - } - } - } - Ok(()) - } - /// dynamic register oauth2 client - pub async fn register_client( - &mut self, - name: &str, - redirect_uri: &str, - ) -> Result { - if self.metadata.is_none() { - return Err(AuthError::NoAuthorizationSupport); - } - - let metadata = self.metadata.as_ref().unwrap(); - let Some(registration_url) = metadata.registration_endpoint.as_ref() else { - return Err(AuthError::RegistrationFailed( - "Dynamic client registration not supported".to_string(), - )); - }; - - // RFC 8414 RECOMMENDS response_types_supported in the metadata. This field is optional, - // but if present and does not include the flow we use ("code"), bail out early with a clear error. - self.validate_response_supported("code")?; - - let registration_request = ClientRegistrationRequest { - client_name: name.to_string(), - redirect_uris: vec![redirect_uri.to_string()], - grant_types: vec![ - "authorization_code".to_string(), - "refresh_token".to_string(), - ], - token_endpoint_auth_method: "none".to_string(), // public client - response_types: vec!["code".to_string()], - }; - - let registration_url = - Url::parse(registration_url).map_err(|error| AuthError::UrlError(error))?; - let body = serde_json::to_vec(®istration_request) - .map_err(|error| AuthError::RegistrationFailed(error.to_string()))?; - let response = self - .send_request(self.build_request( - reqwest::Method::POST, - ®istration_url, - body, - &[("content-type", "application/json")], - )?) - .await - .map_err(|error| AuthError::RegistrationFailed(error.to_string()))?; - - if !response.status().is_success() { - let status = response.status(); - let error_text = String::from_utf8_lossy(response.body()).to_string(); - - return Err(AuthError::RegistrationFailed(format!( - "HTTP {}: {}", - status, error_text - ))); - } - - debug!("registration response: {:?}", response); - let reg_response = match serde_json::from_slice::(response.body()) - { - Ok(response) => response, - Err(e) => { - return Err(AuthError::RegistrationFailed(format!( - "analyze response error: {}", - e - ))); - } - }; - - let config = OAuthClientConfig { - client_id: reg_response.client_id, - // Some IdP returns a response where the field 'client_secret' is present but with empty string value. - // In that case, the interpretation is that the client is a public client and does not have a secret during the - // registration phase here, e.g. dynamic client registrations. - // - // Even though whether or not the empty string is valid is outside of the scope of Oauth2 spec, - // we should treat it as no secret since otherwise we end up authenticating with a valid client_id with an empty client_secret - // as a password, which is not a goal of the client secret. - client_secret: reg_response.client_secret.filter(|s| !s.is_empty()), - redirect_uri: redirect_uri.to_string(), - scopes: vec![], - }; - - self.configure_client(config.clone())?; - Ok(config) - } - - /// use provided client id to configure oauth2 client instead of dynamic registration - /// this is useful when you have a stored client id from previous registration - pub fn configure_client_id(&mut self, client_id: &str) -> Result<(), AuthError> { - let config = OAuthClientConfig { - client_id: client_id.to_string(), - client_secret: None, - scopes: vec![], - redirect_uri: self.base_url.to_string(), - }; - self.configure_client(config) - } - - /// generate authorization url - pub async fn get_authorization_url(&self, scopes: &[&str]) -> Result { - let oauth_client = self - .oauth_client - .as_ref() - .ok_or_else(|| AuthError::InternalError("OAuth client not configured".to_string()))?; - - // ensure the server supports the response type we intend to use when metadata is available - self.validate_response_supported("code")?; - - // generate pkce challenge - let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); - - // build authorization request - let mut auth_request = oauth_client - .authorize_url(CsrfToken::new_random) - .set_pkce_challenge(pkce_challenge); - - // add request scopes - for scope in scopes { - auth_request = auth_request.add_scope(Scope::new(scope.to_string())); - } - - let (auth_url, csrf_token) = auth_request.url(); - - // store pkce verifier for later use via state store - let stored_state = StoredAuthorizationState::new(&pkce_verifier, &csrf_token); - self.state_store - .save(csrf_token.secret(), stored_state) - .await?; - - Ok(auth_url.to_string()) - } - - /// exchange authorization code for access token - pub async fn exchange_code_for_token( - &self, - code: &str, - csrf_token: &str, - ) -> Result, AuthError> { - debug!("start exchange code for token: {:?}", code); - let oauth_client = self - .oauth_client - .as_ref() - .ok_or_else(|| AuthError::InternalError("OAuth client not configured".to_string()))?; - - // Load state from state store using CSRF token as key - let stored_state = - self.state_store.load(csrf_token).await?.ok_or_else(|| { - AuthError::InternalError("Authorization state not found".to_string()) - })?; - - // Delete state after retrieval (one-time use) - self.state_store.delete(csrf_token).await?; - - // Reconstruct the PKCE verifier - let pkce_verifier = stored_state.into_pkce_verifier(); - - debug!("client_id: {:?}", oauth_client.client_id()); - let http_client = self.http_client.clone(); - - // exchange token - let token_result = match oauth_client - .exchange_code(AuthorizationCode::new(code.to_string())) - .set_pkce_verifier(pkce_verifier) - .request_async(&move |request| { - let http_client = http_client.clone(); - async move { http_client.execute(request).await } - }) - .await - { - Ok(token) => token, - Err(RequestTokenError::Parse(_, body)) => { - match serde_json::from_slice::(&body) { - Ok(parsed) => { - warn!( - "token exchange failed to parse completely but included a valid token response. Accepting it." - ); - parsed - } - Err(parse_err) => { - return Err(AuthError::TokenExchangeFailed(parse_err.to_string())); - } - } - } - Err(e) => { - return Err(AuthError::TokenExchangeFailed(e.to_string())); - } - }; - - debug!("exchange token result: {:?}", token_result); - - // Store credentials in the credential store - let client_id = oauth_client.client_id().to_string(); - let stored = StoredCredentials { - client_id, - token_response: Some(token_result.clone()), - }; - self.credential_store.save(stored).await?; - - Ok(token_result) - } - - /// get access token, if expired, refresh it automatically - pub async fn get_access_token(&self) -> Result { - // Load credentials from store - let stored = self.credential_store.load().await?; - let credentials = stored.and_then(|s| s.token_response); - - if let Some(creds) = credentials.as_ref() { - // check token expiry if we have a refresh token or an expiry time - if creds.refresh_token().is_some() || creds.expires_in().is_some() { - let expires_in = creds.expires_in().unwrap_or(Duration::from_secs(0)); - if expires_in <= Duration::from_secs(0) { - tracing::info!("Access token expired, refreshing."); - - let new_creds = self.refresh_token().await?; - tracing::info!("Refreshed access token."); - return Ok(new_creds.access_token().secret().to_string()); - } - } - - Ok(creds.access_token().secret().to_string()) - } else { - Err(AuthError::AuthorizationRequired) - } - } - - /// refresh access token - pub async fn refresh_token( - &self, - ) -> Result, AuthError> { - let oauth_client = self - .oauth_client - .as_ref() - .ok_or_else(|| AuthError::InternalError("OAuth client not configured".to_string()))?; - - let stored = self.credential_store.load().await?; - let current_credentials = stored - .and_then(|s| s.token_response) - .ok_or_else(|| AuthError::AuthorizationRequired)?; - - let refresh_token = current_credentials.refresh_token().ok_or_else(|| { - AuthError::TokenRefreshFailed("No refresh token available".to_string()) - })?; - debug!("refresh token: {:?}", refresh_token); - let http_client = self.http_client.clone(); - - let token_result = oauth_client - .exchange_refresh_token(&RefreshToken::new(refresh_token.secret().to_string())) - .request_async(&move |request| { - let http_client = http_client.clone(); - async move { http_client.execute(request).await } - }) - .await - .map_err(|e| AuthError::TokenRefreshFailed(e.to_string()))?; - - let client_id = oauth_client.client_id().to_string(); - let stored = StoredCredentials { - client_id, - token_response: Some(token_result.clone()), - }; - self.credential_store.save(stored).await?; - - Ok(token_result) - } - - /// prepare request, add authorization header - pub async fn prepare_request( - &self, - request: reqwest::RequestBuilder, - ) -> Result { - let token = self.get_access_token().await?; - Ok(request.header(AUTHORIZATION, format!("Bearer {}", token))) - } - - /// handle response, check if need to re-authorize - pub async fn handle_response( - &self, - response: reqwest::Response, - ) -> Result { - if response.status() == StatusCode::UNAUTHORIZED { - // 401 Unauthorized, need to re-authorize - Err(AuthError::AuthorizationRequired) - } else { - Ok(response) - } - } - - /// Generate discovery endpoint URLs following the priority order in spec-2025-11-25 4.3 "Authorization Server Metadata Discovery". - fn generate_discovery_urls(base_url: &Url) -> Vec { - let mut candidates = Vec::new(); - let path = base_url.path(); - let trimmed = path.trim_start_matches('/').trim_end_matches('/'); - let mut push_candidate = |discovery_path: String| { - let mut discovery_url = base_url.clone(); - discovery_url.set_query(None); - discovery_url.set_fragment(None); - discovery_url.set_path(&discovery_path); - candidates.push(discovery_url); - }; - if trimmed.is_empty() { - // No path components: try OAuth first, then OpenID Connect - push_candidate("/.well-known/oauth-authorization-server".to_string()); - push_candidate("/.well-known/openid-configuration".to_string()); - } else { - // Path components present: follow spec priority order - // 1. OAuth 2.0 with path insertion - push_candidate(format!("/.well-known/oauth-authorization-server/{trimmed}")); - // 2. OpenID Connect with path insertion - push_candidate(format!("/.well-known/openid-configuration/{trimmed}")); - // 3. OpenID Connect with path appending - push_candidate(format!("/{trimmed}/.well-known/openid-configuration")); - // 4. Canonical OAuth fallback (without path suffix) - push_candidate("/.well-known/oauth-authorization-server".to_string()); - } - - candidates - } - - async fn try_discover_oauth_server( - &self, - base_url: &Url, - ) -> Result, AuthError> { - for discovery_url in Self::generate_discovery_urls(base_url) { - if let Some(metadata) = self.fetch_authorization_metadata(&discovery_url).await? { - return Ok(Some(metadata)); - } - } - Ok(None) - } - - async fn fetch_authorization_metadata( - &self, - discovery_url: &Url, - ) -> Result, AuthError> { - debug!("discovery url: {:?}", discovery_url); - let response = match self - .send_request(self.build_request( - reqwest::Method::GET, - discovery_url, - Vec::new(), - [("MCP-Protocol-Version", "2024-11-05")].as_slice(), - )?) - .await - { - Ok(r) => r, - Err(e) => { - debug!("discovery request failed: {}", e); - return Ok(None); - } - }; - - if response.status() != StatusCode::OK { - debug!("discovery returned non-200: {}", response.status()); - return Ok(None); - } - - match serde_json::from_slice::(response.body()) { - Ok(metadata) => Ok(Some(metadata)), - Err(err) => { - debug!("Failed to parse metadata for {}: {}", discovery_url, err); - Ok(None) // malformed JSON ⇒ try next candidate - } - } - } - - async fn discover_oauth_server_via_resource_metadata( - &self, - ) -> Result, AuthError> { - let Some(resource_metadata_url) = self.discover_resource_metadata_url().await? else { - return Ok(None); - }; - - let Some(resource_metadata) = self - .fetch_resource_metadata_from_url(&resource_metadata_url) - .await? - else { - return Ok(None); - }; - - let mut candidates = Vec::new(); - - if let Some(single) = resource_metadata.authorization_server { - candidates.push(single); - } - if let Some(list) = resource_metadata.authorization_servers { - candidates.extend(list); - } - - for candidate in candidates { - let candidate = candidate.trim(); - if candidate.is_empty() { - continue; - } - - let candidate_url = match Url::parse(candidate) { - Ok(url) => url, - Err(_) => match resource_metadata_url.join(candidate) { - Ok(url) => url, - Err(e) => { - debug!("Failed to resolve authorization server URL `{candidate}`: {e}"); - continue; - } - }, - }; - - if candidate_url.path().contains("/.well-known/") { - if let Some(metadata) = self.fetch_authorization_metadata(&candidate_url).await? { - return Ok(Some(metadata)); - } - continue; - } - - if let Some(metadata) = self.try_discover_oauth_server(&candidate_url).await? { - return Ok(Some(metadata)); - } - } - - Ok(None) - } - - async fn discover_resource_metadata_url(&self) -> Result, AuthError> { - if let Ok(Some(resource_metadata_url)) = - self.fetch_resource_metadata_url(&self.base_url).await - { - return Ok(Some(resource_metadata_url)); - } - - // If the primary URL doesn't use WWW-Authenticate, try oauth-protected-resource discovery. - // https://www.rfc-editor.org/rfc/rfc9728.html#name-obtaining-protected-resourc - for candidate_path in - Self::well_known_paths(self.base_url.path(), "oauth-protected-resource") - { - let mut discovery_url = self.base_url.clone(); - discovery_url.set_query(None); - discovery_url.set_fragment(None); - discovery_url.set_path(&candidate_path); - if let Ok(Some(resource_metadata_url)) = - self.fetch_resource_metadata_url(&discovery_url).await - { - return Ok(Some(resource_metadata_url)); - } - } - - Ok(None) - } - - /// Extract the resource metadata url from the WWW-Authenticate header value. - /// https://www.rfc-editor.org/rfc/rfc9728.html#name-use-of-www-authenticate-for - async fn fetch_resource_metadata_url(&self, url: &Url) -> Result, AuthError> { - let response = match self - .send_request(self.build_request( - reqwest::Method::GET, - url, - Vec::new(), - [("MCP-Protocol-Version", "2024-11-05")].as_slice(), - )?) - .await - { - Ok(r) => r, - Err(e) => { - debug!("resource metadata probe failed: {}", e); - return Ok(None); - } - }; - - if response.status() == StatusCode::OK { - return Ok(Some(url.clone())); - } else if response.status() != StatusCode::UNAUTHORIZED { - debug!( - "resource metadata probe returned unexpected status: {}", - response.status() - ); - return Ok(None); - } - - let mut parsed_url = None; - for value in response.headers().get_all(WWW_AUTHENTICATE).iter() { - let Ok(value_str) = value.to_str() else { - continue; - }; - if let Some(url) = - Self::extract_resource_metadata_url_from_header(value_str, &self.base_url) - { - parsed_url = Some(url); - break; - } - } - - Ok(parsed_url) - } - - async fn fetch_resource_metadata_from_url( - &self, - resource_metadata_url: &Url, - ) -> Result, AuthError> { - debug!( - "resource metadata discovery url: {:?}", - resource_metadata_url - ); - let response = match self - .send_request(self.build_request( - reqwest::Method::GET, - resource_metadata_url, - Vec::new(), - [("MCP-Protocol-Version", "2024-11-05")].as_slice(), - )?) - .await - { - Ok(r) => r, - Err(e) => { - debug!("resource metadata request failed: {}", e); - return Ok(None); - } - }; - - if response.status() != StatusCode::OK { - debug!( - "resource metadata request returned non-200: {}", - response.status() - ); - return Ok(None); - } - - let metadata = - serde_json::from_slice::(response.body()).map_err(|e| { - AuthError::MetadataError(format!("Failed to parse resource metadata: {}", e)) - })?; - Ok(Some(metadata)) - } - - /// Extracts a url following `resource_metadata=` in a header value - fn extract_resource_metadata_url_from_header(header: &str, base_url: &Url) -> Option { - let header_lowercase = header.to_ascii_lowercase(); - let fragment_key = "resource_metadata="; - let mut search_offset = 0; - - while let Some(pos) = header_lowercase[search_offset..].find(fragment_key) { - let global_pos = search_offset + pos + fragment_key.len(); - let value_slice = &header[global_pos..]; - if let Some((value, consumed)) = Self::parse_next_header_value(value_slice) { - if let Ok(url) = Url::parse(&value) { - return Some(url); - } - if let Ok(url) = base_url.join(&value) { - return Some(url); - } - debug!("failed to parse resource metadata value `{value}` as URL"); - search_offset = global_pos + consumed; - continue; - } else { - break; - } - } - - None - } - - /// Parses an authentication parameter value from a `WWW-Authenticate` header fragment. - /// The header fragment should start with the header value after the `=` character and then - /// reads until the value ends. - /// - /// Returns the extracted value together with the number of bytes consumed from the provided - /// fragment. Quoted values support escaped characters (e.g. `\"`). The parser skips leading - /// whitespace before reading either a quoted or token value. If no well-formed value is found, - /// `None` is returned. - fn parse_next_header_value(header_fragment: &str) -> Option<(String, usize)> { - let trimmed = header_fragment.trim_start(); - let leading_ws = header_fragment.len() - trimmed.len(); - - if let Some(stripped) = trimmed.strip_prefix('"') { - let mut escaped = false; - let mut result = String::new(); - #[allow(clippy::manual_strip)] - for (idx, ch) in stripped.char_indices() { - if escaped { - result.push(ch); - escaped = false; - continue; - } - match ch { - '\\' => escaped = true, - '"' => return Some((result, leading_ws + idx + 2)), - _ => result.push(ch), - } - } - None - } else { - let end = trimmed - .find(|c: char| c == ',' || c == ';' || c.is_whitespace()) - .unwrap_or(trimmed.len()); - Some((trimmed[..end].to_string(), leading_ws + end)) - } - } -} - -/// oauth2 authorization session, for guiding user to complete the authorization process -pub struct AuthorizationSession { - pub auth_manager: AuthorizationManager, - pub auth_url: String, - pub redirect_uri: String, -} - -impl AuthorizationSession { - /// create new authorization session - pub async fn new( - mut auth_manager: AuthorizationManager, - scopes: &[&str], - redirect_uri: &str, - client_name: Option<&str>, - client_metadata_url: Option<&str>, - ) -> Result { - let metadata = auth_manager.metadata.as_ref(); - let supports_url_based_client_id = metadata - .and_then(|m| { - m.additional_fields - .get("client_id_metadata_document_supported") - }) - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let config = if supports_url_based_client_id { - if let Some(client_metadata_url) = client_metadata_url { - if !is_https_url(client_metadata_url) { - return Err(AuthError::RegistrationFailed(format!( - "client_metadata_url must be a valid HTTPS URL with a non-root pathname, got: {}", - client_metadata_url - ))); - } - // SEP-991: URL-based Client IDs - use URL as client_id directly - OAuthClientConfig { - client_id: client_metadata_url.to_string(), - client_secret: None, - scopes: scopes.iter().map(|s| s.to_string()).collect(), - redirect_uri: redirect_uri.to_string(), - } - } else { - // Fallback to dynamic registration - auth_manager - .register_client(client_name.unwrap_or("MCP Client"), redirect_uri) - .await - .map_err(|e| { - AuthError::RegistrationFailed(format!("Dynamic registration failed: {}", e)) - })? - } - } else { - // Fallback to dynamic registration - match auth_manager - .register_client(client_name.unwrap_or("MCP Client"), redirect_uri) - .await - { - Ok(config) => config, - Err(e) => { - return Err(AuthError::RegistrationFailed(format!( - "Dynamic registration failed: {}", - e - ))); - } - } - }; - - // reset client config - auth_manager.configure_client(config)?; - let auth_url = auth_manager.get_authorization_url(scopes).await?; - - Ok(Self { - auth_manager, - auth_url, - redirect_uri: redirect_uri.to_string(), - }) - } - - /// get client_id and credentials - pub async fn get_credentials(&self) -> Result { - self.auth_manager.get_credentials().await - } - - /// get authorization url - pub fn get_authorization_url(&self) -> &str { - &self.auth_url - } - - /// handle authorization code callback - pub async fn handle_callback( - &self, - code: &str, - csrf_token: &str, - ) -> Result, AuthError> { - self.auth_manager - .exchange_code_for_token(code, csrf_token) - .await - } -} - -/// http client extension, automatically add authorization header -pub struct AuthorizedHttpClient { - auth_manager: Arc, - inner_client: HttpClient, -} - -impl AuthorizedHttpClient { - /// create new authorized http client - pub fn new(auth_manager: Arc, client: Option) -> Self { - let inner_client = client.unwrap_or_default(); - Self { - auth_manager, - inner_client, - } - } - - /// send authorized request - pub async fn request( - &self, - method: reqwest::Method, - url: U, - ) -> Result { - let request = self.inner_client.request(method, url); - self.auth_manager.prepare_request(request).await - } - - /// send get request - pub async fn get(&self, url: U) -> Result { - let request = self.request(reqwest::Method::GET, url).await?; - let response = request.send().await?; - self.auth_manager.handle_response(response).await - } - - /// send post request - pub async fn post(&self, url: U) -> Result { - self.request(reqwest::Method::POST, url).await - } -} - -/// OAuth state machine -/// Use the OAuthState to manage the OAuth client is more recommend -/// But also you can use the AuthorizationManager,AuthorizationSession,AuthorizedHttpClient directly -pub enum OAuthState { - /// the AuthorizationManager - Unauthorized(AuthorizationManager), - /// the AuthorizationSession - Session(AuthorizationSession), - /// the authd AuthorizationManager - Authorized(AuthorizationManager), - /// the authd http client - AuthorizedHttpClient(AuthorizedHttpClient), -} - -impl OAuthState { - /// Create new OAuth state machine - pub async fn new( - base_url: U, - client: Option, - ) -> Result { - let mut manager = AuthorizationManager::new(base_url).await?; - if let Some(client) = client { - manager.with_client(client)?; - } - - Ok(OAuthState::Unauthorized(manager)) - } - - /// Get client_id and OAuth credentials - pub async fn get_credentials(&self) -> Result { - // return client_id and credentials - match self { - OAuthState::Unauthorized(manager) | OAuthState::Authorized(manager) => { - manager.get_credentials().await - } - OAuthState::Session(session) => session.get_credentials().await, - OAuthState::AuthorizedHttpClient(client) => client.auth_manager.get_credentials().await, - } - } - - /// Manually set credentials and move into authorized state - /// Useful if you're caching credentials externally and wish to reuse them - pub async fn set_credentials( - &mut self, - client_id: &str, - credentials: OAuthTokenResponse, - ) -> Result<(), AuthError> { - if let OAuthState::Unauthorized(manager) = self { - let mut manager = std::mem::replace( - manager, - AuthorizationManager::new(DEFAULT_EXCHANGE_URL).await?, - ); - - let stored = StoredCredentials { - client_id: client_id.to_string(), - token_response: Some(credentials), - }; - manager.credential_store.save(stored).await?; - - let metadata = manager.discover_metadata().await?; - manager.metadata = Some(metadata); - - manager.configure_client_id(client_id)?; - - *self = OAuthState::Authorized(manager); - Ok(()) - } else { - Err(AuthError::InternalError( - "Cannot set credentials in this state".to_string(), - )) - } - } - - /// start authorization - pub async fn start_authorization( - &mut self, - scopes: &[&str], - redirect_uri: &str, - client_name: Option<&str>, - ) -> Result<(), AuthError> { - self.start_authorization_with_metadata_url(scopes, redirect_uri, client_name, None) - .await - } - - /// start authorization with optional client metadata URL (SEP-991) - pub async fn start_authorization_with_metadata_url( - &mut self, - scopes: &[&str], - redirect_uri: &str, - client_name: Option<&str>, - client_metadata_url: Option<&str>, - ) -> Result<(), AuthError> { - if let OAuthState::Unauthorized(mut manager) = std::mem::replace( - self, - OAuthState::Unauthorized(AuthorizationManager::new(DEFAULT_EXCHANGE_URL).await?), - ) { - debug!("start discovery"); - let metadata = manager.discover_metadata().await?; - manager.metadata = Some(metadata); - debug!("start session"); - let session = AuthorizationSession::new( - manager, - scopes, - redirect_uri, - client_name, - client_metadata_url, - ) - .await?; - *self = OAuthState::Session(session); - Ok(()) - } else { - Err(AuthError::InternalError( - "Already in session state".to_string(), - )) - } - } - - /// complete authorization - pub async fn complete_authorization(&mut self) -> Result<(), AuthError> { - if let OAuthState::Session(session) = std::mem::replace( - self, - OAuthState::Unauthorized(AuthorizationManager::new(DEFAULT_EXCHANGE_URL).await?), - ) { - *self = OAuthState::Authorized(session.auth_manager); - Ok(()) - } else { - Err(AuthError::InternalError("Not in session state".to_string())) - } - } - /// covert to authorized http client - pub async fn to_authorized_http_client(&mut self) -> Result<(), AuthError> { - if let OAuthState::Authorized(manager) = std::mem::replace( - self, - OAuthState::Authorized(AuthorizationManager::new(DEFAULT_EXCHANGE_URL).await?), - ) { - *self = OAuthState::AuthorizedHttpClient(AuthorizedHttpClient::new( - Arc::new(manager), - None, - )); - Ok(()) - } else { - Err(AuthError::InternalError( - "Not in authorized state".to_string(), - )) - } - } - /// get current authorization url - pub async fn get_authorization_url(&self) -> Result { - match self { - OAuthState::Session(session) => Ok(session.get_authorization_url().to_string()), - OAuthState::Unauthorized(_) => { - Err(AuthError::InternalError("Not in session state".to_string())) - } - OAuthState::Authorized(_) => { - Err(AuthError::InternalError("Already authorized".to_string())) - } - OAuthState::AuthorizedHttpClient(_) => { - Err(AuthError::InternalError("Already authorized".to_string())) - } - } - } - - /// handle authorization callback - pub async fn handle_callback(&mut self, code: &str, csrf_token: &str) -> Result<(), AuthError> { - match self { - OAuthState::Session(session) => { - session.handle_callback(code, csrf_token).await?; - self.complete_authorization().await - } - OAuthState::Unauthorized(_) => { - Err(AuthError::InternalError("Not in session state".to_string())) - } - OAuthState::Authorized(_) => { - Err(AuthError::InternalError("Already authorized".to_string())) - } - OAuthState::AuthorizedHttpClient(_) => { - Err(AuthError::InternalError("Already authorized".to_string())) - } - } - } - - /// get access token - pub async fn get_access_token(&self) -> Result { - match self { - OAuthState::Unauthorized(manager) => manager.get_access_token().await, - OAuthState::Session(_) => { - Err(AuthError::InternalError("Not in manager state".to_string())) - } - OAuthState::Authorized(_) => { - Err(AuthError::InternalError("Already authorized".to_string())) - } - OAuthState::AuthorizedHttpClient(_) => { - Err(AuthError::InternalError("Already authorized".to_string())) - } - } - } - - /// refresh access token - pub async fn refresh_token(&self) -> Result<(), AuthError> { - match self { - OAuthState::Unauthorized(_) => { - Err(AuthError::InternalError("Not in manager state".to_string())) - } - OAuthState::Session(_) => { - Err(AuthError::InternalError("Not in manager state".to_string())) - } - OAuthState::Authorized(manager) => { - manager.refresh_token().await?; - Ok(()) - } - OAuthState::AuthorizedHttpClient(_) => { - Err(AuthError::InternalError("Already authorized".to_string())) - } - } - } - - pub fn into_authorization_manager(self) -> Option { - match self { - OAuthState::Authorized(manager) => Some(manager), - _ => None, - } - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use oauth2::{CsrfToken, PkceCodeVerifier}; - use url::Url; - - use super::{ - AuthError, AuthorizationManager, InMemoryStateStore, StateStore, StoredAuthorizationState, - is_https_url, - }; - - // SEP-991: URL-based Client IDs - // Tests adapted from the TypeScript SDK's isHttpsUrl test suite - #[test] - fn test_is_https_url_scenarios() { - // Returns true for valid https url with path - assert!(is_https_url("https://example.com/client-metadata.json")); - // Returns true for https url with query params - assert!(is_https_url("https://example.com/metadata?version=1")); - // Returns false for https url without path - assert!(!is_https_url("https://example.com")); - assert!(!is_https_url("https://example.com/")); - assert!(!is_https_url("https://")); - // Returns false for http url - assert!(!is_https_url("http://example.com/metadata")); - // Returns false for non-url strings - assert!(!is_https_url("not a url")); - // Returns false for empty string - assert!(!is_https_url("")); - // Returns false for javascript scheme - assert!(!is_https_url("javascript:alert(1)")); - // Returns false for data scheme - assert!(!is_https_url("data:text/html,")); - } - - #[test] - fn parses_resource_metadata_parameter() { - let header = r#"Bearer error="invalid_request", error_description="missing token", resource_metadata="https://example.com/.well-known/oauth-protected-resource/api""#; - let base = Url::parse("https://example.com/api").unwrap(); - let parsed = AuthorizationManager::extract_resource_metadata_url_from_header(header, &base); - assert_eq!( - parsed.unwrap().as_str(), - "https://example.com/.well-known/oauth-protected-resource/api" - ); - } - - #[test] - fn parses_relative_resource_metadata_parameter() { - let header = r#"Bearer error="invalid_request", resource_metadata="/.well-known/oauth-protected-resource/api""#; - let base = Url::parse("https://example.com/api").unwrap(); - let parsed = AuthorizationManager::extract_resource_metadata_url_from_header(header, &base); - assert_eq!( - parsed.unwrap().as_str(), - "https://example.com/.well-known/oauth-protected-resource/api" - ); - } - - #[test] - fn parse_auth_param_value_handles_quoted_string() { - let fragment = r#""example", realm="foo""#; - let parsed = AuthorizationManager::parse_next_header_value(fragment).unwrap(); - assert_eq!(parsed.0, "example"); - assert_eq!(parsed.1, 9); - } - - #[test] - fn parse_auth_param_value_handles_escaped_quotes_and_whitespace() { - let fragment = r#" "a\"b\\c" ,next=value"#; - let parsed = AuthorizationManager::parse_next_header_value(fragment).unwrap(); - assert_eq!(parsed.0, r#"a"b\c"#); - assert_eq!(parsed.1, 12); - } - - #[test] - fn parse_auth_param_value_handles_token_values() { - let fragment = " token,next"; - let parsed = AuthorizationManager::parse_next_header_value(fragment).unwrap(); - assert_eq!(parsed.0, "token"); - assert_eq!(parsed.1, 7); - } - - #[test] - fn parse_auth_param_value_handles_semicolon_separated_tokens() { - let fragment = r#" https://example.com/meta; error="invalid_token""#; - let parsed = AuthorizationManager::parse_next_header_value(fragment).unwrap(); - assert_eq!(parsed.0, "https://example.com/meta"); - assert_eq!(&fragment[..parsed.1], " https://example.com/meta"); - } - - #[test] - fn parse_auth_param_value_handles_semicolon_after_quoted_value() { - let fragment = r#" "https://example.com/meta"; error="invalid_token""#; - let parsed = AuthorizationManager::parse_next_header_value(fragment).unwrap(); - assert_eq!(parsed.0, "https://example.com/meta"); - assert_eq!(&fragment[..parsed.1], r#" "https://example.com/meta""#); - } - - #[test] - fn parse_auth_param_value_returns_none_for_unterminated_quotes() { - let fragment = r#""unterminated,value"#; - assert!(AuthorizationManager::parse_next_header_value(fragment).is_none()); - } - - #[test] - fn well_known_paths_root() { - let paths = AuthorizationManager::well_known_paths("/", "oauth-authorization-server"); - assert_eq!( - paths, - vec!["/.well-known/oauth-authorization-server".to_string()] - ); - } - - #[test] - fn well_known_paths_with_suffix() { - let paths = AuthorizationManager::well_known_paths("/mcp", "oauth-authorization-server"); - assert_eq!( - paths, - vec![ - "/.well-known/oauth-authorization-server/mcp".to_string(), - "/mcp/.well-known/oauth-authorization-server".to_string(), - "/.well-known/oauth-authorization-server".to_string(), - ] - ); - } - - #[test] - fn well_known_paths_trailing_slash() { - let paths = - AuthorizationManager::well_known_paths("/v1/mcp/", "oauth-authorization-server"); - assert_eq!( - paths, - vec![ - "/.well-known/oauth-authorization-server/v1/mcp".to_string(), - "/v1/mcp/.well-known/oauth-authorization-server".to_string(), - "/.well-known/oauth-authorization-server".to_string(), - ] - ); - } - - #[test] - fn generate_discovery_urls() { - // Test root URL (no path components): OAuth first, then OpenID Connect - let base_url = Url::parse("https://auth.example.com").unwrap(); - let urls = AuthorizationManager::generate_discovery_urls(&base_url); - assert_eq!(urls.len(), 2); - assert_eq!( - urls[0].as_str(), - "https://auth.example.com/.well-known/oauth-authorization-server" - ); - assert_eq!( - urls[1].as_str(), - "https://auth.example.com/.well-known/openid-configuration" - ); - - // Test URL with single path segment: follow spec priority order - let base_url = Url::parse("https://auth.example.com/tenant1").unwrap(); - let urls = AuthorizationManager::generate_discovery_urls(&base_url); - assert_eq!(urls.len(), 4); - assert_eq!( - urls[0].as_str(), - "https://auth.example.com/.well-known/oauth-authorization-server/tenant1" - ); - assert_eq!( - urls[1].as_str(), - "https://auth.example.com/.well-known/openid-configuration/tenant1" - ); - assert_eq!( - urls[2].as_str(), - "https://auth.example.com/tenant1/.well-known/openid-configuration" - ); - assert_eq!( - urls[3].as_str(), - "https://auth.example.com/.well-known/oauth-authorization-server" - ); - - // Test URL with path and trailing slash - let base_url = Url::parse("https://auth.example.com/v1/mcp/").unwrap(); - let urls = AuthorizationManager::generate_discovery_urls(&base_url); - assert_eq!(urls.len(), 4); - assert_eq!( - urls[0].as_str(), - "https://auth.example.com/.well-known/oauth-authorization-server/v1/mcp" - ); - assert_eq!( - urls[1].as_str(), - "https://auth.example.com/.well-known/openid-configuration/v1/mcp" - ); - assert_eq!( - urls[2].as_str(), - "https://auth.example.com/v1/mcp/.well-known/openid-configuration" - ); - assert_eq!( - urls[3].as_str(), - "https://auth.example.com/.well-known/oauth-authorization-server" - ); - - // Test URL with multiple path segments - let base_url = Url::parse("https://auth.example.com/tenant1/subtenant").unwrap(); - let urls = AuthorizationManager::generate_discovery_urls(&base_url); - assert_eq!(urls.len(), 4); - assert_eq!( - urls[0].as_str(), - "https://auth.example.com/.well-known/oauth-authorization-server/tenant1/subtenant" - ); - assert_eq!( - urls[1].as_str(), - "https://auth.example.com/.well-known/openid-configuration/tenant1/subtenant" - ); - assert_eq!( - urls[2].as_str(), - "https://auth.example.com/tenant1/subtenant/.well-known/openid-configuration" - ); - assert_eq!( - urls[3].as_str(), - "https://auth.example.com/.well-known/oauth-authorization-server" - ); - } - - // StateStore and StoredAuthorizationState tests - - #[tokio::test] - async fn test_in_memory_state_store_save_and_load() { - let store = InMemoryStateStore::new(); - let pkce = PkceCodeVerifier::new("test-verifier".to_string()); - let csrf = CsrfToken::new("test-csrf".to_string()); - let state = StoredAuthorizationState::new(&pkce, &csrf); - - // Save state - store.save("test-csrf", state).await.unwrap(); - - // Load state - let loaded = store.load("test-csrf").await.unwrap(); - assert!(loaded.is_some()); - let loaded = loaded.unwrap(); - assert_eq!(loaded.csrf_token, "test-csrf"); - assert_eq!(loaded.pkce_verifier, "test-verifier"); - } - - #[tokio::test] - async fn test_in_memory_state_store_load_nonexistent() { - let store = InMemoryStateStore::new(); - let result = store.load("nonexistent").await.unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn test_in_memory_state_store_delete() { - let store = InMemoryStateStore::new(); - let pkce = PkceCodeVerifier::new("verifier".to_string()); - let csrf = CsrfToken::new("csrf".to_string()); - let state = StoredAuthorizationState::new(&pkce, &csrf); - - store.save("csrf", state).await.unwrap(); - store.delete("csrf").await.unwrap(); - - let result = store.load("csrf").await.unwrap(); - assert!(result.is_none()); - } - - #[test] - fn test_stored_authorization_state_serialization() { - let pkce = PkceCodeVerifier::new("my-verifier".to_string()); - let csrf = CsrfToken::new("my-csrf".to_string()); - let state = StoredAuthorizationState::new(&pkce, &csrf); - - // Serialize to JSON - let json = serde_json::to_string(&state).unwrap(); - - // Deserialize back - let deserialized: StoredAuthorizationState = serde_json::from_str(&json).unwrap(); - - assert_eq!(deserialized.pkce_verifier, "my-verifier"); - assert_eq!(deserialized.csrf_token, "my-csrf"); - } - - #[test] - fn test_stored_authorization_state_into_pkce_verifier() { - let pkce = PkceCodeVerifier::new("original-verifier".to_string()); - let csrf = CsrfToken::new("csrf-token".to_string()); - let state = StoredAuthorizationState::new(&pkce, &csrf); - - let recovered = state.into_pkce_verifier(); - assert_eq!(recovered.secret(), "original-verifier"); - } - - #[test] - fn test_stored_authorization_state_created_at() { - let pkce = PkceCodeVerifier::new("verifier".to_string()); - let csrf = CsrfToken::new("csrf".to_string()); - let state = StoredAuthorizationState::new(&pkce, &csrf); - - // created_at should be a reasonable timestamp (after year 2020) - assert!(state.created_at > 1577836800); // Jan 1, 2020 - } - - #[tokio::test] - async fn test_in_memory_state_store_overwrite() { - let store = InMemoryStateStore::new(); - let csrf_key = "same-csrf"; - - // Save first state - let pkce1 = PkceCodeVerifier::new("verifier-1".to_string()); - let csrf1 = CsrfToken::new(csrf_key.to_string()); - let state1 = StoredAuthorizationState::new(&pkce1, &csrf1); - store.save(csrf_key, state1).await.unwrap(); - - // Overwrite with second state - let pkce2 = PkceCodeVerifier::new("verifier-2".to_string()); - let csrf2 = CsrfToken::new(csrf_key.to_string()); - let state2 = StoredAuthorizationState::new(&pkce2, &csrf2); - store.save(csrf_key, state2).await.unwrap(); - - // Should get the second state - let loaded = store.load(csrf_key).await.unwrap().unwrap(); - assert_eq!(loaded.pkce_verifier, "verifier-2"); - } - - #[tokio::test] - async fn test_in_memory_state_store_concurrent_access() { - let store = Arc::new(InMemoryStateStore::new()); - let mut handles = vec![]; - - // Spawn 10 concurrent tasks that each save and load their own state - for i in 0..10 { - let store = Arc::clone(&store); - let handle = tokio::spawn(async move { - let csrf_key = format!("csrf-{}", i); - let verifier = format!("verifier-{}", i); - - let pkce = PkceCodeVerifier::new(verifier.clone()); - let csrf = CsrfToken::new(csrf_key.clone()); - let state = StoredAuthorizationState::new(&pkce, &csrf); - - store.save(&csrf_key, state).await.unwrap(); - let loaded = store.load(&csrf_key).await.unwrap().unwrap(); - assert_eq!(loaded.pkce_verifier, verifier); - - store.delete(&csrf_key).await.unwrap(); - let deleted = store.load(&csrf_key).await.unwrap(); - assert!(deleted.is_none()); - }); - handles.push(handle); - } - - // Wait for all tasks to complete - for handle in handles { - handle.await.unwrap(); - } - } - - #[test] - fn test_discovery_urls_with_path_suffix() { - // When the base URL has a path suffix (e.g., /mcp), the discovery should - // eventually fall back to checking /.well-known/oauth-authorization-server - // at the root, not just /.well-known/oauth-authorization-server/{path}. - let base_url = Url::parse("https://mcp.example.com/mcp").unwrap(); - let urls = AuthorizationManager::generate_discovery_urls(&base_url); - - let canonical_oauth_fallback = - "https://mcp.example.com/.well-known/oauth-authorization-server"; - - assert!( - urls.iter().any(|u| u.as_str() == canonical_oauth_fallback), - "Expected discovery URLs to include canonical OAuth fallback '{}', but got: {:?}", - canonical_oauth_fallback, - urls.iter().map(|u| u.as_str()).collect::>() - ); - } - - #[tokio::test] - async fn test_custom_state_store_with_authorization_manager() { - use std::sync::atomic::{AtomicUsize, Ordering}; - - // Custom state store that tracks calls - #[derive(Debug, Default)] - struct TrackingStateStore { - inner: InMemoryStateStore, - save_count: AtomicUsize, - load_count: AtomicUsize, - delete_count: AtomicUsize, - } - - #[async_trait::async_trait] - impl StateStore for TrackingStateStore { - async fn save( - &self, - csrf_token: &str, - state: StoredAuthorizationState, - ) -> Result<(), AuthError> { - self.save_count.fetch_add(1, Ordering::SeqCst); - self.inner.save(csrf_token, state).await - } - - async fn load( - &self, - csrf_token: &str, - ) -> Result, AuthError> { - self.load_count.fetch_add(1, Ordering::SeqCst); - self.inner.load(csrf_token).await - } - - async fn delete(&self, csrf_token: &str) -> Result<(), AuthError> { - self.delete_count.fetch_add(1, Ordering::SeqCst); - self.inner.delete(csrf_token).await - } - } - - // Verify custom store works standalone - let store = TrackingStateStore::default(); - let pkce = PkceCodeVerifier::new("test-verifier".to_string()); - let csrf = CsrfToken::new("test-csrf".to_string()); - let state = StoredAuthorizationState::new(&pkce, &csrf); - - store.save("test-csrf", state).await.unwrap(); - assert_eq!(store.save_count.load(Ordering::SeqCst), 1); - - let _ = store.load("test-csrf").await.unwrap(); - assert_eq!(store.load_count.load(Ordering::SeqCst), 1); - - store.delete("test-csrf").await.unwrap(); - assert_eq!(store.delete_count.load(Ordering::SeqCst), 1); - - // Verify custom store can be set on AuthorizationManager - let mut manager = AuthorizationManager::new("http://localhost").await.unwrap(); - manager.set_state_store(TrackingStateStore::default()); - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/child_process.rs b/codex-rs/vendor/rmcp/src/transport/child_process.rs deleted file mode 100644 index e33800b18f..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/child_process.rs +++ /dev/null @@ -1,309 +0,0 @@ -use std::process::Stdio; - -use futures::future::Future; -use process_wrap::tokio::{ChildWrapper, CommandWrap}; -use tokio::{ - io::AsyncRead, - process::{ChildStderr, ChildStdin, ChildStdout}, -}; - -use super::{RxJsonRpcMessage, Transport, TxJsonRpcMessage, async_rw::AsyncRwTransport}; -use crate::RoleClient; - -const MAX_WAIT_ON_DROP_SECS: u64 = 3; -/// The parts of a child process. -type ChildProcessParts = ( - Box, - ChildStdout, - ChildStdin, - Option, -); - -/// Extract the stdio handles from a spawned child. -/// Returns `(child, stdout, stdin, stderr)` where `stderr` is `Some` only -/// if the process was spawned with `Stdio::piped()`. -#[inline] -fn child_process(mut child: Box) -> std::io::Result { - let child_stdin = match child.inner_mut().stdin().take() { - Some(stdin) => stdin, - None => return Err(std::io::Error::other("stdin was already taken")), - }; - let child_stdout = match child.inner_mut().stdout().take() { - Some(stdout) => stdout, - None => return Err(std::io::Error::other("stdout was already taken")), - }; - let child_stderr = child.inner_mut().stderr().take(); - Ok((child, child_stdout, child_stdin, child_stderr)) -} - -pub struct TokioChildProcess { - child: ChildWithCleanup, - transport: AsyncRwTransport, -} - -pub struct ChildWithCleanup { - inner: Option>, -} - -impl Drop for ChildWithCleanup { - fn drop(&mut self) { - // We should not use start_kill(), instead we should use kill() to avoid zombies - if let Some(mut inner) = self.inner.take() { - // We don't care about the result, just try to kill it - tokio::spawn(async move { - if let Err(e) = Box::into_pin(inner.kill()).await { - tracing::warn!("Error killing child process: {}", e); - } - }); - } - } -} - -// we hold the child process with stdout, for it's easier to implement AsyncRead -pin_project_lite::pin_project! { - pub struct TokioChildProcessOut { - child: ChildWithCleanup, - #[pin] - child_stdout: ChildStdout, - } -} - -impl TokioChildProcessOut { - /// Get the process ID of the child process. - pub fn id(&self) -> Option { - self.child.inner.as_ref()?.id() - } -} - -impl AsyncRead for TokioChildProcessOut { - fn poll_read( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> std::task::Poll> { - self.project().child_stdout.poll_read(cx, buf) - } -} - -impl TokioChildProcess { - /// Convenience: spawn with default `piped` stdio - pub fn new(command: impl Into) -> std::io::Result { - let (proc, _ignored) = TokioChildProcessBuilder::new(command).spawn()?; - Ok(proc) - } - - /// Builder entry-point allowing fine-grained stdio control. - pub fn builder(command: impl Into) -> TokioChildProcessBuilder { - TokioChildProcessBuilder::new(command) - } - - /// Get the process ID of the child process. - pub fn id(&self) -> Option { - self.child.inner.as_ref()?.id() - } - - /// Gracefully shutdown the child process - /// - /// This will first close the transport to the child process (the server), - /// and wait for the child process to exit normally with a timeout. - /// If the child process doesn't exit within the timeout, it will be killed. - pub async fn graceful_shutdown(&mut self) -> std::io::Result<()> { - if let Some(mut child) = self.child.inner.take() { - self.transport.close().await?; - - let wait_fut = child.wait(); - tokio::select! { - _ = tokio::time::sleep(std::time::Duration::from_secs(MAX_WAIT_ON_DROP_SECS)) => { - if let Err(e) = Box::into_pin(child.kill()).await { - tracing::warn!("Error killing child: {e}"); - return Err(e); - } - }, - res = wait_fut => { - match res { - Ok(status) => { - tracing::info!("Child exited gracefully {}", status); - } - Err(e) => { - tracing::warn!("Error waiting for child: {e}"); - return Err(e); - } - } - } - } - } - Ok(()) - } - - /// Take ownership of the inner child process - pub fn into_inner(mut self) -> Option> { - self.child.inner.take() - } - - /// Split this helper into a reader (stdout) and writer (stdin). - #[deprecated( - since = "0.5.0", - note = "use the Transport trait implementation instead" - )] - pub fn split(self) -> (TokioChildProcessOut, ChildStdin) { - unimplemented!("This method is deprecated, use the Transport trait implementation instead"); - } -} - -/// Builder for `TokioChildProcess` allowing custom `Stdio` configuration. -pub struct TokioChildProcessBuilder { - cmd: CommandWrap, - stdin: Stdio, - stdout: Stdio, - stderr: Stdio, -} - -impl TokioChildProcessBuilder { - fn new(cmd: impl Into) -> Self { - Self { - cmd: cmd.into(), - stdin: Stdio::piped(), - stdout: Stdio::piped(), - stderr: Stdio::inherit(), - } - } - - /// Override the child stdin configuration. - pub fn stdin(mut self, io: impl Into) -> Self { - self.stdin = io.into(); - self - } - /// Override the child stdout configuration. - pub fn stdout(mut self, io: impl Into) -> Self { - self.stdout = io.into(); - self - } - /// Override the child stderr configuration. - pub fn stderr(mut self, io: impl Into) -> Self { - self.stderr = io.into(); - self - } - - /// Spawn the child process. Returns the transport plus an optional captured stderr handle. - pub fn spawn(mut self) -> std::io::Result<(TokioChildProcess, Option)> { - self.cmd - .command_mut() - .stdin(self.stdin) - .stdout(self.stdout) - .stderr(self.stderr); - - let (child, stdout, stdin, stderr_opt) = child_process(self.cmd.spawn()?)?; - - let transport = AsyncRwTransport::new(stdout, stdin); - let proc = TokioChildProcess { - child: ChildWithCleanup { inner: Some(child) }, - transport, - }; - Ok((proc, stderr_opt)) - } -} - -impl Transport for TokioChildProcess { - type Error = std::io::Error; - - fn send( - &mut self, - item: TxJsonRpcMessage, - ) -> impl Future> + Send + 'static { - self.transport.send(item) - } - - fn receive(&mut self) -> impl Future>> + Send { - self.transport.receive() - } - - fn close(&mut self) -> impl Future> + Send { - self.graceful_shutdown() - } -} - -pub trait ConfigureCommandExt { - fn configure(self, f: impl FnOnce(&mut Self)) -> Self; -} - -impl ConfigureCommandExt for tokio::process::Command { - fn configure(mut self, f: impl FnOnce(&mut Self)) -> Self { - f(&mut self); - self - } -} - -#[cfg(unix)] -#[cfg(test)] -mod tests { - use tokio::process::Command; - - use super::*; - - #[tokio::test] - async fn test_tokio_child_process_drop() { - let r = TokioChildProcess::new(Command::new("sleep").configure(|cmd| { - cmd.arg("30"); - })); - assert!(r.is_ok()); - let child_process = r.unwrap(); - let id = child_process.id(); - assert!(id.is_some()); - let id = id.unwrap(); - // Drop the child process - drop(child_process); - // Wait a moment to allow the cleanup task to run - tokio::time::sleep(std::time::Duration::from_secs(MAX_WAIT_ON_DROP_SECS + 1)).await; - // Check if the process is still running - let status = Command::new("ps") - .arg("-p") - .arg(id.to_string()) - .status() - .await; - match status { - Ok(status) => { - assert!( - !status.success(), - "Process with PID {} is still running", - id - ); - } - Err(e) => { - panic!("Failed to check process status: {}", e); - } - } - } - - #[tokio::test] - async fn test_tokio_child_process_graceful_shutdown() { - let r = TokioChildProcess::new(Command::new("sleep").configure(|cmd| { - cmd.arg("30"); - })); - assert!(r.is_ok()); - let mut child_process = r.unwrap(); - let id = child_process.id(); - assert!(id.is_some()); - let id = id.unwrap(); - child_process.graceful_shutdown().await.unwrap(); - // Wait a moment to allow the cleanup task to run - tokio::time::sleep(std::time::Duration::from_secs(MAX_WAIT_ON_DROP_SECS + 1)).await; - // Check if the process is still running - let status = Command::new("ps") - .arg("-p") - .arg(id.to_string()) - .status() - .await; - match status { - Ok(status) => { - assert!( - !status.success(), - "Process with PID {} is still running", - id - ); - } - Err(e) => { - panic!("Failed to check process status: {}", e); - } - } - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/common.rs b/codex-rs/vendor/rmcp/src/transport/common.rs deleted file mode 100644 index e78d4a6e6e..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/common.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[cfg(feature = "transport-streamable-http-server")] -pub mod server_side_http; - -pub mod http_header; - -#[cfg(feature = "__reqwest")] -#[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))] -mod reqwest; - -// Note: This module provides SSE stream parsing and auto-reconnect utilities. -// It's used by the streamable HTTP client (which receives SSE-formatted responses), -// not the removed SSE transport. The name is historical. -#[cfg(feature = "client-side-sse")] -#[cfg_attr(docsrs, doc(cfg(feature = "client-side-sse")))] -pub mod client_side_sse; - -#[cfg(feature = "auth")] -#[cfg_attr(docsrs, doc(cfg(feature = "auth")))] -pub mod auth; diff --git a/codex-rs/vendor/rmcp/src/transport/common/auth.rs b/codex-rs/vendor/rmcp/src/transport/common/auth.rs deleted file mode 100644 index f9e3a07104..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/common/auth.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[cfg(feature = "transport-streamable-http-client")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-client")))] -mod streamable_http_client; diff --git a/codex-rs/vendor/rmcp/src/transport/common/auth/streamable_http_client.rs b/codex-rs/vendor/rmcp/src/transport/common/auth/streamable_http_client.rs deleted file mode 100644 index 49ebefcd64..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/common/auth/streamable_http_client.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::transport::{ - auth::AuthClient, - streamable_http_client::{StreamableHttpClient, StreamableHttpError}, -}; -impl StreamableHttpClient for AuthClient -where - C: StreamableHttpClient + Send + Sync, -{ - type Error = C::Error; - - async fn delete_session( - &self, - uri: std::sync::Arc, - session_id: std::sync::Arc, - mut auth_token: Option, - ) -> Result<(), crate::transport::streamable_http_client::StreamableHttpError> - { - if auth_token.is_none() { - auth_token = Some(self.get_access_token().await?); - } - self.http_client - .delete_session(uri, session_id, auth_token) - .await - } - - async fn get_stream( - &self, - uri: std::sync::Arc, - session_id: std::sync::Arc, - last_event_id: Option, - mut auth_token: Option, - ) -> Result< - futures::stream::BoxStream<'static, Result>, - crate::transport::streamable_http_client::StreamableHttpError, - > { - if auth_token.is_none() { - auth_token = Some(self.get_access_token().await?); - } - self.http_client - .get_stream(uri, session_id, last_event_id, auth_token) - .await - } - - async fn post_message( - &self, - uri: std::sync::Arc, - message: crate::model::ClientJsonRpcMessage, - session_id: Option>, - mut auth_token: Option, - ) -> Result< - crate::transport::streamable_http_client::StreamableHttpPostResponse, - StreamableHttpError, - > { - if auth_token.is_none() { - auth_token = Some(self.get_access_token().await?); - } - self.http_client - .post_message(uri, message, session_id, auth_token) - .await - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/common/client_side_sse.rs b/codex-rs/vendor/rmcp/src/transport/common/client_side_sse.rs deleted file mode 100644 index 4e01994fa4..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/common/client_side_sse.rs +++ /dev/null @@ -1,310 +0,0 @@ -use std::{ - pin::Pin, - sync::Arc, - task::{Poll, ready}, - time::Duration, -}; - -use futures::{Stream, stream::BoxStream}; -use sse_stream::{Error as SseError, Sse}; - -use crate::model::ServerJsonRpcMessage; - -pub type BoxedSseResponse = BoxStream<'static, Result>; - -pub trait SseRetryPolicy: std::fmt::Debug + Send + Sync { - fn retry(&self, current_times: usize) -> Option; -} - -#[derive(Debug, Clone)] -pub struct FixedInterval { - pub max_times: Option, - pub duration: Duration, -} - -impl SseRetryPolicy for FixedInterval { - fn retry(&self, current_times: usize) -> Option { - if let Some(max_times) = self.max_times { - if current_times >= max_times { - return None; - } - } - Some(self.duration) - } -} - -impl FixedInterval { - pub const DEFAULT_MIN_DURATION: Duration = Duration::from_millis(1000); -} - -impl Default for FixedInterval { - fn default() -> Self { - Self { - max_times: None, - duration: Self::DEFAULT_MIN_DURATION, - } - } -} - -#[derive(Debug, Clone)] -pub struct ExponentialBackoff { - pub max_times: Option, - pub base_duration: Duration, -} - -impl ExponentialBackoff { - pub const DEFAULT_DURATION: Duration = Duration::from_millis(1000); -} - -impl Default for ExponentialBackoff { - fn default() -> Self { - Self { - max_times: None, - base_duration: Self::DEFAULT_DURATION, - } - } -} - -impl SseRetryPolicy for ExponentialBackoff { - fn retry(&self, current_times: usize) -> Option { - if let Some(max_times) = self.max_times { - if current_times >= max_times { - return None; - } - } - Some(self.base_duration * (2u32.pow(current_times as u32))) - } -} - -#[derive(Debug, Clone, Copy, Default)] -pub struct NeverRetry; - -impl SseRetryPolicy for NeverRetry { - fn retry(&self, _current_times: usize) -> Option { - None - } -} - -#[derive(Debug, Default)] -pub struct NeverReconnect { - error: Option, -} - -impl SseStreamReconnect for NeverReconnect { - type Error = E; - type Future = futures::future::Ready>; - fn retry_connection(&mut self, _last_event_id: Option<&str>) -> Self::Future { - futures::future::ready(Err(self.error.take().expect("should not be called again"))) - } -} - -/// Abstraction for SSE reconnection logic. Implementors can hook into -/// [`handle_control_event`](Self::handle_control_event) to consume control -/// frames (e.g. `event: endpoint`) that arrive when a server restarts an SSE -/// stream. The default implementation is a no-op, keeping existing behaviour -/// intact. -pub(crate) trait SseStreamReconnect { - type Error: std::error::Error; - type Future: Future> + Send; - fn retry_connection(&mut self, last_event_id: Option<&str>) -> Self::Future; - fn handle_control_event(&mut self, _event: &Sse) -> Result<(), Self::Error> { - Ok(()) - } - fn handle_stream_error( - &mut self, - error: &(dyn std::error::Error + 'static), - last_event_id: Option<&str>, - ) { - if let Some(id) = last_event_id { - tracing::warn!(%id, "sse stream error: {error}"); - } else { - tracing::warn!("sse stream error: {error}"); - } - } -} - -pin_project_lite::pin_project! { - pub(crate) struct SseAutoReconnectStream - where R: SseStreamReconnect - { - retry_policy: Arc, - last_event_id: Option, - server_retry_interval: Option, - connector: R, - #[pin] - state: SseAutoReconnectStreamState, - } -} - -impl SseAutoReconnectStream { - pub fn new( - stream: BoxedSseResponse, - connector: R, - retry_policy: Arc, - ) -> Self { - Self { - retry_policy, - last_event_id: None, - server_retry_interval: None, - connector, - state: SseAutoReconnectStreamState::Connected { stream }, - } - } -} - -impl SseAutoReconnectStream> { - #[allow(dead_code)] - pub(crate) fn never_reconnect(stream: BoxedSseResponse, error_when_reconnect: E) -> Self { - Self { - retry_policy: Arc::new(NeverRetry), - last_event_id: None, - server_retry_interval: None, - connector: NeverReconnect { - error: Some(error_when_reconnect), - }, - state: SseAutoReconnectStreamState::Connected { stream }, - } - } -} - -pin_project_lite::pin_project! { - #[project = SseAutoReconnectStreamStateProj] - pub enum SseAutoReconnectStreamState { - Connected { - #[pin] - stream: BoxedSseResponse, - }, - Retrying { - retry_times: usize, - #[pin] - retrying: F, - }, - WaitingNextRetry { - #[pin] - sleep: tokio::time::Sleep, - retry_times: usize, - }, - Terminated, - } -} - -impl Stream for SseAutoReconnectStream -where - R: SseStreamReconnect, -{ - type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - let mut this = self.as_mut().project(); - // let this_state = this.state.as_mut().project() - let state = this.state.as_mut().project(); - let next_state = match state { - SseAutoReconnectStreamStateProj::Connected { stream } => { - match ready!(stream.poll_next(cx)) { - Some(Ok(sse)) => { - if let Some(new_server_retry) = sse.retry { - *this.server_retry_interval = - Some(Duration::from_millis(new_server_retry)); - } - if let Some(ref event_id) = sse.id { - *this.last_event_id = Some(event_id.clone()); - } - // Only treat blank/`message` events as JSON-RPC payloads. - // Other control frames (endpoint, ping, etc.) are passed to - // the reconnection handler. - let is_message_event = - matches!(sse.event.as_deref(), None | Some("") | Some("message")); - if !is_message_event { - match this.connector.handle_control_event(&sse) { - Ok(()) => return self.poll_next(cx), - Err(e) => { - this.state.set(SseAutoReconnectStreamState::Terminated); - return Poll::Ready(Some(Err(e))); - } - } - } - if let Some(data) = sse.data { - match serde_json::from_str::(&data) { - Err(e) => { - // Downgrade to debug to avoid noisy logs when servers emit - // non-JSON payloads as message frames. Include last_event_id - // to aid troubleshooting while keeping default behaviour. - let last_id = this.last_event_id.as_deref().unwrap_or(""); - tracing::debug!(last_event_id=%last_id, "failed to deserialize server message: {e}"); - return self.poll_next(cx); - } - Ok(message) => { - return Poll::Ready(Some(Ok(message))); - } - }; - } else { - return self.poll_next(cx); - } - } - Some(Err(e)) => { - this.connector - .handle_stream_error(&e, this.last_event_id.as_deref()); - let retrying = this - .connector - .retry_connection(this.last_event_id.as_deref()); - SseAutoReconnectStreamState::Retrying { - retry_times: 0, - retrying, - } - } - None => { - tracing::debug!("sse stream terminated"); - return Poll::Ready(None); - } - } - } - SseAutoReconnectStreamStateProj::Retrying { - retry_times, - retrying, - } => { - let retry_result = ready!(retrying.poll(cx)); - match retry_result { - Ok(new_stream) => SseAutoReconnectStreamState::Connected { stream: new_stream }, - Err(e) => { - tracing::debug!("retry sse stream error: {e}"); - *retry_times += 1; - if let Some(interval) = this.retry_policy.retry(*retry_times) { - let interval = this - .server_retry_interval - .map(|server_retry_interval| server_retry_interval.max(interval)) - .unwrap_or(interval); - let sleep = tokio::time::sleep(interval); - SseAutoReconnectStreamState::WaitingNextRetry { - sleep, - retry_times: *retry_times, - } - } else { - tracing::error!("sse stream error: {e}, max retry times reached"); - this.state.set(SseAutoReconnectStreamState::Terminated); - return Poll::Ready(Some(Err(e))); - } - } - } - } - SseAutoReconnectStreamStateProj::WaitingNextRetry { sleep, retry_times } => { - ready!(sleep.poll(cx)); - let retrying = this - .connector - .retry_connection(this.last_event_id.as_deref()); - let retry_times = *retry_times; - SseAutoReconnectStreamState::Retrying { - retry_times, - retrying, - } - } - SseAutoReconnectStreamStateProj::Terminated => { - return Poll::Ready(None); - } - }; - // update the state - this.state.set(next_state); - self.poll_next(cx) - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/common/http_header.rs b/codex-rs/vendor/rmcp/src/transport/common/http_header.rs deleted file mode 100644 index 84bc7bfb2c..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/common/http_header.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub const HEADER_SESSION_ID: &str = "Mcp-Session-Id"; -pub const HEADER_LAST_EVENT_ID: &str = "Last-Event-Id"; -pub const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream"; -pub const JSON_MIME_TYPE: &str = "application/json"; diff --git a/codex-rs/vendor/rmcp/src/transport/common/reqwest.rs b/codex-rs/vendor/rmcp/src/transport/common/reqwest.rs deleted file mode 100644 index a51fa9552d..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/common/reqwest.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[cfg(feature = "transport-streamable-http-client-reqwest")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-client-reqwest")))] -mod streamable_http_client; diff --git a/codex-rs/vendor/rmcp/src/transport/common/reqwest/streamable_http_client.rs b/codex-rs/vendor/rmcp/src/transport/common/reqwest/streamable_http_client.rs deleted file mode 100644 index 0ecbad20d8..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/common/reqwest/streamable_http_client.rs +++ /dev/null @@ -1,199 +0,0 @@ -use std::{borrow::Cow, sync::Arc}; - -use futures::{StreamExt, stream::BoxStream}; -use http::header::WWW_AUTHENTICATE; -use reqwest::header::ACCEPT; -use sse_stream::{Sse, SseStream}; - -use crate::{ - model::{ClientJsonRpcMessage, ServerJsonRpcMessage}, - transport::{ - common::http_header::{ - EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_SESSION_ID, JSON_MIME_TYPE, - }, - streamable_http_client::*, - }, -}; - -impl From for StreamableHttpError { - fn from(e: reqwest::Error) -> Self { - StreamableHttpError::Client(e) - } -} - -impl StreamableHttpClient for reqwest::Client { - type Error = reqwest::Error; - - async fn get_stream( - &self, - uri: Arc, - session_id: Arc, - last_event_id: Option, - auth_token: Option, - ) -> Result>, StreamableHttpError> { - let mut request_builder = self - .get(uri.as_ref()) - .header(ACCEPT, [EVENT_STREAM_MIME_TYPE, JSON_MIME_TYPE].join(", ")) - .header(HEADER_SESSION_ID, session_id.as_ref()); - if let Some(last_event_id) = last_event_id { - request_builder = request_builder.header(HEADER_LAST_EVENT_ID, last_event_id); - } - if let Some(auth_header) = auth_token { - request_builder = request_builder.bearer_auth(auth_header); - } - let response = request_builder.send().await?; - if response.status() == reqwest::StatusCode::METHOD_NOT_ALLOWED { - return Err(StreamableHttpError::ServerDoesNotSupportSse); - } - let response = response.error_for_status()?; - match response.headers().get(reqwest::header::CONTENT_TYPE) { - Some(ct) => { - if !ct.as_bytes().starts_with(EVENT_STREAM_MIME_TYPE.as_bytes()) - && !ct.as_bytes().starts_with(JSON_MIME_TYPE.as_bytes()) - { - return Err(StreamableHttpError::UnexpectedContentType(Some( - String::from_utf8_lossy(ct.as_bytes()).to_string(), - ))); - } - } - None => { - return Err(StreamableHttpError::UnexpectedContentType(None)); - } - } - let event_stream = SseStream::from_byte_stream(response.bytes_stream()).boxed(); - Ok(event_stream) - } - - async fn delete_session( - &self, - uri: Arc, - session: Arc, - auth_token: Option, - ) -> Result<(), StreamableHttpError> { - let mut request_builder = self.delete(uri.as_ref()); - if let Some(auth_header) = auth_token { - request_builder = request_builder.bearer_auth(auth_header); - } - let response = request_builder - .header(HEADER_SESSION_ID, session.as_ref()) - .send() - .await?; - - // if method no allowed - if response.status() == reqwest::StatusCode::METHOD_NOT_ALLOWED { - tracing::debug!("this server doesn't support deleting session"); - return Ok(()); - } - let _response = response.error_for_status()?; - Ok(()) - } - - async fn post_message( - &self, - uri: Arc, - message: ClientJsonRpcMessage, - session_id: Option>, - auth_token: Option, - ) -> Result> { - let mut request = self - .post(uri.as_ref()) - .header(ACCEPT, [EVENT_STREAM_MIME_TYPE, JSON_MIME_TYPE].join(", ")); - if let Some(auth_header) = auth_token { - request = request.bearer_auth(auth_header); - } - if let Some(session_id) = session_id { - request = request.header(HEADER_SESSION_ID, session_id.as_ref()); - } - let response = request.json(&message).send().await?; - if response.status() == reqwest::StatusCode::UNAUTHORIZED { - if let Some(header) = response.headers().get(WWW_AUTHENTICATE) { - let header = header - .to_str() - .map_err(|_| { - StreamableHttpError::UnexpectedServerResponse(Cow::from( - "invalid www-authenticate header value", - )) - })? - .to_string(); - return Err(StreamableHttpError::AuthRequired(AuthRequiredError { - www_authenticate_header: header, - })); - } - } - let status = response.status(); - if matches!( - status, - reqwest::StatusCode::ACCEPTED | reqwest::StatusCode::NO_CONTENT - ) { - return Ok(StreamableHttpPostResponse::Accepted); - } - let content_type = response.headers().get(reqwest::header::CONTENT_TYPE); - let session_id = response.headers().get(HEADER_SESSION_ID); - let session_id = session_id - .and_then(|v| v.to_str().ok()) - .map(|s| s.to_string()); - match content_type { - Some(ct) if ct.as_bytes().starts_with(EVENT_STREAM_MIME_TYPE.as_bytes()) => { - let event_stream = SseStream::from_byte_stream(response.bytes_stream()).boxed(); - Ok(StreamableHttpPostResponse::Sse(event_stream, session_id)) - } - Some(ct) if ct.as_bytes().starts_with(JSON_MIME_TYPE.as_bytes()) => { - let message: ServerJsonRpcMessage = response.json().await?; - Ok(StreamableHttpPostResponse::Json(message, session_id)) - } - _ => { - // unexpected content type - tracing::error!("unexpected content type: {:?}", content_type); - Err(StreamableHttpError::UnexpectedContentType( - content_type.map(|ct| String::from_utf8_lossy(ct.as_bytes()).to_string()), - )) - } - } - } -} - -impl StreamableHttpClientTransport { - /// Creates a new transport using reqwest with the specified URI. - /// - /// This is a convenience method that creates a transport using the default - /// reqwest client. This method is only available when the - /// `transport-streamable-http-client-reqwest` feature is enabled. - /// - /// # Arguments - /// - /// * `uri` - The server URI to connect to - /// - /// # Example - /// - /// ```rust,no_run - /// use rmcp::transport::StreamableHttpClientTransport; - /// - /// // Enable the reqwest feature in Cargo.toml: - /// // rmcp = { version = "0.5", features = ["transport-streamable-http-client-reqwest"] } - /// - /// let transport = StreamableHttpClientTransport::from_uri("http://localhost:8000/mcp"); - /// ``` - /// - /// # Feature requirement - /// - /// This method requires the `transport-streamable-http-client-reqwest` feature. - pub fn from_uri(uri: impl Into>) -> Self { - StreamableHttpClientTransport::with_client( - reqwest::Client::default(), - StreamableHttpClientTransportConfig { - uri: uri.into(), - auth_header: None, - ..Default::default() - }, - ) - } - - /// Build this transport form a config - /// - /// # Arguments - /// - /// * `config` - The config to use with this transport - pub fn from_config(config: StreamableHttpClientTransportConfig) -> Self { - StreamableHttpClientTransport::with_client(reqwest::Client::default(), config) - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/common/server_side_http.rs b/codex-rs/vendor/rmcp/src/transport/common/server_side_http.rs deleted file mode 100644 index efa50fd094..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/common/server_side_http.rs +++ /dev/null @@ -1,170 +0,0 @@ -#![allow(dead_code)] -use std::{convert::Infallible, fmt::Display, sync::Arc, time::Duration}; - -use bytes::{Buf, Bytes}; -use http::Response; -use http_body::Body; -use http_body_util::{BodyExt, Empty, Full, combinators::BoxBody}; -use sse_stream::{KeepAlive, Sse, SseBody}; -use tokio_util::sync::CancellationToken; - -use super::http_header::EVENT_STREAM_MIME_TYPE; -use crate::model::{ClientJsonRpcMessage, ServerJsonRpcMessage}; - -pub type SessionId = Arc; - -pub fn session_id() -> SessionId { - uuid::Uuid::new_v4().to_string().into() -} - -pub const DEFAULT_AUTO_PING_INTERVAL: Duration = Duration::from_secs(15); - -pub(crate) type BoxResponse = Response>; - -pub(crate) fn accepted_response() -> Response> { - Response::builder() - .status(http::StatusCode::ACCEPTED) - .body(Empty::new().boxed()) - .expect("valid response") -} -pin_project_lite::pin_project! { - struct TokioTimer { - #[pin] - sleep: tokio::time::Sleep, - } -} -impl Future for TokioTimer { - type Output = (); - - fn poll( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - let this = self.project(); - this.sleep.poll(cx) - } -} -impl sse_stream::Timer for TokioTimer { - fn from_duration(duration: Duration) -> Self { - Self { - sleep: tokio::time::sleep(duration), - } - } - - fn reset(self: std::pin::Pin<&mut Self>, when: std::time::Instant) { - let this = self.project(); - this.sleep.reset(tokio::time::Instant::from_std(when)); - } -} - -#[derive(Debug, Clone)] -pub struct ServerSseMessage { - /// The event ID for this message. When set, clients can use this ID - /// with the `Last-Event-ID` header to resume the stream from this point. - pub event_id: Option, - /// The JSON-RPC message content. Set to `None` for priming events. - /// See [SEP-1699](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699) - pub message: Option>, - /// The retry interval hint for clients. Clients should wait this duration - /// before attempting to reconnect. This maps to the SSE `retry:` field. - pub retry: Option, -} - -pub(crate) fn sse_stream_response( - stream: impl futures::Stream + Send + Sync + 'static, - keep_alive: Option, - ct: CancellationToken, -) -> Response> { - use futures::StreamExt; - let stream = stream - .map(|message| { - let mut sse = if let Some(ref msg) = message.message { - let data = serde_json::to_string(msg.as_ref()).expect("valid message"); - Sse::default().data(data) - } else { - // Priming event: empty data per SEP-1699 (just "data:\n") - Sse::default().data("") - }; - - sse.id = message.event_id; - - if let Some(retry) = message.retry { - sse.retry = Some(retry.as_millis() as u64); - } - - Result::::Ok(sse) - }) - .take_until(async move { ct.cancelled().await }); - let stream = SseBody::new(stream); - - let stream = match keep_alive { - Some(duration) => stream - .with_keep_alive::(KeepAlive::new().interval(duration)) - .boxed(), - None => stream.boxed(), - }; - - Response::builder() - .status(http::StatusCode::OK) - .header(http::header::CONTENT_TYPE, EVENT_STREAM_MIME_TYPE) - .header(http::header::CACHE_CONTROL, "no-cache") - .body(stream) - .expect("valid response") -} - -pub(crate) const fn internal_error_response( - context: &str, -) -> impl FnOnce(E) -> Response> { - move |error| { - tracing::error!("Internal server error when {context}: {error}"); - Response::builder() - .status(http::StatusCode::INTERNAL_SERVER_ERROR) - .body( - Full::new(Bytes::from(format!( - "Encounter an error when {context}: {error}" - ))) - .boxed(), - ) - .expect("valid response") - } -} - -pub(crate) fn unexpected_message_response(expect: &str) -> Response> { - Response::builder() - .status(http::StatusCode::UNPROCESSABLE_ENTITY) - .body(Full::new(Bytes::from(format!("Unexpected message, expect {expect}"))).boxed()) - .expect("valid response") -} - -pub(crate) async fn expect_json( - body: B, -) -> Result>> -where - B: Body + Send + 'static, - B::Error: Display, -{ - match body.collect().await { - Ok(bytes) => { - match serde_json::from_reader::<_, ClientJsonRpcMessage>(bytes.aggregate().reader()) { - Ok(message) => Ok(message), - Err(e) => { - let response = Response::builder() - .status(http::StatusCode::UNSUPPORTED_MEDIA_TYPE) - .body( - Full::new(Bytes::from(format!("fail to deserialize request body {e}"))) - .boxed(), - ) - .expect("valid response"); - Err(response) - } - } - } - Err(e) => { - let response = Response::builder() - .status(http::StatusCode::INTERNAL_SERVER_ERROR) - .body(Full::new(Bytes::from(format!("Failed to read request body: {e}"))).boxed()) - .expect("valid response"); - Err(response) - } - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/io.rs b/codex-rs/vendor/rmcp/src/transport/io.rs deleted file mode 100644 index adc2dba0ef..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/io.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// # StdIO Transport -/// -/// Create a pair of [`tokio::io::Stdin`] and [`tokio::io::Stdout`]. -pub fn stdio() -> (tokio::io::Stdin, tokio::io::Stdout) { - (tokio::io::stdin(), tokio::io::stdout()) -} diff --git a/codex-rs/vendor/rmcp/src/transport/sink_stream.rs b/codex-rs/vendor/rmcp/src/transport/sink_stream.rs deleted file mode 100644 index f31743922b..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/sink_stream.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::sync::Arc; - -use futures::{Sink, Stream}; -use tokio::sync::Mutex; - -use super::{IntoTransport, Transport}; -use crate::service::{RxJsonRpcMessage, ServiceRole, TxJsonRpcMessage}; - -pub struct SinkStreamTransport { - stream: St, - sink: Arc>, -} - -impl SinkStreamTransport { - pub fn new(sink: Si, stream: St) -> Self { - Self { - stream, - sink: Arc::new(Mutex::new(sink)), - } - } -} - -impl Transport for SinkStreamTransport -where - St: Send + Stream> + Unpin, - Si: Send + Sink> + Unpin + 'static, - Si::Error: std::error::Error + Send + Sync + 'static, -{ - type Error = Si::Error; - - fn send( - &mut self, - item: TxJsonRpcMessage, - ) -> impl Future> + Send + 'static { - use futures::SinkExt; - let lock = self.sink.clone(); - async move { - let mut write = lock.lock().await; - write.send(item).await - } - } - - fn receive(&mut self) -> impl Future>> { - use futures::StreamExt; - self.stream.next() - } - - async fn close(&mut self) -> Result<(), Self::Error> { - Ok(()) - } -} - -pub enum TransportAdapterSinkStream {} - -impl IntoTransport for (Si, St) -where - Role: ServiceRole, - Si: Send + Sink> + Unpin + 'static, - St: Send + Stream> + Unpin + 'static, - Si::Error: std::error::Error + Send + Sync + 'static, -{ - fn into_transport(self) -> impl Transport + 'static { - SinkStreamTransport::new(self.0, self.1) - } -} - -pub enum TransportAdapterAsyncCombinedRW {} -impl IntoTransport for S -where - Role: ServiceRole, - S: Sink> + Stream> + Send + 'static, - S::Error: std::error::Error + Send + Sync + 'static, -{ - fn into_transport(self) -> impl Transport + 'static { - use futures::StreamExt; - IntoTransport::::into_transport(self.split()) - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/streamable_http_client.rs b/codex-rs/vendor/rmcp/src/transport/streamable_http_client.rs deleted file mode 100644 index 35140c1b2b..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/streamable_http_client.rs +++ /dev/null @@ -1,774 +0,0 @@ -use std::{borrow::Cow, sync::Arc, time::Duration}; - -use futures::{Stream, StreamExt, future::BoxFuture, stream::BoxStream}; -pub use sse_stream::Error as SseError; -use sse_stream::Sse; -use thiserror::Error; -use tokio_util::sync::CancellationToken; -use tracing::debug; - -use super::common::client_side_sse::{ExponentialBackoff, SseRetryPolicy, SseStreamReconnect}; -use crate::{ - RoleClient, - model::{ClientJsonRpcMessage, ServerJsonRpcMessage}, - transport::{ - common::client_side_sse::SseAutoReconnectStream, - worker::{Worker, WorkerQuitReason, WorkerSendRequest, WorkerTransport}, - }, -}; - -type BoxedSseStream = BoxStream<'static, Result>; - -#[derive(Debug)] -pub struct AuthRequiredError { - pub www_authenticate_header: String, -} - -#[derive(Error, Debug)] -pub enum StreamableHttpError { - #[error("SSE error: {0}")] - Sse(#[from] SseError), - #[error("Io error: {0}")] - Io(#[from] std::io::Error), - #[error("Client error: {0}")] - Client(E), - #[error("unexpected end of stream")] - UnexpectedEndOfStream, - #[error("unexpected server response: {0}")] - UnexpectedServerResponse(Cow<'static, str>), - #[error("Unexpected content type: {0:?}")] - UnexpectedContentType(Option), - #[error("Server does not support SSE")] - ServerDoesNotSupportSse, - #[error("Server does not support delete session")] - ServerDoesNotSupportDeleteSession, - #[error("Tokio join error: {0}")] - TokioJoinError(#[from] tokio::task::JoinError), - #[error("Deserialize error: {0}")] - Deserialize(#[from] serde_json::Error), - #[error("Transport channel closed")] - TransportChannelClosed, - #[error("Missing session id in HTTP response")] - MissingSessionIdInResponse, - #[cfg(feature = "auth")] - #[cfg_attr(docsrs, doc(cfg(feature = "auth")))] - #[error("Auth error: {0}")] - Auth(#[from] crate::transport::auth::AuthError), - #[error("Auth required")] - AuthRequired(AuthRequiredError), -} - -#[derive(Debug, Clone, Error)] -pub enum StreamableHttpProtocolError { - #[error("Missing session id in response")] - MissingSessionIdInResponse, -} - -#[allow(clippy::large_enum_variant)] -pub enum StreamableHttpPostResponse { - Accepted, - Json(ServerJsonRpcMessage, Option), - Sse(BoxedSseStream, Option), -} - -impl std::fmt::Debug for StreamableHttpPostResponse { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Accepted => write!(f, "Accepted"), - Self::Json(arg0, arg1) => f.debug_tuple("Json").field(arg0).field(arg1).finish(), - Self::Sse(_, arg1) => f.debug_tuple("Sse").field(arg1).finish(), - } - } -} - -impl StreamableHttpPostResponse { - pub async fn expect_initialized( - self, - ) -> Result<(ServerJsonRpcMessage, Option), StreamableHttpError> - where - E: std::error::Error + Send + Sync + 'static, - { - match self { - Self::Json(message, session_id) => Ok((message, session_id)), - Self::Sse(mut stream, session_id) => { - while let Some(event) = stream.next().await { - let event = event?; - let payload = event.data.unwrap_or_default(); - if payload.trim().is_empty() { - continue; - } - - let message: ServerJsonRpcMessage = serde_json::from_str(&payload)?; - - if matches!(message, ServerJsonRpcMessage::Response(_)) { - return Ok((message, session_id)); - } - - debug!( - ?message, - "received message before initialize response; continuing to drain stream" - ); - } - - Err(StreamableHttpError::UnexpectedServerResponse( - "empty sse stream".into(), - )) - } - _ => Err(StreamableHttpError::UnexpectedServerResponse( - "expect initialized, accepted".into(), - )), - } - } - - pub fn expect_json(self) -> Result> - where - E: std::error::Error + Send + Sync + 'static, - { - match self { - Self::Json(message, ..) => Ok(message), - got => Err(StreamableHttpError::UnexpectedServerResponse( - format!("expect json, got {got:?}").into(), - )), - } - } - - pub fn expect_accepted(self) -> Result<(), StreamableHttpError> - where - E: std::error::Error + Send + Sync + 'static, - { - match self { - Self::Accepted => Ok(()), - got => Err(StreamableHttpError::UnexpectedServerResponse( - format!("expect accepted, got {got:?}").into(), - )), - } - } -} - -pub trait StreamableHttpClient: Clone + Send + 'static { - type Error: std::error::Error + Send + Sync + 'static; - fn post_message( - &self, - uri: Arc, - message: ClientJsonRpcMessage, - session_id: Option>, - auth_header: Option, - ) -> impl Future>> - + Send - + '_; - fn delete_session( - &self, - uri: Arc, - session_id: Arc, - auth_header: Option, - ) -> impl Future>> + Send + '_; - fn get_stream( - &self, - uri: Arc, - session_id: Arc, - last_event_id: Option, - auth_header: Option, - ) -> impl Future< - Output = Result< - BoxStream<'static, Result>, - StreamableHttpError, - >, - > + Send - + '_; -} - -pub struct RetryConfig { - pub max_times: Option, - pub min_duration: Duration, -} - -struct StreamableHttpClientReconnect { - pub client: C, - pub session_id: Arc, - pub uri: Arc, - pub auth_header: Option, -} - -impl SseStreamReconnect for StreamableHttpClientReconnect { - type Error = StreamableHttpError; - type Future = BoxFuture<'static, Result>; - fn retry_connection(&mut self, last_event_id: Option<&str>) -> Self::Future { - let client = self.client.clone(); - let uri = self.uri.clone(); - let session_id = self.session_id.clone(); - let auth_header = self.auth_header.clone(); - let last_event_id = last_event_id.map(|s| s.to_owned()); - Box::pin(async move { - client - .get_stream(uri, session_id, last_event_id, auth_header) - .await - }) - } -} - -#[derive(Debug, Clone, Default)] -pub struct StreamableHttpClientWorker { - pub client: C, - pub config: StreamableHttpClientTransportConfig, -} - -impl StreamableHttpClientWorker { - pub fn new_simple(url: impl Into>) -> Self { - Self { - client: C::default(), - config: StreamableHttpClientTransportConfig { - uri: url.into(), - ..Default::default() - }, - } - } -} - -impl StreamableHttpClientWorker { - pub fn new(client: C, config: StreamableHttpClientTransportConfig) -> Self { - Self { client, config } - } -} - -impl StreamableHttpClientWorker { - async fn execute_sse_stream( - sse_stream: impl Stream>> - + Send - + 'static, - sse_worker_tx: tokio::sync::mpsc::Sender, - close_on_response: bool, - ct: CancellationToken, - ) -> Result<(), StreamableHttpError> { - let mut sse_stream = std::pin::pin!(sse_stream); - loop { - let message = tokio::select! { - event = sse_stream.next() => { - event - } - _ = ct.cancelled() => { - tracing::debug!("cancelled"); - break; - } - }; - let Some(message) = message.transpose()? else { - break; - }; - let is_response = matches!(message, ServerJsonRpcMessage::Response(_)); - let yield_result = sse_worker_tx.send(message).await; - if yield_result.is_err() { - tracing::trace!("streamable http transport worker dropped, exiting"); - break; - } - if close_on_response && is_response { - tracing::debug!("got response, closing sse stream"); - break; - } - } - Ok(()) - } -} - -impl Worker for StreamableHttpClientWorker { - type Role = RoleClient; - type Error = StreamableHttpError; - fn err_closed() -> Self::Error { - StreamableHttpError::TransportChannelClosed - } - fn err_join(e: tokio::task::JoinError) -> Self::Error { - StreamableHttpError::TokioJoinError(e) - } - fn config(&self) -> super::worker::WorkerConfig { - super::worker::WorkerConfig { - name: Some("StreamableHttpClientWorker".into()), - channel_buffer_capacity: self.config.channel_buffer_capacity, - } - } - async fn run( - self, - mut context: super::worker::WorkerContext, - ) -> Result<(), WorkerQuitReason> { - let channel_buffer_capacity = self.config.channel_buffer_capacity; - let (sse_worker_tx, mut sse_worker_rx) = - tokio::sync::mpsc::channel::(channel_buffer_capacity); - let config = self.config.clone(); - let transport_task_ct = context.cancellation_token.clone(); - let _drop_guard = transport_task_ct.clone().drop_guard(); - let WorkerSendRequest { - responder, - message: initialize_request, - } = context.recv_from_handler().await?; - let (message, session_id) = match self - .client - .post_message( - config.uri.clone(), - initialize_request, - None, - self.config.auth_header, - ) - .await - { - Ok(res) => { - let _ = responder.send(Ok(())); - res.expect_initialized::().await.map_err( - WorkerQuitReason::fatal_context("process initialize response"), - )? - } - Err(err) => { - let msg = format!("{:?}", err); - let _ = responder.send(Err(err)); - return Err(WorkerQuitReason::fatal( - StreamableHttpError::TransportChannelClosed, - msg, - )); - } - }; - let session_id: Option> = if let Some(session_id) = session_id { - Some(session_id.into()) - } else { - if !self.config.allow_stateless { - return Err(WorkerQuitReason::fatal( - StreamableHttpError::::MissingSessionIdInResponse, - "process initialize response", - )); - } - None - }; - // Store session info for cleanup when run() exits (not spawned, so cleanup completes before close() returns) - let session_cleanup_info = session_id.as_ref().map(|sid| { - ( - self.client.clone(), - config.uri.clone(), - sid.clone(), - config.auth_header.clone(), - ) - }); - - context.send_to_handler(message).await?; - let initialized_notification = context.recv_from_handler().await?; - // expect a initialized response - self.client - .post_message( - config.uri.clone(), - initialized_notification.message, - session_id.clone(), - config.auth_header.clone(), - ) - .await - .map_err(WorkerQuitReason::fatal_context( - "send initialized notification", - ))? - .expect_accepted::() - .map_err(WorkerQuitReason::fatal_context( - "process initialized notification response", - ))?; - let _ = initialized_notification.responder.send(Ok(())); - #[allow(clippy::large_enum_variant)] - enum Event { - ClientMessage(WorkerSendRequest), - ServerMessage(ServerJsonRpcMessage), - StreamResult(Result<(), StreamableHttpError>), - } - let mut streams = tokio::task::JoinSet::new(); - if let Some(session_id) = &session_id { - let client = self.client.clone(); - let uri = config.uri.clone(); - let session_id = session_id.clone(); - let auth_header = config.auth_header.clone(); - let retry_config = self.config.retry_config.clone(); - let sse_worker_tx = sse_worker_tx.clone(); - let transport_task_ct = transport_task_ct.clone(); - let config_uri = config.uri.clone(); - let config_auth_header = config.auth_header.clone(); - - streams.spawn(async move { - match client - .get_stream(uri.clone(), session_id.clone(), None, auth_header.clone()) - .await - { - Ok(stream) => { - let sse_stream = SseAutoReconnectStream::new( - stream, - StreamableHttpClientReconnect { - client: client.clone(), - session_id: session_id.clone(), - uri: config_uri, - auth_header: config_auth_header, - }, - retry_config, - ); - Self::execute_sse_stream( - sse_stream, - sse_worker_tx, - false, - transport_task_ct.child_token(), - ) - .await - } - Err(StreamableHttpError::ServerDoesNotSupportSse) => { - tracing::debug!("server doesn't support sse, skip common stream"); - Ok(()) - } - Err(e) => { - // fail to get common stream - tracing::error!("fail to get common stream: {e}"); - Err(e) - } - } - }); - } - // Main event loop - capture exit reason so we can do cleanup before returning - let loop_result: Result<(), WorkerQuitReason> = 'main_loop: loop { - let event = tokio::select! { - _ = transport_task_ct.cancelled() => { - tracing::debug!("cancelled"); - break 'main_loop Err(WorkerQuitReason::Cancelled); - } - message = context.recv_from_handler() => { - match message { - Ok(msg) => Event::ClientMessage(msg), - Err(e) => break 'main_loop Err(e), - } - }, - message = sse_worker_rx.recv() => { - let Some(message) = message else { - tracing::trace!("transport dropped, exiting"); - break 'main_loop Err(WorkerQuitReason::HandlerTerminated); - }; - Event::ServerMessage(message) - }, - terminated_stream = streams.join_next(), if !streams.is_empty() => { - match terminated_stream { - Some(result) => { - Event::StreamResult(result.map_err(StreamableHttpError::TokioJoinError).and_then(std::convert::identity)) - } - None => { - continue - } - } - } - }; - match event { - Event::ClientMessage(send_request) => { - let WorkerSendRequest { message, responder } = send_request; - let response = self - .client - .post_message( - config.uri.clone(), - message, - session_id.clone(), - config.auth_header.clone(), - ) - .await; - let send_result = match response { - Err(e) => Err(e), - Ok(StreamableHttpPostResponse::Accepted) => { - tracing::trace!("client message accepted"); - Ok(()) - } - Ok(StreamableHttpPostResponse::Json(message, ..)) => { - context.send_to_handler(message).await?; - Ok(()) - } - Ok(StreamableHttpPostResponse::Sse(stream, ..)) => { - if let Some(session_id) = &session_id { - let sse_stream = SseAutoReconnectStream::new( - stream, - StreamableHttpClientReconnect { - client: self.client.clone(), - session_id: session_id.clone(), - uri: config.uri.clone(), - auth_header: config.auth_header.clone(), - }, - self.config.retry_config.clone(), - ); - streams.spawn(Self::execute_sse_stream( - sse_stream, - sse_worker_tx.clone(), - true, - transport_task_ct.child_token(), - )); - } else { - let sse_stream = SseAutoReconnectStream::never_reconnect( - stream, - StreamableHttpError::::UnexpectedEndOfStream, - ); - streams.spawn(Self::execute_sse_stream( - sse_stream, - sse_worker_tx.clone(), - true, - transport_task_ct.child_token(), - )); - } - tracing::trace!("got new sse stream"); - Ok(()) - } - }; - let _ = responder.send(send_result); - } - Event::ServerMessage(json_rpc_message) => { - // send the message to the handler - if let Err(e) = context.send_to_handler(json_rpc_message).await { - break 'main_loop Err(e); - } - } - Event::StreamResult(result) => { - if result.is_err() { - tracing::warn!( - "sse client event stream terminated with error: {:?}", - result - ); - } - } - } - }; - - // Cleanup session before returning (ensures close() waits for session deletion) - // Use a timeout to prevent indefinite hangs if the server is unresponsive - if let Some((client, url, session_id, auth_header)) = session_cleanup_info { - const SESSION_CLEANUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); - match tokio::time::timeout( - SESSION_CLEANUP_TIMEOUT, - client.delete_session(url, session_id.clone(), auth_header), - ) - .await - { - Ok(Ok(_)) => { - tracing::info!(session_id = session_id.as_ref(), "delete session success") - } - Ok(Err(StreamableHttpError::ServerDoesNotSupportDeleteSession)) => { - tracing::info!( - session_id = session_id.as_ref(), - "server doesn't support delete session" - ) - } - Ok(Err(e)) => { - tracing::error!( - session_id = session_id.as_ref(), - "fail to delete session: {e}" - ); - } - Err(_elapsed) => { - tracing::warn!( - session_id = session_id.as_ref(), - "session cleanup timed out after {:?}", - SESSION_CLEANUP_TIMEOUT - ); - } - } - } - - loop_result - } -} - -/// A client-agnostic HTTP transport for RMCP that supports streaming responses. -/// -/// This transport allows you to choose your preferred HTTP client implementation -/// by implementing the [`StreamableHttpClient`] trait. The transport handles -/// session management, SSE streaming, and automatic reconnection. -/// -/// # Usage -/// -/// ## Using reqwest -/// -/// ```rust,no_run -/// use rmcp::transport::StreamableHttpClientTransport; -/// -/// // Enable the reqwest feature in Cargo.toml: -/// // rmcp = { version = "0.5", features = ["transport-streamable-http-client-reqwest"] } -/// -/// let transport = StreamableHttpClientTransport::from_uri("http://localhost:8000/mcp"); -/// ``` -/// -/// ## Using a custom HTTP client -/// -/// ```rust,no_run -/// use rmcp::transport::streamable_http_client::{ -/// StreamableHttpClient, -/// StreamableHttpClientTransport, -/// StreamableHttpClientTransportConfig -/// }; -/// use std::sync::Arc; -/// use futures::stream::BoxStream; -/// use rmcp::model::ClientJsonRpcMessage; -/// use sse_stream::{Sse, Error as SseError}; -/// -/// #[derive(Clone)] -/// struct MyHttpClient; -/// -/// #[derive(Debug, thiserror::Error)] -/// struct MyError; -/// -/// impl std::fmt::Display for MyError { -/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -/// write!(f, "MyError") -/// } -/// } -/// -/// impl StreamableHttpClient for MyHttpClient { -/// type Error = MyError; -/// -/// async fn post_message( -/// &self, -/// _uri: Arc, -/// _message: ClientJsonRpcMessage, -/// _session_id: Option>, -/// _auth_header: Option, -/// ) -> Result> { -/// todo!() -/// } -/// -/// async fn delete_session( -/// &self, -/// _uri: Arc, -/// _session_id: Arc, -/// _auth_header: Option, -/// ) -> Result<(), rmcp::transport::streamable_http_client::StreamableHttpError> { -/// todo!() -/// } -/// -/// async fn get_stream( -/// &self, -/// _uri: Arc, -/// _session_id: Arc, -/// _last_event_id: Option, -/// _auth_header: Option, -/// ) -> Result>, rmcp::transport::streamable_http_client::StreamableHttpError> { -/// todo!() -/// } -/// } -/// -/// let transport = StreamableHttpClientTransport::with_client( -/// MyHttpClient, -/// StreamableHttpClientTransportConfig::with_uri("http://localhost:8000/mcp") -/// ); -/// ``` -/// -/// # Feature Flags -/// -/// - `transport-streamable-http-client`: Base feature providing the generic transport infrastructure -/// - `transport-streamable-http-client-reqwest`: Includes reqwest HTTP client support with convenience methods -pub type StreamableHttpClientTransport = WorkerTransport>; - -impl StreamableHttpClientTransport { - /// Creates a new transport with a custom HTTP client implementation. - /// - /// This method allows you to use any HTTP client that implements the [`StreamableHttpClient`] trait. - /// Use this when you want to use a custom HTTP client or when the reqwest feature is not enabled. - /// - /// # Arguments - /// - /// * `client` - Your HTTP client implementation - /// * `config` - Transport configuration including the server URI - /// - /// # Example - /// - /// ```rust,no_run - /// use rmcp::transport::streamable_http_client::{ - /// StreamableHttpClient, - /// StreamableHttpClientTransport, - /// StreamableHttpClientTransportConfig - /// }; - /// use std::sync::Arc; - /// use futures::stream::BoxStream; - /// use rmcp::model::ClientJsonRpcMessage; - /// use sse_stream::{Sse, Error as SseError}; - /// - /// // Define your custom client - /// #[derive(Clone)] - /// struct MyHttpClient; - /// - /// #[derive(Debug, thiserror::Error)] - /// struct MyError; - /// - /// impl std::fmt::Display for MyError { - /// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - /// write!(f, "MyError") - /// } - /// } - /// - /// impl StreamableHttpClient for MyHttpClient { - /// type Error = MyError; - /// - /// async fn post_message( - /// &self, - /// _uri: Arc, - /// _message: ClientJsonRpcMessage, - /// _session_id: Option>, - /// _auth_header: Option, - /// ) -> Result> { - /// todo!() - /// } - /// - /// async fn delete_session( - /// &self, - /// _uri: Arc, - /// _session_id: Arc, - /// _auth_header: Option, - /// ) -> Result<(), rmcp::transport::streamable_http_client::StreamableHttpError> { - /// todo!() - /// } - /// - /// async fn get_stream( - /// &self, - /// _uri: Arc, - /// _session_id: Arc, - /// _last_event_id: Option, - /// _auth_header: Option, - /// ) -> Result>, rmcp::transport::streamable_http_client::StreamableHttpError> { - /// todo!() - /// } - /// } - /// - /// let transport = StreamableHttpClientTransport::with_client( - /// MyHttpClient, - /// StreamableHttpClientTransportConfig::with_uri("http://localhost:8000/mcp") - /// ); - /// ``` - pub fn with_client(client: C, config: StreamableHttpClientTransportConfig) -> Self { - let worker = StreamableHttpClientWorker::new(client, config); - WorkerTransport::spawn(worker) - } -} -#[derive(Debug, Clone)] -pub struct StreamableHttpClientTransportConfig { - pub uri: Arc, - pub retry_config: Arc, - pub channel_buffer_capacity: usize, - /// if true, the transport will not require a session to be established - pub allow_stateless: bool, - /// The value to send in the authorization header - pub auth_header: Option, -} - -impl StreamableHttpClientTransportConfig { - pub fn with_uri(uri: impl Into>) -> Self { - Self { - uri: uri.into(), - ..Default::default() - } - } - - /// Set the authorization header to send with requests - /// - /// # Arguments - /// - /// * `value` - A bearer token without the `Bearer ` prefix - pub fn auth_header>(mut self, value: T) -> Self { - // set our authorization header - self.auth_header = Some(value.into()); - self - } -} - -impl Default for StreamableHttpClientTransportConfig { - fn default() -> Self { - Self { - uri: "localhost".into(), - retry_config: Arc::new(ExponentialBackoff::default()), - channel_buffer_capacity: 16, - allow_stateless: true, - auth_header: None, - } - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/streamable_http_server.rs b/codex-rs/vendor/rmcp/src/transport/streamable_http_server.rs deleted file mode 100644 index 733fc5e518..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/streamable_http_server.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod session; -#[cfg(feature = "transport-streamable-http-server")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-server")))] -pub mod tower; -pub use session::{SessionId, SessionManager}; -#[cfg(feature = "transport-streamable-http-server")] -#[cfg_attr(docsrs, doc(cfg(feature = "transport-streamable-http-server")))] -pub use tower::{StreamableHttpServerConfig, StreamableHttpService}; diff --git a/codex-rs/vendor/rmcp/src/transport/streamable_http_server/session.rs b/codex-rs/vendor/rmcp/src/transport/streamable_http_server/session.rs deleted file mode 100644 index d9c5c7f491..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/streamable_http_server/session.rs +++ /dev/null @@ -1,53 +0,0 @@ -use futures::Stream; - -pub use crate::transport::common::server_side_http::{ServerSseMessage, SessionId}; -use crate::{ - RoleServer, - model::{ClientJsonRpcMessage, ServerJsonRpcMessage}, -}; - -pub mod local; -pub mod never; - -pub trait SessionManager: Send + Sync + 'static { - type Error: std::error::Error + Send + 'static; - type Transport: crate::transport::Transport; - /// Create a new session with the given id and configuration. - fn create_session( - &self, - ) -> impl Future> + Send; - fn initialize_session( - &self, - id: &SessionId, - message: ClientJsonRpcMessage, - ) -> impl Future> + Send; - fn has_session(&self, id: &SessionId) - -> impl Future> + Send; - fn close_session(&self, id: &SessionId) - -> impl Future> + Send; - fn create_stream( - &self, - id: &SessionId, - message: ClientJsonRpcMessage, - ) -> impl Future< - Output = Result + Send + Sync + 'static, Self::Error>, - > + Send; - fn accept_message( - &self, - id: &SessionId, - message: ClientJsonRpcMessage, - ) -> impl Future> + Send; - fn create_standalone_stream( - &self, - id: &SessionId, - ) -> impl Future< - Output = Result + Send + Sync + 'static, Self::Error>, - > + Send; - fn resume( - &self, - id: &SessionId, - last_event_id: String, - ) -> impl Future< - Output = Result + Send + Sync + 'static, Self::Error>, - > + Send; -} diff --git a/codex-rs/vendor/rmcp/src/transport/streamable_http_server/session/local.rs b/codex-rs/vendor/rmcp/src/transport/streamable_http_server/session/local.rs deleted file mode 100644 index d68d63e1ad..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/streamable_http_server/session/local.rs +++ /dev/null @@ -1,1043 +0,0 @@ -use std::{ - collections::{HashMap, HashSet, VecDeque}, - num::ParseIntError, - sync::Arc, - time::Duration, -}; - -use futures::Stream; -use thiserror::Error; -use tokio::sync::{ - mpsc::{Receiver, Sender}, - oneshot, -}; -use tokio_stream::wrappers::ReceiverStream; -use tracing::instrument; - -use crate::{ - RoleServer, - model::{ - CancelledNotificationParam, ClientJsonRpcMessage, ClientNotification, ClientRequest, - JsonRpcNotification, JsonRpcRequest, Notification, ProgressNotificationParam, - ProgressToken, RequestId, ServerJsonRpcMessage, ServerNotification, - }, - transport::{ - WorkerTransport, - common::server_side_http::{SessionId, session_id}, - worker::{Worker, WorkerContext, WorkerQuitReason, WorkerSendRequest}, - }, -}; - -#[derive(Debug, Default)] -pub struct LocalSessionManager { - pub sessions: tokio::sync::RwLock>, - pub session_config: SessionConfig, -} - -#[derive(Debug, Error)] -pub enum LocalSessionManagerError { - #[error("Session not found: {0}")] - SessionNotFound(SessionId), - #[error("Session error: {0}")] - SessionError(#[from] SessionError), - #[error("Invalid event id: {0}")] - InvalidEventId(#[from] EventIdParseError), -} -impl SessionManager for LocalSessionManager { - type Error = LocalSessionManagerError; - type Transport = WorkerTransport; - async fn create_session(&self) -> Result<(SessionId, Self::Transport), Self::Error> { - let id = session_id(); - let (handle, worker) = create_local_session(id.clone(), self.session_config.clone()); - self.sessions.write().await.insert(id.clone(), handle); - Ok((id, WorkerTransport::spawn(worker))) - } - async fn initialize_session( - &self, - id: &SessionId, - message: ClientJsonRpcMessage, - ) -> Result { - let sessions = self.sessions.read().await; - let handle = sessions - .get(id) - .ok_or(LocalSessionManagerError::SessionNotFound(id.clone()))?; - let response = handle.initialize(message).await?; - Ok(response) - } - async fn close_session(&self, id: &SessionId) -> Result<(), Self::Error> { - let mut sessions = self.sessions.write().await; - if let Some(handle) = sessions.remove(id) { - handle.close().await?; - } - Ok(()) - } - async fn has_session(&self, id: &SessionId) -> Result { - let sessions = self.sessions.read().await; - Ok(sessions.contains_key(id)) - } - async fn create_stream( - &self, - id: &SessionId, - message: ClientJsonRpcMessage, - ) -> Result + Send + 'static, Self::Error> { - let sessions = self.sessions.read().await; - let handle = sessions - .get(id) - .ok_or(LocalSessionManagerError::SessionNotFound(id.clone()))?; - let receiver = handle.establish_request_wise_channel().await?; - handle - .push_message(message, receiver.http_request_id) - .await?; - Ok(ReceiverStream::new(receiver.inner)) - } - - async fn create_standalone_stream( - &self, - id: &SessionId, - ) -> Result + Send + 'static, Self::Error> { - let sessions = self.sessions.read().await; - let handle = sessions - .get(id) - .ok_or(LocalSessionManagerError::SessionNotFound(id.clone()))?; - let receiver = handle.establish_common_channel().await?; - Ok(ReceiverStream::new(receiver.inner)) - } - - async fn resume( - &self, - id: &SessionId, - last_event_id: String, - ) -> Result + Send + 'static, Self::Error> { - let sessions = self.sessions.read().await; - let handle = sessions - .get(id) - .ok_or(LocalSessionManagerError::SessionNotFound(id.clone()))?; - let receiver = handle.resume(last_event_id.parse()?).await?; - Ok(ReceiverStream::new(receiver.inner)) - } - - async fn accept_message( - &self, - id: &SessionId, - message: ClientJsonRpcMessage, - ) -> Result<(), Self::Error> { - let sessions = self.sessions.read().await; - let handle = sessions - .get(id) - .ok_or(LocalSessionManagerError::SessionNotFound(id.clone()))?; - handle.push_message(message, None).await?; - Ok(()) - } -} - -/// `/request_id>` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct EventId { - http_request_id: Option, - index: usize, -} - -impl std::fmt::Display for EventId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.index)?; - match &self.http_request_id { - Some(http_request_id) => write!(f, "/{http_request_id}"), - None => write!(f, ""), - } - } -} - -#[derive(Debug, Clone, Error)] -pub enum EventIdParseError { - #[error("Invalid index: {0}")] - InvalidIndex(ParseIntError), - #[error("Invalid numeric request id: {0}")] - InvalidNumericRequestId(ParseIntError), - #[error("Missing request id type")] - InvalidRequestIdType, - #[error("Missing request id")] - MissingRequestId, -} - -impl std::str::FromStr for EventId { - type Err = EventIdParseError; - fn from_str(s: &str) -> Result { - if let Some((index, request_id)) = s.split_once("/") { - let index = usize::from_str(index).map_err(EventIdParseError::InvalidIndex)?; - let request_id = u64::from_str(request_id).map_err(EventIdParseError::InvalidIndex)?; - Ok(EventId { - http_request_id: Some(request_id), - index, - }) - } else { - let index = usize::from_str(s).map_err(EventIdParseError::InvalidIndex)?; - Ok(EventId { - http_request_id: None, - index, - }) - } - } -} - -use super::{ServerSseMessage, SessionManager}; - -struct CachedTx { - tx: Sender, - cache: VecDeque, - http_request_id: Option, - capacity: usize, -} - -impl CachedTx { - fn new(tx: Sender, http_request_id: Option) -> Self { - Self { - cache: VecDeque::with_capacity(tx.capacity()), - capacity: tx.capacity(), - tx, - http_request_id, - } - } - fn new_common(tx: Sender) -> Self { - Self::new(tx, None) - } - - fn next_event_id(&self) -> EventId { - let index = self.cache.back().map_or(0, |m| { - m.event_id - .as_deref() - .unwrap_or_default() - .parse::() - .expect("valid event id") - .index - + 1 - }); - EventId { - http_request_id: self.http_request_id, - index, - } - } - - async fn send(&mut self, message: ServerJsonRpcMessage) { - let event_id = self.next_event_id(); - let message = ServerSseMessage { - event_id: Some(event_id.to_string()), - message: Some(Arc::new(message)), - retry: None, - }; - self.cache_and_send(message).await; - } - - async fn send_priming(&mut self, retry: Duration) { - let event_id = self.next_event_id(); - let message = ServerSseMessage { - event_id: Some(event_id.to_string()), - message: None, - retry: Some(retry), - }; - self.cache_and_send(message).await; - } - - async fn cache_and_send(&mut self, message: ServerSseMessage) { - if self.cache.len() >= self.capacity { - self.cache.pop_front(); - self.cache.push_back(message.clone()); - } else { - self.cache.push_back(message.clone()); - } - let _ = self.tx.send(message).await.inspect_err(|e| { - let event_id = &e.0.event_id; - tracing::trace!(?event_id, "trying to send message in a closed session") - }); - } - - async fn sync(&mut self, index: usize) -> Result<(), SessionError> { - let Some(front) = self.cache.front() else { - return Ok(()); - }; - let front_event_id = front - .event_id - .as_deref() - .unwrap_or_default() - .parse::()?; - let sync_index = index.saturating_sub(front_event_id.index); - if sync_index > self.cache.len() { - // invalid index - return Err(SessionError::InvalidEventId); - } - for message in self.cache.iter().skip(sync_index) { - let send_result = self.tx.send(message.clone()).await; - if send_result.is_err() { - let event_id: EventId = message.event_id.as_deref().unwrap_or_default().parse()?; - return Err(SessionError::ChannelClosed(Some(event_id.index as u64))); - } - } - Ok(()) - } -} - -struct HttpRequestWise { - resources: HashSet, - tx: CachedTx, -} - -type HttpRequestId = u64; -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -enum ResourceKey { - McpRequestId(RequestId), - ProgressToken(ProgressToken), -} - -pub struct LocalSessionWorker { - id: SessionId, - next_http_request_id: HttpRequestId, - tx_router: HashMap, - resource_router: HashMap, - common: CachedTx, - event_rx: Receiver, - session_config: SessionConfig, -} - -impl LocalSessionWorker { - pub fn id(&self) -> &SessionId { - &self.id - } -} - -#[derive(Debug, Error)] -pub enum SessionError { - #[error("Invalid request id: {0}")] - DuplicatedRequestId(HttpRequestId), - #[error("Channel closed: {0:?}")] - ChannelClosed(Option), - #[error("Cannot parse event id: {0}")] - EventIdParseError(#[from] EventIdParseError), - #[error("Session service terminated")] - SessionServiceTerminated, - #[error("Invalid event id")] - InvalidEventId, - #[error("IO error: {0}")] - Io(#[from] std::io::Error), -} - -impl From for std::io::Error { - fn from(value: SessionError) -> Self { - match value { - SessionError::Io(io) => io, - _ => std::io::Error::other(format!("Session error: {value}")), - } - } -} - -enum OutboundChannel { - RequestWise { id: HttpRequestId, close: bool }, - Common, -} -#[derive(Debug)] -pub struct StreamableHttpMessageReceiver { - pub http_request_id: Option, - pub inner: Receiver, -} - -impl LocalSessionWorker { - fn unregister_resource(&mut self, resource: &ResourceKey) { - if let Some(http_request_id) = self.resource_router.remove(resource) { - tracing::trace!(?resource, http_request_id, "unregister resource"); - if let Some(channel) = self.tx_router.get_mut(&http_request_id) { - // It's okey to do so, since we don't handle batch json rpc request anymore - // and this can be refactored after the batch request is removed in the coming version. - if channel.resources.is_empty() || matches!(resource, ResourceKey::McpRequestId(_)) - { - tracing::debug!(http_request_id, "close http request wise channel"); - if let Some(channel) = self.tx_router.remove(&http_request_id) { - for resource in channel.resources { - self.resource_router.remove(&resource); - } - } - } - } else { - tracing::warn!(http_request_id, "http request wise channel not found"); - } - } - } - fn register_resource(&mut self, resource: ResourceKey, http_request_id: HttpRequestId) { - tracing::trace!(?resource, http_request_id, "register resource"); - if let Some(channel) = self.tx_router.get_mut(&http_request_id) { - channel.resources.insert(resource.clone()); - self.resource_router.insert(resource, http_request_id); - } - } - fn register_request( - &mut self, - request: &JsonRpcRequest, - http_request_id: HttpRequestId, - ) { - use crate::model::GetMeta; - self.register_resource( - ResourceKey::McpRequestId(request.id.clone()), - http_request_id, - ); - if let Some(progress_token) = request.request.get_meta().get_progress_token() { - self.register_resource( - ResourceKey::ProgressToken(progress_token.clone()), - http_request_id, - ); - } - } - fn catch_cancellation_notification( - &mut self, - notification: &JsonRpcNotification, - ) { - if let ClientNotification::CancelledNotification(n) = ¬ification.notification { - let request_id = n.params.request_id.clone(); - let resource = ResourceKey::McpRequestId(request_id); - self.unregister_resource(&resource); - } - } - fn next_http_request_id(&mut self) -> HttpRequestId { - let id = self.next_http_request_id; - self.next_http_request_id = self.next_http_request_id.wrapping_add(1); - id - } - async fn establish_request_wise_channel( - &mut self, - ) -> Result { - let http_request_id = self.next_http_request_id(); - let (tx, rx) = tokio::sync::mpsc::channel(self.session_config.channel_capacity); - self.tx_router.insert( - http_request_id, - HttpRequestWise { - resources: Default::default(), - tx: CachedTx::new(tx, Some(http_request_id)), - }, - ); - tracing::debug!(http_request_id, "establish new request wise channel"); - Ok(StreamableHttpMessageReceiver { - http_request_id: Some(http_request_id), - inner: rx, - }) - } - fn resolve_outbound_channel(&self, message: &ServerJsonRpcMessage) -> OutboundChannel { - match &message { - ServerJsonRpcMessage::Request(_) => OutboundChannel::Common, - ServerJsonRpcMessage::Notification(JsonRpcNotification { - notification: - ServerNotification::ProgressNotification(Notification { - params: ProgressNotificationParam { progress_token, .. }, - .. - }), - .. - }) => { - let id = self - .resource_router - .get(&ResourceKey::ProgressToken(progress_token.clone())); - - if let Some(id) = id { - OutboundChannel::RequestWise { - id: *id, - close: false, - } - } else { - OutboundChannel::Common - } - } - ServerJsonRpcMessage::Notification(JsonRpcNotification { - notification: - ServerNotification::CancelledNotification(Notification { - params: CancelledNotificationParam { request_id, .. }, - .. - }), - .. - }) => { - if let Some(id) = self - .resource_router - .get(&ResourceKey::McpRequestId(request_id.clone())) - { - OutboundChannel::RequestWise { - id: *id, - close: false, - } - } else { - OutboundChannel::Common - } - } - ServerJsonRpcMessage::Notification(_) => OutboundChannel::Common, - ServerJsonRpcMessage::Response(json_rpc_response) => { - if let Some(id) = self - .resource_router - .get(&ResourceKey::McpRequestId(json_rpc_response.id.clone())) - { - OutboundChannel::RequestWise { - id: *id, - close: false, - } - } else { - OutboundChannel::Common - } - } - ServerJsonRpcMessage::Error(json_rpc_error) => { - if let Some(id) = self - .resource_router - .get(&ResourceKey::McpRequestId(json_rpc_error.id.clone())) - { - OutboundChannel::RequestWise { - id: *id, - close: false, - } - } else { - OutboundChannel::Common - } - } - } - } - async fn handle_server_message( - &mut self, - message: ServerJsonRpcMessage, - ) -> Result<(), SessionError> { - let outbound_channel = self.resolve_outbound_channel(&message); - match outbound_channel { - OutboundChannel::RequestWise { id, close } => { - if let Some(request_wise) = self.tx_router.get_mut(&id) { - request_wise.tx.send(message).await; - if close { - self.tx_router.remove(&id); - } - } else { - return Err(SessionError::ChannelClosed(Some(id))); - } - } - OutboundChannel::Common => self.common.send(message).await, - } - Ok(()) - } - async fn resume( - &mut self, - last_event_id: EventId, - ) -> Result { - match last_event_id.http_request_id { - Some(http_request_id) => { - let request_wise = self - .tx_router - .get_mut(&http_request_id) - .ok_or(SessionError::ChannelClosed(Some(http_request_id)))?; - let channel = tokio::sync::mpsc::channel(self.session_config.channel_capacity); - let (tx, rx) = channel; - request_wise.tx.tx = tx; - let index = last_event_id.index; - // sync messages after index - request_wise.tx.sync(index).await?; - Ok(StreamableHttpMessageReceiver { - http_request_id: Some(http_request_id), - inner: rx, - }) - } - None => { - let channel = tokio::sync::mpsc::channel(self.session_config.channel_capacity); - let (tx, rx) = channel; - self.common.tx = tx; - let index = last_event_id.index; - // sync messages after index - self.common.sync(index).await?; - Ok(StreamableHttpMessageReceiver { - http_request_id: None, - inner: rx, - }) - } - } - } - - async fn close_sse_stream( - &mut self, - http_request_id: Option, - retry_interval: Option, - ) -> Result<(), SessionError> { - match http_request_id { - // Close a request-wise stream - Some(id) => { - let request_wise = self - .tx_router - .get_mut(&id) - .ok_or(SessionError::ChannelClosed(Some(id)))?; - - // Send priming event if retry interval is specified - if let Some(interval) = retry_interval { - request_wise.tx.send_priming(interval).await; - } - - // Close the stream by dropping the sender - let (tx, _rx) = tokio::sync::mpsc::channel(1); - request_wise.tx.tx = tx; - - tracing::debug!( - http_request_id = id, - "closed SSE stream for server-initiated disconnection" - ); - Ok(()) - } - // Close the standalone (common) stream - None => { - // Send priming event if retry interval is specified - if let Some(interval) = retry_interval { - self.common.send_priming(interval).await; - } - - // Close the stream by dropping the sender - let (tx, _rx) = tokio::sync::mpsc::channel(1); - self.common.tx = tx; - - tracing::debug!("closed standalone SSE stream for server-initiated disconnection"); - Ok(()) - } - } - } -} - -#[derive(Debug)] -pub enum SessionEvent { - ClientMessage { - message: ClientJsonRpcMessage, - http_request_id: Option, - }, - EstablishRequestWiseChannel { - responder: oneshot::Sender>, - }, - CloseRequestWiseChannel { - id: HttpRequestId, - responder: oneshot::Sender>, - }, - Resume { - last_event_id: EventId, - responder: oneshot::Sender>, - }, - InitializeRequest { - request: ClientJsonRpcMessage, - responder: oneshot::Sender>, - }, - Close, - CloseSseStream { - /// The HTTP request ID to close. If `None`, closes the standalone (common) stream. - http_request_id: Option, - /// Optional retry interval. If provided, a priming event is sent before closing. - retry_interval: Option, - responder: oneshot::Sender>, - }, -} - -#[derive(Debug, Clone)] -pub enum SessionQuitReason { - ServiceTerminated, - ClientTerminated, - ExpectInitializeRequest, - ExpectInitializeResponse, - Cancelled, -} - -#[derive(Debug, Clone)] -pub struct LocalSessionHandle { - id: SessionId, - // after all event_tx drop, inner task will be terminated - event_tx: Sender, -} - -impl LocalSessionHandle { - /// Get the session id - pub fn id(&self) -> &SessionId { - &self.id - } - - /// Close the session - pub async fn close(&self) -> Result<(), SessionError> { - self.event_tx - .send(SessionEvent::Close) - .await - .map_err(|_| SessionError::SessionServiceTerminated)?; - Ok(()) - } - - /// Send a message to the session - pub async fn push_message( - &self, - message: ClientJsonRpcMessage, - http_request_id: Option, - ) -> Result<(), SessionError> { - self.event_tx - .send(SessionEvent::ClientMessage { - message, - http_request_id, - }) - .await - .map_err(|_| SessionError::SessionServiceTerminated)?; - Ok(()) - } - - /// establish a channel for a http-request, the corresponded message from server will be - /// sent through this channel. The channel will be closed when the request is completed, - /// or you can close it manually by calling [`LocalSessionHandle::close_request_wise_channel`]. - pub async fn establish_request_wise_channel( - &self, - ) -> Result { - let (tx, rx) = tokio::sync::oneshot::channel(); - self.event_tx - .send(SessionEvent::EstablishRequestWiseChannel { responder: tx }) - .await - .map_err(|_| SessionError::SessionServiceTerminated)?; - rx.await - .map_err(|_| SessionError::SessionServiceTerminated)? - } - - /// close the http-request wise channel. - pub async fn close_request_wise_channel( - &self, - request_id: HttpRequestId, - ) -> Result<(), SessionError> { - let (tx, rx) = tokio::sync::oneshot::channel(); - self.event_tx - .send(SessionEvent::CloseRequestWiseChannel { - id: request_id, - responder: tx, - }) - .await - .map_err(|_| SessionError::SessionServiceTerminated)?; - rx.await - .map_err(|_| SessionError::SessionServiceTerminated)? - } - - /// Establish a common channel for general purpose messages. - pub async fn establish_common_channel( - &self, - ) -> Result { - let (tx, rx) = tokio::sync::oneshot::channel(); - self.event_tx - .send(SessionEvent::Resume { - last_event_id: EventId { - http_request_id: None, - index: 0, - }, - responder: tx, - }) - .await - .map_err(|_| SessionError::SessionServiceTerminated)?; - rx.await - .map_err(|_| SessionError::SessionServiceTerminated)? - } - - /// Resume streaming response by the last event id. This is suitable for both request wise and common channel. - pub async fn resume( - &self, - last_event_id: EventId, - ) -> Result { - let (tx, rx) = tokio::sync::oneshot::channel(); - self.event_tx - .send(SessionEvent::Resume { - last_event_id, - responder: tx, - }) - .await - .map_err(|_| SessionError::SessionServiceTerminated)?; - rx.await - .map_err(|_| SessionError::SessionServiceTerminated)? - } - - /// Send an initialize request to the session. And wait for the initialized response. - /// - /// This is used to establish a session with the server. - pub async fn initialize( - &self, - request: ClientJsonRpcMessage, - ) -> Result { - let (tx, rx) = tokio::sync::oneshot::channel(); - self.event_tx - .send(SessionEvent::InitializeRequest { - request, - responder: tx, - }) - .await - .map_err(|_| SessionError::SessionServiceTerminated)?; - rx.await - .map_err(|_| SessionError::SessionServiceTerminated)? - } - - /// Close an SSE stream for a specific request. - /// - /// This closes the SSE connection for a POST request stream, but keeps the session - /// and message cache active. Clients can reconnect using the `Last-Event-ID` header - /// via a GET request to resume receiving messages. - /// - /// # Arguments - /// - /// * `http_request_id` - The HTTP request ID of the stream to close - /// * `retry_interval` - Optional retry interval. If provided, a priming event is sent - pub async fn close_sse_stream( - &self, - http_request_id: HttpRequestId, - retry_interval: Option, - ) -> Result<(), SessionError> { - let (tx, rx) = tokio::sync::oneshot::channel(); - self.event_tx - .send(SessionEvent::CloseSseStream { - http_request_id: Some(http_request_id), - retry_interval, - responder: tx, - }) - .await - .map_err(|_| SessionError::SessionServiceTerminated)?; - rx.await - .map_err(|_| SessionError::SessionServiceTerminated)? - } - - /// Close the standalone SSE stream. - /// - /// This closes the standalone SSE connection (established via GET request), - /// but keeps the session and message cache active. Clients can reconnect using - /// the `Last-Event-ID` header via a GET request to resume receiving messages. - /// - /// # Arguments - /// - /// * `retry_interval` - Optional retry interval. If provided, a priming event is sent - pub async fn close_standalone_sse_stream( - &self, - retry_interval: Option, - ) -> Result<(), SessionError> { - let (tx, rx) = tokio::sync::oneshot::channel(); - self.event_tx - .send(SessionEvent::CloseSseStream { - http_request_id: None, - retry_interval, - responder: tx, - }) - .await - .map_err(|_| SessionError::SessionServiceTerminated)?; - rx.await - .map_err(|_| SessionError::SessionServiceTerminated)? - } -} - -pub type SessionTransport = WorkerTransport; - -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Error)] -pub enum LocalSessionWorkerError { - #[error("transport terminated")] - TransportTerminated, - #[error("unexpected message: {0:?}")] - UnexpectedEvent(SessionEvent), - #[error("fail to send initialize request {0}")] - FailToSendInitializeRequest(SessionError), - #[error("fail to handle message: {0}")] - FailToHandleMessage(SessionError), - #[error("keep alive timeout after {}ms", _0.as_millis())] - KeepAliveTimeout(Duration), - #[error("Transport closed")] - TransportClosed, - #[error("Tokio join error {0}")] - TokioJoinError(#[from] tokio::task::JoinError), -} -impl Worker for LocalSessionWorker { - type Error = LocalSessionWorkerError; - type Role = RoleServer; - fn err_closed() -> Self::Error { - LocalSessionWorkerError::TransportClosed - } - fn err_join(e: tokio::task::JoinError) -> Self::Error { - LocalSessionWorkerError::TokioJoinError(e) - } - fn config(&self) -> crate::transport::worker::WorkerConfig { - crate::transport::worker::WorkerConfig { - name: Some(format!("streamable-http-session-{}", self.id)), - channel_buffer_capacity: self.session_config.channel_capacity, - } - } - #[instrument(name = "streamable_http_session", skip_all, fields(id = self.id.as_ref()))] - async fn run( - mut self, - mut context: WorkerContext, - ) -> Result<(), WorkerQuitReason> { - enum InnerEvent { - FromHttpService(SessionEvent), - FromHandler(WorkerSendRequest), - } - // waiting for initialize request - let evt = self.event_rx.recv().await.ok_or_else(|| { - WorkerQuitReason::fatal( - LocalSessionWorkerError::TransportTerminated, - "get initialize request", - ) - })?; - let SessionEvent::InitializeRequest { request, responder } = evt else { - return Err(WorkerQuitReason::fatal( - LocalSessionWorkerError::UnexpectedEvent(evt), - "get initialize request", - )); - }; - context.send_to_handler(request).await?; - let send_initialize_response = context.recv_from_handler().await?; - responder - .send(Ok(send_initialize_response.message)) - .map_err(|_| { - WorkerQuitReason::fatal( - LocalSessionWorkerError::FailToSendInitializeRequest( - SessionError::SessionServiceTerminated, - ), - "send initialize response", - ) - })?; - send_initialize_response - .responder - .send(Ok(())) - .map_err(|_| WorkerQuitReason::HandlerTerminated)?; - let ct = context.cancellation_token.clone(); - let keep_alive = self.session_config.keep_alive.unwrap_or(Duration::MAX); - loop { - let keep_alive_timeout = tokio::time::sleep(keep_alive); - let event = tokio::select! { - event = self.event_rx.recv() => { - if let Some(event) = event { - InnerEvent::FromHttpService(event) - } else { - return Err(WorkerQuitReason::fatal(LocalSessionWorkerError::TransportTerminated, "waiting next session event")) - } - }, - from_handler = context.recv_from_handler() => { - InnerEvent::FromHandler(from_handler?) - } - _ = ct.cancelled() => { - return Err(WorkerQuitReason::Cancelled) - } - _ = keep_alive_timeout => { - return Err(WorkerQuitReason::fatal(LocalSessionWorkerError::KeepAliveTimeout(keep_alive), "poll next session event")) - } - }; - match event { - InnerEvent::FromHandler(WorkerSendRequest { message, responder }) => { - // catch response - let to_unregister = match &message { - crate::model::JsonRpcMessage::Response(json_rpc_response) => { - let request_id = json_rpc_response.id.clone(); - Some(ResourceKey::McpRequestId(request_id)) - } - crate::model::JsonRpcMessage::Error(json_rpc_error) => { - let request_id = json_rpc_error.id.clone(); - Some(ResourceKey::McpRequestId(request_id)) - } - _ => { - None - // no need to unregister resource - } - }; - let handle_result = self - .handle_server_message(message) - .await - .map_err(LocalSessionWorkerError::FailToHandleMessage); - let _ = responder.send(handle_result).inspect_err(|error| { - tracing::warn!(?error, "failed to send message to http service handler"); - }); - if let Some(to_unregister) = to_unregister { - self.unregister_resource(&to_unregister); - } - } - InnerEvent::FromHttpService(SessionEvent::ClientMessage { - message: json_rpc_message, - http_request_id, - }) => { - match &json_rpc_message { - crate::model::JsonRpcMessage::Request(request) => { - if let Some(http_request_id) = http_request_id { - self.register_request(request, http_request_id) - } - } - crate::model::JsonRpcMessage::Notification(notification) => { - self.catch_cancellation_notification(notification) - } - _ => {} - } - context.send_to_handler(json_rpc_message).await?; - } - InnerEvent::FromHttpService(SessionEvent::EstablishRequestWiseChannel { - responder, - }) => { - let handle_result = self.establish_request_wise_channel().await; - let _ = responder.send(handle_result); - } - InnerEvent::FromHttpService(SessionEvent::CloseRequestWiseChannel { - id, - responder, - }) => { - let _handle_result = self.tx_router.remove(&id); - let _ = responder.send(Ok(())); - } - InnerEvent::FromHttpService(SessionEvent::Resume { - last_event_id, - responder, - }) => { - let handle_result = self.resume(last_event_id).await; - let _ = responder.send(handle_result); - } - InnerEvent::FromHttpService(SessionEvent::Close) => { - return Err(WorkerQuitReason::TransportClosed); - } - InnerEvent::FromHttpService(SessionEvent::CloseSseStream { - http_request_id, - retry_interval, - responder, - }) => { - let handle_result = - self.close_sse_stream(http_request_id, retry_interval).await; - let _ = responder.send(handle_result); - } - _ => { - // ignore - } - } - } - } -} - -#[derive(Debug, Clone)] -pub struct SessionConfig { - /// the capacity of the channel for the session. Default is 16. - pub channel_capacity: usize, - /// if set, the session will be closed after this duration of inactivity. - pub keep_alive: Option, -} - -impl SessionConfig { - pub const DEFAULT_CHANNEL_CAPACITY: usize = 16; -} - -impl Default for SessionConfig { - fn default() -> Self { - Self { - channel_capacity: Self::DEFAULT_CHANNEL_CAPACITY, - keep_alive: None, - } - } -} - -/// Create a new session with the given id and configuration. -/// -/// This function will return a pair of [`LocalSessionHandle`] and [`LocalSessionWorker`]. -/// -/// You can run the [`LocalSessionWorker`] as a transport for mcp server. And use the [`LocalSessionHandle`] operate the session. -pub fn create_local_session( - id: impl Into, - config: SessionConfig, -) -> (LocalSessionHandle, LocalSessionWorker) { - let id = id.into(); - let (event_tx, event_rx) = tokio::sync::mpsc::channel(config.channel_capacity); - let (common_tx, _) = tokio::sync::mpsc::channel(config.channel_capacity); - let common = CachedTx::new_common(common_tx); - tracing::info!(session_id = ?id, "create new session"); - let handle = LocalSessionHandle { - event_tx, - id: id.clone(), - }; - let session_worker = LocalSessionWorker { - next_http_request_id: 0, - id, - tx_router: HashMap::new(), - resource_router: HashMap::new(), - common, - event_rx, - session_config: config.clone(), - }; - (handle, session_worker) -} diff --git a/codex-rs/vendor/rmcp/src/transport/streamable_http_server/session/never.rs b/codex-rs/vendor/rmcp/src/transport/streamable_http_server/session/never.rs deleted file mode 100644 index 436d4cfce2..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/streamable_http_server/session/never.rs +++ /dev/null @@ -1,107 +0,0 @@ -use futures::Stream; -use thiserror::Error; - -use super::{ServerSseMessage, SessionId, SessionManager}; -use crate::{ - RoleServer, - model::{ClientJsonRpcMessage, ServerJsonRpcMessage}, - transport::Transport, -}; - -#[derive(Debug, Clone, Error)] -#[error("Session management is not supported")] -pub struct ErrorSessionManagementNotSupported; -#[derive(Debug, Clone, Default)] -pub struct NeverSessionManager {} -pub enum NeverTransport {} -impl Transport for NeverTransport { - type Error = ErrorSessionManagementNotSupported; - - fn send( - &mut self, - _item: ServerJsonRpcMessage, - ) -> impl Future> + Send + 'static { - futures::future::ready(Err(ErrorSessionManagementNotSupported)) - } - - fn receive(&mut self) -> impl Future> { - futures::future::ready(None) - } - - async fn close(&mut self) -> Result<(), Self::Error> { - Err(ErrorSessionManagementNotSupported) - } -} - -impl SessionManager for NeverSessionManager { - type Error = ErrorSessionManagementNotSupported; - type Transport = NeverTransport; - - fn create_session( - &self, - ) -> impl Future> + Send { - futures::future::ready(Err(ErrorSessionManagementNotSupported)) - } - - fn initialize_session( - &self, - _id: &SessionId, - _message: ClientJsonRpcMessage, - ) -> impl Future> + Send { - futures::future::ready(Err(ErrorSessionManagementNotSupported)) - } - - fn has_session( - &self, - _id: &SessionId, - ) -> impl Future> + Send { - futures::future::ready(Err(ErrorSessionManagementNotSupported)) - } - - fn close_session( - &self, - _id: &SessionId, - ) -> impl Future> + Send { - futures::future::ready(Err(ErrorSessionManagementNotSupported)) - } - - fn create_stream( - &self, - _id: &SessionId, - _message: ClientJsonRpcMessage, - ) -> impl Future< - Output = Result + Send + 'static, Self::Error>, - > + Send { - futures::future::ready(Result::, _>::Err( - ErrorSessionManagementNotSupported, - )) - } - fn create_standalone_stream( - &self, - _id: &SessionId, - ) -> impl Future< - Output = Result + Send + 'static, Self::Error>, - > + Send { - futures::future::ready(Result::, _>::Err( - ErrorSessionManagementNotSupported, - )) - } - fn resume( - &self, - _id: &SessionId, - _last_event_id: String, - ) -> impl Future< - Output = Result + Send + 'static, Self::Error>, - > + Send { - futures::future::ready(Result::, _>::Err( - ErrorSessionManagementNotSupported, - )) - } - fn accept_message( - &self, - _id: &SessionId, - _message: ClientJsonRpcMessage, - ) -> impl Future> + Send { - futures::future::ready(Err(ErrorSessionManagementNotSupported)) - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/streamable_http_server/tower.rs b/codex-rs/vendor/rmcp/src/transport/streamable_http_server/tower.rs deleted file mode 100644 index 37d4a008ca..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/streamable_http_server/tower.rs +++ /dev/null @@ -1,521 +0,0 @@ -use std::{convert::Infallible, fmt::Display, sync::Arc, time::Duration}; - -use bytes::Bytes; -use futures::{StreamExt, future::BoxFuture}; -use http::{Method, Request, Response, header::ALLOW}; -use http_body::Body; -use http_body_util::{BodyExt, Full, combinators::BoxBody}; -use tokio_stream::wrappers::ReceiverStream; -use tokio_util::sync::CancellationToken; - -use super::session::SessionManager; -use crate::{ - RoleServer, - model::{ClientJsonRpcMessage, ClientRequest, GetExtensions}, - serve_server, - service::serve_directly, - transport::{ - OneshotTransport, TransportAdapterIdentity, - common::{ - http_header::{ - EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_SESSION_ID, JSON_MIME_TYPE, - }, - server_side_http::{ - BoxResponse, ServerSseMessage, accepted_response, expect_json, - internal_error_response, sse_stream_response, unexpected_message_response, - }, - }, - }, -}; - -#[derive(Debug, Clone)] -pub struct StreamableHttpServerConfig { - /// The ping message duration for SSE connections. - pub sse_keep_alive: Option, - /// The retry interval for SSE priming events. - pub sse_retry: Option, - /// If true, the server will create a session for each request and keep it alive. - /// When enabled, SSE priming events are sent to enable client reconnection. - pub stateful_mode: bool, - /// Cancellation token for the Streamable HTTP server. - /// - /// When this token is cancelled, all active sessions are terminated and - /// the server stops accepting new requests. - pub cancellation_token: CancellationToken, -} - -impl Default for StreamableHttpServerConfig { - fn default() -> Self { - Self { - sse_keep_alive: Some(Duration::from_secs(15)), - sse_retry: Some(Duration::from_secs(3)), - stateful_mode: true, - cancellation_token: CancellationToken::new(), - } - } -} - -/// # Streamable Http Server -/// -/// ## Extract information from raw http request -/// -/// The http service will consume the request body, however the rest part will be remain and injected into [`crate::model::Extensions`], -/// which you can get from [`crate::service::RequestContext`]. -/// ```rust -/// use rmcp::handler::server::tool::Extension; -/// use http::request::Parts; -/// async fn my_tool(Extension(parts): Extension) { -/// tracing::info!("http parts:{parts:?}") -/// } -/// ``` -pub struct StreamableHttpService { - pub config: StreamableHttpServerConfig, - session_manager: Arc, - service_factory: Arc Result + Send + Sync>, -} - -impl Clone for StreamableHttpService { - fn clone(&self) -> Self { - Self { - config: self.config.clone(), - session_manager: self.session_manager.clone(), - service_factory: self.service_factory.clone(), - } - } -} - -impl tower_service::Service> for StreamableHttpService -where - RequestBody: Body + Send + 'static, - S: crate::Service, - M: SessionManager, - RequestBody::Error: Display, - RequestBody::Data: Send + 'static, -{ - type Response = BoxResponse; - type Error = Infallible; - type Future = BoxFuture<'static, Result>; - fn call(&mut self, req: http::Request) -> Self::Future { - let service = self.clone(); - Box::pin(async move { - let response = service.handle(req).await; - Ok(response) - }) - } - fn poll_ready( - &mut self, - _cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - std::task::Poll::Ready(Ok(())) - } -} - -impl StreamableHttpService -where - S: crate::Service + Send + 'static, - M: SessionManager, -{ - pub fn new( - service_factory: impl Fn() -> Result + Send + Sync + 'static, - session_manager: Arc, - config: StreamableHttpServerConfig, - ) -> Self { - Self { - config, - session_manager, - service_factory: Arc::new(service_factory), - } - } - fn get_service(&self) -> Result { - (self.service_factory)() - } - pub async fn handle(&self, request: Request) -> Response> - where - B: Body + Send + 'static, - B::Error: Display, - { - let method = request.method().clone(); - let allowed_methods = match self.config.stateful_mode { - true => "GET, POST, DELETE", - false => "POST", - }; - let result = match (method, self.config.stateful_mode) { - (Method::POST, _) => self.handle_post(request).await, - // if we're not in stateful mode, we don't support GET or DELETE because there is no session - (Method::GET, true) => self.handle_get(request).await, - (Method::DELETE, true) => self.handle_delete(request).await, - _ => { - // Handle other methods or return an error - let response = Response::builder() - .status(http::StatusCode::METHOD_NOT_ALLOWED) - .header(ALLOW, allowed_methods) - .body(Full::new(Bytes::from("Method Not Allowed")).boxed()) - .expect("valid response"); - return response; - } - }; - match result { - Ok(response) => response, - Err(response) => response, - } - } - async fn handle_get(&self, request: Request) -> Result - where - B: Body + Send + 'static, - B::Error: Display, - { - // check accept header - if !request - .headers() - .get(http::header::ACCEPT) - .and_then(|header| header.to_str().ok()) - .is_some_and(|header| header.contains(EVENT_STREAM_MIME_TYPE)) - { - return Ok(Response::builder() - .status(http::StatusCode::NOT_ACCEPTABLE) - .body( - Full::new(Bytes::from( - "Not Acceptable: Client must accept text/event-stream", - )) - .boxed(), - ) - .expect("valid response")); - } - // check session id - let session_id = request - .headers() - .get(HEADER_SESSION_ID) - .and_then(|v| v.to_str().ok()) - .map(|s| s.to_owned().into()); - let Some(session_id) = session_id else { - // unauthorized - return Ok(Response::builder() - .status(http::StatusCode::UNAUTHORIZED) - .body(Full::new(Bytes::from("Unauthorized: Session ID is required")).boxed()) - .expect("valid response")); - }; - // check if session exists - let has_session = self - .session_manager - .has_session(&session_id) - .await - .map_err(internal_error_response("check session"))?; - if !has_session { - // unauthorized - return Ok(Response::builder() - .status(http::StatusCode::UNAUTHORIZED) - .body(Full::new(Bytes::from("Unauthorized: Session not found")).boxed()) - .expect("valid response")); - } - // check if last event id is provided - let last_event_id = request - .headers() - .get(HEADER_LAST_EVENT_ID) - .and_then(|v| v.to_str().ok()) - .map(|s| s.to_owned()); - if let Some(last_event_id) = last_event_id { - // check if session has this event id - let stream = self - .session_manager - .resume(&session_id, last_event_id) - .await - .map_err(internal_error_response("resume session"))?; - // Resume doesn't need priming - client already has the event ID - Ok(sse_stream_response( - stream, - self.config.sse_keep_alive, - self.config.cancellation_token.child_token(), - )) - } else { - // create standalone stream - let stream = self - .session_manager - .create_standalone_stream(&session_id) - .await - .map_err(internal_error_response("create standalone stream"))?; - // Prepend priming event if sse_retry configured - let stream = if let Some(retry) = self.config.sse_retry { - let priming = ServerSseMessage { - event_id: Some("0".into()), - message: None, - retry: Some(retry), - }; - futures::stream::once(async move { priming }) - .chain(stream) - .left_stream() - } else { - stream.right_stream() - }; - Ok(sse_stream_response( - stream, - self.config.sse_keep_alive, - self.config.cancellation_token.child_token(), - )) - } - } - - async fn handle_post(&self, request: Request) -> Result - where - B: Body + Send + 'static, - B::Error: Display, - { - // check accept header - if !request - .headers() - .get(http::header::ACCEPT) - .and_then(|header| header.to_str().ok()) - .is_some_and(|header| { - header.contains(JSON_MIME_TYPE) && header.contains(EVENT_STREAM_MIME_TYPE) - }) - { - return Ok(Response::builder() - .status(http::StatusCode::NOT_ACCEPTABLE) - .body(Full::new(Bytes::from("Not Acceptable: Client must accept both application/json and text/event-stream")).boxed()) - .expect("valid response")); - } - - // check content type - if !request - .headers() - .get(http::header::CONTENT_TYPE) - .and_then(|header| header.to_str().ok()) - .is_some_and(|header| header.starts_with(JSON_MIME_TYPE)) - { - return Ok(Response::builder() - .status(http::StatusCode::UNSUPPORTED_MEDIA_TYPE) - .body( - Full::new(Bytes::from( - "Unsupported Media Type: Content-Type must be application/json", - )) - .boxed(), - ) - .expect("valid response")); - } - - // json deserialize request body - let (part, body) = request.into_parts(); - let mut message = match expect_json(body).await { - Ok(message) => message, - Err(response) => return Ok(response), - }; - - if self.config.stateful_mode { - // do we have a session id? - let session_id = part - .headers - .get(HEADER_SESSION_ID) - .and_then(|v| v.to_str().ok()); - if let Some(session_id) = session_id { - let session_id = session_id.to_owned().into(); - let has_session = self - .session_manager - .has_session(&session_id) - .await - .map_err(internal_error_response("check session"))?; - if !has_session { - // unauthorized - return Ok(Response::builder() - .status(http::StatusCode::UNAUTHORIZED) - .body(Full::new(Bytes::from("Unauthorized: Session not found")).boxed()) - .expect("valid response")); - } - - // inject request part to extensions - match &mut message { - ClientJsonRpcMessage::Request(req) => { - req.request.extensions_mut().insert(part); - } - ClientJsonRpcMessage::Notification(not) => { - not.notification.extensions_mut().insert(part); - } - _ => { - // skip - } - } - - match message { - ClientJsonRpcMessage::Request(_) => { - let stream = self - .session_manager - .create_stream(&session_id, message) - .await - .map_err(internal_error_response("get session"))?; - // Prepend priming event if sse_retry configured - let stream = if let Some(retry) = self.config.sse_retry { - let priming = ServerSseMessage { - event_id: Some("0".into()), - message: None, - retry: Some(retry), - }; - futures::stream::once(async move { priming }) - .chain(stream) - .left_stream() - } else { - stream.right_stream() - }; - Ok(sse_stream_response( - stream, - self.config.sse_keep_alive, - self.config.cancellation_token.child_token(), - )) - } - ClientJsonRpcMessage::Notification(_) - | ClientJsonRpcMessage::Response(_) - | ClientJsonRpcMessage::Error(_) => { - // handle notification - self.session_manager - .accept_message(&session_id, message) - .await - .map_err(internal_error_response("accept message"))?; - Ok(accepted_response()) - } - } - } else { - let (session_id, transport) = self - .session_manager - .create_session() - .await - .map_err(internal_error_response("create session"))?; - if let ClientJsonRpcMessage::Request(req) = &mut message { - if !matches!(req.request, ClientRequest::InitializeRequest(_)) { - return Err(unexpected_message_response("initialize request")); - } - // inject request part to extensions - req.request.extensions_mut().insert(part); - } else { - return Err(unexpected_message_response("initialize request")); - } - let service = self - .get_service() - .map_err(internal_error_response("get service"))?; - // spawn a task to serve the session - tokio::spawn({ - let session_manager = self.session_manager.clone(); - let session_id = session_id.clone(); - async move { - let service = serve_server::( - service, transport, - ) - .await; - match service { - Ok(service) => { - // on service created - let _ = service.waiting().await; - } - Err(e) => { - tracing::error!("Failed to create service: {e}"); - } - } - let _ = session_manager - .close_session(&session_id) - .await - .inspect_err(|e| { - tracing::error!("Failed to close session {session_id}: {e}"); - }); - } - }); - // get initialize response - let response = self - .session_manager - .initialize_session(&session_id, message) - .await - .map_err(internal_error_response("create stream"))?; - let stream = futures::stream::once(async move { - ServerSseMessage { - event_id: None, - message: Some(Arc::new(response)), - retry: None, - } - }); - // Prepend priming event if sse_retry configured - let stream = if let Some(retry) = self.config.sse_retry { - let priming = ServerSseMessage { - event_id: Some("0".into()), - message: None, - retry: Some(retry), - }; - futures::stream::once(async move { priming }) - .chain(stream) - .left_stream() - } else { - stream.right_stream() - }; - let mut response = sse_stream_response( - stream, - self.config.sse_keep_alive, - self.config.cancellation_token.child_token(), - ); - - response.headers_mut().insert( - HEADER_SESSION_ID, - session_id - .parse() - .map_err(internal_error_response("create session id header"))?, - ); - Ok(response) - } - } else { - let service = self - .get_service() - .map_err(internal_error_response("get service"))?; - match message { - ClientJsonRpcMessage::Request(mut request) => { - request.request.extensions_mut().insert(part); - let (transport, receiver) = - OneshotTransport::::new(ClientJsonRpcMessage::Request(request)); - let service = serve_directly(service, transport, None); - tokio::spawn(async move { - // on service created - let _ = service.waiting().await; - }); - // Stateless mode: no priming (no session to resume) - let stream = ReceiverStream::new(receiver).map(|message| { - tracing::info!(?message); - ServerSseMessage { - event_id: None, - message: Some(Arc::new(message)), - retry: None, - } - }); - Ok(sse_stream_response( - stream, - self.config.sse_keep_alive, - self.config.cancellation_token.child_token(), - )) - } - ClientJsonRpcMessage::Notification(_notification) => { - // ignore - Ok(accepted_response()) - } - ClientJsonRpcMessage::Response(_json_rpc_response) => Ok(accepted_response()), - ClientJsonRpcMessage::Error(_json_rpc_error) => Ok(accepted_response()), - } - } - } - - async fn handle_delete(&self, request: Request) -> Result - where - B: Body + Send + 'static, - B::Error: Display, - { - // check session id - let session_id = request - .headers() - .get(HEADER_SESSION_ID) - .and_then(|v| v.to_str().ok()) - .map(|s| s.to_owned().into()); - let Some(session_id) = session_id else { - // unauthorized - return Ok(Response::builder() - .status(http::StatusCode::UNAUTHORIZED) - .body(Full::new(Bytes::from("Unauthorized: Session ID is required")).boxed()) - .expect("valid response")); - }; - // close session - self.session_manager - .close_session(&session_id) - .await - .map_err(internal_error_response("close session"))?; - Ok(accepted_response()) - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/worker.rs b/codex-rs/vendor/rmcp/src/transport/worker.rs deleted file mode 100644 index 769d448a51..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/worker.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::borrow::Cow; - -use tokio_util::sync::CancellationToken; -use tracing::{Instrument, Level}; - -use super::{IntoTransport, Transport}; -use crate::service::{RxJsonRpcMessage, ServiceRole, TxJsonRpcMessage}; - -#[derive(Debug, thiserror::Error)] -pub enum WorkerQuitReason { - #[error("Join error {0}")] - Join(#[from] tokio::task::JoinError), - #[error("Transport fatal {error}, when {context}")] - Fatal { - error: E, - context: Cow<'static, str>, - }, - #[error("Transport cancelled")] - Cancelled, - #[error("Transport closed")] - TransportClosed, - #[error("Handler terminated")] - HandlerTerminated, -} - -impl WorkerQuitReason { - pub fn fatal(error: E, context: impl Into>) -> Self { - Self::Fatal { - error, - context: context.into(), - } - } - pub fn fatal_context(context: impl Into>) -> impl FnOnce(E) -> Self { - |e| Self::Fatal { - error: e, - context: context.into(), - } - } -} - -pub trait Worker: Sized + Send + 'static { - type Error: std::error::Error + Send + Sync + 'static; - type Role: ServiceRole; - fn err_closed() -> Self::Error; - fn err_join(e: tokio::task::JoinError) -> Self::Error; - fn run( - self, - context: WorkerContext, - ) -> impl Future>> + Send; - fn config(&self) -> WorkerConfig { - WorkerConfig::default() - } -} - -pub struct WorkerSendRequest { - pub message: TxJsonRpcMessage, - pub responder: tokio::sync::oneshot::Sender>, -} - -pub struct WorkerTransport { - rx: tokio::sync::mpsc::Receiver>, - send_service: tokio::sync::mpsc::Sender>, - join_handle: Option>>>, - _drop_guard: tokio_util::sync::DropGuard, - ct: CancellationToken, -} - -pub struct WorkerConfig { - pub name: Option, - pub channel_buffer_capacity: usize, -} - -impl Default for WorkerConfig { - fn default() -> Self { - Self { - name: None, - channel_buffer_capacity: 16, - } - } -} -pub enum WorkerAdapter {} - -impl IntoTransport for W { - fn into_transport(self) -> impl Transport + 'static { - WorkerTransport::spawn(self) - } -} - -impl WorkerTransport { - pub fn cancel_token(&self) -> CancellationToken { - self.ct.clone() - } - pub fn spawn(worker: W) -> Self { - Self::spawn_with_ct(worker, CancellationToken::new()) - } - pub fn spawn_with_ct(worker: W, transport_task_ct: CancellationToken) -> Self { - let config = worker.config(); - let worker_name = config.name; - let (to_transport_tx, from_handler_rx) = - tokio::sync::mpsc::channel::>(config.channel_buffer_capacity); - let (to_handler_tx, from_transport_rx) = - tokio::sync::mpsc::channel::>(config.channel_buffer_capacity); - let context = WorkerContext { - to_handler_tx, - from_handler_rx, - cancellation_token: transport_task_ct.clone(), - }; - - let join_handle = tokio::spawn(async move { - worker - .run(context) - .instrument(tracing::span!( - Level::TRACE, - "transport_worker", - name = worker_name, - )) - .await - .inspect_err(|e| match e { - WorkerQuitReason::Cancelled - | WorkerQuitReason::TransportClosed - | WorkerQuitReason::HandlerTerminated => { - tracing::debug!("worker quit with reason: {:?}", e); - } - WorkerQuitReason::Join(e) => { - tracing::error!("worker quit with join error: {:?}", e); - } - WorkerQuitReason::Fatal { error, context } => { - tracing::error!("worker quit with fatal: {error}, when {context}"); - } - }) - .inspect(|_| { - tracing::debug!("worker quit"); - }) - }); - Self { - rx: from_transport_rx, - send_service: to_transport_tx, - join_handle: Some(join_handle), - ct: transport_task_ct.clone(), - _drop_guard: transport_task_ct.drop_guard(), - } - } -} - -pub struct SendRequest { - pub message: TxJsonRpcMessage, - pub responder: tokio::sync::oneshot::Sender>, -} - -pub struct WorkerContext { - pub to_handler_tx: tokio::sync::mpsc::Sender>, - pub from_handler_rx: tokio::sync::mpsc::Receiver>, - pub cancellation_token: CancellationToken, -} - -impl WorkerContext { - pub async fn send_to_handler( - &mut self, - item: RxJsonRpcMessage, - ) -> Result<(), WorkerQuitReason> { - self.to_handler_tx - .send(item) - .await - .map_err(|_| WorkerQuitReason::HandlerTerminated) - } - - pub async fn recv_from_handler( - &mut self, - ) -> Result, WorkerQuitReason> { - self.from_handler_rx - .recv() - .await - .ok_or(WorkerQuitReason::HandlerTerminated) - } -} - -impl Transport for WorkerTransport { - type Error = W::Error; - - fn send( - &mut self, - item: TxJsonRpcMessage, - ) -> impl Future> + Send + 'static { - let tx = self.send_service.clone(); - let (responder, receiver) = tokio::sync::oneshot::channel(); - let request = WorkerSendRequest { - message: item, - responder, - }; - async move { - tx.send(request).await.map_err(|_| W::err_closed())?; - receiver.await.map_err(|_| W::err_closed())??; - Ok(()) - } - } - async fn receive(&mut self) -> Option> { - self.rx.recv().await - } - async fn close(&mut self) -> Result<(), Self::Error> { - if let Some(handle) = self.join_handle.take() { - self.ct.cancel(); - let _quit_reason = handle.await.map_err(W::err_join)?; - Ok(()) - } else { - Ok(()) - } - } -} diff --git a/codex-rs/vendor/rmcp/src/transport/ws.rs b/codex-rs/vendor/rmcp/src/transport/ws.rs deleted file mode 100644 index 5ec9cbac9e..0000000000 --- a/codex-rs/vendor/rmcp/src/transport/ws.rs +++ /dev/null @@ -1 +0,0 @@ -// Maybe we don't really need a ws implementation?