feat: replace custom mcp-types crate with equivalents from rmcp

This commit is contained in:
Michael Bolin
2026-02-01 17:03:29 -08:00
parent 3dd9a37e0b
commit 136c14de8c
66 changed files with 1280 additions and 8652 deletions
+1 -1
View File
@@ -54,7 +54,6 @@ dunce = { workspace = true }
image = { workspace = true, features = ["jpeg", "png"] }
itertools = { workspace = true }
lazy_static = { workspace = true }
mcp-types = { workspace = true }
pathdiff = { workspace = true }
pulldown-cmark = { workspace = true }
rand = { workspace = true }
@@ -67,6 +66,7 @@ ratatui = { workspace = true, features = [
ratatui-macros = { workspace = true }
regex-lite = { workspace = true }
reqwest = { version = "0.12", features = ["json"] }
rmcp = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["preserve_order"] }
shlex = { workspace = true }
@@ -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;
@@ -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
+93 -92
View File
@@ -46,6 +46,8 @@ use codex_core::protocol::McpInvocation;
use codex_core::protocol::SessionConfiguredEvent;
use codex_core::web_search::web_search_detail;
use codex_otel::RuntimeMetricsSummary;
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;
@@ -54,10 +56,6 @@ use codex_protocol::plan_tool::UpdatePlanArgs;
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;
@@ -1199,7 +1197,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,
}
@@ -1226,7 +1224,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>);
@@ -1249,23 +1247,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),
}
}
}
@@ -1314,7 +1321,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);
@@ -1475,7 +1482,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()
@@ -1491,10 +1498,10 @@ 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 raw_data = base64::engine::general_purpose::STANDARD
.decode(&image.data)
@@ -1580,7 +1587,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>,
@@ -2113,12 +2120,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 {
@@ -2146,13 +2150,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]
@@ -2331,31 +2354,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,
},
);
@@ -2504,13 +2523,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);
@@ -2536,15 +2552,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);
@@ -2570,6 +2583,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);
@@ -2616,24 +2630,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);
@@ -2659,13 +2668,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);
@@ -2692,19 +2700,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);
+20
View File
@@ -177,6 +177,26 @@ pub async fn run_main(
}
};
// Fail fast if CODEX_HOME/rules is malformed (e.g. a file instead of a directory).
// This is a common misconfiguration and should not let the TUI start.
let rules_path = codex_home.join("rules");
match std::fs::metadata(&rules_path) {
Ok(metadata) if !metadata.is_dir() => {
return Ok(AppExitInfo::fatal(format!(
"Failed to initialize codex: failed to load rules: failed to read rules files from {}: not a directory",
rules_path.display()
)));
}
Ok(_) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => {
return Ok(AppExitInfo::fatal(format!(
"Failed to initialize codex: failed to load rules: failed to read rules files from {}: {err}",
rules_path.display()
)));
}
}
let cwd = cli.cwd.clone();
let config_cwd = match cwd.as_deref() {
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?,
@@ -35,7 +35,10 @@ model_provider = "ollama"
std::fs::write(codex_home.join("config.toml"), config_contents)?;
let CodexCliOutput { exit_code, output } = run_codex_cli(codex_home, cwd).await?;
assert_ne!(0, exit_code, "Codex CLI should exit nonzero.");
assert_ne!(
0, exit_code,
"Codex CLI should exit nonzero. Output was:\n{output}"
);
assert!(
output.contains("ERROR: Failed to initialize codex:"),
"expected startup error in output, got: {output}"