diff --git a/src/js-host-api/eslint.config.mjs b/src/js-host-api/eslint.config.mjs index b4eeb37..07f7582 100644 --- a/src/js-host-api/eslint.config.mjs +++ b/src/js-host-api/eslint.config.mjs @@ -20,6 +20,7 @@ export default [ clearTimeout: 'readonly', setInterval: 'readonly', clearInterval: 'readonly', + performance: 'readonly', }, }, rules: { diff --git a/src/js-host-api/examples/mcp-server/README.md b/src/js-host-api/examples/mcp-server/README.md new file mode 100644 index 0000000..1c333c2 --- /dev/null +++ b/src/js-host-api/examples/mcp-server/README.md @@ -0,0 +1,721 @@ +# ๐Ÿ”’ Hyperlight JS โ€” MCP Server Example + +> _"The only winning move is to play... inside a sandbox."_ โ€” WarGames (1983), adapted + +An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that +lets AI agents execute JavaScript code inside a +[Hyperlight](https://github.com/deislabs/hyperlight-js) micro-VM sandbox +with strict CPU time limits. + +

+ Demo: Copilot CLI running JavaScript in a Hyperlight sandbox +

+ +## What It Does + +This MCP server exposes a single tool โ€” **`execute_javascript`** โ€” that: + +1. Takes arbitrary JavaScript source code from an AI agent +2. Executes it inside an isolated Hyperlight sandbox (no filesystem, no network, no host access) +3. Enforces a **configurable CPU time limit** (default 1000ms, with a 5000ms wall-clock backstop) +4. Returns the result (or a timeout/error message) back to the agent + +The sandbox automatically recovers after timeouts via snapshot/restore, +so subsequent invocations work without manual intervention. + +## Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ AI Agent โ”‚ (Copilot Chat, Copilot CLI, Claude Desktop, Cursor, etc.) +โ”‚ "Calculate ฯ€ to โ”‚ +โ”‚ 100 digits" โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ MCP (stdio) + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ MCP Server โ”‚ server.js โ€” @modelcontextprotocol/sdk +โ”‚ execute_javascript โ”‚ +โ”‚ tool handler โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ callHandler({ cpuTimeoutMs: 1000 }) + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Hyperlight Sandbox โ”‚ Isolated micro-VM +โ”‚ QuickJS Engine โ”‚ No I/O, no host access +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ User's JS codeโ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Prerequisites + +### 1. Build Hyperlight JS + +From the repository root: + +```bash +# Build the runtime and native module (recommended) +just build-js-host-api release +``` + +Or manually: + +```bash +# Build the runtime binary +just build release + +# Build the Node.js native module +cd src/js-host-api +npm install +npm run build +``` + +### 2. Install MCP Server Dependencies + +```bash +cd src/js-host-api/examples/mcp-server +npm install +``` + +### 3. Verify It Works + +```bash +# Run the smoke test suite +npm test +``` + +You should see all tests pass, including timeout enforcement and recovery. + +## Client Configuration + +### VS Code โ€” GitHub Copilot Chat + +Add to your workspace `.vscode/mcp.json`: + +```json +{ + "servers": { + "hyperlight-sandbox": { + "type": "stdio", + "command": "node", + "args": ["src/js-host-api/examples/mcp-server/server.js"] + } + } +} +``` + +Or add to your VS Code `settings.json`: + +```json +{ + "mcp": { + "servers": { + "hyperlight-sandbox": { + "type": "stdio", + "command": "node", + "args": ["src/js-host-api/examples/mcp-server/server.js"], + "cwd": "${workspaceFolder}" + } + } + } +} +``` + +Then in Copilot Chat, the `execute_javascript` tool will be available. +Use **Agent mode** (`@workspace` or the agent panel) to interact with MCP tools. + +### Claude Desktop + +Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) +or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): + +```json +{ + "mcpServers": { + "hyperlight-sandbox": { + "command": "node", + "args": [ + "/absolute/path/to/hyperlight-js/src/js-host-api/examples/mcp-server/server.js" + ] + } + } +} +``` + +Restart Claude Desktop after editing the config. + +### Cursor + +Add to your Cursor MCP settings (Settings โ†’ MCP Servers โ†’ Add): + +```json +{ + "mcpServers": { + "hyperlight-sandbox": { + "command": "node", + "args": [ + "/absolute/path/to/hyperlight-js/src/js-host-api/examples/mcp-server/server.js" + ] + } + } +} +``` + +### Claude CLI + +```bash +claude mcp add hyperlight-sandbox -- node /absolute/path/to/server.js +``` + +### GitHub Copilot CLI + +The new [GitHub Copilot CLI](https://github.com/github/copilot-cli) (`copilot` command) +supports MCP servers via `~/.copilot/mcp-config.json`. + +**Option A โ€” Interactive setup** (inside a `copilot` session): + +``` +/mcp add +``` + +Fill in the fields (name: `hyperlight-sandbox`, command: `node`, args: path to +`server.js`) and press Ctrl+S to save. + +**Option B โ€” Manual config** โ€” edit (or create) `~/.copilot/mcp-config.json`: + +```json +{ + "mcpServers": { + "hyperlight-sandbox": { + "type": "stdio", + "command": "node", + "args": [ + "/absolute/path/to/hyperlight-js/src/js-host-api/examples/mcp-server/server.js" + ] + } + } +} +``` + +Then start a session and try a prompt: + +```bash +copilot +# > Write a function that computes all prime factors of a number, run it on 123456789 +``` + +> **Tip:** Use `--allow-tool 'hyperlight-sandbox'` to auto-approve the sandbox +> tool without per-call prompts. + +#### Demo Script + +Ready-made demo scripts are included for both **Linux/macOS** (bash) and +**Windows** (PowerShell 7+) to demonstrate the Copilot CLI integration +end-to-end โ€” no manual config required. + +##### Linux / macOS (bash) + +```bash +cd src/js-host-api/examples/mcp-server + +# Interactive mode โ€” walks you through each demo with pause-between-prompts +./demo-copilot-cli.sh + +# Headless mode โ€” runs all demos non-interactively (CI-friendly) +./demo-copilot-cli.sh --headless + +# Run a single custom prompt +./demo-copilot-cli.sh --prompt "Calculate the first 100 Fibonacci numbers" --headless + +# Use a specific model (default: claude-opus-4.6) +./demo-copilot-cli.sh --model gpt-4o --headless + +# Show the JavaScript code the model generated +./demo-copilot-cli.sh --show-code --headless + +# Show the copilot CLI command being executed (for debugging/copying) +./demo-copilot-cli.sh --show-command --headless + +# Install the MCP server permanently into ~/.copilot/mcp-config.json +./demo-copilot-cli.sh --install + +# Remove it again +./demo-copilot-cli.sh --uninstall +``` + +##### Windows (PowerShell 7+) + +```powershell +cd src\js-host-api\examples\mcp-server + +# Interactive mode +.\demo-copilot-cli.ps1 + +# Headless mode โ€” runs all demos non-interactively +.\demo-copilot-cli.ps1 -Mode Headless + +# Run a single custom prompt +.\demo-copilot-cli.ps1 -Prompt "Calculate the first 100 Fibonacci numbers" -Mode Headless + +# Use a specific model +.\demo-copilot-cli.ps1 -Model gpt-4o -Mode Headless + +# Show the JavaScript code the model generated +.\demo-copilot-cli.ps1 -ShowCode -Mode Headless + +# Show the copilot CLI command being executed +.\demo-copilot-cli.ps1 -ShowCommand -Mode Headless + +# Combine flags freely +.\demo-copilot-cli.ps1 -Prompt "Solve 8-queens" -ShowCode -ShowCommand -Model gpt-4o -Mode Headless + +# Custom sandbox limits +.\demo-copilot-cli.ps1 -CpuTimeout 2000 -HeapSize 32 -Mode Headless + +# Install the MCP server permanently +.\demo-copilot-cli.ps1 -Mode Install + +# Remove it again +.\demo-copilot-cli.ps1 -Mode Uninstall +``` + +> **Note:** The PowerShell script requires PowerShell 7+ (`pwsh`). It is +> **not** compatible with Windows PowerShell 5.1 (`powershell.exe`). + +##### Parameter reference + +| Bash flag | PowerShell param | Description | +| ------------------- | ------------------- | -------------------------------------------------------- | +| `--headless` | `-Mode Headless` | Non-interactive mode โ€” runs and exits (CI-friendly) | +| `--install` | `-Mode Install` | Install MCP config permanently | +| `--uninstall` | `-Mode Uninstall` | Remove MCP config | +| `--prompt ` | `-Prompt ` | Run a single custom prompt instead of built-in demos | +| `--model ` | `-Model ` | LLM model to use (default: `claude-opus-4.6`) | +| `--show-code` | `-ShowCode` | Display the generated JavaScript source code | +| `--show-command` | `-ShowCommand` | Display the copilot CLI command line being executed | +| `--cpu-timeout `| `-CpuTimeout ` | CPU time limit per execution (default: 1000ms) | +| `--wall-timeout `| `-WallTimeout `| Wall-clock backstop per execution (default: 5000ms) | +| `--heap-size ` | `-HeapSize ` | Guest heap size (default: 16MB) | +| `--stack-size ` | `-StackSize ` | Guest stack size (default: 1MB) | + +**What the script does:** + +1. Checks prerequisites (Node.js, Copilot CLI, built native addon) +2. Creates a temporary MCP config for the session (or installs permanently with `--install`) +3. Runs three demo prompts through Copilot CLI's programmatic mode (`-p`): + - **ฯ€ calculation** โ€” Machin formula to 50 decimal places + - **Sieve of Eratosthenes** โ€” all primes below 10,000 + - **Maze generation** โ€” 25ร—25 recursive backtracking maze as ASCII art +4. Displays a per-prompt timing breakdown (model generation vs. tool execution) +5. Reports pass/fail results + +**Security model:** + +The script uses the [documented](https://docs.github.com/copilot/concepts/agents/about-copilot-cli) +`--allow-all-tools` + `--deny-tool` pattern: + +| Flag | Purpose | +| -------------------------- | ----------------------------------------------------- | +| `--allow-all-tools` | Required for `-p` (non-interactive) mode | +| `--deny-tool 'shell'` | Blocks **all** shell command execution | +| `--deny-tool 'write'` | Blocks **all** file write/edit operations | +| `--deny-tool 'read'` | Blocks **all** file read operations | +| `--deny-tool 'fetch'` | Blocks **all** web fetch/HTTP operations | +| `-s` | Silent โ€” agent response only, no stats or retry noise | +| `--disable-builtin-mcps` | Removes the GitHub MCP server | +| `--no-custom-instructions` | Ignores workspace AGENTS.md / copilot-instructions.md | +| `--no-ask-user` | No clarifying questions in programmatic mode | +| `--model ` | LLM model to use (default: `claude-opus-4.6`) | + +`--deny-tool` takes precedence over `--allow-all-tools`, so the agent can +_only_ call our MCP sandbox tool โ€” no shell access, no file writes, no file reads, no web fetches. + +**Model selection:** + +The `--model` / `-Model` flag selects which LLM model the Copilot CLI uses +(default: `claude-opus-4.6`): + +```bash +# Bash +./demo-copilot-cli.sh --model gpt-4o --headless +./demo-copilot-cli.sh --model claude-sonnet-4 --headless +``` + +```powershell +# PowerShell +.\demo-copilot-cli.ps1 -Model gpt-4o -Mode Headless +.\demo-copilot-cli.ps1 -Model claude-sonnet-4 -Mode Headless +``` + +**Timing & observability:** + +After each prompt, the script displays a timing breakdown showing where time +was spent: + +``` +โฑ Timing breakdown: +โฑ Copilot CLI (total round-trip) 12.345s + ๐Ÿค– Model 10.200s (LLM code generation + response) + ๐Ÿ”ง Tool execution 0.145s (MCP tool total) + โ”œโ”€ Sandbox init: 120ms + โ”œโ”€ Handler setup: 2ms + โ”œโ”€ Compile & load: 8ms + โ”œโ”€ Snapshot: 5ms + โ””โ”€ JS execution: 10ms +``` + +- **Model time** is derived by subtracting tool execution time from the total + Copilot CLI round-trip. It includes LLM inference, code generation, and + response formatting. +- **Tool execution** is measured server-side by the MCP server, broken down + into sandbox init (first call only), handler setup, compilation, snapshot, + and actual JavaScript execution. +- The MCP server writes timing data to a JSON-lines file via the + `HYPERLIGHT_TIMING_LOG` environment variable (set automatically by the + demo script). + +**Code inspection (`--show-code` / `-ShowCode`):** + +Display the JavaScript source that the model generated and sent to the sandbox: + +```bash +# Bash +./demo-copilot-cli.sh --show-code --headless +./demo-copilot-cli.sh --show-code --model gpt-4o +``` + +```powershell +# PowerShell +.\demo-copilot-cli.ps1 -ShowCode -Mode Headless +.\demo-copilot-cli.ps1 -ShowCode -Model gpt-4o -Mode Headless +``` + +The generated code is displayed between the Copilot CLI output and the timing +breakdown: + +``` +๐Ÿ“ Generated code: +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const DIGITS = 50; + const SCALE = 10n ** BigInt(DIGITS + 10); + function arccot(x) { ... } + ... + return { pi: formatted }; +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +``` + +This is useful for comparing how different models approach the same problem, +or debugging when results are unexpected. The server writes the received code +to a temp file via the `HYPERLIGHT_CODE_LOG` environment variable (set +automatically by the demo script when `--show-code` / `-ShowCode` is active). + +**Custom prompts (`--prompt` / `-Prompt`):** + +Run a single custom prompt instead of the built-in demo set: + +```bash +# Bash โ€” headless custom prompt +./demo-copilot-cli.sh --prompt "Implement quicksort and sort 1000 random numbers" --headless + +# Bash โ€” interactive: runs your prompt first, then offers built-in demos +./demo-copilot-cli.sh --prompt "Solve the 8-queens problem" +``` + +```powershell +# PowerShell โ€” headless custom prompt +.\demo-copilot-cli.ps1 -Prompt "Implement quicksort and sort 1000 random numbers" -Mode Headless + +# PowerShell โ€” interactive: runs your prompt first, then offers built-in demos +.\demo-copilot-cli.ps1 -Prompt "Solve the 8-queens problem" +``` + +In **headless** mode with `--prompt` / `-Prompt`, only the custom prompt runs +and the script exits. In **interactive** mode the behaviour is the same โ€” the +custom prompt runs and the script exits (built-in demos are skipped). + +**Command inspection (`--show-command` / `-ShowCommand`):** + +Display the full copilot CLI command being executed for each prompt. Useful for +debugging or copying the command to run manually: + +```bash +./demo-copilot-cli.sh --show-command --headless +``` + +```powershell +.\demo-copilot-cli.ps1 -ShowCommand -Mode Headless +``` + +Output (when MCP server is not yet installed, with non-default sandbox limits): + +``` +๐Ÿ”ง Copy-pasteable command: + +โš  The MCP server must be installed before this command will work. + Install it now: + + ./demo-copilot-cli.sh --install --cpu-timeout 5000 --heap-size 32 + + To remove it later: + + ./demo-copilot-cli.sh --uninstall + + copilot \ + -p '' \ + -s \ + --allow-all-tools \ + --deny-tool shell \ + --deny-tool write \ + --deny-tool read \ + --deny-tool fetch \ + --no-custom-instructions \ + --no-ask-user \ + --disable-builtin-mcps \ + --model claude-opus-4.6 +``` + +Once installed, only the command is shown (no warning). + +### Any MCP-Compatible Client + +The server uses **stdio transport** โ€” launch it as a child process and +communicate via NDJSON (newline-delimited JSON) over stdin/stdout: + +```bash +# Each message is JSON.stringify(msg) + '\n' +node /path/to/server.js +``` + +## Example Prompts ๐ŸŽฏ + +Here are some creative prompts to try with your AI agent. Each one will +generate JavaScript, send it to the Hyperlight sandbox via the MCP tool, +and return the result. + +### ๐Ÿ”ข Mathematics + +> **"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"** +> +> Tests: Array operations, algorithmic efficiency, memory usage + +> **"Compute the first 100 digits of Euler's number (e) using the Taylor series"** +> +> Tests: Factorial computation, convergence, floating-point handling + +> **"Run a Monte Carlo simulation with 100,000 random dart throws to estimate ฯ€"** +> +> Tests: Random number generation, statistical methods, loop performance + +### ๐Ÿงฎ Algorithms & Data Structures + +> **"Implement quicksort and mergesort, sort an array of 5,000 random numbers with each, and compare their execution times"** +> +> Tests: Sorting algorithms, Date.now() timing, recursion depth + +> **"Solve the Tower of Hanoi for 15 disks โ€” return the total number of moves and the first 10 moves"** +> +> Tests: Recursive algorithms, exponential growth (2ยนโต - 1 = 32,767 moves) + +> **"Find the longest common subsequence of 'AGGTAB' and 'GXTXAYB' using dynamic programming"** +> +> Tests: 2D array operations, DP table construction + +> **"Implement a trie data structure, insert 1000 random 8-letter words, then search for 100 of them and measure lookup time"** +> +> Tests: Object/Map construction, string manipulation, performance measurement + +### ๐ŸŽจ Creative & Visual + +> **"Generate a Sierpinski triangle as ASCII art with depth 5"** +> +> Tests: Recursive patterns, string building, spatial reasoning + +> **"Create a text-based Mandelbrot set visualization using ASCII characters for a 60ร—30 grid"** +> +> Tests: Complex number arithmetic, nested loops, character mapping + +> **"Generate a maze using recursive backtracking on an 21ร—21 grid and render it as ASCII"** +> +> Tests: Graph traversal, random selection, 2D grid manipulation + +### ๐Ÿ” Cryptography & Encoding + +> **"Implement a Caesar cipher, encrypt 'HELLO WORLD' with shift 13 (ROT13), then decrypt it back"** +> +> Tests: Character code manipulation, string transformation, round-trip verification + +> **"Convert the first 20 Fibonacci numbers to different bases (binary, octal, hex) and return a formatted table"** +> +> Tests: Number base conversion, string formatting, data presentation + +### ๐Ÿงฌ Simulations + +> **"Simulate Conway's Game of Life on a 30ร—30 grid for 50 generations, starting with a random pattern. Return the final grid and population count per generation"** +> +> Tests: 2D array operations, cellular automata rules, state tracking + +> **"Simulate a simple particle system: 100 particles with random velocities bouncing inside a 100ร—100 box for 1000 timesteps. Return the final positions and total collisions"** +> +> Tests: Physics simulation, collision detection, numerical computation + +> **"Model a simple predator-prey ecosystem (Lotkaโ€“Volterra equations) with Euler's method for 1000 timesteps"** +> +> Tests: Differential equations, numerical methods, data collection + +### ๐Ÿงช Brain Teasers + +> **"Solve the 8-queens problem and return all 92 unique solutions"** +> +> Tests: Backtracking, constraint satisfaction, combinatorial search + +> **"Generate all valid combinations of balanced parentheses for n=8 and count them (should be Catalan number Cโ‚ˆ = 1430)"** +> +> Tests: Recursive generation, Catalan numbers, combinatorics + +> **"Find all Pythagorean triples where aยฒ + bยฒ = cยฒ and c < 500"** +> +> Tests: Number theory, nested loop optimization, mathematical verification + +## How the Code Execution Works + +When the AI agent calls `execute_javascript`, the server: + +1. **Wraps** the code as the body of a `handler(event)` function +2. **Loads** it into the Hyperlight sandbox (QuickJS engine inside a micro-VM) +3. **Snapshots** the sandbox state (for recovery after timeouts) +4. **Executes** with `cpuTimeoutMs: 1000` and `wallClockTimeoutMs: 5000` +5. **Returns** the JSON-serializable result, or an error message +6. **Recovers** automatically if execution times out (snapshot/restore) +7. **Logs timing** (if `HYPERLIGHT_TIMING_LOG` is set) โ€” a JSON-lines record + with `initMs`, `setupMs`, `compileMs`, `snapshotMs`, `executeMs`, and `totalMs` +8. **Logs code** (if `HYPERLIGHT_CODE_LOG` is set) โ€” writes the received + JavaScript source to the specified file for inspection + +### Writing Code for the Sandbox + +The code runs as a function body. Use `return` to produce output: + +```javascript +// โœ… Simple computation +let x = 2 + 2; +return { answer: x }; + +// โœ… Complex computation +const primes = []; +for (let n = 2; primes.length < 100; n++) { + let isPrime = true; + for (let d = 2; d * d <= n; d++) { + if (n % d === 0) { + isPrime = false; + break; + } + } + if (isPrime) primes.push(n); +} +return { first100Primes: primes, count: primes.length }; + +// โŒ This won't work โ€” no I/O +fetch('https://example.com'); // fetch is not available +require('fs'); // require is not available +console.log('hello'); // console is not available +``` + +## Security + +The Hyperlight sandbox provides **hardware-level isolation**: + +- ๐Ÿ”’ **No filesystem access** โ€” can't read or write files +- ๐ŸŒ **No network access** โ€” can't make HTTP requests +- ๐Ÿ–ฅ๏ธ **No host access** โ€” can't access environment variables, processes, or system calls +- โฑ๏ธ **CPU bounded** โ€” configurable limit (default 1000ms), enforced by the hypervisor +- ๐Ÿ’พ **Memory bounded** โ€” configurable (default 16MB heap, 1MB stack) +- ๐Ÿ”„ **Automatic recovery** โ€” sandbox rebuilds after failures + +This makes it safe to execute untrusted, AI-generated code. + +## Environment Variables + +| Variable | Default | Description | +| ------------------------------ | -------- | ---------------------------------------------------------------------------------- | +| `HYPERLIGHT_CPU_TIMEOUT_MS` | `1000` | Maximum CPU time per execution (milliseconds). The hypervisor hard-kills the guest when exceeded. | +| `HYPERLIGHT_WALL_TIMEOUT_MS` | `5000` | Maximum wall-clock time per execution (milliseconds). Backstop for edge cases where CPU time alone doesn't catch the issue. | +| `HYPERLIGHT_HEAP_SIZE_MB` | `16` | Guest heap size in megabytes. Increase for memory-heavy computations (large arrays, BigInt work). | +| `HYPERLIGHT_STACK_SIZE_MB` | `1` | Guest stack size in megabytes. Increase for deeply recursive algorithms. | +| `HYPERLIGHT_TIMING_LOG` | โ€” | Path to a file. When set, the server appends one JSON line per tool call with a timing breakdown (init, setup, compile, snapshot, execute, total). Used by the demo script to show model vs. tool time. | +| `HYPERLIGHT_CODE_LOG` | โ€” | Path to a file. When set, the server writes the received JavaScript source code on each tool call. Used by the demo script's `--show-code` flag. | + +Example โ€” tighten limits for a multi-tenant deployment: + +```bash +HYPERLIGHT_CPU_TIMEOUT_MS=500 HYPERLIGHT_HEAP_SIZE_MB=8 node server.js +``` + +## Troubleshooting + +### "Cannot find module '../../lib.js'" + +The native module hasn't been built. Run from the repo root: + +```bash +just build-js-host-api release +``` + +### "Execution timed out" + +The code exceeded the CPU time limit (default: 1000ms). Options: + +- **Increase the timeout** โ€” set the `HYPERLIGHT_CPU_TIMEOUT_MS` environment + variable, or use the demo script's `--cpu-timeout` / `-CpuTimeout` flag: + + ```bash + # Bash โ€” 5 second CPU limit + ./demo-copilot-cli.sh --cpu-timeout 5000 --headless + + # Or via environment variable (works with any MCP client) + HYPERLIGHT_CPU_TIMEOUT_MS=5000 node server.js + ``` + + ```powershell + # PowerShell โ€” 5 second CPU limit + .\demo-copilot-cli.ps1 -CpuTimeout 5000 -Mode Headless + + # Or via environment variable + $env:HYPERLIGHT_CPU_TIMEOUT_MS = 5000; node server.js + ``` + +- **Increase the wall-clock backstop** โ€” if the CPU limit is fine but the + overall execution is being killed, raise `HYPERLIGHT_WALL_TIMEOUT_MS` + (default: 5000ms) via `--wall-timeout` / `-WallTimeout`. + +- Reduce iteration counts or use more efficient algorithms +- Break the problem into smaller pieces + +### Server doesn't start + +Check that: + +1. Node.js >= 18 is installed +2. The native module is built (`ls src/js-host-api/js-host-api.*.node`) +3. Dependencies are installed (`cd examples/mcp-server && npm install`) + +## Files + +| File | Description | +| ------------------------------- | --------------------------------------------------------------------------------- | +| `server.js` | MCP server โ€” stdio transport, `execute_javascript` tool | +| `demo-copilot-cli.sh` | Bash demo script (Linux/macOS) โ€” see [Demo Script](#demo-script) | +| `demo-copilot-cli.ps1` | PowerShell demo script (Windows) โ€” see [Demo Script](#demo-script) | +| `demo.gif` | Animated demo of the Copilot CLI integration | +| `tests/mcp-server.test.js` | Vitest integration tests โ€” validates the server end-to-end | +| `tests/prompt-examples.test.js` | Vitest tests for all README example prompts | +| `tests/timing.test.js` | Vitest tests for timing log output (HYPERLIGHT_TIMING_LOG) | +| `tests/config.test.js` | Vitest tests for env-var configuration (custom limits, invalid values, fallbacks) | +| `vitest.config.js` | Vitest configuration | +| `package.json` | Dependencies and scripts | +| `README.md` | You are here ๐Ÿ“ | diff --git a/src/js-host-api/examples/mcp-server/demo-copilot-cli.ps1 b/src/js-host-api/examples/mcp-server/demo-copilot-cli.ps1 new file mode 100644 index 0000000..c8ea733 --- /dev/null +++ b/src/js-host-api/examples/mcp-server/demo-copilot-cli.ps1 @@ -0,0 +1,801 @@ +#Requires -Version 7.0 +<# +.SYNOPSIS + Hyperlight JS ร— GitHub Copilot CLI Demo (PowerShell edition) + +.DESCRIPTION + "Open the pod bay doors, HAL." โ€” 2001: A Space Odyssey (1968) + ... except this sandbox actually listens when you tell it to stop. + + This script demonstrates using the Hyperlight JS MCP server with + GitHub Copilot CLI. It: + 1. Verifies prerequisites (Copilot CLI + Node.js + built guest) + 2. Configures the MCP server (persistent or session-only) + 3. Runs example prompts via Copilot CLI programmatic mode + +.PARAMETER Mode + Operating mode: Interactive (default), Headless, Install, or Uninstall. + +.PARAMETER Model + LLM model to use (default: claude-opus-4.6). + +.PARAMETER Prompt + A custom prompt to run instead of the built-in demos. Runs the single + prompt and exits (in both Headless and Interactive modes). + +.PARAMETER ShowCode + Display the JavaScript code generated by the model. + +.PARAMETER ShowCommand + Display the copilot CLI command line being executed. Useful for debugging + or copying the command to run manually. + +.PARAMETER CpuTimeout + CPU time limit per execution in milliseconds (default: 1000). + +.PARAMETER WallTimeout + Wall-clock backstop per execution in milliseconds (default: 5000). + +.PARAMETER HeapSize + Guest heap size in megabytes (default: 16). + +.PARAMETER StackSize + Guest stack size in megabytes (default: 1). + +.EXAMPLE + .\demo-copilot-cli.ps1 + # Interactive โ€” walks you through it + +.EXAMPLE + .\demo-copilot-cli.ps1 -Mode Headless + # Non-interactive โ€” runs all demos + +.EXAMPLE + .\demo-copilot-cli.ps1 -Mode Install + # Install MCP config permanently + +.EXAMPLE + .\demo-copilot-cli.ps1 -Model gpt-4o -Mode Headless + # Use a specific model + +.EXAMPLE + .\demo-copilot-cli.ps1 -Prompt "Calculate the first 100 Fibonacci numbers" -Mode Headless + # Run a single custom prompt + +.EXAMPLE + .\demo-copilot-cli.ps1 -ShowCommand -Mode Headless + # Show the copilot CLI command being used + +.EXAMPLE + .\demo-copilot-cli.ps1 -CpuTimeout 2000 -HeapSize 32 -Mode Headless + # Custom sandbox limits +#> + +[CmdletBinding()] +param( + [ValidateSet('Interactive', 'Headless', 'Install', 'Uninstall')] + [string]$Mode = 'Interactive', + + [string]$Model = 'claude-opus-4.6', + + [string]$Prompt, + + [switch]$ShowCode, + + [switch]$ShowCommand, + + [ValidateRange(1, [int]::MaxValue)] + [int]$CpuTimeout = $(if ($env:HYPERLIGHT_CPU_TIMEOUT_MS) { [int]$env:HYPERLIGHT_CPU_TIMEOUT_MS } else { 1000 }), + + [ValidateRange(1, [int]::MaxValue)] + [int]$WallTimeout = $(if ($env:HYPERLIGHT_WALL_TIMEOUT_MS) { [int]$env:HYPERLIGHT_WALL_TIMEOUT_MS } else { 5000 }), + + [ValidateRange(1, [int]::MaxValue)] + [int]$HeapSize = $(if ($env:HYPERLIGHT_HEAP_SIZE_MB) { [int]$env:HYPERLIGHT_HEAP_SIZE_MB } else { 16 }), + + [ValidateRange(1, [int]::MaxValue)] + [int]$StackSize = $(if ($env:HYPERLIGHT_STACK_SIZE_MB) { [int]$env:HYPERLIGHT_STACK_SIZE_MB } else { 1 }) +) + +# โ”€โ”€ Strict Mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# โ”€โ”€ Encoding โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Ensure native commands (copilot, node) emit and receive UTF-8. +# Without this, Windows mangles ฯ€ โ†’ โ•งร‡, โ€” โ†’ ฮ“ร‡รถ, etc. +# "We're not in Kansas anymore." โ€” The Wizard of Oz (1939, close enough) +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$OutputEncoding = [System.Text.Encoding]::UTF8 + +# โ”€โ”€ Paths โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$ServerJs = Join-Path $ScriptDir 'server.js' + +# Copilot CLI config paths โ€” "These go to eleven." โ€” Spinal Tap (1984) +# Copilot CLI's --config-dir defaults to ~/.copilot on ALL platforms, +# NOT %APPDATA% on Windows. Using the wrong path means copilot never +# sees the installed config โ€” "Nobody puts Baby in the wrong corner." +# โ€” Dirty Dancing (1987) +if ($env:XDG_CONFIG_HOME) { + $CopilotConfigDir = $env:XDG_CONFIG_HOME +} else { + $CopilotConfigDir = Join-Path $HOME '.copilot' +} +$McpConfigFile = Join-Path $CopilotConfigDir 'mcp-config.json' +$McpServerName = 'hyperlight-sandbox' + +# โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function Write-Info { param([string]$Message) Write-Host "โ„น $Message" -ForegroundColor Cyan } +function Write-Ok { param([string]$Message) Write-Host "โœ… $Message" -ForegroundColor Green } +function Write-Warn { param([string]$Message) Write-Host "โš  $Message" -ForegroundColor Yellow } +function Write-Fail { param([string]$Message) Write-Host "โŒ $Message" -ForegroundColor Red; exit 1 } + +function Write-Banner { + Write-Host '' + Write-Host '๐Ÿ”’ Hyperlight JS ร— GitHub Copilot CLI Demo' -ForegroundColor Cyan + Write-Host '' +} + +function Write-Separator { + Write-Host '' + Write-Host 'โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€' -ForegroundColor DarkGray + Write-Host '' +} + +function Get-NowMs { + [long]([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()) +} + +function Write-Elapsed { + param([string]$Phase, [long]$StartMs) + $elapsed = (Get-NowMs) - $StartMs + $secs = [math]::Floor($elapsed / 1000) + $frac = $elapsed % 1000 + Write-Host ("โฑ {0,-40} {1}.{2:D3}s" -f $Phase, $secs, $frac) -ForegroundColor DarkGray +} + +# โ”€โ”€ Prerequisite Checks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function Test-Prerequisites { + Write-Info 'Checking prerequisites...' + + # Node.js + $nodeBin = Get-Command node -ErrorAction SilentlyContinue + if (-not $nodeBin) { Write-Fail 'Node.js is required but not installed. Install from https://nodejs.org' } + $nodeVersion = & node --version 2>&1 + Write-Ok "Node.js $nodeVersion" + + # Copilot CLI + $script:CopilotBin = $null + $copilotCmd = Get-Command copilot -ErrorAction SilentlyContinue + if ($copilotCmd) { + $script:CopilotBin = $copilotCmd.Source + } else { + # Check typical VS Code Server location (Linux remote / WSL) + $vscodePath = Join-Path $HOME '.vscode-server/data/User/globalStorage/github.copilot-chat/copilotCli/copilot' + if (Test-Path $vscodePath) { + $script:CopilotBin = $vscodePath + } + # Check typical Windows location + if (-not $script:CopilotBin -and ($IsWindows -or $env:OS -match 'Windows')) { + $winPath = Join-Path $env:LOCALAPPDATA 'Programs\copilot-cli\copilot.exe' + if (Test-Path $winPath) { $script:CopilotBin = $winPath } + } + } + if (-not $script:CopilotBin) { + Write-Fail 'GitHub Copilot CLI not found. Install with: npm install -g @github/copilot' + } + Write-Ok "Copilot CLI found at: $($script:CopilotBin)" + + # Server script + if (-not (Test-Path $ServerJs)) { Write-Fail "Server script not found at: $ServerJs" } + Write-Ok "MCP server: $ServerJs" + + # npm dependencies + $nodeModules = Join-Path $ScriptDir 'node_modules' + if (-not (Test-Path $nodeModules)) { + Write-Warn 'node_modules not found โ€” installing dependencies...' + # Delegate to cmd.exe to dodge PowerShell's long-standing issues + # with .cmd shim argument mangling. "Short Circuit" (1986) โ€” we + # need the real npm, not Johnny 5 having a stroke. + Push-Location $ScriptDir + try { + & cmd /c 'npm install' + if ($LASTEXITCODE -ne 0) { Write-Fail 'npm install failed' } + } finally { + Pop-Location + } + } + Write-Ok 'Dependencies installed' + + # Native addon smoke test โ€” verify the .node binary loads and has + # the methods we need. The old check started server.js and waited + # for a banner that never came (stdio transport blocks on connect). + # This just imports lib.js which validates the native addon at + # module load time. Instant, reliable, catches the exact failure. + # "Trust, but verify." โ€” Reagan (1987) + Write-Info 'Verifying native addon loads correctly...' + $libJs = Join-Path $ScriptDir '../../lib.js' + # node --input-type=module so we can use dynamic import() from stdin + $smokeResult = & node --input-type=module -e "import('file:///' + '$($libJs -replace '\\','/')').then(() => console.log('OK')).catch(e => { console.error(e.message); process.exit(1); })" 2>&1 + if ($LASTEXITCODE -ne 0) { + $errMsg = ($smokeResult | Out-String).Trim() + Write-Fail "Native addon failed to load โ€” rebuild with 'just build'.`n$errMsg" + } + Write-Ok 'Native addon loads successfully' + + Write-Host '' +} + +# โ”€โ”€ MCP Config Management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function Build-McpJson { + <# + .SYNOPSIS + Build the JSON MCP config, forwarding sandbox limits + observability + env vars. Copilot CLI spawns its own server process, so we must + embed config in the MCP config's "env" field. + + Uses native PowerShell JSON instead of node -e to avoid PS7's + native-command argument mangling with multi-line strings. + "Who you gonna call?" โ€” Ghostbusters (1984). ConvertTo-Json. + .PARAMETER ForInstall + When set, uses fixed well-known temp paths for HYPERLIGHT_TIMING_LOG + and HYPERLIGHT_CODE_LOG instead of per-session random paths. This + lets Copilot spawn the server with predictable log locations. + #> + param([switch]$ForInstall) + + $env:HYPERLIGHT_CPU_TIMEOUT_MS = $CpuTimeout + $env:HYPERLIGHT_WALL_TIMEOUT_MS = $WallTimeout + $env:HYPERLIGHT_HEAP_SIZE_MB = $HeapSize + $env:HYPERLIGHT_STACK_SIZE_MB = $StackSize + + $config = [ordered]@{ + mcpServers = [ordered]@{ + $McpServerName = (Build-McpServerEntry -ForInstall:$ForInstall) + } + } + + ConvertTo-Json $config -Depth 5 +} + +function Build-McpServerEntry { + <# + .SYNOPSIS + Build a single MCP server entry (type, command, args, env). + Shared by Build-McpJson (full config) and Install-McpConfig + (merge into existing config). + #> + param([switch]$ForInstall) + + # Build the env block โ€” sandbox limits always included, observability + # vars only when set AND this is not a permanent install. + $envHash = [ordered]@{ + HYPERLIGHT_CPU_TIMEOUT_MS = [string]$CpuTimeout + HYPERLIGHT_WALL_TIMEOUT_MS = [string]$WallTimeout + HYPERLIGHT_HEAP_SIZE_MB = [string]$HeapSize + HYPERLIGHT_STACK_SIZE_MB = [string]$StackSize + } + + if ($ForInstall) { + # Permanent install: use 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) + $tempDir = [System.IO.Path]::GetTempPath() + $envHash['HYPERLIGHT_TIMING_LOG'] = (Join-Path $tempDir 'hyperlight-timing.jsonl') -replace '\\', '/' + $envHash['HYPERLIGHT_CODE_LOG'] = (Join-Path $tempDir 'hyperlight-code.js') -replace '\\', '/' + } else { + # Per-session: use the current env vars (random per-session paths + # set by the script to avoid clobbering between concurrent runs). + if ($env:HYPERLIGHT_TIMING_LOG) { $envHash['HYPERLIGHT_TIMING_LOG'] = $env:HYPERLIGHT_TIMING_LOG } + if ($env:HYPERLIGHT_CODE_LOG) { $envHash['HYPERLIGHT_CODE_LOG'] = $env:HYPERLIGHT_CODE_LOG } + } + + # Use forward slashes in the server path for JSON portability + $serverPath = $ServerJs -replace '\\', '/' + + [ordered]@{ + type = 'stdio' + command = 'node' + args = @($serverPath) + env = $envHash + } +} + +function Install-McpConfig { + Write-Info "Installing MCP server config to $McpConfigFile..." + + if (-not (Test-Path $CopilotConfigDir)) { + New-Item -ItemType Directory -Path $CopilotConfigDir -Force | Out-Null + } + + if (Test-Path $McpConfigFile) { + $existingRaw = Get-Content $McpConfigFile -Raw + if ($existingRaw -match [regex]::Escape($McpServerName)) { + Write-Ok "Already configured in $McpConfigFile" + return + } + + # Backup existing config + $backupPath = "$McpConfigFile.bak" + Copy-Item $McpConfigFile $backupPath + Write-Warn "Backed up existing config to $backupPath" + + # Merge: add our server to existing config โ€” pure PowerShell, + # no node -e shenanigans. "Do. Or do not. There is no try." + # โ€” Yoda, The Empire Strikes Back (1980) + $existingConfig = $existingRaw | ConvertFrom-Json -AsHashtable + if (-not $existingConfig.ContainsKey('mcpServers')) { + $existingConfig['mcpServers'] = @{} + } + $existingConfig['mcpServers'][$McpServerName] = Build-McpServerEntry -ForInstall + $existingConfig | ConvertTo-Json -Depth 5 | Set-Content -Path $McpConfigFile -Encoding utf8NoBOM + } else { + Build-McpJson -ForInstall | Set-Content -Path $McpConfigFile -Encoding utf8NoBOM + } + + Write-Ok "Installed! Config written to $McpConfigFile" + Write-Host (Get-Content $McpConfigFile -Raw) -ForegroundColor DarkGray +} + +function Uninstall-McpConfig { + if (-not (Test-Path $McpConfigFile)) { + Write-Warn "No config file found at $McpConfigFile" + return + } + + $content = Get-Content $McpConfigFile -Raw + if ($content -notmatch [regex]::Escape($McpServerName)) { + Write-Warn "$McpServerName not found in $McpConfigFile" + return + } + + # Pure PowerShell JSON manipulation โ€” no node -e needed. + # "Wax on, wax off." โ€” The Karate Kid (1984) + $config = $content | ConvertFrom-Json -AsHashtable + if ($config.ContainsKey('mcpServers')) { + $config['mcpServers'].Remove($McpServerName) + } + + if (($config['mcpServers'] ?? @{}).Count -eq 0) { + Remove-Item $McpConfigFile + Write-Host 'Config file removed (no servers remaining).' + } else { + $config | ConvertTo-Json -Depth 5 | Set-Content -Path $McpConfigFile -Encoding utf8NoBOM + Write-Host 'Server removed from config.' + } + Write-Ok "Uninstalled $McpServerName from Copilot CLI config" +} + +function Test-ConfigConflict { + <# + .SYNOPSIS + Warn if a permanent install exists โ€” the script's per-session + --additional-mcp-config will override it for the duration of + the session. Same server name = temp config wins via augment + merge semantics. "Danger, Will Robinson!" โ€” Lost in Space + (syndicated throughout the 80s) + #> + if ((Test-Path $McpConfigFile) -and + ((Get-Content $McpConfigFile -Raw) -match [regex]::Escape($McpServerName))) { + Write-Warn "A permanent install of '$McpServerName' exists in $McpConfigFile." + Write-Warn 'The script''s per-session config (--additional-mcp-config) will override it.' + Write-Warn 'To use the permanent config directly, just run: copilot' + } +} + +# โ”€โ”€ Demo Prompts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Each entry is @{ Title = '...'; Prompt = '...' } +$DemoPrompts = @( + @{ + Title = 'Calculate ฯ€ to 50 decimal places' + Prompt = 'Write a function using the Machin formula to calculate ฯ€ to 50 decimal places. Return the result as a JSON object with the value as a string.' + } + @{ + Title = 'Sieve of Eratosthenes' + Prompt = 'Find all prime numbers below 10,000 using the Sieve of Eratosthenes. Return JSON with the count and the last 10 primes.' + } + @{ + Title = 'Maze Generation' + Prompt = 'Generate a random 25ร—25 maze using recursive backtracking. Return it as ASCII art using # for walls and spaces for paths, plus the dimensions as JSON.' + } +) + +# โ”€โ”€ Run Prompt โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function Invoke-DemoPrompt { + <# + .SYNOPSIS + Run a single prompt via Copilot CLI programmatic mode. + Returns $true on success, $false on failure. + #> + param( + [string]$Title, + [string]$Prompt + ) + + $promptStart = Get-NowMs + + Write-Host "๐ŸŽฏ $Title" -ForegroundColor White + Write-Host "Prompt: $Prompt" -ForegroundColor DarkGray + Write-Host "Model: $Model" -ForegroundColor DarkGray + + # Timing log โ€” the MCP server writes a JSON-lines record here each + # time execute_javascript is called. Like Knight Rider's KITT + # dashboard but for sandbox performance. + # Avoid GetTempFileName() โ€” it creates an orphan .tmp file we'd + # never clean up. Construct path directly instead. + $timingLog = Join-Path ([System.IO.Path]::GetTempPath()) "hyperlight-timing-$PID-$(Get-Random).jsonl" + $env:HYPERLIGHT_TIMING_LOG = $timingLog + + # Code log โ€” the MCP server dumps the received JS source here + $codeLog = $null + if ($ShowCode) { + $codeLog = Join-Path ([System.IO.Path]::GetTempPath()) "hyperlight-code-$PID-$(Get-Random).js" + $env:HYPERLIGHT_CODE_LOG = $codeLog + } else { + Remove-Item Env:\HYPERLIGHT_CODE_LOG -ErrorAction SilentlyContinue + } + + # Build MCP config to a temp file โ€” utf8NoBOM to avoid confusing + # copilot CLI's JSON parser with a BOM header. + $mcpTmp = Join-Path ([System.IO.Path]::GetTempPath()) "hyperlight-mcp-$PID-$(Get-Random).json" + Build-McpJson | Set-Content -Path $mcpTmp -Encoding utf8NoBOM + + # Structured prompt โ€” steers the agent to call our tool directly. + # Avoids the "Full Metal Jacket" problem where the agent goes on + # a recon mission reading every file instead of calling the tool. + $fullPrompt = @" +TASK: $Prompt + +INSTRUCTIONS: You have an MCP tool called 'execute_javascript' from the 'hyperlight-sandbox' server. This tool takes a single parameter 'code' containing JavaScript source code. The code runs in a sandboxed QuickJS engine, NOT Node.js โ€” there is no require(), no fetch(), no fs. The code MUST use 'return ' at the top level to produce output (like a function body). Call the execute_javascript tool with your JavaScript code now. Do NOT read files, start servers, run shell commands, or do anything else. After you receive the tool result, present the answer in ONE short response and STOP. Do NOT attempt any follow-up actions, additional tool calls, or continuation. Your task is complete the moment you present the result. +"@ + + # Write the prompt to a temp file and pass via @file to avoid PS7's + # native-command argument mangling with multi-line here-strings. + # The copilot CLI reads -p @file the same as -p "string". + # "Nobody puts Baby in a corner." โ€” Dirty Dancing (1987) + $promptFile = Join-Path ([System.IO.Path]::GetTempPath()) "hyperlight-prompt-$PID-$(Get-Random).txt" + $fullPrompt | Set-Content -Path $promptFile -Encoding utf8NoBOM + + # Build the command args list for copilot CLI. + # We assemble it as an array so we can display it with -ShowCommand + # before actually executing it. + $copilotArgs = @( + '-p', $fullPrompt, + '-s', + '--additional-mcp-config', "@$mcpTmp", + '--allow-all-tools', + '--deny-tool', 'shell', + '--deny-tool', 'write', + '--deny-tool', 'read', + '--deny-tool', 'fetch', + '--no-custom-instructions', + '--no-ask-user', + '--disable-builtin-mcps', + '--model', $Model + ) + + # Emit the command if -ShowCommand was requested. + # "Show me the money!" โ€” Jerry Maguire (1996... close enough to the 80s) + if ($ShowCommand) { + # Copy-pasteable command with the full structured prompt. + # Omits --additional-mcp-config because the temp file gets + # cleaned up. Relies on a permanent install. + $escapedPrompt = $fullPrompt -replace "'", "''" + $mcpInstalled = (Test-Path $McpConfigFile) -and + ((Get-Content $McpConfigFile -Raw) -match [regex]::Escape($McpServerName)) + + # Build the install command with any non-default sandbox limits + # so the permanent config gets the same settings. + # "If you build it, he will come." โ€” Field of Dreams (1989) + $installFlags = '' + if ($CpuTimeout -ne 1000) { $installFlags += " -CpuTimeout $CpuTimeout" } + if ($WallTimeout -ne 5000) { $installFlags += " -WallTimeout $WallTimeout" } + if ($HeapSize -ne 16) { $installFlags += " -HeapSize $HeapSize" } + if ($StackSize -ne 1) { $installFlags += " -StackSize $StackSize" } + + Write-Host '' + Write-Host '๐Ÿ”ง Copy-pasteable command:' -ForegroundColor Cyan + if (-not $mcpInstalled) { + Write-Host '' + Write-Host 'โš  The MCP server must be installed before this command will work.' -ForegroundColor Yellow + Write-Host ' Install it now:' -ForegroundColor Yellow + Write-Host '' + Write-Host (" .\demo-copilot-cli.ps1 -Mode Install{0}" -f $installFlags) -ForegroundColor Yellow + Write-Host '' + Write-Host ' To remove it later:' -ForegroundColor Yellow + Write-Host '' + Write-Host ' .\demo-copilot-cli.ps1 -Mode Uninstall' -ForegroundColor Yellow + } + Write-Host '' + Write-Host ' copilot `' + Write-Host (" -p '{0}' ``" -f $escapedPrompt) + Write-Host ' -s `' + Write-Host ' --allow-all-tools `' + Write-Host ' --deny-tool shell `' + Write-Host ' --deny-tool write `' + Write-Host ' --deny-tool read `' + Write-Host ' --deny-tool fetch `' + Write-Host ' --no-custom-instructions `' + Write-Host ' --no-ask-user `' + Write-Host ' --disable-builtin-mcps `' + Write-Host (" --model {0}" -f $Model) + Write-Host '' + } + + # Flags rationale (from `copilot --help`): + # + # --additional-mcp-config @file Register our MCP server for this session + # only (no permanent install required). + # + # --allow-all-tools REQUIRED for -p (non-interactive) mode. + # Individual --allow-tool patterns only work + # in interactive mode's permission prompts. + # See `copilot --help` for details. + # + # --available-tools Restrict model to ONLY our MCP tool plus + # internal tools the agent needs to function + # (task_complete, report_intent). The model + # cannot call shell, file write, web fetch, + # or any other tool. This is the security + # layer โ€” even though --allow-all-tools is + # set, only whitelisted tools are visible. + # + # --no-custom-instructions Don't load AGENTS.md / copilot-instructions.md + # which could confuse the agent. + # + # --no-ask-user Don't ask clarifying questions in -p mode. + # + # --disable-builtin-mcps Don't load the GitHub MCP server โ€” we only + # want our sandbox. + # + # -p Programmatic (non-interactive) mode. + # + # Security: use the documented --allow-all-tools + --deny-tool pattern + # from https://docs.github.com/copilot/concepts/agents/about-copilot-cli + # + # --allow-all-tools : auto-approve all tools (required for -p mode) + # --deny-tool 'shell' : BLOCK all shell commands (takes precedence) + # --deny-tool 'write' : BLOCK all file write/edit operations + # --deny-tool 'read' : BLOCK file reading (agent should only use our MCP tool) + # --deny-tool 'fetch' : BLOCK web/HTTP requests + # -s : Silent โ€” agent response only, no stats/retry noise + # + # --deny-tool takes precedence over --allow-all-tools per the docs. + # Internal plumbing tools (task_complete, report_intent) are NOT + # in the shell/write/MCP categories so they remain available. + # Our MCP tool (hyperlight-sandbox โ†’ execute_javascript) is allowed + # because it's not denied. + # + # -s suppresses the agentic loop retry noise ("Continuing autonomously", + # "Response was interrupted due to a server error") that occurs when + # the model's continuation turn hits transient API errors after + # already completing the actual task. + # + # We also filter out the "Execution failed" line that leaks through + # -s when the agentic continuation gives up โ€” the actual task was + # already successful at that point. The exit code is still 0. + # + # Like WarGames taught us: the only winning move is not to grant + # shell access. โ€” WarGames (1983) + $copilotStart = Get-NowMs + + try { + # Use --% (stop-parsing token) to prevent PS from mangling + # the native command arguments. Pass prompt via temp file. + $rawOutput = & $script:CopilotBin ` + -p $fullPrompt ` + -s ` + --additional-mcp-config "@$mcpTmp" ` + --allow-all-tools ` + --deny-tool shell ` + --deny-tool write ` + --deny-tool read ` + --deny-tool fetch ` + --no-custom-instructions ` + --no-ask-user ` + --disable-builtin-mcps ` + --model $Model 2>&1 + + # Filter output: + # 1. Drop "Execution failed:" noise from agentic continuation retries + # 2. Strip leading blank lines the CLI emits while waiting for the + # model โ€” keeps blank lines *within* the response body intact. + # "Every breath you take, every blank you makeโ€ฆ" โ€” The Police (1983) + $lines = $rawOutput -split "`n" | + Where-Object { $_ -notmatch '^Execution failed:' } + + # Strip leading blank lines (keep blanks within the body) + $seenContent = $false + $filteredLines = @() + foreach ($line in $lines) { + if (-not $seenContent -and $line.Trim() -eq '') { continue } + $seenContent = $true + $filteredLines += $line + } + + # Output the response + $filteredLines | ForEach-Object { Write-Host $_ } + $success = $true + } + catch { + Write-Warn "Prompt failed (Copilot may not be authenticated or the model may be unavailable)" + Write-Warn $_.Exception.Message + $success = $false + } + + $copilotElapsedMs = (Get-NowMs) - $copilotStart + + # โ”€โ”€ Show generated code (if -ShowCode) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if ($ShowCode -and $codeLog -and (Test-Path $codeLog) -and (Get-Item $codeLog).Length -gt 0) { + Write-Host '' + Write-Host '๐Ÿ“ Generated code:' -ForegroundColor White + Write-Host 'โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€' -ForegroundColor DarkGray + Get-Content $codeLog | ForEach-Object { + Write-Host " $_" -ForegroundColor DarkGray + } + Write-Host 'โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€' -ForegroundColor DarkGray + } + + # โ”€โ”€ Timing breakdown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + Write-Host '' + Write-Host 'โฑ Timing breakdown:' -ForegroundColor White + Write-Elapsed 'Copilot CLI (total round-trip)' $copilotStart + + if ((Test-Path $timingLog) -and (Get-Item $timingLog).Length -gt 0) { + # Parse the last timing record (in case the model called the + # tool more than once, the last call is the interesting one). + # Pure PowerShell โ€” no node -e needed. "ConvertFrom-Json is + # the one who knocks." โ€” adapted from Breaking Bad (close to 80s) + $lastLine = Get-Content $timingLog -Tail 1 + $timing = $lastLine | ConvertFrom-Json + + $toolTotalMs = [int]$timing.totalMs + $toolInitMs = [int]$timing.initMs + $toolSetupMs = [int]$timing.setupMs + $toolCompileMs = [int]$timing.compileMs + $toolSnapMs = [int]$timing.snapshotMs + $toolExecMs = [int]$timing.executeMs + + $modelMs = $copilotElapsedMs - $toolTotalMs + $modelSecs = [math]::Floor($modelMs / 1000) + $modelFrac = $modelMs % 1000 + $toolSecs = [math]::Floor($toolTotalMs / 1000) + $toolFrac = $toolTotalMs % 1000 + + Write-Host (" ๐Ÿค– {0,-36} {1}.{2:D3}s (LLM code generation + response)" -f 'Model', $modelSecs, $modelFrac) -ForegroundColor Cyan + Write-Host (" ๐Ÿ”ง {0,-36} {1}.{2:D3}s (MCP tool total)" -f 'Tool execution', $toolSecs, $toolFrac) -ForegroundColor Cyan + + if ($toolInitMs -gt 0) { + Write-Host (" โ”œโ”€ Sandbox init: {0,5}ms" -f $toolInitMs) -ForegroundColor DarkGray + } + Write-Host (" โ”œโ”€ Handler setup: {0,5}ms" -f $toolSetupMs) -ForegroundColor DarkGray + Write-Host (" โ”œโ”€ Compile & load: {0,5}ms" -f $toolCompileMs) -ForegroundColor DarkGray + Write-Host (" โ”œโ”€ Snapshot: {0,5}ms" -f $toolSnapMs) -ForegroundColor DarkGray + Write-Host (" โ””โ”€ JS execution: {0,5}ms" -f $toolExecMs) -ForegroundColor DarkGray + } else { + Write-Warn 'No tool timing data โ€” the model may not have called the tool' + } + + Write-Elapsed "Total for '$Title'" $promptStart + + # Clean up temp files + Remove-Item $mcpTmp -ErrorAction SilentlyContinue + Remove-Item $timingLog -ErrorAction SilentlyContinue + Remove-Item $promptFile -ErrorAction SilentlyContinue + if ($codeLog) { Remove-Item $codeLog -ErrorAction SilentlyContinue } + + return $success +} + +# โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +Write-Banner +Write-Info "Using model: $Model" +Write-Info "Sandbox limits: CPU ${CpuTimeout}ms, wall ${WallTimeout}ms, heap ${HeapSize}MB, stack ${StackSize}MB" + +switch ($Mode) { + 'Install' { + Test-Prerequisites + Install-McpConfig + Write-Host '' + Write-Info 'You can now use it in Copilot CLI:' + Write-Host ' copilot' -ForegroundColor White + Write-Host ' > Calculate ฯ€ to 50 decimal places using the Machin formula' -ForegroundColor DarkGray + exit 0 + } + + 'Uninstall' { + Uninstall-McpConfig + exit 0 + } + + 'Headless' { + Test-Prerequisites + Test-ConfigConflict + + # If a custom prompt was given, run just that one and exit. + # "There can be only one." โ€” Highlander (1986) + if ($Prompt) { + Write-Info 'Running custom prompt in headless mode...' + Write-Separator + $runStart = Get-NowMs + $result = Invoke-DemoPrompt -Title 'Custom prompt' -Prompt $Prompt + Write-Separator + Write-Elapsed 'Custom prompt' $runStart + exit ([int](-not $result)) + } + + Write-Info "Running $($DemoPrompts.Count) demo prompts in headless mode..." + Write-Separator + + $runStart = Get-NowMs + $passed = 0 + $failed = 0 + + foreach ($demo in $DemoPrompts) { + $result = Invoke-DemoPrompt -Title $demo.Title -Prompt $demo.Prompt + if ($result) { $passed++ } else { $failed++ } + Write-Separator + } + + Write-Elapsed "All $($DemoPrompts.Count) prompts" $runStart + Write-Host '' + Write-Host "Results: $passed passed" -ForegroundColor Green + if ($failed -gt 0) { + Write-Host " $failed failed" -ForegroundColor Red + } + exit $failed + } + + default { + # Interactive mode + Test-Prerequisites + Test-ConfigConflict + + # Run the custom prompt if one was provided, then exit + if ($Prompt) { + Write-Info 'Running your custom prompt...' + Write-Separator + Invoke-DemoPrompt -Title 'Custom prompt' -Prompt $Prompt | Out-Null + Write-Separator + Write-Info 'Done! "I''ll be back." โ€” The Terminator (1984)' + exit 0 + } + + Write-Info "This demo will run $($DemoPrompts.Count) prompts through Copilot CLI," + Write-Info 'each executing JavaScript inside a Hyperlight sandbox.' + Write-Host '' + Write-Info 'The MCP server is configured per-session (no permanent install).' + Write-Info "To install permanently, run: .\demo-copilot-cli.ps1 -Mode Install" + Write-Separator + + Write-Host 'Ready to run demos?' -ForegroundColor Yellow + Read-Host 'Press Enter to continue (or Ctrl+C to cancel)' + Write-Host '' + + for ($i = 0; $i -lt $DemoPrompts.Count; $i++) { + $demo = $DemoPrompts[$i] + Invoke-DemoPrompt -Title $demo.Title -Prompt $demo.Prompt | Out-Null + Write-Separator + + if ($i + 1 -lt $DemoPrompts.Count) { + $answer = Read-Host 'Next demo? (Enter to continue, q to quit)' + if ($answer -eq 'q') { + # "I'll be back." โ€” The Terminator (1984) + Write-Info 'Thanks for watching! "I''ll be back." โ€” The Terminator (1984)' + exit 0 + } + } + } + + Write-Host '๐ŸŽ‰ All demos complete!' -ForegroundColor Green + # "That's all, folks!" โ€” Porky Pig (well, 1930s, close enough) + Write-Host '"That''s all, folks!" โ€” Porky Pig (well, 1930s, close enough)' -ForegroundColor DarkGray + } +} diff --git a/src/js-host-api/examples/mcp-server/demo-copilot-cli.sh b/src/js-host-api/examples/mcp-server/demo-copilot-cli.sh new file mode 100755 index 0000000..0170a9f --- /dev/null +++ b/src/js-host-api/examples/mcp-server/demo-copilot-cli.sh @@ -0,0 +1,735 @@ +#!/usr/bin/env bash +# โ”€โ”€ Hyperlight JS ร— GitHub Copilot CLI Demo โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# +# "Open the pod bay doors, HAL." โ€” 2001: A Space Odyssey (1968) +# ... except this sandbox actually listens when you tell it to stop. +# +# This script demonstrates using the Hyperlight JS MCP server with +# GitHub Copilot CLI. It: +# 1. Verifies prerequisites (Copilot CLI + Node.js + built guest) +# 2. Configures the MCP server (persistent or session-only) +# 3. Runs example prompts via Copilot CLI programmatic mode +# +# Usage: +# ./demo-copilot-cli.sh # Interactive โ€” walks you through it +# ./demo-copilot-cli.sh --headless # Non-interactive โ€” runs all demos +# ./demo-copilot-cli.sh --prompt "Calculate pi to 50 digits" # Run a single prompt +# ./demo-copilot-cli.sh --install # Install MCP config permanently +# ./demo-copilot-cli.sh --uninstall # Remove MCP config +# ./demo-copilot-cli.sh --model gpt-4o # Use a specific model +# ./demo-copilot-cli.sh --cpu-timeout 2000 --heap-size 32 # Custom limits +# +# Options: +# --model LLM model to use (default: claude-opus-4.6) +# --show-code Display the JavaScript code generated by the model +# --show-command Display the copilot CLI command line being executed +# --prompt Run a single custom prompt instead of built-in demos +# --cpu-timeout CPU time limit per execution (default: 1000) +# --wall-timeout Wall-clock backstop per execution (default: 5000) +# --heap-size Guest heap size in megabytes (default: 16) +# --stack-size Guest stack size in megabytes (default: 1) +# +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +set -euo pipefail + +# โ”€โ”€ Colours & Formatting โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +BOLD='\033[1m' +DIM='\033[2m' +RESET='\033[0m' + +# โ”€โ”€ Defaults โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# "These go to eleven." โ€” Spinal Tap (1984) +DEFAULT_MODEL="claude-opus-4.6" +MODEL="${DEFAULT_MODEL}" +SHOW_CODE=false +SHOW_COMMAND=false +CUSTOM_PROMPT="" + +# Sandbox limits โ€” can be overridden via CLI flags or env vars. +# CLI flags take precedence over env vars, env vars over defaults. +CPU_TIMEOUT="${HYPERLIGHT_CPU_TIMEOUT_MS:-1000}" +WALL_TIMEOUT="${HYPERLIGHT_WALL_TIMEOUT_MS:-5000}" +HEAP_SIZE="${HYPERLIGHT_HEAP_SIZE_MB:-16}" +STACK_SIZE="${HYPERLIGHT_STACK_SIZE_MB:-1}" + +# โ”€โ”€ Paths โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SERVER_JS="${SCRIPT_DIR}/server.js" +COPILOT_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.copilot}" +MCP_CONFIG_FILE="${COPILOT_CONFIG_DIR}/mcp-config.json" +MCP_SERVER_NAME="hyperlight-sandbox" + +# โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +info() { echo -e "${CYAN}โ„น${RESET} $*"; } +ok() { echo -e "${GREEN}โœ…${RESET} $*"; } +warn() { echo -e "${YELLOW}โš ${RESET} $*"; } +fail() { echo -e "${RED}โŒ${RESET} $*" >&2; exit 1; } + +banner() { + echo "" + echo -e "${BOLD}${CYAN}๐Ÿ”’ Hyperlight JS ร— GitHub Copilot CLI Demo${RESET}" + echo "" +} + +separator() { + echo -e "${DIM}\nโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€${RESET}\n" +} + +# Timing helpers โ€” because "Time... is not on my side" โ€” The Rolling Stones (1964, close enough) +# Uses millisecond precision via date +%s%3N (GNU coreutils) +now_ms() { + date +%s%3N +} + +# Log elapsed time since a given start timestamp (ms) +# Usage: log_elapsed "phase description" "$start_ms" +log_elapsed() { + local phase="$1" + local start="$2" + local end + end="$(now_ms)" + local elapsed_ms=$(( end - start )) + local elapsed_s elapsed_frac + elapsed_s=$(( elapsed_ms / 1000 )) + elapsed_frac=$(( elapsed_ms % 1000 )) + printf "${DIM}โฑ %-40s %d.%03ds${RESET}\n" "${phase}" "${elapsed_s}" "${elapsed_frac}" +} + +# โ”€โ”€ Prerequisite Checks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +check_prerequisites() { + info "Checking prerequisites..." + + # Node.js + if ! command -v node &>/dev/null; then + fail "Node.js is required but not installed. Install from https://nodejs.org" + fi + ok "Node.js $(node --version)" + + # Copilot CLI + if command -v copilot &>/dev/null; then + COPILOT_BIN="copilot" + elif [[ -x "${HOME}/.vscode-server/data/User/globalStorage/github.copilot-chat/copilotCli/copilot" ]]; then + COPILOT_BIN="${HOME}/.vscode-server/data/User/globalStorage/github.copilot-chat/copilotCli/copilot" + else + fail "GitHub Copilot CLI not found. Install with: npm install -g @github/copilot" + fi + ok "Copilot CLI found at: ${COPILOT_BIN}" + + # Server script + if [[ ! -f "${SERVER_JS}" ]]; then + fail "Server script not found at: ${SERVER_JS}" + fi + ok "MCP server: ${SERVER_JS}" + + # npm dependencies + if [[ ! -d "${SCRIPT_DIR}/node_modules" ]]; then + warn "node_modules not found โ€” installing dependencies..." + (cd "${SCRIPT_DIR}" && npm install) || fail "npm install failed" + fi + ok "Dependencies installed" + + # Native addon smoke test โ€” verify the .node binary loads and has + # the methods we need. The old check started server.js and waited + # for a banner that never came (stdio transport blocks on connect). + # This just imports lib.js which validates the native addon at + # module load time. Instant, reliable, catches the exact failure. + # "Trust, but verify." โ€” Reagan (1987) + info "Verifying native addon loads correctly..." + local lib_js="${SCRIPT_DIR}/../../lib.js" + local smoke_err + smoke_err="$(node --input-type=module -e "import('${lib_js}').then(() => console.log('OK')).catch(e => { console.error(e.message); process.exit(1); })" 2>&1)" + if [[ $? -ne 0 ]]; then + fail "Native addon failed to load โ€” rebuild with 'just build'.\n${smoke_err}" + fi + ok "Native addon loads successfully" + + echo "" +} + +# โ”€โ”€ MCP Config Management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Build the JSON config for this MCP server. +# Passes HYPERLIGHT_TIMING_LOG and HYPERLIGHT_CODE_LOG via the env +# field so the Copilot CLI-spawned server process receives them โ€” +# environment inheritance doesn't work because Copilot CLI is the +# parent process, not our shell. +build_mcp_json() { + # Forward sandbox configuration + observability env vars to the + # MCP server process. Copilot CLI spawns the server itself, so + # normal env inheritance from our shell doesn't apply โ€” we must + # embed them in the MCP config's "env" field. + local install_mode="${1:-}" + + export HYPERLIGHT_CPU_TIMEOUT_MS="${CPU_TIMEOUT}" + export HYPERLIGHT_WALL_TIMEOUT_MS="${WALL_TIMEOUT}" + export HYPERLIGHT_HEAP_SIZE_MB="${HEAP_SIZE}" + export HYPERLIGHT_STACK_SIZE_MB="${STACK_SIZE}" + + node -e " + const installMode = '${install_mode}' === 'install'; + const env = { + HYPERLIGHT_CPU_TIMEOUT_MS: process.env.HYPERLIGHT_CPU_TIMEOUT_MS, + HYPERLIGHT_WALL_TIMEOUT_MS: process.env.HYPERLIGHT_WALL_TIMEOUT_MS, + HYPERLIGHT_HEAP_SIZE_MB: process.env.HYPERLIGHT_HEAP_SIZE_MB, + HYPERLIGHT_STACK_SIZE_MB: process.env.HYPERLIGHT_STACK_SIZE_MB, + }; + 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; + } + + const server = { + type: 'stdio', + command: 'node', + args: ['${SERVER_JS}'], + env, + }; + + console.log(JSON.stringify({ mcpServers: { '${MCP_SERVER_NAME}': server } }, null, 4)); + " +} + +# Install config permanently into ~/.copilot/mcp-config.json +install_mcp_config() { + info "Installing MCP server config to ${MCP_CONFIG_FILE}..." + mkdir -p "${COPILOT_CONFIG_DIR}" + + if [[ -f "${MCP_CONFIG_FILE}" ]]; then + # Check if already configured + if grep -q "${MCP_SERVER_NAME}" "${MCP_CONFIG_FILE}" 2>/dev/null; then + ok "Already configured in ${MCP_CONFIG_FILE}" + return 0 + fi + + # Backup existing config + cp "${MCP_CONFIG_FILE}" "${MCP_CONFIG_FILE}.bak" + warn "Backed up existing config to ${MCP_CONFIG_FILE}.bak" + + # Merge: add our server to existing mcpServers with full env + # block including fixed log paths for the permanent install. + # Use node for reliable JSON merging. + node -e " + const fs = require('fs'); + const existing = JSON.parse(fs.readFileSync('${MCP_CONFIG_FILE}.bak', 'utf8')); + existing.mcpServers = existing.mcpServers || {}; + existing.mcpServers['${MCP_SERVER_NAME}'] = { + type: 'stdio', + command: 'node', + args: ['${SERVER_JS}'], + env: { + HYPERLIGHT_CPU_TIMEOUT_MS: '${CPU_TIMEOUT}', + HYPERLIGHT_WALL_TIMEOUT_MS: '${WALL_TIMEOUT}', + HYPERLIGHT_HEAP_SIZE_MB: '${HEAP_SIZE}', + HYPERLIGHT_STACK_SIZE_MB: '${STACK_SIZE}', + HYPERLIGHT_TIMING_LOG: '/tmp/hyperlight-timing.jsonl', + HYPERLIGHT_CODE_LOG: '/tmp/hyperlight-code.js', + }, + }; + fs.writeFileSync('${MCP_CONFIG_FILE}', JSON.stringify(existing, null, 4) + '\n'); + " + else + build_mcp_json install > "${MCP_CONFIG_FILE}" + fi + + ok "Installed! Config written to ${MCP_CONFIG_FILE}" + echo -e "${DIM}$(cat "${MCP_CONFIG_FILE}")${RESET}" +} + +# Remove our server from the config +uninstall_mcp_config() { + if [[ ! -f "${MCP_CONFIG_FILE}" ]]; then + warn "No config file found at ${MCP_CONFIG_FILE}" + return 0 + fi + + if ! grep -q "${MCP_SERVER_NAME}" "${MCP_CONFIG_FILE}" 2>/dev/null; then + warn "${MCP_SERVER_NAME} not found in ${MCP_CONFIG_FILE}" + return 0 + fi + + node -e " + const fs = require('fs'); + const config = JSON.parse(fs.readFileSync('${MCP_CONFIG_FILE}', 'utf8')); + delete config.mcpServers?.['${MCP_SERVER_NAME}']; + if (Object.keys(config.mcpServers || {}).length === 0) { + fs.unlinkSync('${MCP_CONFIG_FILE}'); + console.log('Config file removed (no servers remaining).'); + } else { + fs.writeFileSync('${MCP_CONFIG_FILE}', JSON.stringify(config, null, 4) + '\n'); + console.log('Server removed from config.'); + } + " + ok "Uninstalled ${MCP_SERVER_NAME} from Copilot CLI config" +} + +# Warn if a permanent install exists โ€” the script's per-session +# --additional-mcp-config will override it for the duration of the +# session. Same server name = temp config wins via augment merge +# semantics. "Danger, Will Robinson!" โ€” Lost in Space (syndicated +# throughout the 80s) +check_config_conflict() { + if [[ -f "${MCP_CONFIG_FILE}" ]] && grep -q "${MCP_SERVER_NAME}" "${MCP_CONFIG_FILE}" 2>/dev/null; then + warn "A permanent install of '${MCP_SERVER_NAME}' exists in ${MCP_CONFIG_FILE}." + warn "The script's per-session config (--additional-mcp-config) will override it." + warn "To use the permanent config directly, just run: copilot" + fi +} + +# โ”€โ”€ Demo Prompts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Array of demo prompts โ€” each is [title, prompt] +DEMO_PROMPTS=( + "Calculate ฯ€ to 50 decimal places" + "Write a function using the Machin formula to calculate ฯ€ to 50 decimal places. Return the result as a JSON object with the value as a string." + + "Sieve of Eratosthenes" + "Find all prime numbers below 10,000 using the Sieve of Eratosthenes. Return JSON with the count and the last 10 primes." + + "Maze Generation" + "Generate a random 25ร—25 maze using recursive backtracking. Return it as ASCII art using # for walls and spaces for paths, plus the dimensions as JSON." +) +DEMO_COUNT=$(( ${#DEMO_PROMPTS[@]} / 2 )) + +# Run a single prompt via Copilot CLI programmatic mode +run_prompt() { + local title="$1" + local prompt="$2" + local prompt_start + prompt_start="$(now_ms)" + + echo -e "${BOLD}๐ŸŽฏ ${title}${RESET}" + echo -e "${DIM}Prompt: ${prompt}${RESET}" + echo -e "${DIM}Model: ${MODEL}${RESET}" + + # Flags rationale (from `copilot --help`): + # + # --additional-mcp-config @file Register our MCP server for this session + # only (no permanent install required). + # + # --allow-all-tools REQUIRED for -p (non-interactive) mode. + # Individual --allow-tool patterns only work + # in interactive mode's permission prompts. + # See `copilot --help` for details. + # + # --available-tools Restrict model to ONLY our MCP tool plus + # internal tools the agent needs to function + # (task_complete, report_intent). The model + # cannot call shell, file write, web fetch, + # or any other tool. This is the security + # layer โ€” even though --allow-all-tools is + # set, only whitelisted tools are visible. + # + # --no-custom-instructions Don't load AGENTS.md / copilot-instructions.md + # which could confuse the agent. + # + # --no-ask-user Don't ask clarifying questions in -p mode. + # + # --disable-builtin-mcps Don't load the GitHub MCP server โ€” we only + # want our sandbox. + # + # -p Programmatic (non-interactive) mode. + # + # Write MCP config to a temp file โ€” the --additional-mcp-config + # flag accepts a file path when prefixed with @. + # Timing log file โ€” the MCP server writes a JSON-lines record here + # each time execute_javascript is called. We read it afterwards to + # show a model-vs-tool breakdown. Like Knight Rider's KITT dashboard + # but for sandbox performance. + # + # Must be set BEFORE build_mcp_json so the env field is included + # in the MCP config (Copilot CLI spawns the server, not our shell, + # so normal env inheritance doesn't apply). + local timing_log + timing_log="$(mktemp "${TMPDIR:-/tmp}/hyperlight-timing-XXXXXX.jsonl")" + export HYPERLIGHT_TIMING_LOG="${timing_log}" + + # Code log โ€” the MCP server dumps the received JS source here + local code_log="" + if [[ "${SHOW_CODE}" == true ]]; then + code_log="$(mktemp "${TMPDIR:-/tmp}/hyperlight-code-XXXXXX.js")" + export HYPERLIGHT_CODE_LOG="${code_log}" + else + unset HYPERLIGHT_CODE_LOG 2>/dev/null || true + fi + + local mcp_tmp + mcp_tmp="$(mktemp "${TMPDIR:-/tmp}/hyperlight-mcp-XXXXXX.json")" + build_mcp_json > "${mcp_tmp}" + + # shellcheck disable=SC2064 + trap "rm -f '${mcp_tmp}' '${timing_log}' '${code_log:-}'" RETURN + + # The prompt is deliberately structured to steer the agent: + # 1. Lead with the task โ€” what JS code to write + # 2. Explicitly name the tool to call + # 3. Remind it the code must use `return` to produce output + # 4. Tell it NOT to read files, start servers, or use shell + # + # This avoids the "Full Metal Jacket" problem where the agent + # goes on a recon mission reading every file in the repo instead + # of just calling the damn tool. + local full_prompt + full_prompt="TASK: ${prompt} + +INSTRUCTIONS: You have an MCP tool called 'execute_javascript' from the 'hyperlight-sandbox' server. \ +This tool takes a single parameter 'code' containing JavaScript source code. \ +The code runs in a sandboxed QuickJS engine, NOT Node.js โ€” there is no require(), no fetch(), no fs. \ +The code MUST use 'return ' at the top level to produce output (like a function body). \ +Call the execute_javascript tool with your JavaScript code now. \ +Do NOT read files, start servers, run shell commands, or do anything else. \ +After you receive the tool result, present the answer in ONE short response and STOP. \ +Do NOT attempt any follow-up actions, additional tool calls, or continuation. \ +Your task is complete the moment you present the result." + + # Security: use the documented --allow-all-tools + --deny-tool pattern + # from https://docs.github.com/copilot/concepts/agents/about-copilot-cli + # + # --allow-all-tools : auto-approve all tools (required for -p mode) + # --deny-tool 'shell' : BLOCK all shell commands (takes precedence) + # --deny-tool 'write' : BLOCK all file write/edit operations + # --deny-tool 'read' : BLOCK file reading (agent should only use our MCP tool) + # --deny-tool 'fetch' : BLOCK web/HTTP requests + # -s : Silent โ€” agent response only, no stats/retry noise + # + # --deny-tool takes precedence over --allow-all-tools per the docs. + # Internal plumbing tools (task_complete, report_intent) are NOT + # in the shell/write/MCP categories so they remain available. + # Our MCP tool (hyperlight-sandbox โ†’ execute_javascript) is allowed + # because it's not denied. + # + # -s suppresses the agentic loop retry noise ("Continuing autonomously", + # "Response was interrupted due to a server error") that occurs when + # the model's continuation turn hits transient API errors after + # already completing the actual task. + # + # We also filter out the "Execution failed" line that leaks through + # -s when the agentic continuation gives up โ€” the actual task was + # already successful at that point. The exit code is still 0. + # + # Like WarGames taught us: the only winning move is not to grant + # shell access. + + # โ€œShow me the money!โ€ โ€” Jerry Maguire (1996... close enough to the 80s) + if [[ "${SHOW_COMMAND}" == true ]]; then + # Copy-pasteable command with the full structured prompt. + # Uses double quotes for -p so single quotes in the prompt + # don't need escaping. Omits --additional-mcp-config because + # the temp file gets cleaned up. Relies on a permanent install. + local mcp_installed=false + if [[ -f "${MCP_CONFIG_FILE}" ]] && grep -q "${MCP_SERVER_NAME}" "${MCP_CONFIG_FILE}" 2>/dev/null; then + mcp_installed=true + fi + + # Build the install command with any non-default sandbox limits + # so the permanent config gets the same settings. + # "If you build it, he will come." โ€” Field of Dreams (1989) + local install_flags="" + [[ "${CPU_TIMEOUT}" != "1000" ]] && install_flags+=" --cpu-timeout ${CPU_TIMEOUT}" + [[ "${WALL_TIMEOUT}" != "5000" ]] && install_flags+=" --wall-timeout ${WALL_TIMEOUT}" + [[ "${HEAP_SIZE}" != "16" ]] && install_flags+=" --heap-size ${HEAP_SIZE}" + [[ "${STACK_SIZE}" != "1" ]] && install_flags+=" --stack-size ${STACK_SIZE}" + + echo "" + echo -e "${CYAN}${BOLD}๐Ÿ”ง Copy-pasteable command:${RESET}" + if [[ "${mcp_installed}" != true ]]; then + echo "" + echo -e "${YELLOW}โš  The MCP server must be installed before this command will work.${RESET}" + echo -e "${YELLOW} Install it now:${RESET}" + echo "" + echo -e "${YELLOW} ./demo-copilot-cli.sh --install${install_flags}${RESET}" + echo "" + echo -e "${YELLOW} To remove it later:${RESET}" + echo "" + echo -e "${YELLOW} ./demo-copilot-cli.sh --uninstall${RESET}" + fi + echo "" + echo " copilot \\" + echo " -p \"${full_prompt}\" \\" + echo " -s \\" + echo " --allow-all-tools \\" + echo " --deny-tool shell \\" + echo " --deny-tool write \\" + echo " --deny-tool read \\" + echo " --deny-tool fetch \\" + echo " --no-custom-instructions \\" + echo " --no-ask-user \\" + echo " --disable-builtin-mcps \\" + echo " --model ${MODEL}" + echo "" + fi + + local copilot_start + copilot_start="$(now_ms)" + + # Pipe through two filters: + # 1. Drop "Execution failed:" noise from agentic continuation retries + # 2. Strip leading blank lines the CLI emits while waiting for the + # model โ€” keeps blank lines *within* the response body intact. + # "Every breath you take, every blank you makeโ€ฆ" โ€” The Police (1983) + "${COPILOT_BIN}" \ + -p "${full_prompt}" \ + -s \ + --additional-mcp-config "@${mcp_tmp}" \ + --allow-all-tools \ + --deny-tool 'shell' \ + --deny-tool 'write' \ + --deny-tool 'read' \ + --deny-tool 'fetch' \ + --no-custom-instructions \ + --no-ask-user \ + --disable-builtin-mcps \ + --model "${MODEL}" 2>&1 \ + | grep -v '^Execution failed:' \ + | awk 'NF{p=1} p' || { + warn "Prompt failed (Copilot may not be authenticated or the model may be unavailable)" + return 1 + } + + local copilot_elapsed_ms + copilot_elapsed_ms=$(( $(now_ms) - copilot_start )) + + # โ”€โ”€ Timing breakdown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # Read the MCP server's timing log to split "model thinking" from + # "tool execution". The server writes one JSON line per tool call. + # โ”€โ”€ Show generated code (if --show-code) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if [[ "${SHOW_CODE}" == true && -n "${code_log}" && -s "${code_log}" ]]; then + echo -e "\n${BOLD}๐Ÿ“ Generated code:${RESET}" + echo -e "${DIM}โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€${RESET}" + # Use sed to indent and dim each line for readability + sed "s/^/ /" "${code_log}" | while IFS= read -r line; do + echo -e "${DIM}${line}${RESET}" + done + echo -e "${DIM}โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€${RESET}" + fi + + echo -e "\n${BOLD}โฑ Timing breakdown:${RESET}" + log_elapsed "Copilot CLI (total round-trip)" "${copilot_start}" + + if [[ -s "${timing_log}" ]]; then + # Parse the last timing record (in case the model called the + # tool more than once, the last call is the interesting one). + # Single node invocation outputs space-separated values. + local timing_values + timing_values="$(tail -1 "${timing_log}" | node -e " + process.stdin.resume(); + let d = ''; + process.stdin.on('data', c => d += c); + process.stdin.on('end', () => { + const t = JSON.parse(d); + // Output: totalMs initMs setupMs compileMs snapshotMs executeMs + console.log([t.totalMs, t.initMs, t.setupMs, t.compileMs, t.snapshotMs, t.executeMs].join(' ')); + }); + ")" + + # Read into individual variables + local tool_total_ms tool_init_ms tool_setup_ms tool_compile_ms tool_snap_ms tool_exec_ms + read -r tool_total_ms tool_init_ms tool_setup_ms tool_compile_ms tool_snap_ms tool_exec_ms <<< "${timing_values}" + + # Model time โ‰ˆ total copilot time minus tool time + local model_ms=$(( copilot_elapsed_ms - tool_total_ms )) + + printf " ${CYAN}%-38s${RESET} %d.%03ds ${DIM}(LLM code generation + response)${RESET}\n" \ + "๐Ÿค– Model" "$(( model_ms / 1000 ))" "$(( model_ms % 1000 ))" + printf " ${CYAN}%-38s${RESET} %d.%03ds ${DIM}(MCP tool total)${RESET}\n" \ + "๐Ÿ”ง Tool execution" "$(( tool_total_ms / 1000 ))" "$(( tool_total_ms % 1000 ))" + + # Sub-breakdown of tool time (indented) + if (( tool_init_ms > 0 )); then + printf " ${DIM}โ”œโ”€ Sandbox init: %5dms${RESET}\n" "${tool_init_ms}" + fi + printf " ${DIM}โ”œโ”€ Handler setup: %5dms${RESET}\n" "${tool_setup_ms}" + printf " ${DIM}โ”œโ”€ Compile & load: %5dms${RESET}\n" "${tool_compile_ms}" + printf " ${DIM}โ”œโ”€ Snapshot: %5dms${RESET}\n" "${tool_snap_ms}" + printf " ${DIM}โ””โ”€ JS execution: %5dms${RESET}\n" "${tool_exec_ms}" + else + warn "No tool timing data โ€” the model may not have called the tool" + fi + + log_elapsed "Total for '${title}'" "${prompt_start}" +} + +# โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +main() { + # Parse arguments โ€” extract --model before dispatching mode + local mode="interactive" + while [[ $# -gt 0 ]]; do + case "$1" in + --model) + if [[ -z "${2:-}" ]]; then + fail "--model requires a value (e.g. --model claude-opus-4.6)" + fi + MODEL="$2" + shift 2 + ;; + --show-code) + SHOW_CODE=true + shift + ;; + --show-command) + SHOW_COMMAND=true + shift + ;; + --prompt) + if [[ -z "${2:-}" ]]; then fail "--prompt requires a value"; fi + CUSTOM_PROMPT="$2" + shift 2 + ;; + --cpu-timeout) + if [[ -z "${2:-}" ]]; then fail "--cpu-timeout requires a value in ms"; fi + CPU_TIMEOUT="$2" + shift 2 + ;; + --wall-timeout) + if [[ -z "${2:-}" ]]; then fail "--wall-timeout requires a value in ms"; fi + WALL_TIMEOUT="$2" + shift 2 + ;; + --heap-size) + if [[ -z "${2:-}" ]]; then fail "--heap-size requires a value in MB"; fi + HEAP_SIZE="$2" + shift 2 + ;; + --stack-size) + if [[ -z "${2:-}" ]]; then fail "--stack-size requires a value in MB"; fi + STACK_SIZE="$2" + shift 2 + ;; + --install|--uninstall|--headless) + mode="$1" + shift + ;; + *) + fail "Unknown argument: $1\nUsage: $0 [--headless] [--install] [--uninstall] [--model ] [--prompt ] [--show-code] [--show-command] [--cpu-timeout ] [--wall-timeout ] [--heap-size ] [--stack-size ]" + ;; + esac + done + + banner + info "Using model: ${BOLD}${MODEL}${RESET}" + info "Sandbox limits: CPU ${BOLD}${CPU_TIMEOUT}ms${RESET}, wall ${BOLD}${WALL_TIMEOUT}ms${RESET}, heap ${BOLD}${HEAP_SIZE}MB${RESET}, stack ${BOLD}${STACK_SIZE}MB${RESET}" + + case "${mode}" in + --install) + check_prerequisites + install_mcp_config + echo "" + info "You can now use it in Copilot CLI:" + echo -e " ${BOLD}copilot${RESET}" + echo -e " ${DIM}> Calculate ฯ€ to 50 decimal places using the Machin formula${RESET}" + exit 0 + ;; + --uninstall) + uninstall_mcp_config + exit 0 + ;; + --headless) + check_prerequisites + check_config_conflict + + # If a custom prompt was given, run just that one and exit. + # "There can be only one." โ€” Highlander (1986) + if [[ -n "${CUSTOM_PROMPT}" ]]; then + info "Running custom prompt in headless mode..." + separator + local run_start + run_start="$(now_ms)" + if run_prompt "Custom prompt" "${CUSTOM_PROMPT}"; then + separator + log_elapsed "Custom prompt" "${run_start}" + exit 0 + else + separator + log_elapsed "Custom prompt" "${run_start}" + exit 1 + fi + fi + + info "Running ${DEMO_COUNT} demo prompts in headless mode..." + separator + + local run_start + run_start="$(now_ms)" + local passed=0 + local failed=0 + + for (( i=0; i<${#DEMO_PROMPTS[@]}; i+=2 )); do + local title="${DEMO_PROMPTS[$i]}" + local prompt="${DEMO_PROMPTS[$((i+1))]}" + + if run_prompt "${title}" "${prompt}"; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi + separator + done + + log_elapsed "All ${DEMO_COUNT} prompts" "${run_start}" + echo -e "\n${BOLD}Results: ${GREEN}${passed} passed${RESET}" + if (( failed > 0 )); then + echo -e " ${RED}${failed} failed${RESET}" + fi + exit "${failed}" + ;; + interactive|*) + check_prerequisites + check_config_conflict + + # Run the custom prompt first if one was provided + if [[ -n "${CUSTOM_PROMPT}" ]]; then + info "Running your custom prompt..." + separator + run_prompt "Custom prompt" "${CUSTOM_PROMPT}" + separator + info "Done! \"I'll be back.\" โ€” The Terminator (1984)" + exit 0 + fi + + info "This demo will run ${DEMO_COUNT} prompts through Copilot CLI," + info "each executing JavaScript inside a Hyperlight sandbox." + echo "" + info "The MCP server is configured per-session (no permanent install)." + info "To install permanently, run: ${BOLD}$0 --install${RESET}" + separator + + echo -e "${YELLOW}Ready to run demos?${RESET}" + read -rp "Press Enter to continue (or Ctrl+C to cancel)... " + echo "" + + for (( i=0; i<${#DEMO_PROMPTS[@]}; i+=2 )); do + local title="${DEMO_PROMPTS[$i]}" + local prompt="${DEMO_PROMPTS[$((i+1))]}" + + run_prompt "${title}" "${prompt}" + separator + + if (( i + 2 < ${#DEMO_PROMPTS[@]} )); then + read -rp "Next demo? (Enter to continue, q to quit) " answer + if [[ "${answer}" == "q" ]]; then + info "Thanks for watching! \"I'll be back.\" โ€” The Terminator (1984)" + exit 0 + fi + fi + done + + echo -e "${GREEN}${BOLD}๐ŸŽ‰ All demos complete!${RESET}" + echo -e "${DIM}\"That's all, folks!\" โ€” Porky Pig (well, 1930s, close enough)${RESET}" + ;; + esac +} + +main "$@" diff --git a/src/js-host-api/examples/mcp-server/demo.gif b/src/js-host-api/examples/mcp-server/demo.gif new file mode 100644 index 0000000..21edb11 Binary files /dev/null and b/src/js-host-api/examples/mcp-server/demo.gif differ diff --git a/src/js-host-api/examples/mcp-server/package-lock.json b/src/js-host-api/examples/mcp-server/package-lock.json new file mode 100644 index 0000000..ceeeb1f --- /dev/null +++ b/src/js-host-api/examples/mcp-server/package-lock.json @@ -0,0 +1,2592 @@ +{ + "name": "@hyperlight/mcp-server-example", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@hyperlight/mcp-server-example", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.26.0", + "zod": "^3.25.0" + }, + "devDependencies": { + "vitest": "^4.0.18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz", + "integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/src/js-host-api/examples/mcp-server/package.json b/src/js-host-api/examples/mcp-server/package.json new file mode 100644 index 0000000..f971d86 --- /dev/null +++ b/src/js-host-api/examples/mcp-server/package.json @@ -0,0 +1,19 @@ +{ + "name": "@hyperlight/mcp-server-example", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "MCP server that executes JavaScript in a Hyperlight sandbox โ€” safe, isolated, CPU-bounded", + "main": "server.js", + "scripts": { + "start": "node server.js", + "test": "vitest run" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.26.0", + "zod": "^3.25.0" + }, + "devDependencies": { + "vitest": "^4.0.18" + } +} diff --git a/src/js-host-api/examples/mcp-server/server.js b/src/js-host-api/examples/mcp-server/server.js new file mode 100644 index 0000000..b94bf05 --- /dev/null +++ b/src/js-host-api/examples/mcp-server/server.js @@ -0,0 +1,404 @@ +#!/usr/bin/env node + +// โ”€โ”€ Hyperlight JS MCP Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// +// An MCP (Model Context Protocol) server that allows AI agents to +// execute JavaScript code inside a Hyperlight sandbox with strict +// CPU time bounding. +// +// "In the sandbox, no one can hear you scream." โ€” Alien (1979), adapted +// +// Features: +// - Isolated execution via Hyperlight (no filesystem, no network) +// - CPU time limit: configurable (default 1000ms) via HYPERLIGHT_CPU_TIMEOUT_MS +// - Wall-clock backstop: configurable (default 5000ms) via HYPERLIGHT_WALL_TIMEOUT_MS +// - Automatic snapshot/restore recovery after timeouts +// - Sandbox reuse across invocations for performance +// - Optional timing log (HYPERLIGHT_TIMING_LOG) for performance analysis +// +// Transport: stdio (standard for local MCP integrations) +// Tool: execute_javascript +// +// Usage: +// node server.js +// +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; +import { createRequire } from 'node:module'; +import { appendFileSync } from 'node:fs'; + +const require = createRequire(import.meta.url); +const { SandboxBuilder } = require('../../lib.js'); + +// โ”€โ”€ Defaults โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** Default maximum CPU time per execution (milliseconds). */ +const DEFAULT_CPU_TIMEOUT_MS = 1000; + +/** Default maximum wall-clock time per execution (milliseconds). */ +const DEFAULT_WALL_CLOCK_TIMEOUT_MS = 5000; + +/** Default guest heap size in megabytes. */ +const DEFAULT_HEAP_SIZE_MB = 16; + +/** Default guest stack size in megabytes. */ +const DEFAULT_STACK_SIZE_MB = 1; + +// โ”€โ”€ Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// +// All sandbox limits are configurable via environment variables. +// The server operator sets the ceiling โ€” the calling agent cannot +// override them. "The power is yours!" โ€” Captain Planet (1990โ€ฆ close enough) + +/** + * Parse a positive integer from an environment variable, falling back + * to `defaultVal` when the variable is unset, empty, or not a valid + * positive integer. + * + * @param {string|undefined} raw โ€” raw env-var value + * @param {number} defaultVal โ€” fallback + * @returns {number} + */ +function parsePositiveInt(raw, defaultVal) { + if (raw === undefined || raw === '') return defaultVal; + const n = Number(raw); + if (!Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) { + console.error( + `[hyperlight] Warning: ignoring invalid value "${raw}", using default ${defaultVal}` + ); + return defaultVal; + } + return n; +} + +/** Maximum CPU time per execution (milliseconds). + * Override with HYPERLIGHT_CPU_TIMEOUT_MS. */ +const CPU_TIMEOUT_MS = parsePositiveInt( + process.env.HYPERLIGHT_CPU_TIMEOUT_MS, + DEFAULT_CPU_TIMEOUT_MS +); + +/** Maximum wall-clock time per execution (milliseconds). Backstop for + * edge cases where CPU time alone doesn't catch the issue. + * Override with HYPERLIGHT_WALL_TIMEOUT_MS. */ +const WALL_CLOCK_TIMEOUT_MS = parsePositiveInt( + process.env.HYPERLIGHT_WALL_TIMEOUT_MS, + DEFAULT_WALL_CLOCK_TIMEOUT_MS +); + +/** Guest heap size in bytes. Override with HYPERLIGHT_HEAP_SIZE_MB (megabytes). */ +const HEAP_SIZE_BYTES = + parsePositiveInt(process.env.HYPERLIGHT_HEAP_SIZE_MB, DEFAULT_HEAP_SIZE_MB) * 1024 * 1024; + +/** Guest stack size in bytes. Override with HYPERLIGHT_STACK_SIZE_MB (megabytes). */ +const STACK_SIZE_BYTES = + parsePositiveInt(process.env.HYPERLIGHT_STACK_SIZE_MB, DEFAULT_STACK_SIZE_MB) * 1024 * 1024; + +/** + * Path to a timing log file. When set (via the HYPERLIGHT_TIMING_LOG + * environment variable), the server appends one JSON line per tool + * invocation with a breakdown of sandbox init, setup, compile, and + * execution times. The demo script reads this to show where time went. + */ +const TIMING_LOG_PATH = process.env.HYPERLIGHT_TIMING_LOG || null; + +/** + * Path to a code log file. When set (via the HYPERLIGHT_CODE_LOG + * environment variable), the server writes the received JavaScript + * source code to this file on each tool invocation. The demo script + * reads it back to show what the model generated. + */ +const CODE_LOG_PATH = process.env.HYPERLIGHT_CODE_LOG || null; + +// โ”€โ”€ Sandbox Lifecycle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// +// The sandbox follows a state machine: +// +// [null] โ”€โ”€buildโ”€โ”€โ–ถ [ProtoJSSandbox] โ”€โ”€loadRuntimeโ”€โ”€โ–ถ [JSSandbox] +// โ”‚ +// addHandler + getLoadedSandbox +// โ”‚ +// โ–ผ +// [LoadedJSSandbox] +// โ”‚ โ”‚ +// callHandler unload +// โ”‚ โ”‚ +// โ–ผ โ–ผ +// (result) [JSSandbox] +// +// Between invocations, we keep the sandbox in [JSSandbox] state so we +// can register new handler code for each execution request. After a +// timeout or unrecoverable error, jsSandbox is set to null and +// rebuilt on the next call. + +/** @type {import('../../index.d.ts').JSSandbox | null} */ +let jsSandbox = null; + +/** + * Build a fresh sandbox from scratch. + * Called once on first invocation, and again after unrecoverable errors. + */ +async function initializeSandbox() { + const builder = new SandboxBuilder(); + builder.setHeapSize(HEAP_SIZE_BYTES); + builder.setStackSize(STACK_SIZE_BYTES); + + const proto = await builder.build(); + jsSandbox = await proto.loadRuntime(); + + // Log to stderr โ€” stdout is reserved for MCP protocol messages + console.error('[hyperlight] Sandbox initialized'); +} + +/** + * Execute arbitrary JavaScript code inside the Hyperlight sandbox. + * + * The code is wrapped as the body of a `handler(event)` function. + * Use `return` to produce a JSON-serializable result. The `event` + * object is an empty `{}` โ€” provided for API consistency but usually + * not needed. + * + * @param {string} code โ€” JavaScript source to execute + * @returns {Promise<{success: boolean, result?: any, error?: string}>} + */ +async function executeJavaScript(code) { + /** Timing record โ€” filled in progressively, written at the end. */ + const timing = { + initMs: 0, + setupMs: 0, + compileMs: 0, + executeMs: 0, + snapshotMs: 0, + totalMs: 0, + }; + const totalStart = performance.now(); + + // Lazy initialization โ€” build sandbox on first call + if (jsSandbox === null) { + const initStart = performance.now(); + await initializeSandbox(); + timing.initMs = Math.round(performance.now() - initStart); + } + + // Wrap user code as a handler function body. + // The user writes code as if inside a function: + // let x = 2 + 2; + // return { answer: x }; + // + // We wrap it as: + // function handler(event) { + // let x = 2 + 2; + // return { answer: x }; + // } + const wrappedCode = `function handler(event) {\n${code}\n}`; + + // โ”€โ”€ Register handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const setupStart = performance.now(); + try { + jsSandbox.clearHandlers(); + jsSandbox.addHandler('execute', wrappedCode); + } catch (err) { + // Sandbox in bad state โ€” force reinit on next call + jsSandbox = null; + return { success: false, error: `Setup error: ${err.message}` }; + } + timing.setupMs = Math.round(performance.now() - setupStart); + + // โ”€โ”€ Compile & load โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const compileStart = performance.now(); + let loaded; + try { + loaded = await jsSandbox.getLoadedSandbox(); + } catch (err) { + // Compilation failed (syntax error in user code, or sandbox consumed) + // The JSSandbox may be consumed โ€” reinitialize on next call + jsSandbox = null; + return { success: false, error: `Compilation error: ${err.message}` }; + } + timing.compileMs = Math.round(performance.now() - compileStart); + + // โ”€โ”€ Snapshot for timeout recovery โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const snapStart = performance.now(); + let snapshot; + try { + snapshot = await loaded.snapshot(); + } catch (err) { + jsSandbox = null; + return { success: false, error: `Snapshot error: ${err.message}` }; + } + timing.snapshotMs = Math.round(performance.now() - snapStart); + + // โ”€โ”€ Execute with CPU + wall-clock guards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const execStart = performance.now(); + try { + const result = await loaded.callHandler( + 'execute', + {}, + { + cpuTimeoutMs: CPU_TIMEOUT_MS, + wallClockTimeoutMs: WALL_CLOCK_TIMEOUT_MS, + } + ); + timing.executeMs = Math.round(performance.now() - execStart); + timing.totalMs = Math.round(performance.now() - totalStart); + writeTiming(timing); + + // Success โ€” return to JSSandbox state for the next invocation + jsSandbox = await loaded.unload(); + return { success: true, result }; + } catch (err) { + // โ”€โ”€ Build a user-friendly error message โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let errorMessage; + if (err.code === 'ERR_CANCELLED') { + errorMessage = + `Execution timed out โ€” CPU limit: ${CPU_TIMEOUT_MS}ms, ` + + `wall-clock limit: ${WALL_CLOCK_TIMEOUT_MS}ms. ` + + 'Try a less expensive computation or reduce iteration count.'; + } else { + errorMessage = `Runtime error: ${err.message}`; + } + + // โ”€โ”€ Attempt recovery โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + try { + if (loaded.poisoned) { + await loaded.restore(snapshot); + } + jsSandbox = await loaded.unload(); + } catch { + // Recovery failed โ€” sandbox will be rebuilt on next call + jsSandbox = null; + } + + timing.executeMs = Math.round(performance.now() - execStart); + timing.totalMs = Math.round(performance.now() - totalStart); + writeTiming(timing); + + return { success: false, error: errorMessage }; + } +} + +/** + * Append a JSON timing record to the timing log file (if configured). + * The demo script reads these to show model vs. tool time breakdown. + * + * @param {Record} timing + */ +function writeTiming(timing) { + if (!TIMING_LOG_PATH) return; + try { + appendFileSync(TIMING_LOG_PATH, JSON.stringify(timing) + '\n'); + } catch { + // Best-effort โ€” don't let logging failures break execution + console.error('[hyperlight] Warning: failed to write timing log'); + } +} + +// โ”€โ”€ MCP Server Setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +const mcpServer = new McpServer({ + name: 'hyperlight-js-sandbox', + version: '0.1.0', +}); + +mcpServer.registerTool( + 'execute_javascript', + { + title: 'Execute JavaScript in Hyperlight Sandbox', + description: [ + 'Execute JavaScript code inside an isolated Hyperlight micro-VM.', + '', + 'The code runs as the body of a function โ€” use `return` to produce', + `a JSON-serializable result. CPU time is hard-limited to ${CPU_TIMEOUT_MS}ms`, + `with a ${WALL_CLOCK_TIMEOUT_MS}ms wall-clock backstop.`, + `Memory: ${HEAP_SIZE_BYTES / (1024 * 1024)}MB heap, ${STACK_SIZE_BYTES / (1024 * 1024)}MB stack.`, + '', + 'The sandbox has NO access to:', + ' - Filesystem, network, or host environment', + ' - Node.js APIs (require, process, fs, etc.)', + ' - Browser APIs (fetch, DOM, setTimeout, etc.)', + '', + 'The sandbox DOES support:', + ' - Full ECMAScript (ES2023) โ€” variables, functions, classes, closures', + ' - Math, String, Array, Object, Map, Set, JSON, RegExp', + ' - BigInt, Symbol, Proxy, Reflect, Promise (sync resolution)', + ' - Typed arrays (Uint8Array, Float64Array, etc.)', + ' - Structured algorithms, data processing, pure computation', + '', + 'Tips:', + ' - Use `return { ... }` to send back structured results', + ` - Keep iteration counts reasonable (${CPU_TIMEOUT_MS}ms CPU limit)`, + ' - No I/O โ€” all data must be computed, not fetched', + ' - Date.now() is available for timing within the sandbox', + ].join('\n'), + inputSchema: z.object({ + code: z + .string() + .describe( + 'JavaScript code to execute as a function body. ' + + 'Use `return` to produce output. ' + + 'Example: `let x = 2 + 2; return { result: x };`' + ), + }), + }, + async ({ code }) => { + // Log the received code if configured (demo --show-code flag) + if (CODE_LOG_PATH) { + try { + appendFileSync(CODE_LOG_PATH, code); + } catch { + console.error('[hyperlight] Warning: failed to write code log'); + } + } + + const startTime = Date.now(); + const { success, result, error } = await executeJavaScript(code); + const elapsed = Date.now() - startTime; + + if (success) { + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + } else { + return { + content: [ + { + type: 'text', + text: `โŒ ${error}\n\n(elapsed: ${elapsed}ms)`, + }, + ], + isError: true, + }; + } + } +); + +// โ”€โ”€ Start Server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +const transport = new StdioServerTransport(); +await mcpServer.connect(transport); + +// Log to stderr โ€” stdout is reserved for MCP JSON-RPC messages +console.error('๐Ÿ”’ Hyperlight JS MCP Server running on stdio'); +console.error(` CPU timeout: ${CPU_TIMEOUT_MS}ms`); +console.error(` Wall-clock timeout: ${WALL_CLOCK_TIMEOUT_MS}ms`); +console.error(` Heap size: ${HEAP_SIZE_BYTES / (1024 * 1024)}MB`); +console.error(` Stack size: ${STACK_SIZE_BYTES / (1024 * 1024)}MB`); + +// โ”€โ”€ Graceful Shutdown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +const shutdown = async () => { + console.error('[hyperlight] Shutting down...'); + await mcpServer.close(); + process.exit(0); +}; + +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); diff --git a/src/js-host-api/examples/mcp-server/tests/config.test.js b/src/js-host-api/examples/mcp-server/tests/config.test.js new file mode 100644 index 0000000..491a4fd --- /dev/null +++ b/src/js-host-api/examples/mcp-server/tests/config.test.js @@ -0,0 +1,270 @@ +// โ”€โ”€ Hyperlight JS MCP Server โ€” Configuration Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// +// Validates that sandbox limits (CPU timeout, wall-clock timeout, heap +// size, stack size) are configurable via environment variables, that +// invalid values fall back to defaults gracefully, and that the tool +// description dynamically reflects the configured limits. +// +// "You can tune a piano, but you can't tuna fish." +// โ€” REO Speedwagon (1978โ€ฆ close enough to the 80s) +// +// Each describe block spawns a separate server process with different +// env var configurations to test the behaviour in isolation. +// +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SERVER_PATH = join(__dirname, '..', 'server.js'); +const PROTOCOL_VERSION = '2025-11-25'; + +// โ”€โ”€ NDJSON Utilities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function send(proc, message) { + proc.stdin.write(JSON.stringify(message) + '\n'); +} + +function waitForResponse(proc) { + return new Promise((resolve, reject) => { + let buffer = ''; + const onData = (chunk) => { + buffer += chunk.toString(); + const idx = buffer.indexOf('\n'); + if (idx === -1) return; + const line = buffer.slice(0, idx).replace(/\r$/, ''); + buffer = buffer.slice(idx + 1); + proc.stdout.off('data', onData); + if (line.length === 0) return; + try { + resolve(JSON.parse(line)); + } catch (_err) { + reject(new Error(`Invalid JSON from server: ${line}`)); + } + }; + proc.stdout.on('data', onData); + }); +} + +/** + * Spawn a server with the given env overrides, perform the MCP + * handshake, and return { server, messageId, callTool, listTools }. + * + * @param {Record} envOverrides + * @returns {Promise<{server: import('node:child_process').ChildProcess, messageId: {value: number}, callTool: (code: string) => Promise, listTools: () => Promise}>} + */ +async function spawnServer(envOverrides = {}) { + const messageId = { value: 1 }; + const server = spawn('node', [SERVER_PATH], { + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, ...envOverrides }, + }); + + // Collect stderr for debugging (vitest captures it) + const stderrChunks = []; + server.stderr.on('data', (d) => { + stderrChunks.push(d.toString()); + process.stderr.write(`[config-test] ${d}`); + }); + + // MCP handshake โ€” initialize + send(server, { + jsonrpc: '2.0', + id: messageId.value++, + method: 'initialize', + params: { + protocolVersion: PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: 'vitest-config-client', version: '1.0.0' }, + }, + }); + + const initResponse = await waitForResponse(server); + expect(initResponse.result).toBeDefined(); + + // MCP handshake โ€” initialized notification + send(server, { + jsonrpc: '2.0', + method: 'notifications/initialized', + }); + await new Promise((r) => setTimeout(r, 200)); + + /** Call execute_javascript and return the full response. */ + const callTool = async (code) => { + send(server, { + jsonrpc: '2.0', + id: messageId.value++, + method: 'tools/call', + params: { + name: 'execute_javascript', + arguments: { code }, + }, + }); + return waitForResponse(server); + }; + + /** Call tools/list and return the full response. */ + const listTools = async () => { + send(server, { + jsonrpc: '2.0', + id: messageId.value++, + method: 'tools/list', + }); + return waitForResponse(server); + }; + + return { server, messageId, callTool, listTools, stderrChunks }; +} + +// โ”€โ”€ Custom CPU Timeout โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('Custom CPU timeout (HYPERLIGHT_CPU_TIMEOUT_MS)', () => { + let ctx; + + beforeAll(async () => { + // Set a very short CPU timeout โ€” 100ms. Computations that take + // ~500ms of CPU should be killed. "Short fuse!" โ€” Rambo (1982) + ctx = await spawnServer({ HYPERLIGHT_CPU_TIMEOUT_MS: '100' }); + }); + + afterAll(() => { + if (ctx?.server) ctx.server.kill(); + }); + + it('should timeout a computation that exceeds the custom limit', async () => { + // This loop burns ~500ms of CPU โ€” well over our 100ms limit. + // With the default 1000ms it would succeed; with 100ms it + // should be killed. + const code = ` + let sum = 0; + for (let i = 0; i < 50000000; i++) sum += i; + return { sum }; + `; + + const response = await ctx.callTool(code); + expect(response.result.isError).toBe(true); + expect(response.result.content[0].text).toContain('timed out'); + // Error message should reflect the custom 100ms limit + expect(response.result.content[0].text).toContain('100ms'); + }); + + it('should still execute fast code successfully', async () => { + // Simple arithmetic โ€” well under 100ms + const response = await ctx.callTool('return { answer: 6 * 7 };'); + const parsed = JSON.parse(response.result.content[0].text); + expect(parsed.answer).toBe(42); + }); +}); + +// โ”€โ”€ Tool Description Reflects Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('Tool description reflects configured limits', () => { + let ctx; + + beforeAll(async () => { + ctx = await spawnServer({ + HYPERLIGHT_CPU_TIMEOUT_MS: '2000', + HYPERLIGHT_WALL_TIMEOUT_MS: '8000', + HYPERLIGHT_HEAP_SIZE_MB: '32', + HYPERLIGHT_STACK_SIZE_MB: '2', + }); + }); + + afterAll(() => { + if (ctx?.server) ctx.server.kill(); + }); + + it('should include custom CPU timeout in tool description', async () => { + const response = await ctx.listTools(); + const jsTool = response.result.tools.find((t) => t.name === 'execute_javascript'); + expect(jsTool).toBeDefined(); + expect(jsTool.description).toContain('2000ms'); + }); + + it('should include custom wall-clock timeout in tool description', async () => { + const response = await ctx.listTools(); + const jsTool = response.result.tools.find((t) => t.name === 'execute_javascript'); + expect(jsTool.description).toContain('8000ms'); + }); + + it('should include custom heap size in tool description', async () => { + const response = await ctx.listTools(); + const jsTool = response.result.tools.find((t) => t.name === 'execute_javascript'); + expect(jsTool.description).toContain('32MB'); + }); + + it('should include custom stack size in tool description', async () => { + const response = await ctx.listTools(); + const jsTool = response.result.tools.find((t) => t.name === 'execute_javascript'); + expect(jsTool.description).toContain('2MB'); + }); +}); + +// โ”€โ”€ Invalid Env Vars Fallback โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('Invalid env vars fall back to defaults', () => { + let ctx; + + beforeAll(async () => { + // "Garbage in, defaults out" โ€” every sysadmin ever + ctx = await spawnServer({ + HYPERLIGHT_CPU_TIMEOUT_MS: 'banana', + HYPERLIGHT_WALL_TIMEOUT_MS: '-999', + HYPERLIGHT_HEAP_SIZE_MB: '0', + HYPERLIGHT_STACK_SIZE_MB: '3.14', + }); + }); + + afterAll(() => { + if (ctx?.server) ctx.server.kill(); + }); + + it('should start successfully despite invalid config', async () => { + // If we got here, the server started and completed the MCP + // handshake โ€” that's the main assertion. + const response = await ctx.callTool('return { ok: true };'); + const parsed = JSON.parse(response.result.content[0].text); + expect(parsed.ok).toBe(true); + }); + + it('should use default CPU timeout (code that runs under 1000ms succeeds)', async () => { + // This light computation should succeed with the default + // 1000ms timeout but would fail if the server had somehow + // parsed 'banana' as 0 or some tiny value. + const code = ` + const primes = []; + for (let n = 2; primes.length < 100; n++) { + let ok = true; + for (let d = 2; d * d <= n; d++) { + if (n % d === 0) { ok = false; break; } + } + if (ok) primes.push(n); + } + return { count: primes.length }; + `; + const response = await ctx.callTool(code); + const parsed = JSON.parse(response.result.content[0].text); + expect(parsed.count).toBe(100); + }); + + it('should show default values in tool description', async () => { + const response = await ctx.listTools(); + const jsTool = response.result.tools.find((t) => t.name === 'execute_javascript'); + // Default values should appear since the invalid ones were rejected + expect(jsTool.description).toContain('1000ms'); + expect(jsTool.description).toContain('5000ms'); + expect(jsTool.description).toContain('16MB'); + expect(jsTool.description).toContain('1MB'); + }); + + it('should log warnings to stderr about invalid values', () => { + const stderr = ctx.stderrChunks.join(''); + expect(stderr).toContain('ignoring invalid value "banana"'); + expect(stderr).toContain('ignoring invalid value "-999"'); + expect(stderr).toContain('ignoring invalid value "0"'); + expect(stderr).toContain('ignoring invalid value "3.14"'); + }); +}); diff --git a/src/js-host-api/examples/mcp-server/tests/mcp-server.test.js b/src/js-host-api/examples/mcp-server/tests/mcp-server.test.js new file mode 100644 index 0000000..8891666 --- /dev/null +++ b/src/js-host-api/examples/mcp-server/tests/mcp-server.test.js @@ -0,0 +1,247 @@ +// โ”€โ”€ Hyperlight JS MCP Server โ€” Integration Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// +// Tests the MCP server end-to-end by spawning it as a child process +// and communicating via stdio using NDJSON (newline-delimited JSON), +// which is the framing format used by the MCP stdio transport. +// +// "Trust, but verify." โ€” Reagan (1987), also good advice for sandboxes +// +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +/** Path to server.js โ€” one directory up from tests/ */ +const SERVER_PATH = join(__dirname, '..', 'server.js'); + +/** + * The protocol version the MCP SDK (v1.26.0) expects. + * Must match LATEST_PROTOCOL_VERSION in the SDK. + */ +const PROTOCOL_VERSION = '2025-11-25'; + +// โ”€โ”€ NDJSON Framing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// +// MCP stdio transport uses newline-delimited JSON (NDJSON): +// - Send: JSON.stringify(message) + '\n' +// - Receive: read lines, parse each as JSON +// +// NOT Content-Length / LSP framing. + +/** + * Send a JSON-RPC message to the server via stdin (NDJSON framing). + * + * @param {import('node:child_process').ChildProcess} proc + * @param {object} message โ€” JSON-RPC message object + */ +function send(proc, message) { + proc.stdin.write(JSON.stringify(message) + '\n'); +} + +/** + * Wait for the next JSON-RPC response from the server's stdout. + * Reads newline-delimited JSON. + * + * @param {import('node:child_process').ChildProcess} proc + * @returns {Promise} โ€” parsed JSON-RPC response + */ +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); + }); +} + +// โ”€โ”€ Test Suite โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('MCP Server', () => { + let server; + let messageId = 1; + + /** + * Call the execute_javascript MCP tool and return the parsed response. + * + * @param {string} code โ€” JavaScript code to execute + * @returns {Promise} โ€” full JSON-RPC response + */ + async function callExecuteJavaScript(code) { + send(server, { + jsonrpc: '2.0', + id: messageId++, + method: 'tools/call', + params: { + name: 'execute_javascript', + arguments: { code }, + }, + }); + return waitForResponse(server); + } + + beforeAll(async () => { + // Start the MCP server as a child process + server = spawn('node', [SERVER_PATH], { + stdio: ['pipe', 'pipe', 'pipe'], + }); + + // Surface server stderr for debugging (vitest captures it) + server.stderr.on('data', (d) => { + process.stderr.write(`[mcp-server] ${d}`); + }); + + // MCP handshake โ€” initialize + send(server, { + jsonrpc: '2.0', + id: messageId++, + method: 'initialize', + params: { + protocolVersion: PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: 'vitest-mcp-client', version: '1.0.0' }, + }, + }); + + const initResponse = await waitForResponse(server); + expect(initResponse.result).toBeDefined(); + expect(initResponse.result.serverInfo?.name).toBe('hyperlight-js-sandbox'); + + // MCP handshake โ€” send initialized notification (no response expected) + send(server, { + jsonrpc: '2.0', + method: 'notifications/initialized', + }); + + // Let the notification process + await new Promise((r) => setTimeout(r, 200)); + }); + + afterAll(() => { + if (server) { + server.kill(); + } + }); + + // โ”€โ”€ Tool Discovery โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + it('should list execute_javascript tool', async () => { + send(server, { + jsonrpc: '2.0', + id: messageId++, + method: 'tools/list', + }); + const response = await waitForResponse(server); + + const tools = response.result?.tools; + expect(tools).toBeInstanceOf(Array); + + const jsTool = tools.find((t) => t.name === 'execute_javascript'); + expect(jsTool).toBeDefined(); + expect(jsTool.description).toContain('Hyperlight'); + expect(jsTool.inputSchema.properties.code).toBeDefined(); + }); + + // โ”€โ”€ Successful Execution โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + it('should execute simple arithmetic', async () => { + const response = await callExecuteJavaScript('return { result: 2 + 2 };'); + const parsed = JSON.parse(response.result.content[0].text); + expect(parsed.result).toBe(4); + }); + + it('should compute Fibonacci sequence', async () => { + const code = [ + 'const fib = [0, 1];', + 'for (let i = 2; i < 10; i++) {', + ' fib.push(fib[i - 1] + fib[i - 2]);', + '}', + 'return { fibonacci: fib };', + ].join('\n'); + + const response = await callExecuteJavaScript(code); + const parsed = JSON.parse(response.result.content[0].text); + expect(parsed.fibonacci).toEqual([0, 1, 1, 2, 3, 5, 8, 13, 21, 34]); + }); + + it('should handle string operations', async () => { + const code = ` + const msg = 'Hello, Hyperlight!'; + return { upper: msg.toUpperCase(), length: msg.length }; + `; + const response = await callExecuteJavaScript(code); + const parsed = JSON.parse(response.result.content[0].text); + expect(parsed.upper).toBe('HELLO, HYPERLIGHT!'); + expect(parsed.length).toBe(18); + }); + + it('should handle array operations', async () => { + const code = ` + const nums = [5, 3, 8, 1, 9, 2, 7, 4, 6]; + return { + sorted: nums.slice().sort((a, b) => a - b), + sum: nums.reduce((a, b) => a + b, 0), + max: Math.max(...nums), + }; + `; + const response = await callExecuteJavaScript(code); + const parsed = JSON.parse(response.result.content[0].text); + expect(parsed.sorted).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); + expect(parsed.sum).toBe(45); + expect(parsed.max).toBe(9); + }); + + // โ”€โ”€ Timeout Enforcement โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + it('should kill infinite loops with CPU timeout', async () => { + const response = await callExecuteJavaScript('while (true) {}'); + + expect(response.result.isError).toBe(true); + expect(response.result.content[0].text).toContain('timed out'); + }); + + // โ”€โ”€ Recovery โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + it('should recover and execute after a timeout', async () => { + // Previous test caused a timeout โ€” this verifies recovery + const response = await callExecuteJavaScript('return { result: 3 * 7 };'); + const parsed = JSON.parse(response.result.content[0].text); + expect(parsed.result).toBe(21); + }); + + // โ”€โ”€ Error Handling โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + it('should report syntax errors gracefully', async () => { + const response = await callExecuteJavaScript('this is not valid javascript ???'); + expect(response.result.isError).toBe(true); + }); + + it('should report runtime errors gracefully', async () => { + const response = await callExecuteJavaScript('throw new Error("deliberate failure");'); + expect(response.result.isError).toBe(true); + expect(response.result.content[0].text).toContain('deliberate failure'); + }); +}); diff --git a/src/js-host-api/examples/mcp-server/tests/prompt-examples.test.js b/src/js-host-api/examples/mcp-server/tests/prompt-examples.test.js new file mode 100644 index 0000000..7dc3e3c --- /dev/null +++ b/src/js-host-api/examples/mcp-server/tests/prompt-examples.test.js @@ -0,0 +1,910 @@ +// โ”€โ”€ README Example Prompts โ€” Validation Test Suite โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// +// Exercises every example prompt from the README by generating the +// JavaScript code an AI agent would produce, executing it through +// the MCP server, and asserting the results are correct. +// +// "If you build it, they will come." โ€” Field of Dreams (1989) +// "If you prompt it, it better work." โ€” Us (2026) +// +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SERVER_PATH = join(__dirname, '..', 'server.js'); +const PROTOCOL_VERSION = '2025-11-25'; + +// โ”€โ”€ NDJSON Utilities (MCP stdio transport framing) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function send(proc, message) { + proc.stdin.write(JSON.stringify(message) + '\n'); +} + +function waitForResponse(proc) { + return new Promise((resolve, reject) => { + let buffer = ''; + const onData = (chunk) => { + buffer += chunk.toString(); + const idx = buffer.indexOf('\n'); + if (idx === -1) return; + const line = buffer.slice(0, idx).replace(/\r$/, ''); + buffer = buffer.slice(idx + 1); + proc.stdout.off('data', onData); + if (line.length === 0) return; + try { + resolve(JSON.parse(line)); + } catch (_err) { + reject(new Error(`Invalid JSON from server: ${line}`)); + } + }; + proc.stdout.on('data', onData); + }); +} + +// โ”€โ”€ Code Implementations for Each README Prompt โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// +// Each constant is the JavaScript code an AI agent would generate +// in response to the corresponding README prompt. The code runs as +// the body of `function handler(event) { ... }` inside the sandbox. + +// โ”€โ”€ Mathematics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** 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' }; +`; + +/** Prompt: "Find all prime numbers below 10,000 using the Sieve of Eratosthenes" */ +const SIEVE_CODE = ` +const limit = 10000; +const sieve = new Array(limit).fill(true); +sieve[0] = sieve[1] = false; +for (let i = 2; i * i < limit; i++) { + if (sieve[i]) { + for (let j = i * i; j < limit; j += i) sieve[j] = false; + } +} +const primes = []; +for (let i = 0; i < limit; i++) { + if (sieve[i]) primes.push(i); +} +return { count: primes.length, last10: primes.slice(-10) }; +`; + +/** Prompt: "Compute the first 100 digits of Euler's number (e) using the Taylor series" */ +const EULER_100_DIGITS_CODE = ` +// e = ฮฃ 1/n! for n = 0, 1, 2, ... +// Using BigInt fixed-point arithmetic for 100+ digits of precision. +const DIGITS = 100; +const SCALE = 10n ** BigInt(DIGITS + 15); // extra precision for rounding + +let sum = 0n; +let factorial = 1n; +for (let n = 0; n < 200; n++) { + sum += SCALE / factorial; + factorial *= BigInt(n + 1); +} + +const s = sum.toString(); +const formatted = s[0] + '.' + s.slice(1, DIGITS + 1); +return { e: formatted, digits: DIGITS, method: 'Taylor series with BigInt' }; +`; + +/** Prompt: "Run a Monte Carlo simulation with 100,000 random dart throws to estimate ฯ€" */ +const MONTE_CARLO_CODE = ` +let inside = 0; +const N = 100000; +for (let i = 0; i < N; i++) { + const x = Math.random(); + const y = Math.random(); + if (x * x + y * y <= 1) inside++; +} +const piEstimate = 4 * inside / N; +return { + pi: piEstimate, + throws: N, + inside, + error: Math.abs(piEstimate - Math.PI), +}; +`; + +// โ”€โ”€ Algorithms & Data Structures โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** Prompt: "Implement quicksort and mergesort, sort an array of 5,000 random numbers" */ +const SORT_COMPARISON_CODE = ` +function quicksort(arr) { + if (arr.length <= 1) return arr; + const pivot = arr[Math.floor(arr.length / 2)]; + const left = arr.filter(x => x < pivot); + const mid = arr.filter(x => x === pivot); + const right = arr.filter(x => x > pivot); + return [...quicksort(left), ...mid, ...quicksort(right)]; +} + +function mergesort(arr) { + if (arr.length <= 1) return arr; + const m = Math.floor(arr.length / 2); + const left = mergesort(arr.slice(0, m)); + const right = mergesort(arr.slice(m)); + const out = []; + let i = 0, j = 0; + while (i < left.length && j < right.length) { + out.push(left[i] <= right[j] ? left[i++] : right[j++]); + } + while (i < left.length) out.push(left[i++]); + while (j < right.length) out.push(right[j++]); + return out; +} + +const N = 5000; +const arr = Array.from({ length: N }, () => Math.floor(Math.random() * 1000000)); +const qsorted = quicksort(arr.slice()); +const msorted = mergesort(arr.slice()); + +return { + size: N, + quicksortCorrect: qsorted.every((v, i, a) => i === 0 || a[i - 1] <= v), + mergesortCorrect: msorted.every((v, i, a) => i === 0 || a[i - 1] <= v), + match: JSON.stringify(qsorted) === JSON.stringify(msorted), + first5: qsorted.slice(0, 5), + last5: qsorted.slice(-5), +}; +`; + +/** Prompt: "Solve the Tower of Hanoi for 15 disks" */ +const TOWER_OF_HANOI_CODE = ` +const N = 15; +let moveCount = 0; +const firstMoves = []; + +function hanoi(n, from, to, via) { + if (n === 0) return; + hanoi(n - 1, from, via, to); + moveCount++; + if (firstMoves.length < 10) { + firstMoves.push({ move: moveCount, disk: n, from, to }); + } + hanoi(n - 1, via, to, from); +} + +hanoi(N, 'A', 'C', 'B'); +return { disks: N, totalMoves: moveCount, firstMoves }; +`; + +/** Prompt: "Find the longest common subsequence of 'AGGTAB' and 'GXTXAYB'" */ +const LCS_CODE = ` +const s1 = 'AGGTAB'; +const s2 = 'GXTXAYB'; +const m = s1.length, n = s2.length; + +// Build DP table +const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); +for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + if (s1[i - 1] === s2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } +} + +// Backtrack to recover the subsequence +let lcs = ''; +let i = m, j = n; +while (i > 0 && j > 0) { + if (s1[i - 1] === s2[j - 1]) { + lcs = s1[i - 1] + lcs; + i--; j--; + } else if (dp[i - 1][j] > dp[i][j - 1]) { + i--; + } else { + j--; + } +} + +return { s1, s2, lcs, length: lcs.length }; +`; + +/** Prompt: "Implement a trie data structure, insert 1000 random 8-letter words" */ +const TRIE_CODE = ` +class TrieNode { + constructor() { this.children = {}; this.isEnd = false; } +} + +class Trie { + constructor() { this.root = new TrieNode(); } + insert(word) { + let node = this.root; + for (const ch of word) { + if (!node.children[ch]) node.children[ch] = new TrieNode(); + node = node.children[ch]; + } + node.isEnd = true; + } + search(word) { + let node = this.root; + for (const ch of word) { + if (!node.children[ch]) return false; + node = node.children[ch]; + } + return node.isEnd; + } +} + +function randomWord() { + const chars = 'abcdefghijklmnopqrstuvwxyz'; + let w = ''; + for (let i = 0; i < 8; i++) w += chars[Math.floor(Math.random() * 26)]; + return w; +} + +const trie = new Trie(); +const words = []; +for (let i = 0; i < 1000; i++) { + const w = randomWord(); + words.push(w); + trie.insert(w); +} + +// Search for 100 known words โ€” all should be found +let found = 0; +for (let i = 0; i < 100; i++) { + if (trie.search(words[i])) found++; +} + +// Search for words that almost certainly don't exist +let falsePositives = 0; +for (let i = 0; i < 100; i++) { + if (trie.search('zz' + randomWord().slice(2))) falsePositives++; +} + +return { inserted: words.length, searched: 100, found, falsePositives }; +`; + +// โ”€โ”€ Creative & Visual โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** Prompt: "Generate a Sierpinski triangle as ASCII art with depth 5" */ +const SIERPINSKI_CODE = ` +const depth = 5; +const rows = Math.pow(2, depth); // 32 rows + +const lines = []; +for (let y = 0; y < rows; y++) { + let line = ' '.repeat(rows - y - 1); // leading spaces for centering + for (let x = 0; x <= y; x++) { + // Pascal's triangle mod 2: (x & y) === x iff C(y,x) is odd + line += (x & y) === x ? '* ' : ' '; + } + lines.push(line.trimEnd()); +} + +return { art: lines.join('\\n'), rows: lines.length, depth }; +`; + +/** Prompt: "Create a text-based Mandelbrot set visualization (60ร—30 ASCII)" */ +const MANDELBROT_CODE = ` +const WIDTH = 60, HEIGHT = 30, MAX_ITER = 100; +const chars = ' .:-=+*#%@'; + +const lines = []; +for (let y = 0; y < HEIGHT; y++) { + let line = ''; + for (let x = 0; x < WIDTH; x++) { + const cx = (x / WIDTH) * 3.5 - 2.5; + const cy = (y / HEIGHT) * 2 - 1; + let zx = 0, zy = 0, iter = 0; + while (zx * zx + zy * zy < 4 && iter < MAX_ITER) { + const tmp = zx * zx - zy * zy + cx; + zy = 2 * zx * zy + cy; + zx = tmp; + iter++; + } + line += chars[Math.floor(iter / MAX_ITER * (chars.length - 1))]; + } + lines.push(line); +} + +return { art: lines.join('\\n'), width: WIDTH, height: HEIGHT }; +`; + +/** Prompt: "Generate a maze using recursive backtracking on a 21ร—21 grid" */ +const MAZE_CODE = ` +const SIZE = 21; +const WALL = '#', PATH = ' '; +const grid = Array.from({ length: SIZE }, () => new Array(SIZE).fill(WALL)); + +function carve(x, y) { + grid[y][x] = PATH; + const dirs = [[2,0],[0,2],[-2,0],[0,-2]]; + // Fisher-Yates shuffle + for (let i = dirs.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [dirs[i], dirs[j]] = [dirs[j], dirs[i]]; + } + for (const [dx, dy] of dirs) { + const nx = x + dx, ny = y + dy; + if (nx > 0 && nx < SIZE && ny > 0 && ny < SIZE && grid[ny][nx] === WALL) { + grid[y + dy / 2][x + dx / 2] = PATH; // carve wall between + carve(nx, ny); + } + } +} + +carve(1, 1); +grid[0][1] = PATH; // entrance +grid[SIZE - 1][SIZE - 2] = PATH; // exit + +const art = grid.map(r => r.join('')).join('\\n'); +return { art, size: SIZE, entrance: [0, 1], exit: [SIZE - 1, SIZE - 2] }; +`; + +// โ”€โ”€ Cryptography & Encoding โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** Prompt: "Implement a Caesar cipher, ROT13 'HELLO WORLD', then decrypt" */ +const ROT13_CODE = ` +function caesarShift(text, shift) { + return text.split('').map(ch => { + const code = ch.charCodeAt(0); + if (code >= 65 && code <= 90) { + return String.fromCharCode(((code - 65 + shift) % 26 + 26) % 26 + 65); + } + if (code >= 97 && code <= 122) { + return String.fromCharCode(((code - 97 + shift) % 26 + 26) % 26 + 97); + } + return ch; + }).join(''); +} + +const original = 'HELLO WORLD'; +const encrypted = caesarShift(original, 13); +const decrypted = caesarShift(encrypted, 13); +return { original, encrypted, decrypted, roundTrip: original === decrypted }; +`; + +/** Prompt: "Convert the first 20 Fibonacci numbers to different bases" */ +const FIBONACCI_BASES_CODE = ` +const fibs = [0, 1]; +for (let i = 2; i < 20; i++) fibs.push(fibs[i - 1] + fibs[i - 2]); + +const table = fibs.map((n, i) => ({ + index: i, + decimal: n, + binary: n.toString(2), + octal: n.toString(8), + hex: n.toString(16).toUpperCase(), +})); + +return { table, count: table.length }; +`; + +// โ”€โ”€ Simulations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** Prompt: "Simulate Conway's Game of Life on a 30ร—30 grid for 50 generations" */ +const GAME_OF_LIFE_CODE = ` +const ROWS = 30, COLS = 30, GENS = 50; +// Deterministic seed pattern (glider + block + blinker) for reproducibility +let grid = Array.from({ length: ROWS }, () => new Array(COLS).fill(0)); + +// Place a glider at (1,1) +grid[1][2] = 1; grid[2][3] = 1; grid[3][1] = 1; grid[3][2] = 1; grid[3][3] = 1; +// Place a block at (10,10) +grid[10][10] = 1; grid[10][11] = 1; grid[11][10] = 1; grid[11][11] = 1; +// Place a blinker at (20,15) +grid[20][15] = 1; grid[21][15] = 1; grid[22][15] = 1; +// Place an r-pentomino at (15,20) โ€” chaotic evolution +grid[15][21] = 1; grid[15][22] = 1; grid[16][20] = 1; grid[16][21] = 1; grid[17][21] = 1; + +function countNeighbors(g, r, c) { + let count = 0; + for (let dr = -1; dr <= 1; dr++) { + for (let dc = -1; dc <= 1; dc++) { + if (dr === 0 && dc === 0) continue; + const nr = r + dr, nc = c + dc; + if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS) count += g[nr][nc]; + } + } + return count; +} + +const popHistory = []; +for (let gen = 0; gen < GENS; gen++) { + let pop = 0; + for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) pop += grid[r][c]; + popHistory.push(pop); + + const next = Array.from({ length: ROWS }, () => new Array(COLS).fill(0)); + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + const n = countNeighbors(grid, r, c); + next[r][c] = grid[r][c] === 1 ? (n === 2 || n === 3 ? 1 : 0) : (n === 3 ? 1 : 0); + } + } + grid = next; +} +// Final population +let finalPop = 0; +for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) finalPop += grid[r][c]; + +return { populationPerGen: popHistory, finalPopulation: finalPop, generations: GENS, gridSize: [ROWS, COLS] }; +`; + +/** Prompt: "Simulate a particle system: 100 particles bouncing in a 100ร—100 box" */ +const PARTICLE_SYSTEM_CODE = ` +const N = 100, BOX = 100, STEPS = 1000; +const particles = Array.from({ length: N }, () => ({ + x: Math.random() * BOX, + y: Math.random() * BOX, + vx: (Math.random() - 0.5) * 4, + vy: (Math.random() - 0.5) * 4, +})); + +let totalBounces = 0; +for (let step = 0; step < STEPS; step++) { + for (const p of particles) { + p.x += p.vx; + p.y += p.vy; + if (p.x < 0) { p.x = -p.x; p.vx = -p.vx; totalBounces++; } + if (p.x > BOX) { p.x = 2 * BOX - p.x; p.vx = -p.vx; totalBounces++; } + if (p.y < 0) { p.y = -p.y; p.vy = -p.vy; totalBounces++; } + if (p.y > BOX) { p.y = 2 * BOX - p.y; p.vy = -p.vy; totalBounces++; } + } +} + +const allInBounds = particles.every(p => + p.x >= 0 && p.x <= BOX && p.y >= 0 && p.y <= BOX +); +return { + particles: N, timesteps: STEPS, boxSize: BOX, + totalBounces, allInBounds, + samplePositions: particles.slice(0, 5).map(p => ({ + x: Math.round(p.x * 100) / 100, + y: Math.round(p.y * 100) / 100, + })), +}; +`; + +/** Prompt: "Model a predator-prey ecosystem (Lotka-Volterra) with Euler's method" */ +const LOTKA_VOLTERRA_CODE = ` +// dx/dt = alpha*x - beta*x*y (prey growth minus predation) +// dy/dt = delta*x*y - gamma*y (predator growth minus death) +const alpha = 1.1, beta = 0.4, delta = 0.1, gamma = 0.4; +const dt = 0.01; +const STEPS = 1000; + +let x = 10; // initial prey +let y = 5; // initial predators +const prey = [x], pred = [y]; + +for (let i = 0; i < STEPS; i++) { + const dx = (alpha * x - beta * x * y) * dt; + const dy = (delta * x * y - gamma * y) * dt; + x = Math.max(0, x + dx); + y = Math.max(0, y + dy); + prey.push(Math.round(x * 1000) / 1000); + pred.push(Math.round(y * 1000) / 1000); +} + +// Compute min/max manually (avoid spread on 1001-element array) +let preyMin = Infinity, preyMax = -Infinity; +let predMin = Infinity, predMax = -Infinity; +for (const v of prey) { if (v < preyMin) preyMin = v; if (v > preyMax) preyMax = v; } +for (const v of pred) { if (v < predMin) predMin = v; if (v > predMax) predMax = v; } + +return { + model: 'Lotka-Volterra', + params: { alpha, beta, delta, gamma, dt }, + steps: STEPS, + finalState: { prey: prey[STEPS], predators: pred[STEPS] }, + preyRange: { min: preyMin, max: preyMax }, + predRange: { min: predMin, max: predMax }, + preySample: prey.filter((_, i) => i % 100 === 0), + predSample: pred.filter((_, i) => i % 100 === 0), +}; +`; + +// โ”€โ”€ Brain Teasers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** Prompt: "Solve the 8-queens problem and return all 92 unique solutions" */ +const EIGHT_QUEENS_CODE = ` +const N = 8; +const solutions = []; + +function solve(board, row) { + if (row === N) { solutions.push(board.slice()); return; } + for (let col = 0; col < N; col++) { + let safe = true; + for (let r = 0; r < row; r++) { + if (board[r] === col || + board[r] - r === col - row || + board[r] + r === col + row) { + safe = false; + break; + } + } + if (safe) { board[row] = col; solve(board, row + 1); } + } +} + +solve(new Array(N), 0); + +const firstBoard = solutions[0].map(col => + '.'.repeat(col) + 'Q' + '.'.repeat(N - col - 1) +).join('\\n'); + +return { + n: N, + totalSolutions: solutions.length, + firstSolution: solutions[0], + firstBoard, + lastSolution: solutions[solutions.length - 1], +}; +`; + +/** Prompt: "Generate all valid balanced parentheses for n=8 (Catalan Cโ‚ˆ = 1430)" */ +const BALANCED_PARENS_CODE = ` +const N = 8; +const combos = []; + +function generate(open, close, current) { + if (current.length === 2 * N) { combos.push(current); return; } + if (open < N) generate(open + 1, close, current + '('); + if (close < open) generate(open, close + 1, current + ')'); +} + +generate(0, 0, ''); +return { + n: N, + count: combos.length, + first5: combos.slice(0, 5), + last5: combos.slice(-5), +}; +`; + +/** Prompt: "Find all Pythagorean triples where aยฒ + bยฒ = cยฒ and c < 500" */ +const PYTHAGOREAN_TRIPLES_CODE = ` +const MAX_C = 500; +const triples = []; + +for (let a = 1; a < MAX_C; a++) { + for (let b = a; b < MAX_C; b++) { + const c2 = a * a + b * b; + if (c2 >= MAX_C * MAX_C) break; + const c = Math.round(Math.sqrt(c2)); + if (c * c === c2) triples.push([a, b, c]); + } +} + +const allValid = triples.every(([a, b, c]) => a * a + b * b === c * c); +return { + maxC: MAX_C, + count: triples.length, + allValid, + first10: triples.slice(0, 10), + last5: triples.slice(-5), + smallest: triples[0], + largest: triples[triples.length - 1], +}; +`; + +// โ”€โ”€ Test Suite โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('README Example Prompts', () => { + let server; + let messageId = 1; + + /** + * Call the execute_javascript MCP tool and parse the result. + * Fails the test with a descriptive message if the tool returns an error. + */ + async function executeAndParse(code) { + send(server, { + jsonrpc: '2.0', + id: messageId++, + method: 'tools/call', + params: { name: 'execute_javascript', arguments: { code } }, + }); + const response = await waitForResponse(server); + + // Fail fast with the server's error message if execution failed + if (response.result?.isError) { + expect.fail(`Tool returned error: ${response.result.content[0].text}`); + } + + return JSON.parse(response.result.content[0].text); + } + + beforeAll(async () => { + server = spawn('node', [SERVER_PATH], { + stdio: ['pipe', 'pipe', 'pipe'], + }); + server.stderr.on('data', (d) => { + process.stderr.write(`[mcp-server] ${d}`); + }); + + // MCP handshake + send(server, { + jsonrpc: '2.0', + id: messageId++, + method: 'initialize', + params: { + protocolVersion: PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: 'vitest-prompt-client', version: '1.0.0' }, + }, + }); + const init = await waitForResponse(server); + expect(init.result).toBeDefined(); + + send(server, { + jsonrpc: '2.0', + method: 'notifications/initialized', + }); + await new Promise((r) => setTimeout(r, 200)); + }); + + afterAll(() => { + if (server) server.kill(); + }); + + // โ”€โ”€ Mathematics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + describe('Mathematics', () => { + it('ฯ€ to 50 decimal places (Machin formula)', async () => { + const result = await executeAndParse(PI_50_DIGITS_CODE); + expect(result.digits).toBe(50); + // Verify first 15 known digits of ฯ€ + expect(result.pi).toMatch(/^3\.14159265358979/); + // Verify we got 50 digits after the decimal point + const afterDot = result.pi.split('.')[1]; + expect(afterDot.length).toBe(50); + }); + + it('Sieve of Eratosthenes โ€” primes below 10,000', async () => { + const result = await executeAndParse(SIEVE_CODE); + expect(result.count).toBe(1229); + expect(result.last10).toEqual([ + 9887, 9901, 9907, 9923, 9929, 9931, 9941, 9949, 9967, 9973, + ]); + }); + + it("Euler's number (e) to 100 digits", async () => { + const result = await executeAndParse(EULER_100_DIGITS_CODE); + expect(result.digits).toBe(100); + // First 15 known digits of e + expect(result.e).toMatch(/^2\.71828182845904/); + const afterDot = result.e.split('.')[1]; + expect(afterDot.length).toBe(100); + }); + + it('Monte Carlo estimation of ฯ€ (100K throws)', async () => { + const result = await executeAndParse(MONTE_CARLO_CODE); + expect(result.throws).toBe(100000); + // ฯ€ estimate should be within 0.1 of actual ฯ€ (reasonable for 100K throws) + expect(result.pi).toBeGreaterThan(3.0); + expect(result.pi).toBeLessThan(3.3); + expect(result.error).toBeLessThan(0.1); + }); + }); + + // โ”€โ”€ Algorithms & Data Structures โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + describe('Algorithms & Data Structures', () => { + it('Quicksort vs Mergesort (5,000 elements)', async () => { + const result = await executeAndParse(SORT_COMPARISON_CODE); + expect(result.size).toBe(5000); + expect(result.quicksortCorrect).toBe(true); + expect(result.mergesortCorrect).toBe(true); + expect(result.match).toBe(true); + // First element should be smallest + expect(result.first5[0]).toBeLessThanOrEqual(result.first5[1]); + // Last element should be largest + expect(result.last5[3]).toBeLessThanOrEqual(result.last5[4]); + }); + + it('Tower of Hanoi (15 disks)', async () => { + const result = await executeAndParse(TOWER_OF_HANOI_CODE); + expect(result.disks).toBe(15); + // 2^15 - 1 = 32,767 moves + expect(result.totalMoves).toBe(Math.pow(2, 15) - 1); + expect(result.firstMoves).toHaveLength(10); + // First move is always disk 1 (for odd n like 15: Aโ†’C) + expect(result.firstMoves[0].disk).toBe(1); + }); + + it('Longest Common Subsequence', async () => { + const result = await executeAndParse(LCS_CODE); + expect(result.s1).toBe('AGGTAB'); + expect(result.s2).toBe('GXTXAYB'); + expect(result.lcs).toBe('GTAB'); + expect(result.length).toBe(4); + }); + + it('Trie data structure (1000 words)', async () => { + const result = await executeAndParse(TRIE_CODE); + expect(result.inserted).toBe(1000); + expect(result.searched).toBe(100); + // All 100 searched words were inserted, so all should be found + expect(result.found).toBe(100); + // Random "zz..." words almost certainly won't exist + expect(result.falsePositives).toBeLessThan(5); + }); + }); + + // โ”€โ”€ Creative & Visual โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + describe('Creative & Visual', () => { + it('Sierpinski triangle (depth 5)', async () => { + const result = await executeAndParse(SIERPINSKI_CODE); + expect(result.depth).toBe(5); + expect(result.rows).toBe(32); // 2^5 + // Art should contain asterisks and spaces + expect(result.art).toContain('*'); + expect(result.art.split('\n')).toHaveLength(32); + }); + + it('Mandelbrot set (60ร—30 ASCII)', async () => { + const result = await executeAndParse(MANDELBROT_CODE); + expect(result.width).toBe(60); + expect(result.height).toBe(30); + const lines = result.art.split('\n'); + expect(lines).toHaveLength(30); + // Each line should be 60 characters wide + lines.forEach((line) => expect(line.length).toBe(60)); + }); + + it('Maze generation (21ร—21)', async () => { + const result = await executeAndParse(MAZE_CODE); + expect(result.size).toBe(21); + expect(result.entrance).toEqual([0, 1]); + expect(result.exit).toEqual([20, 19]); + const lines = result.art.split('\n'); + expect(lines).toHaveLength(21); + // Each line should be 21 characters + lines.forEach((line) => expect(line.length).toBe(21)); + // Should contain both walls and paths + expect(result.art).toContain('#'); + expect(result.art).toContain(' '); + }); + }); + + // โ”€โ”€ Cryptography & Encoding โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + describe('Cryptography & Encoding', () => { + it('Caesar cipher / ROT13', async () => { + const result = await executeAndParse(ROT13_CODE); + expect(result.original).toBe('HELLO WORLD'); + expect(result.encrypted).toBe('URYYB JBEYQ'); + expect(result.decrypted).toBe('HELLO WORLD'); + expect(result.roundTrip).toBe(true); + }); + + it('Fibonacci base conversion', async () => { + const result = await executeAndParse(FIBONACCI_BASES_CODE); + expect(result.count).toBe(20); + // Verify first few Fibonacci numbers + expect(result.table[0].decimal).toBe(0); + expect(result.table[1].decimal).toBe(1); + expect(result.table[2].decimal).toBe(1); + expect(result.table[6].decimal).toBe(8); + expect(result.table[6].binary).toBe('1000'); + expect(result.table[6].octal).toBe('10'); + expect(result.table[6].hex).toBe('8'); + // Verify 19th Fibonacci (Fโ‚โ‚‰ = 4181) + expect(result.table[19].decimal).toBe(4181); + expect(result.table[19].hex).toBe('1055'); + }); + }); + + // โ”€โ”€ Simulations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + describe('Simulations', () => { + it("Conway's Game of Life (30ร—30, 50 gens)", async () => { + const result = await executeAndParse(GAME_OF_LIFE_CODE); + expect(result.generations).toBe(50); + expect(result.gridSize).toEqual([30, 30]); + // Should have 50 population entries (one per generation) + expect(result.populationPerGen).toHaveLength(50); + // All population values should be non-negative integers + result.populationPerGen.forEach((pop) => { + expect(pop).toBeGreaterThanOrEqual(0); + expect(Number.isInteger(pop)).toBe(true); + }); + // Initial population: glider(5) + block(4) + blinker(3) + r-pentomino(5) = 17 + expect(result.populationPerGen[0]).toBe(17); + }); + + it('Particle system (100 particles, 1000 steps)', async () => { + const result = await executeAndParse(PARTICLE_SYSTEM_CODE); + expect(result.particles).toBe(100); + expect(result.timesteps).toBe(1000); + expect(result.boxSize).toBe(100); + expect(result.allInBounds).toBe(true); + expect(result.totalBounces).toBeGreaterThan(0); + expect(result.samplePositions).toHaveLength(5); + }); + + it('Lotka-Volterra predator-prey', async () => { + const result = await executeAndParse(LOTKA_VOLTERRA_CODE); + expect(result.model).toBe('Lotka-Volterra'); + expect(result.steps).toBe(1000); + // Both populations should remain positive + expect(result.finalState.prey).toBeGreaterThan(0); + expect(result.finalState.predators).toBeGreaterThan(0); + // Should show oscillatory behaviour โ€” range should be non-trivial + expect(result.preyRange.max).toBeGreaterThan(result.preyRange.min); + expect(result.predRange.max).toBeGreaterThan(result.predRange.min); + // Samples should have 11 entries (every 100 steps from 0..1000) + expect(result.preySample).toHaveLength(11); + }); + }); + + // โ”€โ”€ Brain Teasers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + describe('Brain Teasers', () => { + it('8-queens (all 92 solutions)', async () => { + const result = await executeAndParse(EIGHT_QUEENS_CODE); + expect(result.n).toBe(8); + // The 8-queens problem has exactly 92 distinct solutions + expect(result.totalSolutions).toBe(92); + // First solution should be a valid placement + expect(result.firstSolution).toHaveLength(8); + // Board visualization should have 8 lines with Q characters + expect(result.firstBoard.split('\n')).toHaveLength(8); + expect(result.firstBoard).toContain('Q'); + }); + + it('Balanced parentheses (n=8, Catalan Cโ‚ˆ=1430)', async () => { + const result = await executeAndParse(BALANCED_PARENS_CODE); + expect(result.n).toBe(8); + // Catalan number Cโ‚ˆ = 1430 + expect(result.count).toBe(1430); + // First combination should be all opens then all closes + expect(result.first5[0]).toBe('(((((((())))))))'); + // Last combination should be alternating + expect(result.last5[4]).toBe('()()()()()()()()'); + }); + + it('Pythagorean triples (c < 500)', async () => { + const result = await executeAndParse(PYTHAGOREAN_TRIPLES_CODE); + expect(result.maxC).toBe(500); + expect(result.allValid).toBe(true); + expect(result.count).toBeGreaterThan(0); + // The smallest triple is (3, 4, 5) + expect(result.smallest).toEqual([3, 4, 5]); + // All triples should have c < 500 + expect(result.largest[2]).toBeLessThan(500); + // Verify a well-known triple is present + expect(result.first10).toContainEqual([3, 4, 5]); + expect(result.first10).toContainEqual([5, 12, 13]); + }); + }); +}); diff --git a/src/js-host-api/examples/mcp-server/tests/timing.test.js b/src/js-host-api/examples/mcp-server/tests/timing.test.js new file mode 100644 index 0000000..458fab3 --- /dev/null +++ b/src/js-host-api/examples/mcp-server/tests/timing.test.js @@ -0,0 +1,273 @@ +// โ”€โ”€ Hyperlight JS MCP Server โ€” Timing Log Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// +// Validates that the MCP server writes correct timing data to the +// HYPERLIGHT_TIMING_LOG file when the environment variable is set. +// +// "Time is an illusion. Lunchtime doubly so." +// โ€” Ford Prefect, The Hitchhiker's Guide (1979โ€ฆ close enough to the 80s) +// +// These tests spawn the server with HYPERLIGHT_TIMING_LOG pointed at a +// temp file, run tool invocations, then inspect the JSON-lines output. +// +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { mkdtempSync, readFileSync, unlinkSync, rmdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SERVER_PATH = join(__dirname, '..', 'server.js'); +const PROTOCOL_VERSION = '2025-11-25'; + +// โ”€โ”€ NDJSON Utilities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function send(proc, message) { + proc.stdin.write(JSON.stringify(message) + '\n'); +} + +function waitForResponse(proc) { + return new Promise((resolve, reject) => { + let buffer = ''; + const onData = (chunk) => { + buffer += chunk.toString(); + const idx = buffer.indexOf('\n'); + if (idx === -1) return; + const line = buffer.slice(0, idx).replace(/\r$/, ''); + buffer = buffer.slice(idx + 1); + proc.stdout.off('data', onData); + if (line.length === 0) return; + try { + resolve(JSON.parse(line)); + } catch (_err) { + reject(new Error(`Invalid JSON from server: ${line}`)); + } + }; + proc.stdout.on('data', onData); + }); +} + +// โ”€โ”€ Expected timing record fields โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** Every timing JSON line must contain these keys. */ +const TIMING_FIELDS = ['initMs', 'setupMs', 'compileMs', 'executeMs', 'snapshotMs', 'totalMs']; + +// โ”€โ”€ Test Suite โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe('Timing Log (HYPERLIGHT_TIMING_LOG)', () => { + let server; + let messageId = 1; + let timingLogPath; + let tmpDir; + + async function callExecuteJavaScript(code) { + send(server, { + jsonrpc: '2.0', + id: messageId++, + method: 'tools/call', + params: { + name: 'execute_javascript', + arguments: { code }, + }, + }); + return waitForResponse(server); + } + + /** Read all timing records from the log file. */ + function readTimingRecords() { + try { + const content = readFileSync(timingLogPath, 'utf8').trim(); + if (!content) return []; + return content.split('\n').map((line) => JSON.parse(line)); + } catch { + return []; + } + } + + beforeAll(async () => { + // Create a temp directory and timing log file path + tmpDir = mkdtempSync(join(tmpdir(), 'hyperlight-timing-test-')); + timingLogPath = join(tmpDir, 'timing.jsonl'); + + // Start server with HYPERLIGHT_TIMING_LOG set + server = spawn('node', [SERVER_PATH], { + stdio: ['pipe', 'pipe', 'pipe'], + env: { + ...process.env, + HYPERLIGHT_TIMING_LOG: timingLogPath, + }, + }); + + server.stderr.on('data', (d) => { + process.stderr.write(`[timing-test] ${d}`); + }); + + // MCP handshake + send(server, { + jsonrpc: '2.0', + id: messageId++, + method: 'initialize', + params: { + protocolVersion: PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: 'vitest-timing-client', version: '1.0.0' }, + }, + }); + + const initResponse = await waitForResponse(server); + expect(initResponse.result).toBeDefined(); + + send(server, { + jsonrpc: '2.0', + method: 'notifications/initialized', + }); + + await new Promise((r) => setTimeout(r, 200)); + }); + + afterAll(() => { + if (server) { + server.kill(); + } + // Clean up temp files + try { + unlinkSync(timingLogPath); + } catch { + // may not exist if no tests wrote + } + try { + rmdirSync(tmpDir); + } catch { + // best effort + } + }); + + // โ”€โ”€ Timing record structure โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + it('should write a timing record on successful execution', async () => { + const response = await callExecuteJavaScript('return { answer: 6 * 7 };'); + // Verify the tool call itself succeeded + const parsed = JSON.parse(response.result.content[0].text); + expect(parsed.answer).toBe(42); + + // Check the timing log + const records = readTimingRecords(); + expect(records.length).toBeGreaterThanOrEqual(1); + + const record = records[records.length - 1]; + for (const field of TIMING_FIELDS) { + expect(record).toHaveProperty(field); + expect(typeof record[field]).toBe('number'); + } + }); + + it('should have all timing values as non-negative integers', async () => { + const records = readTimingRecords(); + expect(records.length).toBeGreaterThanOrEqual(1); + + const record = records[records.length - 1]; + for (const field of TIMING_FIELDS) { + expect(record[field]).toBeGreaterThanOrEqual(0); + expect(Number.isInteger(record[field])).toBe(true); + } + }); + + it('should have totalMs >= sum of sub-phase times', async () => { + const records = readTimingRecords(); + const record = records[records.length - 1]; + + // totalMs should be at least the sum of the individual phases + // (with some tolerance for rounding) + const sumOfParts = + record.initMs + + record.setupMs + + record.compileMs + + record.snapshotMs + + record.executeMs; + + expect(record.totalMs).toBeGreaterThanOrEqual(sumOfParts - 2); + }); + + it('should include initMs > 0 on the first call (sandbox cold start)', async () => { + // The first record should have a non-zero initMs because the + // sandbox was created from scratch + const records = readTimingRecords(); + expect(records.length).toBeGreaterThanOrEqual(1); + expect(records[0].initMs).toBeGreaterThan(0); + }); + + it('should have initMs === 0 on subsequent calls (sandbox reuse)', async () => { + // Execute a second call โ€” sandbox should already be warm + await callExecuteJavaScript('return { warm: true };'); + + const records = readTimingRecords(); + expect(records.length).toBeGreaterThanOrEqual(2); + + // The latest record should have initMs === 0 (no re-init) + const latest = records[records.length - 1]; + expect(latest.initMs).toBe(0); + }); + + it('should write a timing record even on timeout errors', async () => { + const recordsBefore = readTimingRecords(); + const countBefore = recordsBefore.length; + + // Trigger a timeout + const response = await callExecuteJavaScript('while (true) {}'); + expect(response.result.isError).toBe(true); + + const recordsAfter = readTimingRecords(); + expect(recordsAfter.length).toBe(countBefore + 1); + + // The timeout record should still have valid structure + const timeoutRecord = recordsAfter[recordsAfter.length - 1]; + for (const field of TIMING_FIELDS) { + expect(timeoutRecord).toHaveProperty(field); + expect(typeof timeoutRecord[field]).toBe('number'); + } + + // executeMs should be substantial (at least ~1000ms for CPU timeout) + expect(timeoutRecord.executeMs).toBeGreaterThanOrEqual(500); + }); + + it('should write a new record per invocation (JSON-lines format)', async () => { + const recordsBefore = readTimingRecords(); + const countBefore = recordsBefore.length; + + // Run two more calls + await callExecuteJavaScript('return 1;'); + await callExecuteJavaScript('return 2;'); + + const recordsAfter = readTimingRecords(); + expect(recordsAfter.length).toBe(countBefore + 2); + }); + + it('should measure non-trivial executeMs for computation-heavy code', async () => { + // Sieve of Eratosthenes โ€” enough work to register measurable time + const code = ` + const limit = 100000; + const sieve = new Array(limit).fill(true); + sieve[0] = sieve[1] = false; + for (let i = 2; i * i < limit; i++) { + if (sieve[i]) { + for (let j = i * i; j < limit; j += i) sieve[j] = false; + } + } + let count = 0; + for (let i = 0; i < limit; i++) if (sieve[i]) count++; + return { primeCount: count }; + `; + + await callExecuteJavaScript(code); + + const records = readTimingRecords(); + const latest = records[records.length - 1]; + + // executeMs should be measurable (> 0) for real computation + expect(latest.executeMs).toBeGreaterThanOrEqual(0); + // totalMs should always be >= executeMs + expect(latest.totalMs).toBeGreaterThanOrEqual(latest.executeMs); + }); +}); diff --git a/src/js-host-api/examples/mcp-server/vitest.config.js b/src/js-host-api/examples/mcp-server/vitest.config.js new file mode 100644 index 0000000..b951099 --- /dev/null +++ b/src/js-host-api/examples/mcp-server/vitest.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + // Test files pattern + include: ['tests/**/*.test.js'], + // Generous timeout โ€” sandbox builds can be slow, and timeout tests + // need time to actually time out + testTimeout: 30000, + // Hook timeout โ€” beforeAll spawns the server and initializes the + // sandbox which involves compiling the QuickJS runtime + hookTimeout: 60000, + }, +});