loading…
Search for a command to run...
loading…
A Javascript code execution sandbox that uses v8 to isolate code to run AI generated javascript locally without fear. Supports heap snapshotting for persistent
A Javascript code execution sandbox that uses v8 to isolate code to run AI generated javascript locally without fear. Supports heap snapshotting for persistent sessions.
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.
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.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 and Promises via the deno_core event loop.WebAssembly JavaScript API (WebAssembly.Module, WebAssembly.Instance, WebAssembly.validate).import syntax. Packages are fetched from esm.sh at runtime — no npm install needed. (e.g., import { camelCase } from "npm:[email protected]")fs module for Node.js-compatible file operations (read, write, delete, etc.), with every operation checked against a Rego policy before execution.fetch() function for JavaScript following the web standard Fetch API, with every HTTP request checked against an Open Policy Agent policy before execution.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).
Advanced users: If you prefer to build from source, see the Build from Source section at the end of this document.
mcp-v8 supports the following command line arguments:
--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.
--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)--heap-memory-max <megabytes>: Maximum V8 heap memory per isolate in megabytes (1–64, default: 8).--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-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)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).These options enable an OPA-gated fetch() function in the JavaScript runtime. When --opa-url is set, a fetch(url, opts?) global becomes available. fetch() follows the web standard Fetch API — it returns a Promise that resolves to a Response object. Every outbound HTTP request is first checked against an OPA policy — the request is only made if the policy returns {"allow": true}.
--opa-url <URL>: OPA server URL (e.g. http://localhost:8181). Enables fetch() in the JS runtime.--opa-fetch-policy <path>: OPA policy path appended to /v1/data/ (default: mcp/fetch). Requires --opa-url.--fetch-header <RULE>: Inject headers into fetch requests matching host/method rules. Format: host=<host>,header=<name>,value=<val>[,methods=GET;POST]. Can be specified multiple times. Requires --opa-url. See Fetch Header Injection for details.--fetch-header-config <PATH>: Path to a JSON file with header injection rules. Format: [{"host": "...", "methods": [...], "headers": {...}}]. Requires --opa-url. See Fetch Header Injection for details.Example:
mcp-v8 --stateless --http-port 3000 \
--opa-url http://localhost:8181 \
--opa-fetch-policy mcp/fetch
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.
After installation, you can run the server directly. Choose one of the following options:
# 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
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:
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
run_js uses an async execution model — it submits code for background execution and returns an execution ID immediately. Use get_execution to poll for completion and retrieve the result, and get_execution_output to read console output.
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
Example:
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 }
console.log, console.info, console.warn, and console.error are fully supported. Output is streamed to persistent storage during execution and can be read in real-time using get_execution_output.
get_execution_output supports two pagination modes:
line_offset + line_limit — fetch N lines starting from line Mbyte_offset + byte_limit — fetch N bytes starting from byte MBoth 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.
| Tool | Description |
|---|---|
run_js |
Submit JavaScript/TypeScript code for async execution. Returns an execution_id immediately. Parameters: code (required), heap_memory_max_mb (optional, 4–64, default: 8), execution_timeout_secs (optional, 1–300, default: 30). |
get_execution |
Poll execution status and result. Returns execution_id, status, result (if completed), error (if failed), started_at, completed_at. |
get_execution_output |
Read paginated console output. Supports line-based (line_offset + line_limit) or byte-based (byte_offset + byte_limit) pagination. |
cancel_execution |
Terminate a running V8 execution. |
list_executions |
List all executions with their status. |
In stateful mode, run_js accepts additional parameters: heap (SHA-256 hash to resume from) and session (human-readable session name for logging).
| 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)Stateless mode runs each JavaScript execution in a fresh V8 isolate without any heap persistence.
Benefits:
Example use case: Simple calculations, data transformations, or any scenario where you don't need to persist state between executions.
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:
Example use case: Building a data structure incrementally, maintaining session state, or reusing expensive computations.
You can tag executions with a human-readable session name by passing the session parameter to run_js. When a session name is provided, 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:
run_js with code: "var x = 1; x;" and session: "my-project" → receives execution_id.get_execution with the execution_id → receives { status: "completed", result: "1", heap: "ab12..." }.heap hash and session: "my-project" in subsequent run_js calls to continue and log the session.list_sessions to see ["my-project"].list_session_snapshots with session: "my-project" to see the full execution history.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"]
}
}
}
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/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"]
}
}
}
You can also use the hosted version on Railway without installing anything locally:
https://mcp-js-production.up.railway.app/sseAsk Claude or Cursor: "Run this JavaScript: console.log(1 + 2)"
The agent will:
run_js with code: "console.log(1 + 2)" → receives execution_idget_execution with the execution_id → receives { status: "completed" }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.
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.
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"
Use the jsr: prefix for packages from the JSR registry:
import { camelCase } from "jsr:@luca/[email protected]";
camelCase("hello world"); // → "helloWorld"
Import directly from any URL that serves ES modules:
import { pascalCase } from "https://deno.land/x/case/mod.ts";
pascalCase("hello world"); // → "HelloWorld"
npm: specifiers are rewritten to https://esm.sh/<package> URLsjsr: specifiers are rewritten to https://esm.sh/jsr/<package> URLshttps:// and http:// URLs are fetched directly./utils.js) resolve against the parent module's URL.ts, .tsx) fetched from URLs are automatically type-stripped before executionnpm:[email protected]) for reproducible resultsawait 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]]
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.
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.
When the server is started with --opa-url, JavaScript code can use a fetch(url, opts?) function following the web standard Fetch API. Every request is checked against an OPA policy 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 stringmethod: HTTP method (e.g. "GET", "POST")headers: request headers (keys normalized to lowercase)url_parsed: parsed URL components — scheme, host, port, path, query2. Start the server with OPA enabled
mcp-v8 --stateless --http-port 3000 \
--opa-url http://localhost:8181 \
--opa-fetch-policy mcp/fetch
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:
.ok, .status, .statusText, .url, .redirected, .type, .bodyUsed.text(), .json(), .clone() (.text() and .json() return Promises).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.
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. This is useful for injecting authentication tokens, API keys, or other credentials without embedding them 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.
--fetch-header)Use --fetch-header to define rules inline. The format is:
host=<host>,header=<name>,value=<val>[,methods=GET;POST]
host — Host to match (exact or wildcard, see below). Required.header — Header name to inject. Required.value — Header value to inject. Required.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 --opa-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"
--fetch-header-config)For managing many rules, use a JSON config file. Each rule can inject multiple headers at once:
[
{
"host": "api.github.com",
"methods": ["GET", "POST"],
"headers": {
"Authorization": "Bearer ghp_xxxx",
"X-GitHub-Api-Version": "2022-11-28"
}
},
{
"host": "*.example.com",
"headers": {
"X-API-Key": "secret123"
}
}
]
mcp-v8 --stateless --opa-url http://localhost:8181 \
--fetch-header-config headers.json
Both --fetch-header and --fetch-header-config can be used together — their rules are merged.
api.github.com matches only api.github.com.*.github.com matches api.github.com, github.com, and sub.api.github.com.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)
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 accesseddestination: (optional) the destination path for operations like rename and copyFilerecursive: (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);
.wasm FilesInstead 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
You can configure heap storage using the following command line arguments:
--s3-bucket <bucket>mcp-v8 --s3-bucket my-bucket-name--s3-bucket <bucket> --cache-dir <path>mcp-v8 --s3-bucket my-bucket-name --cache-dir /tmp/mcp-v8-cache--directory-path <path>mcp-v8 --directory-path /tmp/mcp-v8-heaps--statelessmcp-v8 --statelessNote: Only one storage option can be used at a time. If multiple are provided, the server will return an error.
setTimeout and setInterval are not available.window, document, or other browser-specific objects.If you prefer to build from source instead of using the install script:
cd server
cargo build --release
The built binary will be located at server/target/release/server. You can use this path in the integration steps above instead of /usr/local/bin/mcp-v8 if desired.
Comparison of single-node vs 3-node cluster at various request rates.
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 |
| 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 | ████████████████████████ |
single = 1 MCP-V8 node; cluster = 3 MCP-V8 nodes with RaftДобавь это в claude_desktop_config.json и перезапусти Claude Desktop.
{
"mcpServers": {
"r33drichards-mcp-js": {
"command": "npx",
"args": []
}
}
}