mcp-js

mcp
Guvenlik Denetimi
Basarisiz
Health Gecti
  • License — License: AGPL-3.0
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Community trust — 43 GitHub stars
Code Basarisiz
  • rm -rf — Recursive force deletion command in .github/workflows/load-test.yml
  • fs module — File system access in .github/workflows/load-test.yml
Permissions Gecti
  • Permissions — No dangerous permissions requested

Bu listing icin henuz AI raporu yok.

SUMMARY

MCP server that exposes a V8 JavaScript runtime as a tool for AI agents like Claude and Cursor. Supports persistent heap snapshots via S3 or local filesystem, and is ready for integration with modern AI development environments.

README.md

mcp-v8: V8 JavaScript MCP Server

A Rust-based Model Context Protocol (MCP) server that exposes a V8 JavaScript runtime as a tool for AI agents like Claude and Cursor. Supports persistent heap snapshots via S3 or local filesystem, and is ready for integration with modern AI development environments.

Documentation

The public documentation site is built with MkDocs from site-docs/. That
tree is intended to become the primary published documentation surface for this
repository.

Features

  • Async Execution Model: run_js returns immediately with an execution ID. Poll status with get_execution, read console output with get_execution_output, and cancel running executions with cancel_execution.
  • Console Output: Full support for console.log, console.info, console.warn, and console.error. Output is streamed to persistent storage during execution and can be read in real-time with paginated access (line-based or byte-based).
  • Async/Await Support: Full support for async/await and Promises via the deno_core event loop.
  • V8 JavaScript Execution: Run arbitrary JavaScript code in a secure, isolated V8 engine.
  • TypeScript Support: Run TypeScript code directly — types are stripped before execution using SWC. This is type removal only, not type checking.
  • WebAssembly Support: Compile and run WASM modules using the standard WebAssembly JavaScript API (WebAssembly.Module, WebAssembly.Instance, WebAssembly.validate).
  • ES Module Imports: Import npm packages, JSR packages, and URL modules using Deno-style import syntax. Packages are fetched from esm.sh at runtime — no npm install needed. (e.g., import { camelCase } from "npm:[email protected]")
  • Content-Addressed Heap Snapshots: Persist and restore V8 heap state between runs using content-addressed storage, supporting both S3 and local file storage.
  • Stateless Mode: Optional mode for fresh executions without heap persistence, ideal for serverless environments.
  • MCP Protocol: Implements the Model Context Protocol for seamless tool integration with Claude, Cursor, and other MCP clients.
  • Configurable Storage: Choose between S3, local directory, or stateless mode at runtime.
  • Multiple Transports: Supports stdio, Streamable HTTP (MCP 2025-03-26+), and SSE (Server-Sent Events) transport protocols.
  • Clustering: Optional Raft-based clustering for distributed coordination, replicated session logging, and horizontal scaling.
  • Concurrency Control: Configurable concurrent V8 execution limits with semaphore-based throttling.
  • Policy-Gated Filesystem Access: Optional fs module for Node.js-compatible file operations (read, write, delete, etc.), with every operation checked against a Rego policy before execution.
  • OPA-Gated Fetch: Optional fetch() function for JavaScript following the web standard Fetch API, with every HTTP request checked against an Open Policy Agent policy before execution.
  • Fetch Header Injection: Automatically inject headers (e.g., auth tokens, API keys) into outgoing fetch requests based on host and method matching rules, configured via CLI flags or a JSON config file.

Installation

Install mcp-v8 using the provided install script:

curl -fsSL https://raw.githubusercontent.com/r33drichards/mcp-js/main/install.sh | sudo bash

This will automatically download and install the latest release for your platform to /usr/local/bin/mcp-v8 (you may be prompted for your password).

Installing mcp-v8-cli

Install the CLI client separately using install-cli.sh:

curl -fsSL https://raw.githubusercontent.com/r33drichards/mcp-js/main/install-cli.sh | sudo bash

This installs mcp-v8-cli to /usr/local/bin/mcp-v8-cli.

You can also install the CLI via cargo (once the crate is published to crates.io):

cargo install mcp-v8-client

Or download a pre-built binary directly from the GitHub Releases page.


Advanced users: If you prefer to build from source, see the Build from Source section at the end of this document.

HTTP API & OpenAPI

When running with --http-port or --sse-port, the server exposes a plain REST API alongside the MCP transport:

Endpoint Method Description
/api/exec POST Submit JS code for async execution
/api/executions GET List all executions
/api/executions/{id} GET Get status + result of an execution
/api/executions/{id}/output GET Read paginated console output
/api/executions/{id}/cancel POST Cancel a running execution
/swagger-ui GET Interactive Swagger UI
/api-doc/openapi.json GET OpenAPI 3.0 specification

The OpenAPI specification is also committed to the repo root as openapi.json.

Regenerating openapi.json

cd server
cargo build --release
./target/release/server --print-openapi > ../openapi.json
cp ../openapi.json ../mcp-v8-client/openapi.json

mcp-v8-cli

The mcp-v8-cli binary is a fully-typed command-line client for the HTTP API, auto-generated from the OpenAPI spec via progenitor.

Usage: mcp-v8-cli [OPTIONS] <COMMAND>

Options:
  --url <URL>    Base URL of the mcp-v8 server [env: MCP_V8_URL] [default: http://localhost:3000]
  -j, --json     Output raw JSON

Commands:
  exec                      Submit JavaScript code for asynchronous execution
  executions list           List all known executions
  executions get <ID>       Get status and result of an execution
  executions output <ID>    Read paginated console output
  executions cancel <ID>    Cancel a running execution

CLI Examples

# Start the server in HTTP mode
mcp-v8 --stateless --http-port 3000 &

# Submit code and get an execution ID
mcp-v8-cli exec "console.log('hello'); 1 + 1"
# ✅ Execution queued
#    execution_id: 550e8400-e29b-41d4-a716-446655440000

# Poll until complete
mcp-v8-cli executions get 550e8400-e29b-41d4-a716-446655440000
# status : completed
# result : 2

# Read console output
mcp-v8-cli executions output 550e8400-e29b-41d4-a716-446655440000
# hello

# Pipe-friendly JSON output
mcp-v8-cli --json exec "42" | jq .execution_id

# Cancel a long-running execution
mcp-v8-cli executions cancel 550e8400-e29b-41d4-a716-446655440000

# Point at a remote server via env var
export MCP_V8_URL=https://my-server.example.com
mcp-v8-cli executions list

mcp-v8-client (Rust crate)

The mcp-v8-client crate provides a typed Rust client generated at build time from openapi.json:

[dependencies]
mcp-v8-client = "0.1.0"   # crates.io
# or from git:
# mcp-v8-client = { git = "https://github.com/r33drichards/mcp-js" }
use mcp_v8_client::Client;

let client = Client::new("http://localhost:3000");
let body = mcp_v8_client::types::ExecRequest {
    code: "1 + 1".to_string(),
    heap: None, session: None,
    heap_memory_max_mb: None, execution_timeout_secs: None, tags: None,
};
let resp = client.exec_handler(&body).await?;
println!("execution_id: {}", resp.into_inner().execution_id);

Command Line Arguments

mcp-v8 supports the following command line arguments:

Utility Options

  • --print-openapi: Print the OpenAPI JSON specification to stdout and exit. Use this to regenerate openapi.json:
    mcp-v8 --print-openapi > openapi.json
    

Storage Options

  • --s3-bucket <bucket>: Use AWS S3 for heap snapshots. Specify the S3 bucket name. (Conflicts with --stateless)
  • --cache-dir <path>: Local filesystem cache directory for S3 write-through caching. Reduces latency by caching snapshots locally. (Requires --s3-bucket)
  • --directory-path <path>: Use a local directory for heap snapshots. Specify the directory path. (Conflicts with --stateless)
  • --stateless: Run in stateless mode - no heap snapshots are saved or loaded. Each JavaScript execution starts with a fresh V8 isolate. (Conflicts with --s3-bucket and --directory-path)

Note: For heap storage, if neither --s3-bucket, --directory-path, nor --stateless is provided, the server defaults to using /tmp/mcp-v8-heaps as the local directory.

Transport Options

  • --http-port <port>: Enable Streamable HTTP transport (MCP 2025-03-26+) on the specified port. Serves the MCP endpoint at /mcp and a plain API at /api/exec. If not provided, the server uses stdio transport (default). (Conflicts with --sse-port)
  • --sse-port <port>: Enable SSE (Server-Sent Events) transport on the specified port. Exposes /sse for the event stream and /message for client requests. (Conflicts with --http-port)

Execution Limits

  • --heap-memory-max <megabytes>: Maximum V8 heap memory per isolate in megabytes (default: 8, minimum: 1).
  • --execution-timeout <seconds>: Maximum execution timeout in seconds (1–300, default: 30).
  • --max-concurrent-executions <n>: Maximum number of concurrent V8 executions (default: CPU core count). Controls how many JavaScript executions can run in parallel.

Session Logging

  • --session-db-path <path>: Path to the sled database used for session logging (default: /tmp/mcp-v8-sessions). Only applies in stateful mode. (Conflicts with --stateless)

Cluster Options

These options enable Raft-based clustering for distributed coordination and replicated session logging.

  • --cluster-port <port>: Port for the Raft cluster HTTP server. Enables cluster mode when set. (Requires --http-port or --sse-port)
  • --node-id <id>: Unique node identifier within the cluster (default: node1).
  • --peers <peers>: Comma-separated list of seed peer addresses. Format: id@host:port or host:port. Peers can also join dynamically via POST /raft/join.
  • --join <address>: Join an existing cluster by contacting this seed address (host:port). The node registers itself with the cluster leader.
  • --advertise-addr <addr>: Advertise address for this node (host:port). Used for peer discovery and write forwarding. Defaults to <node-id>:<cluster-port>.
  • --heartbeat-interval <ms>: Raft heartbeat interval in milliseconds (default: 100).
  • --election-timeout-min <ms>: Minimum election timeout in milliseconds (default: 300).
  • --election-timeout-max <ms>: Maximum election timeout in milliseconds (default: 500).

OPA / Fetch Options

These options enable a policy-gated fetch() function in the JavaScript runtime. When fetch policies are configured via --policies-json, a fetch(url, opts?) global becomes available. fetch() follows the web standard Fetch API — it returns a Promise that resolves to a Response object. If fetch-header rules are configured, matching headers are applied first, then the resulting request is evaluated by policy.

  • --fetch-header <RULE>: Inject static headers or dynamic OAuth client-credentials tokens into matching fetch requests. Static format: host=<host>,header=<name>,value=<val>[,methods=GET;POST]. Dynamic format: host=<host>,header=<name>,token_url=<url>,client_id=<id>,client_secret=<secret>[,scope=<scope>][,refresh_buffer_secs=30][,methods=GET;POST]. Can be specified multiple times. Requires fetch to be enabled via --policies-json. See Fetch Header Injection for details.
  • --fetch-header-config <PATH>: Path to a JSON file with fetch header injection rules. Each rule uses either a static headers object or a dynamic auth block. Requires fetch to be enabled via --policies-json. See Fetch Header Injection for details.

Example:

mcp-v8 --stateless --http-port 3000 \
  --policies-json '{"fetch":{"policies":[{"url":"http://localhost:8181"}]}}'

WASM Module Options

Pre-load WebAssembly modules that are available as global variables in every JavaScript execution.

  • --wasm-module <name>=<path>: Pre-load a .wasm file and expose its exports as a global variable with the given name. Can be specified multiple times for multiple modules.
  • --wasm-config <path>: Path to a JSON config file mapping global names to .wasm file paths. Format: {"name": "/path/to/module.wasm", ...}.

Both options can be used together. CLI flags and config file entries are merged; duplicate names cause an error.

Example — CLI flags:

mcp-v8 --stateless --wasm-module math=/path/to/math.wasm --wasm-module crypto=/path/to/crypto.wasm

Example — Config file (wasm-modules.json):

{
  "math": "/path/to/math.wasm",
  "crypto": "/path/to/crypto.wasm"
}
mcp-v8 --stateless --wasm-config wasm-modules.json

After loading, the module exports are available directly in JavaScript:

math.add(21, 21); // → 42

Modules with imports (e.g. WASI modules like SQLite) are also supported. When a module has imports, auto-instantiation is skipped and the compiled WebAssembly.Module is exposed as __wasm_<name>. Your JavaScript code can then instantiate it with the required imports:

// __wasm_sqlite is the compiled WebAssembly.Module
var instance = new WebAssembly.Instance(__wasm_sqlite, {
    wasi_snapshot_preview1: { /* WASI stubs */ },
});
instance.exports.sqlite3_open(/* ... */);

Self-contained modules (no imports) are auto-instantiated as before — their exports are set directly on <name>, and the compiled Module is also available as __wasm_<name>.

See the SQLite WASM example for a complete working example.

Quick Start

After installation, you can run the server directly. Choose one of the following options:

Stdio Transport (Default)

# Use S3 for heap storage (recommended for cloud/persistent use)
mcp-v8 --s3-bucket my-bucket-name

# Use local filesystem directory for heap storage (recommended for local development)
mcp-v8 --directory-path /tmp/mcp-v8-heaps

# Use stateless mode - no heap persistence (recommended for one-off computations)
mcp-v8 --stateless

HTTP Transport (Streamable HTTP)

The HTTP transport uses the Streamable HTTP protocol (MCP 2025-03-26+), which supports bidirectional communication over standard HTTP. The MCP endpoint is served at /mcp:

# Start HTTP server on port 8080 with local filesystem storage
mcp-v8 --directory-path /tmp/mcp-v8-heaps --http-port 8080

# Start HTTP server on port 8080 with S3 storage
mcp-v8 --s3-bucket my-bucket-name --http-port 8080

# Start HTTP server on port 8080 in stateless mode
mcp-v8 --stateless --http-port 8080

The HTTP transport also exposes a plain HTTP API at POST /api/exec for direct JavaScript execution without MCP framing.

The HTTP transport is useful for:

  • Network-based MCP clients
  • Load-balanced and horizontally-scaled deployments
  • Testing and debugging with tools like the MCP Inspector
  • Containerized deployments
  • Remote MCP server access

SSE Transport

Server-Sent Events (SSE) transport for streaming responses:

# Start SSE server on port 8081 with local filesystem storage
mcp-v8 --directory-path /tmp/mcp-v8-heaps --sse-port 8081

# Start SSE server on port 8081 in stateless mode
mcp-v8 --stateless --sse-port 8081

MCP Tools

Execution Workflow

Execution differs by transport mode:

Stateful MCP:
1. run_js(code)           → { execution_id }
2. get_execution(id)      → { status: "running" | "completed" | "failed" | "cancelled" | "timed_out", result, error }
3. get_execution_output(id, line_offset, line_limit)  → paginated console output

Stateless MCP:
1. run_js(code)           → { output, error? }

Example:

Stateful MCP:
Call run_js with code: "console.log('hello');"
  → { execution_id: "abc-123" }

Call get_execution with execution_id: "abc-123"
  → { status: "completed" }

Call get_execution_output with execution_id: "abc-123"
  → { data: "hello\n", total_lines: 1 }

Stateless MCP:
Call run_js with code: "console.log('hello');"
  → { output: "hello\n" }

Console Output

console.log, console.info, console.warn, and console.error are fully supported. In stateful MCP and the REST API, output is streamed to persistent storage during execution and can be read using get_execution_output or GET /api/executions/{id}/output. In stateless MCP, run_js returns the collected console output directly.

get_execution_output supports two pagination modes:

  • Line mode: line_offset + line_limit — fetch N lines starting from line M
  • Byte mode: byte_offset + byte_limit — fetch N bytes starting from byte M

Both modes return position info in both coordinate systems for cross-referencing. Use next_line_offset or next_byte_offset from a response to resume reading.

Tools

Tool Description
run_js Stateful MCP: submit async execution and get execution_id. Stateless MCP: wait internally and return {output, error}. Parameters common to both: code, heap_memory_max_mb, execution_timeout_secs.
get_execution Stateful MCP only. Poll execution status and result.
get_execution_output Stateful MCP only. Read paginated console output.
cancel_execution Stateful MCP only. Terminate a running V8 execution.
list_executions Stateful MCP only. List all executions with their status.

Additional Tools (Stateful Mode Only)

In stateful MCP, run_js accepts an additional heap parameter (SHA-256 hash to resume a previous heap snapshot). Session identity for MCP calls comes from the X-MCP-Session-Id header rather than a session tool parameter.

Tool Description
list_sessions List all named sessions.
list_session_snapshots Browse execution history for a session. Accepts session (required) and fields (optional, comma-separated: index, input_heap, output_heap, code, timestamp).
get_heap_tags Get tags for a heap snapshot.
set_heap_tags Set or replace tags on a heap snapshot.
delete_heap_tags Delete specific tag keys from a heap snapshot.
query_heaps_by_tags Find heap snapshots matching tag criteria.

Stateless vs Stateful Mode

Stateless Mode (--stateless)

Stateless mode runs each JavaScript execution in a fresh V8 isolate without any heap persistence.

Benefits:

  • Faster execution: No snapshot creation/serialization overhead
  • No storage I/O: Doesn't read or write heap files
  • Fresh isolates: Every JS execution starts clean
  • Perfect for: One-off computations, stateless functions, serverless environments

Example use case: Simple calculations, data transformations, or any scenario where you don't need to persist state between executions.

Stateful Mode (default)

Stateful mode persists the V8 heap state between executions using content-addressed storage backed by either S3 or local filesystem.

Each execution returns a heap content hash (a 64-character SHA-256 hex string) that identifies the snapshot. Pass this hash in the next run_js call to resume from that state. Omit heap to start a fresh session.

Benefits:

  • State persistence: Variables and objects persist between runs
  • Content-addressed: Snapshots are keyed by their SHA-256 hash — no naming collisions, safe concurrent access, and natural deduplication
  • Immutable snapshots: Once written, a snapshot at a given hash never changes
  • Perfect for: Interactive sessions, building up complex state over time

Example use case: Building a data structure incrementally, maintaining session state, or reusing expensive computations.

Named Sessions

You can tag executions with a human-readable session name for history tracking. On the REST API, pass the session field in the request body. On MCP, send the X-MCP-Session-Id header during initialization. The server logs each execution (input heap, output heap, code, and timestamp) to an embedded sled database.

Two additional tools are available in stateful mode for browsing session history:

  • list_sessions — Returns an array of all session names that have been used.
  • list_session_snapshots — Returns the log entries for a given session. Accepts a required session parameter and an optional fields parameter (comma-separated) to select specific fields: index, input_heap, output_heap, code, timestamp.

The session database path defaults to /tmp/mcp-v8-sessions and can be overridden with --session-db-path.

Example workflow:

  1. Call run_js with code: "var x = 1; x;" while using session my-project → receives execution_id.
  2. Call get_execution with the execution_id → receives { status: "completed", result: "1", heap: "ab12..." }.
  3. Pass the returned heap hash in subsequent run_js calls to continue that heap, while keeping the same session identity for history tracking.
  4. Call list_sessions to see ["my-project"].
  5. Call list_session_snapshots while using session my-project to see the full execution history.

Integration

Claude for Desktop

  1. Install the server as above.
  2. Open Claude Desktop → Settings → Developer → Edit Config.
  3. Add your server to claude_desktop_config.json:

Stateful mode with S3:

{
  "mcpServers": {
    "js": {
      "command": "mcp-v8",
      "args": ["--s3-bucket", "my-bucket-name"]
    }
  }
}

Stateful mode with local filesystem:

{
  "mcpServers": {
    "js": {
      "command": "mcp-v8",
      "args": ["--directory-path", "/tmp/mcp-v8-heaps"]
    }
  }
}

Stateless mode:

{
  "mcpServers": {
    "js": {
      "command": "mcp-v8",
      "args": ["--stateless"]
    }
  }
}
  1. Restart Claude Desktop. The new tools will appear under the hammer icon.

Claude Code CLI

Add the MCP server to Claude Code using the claude mcp add command:

Stdio transport (local):

# Stateful mode with local filesystem
claude mcp add mcp-v8 -- mcp-v8 --directory-path /tmp/mcp-v8-heaps

# Stateless mode
claude mcp add mcp-v8 -- mcp-v8 --stateless

SSE transport (remote):

claude mcp add mcp-v8 -t sse https://mcp-js-production.up.railway.app/sse

Then test by running claude and asking: "Run this JavaScript: console.log([1,2,3].map(x => x * 2))"

Cursor

  1. Install the server as above.
  2. Create or edit .cursor/mcp.json in your project root:

Stateful mode with local filesystem:

{
  "mcpServers": {
    "js": {
      "command": "mcp-v8",
      "args": ["--directory-path", "/tmp/mcp-v8-heaps"]
    }
  }
}

Stateless mode:

{
  "mcpServers": {
    "js": {
      "command": "mcp-v8",
      "args": ["--stateless"]
    }
  }
}
  1. Restart Cursor. The MCP tools will be available in the UI.

Claude (Web/Cloud) via Railway

You can also use the hosted version on Railway without installing anything locally:

  1. Go to Claude's connectors settings page
  2. Add a new custom connector:
    • Name: "mcp-v8"
    • URL: https://mcp-js-production.up.railway.app/sse

Example Usage

Ask Claude or Cursor: "Run this JavaScript: console.log(1 + 2)"

The agent will:

  1. Call run_js with code: "console.log(1 + 2)" → receives execution_id
  2. Call get_execution with the execution_id → receives { status: "completed" }
  3. Call get_execution_output with the execution_id → receives { data: "3\n", total_lines: 1 }

In stateful mode, get_execution also returns a heap content hash — pass it back in the next run_js call to resume from that state.

Importing Packages

You can import npm packages, JSR packages, and URL modules directly in your JavaScript code using ES module import syntax. Packages are fetched from esm.sh at runtime — no npm install or pre-installation step is needed.

npm Packages

Use the npm: prefix followed by the package name and version:

import { camelCase } from "npm:[email protected]";
camelCase("hello world"); // → "helloWorld"
import dayjs from "npm:[email protected]";
dayjs("2025-01-15").format("MMMM D, YYYY"); // → "January 15, 2025"

JSR Packages

Use the jsr: prefix for packages from the JSR registry:

import { camelCase } from "jsr:@luca/[email protected]";
camelCase("hello world"); // → "helloWorld"

URL Imports

Import directly from any URL that serves ES modules:

import { pascalCase } from "https://deno.land/x/case/mod.ts";
pascalCase("hello world"); // → "HelloWorld"

How It Works

  • npm: specifiers are rewritten to https://esm.sh/<package> URLs
  • jsr: specifiers are rewritten to https://esm.sh/jsr/<package> URLs
  • https:// and http:// URLs are fetched directly
  • Relative imports (e.g., ./utils.js) resolve against the parent module's URL
  • TypeScript modules (.ts, .tsx) fetched from URLs are automatically type-stripped before execution

Tips

  • Always pin versions (e.g., npm:[email protected]) for reproducible results
  • Only packages that ship as ES modules are supported — CommonJS-only packages won't work directly, but esm.sh converts many of them automatically
  • Imports are fetched over the network at runtime, so the first execution may be slower while modules are downloaded
  • Top-level await is supported, so you can use import() dynamically as well:
    const { default: _ } = await import("npm:[email protected]");
    _.chunk([1, 2, 3, 4, 5], 2); // → [[1, 2], [3, 4], [5]]
    

WebAssembly

You can compile and run WebAssembly modules using the standard WebAssembly JavaScript API:

const wasmBytes = new Uint8Array([
  0x00,0x61,0x73,0x6d, // magic
  0x01,0x00,0x00,0x00, // version
  0x01,0x07,0x01,0x60,0x02,0x7f,0x7f,0x01,0x7f, // type: (i32,i32)->i32
  0x03,0x02,0x01,0x00, // function section
  0x07,0x07,0x01,0x03,0x61,0x64,0x64,0x00,0x00, // export "add"
  0x0a,0x09,0x01,0x07,0x00,0x20,0x00,0x20,0x01,0x6a,0x0b // body: local.get 0, local.get 1, i32.add
]);
const mod = new WebAssembly.Module(wasmBytes);
const inst = new WebAssembly.Instance(mod);
inst.exports.add(21, 21); // → 42

Both synchronous (WebAssembly.Module / WebAssembly.Instance) and async (WebAssembly.compile, WebAssembly.instantiate) WebAssembly APIs are supported. The runtime resolves Promises automatically via the event loop.

Alternatively, you can pre-load .wasm files at server startup using --wasm-module or --wasm-config so they are available as globals in every execution without inline byte arrays. See WASM Module Options for details.

SQLite WASM

Run a full SQLite database inside mcp-v8 using SQLite WASM:

# Build the WASM module (requires Emscripten)
./examples/sqlite-wasm/build.sh

# Start the server with SQLite pre-loaded
mcp-v8 --stateless --wasm-module sqlite=examples/sqlite-wasm/sqlite3.wasm

Then run SQL from JavaScript:

var db = new SQLite();
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");
db.exec("INSERT INTO users (name, age) VALUES ('Alice', 30)");
var result = db.query("SELECT * FROM users");
db.close();
JSON.stringify(result.rows);  // → [{"id":1,"name":"Alice","age":30}]

See examples/sqlite-wasm/ for the full example including the SQLite wrapper code and WASI import stubs.

OPA-Gated Fetch

When the server is started with fetch policies configured via --policies-json, JavaScript code can use a fetch(url, opts?) function following the web standard Fetch API. If fetch-header rules are configured, they are applied before policy evaluation, and the policy sees the final request headers before the HTTP call is made.

1. Write an OPA policy

Create a Rego policy that controls which requests are allowed:

package mcp.fetch

default allow = false

# Allow GET requests to a specific API host
allow if {
    input.method == "GET"
    input.url_parsed.host == "api.example.com"
    startswith(input.url_parsed.path, "/public/")
}

The policy input includes:

  • operation: always "fetch"
  • url: the full URL string
  • method: HTTP method (e.g. "GET", "POST")
  • headers: request headers (keys normalized to lowercase)
  • url_parsed: parsed URL components — scheme, host, port, path, query

2. Start the server with fetch policy enabled

mcp-v8 --stateless --http-port 3000 \
  --policies-json '{"fetch":{"policies":[{"url":"http://localhost:8181"}]}}'

3. Use fetch() in JavaScript

const resp = await fetch("https://api.example.com/public/data");
resp.status;              // 200
resp.ok;                  // true
await resp.text();        // response body as string
await resp.json();        // parsed JSON
resp.headers.get("content-type"); // header value

The response object supports:

  • Properties: .ok, .status, .statusText, .url, .redirected, .type, .bodyUsed
  • Methods: .text(), .json(), .clone() (.text() and .json() return Promises)
  • Headers: .headers.get(name), .headers.has(name), .headers.entries(), .headers.keys(), .headers.values(), .headers.forEach(fn)

fetch() also accepts an options object:

const resp = await fetch("https://api.example.com/data", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ key: "value" })
});
JSON.stringify(await resp.json());

If the OPA policy denies a request, the Promise returned by fetch() is rejected with an error.

Fetch Header Injection

When using OPA-gated fetch, you can configure automatic header injection rules that add headers to outgoing fetch() requests based on the target host and HTTP method. Rules can be static (headers) or dynamic (auth via OAuth2 client credentials), which lets the server acquire and cache bearer tokens without embedding secrets in JavaScript code.

Header injection rules are evaluated per-request. If a rule's host pattern and method filter match, its headers are injected into the request. User-provided headers always take precedence — a rule will not overwrite a header that JavaScript code already set.

There are no new top-level CLI flags for dynamic auth. The feature is configured through additional key-value pairs on --fetch-header or through the auth object inside --fetch-header-config.

CLI Flags (--fetch-header)

Use --fetch-header to define rules inline.

Static header syntax:

host=<host>,header=<name>,value=<val>[,methods=GET;POST]

Dynamic OAuth client-credentials syntax:

host=<host>,header=<name>,token_url=<url>,client_id=<id>,client_secret=<secret>[,scope=<scope>][,refresh_buffer_secs=30][,methods=GET;POST]
  • host — Host to match (exact or wildcard, see below). Required.
  • header — Header name to inject. Required.
  • value — Static header value to inject. Required for static rules.
  • token_url — OAuth2 token endpoint for client credentials. Required for dynamic rules.
  • client_id — OAuth2 client ID. Required for dynamic rules.
  • client_secret — OAuth2 client secret. Required for dynamic rules.
  • scope — Optional OAuth2 scope sent on client-credentials acquisition.
  • refresh_buffer_secs — Optional early-refresh window in seconds. Defaults to 30.
  • methods — Semicolon-separated HTTP methods to match. Optional. If omitted, the rule applies to all methods.

Can be specified multiple times for multiple rules:

mcp-v8 --stateless --policies-json '{"fetch":{"policies":[{"url":"http://localhost:8181"}]}}' \
  --fetch-header "host=api.github.com,header=Authorization,value=Bearer ghp_xxxx" \
  --fetch-header "host=api.example.com,header=X-API-Key,value=secret123"
mcp-v8 --stateless \
  --policies-json '{"fetch":{"policies":[{"url":"http://localhost:8181"}]}}' \
  --fetch-header "host=api.example.com,header=Authorization,token_url=https://issuer.example.com/oauth2/token,client_id=my-client,client_secret=${CLIENT_SECRET},scope=read:all,refresh_buffer_secs=45"

JSON Config File (--fetch-header-config)

For managing many rules, use a JSON config file. Each rule uses either a static headers object or a dynamic auth block:

[
  {
    "host": "api.github.com",
    "methods": ["GET", "POST"],
    "headers": {
      "Authorization": "Bearer ghp_xxxx",
      "X-GitHub-Api-Version": "2022-11-28"
    }
  },
  {
    "host": "api.example.com",
    "methods": ["GET", "POST"],
    "auth": {
      "type": "oauth_client_credentials",
      "header": "Authorization",
      "token_url": "https://issuer.example.com/oauth2/token",
      "client_id": "my-client",
      "client_secret": "my-secret",
      "scope": "read:all",
      "refresh_buffer_secs": 45
    }
  }
]
mcp-v8 --stateless --policies-json '{"fetch":{"policies":[{"url":"http://localhost:8181"}]}}' \
  --fetch-header-config headers.json

Both --fetch-header and --fetch-header-config can be used together — their rules are merged.
Each JSON rule must choose exactly one style: static headers or dynamic auth.

JSON config files are read literally; they do not expand shell-style placeholders such as ${CLIENT_SECRET}.

Host Matching

  • Exact match: api.github.com matches only api.github.com.
  • Wildcard: *.github.com matches api.github.com, github.com, and sub.api.github.com.
  • Host matching is case-insensitive.

Token Reuse, Refresh, And Reacquire

Dynamic OAuth rules cache tokens per rule and reuse them across matching fetches until the token is close to expiry. When the token endpoint returns a refresh_token, mcp-v8 attempts a refresh first once the cached token expires. If refresh is unavailable or fails, the server falls back to a fresh client-credentials token acquisition automatically.

refresh_buffer_secs controls how early the server treats a token as stale. This keeps fetches from racing the exact expiry boundary while still reusing the same bearer token across normal back-to-back requests.

Header Precedence

Headers set explicitly in JavaScript fetch() calls always win. Injection rules only add headers that are not already present:

// Rule: host=api.example.com, header=Authorization, value=Bearer injected

// Header is injected (not set by code):
await fetch("https://api.example.com/data");
// → request includes Authorization: Bearer injected

// User header takes precedence:
await fetch("https://api.example.com/data", {
  headers: { "Authorization": "Bearer my-own-token" }
});
// → request includes Authorization: Bearer my-own-token (rule skipped)

The same precedence rule applies to dynamic OAuth injection: if user code sets Authorization, the token source is skipped and no token lookup happens for that request.

Security Notes

  • Prefer environment variables, mounted config files, or container/orchestrator secrets over hard-coded client secrets.
  • Be aware that inline CLI secrets can be visible in shell history, process listings, and container metadata.
  • JavaScript running inside run_js does not receive injected header values automatically, but your upstream service will still receive them and can reflect them back, so treat the target host as trusted.
  • Never commit real client secrets or token-bearing config files.

For a quick local smoke test, docker-compose.secure-sessions.yml starts mcp-js with a dynamic --fetch-header rule pointed at this repo's local Keycloak realm, and the helper verifies bearer injection and token reuse:

docker compose -f docker-compose.secure-sessions.yml up --build -d
deno run -A scripts/test-keycloak-fetch-injection.ts
docker compose -f docker-compose.secure-sessions.yml down -v

For a full local development walkthrough with Keycloak, see tutorials/oauth-client-credentials-fetch-injection.md.

Policy-Gated Filesystem Access

When the server is started with a Rego policy configuration, JavaScript code can use an fs module providing Node.js-compatible file operations. Every operation is evaluated against a Rego policy before execution.

The fs module supports the following operations:

const data = await fs.readFile("/tmp/data.txt");          // string (utf-8)
const data = await fs.readFile("/tmp/data.bin", "buffer"); // Uint8Array
await fs.writeFile("/tmp/out.txt", "hello");              // string data
await fs.writeFile("/tmp/out.bin", uint8array);           // binary data
await fs.appendFile("/tmp/out.txt", " world");
const entries = await fs.readdir("/tmp");                  // string[]
const info = await fs.stat("/tmp/data.txt");               // {size,isFile,isDirectory,...}
await fs.mkdir("/tmp/newdir", { recursive: true });
await fs.rm("/tmp/data.txt");
await fs.rm("/tmp/newdir", { recursive: true });
await fs.rename("/tmp/old.txt", "/tmp/new.txt");
await fs.copyFile("/tmp/a.txt", "/tmp/b.txt");
const bool = await fs.exists("/tmp/data.txt");

1. Write a Rego policy

Create a Rego policy that controls which filesystem operations are allowed:

package mcp.fs

default allow = false

# Allow reading from /tmp
allow if {
    input.operation == "readFile"
    startswith(input.path, "/tmp/")
}

# Allow writing to /tmp
allow if {
    input.operation == "writeFile"
    startswith(input.path, "/tmp/")
}

# Allow other common operations
allow if {
    input.operation in ["readdir", "stat", "exists"]
    startswith(input.path, "/tmp/")
}

The policy input includes:

  • operation: the filesystem operation being performed (e.g., "readFile", "writeFile", "mkdir", "rm", "rename", "copyFile", "appendFile", "readdir", "stat", "exists")
  • path: the file or directory path being accessed
  • destination: (optional) the destination path for operations like rename and copyFile
  • recursive: (optional, boolean) whether a recursive operation was requested (for mkdir and rm)
  • encoding: (optional) the encoding parameter for readFile (either "utf8" or "buffer")

2. Start the server with policy configuration

Use --policies-json to enable filesystem access with local Rego policies:

mcp-v8 --stateless --http-port 3000 \
  --policies-json /path/to/policies.json

The policies.json file should contain policy configuration objects. See the POLICIES section for detailed configuration examples.

3. Use fs in JavaScript

All fs operations return Promises and can be used with await:

// Read a file
const content = await fs.readFile("/tmp/data.txt");
console.log(content);

// Write a file
await fs.writeFile("/tmp/output.txt", "Hello, World!");

// Check if a file exists
const exists = await fs.exists("/tmp/output.txt");
console.log(exists); // true

// Get file metadata
const stats = await fs.stat("/tmp/output.txt");
console.log(stats.size); // file size in bytes

// List directory contents
const files = await fs.readdir("/tmp");
console.log(files); // array of filenames

// Create a directory
await fs.mkdir("/tmp/mydir", { recursive: true });

// Copy a file
await fs.copyFile("/tmp/output.txt", "/tmp/backup.txt");

// Rename a file
await fs.rename("/tmp/backup.txt", "/tmp/backup_old.txt");

// Delete a file
await fs.rm("/tmp/backup_old.txt");

// Delete a directory recursively
await fs.rm("/tmp/mydir", { recursive: true });

If a policy denies an operation, the Promise returned by the fs operation is rejected with an error message indicating that the operation was denied by policy.

Binary Data

When reading binary files, specify "buffer" as the encoding parameter:

const buffer = await fs.readFile("/tmp/image.png", "buffer");
// buffer is a Uint8Array

// Write binary data
const newBuffer = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
await fs.writeFile("/tmp/binary.bin", newBuffer);

Loading .wasm Files

Instead of embedding raw bytes in JavaScript, you can compile a .wasm file once and load it at server startup. The module's exports are then available as a global variable in every execution.

1. Create a WASM module

Write a WebAssembly Text Format (.wat) file and compile it with wat2wasm:

;; math.wat — exports add(i32, i32) -> i32
(module
  (func $add (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add)
  (export "add" (func $add)))
wat2wasm math.wat -o math.wasm

2. Start the server with the module

# Single module
mcp-v8 --stateless --wasm-module math=./math.wasm

# Multiple modules
mcp-v8 --stateless \
  --wasm-module math=./math.wasm \
  --wasm-module physics=./physics.wasm

Or use a JSON config file for many modules:

{
  "math": "./math.wasm",
  "physics": "./physics.wasm"
}
mcp-v8 --stateless --wasm-config wasm-modules.json

3. Call exports from JavaScript

The module's exports are available directly on the global variable:

math.add(2, 3);          // → 5
math.add(100, 200);      // → 300

When multiple modules are loaded, each is its own global:

var sum = math.add(10, 5);           // 15
var product = physics.multiply(3, 4); // 12
sum + product;                        // → 27

Heap Storage Options

You can configure heap storage using the following command line arguments:

  • S3: --s3-bucket <bucket>
    • Example: mcp-v8 --s3-bucket my-bucket-name
    • Requires AWS credentials in your environment.
    • Ideal for cloud deployments and sharing state across instances.
  • S3 with write-through cache: --s3-bucket <bucket> --cache-dir <path>
    • Example: mcp-v8 --s3-bucket my-bucket-name --cache-dir /tmp/mcp-v8-cache
    • Reads from local cache first, writes to both local cache and S3.
    • Reduces latency for repeated snapshot access.
  • Filesystem: --directory-path <path>
    • Example: mcp-v8 --directory-path /tmp/mcp-v8-heaps
    • Stores heap snapshots locally on disk.
    • Ideal for local development and testing.
  • Stateless: --stateless
    • Example: mcp-v8 --stateless
    • No heap persistence - each execution starts fresh.
    • Ideal for one-off computations and serverless environments.

Note: Only one storage option can be used at a time. If multiple are provided, the server will return an error.

Limitations

  • No timers: Functions like setTimeout and setInterval are not available.
  • No DOM or browser APIs: This is not a browser environment; there is no access to window, document, or other browser-specific objects.
  • TypeScript: type removal only: TypeScript type annotations are stripped before execution. No type checking is performed — invalid types are silently removed, not reported as errors.

Build from Source (Advanced)

If you prefer to build from source instead of using the install script:

Prerequisites

  • Rust (nightly toolchain recommended)
  • (Optional) AWS credentials for S3 storage

Build the Server

cd server
cargo build --release

The built binary will be located at target/release/server. You can use this path in the integration steps above instead of /usr/local/bin/mcp-v8 if desired.

MCP-V8 Load Test Benchmark Report v0.1.0

Comparison of single-node vs 3-node cluster at various request rates.

Results

ran on railway gha runners on pr

Topology Target Rate Actual Iter/s HTTP Req/s Exec Avg (ms) Exec p95 (ms) Exec p99 (ms) Success % Dropped Max VUs
cluster-stateful 100/s 99.5 99.5 44.9 196.88 416.99 100% 31 41
cluster-stateful 200/s 199.6 199.6 23.22 79.32 131.13 100% 13 33
cluster-stateless 1000/s 999.9 999.9 3.82 7.72 13.09 100% 0 100
cluster-stateless 100/s 100 100 3.67 5.65 8.03 100% 0 10
cluster-stateless 200/s 200 200 3.56 5.9 8.61 100% 0 20
cluster-stateless 500/s 500 500 3.42 5.85 9.2 100% 0 50
single-stateful 100/s 99.1 99.1 215.12 362.5 376.6 100% 32 42
single-stateful 200/s 97.8 97.8 1948.82 2212.55 2960.96 100% 5939 200
single-stateless 1000/s 977.1 977.1 60.98 482.98 602.38 100% 843 561
single-stateless 100/s 100 100 3.71 5.73 8.73 100% 0 10
single-stateless 200/s 200 200 3.61 5.43 7.74 100% 0 20
single-stateless 500/s 500 500 4.67 8.49 27.98 100% 0 50

P95 Latency

Topology Rate P95 (ms)
cluster-stateful 100/s 196.88 █████████████████████
cluster-stateful 200/s 79.32 █████████████████
cluster-stateless 100/s 5.65 ███████
cluster-stateless 200/s 5.9 ███████
cluster-stateless 500/s 5.85 ███████
cluster-stateless 1000/s 7.72 ████████
single-stateful 100/s 362.5 ███████████████████████
single-stateful 200/s 2212.55 ██████████████████████████████
single-stateless 100/s 5.73 ███████
single-stateless 200/s 5.43 ██████
single-stateless 500/s 8.49 ████████
single-stateless 1000/s 482.98 ████████████████████████

Notes

  • Target Rate: The configured constant-arrival-rate (requests/second k6 attempts)
  • Actual Iter/s: Achieved iterations per second (each iteration = 1 POST /api/exec)
  • HTTP Req/s: Total HTTP requests per second (1 per iteration)
  • Dropped: Iterations k6 couldn't schedule because VUs were exhausted (indicates server saturation)
  • Topology: single = 1 MCP-V8 node; cluster = 3 MCP-V8 nodes with Raft

Yorumlar (0)

Sonuc bulunamadi