mirror of
https://github.com/openai/codex.git
synced 2026-11-03 08:22:30 +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: https://github.com/openai/codex/blob/8b95d3e082376f4cb23e92641705a22afb28a9da/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:
1 parent
8f5edddf71
commit
66447d5d2c
92 files changed
+1629
-8273
No files matched your search
@@ -23,11 +23,11 @@ use codex_core::protocol::ExecPolicyAmendment;
|
||||
use codex_core::protocol::FileChange;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::ReviewDecision;
|
||||
use codex_protocol::mcp::RequestId;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use mcp_types::RequestId;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Stylize;
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ expression: "lines[start_idx..].join(\"\\n\")"
|
||||
exec, linux-sandbox, tui, login, ollama, and mcp.
|
||||
|
||||
• Ran for d in ansi-escape apply-patch arg0 cli common core exec execpolicy
|
||||
│ file-search linux-sandbox login mcp-client mcp-server mcp-types ollama
|
||||
│ file-search linux-sandbox login mcp-client mcp-server ollama
|
||||
│ tui; do echo "--- $d/Cargo.toml"; sed -n '1,200p' $d/Cargo.toml; echo;
|
||||
│ … +1 lines
|
||||
└ --- ansi-escape/Cargo.toml
|
||||
|
||||
@@ -47,6 +47,8 @@ use codex_core::protocol::SessionConfiguredEvent;
|
||||
use codex_core::web_search::web_search_detail;
|
||||
use codex_otel::RuntimeMetricsSummary;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::mcp::Resource;
|
||||
use codex_protocol::mcp::ResourceTemplate;
|
||||
use codex_protocol::models::WebSearchAction;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::plan_tool::PlanItemArg;
|
||||
@@ -57,10 +59,6 @@ use codex_protocol::request_user_input::RequestUserInputQuestion;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
use image::DynamicImage;
|
||||
use image::ImageReader;
|
||||
use mcp_types::EmbeddedResourceResource;
|
||||
use mcp_types::Resource;
|
||||
use mcp_types::ResourceLink;
|
||||
use mcp_types::ResourceTemplate;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
@@ -1203,7 +1201,7 @@ pub(crate) struct McpToolCallCell {
|
||||
invocation: McpInvocation,
|
||||
start_time: Instant,
|
||||
duration: Option<Duration>,
|
||||
result: Option<Result<mcp_types::CallToolResult, String>>,
|
||||
result: Option<Result<codex_protocol::mcp::CallToolResult, String>>,
|
||||
animations_enabled: bool,
|
||||
}
|
||||
|
||||
@@ -1230,7 +1228,7 @@ impl McpToolCallCell {
|
||||
pub(crate) fn complete(
|
||||
&mut self,
|
||||
duration: Duration,
|
||||
result: Result<mcp_types::CallToolResult, String>,
|
||||
result: Result<codex_protocol::mcp::CallToolResult, String>,
|
||||
) -> Option<Box<dyn HistoryCell>> {
|
||||
let image_cell = try_new_completed_mcp_tool_call_with_image_output(&result)
|
||||
.map(|cell| Box::new(cell) as Box<dyn HistoryCell>);
|
||||
@@ -1253,23 +1251,32 @@ impl McpToolCallCell {
|
||||
self.result = Some(Err("interrupted".to_string()));
|
||||
}
|
||||
|
||||
fn render_content_block(block: &mcp_types::ContentBlock, width: usize) -> String {
|
||||
match block {
|
||||
mcp_types::ContentBlock::TextContent(text) => {
|
||||
fn render_content_block(block: &serde_json::Value, width: usize) -> String {
|
||||
let content = match serde_json::from_value::<rmcp::model::Content>(block.clone()) {
|
||||
Ok(content) => content,
|
||||
Err(_) => {
|
||||
return format_and_truncate_tool_result(
|
||||
&block.to_string(),
|
||||
TOOL_CALL_MAX_LINES,
|
||||
width,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
match content.raw {
|
||||
rmcp::model::RawContent::Text(text) => {
|
||||
format_and_truncate_tool_result(&text.text, TOOL_CALL_MAX_LINES, width)
|
||||
}
|
||||
mcp_types::ContentBlock::ImageContent(_) => "<image content>".to_string(),
|
||||
mcp_types::ContentBlock::AudioContent(_) => "<audio content>".to_string(),
|
||||
mcp_types::ContentBlock::EmbeddedResource(resource) => {
|
||||
let uri = match &resource.resource {
|
||||
EmbeddedResourceResource::TextResourceContents(text) => text.uri.clone(),
|
||||
EmbeddedResourceResource::BlobResourceContents(blob) => blob.uri.clone(),
|
||||
rmcp::model::RawContent::Image(_) => "<image content>".to_string(),
|
||||
rmcp::model::RawContent::Audio(_) => "<audio content>".to_string(),
|
||||
rmcp::model::RawContent::Resource(resource) => {
|
||||
let uri = match resource.resource {
|
||||
rmcp::model::ResourceContents::TextResourceContents { uri, .. } => uri,
|
||||
rmcp::model::ResourceContents::BlobResourceContents { uri, .. } => uri,
|
||||
};
|
||||
format!("embedded resource: {uri}")
|
||||
}
|
||||
mcp_types::ContentBlock::ResourceLink(ResourceLink { uri, .. }) => {
|
||||
format!("link: {uri}")
|
||||
}
|
||||
rmcp::model::RawContent::ResourceLink(link) => format!("link: {}", link.uri),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1318,7 +1325,7 @@ impl HistoryCell for McpToolCallCell {
|
||||
|
||||
if let Some(result) = &self.result {
|
||||
match result {
|
||||
Ok(mcp_types::CallToolResult { content, .. }) => {
|
||||
Ok(codex_protocol::mcp::CallToolResult { content, .. }) => {
|
||||
if !content.is_empty() {
|
||||
for block in content {
|
||||
let text = Self::render_content_block(block, detail_wrap_width);
|
||||
@@ -1479,7 +1486,7 @@ pub(crate) fn new_web_search_call(
|
||||
/// `invalid_base64_then_image`, or `invalid_image_bytes_then_image` to ensure this path triggers
|
||||
/// even when the first block is not a valid image.
|
||||
fn try_new_completed_mcp_tool_call_with_image_output(
|
||||
result: &Result<mcp_types::CallToolResult, String>,
|
||||
result: &Result<codex_protocol::mcp::CallToolResult, String>,
|
||||
) -> Option<CompletedMcpToolCallWithImageOutput> {
|
||||
let image = result
|
||||
.as_ref()
|
||||
@@ -1495,13 +1502,18 @@ fn try_new_completed_mcp_tool_call_with_image_output(
|
||||
///
|
||||
/// Returns `None` when the block is not an image, when base64 decoding fails, when the format
|
||||
/// cannot be inferred, or when the image decoder rejects the bytes.
|
||||
fn decode_mcp_image(block: &mcp_types::ContentBlock) -> Option<DynamicImage> {
|
||||
let image = match block {
|
||||
mcp_types::ContentBlock::ImageContent(image) => image,
|
||||
_ => return None,
|
||||
fn decode_mcp_image(block: &serde_json::Value) -> Option<DynamicImage> {
|
||||
let content = serde_json::from_value::<rmcp::model::Content>(block.clone()).ok()?;
|
||||
let rmcp::model::RawContent::Image(image) = content.raw else {
|
||||
return None;
|
||||
};
|
||||
let base64_data = if let Some(data_url) = image.data.strip_prefix("data:") {
|
||||
data_url.split_once(',')?.1
|
||||
} else {
|
||||
image.data.as_str()
|
||||
};
|
||||
let raw_data = base64::engine::general_purpose::STANDARD
|
||||
.decode(&image.data)
|
||||
.decode(base64_data)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decode image data: {e}");
|
||||
e
|
||||
@@ -1584,7 +1596,7 @@ pub(crate) fn empty_mcp_output() -> PlainHistoryCell {
|
||||
/// Render MCP tools grouped by connection using the fully-qualified tool names.
|
||||
pub(crate) fn new_mcp_tools_output(
|
||||
config: &Config,
|
||||
tools: HashMap<String, mcp_types::Tool>,
|
||||
tools: HashMap<String, codex_protocol::mcp::Tool>,
|
||||
resources: HashMap<String, Vec<Resource>>,
|
||||
resource_templates: HashMap<String, Vec<ResourceTemplate>>,
|
||||
auth_statuses: &HashMap<String, McpAuthStatus>,
|
||||
@@ -2262,12 +2274,9 @@ mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_core::protocol::ExecCommandSource;
|
||||
use mcp_types::CallToolResult;
|
||||
use mcp_types::ContentBlock;
|
||||
use mcp_types::ImageContent;
|
||||
use mcp_types::TextContent;
|
||||
use mcp_types::Tool;
|
||||
use mcp_types::ToolInputSchema;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::mcp::Tool;
|
||||
use rmcp::model::Content;
|
||||
|
||||
const SMALL_PNG_BASE64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
|
||||
async fn test_config() -> Config {
|
||||
@@ -2295,13 +2304,32 @@ mod tests {
|
||||
render_lines(&cell.transcript_lines(u16::MAX))
|
||||
}
|
||||
|
||||
fn image_block(data: &str) -> ContentBlock {
|
||||
ContentBlock::ImageContent(ImageContent {
|
||||
annotations: None,
|
||||
data: data.to_string(),
|
||||
mime_type: "image/png".into(),
|
||||
r#type: "image".into(),
|
||||
})
|
||||
fn image_block(data: &str) -> serde_json::Value {
|
||||
serde_json::to_value(Content::image(data.to_string(), "image/png"))
|
||||
.expect("image content should serialize")
|
||||
}
|
||||
|
||||
fn text_block(text: &str) -> serde_json::Value {
|
||||
serde_json::to_value(Content::text(text)).expect("text content should serialize")
|
||||
}
|
||||
|
||||
fn resource_link_block(
|
||||
uri: &str,
|
||||
name: &str,
|
||||
title: Option<&str>,
|
||||
description: Option<&str>,
|
||||
) -> serde_json::Value {
|
||||
serde_json::to_value(Content::resource_link(rmcp::model::RawResource {
|
||||
uri: uri.to_string(),
|
||||
name: name.to_string(),
|
||||
title: title.map(str::to_string),
|
||||
description: description.map(str::to_string),
|
||||
mime_type: None,
|
||||
size: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
}))
|
||||
.expect("resource link content should serialize")
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2480,31 +2508,27 @@ mod tests {
|
||||
tools.insert(
|
||||
"mcp__docs__list".to_string(),
|
||||
Tool {
|
||||
annotations: None,
|
||||
description: None,
|
||||
input_schema: ToolInputSchema {
|
||||
properties: None,
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
name: "list".to_string(),
|
||||
output_schema: None,
|
||||
title: None,
|
||||
input_schema: serde_json::json!({"type": "object", "properties": {}}),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
);
|
||||
tools.insert(
|
||||
"mcp__http__ping".to_string(),
|
||||
Tool {
|
||||
annotations: None,
|
||||
description: None,
|
||||
input_schema: ToolInputSchema {
|
||||
properties: None,
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
name: "ping".to_string(),
|
||||
output_schema: None,
|
||||
title: None,
|
||||
input_schema: serde_json::json!({"type": "object", "properties": {}}),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2653,13 +2677,10 @@ mod tests {
|
||||
};
|
||||
|
||||
let result = CallToolResult {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
annotations: None,
|
||||
text: "Found styling guidance in styles.md".into(),
|
||||
r#type: "text".into(),
|
||||
})],
|
||||
content: vec![text_block("Found styling guidance in styles.md")],
|
||||
is_error: None,
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-2".into(), invocation, true);
|
||||
@@ -2685,15 +2706,12 @@ mod tests {
|
||||
|
||||
let result = CallToolResult {
|
||||
content: vec![
|
||||
ContentBlock::TextContent(TextContent {
|
||||
annotations: None,
|
||||
text: "Here is the image:".into(),
|
||||
r#type: "text".into(),
|
||||
}),
|
||||
text_block("Here is the image:"),
|
||||
image_block(SMALL_PNG_BASE64),
|
||||
],
|
||||
is_error: None,
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-image".into(), invocation, true);
|
||||
@@ -2705,6 +2723,33 @@ mod tests {
|
||||
assert_eq!(rendered, vec!["tool result (image output)"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_mcp_tool_call_accepts_data_url_image_blocks() {
|
||||
let invocation = McpInvocation {
|
||||
server: "image".into(),
|
||||
tool: "generate".into(),
|
||||
arguments: Some(json!({
|
||||
"prompt": "tiny image",
|
||||
})),
|
||||
};
|
||||
|
||||
let data_url = format!("data:image/png;base64,{SMALL_PNG_BASE64}");
|
||||
let result = CallToolResult {
|
||||
content: vec![image_block(&data_url)],
|
||||
is_error: None,
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-image-data-url".into(), invocation, true);
|
||||
let extra_cell = cell
|
||||
.complete(Duration::from_millis(25), Ok(result))
|
||||
.expect("expected image cell");
|
||||
|
||||
let rendered = render_lines(&extra_cell.display_lines(80));
|
||||
assert_eq!(rendered, vec!["tool result (image output)"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_mcp_tool_call_skips_invalid_image_blocks() {
|
||||
let invocation = McpInvocation {
|
||||
@@ -2719,6 +2764,7 @@ mod tests {
|
||||
content: vec![image_block("not-base64"), image_block(SMALL_PNG_BASE64)],
|
||||
is_error: None,
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-image-2".into(), invocation, true);
|
||||
@@ -2765,24 +2811,19 @@ mod tests {
|
||||
|
||||
let result = CallToolResult {
|
||||
content: vec![
|
||||
ContentBlock::TextContent(TextContent {
|
||||
annotations: None,
|
||||
text: "Found styling guidance in styles.md and additional notes in CONTRIBUTING.md.".into(),
|
||||
r#type: "text".into(),
|
||||
}),
|
||||
ContentBlock::ResourceLink(ResourceLink {
|
||||
annotations: None,
|
||||
description: Some("Link to styles documentation".into()),
|
||||
mime_type: None,
|
||||
name: "styles.md".into(),
|
||||
size: None,
|
||||
title: Some("Styles".into()),
|
||||
r#type: "resource_link".into(),
|
||||
uri: "file:///docs/styles.md".into(),
|
||||
}),
|
||||
text_block(
|
||||
"Found styling guidance in styles.md and additional notes in CONTRIBUTING.md.",
|
||||
),
|
||||
resource_link_block(
|
||||
"file:///docs/styles.md",
|
||||
"styles.md",
|
||||
Some("Styles"),
|
||||
Some("Link to styles documentation"),
|
||||
),
|
||||
],
|
||||
is_error: None,
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-4".into(), invocation, true);
|
||||
@@ -2808,13 +2849,12 @@ mod tests {
|
||||
};
|
||||
|
||||
let result = CallToolResult {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
annotations: None,
|
||||
text: "Line one of the response, which is quite long and needs wrapping.\nLine two continues the response with more detail.".into(),
|
||||
r#type: "text".into(),
|
||||
})],
|
||||
content: vec![text_block(
|
||||
"Line one of the response, which is quite long and needs wrapping.\nLine two continues the response with more detail.",
|
||||
)],
|
||||
is_error: None,
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-5".into(), invocation, true);
|
||||
@@ -2841,19 +2881,12 @@ mod tests {
|
||||
|
||||
let result = CallToolResult {
|
||||
content: vec![
|
||||
ContentBlock::TextContent(TextContent {
|
||||
annotations: None,
|
||||
text: "Latency summary: p50=120ms, p95=480ms.".into(),
|
||||
r#type: "text".into(),
|
||||
}),
|
||||
ContentBlock::TextContent(TextContent {
|
||||
annotations: None,
|
||||
text: "No anomalies detected.".into(),
|
||||
r#type: "text".into(),
|
||||
}),
|
||||
text_block("Latency summary: p50=120ms, p95=480ms."),
|
||||
text_block("No anomalies detected."),
|
||||
],
|
||||
is_error: None,
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let mut cell = new_active_mcp_tool_call("call-6".into(), invocation, true);
|
||||
|
||||
Reference in new issue
Block a user