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

Node-dependent npm packages

For esm.sh's canonical https://esm.sh/node/<builtin>.mjs imports, the runtime serves its embedded node: compatibility module after module-policy evaluation. This avoids incomplete CDN browser shims. It does not provide a full Node.js installation; unsupported built-ins and native addons still need another runtime. Version-pinned pngjs supports synchronous and asynchronous PNG round trips:

const { PNG } = await import("npm:pngjs@7.0.0");
const pixels = new Uint8Array([255, 0, 0, 255]);
const bytes = PNG.sync.write({ width: 1, height: 1, data: pixels });
const image = PNG.sync.read(bytes);
artifact("red-pixel", "image/png", bytes);

The node:zlib implementation supports sync gzip/deflate and the legacy Deflate/Inflate surface used by this package. Advanced tuning options, full stream backpressure, and undocumented Node internals are not fully implemented. The regression tests exercise both the pinned offline package and its live CDN build (cargo test --test pngjs -- --ignored).