use mcp_execution_codegen::progressive::ProgressiveGenerator;
use mcp_execution_core::{ServerId, ToolName};
use mcp_execution_introspector::{ServerCapabilities, ServerInfo, ToolInfo};
use serde_json::json;
use std::process::Command;
fn create_test_server_info() -> ServerInfo {
ServerInfo {
id: ServerId::new("github").unwrap(),
name: "GitHub".to_string(),
version: "1.0.0".to_string(),
tools: vec![
ToolInfo {
name: ToolName::new("create_issue").unwrap(),
description: "Creates a new issue".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"repo": {
"type": "string",
"description": "Repository name"
},
"title": {
"type": "string",
"description": "Issue title"
},
"body": {
"type": "string",
"description": "Issue body"
}
},
"required": ["repo", "title"]
}),
output_schema: None,
},
ToolInfo {
name: ToolName::new("update_issue").unwrap(),
description: "Updates an existing issue".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"repo": {
"type": "string"
},
"issue_number": {
"type": "number"
},
"title": {
"type": "string"
}
},
"required": ["repo", "issue_number"]
}),
output_schema: None,
},
ToolInfo {
name: ToolName::new("get_issue").unwrap(),
description: "Gets issue information".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"repo": {
"type": "string"
},
"issue_number": {
"type": "number"
}
},
"required": ["repo", "issue_number"]
}),
output_schema: None,
},
],
capabilities: ServerCapabilities {
supports_tools: true,
supports_resources: false,
supports_prompts: false,
},
}
}
#[test]
fn test_progressive_generator_creates_correct_number_of_files() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
assert_eq!(code.file_count(), 8);
}
#[test]
fn test_progressive_tool_files_exist() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let file_paths: Vec<_> = code.files.iter().map(|f| f.path.as_str()).collect();
assert!(
file_paths.contains(&"createIssue.ts"),
"Missing createIssue.ts"
);
assert!(
file_paths.contains(&"updateIssue.ts"),
"Missing updateIssue.ts"
);
assert!(file_paths.contains(&"getIssue.ts"), "Missing getIssue.ts");
assert!(file_paths.contains(&"index.ts"), "Missing index.ts");
assert!(
file_paths.contains(&"_runtime/mcp-bridge.ts"),
"Missing _runtime/mcp-bridge.ts"
);
assert!(
file_paths.contains(&"tsconfig.json"),
"Missing tsconfig.json"
);
}
#[test]
fn test_progressive_tool_file_structure() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let create_issue_file = code
.files
.iter()
.find(|f| f.path == "createIssue.ts")
.expect("createIssue.ts not found");
let content = &create_issue_file.content;
assert!(
content.contains("export async function createIssue"),
"Missing function export"
);
assert!(
content.contains("export type createIssueParams = {"),
"Missing Params type alias"
);
assert!(
content.contains(
"export type createIssueResult = Record<string, unknown> | unknown[] | string;"
),
"Missing widened Result type union"
);
assert!(content.contains("callMCPTool"), "Missing callMCPTool call");
assert!(
content.contains(") as createIssueResult;"),
"Missing cast of callMCPTool's return value to createIssueResult"
);
assert!(
content.contains("'github'"),
"Missing server_id in callMCPTool"
);
assert!(
content.contains("'create_issue'"),
"Missing tool name in callMCPTool"
);
}
#[test]
fn test_progressive_tool_file_has_proper_types() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let create_issue_file = code
.files
.iter()
.find(|f| f.path == "createIssue.ts")
.expect("createIssue.ts not found");
let content = &create_issue_file.content;
assert!(
content.contains("repo: string;"),
"Missing required repo field"
);
assert!(
content.contains("title: string;"),
"Missing required title field"
);
assert!(
content.contains("body?: string;"),
"Missing optional body field"
);
}
#[test]
fn test_progressive_index_structure() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let index_file = code
.files
.iter()
.find(|f| f.path == "index.ts")
.expect("index.ts not found");
let content = &index_file.content;
assert!(
content.contains("export { createIssue"),
"Missing createIssue export"
);
assert!(
content.contains("export { updateIssue"),
"Missing updateIssue export"
);
assert!(
content.contains("export { getIssue"),
"Missing getIssue export"
);
assert!(
content.contains("createIssueParams"),
"Missing Params type export"
);
assert!(
content.contains("createIssueResult"),
"Missing Result type export"
);
assert!(
content.contains("3 tools"),
"Missing tool count in documentation"
);
assert!(
content.contains("export { callMCPTool }"),
"Missing callMCPTool export"
);
}
#[test]
fn test_progressive_index_doc_comment_not_prematurely_closed() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let index_file = code
.files
.iter()
.find(|f| f.path == "index.ts")
.expect("index.ts not found");
let content = &index_file.content;
let doc_start = content.find("/**").expect("Missing opening JSDoc block");
let doc_end = content[doc_start..]
.find("*/")
.map(|i| doc_start + i)
.expect("Missing JSDoc close");
assert!(
content[doc_start..doc_end].contains("@packageDocumentation"),
"Top-level JSDoc block closed prematurely before @packageDocumentation \
— likely a nested /* ... */ inside the doc comment (regression for #139)"
);
}
#[test]
fn test_progressive_index_uses_ts_specifiers_not_js() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let index_file = code
.files
.iter()
.find(|f| f.path == "index.ts")
.expect("index.ts not found");
let content = &index_file.content;
assert!(
content.contains("from './createIssue.ts';"),
"tool re-export must use a .ts specifier: {content}"
);
assert!(
content.contains("from './_runtime/mcp-bridge.ts';"),
"runtime bridge re-export must use a .ts specifier: {content}"
);
assert!(
!content.contains(".js';") && !content.contains(".js\";"),
"index.ts must not import/export any sibling file with a .js specifier — the files on \
disk are always .ts, and a .js specifier only resolves under tsc's type-checking, not \
Node's real ESM resolution: {content}"
);
}
#[test]
fn test_progressive_runtime_bridge_structure() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge_file = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let content = &bridge_file.content;
assert!(
content.contains("export async function callMCPTool"),
"Missing callMCPTool export"
);
assert!(
content.contains("serverId: string"),
"Missing serverId parameter"
);
assert!(
content.contains("toolName: string"),
"Missing toolName parameter"
);
assert!(
content.contains("params: Record<string, unknown>"),
"Missing params parameter"
);
assert!(
content.contains("@param serverId"),
"Missing serverId JSDoc"
);
assert!(
content.contains("@param toolName"),
"Missing toolName JSDoc"
);
assert!(content.contains("@param params"), "Missing params JSDoc");
}
#[test]
fn test_progressive_generator_with_empty_server() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = ServerInfo {
id: ServerId::new("empty").unwrap(),
name: "Empty Server".to_string(),
version: "1.0.0".to_string(),
tools: vec![],
capabilities: ServerCapabilities {
supports_tools: true,
supports_resources: false,
supports_prompts: false,
},
};
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
assert_eq!(code.file_count(), 5);
let file_paths: Vec<_> = code.files.iter().map(|f| f.path.as_str()).collect();
assert!(file_paths.contains(&"index.ts"));
assert!(file_paths.contains(&"_runtime/mcp-bridge.ts"));
assert!(file_paths.contains(&"package.json"));
assert!(file_paths.contains(&"tsconfig.json"));
}
#[test]
fn test_progressive_tool_camel_case_conversion() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = ServerInfo {
id: ServerId::new("test").unwrap(),
name: "Test".to_string(),
version: "1.0.0".to_string(),
tools: vec![ToolInfo {
name: ToolName::new("send_test_message").unwrap(),
description: "Test tool".to_string(),
input_schema: json!({
"type": "object",
"properties": {},
}),
output_schema: None,
}],
capabilities: ServerCapabilities {
supports_tools: true,
supports_resources: false,
supports_prompts: false,
},
};
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
assert!(
code.files
.iter()
.any(|f| f.path.as_str() == "sendTestMessage.ts")
);
let tool_file = code
.files
.iter()
.find(|f| f.path == "sendTestMessage.ts")
.expect("sendTestMessage.ts not found");
assert!(tool_file.content.contains("function sendTestMessage"));
}
#[test]
fn test_progressive_tool_with_complex_types() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = ServerInfo {
id: ServerId::new("test").unwrap(),
name: "Test".to_string(),
version: "1.0.0".to_string(),
tools: vec![ToolInfo {
name: ToolName::new("complex_tool").unwrap(),
description: "Tool with complex types".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {"type": "string"}
},
"config": {
"type": "object"
},
"count": {
"type": "number"
},
"enabled": {
"type": "boolean"
}
},
"required": ["items"]
}),
output_schema: None,
}],
capabilities: ServerCapabilities {
supports_tools: true,
supports_resources: false,
supports_prompts: false,
},
};
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let tool_file = code
.files
.iter()
.find(|f| f.path == "complexTool.ts")
.expect("complexTool.ts not found");
let content = &tool_file.content;
assert!(content.contains("items: string[]"), "Missing array type");
assert!(
content.contains("config?: Record<string, unknown>"),
"Missing object type"
);
assert!(content.contains("count?: number"), "Missing number type");
assert!(
content.contains("enabled?: boolean"),
"Missing boolean type"
);
}
const fn tsc_program() -> &'static str {
if cfg!(windows) { "tsc.cmd" } else { "tsc" }
}
fn require_ts_toolchain(test_name: &str, require_node: bool) -> bool {
let tsc_missing = Command::new(tsc_program())
.arg("--version")
.output()
.is_err();
let node_missing = require_node && Command::new("node").arg("--version").output().is_err();
if !tsc_missing && !node_missing {
return true;
}
let missing = match (tsc_missing, node_missing) {
(true, true) => "`tsc` and `node`",
(true, false) => "`tsc`",
(false, true) => "`node`",
(false, false) => unreachable!("checked above"),
};
assert!(
std::env::var_os("CI").is_none(),
"{test_name}: {missing} not found on PATH in CI — this test exists to catch \
TypeScript-level regressions and must not silently skip in CI. Install Node.js and \
run `npm install -g typescript` in this CI job."
);
eprintln!("skipping {test_name}: {missing} not found on PATH");
false
}
const fn npm_program() -> &'static str {
if cfg!(windows) { "npm.cmd" } else { "npm" }
}
fn install_declared_dev_dependencies(dir: &std::path::Path, test_name: &str) -> bool {
if Command::new(npm_program())
.arg("--version")
.output()
.is_err()
{
assert!(
std::env::var_os("CI").is_none(),
"{test_name}: npm not found on PATH in CI — cannot install the generated \
package.json's devDependencies, so this test cannot catch a missing @types/node \
regression."
);
eprintln!("skipping {test_name}: npm not found on PATH");
return false;
}
let output = Command::new(npm_program())
.args([
"install",
"--no-save",
"--no-audit",
"--no-fund",
"--include=dev",
])
.current_dir(dir)
.output()
.expect("Failed to run npm install");
if !output.status.success() {
assert!(
std::env::var_os("CI").is_none(),
"{test_name}: npm install failed in CI:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
eprintln!(
"skipping {test_name}: npm install failed (no network access?):\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
return false;
}
true
}
#[test]
fn test_generated_tool_passes_tsc_noemit() {
let test_name = "test_generated_tool_passes_tsc_noemit";
if !require_ts_toolchain(test_name, true) {
return;
}
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let dir = tempfile::tempdir().expect("Failed to create temp dir");
for file in &code.files {
let path = dir.path().join(&file.path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("Failed to create parent dir for file");
}
std::fs::write(&path, &file.content).expect("Failed to write generated file");
}
if !install_declared_dev_dependencies(dir.path(), test_name) {
return;
}
let output = Command::new(tsc_program())
.arg("--noEmit")
.arg("-p")
.arg(dir.path().join("tsconfig.json"))
.output()
.expect("Failed to run tsc");
assert!(
output.status.success(),
"tsc --noEmit failed on the real generated package:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn require_node_strip_types(test_name: &str) -> bool {
let supported = Command::new("node")
.args(["--experimental-strip-types", "--eval", ""])
.output()
.is_ok_and(|output| output.status.success());
if supported {
return true;
}
assert!(
std::env::var_os("CI").is_none(),
"{test_name}: `node --experimental-strip-types` not available in CI (needs Node \
22.6+) — this test exists to catch ERR_MODULE_NOT_FOUND regressions that `tsc \
--noEmit` cannot see."
);
eprintln!("skipping {test_name}: `node --experimental-strip-types` not available");
false
}
#[test]
fn test_generated_index_resolves_at_runtime_under_node_esm() {
if !require_node_strip_types("test_generated_index_resolves_at_runtime_under_node_esm") {
return;
}
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let dir = tempfile::tempdir().expect("Failed to create temp dir");
for file in &code.files {
let path = dir.path().join(&file.path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.expect("Failed to create parent dir for generated file");
}
std::fs::write(&path, &file.content).expect("Failed to write generated file");
}
let output = Command::new("node")
.arg("--experimental-strip-types")
.arg("index.ts")
.current_dir(dir.path())
.output()
.expect("Failed to run node");
assert!(
output.status.success(),
"node failed to load the generated index.ts — likely a `.js` import specifier \
pointing at a file that only exists as `.ts` on disk (regression for #256):\n\
stdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn run_bridge_harness(
test_name: &str,
bridge_ts: &str,
mcp_json: &serde_json::Value,
server_id: &str,
) -> Option<(bool, String, String)> {
run_bridge_harness_with_env(test_name, bridge_ts, mcp_json, server_id, &[])
}
fn run_bridge_harness_with_env(
test_name: &str,
bridge_ts: &str,
mcp_json: &serde_json::Value,
server_id: &str,
extra_env: &[(&str, &str)],
) -> Option<(bool, String, String)> {
let harness_ts = format!(
"import {{ callMCPTool }} from './mcp-bridge.js';\n\
\n\
const watchdog = setTimeout(() => {{\n\
\x20\x20console.log('TIMEOUT: did not settle before spawning');\n\
\x20\x20process.exit(2);\n\
}}, 5000);\n\
\n\
callMCPTool('{server_id}', 'noop', {{}}).then(\n\
\x20\x20() => {{\n\
\x20\x20\x20\x20clearTimeout(watchdog);\n\
\x20\x20\x20\x20console.log('UNEXPECTED_SUCCESS');\n\
\x20\x20\x20\x20process.exit(1);\n\
\x20\x20}},\n\
\x20\x20(err: unknown) => {{\n\
\x20\x20\x20\x20clearTimeout(watchdog);\n\
\x20\x20\x20\x20console.log('REJECTED:', String(err));\n\
\x20\x20\x20\x20process.exit(0);\n\
\x20\x20}}\n\
);\n"
);
compile_and_run_bridge_harness(test_name, bridge_ts, mcp_json, &harness_ts, extra_env)
}
fn compile_and_run_bridge_harness(
test_name: &str,
bridge_ts: &str,
mcp_json: &serde_json::Value,
harness_ts: &str,
extra_env: &[(&str, &str)],
) -> Option<(bool, String, String)> {
if !require_ts_toolchain(test_name, true) {
return None;
}
let dir = tempfile::tempdir().expect("Failed to create temp dir");
let home_dir = dir.path().join("home");
let claude_dir = home_dir.join(".claude");
std::fs::create_dir_all(&claude_dir).expect("Failed to create fake $HOME/.claude");
std::fs::write(claude_dir.join("mcp.json"), mcp_json.to_string())
.expect("Failed to write mcp.json");
let src_dir = dir.path().join("src");
std::fs::create_dir_all(&src_dir).expect("Failed to create src dir");
std::fs::write(src_dir.join("package.json"), "{\"type\":\"module\"}\n")
.expect("Failed to write src/package.json");
std::fs::write(src_dir.join("mcp-bridge.ts"), bridge_ts)
.expect("Failed to write mcp-bridge.ts");
std::fs::write(src_dir.join("harness.ts"), harness_ts).expect("Failed to write harness.ts");
let dist_dir = dir.path().join("dist");
let tsc_output = Command::new(tsc_program())
.args(["--noCheck", "--module", "NodeNext", "--moduleResolution"])
.arg("NodeNext")
.arg("--target")
.arg("ES2022")
.arg("--outDir")
.arg(&dist_dir)
.arg(src_dir.join("mcp-bridge.ts"))
.arg(src_dir.join("harness.ts"))
.output()
.expect("Failed to run tsc");
assert!(
tsc_output.status.success(),
"tsc failed to compile the rendered runtime bridge:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&tsc_output.stdout),
String::from_utf8_lossy(&tsc_output.stderr)
);
std::fs::write(dist_dir.join("package.json"), "{\"type\":\"module\"}\n")
.expect("Failed to write dist/package.json");
let harness_path = dist_dir.join("harness.js");
let owned_extra_env: Vec<(String, String)> = extra_env
.iter()
.map(|&(k, v)| (k.to_string(), v.to_string()))
.collect();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut cmd = Command::new("node");
cmd.arg(&harness_path)
.env("HOME", &home_dir)
.env("USERPROFILE", &home_dir);
for (key, value) in &owned_extra_env {
cmd.env(key, value);
}
let result = cmd.output();
let _ = tx.send(result);
});
let output = rx
.recv_timeout(std::time::Duration::from_secs(15))
.unwrap_or_else(|_| {
panic!(
"node harness for {test_name} did not exit within 15s; the bridge may be \
hanging instead of settling every pending request"
)
})
.expect("Failed to run node");
Some((
output.status.success(),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
))
}
#[test]
fn test_runtime_bridge_rejects_forbidden_env_var_before_spawn() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let mcp_json = json!({
"mcpServers": {
"github": {
"command": "node",
"args": ["--version"],
"env": { "LD_PRELOAD": "/tmp/evil.so" }
}
}
});
let Some((success, stdout, stderr)) = run_bridge_harness(
"test_runtime_bridge_rejects_forbidden_env_var_before_spawn",
&bridge.content,
&mcp_json,
"github",
) else {
return;
};
assert!(
success,
"bridge did not reject the hostile LD_PRELOAD config before spawning:\n\
stdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("REJECTED:"),
"expected the bridge to reject the config: stdout: {stdout}, stderr: {stderr}"
);
assert!(
stdout.contains("LD_PRELOAD"),
"rejection reason should name the forbidden env var: {stdout}"
);
}
#[test]
fn test_runtime_bridge_rejects_http_transport_with_clear_error_not_crash() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let mcp_json = json!({
"mcpServers": {
"github": {
"transport": "http",
"url": "https://api.example.com/mcp"
}
}
});
let Some((success, stdout, stderr)) = run_bridge_harness(
"test_runtime_bridge_rejects_http_transport_with_clear_error_not_crash",
&bridge.content,
&mcp_json,
"github",
) else {
return;
};
assert!(
success,
"bridge did not reject the http-transport config cleanly:\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("REJECTED:"),
"expected the bridge to reject the config: stdout: {stdout}, stderr: {stderr}"
);
assert!(
!stdout.contains("Cannot read properties of undefined"),
"must fail with a clear, intentional error, not the pre-fix opaque TypeError: {stdout}"
);
assert!(
stdout.contains("http") || stdout.contains("transport"),
"rejection reason should mention the unsupported transport: {stdout}"
);
}
#[test]
fn test_runtime_bridge_rejects_http_transport_bad_url_scheme() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
for url in ["file:///etc/passwd", "ftp://host/path"] {
let mcp_json = json!({
"mcpServers": {
"github": {
"transport": "http",
"url": url
}
}
});
let Some((success, stdout, stderr)) = run_bridge_harness(
"test_runtime_bridge_rejects_http_transport_bad_url_scheme",
&bridge.content,
&mcp_json,
"github",
) else {
return;
};
assert!(
success,
"bridge did not reject url {url}:\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("http:// or https://"),
"rejection reason should name the required scheme for url {url}: {stdout}"
);
}
}
#[test]
fn test_runtime_bridge_rejects_http_transport_missing_url() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let mcp_json = json!({
"mcpServers": {
"github": {
"transport": "http"
}
}
});
let Some((success, stdout, stderr)) = run_bridge_harness(
"test_runtime_bridge_rejects_http_transport_missing_url",
&bridge.content,
&mcp_json,
"github",
) else {
return;
};
assert!(
success,
"bridge did not reject missing url cleanly:\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(
!stdout.contains("Cannot read properties of undefined"),
"must fail with a clear, intentional error, not an opaque TypeError: {stdout}"
);
assert!(
stdout.contains("url is required"),
"rejection reason should mention the missing url: {stdout}"
);
}
#[test]
fn test_runtime_bridge_rejects_http_transport_unsafe_headers() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let mcp_json = json!({
"mcpServers": {
"github": {
"transport": "http",
"url": "https://api.example.com/mcp",
"headers": { "X Bad Header": "value" }
}
}
});
let Some((success, stdout, stderr)) = run_bridge_harness(
"test_runtime_bridge_rejects_http_transport_unsafe_headers",
&bridge.content,
&mcp_json,
"github",
) else {
return;
};
assert!(
success,
"bridge did not reject the unsafe header name cleanly:\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("header name"),
"rejection reason should mention the header name: {stdout}"
);
}
#[test]
fn test_runtime_bridge_rejects_http_transport_control_char_in_header_value() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let mcp_json = json!({
"mcpServers": {
"github": {
"transport": "http",
"url": "https://api.example.com/mcp",
"headers": { "Authorization": "Bearer sekrit\r\nX-Injected: evil" }
}
}
});
let Some((success, stdout, stderr)) = run_bridge_harness(
"test_runtime_bridge_rejects_http_transport_control_char_in_header_value",
&bridge.content,
&mcp_json,
"github",
) else {
return;
};
assert!(
success,
"bridge did not reject the unsafe header value cleanly:\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("Authorization"),
"rejection reason should name the header: {stdout}"
);
assert!(
!stdout.contains("sekrit") && !stdout.contains("X-Injected"),
"the header VALUE must never appear in the error message: {stdout}"
);
}
#[test]
fn test_runtime_bridge_rejects_http_transport_duplicate_case_insensitive_headers() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let mcp_json = json!({
"mcpServers": {
"github": {
"transport": "http",
"url": "https://api.example.com/mcp",
"headers": { "Authorization": "Bearer one", "authorization": "Bearer two" }
}
}
});
let Some((success, stdout, stderr)) = run_bridge_harness(
"test_runtime_bridge_rejects_http_transport_duplicate_case_insensitive_headers",
&bridge.content,
&mcp_json,
"github",
) else {
return;
};
assert!(
success,
"bridge did not reject the duplicate header cleanly:\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("duplicate header"),
"rejection reason should mention the duplicate header: {stdout}"
);
}
fn write_test_script(content: &str) -> String {
let dir = tempfile::tempdir().expect("Failed to create temp dir for test script");
let path = dir.path().join("script.js");
std::fs::write(&path, content).expect("Failed to write test script");
Box::leak(Box::new(dir));
path.to_string_lossy().into_owned()
}
#[test]
fn test_runtime_bridge_rejects_when_child_exits_before_responding() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let script_path = write_test_script("process.exit(1);\n");
let mcp_json = json!({
"mcpServers": {
"github": {
"command": "node",
"args": [script_path]
}
}
});
let Some((success, stdout, stderr)) = run_bridge_harness(
"test_runtime_bridge_rejects_when_child_exits_before_responding",
&bridge.content,
&mcp_json,
"github",
) else {
return;
};
assert!(
success,
"bridge did not reject the dead server cleanly (may have hung until the harness \
watchdog fired):\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("REJECTED:"),
"expected the bridge to reject the call: stdout: {stdout}, stderr: {stderr}"
);
assert!(
stdout.contains("exited before responding"),
"rejection reason should explain the process died before replying: {stdout}"
);
}
#[test]
fn test_runtime_bridge_times_out_when_server_never_replies() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let script_path = write_test_script("setInterval(function keepAlive() {}, 1000);\n");
let mcp_json = json!({
"mcpServers": {
"github": {
"command": "node",
"args": [script_path]
}
}
});
let Some((success, stdout, stderr)) = run_bridge_harness_with_env(
"test_runtime_bridge_times_out_when_server_never_replies",
&bridge.content,
&mcp_json,
"github",
&[("MCPBRIDGE_REQUEST_TIMEOUT_MS", "200")],
) else {
return;
};
assert!(
success,
"bridge did not time out cleanly (may have hung until the harness watchdog \
fired):\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("REJECTED:"),
"expected the bridge to reject the call: stdout: {stdout}, stderr: {stderr}"
);
assert!(
stdout.contains("Timed out"),
"rejection reason should explain the request timed out: {stdout}"
);
}
#[test]
fn test_runtime_bridge_dispatches_concurrent_out_of_order_responses_by_request_id() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let fake_server_js = r"
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin, terminal: false });
const pendingToolCalls = [];
rl.on('line', (line) => {
const trimmed = line.trim();
if (trimmed.length === 0) return;
const message = JSON.parse(trimmed);
if (message.method === 'initialize') {
const response = {
jsonrpc: '2.0',
id: message.id,
result: {
protocolVersion: '2024-11-05',
capabilities: {},
serverInfo: { name: 'fake', version: '0.0.0' }
}
};
process.stdout.write(JSON.stringify(response) + '\n');
return;
}
if (message.method === 'tools/call') {
pendingToolCalls.push(message);
if (pendingToolCalls.length < 2) return;
// Deliberately reply in the REVERSE of arrival order: the second request received gets
// its response written first. This is the out-of-order scenario from issue #232 — a
// correct dispatcher must still resolve each caller with the response matching ITS OWN
// request id, not whichever response happens to arrive first.
const [receivedFirst, receivedSecond] = pendingToolCalls;
for (const call of [receivedSecond, receivedFirst]) {
const response = {
jsonrpc: '2.0',
id: call.id,
result: {
content: [{ type: 'text', text: `${call.params.arguments.value}-response` }]
}
};
process.stdout.write(JSON.stringify(response) + '\n');
}
}
});
";
let script_path = write_test_script(fake_server_js);
let mcp_json = json!({
"mcpServers": {
"fake": {
"command": "node",
"args": [script_path]
}
}
});
let harness_ts = r"
import { callMCPTool } from './mcp-bridge.js';
const watchdog = setTimeout(() => {
console.log('TIMEOUT: did not settle both calls');
process.exit(2);
}, 5000);
const first = callMCPTool('fake', 'echo', { value: 'first' });
const second = callMCPTool('fake', 'echo', { value: 'second' });
Promise.all([first, second]).then(
([firstResult, secondResult]) => {
clearTimeout(watchdog);
console.log('FIRST:', JSON.stringify(firstResult));
console.log('SECOND:', JSON.stringify(secondResult));
if (firstResult === 'first-response' && secondResult === 'second-response') {
console.log('MATCH');
process.exit(0);
} else {
console.log('MISMATCH');
process.exit(1);
}
},
(err: unknown) => {
clearTimeout(watchdog);
console.log('REJECTED:', String(err));
process.exit(3);
}
);
";
let Some((success, stdout, stderr)) = compile_and_run_bridge_harness(
"test_runtime_bridge_dispatches_concurrent_out_of_order_responses_by_request_id",
&bridge.content,
&mcp_json,
harness_ts,
&[],
) else {
return;
};
assert!(
success,
"bridge did not dispatch out-of-order concurrent responses correctly:\n\
stdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("MATCH"),
"each caller must resolve with the response matching its own request id, not the \
other caller's response: stdout: {stdout}, stderr: {stderr}"
);
}
fn respond_once_fake_server_js(result_json: &str) -> String {
format!(
r"
const readline = require('readline');
const rl = readline.createInterface({{ input: process.stdin, terminal: false }});
rl.on('line', (line) => {{
const trimmed = line.trim();
if (trimmed.length === 0) return;
const message = JSON.parse(trimmed);
if (message.method === 'initialize') {{
const response = {{
jsonrpc: '2.0',
id: message.id,
result: {{
protocolVersion: '2024-11-05',
capabilities: {{}},
serverInfo: {{ name: 'fake', version: '0.0.0' }}
}}
}};
process.stdout.write(JSON.stringify(response) + '\n');
return;
}}
if (message.method === 'tools/call') {{
const response = {{
jsonrpc: '2.0',
id: message.id,
result: {result_json}
}};
process.stdout.write(JSON.stringify(response) + '\n');
}}
}});
"
)
}
const SINGLE_CALL_HARNESS_TS: &str = r"
import { callMCPTool } from './mcp-bridge.js';
callMCPTool('fake', 'noop', {}).then(
(result: unknown) => {
console.log('RESOLVED:', JSON.stringify(result));
process.exit(0);
},
(err: unknown) => {
console.log('REJECTED:', String(err));
process.exit(0);
}
);
";
#[test]
fn test_runtime_bridge_surfaces_tool_error_with_empty_content() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let script_path = write_test_script(&respond_once_fake_server_js(
r#"{ "isError": true, "content": [] }"#,
));
let mcp_json = json!({
"mcpServers": { "fake": { "command": "node", "args": [script_path] } }
});
let Some((success, stdout, stderr)) = compile_and_run_bridge_harness(
"test_runtime_bridge_surfaces_tool_error_with_empty_content",
&bridge.content,
&mcp_json,
SINGLE_CALL_HARNESS_TS,
&[],
) else {
return;
};
assert!(
success,
"harness process itself must exit 0 regardless of resolve/reject: stdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("REJECTED:") && stdout.contains("Tool returned error"),
"an empty content array on an isError response must reject with a clear \
'Tool returned error' message, not crash on an unguarded content[0] dereference: \
stdout: {stdout}, stderr: {stderr}"
);
assert!(
!stdout.contains("Cannot read properties"),
"must not crash with an unguarded property-access TypeError: stdout: {stdout}"
);
}
#[test]
fn test_runtime_bridge_returns_structured_content_when_content_empty() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let script_path = write_test_script(&respond_once_fake_server_js(
r#"{ "content": [], "structuredContent": { "answer": 42 } }"#,
));
let mcp_json = json!({
"mcpServers": { "fake": { "command": "node", "args": [script_path] } }
});
let Some((success, stdout, stderr)) = compile_and_run_bridge_harness(
"test_runtime_bridge_returns_structured_content_when_content_empty",
&bridge.content,
&mcp_json,
SINGLE_CALL_HARNESS_TS,
&[],
) else {
return;
};
assert!(
success,
"harness process itself must exit 0 regardless of resolve/reject: stdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains(r#"RESOLVED: {"answer":42}"#),
"a structuredContent-only response must resolve with structuredContent, not crash or \
resolve with something else: stdout: {stdout}, stderr: {stderr}"
);
}
#[test]
fn test_runtime_bridge_rejects_empty_content_without_structured_content() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let script_path = write_test_script(&respond_once_fake_server_js(r#"{ "content": [] }"#));
let mcp_json = json!({
"mcpServers": { "fake": { "command": "node", "args": [script_path] } }
});
let Some((success, stdout, stderr)) = compile_and_run_bridge_harness(
"test_runtime_bridge_rejects_empty_content_without_structured_content",
&bridge.content,
&mcp_json,
SINGLE_CALL_HARNESS_TS,
&[],
) else {
return;
};
assert!(
success,
"harness process itself must exit 0 regardless of resolve/reject: stdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("REJECTED:"),
"an empty content array with no structuredContent must reject with a clear error, not \
crash or silently resolve: stdout: {stdout}, stderr: {stderr}"
);
assert!(
!stdout.contains("Cannot read properties"),
"must not crash with an unguarded property-access TypeError: stdout: {stdout}"
);
}
#[test]
fn test_runtime_bridge_rejects_null_first_content_element() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let script_path = write_test_script(&respond_once_fake_server_js(r#"{ "content": [null] }"#));
let mcp_json = json!({
"mcpServers": { "fake": { "command": "node", "args": [script_path] } }
});
let Some((success, stdout, stderr)) = compile_and_run_bridge_harness(
"test_runtime_bridge_rejects_null_first_content_element",
&bridge.content,
&mcp_json,
SINGLE_CALL_HARNESS_TS,
&[],
) else {
return;
};
assert!(
success,
"harness process itself must exit 0 regardless of resolve/reject: stdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("REJECTED:"),
"a null first content element must reject with a clear error, not crash or silently \
resolve: stdout: {stdout}, stderr: {stderr}"
);
assert!(
!stdout.contains("Cannot read properties"),
"must not crash with an unguarded property-access TypeError: stdout: {stdout}"
);
}
#[test]
fn test_runtime_bridge_falls_back_to_structured_content_on_null_first_content_element() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let script_path = write_test_script(&respond_once_fake_server_js(
r#"{ "content": [null], "structuredContent": { "answer": 42 } }"#,
));
let mcp_json = json!({
"mcpServers": { "fake": { "command": "node", "args": [script_path] } }
});
let Some((success, stdout, stderr)) = compile_and_run_bridge_harness(
"test_runtime_bridge_falls_back_to_structured_content_on_null_first_content_element",
&bridge.content,
&mcp_json,
SINGLE_CALL_HARNESS_TS,
&[],
) else {
return;
};
assert!(
success,
"harness process itself must exit 0 regardless of resolve/reject: stdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains(r#"RESOLVED: {"answer":42}"#),
"a null first content element with populated structuredContent must resolve with \
structuredContent, not discard it and reject: stdout: {stdout}, stderr: {stderr}"
);
}
#[test]
fn test_runtime_bridge_treats_null_structured_content_as_absent() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let script_path = write_test_script(&respond_once_fake_server_js(
r#"{ "content": [], "structuredContent": null }"#,
));
let mcp_json = json!({
"mcpServers": { "fake": { "command": "node", "args": [script_path] } }
});
let Some((success, stdout, stderr)) = compile_and_run_bridge_harness(
"test_runtime_bridge_treats_null_structured_content_as_absent",
&bridge.content,
&mcp_json,
SINGLE_CALL_HARNESS_TS,
&[],
) else {
return;
};
assert!(
success,
"harness process itself must exit 0 regardless of resolve/reject: stdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("REJECTED:") && stdout.contains("no content and no structuredContent"),
"a literal structuredContent: null must be treated as absent and reject with the \
standard empty-content error, not resolve with null: stdout: {stdout}, stderr: {stderr}"
);
}
#[test]
fn test_runtime_bridge_surfaces_structured_content_on_tool_error() {
let generator = ProgressiveGenerator::new().expect("Failed to create generator");
let server_info = create_test_server_info();
let code = generator
.generate(&server_info)
.expect("Failed to generate code");
let bridge = code
.files
.iter()
.find(|f| f.path == "_runtime/mcp-bridge.ts")
.expect("_runtime/mcp-bridge.ts not found");
let script_path = write_test_script(&respond_once_fake_server_js(
r#"{ "isError": true, "content": [], "structuredContent": { "reason": "boom" } }"#,
));
let mcp_json = json!({
"mcpServers": { "fake": { "command": "node", "args": [script_path] } }
});
let Some((success, stdout, stderr)) = compile_and_run_bridge_harness(
"test_runtime_bridge_surfaces_structured_content_on_tool_error",
&bridge.content,
&mcp_json,
SINGLE_CALL_HARNESS_TS,
&[],
) else {
return;
};
assert!(
success,
"harness process itself must exit 0 regardless of resolve/reject: stdout: {stdout}\nstderr: {stderr}"
);
assert!(
stdout.contains("REJECTED:")
&& stdout.contains("Tool returned error")
&& stdout.contains(r#"{"reason":"boom"}"#),
"an isError response with populated structuredContent must surface it in the error \
message instead of falling back to 'Unknown error': stdout: {stdout}, stderr: {stderr}"
);
assert!(
!stdout.contains("Unknown error"),
"structuredContent detail must take priority over the generic fallback: stdout: {stdout}"
);
}