use std::path::Path;
use apcore::Executor;
use apcore_toolkit::ScannedModule;
use apexe::mcp::McpServerBuilder;
use apexe::module::{build_executor, ExecutorOptions};
use apexe::output::YamlOutput;
use serde_json::json;
use std::sync::Arc;
use tempfile::TempDir;
fn write_echo_binding(dir: &Path) {
let module = ScannedModule::new(
"cli.echo".to_string(),
"Echo a message".to_string(),
json!({
"type": "object",
"properties": { "message": { "type": "string" } },
"additionalProperties": false
}),
json!({ "type": "object" }),
vec!["cli".to_string()],
"exec:///bin/echo".to_string(),
);
YamlOutput::without_verification()
.write(&[module], dir, false)
.expect("binding should be written");
}
fn exec_opts(dir: &Path) -> ExecutorOptions<'_> {
ExecutorOptions {
modules_dir: Some(dir),
timeout_ms: 5_000,
acl_path: None,
audit_path: None,
enable_logging: false,
enable_approval: false,
enable_circuit_breaker: false,
enable_retry: false,
approval_store: None,
}
}
fn build_test_executor(dir: &Path) -> Arc<Executor> {
build_executor(&exec_opts(dir)).expect("executor should build from bindings")
}
#[test]
fn test_mcp_server_builds_and_exposes_scanned_tools() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
McpServerBuilder::new()
.modules_dir(dir.path())
.build()
.expect("MCP server should build from bindings");
let tools = McpServerBuilder::new()
.modules_dir(dir.path())
.export_openai_tools()
.expect("tool export should succeed");
let blob = serde_json::to_string(&tools).unwrap();
assert!(
blob.contains("echo"),
"scanned echo module should be exposed as a tool: {blob}"
);
}
#[tokio::test]
async fn test_mcp_tools_call_executes() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
let executor = build_test_executor(dir.path());
let result = executor
.call(
"cli.echo",
json!({ "message": "hello_from_mcp" }),
None,
None,
)
.await
.expect("tools/call should execute");
let stdout = result["stdout"].as_str().unwrap_or_default();
assert!(
stdout.contains("hello_from_mcp"),
"echo should emit the message on stdout: {result}"
);
assert_eq!(result["exit_code"], 0);
}
#[tokio::test]
async fn test_mcp_tools_call_injection_blocked() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
let executor = build_test_executor(dir.path());
let result = executor
.call("cli.echo", json!({ "message": "ok; rm -rf /" }), None, None)
.await;
assert!(
result.is_err(),
"shell metacharacters must be rejected, got: {result:?}"
);
}
#[tokio::test]
async fn test_mcp_tools_call_unknown_module_errors() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
let executor = build_test_executor(dir.path());
let result = executor
.call("cli.nonexistent", json!({}), None, None)
.await;
assert!(result.is_err(), "unknown module should error");
}