From 865e518771e4c0800ee748a13e05088d12ff31a4 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 2 May 2025 16:38:05 -0700 Subject: [PATCH 1/2] fix: mcp-types serialization wasn't quite working (#791) While creating a basic MCP server in https://github.com/openai/codex/pull/792, I discovered a number of bugs with the initial `mcp-types` crate that I needed to fix in order to implement the server. For example, I discovered that when serializing a message, `"jsonrpc": "2.0"` was not being included. I changed the codegen so that the field is added as: ```rust #[serde(rename = "jsonrpc", default = "default_jsonrpc")] pub jsonrpc: String, ``` This ensures that the field is serialized as `"2.0"`, though the field still has to be assigned, which is tedious. I may experiment with `Default` or something else in the future. (I also considered creating a custom serializer, but I'm not sure it's worth the trouble.) While here, I also added `MCP_SCHEMA_VERSION` and `JSONRPC_VERSION` as `pub const`s for the crate. I also discovered that MCP rejects sending `null` for optional fields, so I had to add `#[serde(skip_serializing_if = "Option::is_none")]` on `Option` fields. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/791). * #792 * __->__ #791 --- codex-rs/mcp-types/generate_mcp_types.py | 76 ++++-- codex-rs/mcp-types/src/lib.rs | 285 ++++++++++++++++++++--- codex-rs/mcp-types/tests/initialize.rs | 2 + 3 files changed, 316 insertions(+), 47 deletions(-) diff --git a/codex-rs/mcp-types/generate_mcp_types.py b/codex-rs/mcp-types/generate_mcp_types.py index f613aa74eb..92ac981224 100755 --- a/codex-rs/mcp-types/generate_mcp_types.py +++ b/codex-rs/mcp-types/generate_mcp_types.py @@ -13,6 +13,8 @@ from pathlib import Path # Helper first so it is defined when other functions call it. from typing import Any, Literal +SCHEMA_VERSION = "2025-03-26" +JSONRPC_VERSION = "2.0" STANDARD_DERIVE = "#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]\n" @@ -30,7 +32,7 @@ def main() -> int: num_args = len(sys.argv) if num_args == 1: schema_file = ( - Path(__file__).resolve().parent / "schema" / "2025-03-26" / "schema.json" + Path(__file__).resolve().parent / "schema" / SCHEMA_VERSION / "schema.json" ) elif num_args == 2: schema_file = Path(sys.argv[1]) @@ -48,7 +50,7 @@ def main() -> int: DEFINITIONS = schema_json["definitions"] out = [ - """ + f""" // @generated // DO NOT EDIT THIS FILE DIRECTLY. // Run the following in the crate root to regenerate this file: @@ -61,18 +63,23 @@ use serde::Serialize; use serde::de::DeserializeOwned; use std::convert::TryFrom; +pub const MCP_SCHEMA_VERSION: &str = "{SCHEMA_VERSION}"; +pub const JSONRPC_VERSION: &str = "{JSONRPC_VERSION}"; + /// Paired request/response types for the Model Context Protocol (MCP). -pub trait ModelContextProtocolRequest { +pub trait ModelContextProtocolRequest {{ const METHOD: &'static str; type Params: DeserializeOwned + Serialize + Send + Sync + 'static; type Result: DeserializeOwned + Serialize + Send + Sync + 'static; -} +}} /// One-way message in the Model Context Protocol (MCP). -pub trait ModelContextProtocolNotification { +pub trait ModelContextProtocolNotification {{ const METHOD: &'static str; type Params: DeserializeOwned + Serialize + Send + Sync + 'static; -} +}} + +fn default_jsonrpc() -> String {{ JSONRPC_VERSION.to_owned() }} """ ] @@ -174,6 +181,10 @@ pub trait ModelContextProtocolNotification { def add_definition(name: str, definition: dict[str, Any], out: list[str]) -> None: + if name == "Result": + out.append("pub type Result = serde_json::Value;\n\n") + return + # Capture description description = definition.get("description") @@ -181,6 +192,14 @@ def add_definition(name: str, definition: dict[str, Any], out: list[str]) -> Non if properties: required_props = set(definition.get("required", [])) out.extend(define_struct(name, properties, required_props, description)) + + # Special carve-out for Result types: + if name.endswith("Result"): + out.extend(f"impl From<{name}> for serde_json::Value {{\n") + out.append(f" fn from(value: {name}) -> Self {{\n") + out.append(" serde_json::to_value(value).unwrap()\n") + out.append(" }\n") + out.append("}\n\n") return enum_values = definition.get("enum", []) @@ -245,10 +264,6 @@ class StructField: serde: str | None = None def append(self, out: list[str], supports_const: bool) -> None: - # Omit these for now. - if self.name == "jsonrpc": - return - if self.serde: out.append(f" {self.serde}\n") if self.viz == "const": @@ -273,11 +288,22 @@ def define_struct( if prop_name == "_meta": # TODO? continue + elif prop_name == "jsonrpc": + fields.append( + StructField( + "pub", + "jsonrpc", + "String", # cannot use `&'static str` because of Deserialize + '#[serde(rename = "jsonrpc", default = "default_jsonrpc")]', + ) + ) + continue prop_type = map_type(prop, prop_name, name) - if prop_name not in required_props: + is_optional = prop_name not in required_props + if is_optional: prop_type = f"Option<{prop_type}>" - rs_prop = rust_prop_name(prop_name) + rs_prop = rust_prop_name(prop_name, is_optional) if prop_type.startswith("&'static str"): fields.append(StructField("const", rs_prop.name, prop_type, rs_prop.serde)) else: @@ -565,16 +591,32 @@ class RustProp: serde: str | None = None -def rust_prop_name(name: str) -> RustProp: +def rust_prop_name(name: str, is_optional: bool) -> RustProp: """Convert a JSON property name to a Rust property name.""" + prop_name: str + is_rename = False if name == "type": - return RustProp("r#type", None) + prop_name = "r#type" elif name == "ref": - return RustProp("r#ref", None) + prop_name = "r#ref" elif snake_case := to_snake_case(name): - return RustProp(snake_case, f'#[serde(rename = "{name}")]') + prop_name = snake_case + is_rename = True else: - return RustProp(name, None) + prop_name = name + + serde_annotations = [] + if is_rename: + serde_annotations.append(f'rename = "{name}"') + if is_optional: + serde_annotations.append("default") + serde_annotations.append('skip_serializing_if = "Option::is_none"') + + if serde_annotations: + serde_str = f'#[serde({", ".join(serde_annotations)})]' + else: + serde_str = None + return RustProp(prop_name, serde_str) def to_snake_case(name: str) -> str: diff --git a/codex-rs/mcp-types/src/lib.rs b/codex-rs/mcp-types/src/lib.rs index 4ae0fa09cf..c8925cfe3a 100644 --- a/codex-rs/mcp-types/src/lib.rs +++ b/codex-rs/mcp-types/src/lib.rs @@ -10,6 +10,9 @@ use serde::Deserialize; use serde::Serialize; use std::convert::TryFrom; +pub const MCP_SCHEMA_VERSION: &str = "2025-03-26"; +pub const JSONRPC_VERSION: &str = "2.0"; + /// Paired request/response types for the Model Context Protocol (MCP). pub trait ModelContextProtocolRequest { const METHOD: &'static str; @@ -23,16 +26,23 @@ pub trait ModelContextProtocolNotification { type Params: DeserializeOwned + Serialize + Send + Sync + 'static; } +fn default_jsonrpc() -> String { + JSONRPC_VERSION.to_owned() +} + /// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Annotations { + #[serde(default, skip_serializing_if = "Option::is_none")] pub audience: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] pub priority: Option, } /// Audio provided to or from an LLM. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct AudioContent { + #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, pub data: String, #[serde(rename = "mimeType")] @@ -43,7 +53,7 @@ pub struct AudioContent { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct BlobResourceContents { pub blob: String, - #[serde(rename = "mimeType")] + #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] pub mime_type: Option, pub uri: String, } @@ -59,6 +69,7 @@ impl ModelContextProtocolRequest for CallToolRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct CallToolRequestParams { + #[serde(default, skip_serializing_if = "Option::is_none")] pub arguments: Option, pub name: String, } @@ -76,7 +87,7 @@ pub struct CallToolRequestParams { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct CallToolResult { pub content: Vec, - #[serde(rename = "isError")] + #[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")] pub is_error: Option, } @@ -88,6 +99,12 @@ pub enum CallToolResultContent { EmbeddedResource(EmbeddedResource), } +impl From for serde_json::Value { + fn from(value: CallToolResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum CancelledNotification {} @@ -98,6 +115,7 @@ impl ModelContextProtocolNotification for CancelledNotification { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct CancelledNotificationParams { + #[serde(default, skip_serializing_if = "Option::is_none")] pub reason: Option, #[serde(rename = "requestId")] pub request_id: RequestId, @@ -106,15 +124,22 @@ pub struct CancelledNotificationParams { /// Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ClientCapabilities { + #[serde(default, skip_serializing_if = "Option::is_none")] pub experimental: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub roots: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub sampling: Option, } /// Present if the client supports listing roots. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ClientCapabilitiesRoots { - #[serde(rename = "listChanged")] + #[serde( + rename = "listChanged", + default, + skip_serializing_if = "Option::is_none" + )] pub list_changed: Option, } @@ -202,12 +227,19 @@ pub struct CompleteResult { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct CompleteResultCompletion { - #[serde(rename = "hasMore")] + #[serde(rename = "hasMore", default, skip_serializing_if = "Option::is_none")] pub has_more: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub total: Option, pub values: Vec, } +impl From for serde_json::Value { + fn from(value: CompleteResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum CreateMessageRequest {} @@ -219,18 +251,36 @@ impl ModelContextProtocolRequest for CreateMessageRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct CreateMessageRequestParams { - #[serde(rename = "includeContext")] + #[serde( + rename = "includeContext", + default, + skip_serializing_if = "Option::is_none" + )] pub include_context: Option, #[serde(rename = "maxTokens")] pub max_tokens: i64, pub messages: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] pub metadata: Option, - #[serde(rename = "modelPreferences")] + #[serde( + rename = "modelPreferences", + default, + skip_serializing_if = "Option::is_none" + )] pub model_preferences: Option, - #[serde(rename = "stopSequences")] + #[serde( + rename = "stopSequences", + default, + skip_serializing_if = "Option::is_none" + )] pub stop_sequences: Option>, - #[serde(rename = "systemPrompt")] + #[serde( + rename = "systemPrompt", + default, + skip_serializing_if = "Option::is_none" + )] pub system_prompt: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub temperature: Option, } @@ -240,7 +290,11 @@ pub struct CreateMessageResult { pub content: CreateMessageResultContent, pub model: String, pub role: Role, - #[serde(rename = "stopReason")] + #[serde( + rename = "stopReason", + default, + skip_serializing_if = "Option::is_none" + )] pub stop_reason: Option, } @@ -251,6 +305,12 @@ pub enum CreateMessageResultContent { AudioContent(AudioContent), } +impl From for serde_json::Value { + fn from(value: CreateMessageResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Cursor(String); @@ -260,6 +320,7 @@ pub struct Cursor(String); /// of the LLM and/or the user. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct EmbeddedResource { + #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, pub resource: EmbeddedResourceResource, pub r#type: String, // &'static str = "resource" @@ -284,6 +345,7 @@ impl ModelContextProtocolRequest for GetPromptRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct GetPromptRequestParams { + #[serde(default, skip_serializing_if = "Option::is_none")] pub arguments: Option, pub name: String, } @@ -291,13 +353,21 @@ pub struct GetPromptRequestParams { /// The server's response to a prompts/get request from the client. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct GetPromptResult { + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, pub messages: Vec, } +impl From for serde_json::Value { + fn from(value: GetPromptResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + /// An image provided to or from an LLM. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ImageContent { + #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, pub data: String, #[serde(rename = "mimeType")] @@ -334,6 +404,7 @@ pub struct InitializeRequestParams { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct InitializeResult { pub capabilities: ServerCapabilities, + #[serde(default, skip_serializing_if = "Option::is_none")] pub instructions: Option, #[serde(rename = "protocolVersion")] pub protocol_version: String, @@ -341,6 +412,12 @@ pub struct InitializeResult { pub server_info: Implementation, } +impl From for serde_json::Value { + fn from(value: InitializeResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum InitializedNotification {} @@ -370,11 +447,14 @@ pub type JSONRPCBatchResponse = Vec; pub struct JSONRPCError { pub error: JSONRPCErrorError, pub id: RequestId, + #[serde(rename = "jsonrpc", default = "default_jsonrpc")] + pub jsonrpc: String, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct JSONRPCErrorError { pub code: i64, + #[serde(default, skip_serializing_if = "Option::is_none")] pub data: Option, pub message: String, } @@ -394,7 +474,10 @@ pub enum JSONRPCMessage { /// A notification which does not expect a response. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct JSONRPCNotification { + #[serde(rename = "jsonrpc", default = "default_jsonrpc")] + pub jsonrpc: String, pub method: String, + #[serde(default, skip_serializing_if = "Option::is_none")] pub params: Option, } @@ -402,7 +485,10 @@ pub struct JSONRPCNotification { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct JSONRPCRequest { pub id: RequestId, + #[serde(rename = "jsonrpc", default = "default_jsonrpc")] + pub jsonrpc: String, pub method: String, + #[serde(default, skip_serializing_if = "Option::is_none")] pub params: Option, } @@ -410,6 +496,8 @@ pub struct JSONRPCRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct JSONRPCResponse { pub id: RequestId, + #[serde(rename = "jsonrpc", default = "default_jsonrpc")] + pub jsonrpc: String, pub result: Result, } @@ -424,17 +512,28 @@ impl ModelContextProtocolRequest for ListPromptsRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ListPromptsRequestParams { + #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } /// The server's response to a prompts/list request from the client. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ListPromptsResult { - #[serde(rename = "nextCursor")] + #[serde( + rename = "nextCursor", + default, + skip_serializing_if = "Option::is_none" + )] pub next_cursor: Option, pub prompts: Vec, } +impl From for serde_json::Value { + fn from(value: ListPromptsResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum ListResourceTemplatesRequest {} @@ -446,18 +545,29 @@ impl ModelContextProtocolRequest for ListResourceTemplatesRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ListResourceTemplatesRequestParams { + #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } /// The server's response to a resources/templates/list request from the client. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ListResourceTemplatesResult { - #[serde(rename = "nextCursor")] + #[serde( + rename = "nextCursor", + default, + skip_serializing_if = "Option::is_none" + )] pub next_cursor: Option, #[serde(rename = "resourceTemplates")] pub resource_templates: Vec, } +impl From for serde_json::Value { + fn from(value: ListResourceTemplatesResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum ListResourcesRequest {} @@ -469,17 +579,28 @@ impl ModelContextProtocolRequest for ListResourcesRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ListResourcesRequestParams { + #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } /// The server's response to a resources/list request from the client. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ListResourcesResult { - #[serde(rename = "nextCursor")] + #[serde( + rename = "nextCursor", + default, + skip_serializing_if = "Option::is_none" + )] pub next_cursor: Option, pub resources: Vec, } +impl From for serde_json::Value { + fn from(value: ListResourcesResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum ListRootsRequest {} @@ -497,6 +618,12 @@ pub struct ListRootsResult { pub roots: Vec, } +impl From for serde_json::Value { + fn from(value: ListRootsResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum ListToolsRequest {} @@ -508,17 +635,28 @@ impl ModelContextProtocolRequest for ListToolsRequest { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ListToolsRequestParams { + #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } /// The server's response to a tools/list request from the client. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ListToolsResult { - #[serde(rename = "nextCursor")] + #[serde( + rename = "nextCursor", + default, + skip_serializing_if = "Option::is_none" + )] pub next_cursor: Option, pub tools: Vec, } +impl From for serde_json::Value { + fn from(value: ListToolsResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + /// The severity of a log message. /// /// These map to syslog message severities, as specified in RFC-5424: @@ -555,6 +693,7 @@ impl ModelContextProtocolNotification for LoggingMessageNotification { pub struct LoggingMessageNotificationParams { pub data: serde_json::Value, pub level: LoggingLevel, + #[serde(default, skip_serializing_if = "Option::is_none")] pub logger: Option, } @@ -564,6 +703,7 @@ pub struct LoggingMessageNotificationParams { /// to the client to interpret. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ModelHint { + #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, } @@ -580,38 +720,64 @@ pub struct ModelHint { /// balance them against other considerations. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ModelPreferences { - #[serde(rename = "costPriority")] + #[serde( + rename = "costPriority", + default, + skip_serializing_if = "Option::is_none" + )] pub cost_priority: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub hints: Option>, - #[serde(rename = "intelligencePriority")] + #[serde( + rename = "intelligencePriority", + default, + skip_serializing_if = "Option::is_none" + )] pub intelligence_priority: Option, - #[serde(rename = "speedPriority")] + #[serde( + rename = "speedPriority", + default, + skip_serializing_if = "Option::is_none" + )] pub speed_priority: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Notification { pub method: String, + #[serde(default, skip_serializing_if = "Option::is_none")] pub params: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct PaginatedRequest { pub method: String, + #[serde(default, skip_serializing_if = "Option::is_none")] pub params: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct PaginatedRequestParams { + #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct PaginatedResult { - #[serde(rename = "nextCursor")] + #[serde( + rename = "nextCursor", + default, + skip_serializing_if = "Option::is_none" + )] pub next_cursor: Option, } +impl From for serde_json::Value { + fn from(value: PaginatedResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum PingRequest {} @@ -631,10 +797,12 @@ impl ModelContextProtocolNotification for ProgressNotification { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ProgressNotificationParams { + #[serde(default, skip_serializing_if = "Option::is_none")] pub message: Option, pub progress: f64, #[serde(rename = "progressToken")] pub progress_token: ProgressToken, + #[serde(default, skip_serializing_if = "Option::is_none")] pub total: Option, } @@ -648,7 +816,9 @@ pub enum ProgressToken { /// A prompt or prompt template that the server offers. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Prompt { + #[serde(default, skip_serializing_if = "Option::is_none")] pub arguments: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, pub name: String, } @@ -656,8 +826,10 @@ pub struct Prompt { /// Describes an argument that a prompt can accept. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct PromptArgument { + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] pub required: Option, } @@ -720,9 +892,16 @@ pub enum ReadResourceResultContents { BlobResourceContents(BlobResourceContents), } +impl From for serde_json::Value { + fn from(value: ReadResourceResult) -> Self { + serde_json::to_value(value).unwrap() + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Request { pub method: String, + #[serde(default, skip_serializing_if = "Option::is_none")] pub params: Option, } @@ -736,11 +915,14 @@ pub enum RequestId { /// A known resource that the server is capable of reading. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Resource { + #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, - #[serde(rename = "mimeType")] + #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] pub mime_type: Option, pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] pub size: Option, pub uri: String, } @@ -748,7 +930,7 @@ pub struct Resource { /// The contents of a specific resource or sub-resource. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ResourceContents { - #[serde(rename = "mimeType")] + #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] pub mime_type: Option, pub uri: String, } @@ -771,9 +953,11 @@ pub struct ResourceReference { /// A template description for resources available on the server. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ResourceTemplate { + #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, - #[serde(rename = "mimeType")] + #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] pub mime_type: Option, pub name: String, #[serde(rename = "uriTemplate")] @@ -793,8 +977,7 @@ pub struct ResourceUpdatedNotificationParams { pub uri: String, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct Result {} +pub type Result = serde_json::Value; /// The sender or recipient of messages and data in a conversation. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] @@ -808,6 +991,7 @@ pub enum Role { /// Represents a root directory or file that the server can operate on. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Root { + #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, pub uri: String, } @@ -837,33 +1021,52 @@ pub enum SamplingMessageContent { /// Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ServerCapabilities { + #[serde(default, skip_serializing_if = "Option::is_none")] pub completions: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub experimental: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub logging: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub prompts: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub resources: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub tools: Option, } /// Present if the server offers any tools to call. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ServerCapabilitiesTools { - #[serde(rename = "listChanged")] + #[serde( + rename = "listChanged", + default, + skip_serializing_if = "Option::is_none" + )] pub list_changed: Option, } /// Present if the server offers any resources to read. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ServerCapabilitiesResources { - #[serde(rename = "listChanged")] + #[serde( + rename = "listChanged", + default, + skip_serializing_if = "Option::is_none" + )] pub list_changed: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub subscribe: Option, } /// Present if the server offers any prompt templates. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ServerCapabilitiesPrompts { - #[serde(rename = "listChanged")] + #[serde( + rename = "listChanged", + default, + skip_serializing_if = "Option::is_none" + )] pub list_changed: Option, } @@ -948,6 +1151,7 @@ pub struct SubscribeRequestParams { /// Text provided to or from an LLM. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct TextContent { + #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, pub text: String, pub r#type: String, // &'static str = "text" @@ -955,7 +1159,7 @@ pub struct TextContent { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct TextResourceContents { - #[serde(rename = "mimeType")] + #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] pub mime_type: Option, pub text: String, pub uri: String, @@ -964,7 +1168,9 @@ pub struct TextResourceContents { /// Definition for a tool the client can call. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Tool { + #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(rename = "inputSchema")] pub input_schema: ToolInputSchema, @@ -974,7 +1180,9 @@ pub struct Tool { /// A JSON Schema object defining the expected parameters for the tool. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ToolInputSchema { + #[serde(default, skip_serializing_if = "Option::is_none")] pub properties: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub required: Option>, pub r#type: String, // &'static str = "object" } @@ -989,14 +1197,31 @@ pub struct ToolInputSchema { /// received from untrusted servers. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ToolAnnotations { - #[serde(rename = "destructiveHint")] + #[serde( + rename = "destructiveHint", + default, + skip_serializing_if = "Option::is_none" + )] pub destructive_hint: Option, - #[serde(rename = "idempotentHint")] + #[serde( + rename = "idempotentHint", + default, + skip_serializing_if = "Option::is_none" + )] pub idempotent_hint: Option, - #[serde(rename = "openWorldHint")] + #[serde( + rename = "openWorldHint", + default, + skip_serializing_if = "Option::is_none" + )] pub open_world_hint: Option, - #[serde(rename = "readOnlyHint")] + #[serde( + rename = "readOnlyHint", + default, + skip_serializing_if = "Option::is_none" + )] pub read_only_hint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, } diff --git a/codex-rs/mcp-types/tests/initialize.rs b/codex-rs/mcp-types/tests/initialize.rs index e857f8db3e..12e7f0f936 100644 --- a/codex-rs/mcp-types/tests/initialize.rs +++ b/codex-rs/mcp-types/tests/initialize.rs @@ -5,6 +5,7 @@ use mcp_types::InitializeRequestParams; use mcp_types::JSONRPCMessage; use mcp_types::JSONRPCRequest; use mcp_types::RequestId; +use mcp_types::JSONRPC_VERSION; use serde_json::json; #[test] @@ -30,6 +31,7 @@ fn deserialize_initialize_request() { }; let expected_req = JSONRPCRequest { + jsonrpc: JSONRPC_VERSION.into(), id: RequestId::Integer(1), method: "initialize".into(), params: Some(json!({ From 45c8623fe919daf3f20eb0b75470064741aed56a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 2 May 2025 16:38:21 -0700 Subject: [PATCH 2/2] feat: introduce mcp-server crate --- codex-rs/Cargo.lock | 13 + codex-rs/Cargo.toml | 1 + codex-rs/mcp-server/Cargo.toml | 30 ++ codex-rs/mcp-server/src/main.rs | 108 +++++ codex-rs/mcp-server/src/message_processor.rs | 422 +++++++++++++++++++ 5 files changed, 574 insertions(+) create mode 100644 codex-rs/mcp-server/Cargo.toml create mode 100644 codex-rs/mcp-server/src/main.rs create mode 100644 codex-rs/mcp-server/src/message_processor.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ed0b562b33..f2f865b02b 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -556,6 +556,19 @@ dependencies = [ "tempfile", ] +[[package]] +name = "codex-mcp-server" +version = "0.1.0" +dependencies = [ + "codex-core", + "mcp-types", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "codex-tui" version = "0.1.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index ded979158e..55aab2101b 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -7,6 +7,7 @@ members = [ "core", "exec", "execpolicy", + "mcp-server", "mcp-types", "tui", ] diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml new file mode 100644 index 0000000000..258a37aace --- /dev/null +++ b/codex-rs/mcp-server/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "codex-mcp-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +# +# codex-core contains optional functionality that is gated behind the "cli" +# feature. Unfortunately there is an unconditional reference to a module that +# is only compiled when the feature is enabled, which breaks the build when +# the default (no-feature) variant is used. +# +# We therefore explicitly enable the "cli" feature when codex-mcp-server pulls +# in codex-core so that the required symbols are present. This does _not_ +# change the public API of codex-core – it merely opts into compiling the +# extra, feature-gated source files so the build succeeds. +# +codex-core = { path = "../core", features = ["cli"] } +mcp-types = { path = "../mcp-types" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tracing = { version = "0.1.41", features = ["log"] } +tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } +tokio = { version = "1", features = [ + "io-std", + "macros", + "process", + "rt-multi-thread", + "signal", +] } diff --git a/codex-rs/mcp-server/src/main.rs b/codex-rs/mcp-server/src/main.rs new file mode 100644 index 0000000000..be52adc336 --- /dev/null +++ b/codex-rs/mcp-server/src/main.rs @@ -0,0 +1,108 @@ +//! Prototype MCP server. + +use std::io::Result as IoResult; + +use mcp_types::JSONRPCMessage; +use tokio::io::AsyncBufReadExt; +use tokio::io::AsyncWriteExt; +use tokio::io::BufReader; +use tokio::io::{self}; +use tokio::sync::mpsc; +use tracing::debug; +use tracing::error; +use tracing::info; + +mod message_processor; +use crate::message_processor::MessageProcessor; + +/// Size of the bounded channels used to communicate between tasks. The value +/// is a balance between throughput and memory usage – 128 messages should be +/// plenty for an interactive CLI. +const CHANNEL_CAPACITY: usize = 128; + +#[tokio::main] +async fn main() -> IoResult<()> { + // Install a simple subscriber so `tracing` output is visible. Users can + // control the log level with `RUST_LOG`. + tracing_subscriber::fmt::init(); + + // Set up channels. + let (incoming_tx, mut incoming_rx) = mpsc::channel::(CHANNEL_CAPACITY); + let (outgoing_tx, mut outgoing_rx) = mpsc::channel::(CHANNEL_CAPACITY); + + // Task: read from stdin, push to `incoming_tx`. + let stdin_reader_handle = tokio::spawn({ + let incoming_tx = incoming_tx.clone(); + async move { + let stdin = io::stdin(); + let reader = BufReader::new(stdin); + let mut lines = reader.lines(); + + while let Some(line) = lines.next_line().await.unwrap_or_default() { + match serde_json::from_str::(&line) { + Ok(msg) => { + if incoming_tx.send(msg).await.is_err() { + // Receiver gone – nothing left to do. + break; + } + } + Err(e) => error!("Failed to deserialize JSONRPCMessage: {e}"), + } + } + + debug!("stdin reader finished (EOF)"); + } + }); + + // Task: process incoming messages. + let processor_handle = tokio::spawn({ + let mut processor = MessageProcessor::new(outgoing_tx.clone()); + async move { + while let Some(msg) = incoming_rx.recv().await { + match msg { + JSONRPCMessage::Request(r) => processor.process_request(r), + JSONRPCMessage::Response(r) => processor.process_response(r), + JSONRPCMessage::Notification(n) => processor.process_notification(n), + JSONRPCMessage::BatchRequest(b) => processor.process_batch_request(b), + JSONRPCMessage::Error(e) => processor.process_error(e), + JSONRPCMessage::BatchResponse(b) => processor.process_batch_response(b), + } + } + + info!("processor task exited (channel closed)"); + } + }); + + // Task: write outgoing messages to stdout. + let stdout_writer_handle = tokio::spawn(async move { + let mut stdout = io::stdout(); + while let Some(msg) = outgoing_rx.recv().await { + match serde_json::to_string(&msg) { + Ok(json) => { + if let Err(e) = stdout.write_all(json.as_bytes()).await { + error!("Failed to write to stdout: {e}"); + break; + } + if let Err(e) = stdout.write_all(b"\n").await { + error!("Failed to write newline to stdout: {e}"); + break; + } + if let Err(e) = stdout.flush().await { + error!("Failed to flush stdout: {e}"); + break; + } + } + Err(e) => error!("Failed to serialize JSONRPCMessage: {e}"), + } + } + + info!("stdout writer exited (channel closed)"); + }); + + // Wait for all tasks to finish. The typical exit path is the stdin reader + // hitting EOF which, once it drops `incoming_tx`, propagates shutdown to + // the processor and then to the stdout task. + let _ = tokio::join!(stdin_reader_handle, processor_handle, stdout_writer_handle); + + Ok(()) +} diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs new file mode 100644 index 0000000000..18bcbc3e9a --- /dev/null +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -0,0 +1,422 @@ +//! Very small proof-of-concept request router for the MCP prototype server. + +use mcp_types::CallToolRequestParams; +use mcp_types::CallToolResultContent; +use mcp_types::ClientRequest; +use mcp_types::JSONRPCBatchRequest; +use mcp_types::JSONRPCBatchResponse; +use mcp_types::JSONRPCError; +use mcp_types::JSONRPCErrorError; +use mcp_types::JSONRPCMessage; +use mcp_types::JSONRPCNotification; +use mcp_types::JSONRPCRequest; +use mcp_types::JSONRPCResponse; +use mcp_types::ListToolsResult; +use mcp_types::ModelContextProtocolRequest; +use mcp_types::RequestId; +use mcp_types::ServerCapabilitiesTools; +use mcp_types::ServerNotification; +use mcp_types::TextContent; +use mcp_types::Tool; +use mcp_types::ToolInputSchema; +use mcp_types::JSONRPC_VERSION; +use serde_json::json; +use tokio::sync::mpsc; + +pub(crate) struct MessageProcessor { + outgoing: mpsc::Sender, + initialized: bool, +} + +impl MessageProcessor { + /// Create a new `MessageProcessor`, retaining a handle to the outgoing + /// `Sender` so handlers can enqueue messages to be written to stdout. + pub(crate) fn new(outgoing: mpsc::Sender) -> Self { + Self { + outgoing, + initialized: false, + } + } + + pub(crate) fn process_request(&mut self, request: JSONRPCRequest) { + // Hold on to the ID so we can respond. + let request_id = request.id.clone(); + + let client_request = match ClientRequest::try_from(request) { + Ok(client_request) => client_request, + Err(e) => { + tracing::warn!("Failed to convert request: {e}"); + return; + } + }; + + // Dispatch to a dedicated handler for each request type. + match client_request { + ClientRequest::InitializeRequest(params) => { + self.handle_initialize(request_id, params); + } + ClientRequest::PingRequest(params) => { + self.handle_ping(params); + } + ClientRequest::ListResourcesRequest(params) => { + self.handle_list_resources(params); + } + ClientRequest::ListResourceTemplatesRequest(params) => { + self.handle_list_resource_templates(params); + } + ClientRequest::ReadResourceRequest(params) => { + self.handle_read_resource(params); + } + ClientRequest::SubscribeRequest(params) => { + self.handle_subscribe(params); + } + ClientRequest::UnsubscribeRequest(params) => { + self.handle_unsubscribe(params); + } + ClientRequest::ListPromptsRequest(params) => { + self.handle_list_prompts(params); + } + ClientRequest::GetPromptRequest(params) => { + self.handle_get_prompt(params); + } + ClientRequest::ListToolsRequest(params) => { + self.handle_list_tools(request_id, params); + } + ClientRequest::CallToolRequest(params) => { + self.handle_call_tool(request_id, params); + } + ClientRequest::SetLevelRequest(params) => { + self.handle_set_level(params); + } + ClientRequest::CompleteRequest(params) => { + self.handle_complete(params); + } + } + } + + /// Handle a standalone JSON-RPC response originating from the peer. + pub(crate) fn process_response(&mut self, response: JSONRPCResponse) { + tracing::info!("<- response: {:?}", response); + } + + /// Handle a fire-and-forget JSON-RPC notification. + pub(crate) fn process_notification(&mut self, notification: JSONRPCNotification) { + let server_notification = match ServerNotification::try_from(notification) { + Ok(n) => n, + Err(e) => { + tracing::warn!("Failed to convert notification: {e}"); + return; + } + }; + + // Similar to requests, route each notification type to its own stub + // handler so additional logic can be implemented incrementally. + match server_notification { + ServerNotification::CancelledNotification(params) => { + self.handle_cancelled_notification(params); + } + ServerNotification::ProgressNotification(params) => { + self.handle_progress_notification(params); + } + ServerNotification::ResourceListChangedNotification(params) => { + self.handle_resource_list_changed(params); + } + ServerNotification::ResourceUpdatedNotification(params) => { + self.handle_resource_updated(params); + } + ServerNotification::PromptListChangedNotification(params) => { + self.handle_prompt_list_changed(params); + } + ServerNotification::ToolListChangedNotification(params) => { + self.handle_tool_list_changed(params); + } + ServerNotification::LoggingMessageNotification(params) => { + self.handle_logging_message(params); + } + } + } + + /// Handle a batch of requests and/or notifications. + pub(crate) fn process_batch_request(&mut self, batch: JSONRPCBatchRequest) { + tracing::info!("<- batch request containing {} item(s)", batch.len()); + for item in batch { + match item { + mcp_types::JSONRPCBatchRequestItem::JSONRPCRequest(req) => { + self.process_request(req); + } + mcp_types::JSONRPCBatchRequestItem::JSONRPCNotification(note) => { + self.process_notification(note); + } + } + } + } + + /// Handle an error object received from the peer. + pub(crate) fn process_error(&mut self, err: JSONRPCError) { + tracing::error!("<- error: {:?}", err); + } + + /// Handle a batch of responses/errors. + pub(crate) fn process_batch_response(&mut self, batch: JSONRPCBatchResponse) { + tracing::info!("<- batch response containing {} item(s)", batch.len()); + for item in batch { + match item { + mcp_types::JSONRPCBatchResponseItem::JSONRPCResponse(resp) => { + self.process_response(resp); + } + mcp_types::JSONRPCBatchResponseItem::JSONRPCError(err) => { + self.process_error(err); + } + } + } + } + + fn handle_initialize( + &mut self, + id: RequestId, + params: ::Params, + ) { + tracing::info!("initialize -> params: {:?}", params); + + if self.initialized { + // Already initialised: send JSON-RPC error response. + let error_msg = JSONRPCMessage::Error(JSONRPCError { + jsonrpc: JSONRPC_VERSION.into(), + id, + error: JSONRPCErrorError { + code: -32600, // Invalid Request + message: "initialize called more than once".to_string(), + data: None, + }, + }); + + if let Err(e) = self.outgoing.try_send(error_msg) { + tracing::error!("Failed to send initialization error: {e}"); + } + return; + } + + self.initialized = true; + + // Build a minimal InitializeResult. Fill with placeholders. + let result = mcp_types::InitializeResult { + capabilities: mcp_types::ServerCapabilities { + completions: None, + experimental: None, + logging: None, + prompts: None, + resources: None, + tools: Some(ServerCapabilitiesTools { + list_changed: Some(true), + }), + }, + instructions: None, + protocol_version: params.protocol_version.clone(), + server_info: mcp_types::Implementation { + name: "codex-mcp-server".to_string(), + version: mcp_types::MCP_SCHEMA_VERSION.to_string(), + }, + }; + + self.send_response::(id, result); + } + + fn send_response(&self, id: RequestId, result: T::Result) + where + T: ModelContextProtocolRequest, + { + let response = JSONRPCMessage::Response(JSONRPCResponse { + jsonrpc: JSONRPC_VERSION.into(), + id, + result: serde_json::to_value(result).unwrap(), + }); + + if let Err(e) = self.outgoing.try_send(response) { + tracing::error!("Failed to send response: {e}"); + } + } + + fn handle_ping( + &self, + params: ::Params, + ) { + tracing::info!("ping -> params: {:?}", params); + } + + fn handle_list_resources( + &self, + params: ::Params, + ) { + tracing::info!("resources/list -> params: {:?}", params); + } + + fn handle_list_resource_templates( + &self, + params: + ::Params, + ) { + tracing::info!("resources/templates/list -> params: {:?}", params); + } + + fn handle_read_resource( + &self, + params: ::Params, + ) { + tracing::info!("resources/read -> params: {:?}", params); + } + + fn handle_subscribe( + &self, + params: ::Params, + ) { + tracing::info!("resources/subscribe -> params: {:?}", params); + } + + fn handle_unsubscribe( + &self, + params: ::Params, + ) { + tracing::info!("resources/unsubscribe -> params: {:?}", params); + } + + fn handle_list_prompts( + &self, + params: ::Params, + ) { + tracing::info!("prompts/list -> params: {:?}", params); + } + + fn handle_get_prompt( + &self, + params: ::Params, + ) { + tracing::info!("prompts/get -> params: {:?}", params); + } + + fn handle_list_tools( + &self, + id: RequestId, + params: ::Params, + ) { + tracing::trace!("tools/list -> {params:?}"); + let result = ListToolsResult { + tools: vec![Tool { + name: "echo".to_string(), + input_schema: ToolInputSchema { + r#type: "object".to_string(), + properties: Some(json!({ + "input": { + "type": "string", + "description": "The input to echo back" + } + })), + required: Some(vec!["input".to_string()]), + }, + description: Some("Echoes the request back".to_string()), + annotations: None, + }], + next_cursor: None, + }; + + self.send_response::(id, result); + } + + fn handle_call_tool( + &self, + id: RequestId, + params: ::Params, + ) { + tracing::info!("tools/call -> params: {:?}", params); + let CallToolRequestParams { name, arguments } = params; + match name.as_str() { + "echo" => { + let result = mcp_types::CallToolResult { + content: vec![CallToolResultContent::TextContent(TextContent { + r#type: "text".to_string(), + text: format!("Echo: {arguments:?}"), + annotations: None, + })], + is_error: None, + }; + self.send_response::(id, result); + } + _ => { + let result = mcp_types::CallToolResult { + content: vec![], + is_error: Some(true), + }; + self.send_response::(id, result); + } + } + } + + fn handle_set_level( + &self, + params: ::Params, + ) { + tracing::info!("logging/setLevel -> params: {:?}", params); + } + + fn handle_complete( + &self, + params: ::Params, + ) { + tracing::info!("completion/complete -> params: {:?}", params); + } + + // --------------------------------------------------------------------- + // Notification handlers + // --------------------------------------------------------------------- + + fn handle_cancelled_notification( + &self, + params: ::Params, + ) { + tracing::info!("notifications/cancelled -> params: {:?}", params); + } + + fn handle_progress_notification( + &self, + params: ::Params, + ) { + tracing::info!("notifications/progress -> params: {:?}", params); + } + + fn handle_resource_list_changed( + &self, + params: ::Params, + ) { + tracing::info!( + "notifications/resources/list_changed -> params: {:?}", + params + ); + } + + fn handle_resource_updated( + &self, + params: ::Params, + ) { + tracing::info!("notifications/resources/updated -> params: {:?}", params); + } + + fn handle_prompt_list_changed( + &self, + params: ::Params, + ) { + tracing::info!("notifications/prompts/list_changed -> params: {:?}", params); + } + + fn handle_tool_list_changed( + &self, + params: ::Params, + ) { + tracing::info!("notifications/tools/list_changed -> params: {:?}", params); + } + + fn handle_logging_message( + &self, + params: ::Params, + ) { + tracing::info!("notifications/message -> params: {:?}", params); + } +}