mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
WIP: add Codex Apps library MCP integration
This commit is contained in:
Generated
+1
@@ -1987,6 +1987,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"shlex",
|
||||
"similar",
|
||||
"tempfile",
|
||||
|
||||
@@ -753,7 +753,7 @@ impl MessageProcessor {
|
||||
| ClientRequest::TurnSteer { request_id, .. } = &codex_request
|
||||
{
|
||||
self.analytics_events_client.track_request(
|
||||
connection_id.0,
|
||||
connection_request_id.connection_id.0,
|
||||
request_id.clone(),
|
||||
codex_request.clone(),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp_connection_manager::ToolInfo;
|
||||
|
||||
pub const SEARCH_LIBRARY_FILES_TOOL_NAME: &str = "search_library_files";
|
||||
pub const LIST_LIBRARY_DIRECTORY_NODES_TOOL_NAME: &str = "list_library_directory_nodes";
|
||||
pub const DOWNLOAD_LIBRARY_FILE_TOOL_NAME: &str = "download_library_file";
|
||||
pub const CREATE_LIBRARY_FILE_TOOL_NAME: &str = "create_library_file";
|
||||
pub const WRITEBACK_LIBRARY_FILE_TOOL_NAME: &str = "writeback_library_file";
|
||||
|
||||
pub fn is_codex_apps_library_tool(tool_name: &str) -> bool {
|
||||
matches!(
|
||||
tool_name,
|
||||
SEARCH_LIBRARY_FILES_TOOL_NAME
|
||||
| LIST_LIBRARY_DIRECTORY_NODES_TOOL_NAME
|
||||
| DOWNLOAD_LIBRARY_FILE_TOOL_NAME
|
||||
| CREATE_LIBRARY_FILE_TOOL_NAME
|
||||
| WRITEBACK_LIBRARY_FILE_TOOL_NAME
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn append_builtin_codex_apps_library_tools(
|
||||
mut tools: Vec<ToolInfo>,
|
||||
server_instructions: Option<&str>,
|
||||
) -> Vec<ToolInfo> {
|
||||
let existing_names = tools
|
||||
.iter()
|
||||
.filter(|tool| tool.server_name == CODEX_APPS_MCP_SERVER_NAME)
|
||||
.map(|tool| tool.tool.name.to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
for tool in builtin_codex_apps_library_tools(server_instructions) {
|
||||
if existing_names.contains(tool.tool.name.as_ref()) {
|
||||
continue;
|
||||
}
|
||||
tools.push(tool);
|
||||
}
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
fn builtin_codex_apps_library_tools(server_instructions: Option<&str>) -> Vec<ToolInfo> {
|
||||
vec![
|
||||
library_tool(
|
||||
SEARCH_LIBRARY_FILES_TOOL_NAME,
|
||||
"Search or list recent library files through the ChatGPT File API. Use this for library discovery instead of local path walking. Optional arguments: q, limit, cursor, category, state.",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"q": {
|
||||
"type": "string",
|
||||
"description": "Optional filename substring query. Omit to list recent files."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 200,
|
||||
"description": "Maximum number of results to return. Defaults to 10."
|
||||
},
|
||||
"cursor": {
|
||||
"type": "string",
|
||||
"description": "Optional pagination cursor from a previous response."
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Optional library file category filter."
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description": "Optional library file state filter."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
ToolAnnotations {
|
||||
destructive_hint: Some(false),
|
||||
idempotent_hint: Some(true),
|
||||
open_world_hint: Some(false),
|
||||
read_only_hint: Some(true),
|
||||
title: None,
|
||||
},
|
||||
server_instructions,
|
||||
),
|
||||
library_tool(
|
||||
LIST_LIBRARY_DIRECTORY_NODES_TOOL_NAME,
|
||||
"List the immediate child folders and files for a library directory through the ChatGPT File API. Use parent_directory_id omitted for the synthetic root.",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parent_directory_id": {
|
||||
"type": "string",
|
||||
"description": "Optional parent directory id. Omit to list the synthetic library root."
|
||||
},
|
||||
"cursor": {
|
||||
"type": "string",
|
||||
"description": "Optional pagination cursor from a previous response."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
ToolAnnotations {
|
||||
destructive_hint: Some(false),
|
||||
idempotent_hint: Some(true),
|
||||
open_world_hint: Some(false),
|
||||
read_only_hint: Some(true),
|
||||
title: None,
|
||||
},
|
||||
server_instructions,
|
||||
),
|
||||
library_tool(
|
||||
DOWNLOAD_LIBRARY_FILE_TOOL_NAME,
|
||||
"Download a library file into a managed per-thread local cache and return the hydrated local filesystem path. Use this when a task needs normal local file access.",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Backing storage file_id for the library file."
|
||||
},
|
||||
"file_name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Filename to use for the hydrated local copy."
|
||||
},
|
||||
"library_file_id": {
|
||||
"type": "string",
|
||||
"description": "Optional library object id for sidecar metadata."
|
||||
}
|
||||
},
|
||||
"required": ["file_id", "file_name"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
ToolAnnotations {
|
||||
destructive_hint: Some(false),
|
||||
idempotent_hint: Some(true),
|
||||
open_world_hint: Some(false),
|
||||
read_only_hint: Some(true),
|
||||
title: None,
|
||||
},
|
||||
server_instructions,
|
||||
),
|
||||
library_tool(
|
||||
CREATE_LIBRARY_FILE_TOOL_NAME,
|
||||
"Create a new text file in the user's library through the ChatGPT File API. V1 is create-new-only: do not use this to update, replace, or delete an existing library file.",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Name for the new library file, including extension."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "UTF-8 text content for the new file. Maximum size is 500 MB."
|
||||
}
|
||||
},
|
||||
"required": ["file_name", "content"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
ToolAnnotations {
|
||||
destructive_hint: Some(false),
|
||||
idempotent_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
read_only_hint: Some(false),
|
||||
title: None,
|
||||
},
|
||||
server_instructions,
|
||||
),
|
||||
library_tool(
|
||||
WRITEBACK_LIBRARY_FILE_TOOL_NAME,
|
||||
"Write back a hydrated local library file by creating a new library file only if the local copy changed. V1 never updates, replaces, or deletes the source file.",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"local_path": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Absolute local path returned by download_library_file for the hydrated copy."
|
||||
},
|
||||
"file_name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Optional name for the new library file. Defaults to the hydrated source filename."
|
||||
}
|
||||
},
|
||||
"required": ["local_path"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
ToolAnnotations {
|
||||
destructive_hint: Some(false),
|
||||
idempotent_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
read_only_hint: Some(false),
|
||||
title: None,
|
||||
},
|
||||
server_instructions,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn library_tool(
|
||||
name: &str,
|
||||
description: &str,
|
||||
input_schema: JsonValue,
|
||||
annotations: ToolAnnotations,
|
||||
server_instructions: Option<&str>,
|
||||
) -> ToolInfo {
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
callable_name: name.to_string(),
|
||||
callable_namespace: "mcp__codex_apps__".to_string(),
|
||||
server_instructions: server_instructions.map(str::to_string),
|
||||
tool: Tool {
|
||||
name: name.to_string().into(),
|
||||
title: None,
|
||||
description: Some(description.to_string().into()),
|
||||
input_schema: Arc::new(input_schema.as_object().cloned().unwrap_or_else(Map::new)),
|
||||
output_schema: None,
|
||||
annotations: Some(annotations),
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
connector_description: None,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
pub(crate) mod codex_apps_library_tools;
|
||||
pub(crate) mod mcp;
|
||||
pub(crate) mod mcp_connection_manager;
|
||||
pub(crate) mod mcp_tool_names;
|
||||
|
||||
pub use codex_apps_library_tools::CREATE_LIBRARY_FILE_TOOL_NAME;
|
||||
pub use codex_apps_library_tools::DOWNLOAD_LIBRARY_FILE_TOOL_NAME;
|
||||
pub use codex_apps_library_tools::LIST_LIBRARY_DIRECTORY_NODES_TOOL_NAME;
|
||||
pub use codex_apps_library_tools::SEARCH_LIBRARY_FILES_TOOL_NAME;
|
||||
pub use codex_apps_library_tools::WRITEBACK_LIBRARY_FILE_TOOL_NAME;
|
||||
pub use codex_apps_library_tools::is_codex_apps_library_tool;
|
||||
pub use mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
pub use mcp::McpAuthStatusEntry;
|
||||
pub use mcp::McpConfig;
|
||||
|
||||
@@ -20,6 +20,7 @@ use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::McpAuthStatusEntry;
|
||||
use crate::codex_apps_library_tools::append_builtin_codex_apps_library_tools;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::McpConfig;
|
||||
use crate::mcp::ToolPluginProvenance;
|
||||
@@ -564,7 +565,8 @@ impl AsyncManagedClient {
|
||||
|
||||
async fn listed_tools(&self) -> Option<Vec<ToolInfo>> {
|
||||
let annotate_tools = |tools: Vec<ToolInfo>| {
|
||||
let mut tools = tools;
|
||||
let mut tools =
|
||||
append_builtin_codex_apps_library_tools(tools, /*server_instructions*/ None);
|
||||
for tool in &mut tools {
|
||||
if tool.server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
|
||||
@@ -1685,6 +1687,11 @@ async fn list_tools_for_client_uncached(
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let tools = if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
append_builtin_codex_apps_library_tools(tools, server_instructions)
|
||||
} else {
|
||||
tools
|
||||
};
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
return Ok(filter_disallowed_codex_apps_tools(tools));
|
||||
}
|
||||
|
||||
@@ -727,7 +727,10 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty(
|
||||
let timeout_result =
|
||||
tokio::time::timeout(Duration::from_millis(10), manager.list_all_tools()).await;
|
||||
let tools = timeout_result.expect("cache-hit startup snapshot should not block");
|
||||
assert!(tools.is_empty());
|
||||
assert!(
|
||||
tools.contains_key("mcp__codex_apps__search_library_files"),
|
||||
"builtin codex apps library tools should still be available from an empty startup snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -97,6 +97,7 @@ rmcp = { workspace = true, default-features = false, features = [
|
||||
] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
similar = { workspace = true }
|
||||
|
||||
@@ -1131,6 +1131,70 @@ async fn mcp_tool_exposure_directly_exposes_explicit_apps_without_deferred_overl
|
||||
assert!(deferred_tools.contains_key("mcp__rmcp__tool_0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_exposure_directly_exposes_builtin_codex_apps_library_tools_without_connectors() {
|
||||
let config = test_config();
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
|
||||
let mcp_tools = HashMap::from([(
|
||||
"mcp__codex_apps__search_library_files".to_string(),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"search_library_files",
|
||||
/*connector_id*/ None,
|
||||
/*connector_name*/ None,
|
||||
),
|
||||
)]);
|
||||
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
&mcp_tools,
|
||||
/*connectors*/ None,
|
||||
&[],
|
||||
&config,
|
||||
&tools_config,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
exposure.direct_tools.into_keys().collect::<Vec<_>>(),
|
||||
vec!["mcp__codex_apps__search_library_files".to_string()]
|
||||
);
|
||||
assert!(exposure.deferred_tools.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_exposure_keeps_builtin_codex_apps_library_tools_direct_in_large_search_sets() {
|
||||
let config = test_config();
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
|
||||
let mut mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD);
|
||||
mcp_tools.insert(
|
||||
"mcp__codex_apps__search_library_files".to_string(),
|
||||
make_mcp_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"search_library_files",
|
||||
/*connector_id*/ None,
|
||||
/*connector_name*/ None,
|
||||
),
|
||||
);
|
||||
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
&mcp_tools,
|
||||
/*connectors*/ None,
|
||||
&[],
|
||||
&config,
|
||||
&tools_config,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
exposure.direct_tools.into_keys().collect::<Vec<_>>(),
|
||||
vec!["mcp__codex_apps__search_library_files".to_string()]
|
||||
);
|
||||
let deferred_tools = exposure
|
||||
.deferred_tools
|
||||
.as_ref()
|
||||
.expect("large tool sets should be discoverable through tool_search");
|
||||
assert!(deferred_tools.contains_key("mcp__codex_apps__search_library_files"));
|
||||
assert!(deferred_tools.contains_key("mcp__rmcp__tool_0"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconstruct_history_matches_live_compactions() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
|
||||
@@ -44,6 +44,7 @@ mod installation_id;
|
||||
pub(crate) mod instructions;
|
||||
pub(crate) mod landlock;
|
||||
pub use landlock::spawn_command_under_linux_sandbox;
|
||||
mod library_mcp;
|
||||
pub(crate) mod mcp;
|
||||
mod mcp_skill_dependencies;
|
||||
mod mcp_tool_approval_templates;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@ use crate::guardian::guardian_timeout_message;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::library_mcp::handle_library_mcp_tool_call;
|
||||
use crate::mcp_openai_file::rewrite_mcp_tool_arguments_for_openai_files;
|
||||
use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam;
|
||||
use crate::mcp_tool_approval_templates::render_mcp_tool_approval_template;
|
||||
@@ -38,6 +39,7 @@ use codex_features::Feature;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::SandboxState;
|
||||
use codex_mcp::declared_openai_file_input_param_names;
|
||||
use codex_mcp::is_codex_apps_library_tool;
|
||||
use codex_mcp::mcp_permission_prompt_is_auto_approved;
|
||||
use codex_otel::sanitize_metric_tag_value;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
@@ -460,6 +462,16 @@ async fn execute_mcp_tool_call(
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
request_meta: Option<serde_json::Value>,
|
||||
) -> Result<CallToolResult, String> {
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME && is_codex_apps_library_tool(tool_name) {
|
||||
return sanitize_mcp_tool_result_for_model(
|
||||
turn_context
|
||||
.model_info
|
||||
.input_modalities
|
||||
.contains(&InputModality::Image),
|
||||
handle_library_mcp_tool_call(sess, turn_context, tool_name, arguments_value).await,
|
||||
);
|
||||
}
|
||||
|
||||
let rewritten_arguments = rewrite_mcp_tool_arguments_for_openai_files(
|
||||
sess,
|
||||
turn_context,
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::collections::HashSet;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ToolInfo as McpToolInfo;
|
||||
use codex_mcp::filter_non_codex_apps_mcp_tools_only;
|
||||
use codex_mcp::is_codex_apps_library_tool;
|
||||
use codex_tools::ToolsConfig;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -24,6 +25,7 @@ pub(crate) fn build_mcp_tool_exposure(
|
||||
tools_config: &ToolsConfig,
|
||||
) -> McpToolExposure {
|
||||
let mut deferred_tools = filter_non_codex_apps_mcp_tools_only(all_mcp_tools);
|
||||
deferred_tools.extend(filter_builtin_codex_apps_library_tools(all_mcp_tools));
|
||||
if let Some(connectors) = connectors {
|
||||
deferred_tools.extend(filter_codex_apps_mcp_tools(
|
||||
all_mcp_tools,
|
||||
@@ -45,12 +47,31 @@ pub(crate) fn build_mcp_tool_exposure(
|
||||
deferred_tools.remove(direct_tool_name);
|
||||
}
|
||||
|
||||
let mut direct_tools = filter_builtin_codex_apps_library_tools(all_mcp_tools);
|
||||
direct_tools.extend(filter_codex_apps_mcp_tools(
|
||||
all_mcp_tools,
|
||||
explicitly_enabled_connectors,
|
||||
config,
|
||||
));
|
||||
McpToolExposure {
|
||||
direct_tools,
|
||||
deferred_tools: (!deferred_tools.is_empty()).then_some(deferred_tools),
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_builtin_codex_apps_library_tools(
|
||||
mcp_tools: &HashMap<String, McpToolInfo>,
|
||||
) -> HashMap<String, McpToolInfo> {
|
||||
mcp_tools
|
||||
.iter()
|
||||
.filter(|(_, tool)| {
|
||||
tool.server_name == CODEX_APPS_MCP_SERVER_NAME
|
||||
&& is_codex_apps_library_tool(tool.tool.name.as_ref())
|
||||
})
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn filter_codex_apps_mcp_tools(
|
||||
mcp_tools: &HashMap<String, McpToolInfo>,
|
||||
connectors: &[connectors::AppInfo],
|
||||
|
||||
Reference in New Issue
Block a user