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.
This commit is contained in:
@@ -13,6 +13,8 @@ use anyhow::Context;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use codex_mcp_client::McpClient;
|
use codex_mcp_client::McpClient;
|
||||||
|
use mcp_types::ClientCapabilities;
|
||||||
|
use mcp_types::Implementation;
|
||||||
use mcp_types::Tool;
|
use mcp_types::Tool;
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -83,7 +85,33 @@ impl McpConnectionManager {
|
|||||||
join_set.spawn(async move {
|
join_set.spawn(async move {
|
||||||
let McpServerConfig { command, args, env } = cfg;
|
let McpServerConfig { command, args, env } = cfg;
|
||||||
let client_res = McpClient::new_stdio_client(command, args, env).await;
|
let client_res = McpClient::new_stdio_client(command, args, env).await;
|
||||||
(server_name, client_res)
|
match client_res {
|
||||||
|
Ok(client) => {
|
||||||
|
// Initialize the client.
|
||||||
|
let params = mcp_types::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_types::MCP_SCHEMA_VERSION.to_owned(),
|
||||||
|
};
|
||||||
|
let initialize_notification_params = None;
|
||||||
|
let timeout = Some(Duration::from_secs(10));
|
||||||
|
match client
|
||||||
|
.initialize(params, initialize_notification_params, timeout)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_response) => (server_name, Ok(client)),
|
||||||
|
Err(e) => (server_name, Err(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => (server_name, Err(e.into())),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +127,7 @@ impl McpConnectionManager {
|
|||||||
clients.insert(server_name, std::sync::Arc::new(client));
|
clients.insert(server_name, std::sync::Arc::new(client));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
errors.insert(server_name, e.into());
|
errors.insert(server_name, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,16 @@
|
|||||||
//! program. The utility connects, issues a `tools/list` request and prints the
|
//! program. The utility connects, issues a `tools/list` request and prints the
|
||||||
//! server's response as pretty JSON.
|
//! server's response as pretty JSON.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use codex_mcp_client::McpClient;
|
use codex_mcp_client::McpClient;
|
||||||
|
use mcp_types::ClientCapabilities;
|
||||||
|
use mcp_types::Implementation;
|
||||||
|
use mcp_types::InitializeRequestParams;
|
||||||
use mcp_types::ListToolsRequestParams;
|
use mcp_types::ListToolsRequestParams;
|
||||||
|
use mcp_types::MCP_SCHEMA_VERSION;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
@@ -33,6 +39,25 @@ async fn main() -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to spawn subprocess: {original_args:?}"))?;
|
.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 initialize_notification_params = None;
|
||||||
|
let timeout = Some(Duration::from_secs(10));
|
||||||
|
let response = client
|
||||||
|
.initialize(params, initialize_notification_params, timeout)
|
||||||
|
.await?;
|
||||||
|
eprintln!("initialize response: {response:?}");
|
||||||
|
|
||||||
// Issue `tools/list` request (no params).
|
// Issue `tools/list` request (no params).
|
||||||
let timeout = None;
|
let timeout = None;
|
||||||
let tools = client
|
let tools = client
|
||||||
|
|||||||
@@ -17,10 +17,14 @@ use std::sync::atomic::AtomicI64;
|
|||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use mcp_types::CallToolRequest;
|
use mcp_types::CallToolRequest;
|
||||||
use mcp_types::CallToolRequestParams;
|
use mcp_types::CallToolRequestParams;
|
||||||
|
use mcp_types::InitializeRequest;
|
||||||
|
use mcp_types::InitializeRequestParams;
|
||||||
|
use mcp_types::InitializedNotification;
|
||||||
use mcp_types::JSONRPC_VERSION;
|
use mcp_types::JSONRPC_VERSION;
|
||||||
use mcp_types::JSONRPCMessage;
|
use mcp_types::JSONRPCMessage;
|
||||||
use mcp_types::JSONRPCNotification;
|
use mcp_types::JSONRPCNotification;
|
||||||
@@ -29,6 +33,7 @@ use mcp_types::JSONRPCResponse;
|
|||||||
use mcp_types::ListToolsRequest;
|
use mcp_types::ListToolsRequest;
|
||||||
use mcp_types::ListToolsRequestParams;
|
use mcp_types::ListToolsRequestParams;
|
||||||
use mcp_types::ListToolsResult;
|
use mcp_types::ListToolsResult;
|
||||||
|
use mcp_types::ModelContextProtocolNotification;
|
||||||
use mcp_types::ModelContextProtocolRequest;
|
use mcp_types::ModelContextProtocolRequest;
|
||||||
use mcp_types::RequestId;
|
use mcp_types::RequestId;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -74,6 +79,8 @@ pub struct McpClient {
|
|||||||
|
|
||||||
impl McpClient {
|
impl McpClient {
|
||||||
/// Spawn the given command and establish an MCP session over its STDIO.
|
/// Spawn the given command and establish an MCP session over its STDIO.
|
||||||
|
/// Caller is responsible for sending the `initialize` request. See
|
||||||
|
/// [`initialize`](Self::initialize) for details.
|
||||||
pub async fn new_stdio_client(
|
pub async fn new_stdio_client(
|
||||||
program: String,
|
program: String,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
@@ -273,6 +280,52 @@ impl McpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn send_notification<N>(&self, params: N::Params) -> Result<()>
|
||||||
|
where
|
||||||
|
N: ModelContextProtocolNotification,
|
||||||
|
N::Params: Serialize,
|
||||||
|
{
|
||||||
|
// Serialize params -> JSON. For many request types `Params` is
|
||||||
|
// `Option<T>` 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"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Negotiates the initialization with the MCP server. Sends an `initialize`
|
||||||
|
/// request with the specified `initialize_params` and then the
|
||||||
|
/// `notifications/initialized` notification once the response has been
|
||||||
|
/// received. Returns the response to the `initialize` request.
|
||||||
|
pub async fn initialize(
|
||||||
|
&self,
|
||||||
|
initialize_params: InitializeRequestParams,
|
||||||
|
initialize_notification_params: Option<serde_json::Value>,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
) -> Result<mcp_types::InitializeResult> {
|
||||||
|
let response = self
|
||||||
|
.send_request::<InitializeRequest>(initialize_params, timeout)
|
||||||
|
.await?;
|
||||||
|
self.send_notification::<InitializedNotification>(initialize_notification_params)
|
||||||
|
.await?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
/// Convenience wrapper around `tools/list`.
|
/// Convenience wrapper around `tools/list`.
|
||||||
pub async fn list_tools(
|
pub async fn list_tools(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
Reference in New Issue
Block a user