Skip to content

feat: add MCP server example for sandboxed JavaScript execution#35

Open
simongdavies wants to merge 2 commits intomainfrom
add-mcp-example
Open

feat: add MCP server example for sandboxed JavaScript execution#35
simongdavies wants to merge 2 commits intomainfrom
add-mcp-example

Conversation

@simongdavies
Copy link
Contributor

Add an MCP (Model Context Protocol) server that exposes an execute_javascript tool, allowing AI agents to run arbitrary JavaScript inside an isolated Hyperlight micro-VM sandbox with strict CPU time limits and automatic snapshot/restore recovery after timeouts.

Includes server implementation, demo scripts (PowerShell and Bash), vitest test suite, and documentation.

@simongdavies simongdavies added the kind/enhancement New feature or improvement label Mar 3, 2026
Add an MCP (Model Context Protocol) server that exposes an
execute_javascript tool, allowing AI agents to run arbitrary JavaScript
inside an isolated Hyperlight micro-VM sandbox with strict CPU time
limits and automatic snapshot/restore recovery after timeouts.

Includes server implementation, demo scripts (PowerShell and Bash),
vitest test suite, and documentation.

Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new example MCP (Model Context Protocol) server under src/js-host-api/examples/mcp-server that lets MCP clients execute JavaScript inside a Hyperlight sandbox with configurable resource limits, plus demo scripts, documentation, and a Vitest-based integration test suite.

Changes:

  • Introduces an MCP stdio server (execute_javascript) that compiles/runs JS inside a reusable Hyperlight sandbox with CPU + wall-clock timeouts, snapshot/restore recovery, and optional timing/code logs.
  • Adds Vitest config + multiple integration-style test suites covering tool behavior, timeouts/recovery, env-var configurability, and timing log output.
  • Adds end-to-end demo scripts (bash + PowerShell) and a README describing setup and client configuration.

Reviewed changes

Copilot reviewed 11 out of 13 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/js-host-api/examples/mcp-server/server.js MCP server implementation; sandbox lifecycle, limits, logging, and tool registration.
src/js-host-api/examples/mcp-server/package.json Example package definition with MCP SDK, Zod, and Vitest.
src/js-host-api/examples/mcp-server/vitest.config.js Vitest configuration for the example’s tests and timeouts.
src/js-host-api/examples/mcp-server/tests/mcp-server.test.js End-to-end MCP protocol/tool integration tests via stdio NDJSON.
src/js-host-api/examples/mcp-server/tests/config.test.js Tests for env-configurable limits, defaults, and stderr warnings.
src/js-host-api/examples/mcp-server/tests/timing.test.js Tests for HYPERLIGHT_TIMING_LOG JSONL output and timing fields.
src/js-host-api/examples/mcp-server/tests/prompt-examples.test.js Large suite validating outputs for “README prompt” examples.
src/js-host-api/examples/mcp-server/demo-copilot-cli.sh Bash demo script to run prompts via Copilot CLI with MCP config.
src/js-host-api/examples/mcp-server/demo-copilot-cli.ps1 PowerShell demo script to run prompts via Copilot CLI with MCP config.
src/js-host-api/examples/mcp-server/README.md End-user documentation for the example server and demos.
src/js-host-api/eslint.config.mjs Adds performance as an allowed global (used by the new server).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +52 to +78
function waitForResponse(proc) {
return new Promise((resolve, reject) => {
let buffer = '';

const onData = (chunk) => {
buffer += chunk.toString();

// Look for a complete line (NDJSON delimiter)
const newlineIdx = buffer.indexOf('\n');
if (newlineIdx === -1) return; // need more data

const line = buffer.slice(0, newlineIdx).replace(/\r$/, '');
buffer = buffer.slice(newlineIdx + 1);

proc.stdout.off('data', onData);

if (line.length === 0) return; // skip empty lines

try {
resolve(JSON.parse(line));
} catch (_err) {
reject(new Error(`Invalid JSON from server: ${line}`));
}
};

proc.stdout.on('data', onData);
});
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waitForResponse keeps its buffer local to a single call and removes the data handler as soon as it parses the first newline. If multiple NDJSON messages arrive in one stdout chunk, any extra lines are dropped and the next waitForResponse call can hang waiting for a message that was already read. Consider implementing a per-process line queue/reader that preserves leftover buffered data across calls.

Copilot uses AI. Check for mistakes.
Comment on lines +183 to +192
if (installMode) {
// Permanent install: fixed well-known paths so Copilot can
// spawn the server with predictable log locations.
// \"Roads? Where we're going, we don't need roads.\" — Back to the Future (1985)
env.HYPERLIGHT_TIMING_LOG = '/tmp/hyperlight-timing.jsonl';
env.HYPERLIGHT_CODE_LOG = '/tmp/hyperlight-code.js';
} else {
if (process.env.HYPERLIGHT_TIMING_LOG) env.HYPERLIGHT_TIMING_LOG = process.env.HYPERLIGHT_TIMING_LOG;
if (process.env.HYPERLIGHT_CODE_LOG) env.HYPERLIGHT_CODE_LOG = process.env.HYPERLIGHT_CODE_LOG;
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In install mode, the script writes a permanent Copilot MCP config that always sets HYPERLIGHT_CODE_LOG to a fixed path. That means every future Copilot session using this config will persist all executed JS to disk by default, which is a privacy/security footgun and can grow unbounded over time. Consider making code logging opt-in (only set HYPERLIGHT_CODE_LOG when --show-code is requested, or gate it behind a separate --enable-code-log flag) and/or add rotation/truncation guidance.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +83
/** Prompt: "Calculate π to 50 decimal places using the Bailey–Borwein–Plouffe formula" */
const PI_50_DIGITS_CODE = `
// Machin's formula: π/4 = 4·arctan(1/5) - arctan(1/239)
// (BBP naturally produces hex digits; Machin is better for decimal output)
// Using BigInt for arbitrary-precision fixed-point arithmetic.
const DIGITS = 50;
const SCALE = 10n ** BigInt(DIGITS + 10); // extra precision buffer

function arccot(x) {
const bx = BigInt(x);
const x2 = bx * bx;
let power = SCALE / bx; // 1/x at our scale
let sum = power;
for (let n = 1; n < 120; n++) {
power = -power / x2;
const term = power / BigInt(2 * n + 1);
if (term === 0n) break;
sum += term;
}
return sum;
}

// π = 4 × (4·arccot(5) - arccot(239))
const pi = 4n * (4n * arccot(5) - arccot(239));
const s = pi.toString();
const formatted = s[0] + '.' + s.slice(1, DIGITS + 1);
return { pi: formatted, digits: DIGITS, method: 'Machin formula with BigInt' };
`;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file claims each constant implements the corresponding README prompt, but several prompt labels don't match the code (e.g., this π example says “Bailey–Borwein–Plouffe formula” while the implementation and method field are Machin). To keep the tests/documentation trustworthy, either update the prompt text to match what’s implemented or update the code to actually follow the prompt.

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 13 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +145 to +148
const builder = new SandboxBuilder();
builder.setHeapSize(HEAP_SIZE_BYTES);
builder.setStackSize(STACK_SIZE_BYTES);

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SandboxBuilder in js-host-api exposes setScratchSize() (scratch includes the stack), but there is no setStackSize() method. Calling builder.setStackSize(...) will throw at runtime and prevent the server/tests from starting. Use setScratchSize(...) instead (and consider renaming the env var/description from “stack” to “scratch/stack”).

Copilot uses AI. Check for mistakes.
// timeout or unrecoverable error, jsSandbox is set to null and
// rebuilt on the next call.

/** @type {import('../../index.d.ts').JSSandbox | null} */
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This JSDoc type reference points at ../../index.d.ts, but there is no src/js-host-api/index.d.ts in the repo. This makes the annotation misleading/broken for editors. Point it at an existing type source (or remove the import-based type).

Suggested change
/** @type {import('../../index.d.ts').JSSandbox | null} */
/** @type {any | null} */

Copilot uses AI. Check for mistakes.
# Clean up temp files
Remove-Item $mcpTmp -ErrorAction SilentlyContinue
Remove-Item $timingLog -ErrorAction SilentlyContinue
Remove-Item $promptFile -ErrorAction SilentlyContinue
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$promptFile is never defined in this function, but it’s referenced under Set-StrictMode -Version Latest. That will throw (“variable cannot be retrieved because it has not been set”) and break the demo script. Remove this cleanup line or initialize $promptFile = $null (and only remove when it was created).

Suggested change
Remove-Item $promptFile -ErrorAction SilentlyContinue

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +67
function waitForResponse(proc) {
return new Promise((resolve, reject) => {
let buffer = '';

const onData = (chunk) => {
buffer += chunk.toString();

// Look for a complete line (NDJSON delimiter)
const newlineIdx = buffer.indexOf('\n');
if (newlineIdx === -1) return; // need more data

const line = buffer.slice(0, newlineIdx).replace(/\r$/, '');
buffer = buffer.slice(newlineIdx + 1);

proc.stdout.off('data', onData);

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waitForResponse() only reads up to the first newline in the current stdout chunk and then detaches the listener, dropping any additional NDJSON messages that arrived in the same chunk. This can make the integration tests flaky. Use a persistent per-process line buffer (like the WeakMap approach used in the other test files here) so extra lines aren’t lost across calls.

Copilot uses AI. Check for mistakes.
Comment on lines +499 to +503
> **"Calculate π to 50 decimal places using the Bailey–Borwein–Plouffe formula"**
>
> Tests: BigInt arithmetic, series computation, precision handling

> **"Find all prime numbers below 10,000 using the Sieve of Eratosthenes and return the count and the last 10 primes"**
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example prompt says “Bailey–Borwein–Plouffe formula”, but the rest of this example/test suite uses Machin’s formula for decimal digits. Either update the prompt text to Machin’s formula (recommended for decimal output) or update the implementation examples to actually use BBP (and clarify the digit/base differences).

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +100
/** Prompt: "Calculate π to 50 decimal places using the Bailey–Borwein–Plouffe formula" */
const PI_50_DIGITS_CODE = `
// Machin's formula: π/4 = 4·arctan(1/5) - arctan(1/239)
// (BBP naturally produces hex digits; Machin is better for decimal output)
// Using BigInt for arbitrary-precision fixed-point arithmetic.
const DIGITS = 50;
const SCALE = 10n ** BigInt(DIGITS + 10); // extra precision buffer
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prompt comment says “BBP formula”, but the implementation below is Machin’s formula (and the returned method field also says Machin). Align the comment/prompt with the actual algorithm to avoid confusion when maintaining these examples.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/enhancement New feature or improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants