diff --git a/crates/rmcp/src/handler/client.rs b/crates/rmcp/src/handler/client.rs index d023c5d2..15b1c0c0 100644 --- a/crates/rmcp/src/handler/client.rs +++ b/crates/rmcp/src/handler/client.rs @@ -26,6 +26,10 @@ impl Service for H { .create_elicitation(request.params, context) .await .map(ClientResult::CreateElicitationResult), + ServerRequest::CustomRequest(request) => self + .on_custom_request(request, context) + .await + .map(ClientResult::CustomResult), } } @@ -123,6 +127,20 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { })) } + 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, diff --git a/crates/rmcp/src/handler/server.rs b/crates/rmcp/src/handler/server.rs index 2b55cacb..b16aeddc 100644 --- a/crates/rmcp/src/handler/server.rs +++ b/crates/rmcp/src/handler/server.rs @@ -69,6 +69,10 @@ impl Service for H { .list_tools(request.params, context) .await .map(ServerResult::ListToolsResult), + ClientRequest::CustomRequest(request) => self + .on_custom_request(request, context) + .await + .map(ServerResult::CustomResult), } } @@ -200,6 +204,19 @@ pub trait ServerHandler: Sized + Send + Sync + 'static { ) -> impl Future> + Send + '_ { std::future::ready(Ok(ListToolsResult::default())) } + 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, diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 92eca08b..8837bdf1 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -55,6 +55,7 @@ macro_rules! object { /// /// 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 {} @@ -606,6 +607,23 @@ 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))] @@ -661,6 +679,40 @@ impl CustomNotification { } } +/// 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. @@ -1757,11 +1809,12 @@ ts_union!( | SubscribeRequest | UnsubscribeRequest | CallToolRequest - | ListToolsRequest; + | ListToolsRequest + | CustomRequest; ); impl ClientRequest { - pub fn method(&self) -> &'static str { + pub fn method(&self) -> &str { match &self { ClientRequest::PingRequest(r) => r.method.as_str(), ClientRequest::InitializeRequest(r) => r.method.as_str(), @@ -1776,6 +1829,7 @@ impl ClientRequest { ClientRequest::UnsubscribeRequest(r) => r.method.as_str(), ClientRequest::CallToolRequest(r) => r.method.as_str(), ClientRequest::ListToolsRequest(r) => r.method.as_str(), + ClientRequest::CustomRequest(r) => r.method.as_str(), } } } @@ -1790,7 +1844,12 @@ ts_union!( ); ts_union!( - export type ClientResult = box CreateMessageResult | ListRootsResult | CreateElicitationResult | EmptyResult; + export type ClientResult = + box CreateMessageResult + | ListRootsResult + | CreateElicitationResult + | EmptyResult + | CustomResult; ); impl ClientResult { @@ -1806,7 +1865,8 @@ ts_union!( | PingRequest | CreateMessageRequest | ListRootsRequest - | CreateElicitationRequest; + | CreateElicitationRequest + | CustomRequest; ); ts_union!( @@ -1834,6 +1894,7 @@ ts_union!( | ListToolsResult | CreateElicitationResult | EmptyResult + | CustomResult ; ); @@ -1960,6 +2021,40 @@ mod tests { 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!( { diff --git a/crates/rmcp/src/model/meta.rs b/crates/rmcp/src/model/meta.rs index 1054d672..e93ebf19 100644 --- a/crates/rmcp/src/model/meta.rs +++ b/crates/rmcp/src/model/meta.rs @@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use super::{ - ClientNotification, ClientRequest, CustomNotification, Extensions, JsonObject, JsonRpcMessage, - NumberOrString, ProgressToken, ServerNotification, ServerRequest, + ClientNotification, ClientRequest, CustomNotification, CustomRequest, Extensions, JsonObject, + JsonRpcMessage, NumberOrString, ProgressToken, ServerNotification, ServerRequest, }; pub trait GetMeta { @@ -38,6 +38,26 @@ impl GetMeta for CustomNotification { } } +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 { @@ -86,6 +106,7 @@ variant_extension! { UnsubscribeRequest CallToolRequest ListToolsRequest + CustomRequest } } @@ -95,6 +116,7 @@ variant_extension! { CreateMessageRequest ListRootsRequest CreateElicitationRequest + CustomRequest } } diff --git a/crates/rmcp/src/model/serde_impl.rs b/crates/rmcp/src/model/serde_impl.rs index b43335f3..8b88b5e0 100644 --- a/crates/rmcp/src/model/serde_impl.rs +++ b/crates/rmcp/src/model/serde_impl.rs @@ -3,8 +3,8 @@ use std::borrow::Cow; use serde::{Deserialize, Serialize}; use super::{ - CustomNotification, Extensions, Meta, Notification, NotificationNoParam, Request, - RequestNoParam, RequestOptionalParam, + CustomNotification, CustomRequest, Extensions, Meta, Notification, NotificationNoParam, + Request, RequestNoParam, RequestOptionalParam, }; #[derive(Serialize, Deserialize)] struct WithMeta<'a, P> { @@ -249,6 +249,59 @@ where } } +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 diff --git a/crates/rmcp/tests/test_custom_request.rs b/crates/rmcp/tests/test_custom_request.rs new file mode 100644 index 00000000..83a8d347 --- /dev/null +++ b/crates/rmcp/tests/test_custom_request.rs @@ -0,0 +1,189 @@ +use std::sync::Arc; + +use rmcp::{ + ClientHandler, ServerHandler, ServiceExt, + model::{ + ClientRequest, ClientResult, CustomRequest, CustomResult, ServerRequest, ServerResult, + }, +}; +use serde_json::json; +use tokio::sync::{Mutex, Notify}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +type CustomRequestPayload = (String, Option); + +struct CustomRequestServer { + receive_signal: Arc, + payload: Arc>>, +} + +impl ServerHandler for CustomRequestServer { + async fn on_custom_request( + &self, + request: CustomRequest, + _context: rmcp::service::RequestContext, + ) -> Result { + let CustomRequest { method, params, .. } = request; + *self.payload.lock().await = Some((method, params)); + self.receive_signal.notify_one(); + Ok(CustomResult::new(json!({ "status": "ok" }))) + } +} + +#[tokio::test] +async fn test_custom_client_request_reaches_server() -> anyhow::Result<()> { + let _ = tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "debug".to_string().into()), + ) + .with(tracing_subscriber::fmt::layer()) + .try_init(); + + let (server_transport, client_transport) = tokio::io::duplex(4096); + let receive_signal = Arc::new(Notify::new()); + let payload = Arc::new(Mutex::new(None)); + + { + let receive_signal = receive_signal.clone(); + let payload = payload.clone(); + tokio::spawn(async move { + let server = CustomRequestServer { + receive_signal, + payload, + } + .serve(server_transport) + .await?; + server.waiting().await?; + anyhow::Ok(()) + }); + } + + let client = ().serve(client_transport).await?; + + let response = client + .send_request(ClientRequest::CustomRequest(CustomRequest::new( + "requests/custom-test", + Some(json!({ "foo": "bar" })), + ))) + .await?; + + tokio::time::timeout(std::time::Duration::from_secs(5), receive_signal.notified()).await?; + + let (method, params) = payload.lock().await.take().expect("payload set"); + assert_eq!("requests/custom-test", method); + assert_eq!(Some(json!({ "foo": "bar" })), params); + + match response { + ServerResult::CustomResult(result) => { + assert_eq!(result.0, json!({ "status": "ok" })); + } + other => panic!("Expected custom result, got: {other:?}"), + } + + client.cancel().await?; + Ok(()) +} + +struct CustomRequestClient { + receive_signal: Arc, + payload: Arc>>, +} + +impl ClientHandler for CustomRequestClient { + async fn on_custom_request( + &self, + request: CustomRequest, + _context: rmcp::service::RequestContext, + ) -> Result { + let CustomRequest { method, params, .. } = request; + *self.payload.lock().await = Some((method, params)); + self.receive_signal.notify_one(); + Ok(CustomResult::new(json!({ "status": "ok" }))) + } +} + +struct CustomRequestServerNotifier { + receive_signal: Arc, + response: Arc>>>, +} + +impl ServerHandler for CustomRequestServerNotifier { + async fn on_initialized(&self, context: rmcp::service::NotificationContext) { + let peer = context.peer.clone(); + let receive_signal = self.receive_signal.clone(); + let response = self.response.clone(); + tokio::spawn(async move { + let result = peer + .send_request(ServerRequest::CustomRequest(CustomRequest::new( + "requests/custom-server", + Some(json!({ "ping": "pong" })), + ))) + .await; + let payload = match result { + Ok(ClientResult::CustomResult(result)) => Ok(result.0), + Ok(other) => Err(format!("Unexpected response: {other:?}")), + Err(err) => Err(format!("Failed to send request: {err:?}")), + }; + *response.lock().await = Some(payload); + receive_signal.notify_one(); + }); + } +} + +#[tokio::test] +async fn test_custom_server_request_reaches_client() -> anyhow::Result<()> { + let _ = tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "debug".to_string().into()), + ) + .with(tracing_subscriber::fmt::layer()) + .try_init(); + + let (server_transport, client_transport) = tokio::io::duplex(4096); + let response_signal = Arc::new(Notify::new()); + let response = Arc::new(Mutex::new(None)); + tokio::spawn({ + let response_signal = response_signal.clone(); + let response = response.clone(); + async move { + let server = CustomRequestServerNotifier { + receive_signal: response_signal, + response, + } + .serve(server_transport) + .await?; + server.waiting().await?; + anyhow::Ok(()) + } + }); + + let receive_signal = Arc::new(Notify::new()); + let payload = Arc::new(Mutex::new(None)); + + let client = CustomRequestClient { + receive_signal: receive_signal.clone(), + payload: payload.clone(), + } + .serve(client_transport) + .await?; + + tokio::time::timeout(std::time::Duration::from_secs(5), receive_signal.notified()).await?; + tokio::time::timeout( + std::time::Duration::from_secs(5), + response_signal.notified(), + ) + .await?; + + let (method, params) = payload.lock().await.take().expect("payload set"); + assert_eq!("requests/custom-server", method); + assert_eq!(Some(json!({ "ping": "pong" })), params); + + let response = response.lock().await.take().expect("response set"); + let response = response.expect("custom request response ok"); + assert_eq!(response, json!({ "status": "ok" })); + + client.cancel().await?; + Ok(()) +} diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json index c1c3a73e..4474dc82 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json @@ -288,6 +288,9 @@ }, { "$ref": "#/definitions/EmptyObject" + }, + { + "$ref": "#/definitions/CustomResult" } ] }, @@ -409,6 +412,22 @@ "method" ] }, + "CustomRequest": { + "description": "A catch-all request either side can use to send custom messages to its peer.\n\nThis preserves the raw `method` name and `params` payload so handlers can\ndeserialize them into domain-specific types.", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "params": true + }, + "required": [ + "method" + ] + }, + "CustomResult": { + "description": "A catch-all response either side can use for custom requests." + }, "ElicitationAction": { "description": "Represents the possible actions a user can take in response to an elicitation request.\n\nWhen a server requests user input through elicitation, the user can:\n- Accept: Provide the requested information and continue\n- Decline: Refuse to provide the information but continue the operation\n- Cancel: Stop the entire operation", "oneOf": [ @@ -444,7 +463,8 @@ }, "EmptyObject": { "description": "This is commonly used for representing empty objects in MCP messages.\n\nwithout returning any specific data.", - "type": "object" + "type": "object", + "additionalProperties": false }, "ErrorCode": { "description": "Standard JSON-RPC error codes used throughout the MCP protocol.\n\nThese codes follow the JSON-RPC 2.0 specification and provide\nstandardized error reporting across all MCP implementations.", @@ -707,6 +727,9 @@ }, { "$ref": "#/definitions/RequestOptionalParam4" + }, + { + "$ref": "#/definitions/CustomRequest" } ], "required": [ diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json index c1c3a73e..4474dc82 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json @@ -288,6 +288,9 @@ }, { "$ref": "#/definitions/EmptyObject" + }, + { + "$ref": "#/definitions/CustomResult" } ] }, @@ -409,6 +412,22 @@ "method" ] }, + "CustomRequest": { + "description": "A catch-all request either side can use to send custom messages to its peer.\n\nThis preserves the raw `method` name and `params` payload so handlers can\ndeserialize them into domain-specific types.", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "params": true + }, + "required": [ + "method" + ] + }, + "CustomResult": { + "description": "A catch-all response either side can use for custom requests." + }, "ElicitationAction": { "description": "Represents the possible actions a user can take in response to an elicitation request.\n\nWhen a server requests user input through elicitation, the user can:\n- Accept: Provide the requested information and continue\n- Decline: Refuse to provide the information but continue the operation\n- Cancel: Stop the entire operation", "oneOf": [ @@ -444,7 +463,8 @@ }, "EmptyObject": { "description": "This is commonly used for representing empty objects in MCP messages.\n\nwithout returning any specific data.", - "type": "object" + "type": "object", + "additionalProperties": false }, "ErrorCode": { "description": "Standard JSON-RPC error codes used throughout the MCP protocol.\n\nThese codes follow the JSON-RPC 2.0 specification and provide\nstandardized error reporting across all MCP implementations.", @@ -707,6 +727,9 @@ }, { "$ref": "#/definitions/RequestOptionalParam4" + }, + { + "$ref": "#/definitions/CustomRequest" } ], "required": [ diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json index dcc3086f..bde58c3b 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json @@ -392,19 +392,6 @@ "content" ] }, - "CustomNotification": { - "description": "A catch-all notification either side can use to send custom messages to its peer.\n\nThis preserves the raw `method` name and `params` payload so handlers can\ndeserialize them into domain-specific types.", - "type": "object", - "properties": { - "method": { - "type": "string" - }, - "params": true - }, - "required": [ - "method" - ] - }, "CancelledNotificationMethod": { "type": "string", "format": "const", @@ -606,6 +593,35 @@ "maxTokens" ] }, + "CustomNotification": { + "description": "A catch-all notification either side can use to send custom messages to its peer.\n\nThis preserves the raw `method` name and `params` payload so handlers can\ndeserialize them into domain-specific types.", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "params": true + }, + "required": [ + "method" + ] + }, + "CustomRequest": { + "description": "A catch-all request either side can use to send custom messages to its peer.\n\nThis preserves the raw `method` name and `params` payload so handlers can\ndeserialize them into domain-specific types.", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "params": true + }, + "required": [ + "method" + ] + }, + "CustomResult": { + "description": "A catch-all response either side can use for custom requests." + }, "ElicitationAction": { "description": "Represents the possible actions a user can take in response to an elicitation request.\n\nWhen a server requests user input through elicitation, the user can:\n- Accept: Provide the requested information and continue\n- Decline: Refuse to provide the information but continue the operation\n- Cancel: Stop the entire operation", "oneOf": [ @@ -682,7 +698,8 @@ }, "EmptyObject": { "description": "This is commonly used for representing empty objects in MCP messages.\n\nwithout returning any specific data.", - "type": "object" + "type": "object", + "additionalProperties": false }, "EnumSchema": { "description": "Schema definition for enum properties.\n\nCompliant with MCP 2025-06-18 specification for elicitation schemas.\nEnums must have string type and can optionally include human-readable names.", @@ -1021,6 +1038,9 @@ }, { "$ref": "#/definitions/Request2" + }, + { + "$ref": "#/definitions/CustomRequest" } ], "required": [ @@ -2271,6 +2291,9 @@ }, { "$ref": "#/definitions/EmptyObject" + }, + { + "$ref": "#/definitions/CustomResult" } ] }, 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 dcc3086f..bde58c3b 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 @@ -392,19 +392,6 @@ "content" ] }, - "CustomNotification": { - "description": "A catch-all notification either side can use to send custom messages to its peer.\n\nThis preserves the raw `method` name and `params` payload so handlers can\ndeserialize them into domain-specific types.", - "type": "object", - "properties": { - "method": { - "type": "string" - }, - "params": true - }, - "required": [ - "method" - ] - }, "CancelledNotificationMethod": { "type": "string", "format": "const", @@ -606,6 +593,35 @@ "maxTokens" ] }, + "CustomNotification": { + "description": "A catch-all notification either side can use to send custom messages to its peer.\n\nThis preserves the raw `method` name and `params` payload so handlers can\ndeserialize them into domain-specific types.", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "params": true + }, + "required": [ + "method" + ] + }, + "CustomRequest": { + "description": "A catch-all request either side can use to send custom messages to its peer.\n\nThis preserves the raw `method` name and `params` payload so handlers can\ndeserialize them into domain-specific types.", + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "params": true + }, + "required": [ + "method" + ] + }, + "CustomResult": { + "description": "A catch-all response either side can use for custom requests." + }, "ElicitationAction": { "description": "Represents the possible actions a user can take in response to an elicitation request.\n\nWhen a server requests user input through elicitation, the user can:\n- Accept: Provide the requested information and continue\n- Decline: Refuse to provide the information but continue the operation\n- Cancel: Stop the entire operation", "oneOf": [ @@ -682,7 +698,8 @@ }, "EmptyObject": { "description": "This is commonly used for representing empty objects in MCP messages.\n\nwithout returning any specific data.", - "type": "object" + "type": "object", + "additionalProperties": false }, "EnumSchema": { "description": "Schema definition for enum properties.\n\nCompliant with MCP 2025-06-18 specification for elicitation schemas.\nEnums must have string type and can optionally include human-readable names.", @@ -1021,6 +1038,9 @@ }, { "$ref": "#/definitions/Request2" + }, + { + "$ref": "#/definitions/CustomRequest" } ], "required": [ @@ -2271,6 +2291,9 @@ }, { "$ref": "#/definitions/EmptyObject" + }, + { + "$ref": "#/definitions/CustomResult" } ] },