Skip to content

Commit 115fb0b

Browse files
authored
fix: navigate initialization phase before tools/list request in MCP client (#904)
Apparently the MCP server implemented in JavaScript did not require the `initialize` handshake before responding to tool list/call, so I missed this.
1 parent ab4cb94 commit 115fb0b

File tree

3 files changed

+108
-2
lines changed

3 files changed

+108
-2
lines changed

codex-rs/core/src/mcp_connection_manager.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use anyhow::Context;
1313
use anyhow::Result;
1414
use anyhow::anyhow;
1515
use codex_mcp_client::McpClient;
16+
use mcp_types::ClientCapabilities;
17+
use mcp_types::Implementation;
1618
use mcp_types::Tool;
1719
use tokio::task::JoinSet;
1820
use tracing::info;
@@ -83,7 +85,33 @@ impl McpConnectionManager {
8385
join_set.spawn(async move {
8486
let McpServerConfig { command, args, env } = cfg;
8587
let client_res = McpClient::new_stdio_client(command, args, env).await;
86-
(server_name, client_res)
88+
match client_res {
89+
Ok(client) => {
90+
// Initialize the client.
91+
let params = mcp_types::InitializeRequestParams {
92+
capabilities: ClientCapabilities {
93+
experimental: None,
94+
roots: None,
95+
sampling: None,
96+
},
97+
client_info: Implementation {
98+
name: "codex-mcp-client".to_owned(),
99+
version: env!("CARGO_PKG_VERSION").to_owned(),
100+
},
101+
protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_owned(),
102+
};
103+
let initialize_notification_params = None;
104+
let timeout = Some(Duration::from_secs(10));
105+
match client
106+
.initialize(params, initialize_notification_params, timeout)
107+
.await
108+
{
109+
Ok(_response) => (server_name, Ok(client)),
110+
Err(e) => (server_name, Err(e)),
111+
}
112+
}
113+
Err(e) => (server_name, Err(e.into())),
114+
}
87115
});
88116
}
89117

@@ -99,7 +127,7 @@ impl McpConnectionManager {
99127
clients.insert(server_name, std::sync::Arc::new(client));
100128
}
101129
Err(e) => {
102-
errors.insert(server_name, e.into());
130+
errors.insert(server_name, e);
103131
}
104132
}
105133
}

codex-rs/mcp-client/src/main.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@
1010
//! program. The utility connects, issues a `tools/list` request and prints the
1111
//! server's response as pretty JSON.
1212
13+
use std::time::Duration;
14+
1315
use anyhow::Context;
1416
use anyhow::Result;
1517
use codex_mcp_client::McpClient;
18+
use mcp_types::ClientCapabilities;
19+
use mcp_types::Implementation;
20+
use mcp_types::InitializeRequestParams;
1621
use mcp_types::ListToolsRequestParams;
22+
use mcp_types::MCP_SCHEMA_VERSION;
1723

1824
#[tokio::main]
1925
async fn main() -> Result<()> {
@@ -33,6 +39,25 @@ async fn main() -> Result<()> {
3339
.await
3440
.with_context(|| format!("failed to spawn subprocess: {original_args:?}"))?;
3541

42+
let params = InitializeRequestParams {
43+
capabilities: ClientCapabilities {
44+
experimental: None,
45+
roots: None,
46+
sampling: None,
47+
},
48+
client_info: Implementation {
49+
name: "codex-mcp-client".to_owned(),
50+
version: env!("CARGO_PKG_VERSION").to_owned(),
51+
},
52+
protocol_version: MCP_SCHEMA_VERSION.to_owned(),
53+
};
54+
let initialize_notification_params = None;
55+
let timeout = Some(Duration::from_secs(10));
56+
let response = client
57+
.initialize(params, initialize_notification_params, timeout)
58+
.await?;
59+
eprintln!("initialize response: {response:?}");
60+
3661
// Issue `tools/list` request (no params).
3762
let timeout = None;
3863
let tools = client

codex-rs/mcp-client/src/mcp_client.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ use std::sync::atomic::AtomicI64;
1717
use std::sync::atomic::Ordering;
1818
use std::time::Duration;
1919

20+
use anyhow::Context;
2021
use anyhow::Result;
2122
use anyhow::anyhow;
2223
use mcp_types::CallToolRequest;
2324
use mcp_types::CallToolRequestParams;
25+
use mcp_types::InitializeRequest;
26+
use mcp_types::InitializeRequestParams;
27+
use mcp_types::InitializedNotification;
2428
use mcp_types::JSONRPC_VERSION;
2529
use mcp_types::JSONRPCMessage;
2630
use mcp_types::JSONRPCNotification;
@@ -29,6 +33,7 @@ use mcp_types::JSONRPCResponse;
2933
use mcp_types::ListToolsRequest;
3034
use mcp_types::ListToolsRequestParams;
3135
use mcp_types::ListToolsResult;
36+
use mcp_types::ModelContextProtocolNotification;
3237
use mcp_types::ModelContextProtocolRequest;
3338
use mcp_types::RequestId;
3439
use serde::Serialize;
@@ -74,6 +79,8 @@ pub struct McpClient {
7479

7580
impl McpClient {
7681
/// Spawn the given command and establish an MCP session over its STDIO.
82+
/// Caller is responsible for sending the `initialize` request. See
83+
/// [`initialize`](Self::initialize) for details.
7784
pub async fn new_stdio_client(
7885
program: String,
7986
args: Vec<String>,
@@ -273,6 +280,52 @@ impl McpClient {
273280
}
274281
}
275282

283+
pub async fn send_notification<N>(&self, params: N::Params) -> Result<()>
284+
where
285+
N: ModelContextProtocolNotification,
286+
N::Params: Serialize,
287+
{
288+
// Serialize params -> JSON. For many request types `Params` is
289+
// `Option<T>` and `None` should be encoded as *absence* of the field.
290+
let params_json = serde_json::to_value(&params)?;
291+
let params_field = if params_json.is_null() {
292+
None
293+
} else {
294+
Some(params_json)
295+
};
296+
297+
let method = N::METHOD.to_string();
298+
let jsonrpc_notification = JSONRPCNotification {
299+
jsonrpc: JSONRPC_VERSION.to_string(),
300+
method: method.clone(),
301+
params: params_field,
302+
};
303+
304+
let notification = JSONRPCMessage::Notification(jsonrpc_notification);
305+
self.outgoing_tx
306+
.send(notification)
307+
.await
308+
.with_context(|| format!("failed to send notification `{method}` to writer task"))
309+
}
310+
311+
/// Negotiates the initialization with the MCP server. Sends an `initialize`
312+
/// request with the specified `initialize_params` and then the
313+
/// `notifications/initialized` notification once the response has been
314+
/// received. Returns the response to the `initialize` request.
315+
pub async fn initialize(
316+
&self,
317+
initialize_params: InitializeRequestParams,
318+
initialize_notification_params: Option<serde_json::Value>,
319+
timeout: Option<Duration>,
320+
) -> Result<mcp_types::InitializeResult> {
321+
let response = self
322+
.send_request::<InitializeRequest>(initialize_params, timeout)
323+
.await?;
324+
self.send_notification::<InitializedNotification>(initialize_notification_params)
325+
.await?;
326+
Ok(response)
327+
}
328+
276329
/// Convenience wrapper around `tools/list`.
277330
pub async fn list_tools(
278331
&self,

0 commit comments

Comments
 (0)