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.

See also