Fix MCP tool calling (#14491)

Properly escape mcp tool names and make tools only available via
imports.
This commit is contained in:
pakrym-oai
2026-03-12 13:38:52 -07:00
committed by GitHub
Unverified
parent a5a4899d0c
commit dadffd27d4
9 changed files with 317 additions and 82 deletions
+48 -62
View File
@@ -1,43 +1,8 @@
const __codexEnabledTools = __CODE_MODE_ENABLED_TOOLS_PLACEHOLDER__;
const __codexEnabledToolNames = __codexEnabledTools.map((tool) => tool.tool_name);
const __codexContentItems = Array.isArray(globalThis.__codexContentItems)
? globalThis.__codexContentItems
: [];
function __codexCloneContentItem(item) {
if (!item || typeof item !== 'object') {
throw new TypeError('content item must be an object');
}
switch (item.type) {
case 'input_text':
if (typeof item.text !== 'string') {
throw new TypeError('content item "input_text" requires a string text field');
}
return { type: 'input_text', text: item.text };
case 'input_image':
if (typeof item.image_url !== 'string') {
throw new TypeError('content item "input_image" requires a string image_url field');
}
return { type: 'input_image', image_url: item.image_url };
default:
throw new TypeError(`unsupported content item type "${item.type}"`);
}
}
function __codexNormalizeRawContentItems(value) {
if (Array.isArray(value)) {
return value.flatMap((entry) => __codexNormalizeRawContentItems(entry));
}
return [__codexCloneContentItem(value)];
}
function __codexNormalizeContentItems(value) {
if (typeof value === 'string') {
return [{ type: 'input_text', text: value }];
}
return __codexNormalizeRawContentItems(value);
}
Object.defineProperty(globalThis, '__codexContentItems', {
value: __codexContentItems,
configurable: true,
@@ -45,33 +10,54 @@ Object.defineProperty(globalThis, '__codexContentItems', {
writable: false,
});
globalThis.codex = {
enabledTools: Object.freeze(__codexEnabledToolNames.slice()),
};
globalThis.add_content = (value) => {
const contentItems = __codexNormalizeContentItems(value);
__codexContentItems.push(...contentItems);
return contentItems;
};
globalThis.console = Object.freeze({
log() {},
info() {},
warn() {},
error() {},
debug() {},
});
for (const name of __codexEnabledToolNames) {
if (!(name in globalThis)) {
Object.defineProperty(globalThis, name, {
value: async (args) => __codex_tool_call(name, args),
configurable: true,
enumerable: false,
writable: false,
});
(() => {
function cloneContentItem(item) {
if (!item || typeof item !== 'object') {
throw new TypeError('content item must be an object');
}
switch (item.type) {
case 'input_text':
if (typeof item.text !== 'string') {
throw new TypeError('content item "input_text" requires a string text field');
}
return { type: 'input_text', text: item.text };
case 'input_image':
if (typeof item.image_url !== 'string') {
throw new TypeError('content item "input_image" requires a string image_url field');
}
return { type: 'input_image', image_url: item.image_url };
default:
throw new TypeError(`unsupported content item type "${item.type}"`);
}
}
}
function normalizeRawContentItems(value) {
if (Array.isArray(value)) {
return value.flatMap((entry) => normalizeRawContentItems(entry));
}
return [cloneContentItem(value)];
}
function normalizeContentItems(value) {
if (typeof value === 'string') {
return [{ type: 'input_text', text: value }];
}
return normalizeRawContentItems(value);
}
globalThis.add_content = (value) => {
const contentItems = normalizeContentItems(value);
__codexContentItems.push(...contentItems);
return contentItems;
};
globalThis.console = Object.freeze({
log() {},
info() {},
warn() {},
error() {},
debug() {},
});
})();
__CODE_MODE_USER_CODE_PLACEHOLDER__
@@ -16,4 +16,3 @@
- `set_max_output_tokens_per_exec_call(value)`: sets the token budget for direct `exec` results. By default the result is truncated to 10000 tokens.
- `set_yield_time(value)`: asks `exec` to yield early after that many milliseconds if the script is still running.
- `yield_control()`: yields the accumulated output to the model immediately while the script keeps running.
+3 -1
View File
@@ -17,6 +17,7 @@ use crate::codex::TurnContext;
use crate::tools::ToolRouter;
use crate::tools::code_mode_description::augment_tool_spec_for_code_mode;
use crate::tools::code_mode_description::code_mode_tool_reference;
use crate::tools::code_mode_description::normalize_code_mode_identifier;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::parallel::ToolCallRuntime;
@@ -233,10 +234,11 @@ fn enabled_tool_from_spec(spec: ToolSpec) -> Option<protocol::EnabledTool> {
};
Some(protocol::EnabledTool {
global_name: normalize_code_mode_identifier(&tool_name),
tool_name,
module_path: reference.module_path,
namespace: reference.namespace,
name: reference.tool_key,
name: normalize_code_mode_identifier(&reference.tool_key),
description,
kind,
})
@@ -17,6 +17,7 @@ pub(super) enum CodeModeToolKind {
#[derive(Clone, Debug, Serialize)]
pub(super) struct EnabledTool {
pub(super) tool_name: String,
pub(super) global_name: String,
#[serde(rename = "module")]
pub(super) module_path: String,
pub(super) namespace: Vec<String>,
+25 -7
View File
@@ -134,8 +134,8 @@ function codeModeWorkerMain() {
function createToolsNamespace(callTool, enabledTools) {
const tools = Object.create(null);
for (const { tool_name } of enabledTools) {
Object.defineProperty(tools, tool_name, {
for (const { tool_name, global_name } of enabledTools) {
Object.defineProperty(tools, global_name, {
value: async (args) => callTool(tool_name, args),
configurable: false,
enumerable: true,
@@ -163,9 +163,9 @@ function codeModeWorkerMain() {
const allTools = createAllToolsMetadata(enabledTools);
const exportNames = ['ALL_TOOLS'];
for (const { tool_name } of enabledTools) {
if (tool_name !== 'ALL_TOOLS') {
exportNames.push(tool_name);
for (const { global_name } of enabledTools) {
if (global_name !== 'ALL_TOOLS') {
exportNames.push(global_name);
}
}
@@ -382,6 +382,24 @@ function codeModeWorkerMain() {
};
}
async function resolveDynamicModule(specifier, resolveModule) {
const module = resolveModule(specifier);
if (module.status === 'unlinked') {
await module.link(resolveModule);
}
if (module.status === 'linked' || module.status === 'evaluating') {
await module.evaluate();
}
if (module.status === 'errored') {
throw module.error;
}
return module;
}
async function runModule(context, start, state, callTool) {
const resolveModule = createModuleResolver(
context,
@@ -392,7 +410,8 @@ function codeModeWorkerMain() {
const mainModule = new SourceTextModule(start.source, {
context,
identifier: 'exec_main.mjs',
importModuleDynamically: async (specifier) => resolveModule(specifier),
importModuleDynamically: async (specifier) =>
resolveDynamicModule(specifier, resolveModule),
});
await mainModule.link(resolveModule);
@@ -408,7 +427,6 @@ function codeModeWorkerMain() {
const callTool = createToolCaller();
const context = vm.createContext({
__codexContentItems: createContentItems(),
__codex_tool_call: callTool,
});
try {