ES module imports

Recipes for enabling external ES module imports and restricting which modules are importable.

Enable external module imports

Pass --allow-external-modules when starting the server. Without this flag every import statement that resolves to an external URL is rejected immediately at the resolve step — no network traffic is attempted.

mcp-v8 --http-port=3000 --allow-external-modules

The server logs External module imports: ENABLED on startup.

Use the supported specifier forms

Form Example Resolves to
npm:<pkg> import _ from "npm:lodash-es" https://esm.sh/lodash-es
jsr:<scope>/<pkg> import { camelCase } from "jsr:@luca/cases" https://esm.sh/jsr/@luca/cases
Full URL import { format } from "https://esm.sh/date-fns" used as-is

Relative specifiers (./utils.js) are resolved against the referring module's URL using standard URL resolution.

Restrict importable modules with a policy

Use a modules OPA/Rego policy to allowlist specific packages, hosts, or URL patterns. The policy is evaluated at load time; if it returns false the fetch is blocked and execution fails with a policy-denial error.

Step 1 — Write the Rego policy

Create policies/modules.rego (the example in the repo is a good starting point):

package mcp.modules

default allow = false

allow if {
    input.specifier_type == "npm"
    input.url_parsed.host == "esm.sh"
    startswith(input.url_parsed.path, "/lodash-es")
}

Step 2 — Load the policy inline

Pass the policy file path or inline JSON to --policies-json:

mcp-v8 --http-port=3000 \
  --allow-external-modules \
  --policies-json '{"modules":{"policies":[{"url":"file:///path/to/policies/modules.rego"}]}}'

The server logs how many policies were loaded for the modules chain.

Step 3 — Verify the policy is enforced

An import not matched by the policy returns:

Module import denied by policy: 'https://esm.sh/axios' is not allowed by the module policy

Use the docker-compose.module-policy.yml pattern

The repo ships docker-compose.module-policy.yml, which runs an OPA server alongside two mcp-v8 instances — one with modules disabled (the default) and one with --allow-external-modules and a remote OPA policy:

docker compose -f docker-compose.module-policy.yml up
  • mcp-default (port 3001) — modules disabled; demonstrates the default posture.
  • mcp-opa-policy (port 3002) — modules enabled, all imports gated by the OPA server.

The mcp-opa-policy service is configured with:

--allow-external-modules
--policies-json={"modules":{"policies":[{"url":"http://opa:8181","policy_path":"mcp/modules"}]}}

This points the modules policy chain at the OPA server's mcp/modules decision endpoint (data.mcp.modules.allow). The OPA server is loaded with the files in policies/ at startup.

To test the allowlisted packages against the OPA-gated instance:

mcp-v8-cli --url http://localhost:3002 run_js \
  --code 'import { v4 } from "npm:uuid"; console.log(v4());'

To confirm a blocked package is denied:

mcp-v8-cli --url http://localhost:3002 run_js \
  --code 'import axios from "npm:axios"; console.log("ok");'
# => Module import denied by policy

Use a remote OPA server independently

mcp-v8 --http-port=3000 \
  --allow-external-modules \
  --policies-json '{"modules":{"policies":[{"url":"http://opa:8181","policy_path":"mcp/modules"}]}}'

The url entry delegates every module-allow decision to the remote OPA server. You can combine a local Rego file with a remote OPA entry in the same policies array; both must allow for the import to proceed.

See also