Security policies (OPA/Rego)¶
Policies are written in Rego and are evaluated before each capability call. This guide shows focused recipes for common gating tasks. See Concepts: Security policies for the evaluation model and the input documents each capability receives.
Gate network access by host and method¶
Write a fetch.rego that restricts to GET-only on a specific host:
package mcp.fetch
default allow = false
allow if {
input.method == "GET"
input.url_parsed.host == "api.example.com"
}
Enable it:
mcp-v8 --http-port 3000 \
--policies-json '{"fetch":{"policies":[{"url":"file:///etc/mcp/fetch.rego"}]}}'
To allow a set of hosts, use a Rego set:
package mcp.fetch
default allow = false
allowed_hosts := {"api.example.com", "cdn.example.com"}
allow if {
allowed_hosts[input.url_parsed.host]
input.method == "GET"
}
To allow wildcard subdomains (e.g., *.example.com):
allow if {
endswith(input.url_parsed.host, ".example.com")
}
Gate filesystem access by path prefix¶
Write a filesystem.rego that restricts reads and writes to a specific directory:
package mcp.filesystem
default allow = false
# Operations with no destination (readFile, writeFile, mkdir, etc.)
allow if {
startswith(input.path, "/data/workspace/")
not input.destination
}
# For operations with a destination (rename, copyFile), check both paths.
allow if {
startswith(input.path, "/data/workspace/")
startswith(input.destination, "/data/workspace/")
}
Enable it:
mcp-v8 --http-port 3000 \
--policies-json '{"filesystem":{"policies":[{"url":"file:///etc/mcp/filesystem.rego"}]}}'
The input.operation field carries the exact operation name (readFile, writeFile, mkdir, rm, rename, copyFile, appendFile, stat, exists, readdir). Gate specific operations by checking it:
# Allow reads everywhere but restrict writes.
allow if {
input.operation == "readFile"
startswith(input.path, "/data/")
}
allow if {
input.operation == "writeFile"
startswith(input.path, "/data/workspace/")
}
Gate subprocess execution by command¶
Write a subprocess.rego that allows only specific commands:
package mcp.subprocess
default allow = false
# Deno.Command: allow echo and cat.
allowed_commands := {"echo", "cat"}
allow if {
input.operation == "command_output"
allowed_commands[input.command]
}
# child_process.exec: allow commands starting with "echo" or "cat".
allowed_exec_patterns := {"echo", "cat"}
allow if {
input.operation == "exec"
some pattern in allowed_exec_patterns
startswith(input.args[1], pattern)
}
Enable it:
mcp-v8 --http-port 3000 \
--policies-json '{"subprocess":{"policies":[{"url":"file:///etc/mcp/subprocess.rego"}]}}'
The input.operation field is "command_output" for Deno.Command, and "exec" for child_process.exec. For exec, input.args[1] is the shell command string.
To allow all subprocess calls only within a specific working directory:
allow if {
input.cwd != null
startswith(input.cwd, "/tmp/sandbox/")
}
Gate ES module imports¶
Enable external module imports and gate them by package:
package mcp.modules
default allow = false
# Allow specific npm packages via esm.sh.
allowed_npm := {"lodash-es", "uuid"}
allow if {
input.specifier_type == "npm"
input.url_parsed.host == "esm.sh"
some pkg in allowed_npm
startswith(input.url_parsed.path, sprintf("/%s", [pkg]))
}
# Allow any URL from trusted CDN hosts.
trusted_hosts := {"cdn.jsdelivr.net", "unpkg.com"}
allow if {
input.specifier_type == "url"
trusted_hosts[input.url_parsed.host]
}
Enable both flags — --allow-external-modules unlocks the import mechanism, and the modules policy gates individual imports:
mcp-v8 --http-port 3000 \
--allow-external-modules \
--policies-json '{"modules":{"policies":[{"url":"file:///etc/mcp/modules.rego"}]}}'
The input.specifier_type is "npm" for npm: imports resolved via esm.sh, "jsr" for jsr: imports, and "url" for direct URL imports.
Gate upstream MCP tool calls¶
Write a mcp_tools.rego that allows specific server/tool combinations:
package mcp.tools
default allow = false
# Allow all tools on the "math" server.
allow if {
input.server == "math"
}
# Allow only the "query" tool on the "db" server.
allow if {
input.server == "db"
input.tool == "query"
}
Enable it (requires --mcp-server to be configured first):
mcp-v8 --http-port 3000 \
--mcp-server math=stdio:math-server \
--mcp-server db=stdio:db-server \
--policies-json '{"mcp_tools":{"policies":[{"url":"file:///etc/mcp/mcp_tools.rego"}]}}'
The input.arguments field contains the tool call arguments as a JSON object (or null if no arguments were provided). Inspect argument values to enforce fine-grained access:
allow if {
input.server == "db"
input.tool == "query"
input.arguments.database == "readonly_db"
}
Use a remote OPA server instead of a local Rego file¶
Point the policy URL at a running OPA server using http:// or https://:
{
"fetch": {
"policies": [{
"url": "http://opa.internal:8181",
"policy_path": "mcp/fetch"
}]
}
}
The server posts the input document to POST /v1/data/{policy_path} and reads result.allow. The default policy_path is mcp/<category> (for example mcp/fetch). Override it with "policy_path" when your OPA policy lives at a different path.
The OPA server must expose the standard OPA REST API. Start one with:
opa run --server --addr :8181 fetch.rego
Load a Rego file via file URL¶
Use a file:// URL with an absolute path:
{
"fetch": {
"policies": [{"url": "file:///etc/mcp/fetch.rego"}]
}
}
Override the default rule name with "rule" if your package path differs:
{
"fetch": {
"policies": [{
"url": "file:///etc/mcp/fetch.rego",
"rule": "data.custom.pkg.allow"
}]
}
}
Load a directory of Rego files¶
Point the file:// URL at a directory and all .rego files in it are loaded into one regorus engine instance. Files are loaded in alphabetical order:
{
"fetch": {
"policies": [{"url": "file:///etc/mcp/policies/"}]
}
}
This lets you split a policy into multiple files (e.g., a base file with a default deny and one file per allowed domain).
Chain multiple policies with any mode¶
By default, all policies in the list must allow (mode: "all"). Use mode: "any" to allow the call if at least one policy permits it:
{
"fetch": {
"mode": "any",
"policies": [
{"url": "file:///etc/mcp/fetch-org.rego"},
{"url": "http://opa.internal:8181", "policy_path": "mcp/fetch"}
]
}
}
Mix local and remote evaluators in the same chain. For example, use a fast local allowlist as the first entry and a remote OPA as the authoritative fallback.