Subprocess execution¶
Practical recipes for enabling subprocess support, restricting what commands can run, and working with process output.
Enable subprocess¶
Subprocess is disabled by default. To enable it, supply a subprocess key in the policies JSON passed to --policies-json. Without it, any call to Deno.Command or child_process.exec throws immediately.
Create a minimal policies.json:
{
"subprocess": {
"policies": [
{ "url": "file:///path/to/subprocess.rego" }
]
}
}
Start the server:
mcp-v8 --http-port 8080 --policies-json /path/to/policies.json
Allow only specific commands via Rego¶
Allow a fixed set of binaries (Deno.Command)¶
Deno.Command uses operation == "command_output". The input.command field holds the binary name or path exactly as passed to the constructor.
package mcp.subprocess
default allow = false
allowed_commands := {"echo", "date", "uname"}
allow if {
input.operation == "command_output"
allowed_commands[input.command]
}
Allow specific shell-command prefixes (child_process.exec)¶
child_process.exec uses operation == "exec". The command string passed to the shell is in input.args[1] (because the actual binary is /bin/sh with -c as args[0]).
package mcp.subprocess
default allow = false
allow if {
input.operation == "exec"
startswith(input.args[1], "echo ")
}
allow if {
input.operation == "exec"
startswith(input.args[1], "ls ")
}
Restrict by working directory¶
You can gate all subprocess operations on the cwd field:
package mcp.subprocess
default allow = false
allow if {
input.cwd != null
startswith(input.cwd, "/tmp/sandbox/")
}
Combine multiple rules¶
Rules compose naturally: add allow clauses for each permitted case. The policy evaluator uses data.mcp.subprocess.allow as its entrypoint.
Capture stdout¶
child_process.exec returns stdout as a UTF-8 string by default:
const { stdout } = await child_process.exec("hostname");
stdout.trim(); // "myhost"
Deno.Command.output() always returns stdout as a Uint8Array; decode it:
const cmd = new Deno.Command("hostname");
const { stdout } = await cmd.output();
new TextDecoder().decode(stdout).trim(); // "myhost"
Capture stderr¶
Both APIs expose a stderr field alongside stdout:
const { stdout, stderr, code } = await child_process.exec("ls /nonexistent");
// stderr: "ls: /nonexistent: No such file or directory\n"
// code: 1
// success: false
Check the exit code¶
Both APIs return code (integer) and success (boolean):
const { code, success } = await child_process.exec("false");
// code: 1
// success: false
code is -1 when the process was killed by a signal and no numeric exit code is available.
Get binary output¶
Set encoding: "buffer" on child_process.exec to receive stdout/stderr as Uint8Array instead of strings:
const { stdout } = await child_process.exec("cat /bin/sh", { encoding: "buffer" });
// stdout is Uint8Array containing raw bytes
Deno.Command.output() always returns Uint8Array regardless of content.
Set a working directory¶
Pass cwd in the options object:
const { stdout } = await child_process.exec("pwd", { cwd: "/tmp" });
// stdout: "/tmp\n"
const cmd = new Deno.Command("pwd", { cwd: "/var" });
const { stdout } = await cmd.output();
new TextDecoder().decode(stdout).trim(); // "/var"
Inject environment variables¶
const { stdout } = await child_process.exec("printenv MY_VAR", {
env: { MY_VAR: "hello" },
});
// stdout: "hello\n"
The environment provided replaces additional variables; the process inherits the server's environment unless you override it.
Use a remote OPA server¶
Point the policy URL at a running OPA instance:
{
"subprocess": {
"policies": [
{ "url": "http://opa.internal:8181", "policy_path": "mcp/subprocess" }
]
}
}
The server posts the input document to OPA and checks data.mcp.subprocess.allow.