diff --git a/conformance/src/bin/client.rs b/conformance/src/bin/client.rs index b9c8cea9d..253451729 100644 --- a/conformance/src/bin/client.rs +++ b/conformance/src/bin/client.rs @@ -252,12 +252,8 @@ async fn perform_oauth_flow_preregistered( manager.set_metadata(metadata); // Configure with pre-registered credentials - let config = rmcp::transport::auth::OAuthClientConfig { - client_id: client_id.to_string(), - client_secret: Some(client_secret.to_string()), - scopes: vec![], - redirect_uri: REDIRECT_URI.to_string(), - }; + let config = rmcp::transport::auth::OAuthClientConfig::new(client_id, REDIRECT_URI) + .with_client_secret(client_secret); manager.configure_client(config)?; let scopes = manager.select_scopes(None, &[]); diff --git a/conformance/src/bin/server.rs b/conformance/src/bin/server.rs index bfa98f42c..5ca4b5922 100644 --- a/conformance/src/bin/server.rs +++ b/conformance/src/bin/server.rs @@ -818,10 +818,7 @@ async fn main() -> anyhow::Result<()> { tracing::info!("Starting conformance server on {}", bind_addr); let server = ConformanceServer::new(); - let config = StreamableHttpServerConfig { - stateful_mode: true, - ..Default::default() - }; + let config = StreamableHttpServerConfig::default(); let service = StreamableHttpService::new( move || Ok(server.clone()), LocalSessionManager::default().into(), diff --git a/crates/rmcp/Cargo.toml b/crates/rmcp/Cargo.toml index cbf02ea48..bc59c5933 100644 --- a/crates/rmcp/Cargo.toml +++ b/crates/rmcp/Cargo.toml @@ -9,6 +9,10 @@ readme = { workspace = true } description = "Rust SDK for Model Context Protocol" documentation = "https://docs.rs/rmcp" +[lints.clippy] +exhaustive_structs = "warn" +exhaustive_enums = "warn" + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/rmcp/src/handler/server/common.rs b/crates/rmcp/src/handler/server/common.rs index 2ecab6386..74c49d887 100644 --- a/crates/rmcp/src/handler/server/common.rs +++ b/crates/rmcp/src/handler/server/common.rs @@ -138,6 +138,7 @@ where } } +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct Extension(pub T); impl FromContextPart for Extension @@ -182,6 +183,7 @@ where } } +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RequestId(pub crate::model::RequestId); impl FromContextPart for RequestId diff --git a/crates/rmcp/src/handler/server/prompt.rs b/crates/rmcp/src/handler/server/prompt.rs index 27a03e835..11ca4bf83 100644 --- a/crates/rmcp/src/handler/server/prompt.rs +++ b/crates/rmcp/src/handler/server/prompt.rs @@ -20,6 +20,7 @@ use crate::{ }; /// Context for prompt retrieval operations +#[non_exhaustive] pub struct PromptContext<'a, S> { pub server: &'a S, pub name: String, @@ -117,6 +118,7 @@ impl IntoGetPromptResult for Result // Future wrapper that automatically handles IntoGetPromptResult conversion pin_project_lite::pin_project! { #[project = IntoGetPromptResultFutProj] + #[non_exhaustive] pub enum IntoGetPromptResultFut { Pending { #[pin] @@ -151,6 +153,7 @@ where } // Prompt-specific extractor for prompt name +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct PromptName(pub String); impl FromContextPart> for PromptName { diff --git a/crates/rmcp/src/handler/server/router.rs b/crates/rmcp/src/handler/server/router.rs index 1f34ba5b2..08beb61d2 100644 --- a/crates/rmcp/src/handler/server/router.rs +++ b/crates/rmcp/src/handler/server/router.rs @@ -13,6 +13,7 @@ use crate::{ pub mod prompt; pub mod tool; +#[non_exhaustive] pub struct Router { pub tool_router: tool::ToolRouter, pub prompt_router: prompt::PromptRouter, diff --git a/crates/rmcp/src/handler/server/router/prompt.rs b/crates/rmcp/src/handler/server/router/prompt.rs index b5ea4a47f..e952b2a39 100644 --- a/crates/rmcp/src/handler/server/router/prompt.rs +++ b/crates/rmcp/src/handler/server/router/prompt.rs @@ -6,6 +6,7 @@ use crate::{ service::{MaybeBoxFuture, MaybeSend}, }; +#[non_exhaustive] pub struct PromptRoute { #[allow(clippy::type_complexity)] pub get: Arc>, @@ -90,6 +91,7 @@ where } /// Adapter for functions generated by the #\[prompt\] macro +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct PromptAttrGenerateFunctionAdapter; impl IntoPromptRoute for F @@ -103,6 +105,7 @@ where } #[derive(Debug)] +#[non_exhaustive] pub struct PromptRouter { #[allow(clippy::type_complexity)] pub map: std::collections::HashMap, PromptRoute>, diff --git a/crates/rmcp/src/handler/server/router/tool.rs b/crates/rmcp/src/handler/server/router/tool.rs index 42c582c40..87f0db6c3 100644 --- a/crates/rmcp/src/handler/server/router/tool.rs +++ b/crates/rmcp/src/handler/server/router/tool.rs @@ -136,6 +136,7 @@ use crate::{ service::{MaybeBoxFuture, MaybeSend}, }; +#[non_exhaustive] pub struct ToolRoute { #[allow(clippy::type_complexity)] pub call: Arc>, @@ -216,6 +217,7 @@ where } } +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ToolAttrGenerateFunctionAdapter; impl IntoToolRoute for F where @@ -251,6 +253,7 @@ where } } +#[non_exhaustive] pub struct WithToolAttr where C: CallToolHandler + MaybeSend + Clone + 'static, @@ -292,6 +295,7 @@ where } } #[derive(Debug)] +#[non_exhaustive] pub struct ToolRouter { #[allow(clippy::type_complexity)] pub map: std::collections::HashMap, ToolRoute>, diff --git a/crates/rmcp/src/handler/server/tool.rs b/crates/rmcp/src/handler/server/tool.rs index 0ad8ce61a..03beface4 100644 --- a/crates/rmcp/src/handler/server/tool.rs +++ b/crates/rmcp/src/handler/server/tool.rs @@ -29,6 +29,7 @@ pub fn parse_json_object(input: JsonObject) -> Result { pub request_context: RequestContext, pub service: &'s S, @@ -104,6 +105,7 @@ impl IntoCallToolResult for Result { pin_project_lite::pin_project! { #[project = IntoCallToolResultFutProj] + #[non_exhaustive] pub enum IntoCallToolResultFut { Pending { #[pin] @@ -163,6 +165,7 @@ pub type DynCallToolHandler = -> futures::future::LocalBoxFuture<'s, Result>; // Tool-specific extractor for tool name +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ToolName(pub Cow<'static, str>); impl FromContextPart> for ToolName { diff --git a/crates/rmcp/src/handler/server/wrapper/json.rs b/crates/rmcp/src/handler/server/wrapper/json.rs index 8eae30268..bc2170a3b 100644 --- a/crates/rmcp/src/handler/server/wrapper/json.rs +++ b/crates/rmcp/src/handler/server/wrapper/json.rs @@ -14,6 +14,7 @@ use crate::{ /// 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. +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct Json(pub T); // Implement JsonSchema for Json to delegate to T's schema diff --git a/crates/rmcp/src/handler/server/wrapper/parameters.rs b/crates/rmcp/src/handler/server/wrapper/parameters.rs index 9de73dd66..ab3eb0ce1 100644 --- a/crates/rmcp/src/handler/server/wrapper/parameters.rs +++ b/crates/rmcp/src/handler/server/wrapper/parameters.rs @@ -42,6 +42,7 @@ use schemars::JsonSchema; /// - Returns appropriate error responses if deserialization fails #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(transparent)] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct Parameters

(pub P); impl JsonSchema for Parameters

{ diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 482384354..ba6c35156 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -58,6 +58,7 @@ macro_rules! object { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Copy, Eq)] #[serde(deny_unknown_fields)] #[cfg_attr(feature = "server", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct EmptyObject {} pub trait ConstString: Default { @@ -70,6 +71,7 @@ pub trait ConstString: Default { macro_rules! const_string { ($name:ident = $value:literal) => { #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] + #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct $name; impl ConstString for $name { @@ -196,6 +198,7 @@ impl<'de> Deserialize<'de> for ProtocolVersion { /// 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)] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum NumberOrString { /// A numeric identifier Number(i64), @@ -292,6 +295,7 @@ pub type RequestId = NumberOrString; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Hash, Eq)] #[serde(transparent)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ProgressToken(pub NumberOrString); // ============================================================================= @@ -338,6 +342,7 @@ impl GetExtensions for Request { #[derive(Debug, Clone, Default)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RequestOptionalParam { pub method: M, // #[serde(skip_serializing_if = "Option::is_none")] @@ -361,6 +366,7 @@ impl RequestOptionalParam { #[derive(Debug, Clone, Default)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RequestNoParam { pub method: M, /// extensions will carry anything possible in the context, including [`Meta`] @@ -403,6 +409,7 @@ impl Notification { #[derive(Debug, Clone, Default)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct NotificationNoParam { pub method: M, /// extensions will carry anything possible in the context, including [`Meta`] @@ -414,6 +421,7 @@ pub struct NotificationNoParam { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct JsonRpcRequest { pub jsonrpc: JsonRpcVersion2_0, pub id: RequestId, @@ -435,6 +443,7 @@ impl JsonRpcRequest { type DefaultResponse = JsonObject; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct JsonRpcResponse { pub jsonrpc: JsonRpcVersion2_0, pub id: RequestId, @@ -443,6 +452,7 @@ pub struct JsonRpcResponse { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct JsonRpcError { pub jsonrpc: JsonRpcVersion2_0, pub id: RequestId, @@ -462,6 +472,7 @@ impl JsonRpcError { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct JsonRpcNotification { pub jsonrpc: JsonRpcVersion2_0, #[serde(flatten)] @@ -475,6 +486,7 @@ pub struct JsonRpcNotification { #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] #[serde(transparent)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ErrorCode(pub i32); impl ErrorCode { @@ -493,6 +505,7 @@ impl ErrorCode { /// providing a standardized way to communicate errors between clients and servers. #[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ErrorData { /// The error type that occurred (using standard JSON-RPC error codes) pub code: ErrorCode, @@ -552,6 +565,7 @@ impl ErrorData { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(untagged)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum JsonRpcMessage { /// A single request expecting a response Request(JsonRpcRequest), @@ -651,6 +665,7 @@ impl From for () { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(transparent)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct CustomResult(pub Value); impl CustomResult { @@ -667,6 +682,7 @@ impl CustomResult { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct CancelledNotificationParam { pub request_id: RequestId, pub reason: Option, @@ -691,6 +707,7 @@ pub type CancelledNotification = /// deserialize them into domain-specific types. #[derive(Debug, Clone)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct CustomNotification { pub method: String, pub params: Option, @@ -725,6 +742,7 @@ impl CustomNotification { /// deserialize them into domain-specific types. #[derive(Debug, Clone)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct CustomRequest { pub method: String, pub params: Option, @@ -1050,6 +1068,7 @@ const_string!(ProgressNotificationMethod = "notifications/progress"); #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] 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. @@ -1097,6 +1116,7 @@ macro_rules! paginated_result { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] + #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct $t { #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option, @@ -1293,6 +1313,7 @@ const_string!(ResourceUpdatedNotificationMethod = "notifications/resources/updat #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ResourceUpdatedNotificationParam { /// The URI of the resource that was updated pub uri: String, @@ -1392,6 +1413,7 @@ pub type ToolListChangedNotification = NotificationNoParam { Single(T), Multiple(Vec), @@ -1665,6 +1691,7 @@ pub struct SamplingMessage { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum SamplingMessageContent { Text(RawTextContent), Image(RawImageContent), @@ -1792,6 +1819,7 @@ impl TryFrom for SamplingContent { /// should be provided to the LLM when processing sampling requests. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum ContextInclusion { /// Include context from all connected MCP servers #[serde(rename = "allServers")] @@ -2119,6 +2147,7 @@ impl ModelHint { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct CompletionContext { /// Previously resolved argument values that can inform completion suggestions #[serde(skip_serializing_if = "Option::is_none")] @@ -2209,6 +2238,7 @@ pub type CompleteRequest = Request #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct CompletionInfo { pub values: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -2302,6 +2332,7 @@ impl CompleteResult { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(tag = "type")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum Reference { #[serde(rename = "ref/resource")] Resource(ResourceReference), @@ -2353,6 +2384,7 @@ impl Reference { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ResourceReference { pub uri: String, } @@ -2386,6 +2418,7 @@ const_string!(CompleteRequestMethod = "completion/complete"); #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ArgumentInfo { pub name: String, pub value: String, @@ -2460,6 +2493,7 @@ const_string!(ElicitationCompletionNotificationMethod = "notifications/elicitati #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "lowercase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum ElicitationAction { /// User accepts the request and provides the requested information Accept, @@ -2571,6 +2605,7 @@ impl TryFrom for CreateElicitati try_from = "CreateElicitationRequestParamDeserializeHelper" )] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum CreateElicitationRequestParams { #[serde(rename = "form", rename_all = "camelCase")] FormElicitationParams { @@ -2631,6 +2666,7 @@ pub type CreateElicitationRequestParam = CreateElicitationRequestParams; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct CreateElicitationResult { /// The user's decision on how to handle the elicitation request pub action: ElicitationAction, @@ -2666,6 +2702,7 @@ pub type CreateElicitationRequest = #[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ElicitationResponseNotificationParam { pub elicitation_id: String, } @@ -3032,6 +3069,7 @@ pub type GetTaskInfoRequest = Request; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct GetTaskInfoParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] @@ -3061,6 +3099,7 @@ pub type GetTaskResultRequest = Request; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct CancelTaskParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] @@ -3161,6 +3201,7 @@ macro_rules! ts_union { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(untagged)] #[allow(clippy::large_enum_variant)] + #[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum $U { $($declared)* diff --git a/crates/rmcp/src/model/annotated.rs b/crates/rmcp/src/model/annotated.rs index 9158e10be..e2e750824 100644 --- a/crates/rmcp/src/model/annotated.rs +++ b/crates/rmcp/src/model/annotated.rs @@ -39,6 +39,7 @@ impl Annotations { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct Annotated { #[serde(flatten)] pub raw: T, diff --git a/crates/rmcp/src/model/capabilities.rs b/crates/rmcp/src/model/capabilities.rs index b47a8a849..33aae6908 100644 --- a/crates/rmcp/src/model/capabilities.rs +++ b/crates/rmcp/src/model/capabilities.rs @@ -34,6 +34,7 @@ pub type ExtensionCapabilities = BTreeMap; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct PromptsCapability { #[serde(skip_serializing_if = "Option::is_none")] pub list_changed: Option, @@ -42,6 +43,7 @@ pub struct PromptsCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ResourcesCapability { #[serde(skip_serializing_if = "Option::is_none")] pub subscribe: Option, @@ -52,6 +54,7 @@ pub struct ResourcesCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ToolsCapability { #[serde(skip_serializing_if = "Option::is_none")] pub list_changed: Option, @@ -60,6 +63,7 @@ pub struct ToolsCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RootsCapabilities { #[serde(skip_serializing_if = "Option::is_none")] pub list_changed: Option, @@ -69,6 +73,7 @@ pub struct RootsCapabilities { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct TasksCapability { #[serde(skip_serializing_if = "Option::is_none")] pub requests: Option, @@ -82,6 +87,7 @@ pub struct TasksCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct TaskRequestsCapability { #[serde(skip_serializing_if = "Option::is_none")] pub sampling: Option, @@ -94,6 +100,7 @@ pub struct TaskRequestsCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct SamplingTaskCapability { #[serde(skip_serializing_if = "Option::is_none")] pub create_message: Option, @@ -102,6 +109,7 @@ pub struct SamplingTaskCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ElicitationTaskCapability { #[serde(skip_serializing_if = "Option::is_none")] pub create: Option, @@ -110,6 +118,7 @@ pub struct ElicitationTaskCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ToolsTaskCapability { #[serde(skip_serializing_if = "Option::is_none")] pub call: Option, @@ -190,6 +199,7 @@ impl TasksCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] 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 @@ -201,6 +211,7 @@ pub struct FormElicitationCapability { /// Capability for URL mode elicitation. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct UrlElicitationCapability {} /// Elicitation allows servers to request interactive input from users during tool execution. @@ -209,6 +220,7 @@ pub struct UrlElicitationCapability {} #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ElicitationCapability { /// Whether client supports form-based elicitation. #[serde(skip_serializing_if = "Option::is_none")] @@ -222,6 +234,7 @@ pub struct ElicitationCapability { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct SamplingCapability { /// Support for `tools` and `toolChoice` parameters #[serde(skip_serializing_if = "Option::is_none")] @@ -310,10 +323,12 @@ macro_rules! builder { ($Target: ident {$($f: ident: $T: ty),* $(,)?}) => { paste! { #[derive(Default, Clone, Copy, Debug)] + #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct [<$Target BuilderState>]< $(const [<$f:upper>]: bool = false,)* >; #[derive(Debug, Default)] + #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct [<$Target Builder>]]> { $(pub $f: Option<$T>,)* pub state: PhantomData diff --git a/crates/rmcp/src/model/content.rs b/crates/rmcp/src/model/content.rs index 83658b023..7054e2b0e 100644 --- a/crates/rmcp/src/model/content.rs +++ b/crates/rmcp/src/model/content.rs @@ -9,6 +9,7 @@ use super::{AnnotateAble, Annotated, resource::ResourceContents}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RawTextContent { pub text: String, /// Optional protocol-level metadata for this content block @@ -19,6 +20,7 @@ pub type TextContent = Annotated; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RawImageContent { /// The base64-encoded image pub data: String, @@ -32,6 +34,7 @@ pub type ImageContent = Annotated; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RawEmbeddedResource { /// Optional protocol-level metadata for this content block #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] @@ -63,6 +66,7 @@ impl EmbeddedResource { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RawAudioContent { pub data: String, pub mime_type: String, @@ -145,6 +149,7 @@ impl ToolResultContent { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum RawContent { Text(RawTextContent), Image(RawImageContent), diff --git a/crates/rmcp/src/model/elicitation_schema.rs b/crates/rmcp/src/model/elicitation_schema.rs index cdbb87d6d..73ab62257 100644 --- a/crates/rmcp/src/model/elicitation_schema.rs +++ b/crates/rmcp/src/model/elicitation_schema.rs @@ -49,6 +49,7 @@ const_string!(ArrayTypeConst = "array"); #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum PrimitiveSchema { /// Enum property (explicit enum schema) Enum(EnumSchema), @@ -70,6 +71,7 @@ pub enum PrimitiveSchema { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case")] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum StringFormat { /// Email address format Email, @@ -344,6 +346,7 @@ impl NumberSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct IntegerSchema { /// Type discriminator #[serde(rename = "type")] @@ -510,6 +513,7 @@ impl BooleanSchema { /// Represent single entry for titled item #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ConstTitle { #[serde(rename = "const")] pub const_: String, @@ -530,6 +534,7 @@ impl ConstTitle { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct LegacyEnumSchema { #[serde(rename = "type")] pub type_: StringTypeConst, @@ -595,6 +600,7 @@ impl TitledSingleSelectEnumSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum SingleSelectEnumSchema { Untitled(UntitledSingleSelectEnumSchema), Titled(TitledSingleSelectEnumSchema), @@ -603,6 +609,7 @@ pub enum SingleSelectEnumSchema { /// Items for untitled multi-select options #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct UntitledItems { #[serde(rename = "type")] pub type_: StringTypeConst, @@ -613,6 +620,7 @@ pub struct UntitledItems { /// Items for titled multi-select options #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct TitledItems { // MCP spec requires "anyOf" for multi-select enums (allows any combination) // Alias "oneOf" for compatibility with schemars @@ -718,6 +726,7 @@ impl TitledMultiSelectEnumSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum MultiSelectEnumSchema { Untitled(UntitledMultiSelectEnumSchema), Titled(TitledMultiSelectEnumSchema), @@ -741,6 +750,7 @@ pub enum MultiSelectEnumSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum EnumSchema { Single(SingleSelectEnumSchema), Multi(MultiSelectEnumSchema), @@ -749,9 +759,11 @@ pub enum EnumSchema { /// Marker type for single-select enum builder #[derive(Debug)] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct SingleSelect; /// Marker type for multi-select enum builder #[derive(Debug)] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct MultiSelect; /// Builder for EnumSchema /// Allows to create various enum schema types (single/multi select, titled/untitled) @@ -1077,6 +1089,7 @@ impl EnumSchema { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ElicitationSchema { /// Always "object" for elicitation schemas #[serde(rename = "type")] @@ -1221,6 +1234,7 @@ impl ElicitationSchema { /// .build(); /// ``` #[derive(Debug, Default)] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct ElicitationSchemaBuilder { pub properties: BTreeMap, pub required: Vec, diff --git a/crates/rmcp/src/model/meta.rs b/crates/rmcp/src/model/meta.rs index c60762a35..186db6a24 100644 --- a/crates/rmcp/src/model/meta.rs +++ b/crates/rmcp/src/model/meta.rs @@ -195,6 +195,7 @@ variant_extension! { #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(transparent)] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct Meta(pub JsonObject); const PROGRESS_TOKEN_FIELD: &str = "progressToken"; impl Meta { diff --git a/crates/rmcp/src/model/prompt.rs b/crates/rmcp/src/model/prompt.rs index 531a86d25..72ea0e469 100644 --- a/crates/rmcp/src/model/prompt.rs +++ b/crates/rmcp/src/model/prompt.rs @@ -138,6 +138,7 @@ impl PromptArgument { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum PromptMessageRole { User, Assistant, @@ -147,6 +148,7 @@ pub enum PromptMessageRole { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum PromptMessageContent { /// Plain text content Text { text: String }, diff --git a/crates/rmcp/src/model/resource.rs b/crates/rmcp/src/model/resource.rs index 8a25e25ba..cd5c15d78 100644 --- a/crates/rmcp/src/model/resource.rs +++ b/crates/rmcp/src/model/resource.rs @@ -6,6 +6,7 @@ use super::{Annotated, Icon, Meta}; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RawResource { /// URI representing the resource location (e.g., "file:///path/to/file" or "str:///content") pub uri: String, @@ -39,6 +40,7 @@ pub type Resource = Annotated; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RawResourceTemplate { pub uri_template: String, pub name: String, @@ -58,6 +60,7 @@ pub type ResourceTemplate = Annotated; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(untagged)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum ResourceContents { #[serde(rename_all = "camelCase")] TextResourceContents { diff --git a/crates/rmcp/src/model/task.rs b/crates/rmcp/src/model/task.rs index 343c925ef..dbdc34068 100644 --- a/crates/rmcp/src/model/task.rs +++ b/crates/rmcp/src/model/task.rs @@ -7,6 +7,7 @@ use super::Meta; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum TaskStatus { /// The receiver accepted the request and is currently working on it. #[default] @@ -110,6 +111,7 @@ impl CreateTaskResult { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct GetTaskResult { #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, @@ -160,6 +162,7 @@ impl<'de> serde::Deserialize<'de> for GetTaskPayloadResult { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct CancelTaskResult { #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, @@ -171,6 +174,7 @@ pub struct CancelTaskResult { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct TaskList { pub tasks: Vec, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/rmcp/src/model/tool.rs b/crates/rmcp/src/model/tool.rs index ca6e56915..66d29bc10 100644 --- a/crates/rmcp/src/model/tool.rs +++ b/crates/rmcp/src/model/tool.rs @@ -51,6 +51,7 @@ pub struct Tool { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")] pub enum TaskSupport { /// Clients MUST NOT invoke this tool as a task (default behavior). #[default] diff --git a/crates/rmcp/src/service.rs b/crates/rmcp/src/service.rs index 3bad42519..65b5ee719 100644 --- a/crates/rmcp/src/service.rs +++ b/crates/rmcp/src/service.rs @@ -307,6 +307,7 @@ type Responder = tokio::sync::oneshot::Sender; /// /// or wait for response by call [`RequestHandle::await_response`] #[derive(Debug)] +#[non_exhaustive] pub struct RequestHandle { pub rx: tokio::sync::oneshot::Receiver>, pub options: PeerRequestOptions, @@ -398,6 +399,7 @@ impl std::fmt::Debug for Peer { type ProxyOutbound = mpsc::Receiver>; #[derive(Debug, Default)] +#[non_exhaustive] pub struct PeerRequestOptions { pub timeout: Option, pub meta: Option, @@ -648,6 +650,7 @@ pub enum QuitReason { /// Request execution context #[derive(Debug, Clone)] +#[non_exhaustive] pub struct RequestContext { /// this token will be cancelled when the [`CancelledNotification`] is received. pub ct: CancellationToken, @@ -673,6 +676,7 @@ impl RequestContext { /// Request execution context #[derive(Debug, Clone)] +#[non_exhaustive] pub struct NotificationContext { pub meta: Meta, pub extensions: Extensions, diff --git a/crates/rmcp/src/service/client.rs b/crates/rmcp/src/service/client.rs index 8b49606e4..8031e66ae 100644 --- a/crates/rmcp/src/service/client.rs +++ b/crates/rmcp/src/service/client.rs @@ -140,6 +140,7 @@ where } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RoleClient; impl ServiceRole for RoleClient { diff --git a/crates/rmcp/src/service/server.rs b/crates/rmcp/src/service/server.rs index 5946d23a4..dcf7993a6 100644 --- a/crates/rmcp/src/service/server.rs +++ b/crates/rmcp/src/service/server.rs @@ -27,6 +27,7 @@ use crate::{ }; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RoleServer; impl ServiceRole for RoleServer { @@ -571,6 +572,7 @@ macro_rules! elicit_safe { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] pub enum ElicitationMode { Form, Url, diff --git a/crates/rmcp/src/task_manager.rs b/crates/rmcp/src/task_manager.rs index 774c542f8..32bcf8f0e 100644 --- a/crates/rmcp/src/task_manager.rs +++ b/crates/rmcp/src/task_manager.rs @@ -19,6 +19,7 @@ pub type OperationFuture = /// Describes metadata associated with an enqueued task. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct OperationDescriptor { pub operation_id: String, pub name: String, @@ -55,6 +56,7 @@ impl OperationDescriptor { } /// Operation message describing a unit of asynchronous work. +#[non_exhaustive] pub struct OperationMessage { pub descriptor: OperationDescriptor, pub future: OperationFuture, @@ -91,6 +93,7 @@ struct RunningTask { descriptor: OperationDescriptor, } +#[non_exhaustive] pub struct TaskResult { pub descriptor: OperationDescriptor, pub result: Result, Error>, diff --git a/crates/rmcp/src/transport.rs b/crates/rmcp/src/transport.rs index 2a11f4fed..04a8e1c6a 100644 --- a/crates/rmcp/src/transport.rs +++ b/crates/rmcp/src/transport.rs @@ -153,6 +153,7 @@ where fn into_transport(self) -> impl Transport + 'static; } +#[non_exhaustive] pub enum TransportAdapterIdentity {} impl IntoTransport for T where @@ -233,6 +234,7 @@ where #[derive(Debug, thiserror::Error)] #[error("Transport [{transport_name}] error: {error}")] +#[non_exhaustive] pub struct DynamicTransportError { pub transport_name: Cow<'static, str>, pub transport_type_id: std::any::TypeId, diff --git a/crates/rmcp/src/transport/async_rw.rs b/crates/rmcp/src/transport/async_rw.rs index ff4ecc65b..b14d94c33 100644 --- a/crates/rmcp/src/transport/async_rw.rs +++ b/crates/rmcp/src/transport/async_rw.rs @@ -16,6 +16,7 @@ use tokio_util::{ use super::{IntoTransport, Transport}; use crate::service::{RxJsonRpcMessage, ServiceRole, TxJsonRpcMessage}; +#[non_exhaustive] pub enum TransportAdapterAsyncRW {} impl IntoTransport for (R, W) @@ -29,6 +30,7 @@ where } } +#[non_exhaustive] pub enum TransportAdapterAsyncCombinedRW {} impl IntoTransport for S where @@ -277,6 +279,7 @@ fn try_parse_with_compatibility( } #[derive(Debug, Error)] +#[non_exhaustive] pub enum JsonRpcMessageCodecError { #[error("max line length exceeded")] MaxLineLengthExceeded, diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index 051349d84..349e65066 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -60,6 +60,7 @@ const DEFAULT_EXCHANGE_URL: &str = "http://localhost"; /// Stored credentials for OAuth2 authorization #[derive(Clone, Serialize, Deserialize)] +#[non_exhaustive] pub struct StoredCredentials { pub client_id: String, pub token_response: Option, @@ -134,6 +135,7 @@ impl CredentialStore for InMemoryCredentialStore { /// Stored authorization state for OAuth2 PKCE flow #[derive(Clone, Serialize, Deserialize)] +#[non_exhaustive] pub struct StoredAuthorizationState { pub pkce_verifier: String, pub csrf_token: String, @@ -172,6 +174,7 @@ impl std::fmt::Debug for StoredAuthorizationState { /// } /// ``` #[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[non_exhaustive] pub struct VendorExtraTokenFields(pub HashMap); impl ExtraTokenFields for VendorExtraTokenFields {} @@ -257,6 +260,7 @@ impl StateStore for InMemoryStateStore { /// HTTP client with OAuth 2.0 authorization #[derive(Clone)] +#[non_exhaustive] pub struct AuthClient { pub http_client: C, pub auth_manager: Arc>, @@ -350,6 +354,7 @@ pub enum AuthError { /// oauth2 metadata #[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[non_exhaustive] pub struct AuthorizationMetadata { pub authorization_endpoint: String, pub token_endpoint: String, @@ -373,6 +378,7 @@ struct ResourceServerMetadata { /// Parameters extracted from WWW-Authenticate header #[derive(Debug, Clone, Default)] +#[non_exhaustive] pub struct WWWAuthenticateParams { pub resource_metadata_url: Option, pub scope: Option, @@ -394,6 +400,7 @@ impl WWWAuthenticateParams { /// oauth2 client config #[derive(Debug, Clone)] +#[non_exhaustive] pub struct OAuthClientConfig { pub client_id: String, pub client_secret: Option, @@ -401,6 +408,27 @@ pub struct OAuthClientConfig { pub redirect_uri: String, } +impl OAuthClientConfig { + pub fn new(client_id: impl Into, redirect_uri: impl Into) -> Self { + Self { + client_id: client_id.into(), + client_secret: None, + scopes: Vec::new(), + redirect_uri: redirect_uri.into(), + } + } + + pub fn with_client_secret(mut self, secret: impl Into) -> Self { + self.client_secret = Some(secret.into()); + self + } + + pub fn with_scopes(mut self, scopes: Vec) -> Self { + self.scopes = scopes; + self + } +} + // add type aliases for oauth2 types type OAuthErrorResponse = oauth2::StandardErrorResponse; @@ -440,6 +468,7 @@ pub const EXTENSION_OAUTH_CLIENT_CREDENTIALS: &str = /// JWT signing algorithm for private_key_jwt authentication (SEP-1046) #[cfg(feature = "auth-client-credentials-jwt")] #[derive(Debug, Clone, Copy)] +#[non_exhaustive] pub enum JwtSigningAlgorithm { RS256, RS384, @@ -477,6 +506,7 @@ impl JwtSigningAlgorithm { /// - `ClientSecret`: credentials sent in the request body /// - `PrivateKeyJwt`: RFC 7523 signed JWT assertion (requires `auth-client-credentials-jwt` feature) #[derive(Debug, Clone)] +#[non_exhaustive] pub enum ClientCredentialsConfig { /// Client secret authentication (credentials in request body) ClientSecret { @@ -534,6 +564,7 @@ impl ClientCredentialsConfig { /// Configuration for scope upgrade behavior #[derive(Debug, Clone)] +#[non_exhaustive] pub struct ScopeUpgradeConfig { /// Maximum number of scope upgrade attempts before giving up pub max_upgrade_attempts: u32, @@ -579,6 +610,7 @@ pub(crate) struct ClientRegistrationRequest { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub struct ClientRegistrationResponse { pub client_id: String, pub client_secret: Option, @@ -589,6 +621,18 @@ pub struct ClientRegistrationResponse { pub additional_fields: HashMap, } +impl ClientRegistrationResponse { + pub fn new(client_id: impl Into, redirect_uris: Vec) -> Self { + Self { + client_id: client_id.into(), + client_secret: None, + client_name: None, + redirect_uris, + additional_fields: HashMap::new(), + } + } +} + /// 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 { @@ -2045,6 +2089,7 @@ impl AuthorizationManager { } /// oauth2 authorization session, for guiding user to complete the authorization process +#[non_exhaustive] pub struct AuthorizationSession { pub auth_manager: AuthorizationManager, pub auth_url: String, @@ -2197,6 +2242,7 @@ impl AuthorizedHttpClient { /// OAuth state machine /// Use the OAuthState to manage the OAuth client is more recommend /// But also you can use the AuthorizationManager,AuthorizationSession,AuthorizedHttpClient directly +#[non_exhaustive] pub enum OAuthState { /// the AuthorizationManager Unauthorized(AuthorizationManager), diff --git a/crates/rmcp/src/transport/common/client_side_sse.rs b/crates/rmcp/src/transport/common/client_side_sse.rs index b826b12d4..fc9e15eb7 100644 --- a/crates/rmcp/src/transport/common/client_side_sse.rs +++ b/crates/rmcp/src/transport/common/client_side_sse.rs @@ -17,6 +17,7 @@ pub trait SseRetryPolicy: std::fmt::Debug + Send + Sync { } #[derive(Debug, Clone)] +#[non_exhaustive] pub struct FixedInterval { pub max_times: Option, pub duration: Duration, @@ -47,6 +48,7 @@ impl Default for FixedInterval { } #[derive(Debug, Clone)] +#[non_exhaustive] pub struct ExponentialBackoff { pub max_times: Option, pub base_duration: Duration, @@ -77,6 +79,7 @@ impl SseRetryPolicy for ExponentialBackoff { } #[derive(Debug, Clone, Copy, Default)] +#[non_exhaustive] pub struct NeverRetry; impl SseRetryPolicy for NeverRetry { @@ -169,6 +172,7 @@ impl SseAutoReconnectStream> { pin_project_lite::pin_project! { #[project = SseAutoReconnectStreamStateProj] + #[non_exhaustive] pub enum SseAutoReconnectStreamState { Connected { #[pin] diff --git a/crates/rmcp/src/transport/common/server_side_http.rs b/crates/rmcp/src/transport/common/server_side_http.rs index efa50fd09..d24b19af6 100644 --- a/crates/rmcp/src/transport/common/server_side_http.rs +++ b/crates/rmcp/src/transport/common/server_side_http.rs @@ -58,6 +58,7 @@ impl sse_stream::Timer for TokioTimer { } #[derive(Debug, Clone)] +#[non_exhaustive] 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. diff --git a/crates/rmcp/src/transport/common/unix_socket.rs b/crates/rmcp/src/transport/common/unix_socket.rs index 3af987973..9170c1296 100644 --- a/crates/rmcp/src/transport/common/unix_socket.rs +++ b/crates/rmcp/src/transport/common/unix_socket.rs @@ -21,6 +21,7 @@ use crate::{ }; #[derive(Debug, thiserror::Error)] +#[non_exhaustive] pub enum UnixSocketError { #[error("hyper error: {0}")] Hyper(#[from] hyper::Error), diff --git a/crates/rmcp/src/transport/sink_stream.rs b/crates/rmcp/src/transport/sink_stream.rs index f31743922..6286b53b3 100644 --- a/crates/rmcp/src/transport/sink_stream.rs +++ b/crates/rmcp/src/transport/sink_stream.rs @@ -50,6 +50,7 @@ where } } +#[non_exhaustive] pub enum TransportAdapterSinkStream {} impl IntoTransport for (Si, St) @@ -64,6 +65,7 @@ where } } +#[non_exhaustive] pub enum TransportAdapterAsyncCombinedRW {} impl IntoTransport for S where diff --git a/crates/rmcp/src/transport/streamable_http_client.rs b/crates/rmcp/src/transport/streamable_http_client.rs index 9a27b4935..980e63db1 100644 --- a/crates/rmcp/src/transport/streamable_http_client.rs +++ b/crates/rmcp/src/transport/streamable_http_client.rs @@ -24,11 +24,13 @@ use crate::{ type BoxedSseStream = BoxStream<'static, Result>; #[derive(Debug)] +#[non_exhaustive] pub struct AuthRequiredError { pub www_authenticate_header: String, } #[derive(Debug)] +#[non_exhaustive] pub struct InsufficientScopeError { pub www_authenticate_header: String, pub required_scope: Option, @@ -212,6 +214,7 @@ pub trait StreamableHttpClient: Clone + Send + 'static { + '_; } +#[non_exhaustive] pub struct RetryConfig { pub max_times: Option, pub min_duration: Duration, @@ -253,6 +256,7 @@ struct SessionCleanupInfo { } #[derive(Debug, Clone, Default)] +#[non_exhaustive] pub struct StreamableHttpClientWorker { pub client: C, pub config: StreamableHttpClientTransportConfig, @@ -1046,6 +1050,7 @@ impl StreamableHttpClientTransport { } } #[derive(Debug, Clone)] +#[non_exhaustive] pub struct StreamableHttpClientTransportConfig { pub uri: Arc, pub retry_config: Arc, diff --git a/crates/rmcp/src/transport/streamable_http_server/session/local.rs b/crates/rmcp/src/transport/streamable_http_server/session/local.rs index cad533802..2d2059c59 100644 --- a/crates/rmcp/src/transport/streamable_http_server/session/local.rs +++ b/crates/rmcp/src/transport/streamable_http_server/session/local.rs @@ -29,12 +29,14 @@ use crate::{ }; #[derive(Debug, Default)] +#[non_exhaustive] pub struct LocalSessionManager { pub sessions: tokio::sync::RwLock>, pub session_config: SessionConfig, } #[derive(Debug, Error)] +#[non_exhaustive] pub enum LocalSessionManagerError { #[error("Session not found: {0}")] SessionNotFound(SessionId), @@ -148,6 +150,7 @@ impl std::fmt::Display for EventId { } #[derive(Debug, Clone, Error)] +#[non_exhaustive] pub enum EventIdParseError { #[error("Invalid index: {0}")] InvalidIndex(ParseIntError), @@ -310,6 +313,7 @@ impl LocalSessionWorker { } #[derive(Debug, Error)] +#[non_exhaustive] pub enum SessionError { #[error("Invalid request id: {0}")] DuplicatedRequestId(HttpRequestId), @@ -339,6 +343,7 @@ enum OutboundChannel { Common, } #[derive(Debug)] +#[non_exhaustive] pub struct StreamableHttpMessageReceiver { pub http_request_id: Option, pub inner: Receiver, @@ -657,6 +662,7 @@ impl LocalSessionWorker { } #[derive(Debug)] +#[non_exhaustive] pub enum SessionEvent { ClientMessage { message: ClientJsonRpcMessage, @@ -1059,6 +1065,7 @@ impl Worker for LocalSessionWorker { } #[derive(Debug, Clone)] +#[non_exhaustive] pub struct SessionConfig { /// the capacity of the channel for the session. Default is 16. pub channel_capacity: usize, diff --git a/crates/rmcp/src/transport/streamable_http_server/session/never.rs b/crates/rmcp/src/transport/streamable_http_server/session/never.rs index 436d4cfce..a2f72d820 100644 --- a/crates/rmcp/src/transport/streamable_http_server/session/never.rs +++ b/crates/rmcp/src/transport/streamable_http_server/session/never.rs @@ -10,9 +10,12 @@ use crate::{ #[derive(Debug, Clone, Error)] #[error("Session management is not supported")] +#[non_exhaustive] pub struct ErrorSessionManagementNotSupported; #[derive(Debug, Clone, Default)] +#[non_exhaustive] pub struct NeverSessionManager {} +#[non_exhaustive] pub enum NeverTransport {} impl Transport for NeverTransport { type Error = ErrorSessionManagementNotSupported; diff --git a/crates/rmcp/src/transport/streamable_http_server/tower.rs b/crates/rmcp/src/transport/streamable_http_server/tower.rs index 0130467df..7f4d888c7 100644 --- a/crates/rmcp/src/transport/streamable_http_server/tower.rs +++ b/crates/rmcp/src/transport/streamable_http_server/tower.rs @@ -30,6 +30,7 @@ use crate::{ }; #[derive(Debug, Clone)] +#[non_exhaustive] pub struct StreamableHttpServerConfig { /// The ping message duration for SSE connections. pub sse_keep_alive: Option, @@ -62,6 +63,33 @@ impl Default for StreamableHttpServerConfig { } } +impl StreamableHttpServerConfig { + pub fn with_sse_keep_alive(mut self, duration: Option) -> Self { + self.sse_keep_alive = duration; + self + } + + pub fn with_sse_retry(mut self, duration: Option) -> Self { + self.sse_retry = duration; + self + } + + pub fn with_stateful_mode(mut self, stateful: bool) -> Self { + self.stateful_mode = stateful; + self + } + + pub fn with_json_response(mut self, json_response: bool) -> Self { + self.json_response = json_response; + self + } + + pub fn with_cancellation_token(mut self, token: CancellationToken) -> Self { + self.cancellation_token = token; + self + } +} + #[expect( clippy::result_large_err, reason = "BoxResponse is intentionally large; matches other handlers in this file" diff --git a/crates/rmcp/src/transport/worker.rs b/crates/rmcp/src/transport/worker.rs index d7c53afd4..a5d722d44 100644 --- a/crates/rmcp/src/transport/worker.rs +++ b/crates/rmcp/src/transport/worker.rs @@ -53,6 +53,7 @@ pub trait Worker: Sized + Send + 'static { } } +#[non_exhaustive] pub struct WorkerSendRequest { pub message: TxJsonRpcMessage, pub responder: tokio::sync::oneshot::Sender>, @@ -66,6 +67,7 @@ pub struct WorkerTransport { ct: CancellationToken, } +#[non_exhaustive] pub struct WorkerConfig { pub name: Option, pub channel_buffer_capacity: usize, @@ -79,6 +81,7 @@ impl Default for WorkerConfig { } } } +#[non_exhaustive] pub enum WorkerAdapter {} impl IntoTransport for W { @@ -143,11 +146,13 @@ impl WorkerTransport { } } +#[non_exhaustive] pub struct SendRequest { pub message: TxJsonRpcMessage, pub responder: tokio::sync::oneshot::Sender>, } +#[non_exhaustive] pub struct WorkerContext { pub to_handler_tx: tokio::sync::mpsc::Sender>, pub from_handler_rx: tokio::sync::mpsc::Receiver>, diff --git a/crates/rmcp/tests/test_complex_schema.rs b/crates/rmcp/tests/test_complex_schema.rs index a9c41a3c5..74b9be7c9 100644 --- a/crates/rmcp/tests/test_complex_schema.rs +++ b/crates/rmcp/tests/test_complex_schema.rs @@ -1,3 +1,5 @@ +#![allow(clippy::exhaustive_structs, clippy::exhaustive_enums)] + use rmcp::{ ErrorData as McpError, handler::server::wrapper::Parameters, model::*, schemars, tool, tool_router, diff --git a/crates/rmcp/tests/test_deserialization.rs b/crates/rmcp/tests/test_deserialization.rs index 73621f487..ffcb51eff 100644 --- a/crates/rmcp/tests/test_deserialization.rs +++ b/crates/rmcp/tests/test_deserialization.rs @@ -13,3 +13,122 @@ fn test_tool_list_result() { }) )); } + +/// Regression tests for `#[serde(untagged)]` deserialization of `ServerResult`. +/// +/// `ServerResult` is an untagged enum, so serde tries each variant in declaration +/// order. `GetTaskPayloadResult` has a custom `Deserialize` impl that always fails +/// so it is skipped, and `CustomResult(Value)` acts as the catch-all. If variant +/// ordering changes or the custom impl is removed, these tests will catch the +/// regression. +mod untagged_server_result { + use rmcp::model::{CallToolResult, JsonRpcResponse, ServerJsonRpcMessage, ServerResult}; + use serde_json::json; + + /// Helper: wrap a result value in a JSON-RPC response envelope. + fn wrap_response(result: serde_json::Value) -> serde_json::Value { + json!({ + "jsonrpc": "2.0", + "id": 1, + "result": result + }) + } + + /// Parse a JSON-RPC response and return the inner `ServerResult`. + fn parse_result(json: serde_json::Value) -> ServerResult { + let msg: ServerJsonRpcMessage = serde_json::from_value(json).unwrap(); + match msg { + ServerJsonRpcMessage::Response(JsonRpcResponse { result, .. }) => result, + other => panic!("expected Response, got {other:?}"), + } + } + + #[test] + fn initialize_result_deserializes_to_correct_variant() { + let result = parse_result(wrap_response(json!({ + "protocolVersion": "2025-03-26", + "capabilities": {}, + "serverInfo": { + "name": "test-server", + "version": "1.0.0" + } + }))); + assert!( + matches!(result, ServerResult::InitializeResult(_)), + "expected InitializeResult, got {result:?}" + ); + } + + #[test] + fn call_tool_result_deserializes_to_correct_variant() { + let result = parse_result(wrap_response(json!({ + "content": [ + { "type": "text", "text": "hello" } + ] + }))); + assert!( + matches!(result, ServerResult::CallToolResult(_)), + "expected CallToolResult, got {result:?}" + ); + } + + #[test] + fn empty_object_deserializes_to_empty_result() { + let result = parse_result(wrap_response(json!({}))); + assert!( + matches!(result, ServerResult::EmptyResult(_)), + "expected EmptyResult, got {result:?}" + ); + } + + #[test] + fn unknown_shape_falls_through_to_custom_result() { + // A value that doesn't match any known result type should land in + // CustomResult, NOT GetTaskPayloadResult. + let result = parse_result(wrap_response(json!({ + "some_unknown_field": "some_value", + "number": 42 + }))); + assert!( + matches!(result, ServerResult::CustomResult(_)), + "expected CustomResult, got {result:?}" + ); + } + + #[test] + fn arbitrary_json_value_does_not_deserialize_as_get_task_payload_result() { + // GetTaskPayloadResult wraps a bare Value, but its custom Deserialize + // always fails so serde skips it during untagged resolution. + // Any JSON value must fall through to CustomResult instead. + for value in [json!(42), json!("hello"), json!(null), json!([1, 2, 3])] { + let result = parse_result(wrap_response(value.clone())); + assert!( + matches!(result, ServerResult::CustomResult(_)), + "value {value} should deserialize as CustomResult, got {result:?}" + ); + } + } + + #[test] + fn round_trip_initialize_result_preserves_variant() { + let json = json!({ + "protocolVersion": "2025-03-26", + "capabilities": {}, + "serverInfo": { "name": "test", "version": "1.0" } + }); + // Parse as ServerResult, serialize back, parse again — must stay InitializeResult. + let result = parse_result(wrap_response(json.clone())); + assert!(matches!(&result, ServerResult::InitializeResult(_))); + let reserialized = serde_json::to_value(&result).unwrap(); + let result2 = parse_result(wrap_response(reserialized)); + assert!(matches!(result2, ServerResult::InitializeResult(_))); + } + + #[test] + fn round_trip_call_tool_result_preserves_variant() { + let original = CallToolResult::success(vec![rmcp::model::Content::text("hello world")]); + let json = serde_json::to_value(&original).unwrap(); + let result = parse_result(wrap_response(json)); + assert!(matches!(result, ServerResult::CallToolResult(_))); + } +} diff --git a/crates/rmcp/tests/test_elicitation.rs b/crates/rmcp/tests/test_elicitation.rs index 7d946a2bf..bfa8cc493 100644 --- a/crates/rmcp/tests/test_elicitation.rs +++ b/crates/rmcp/tests/test_elicitation.rs @@ -1458,20 +1458,15 @@ async fn test_peer_request_options_timeout() { let timeout = Some(Duration::from_secs(15)); - let options = PeerRequestOptions { - timeout, - meta: None, - }; + let mut options = PeerRequestOptions::default(); + options.timeout = timeout; // Verify timeout is properly stored assert_eq!(options.timeout, timeout); assert!(options.meta.is_none()); // Test with no timeout - let options_no_timeout = PeerRequestOptions { - timeout: None, - meta: None, - }; + let options_no_timeout = PeerRequestOptions::default(); assert!(options_no_timeout.timeout.is_none()); } diff --git a/crates/rmcp/tests/test_json_schema_detection.rs b/crates/rmcp/tests/test_json_schema_detection.rs index af587319d..5d982cd66 100644 --- a/crates/rmcp/tests/test_json_schema_detection.rs +++ b/crates/rmcp/tests/test_json_schema_detection.rs @@ -1,3 +1,4 @@ +#![allow(clippy::exhaustive_structs)] //cargo test --test test_json_schema_detection --features "client server macros" use rmcp::{ Json, ServerHandler, handler::server::router::tool::ToolRouter, tool, tool_handler, tool_router, diff --git a/crates/rmcp/tests/test_message_protocol.rs b/crates/rmcp/tests/test_message_protocol.rs index 898040dbd..6fdb4285d 100644 --- a/crates/rmcp/tests/test_message_protocol.rs +++ b/crates/rmcp/tests/test_message_protocol.rs @@ -8,7 +8,6 @@ use rmcp::{ model::*, service::{RequestContext, Service}, }; -use tokio_util::sync::CancellationToken; // Tests start here #[tokio::test] @@ -48,13 +47,7 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(1), - meta: Default::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(1), client.peer().clone()), ) .await?; @@ -85,13 +78,7 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(2), - meta: Default::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(2), client.peer().clone()), ) .await?; @@ -122,13 +109,7 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(3), - meta: Default::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(3), client.peer().clone()), ) .await?; @@ -179,13 +160,7 @@ async fn test_context_inclusion_ignored_integration() -> anyhow::Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(1), - meta: Meta::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(1), client.peer().clone()), ) .await?; @@ -241,13 +216,7 @@ async fn test_message_sequence_integration() -> anyhow::Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(1), - meta: Meta::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(1), client.peer().clone()), ) .await?; @@ -307,13 +276,7 @@ async fn test_message_sequence_validation_integration() -> anyhow::Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(1), - meta: Meta::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(1), client.peer().clone()), ) .await?; @@ -330,13 +293,7 @@ async fn test_message_sequence_validation_integration() -> anyhow::Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(2), - meta: Meta::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(2), client.peer().clone()), ) .await; @@ -370,13 +327,7 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(1), - meta: Meta::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(1), client.peer().clone()), ) .await?; @@ -405,13 +356,7 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(2), - meta: Meta::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(2), client.peer().clone()), ) .await?; @@ -457,13 +402,7 @@ async fn test_context_inclusion() -> anyhow::Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(1), - meta: Meta::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(1), client.peer().clone()), ) .await?; diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json index bd8f744b0..c1aa13dea 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json @@ -388,7 +388,6 @@ "content": { "description": "The content returned by the tool (text, images, etc.)", "type": "array", - "default": [], "items": { "$ref": "#/definitions/Annotated" } @@ -403,7 +402,10 @@ "structuredContent": { "description": "An optional JSON object that represents the structured result of the tool call" } - } + }, + "required": [ + "content" + ] }, "CancelTaskResult": { "description": "Response to a `tasks/cancel` request.\n\nPer spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.", diff --git a/crates/rmcp/tests/test_sampling.rs b/crates/rmcp/tests/test_sampling.rs index 62bba1123..7bd6cd118 100644 --- a/crates/rmcp/tests/test_sampling.rs +++ b/crates/rmcp/tests/test_sampling.rs @@ -8,7 +8,6 @@ use rmcp::{ model::*, service::{RequestContext, Service}, }; -use tokio_util::sync::CancellationToken; #[tokio::test] async fn test_basic_sampling_message_creation() -> Result<()> { @@ -126,13 +125,7 @@ async fn test_sampling_integration_with_test_handlers() -> Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(1), - meta: Default::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(1), client.peer().clone()), ) .await?; @@ -189,13 +182,7 @@ async fn test_sampling_no_context_inclusion() -> Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(2), - meta: Default::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(2), client.peer().clone()), ) .await?; @@ -253,13 +240,7 @@ async fn test_sampling_error_invalid_message_sequence() -> Result<()> { let result = handler .handle_request( request.clone(), - RequestContext { - peer: client.peer().clone(), - ct: CancellationToken::new(), - id: NumberOrString::Number(3), - meta: Default::default(), - extensions: Default::default(), - }, + RequestContext::new(NumberOrString::Number(3), client.peer().clone()), ) .await; diff --git a/crates/rmcp/tests/test_sse_concurrent_streams.rs b/crates/rmcp/tests/test_sse_concurrent_streams.rs index a7821fe9d..37bbfd721 100644 --- a/crates/rmcp/tests/test_sse_concurrent_streams.rs +++ b/crates/rmcp/tests/test_sse_concurrent_streams.rs @@ -74,13 +74,7 @@ async fn start_test_server(ct: CancellationToken, trigger: Arc) -> Strin let service = StreamableHttpService::new( move || Ok(server.clone()), Arc::new(LocalSessionManager::default()), - StreamableHttpServerConfig { - stateful_mode: true, - sse_keep_alive: Some(Duration::from_secs(15)), - sse_retry: Some(Duration::from_secs(3)), - cancellation_token: ct.child_token(), - ..Default::default() - }, + StreamableHttpServerConfig::default().with_cancellation_token(ct.child_token()), ); let router = axum::Router::new().nest_service("/mcp", service); diff --git a/crates/rmcp/tests/test_streamable_http_json_response.rs b/crates/rmcp/tests/test_streamable_http_json_response.rs index b023acd06..09dd69ccd 100644 --- a/crates/rmcp/tests/test_streamable_http_json_response.rs +++ b/crates/rmcp/tests/test_streamable_http_json_response.rs @@ -37,13 +37,13 @@ async fn spawn_server( #[tokio::test] async fn stateless_json_response_returns_application_json() -> anyhow::Result<()> { let ct = CancellationToken::new(); - let (client, url, ct) = spawn_server(StreamableHttpServerConfig { - stateful_mode: false, - json_response: true, - sse_keep_alive: None, - cancellation_token: ct.child_token(), - ..Default::default() - }) + let (client, url, ct) = spawn_server( + StreamableHttpServerConfig::default() + .with_stateful_mode(false) + .with_json_response(true) + .with_sse_keep_alive(None) + .with_cancellation_token(ct.child_token()), + ) .await; let response = client @@ -79,13 +79,12 @@ async fn stateless_json_response_returns_application_json() -> anyhow::Result<() #[tokio::test] async fn stateless_sse_mode_default_unchanged() -> anyhow::Result<()> { let ct = CancellationToken::new(); - let (client, url, ct) = spawn_server(StreamableHttpServerConfig { - stateful_mode: false, - json_response: false, - sse_keep_alive: None, - cancellation_token: ct.child_token(), - ..Default::default() - }) + let (client, url, ct) = spawn_server( + StreamableHttpServerConfig::default() + .with_stateful_mode(false) + .with_sse_keep_alive(None) + .with_cancellation_token(ct.child_token()), + ) .await; let response = client @@ -122,13 +121,12 @@ async fn stateless_sse_mode_default_unchanged() -> anyhow::Result<()> { async fn json_response_ignored_in_stateful_mode() -> anyhow::Result<()> { let ct = CancellationToken::new(); // json_response: true has no effect when stateful_mode: true — server still uses SSE - let (client, url, ct) = spawn_server(StreamableHttpServerConfig { - stateful_mode: true, - json_response: true, - sse_keep_alive: None, - cancellation_token: ct.child_token(), - ..Default::default() - }) + let (client, url, ct) = spawn_server( + StreamableHttpServerConfig::default() + .with_json_response(true) + .with_sse_keep_alive(None) + .with_cancellation_token(ct.child_token()), + ) .await; let response = client diff --git a/crates/rmcp/tests/test_streamable_http_priming.rs b/crates/rmcp/tests/test_streamable_http_priming.rs index 5e771024c..3be3700b8 100644 --- a/crates/rmcp/tests/test_streamable_http_priming.rs +++ b/crates/rmcp/tests/test_streamable_http_priming.rs @@ -18,12 +18,9 @@ async fn test_priming_on_stream_start() -> anyhow::Result<()> { StreamableHttpService::new( || Ok(Calculator::new()), Default::default(), - StreamableHttpServerConfig { - stateful_mode: true, - sse_keep_alive: None, - cancellation_token: ct.child_token(), - ..Default::default() - }, + StreamableHttpServerConfig::default() + .with_sse_keep_alive(None) + .with_cancellation_token(ct.child_token()), ); let router = axum::Router::new().nest_service("/mcp", service); @@ -87,12 +84,9 @@ async fn test_priming_on_stream_close() -> anyhow::Result<()> { let service = StreamableHttpService::new( || Ok(Calculator::new()), session_manager.clone(), - StreamableHttpServerConfig { - stateful_mode: true, - sse_keep_alive: None, - cancellation_token: ct.child_token(), - ..Default::default() - }, + StreamableHttpServerConfig::default() + .with_sse_keep_alive(None) + .with_cancellation_token(ct.child_token()), ); let router = axum::Router::new().nest_service("/mcp", service); diff --git a/crates/rmcp/tests/test_streamable_http_stale_session.rs b/crates/rmcp/tests/test_streamable_http_stale_session.rs index b385cc52b..d96d83c73 100644 --- a/crates/rmcp/tests/test_streamable_http_stale_session.rs +++ b/crates/rmcp/tests/test_streamable_http_stale_session.rs @@ -32,12 +32,9 @@ async fn test_stale_session_id_returns_status_aware_error() -> anyhow::Result<() StreamableHttpService::new( || Ok(Calculator::new()), Default::default(), - StreamableHttpServerConfig { - stateful_mode: true, - sse_keep_alive: None, - cancellation_token: ct.child_token(), - ..Default::default() - }, + StreamableHttpServerConfig::default() + .with_sse_keep_alive(None) + .with_cancellation_token(ct.child_token()), ); let router = axum::Router::new().nest_service("/mcp", service); @@ -103,12 +100,9 @@ async fn test_transparent_reinitialization_on_session_expiry() -> anyhow::Result let service = StreamableHttpService::new( || Ok(Calculator::new()), session_manager.clone(), - StreamableHttpServerConfig { - stateful_mode: true, - sse_keep_alive: None, - cancellation_token: ct.child_token(), - ..Default::default() - }, + StreamableHttpServerConfig::default() + .with_sse_keep_alive(None) + .with_cancellation_token(ct.child_token()), ); let router = axum::Router::new().nest_service("/mcp", service); @@ -183,12 +177,9 @@ async fn test_session_expired_error_when_reinit_disabled() -> anyhow::Result<()> let service = StreamableHttpService::new( || Ok(Calculator::new()), session_manager.clone(), - StreamableHttpServerConfig { - stateful_mode: true, - sse_keep_alive: None, - cancellation_token: ct.child_token(), - ..Default::default() - }, + StreamableHttpServerConfig::default() + .with_sse_keep_alive(None) + .with_cancellation_token(ct.child_token()), ); let router = axum::Router::new().nest_service("/mcp", service); diff --git a/crates/rmcp/tests/test_structured_output.rs b/crates/rmcp/tests/test_structured_output.rs index b498d3120..7bb62e650 100644 --- a/crates/rmcp/tests/test_structured_output.rs +++ b/crates/rmcp/tests/test_structured_output.rs @@ -1,3 +1,4 @@ +#![allow(clippy::exhaustive_structs)] //cargo test --test test_structured_output --features "client server macros" use rmcp::{ Json, ServerHandler, diff --git a/crates/rmcp/tests/test_tool_builder_methods.rs b/crates/rmcp/tests/test_tool_builder_methods.rs index f93c05462..8be7e5c3e 100644 --- a/crates/rmcp/tests/test_tool_builder_methods.rs +++ b/crates/rmcp/tests/test_tool_builder_methods.rs @@ -1,3 +1,4 @@ +#![allow(clippy::exhaustive_structs)] //cargo test --test test_tool_builder_methods --features "client server macros" use rmcp::model::{JsonObject, Tool}; use schemars::JsonSchema; diff --git a/crates/rmcp/tests/test_with_js.rs b/crates/rmcp/tests/test_with_js.rs index 685ea1430..0dbd93f3f 100644 --- a/crates/rmcp/tests/test_with_js.rs +++ b/crates/rmcp/tests/test_with_js.rs @@ -69,12 +69,9 @@ async fn test_with_js_streamable_http_client() -> anyhow::Result<()> { StreamableHttpService::new( || Ok(Calculator::new()), Default::default(), - StreamableHttpServerConfig { - stateful_mode: true, - sse_keep_alive: None, - cancellation_token: ct.child_token(), - ..Default::default() - }, + StreamableHttpServerConfig::default() + .with_sse_keep_alive(None) + .with_cancellation_token(ct.child_token()), ); let router = axum::Router::new().nest_service("/mcp", service); let tcp_listener = tokio::net::TcpListener::bind(STREAMABLE_HTTP_BIND_ADDRESS).await?; diff --git a/examples/servers/src/complex_auth_streamhttp.rs b/examples/servers/src/complex_auth_streamhttp.rs index 4afacf8d2..34c4b1584 100644 --- a/examples/servers/src/complex_auth_streamhttp.rs +++ b/examples/servers/src/complex_auth_streamhttp.rs @@ -51,12 +51,9 @@ impl McpOAuthStore { let mut clients = HashMap::new(); clients.insert( "mcp-client".to_string(), - OAuthClientConfig { - client_id: "mcp-client".to_string(), - client_secret: Some("mcp-client-secret".to_string()), - scopes: vec!["profile".to_string(), "email".to_string()], - redirect_uri: "http://localhost:8080/callback".to_string(), - }, + OAuthClientConfig::new("mcp-client", "http://localhost:8080/callback") + .with_client_secret("mcp-client-secret") + .with_scopes(vec!["profile".to_string(), "email".to_string()]), ); Self { @@ -520,17 +517,16 @@ async fn oauth_authorization_server() -> impl IntoResponse { "response_types_supported".into(), Value::Array(vec![Value::String("code".into())]), ); - let metadata = AuthorizationMetadata { - authorization_endpoint: format!("http://{}/oauth/authorize", BIND_ADDRESS), - token_endpoint: format!("http://{}/oauth/token", BIND_ADDRESS), - scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]), - registration_endpoint: Some(format!("http://{}/oauth/register", BIND_ADDRESS)), - response_types_supported: Some(vec!["code".to_string()]), - code_challenge_methods_supported: Some(vec!["S256".to_string()]), - issuer: Some(BIND_ADDRESS.to_string()), - jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)), - additional_fields, - }; + let mut metadata = AuthorizationMetadata::default(); + metadata.authorization_endpoint = format!("http://{}/oauth/authorize", BIND_ADDRESS); + metadata.token_endpoint = format!("http://{}/oauth/token", BIND_ADDRESS); + metadata.scopes_supported = Some(vec!["profile".to_string(), "email".to_string()]); + metadata.registration_endpoint = Some(format!("http://{}/oauth/register", BIND_ADDRESS)); + metadata.response_types_supported = Some(vec!["code".to_string()]); + metadata.code_challenge_methods_supported = Some(vec!["S256".to_string()]); + metadata.issuer = Some(BIND_ADDRESS.to_string()); + metadata.jwks_uri = Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)); + metadata.additional_fields = additional_fields; debug!("metadata: {:?}", metadata); (StatusCode::OK, Json(metadata)) } @@ -556,12 +552,8 @@ async fn oauth_register( let client_id = format!("client-{}", Uuid::new_v4()); let client_secret = generate_random_string(32); - let client = OAuthClientConfig { - client_id: client_id.clone(), - client_secret: Some(client_secret.clone()), - redirect_uri: req.redirect_uris[0].clone(), - scopes: vec![], - }; + let client = OAuthClientConfig::new(client_id.clone(), req.redirect_uris[0].clone()) + .with_client_secret(client_secret.clone()); state .clients @@ -570,13 +562,9 @@ async fn oauth_register( .insert(client_id.clone(), client); // return client information - let response = ClientRegistrationResponse { - client_id, - client_secret: Some(client_secret), - client_name: Some(req.client_name), - redirect_uris: req.redirect_uris, - additional_fields: HashMap::new(), - }; + let mut response = ClientRegistrationResponse::new(client_id, req.redirect_uris); + response.client_secret = Some(client_secret); + response.client_name = Some(req.client_name); (StatusCode::CREATED, Json(response)).into_response() } diff --git a/examples/servers/src/counter_streamhttp.rs b/examples/servers/src/counter_streamhttp.rs index db9b9df18..3811c09f4 100644 --- a/examples/servers/src/counter_streamhttp.rs +++ b/examples/servers/src/counter_streamhttp.rs @@ -25,10 +25,7 @@ async fn main() -> anyhow::Result<()> { let service = StreamableHttpService::new( || Ok(Counter::new()), LocalSessionManager::default().into(), - StreamableHttpServerConfig { - cancellation_token: ct.child_token(), - ..Default::default() - }, + StreamableHttpServerConfig::default().with_cancellation_token(ct.child_token()), ); let router = axum::Router::new().nest_service("/mcp", service);