mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
feat: add MCP protocol types and rmcp adapters
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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+285
-18
@@ -9,9 +9,13 @@ 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::Deserialize;
|
||||
use serde_json::Value;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::AuthManager;
|
||||
@@ -201,8 +205,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) {
|
||||
@@ -215,6 +219,155 @@ pub fn group_tools_by_server(
|
||||
grouped
|
||||
}
|
||||
|
||||
fn deserialize_lossy_opt_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let number = Option::<serde_json::Number>::deserialize(deserializer)?;
|
||||
let Some(number) = number else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if let Some(v) = number.as_i64() {
|
||||
return Ok(Some(v));
|
||||
}
|
||||
if let Some(v) = number.as_u64() {
|
||||
return Ok(i64::try_from(v).ok());
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ToolSerde {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(default, rename = "inputSchema", alias = "input_schema")]
|
||||
input_schema: Value,
|
||||
#[serde(default, rename = "outputSchema", alias = "output_schema")]
|
||||
output_schema: Option<Value>,
|
||||
#[serde(default)]
|
||||
annotations: Option<Value>,
|
||||
#[serde(default)]
|
||||
icons: Option<Vec<Value>>,
|
||||
#[serde(rename = "_meta", default)]
|
||||
meta: Option<Value>,
|
||||
}
|
||||
|
||||
impl From<ToolSerde> for Tool {
|
||||
fn from(value: ToolSerde) -> Self {
|
||||
let ToolSerde {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
input_schema,
|
||||
output_schema,
|
||||
annotations,
|
||||
icons,
|
||||
meta,
|
||||
} = value;
|
||||
Self {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
input_schema,
|
||||
output_schema,
|
||||
annotations,
|
||||
icons,
|
||||
meta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ResourceSerde {
|
||||
#[serde(default)]
|
||||
annotations: Option<Value>,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(rename = "mimeType", alias = "mime_type", default)]
|
||||
mime_type: Option<String>,
|
||||
name: String,
|
||||
#[serde(default, deserialize_with = "deserialize_lossy_opt_i64")]
|
||||
size: Option<i64>,
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
uri: String,
|
||||
#[serde(default)]
|
||||
icons: Option<Vec<Value>>,
|
||||
#[serde(rename = "_meta", default)]
|
||||
meta: Option<Value>,
|
||||
}
|
||||
|
||||
impl From<ResourceSerde> for Resource {
|
||||
fn from(value: ResourceSerde) -> Self {
|
||||
let ResourceSerde {
|
||||
annotations,
|
||||
description,
|
||||
mime_type,
|
||||
name,
|
||||
size,
|
||||
title,
|
||||
uri,
|
||||
icons,
|
||||
meta,
|
||||
} = value;
|
||||
Self {
|
||||
annotations,
|
||||
description,
|
||||
mime_type,
|
||||
name,
|
||||
size,
|
||||
title,
|
||||
uri,
|
||||
icons,
|
||||
meta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ResourceTemplateSerde {
|
||||
#[serde(default)]
|
||||
annotations: Option<Value>,
|
||||
#[serde(rename = "uriTemplate", alias = "uri_template")]
|
||||
uri_template: String,
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(rename = "mimeType", alias = "mime_type", default)]
|
||||
mime_type: Option<String>,
|
||||
}
|
||||
|
||||
impl From<ResourceTemplateSerde> for ResourceTemplate {
|
||||
fn from(value: ResourceTemplateSerde) -> Self {
|
||||
let ResourceTemplateSerde {
|
||||
annotations,
|
||||
uri_template,
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
mime_type,
|
||||
} = value;
|
||||
Self {
|
||||
annotations,
|
||||
uri_template,
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
mime_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn collect_mcp_snapshot_from_manager(
|
||||
mcp_connection_manager: &McpConnectionManager,
|
||||
auth_status_entries: HashMap<String, crate::mcp::auth::McpAuthStatusEntry>,
|
||||
@@ -230,11 +383,98 @@ 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)| match serde_json::to_value(tool.tool) {
|
||||
Ok(value) => match serde_json::from_value::<ToolSerde>(value) {
|
||||
Ok(tool) => Some((name, tool.into())),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to convert MCP tool '{name}': {err}");
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to serialize MCP tool '{name}': {err}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.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::<ResourceSerde>(value.clone()) {
|
||||
Ok(resource) => Some(resource.into()),
|
||||
Err(err) => {
|
||||
let (uri, resource_name) = match value {
|
||||
Value::Object(obj) => (
|
||||
obj.get("uri")
|
||||
.and_then(|v| v.as_str().map(ToString::to_string)),
|
||||
obj.get("name")
|
||||
.and_then(|v| v.as_str().map(ToString::to_string)),
|
||||
),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
tracing::warn!(
|
||||
"Failed to convert 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::<ResourceTemplateSerde>(value.clone()) {
|
||||
Ok(template) => Some(template.into()),
|
||||
Err(err) => {
|
||||
let (uri_template, template_name) = match value {
|
||||
Value::Object(obj) => (
|
||||
obj.get("uriTemplate")
|
||||
.or_else(|| obj.get("uri_template"))
|
||||
.and_then(|v| v.as_str().map(ToString::to_string)),
|
||||
obj.get("name")
|
||||
.and_then(|v| v.as_str().map(ToString::to_string)),
|
||||
),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
tracing::warn!(
|
||||
"Failed to convert 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,24 +484,51 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_size_deserializes_without_narrowing() {
|
||||
let resource = serde_json::json!({
|
||||
"name": "big",
|
||||
"uri": "file:///tmp/big",
|
||||
"size": 5_000_000_000u64,
|
||||
});
|
||||
|
||||
let parsed = serde_json::from_value::<ResourceSerde>(resource).expect("should deserialize");
|
||||
assert_eq!(parsed.size, Some(5_000_000_000));
|
||||
|
||||
let resource = serde_json::json!({
|
||||
"name": "negative",
|
||||
"uri": "file:///tmp/negative",
|
||||
"size": -1,
|
||||
});
|
||||
|
||||
let parsed = serde_json::from_value::<ResourceSerde>(resource).expect("should deserialize");
|
||||
assert_eq!(parsed.size, Some(-1));
|
||||
|
||||
let resource = serde_json::json!({
|
||||
"name": "too_big_for_i64",
|
||||
"uri": "file:///tmp/too_big_for_i64",
|
||||
"size": 18446744073709551615u64,
|
||||
});
|
||||
|
||||
let parsed = serde_json::from_value::<ResourceSerde>(resource).expect("should deserialize");
|
||||
assert_eq!(parsed.size, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_qualified_tool_name_returns_server_and_tool() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -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,34 @@ impl McpConnectionManager {
|
||||
));
|
||||
}
|
||||
|
||||
client
|
||||
let result: rmcp::model::CallToolResult = 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}`"))?;
|
||||
|
||||
let content = result
|
||||
.content
|
||||
.into_iter()
|
||||
.map(|content| {
|
||||
serde_json::to_value(content)
|
||||
.unwrap_or_else(|_| serde_json::Value::String("<content>".to_string()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(CallToolResult {
|
||||
content,
|
||||
structured_content: result.structured_content,
|
||||
is_error: result.is_error,
|
||||
meta: result.meta.and_then(|meta| serde_json::to_value(meta).ok()),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 +674,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 +690,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 +873,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 +988,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 +1071,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;
|
||||
|
||||
@@ -209,7 +209,7 @@ where
|
||||
use tokio::time::timeout;
|
||||
loop {
|
||||
// Allow a bit more time to accommodate async startup work (e.g. config IO, tool discovery)
|
||||
let ev = timeout(wait_time.max(Duration::from_secs(5)), codex.next_event())
|
||||
let ev = timeout(wait_time.max(Duration::from_secs(10)), codex.next_event())
|
||||
.await
|
||||
.expect("timeout waiting for event")
|
||||
.expect("stream ended unexpectedly");
|
||||
|
||||
@@ -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