diff --git a/codex-rs/mcp-client/src/main.rs b/codex-rs/mcp-client/src/main.rs index eb7842523d..04752df69a 100644 --- a/codex-rs/mcp-client/src/main.rs +++ b/codex-rs/mcp-client/src/main.rs @@ -10,10 +10,17 @@ //! program. The utility connects, issues a `tools/list` request and prints the //! server's response as pretty JSON. +use std::time::Duration; + use anyhow::Context; use anyhow::Result; use codex_mcp_client::McpClient; +use mcp_types::ClientCapabilities; +use mcp_types::Implementation; +use mcp_types::InitializeRequest; +use mcp_types::InitializeRequestParams; use mcp_types::ListToolsRequestParams; +use mcp_types::MCP_SCHEMA_VERSION; #[tokio::main] async fn main() -> Result<()> { @@ -33,6 +40,28 @@ async fn main() -> Result<()> { .await .with_context(|| format!("failed to spawn subprocess: {original_args:?}"))?; + let params = InitializeRequestParams { + capabilities: ClientCapabilities { + experimental: None, + roots: None, + sampling: None, + }, + client_info: Implementation { + name: "codex-mcp-client".to_owned(), + version: env!("CARGO_PKG_VERSION").to_owned(), + }, + protocol_version: MCP_SCHEMA_VERSION.to_owned(), + }; + let timeout = Some(Duration::from_secs(10)); + let response = client + .send_request::(params, timeout) + .await?; + eprintln!("initialize response: {response:?}"); + + client + .send_notification::(None) + .await?; + // Issue `tools/list` request (no params). let timeout = None; let tools = client diff --git a/codex-rs/mcp-client/src/mcp_client.rs b/codex-rs/mcp-client/src/mcp_client.rs index 641de0e89a..40869e0e38 100644 --- a/codex-rs/mcp-client/src/mcp_client.rs +++ b/codex-rs/mcp-client/src/mcp_client.rs @@ -17,6 +17,7 @@ use std::sync::atomic::AtomicI64; use std::sync::atomic::Ordering; use std::time::Duration; +use anyhow::Context; use anyhow::Result; use anyhow::anyhow; use mcp_types::CallToolRequest; @@ -29,6 +30,7 @@ use mcp_types::JSONRPCResponse; use mcp_types::ListToolsRequest; use mcp_types::ListToolsRequestParams; use mcp_types::ListToolsResult; +use mcp_types::ModelContextProtocolNotification; use mcp_types::ModelContextProtocolRequest; use mcp_types::RequestId; use serde::Serialize; @@ -273,6 +275,34 @@ impl McpClient { } } + pub async fn send_notification(&self, params: N::Params) -> Result<()> + where + N: ModelContextProtocolNotification, + N::Params: Serialize, + { + // Serialize params -> JSON. For many request types `Params` is + // `Option` and `None` should be encoded as *absence* of the field. + let params_json = serde_json::to_value(¶ms)?; + let params_field = if params_json.is_null() { + None + } else { + Some(params_json) + }; + + let method = N::METHOD.to_string(); + let jsonrpc_notification = JSONRPCNotification { + jsonrpc: JSONRPC_VERSION.to_string(), + method: method.clone(), + params: params_field, + }; + + let notification = JSONRPCMessage::Notification(jsonrpc_notification); + self.outgoing_tx + .send(notification) + .await + .with_context(|| format!("failed to send notification `{method}` to writer task")) + } + /// Convenience wrapper around `tools/list`. pub async fn list_tools( &self,