fix(code-execution): support bracket notation and multiple import styles (#6290)

Signed-off-by: rabi <ramishra@redhat.com>
This commit is contained in:
Rabi Mishra
2026-01-06 00:21:52 +05:30
committed by GitHub
parent 59088e033e
commit ddaedd8b76
2 changed files with 100 additions and 26 deletions
@@ -257,8 +257,8 @@ impl ToolInfo {
.join(", "); .join(", ");
let desc = self.description.lines().next().unwrap_or(""); let desc = self.description.lines().next().unwrap_or("");
format!( format!(
"{}({{ {params} }}): {} - {desc}", "{}[\"{}\"]({{{params}}}): {} - {desc}",
self.tool_name, self.return_type self.server_name, self.tool_name, self.return_type
) )
} }
} }
@@ -268,29 +268,45 @@ thread_local! {
const { std::cell::RefCell::new(None) }; const { std::cell::RefCell::new(None) };
} }
fn create_server_module(server_tools: &[&ToolInfo], ctx: &mut Context) -> Module { fn create_server_module(
let (export_names, tool_data): (Vec<JsString>, Vec<(String, String)>) = server_tools server_name: &str,
server_tools: &[&ToolInfo],
ctx: &mut Context,
) -> Module {
let tool_data: Vec<(String, String)> = server_tools
.iter() .iter()
.map(|t| { .map(|t| (t.tool_name.clone(), t.full_name.clone()))
( .collect();
js_string!(t.tool_name.as_str()),
(t.tool_name.clone(), t.full_name.clone()), let mut export_names: Vec<JsString> = server_tools
) .iter()
}) .map(|t| js_string!(t.tool_name.as_str()))
.unzip(); .collect();
export_names.push(js_string!(server_name));
let server_name_owned = server_name.to_string();
Module::synthetic( Module::synthetic(
&export_names, &export_names,
SyntheticModuleInitializer::from_copy_closure_with_captures( SyntheticModuleInitializer::from_copy_closure_with_captures(
|module, tools, context| { |module, (tools, server_name), context| {
for (tool_name, full_name) in tools { let namespace_obj = boa_engine::JsObject::with_null_proto();
for (tool_name, full_name) in tools.iter() {
let func = create_tool_function(full_name.clone()); let func = create_tool_function(full_name.clone());
let js_func = func.to_js_function(context.realm()); let js_func = func.to_js_function(context.realm());
module.set_export(&js_string!(tool_name.as_str()), js_func.into())?; module.set_export(&js_string!(tool_name.as_str()), js_func.clone().into())?;
namespace_obj
.set(js_string!(tool_name.as_str()), js_func, false, context)
.map_err(|e| {
JsNativeError::error().with_message(format!("Failed to set prop: {e}"))
})?;
} }
module.set_export(&js_string!(server_name.as_str()), namespace_obj.into())?;
Ok(()) Ok(())
}, },
tool_data, (tool_data, server_name_owned),
), ),
None, None,
None, None,
@@ -364,7 +380,7 @@ fn run_js_module(
} }
for (server_name, server_tools) in &by_server { for (server_name, server_tools) in &by_server {
let module = create_server_module(server_tools, &mut ctx); let module = create_server_module(server_name, server_tools, &mut ctx);
loader.insert(*server_name, module); loader.insert(*server_name, module);
} }
@@ -524,11 +540,9 @@ impl CodeExecutionClient {
if server_tools.is_empty() { if server_tools.is_empty() {
return Err(format!("Module not found: {server}")); return Err(format!("Module not found: {server}"));
} }
let names: Vec<_> = server_tools.iter().map(|t| t.tool_name.as_str()).collect();
let sigs: Vec<_> = server_tools.iter().map(|t| t.to_signature()).collect(); let sigs: Vec<_> = server_tools.iter().map(|t| t.to_signature()).collect();
Ok(vec![Content::text(format!( Ok(vec![Content::text(format!(
"// import {{ {} }} from \"{server}\";\n\n{}", "// import * as {server} from \"{server}\";\n\n{}",
names.join(", "),
sigs.join("\n") sigs.join("\n")
))]) ))])
} }
@@ -538,7 +552,7 @@ impl CodeExecutionClient {
.find(|t| t.server_name == *server && t.tool_name == *tool) .find(|t| t.server_name == *server && t.tool_name == *tool)
.ok_or_else(|| format!("Tool not found: {server}/{tool}"))?; .ok_or_else(|| format!("Tool not found: {server}/{tool}"))?;
Ok(vec![Content::text(format!( Ok(vec![Content::text(format!(
"// import {{ {tool} }} from \"{server}\";\n\n{}\n\n{}", "// import * as {server} from \"{server}\";\n\n{}\n\n{}",
t.to_signature(), t.to_signature(),
t.description t.description
))]) ))])
@@ -1087,21 +1101,21 @@ mod tests {
"github__get_me", "github__get_me",
serde_json::json!({"type": "object", "properties": {}}), serde_json::json!({"type": "object", "properties": {}}),
None, None,
"get_me({ }): string - Get details of the authenticated user"; "github[\"get_me\"]({}): string - Get details of the authenticated user";
"no params, no output schema" "no params, no output schema"
)] )]
#[test_case( #[test_case(
"filesystem__read_text_file", "filesystem__read_text_file",
serde_json::json!({"type": "object", "properties": {"path": {"type": "string"}, "tail": {"type": "number"}, "head": {"type": "number"}}, "required": ["path"]}), serde_json::json!({"type": "object", "properties": {"path": {"type": "string"}, "tail": {"type": "number"}, "head": {"type": "number"}}, "required": ["path"]}),
Some(serde_json::json!({"type": "object", "properties": {"content": {"type": "string"}}, "required": ["content"]})), Some(serde_json::json!({"type": "object", "properties": {"content": {"type": "string"}}, "required": ["content"]})),
"read_text_file({ head?: number, path: string, tail?: number }): { content: string } - Read the complete contents of a file"; "filesystem[\"read_text_file\"]({head?: number, path: string, tail?: number}): { content: string } - Read the complete contents of a file";
"optional number params, object output" "optional number params, object output"
)] )]
#[test_case( #[test_case(
"memory__create_entities", "memory__create_entities",
serde_json::json!({"type": "object", "properties": {"entities": {"type": "array", "items": {"type": "object", "properties": {"name": {"type": "string"}, "entityType": {"type": "string"}, "observations": {"type": "array", "items": {"type": "string"}}}, "required": ["name", "entityType", "observations"]}}}, "required": ["entities"]}), serde_json::json!({"type": "object", "properties": {"entities": {"type": "array", "items": {"type": "object", "properties": {"name": {"type": "string"}, "entityType": {"type": "string"}, "observations": {"type": "array", "items": {"type": "string"}}}, "required": ["name", "entityType", "observations"]}}}, "required": ["entities"]}),
Some(serde_json::json!({"type": "object", "properties": {"entities": {"type": "array", "items": {"type": "object", "properties": {"name": {"type": "string"}, "entityType": {"type": "string"}, "observations": {"type": "array", "items": {"type": "string"}}}, "required": ["name", "entityType", "observations"]}}}, "required": ["entities"]})), Some(serde_json::json!({"type": "object", "properties": {"entities": {"type": "array", "items": {"type": "object", "properties": {"name": {"type": "string"}, "entityType": {"type": "string"}, "observations": {"type": "array", "items": {"type": "string"}}}, "required": ["name", "entityType", "observations"]}}}, "required": ["entities"]})),
"create_entities({ entities: { entityType: string, name: string, observations: string[] }[] }): { entities: { entityType: string, name: string, observations: string[] }[] } - Create multiple new entities"; "memory[\"create_entities\"]({entities: { entityType: string, name: string, observations: string[] }[]}): { entities: { entityType: string, name: string, observations: string[] }[] } - Create multiple new entities";
"nested object array with typed props" "nested object array with typed props"
)] )]
#[test_case( #[test_case(
@@ -1111,7 +1125,7 @@ mod tests {
"state": {"type": "string", "enum": ["read", "done"]} "state": {"type": "string", "enum": ["read", "done"]}
}, "required": ["threadID", "state"]}), }, "required": ["threadID", "state"]}),
None, None,
"dismiss_notification({ state: \"read\" | \"done\", threadID: string }): string - Dismiss a notification"; "github[\"dismiss_notification\"]({state: \"read\" | \"done\", threadID: string}): string - Dismiss a notification";
"enum param, no output schema" "enum param, no output schema"
)] )]
#[test_case( #[test_case(
@@ -1121,9 +1135,20 @@ mod tests {
"save_as": {"oneOf": [{"const": "text"}, {"const": "json"}, {"const": "binary"}]} "save_as": {"oneOf": [{"const": "text"}, {"const": "json"}, {"const": "binary"}]}
}, "required": ["url"]}), }, "required": ["url"]}),
None, None,
"web_scrape({ save_as?: \"text\" | \"json\" | \"binary\", url: string }): string - Scrape content from URL"; "computercontroller[\"web_scrape\"]({save_as?: \"text\" | \"json\" | \"binary\", url: string}): string - Scrape content from URL";
"oneOf const param (schemars), no output schema" "oneOf const param (schemars), no output schema"
)] )]
#[test_case(
"kiwitravel__search-flight",
serde_json::json!({"type": "object", "properties": {
"flyFrom": {"type": "string"},
"flyTo": {"type": "string"},
"departureDate": {"type": "string"}
}, "required": ["flyFrom", "flyTo", "departureDate"]}),
None,
"kiwitravel[\"search-flight\"]({departureDate: string, flyFrom: string, flyTo: string}): string - Search for flights";
"hyphenated tool name uses bracket notation"
)]
fn test_mcp_tool_signature( fn test_mcp_tool_signature(
name: &str, name: &str,
input: serde_json::Value, input: serde_json::Value,
@@ -1194,4 +1219,53 @@ mod tests {
fn test_tool_result(code: &str, tools: &[(&str, &str)], expected: &str) { fn test_tool_result(code: &str, tools: &[(&str, &str)], expected: &str) {
assert_eq!(eval_with_tools(code, tools), expected); assert_eq!(eval_with_tools(code, tools), expected);
} }
#[test]
fn test_namespace_import_with_synthetic_module() {
let tools = vec![ToolInfo {
server_name: "testserver".to_string(),
tool_name: "get_value".to_string(),
full_name: "testserver__get_value".to_string(),
description: "Get a value".to_string(),
params: vec![],
return_type: "string".to_string(),
}];
let (tx, _rx) = mpsc::unbounded_channel();
let code_named = r#"import { get_value } from "testserver"; typeof get_value"#;
let result = run_js_module(code_named, &tools, tx.clone());
assert!(
result.is_ok(),
"Named import should work: {:?}",
result.err()
);
let code_namespace =
r#"import * as testserver from "testserver"; typeof testserver.get_value"#;
let result = run_js_module(code_namespace, &tools, tx.clone());
assert!(
result.is_ok(),
"Namespace import should work: {:?}",
result.err()
);
let code_server_named =
r#"import { testserver } from "testserver"; typeof testserver.get_value"#;
let result = run_js_module(code_server_named, &tools, tx.clone());
assert!(
result.is_ok(),
"Server-named import should work: {:?}",
result.err()
);
let code_bracket =
r#"import { testserver } from "testserver"; typeof testserver["get_value"]"#;
let result = run_js_module(code_bracket, &tools, tx);
assert!(
result.is_ok(),
"Bracket notation should work: {:?}",
result.err()
);
}
} }
+1 -1
View File
@@ -114,7 +114,7 @@ async fn test_acp_with_builtin_and_mcp() {
include_str!("./test_data/openai_builtin_read_modules.txt"), include_str!("./test_data/openai_builtin_read_modules.txt"),
), ),
( (
r#"get_code({ }): string - Get the code"#.into(), r#"lookup[\"get_code\"]({}): string - Get the code"#.into(),
include_str!("./test_data/openai_builtin_execute.txt"), include_str!("./test_data/openai_builtin_execute.txt"),
), ),
( (