diff --git a/crates/rmcp/src/handler/server.rs b/crates/rmcp/src/handler/server.rs index 0fb4bf89..54be83bd 100644 --- a/crates/rmcp/src/handler/server.rs +++ b/crates/rmcp/src/handler/server.rs @@ -27,6 +27,9 @@ impl Service for H { // `context` is moved into the dispatch below, so read the negotiated version first. let protocol_version = context.protocol_version(); let result = match request { + ClientRequest::DiscoverRequest(_request) => { + Err(McpError::method_not_found::()) + } ClientRequest::InitializeRequest(request) => self .initialize(request.params, context) .await diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index fc45f1ef..d800a968 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -808,6 +808,55 @@ impl CustomRequest { } } +const_string!(DiscoverRequestMethod = "server/discover"); + +/// Sent from the client to discover server identity, capabilities, and supported protocol versions. +pub type DiscoverRequest = Request; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[non_exhaustive] +pub struct DiscoverResult { + pub supported_versions: Vec, + pub capabilities: ServerCapabilities, + pub server_info: Implementation, + #[serde(skip_serializing_if = "Option::is_none")] + pub instructions: Option, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +impl DiscoverResult { + pub fn new( + supported_versions: impl Into>, + capabilities: ServerCapabilities, + ) -> Self { + Self { + supported_versions: supported_versions.into(), + capabilities, + server_info: Implementation::from_build_env(), + instructions: None, + meta: None, + } + } + + pub fn with_server_info(mut self, server_info: Implementation) -> Self { + self.server_info = server_info; + self + } + + pub fn with_instructions(mut self, instructions: impl Into) -> Self { + self.instructions = Some(instructions.into()); + self + } + + pub fn with_meta(mut self, meta: Meta) -> Self { + self.meta = Some(meta); + self + } +} + const_string!(InitializeResultMethod = "initialize"); /// # Initialization /// This request is sent from the client to the server when it first connects, asking it to begin initialization. @@ -3572,6 +3621,7 @@ macro_rules! ts_union { ts_union!( export type ClientRequest = + | DiscoverRequest | PingRequest | InitializeRequest | CompleteRequest @@ -3595,6 +3645,7 @@ ts_union!( impl ClientRequest { pub fn method(&self) -> &str { match &self { + ClientRequest::DiscoverRequest(r) => r.method.as_str(), ClientRequest::PingRequest(r) => r.method.as_str(), ClientRequest::InitializeRequest(r) => r.method.as_str(), ClientRequest::CompleteRequest(r) => r.method.as_str(), @@ -3669,6 +3720,7 @@ ts_union!( ts_union!( export type ServerResult = + | DiscoverResult | InitializeResult | CompleteResult | GetPromptResult diff --git a/crates/rmcp/src/model/meta.rs b/crates/rmcp/src/model/meta.rs index 4c9cd618..0e4d6f65 100644 --- a/crates/rmcp/src/model/meta.rs +++ b/crates/rmcp/src/model/meta.rs @@ -138,6 +138,7 @@ macro_rules! variant_extension { variant_extension! { ClientRequest { + DiscoverRequest PingRequest InitializeRequest CompleteRequest diff --git a/crates/rmcp/tests/test_deserialization.rs b/crates/rmcp/tests/test_deserialization.rs index 58e9a58a..468f7bd2 100644 --- a/crates/rmcp/tests/test_deserialization.rs +++ b/crates/rmcp/tests/test_deserialization.rs @@ -1,4 +1,50 @@ -use rmcp::model::{JsonRpcResponse, ServerJsonRpcMessage, ServerResult}; +use rmcp::model::{ + ClientJsonRpcMessage, ClientRequest, JsonRpcResponse, ServerJsonRpcMessage, ServerResult, +}; +use serde_json::json; + +#[test] +fn test_discover_request() { + let message: ClientJsonRpcMessage = serde_json::from_value(json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "server/discover", + "params": {} + })) + .unwrap(); + + assert!(matches!( + message, + ClientJsonRpcMessage::Request(r) + if matches!(r.request, ClientRequest::DiscoverRequest(_)) + )); +} + +#[test] +fn test_discover_result() { + let message: ServerJsonRpcMessage = serde_json::from_value(json!({ + "jsonrpc": "2.0", + "id": 1, + "result": { + "supportedVersions": ["2026-07-28", "2025-11-25"], + "capabilities": {}, + "serverInfo": { + "name": "test-server", + "version": "1.0.0" + } + } + })) + .unwrap(); + + assert!(matches!( + message, + ServerJsonRpcMessage::Response(JsonRpcResponse { + result: ServerResult::DiscoverResult(_), + .. + }) + )); +} + #[test] fn test_tool_list_result() { let json = std::fs::read("tests/test_deserialization/tool_list_result.json").unwrap(); 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 3bfdb7c4..6d4f4002 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 @@ -530,6 +530,11 @@ "CustomResult": { "description": "A catch-all response either side can use for custom requests." }, + "DiscoverRequestMethod": { + "type": "string", + "format": "const", + "const": "server/discover" + }, "ElicitResult": { "description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.", "type": "object", @@ -1038,10 +1043,10 @@ }, "anyOf": [ { - "$ref": "#/definitions/RequestNoParam" + "$ref": "#/definitions/Request" }, { - "$ref": "#/definitions/Request" + "$ref": "#/definitions/RequestNoParam" }, { "$ref": "#/definitions/Request2" @@ -1052,6 +1057,9 @@ { "$ref": "#/definitions/Request4" }, + { + "$ref": "#/definitions/Request5" + }, { "$ref": "#/definitions/RequestOptionalParam" }, @@ -1061,9 +1069,6 @@ { "$ref": "#/definitions/RequestOptionalParam3" }, - { - "$ref": "#/definitions/Request5" - }, { "$ref": "#/definitions/Request6" }, @@ -1073,20 +1078,23 @@ { "$ref": "#/definitions/Request8" }, + { + "$ref": "#/definitions/Request9" + }, { "$ref": "#/definitions/RequestOptionalParam4" }, { - "$ref": "#/definitions/Request9" + "$ref": "#/definitions/Request10" }, { "$ref": "#/definitions/RequestOptionalParam5" }, { - "$ref": "#/definitions/Request10" + "$ref": "#/definitions/Request11" }, { - "$ref": "#/definitions/Request11" + "$ref": "#/definitions/Request12" }, { "$ref": "#/definitions/CustomRequest" @@ -1426,10 +1434,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/InitializeResultMethod" + "$ref": "#/definitions/DiscoverRequestMethod" }, "params": { - "$ref": "#/definitions/InitializeRequestParams" + "$ref": "#/definitions/EmptyObject" } }, "required": [ @@ -1438,6 +1446,22 @@ ] }, "Request10": { + "description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)", + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/GetTaskMethod" + }, + "params": { + "$ref": "#/definitions/GetTaskParams" + } + }, + "required": [ + "method", + "params" + ] + }, + "Request11": { "description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)", "type": "object", "properties": { @@ -1453,7 +1477,7 @@ "params" ] }, - "Request11": { + "Request12": { "description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)", "type": "object", "properties": { @@ -1474,10 +1498,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/CompleteRequestMethod" + "$ref": "#/definitions/InitializeResultMethod" }, "params": { - "$ref": "#/definitions/CompleteRequestParams" + "$ref": "#/definitions/InitializeRequestParams" } }, "required": [ @@ -1490,10 +1514,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/SetLevelRequestMethod" + "$ref": "#/definitions/CompleteRequestMethod" }, "params": { - "$ref": "#/definitions/SetLevelRequestParams" + "$ref": "#/definitions/CompleteRequestParams" } }, "required": [ @@ -1506,10 +1530,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/GetPromptRequestMethod" + "$ref": "#/definitions/SetLevelRequestMethod" }, "params": { - "$ref": "#/definitions/GetPromptRequestParams" + "$ref": "#/definitions/SetLevelRequestParams" } }, "required": [ @@ -1522,10 +1546,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/ReadResourceRequestMethod" + "$ref": "#/definitions/GetPromptRequestMethod" }, "params": { - "$ref": "#/definitions/ReadResourceRequestParams" + "$ref": "#/definitions/GetPromptRequestParams" } }, "required": [ @@ -1538,10 +1562,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/SubscribeRequestMethod" + "$ref": "#/definitions/ReadResourceRequestMethod" }, "params": { - "$ref": "#/definitions/SubscribeRequestParams" + "$ref": "#/definitions/ReadResourceRequestParams" } }, "required": [ @@ -1554,10 +1578,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/UnsubscribeRequestMethod" + "$ref": "#/definitions/SubscribeRequestMethod" }, "params": { - "$ref": "#/definitions/UnsubscribeRequestParams" + "$ref": "#/definitions/SubscribeRequestParams" } }, "required": [ @@ -1570,10 +1594,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/CallToolRequestMethod" + "$ref": "#/definitions/UnsubscribeRequestMethod" }, "params": { - "$ref": "#/definitions/CallToolRequestParams" + "$ref": "#/definitions/UnsubscribeRequestParams" } }, "required": [ @@ -1586,10 +1610,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/GetTaskMethod" + "$ref": "#/definitions/CallToolRequestMethod" }, "params": { - "$ref": "#/definitions/GetTaskParams" + "$ref": "#/definitions/CallToolRequestParams" } }, "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 3bfdb7c4..6d4f4002 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 @@ -530,6 +530,11 @@ "CustomResult": { "description": "A catch-all response either side can use for custom requests." }, + "DiscoverRequestMethod": { + "type": "string", + "format": "const", + "const": "server/discover" + }, "ElicitResult": { "description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.", "type": "object", @@ -1038,10 +1043,10 @@ }, "anyOf": [ { - "$ref": "#/definitions/RequestNoParam" + "$ref": "#/definitions/Request" }, { - "$ref": "#/definitions/Request" + "$ref": "#/definitions/RequestNoParam" }, { "$ref": "#/definitions/Request2" @@ -1052,6 +1057,9 @@ { "$ref": "#/definitions/Request4" }, + { + "$ref": "#/definitions/Request5" + }, { "$ref": "#/definitions/RequestOptionalParam" }, @@ -1061,9 +1069,6 @@ { "$ref": "#/definitions/RequestOptionalParam3" }, - { - "$ref": "#/definitions/Request5" - }, { "$ref": "#/definitions/Request6" }, @@ -1073,20 +1078,23 @@ { "$ref": "#/definitions/Request8" }, + { + "$ref": "#/definitions/Request9" + }, { "$ref": "#/definitions/RequestOptionalParam4" }, { - "$ref": "#/definitions/Request9" + "$ref": "#/definitions/Request10" }, { "$ref": "#/definitions/RequestOptionalParam5" }, { - "$ref": "#/definitions/Request10" + "$ref": "#/definitions/Request11" }, { - "$ref": "#/definitions/Request11" + "$ref": "#/definitions/Request12" }, { "$ref": "#/definitions/CustomRequest" @@ -1426,10 +1434,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/InitializeResultMethod" + "$ref": "#/definitions/DiscoverRequestMethod" }, "params": { - "$ref": "#/definitions/InitializeRequestParams" + "$ref": "#/definitions/EmptyObject" } }, "required": [ @@ -1438,6 +1446,22 @@ ] }, "Request10": { + "description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)", + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/GetTaskMethod" + }, + "params": { + "$ref": "#/definitions/GetTaskParams" + } + }, + "required": [ + "method", + "params" + ] + }, + "Request11": { "description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)", "type": "object", "properties": { @@ -1453,7 +1477,7 @@ "params" ] }, - "Request11": { + "Request12": { "description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)", "type": "object", "properties": { @@ -1474,10 +1498,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/CompleteRequestMethod" + "$ref": "#/definitions/InitializeResultMethod" }, "params": { - "$ref": "#/definitions/CompleteRequestParams" + "$ref": "#/definitions/InitializeRequestParams" } }, "required": [ @@ -1490,10 +1514,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/SetLevelRequestMethod" + "$ref": "#/definitions/CompleteRequestMethod" }, "params": { - "$ref": "#/definitions/SetLevelRequestParams" + "$ref": "#/definitions/CompleteRequestParams" } }, "required": [ @@ -1506,10 +1530,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/GetPromptRequestMethod" + "$ref": "#/definitions/SetLevelRequestMethod" }, "params": { - "$ref": "#/definitions/GetPromptRequestParams" + "$ref": "#/definitions/SetLevelRequestParams" } }, "required": [ @@ -1522,10 +1546,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/ReadResourceRequestMethod" + "$ref": "#/definitions/GetPromptRequestMethod" }, "params": { - "$ref": "#/definitions/ReadResourceRequestParams" + "$ref": "#/definitions/GetPromptRequestParams" } }, "required": [ @@ -1538,10 +1562,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/SubscribeRequestMethod" + "$ref": "#/definitions/ReadResourceRequestMethod" }, "params": { - "$ref": "#/definitions/SubscribeRequestParams" + "$ref": "#/definitions/ReadResourceRequestParams" } }, "required": [ @@ -1554,10 +1578,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/UnsubscribeRequestMethod" + "$ref": "#/definitions/SubscribeRequestMethod" }, "params": { - "$ref": "#/definitions/UnsubscribeRequestParams" + "$ref": "#/definitions/SubscribeRequestParams" } }, "required": [ @@ -1570,10 +1594,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/CallToolRequestMethod" + "$ref": "#/definitions/UnsubscribeRequestMethod" }, "params": { - "$ref": "#/definitions/CallToolRequestParams" + "$ref": "#/definitions/UnsubscribeRequestParams" } }, "required": [ @@ -1586,10 +1610,10 @@ "type": "object", "properties": { "method": { - "$ref": "#/definitions/GetTaskMethod" + "$ref": "#/definitions/CallToolRequestMethod" }, "params": { - "$ref": "#/definitions/GetTaskParams" + "$ref": "#/definitions/CallToolRequestParams" } }, "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 fcf821f5..8da50d27 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 @@ -619,6 +619,41 @@ "CustomResult": { "description": "A catch-all response either side can use for custom requests." }, + "DiscoverResult": { + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "capabilities": { + "$ref": "#/definitions/ServerCapabilities" + }, + "instructions": { + "type": [ + "string", + "null" + ] + }, + "serverInfo": { + "$ref": "#/definitions/Implementation" + }, + "supportedVersions": { + "type": "array", + "items": { + "$ref": "#/definitions/ProtocolVersion" + } + } + }, + "required": [ + "supportedVersions", + "capabilities", + "serverInfo" + ] + }, "ElicitRequestParams": { "description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n1. Form-based elicitation request\n```rust\nuse rmcp::model::*;\n\nlet params = ElicitRequestParams::FormElicitationParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```\n2. URL-based elicitation request\n```rust\nuse rmcp::model::*;\nlet params = ElicitRequestParams::UrlElicitationParams {\n meta: None,\n message: \"Please provide your feedback at the following URL\".to_string(),\n url: \"https://example.com/feedback\".to_string(),\n elicitation_id: \"unique-id-123\".to_string(),\n};\n```", "anyOf": [ @@ -2696,6 +2731,9 @@ }, "ServerResult": { "anyOf": [ + { + "$ref": "#/definitions/DiscoverResult" + }, { "$ref": "#/definitions/InitializeResult" }, 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 fcf821f5..8da50d27 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 @@ -619,6 +619,41 @@ "CustomResult": { "description": "A catch-all response either side can use for custom requests." }, + "DiscoverResult": { + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "capabilities": { + "$ref": "#/definitions/ServerCapabilities" + }, + "instructions": { + "type": [ + "string", + "null" + ] + }, + "serverInfo": { + "$ref": "#/definitions/Implementation" + }, + "supportedVersions": { + "type": "array", + "items": { + "$ref": "#/definitions/ProtocolVersion" + } + } + }, + "required": [ + "supportedVersions", + "capabilities", + "serverInfo" + ] + }, "ElicitRequestParams": { "description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n1. Form-based elicitation request\n```rust\nuse rmcp::model::*;\n\nlet params = ElicitRequestParams::FormElicitationParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```\n2. URL-based elicitation request\n```rust\nuse rmcp::model::*;\nlet params = ElicitRequestParams::UrlElicitationParams {\n meta: None,\n message: \"Please provide your feedback at the following URL\".to_string(),\n url: \"https://example.com/feedback\".to_string(),\n elicitation_id: \"unique-id-123\".to_string(),\n};\n```", "anyOf": [ @@ -2696,6 +2731,9 @@ }, "ServerResult": { "anyOf": [ + { + "$ref": "#/definitions/DiscoverResult" + }, { "$ref": "#/definitions/InitializeResult" },