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 the reference for the full JSON schema and input documents.

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 for each category is listed in the reference. 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.

See also