mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
feat: replace custom mcp-types crate with equivalents from rmcp
This commit is contained in:
@@ -55,7 +55,6 @@ indexmap = { workspace = true }
|
||||
indoc = { workspace = true }
|
||||
keyring = { workspace = true, features = ["crypto-rust"] }
|
||||
libc = { workspace = true }
|
||||
mcp-types = { workspace = true }
|
||||
multimap = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
os_info = { workspace = true }
|
||||
@@ -63,6 +62,12 @@ rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
regex-lite = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json", "stream"] }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"base64",
|
||||
"macros",
|
||||
"schemars",
|
||||
"server",
|
||||
] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
+31
-24
@@ -50,6 +50,7 @@ use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::items::PlanItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
@@ -71,14 +72,12 @@ use codex_rmcp_client::OAuthCredentialsStoreMode;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::prelude::*;
|
||||
use futures::stream::FuturesOrdered;
|
||||
use mcp_types::CallToolResult;
|
||||
use mcp_types::ListResourceTemplatesRequestParams;
|
||||
use mcp_types::ListResourceTemplatesResult;
|
||||
use mcp_types::ListResourcesRequestParams;
|
||||
use mcp_types::ListResourcesResult;
|
||||
use mcp_types::ReadResourceRequestParams;
|
||||
use mcp_types::ReadResourceResult;
|
||||
use mcp_types::RequestId;
|
||||
use rmcp::model::ListResourceTemplatesResult;
|
||||
use rmcp::model::ListResourcesResult;
|
||||
use rmcp::model::PaginatedRequestParam;
|
||||
use rmcp::model::ReadResourceRequestParam;
|
||||
use rmcp::model::ReadResourceResult;
|
||||
use rmcp::model::RequestId;
|
||||
use serde_json;
|
||||
use serde_json::Value;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -2202,7 +2201,7 @@ impl Session {
|
||||
pub async fn list_resources(
|
||||
&self,
|
||||
server: &str,
|
||||
params: Option<ListResourcesRequestParams>,
|
||||
params: Option<PaginatedRequestParam>,
|
||||
) -> anyhow::Result<ListResourcesResult> {
|
||||
self.services
|
||||
.mcp_connection_manager
|
||||
@@ -2215,7 +2214,7 @@ impl Session {
|
||||
pub async fn list_resource_templates(
|
||||
&self,
|
||||
server: &str,
|
||||
params: Option<ListResourceTemplatesRequestParams>,
|
||||
params: Option<PaginatedRequestParam>,
|
||||
) -> anyhow::Result<ListResourceTemplatesResult> {
|
||||
self.services
|
||||
.mcp_connection_manager
|
||||
@@ -2228,7 +2227,7 @@ impl Session {
|
||||
pub async fn read_resource(
|
||||
&self,
|
||||
server: &str,
|
||||
params: ReadResourceRequestParams,
|
||||
params: ReadResourceRequestParam,
|
||||
) -> anyhow::Result<ReadResourceResult> {
|
||||
self.services
|
||||
.mcp_connection_manager
|
||||
@@ -2555,10 +2554,10 @@ mod handlers {
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use codex_protocol::mcp::RequestId as ProtocolRequestId;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_rmcp_client::ElicitationAction;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use mcp_types::RequestId;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
@@ -2729,7 +2728,7 @@ mod handlers {
|
||||
pub async fn resolve_elicitation(
|
||||
sess: &Arc<Session>,
|
||||
server_name: String,
|
||||
request_id: RequestId,
|
||||
request_id: ProtocolRequestId,
|
||||
decision: codex_protocol::approvals::ElicitationAction,
|
||||
) {
|
||||
let action = match decision {
|
||||
@@ -2744,6 +2743,12 @@ mod handlers {
|
||||
ElicitationAction::Decline | ElicitationAction::Cancel => None,
|
||||
};
|
||||
let response = ElicitationResponse { action, content };
|
||||
let request_id = match request_id {
|
||||
ProtocolRequestId::String(value) => {
|
||||
rmcp::model::NumberOrString::String(std::sync::Arc::from(value))
|
||||
}
|
||||
ProtocolRequestId::Integer(value) => rmcp::model::NumberOrString::Number(value),
|
||||
};
|
||||
if let Err(err) = sess
|
||||
.resolve_elicitation(server_name, request_id, response)
|
||||
.await
|
||||
@@ -4536,8 +4541,7 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use mcp_types::ContentBlock;
|
||||
use mcp_types::TextContent;
|
||||
use codex_protocol::mcp::CallToolResult as McpCallToolResult;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
@@ -5121,7 +5125,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn prefers_structured_content_when_present() {
|
||||
let ctr = CallToolResult {
|
||||
let ctr = McpCallToolResult {
|
||||
// Content present but should be ignored because structured_content is set.
|
||||
content: vec![text_block("ignored")],
|
||||
is_error: None,
|
||||
@@ -5129,6 +5133,7 @@ mod tests {
|
||||
"ok": true,
|
||||
"value": 42
|
||||
})),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let got = FunctionCallOutputPayload::from(&ctr);
|
||||
@@ -5167,10 +5172,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn falls_back_to_content_when_structured_is_null() {
|
||||
let ctr = CallToolResult {
|
||||
let ctr = McpCallToolResult {
|
||||
content: vec![text_block("hello"), text_block("world")],
|
||||
is_error: None,
|
||||
structured_content: Some(serde_json::Value::Null),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let got = FunctionCallOutputPayload::from(&ctr);
|
||||
@@ -5186,10 +5192,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn success_flag_reflects_is_error_true() {
|
||||
let ctr = CallToolResult {
|
||||
let ctr = McpCallToolResult {
|
||||
content: vec![text_block("unused")],
|
||||
is_error: Some(true),
|
||||
structured_content: Some(json!({ "message": "bad" })),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let got = FunctionCallOutputPayload::from(&ctr);
|
||||
@@ -5204,10 +5211,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn success_flag_true_with_no_error_and_content_used() {
|
||||
let ctr = CallToolResult {
|
||||
let ctr = McpCallToolResult {
|
||||
content: vec![text_block("alpha")],
|
||||
is_error: Some(false),
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let got = FunctionCallOutputPayload::from(&ctr);
|
||||
@@ -5258,11 +5266,10 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn text_block(s: &str) -> ContentBlock {
|
||||
ContentBlock::TextContent(TextContent {
|
||||
annotations: None,
|
||||
text: s.to_string(),
|
||||
r#type: "text".to_string(),
|
||||
fn text_block(s: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "text",
|
||||
"text": s,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -417,7 +417,7 @@ impl ConfigBuilder {
|
||||
fallback_cwd,
|
||||
} = self;
|
||||
let codex_home = codex_home.map_or_else(find_codex_home, std::io::Result::Ok)?;
|
||||
let cli_overrides = cli_overrides.unwrap_or_default();
|
||||
let mut cli_overrides = cli_overrides.unwrap_or_default();
|
||||
let mut harness_overrides = harness_overrides.unwrap_or_default();
|
||||
let loader_overrides = loader_overrides.unwrap_or_default();
|
||||
let cwd_override = harness_overrides.cwd.as_deref().or(fallback_cwd.as_deref());
|
||||
@@ -426,6 +426,36 @@ impl ConfigBuilder {
|
||||
None => AbsolutePathBuf::current_dir()?,
|
||||
};
|
||||
harness_overrides.cwd = Some(cwd.to_path_buf());
|
||||
|
||||
// Support `-c model_instructions_file=...` by eagerly reading the file
|
||||
// and translating it into a `base_instructions` override. This keeps
|
||||
// the behavior consistent across all entrypoints that use
|
||||
// `ConfigBuilder`.
|
||||
if harness_overrides.base_instructions.is_none() {
|
||||
let mut model_instructions_file_override = None;
|
||||
for (key, value) in &cli_overrides {
|
||||
if key == "model_instructions_file" {
|
||||
model_instructions_file_override = Some(value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = model_instructions_file_override {
|
||||
cli_overrides.retain(|(key, _)| key != "model_instructions_file");
|
||||
let TomlValue::String(path) = value else {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"model_instructions_file override must be a string",
|
||||
));
|
||||
};
|
||||
|
||||
let path = AbsolutePathBuf::resolve_path_against_base(path, cwd.as_path())?;
|
||||
if let Some(contents) =
|
||||
Config::try_read_non_empty_file(Some(&path), "model instructions file")?
|
||||
{
|
||||
harness_overrides.base_instructions = Some(contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
let config_layer_stack = load_config_layers_state(
|
||||
&codex_home,
|
||||
Some(cwd),
|
||||
|
||||
@@ -9,9 +9,12 @@ use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_channel::unbounded;
|
||||
use codex_protocol::mcp::Resource;
|
||||
use codex_protocol::mcp::ResourceTemplate;
|
||||
use codex_protocol::mcp::Tool;
|
||||
use codex_protocol::protocol::McpListToolsResponseEvent;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use mcp_types::Tool as McpTool;
|
||||
use serde_json::Value;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::AuthManager;
|
||||
@@ -201,8 +204,8 @@ pub fn split_qualified_tool_name(qualified_name: &str) -> Option<(String, String
|
||||
}
|
||||
|
||||
pub fn group_tools_by_server(
|
||||
tools: &HashMap<String, McpTool>,
|
||||
) -> HashMap<String, HashMap<String, McpTool>> {
|
||||
tools: &HashMap<String, Tool>,
|
||||
) -> HashMap<String, HashMap<String, Tool>> {
|
||||
let mut grouped = HashMap::new();
|
||||
for (qualified_name, tool) in tools {
|
||||
if let Some((server_name, tool_name)) = split_qualified_tool_name(qualified_name) {
|
||||
@@ -230,11 +233,85 @@ pub(crate) async fn collect_mcp_snapshot_from_manager(
|
||||
.map(|(name, entry)| (name.clone(), entry.auth_status))
|
||||
.collect();
|
||||
|
||||
let tools = tools
|
||||
.into_iter()
|
||||
.filter_map(|(name, tool)| {
|
||||
let tool_value = serde_json::to_value(tool.tool).ok()?;
|
||||
let tool = serde_json::from_value::<Tool>(tool_value).ok()?;
|
||||
Some((name, tool))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let resources = resources
|
||||
.into_iter()
|
||||
.map(|(name, resources)| {
|
||||
let resources = resources
|
||||
.into_iter()
|
||||
.filter_map(|resource| match serde_json::to_value(resource) {
|
||||
Ok(value) => match serde_json::from_value::<Resource>(value.clone()) {
|
||||
Ok(resource) => Some(resource),
|
||||
Err(err) => {
|
||||
let (uri, resource_name) = match value {
|
||||
Value::Object(mut obj) => (
|
||||
obj.remove("uri").and_then(|v| v.as_str().map(ToString::to_string)),
|
||||
obj.remove("name").and_then(|v| v.as_str().map(ToString::to_string)),
|
||||
),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
tracing::warn!(
|
||||
"Failed to deserialize MCP resource (uri={uri:?}, name={resource_name:?}): {err}"
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to serialize MCP resource: {err}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
(name, resources)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let resource_templates = resource_templates
|
||||
.into_iter()
|
||||
.map(|(name, templates)| {
|
||||
let templates = templates
|
||||
.into_iter()
|
||||
.filter_map(|template| match serde_json::to_value(template) {
|
||||
Ok(value) => match serde_json::from_value::<ResourceTemplate>(value.clone()) {
|
||||
Ok(template) => Some(template),
|
||||
Err(err) => {
|
||||
let (uri_template, template_name) = match value {
|
||||
Value::Object(mut obj) => (
|
||||
obj.remove("uriTemplate")
|
||||
.or_else(|| obj.remove("uri_template"))
|
||||
.and_then(|v| v.as_str().map(ToString::to_string)),
|
||||
obj.remove("name").and_then(|v| v.as_str().map(ToString::to_string)),
|
||||
),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
tracing::warn!(
|
||||
"Failed to deserialize MCP resource template (uri_template={uri_template:?}, name={template_name:?}): {err}"
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to serialize MCP resource template: {err}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
(name, templates)
|
||||
})
|
||||
.collect();
|
||||
|
||||
McpListToolsResponseEvent {
|
||||
tools: tools
|
||||
.into_iter()
|
||||
.map(|(name, tool)| (name, tool.tool))
|
||||
.collect(),
|
||||
tools,
|
||||
resources,
|
||||
resource_templates,
|
||||
auth_statuses,
|
||||
@@ -244,21 +321,17 @@ pub(crate) async fn collect_mcp_snapshot_from_manager(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mcp_types::ToolInputSchema;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn make_tool(name: &str) -> McpTool {
|
||||
McpTool {
|
||||
annotations: None,
|
||||
description: None,
|
||||
input_schema: ToolInputSchema {
|
||||
properties: None,
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
fn make_tool(name: &str) -> Tool {
|
||||
Tool {
|
||||
name: name.to_string(),
|
||||
output_schema: None,
|
||||
title: None,
|
||||
description: None,
|
||||
input_schema: serde_json::json!({"type": "object", "properties": {}}),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ use async_channel::Sender;
|
||||
use codex_async_utils::CancelErr;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::mcp::RequestId as ProtocolRequestId;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::McpStartupCompleteEvent;
|
||||
@@ -37,22 +39,23 @@ use codex_rmcp_client::SendElicitation;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::future::FutureExt;
|
||||
use futures::future::Shared;
|
||||
use mcp_types::ClientCapabilities;
|
||||
use mcp_types::Implementation;
|
||||
use mcp_types::ListResourceTemplatesRequestParams;
|
||||
use mcp_types::ListResourceTemplatesResult;
|
||||
use mcp_types::ListResourcesRequestParams;
|
||||
use mcp_types::ListResourcesResult;
|
||||
use mcp_types::ReadResourceRequestParams;
|
||||
use mcp_types::ReadResourceResult;
|
||||
use mcp_types::RequestId;
|
||||
use mcp_types::Resource;
|
||||
use mcp_types::ResourceTemplate;
|
||||
use mcp_types::Tool;
|
||||
use rmcp::model::ClientCapabilities;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
use rmcp::model::Implementation;
|
||||
use rmcp::model::InitializeRequestParam;
|
||||
use rmcp::model::ListResourceTemplatesResult;
|
||||
use rmcp::model::ListResourcesResult;
|
||||
use rmcp::model::PaginatedRequestParam;
|
||||
use rmcp::model::ProtocolVersion;
|
||||
use rmcp::model::ReadResourceRequestParam;
|
||||
use rmcp::model::ReadResourceResult;
|
||||
use rmcp::model::RequestId;
|
||||
use rmcp::model::Resource;
|
||||
use rmcp::model::ResourceTemplate;
|
||||
use rmcp::model::Tool;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use sha1::Digest;
|
||||
use sha1::Sha1;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -198,7 +201,14 @@ impl ElicitationRequestManager {
|
||||
id: "mcp_elicitation_request".to_string(),
|
||||
msg: EventMsg::ElicitationRequest(ElicitationRequestEvent {
|
||||
server_name,
|
||||
id,
|
||||
id: match id.clone() {
|
||||
rmcp::model::NumberOrString::String(value) => {
|
||||
ProtocolRequestId::String(value.to_string())
|
||||
}
|
||||
rmcp::model::NumberOrString::Number(value) => {
|
||||
ProtocolRequestId::Integer(value)
|
||||
}
|
||||
},
|
||||
message: elicitation.message,
|
||||
}),
|
||||
})
|
||||
@@ -493,7 +503,7 @@ impl McpConnectionManager {
|
||||
let mut cursor: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let params = cursor.as_ref().map(|next| ListResourcesRequestParams {
|
||||
let params = cursor.as_ref().map(|next| PaginatedRequestParam {
|
||||
cursor: Some(next.clone()),
|
||||
});
|
||||
let response = match client.list_resources(params, timeout).await {
|
||||
@@ -558,11 +568,9 @@ impl McpConnectionManager {
|
||||
let mut cursor: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let params = cursor
|
||||
.as_ref()
|
||||
.map(|next| ListResourceTemplatesRequestParams {
|
||||
cursor: Some(next.clone()),
|
||||
});
|
||||
let params = cursor.as_ref().map(|next| PaginatedRequestParam {
|
||||
cursor: Some(next.clone()),
|
||||
});
|
||||
let response = match client.list_resource_templates(params, timeout).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => return (server_name_cloned, Err(err)),
|
||||
@@ -615,7 +623,7 @@ impl McpConnectionManager {
|
||||
server: &str,
|
||||
tool: &str,
|
||||
arguments: Option<serde_json::Value>,
|
||||
) -> Result<mcp_types::CallToolResult> {
|
||||
) -> Result<CallToolResult> {
|
||||
let client = self.client_by_name(server).await?;
|
||||
if !client.tool_filter.allows(tool) {
|
||||
return Err(anyhow!(
|
||||
@@ -623,18 +631,22 @@ impl McpConnectionManager {
|
||||
));
|
||||
}
|
||||
|
||||
client
|
||||
let result = client
|
||||
.client
|
||||
.call_tool(tool.to_string(), arguments, client.tool_timeout)
|
||||
.await
|
||||
.with_context(|| format!("tool call failed for `{server}/{tool}`"))
|
||||
.with_context(|| format!("tool call failed for `{server}/{tool}`"))?;
|
||||
|
||||
serde_json::to_value(result)
|
||||
.and_then(serde_json::from_value)
|
||||
.context("failed to convert MCP tool result")
|
||||
}
|
||||
|
||||
/// List resources from the specified server.
|
||||
pub async fn list_resources(
|
||||
&self,
|
||||
server: &str,
|
||||
params: Option<ListResourcesRequestParams>,
|
||||
params: Option<PaginatedRequestParam>,
|
||||
) -> Result<ListResourcesResult> {
|
||||
let managed = self.client_by_name(server).await?;
|
||||
let timeout = managed.tool_timeout;
|
||||
@@ -650,7 +662,7 @@ impl McpConnectionManager {
|
||||
pub async fn list_resource_templates(
|
||||
&self,
|
||||
server: &str,
|
||||
params: Option<ListResourceTemplatesRequestParams>,
|
||||
params: Option<PaginatedRequestParam>,
|
||||
) -> Result<ListResourceTemplatesResult> {
|
||||
let managed = self.client_by_name(server).await?;
|
||||
let client = managed.client.clone();
|
||||
@@ -666,7 +678,7 @@ impl McpConnectionManager {
|
||||
pub async fn read_resource(
|
||||
&self,
|
||||
server: &str,
|
||||
params: ReadResourceRequestParams,
|
||||
params: ReadResourceRequestParam,
|
||||
) -> Result<ReadResourceResult> {
|
||||
let managed = self.client_by_name(server).await?;
|
||||
let client = managed.client.clone();
|
||||
@@ -849,25 +861,25 @@ async fn start_server_task(
|
||||
tx_event: Sender<Event>,
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
) -> Result<ManagedClient, StartupOutcomeError> {
|
||||
let params = mcp_types::InitializeRequestParams {
|
||||
let params = InitializeRequestParam {
|
||||
capabilities: ClientCapabilities {
|
||||
experimental: None,
|
||||
roots: None,
|
||||
sampling: None,
|
||||
// https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#capabilities
|
||||
// indicates this should be an empty object.
|
||||
elicitation: Some(json!({})),
|
||||
elicitation: Some(ElicitationCapability {
|
||||
schema_validation: None,
|
||||
}),
|
||||
},
|
||||
client_info: Implementation {
|
||||
name: "codex-mcp-client".to_owned(),
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
title: Some("Codex".into()),
|
||||
// This field is used by Codex when it is an MCP
|
||||
// server: it should not be used when Codex is
|
||||
// an MCP client.
|
||||
user_agent: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
},
|
||||
protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_owned(),
|
||||
protocol_version: ProtocolVersion::V_2025_06_18,
|
||||
};
|
||||
|
||||
let send_elicitation = elicitation_requests.make_sender(server_name.clone(), tx_event);
|
||||
@@ -964,7 +976,7 @@ async fn list_tools_for_client(
|
||||
}
|
||||
ToolInfo {
|
||||
server_name: server_name.to_owned(),
|
||||
tool_name: tool_def.name.clone(),
|
||||
tool_name: tool_def.name.to_string(),
|
||||
tool: tool_def,
|
||||
connector_id: tool.connector_id,
|
||||
connector_name,
|
||||
@@ -1047,24 +1059,23 @@ mod mcp_init_error_display_tests {}
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
use mcp_types::ToolInputSchema;
|
||||
use rmcp::model::JsonObject;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
|
||||
ToolInfo {
|
||||
server_name: server_name.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
tool: Tool {
|
||||
annotations: None,
|
||||
description: Some(format!("Test tool: {tool_name}")),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: None,
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
name: tool_name.to_string(),
|
||||
output_schema: None,
|
||||
name: tool_name.to_string().into(),
|
||||
title: None,
|
||||
description: Some(format!("Test tool: {tool_name}").into()),
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::protocol::EventMsg;
|
||||
use crate::protocol::McpInvocation;
|
||||
use crate::protocol::McpToolCallBeginEvent;
|
||||
use crate::protocol::McpToolCallEndEvent;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -18,7 +19,7 @@ use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestion;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use mcp_types::ToolAnnotations;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Handles the specified tool call dispatches the appropriate
|
||||
@@ -72,7 +73,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
.await;
|
||||
|
||||
let start = Instant::now();
|
||||
let result = sess
|
||||
let result: Result<CallToolResult, String> = sess
|
||||
.call_tool(&server, &tool_name, arguments_value.clone())
|
||||
.await
|
||||
.map_err(|e| format!("tool call error: {e:?}"));
|
||||
@@ -134,7 +135,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
|
||||
let start = Instant::now();
|
||||
// Perform the tool call.
|
||||
let result = sess
|
||||
let result: Result<CallToolResult, String> = sess
|
||||
.call_tool(&server, &tool_name, arguments_value.clone())
|
||||
.await
|
||||
.map_err(|e| format!("tool call error: {e:?}"));
|
||||
@@ -341,7 +342,7 @@ async fn notify_mcp_tool_call_skip(
|
||||
call_id: &str,
|
||||
invocation: McpInvocation,
|
||||
message: String,
|
||||
) -> Result<mcp_types::CallToolResult, String> {
|
||||
) -> Result<CallToolResult, String> {
|
||||
let tool_call_begin_event = EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
|
||||
call_id: call_id.to_string(),
|
||||
invocation: invocation.clone(),
|
||||
|
||||
@@ -4,12 +4,12 @@ use crate::tools::TELEMETRY_PREVIEW_MAX_BYTES;
|
||||
use crate::tools::TELEMETRY_PREVIEW_MAX_LINES;
|
||||
use crate::tools::TELEMETRY_PREVIEW_TRUNCATION_NOTICE;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ShellToolCallParams;
|
||||
use codex_utils_string::take_bytes_at_char_boundary;
|
||||
use mcp_types::CallToolResult;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
|
||||
pub struct McpHandler;
|
||||
|
||||
@@ -54,7 +55,16 @@ impl ToolHandler for McpHandler {
|
||||
|
||||
match response {
|
||||
codex_protocol::models::ResponseInputItem::McpToolCallOutput { result, .. } => {
|
||||
Ok(ToolOutput::Mcp { result })
|
||||
Ok(ToolOutput::Mcp {
|
||||
result: result.and_then(|result| {
|
||||
serde_json::to_value(result)
|
||||
.map_err(|err| err.to_string())
|
||||
.and_then(|value| {
|
||||
serde_json::from_value::<CallToolResult>(value)
|
||||
.map_err(|err| err.to_string())
|
||||
})
|
||||
}),
|
||||
})
|
||||
}
|
||||
codex_protocol::models::ResponseInputItem::FunctionCallOutput { output, .. } => {
|
||||
let codex_protocol::models::FunctionCallOutputPayload {
|
||||
|
||||
@@ -4,17 +4,14 @@ use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use mcp_types::CallToolResult;
|
||||
use mcp_types::ContentBlock;
|
||||
use mcp_types::ListResourceTemplatesRequestParams;
|
||||
use mcp_types::ListResourceTemplatesResult;
|
||||
use mcp_types::ListResourcesRequestParams;
|
||||
use mcp_types::ListResourcesResult;
|
||||
use mcp_types::ReadResourceRequestParams;
|
||||
use mcp_types::ReadResourceResult;
|
||||
use mcp_types::Resource;
|
||||
use mcp_types::ResourceTemplate;
|
||||
use mcp_types::TextContent;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use rmcp::model::ListResourceTemplatesResult;
|
||||
use rmcp::model::ListResourcesResult;
|
||||
use rmcp::model::PaginatedRequestParam;
|
||||
use rmcp::model::ReadResourceRequestParam;
|
||||
use rmcp::model::ReadResourceResult;
|
||||
use rmcp::model::Resource;
|
||||
use rmcp::model::ResourceTemplate;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
@@ -264,7 +261,7 @@ async fn handle_list_resources(
|
||||
|
||||
let payload_result: Result<ListResourcesPayload, FunctionCallError> = async {
|
||||
if let Some(server_name) = server.clone() {
|
||||
let params = cursor.clone().map(|value| ListResourcesRequestParams {
|
||||
let params = cursor.clone().map(|value| PaginatedRequestParam {
|
||||
cursor: Some(value),
|
||||
});
|
||||
let result = session
|
||||
@@ -371,11 +368,9 @@ async fn handle_list_resource_templates(
|
||||
|
||||
let payload_result: Result<ListResourceTemplatesPayload, FunctionCallError> = async {
|
||||
if let Some(server_name) = server.clone() {
|
||||
let params = cursor
|
||||
.clone()
|
||||
.map(|value| ListResourceTemplatesRequestParams {
|
||||
cursor: Some(value),
|
||||
});
|
||||
let params = cursor.clone().map(|value| PaginatedRequestParam {
|
||||
cursor: Some(value),
|
||||
});
|
||||
let result = session
|
||||
.list_resource_templates(&server_name, params)
|
||||
.await
|
||||
@@ -482,7 +477,7 @@ async fn handle_read_resource(
|
||||
|
||||
let payload_result: Result<ReadResourcePayload, FunctionCallError> = async {
|
||||
let result = session
|
||||
.read_resource(&server, ReadResourceRequestParams { uri: uri.clone() })
|
||||
.read_resource(&server, ReadResourceRequestParam { uri: uri.clone() })
|
||||
.await
|
||||
.map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!("resources/read failed: {err:#}"))
|
||||
@@ -551,13 +546,10 @@ async fn handle_read_resource(
|
||||
|
||||
fn call_tool_result_from_content(content: &str, success: Option<bool>) -> CallToolResult {
|
||||
CallToolResult {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
annotations: None,
|
||||
text: content.to_string(),
|
||||
r#type: "text".to_string(),
|
||||
})],
|
||||
is_error: success.map(|value| !value),
|
||||
content: vec![serde_json::json!({"type": "text", "text": content})],
|
||||
structured_content: None,
|
||||
is_error: success.map(|value| !value),
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,32 +670,33 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mcp_types::ListResourcesResult;
|
||||
use mcp_types::ResourceTemplate;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::AnnotateAble;
|
||||
use serde_json::json;
|
||||
|
||||
fn resource(uri: &str, name: &str) -> Resource {
|
||||
Resource {
|
||||
annotations: None,
|
||||
rmcp::model::RawResource {
|
||||
uri: uri.to_string(),
|
||||
name: name.to_string(),
|
||||
title: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
name: name.to_string(),
|
||||
size: None,
|
||||
title: None,
|
||||
uri: uri.to_string(),
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
.no_annotation()
|
||||
}
|
||||
|
||||
fn template(uri_template: &str, name: &str) -> ResourceTemplate {
|
||||
ResourceTemplate {
|
||||
annotations: None,
|
||||
description: None,
|
||||
mime_type: None,
|
||||
rmcp::model::RawResourceTemplate {
|
||||
uri_template: uri_template.to_string(),
|
||||
name: name.to_string(),
|
||||
title: None,
|
||||
uri_template: uri_template.to_string(),
|
||||
description: None,
|
||||
mime_type: None,
|
||||
}
|
||||
.no_annotation()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -719,6 +712,7 @@ mod tests {
|
||||
#[test]
|
||||
fn list_resources_payload_from_single_server_copies_next_cursor() {
|
||||
let result = ListResourcesResult {
|
||||
meta: None,
|
||||
next_cursor: Some("cursor-1".to_string()),
|
||||
resources: vec![resource("memo://id", "memo")],
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ use codex_protocol::models::LocalShellAction;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::models::ShellToolCallParams;
|
||||
use rmcp::model::Tool as McpTool;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
@@ -34,7 +35,7 @@ pub struct ToolRouter {
|
||||
impl ToolRouter {
|
||||
pub fn from_config(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<HashMap<String, mcp_types::Tool>>,
|
||||
mcp_tools: Option<HashMap<String, McpTool>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> Self {
|
||||
let builder = build_specs(config, mcp_tools, dynamic_tools);
|
||||
|
||||
+96
-166
@@ -1087,34 +1087,27 @@ pub(crate) fn create_tools_json_for_chat_completions_api(
|
||||
|
||||
pub(crate) fn mcp_tool_to_openai_tool(
|
||||
fully_qualified_name: String,
|
||||
tool: mcp_types::Tool,
|
||||
tool: rmcp::model::Tool,
|
||||
) -> Result<ResponsesApiTool, serde_json::Error> {
|
||||
let mcp_types::Tool {
|
||||
let rmcp::model::Tool {
|
||||
description,
|
||||
mut input_schema,
|
||||
input_schema,
|
||||
..
|
||||
} = tool;
|
||||
|
||||
// OpenAI models mandate the "properties" field in the schema. The Agents
|
||||
// SDK fixed this by inserting an empty object for "properties" if it is not
|
||||
// already present https://github.com/openai/openai-agents-python/issues/449
|
||||
// so here we do the same.
|
||||
if input_schema.properties.is_none() {
|
||||
input_schema.properties = Some(serde_json::Value::Object(serde_json::Map::new()));
|
||||
}
|
||||
let mut serialized_input_schema = serde_json::Value::Object(input_schema.as_ref().clone());
|
||||
|
||||
// Serialize to a raw JSON value so we can sanitize schemas coming from MCP
|
||||
// servers. Some servers omit the top-level or nested `type` in JSON
|
||||
// Schemas (e.g. using enum/anyOf), or use unsupported variants like
|
||||
// `integer`. Our internal JsonSchema is a small subset and requires
|
||||
// `type`, so we coerce/sanitize here for compatibility.
|
||||
let mut serialized_input_schema = serde_json::to_value(input_schema)?;
|
||||
sanitize_json_schema(&mut serialized_input_schema);
|
||||
let input_schema = serde_json::from_value::<JsonSchema>(serialized_input_schema)?;
|
||||
|
||||
Ok(ResponsesApiTool {
|
||||
name: fully_qualified_name,
|
||||
description: description.unwrap_or_default(),
|
||||
description: description.map(Into::into).unwrap_or_default(),
|
||||
strict: false,
|
||||
parameters: input_schema,
|
||||
})
|
||||
@@ -1254,7 +1247,7 @@ fn sanitize_json_schema(value: &mut JsonValue) {
|
||||
/// Builds the tool registry builder while collecting tool specs for later serialization.
|
||||
pub(crate) fn build_specs(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<HashMap<String, mcp_types::Tool>>,
|
||||
mcp_tools: Option<HashMap<String, rmcp::model::Tool>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolRegistryBuilder {
|
||||
use crate::tools::handlers::ApplyPatchHandler;
|
||||
@@ -1410,7 +1403,7 @@ pub(crate) fn build_specs(
|
||||
}
|
||||
|
||||
if let Some(mcp_tools) = mcp_tools {
|
||||
let mut entries: Vec<(String, mcp_types::Tool)> = mcp_tools.into_iter().collect();
|
||||
let mut entries: Vec<(String, rmcp::model::Tool)> = mcp_tools.into_iter().collect();
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
for (name, tool) in entries.into_iter() {
|
||||
@@ -1452,11 +1445,27 @@ mod tests {
|
||||
use crate::config::test_config;
|
||||
use crate::models_manager::manager::ModelsManager;
|
||||
use crate::tools::registry::ConfiguredToolSpec;
|
||||
use mcp_types::ToolInputSchema;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn mcp_tool(
|
||||
name: &str,
|
||||
description: &str,
|
||||
input_schema: serde_json::Value,
|
||||
) -> rmcp::model::Tool {
|
||||
rmcp::model::Tool {
|
||||
name: name.to_string().into(),
|
||||
title: None,
|
||||
description: Some(description.to_string().into()),
|
||||
input_schema: std::sync::Arc::new(rmcp::model::object(input_schema)),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_name(tool: &ToolSpec) -> &str {
|
||||
match tool {
|
||||
ToolSpec::Function(ResponsesApiTool { name, .. }) => name,
|
||||
@@ -2026,37 +2035,26 @@ mod tests {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"test_server/do_something_cool".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "do_something_cool".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"string_argument": {
|
||||
"type": "string",
|
||||
},
|
||||
"number_argument": {
|
||||
"type": "number",
|
||||
},
|
||||
mcp_tool(
|
||||
"do_something_cool",
|
||||
"Do something cool",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_argument": { "type": "string" },
|
||||
"number_argument": { "type": "number" },
|
||||
"object_argument": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_property": { "type": "string" },
|
||||
"number_property": { "type": "number" },
|
||||
},
|
||||
"required": [
|
||||
"string_property",
|
||||
"number_property",
|
||||
],
|
||||
"additionalProperties": Some(false),
|
||||
"required": ["string_property", "number_property"],
|
||||
"additionalProperties": false,
|
||||
},
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("Do something cool".to_string()),
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
@@ -2120,51 +2118,18 @@ mod tests {
|
||||
});
|
||||
|
||||
// Intentionally construct a map with keys that would sort alphabetically.
|
||||
let tools_map: HashMap<String, mcp_types::Tool> = HashMap::from([
|
||||
let tools_map: HashMap<String, rmcp::model::Tool> = HashMap::from([
|
||||
(
|
||||
"test_server/do".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "a".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("a".to_string()),
|
||||
},
|
||||
mcp_tool("a", "a", serde_json::json!({"type": "object"})),
|
||||
),
|
||||
(
|
||||
"test_server/something".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "b".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("b".to_string()),
|
||||
},
|
||||
mcp_tool("b", "b", serde_json::json!({"type": "object"})),
|
||||
),
|
||||
(
|
||||
"test_server/cool".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "c".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("c".to_string()),
|
||||
},
|
||||
mcp_tool("c", "c", serde_json::json!({"type": "object"})),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -2200,22 +2165,16 @@ mod tests {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/search".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "search".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"query": {
|
||||
"description": "search query"
|
||||
}
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("Search docs".to_string()),
|
||||
},
|
||||
mcp_tool(
|
||||
"search",
|
||||
"Search docs",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"description": "search query"}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
@@ -2258,20 +2217,14 @@ mod tests {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/paginate".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "paginate".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"page": { "type": "integer" }
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("Pagination".to_string()),
|
||||
},
|
||||
mcp_tool(
|
||||
"paginate",
|
||||
"Pagination",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"page": {"type": "integer"}}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
@@ -2313,20 +2266,14 @@ mod tests {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/tags".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "tags".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"tags": { "type": "array" }
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("Tags".to_string()),
|
||||
},
|
||||
mcp_tool(
|
||||
"tags",
|
||||
"Tags",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"tags": {"type": "array"}}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
@@ -2370,20 +2317,16 @@ mod tests {
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"dash/value".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "value".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"value": { "anyOf": [ { "type": "string" }, { "type": "number" } ] }
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("AnyOf Value".to_string()),
|
||||
},
|
||||
mcp_tool(
|
||||
"value",
|
||||
"AnyOf Value",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {"anyOf": [{"type": "string"}, {"type": "number"}]}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
@@ -2482,46 +2425,33 @@ Examples of valid command strings:
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
"test_server/do_something_cool".to_string(),
|
||||
mcp_types::Tool {
|
||||
name: "do_something_cool".to_string(),
|
||||
input_schema: ToolInputSchema {
|
||||
properties: Some(serde_json::json!({
|
||||
"string_argument": {
|
||||
"type": "string",
|
||||
},
|
||||
"number_argument": {
|
||||
"type": "number",
|
||||
},
|
||||
mcp_tool(
|
||||
"do_something_cool",
|
||||
"Do something cool",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_argument": {"type": "string"},
|
||||
"number_argument": {"type": "number"},
|
||||
"object_argument": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_property": { "type": "string" },
|
||||
"number_property": { "type": "number" },
|
||||
"string_property": {"type": "string"},
|
||||
"number_property": {"type": "number"}
|
||||
},
|
||||
"required": [
|
||||
"string_property",
|
||||
"number_property",
|
||||
],
|
||||
"required": ["string_property", "number_property"],
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"addtl_prop": { "type": "string" },
|
||||
"addtl_prop": {"type": "string"}
|
||||
},
|
||||
"required": [
|
||||
"addtl_prop",
|
||||
],
|
||||
"additionalProperties": false,
|
||||
},
|
||||
},
|
||||
})),
|
||||
required: None,
|
||||
r#type: "object".to_string(),
|
||||
},
|
||||
output_schema: None,
|
||||
title: None,
|
||||
annotations: None,
|
||||
description: Some("Do something cool".to_string()),
|
||||
},
|
||||
"required": ["addtl_prop"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)])),
|
||||
&[],
|
||||
)
|
||||
|
||||
@@ -26,7 +26,6 @@ use core_test_support::skip_if_no_network;
|
||||
use core_test_support::stdio_server_bin;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use mcp_types::ContentBlock;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use serial_test::serial;
|
||||
@@ -307,14 +306,10 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
|
||||
let base64_only = OPENAI_PNG
|
||||
.strip_prefix("data:image/png;base64,")
|
||||
.expect("data url prefix");
|
||||
match &result.content[0] {
|
||||
ContentBlock::ImageContent(img) => {
|
||||
assert_eq!(img.mime_type, "image/png");
|
||||
assert_eq!(img.r#type, "image");
|
||||
assert_eq!(img.data, base64_only);
|
||||
}
|
||||
other => panic!("expected image content, got {other:?}"),
|
||||
}
|
||||
let entry = result.content[0].as_object().expect("content object");
|
||||
assert_eq!(entry.get("type"), Some(&json!("image")));
|
||||
assert_eq!(entry.get("mimeType"), Some(&json!("image/png")));
|
||||
assert_eq!(entry.get("data"), Some(&json!(base64_only)));
|
||||
|
||||
wait_for_event(&fixture.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user