mirror of
https://github.com/openai/codex.git
synced 2026-04-26 15:45:02 +00:00
feat: replace custom mcp-types crate with equivalents from rmcp (#10349)
We started working with MCP in Codex before
https://crates.io/crates/rmcp was mature, so we had our own crate for
MCP types that was generated from the MCP schema:
8b95d3e082/codex-rs/mcp-types/README.md
Now that `rmcp` is more mature, it makes more sense to use their MCP
types in Rust, as they handle details (like the `_meta` field) that our
custom version ignored. Though one advantage that our custom types had
is that our generated types implemented `JsonSchema` and `ts_rs::TS`,
whereas the types in `rmcp` do not. As such, part of the work of this PR
is leveraging the adapters between `rmcp` types and the serializable
types that are API for us (app server and MCP) introduced in #10356.
Note this PR results in a number of changes to
`codex-rs/app-server-protocol/schema`, which merit special attention
during review. We must ensure that these changes are still
backwards-compatible, which is possible because we have:
```diff
- export type CallToolResult = { content: Array<ContentBlock>, isError?: boolean, structuredContent?: JsonValue, };
+ export type CallToolResult = { content: Array<JsonValue>, structuredContent?: JsonValue, isError?: boolean, _meta?: JsonValue, };
```
so `ContentBlock` has been replaced with the more general `JsonValue`.
Note that `ContentBlock` was defined as:
```typescript
export type ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource;
```
so the deletion of those individual variants should not be a cause of
great concern.
Similarly, we have the following change in
`codex-rs/app-server-protocol/schema/typescript/Tool.ts`:
```
- export type Tool = { annotations?: ToolAnnotations, description?: string, inputSchema: ToolInputSchema, name: string, outputSchema?: ToolOutputSchema, title?: string, };
+ export type Tool = { name: string, title?: string, description?: string, inputSchema: JsonValue, outputSchema?: JsonValue, annotations?: JsonValue, icons?: Array<JsonValue>, _meta?: JsonValue, };
```
so:
- `annotations?: ToolAnnotations` ➡️ `JsonValue`
- `inputSchema: ToolInputSchema` ➡️ `JsonValue`
- `outputSchema?: ToolOutputSchema` ➡️ `JsonValue`
and two new fields: `icons?: Array<JsonValue>, _meta?: JsonValue`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/10349).
* #10357
* __->__ #10349
* #10356
This commit is contained in:
@@ -12,7 +12,7 @@ anyhow = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-mcp-server = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
mcp-types = { workspace = true }
|
||||
rmcp = { workspace = true }
|
||||
os_info = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -6,14 +6,16 @@ pub use core_test_support::format_with_current_shell;
|
||||
pub use core_test_support::format_with_current_shell_display_non_login;
|
||||
pub use core_test_support::format_with_current_shell_non_login;
|
||||
pub use mcp_process::McpProcess;
|
||||
use mcp_types::JSONRPCResponse;
|
||||
pub use mock_model_server::create_mock_chat_completions_server;
|
||||
pub use responses::create_apply_patch_sse_response;
|
||||
pub use responses::create_final_assistant_message_sse_response;
|
||||
pub use responses::create_shell_command_sse_response;
|
||||
use rmcp::model::JsonRpcResponse;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
pub fn to_response<T: DeserializeOwned>(response: JSONRPCResponse) -> anyhow::Result<T> {
|
||||
pub fn to_response<T: DeserializeOwned>(
|
||||
response: JsonRpcResponse<serde_json::Value>,
|
||||
) -> anyhow::Result<T> {
|
||||
let value = serde_json::to_value(response.result)?;
|
||||
let codex_response = serde_json::from_value(value)?;
|
||||
Ok(codex_response)
|
||||
|
||||
@@ -12,19 +12,21 @@ use tokio::process::ChildStdout;
|
||||
use anyhow::Context;
|
||||
use codex_mcp_server::CodexToolCallParam;
|
||||
|
||||
use mcp_types::CallToolRequestParams;
|
||||
use mcp_types::ClientCapabilities;
|
||||
use mcp_types::Implementation;
|
||||
use mcp_types::InitializeRequestParams;
|
||||
use mcp_types::JSONRPC_VERSION;
|
||||
use mcp_types::JSONRPCMessage;
|
||||
use mcp_types::JSONRPCNotification;
|
||||
use mcp_types::JSONRPCRequest;
|
||||
use mcp_types::JSONRPCResponse;
|
||||
use mcp_types::ModelContextProtocolNotification;
|
||||
use mcp_types::ModelContextProtocolRequest;
|
||||
use mcp_types::RequestId;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::CallToolRequestParam;
|
||||
use rmcp::model::ClientCapabilities;
|
||||
use rmcp::model::CustomNotification;
|
||||
use rmcp::model::CustomRequest;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
use rmcp::model::Implementation;
|
||||
use rmcp::model::InitializeRequestParam;
|
||||
use rmcp::model::JsonRpcMessage;
|
||||
use rmcp::model::JsonRpcNotification;
|
||||
use rmcp::model::JsonRpcRequest;
|
||||
use rmcp::model::JsonRpcResponse;
|
||||
use rmcp::model::JsonRpcVersion2_0;
|
||||
use rmcp::model::ProtocolVersion;
|
||||
use rmcp::model::RequestId;
|
||||
use serde_json::json;
|
||||
use tokio::process::Command;
|
||||
|
||||
@@ -110,9 +112,11 @@ impl McpProcess {
|
||||
pub async fn initialize(&mut self) -> anyhow::Result<()> {
|
||||
let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let params = InitializeRequestParams {
|
||||
let params = InitializeRequestParam {
|
||||
capabilities: ClientCapabilities {
|
||||
elicitation: Some(json!({})),
|
||||
elicitation: Some(ElicitationCapability {
|
||||
schema_validation: None,
|
||||
}),
|
||||
experimental: None,
|
||||
roots: None,
|
||||
sampling: None,
|
||||
@@ -121,17 +125,17 @@ impl McpProcess {
|
||||
name: "elicitation test".into(),
|
||||
title: Some("Elicitation Test".into()),
|
||||
version: "0.0.0".into(),
|
||||
user_agent: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
},
|
||||
protocol_version: mcp_types::MCP_SCHEMA_VERSION.into(),
|
||||
protocol_version: ProtocolVersion::V_2025_03_26,
|
||||
};
|
||||
let params_value = serde_json::to_value(params)?;
|
||||
|
||||
self.send_jsonrpc_message(JSONRPCMessage::Request(JSONRPCRequest {
|
||||
jsonrpc: JSONRPC_VERSION.into(),
|
||||
id: RequestId::Integer(request_id),
|
||||
method: mcp_types::InitializeRequest::METHOD.into(),
|
||||
params: Some(params_value),
|
||||
self.send_jsonrpc_message(JsonRpcMessage::Request(JsonRpcRequest {
|
||||
jsonrpc: JsonRpcVersion2_0,
|
||||
id: RequestId::Number(request_id),
|
||||
request: CustomRequest::new("initialize", Some(params_value)),
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -146,33 +150,38 @@ impl McpProcess {
|
||||
os_info.architecture().unwrap_or("unknown"),
|
||||
codex_core::terminal::user_agent()
|
||||
);
|
||||
let JsonRpcMessage::Response(JsonRpcResponse {
|
||||
jsonrpc,
|
||||
id,
|
||||
result,
|
||||
}) = initialized
|
||||
else {
|
||||
anyhow::bail!("expected initialize response message, got: {initialized:?}")
|
||||
};
|
||||
assert_eq!(jsonrpc, JsonRpcVersion2_0);
|
||||
assert_eq!(id, RequestId::Number(request_id));
|
||||
assert_eq!(
|
||||
JSONRPCMessage::Response(JSONRPCResponse {
|
||||
jsonrpc: JSONRPC_VERSION.into(),
|
||||
id: RequestId::Integer(request_id),
|
||||
result: json!({
|
||||
"capabilities": {
|
||||
"tools": {
|
||||
"listChanged": true
|
||||
},
|
||||
result,
|
||||
json!({
|
||||
"capabilities": {
|
||||
"tools": {
|
||||
"listChanged": true
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "codex-mcp-server",
|
||||
"title": "Codex",
|
||||
"version": "0.0.0",
|
||||
"user_agent": user_agent
|
||||
},
|
||||
"protocolVersion": mcp_types::MCP_SCHEMA_VERSION
|
||||
})
|
||||
}),
|
||||
initialized
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "codex-mcp-server",
|
||||
"title": "Codex",
|
||||
"version": "0.0.0",
|
||||
"user_agent": user_agent
|
||||
},
|
||||
"protocolVersion": ProtocolVersion::V_2025_03_26
|
||||
})
|
||||
);
|
||||
|
||||
// Send notifications/initialized to ack the response.
|
||||
self.send_jsonrpc_message(JSONRPCMessage::Notification(JSONRPCNotification {
|
||||
jsonrpc: JSONRPC_VERSION.into(),
|
||||
method: mcp_types::InitializedNotification::METHOD.into(),
|
||||
params: None,
|
||||
self.send_jsonrpc_message(JsonRpcMessage::Notification(JsonRpcNotification {
|
||||
jsonrpc: JsonRpcVersion2_0,
|
||||
notification: CustomNotification::new("notifications/initialized", None),
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@@ -185,12 +194,15 @@ impl McpProcess {
|
||||
&mut self,
|
||||
params: CodexToolCallParam,
|
||||
) -> anyhow::Result<i64> {
|
||||
let codex_tool_call_params = CallToolRequestParams {
|
||||
name: "codex".to_string(),
|
||||
arguments: Some(serde_json::to_value(params)?),
|
||||
let codex_tool_call_params = CallToolRequestParam {
|
||||
name: "codex".into(),
|
||||
arguments: Some(match serde_json::to_value(params)? {
|
||||
serde_json::Value::Object(map) => map,
|
||||
_ => unreachable!("params serialize to object"),
|
||||
}),
|
||||
};
|
||||
self.send_request(
|
||||
mcp_types::CallToolRequest::METHOD,
|
||||
"tools/call",
|
||||
Some(serde_json::to_value(codex_tool_call_params)?),
|
||||
)
|
||||
.await
|
||||
@@ -203,11 +215,10 @@ impl McpProcess {
|
||||
) -> anyhow::Result<i64> {
|
||||
let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let message = JSONRPCMessage::Request(JSONRPCRequest {
|
||||
jsonrpc: JSONRPC_VERSION.into(),
|
||||
id: RequestId::Integer(request_id),
|
||||
method: method.to_string(),
|
||||
params,
|
||||
let message = JsonRpcMessage::Request(JsonRpcRequest {
|
||||
jsonrpc: JsonRpcVersion2_0,
|
||||
id: RequestId::Number(request_id),
|
||||
request: CustomRequest::new(method, params),
|
||||
});
|
||||
self.send_jsonrpc_message(message).await?;
|
||||
Ok(request_id)
|
||||
@@ -218,15 +229,18 @@ impl McpProcess {
|
||||
id: RequestId,
|
||||
result: serde_json::Value,
|
||||
) -> anyhow::Result<()> {
|
||||
self.send_jsonrpc_message(JSONRPCMessage::Response(JSONRPCResponse {
|
||||
jsonrpc: JSONRPC_VERSION.into(),
|
||||
self.send_jsonrpc_message(JsonRpcMessage::Response(JsonRpcResponse {
|
||||
jsonrpc: JsonRpcVersion2_0,
|
||||
id,
|
||||
result,
|
||||
}))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_jsonrpc_message(&mut self, message: JSONRPCMessage) -> anyhow::Result<()> {
|
||||
async fn send_jsonrpc_message(
|
||||
&mut self,
|
||||
message: JsonRpcMessage<CustomRequest, serde_json::Value, CustomNotification>,
|
||||
) -> anyhow::Result<()> {
|
||||
eprintln!("writing message to stdin: {message:?}");
|
||||
let payload = serde_json::to_string(&message)?;
|
||||
self.stdin.write_all(payload.as_bytes()).await?;
|
||||
@@ -235,31 +249,37 @@ impl McpProcess {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_jsonrpc_message(&mut self) -> anyhow::Result<JSONRPCMessage> {
|
||||
async fn read_jsonrpc_message(
|
||||
&mut self,
|
||||
) -> anyhow::Result<JsonRpcMessage<CustomRequest, serde_json::Value, CustomNotification>> {
|
||||
let mut line = String::new();
|
||||
self.stdout.read_line(&mut line).await?;
|
||||
let message = serde_json::from_str::<JSONRPCMessage>(&line)?;
|
||||
let message = serde_json::from_str::<
|
||||
JsonRpcMessage<CustomRequest, serde_json::Value, CustomNotification>,
|
||||
>(&line)?;
|
||||
eprintln!("read message from stdout: {message:?}");
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub async fn read_stream_until_request_message(&mut self) -> anyhow::Result<JSONRPCRequest> {
|
||||
pub async fn read_stream_until_request_message(
|
||||
&mut self,
|
||||
) -> anyhow::Result<JsonRpcRequest<CustomRequest>> {
|
||||
eprintln!("in read_stream_until_request_message()");
|
||||
|
||||
loop {
|
||||
let message = self.read_jsonrpc_message().await?;
|
||||
|
||||
match message {
|
||||
JSONRPCMessage::Notification(_) => {
|
||||
JsonRpcMessage::Notification(_) => {
|
||||
eprintln!("notification: {message:?}");
|
||||
}
|
||||
JSONRPCMessage::Request(jsonrpc_request) => {
|
||||
JsonRpcMessage::Request(jsonrpc_request) => {
|
||||
return Ok(jsonrpc_request);
|
||||
}
|
||||
JSONRPCMessage::Error(_) => {
|
||||
JsonRpcMessage::Error(_) => {
|
||||
anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}");
|
||||
}
|
||||
JSONRPCMessage::Response(_) => {
|
||||
JsonRpcMessage::Response(_) => {
|
||||
anyhow::bail!("unexpected JSONRPCMessage::Response: {message:?}");
|
||||
}
|
||||
}
|
||||
@@ -269,22 +289,22 @@ impl McpProcess {
|
||||
pub async fn read_stream_until_response_message(
|
||||
&mut self,
|
||||
request_id: RequestId,
|
||||
) -> anyhow::Result<JSONRPCResponse> {
|
||||
) -> anyhow::Result<JsonRpcResponse<serde_json::Value>> {
|
||||
eprintln!("in read_stream_until_response_message({request_id:?})");
|
||||
|
||||
loop {
|
||||
let message = self.read_jsonrpc_message().await?;
|
||||
match message {
|
||||
JSONRPCMessage::Notification(_) => {
|
||||
JsonRpcMessage::Notification(_) => {
|
||||
eprintln!("notification: {message:?}");
|
||||
}
|
||||
JSONRPCMessage::Request(_) => {
|
||||
JsonRpcMessage::Request(_) => {
|
||||
anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}");
|
||||
}
|
||||
JSONRPCMessage::Error(_) => {
|
||||
JsonRpcMessage::Error(_) => {
|
||||
anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}");
|
||||
}
|
||||
JSONRPCMessage::Response(jsonrpc_response) => {
|
||||
JsonRpcMessage::Response(jsonrpc_response) => {
|
||||
if jsonrpc_response.id == request_id {
|
||||
return Ok(jsonrpc_response);
|
||||
}
|
||||
@@ -297,15 +317,15 @@ impl McpProcess {
|
||||
/// Method "codex/event" with params.msg.type == "task_complete".
|
||||
pub async fn read_stream_until_legacy_task_complete_notification(
|
||||
&mut self,
|
||||
) -> anyhow::Result<JSONRPCNotification> {
|
||||
) -> anyhow::Result<JsonRpcNotification<CustomNotification>> {
|
||||
eprintln!("in read_stream_until_legacy_task_complete_notification()");
|
||||
|
||||
loop {
|
||||
let message = self.read_jsonrpc_message().await?;
|
||||
match message {
|
||||
JSONRPCMessage::Notification(notification) => {
|
||||
let is_match = if notification.method == "codex/event" {
|
||||
if let Some(params) = ¬ification.params {
|
||||
JsonRpcMessage::Notification(notification) => {
|
||||
let is_match = if notification.notification.method == "codex/event" {
|
||||
if let Some(params) = ¬ification.notification.params {
|
||||
params
|
||||
.get("msg")
|
||||
.and_then(|m| m.get("type"))
|
||||
@@ -324,13 +344,13 @@ impl McpProcess {
|
||||
eprintln!("ignoring notification: {notification:?}");
|
||||
}
|
||||
}
|
||||
JSONRPCMessage::Request(_) => {
|
||||
JsonRpcMessage::Request(_) => {
|
||||
anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}");
|
||||
}
|
||||
JSONRPCMessage::Error(_) => {
|
||||
JsonRpcMessage::Error(_) => {
|
||||
anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}");
|
||||
}
|
||||
JSONRPCMessage::Response(_) => {
|
||||
JsonRpcMessage::Response(_) => {
|
||||
anyhow::bail!("unexpected JSONRPCMessage::Response: {message:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user