#
tokens: 48284/50000 82/90 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/alfonsograziano/node-code-sandbox-mcp?page={x} to view the full context.

# Directory Structure

```
├── .commitlintrc
├── .env.sample
├── .github
│   └── workflows
│       ├── docker.yaml
│       ├── publish-node-chartjs-canvas.yaml
│       ├── publish-on-npm.yaml
│       └── test.yaml
├── .gitignore
├── .husky
│   ├── commit-msg
│   └── pre-commit
├── .lintstagedrc
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc.js
├── .vscode
│   ├── extensions.json
│   ├── mcp.json
│   └── settings.json
├── assets
│   └── images
│       └── website_homepage.png
├── Dockerfile
├── eslint.config.js
├── evals
│   ├── auditClient.ts
│   ├── basicEvals.json
│   ├── evals.json
│   └── index.ts
├── examples
│   ├── docker.js
│   ├── ephemeral.js
│   ├── ephemeralWithDependencies.js
│   ├── ephemeralWithFiles.js
│   ├── playwright.js
│   └── simpleSandbox.js
├── images
│   └── node-chartjs-canvas
│       └── Dockerfile
├── NODE_GUIDELINES.md
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── config.ts
│   ├── containerUtils.ts
│   ├── dockerUtils.ts
│   ├── linterUtils.ts
│   ├── logger.ts
│   ├── runUtils.ts
│   ├── server.ts
│   ├── snapshotUtils.ts
│   ├── tools
│   │   ├── exec.ts
│   │   ├── getDependencyTypes.ts
│   │   ├── initialize.ts
│   │   ├── runJs.ts
│   │   ├── runJsEphemeral.ts
│   │   ├── searchNpmPackages.ts
│   │   └── stop.ts
│   ├── types.ts
│   └── utils.ts
├── test
│   ├── execInSandbox.test.ts
│   ├── getDependencyTypes.test.ts
│   ├── initialize.test.ts
│   ├── initializeSandbox.test.ts
│   ├── runJs-cache.test.ts
│   ├── runJs.test.ts
│   ├── runJsEphemeral.test.ts
│   ├── runJsListenOnPort.test.ts
│   ├── runMCPClient.test.ts
│   ├── sandbox.test.ts
│   ├── searchNpmPackages.test.ts
│   ├── snapshotUtils.test.ts
│   ├── stopSandbox.test.ts
│   ├── unit
│   │   └── linterUtils.test.ts
│   ├── utils.test.ts
│   └── utils.ts
├── tsconfig.build.json
├── tsconfig.json
├── USE_CASE.md
├── vitest.config.ts
└── website
    ├── .gitignore
    ├── index.html
    ├── LICENSE.md
    ├── package-lock.json
    ├── package.json
    ├── postcss.config.js
    ├── public
    │   └── images
    │       ├── client.png
    │       ├── graph-gpt_markdown.png
    │       ├── graph-gpt_reference_section.png
    │       ├── graph-gpt.png
    │       ├── js_ai.jpeg
    │       └── simple_agent.jpeg
    ├── src
    │   ├── App.tsx
    │   ├── Components
    │   │   ├── Footer.tsx
    │   │   ├── GettingStarted.tsx
    │   │   └── Header.tsx
    │   ├── index.css
    │   ├── main.tsx
    │   ├── pages
    │   │   ├── GraphGPT.tsx
    │   │   ├── Home.tsx
    │   │   ├── NodeMCPServer.tsx
    │   │   └── TinyAgent.tsx
    │   ├── polyfills.ts
    │   ├── useCases.ts
    │   └── vite-env.d.ts
    ├── tailwind.config.js
    ├── tsconfig.json
    ├── tsconfig.node.json
    ├── vite-env.d.ts
    └── vite.config.ts
```

# Files

--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------

```
23

```

--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------

```
engine-strict=true

```

--------------------------------------------------------------------------------
/.commitlintrc:
--------------------------------------------------------------------------------

```
{
  "extends": [
    "@commitlint/config-conventional"
  ]
}

```

--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------

```
node_modules
dist
*.log
coverage
.env
package-lock.json
yarn.lock
pnpm-lock.yaml 
```

--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------

```
{
  "**/*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
  "**/*.{json,md,yml,yaml}": ["prettier --write"]
}

```

--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------

```
OPENAI_API_KEY=ADD_HERE_YOUR_OPENAI_API_KEY
OPENAI_MODEL=ENTER_HERE_THE_CHOSEN_MODEL
FILES_DIR=/Users/youruser/Desktop
SANDBOX_MEMORY_LIMIT=
SANDBOX_CPU_LIMIT=
```

--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------

```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

.vercel

```

--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------

```javascript
export default {
  semi: true, // Use semicolons
  trailingComma: 'es5', // Add trailing commas where valid in ES5 (objects, arrays, etc.)
  singleQuote: true, // Use single quotes instead of double quotes
  printWidth: 80, // Specify the line length that the printer will wrap on
  tabWidth: 2, // Specify the number of spaces per indentation-level
  useTabs: false, // Indent lines with spaces instead of tabs
  endOfLine: 'lf', // Use Linux line endings
};

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# vitepress build output
**/.vitepress/dist

# vitepress cache directory
**/.vitepress/cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# General
.DS_Store

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# 🐢🚀 Node.js Sandbox MCP Server

Node.js server implementing the Model Context Protocol (MCP) for running arbitrary JavaScript in ephemeral Docker containers with on‑the‑fly npm dependency installation.

![Website Preview](https://raw.githubusercontent.com/alfonsograziano/node-code-sandbox-mcp/master/assets/images/website_homepage.png)

👉 [Look at the official website](https://jsdevai.com/)

📦 [Available on Docker Hub](https://hub.docker.com/r/mcp/node-code-sandbox)

## Features

- Start and manage isolated Node.js sandbox containers
- Execute arbitrary shell commands inside containers
- Install specified npm dependencies per job
- Run ES module JavaScript snippets and capture stdout
- Tear down containers cleanly
- **Detached Mode:** Keep the container alive after script execution (e.g. for long-running servers)

> Note: Containers run with controlled CPU/memory limits.

## Explore Cool Use Cases

If you want ideas for cool and powerful ways to use this library, check out the [use cases section on the website](https://jsdevai.com/#use-cases)
It contains a curated list of prompts, examples, and creative experiments you can try with the Node.js Sandbox MCP Server.

## ⚠️ Prerequisites

To use this MCP server, Docker must be installed and running on your machine.

**Tip:** Pre-pull any Docker images you'll need to avoid delays during first execution.

Example recommended images:

- node:lts-slim
- mcr.microsoft.com/playwright:v1.55.0-noble
- alfonsograziano/node-chartjs-canvas:latest

## Getting started

In order to get started with this MCP server, first of all you need to connect it to a client (for example Claude Desktop).

Once it's running, you can test that it's fully working with a couple of test prompts:

- Validate that the tool can run:

  ```markdown
  Create and run a JS script with a console.log("Hello World")
  ```

  This should run a console.log and in the tool response you should be able to see Hello World.

- Validate that you can install dependencies and save files

  ```markdown
  Create and run a JS script that generates a QR code for the URL `https://nodejs.org/en`, and save it as `qrcode.png` **Tip:** Use the `qrcode` package.
  ```

  This should create a file in your mounted directory (for example the Desktop) called "qrcode.png"

### Usage with Claude Desktop

Add this to your `claude_desktop_config.json`:
You can follow the [Official Guide](https://modelcontextprotocol.io/quickstart/user) to install this MCP server

```json
{
  "mcpServers": {
    "js-sandbox": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-v",
        "/var/run/docker.sock:/var/run/docker.sock",
        "-v",
        "$HOME/Desktop/sandbox-output:/root",
        "-e",
        "FILES_DIR=$HOME/Desktop/sandbox-output",
        "-e",
        "SANDBOX_MEMORY_LIMIT=512m", // optional
        "-e",
        "SANDBOX_CPU_LIMIT=0.75", // optional
        "mcp/node-code-sandbox"
      ]
    }
  }
}
```

or with NPX:

```json
{
  "mcpServers": {
    "node-code-sandbox-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "node-code-sandbox-mcp"],
      "env": {
        "FILES_DIR": "/Users/alfonsograziano/Desktop/node-sandbox",
        "SANDBOX_MEMORY_LIMIT": "512m", // optional
        "SANDBOX_CPU_LIMIT": "0.75" // optional
      }
    }
  }
}
```

> Note: Ensure your working directory points to the built server, and Docker is installed/running.

### Docker

Run the server in a container (mount Docker socket if needed), and pass through your desired host output directory as an env var:

```shell
# Build locally if necessary
# docker build -t mcp/node-code-sandbox .

docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v "$HOME/Desktop/sandbox-output":"/root" \
  -e FILES_DIR="$HOME/Desktop/sandbox-output" \
  -e SANDBOX_MEMORY_LIMIT="512m" \
  -e SANDBOX_CPU_LIMIT="0.5" \
  mcp/node-code-sandbox stdio
```

This bind-mounts your host folder into the container at the **same absolute path** and makes `FILES_DIR` available inside the MCP server.

#### Ephemeral usage – **no persistent storage**

```bash
docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  alfonsograziano/node-code-sandbox-mcp stdio
```

### Usage with VS Code

**Quick install** buttons (VS Code & Insiders):

Install js-sandbox-mcp (NPX) Install js-sandbox-mcp (Docker)

**Manual configuration**: Add to your VS Code `settings.json` or `.vscode/mcp.json`:

```json
"mcp": {
    "servers": {
        "js-sandbox": {
            "command": "docker",
            "args": [
                "run",
                "-i",
                "--rm",
                "-v", "/var/run/docker.sock:/var/run/docker.sock",
                "-v", "$HOME/Desktop/sandbox-output:/root", // optional
                "-e", "FILES_DIR=$HOME/Desktop/sandbox-output",  // optional
                "-e", "SANDBOX_MEMORY_LIMIT=512m",
                "-e", "SANDBOX_CPU_LIMIT=1",
                "mcp/node-code-sandbox"
              ]
        }
    }
}
```

## API

## Tools

### run_js_ephemeral

Run a one-off JS script in a brand-new disposable container.

**Inputs:**

- `image` (string, optional): Docker image to use (default: `node:lts-slim`).
- `code` (string, required): JavaScript source to execute.
- `dependencies` (array of `{ name, version }`, optional): NPM packages and versions to install (default: `[]`).

**Behavior:**

1. Creates a fresh container.
2. Writes your `index.js` and a minimal `package.json`.
3. Installs the specified dependencies.
4. Executes the script.
5. Tears down (removes) the container.
6. Returns the captured stdout.
7. If your code saves any files in the current directory, these files will be returned automatically.
   - Images (e.g., PNG, JPEG) are returned as `image` content.
   - Other files (e.g., `.txt`, `.json`) are returned as `resource` content.
   - Note: the file saving feature is currently available only in the ephemeral tool.

> **Tip:** To get files back, simply save them during your script execution.

**Example Call:**

```jsonc
{
  "name": "run_js_ephemeral",
  "arguments": {
    "image": "node:lts-slim",
    "code": "console.log('One-shot run!');",
    "dependencies": [{ "name": "lodash", "version": "^4.17.21" }],
  },
}
```

**Example to save a file:**

```javascript
import fs from 'fs/promises';

await fs.writeFile('hello.txt', 'Hello world!');
console.log('Saved hello.txt');
```

This will return the console output **and** the `hello.txt` file.

### sandbox_initialize

Start a fresh sandbox container.

- **Input**:
  - `image` (_string_, optional, default: `node:lts-slim`): Docker image for the sandbox
  - `port` (_number_, optional): If set, maps this container port to the host
- **Output**: Container ID string

### sandbox_exec

Run shell commands inside the running sandbox.

- **Input**:
  - `container_id` (_string_): ID from `sandbox_initialize`
  - `commands` (_string[]_): Array of shell commands to execute
- **Output**: Combined stdout of each command

### run_js

Install npm dependencies and execute JavaScript code.

- **Input**:
  - `container_id` (_string_): ID from `sandbox_initialize`
  - `code` (_string_): JS source to run (ES modules supported)
  - `dependencies` (_array of `{ name, version }`_, optional, default: `[]`): npm package names → semver versions
  - `listenOnPort` (_number_, optional): If set, leaves the process running and exposes this port to the host (**Detached Mode**)

- **Behavior:**
  1. Creates a temp workspace inside the container
  2. Writes `index.js` and a minimal `package.json`
  3. Runs `npm install --omit=dev --ignore-scripts --no-audit --loglevel=error`
  4. Executes `node index.js` and captures stdout, or leaves process running in background if `listenOnPort` is set
  5. Cleans up workspace unless running in detached mode

- **Output**: Script stdout or background execution notice

### sandbox_stop

Terminate and remove the sandbox container.

- **Input**:
  - `container_id` (_string_): ID from `sandbox_initialize`
- **Output**: Confirmation message

### search_npm_packages

Search for npm packages by a search term and get their name, description, and a README snippet.

- **Input**:
  - `searchTerm` (_string_, required): The term to search for in npm packages. Should contain all relevant context. Use plus signs (+) to combine related terms (e.g., "react+components" for React component libraries).
  - `qualifiers` (_object_, optional): Optional qualifiers to filter the search results:
    - `author` (_string_, optional): Filter by package author name
    - `maintainer` (_string_, optional): Filter by package maintainer name
    - `scope` (_string_, optional): Filter by npm scope (e.g., "@vue" for Vue.js packages)
    - `keywords` (_string_, optional): Filter by package keywords
    - `not` (_string_, optional): Exclude packages matching this criteria (e.g., "insecure")
    - `is` (_string_, optional): Include only packages matching this criteria (e.g., "unstable")
    - `boostExact` (_string_, optional): Boost exact matches for this term in search results

- **Behavior:**
  1. Searches the npm registry using the provided search term and qualifiers
  2. Returns up to 5 packages sorted by popularity
  3. For each package, provides name, description, and README snippet (first 500 characters)

- **Output**: JSON array containing package details with name, description, and README snippet

## Usage Tips

- **Session-based tools** (`sandbox_initialize` ➔ `run_js` ➔ `sandbox_stop`) are ideal when you want to:
  - Keep a long-lived sandbox container open.
  - Run multiple commands or scripts in the same environment.
  - Incrementally install and reuse dependencies.
- **One-shot execution** with `run_js_ephemeral` is perfect for:
  - Quick experiments or simple scripts.
  - Cases where you don't need to maintain state or cache dependencies.
  - Clean, atomic runs without worrying about manual teardown.
- **Detached mode** is useful when you want to:
  - Spin up servers or long-lived services on-the-fly
  - Expose and test endpoints from running containers

Choose the workflow that best fits your use-case!

## Build

Compile and bundle:

```shell
npm install
npm run build
```

## License

MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

```

--------------------------------------------------------------------------------
/website/LICENSE.md:
--------------------------------------------------------------------------------

```markdown
MIT License

Copyright (c) 2022 joshcs.eth | jcs.sol

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

```

--------------------------------------------------------------------------------
/website/src/vite-env.d.ts:
--------------------------------------------------------------------------------

```typescript
/// <reference types="vite/client" />

```

--------------------------------------------------------------------------------
/website/vite-env.d.ts:
--------------------------------------------------------------------------------

```typescript
/// <reference types="vite/client" />

```

--------------------------------------------------------------------------------
/website/src/index.css:
--------------------------------------------------------------------------------

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

```

--------------------------------------------------------------------------------
/website/postcss.config.js:
--------------------------------------------------------------------------------

```javascript
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

--------------------------------------------------------------------------------
/website/src/polyfills.ts:
--------------------------------------------------------------------------------

```typescript
import { Buffer } from "buffer";

window.global = window.global ?? window;
window.Buffer = window.Buffer ?? Buffer;

export {};

```

--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------

```json
{
  "recommendations": [
    "esbenp.prettier-vscode",
    "dbaeumer.vscode-eslint",
    "ms-azuretools.vscode-containers"
  ]
}

```

--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------

```json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "rootDir": "src"
  },
  "exclude": ["test/**/*.ts", "evals/**/*.ts"]
}

```

--------------------------------------------------------------------------------
/evals/basicEvals.json:
--------------------------------------------------------------------------------

```json
[
  {
    "id": "hello-world",
    "prompt": "Create and run a simple Node.js script that prints \"Hello, World!\" to the console."
  }
]

```

--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------

```typescript
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['**/*.{test,spec}.{js,ts}'],
  },
});

```

--------------------------------------------------------------------------------
/website/tsconfig.node.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "composite": true,
    "module": "esnext",
    "moduleResolution": "node"
  },
  "include": ["vite.config.ts"]
}

```

--------------------------------------------------------------------------------
/website/vite.config.ts:
--------------------------------------------------------------------------------

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()]
})

```

--------------------------------------------------------------------------------
/website/tailwind.config.js:
--------------------------------------------------------------------------------

```javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

```

--------------------------------------------------------------------------------
/website/src/main.tsx:
--------------------------------------------------------------------------------

```typescript
import './polyfills';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```

--------------------------------------------------------------------------------
/website/index.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Node.js MCP Server</title>
   
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

--------------------------------------------------------------------------------
/.vscode/mcp.json:
--------------------------------------------------------------------------------

```json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "id_files_dir_node_code_sandbox_mcp",
      "description": "Files directory for the Node Code Sandbox MCP",
      "password": false
    }
  ],
  "servers": {
    "node-code-sandbox-mcp (dev)": {
      "type": "stdio",
      "command": "node",
      "args": ["${workspaceFolder}/src/server.ts"],
      "env": {
        "FILES_DIR": "${input:id_files_dir_node_code_sandbox_mcp}"
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/website/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

```

--------------------------------------------------------------------------------
/.github/workflows/publish-on-npm.yaml:
--------------------------------------------------------------------------------

```yaml
name: Publish Package

on:
  push:
    # only fire on tag pushes matching semver-style vMAJOR.MINOR.PATCH
    tags:
      - 'v*.*.*'

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '23'
          registry-url: 'https://registry.npmjs.org'

      - name: Install dependencies
        run: npm ci

      - name: Publish to npm
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

```

--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------

```json
{
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[jsonc]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  },
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnPaste": true,
  "editor.formatOnSave": true,
  "typescript.tsdk": "node_modules/typescript/lib"
}

```

--------------------------------------------------------------------------------
/website/src/App.tsx:
--------------------------------------------------------------------------------

```typescript
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import NodeMCPServer from './pages/NodeMCPServer';
import TinyAgent from './pages/TinyAgent';
import GraphGPT from './pages/GraphGPT';

const App: React.FC = () => {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/mcp" element={<NodeMCPServer />} />
        <Route path="/tiny-agent" element={<TinyAgent />} />
        <Route path="/graph-gpt" element={<GraphGPT />} />
      </Routes>
    </Router>
  );
};

export default App;

```

--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "my-project",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@types/react-router-dom": "^5.3.3",
    "buffer": "^6.0.3",
    "clsx": "^2.1.1",
    "ethers": "^5.6.9",
    "lucide-react": "^0.509.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "react-icons": "^4.4.0",
    "react-router-dom": "^7.8.1"
  },
  "devDependencies": {
    "@types/react": "^18.0.0",
    "@types/react-dom": "^18.0.0",
    "@vitejs/plugin-react": "^1.3.0",
    "autoprefixer": "^10.4.7",
    "postcss": "^8.4.14",
    "tailwindcss": "^3.1.4",
    "typescript": "^4.6.3",
    "vite": "^3.0.0"
  }
}

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "allowImportingTsExtensions": true,
    "baseUrl": ".",
    "erasableSyntaxOnly": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "newLine": "lf",
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "outDir": "dist",
    "removeComments": true,
    "resolveJsonModule": true,
    "rewriteRelativeImportExtensions": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "ESNext",
    "verbatimModuleSyntax": true
  },
  "include": ["src/**/*.ts", "test/**/*.ts", "evals/**/*.ts"],
  "exclude": ["node_modules"]
}

```

--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------

```yaml
name: Run Docker-Based Tests

on:
  pull_request:
    branches:
      - master
  workflow_dispatch: {}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js 23
        uses: actions/setup-node@v4
        with:
          node-version: '23'

      - name: Install dependencies
        run: npm ci
      - name: Pull required Docker images
        run: |
          docker pull --platform=linux/amd64 node:lts-slim
          docker pull --platform=linux/amd64 mcr.microsoft.com/playwright:v1.53.2-noble
          docker pull --platform=linux/amd64 alfonsograziano/node-code-sandbox-mcp:latest

      - name: Run tests
        run: npm test

```

--------------------------------------------------------------------------------
/.github/workflows/docker.yaml:
--------------------------------------------------------------------------------

```yaml
name: Publish Multi-Arch Image
on:
  push:
    # only fire on tag pushes matching vMAJOR.MINOR.PATCH
    tags:
      - 'v*.*.*'
  workflow_dispatch: {}
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up QEMU for cross-build
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build & push multi-arch image
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64

```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
export type McpContentText = {
  type: 'text';
  text: string;
};

export type McpContentImage = {
  type: 'image';
  data: string;
  mimeType: string;
};

export type McpContentAudio = {
  type: 'audio';
  data: string;
  mimeType: string;
};

export type McpContentTextResource = {
  type: 'resource';
  resource: {
    text: string;
    uri: string;
    mimeType?: string;
  };
};

export type McpContentResource = {
  type: 'resource';
  resource:
    | {
        text: string;
        uri: string;
        mimeType?: string;
      }
    | {
        uri: string;
        blob: string;
        mimeType?: string;
      };
};

export type McpContent =
  | McpContentText
  | McpContentImage
  | McpContentAudio
  | McpContentResource;

export type McpResponse = {
  content: McpContent[];
  _meta?: Record<string, unknown>;
  isError?: boolean;
};

export const textContent = (text: string): McpContent => ({
  type: 'text',
  text,
});

```

--------------------------------------------------------------------------------
/.github/workflows/publish-node-chartjs-canvas.yaml:
--------------------------------------------------------------------------------

```yaml
name: Publish Node-Chartjs-Canvas Multi-Arch Image
on:
  push:
    # only fire on tag pushes matching vMAJOR.MINOR.PATCH
    tags:
      - 'v*.*.*'
    paths:
      # Trigger only when Dockerfile changes
      - 'images/node-chartjs-canvas/Dockerfile'
  workflow_dispatch: {}
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up QEMU for cross-build
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build & push multi-arch image
        uses: docker/build-push-action@v6
        with:
          context: ./images/node-chartjs-canvas
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ${{ secrets.DOCKER_USERNAME }}/node-chartjs-canvas:latest

```

--------------------------------------------------------------------------------
/examples/ephemeral.js:
--------------------------------------------------------------------------------

```javascript
import path from 'path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

async function main() {
  // 1️⃣ Create the MCP client
  const client = new Client({
    name: 'ephemeral-example',
    version: '1.0.0',
  });

  // 2️⃣ Connect to your js-sandbox-mcp server
  await client.connect(
    new StdioClientTransport({
      command: 'npm',
      args: ['run', 'dev'],
      cwd: path.resolve('..'),
      env: { ...process.env },
    })
  );

  console.log('✅ Connected to js-sandbox-mcp');

  // 3️⃣ Use the new run_js_ephemeral tool in one step
  const result = await client.callTool({
    name: 'run_js_ephemeral',
    arguments: {
      image: 'node:lts-slim',
      code: `
        import { randomUUID } from 'node:crypto';
        console.log('Ephemeral run! Your UUID is', randomUUID());
      `,
      dependencies: [],
    },
  });

  console.log('▶️ run_js_ephemeral output:\n', result.content[0].text);

  process.exit(0);
}

main().catch((err) => {
  console.error('❌ Error in ephemeral example:', err);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/test/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { execFileSync } from 'node:child_process';
import { describe, it } from 'vitest';
import path from 'path';
/**
 * Utility to check if a Docker container is running
 */
export function isContainerRunning(containerId: string): boolean {
  try {
    const output = execFileSync(
      'docker',
      ['inspect', '-f', '{{.State.Running}}', containerId],
      { encoding: 'utf8' }
    ).trim();
    return output === 'true';
  } catch {
    return false;
  }
}

/**
 * Utility to check if a Docker container exists
 */
export function containerExists(containerId: string): boolean {
  try {
    execFileSync('docker', ['inspect', containerId]);
    return true;
  } catch {
    return false;
  }
}

export const describeIfLocal = process.env.CI ? describe.skip : describe;
export const testIfLocal = process.env.CI ? it.skip : it;

export function normalizeMountPath(hostPath: string) {
  if (process.platform === 'win32') {
    // e.g. C:\Users\alfon\Temp\ws-abc  →  /c/Users/alfon/Temp/ws-abc
    const drive = hostPath[0].toLowerCase();
    const rest = hostPath.slice(2).split(path.sep).join('/');
    return `/${drive}/${rest}`;
  }
  return hostPath;
}

```

--------------------------------------------------------------------------------
/src/tools/exec.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { execFileSync } from 'node:child_process';
import { type McpResponse, textContent } from '../types.ts';
import {
  DOCKER_NOT_RUNNING_ERROR,
  isDockerRunning,
  sanitizeContainerId,
  sanitizeShellCommand,
} from '../utils.ts';

export const argSchema = {
  container_id: z.string(),
  commands: z.array(z.string().min(1)),
};

export default async function execInSandbox({
  container_id,
  commands,
}: {
  container_id: string;
  commands: string[];
}): Promise<McpResponse> {
  const validId = sanitizeContainerId(container_id);
  if (!validId) {
    return {
      content: [textContent('Invalid container ID')],
    };
  }

  if (!isDockerRunning()) {
    return {
      content: [textContent(DOCKER_NOT_RUNNING_ERROR)],
    };
  }

  const output: string[] = [];
  for (const cmd of commands) {
    const sanitizedCmd = sanitizeShellCommand(cmd);
    if (!sanitizedCmd)
      throw new Error(
        'Cannot run command as it contains dangerous metacharacters'
      );
    output.push(
      execFileSync('docker', ['exec', validId, '/bin/sh', '-c', sanitizedCmd], {
        encoding: 'utf8',
      })
    );
  }
  return { content: [textContent(output.join('\n'))] };
}

```

--------------------------------------------------------------------------------
/images/node-chartjs-canvas/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Build dependencies
FROM node:lts AS builder

RUN apt-get update && \
    apt-get install -y --no-install-recommends debian-archive-keyring && \
    apt-get update && \
    apt-get install -y --no-install-recommends \
    build-essential python3 pkg-config \
    libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /build
RUN npm install --omit=dev --prefer-offline --no-audit [email protected] @mermaid-js/mermaid-cli

# Chromium stage
FROM node:lts-slim

# Runtime dependencies only
RUN apt-get update && \
    apt-get install -y --no-install-recommends debian-archive-keyring && \
    apt-get update && \
    apt-get install -y --no-install-recommends \
    libcairo2 libpango1.0-0 libjpeg62-turbo libgif7 librsvg2-2 \
    chromium && \
    rm -rf /var/lib/apt/lists/*

RUN groupadd -r chromium && \
    useradd -r -g chromium -G audio,video chromium && \
    mkdir -p /home/chromium /workspace && \
    chown -R chromium:chromium /home/chromium /workspace

WORKDIR /workspace
COPY --from=builder --chown=chromium:chromium /build/node_modules ./node_modules

ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
    HOME=/home/chromium

USER chromium

RUN ./node_modules/.bin/mmdc -h

```

--------------------------------------------------------------------------------
/examples/docker.js:
--------------------------------------------------------------------------------

```javascript
import path from 'path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

async function main() {
  // 1️⃣ Create the MCP client
  const client = new Client({
    name: 'ephemeral-example',
    version: '1.0.0',
  });

  // 2️⃣ Connect to your js-sandbox-mcp server
  await client.connect(
    new StdioClientTransport({
      command: 'docker',
      args: [
        'run',
        '-i',
        '--rm',
        '-v',
        '/var/run/docker.sock:/var/run/docker.sock',
        'alfonsograziano/node-code-sandbox-mcp',
      ],
      env: {
        PATH: process.env.PATH,
      },
    })
  );

  console.log('✅ Connected to js-sandbox-mcp');

  // 3️⃣ Use the new run_js_ephemeral tool in one step
  const result = await client.callTool({
    name: 'run_js_ephemeral',
    arguments: {
      image: 'node:lts-slim',
      code: `
        import { randomUUID } from 'node:crypto';
        console.log('Ephemeral run! Your UUID is', randomUUID());
      `,
      dependencies: [],
    },
  });

  console.log('▶️ run_js_ephemeral output:\n', result.content[0].text);

  process.exit(0);
}

main().catch((err) => {
  console.error('❌ Error in ephemeral example:', err);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/examples/ephemeralWithDependencies.js:
--------------------------------------------------------------------------------

```javascript
import path from 'path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

async function main() {
  // 1️⃣ Create the MCP client
  const client = new Client({
    name: 'ephemeral-with-deps-example',
    version: '1.0.0',
  });

  // 2️⃣ Connect to your js-sandbox-mcp server
  await client.connect(
    new StdioClientTransport({
      command: 'npm',
      args: ['run', 'dev'],
      cwd: path.resolve('..'),
      env: { ...process.env },
    })
  );

  console.log('✅ Connected to js-sandbox-mcp');

  // 3️⃣ Use the run_js_ephemeral tool with a dependency (lodash)
  const result = await client.callTool({
    name: 'run_js_ephemeral',
    arguments: {
      image: 'node:lts-slim',
      code: `
        import _ from 'lodash';
        const names = ['Alice', 'Bob', 'Carol', 'Dave'];
        const shuffled = _.shuffle(names);
        console.log('Shuffled names:', shuffled.join(', '));
      `,
      dependencies: [
        {
          name: 'lodash',
          version: '^4.17.21',
        },
      ],
    },
  });

  console.log('▶️ run_js_ephemeral output:\n', result.content[0].text);

  process.exit(0);
}

main().catch((err) => {
  console.error('❌ Error in ephemeral-with-deps example:', err);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/test/sandbox.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from 'vitest';
import initializeSandbox from '../src/tools/initialize.ts';
import stopSandbox from '../src/tools/stop.ts';
import execInSandbox from '../src/tools/exec.ts';
import { containerExists, isContainerRunning } from './utils.ts';

describe('sandbox full lifecycle', () => {
  it('should create, exec in, and remove a Docker sandbox container', async () => {
    // Step 1: Start container
    const start = await initializeSandbox({});
    const content = start.content[0];
    if (content.type !== 'text') throw new Error('Unexpected content type');
    const containerId = content.text;

    expect(containerId).toMatch(/^js-sbx-/);
    expect(isContainerRunning(containerId)).toBe(true);

    // Step 2: Execute command
    const execResult = await execInSandbox({
      container_id: containerId,
      commands: ['echo Hello World', 'uname -a'],
    });

    const execOutput = execResult.content[0];
    if (execOutput.type !== 'text') throw new Error('Unexpected content type');

    expect(execOutput.text).toContain('Hello World');
    expect(execOutput.text).toMatch(/Linux|Unix/); // should match OS output

    // Step 3: Stop container
    const stop = await stopSandbox({ container_id: containerId });
    const stopMsg = stop.content[0];
    if (stopMsg.type !== 'text') throw new Error('Unexpected content type');

    expect(stopMsg.text).toContain(`Container ${containerId} removed.`);
    expect(containerExists(containerId)).toBe(false);
  });
});

```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
// eslint.config.js

import js from '@eslint/js'; // for built‑in configs
import { FlatCompat } from '@eslint/eslintrc'; // to translate shareables
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import prettierPlugin from 'eslint-plugin-prettier';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

// reproduce __dirname in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Tell FlatCompat about eslint:recommended (and eslint:all, if you ever need it)
const compat = new FlatCompat({
  baseDirectory: __dirname,
  recommendedConfig: js.configs.recommended,
  allConfig: js.configs.all,
});

export default [
  // --- ignore patterns ---
  {
    ignores: [
      'node_modules',
      'dist',
      '*.log',
      'coverage',
      '.env',
      'package-lock.json',
      'yarn.lock',
      'pnpm-lock.yaml',
      'examples',
    ],
  },

  // bring in eslint:recommended, plugin:@typescript-eslint/recommended & prettier
  ...compat.extends(
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier'
  ),

  // our overrides for TypeScript files
  {
    files: ['*.ts', '*.tsx'],
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        ecmaVersion: 12,
        sourceType: 'module',
      },
    },
    plugins: {
      '@typescript-eslint': tsPlugin,
      prettier: prettierPlugin,
    },
    rules: {
      'prettier/prettier': 'error',
    },
  },
];

```

--------------------------------------------------------------------------------
/src/tools/stop.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { execFileSync } from 'node:child_process';
import { type McpResponse, textContent } from '../types.ts';
import {
  DOCKER_NOT_RUNNING_ERROR,
  isDockerRunning,
  sanitizeContainerId,
} from '../utils.ts';
import { activeSandboxContainers } from '../containerUtils.ts';

export const argSchema = {
  container_id: z.string().regex(/^[a-zA-Z0-9_.-]+$/, 'Invalid container ID'),
};

export default async function stopSandbox({
  container_id,
}: {
  container_id: string;
}): Promise<McpResponse> {
  if (!isDockerRunning()) {
    return {
      content: [textContent(DOCKER_NOT_RUNNING_ERROR)],
    };
  }

  const validId = sanitizeContainerId(container_id);
  if (!validId) {
    return {
      content: [textContent('Invalid container ID')],
    };
  }

  try {
    // Use execFileSync with validated container_id
    execFileSync('docker', ['rm', '-f', validId]);
    activeSandboxContainers.delete(validId);

    return {
      content: [textContent(`Container ${container_id} removed.`)],
    };
  } catch (error) {
    // Handle any errors that occur during container removal
    const errorMessage = error instanceof Error ? error.message : String(error);
    console.error(
      `[stopSandbox] Error removing container ${container_id}: ${errorMessage}`
    );

    // Still remove from our registry even if Docker command failed
    activeSandboxContainers.delete(validId);

    return {
      content: [
        textContent(
          `Error removing container ${container_id}: ${errorMessage}`
        ),
      ],
    };
  }
}

```

--------------------------------------------------------------------------------
/examples/playwright.js:
--------------------------------------------------------------------------------

```javascript
import path from 'path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

async function main() {
  // 1️⃣ Create the MCP client
  const client = new Client({
    name: 'ephemeral-example',
    version: '1.0.0',
  });

  // 2️⃣ Connect to your js-sandbox-mcp server
  await client.connect(
    new StdioClientTransport({
      command: 'npm',
      args: ['run', 'dev'],
      cwd: path.resolve('..'),
      env: {
        ...process.env,
        //TODO: Change this with your user!
        FILES_DIR: '/Users/your_user/Desktop',
      },
    })
  );

  console.log('✅ Connected to js-sandbox-mcp');

  // 3️⃣ Use the new run_js_ephemeral tool in one step
  const result = await client.callTool({
    name: 'run_js_ephemeral',
    arguments: {
      // Use the ofcicial MS playwright image
      image: 'mcr.microsoft.com/playwright:v1.53.2-noble',
      code: `
        import { chromium } from 'playwright';
  
        (async () => {
          const browser = await chromium.launch();
          const page = await browser.newPage();
          await page.goto('https://example.com');
          await page.screenshot({ path: 'screenshot_test.png' });
          await browser.close();
        })();
      `,
      dependencies: [
        {
          name: 'playwright',
          version: '^1.52.0',
        },
      ],
    },
  });

  console.log('▶️ run_js_ephemeral output:\n', result.content[0].text);

  process.exit(0);
}

main().catch((err) => {
  console.error('❌ Error in ephemeral example:', err);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';

// Will be set once the server is initialized
let serverInstance: McpServer | null = null;

/**
 * Set the server instance for logging
 * @param server MCP server instance
 */
export function setServerInstance(server: McpServer): void {
  serverInstance = server;
}

/**
 * Log levels supported by MCP
 */
export type LogLevel = 'debug' | 'info' | 'warning' | 'error';

/**
 * Send a logging message using the MCP protocol
 * Falls back to console.error if server is not initialized
 * @param level Log level
 * @param message Message to log
 * @param data Optional data to include
 */
export function log(level: LogLevel, message: string, data?: unknown): void {
  if (serverInstance) {
    // Access the server through the internal server property
    // @ts-expect-error - _server is not documented in the public API but is available
    const internalServer = serverInstance._server;
    if (
      internalServer &&
      typeof internalServer.sendLoggingMessage === 'function'
    ) {
      internalServer.sendLoggingMessage({
        level,
        data: data ? `${message}: ${JSON.stringify(data)}` : message,
      });
      return;
    }
  }

  // Fallback if server is not initialized yet or doesn't support logging
  console.error(`[${level.toUpperCase()}] ${message}`, data || '');
}

/**
 * Convenience methods for different log levels
 */
export const logger = {
  debug: (message: string, data?: unknown) => log('debug', message, data),
  info: (message: string, data?: unknown) => log('info', message, data),
  warning: (message: string, data?: unknown) => log('warning', message, data),
  error: (message: string, data?: unknown) => log('error', message, data),
};

```

--------------------------------------------------------------------------------
/examples/simpleSandbox.js:
--------------------------------------------------------------------------------

```javascript
import path from 'path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

async function main() {
  // 1️⃣ Create the MCP client
  const client = new Client({
    name: 'example-client',
    version: '1.0.0',
  });

  // 2️⃣ Launch & connect to the js-sandbox-mcp server
  await client.connect(
    new StdioClientTransport({
      command: 'npm',
      args: ['run', 'dev'], // runs `npm run dev` in the sandbox folder
      cwd: path.resolve('..'),
    })
  );

  console.log('✅ Connected to js-sandbox-mcp');

  // 3️⃣ List available tools
  const tools = await client.listTools();
  console.log('🔧 Available tools:');
  console.dir(tools, { depth: null });

  // 4️⃣ Initialize a fresh sandbox container
  const initResult = await client.callTool({
    name: 'sandbox_initialize',
    arguments: {
      /* no args = uses default node:lts-slim */
    },
  });
  const containerId = initResult.content[0].text;
  console.log(`🐳 Container started: ${containerId}`);

  // 5️⃣ Run a JS snippet inside the container
  const runResult = await client.callTool({
    name: 'run_js',
    arguments: {
      container_id: containerId,
      code: `
        import { randomUUID } from 'node:crypto';
        console.log('Hello from sandbox! Your UUID is', randomUUID());
      `,
      dependencies: [],
    },
  });
  console.log('▶️ run_js output:\n', runResult.content[0].text);

  // 6️⃣ Tear down the container
  const stopResult = await client.callTool({
    name: 'sandbox_stop',
    arguments: { container_id: containerId },
  });
  console.log('🛑', stopResult.content[0].text);

  process.exit(0);
}

main().catch((err) => {
  console.error('❌ Error in example:', err);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/website/src/Components/Footer.tsx:
--------------------------------------------------------------------------------

```typescript
import React from 'react';
import { Link } from 'react-router-dom';

const Footer: React.FC = () => {
  return (
    <footer className="bg-gray-900 text-white py-12">
      <div className="max-w-6xl mx-auto px-6">
        <div className="grid grid-cols-1 md:grid-cols-4 gap-8">
          <div>
            <h3 className="text-xl font-bold mb-4">jsdevai.com</h3>
            <p className="text-gray-400">
              The ultimate destination for JavaScript developers building
              AI-powered applications.
            </p>
          </div>
          <div>
            <h4 className="font-semibold mb-4">Tools</h4>
            <ul className="space-y-2 text-gray-400">
              <li>
                <Link to="/mcp" className="hover:text-white transition">
                  Node.js Sandbox MCP
                </Link>
              </li>
              <li>
                <Link to="/tiny-agent" className="hover:text-white transition">
                  Tiny Agent
                </Link>
              </li>
              <li>
                <Link to="/graph-gpt" className="hover:text-white transition">
                  GraphGPT
                </Link>
              </li>
            </ul>
          </div>

          <div>
            <h4 className="font-semibold mb-4">Community</h4>
            <ul className="space-y-2 text-gray-400">
              <li>
                <a href="#" className="hover:text-white transition">
                  GitHub
                </a>
              </li>
            </ul>
          </div>
        </div>
        <div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
          <p>
            &copy; 2025 jsdevai.com • Empowering JavaScript developers in the AI
            revolution
          </p>
        </div>
      </div>
    </footer>
  );
};

export default Footer;

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "node-code-sandbox-mcp",
  "description": "Run arbitrary JavaScript inside disposable Docker containers and install npm dependencies on the fly.",
  "version": "1.3.0",
  "type": "module",
  "bin": {
    "node-code-sandbox-mcp": "dist/server.js"
  },
  "files": [
    "dist",
    "NODE_GUIDELINES.md"
  ],
  "scripts": {
    "dev": "node --env-file .env --watch src/server.ts",
    "dev:evals": "node evals/index.ts",
    "build": "rimraf dist && tsc -p tsconfig.build.json && shx chmod +x dist/*.js",
    "start": "node dist/server.js",
    "test": "vitest",
    "test:coverage": "vitest --coverage",
    "inspector": "npx @modelcontextprotocol/inspector npm run dev",
    "lint": "eslint . --ext .ts --report-unused-disable-directives --max-warnings 0",
    "format": "prettier --write .",
    "check": "npm run lint && npm run format",
    "pre-commit": "lint-staged",
    "prepublishOnly": "npm run build",
    "prepare": "husky",
    "release": "standard-version",
    "major": "npm run release -- --release-as major",
    "minor": "npm run release -- --release-as minor",
    "patch": "npm run release -- --release-as patch",
    "push-release": "git push --follow-tags origin master"
  },
  "dependencies": {
    "@eslint/eslintrc": "^3.1.0",
    "@modelcontextprotocol/sdk": "^1.17.3",
    "@typescript-eslint/eslint-plugin": "^8.40.0",
    "@typescript-eslint/parser": "^8.40.0",
    "dotenv": "^17.2.1",
    "eslint": "^9.33.0",
    "eslint-config-prettier": "^10.1.8",
    "eslint-plugin-n": "^17.21.3",
    "eslint-plugin-prettier": "^5.5.4",
    "mime-types": "^3.0.1",
    "npm-registry-sdk": "^1.2.1",
    "openai": "^5.13.1",
    "tmp": "^0.2.5"
  },
  "devDependencies": {
    "@commitlint/cli": "^19.8.1",
    "@commitlint/config-conventional": "^19.8.1",
    "@types/lint-staged": "^14.0.0",
    "@types/mime-types": "^3.0.1",
    "@types/node": "^24.3.0",
    "@types/tmp": "^0.2.6",
    "@vitest/coverage-v8": "^3.2.4",
    "husky": "^9.1.7",
    "lint-staged": "^16.1.5",
    "prettier": "^3.6.2",
    "rimraf": "^6.0.1",
    "shx": "^0.4.0",
    "standard-version": "^9.5.0",
    "typescript": "^5.9.2",
    "vitest": "^3.2.4"
  },
  "engines": {
    "node": ">=23.10.0"
  }
}

```

--------------------------------------------------------------------------------
/test/initializeSandbox.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import initializeSandbox from '../src/tools/initialize.ts';
import * as childProcess from 'node:child_process';
import * as crypto from 'node:crypto';
import * as utils from '../src/utils.ts';
import * as types from '../src/types.ts';

vi.mock('node:child_process');
vi.mock('node:crypto');
vi.mock('../types');

describe('initializeSandbox', () => {
  const fakeUUID = '123e4567-e89b-12d3-a456-426614174000';
  const fakeContainerName = `js-sbx-${fakeUUID}`;

  beforeEach(() => {
    vi.resetAllMocks();
    vi.spyOn(crypto, 'randomUUID').mockReturnValue(fakeUUID);
    vi.spyOn(childProcess, 'execFileSync').mockImplementation(() =>
      Buffer.from('')
    );
    vi.spyOn(types, 'textContent').mockImplementation((name) => ({
      type: 'text',
      text: name,
    }));
  });

  it('should return an error message if Docker is not running', async () => {
    vi.spyOn(utils, 'isDockerRunning').mockReturnValue(false);
    const result = await initializeSandbox({});
    expect(result).toEqual({
      content: [
        {
          type: 'text',
          text: 'Error: Docker is not running. Please start Docker and try again.',
        },
      ],
    });
  });

  it('should use the default image when none is provided', async () => {
    const result = await initializeSandbox({});
    expect(childProcess.execFileSync).toHaveBeenCalledWith(
      'docker',
      expect.arrayContaining([
        '--name',
        fakeContainerName,
        utils.DEFAULT_NODE_IMAGE,
      ]),
      expect.any(Object)
    );
    expect(result).toEqual({
      content: [{ type: 'text', text: fakeContainerName }],
    });
  });

  it('should use the provided image', async () => {
    const customImage = 'node:20-alpine';
    const result = await initializeSandbox({ image: customImage });
    expect(childProcess.execFileSync).toHaveBeenCalledWith(
      'docker',
      expect.arrayContaining(['--name', fakeContainerName, customImage]),
      expect.any(Object)
    );
    if (result.content[0].type === 'text') {
      expect(result.content[0].text).toBe(fakeContainerName);
    } else {
      throw new Error('Unexpected content type');
    }
  });
});

```

--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';

const DEFAULT_TIMEOUT_SECONDS = 3600;
const DEFAULT_RUN_SCRIPT_TIMEOUT = 30_000;

const envSchema = z.object({
  NODE_CONTAINER_TIMEOUT: z.string().optional(),
  RUN_SCRIPT_TIMEOUT: z.string().optional(),
  SANDBOX_MEMORY_LIMIT: z
    .string()
    .regex(/^\d+(\.\d+)?[mMgG]?$/, {
      message: 'SANDBOX_MEMORY_LIMIT must be like "512m", "1g", or bytes',
    })
    .optional()
    .nullable(),
  SANDBOX_CPU_LIMIT: z
    .string()
    .regex(/^\d+(\.\d+)?$/, {
      message: 'SANDBOX_CPU_LIMIT must be numeric (e.g. "0.5", "2")',
    })
    .optional()
    .nullable(),
  FILES_DIR: z.string().optional().nullable(),
});

// Schema for the final config object with transformations and defaults
const configSchema = z.object({
  containerTimeoutSeconds: z.number().positive(),
  containerTimeoutMilliseconds: z.number().positive(),
  runScriptTimeoutMilliseconds: z.number().positive(),
  rawMemoryLimit: z.string().optional(),
  rawCpuLimit: z.string().optional(),
  filesDir: z.string().optional(),
});

function loadConfig() {
  const parsedEnv = envSchema.safeParse(process.env);

  if (!parsedEnv.success) {
    throw new Error('Invalid environment variables');
  }

  const timeoutString = parsedEnv.data.NODE_CONTAINER_TIMEOUT;
  let seconds = DEFAULT_TIMEOUT_SECONDS;

  if (timeoutString) {
    const parsedSeconds = parseInt(timeoutString, 10);
    if (!isNaN(parsedSeconds) && parsedSeconds > 0) {
      seconds = parsedSeconds;
    }
  }

  const runScriptTimeoutMillisecondsString = parsedEnv.data.RUN_SCRIPT_TIMEOUT;
  let runScriptTimeoutMilliseconds = DEFAULT_RUN_SCRIPT_TIMEOUT;

  if (runScriptTimeoutMillisecondsString) {
    const parsedSeconds = parseInt(runScriptTimeoutMillisecondsString, 10);
    if (!isNaN(parsedSeconds) && parsedSeconds > 0) {
      runScriptTimeoutMilliseconds = parsedSeconds;
    }
  }

  const milliseconds = seconds * 1000;
  const memRaw = parsedEnv.data.SANDBOX_MEMORY_LIMIT;
  const cpuRaw = parsedEnv.data.SANDBOX_CPU_LIMIT;
  const filesDir = parsedEnv.data.FILES_DIR;

  return configSchema.parse({
    containerTimeoutSeconds: seconds,
    containerTimeoutMilliseconds: milliseconds,
    runScriptTimeoutMilliseconds: runScriptTimeoutMilliseconds,
    rawMemoryLimit: memRaw,
    rawCpuLimit: cpuRaw,
    filesDir: filesDir,
  });
}

export const getConfig = loadConfig;

```

--------------------------------------------------------------------------------
/src/runUtils.ts:
--------------------------------------------------------------------------------

```typescript
import fs from 'fs/promises';
import path from 'path';
import tmp from 'tmp';
import { pathToFileURL } from 'url';
import mime from 'mime-types';
import { textContent, type McpContent } from './types.ts';
import { isRunningInDocker } from './utils.ts';
import { getConfig } from './config.ts';

export async function prepareWorkspace({
  code,
  dependenciesRecord,
}: {
  code: string;
  dependenciesRecord: Record<string, string>;
}) {
  const localTmp = tmp.dirSync({ unsafeCleanup: true });

  await fs.writeFile(path.join(localTmp.name, 'index.js'), code);
  await fs.writeFile(
    path.join(localTmp.name, 'package.json'),
    JSON.stringify(
      { type: 'module', dependencies: dependenciesRecord },
      null,
      2
    )
  );

  return localTmp;
}

export async function extractOutputsFromDir({
  dirPath,
  outputDir,
}: {
  dirPath: string;
  outputDir: string;
}): Promise<McpContent[]> {
  const contents: McpContent[] = [];
  const imageTypes = new Set(['image/jpeg', 'image/png']);

  await fs.mkdir(outputDir, { recursive: true });

  const dirents = await fs.readdir(dirPath, { withFileTypes: true });

  for (const dirent of dirents) {
    if (!dirent.isFile()) continue;

    const fname = dirent.name;
    if (
      fname === 'index.js' ||
      fname === 'package.json' ||
      fname === 'package-lock.json'
    )
      continue;

    const fullPath = path.join(dirPath, fname);
    const destPath = path.join(outputDir, fname);
    await fs.copyFile(fullPath, destPath);

    const hostPath = path.join(getFilesDir(), fname);
    contents.push(textContent(`I saved the file ${fname} at ${hostPath}`));

    const mimeType = mime.lookup(fname) || 'application/octet-stream';

    if (imageTypes.has(mimeType)) {
      const b64 = await fs.readFile(fullPath, { encoding: 'base64' });
      contents.push({
        type: 'image',
        data: b64,
        mimeType,
      });
    }

    contents.push({
      type: 'resource',
      resource: {
        uri: pathToFileURL(hostPath).href,
        mimeType,
        text: fname,
      },
    });
  }

  return contents;
}

export function getHostOutputDir(): string {
  const isContainer = isRunningInDocker();
  return isContainer
    ? path.resolve(process.env.HOME || process.cwd())
    : getFilesDir();
}

// This FILES_DIR is an env var coming from the user
export const getFilesDir = (): string   => {
  return getConfig().filesDir!;             
};

export const getMountFlag = (): string  => {
  const dir = getFilesDir();
  return dir ? `-v ${dir}:/workspace/files` : '';
};
```

--------------------------------------------------------------------------------
/src/linterUtils.ts:
--------------------------------------------------------------------------------

```typescript
import { ESLint } from 'eslint';
import globals from 'globals';
import nPlugin from 'eslint-plugin-n';
import prettierPlugin from 'eslint-plugin-prettier';

const eslint = new ESLint({
  fix: true,
  overrideConfigFile: 'eslint.config.js',

  overrideConfig: {
    plugins: {
      n: nPlugin,
      prettier: prettierPlugin,
    },
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        ...globals.node,
      },
    },
    rules: {
      // --- Best Practices & Bug Prevention for JS ---
      eqeqeq: ['error', 'always'],
      'no-unused-vars': ['warn', { args: 'none', ignoreRestSiblings: true }],
      'no-console': ['warn', { allow: ['warn', 'error'] }],
      'no-return-await': 'error',
      'no-throw-literal': 'error',

      // --- Modern JavaScript & Code Style ---
      'no-var': 'error',
      'prefer-const': 'error',
      'object-shorthand': 'error',
      'prefer-template': 'error',
      'prefer-arrow-callback': 'error',
      'prefer-destructuring': ['warn', { object: true, array: false }],

      // Re-apply prettier rule to ensure it has priority
      'prettier/prettier': 'error',

      // --- Node.js Specific Rules ---
      'n/handle-callback-err': 'error',
      'n/no-deprecated-api': 'error',
      'n/no-new-require': 'error',
      'n/no-unpublished-import': 'off', // Disabled because at linting stage we have not yet run npm i
      'n/no-missing-import': 'off',
    },
  },
});

/**
 * Lints and auto-fixes the given code string.
 * @param code The source code generated by the LLM.
 * @returns An object with the fixed code and a formatted string of remaining errors.
 */
export async function lintAndRefactorCode(code: string): Promise<{
  fixedCode: string;
  errorReport: string | null;
}> {
  const results = await eslint.lintText(code);
  const result = results[0]; // We are only linting one string

  // The 'output' property contains the fixed code if fixes were applied.
  // If no fixes were needed, it's undefined, so we fall back to the original code.
  const fixedCode = result.output ?? code;

  // Filter for errors that could not be auto-fixed
  const remainingErrors = result.messages.filter(
    (msg) => msg.severity === 2 // 2 for 'error'
  );

  if (remainingErrors.length > 0) {
    // Format the remaining errors for feedback
    const errorReport = remainingErrors
      .map(
        (err) => `L${err.line}:${err.column}: ${err.message} (${err.ruleId})`
      )
      .join('\n');
    return { fixedCode, errorReport };
  }

  return { fixedCode, errorReport: null };
}

```

--------------------------------------------------------------------------------
/test/runJs-cache.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as tmp from 'tmp';
import { execSync } from 'node:child_process';
import runJs from '../src/tools/runJs.ts';
import { DEFAULT_NODE_IMAGE } from '../src/utils.ts';
import { forceStopContainer } from '../src/dockerUtils.ts';

function startSandboxContainer(): string {
  return execSync(
    `docker run -d --network host --memory 512m --cpus 1 --workdir /workspace ${DEFAULT_NODE_IMAGE} tail -f /dev/null`,
    { encoding: 'utf-8' }
  ).trim();
}
let tmpDir: tmp.DirResult;

describe('runJs npm install benchmarking', () => {
  beforeEach(() => {
    tmpDir = tmp.dirSync({ unsafeCleanup: true });
    process.env.FILES_DIR = tmpDir.name;
  });

  afterEach(() => {
    tmpDir.removeCallback();
    delete process.env.FILES_DIR;
  });

  it('should install dependency faster on second run due to caching', async () => {
    const containerId = startSandboxContainer();

    try {
      const dependency = { name: 'lodash', version: '^4.17.21' };

      // First run: benchmark install
      const result1 = await runJs({
        container_id: containerId,
        code: "console.log('Hello')",
        dependencies: [dependency],
      });

      const telemetryItem1 = result1.content.find(
        (c) => c.type === 'text' && c.text.startsWith('Telemetry:')
      );
      expect(telemetryItem1).toBeDefined();
      const telemetry1 = JSON.parse(
        (telemetryItem1 && telemetryItem1.type === 'text'
          ? telemetryItem1.text
          : ''
        ).replace('Telemetry:\n', '')
      );
      const installTimeMs1 = telemetry1.installTimeMs;

      // Second run: same install again, expect faster
      const result2 = await runJs({
        container_id: containerId,
        code: "console.log('Hello')",
        dependencies: [dependency],
      });

      const telemetryItem2 = result2.content.find(
        (c) => c.type === 'text' && c.text.startsWith('Telemetry:')
      );
      expect(telemetryItem2).toBeDefined();
      const telemetry2 = JSON.parse(
        (telemetryItem2 && telemetryItem2.type === 'text'
          ? telemetryItem2.text
          : ''
        ).replace('Telemetry:\n', '')
      );
      const installTimeMs2 = telemetry2.installTimeMs;
      // Assert that second install is faster
      try {
        expect(installTimeMs2).toBeLessThan(installTimeMs1);
      } catch (error) {
        console.error('Error in assertion:', error);
        console.log(`First install time: ${installTimeMs1}ms`);
        console.log(`Second install time: ${installTimeMs2}ms`);
        throw error; // Re-throw the error to fail the test
      }
    } finally {
      forceStopContainer(containerId);
    }
  }, 20_000);
});

```

--------------------------------------------------------------------------------
/examples/ephemeralWithFiles.js:
--------------------------------------------------------------------------------

```javascript
import path from 'path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

async function main() {
  // 1️⃣ Create the MCP client
  const client = new Client({
    name: 'ephemeral-with-deps-example',
    version: '1.0.0',
  });

  // Host path where you want outputs to land
  const FILES_DIR = '/Users/alfonsograziano/Desktop';

  // Resolve it against $HOME (in case you ever switch to a relative subfolder)
  const hostOutput = path.resolve(process.env.HOME, FILES_DIR);

  // Where we’ll mount that folder _inside_ the MCP‐server container
  const containerOutput = '/root';

  // 2️⃣ Connect to your js-sandbox-mcp server

  await client.connect(
    new StdioClientTransport({
      command: 'npm',
      args: ['run', 'dev'],
      cwd: path.resolve('..'),
      env: { ...process.env, FILES_DIR },
    })
  );

  // await client.connect(
  //   new StdioClientTransport({
  //     command: "docker",
  //     args: [
  //       // 1) Start a new container
  //       "run",
  //       // 2) Keep STDIN open and allocate a pseudo-TTY (required for MCP over stdio)
  //       "-i",
  //       // 3) Remove the container automatically when it exits
  //       "--rm",

  //       // 4) Give the MCP-server access to the Docker socket
  //       //    so it can spin up inner “ephemeral” containers
  //       "-v",
  //       "/var/run/docker.sock:/var/run/docker.sock",

  //       // 5) Bind-mount your Desktop folder into the container at /root
  //       "-v",
  //       `${hostOutput}:${containerOutput}`,

  //       // 6) Pass your host’s output-dir env var _into_ the MCP-server
  //       "-e",
  //       `FILES_DIR=${hostOutput}`,

  //       // 7) The MCP-server image that will manage your ephemeral sandboxes
  //       "alfonsograziano/node-code-sandbox-mcp",
  //     ],
  //     env: {
  //       // inherit your shell’s env
  //       ...process.env,
  //       // also set FILES_DIR inside the MCP-server process
  //       FILES_DIR,
  //     },
  //   })
  // );

  console.log('✅ Connected to js-sandbox-mcp');

  // 3️⃣ Use the run_js_ephemeral tool with a dependency (lodash)
  const result = await client.callTool({
    name: 'run_js_ephemeral',
    arguments: {
      image: 'node:lts-slim',
      code: `
          import fs from 'fs/promises';  
          await fs.writeFile('hello_world.txt', 'Hello world!');
      `,
      dependencies: [
        {
          name: 'lodash',
          version: '^4.17.21',
        },
      ],
    },
  });

  console.log('▶️ run_js_ephemeral output:\n', JSON.stringify(result, null, 2));

  process.exit(0);
}

main().catch((err) => {
  console.error('❌ Error in ephemeral-with-deps example:', err);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/src/tools/getDependencyTypes.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import type { McpResponse } from '../types.ts';
import { textContent } from '../types.ts';
import { logger } from '../logger.ts';

export const argSchema = {
  dependencies: z.array(
    z.object({
      name: z.string(),
      version: z.string().optional(),
    })
  ),
};

export default async function getDependencyTypes({
  dependencies,
}: {
  dependencies: { name: string; version?: string }[];
}): Promise<McpResponse> {
  const results: {
    name: string;
    hasTypes: boolean;
    types?: string;
    typesPackage?: string;
    version?: string;
  }[] = [];

  for (const dep of dependencies) {
    const info: (typeof results)[number] = { name: dep.name, hasTypes: false };
    try {
      const pkgRes = await fetch(`https://registry.npmjs.org/${dep.name}`);
      if (pkgRes.ok) {
        const pkgMeta = (await pkgRes.json()) as any;
        const latestTag = pkgMeta['dist-tags']?.latest as string;
        const versionToUse = dep.version || latestTag;
        const versionData = pkgMeta.versions?.[versionToUse];
        // Check for in-package types
        if (versionData) {
          const typesField = versionData.types || versionData.typings;
          if (typesField) {
            const url = `https://unpkg.com/${dep.name}@${versionToUse}/${typesField}`;
            const contentRes = await fetch(url);
            if (contentRes.ok) {
              info.hasTypes = true;
              info.types = await contentRes.text();
              info.version = versionToUse;
              results.push(info);
              continue;
            }
          }
        }

        // Fallback to @types package
        const sanitized = dep.name.replace('@', '').replace('/', '__');
        const typesName = `@types/${sanitized}`;
        const typesRes = await fetch(
          `https://registry.npmjs.org/${encodeURIComponent(typesName)}`
        );
        if (typesRes.ok) {
          const typesMeta = (await typesRes.json()) as any;
          const typesVersion = typesMeta['dist-tags']?.latest as string;
          const typesVersionData = typesMeta.versions?.[typesVersion];
          const typesField =
            typesVersionData?.types ||
            typesVersionData?.typings ||
            'index.d.ts';
          const url = `https://unpkg.com/${typesName}@${typesVersion}/${typesField}`;
          const contentRes = await fetch(url);
          if (contentRes.ok) {
            info.hasTypes = true;
            info.typesPackage = typesName;
            info.version = typesVersion;
            info.types = await contentRes.text();
          }
        }
      }
    } catch (e) {
      logger.info(`Failed to fetch type info for ${dep.name}: ${e}`);
    }
    results.push(info);
  }

  return { content: [textContent(JSON.stringify(results))] };
}

```

--------------------------------------------------------------------------------
/src/dockerUtils.ts:
--------------------------------------------------------------------------------

```typescript
import { execFile, execFileSync } from 'child_process';
import util from 'util';
import { logger } from './logger.ts';
import { getConfig } from './config.ts';
import { textContent } from './types.ts';
import { sanitizeContainerId, sanitizeShellCommand } from './utils.ts';

const execFilePromise = util.promisify(execFile);

/**
 * Attempts to forcefully stop and remove a Docker container by its ID.
 * Logs errors but does not throw them to allow cleanup flows to continue.
 * Does NOT manage any external container registry/map.
 * @param containerId The ID of the container to stop and remove.
 */
export async function forceStopContainer(containerId: string): Promise<void> {
  logger.info(
    `Attempting to stop and remove container via dockerUtils: ${containerId}`
  );
  try {
    // Sanitize containerId
    const safeId = sanitizeContainerId(containerId);
    if (!safeId) throw new Error('Invalid containerId');
    // Force stop the container (ignores errors if already stopped)
    await execFilePromise('docker', ['stop', safeId]);
    // Force remove the container (ignores errors if already removed)
    await execFilePromise('docker', ['rm', '-f', safeId]);
    logger.info(
      `Successfully issued stop/remove commands for container: ${containerId}`
    );
  } catch (error) {
    // Log errors but don't throw
    logger.error(
      `Error during docker stop/remove commands for container ${containerId}`,
      typeof error === 'object' &&
        error !== null &&
        ('stderr' in error || 'message' in error)
        ? (error as { stderr?: string; message?: string }).stderr ||
            (error as { message: string }).message
        : String(error)
    );
  }
}

export type NodeExecResult = {
  output: string | null;
  error: Error | null;
  duration: number;
};

export function safeExecNodeInContainer({
  containerId,
  timeoutMs = getConfig().runScriptTimeoutMilliseconds,
  command = 'node index.js',
}: {
  containerId: string;
  timeoutMs?: number;
  command?: string;
}): NodeExecResult {
  const runStart = Date.now();
  // Sanitize command
  const safeCmd = sanitizeShellCommand(command);
  if (!safeCmd) {
    return { output: null, error: new Error('Invalid command'), duration: 0 };
  }
  try {
    const output = execFileSync(
      'docker',
      ['exec', containerId, '/bin/sh', '-c', safeCmd],
      { encoding: 'utf8', timeout: timeoutMs }
    );
    return { output, error: null, duration: Date.now() - runStart };
  } catch (err) {
    const error = err instanceof Error ? err : new Error(String(err));
    return { output: null, error, duration: Date.now() - runStart };
  }
}

export const getContentFromError = (
  error: Error,
  telemetry: Record<string, unknown>
) => {
  return {
    content: [
      textContent(`Error during execution: ${error.message}`),
      textContent(`Telemetry:\n${JSON.stringify(telemetry, null, 2)}`),
    ],
  };
};

```

--------------------------------------------------------------------------------
/test/snapshotUtils.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as tmp from 'tmp';
import * as fs from 'fs';
import * as path from 'path';
import { getSnapshot, detectChanges } from '../src/snapshotUtils.ts';

let tmpDir: tmp.DirResult;

function createFile(filePath: string, content = '') {
  fs.writeFileSync(filePath, content);
}

function createDir(dirPath: string) {
  fs.mkdirSync(dirPath);
}

describe('Filesystem snapshot and change detection', () => {
  beforeEach(() => {
    tmpDir = tmp.dirSync({ unsafeCleanup: true });
  });

  afterEach(() => {
    tmpDir.removeCallback();
  });

  it('getSnapshot returns correct structure for files and directories', async () => {
    const file1 = path.join(tmpDir.name, 'file1.txt');
    const subDir = path.join(tmpDir.name, 'sub');
    const file2 = path.join(subDir, 'file2.txt');

    createFile(file1, 'Hello');
    createDir(subDir);
    createFile(file2, 'World');

    const snapshot = await getSnapshot(tmpDir.name);

    expect(Object.keys(snapshot)).toContain(file1);
    expect(Object.keys(snapshot)).toContain(subDir);
    expect(Object.keys(snapshot)).toContain(file2);

    expect(snapshot[file1].isDirectory).toBe(false);
    expect(snapshot[subDir].isDirectory).toBe(true);
    expect(snapshot[file2].isDirectory).toBe(false);
  });

  it('detectChanges detects created files', async () => {
    const initialSnapshot = await getSnapshot(tmpDir.name);

    const newFile = path.join(tmpDir.name, 'newFile.txt');
    createFile(newFile, 'New content');

    const changes = await detectChanges(
      initialSnapshot,
      tmpDir.name,
      Date.now() - 1000
    );

    expect(changes).toEqual([
      {
        type: 'created',
        path: newFile,
        isDirectory: false,
      },
    ]);
  });

  it('detectChanges detects deleted files', async () => {
    const fileToDelete = path.join(tmpDir.name, 'toDelete.txt');
    createFile(fileToDelete, 'To be deleted');

    const snapshotBeforeDelete = await getSnapshot(tmpDir.name);
    fs.unlinkSync(fileToDelete);

    const changes = await detectChanges(
      snapshotBeforeDelete,
      tmpDir.name,
      Date.now() - 1000
    );

    expect(changes).toEqual([
      {
        type: 'deleted',
        path: fileToDelete,
        isDirectory: false,
      },
    ]);
  });

  it('detectChanges detects updated files', async () => {
    const fileToUpdate = path.join(tmpDir.name, 'update.txt');
    createFile(fileToUpdate, 'Original');

    const snapshot = await getSnapshot(tmpDir.name);

    // Wait to ensure mtimeMs changes
    await new Promise((resolve) => setTimeout(resolve, 20));
    fs.writeFileSync(fileToUpdate, 'Updated');

    const changes = await detectChanges(
      snapshot,
      tmpDir.name,
      Date.now() - 1000
    );

    expect(changes).toEqual([
      {
        type: 'updated',
        path: fileToUpdate,
        isDirectory: false,
      },
    ]);
  });
});

```

--------------------------------------------------------------------------------
/src/tools/initialize.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { execFileSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { type McpResponse, textContent } from '../types.ts';
import {
  DEFAULT_NODE_IMAGE,
  DOCKER_NOT_RUNNING_ERROR,
  isDockerRunning,
  computeResourceLimits,
} from '../utils.ts';
import { getMountFlag } from '../runUtils.ts';
import { activeSandboxContainers } from '../containerUtils.ts';
import { logger } from '../logger.ts';
import stopSandbox from './stop.ts';

// Instead of importing serverRunId directly, we'll have a variable that gets set
let serverRunId = 'unknown';

// Function to set the serverRunId from the server.ts file
export function setServerRunId(id: string) {
  serverRunId = id;
}

export const argSchema = {
  image: z.string().optional(),
  port: z
    .number()
    .optional()
    .describe('If set, maps this container port to the host'),
};

export default async function initializeSandbox({
  image = DEFAULT_NODE_IMAGE,
  port,
}: {
  image?: string;
  port?: number;
}): Promise<McpResponse> {
  if (!isDockerRunning()) {
    return {
      content: [textContent(DOCKER_NOT_RUNNING_ERROR)],
    };
  }

  const containerId = `js-sbx-${randomUUID()}`;
  const creationTimestamp = Date.now();

  const portOption = port ? `-p ${port}:${port}` : `--network host`; // prefer --network host if no explicit port mapping

  // Construct labels
  const labels = [
    `mcp-sandbox=true`,
    `mcp-server-run-id=${serverRunId}`,
    `mcp-creation-timestamp=${creationTimestamp}`,
  ];
  const { memFlag, cpuFlag } = computeResourceLimits(image);
  const mountFlag = getMountFlag();

  try {
    const args = [
      'run',
      '-d',
      ...portOption.split(' '),
      ...memFlag.split(' '),
      ...cpuFlag.split(' '),
      '--workdir',
      '/workspace',
      ...mountFlag.split(' '),
      ...labels.flatMap((label) => ['--label', label]),
      '--name',
      containerId,
      image,
      'tail',
      '-f',
      '/dev/null',
    ].filter(Boolean);

    execFileSync('docker', args, { stdio: 'ignore' });

    // Register the container only after successful creation
    activeSandboxContainers.set(containerId, creationTimestamp);
    logger.info(`Registered container ${containerId}`);

    return {
      content: [textContent(containerId)],
    };
  } catch (error) {
    logger.error(`Failed to initialize container ${containerId}`, error);
    // Ensure partial cleanup if execFileSync fails after container might be created but before registration
    try {
      stopSandbox({ container_id: containerId });
    } catch (cleanupError: unknown) {
      // Ignore cleanup errors - log it just in case
      logger.warning(
        `Ignoring error during cleanup attempt for ${containerId}: ${String(cleanupError)}`
      );
    }
    return {
      content: [
        textContent(
          `Failed to initialize sandbox container: ${error instanceof Error ? error.message : String(error)}`
        ),
      ],
    };
  }
}

```

--------------------------------------------------------------------------------
/evals/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
import { OpenAIAuditClient } from './auditClient.ts';

dotenv.config();

/**
 * evalRunner Configuration
 *
 * - evalsPath:   Path to a JSON file containing an array of eval definitions {
 *                   id: string,
 *                   prompt: string
 *               }
 * - batchSize:   Number of evals to process per batch.
 * - outputPath:  Path to write results in JSONL format.
 */
const config = {
  evalsPath: './evals/basicEvals.json',
  batchSize: 5,
  outputPath: './evalResults.jsonl',
};

async function run() {
  const { evalsPath, batchSize, outputPath } = config;

  // Load eval definitions
  if (!fs.existsSync(evalsPath)) {
    console.error(`Evals file not found at ${evalsPath}`);
    process.exit(1);
  }
  const evals = JSON.parse(fs.readFileSync(evalsPath, 'utf-8'));
  if (!Array.isArray(evals)) {
    console.error('Evals file must export an array of {id, prompt} objects.');
    process.exit(1);
  }

  // Initialize OpenAI Audit Client
  const client = new OpenAIAuditClient({
    apiKey: process.env.OPENAI_API_KEY!,
    model: process.env.OPENAI_MODEL || 'gpt-4o-mini',
  });
  await client.initializeClient();
  console.log('OpenAI Audit Client initialized');

  // Ensure output directory exists
  const outDir = path.dirname(outputPath);
  if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });

  // Process in batches
  for (let i = 0; i < evals.length; i += batchSize) {
    const batch = evals.slice(i, i + batchSize);
    console.log(
      `Processing batch ${i / batchSize + 1} (${batch.length} evals)...`
    );

    const promises = batch.map(async ({ id, prompt }) => {
      const startTimeInMillis = new Date().getTime();
      const startHumanRadableTime = new Date().toISOString();
      try {
        const fullResponse = await client.chat({
          messages: [{ role: 'user', content: prompt }],
        });
        const endTimeInMillis = new Date().getTime();
        const endHumanRadableTime = new Date().toISOString();
        const durationInMillis = endTimeInMillis - startTimeInMillis;
        const humanRadableDuration = `${startHumanRadableTime} - ${endHumanRadableTime}`;

        return {
          id,
          fullResponse,
          timing: {
            startTimeInMillis,
            endTimeInMillis,
            startHumanRadableTime,
            endHumanRadableTime,
            durationInMillis,
            humanRadableDuration,
          },
        };
      } catch (err) {
        const errorMessage =
          err instanceof Error ? err.message : `Unknown error: ${err}`;
        return { id, prompt, error: errorMessage };
      }
    });

    const results = await Promise.all(promises);

    // Append each result as a JSON line
    for (const result of results) {
      fs.appendFileSync(outputPath, JSON.stringify(result, null, 2) + '\n');
    }
    console.log(`Batch ${i / batchSize + 1} done.`);
  }

  console.log('All evals processed. Results saved to', config.outputPath);
}

run().catch((err) => {
  console.error('Error running evalRunner:', err);
  process.exit(1);
});

```

--------------------------------------------------------------------------------
/evals/evals.json:
--------------------------------------------------------------------------------

```json
[
  {
    "id": "generate-qr-code",
    "prompt": "Create and run a JS script that generates a QR code for the URL 'https://nodejs.org/en' and saves it as 'qrcode.png'. Use the 'qrcode' package."
  },
  {
    "id": "test-regular-expressions",
    "prompt": "Create and run a JavaScript script that defines a complex regular expression to match valid mathematical expressions containing nested parentheses (e.g., ((2+3)_(4-5))), allowing numbers, +, -, _, / operators, and properly nested parentheses. The script must handle nesting up to 3-4 levels, include at least 10 unit tests covering correct and incorrect cases using assert or manual errors, and include a short comment explaining the regex structure."
  },
  {
    "id": "create-csv-random-data",
    "prompt": "Create and execute a JS script that generates 200 rows of CSV data with full name, random number, and valid email, then writes it to a file called 'fake_data.csv'."
  },
  {
    "id": "scrape-webpage-title",
    "prompt": "Create and run a JS script that fetches 'https://example.com', saves the HTML to 'example.html', extracts the <title> tag, and prints it to the console. Use 'cheerio'."
  },
  {
    "id": "create-pdf-report",
    "prompt": "Create a JS script that generates a PDF file named 'getting-started-javascript.pdf' containing a playful, colorful 'Getting Started with JavaScript' tutorial for a 10-year-old, covering console.log(), variables, and a first program. Use 'pdf-lib' or 'pdfkit' and save via fs."
  },
  {
    "id": "fetch-api-save-json",
    "prompt": "Create and run a JS script that fetches data from the GitHub API endpoint 'https://api.github.com/repos/nodejs/node' and saves part of the response to 'nodejs_info.json'."
  },
  {
    "id": "markdown-to-html-converter",
    "prompt": "Write a JS script that takes a Markdown string and converts it into HTML, then saves the result to 'content_converted.html'. Use the example markdown: '# Welcome to My Page\n\nThis is a simple page created from **Markdown**!\n\n- Learn JavaScript\n- Learn Markdown\n- Build Cool Stuff 🚀'. Use 'marked'."
  },
  {
    "id": "generate-random-data",
    "prompt": "Create a JS script that generates a list of 100 fake users with names, emails, and addresses, then saves them to a JSON file called 'fake_users.json'. Use '@faker-js/faker'."
  },
  {
    "id": "evaluate-complex-math-expression",
    "prompt": "Create a JS script that evaluates the expression '((5 + 8) * (15 / 3) - (9 - (4 * 6)) + (10 / (2 + 6))) ^ 2 + sqrt(64) - factorial(6) + (24 / (5 + 7 * (3 ^ 2))) + log(1000) * sin(30 * pi / 180) - cos(60 * pi / 180) + tan(45 * pi / 180) + (4 ^ 3 - 2 ^ (5 - 2)) * (sqrt(81) / 9)'. Use 'math.js'."
  },
  {
    "id": "take-screenshot-with-playwright",
    "prompt": "Create and run a JS script that launches a Chromium browser, navigates to 'https://example.com', and takes a screenshot saved as 'screenshot_test.png'. Use the official Playwright Docker image and install the 'playwright' package dynamically."
  },
  {
    "id": "generate-chart",
    "prompt": "Write a JS script that generates a bar chart using 'chartjs-node-canvas' showing Monthly Revenue Growth for January ($12,000), February ($15,500), March ($14,200), April ($18,300), May ($21,000), and June ($24,500). Title: 'Monthly Revenue Growth (2025)', X-axis 'Month', Y-axis 'Revenue (USD)', and save as 'chart.png'."
  },
  {
    "id": "hello-world",
    "prompt": "Create and run a simple Node.js script that prints \"Hello, World!\" to the console."
  }
]

```

--------------------------------------------------------------------------------
/src/snapshotUtils.ts:
--------------------------------------------------------------------------------

```typescript
import path from 'node:path';
import { glob, stat, readFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';

import mime from 'mime-types';

import { getFilesDir } from './runUtils.ts';
import { type McpContent, textContent } from './types.ts';
import { isRunningInDocker } from './utils.ts';
import type { Dirent } from 'node:fs';

type ChangeType = 'created' | 'updated' | 'deleted';
type Change = {
  type: ChangeType;
  path: string;
  isDirectory: boolean;
};

type FileSnapshot = Record<string, { mtimeMs: number; isDirectory: boolean }>;

export const getMountPointDir = () => {
  if (isRunningInDocker()) {
    return '/root';
  }
  return getFilesDir();
};

export async function getSnapshot(dir: string): Promise<FileSnapshot> {
  const snapshot: FileSnapshot = {};

  const executor = glob('**/*', {
    cwd: dir,
    withFileTypes: true,
    exclude: (file: string | Dirent): boolean => {
      const name = typeof file === 'string' ? file : file.name;
      return ['.git', 'node_modules'].includes(name);
    },
  });

  for await (const entry of executor) {
    const fullPath = path.join(entry.parentPath, entry.name);
    const stats = await stat(fullPath);
    snapshot[fullPath] = {
      mtimeMs: stats.mtimeMs,
      isDirectory: entry.isDirectory(),
    };
  }

  return snapshot;
}

export async function detectChanges(
  prevSnapshot: FileSnapshot,
  dir: string,
  sinceTimeMs: number
): Promise<Change[]> {
  const changes: Change[] = [];
  const currentSnapshot = await getSnapshot(dir);

  const allPaths = new Set([
    ...Object.keys(prevSnapshot),
    ...Object.keys(currentSnapshot),
  ]);

  for (const filePath of allPaths) {
    const prev = prevSnapshot[filePath];
    const curr = currentSnapshot[filePath];

    if (!prev && curr && curr.mtimeMs >= sinceTimeMs) {
      changes.push({
        type: 'created',
        path: filePath,
        isDirectory: curr.isDirectory,
      });
    } else if (prev && !curr) {
      changes.push({
        type: 'deleted',
        path: filePath,
        isDirectory: prev.isDirectory,
      });
    } else if (
      prev &&
      curr &&
      curr.mtimeMs > prev.mtimeMs &&
      curr.mtimeMs >= sinceTimeMs
    ) {
      changes.push({
        type: 'updated',
        path: filePath,
        isDirectory: curr.isDirectory,
      });
    }
  }

  return changes;
}

export async function changesToMcpContent(
  changes: Change[]
): Promise<McpContent[]> {
  const contents: McpContent[] = [];
  const imageTypes = new Set(['image/jpeg', 'image/png']);

  // Build single summary message
  const summaryLines = changes.map((change) => {
    const fname = path.basename(change.path);
    return `- ${fname} was ${change.type}`;
  });

  if (summaryLines.length > 0) {
    contents.push(
      textContent(`List of changed files:\n${summaryLines.join('\n')}`)
    );
  }

  // Add image/resource entries for created/updated (not deleted)
  for (const change of changes) {
    if (change.type === 'deleted') continue;

    const mimeType = mime.lookup(change.path) || 'application/octet-stream';

    if (imageTypes.has(mimeType)) {
      const b64 = await readFile(change.path, {
        encoding: 'base64',
      });
      contents.push({
        type: 'image',
        data: b64,
        mimeType,
      });
    }

    const hostPath = path.join(getFilesDir(), path.basename(change.path));

    contents.push({
      type: 'resource',
      resource: {
        uri: pathToFileURL(hostPath).href,
        mimeType,
        text: path.basename(change.path),
      },
    });
  }

  return contents;
}

```

--------------------------------------------------------------------------------
/test/stopSandbox.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import stopSandbox from '../src/tools/stop.ts';
import * as childProcess from 'node:child_process';
import * as utils from '../src/utils.ts';

vi.mock('node:child_process');
vi.mock('../src/types', () => ({
  textContent: (text: string) => ({ type: 'text', text }),
}));
vi.mock('../src/utils.ts', async () => {
  const actual =
    await vi.importActual<typeof import('../src/utils.ts')>('../src/utils.ts');
  return {
    ...actual,
    DOCKER_NOT_RUNNING_ERROR: actual.DOCKER_NOT_RUNNING_ERROR,
    isDockerRunning: vi.fn(() => true),
    sanitizeContainerId: actual.sanitizeContainerId,
  };
});

beforeEach(() => {
  vi.resetAllMocks();
  vi.spyOn(childProcess, 'execFileSync').mockImplementation(() =>
    Buffer.from('')
  );
});

afterEach(() => {
  vi.restoreAllMocks();
});

describe('stopSandbox', () => {
  const fakeContainerId = 'js-sbx-test123'; // valid container ID

  it('should remove the container with the given ID', async () => {
    const result = await stopSandbox({ container_id: fakeContainerId });
    expect(childProcess.execFileSync).toHaveBeenCalledWith('docker', [
      'rm',
      '-f',
      fakeContainerId,
    ]);
    expect(result).toEqual({
      content: [
        { type: 'text', text: `Container ${fakeContainerId} removed.` },
      ],
    });
  });

  it('should return an error message when Docker is not running', async () => {
    vi.mocked(utils.isDockerRunning).mockReturnValue(false);
    const result = await stopSandbox({ container_id: fakeContainerId });
    expect(childProcess.execFileSync).not.toHaveBeenCalled();
    expect(result).toEqual({
      content: [{ type: 'text', text: utils.DOCKER_NOT_RUNNING_ERROR }],
    });
  });

  it('should handle errors when removing the container', async () => {
    vi.spyOn(console, 'error').mockImplementation(() => {});
    const errorMessage = 'Container not found';
    vi.mocked(childProcess.execFileSync).mockImplementation(() => {
      throw new Error(errorMessage);
    });
    const result = await stopSandbox({ container_id: fakeContainerId });
    expect(childProcess.execFileSync).toHaveBeenCalledWith('docker', [
      'rm',
      '-f',
      fakeContainerId,
    ]);
    expect(result).toEqual({
      content: [
        {
          type: 'text',
          text: `Error removing container ${fakeContainerId}: ${errorMessage}`,
        },
      ],
    });
  });

  it('should reject invalid container_id', async () => {
    const result = await stopSandbox({ container_id: 'bad;id$(rm -rf /)' });
    expect(result).toEqual({
      content: [
        {
          type: 'text',
          text: 'Invalid container ID',
        },
      ],
    });
    expect(childProcess.execFileSync).not.toHaveBeenCalled();
  });
});

describe('Command injection prevention', () => {
  const dangerousIds = [
    '$(touch /tmp/pwned)',
    '`touch /tmp/pwned`',
    'bad;id',
    'js-sbx-123 && rm -rf /',
    'js-sbx-123 | echo hacked',
    'js-sbx-123 > /tmp/pwned',
    'js-sbx-123 $(id)',
    'js-sbx-123; echo pwned',
    'js-sbx-123`echo pwned`',
    'js-sbx-123/../../etc/passwd',
    'js-sbx-123\nrm -rf /',
    '',
    ' ',
    'js-sbx-123$',
    'js-sbx-123#',
  ];

  dangerousIds.forEach((payload) => {
    it(`should reject dangerous container_id: "${payload}"`, async () => {
      const result = await stopSandbox({ container_id: payload });
      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Invalid container ID',
          },
        ],
      });
      expect(childProcess.execFileSync).not.toHaveBeenCalled();
    });
  });
});

```

--------------------------------------------------------------------------------
/test/execInSandbox.test.ts:
--------------------------------------------------------------------------------

```typescript
import {
  describe,
  it,
  expect,
  beforeAll,
  afterAll,
  beforeEach,
  afterEach,
} from 'vitest';
import initializeSandbox from '../src/tools/initialize.ts';
import execInSandbox from '../src/tools/exec.ts';
import stopSandbox from '../src/tools/stop.ts';
import * as utils from '../src/utils.ts';
import { vi } from 'vitest';

let containerId: string;

beforeAll(async () => {
  const result = await initializeSandbox({});
  const content = result.content[0];
  if (content.type !== 'text') throw new Error('Unexpected content type');
  containerId = content.text;
});

afterAll(async () => {
  await stopSandbox({ container_id: containerId });
});

describe('execInSandbox', () => {
  it('should return an error if Docker is not running', async () => {
    vi.spyOn(utils, 'isDockerRunning').mockReturnValue(false);

    const result = await initializeSandbox({});
    expect(result).toEqual({
      content: [
        {
          type: 'text',
          text: 'Error: Docker is not running. Please start Docker and try again.',
        },
      ],
    });

    vi.restoreAllMocks();
  });
  it('should execute a single command and return its output', async () => {
    const result = await execInSandbox({
      container_id: containerId,
      commands: ['echo Hello'],
    });

    expect(result.content[0].type).toBe('text');
    if (result.content[0].type === 'text') {
      expect(result.content[0].text.trim()).toBe('Hello');
    } else {
      throw new Error('Unexpected content type');
    }
  });

  it('should execute multiple commands and join their outputs', async () => {
    const result = await execInSandbox({
      container_id: containerId,
      commands: ['echo First', 'echo Second'],
    });

    let output: string[] = [];
    if (result.content[0].type === 'text') {
      output = result.content[0].text.trim().split('\n');
      expect(output).toEqual(['First', '', 'Second']);
    } else {
      throw new Error('Unexpected content type');
    }
  });

  it('should handle command with special characters', async () => {
    const result = await execInSandbox({
      container_id: containerId,
      commands: ['echo "Special: $HOME"'],
    });

    if (result.content[0].type === 'text') {
      expect(result.content[0].text.trim()).toContain('Special:');
    } else {
      throw new Error('Unexpected content type');
    }
  });
});

describe('Command injection prevention', () => {
  beforeEach(() => {
    vi.doMock('node:child_process', () => ({
      execFileSync: vi.fn(() => Buffer.from('')),
      execFile: vi.fn(() => Buffer.from('')),
    }));
  });

  afterEach(() => {
    vi.resetModules();
    vi.resetAllMocks();
    vi.restoreAllMocks();
  });

  const dangerousIds = [
    '$(touch /tmp/pwned)',
    '`touch /tmp/pwned`',
    'bad;id',
    'js-sbx-123 && rm -rf /',
    'js-sbx-123 | echo hacked',
    'js-sbx-123 > /tmp/pwned',
    'js-sbx-123 $(id)',
    'js-sbx-123; echo pwned',
    'js-sbx-123`echo pwned`',
    'js-sbx-123/../../etc/passwd',
    'js-sbx-123\nrm -rf /',
    '',
    ' ',
    'js-sbx-123$',
    'js-sbx-123#',
  ];

  dangerousIds.forEach((payload) => {
    it(`should reject dangerous container_id: "${payload}"`, async () => {
      const { default: execInSandbox } = await import('../src/tools/exec.ts');
      const childProcess = await import('node:child_process');
      const result = await execInSandbox({
        container_id: payload,
        commands: ['echo test'],
      });
      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Invalid container ID',
          },
        ],
      });
      const execFileSyncCall = vi.mocked(childProcess.execFileSync).mock.calls;
      expect(execFileSyncCall.length).toBe(0);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/containerUtils.ts:
--------------------------------------------------------------------------------

```typescript
import { forceStopContainer as dockerForceStopContainer } from './dockerUtils.ts';
import { logger } from './logger.ts';

// Registry for active sandbox containers: Map<containerId, creationTimestamp>
export const activeSandboxContainers = new Map<string, number>();

/**
 * Starts the periodic scavenger task to clean up timed-out containers.
 * @param containerTimeoutMilliseconds The maximum allowed age for a container in milliseconds.
 * @param containerTimeoutSeconds The timeout in seconds (for logging).
 * @param checkIntervalMilliseconds How often the scavenger should check (defaults to 60000ms).
 * @returns The interval handle returned by setInterval.
 */
export function startScavenger(
  containerTimeoutMilliseconds: number,
  containerTimeoutSeconds: number,
  checkIntervalMilliseconds = 60 * 1000
): NodeJS.Timeout {
  logger.info(
    `Starting container scavenger. Timeout: ${containerTimeoutSeconds}s, Check Interval: ${checkIntervalMilliseconds / 1000}s`
  );

  const scavengerInterval = setInterval(() => {
    const now = Date.now();
    if (activeSandboxContainers.size > 0) {
      logger.debug(
        `Checking ${activeSandboxContainers.size} active containers for timeout (${containerTimeoutSeconds}s)...`
      );
    }
    for (const [
      containerId,
      creationTimestamp,
    ] of activeSandboxContainers.entries()) {
      if (now - creationTimestamp > containerTimeoutMilliseconds) {
        logger.warning(
          `Container ${containerId} timed out (created at ${new Date(creationTimestamp).toISOString()}). Forcing removal.`
        );

        dockerForceStopContainer(containerId)
          .then(() => {
            // Remove from registry AFTER docker command attempt
            activeSandboxContainers.delete(containerId);
            logger.info(`Removed container ${containerId} from registry.`);
          })
          .catch((error) => {
            // Log error from force stop attempt but continue scavenger
            logger.error(`Error during forced stop of ${containerId}`, error);
            // Still attempt to remove from registry if Docker failed
            activeSandboxContainers.delete(containerId);
            logger.info(
              `Removed container ${containerId} from registry after error.`
            );
          });
      }
    }
  }, checkIntervalMilliseconds);

  return scavengerInterval;
}

/**
 * Attempts to stop and remove all containers currently listed in the
 * activeSandboxContainers registry.
 * Should be called during graceful shutdown.
 */
export async function cleanActiveContainers(): Promise<void> {
  const containersToClean = Array.from(activeSandboxContainers.keys());

  if (containersToClean.length === 0) {
    logger.info('[Shutdown Cleanup] No active containers to clean up.');
    return;
  }

  logger.info(
    `[Shutdown Cleanup] Cleaning up ${containersToClean.length} active containers...`
  );

  const cleanupPromises = containersToClean.map(async (id) => {
    try {
      await dockerForceStopContainer(id); // Attempt to stop/remove via Docker
    } catch (error) {
      // Log error but continue, registry removal happens regardless
      logger.error(`[Shutdown Cleanup] Error stopping container ${id}`, error);
    } finally {
      activeSandboxContainers.delete(id); // Always remove from registry
      logger.info(`[Shutdown Cleanup] Removed container ${id} from registry.`);
    }
  });

  const results = await Promise.allSettled(cleanupPromises);
  logger.info('[Shutdown Cleanup] Container cleanup finished.');

  results.forEach((result, index) => {
    if (result.status === 'rejected') {
      logger.error(
        `[Shutdown Cleanup] Promise for container ${containersToClean[index]} rejected`,
        result.reason
      );
    }
  });
}

```

--------------------------------------------------------------------------------
/test/initialize.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as childProcess from 'node:child_process';
import * as utils from '../src/utils.ts';

vi.mock('node:child_process', () => ({
  execFileSync: vi.fn(() => Buffer.from('')),
}));
vi.mock('../src/utils');
vi.mocked(utils).computeResourceLimits = vi
  .fn()
  .mockReturnValue({ memFlag: '', cpuFlag: '' });

vi.mock('../src/containerUtils', () => ({
  activeSandboxContainers: new Map(),
}));

describe('initialize module', () => {
  beforeEach(() => {
    vi.resetAllMocks();
    vi.spyOn(utils, 'isDockerRunning').mockReturnValue(true);
    vi.spyOn(utils, 'computeResourceLimits').mockReturnValue({
      memFlag: '',
      cpuFlag: '',
    });
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe('setServerRunId', () => {
    it('should set the server run ID correctly', async () => {
      vi.doMock('../src/runUtils', () => ({
        getFilesDir: vi.fn().mockReturnValue(''),
        getMountFlag: vi.fn().mockReturnValue(''),
      }));
      vi.resetModules();
      const mod = await import('../src/tools/initialize.ts');
      const initializeSandbox = mod.default;
      const setServerRunId = mod.setServerRunId;

      // Set a test server run ID
      const testId = 'test-server-run-id';
      setServerRunId(testId);

      // Call initialize function to create a container
      await initializeSandbox({});

      // Verify that execFileSync was called with the correct label containing our test ID
      expect(childProcess.execFileSync).toHaveBeenCalled();
      const execFileSyncCall = vi.mocked(childProcess.execFileSync).mock
        .calls[0][1] as string[];
      // Join the args array to a string for easier matching
      expect(execFileSyncCall.join(' ')).toContain(
        `--label mcp-server-run-id=${testId}`
      );
    });

    it('should use unknown as the default server run ID if not set', async () => {
      vi.doMock('../src/runUtils', () => ({
        getFilesDir: vi.fn().mockReturnValue(''),
        getMountFlag: vi.fn().mockReturnValue(''),
      }));
      vi.resetModules();
      const { default: initializeSandbox } = await import(
        '../src/tools/initialize.ts'
      );

      // Call initialize without setting the server run ID
      await initializeSandbox({});

      // Verify that execFileSync was called with the default "unknown" ID
      expect(childProcess.execFileSync).toHaveBeenCalled();
      const execFileSyncCall = vi.mocked(childProcess.execFileSync).mock
        .calls[0][1] as string[];
      expect(execFileSyncCall.join(' ')).toContain(
        '--label mcp-server-run-id=unknown'
      );
    });
  });

  describe('volume mount behaviour', () => {
    it('does NOT include a -v flag when FILES_DIR is unset', async () => {
      vi.doMock('../src/runUtils', () => ({
        getFilesDir: vi.fn().mockReturnValue(''),
        getMountFlag: vi.fn().mockReturnValue(''),
      }));
      vi.resetModules();
      const { default: initializeSandbox } = await import(
        '../src/tools/initialize.ts'
      );

      await initializeSandbox({});

      const args = vi.mocked(childProcess.execFileSync).mock
        .calls[0][1] as string[];
      expect(args.join(' ')).not.toContain('-v ');
    });

    it('includes the -v flag when getMountFlag returns one', async () => {
      vi.doMock('../src/runUtils', () => ({
        getFilesDir: vi.fn().mockReturnValue('/host/dir'),
        getMountFlag: vi.fn().mockReturnValue('-v /host/dir:/workspace/files'),
      }));
      vi.resetModules();
      const { default: initializeSandbox } = await import(
        '../src/tools/initialize.ts'
      );

      await initializeSandbox({});

      const args = vi.mocked(childProcess.execFileSync).mock
        .calls[0][1] as string[];
      expect(args.join(' ')).toContain('-v /host/dir:/workspace/files');
    });
  });
});

```

--------------------------------------------------------------------------------
/evals/auditClient.ts:
--------------------------------------------------------------------------------

```typescript
import { OpenAI } from 'openai';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

/**
 * Settings for the OpenAIAuditClient
 */
export interface AuditClientSettings {
  apiKey?: string; // OpenAI API key
  model: string; // Model to use for chat completions
}

/**
 * A client wrapper that calls OpenAI chat completions with tool support and returns detailed audit entries,
 * including timing for each tool invocation.
 */
export class OpenAIAuditClient {
  private openai: OpenAI;
  private model: string;
  private client: Client;
  private availableTools: OpenAI.Chat.ChatCompletionTool[] = [];

  constructor(settings: AuditClientSettings) {
    const { apiKey, model } = settings;
    this.openai = new OpenAI({ apiKey });
    this.model = model;
    this.client = new Client({ name: 'node_js_sandbox', version: '1.0.0' });
  }

  /**
   * Initializes the sandbox client by launching the Docker-based MCP server and loading available tools.
   */
  public async initializeClient() {
    const userOutputDir = process.env.FILES_DIR;
    await this.client.connect(
      new StdioClientTransport({
        command: 'docker',
        args: [
          'run',
          '-i',
          '--rm',
          '-v',
          '/var/run/docker.sock:/var/run/docker.sock',
          '-v',
          `${userOutputDir}:/root`,
          '-e',
          `FILES_DIR=${userOutputDir}`,
          'alfonsograziano/node-code-sandbox-mcp',
        ],
      })
    );

    const { tools } = await this.client.listTools();
    this.availableTools = tools.map((tool) => ({
      type: 'function',
      function: {
        parameters: tool.inputSchema,
        ...tool,
      },
    }));
  }

  /**
   * Call OpenAI's chat completions with automatic tool usage.
   * Returns the sequence of messages, responses, and timing details for each tool invocation.
   * @param requestOptions - Includes messages to send
   */
  public async chat(
    requestOptions: Omit<OpenAI.Chat.ChatCompletionCreateParams, 'model'>
  ): Promise<{
    responses: OpenAI.Chat.Completions.ChatCompletion[];
    messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
    toolRuns: Array<{
      toolName: string;
      toolCallId: string;
      params: unknown;
      durationMs: number;
    }>;
  }> {
    const messages = [...requestOptions.messages];
    const responses: OpenAI.Chat.Completions.ChatCompletion[] = [];
    const toolRuns: Array<{
      toolName: string;
      toolCallId: string;
      params: unknown;
      durationMs: number;
    }> = [];
    let interactionCount = 0;
    const maxInteractions = 10;

    while (interactionCount++ < maxInteractions) {
      const response = await this.openai.chat.completions.create({
        model: this.model,
        messages,
        tools: this.availableTools,
        tool_choice: 'auto',
      });
      responses.push(response);
      const message = response.choices[0].message;
      messages.push(message);

      if (message.tool_calls) {
        for (const toolCall of message.tool_calls) {
          const functionName = toolCall.function.name;
          const params = JSON.parse(toolCall.function.arguments || '{}');
          const start = Date.now();
          const result = await this.client.callTool({
            name: functionName,
            arguments: params,
          });
          const durationMs = Date.now() - start;

          // record tool invocation details
          toolRuns.push({
            toolName: functionName,
            toolCallId: toolCall.id,
            params,
            durationMs,
          });

          messages.push({
            role: 'tool',
            tool_call_id: toolCall.id,
            content: JSON.stringify(result),
          });
        }
      } else {
        break;
      }
    }

    return { responses, messages, toolRuns };
  }

  /**
   * Exposes the list of available tools for inspection.
   */
  public getAvailableTools() {
    return this.availableTools;
  }
}

```

--------------------------------------------------------------------------------
/test/searchNpmPackages.test.ts:
--------------------------------------------------------------------------------

```typescript
import {
  describe,
  it,
  expect,
  vi,
  beforeEach,
  afterEach,
  type Mock,
} from 'vitest';
import searchNpmPackages from '../src/tools/searchNpmPackages.ts';
import { NpmRegistry } from 'npm-registry-sdk';
import type { McpContentText } from '../src/types.ts';

vi.mock('npm-registry-sdk');

describe('searchNpmPackages', () => {
  let searchMock: ReturnType<typeof vi.fn>;
  let getPackageMock: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    searchMock = vi.fn();
    getPackageMock = vi.fn();

    // Configure the mocked NpmsIO constructor to return our specific mock methods
    (NpmRegistry as Mock).mockImplementation(() => {
      return {
        search: searchMock,
        getPackage: getPackageMock,
      };
    });
  });

  afterEach(() => {
    vi.resetAllMocks();
  });

  it('should return packages with their details when search is successful', async () => {
    const mockSearchResults = {
      total: 2,
      objects: [
        {
          package: { name: 'package1' },
          score: { detail: { popularity: 1 } },
        },
        {
          package: { name: 'package2' },
          score: { detail: { popularity: 0.5 } },
        },
      ],
    };

    const mockPackageInfos = [
      {
        name: 'package1',
        description: 'Test package 1',
        readme: 'Package 1 readme content',
      },
      {
        name: 'package2',
        description: 'Test package 2',
        readme: 'Package 2 readme content',
      },
    ];

    searchMock.mockResolvedValue(mockSearchResults);
    getPackageMock.mockImplementation((pkg) =>
      Promise.resolve(mockPackageInfos.find((p) => p.name === pkg))
    );

    const result = await searchNpmPackages({
      searchTerm: 'test-package',
    });

    expect(searchMock).toHaveBeenCalledWith('test-package', {
      qualifiers: undefined,
    });
    expect(getPackageMock).toHaveBeenCalledTimes(2);
    expect(JSON.parse((result.content[0] as McpContentText).text)).toEqual([
      {
        name: 'package1',
        description: 'Test package 1',
        readmeSnippet: 'Package 1 readme content',
      },
      {
        name: 'package2',
        description: 'Test package 2',
        readmeSnippet: 'Package 2 readme content',
      },
    ]);
  });

  it('should return "No packages found" when search returns no results', async () => {
    searchMock.mockResolvedValue({ total: 0, objects: [] });

    const result = await searchNpmPackages({
      searchTerm: 'nonexistent-package',
    });

    expect(searchMock).toHaveBeenCalledWith('nonexistent-package', {
      qualifiers: undefined,
    });
    expect(getPackageMock).not.toHaveBeenCalled();
    expect((result.content[0] as McpContentText).text).toBe(
      'No packages found.'
    );
  });

  it('should apply search qualifiers when provided', async () => {
    const mockSearchResults = {
      total: 1,
      objects: [
        {
          package: { name: 'qualified-package' },
          score: { detail: { popularity: 1 } },
        },
      ],
    };

    const mockPackageInfo = {
      name: 'qualified-package',
      description: 'Qualified package',
      readme: 'Qualified package readme',
    };

    searchMock.mockResolvedValue(mockSearchResults);
    getPackageMock.mockResolvedValue(mockPackageInfo);

    const qualifiers = {
      author: 'test-author',
      keywords: 'test',
    };

    const result = await searchNpmPackages({
      searchTerm: 'test-package',
      qualifiers,
    });

    expect(searchMock).toHaveBeenCalledWith('test-package', { qualifiers });
    expect(getPackageMock).toHaveBeenCalledWith('qualified-package');
    expect(JSON.parse((result.content[0] as McpContentText).text)).toEqual([
      {
        name: 'qualified-package',
        description: 'Qualified package',
        readmeSnippet: 'Qualified package readme',
      },
    ]);
  });

  it('should handle packages with missing description or readme', async () => {
    const mockSearchResults = {
      total: 1,
      objects: [
        {
          package: { name: 'incomplete-package' },
          score: { detail: { popularity: 1 } },
        },
      ],
    };

    const mockPackageInfo = {
      name: 'incomplete-package',
      description: undefined,
      readme: undefined,
    };

    searchMock.mockResolvedValue(mockSearchResults);
    getPackageMock.mockResolvedValue(mockPackageInfo);

    const result = await searchNpmPackages({
      searchTerm: 'incomplete-package',
    });

    expect(JSON.parse((result.content[0] as McpContentText).text)).toEqual([
      {
        name: 'incomplete-package',
        description: 'No description available.',
        readmeSnippet: 'README not available.',
      },
    ]);
  });

  it('should handle search errors gracefully', async () => {
    const error = new Error('Search failed');
    searchMock.mockRejectedValue(error);

    const result = await searchNpmPackages({
      searchTerm: 'test-package',
    });

    expect(result).toEqual({
      content: [
        {
          text: 'Failed to search npm packages for "test-package". Error: Search failed',
          type: 'text',
        },
      ],
      isError: true,
    });
    expect(getPackageMock).not.toHaveBeenCalled();
  });
});

```

--------------------------------------------------------------------------------
/src/tools/runJsEphemeral.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { execFileSync } from 'child_process';
import tmp from 'tmp';
import { randomUUID } from 'crypto';
import { type McpResponse, textContent, type McpContent } from '../types.ts';
import {
  DEFAULT_NODE_IMAGE,
  DOCKER_NOT_RUNNING_ERROR,
  generateSuggestedImages,
  isDockerRunning,
  preprocessDependencies,
  computeResourceLimits,
} from '../utils.ts';
import { prepareWorkspace, getMountFlag } from '../runUtils.ts';
import {
  changesToMcpContent,
  detectChanges,
  getSnapshot,
  getMountPointDir,
} from '../snapshotUtils.ts';
import {
  getContentFromError,
  safeExecNodeInContainer,
} from '../dockerUtils.ts';
import { lintAndRefactorCode } from '../linterUtils.ts';

const NodeDependency = z.object({
  name: z.string().describe('npm package name, e.g. lodash'),
  version: z.string().describe('npm package version range, e.g. ^4.17.21'),
});

export const argSchema = {
  image: z
    .string()
    .optional()
    .default(DEFAULT_NODE_IMAGE)
    .describe(
      'Docker image to use for ephemeral execution. e.g. ' +
        generateSuggestedImages()
    ),
  // We use an array of { name, version } items instead of a record
  // because the OpenAI function-calling schema doesn’t reliably support arbitrary
  // object keys. An explicit array ensures each dependency has a clear, uniform
  // structure the model can populate.
  // Schema for a single dependency item
  dependencies: z
    .array(NodeDependency)
    .default([])
    .describe(
      'A list of npm dependencies to install before running the code. ' +
        'Each item must have a `name` (package) and `version` (range). ' +
        'If none, returns an empty array.'
    ),
  code: z
    .string()
    .describe('JavaScript code to run inside the ephemeral container.'),
};

type NodeDependenciesArray = Array<{ name: string; version: string }>;

export default async function runJsEphemeral({
  image = DEFAULT_NODE_IMAGE,
  code,
  dependencies = [],
}: {
  image?: string;
  code: string;
  dependencies?: NodeDependenciesArray;
}): Promise<McpResponse> {
  if (!isDockerRunning()) {
    return { content: [textContent(DOCKER_NOT_RUNNING_ERROR)] };
  }

  // Lint and refactor the code first.
  const { fixedCode, errorReport } = await lintAndRefactorCode(code);

  const telemetry: Record<string, unknown> = {};
  const dependenciesRecord = preprocessDependencies({ dependencies, image });
  const containerId = `js-ephemeral-${randomUUID()}`;
  const tmpDir = tmp.dirSync({ unsafeCleanup: true });
  const { memFlag, cpuFlag } = computeResourceLimits(image);
  const mountFlag = getMountFlag();

  try {
    // Start an ephemeral container
    execFileSync('docker', [
      'run',
      '-d',
      '--network',
      'host',
      ...memFlag.split(' ').filter(Boolean),
      ...cpuFlag.split(' ').filter(Boolean),
      '--workdir',
      '/workspace',
      ...mountFlag.split(' ').filter(Boolean),
      '--name',
      containerId,
      image,
      'tail',
      '-f',
      '/dev/null',
    ]);

    // Prepare workspace locally
    const localWorkspace = await prepareWorkspace({
      code: fixedCode,
      dependenciesRecord,
    });
    execFileSync('docker', [
      'cp',
      `${localWorkspace.name}/.`,
      `${containerId}:/workspace`,
    ]);

    // Generate snapshot of the workspace
    const snapshotStartTime = Date.now();
    const snapshot = await getSnapshot(getMountPointDir());

    // Run install and script inside container
    const installCmd =
      'npm install --omit=dev --prefer-offline --no-audit --loglevel=error';

    if (dependencies.length > 0) {
      const installStart = Date.now();
      const installOutput = execFileSync(
        'docker',
        ['exec', containerId, '/bin/sh', '-c', installCmd],
        { encoding: 'utf8' }
      );
      telemetry.installTimeMs = Date.now() - installStart;
      telemetry.installOutput = installOutput;
    } else {
      telemetry.installTimeMs = 0;
      telemetry.installOutput = 'Skipped npm install (no dependencies)';
    }

    const { output, error, duration } = safeExecNodeInContainer({
      containerId,
    });
    telemetry.runTimeMs = duration;
    if (error) {
      const errorResponse = getContentFromError(error, telemetry);
      if (errorReport) {
        errorResponse.content.unshift(
          textContent(
            `Linting issues found (some may have been auto-fixed):\n${errorReport}`
          )
        );
      }
      return errorResponse;
    }

    // Detect the file changed during the execution of the tool in the mounted workspace
    // and report the changes to the user
    const changes = await detectChanges(
      snapshot,
      getMountPointDir(),
      snapshotStartTime
    );

    const extractedContents = await changesToMcpContent(changes);

    const responseContent: McpContent[] = [];
    if (errorReport) {
      responseContent.push(
        textContent(
          `Linting issues found (some may have been auto-fixed):\n${errorReport}`
        )
      );
    }

    return {
      content: [
        ...(responseContent.length ? responseContent : []),
        textContent(`Node.js process output:\n${output}`),
        ...extractedContents,
        textContent(`Telemetry:\n${JSON.stringify(telemetry, null, 2)}`),
      ],
    };
  } finally {
    execFileSync('docker', ['rm', '-f', containerId]);
    tmpDir.removeCallback();
  }
}

```

--------------------------------------------------------------------------------
/test/runJsListenOnPort.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as tmp from 'tmp';

import runJs from '../src/tools/runJs.ts';
import initializeSandbox from '../src/tools/initialize.ts';
import stopSandbox from '../src/tools/stop.ts';

let tmpDir: tmp.DirResult;

describe('runJs with listenOnPort using Node.js http module', () => {
  beforeEach(() => {
    tmpDir = tmp.dirSync({ unsafeCleanup: true });
    process.env.FILES_DIR = tmpDir.name;
  });

  afterEach(() => {
    tmpDir.removeCallback();
    delete process.env.FILES_DIR;
  });

  it('should start a basic HTTP server in the container and expose it on the given port', async () => {
    const port = 20000 + Math.floor(Math.random() * 10000);
    const start = await initializeSandbox({ port });
    const content = start.content[0];
    if (content.type !== 'text') throw new Error('Unexpected content type');
    const containerId = content.text;

    try {
      // RunJS returns a promise that resolves when the server is started and listening
      // on the specified port. The server will run in the background.
      const result = await runJs({
        container_id: containerId,
        code: `
          import http from 'http';

          const server = http.createServer((req, res) => {
            res.writeHead(200, { 'Content-Type': 'text/plain' });
            res.end('ok');
          });

          server.listen(${port}, '0.0.0.0', () => {
            console.log('Server started');
          });
        `,
        dependencies: [],
        listenOnPort: port,
      });

      expect(result).toBeDefined();
      expect(result.content[0].type).toBe('text');

      if (result.content[0].type === 'text') {
        expect(result.content[0].text).toContain(
          'Server started in background'
        );
      }

      const res = await fetch(`http://localhost:${port}`);
      const body = await res.text();
      expect(body).toBe('ok');
    } finally {
      await stopSandbox({ container_id: containerId });
    }
  }, 10_000);

  it('should start an Express server and return book data from endpoints', async () => {
    const port = 20000 + Math.floor(Math.random() * 10000);
    const start = await initializeSandbox({ port });
    const content = start.content[0];
    if (content.type !== 'text') throw new Error('Unexpected content type');
    const containerId = content.text;

    try {
      const result = await runJs({
        container_id: containerId,
        code: `
          import express from 'express';
          const app = express();
          const port = ${port};

          const books = [
              {
                  title: 'The Great Gatsby',
                  author: 'F. Scott Fitzgerald',
                  isbn: '9780743273565',
                  publishedYear: 1925,
                  genres: ['Fiction', 'Classic'],
                  available: true
              },
              {
                  title: '1984',
                  author: 'George Orwell',
                  isbn: '9780451524935',
                  publishedYear: 1949,
                  genres: ['Fiction', 'Dystopian'],
                  available: true
              },
              {
                  title: 'To Kill a Mockingbird',
                  author: 'Harper Lee',
                  isbn: '9780061120084',
                  publishedYear: 1960,
                  genres: ['Fiction', 'Classic'],
                  available: false
              },
              {
                  title: 'The Catcher in the Rye',
                  author: 'J.D. Salinger',
                  isbn: '9780316769488',
                  publishedYear: 1951,
                  genres: ['Fiction', 'Classic'],
                  available: true
              },
              {
                  title: 'The Hobbit',
                  author: 'J.R.R. Tolkien',
                  isbn: '9780547928227',
                  publishedYear: 1937,
                  genres: ['Fantasy', 'Adventure'],
                  available: true
              }
          ];

          app.get('/books', (req, res) => {
              res.json(books);
          });

          app.get('/books/:isbn', (req, res) => {
              const book = books.find(b => b.isbn === req.params.isbn);
              if (book) {
                  res.json(book);
              } else {
                  res.sendStatus(404);
              }
          });

          app.listen(port, '0.0.0.0', () => {
              console.log('Server started');
          });
        `,
        dependencies: [
          {
            name: 'express',
            version: '4.18.2',
          },
        ],
        listenOnPort: port,
      });

      expect(result).toBeDefined();
      expect(result.content[0].type).toBe('text');
      // expect(result.content[0].text).toContain("Server started in background");

      const resAll = await fetch(`http://localhost:${port}/books`);
      expect(resAll.status).toBe(200);
      const books = await resAll.json();
      expect(Array.isArray(books)).toBe(true);
      expect(books.length).toBeGreaterThanOrEqual(5);

      const resSingle = await fetch(
        `http://localhost:${port}/books/9780451524935`
      );
      expect(resSingle.status).toBe(200);
      const book = await resSingle.json();
      expect(book.title).toBe('1984');

      const resNotFound = await fetch(
        `http://localhost:${port}/books/nonexistent`
      );
      expect(resNotFound.status).toBe(404);
    } finally {
      await stopSandbox({ container_id: containerId });
    }
  }, 30_000);
});

```

--------------------------------------------------------------------------------
/website/src/Components/GettingStarted.tsx:
--------------------------------------------------------------------------------

```typescript
import React, { useState } from 'react';
import clsx from 'clsx';

const GettingStarted: React.FC = () => {
  const [selectedClient, setSelectedClient] = useState<'claude' | 'vscode'>(
    'claude'
  );
  const [variant, setVariant] = useState<'docker' | 'npx'>('docker');

  const codeSnippets = {
    claude: {
      docker: {
        mcpServers: {
          'js-sandbox': {
            command: 'docker',
            args: [
              'run',
              '-i',
              '--rm',
              '-v',
              '/var/run/docker.sock:/var/run/docker.sock',
              '-v',
              '$HOME/Desktop/sandbox-output:/root',
              '-e',
              'FILES_DIR=$HOME/Desktop/sandbox-output',
              'mcp/node-code-sandbox',
            ],
          },
        },
      },
      npx: {
        mcpServers: {
          'node-code-sandbox-mcp': {
            type: 'stdio',
            command: 'npx',
            args: ['-y', 'node-code-sandbox-mcp'],
            env: {
              FILES_DIR: '/Users/your_user/Desktop/node-sandbox',
            },
          },
        },
      },
    },
    vscode: {
      docker: {
        mcp: {
          servers: {
            'js-sandbox': {
              command: 'docker',
              args: [
                'run',
                '-i',
                '--rm',
                '-v',
                '/var/run/docker.sock:/var/run/docker.sock',
                '-v',
                '$HOME/Desktop/sandbox-output:/root',
                '-e',
                'FILES_DIR=$HOME/Desktop/sandbox-output',
                'mcp/node-code-sandbox',
              ],
            },
          },
        },
      },
      npx: {
        mcp: {
          servers: {
            'js-sandbox': {
              type: 'stdio',
              command: 'npx',
              args: ['-y', 'node-code-sandbox-mcp'],
              env: {
                FILES_DIR: '/Users/your_user/Desktop/node-sandbox',
              },
            },
          },
        },
      },
    },
  };

  const tabs = [
    { key: 'claude', label: 'Claude Desktop' },
    { key: 'vscode', label: 'VS Code' },
  ];

  return (
    <section id="getting-started" className="max-w-6xl mx-auto py-16">
      <h2 className="text-3xl font-bold text-center mb-4">Getting Started</h2>
      <p className="text-center text-gray-700 mb-8">
        To get started, first of all you need to import the server in your MCP
        client.
      </p>

      <div className="bg-white rounded-xl shadow p-6 space-y-6">
        {/* Docker Hub Info */}
        <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
          <h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
            <span className="text-blue-700">📦</span> Docker Hub Available
          </h3>
          <p className="text-sm text-gray-700">
            The MCP server is now available on Docker Hub. You can pull it
            directly using:
            <code className="block bg-gray-100 p-2 rounded mt-2 text-xs">
              docker pull mcp/node-code-sandbox
            </code>
            <a
              href="https://hub.docker.com/r/mcp/node-code-sandbox"
              target="_blank"
              rel="noopener noreferrer"
              className="text-blue-600 hover:underline text-sm inline-block mt-2"
            >
              View on Docker Hub →
            </a>
          </p>
        </div>

        {/* Tabs + Switch */}
        <div className="flex flex-wrap justify-between items-center gap-4">
          <div className="flex gap-2">
            {tabs.map((tab) => (
              <button
                key={tab.key}
                onClick={() =>
                  setSelectedClient(tab.key as typeof selectedClient)
                }
                className={clsx(
                  'px-4 py-2 rounded-full text-sm font-medium border transition-all',
                  selectedClient === tab.key
                    ? 'bg-green-600 text-white'
                    : 'bg-gray-100 text-gray-800 hover:bg-gray-200'
                )}
              >
                {tab.label}
              </button>
            ))}
          </div>
          <div className="flex items-center gap-2 text-sm">
            <span className="text-gray-600">Variant:</span>
            <button
              onClick={() => setVariant('docker')}
              className={clsx(
                'px-3 py-1 rounded-full border text-sm font-medium',
                variant === 'docker'
                  ? 'bg-green-600 text-white'
                  : 'bg-white text-gray-800 hover:bg-gray-100'
              )}
            >
              Docker
            </button>
            <button
              onClick={() => setVariant('npx')}
              className={clsx(
                'px-3 py-1 rounded-full border text-sm font-medium',
                variant === 'npx'
                  ? 'bg-green-600 text-white'
                  : 'bg-white text-gray-800 hover:bg-gray-100'
              )}
            >
              NPX
            </button>
          </div>
        </div>

        {/* Config Code */}
        <pre className="bg-gray-100 p-4 rounded-lg text-xs overflow-x-auto whitespace-pre">
          {JSON.stringify(codeSnippets[selectedClient][variant], null, 2)}
        </pre>

        <p className="text-sm text-gray-700">
          Copy this snippet into your configuration file. Ensure Docker is
          running and your volumes are mounted correctly.
        </p>
      </div>
    </section>
  );
};

export default GettingStarted;

```

--------------------------------------------------------------------------------
/NODE_GUIDELINES.md:
--------------------------------------------------------------------------------

```markdown
You are an expert Node.js developer. Your purpose is to write modern, efficient, and secure JavaScript code for the Node.js runtime.

You must **strictly adhere** to the following guidelines in all the code you generate. Failure to follow these rules will result in incorrect and unsafe code.

---

### **1. Core Principles: Modern Syntax and APIs**

#### **1.1. Embrace ES Modules (ESM)**

- **Default to ESM:** Write all code using ES Modules (`import`/`export` syntax). This is the modern standard.
- **No CommonJS:** **DO NOT** use CommonJS (`require()`/`module.exports`).
- **Top-Level Await:** Use top-level `await` for asynchronous initialization in your main application file.

#### **1.2. Use Native APIs First**

- **HTTP Requests:** Use the global `fetch` API for all HTTP requests. **DO NOT** use `node-fetch`, `axios`, or the deprecated `request` package.
- **Testing:** Use the `node:test` module and `node:assert` for writing tests. **DO NOT** use Jest, Mocha, or Chai unless specifically requested.
- **URL Parsing:** Use the global `URL` constructor (`new URL(...)`). **DO NOT** use the legacy `url.parse()` API.

#### **1.3. Master Asynchronous Patterns**

- **`async/await` is Mandatory:** Use `async/await` for all asynchronous operations. It is non-negotiable for clarity and error handling.
- **No Callback Hell:** **NEVER** write nested callbacks (the "pyramid of doom"). If you must interface with a callback-based legacy API, wrap it with `util.promisify`.
- **Avoid Raw Promises Where Possible:** Do not chain `.then()` and `.catch()` when `async/await` provides a cleaner, linear control flow.

---

### **2. Performance and Concurrency**

#### **2.1. Never Block the Event Loop**

- **No Synchronous I/O:** The event loop is for non-blocking I/O. **NEVER** use synchronous functions like `fs.readFileSync()`, `crypto.randomBytesSync()`, or `child_process.execSync()` in a server or main thread context. Use their asynchronous promise-based counterparts (e.g., `fs.readFile()` from `fs/promises`).
- **Offload CPU-Intensive Work:** For heavy computations (e.g., complex calculations, image processing, synchronous bcrypt hashing), use `node:worker_threads` to avoid blocking the main thread.

#### **2.2. Implement Streaming and Backpressure**

- **Use Streams for Large Data:** For handling large files or network payloads, always use Node.js streams (`fs.createReadStream`, `fs.createWriteStream`). This keeps memory usage low and constant.
- **Respect Backpressure:** Use `stream.pipeline` from the `stream/promises` module to correctly chain streams and handle backpressure automatically. This prevents memory overload when a readable stream is faster than a writable one.

---

### **3. Error Handling and Resilience**

#### **3.1. Handle Errors Robustly**

- **Comprehensive `try...catch`:** Wrap all `await` calls in `try...catch` blocks to handle potential runtime errors gracefully.
- **No Unhandled Rejections:** Every promise chain must end with a `catch` or be handled by a `try...catch` block. Unhandled promise rejections will crash the application.
- **Centralized Error Handling:** In server applications (like Express), use centralized error-handling middleware to catch and process all errors consistently.

#### **3.2. Build Resilient Services**

- **Set Timeouts:** When making outbound network requests (e.g., with `fetch`), always use an `AbortSignal` to enforce a timeout. Never allow a request to hang indefinitely.
- **Implement Graceful Shutdown:** Your application must handle `SIGINT` and `SIGTERM` signals. On shutdown, you must:
  1.  Stop accepting new requests.
  2.  Finish processing in-flight requests.
  3.  Close database connections and other resources.
  4.  Exit the process with `process.exit(0)`.

---

### **4. Security First**

#### **4.1. Avoid Common Vulnerabilities**

- **Validate All Inputs:** Never trust user input. Use a schema validation library like `zod` or `joi` to validate request bodies, query parameters, and headers.
- **Prevent Injection:** Use parameterized queries or ORMs to prevent SQL injection. Never construct database queries with string concatenation.
- **Safe Child Processes:** **DO NOT** use `child_process.exec` with unescaped user input, as this can lead to command injection. Use `child_process.execFile` with an array of arguments instead.
- **Secure Dependencies:** Always use a lockfile (`package-lock.json`). Regularly audit dependencies with `npm audit`.

#### **4.2. Secure Coding Practices**

- **No Unsafe Execution:** **NEVER** use `eval()` or `new Function('...')` with dynamic strings. It is a massive security risk.
- **Handle Paths Safely:** Use `path.join()` or `path.resolve()` to construct file system paths. Do not use string concatenation, which is vulnerable to path traversal attacks.
- **Manage Secrets:** **NEVER** hardcode secrets (API keys, passwords) in the source code. Load them from environment variables (e.g., using `dotenv` in development).

---

### **5. Code Style and Structure**

#### **5.1. Modern JavaScript Syntax**

- **`const` Over `let`:** Use `const` by default. Only use `let` if a variable must be reassigned. **NEVER** use `var`.
- **Strict Equality:** Always use strict equality (`===` and `!==`). **DO NOT** use loose equality (`==` and `!=`).
- **No Prototype Extension:** **NEVER** modify the prototypes of built-in objects like `Object.prototype` or `Array.prototype`.

#### **5.2. Maintain Clean Code**

- **Avoid Global State:** Do not store request-specific or user-specific data in global variables. This leads to memory leaks and security issues. Use a request context or dependency injection.
- **Pure Functions:** Prefer pure functions that do not have side effects. Avoid modifying function arguments directly.
- **Prevent Circular Dependencies:** Structure your files and modules to avoid circular `import` statements, which can cause runtime errors.

```

--------------------------------------------------------------------------------
/src/tools/runJs.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { execFileSync } from 'node:child_process';
import { type McpResponse, textContent, type McpContent } from '../types.ts';
import { prepareWorkspace } from '../runUtils.ts';
import {
  DOCKER_NOT_RUNNING_ERROR,
  isDockerRunning,
  waitForPortHttp,
  sanitizeContainerId,
} from '../utils.ts';
import {
  changesToMcpContent,
  detectChanges,
  getMountPointDir,
  getSnapshot,
} from '../snapshotUtils.ts';
import {
  getContentFromError,
  safeExecNodeInContainer,
} from '../dockerUtils.ts';
import { lintAndRefactorCode } from '../linterUtils.ts';

const NodeDependency = z.object({
  name: z.string().describe('npm package name, e.g. lodash'),
  version: z.string().describe('npm package version range, e.g. ^4.17.21'),
});

export const argSchema = {
  container_id: z.string().describe('Docker container identifier'),
  // We use an array of { name, version } items instead of a record
  // because the OpenAI function-calling schema doesn’t reliably support arbitrary
  // object keys. An explicit array ensures each dependency has a clear, uniform
  // structure the model can populate.
  // Schema for a single dependency item
  dependencies: z
    .array(NodeDependency)
    .default([])
    .describe(
      'A list of npm dependencies to install before running the code. ' +
        'Each item must have a `name` (package) and `version` (range). ' +
        'If none, returns an empty array.'
    ),
  code: z.string().describe('JavaScript code to run inside the container.'),
  listenOnPort: z
    .number()
    .optional()
    .describe(
      'If set, leaves the process running and exposes this port to the host.'
    ),
};

type DependenciesArray = Array<{ name: string; version: string }>;

export default async function runJs({
  container_id,
  code,
  dependencies = [],
  listenOnPort,
}: {
  container_id: string;
  code: string;
  dependencies?: DependenciesArray;
  listenOnPort?: number;
}): Promise<McpResponse> {
  const validId = sanitizeContainerId(container_id);
  if (!validId) {
    return { content: [textContent('Invalid container ID')] };
  }

  if (!isDockerRunning()) {
    return { content: [textContent(DOCKER_NOT_RUNNING_ERROR)] };
  }

  // Lint and refactor the code first.
  const { fixedCode, errorReport } = await lintAndRefactorCode(code);

  const telemetry: Record<string, unknown> = {};
  const dependenciesRecord: Record<string, string> = Object.fromEntries(
    dependencies.map(({ name, version }) => [name, version])
  );

  // Create workspace in container
  const localWorkspace = await prepareWorkspace({
    code: fixedCode,
    dependenciesRecord,
  });
  execFileSync('docker', [
    'cp',
    `${localWorkspace.name}/.`,
    `${validId}:/workspace`,
  ]);

  let rawOutput: string = '';

  // Generate snapshot of the workspace
  const snapshotStartTime = Date.now();
  const snapshot = await getSnapshot(getMountPointDir());

  if (listenOnPort) {
    if (dependencies.length > 0) {
      const installStart = Date.now();
      const installOutput = execFileSync(
        'docker',
        [
          'exec',
          validId,
          '/bin/sh',
          '-c',
          'npm install --omit=dev --prefer-offline --no-audit --loglevel=error',
        ],
        { encoding: 'utf8' }
      );
      telemetry.installTimeMs = Date.now() - installStart;
      telemetry.installOutput = installOutput;
    } else {
      telemetry.installTimeMs = 0;
      telemetry.installOutput = 'Skipped npm install (no dependencies)';
    }

    const { error, duration } = safeExecNodeInContainer({
      containerId: validId,
      command: `nohup node index.js > output.log 2>&1 &`,
    });
    telemetry.runTimeMs = duration;
    if (error) {
      const errorResponse = getContentFromError(error, telemetry);
      if (errorReport) {
        errorResponse.content.unshift(
          textContent(
            `Linting issues found (some may have been auto-fixed):\n${errorReport}`
          )
        );
      }
      return errorResponse;
    }

    await waitForPortHttp(listenOnPort);
    rawOutput = `Server started in background; logs at /output.log`;
  } else {
    if (dependencies.length > 0) {
      const installStart = Date.now();
      const fullCmd =
        'npm install --omit=dev --prefer-offline --no-audit --loglevel=error';
      const installOutput = execFileSync(
        'docker',
        ['exec', validId, '/bin/sh', '-c', fullCmd],
        { encoding: 'utf8' }
      );
      telemetry.installTimeMs = Date.now() - installStart;
      telemetry.installOutput = installOutput;
    } else {
      telemetry.installTimeMs = 0;
      telemetry.installOutput = 'Skipped npm install (no dependencies)';
    }

    const { output, error, duration } = safeExecNodeInContainer({
      containerId: validId,
    });

    if (output) rawOutput = output;
    telemetry.runTimeMs = duration;
    if (error) {
      const errorResponse = getContentFromError(error, telemetry);
      if (errorReport) {
        errorResponse.content.unshift(
          textContent(
            `Linting issues found (some may have been auto-fixed):\n${errorReport}`
          )
        );
      }
      return errorResponse;
    }
  }

  // Detect the file changed during the execution of the tool in the mounted workspace
  // and report the changes to the user
  const changes = await detectChanges(
    snapshot,
    getMountPointDir(),
    snapshotStartTime
  );

  const extractedContents = await changesToMcpContent(changes);
  localWorkspace.removeCallback();

  const responseContent: McpContent[] = [];
  if (errorReport) {
    responseContent.push(
      textContent(
        `Linting issues found (some may have been auto-fixed):\n${errorReport}`
      )
    );
  }

  return {
    content: [
      ...(responseContent.length ? responseContent : []),
      textContent(`Node.js process output:\n${rawOutput}`),
      ...extractedContents,
      textContent(`Telemetry:\n${JSON.stringify(telemetry, null, 2)}`),
    ],
  };
}

```

--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { existsSync, readFileSync } from 'fs';
import { execFileSync } from 'node:child_process';
import { getConfig } from './config.ts';

export function isRunningInDocker() {
  // 1. The "/.dockerenv" sentinel file
  if (existsSync('/.dockerenv')) return true;

  // 2. cgroup data often embeds "docker" or "kubepods"
  try {
    if (existsSync('/proc/1/cgroup')) {
      const cgroup = readFileSync('/proc/1/cgroup', 'utf8');
      if (cgroup.includes('docker') || cgroup.includes('kubepods')) {
        return true;
      }
    }
  } catch {
    // unreadable or missing → assume "not"
  }

  // 3. Check for environment variables commonly set in Docker
  if (process.env.DOCKER_CONTAINER || process.env.DOCKER_ENV) {
    return true;
  }

  // On macOS or Windows for tests, just return false
  return false;
}

export function preprocessDependencies({
  dependencies,
  image,
}: {
  dependencies: Array<{ name: string; version: string }>;
  image?: string;
}): Record<string, string> {
  const dependenciesRecord: Record<string, string> = Object.fromEntries(
    dependencies.map(({ name, version }) => [name, version])
  );

  // This image has a pre-cached version of chartjs-node-canvas,
  // but we still need to explicitly declare it in package.json
  if (image?.includes('alfonsograziano/node-chartjs-canvas')) {
    dependenciesRecord['chartjs-node-canvas'] = '4.0.0';
    dependenciesRecord['@mermaid-js/mermaid-cli'] = '^11.4.2';
  }

  return dependenciesRecord;
}

export const DEFAULT_NODE_IMAGE = 'node:lts-slim';

export const PLAYWRIGHT_IMAGE = 'mcr.microsoft.com/playwright:v1.55.0-noble';

export const suggestedImages = {
  'node:lts-slim': {
    description: 'Node.js LTS version, slim variant.',
    reason: 'Lightweight and fast for JavaScript execution tasks.',
  },
  [PLAYWRIGHT_IMAGE]: {
    description: 'Playwright image for browser automation.',
    reason: 'Preconfigured for running Playwright scripts.',
  },
  'alfonsograziano/node-chartjs-canvas:latest': {
    description:
      'Chart.js image for chart generation and mermaid charts generation.',
    reason: `'Preconfigured for generating charts with chartjs-node-canvas and Mermaid. Minimal Mermaid example:
    import fs from "fs";
    import { run } from "@mermaid-js/mermaid-cli";
    fs.writeFileSync("./files/diagram.mmd", "graph LR; A-->B;", "utf8");
    await run("./files/diagram.mmd", "./files/diagram.svg");`,
  },
};

export const generateSuggestedImages = () => {
  return Object.entries(suggestedImages)
    .map(([image, { description, reason }]) => {
      return `- **${image}**: ${description} (${reason})`;
    })
    .join('\n');
};

export async function waitForPortHttp(
  port: number,
  timeoutMs = 10_000,
  intervalMs = 250
): Promise<void> {
  const start = Date.now();

  while (Date.now() - start < timeoutMs) {
    try {
      const res = await fetch(`http://localhost:${port}`);
      if (res.ok || res.status === 404) return; // server is up
    } catch {
      // server not ready
    }

    await new Promise((r) => setTimeout(r, intervalMs));
  }

  throw new Error(
    `Timeout: Server did not respond on http://localhost:${port} within ${timeoutMs}ms`
  );
}

export function isDockerRunning() {
  try {
    execFileSync('docker', ['info']);
    return true;
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
  } catch (e) {
    return false;
  }
}
export const DOCKER_NOT_RUNNING_ERROR =
  'Error: Docker is not running. Please start Docker and try again.';

export interface Limits {
  memory?: string;
  cpus?: string;
}

export const IMAGE_DEFAULTS: Record<string, Limits> = {
  'node:lts-slim': { memory: '512m', cpus: '1' },
  'alfonsograziano/node-chartjs': { memory: '2g', cpus: '2' },
  'mcr.microsoft.com/playwright': { memory: '2g', cpus: '2' },
};

export function computeResourceLimits(image: string) {
  const base = { memFlag: '', cpuFlag: '' };
  if (!image) return base;

  const def =
    Object.entries(IMAGE_DEFAULTS).find(([key]) => image.includes(key))?.[1] ??
    {};

  const memory = getConfig().rawMemoryLimit ?? def.memory;
  const cpus = getConfig().rawCpuLimit ?? def.cpus;

  return {
    ...base,
    memFlag: memory ? `--memory ${memory}` : '',
    cpuFlag: cpus ? `--cpus ${cpus}` : '',
  };
}

/**
 * Sanitizes and validates a Docker container ID or name.
 * Docker container names/IDs must match [a-zA-Z0-9][a-zA-Z0-9_.-]*
 * @param id The container ID or name to validate
 * @returns The sanitized ID if valid, otherwise null
 */
export function sanitizeContainerId(id: string): string | null {
  // Docker container names/IDs: https://docs.docker.com/engine/reference/commandline/run/#container-name
  // Allow alphanumerics, underscores, periods, dashes. Must start with alphanumeric.
  if (typeof id !== 'string') return null;
  if (/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(id)) return id;
  return null;
}

/**
 * Sanitizes and validates a Docker image name (optionally with tag).
 * @param image The image name to validate
 * @returns The sanitized image name if valid, otherwise null
 */
export function sanitizeImageName(image: string): string | null {
  // Docker image names: [registry/][user/]repo[:tag]
  // Allow alphanumerics, underscores, periods, dashes, slashes, colons
  if (typeof image !== 'string') return null;
  if (/^[a-zA-Z0-9_.:/-]+$/.test(image)) return image;
  return null;
}

/**
 * Sanitizes a shell command to be run inside a container. This is a basic check;
 * for more advanced needs, consider whitelisting allowed commands.
 * @param cmd The command string
 * @returns The sanitized command if valid, otherwise null
 */
export function sanitizeShellCommand(cmd: string): string | null {
  // For now, just check it's a non-empty string and doesn't contain dangerous metacharacters
  if (typeof cmd !== 'string' || !cmd.trim()) return null;
  // Disallow command substitution (backticks and $()) which are most dangerous
  if (/[`]|\$\([^)]+\)/.test(cmd)) return null;
  // Allow >, <, &, | for redirection and backgrounding, as needed for listenOnPort
  // Still block backticks and $() for command substitution
  return cmd;
}

```

--------------------------------------------------------------------------------
/test/runMCPClient.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { tmpdir } from 'os';
import { mkdtempSync, rmSync, readFileSync } from 'fs';
import path from 'path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import dotenv from 'dotenv';
import { type McpResponse } from '../src/types.ts';
import fs from 'fs';
import { execSync } from 'child_process';
import { normalizeMountPath } from './utils.ts';

dotenv.config();
const __dirname = path.dirname(new URL(import.meta.url).pathname);

describe('Node.js Code Sandbox MCP Tests', () => {
  let hostWorkspaceDir: string;
  let containerWorkspaceDir: string;
  let client: Client;

  beforeAll(async () => {
    hostWorkspaceDir = mkdtempSync(path.join(tmpdir(), 'ws-'));
    containerWorkspaceDir = normalizeMountPath(hostWorkspaceDir);

    // Build the latest version of the Docker image
    execSync('docker build -t alfonsograziano/node-code-sandbox-mcp .', {
      stdio: 'inherit',
    });

    client = new Client({ name: 'node_js_sandbox_test', version: '1.0.0' });

    await client.connect(
      new StdioClientTransport({
        command: 'docker',
        args: [
          'run',
          '-i',
          '--rm',
          '-v',
          '/var/run/docker.sock:/var/run/docker.sock',
          '-v',
          `${hostWorkspaceDir}:/root`,
          '-e',
          `FILES_DIR=${containerWorkspaceDir}`,
          'alfonsograziano/node-code-sandbox-mcp',
        ],
      })
    );
  }, 200_000);

  afterAll(() => {
    rmSync(hostWorkspaceDir, { recursive: true, force: true });
    // Optional: Stop any potentially lingering client connections
    client?.close();
  });

  it('should run a console.log', async () => {
    const code = `console.log("Hello from workspace!");`;

    const result = (await client.callTool({
      name: 'run_js_ephemeral',
      arguments: { code, dependencies: [] },
    })) as { content: Array<{ type: string; text: string }> };

    expect(result).toBeDefined();
    expect(result.content).toBeInstanceOf(Array);
    expect(result.content[0]).toMatchObject({
      type: 'text',
    });

    const outputText = result.content[0].text;
    expect(outputText).toContain('Hello from workspace!');
    expect(outputText).toContain('Node.js process output');
  });

  describe('runJsEphemeral via MCP client (files)', () => {
    it('should read and write files using the host-mounted /files', async () => {
      const inputFileName = 'text.txt';
      const inputFilePath = path.join(hostWorkspaceDir, inputFileName);
      const inputContent = 'This is a file from the host.';
      fs.writeFileSync(inputFilePath, inputContent, 'utf-8');

      const outputFileName = 'output-host.txt';
      const outputContent = 'This file was created in the sandbox.';

      const code = `
        import fs from 'fs';
        const input = fs.readFileSync('./files/${inputFileName}', 'utf-8');
        console.log('Input file content:', input);
        fs.writeFileSync('./files/${outputFileName}', input + ' | ${outputContent}');
        console.log('Files processed.');
      `;

      const result = (await client.callTool({
        name: 'run_js_ephemeral',
        arguments: { code, dependencies: [] },
      })) as McpResponse;

      expect(result).toBeDefined();
      expect(result.content).toBeDefined();

      // Process output
      const processOutput = result.content.find(
        (item) =>
          item.type === 'text' &&
          item.text.startsWith('Node.js process output:')
      );
      expect(processOutput).toBeDefined();
      expect((processOutput as { text: string }).text).toContain(
        'Input file content: This is a file from the host.'
      );
      expect((processOutput as { text: string }).text).toContain(
        'Files processed.'
      );

      // File creation message
      const fileChangeInfo = result.content.find(
        (item) =>
          item.type === 'text' && item.text.startsWith('List of changed files:')
      );
      expect(fileChangeInfo).toBeDefined();
      expect((fileChangeInfo as { text: string }).text).toContain(
        '- output-host.txt was created'
      );

      // Resource
      const resource = result.content.find(
        (item) =>
          item.type === 'resource' &&
          'resource' in item &&
          typeof item.resource?.uri === 'string'
      );
      expect(resource).toBeDefined();
      const resourceData = (
        resource as {
          resource: { mimeType: string; uri: string; text: string };
        }
      ).resource;
      expect(resourceData.mimeType).toBe('text/plain');
      expect(resourceData.uri).toContain('output-host.txt');
      expect(resourceData.uri).toContain('file://');
      expect(resourceData.text).toBe('output-host.txt');

      // Telemetry
      const telemetry = result.content.find(
        (item) => item.type === 'text' && item.text.startsWith('Telemetry:')
      );
      expect(telemetry).toBeDefined();
      expect((telemetry as { text: string }).text).toContain('"installTimeMs"');
      expect((telemetry as { text: string }).text).toContain('"runTimeMs"');
    });
  });

  describe('run-node-js-script prompt', () => {
    it('should include NODE_GUIDELINES.md in the prompt response', async () => {
      const expectedGuidelines = readFileSync(
        path.join(__dirname, '../NODE_GUIDELINES.md'),
        'utf-8'
      );

      const userPrompt = "Please create a file named 'test.txt'.";

      // Get the prompt
      const result = await client.getPrompt({
        name: 'run-node-js-script',
        arguments: { prompt: userPrompt },
      });

      expect(result).toBeDefined();
      expect(result.messages).toBeInstanceOf(Array);
      expect(result.messages.length).toBeGreaterThan(0);

      const messageContent = result.messages[0].content;
      expect(messageContent.type).toBe('text');

      const responseText = messageContent.text;

      // Check that the response text includes both the user's prompt and the full guidelines
      expect(responseText).toContain(userPrompt);
      expect(responseText).toContain(expectedGuidelines);
    });
  });
}, 200_000);

```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import {
  McpServer,
  ResourceTemplate,
} from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { randomUUID } from 'crypto';
import initializeSandbox, {
  argSchema as initializeSchema,
  setServerRunId,
} from './tools/initialize.ts';
import execInSandbox, { argSchema as execSchema } from './tools/exec.ts';
import runJs, { argSchema as runJsSchema } from './tools/runJs.ts';
import stopSandbox, { argSchema as stopSchema } from './tools/stop.ts';
import runJsEphemeral, {
  argSchema as ephemeralSchema,
} from './tools/runJsEphemeral.ts';
import mime from 'mime-types';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { z } from 'zod';
import { getConfig } from './config.ts';
import { startScavenger, cleanActiveContainers } from './containerUtils.ts';
import { setServerInstance, logger } from './logger.ts';
import getDependencyTypes, {
  argSchema as getDependencyTypesSchema,
} from './tools/getDependencyTypes.ts';
import searchNpmPackages, {
  SearchNpmPackagesToolSchema,
} from './tools/searchNpmPackages.ts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

import packageJson from '../package.json' with { type: 'json' };

export const serverRunId = randomUUID();
setServerRunId(serverRunId);

const nodeGuidelines = await fs.readFile(
  path.join(__dirname, '..', 'NODE_GUIDELINES.md'),
  'utf-8'
);

// Create the server with logging capability enabled
const server = new McpServer(
  {
    name: packageJson.name,
    version: packageJson.version,
    description: packageJson.description,
  },
  {
    capabilities: {
      logging: {},
      tools: {},
    },
  }
);

// Set the server instance for logging
setServerInstance(server);

// Configure server tools and resources
server.tool(
  'sandbox_initialize',
  'Start a new isolated Docker container running Node.js. Used to set up a sandbox session for multiple commands and scripts.',
  initializeSchema,
  initializeSandbox
);

server.tool(
  'sandbox_exec',
  'Execute one or more shell commands inside a running sandbox container. Requires a sandbox initialized beforehand.',
  execSchema,
  execInSandbox
);

server.tool(
  'run_js',
  `Install npm dependencies and run JavaScript code inside a running sandbox container.
  After running, you must manually stop the sandbox to free resources.
  The code must be valid ESModules (import/export syntax). Best for complex workflows where you want to reuse the environment across multiple executions.
  When reading and writing from the Node.js processes, you always need to read from and write to the "./files" directory to ensure persistence on the mounted volume.`,
  runJsSchema,
  runJs
);

server.tool(
  'sandbox_stop',
  'Terminate and remove a running sandbox container. Should be called after finishing work in a sandbox initialized with sandbox_initialize.',
  stopSchema,
  stopSandbox
);

server.tool(
  'run_js_ephemeral',
  `Run a JavaScript snippet in a temporary disposable container with optional npm dependencies, then automatically clean up. 
  The code must be valid ESModules (import/export syntax). Ideal for simple one-shot executions without maintaining a sandbox or managing cleanup manually.
  When reading and writing from the Node.js processes, you always need to read from and write to the "./files" directory to ensure persistence on the mounted volume.
  This includes images (e.g., PNG, JPEG) and other files (e.g., text, JSON, binaries).

  Example:
  \`\`\`js
  import fs from "fs/promises";
  await fs.writeFile("./files/hello.txt", "Hello world!");
  console.log("Saved ./files/hello.txt");
  \`\`\`
`,
  ephemeralSchema,
  runJsEphemeral
);

server.tool(
  'get_dependency_types',
  `
  Given an array of npm package names (and optional versions), 
  fetch whether each package ships its own TypeScript definitions 
  or has a corresponding @types/… package, and return the raw .d.ts text.
  
  Useful whenwhen you're about to run a Node.js script against an unfamiliar dependency 
  and want to inspect what APIs and types it exposes.
  `,
  getDependencyTypesSchema,
  getDependencyTypes
);

server.tool(
  'search_npm_packages',
  'Search for npm packages by a search term and get their name, description, and a README snippet.',
  SearchNpmPackagesToolSchema.shape,
  searchNpmPackages
);

server.resource(
  'file',
  new ResourceTemplate('file://{+filepath}', { list: undefined }),
  async (uri) => {
    const filepath = new URL(uri).pathname;
    const data = await fs.readFile(filepath);
    const mimeType = mime.lookup(filepath) || 'application/octet-stream';
    return {
      contents: [
        {
          uri: uri.toString(),
          mimeType,
          blob: data.toString('base64'),
        },
      ],
    };
  }
);

server.prompt('run-node-js-script', { prompt: z.string() }, ({ prompt }) => ({
  messages: [
    {
      role: 'user',
      content: {
        type: 'text',
        text:
          `Here is my prompt:\n\n${prompt}\n\n` +
          nodeGuidelines +
          `\n---\n\n` +
          `Please write and run a Node.js script that fulfills my prompt.`,
      },
    },
  ],
}));

const scavengerIntervalHandle = startScavenger(
  getConfig().containerTimeoutMilliseconds,
  getConfig().containerTimeoutSeconds
);

async function gracefulShutdown(signal: string) {
  logger.info(`Received ${signal}. Starting graceful shutdown...`);

  clearInterval(scavengerIntervalHandle);
  logger.info('Stopped container scavenger.');

  await cleanActiveContainers();

  setTimeout(() => {
    logger.info('Exiting.');
    process.exit(0);
  }, 500);
}

process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGUSR2', () => gracefulShutdown('SIGUSR2'));

// Set up the transport
const transport = new StdioServerTransport();

// Connect the server to start receiving and sending messages
logger.info('Initializing server...');
await server.connect(transport);
logger.info('Server started and connected successfully');
logger.info(
  `Container timeout set to: ${getConfig().containerTimeoutSeconds} seconds (${getConfig().containerTimeoutMilliseconds}ms)`
);

```

--------------------------------------------------------------------------------
/test/getDependencyTypes.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import type { Mock } from 'vitest';
import { z } from 'zod';
import getDependencyTypes, {
  argSchema,
} from '../src/tools/getDependencyTypes.ts';
import type { McpContentText } from '../src/types.ts';

// Schema validation tests
describe('argSchema', () => {
  it('should accept valid dependencies array with version', () => {
    const input = { dependencies: [{ name: 'foo', version: '1.2.3' }] };
    const parsed = z.object(argSchema).parse(input);
    expect(parsed).toEqual(input);
  });

  it('should accept valid dependencies array without version', () => {
    const input = { dependencies: [{ name: 'bar' }] };
    const parsed = z.object(argSchema).parse(input);
    expect(parsed).toEqual({
      dependencies: [{ name: 'bar', version: undefined }],
    });
  });

  it('should reject when dependencies is missing', () => {
    expect(() => z.object(argSchema).parse({} as any)).toThrow();
  });
});

// Tool behavior tests
describe('getDependencyTypes', () => {
  beforeEach(() => {
    // Stub global.fetch
    vi.stubGlobal('fetch', vi.fn());
  });

  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('should return in-package types when available', async () => {
    // Mock registry metadata fetch
    (fetch as Mock).mockImplementation((url: string) => {
      if (url === 'https://registry.npmjs.org/foo') {
        return Promise.resolve({
          ok: true,
          json: async () => ({
            'dist-tags': { latest: '1.0.0' },
            versions: { '1.0.0': { types: 'index.d.ts' } },
          }),
        });
      }
      if (url === 'https://unpkg.com/[email protected]/index.d.ts') {
        return Promise.resolve({
          ok: true,
          text: async () => '/* foo types */',
        });
      }
      return Promise.resolve({ ok: false });
    });

    const response = await getDependencyTypes({
      dependencies: [{ name: 'foo' }],
    });
    // Verify structure
    expect(response.content).toHaveLength(1);
    const item = response.content[0] as McpContentText;
    expect(item.type).toBe('text');

    const parsed = JSON.parse(item.text) as any[];
    expect(parsed).toEqual([
      {
        name: 'foo',
        hasTypes: true,
        types: '/* foo types */',
        version: '1.0.0',
      },
    ]);
  });

  it('should fallback to @types package when in-package types are missing', async () => {
    (fetch as Mock).mockImplementation((url: string) => {
      if (url === 'https://registry.npmjs.org/bar') {
        return Promise.resolve({
          ok: true,
          json: async () => ({
            'dist-tags': { latest: '2.0.0' },
            versions: { '2.0.0': {} },
          }),
        });
      }
      if (url === 'https://registry.npmjs.org/%40types%2Fbar') {
        return Promise.resolve({
          ok: true,
          json: async () => ({
            'dist-tags': { latest: '3.1.4' },
            versions: { '3.1.4': { typings: 'index.d.ts' } },
          }),
        });
      }
      if (url === 'https://unpkg.com/@types/[email protected]/index.d.ts') {
        return Promise.resolve({
          ok: true,
          text: async () => '/* bar external types */',
        });
      }
      return Promise.resolve({ ok: false });
    });

    const response = await getDependencyTypes({
      dependencies: [{ name: 'bar' }],
    });
    expect(response.content).toHaveLength(1);
    const item = response.content[0] as McpContentText;
    expect(item.type).toBe('text');

    const parsed = JSON.parse(item.text) as any[];
    expect(parsed).toEqual([
      {
        name: 'bar',
        hasTypes: true,
        types: '/* bar external types */',
        typesPackage: '@types/bar',
        version: '3.1.4',
      },
    ]);
  });

  it('should return hasTypes false when no types are available', async () => {
    (fetch as Mock).mockImplementation((url: string) => {
      if (url.startsWith('https://registry.npmjs.org/')) {
        return Promise.resolve({
          ok: true,
          json: async () => ({
            'dist-tags': { latest: '0.1.0' },
            versions: { '0.1.0': {} },
          }),
        });
      }
      // @types lookup fails
      return Promise.resolve({ ok: false });
    });

    const response = await getDependencyTypes({
      dependencies: [{ name: 'baz' }],
    });
    const item = response.content[0] as McpContentText;

    const parsed = JSON.parse(item.text) as any[];
    expect(parsed).toEqual([{ name: 'baz', hasTypes: false }]);
  });

  it('should handle fetch errors gracefully', async () => {
    (fetch as Mock).mockImplementation(() => {
      throw new Error('Network failure');
    });

    const response = await getDependencyTypes({
      dependencies: [{ name: 'qux' }],
    });
    const item = response.content[0] as McpContentText;

    const parsed = JSON.parse(item.text) as any[];
    expect(parsed).toEqual([{ name: 'qux', hasTypes: false }]);
  });
});

describe('getDependencyTypes integration test', () => {
  it('should fetch real types for dayjs', async () => {
    // Restore real fetch to allow network requests
    vi.restoreAllMocks();

    const response = await getDependencyTypes({
      dependencies: [{ name: 'dayjs', version: '1.11.7' }],
    });

    expect(response.content).toHaveLength(1);
    const item = response.content[0] as McpContentText;
    expect(item.type).toBe('text');

    const parsed = JSON.parse(item.text) as any[];

    expect(parsed[0].name).toBe('dayjs');
    expect(parsed[0].hasTypes).toBe(true);
    expect(parsed[0].types).toBeDefined();
    expect(parsed[0].types.length).toBeGreaterThan(0);
    expect(parsed[0].version).toBe('1.11.7');
  }, 15_000);

  it('should fetch external types from @types for express', async () => {
    // Restore real fetch to allow network requests
    vi.restoreAllMocks();

    const response = await getDependencyTypes({
      dependencies: [{ name: 'express', version: '4.17.1' }],
    });

    expect(response.content).toHaveLength(1);
    const item = response.content[0] as McpContentText;
    expect(item.type).toBe('text');

    const parsed = JSON.parse(item.text) as any[];

    expect(parsed[0].name).toBe('express');
    expect(parsed[0].hasTypes).toBe(true);
    expect(parsed[0].typesPackage).toBe('@types/express');
    expect(parsed[0].types).toBeDefined();
    expect(parsed[0].types.length).toBeGreaterThan(0);
    expect(typeof parsed[0].version).toBe('string');
  }, 15_000);
});

```

--------------------------------------------------------------------------------
/src/tools/searchNpmPackages.ts:
--------------------------------------------------------------------------------

```typescript
import { NpmRegistry, type PackageInfo } from 'npm-registry-sdk';
import { z } from 'zod';

import { logger } from '../logger.ts';
import { type McpResponse, textContent } from '../types.ts';

/**
 * Zod schema for validating npm package search parameters
 */
export const SearchNpmPackagesToolSchema = z.object({
  searchTerm: z
    .string()
    .min(1, { message: 'Search term cannot be empty.' })
    .regex(/^\S+$/, { message: 'Search term cannot contain spaces.' })
    .describe(
      'The term to search for in npm packages. Should contain all relevant context. Should ideally be text that might appear in the package name, description, or keywords. Use plus signs (+) to combine related terms (e.g., "react+components" for React component libraries). For filtering by author, maintainer, or scope, use the qualifiers field instead of including them in the search term. Examples: "express" for Express.js, "ui+components" for UI component packages, "testing+jest" for Jest testing utilities.'
    ),
  qualifiers: z
    .object({
      author: z.string().optional().describe('Filter by package author name'),
      maintainer: z
        .string()
        .optional()
        .describe('Filter by package maintainer name'),
      scope: z
        .string()
        .optional()
        .describe('Filter by npm scope (e.g., "@vue" for Vue.js packages)'),
      keywords: z.string().optional().describe('Filter by package keywords'),
      not: z
        .string()
        .optional()
        .describe('Exclude packages matching this criteria (e.g., "insecure")'),
      is: z
        .string()
        .optional()
        .describe(
          'Include only packages matching this criteria (e.g., "unstable")'
        ),
      boostExact: z
        .string()
        .optional()
        .describe('Boost exact matches for this term in search results'),
    })
    .optional()
    .describe(
      'Optional qualifiers to filter the search results. For example, { not: "insecure" } will exclude insecure packages, { author: "sindresorhus" } will only show packages by that author, { scope: "@vue" } will only show Vue.js scoped packages.'
    ),
});

type SearchNpmPackagesToolSchemaType = z.infer<
  typeof SearchNpmPackagesToolSchema
>;

type PackageDetails = {
  /** The name of the package */
  name: string;
  /** A brief description of the package */
  description: string;
  /** A snippet from the package's README file */
  readmeSnippet: string;
};

class SearchNpmPackagesTool {
  private readonly registry: NpmRegistry;
  private readonly maxResults = 5;
  private readonly maxReadmeLength = 500;

  constructor() {
    this.registry = new NpmRegistry();
  }

  /**
   * Searches for npm packages based on the provided search term and qualifiers
   * @param {SearchNpmPackagesToolSchemaType} params - Search parameters including search term and optional qualifiers
   * @returns {Promise<McpResponse>} A response containing the search results or an error message
   */
  public async searchPackages({
    searchTerm,
    qualifiers,
  }: SearchNpmPackagesToolSchemaType): Promise<McpResponse> {
    const searchResults = await this.registry.search(searchTerm, {
      qualifiers,
    });

    if (!searchResults.total) {
      return {
        content: [textContent('No packages found.')],
      };
    }

    const packages = searchResults.objects
      .sort((a, b) => b.score.detail.popularity - a.score.detail.popularity)
      .slice(0, this.maxResults)
      .map((result) => result.package.name);

    const packagesInfos = await this.getPackagesDetails(packages);

    return {
      content: [textContent(JSON.stringify(packagesInfos, null, 2))],
    };
  }

  /**
   * Retrieves detailed information for multiple packages
   * @param {string[]} packages - Array of package names to get details for
   * @returns {Promise<PackageDetails[]>} Array of package details
   * @private
   */
  private async getPackagesDetails(
    packages: string[]
  ): Promise<PackageDetails[]> {
    const multiPackageInfo: PackageInfo[] = await Promise.all(
      packages.map((pkg) => this.registry.getPackage(pkg))
    );

    const packagesDetails: PackageDetails[] = [];

    for (const packageInfo of Object.values(multiPackageInfo)) {
      packagesDetails.push({
        name: packageInfo.name,
        description: packageInfo.description || 'No description available.',
        readmeSnippet: this.extractReadmeSnippet(packageInfo.readme),
      });
    }

    return packagesDetails;
  }

  /**
   * Extracts a snippet from a package's README file
   * @param {string | undefined} readme - The full README content
   * @returns {string} A truncated snippet of the README or a default message if README is not available
   * @private
   */
  private extractReadmeSnippet(readme: string | undefined): string {
    if (!readme) {
      return 'README not available.';
    }

    const snippet = readme.substring(0, this.maxReadmeLength);
    return snippet.length === this.maxReadmeLength ? snippet + '...' : snippet;
  }
}

/**
 * Search for npm packages by a search term and get their name, description, and a README snippet.
 * This is an MCP (Model Context Protocol) tool that allows LLMs to discover and analyze npm packages.
 *
 * Returns up to 5 packages sorted by popularity, each containing:
 * - Package name
 * - Description
 * - README snippet (first 500 characters)
 *
 * Use qualifiers to filter results by author, scope, keywords, or exclude unwanted packages.
 *
 * @param {SearchNpmPackagesToolSchemaType} params - Search parameters including search term and optional qualifiers
 * @returns {Promise<McpResponse>} A response containing the search results formatted as JSON, or an error message
 *
 * @example
 * // Basic search
 * searchNpmPackages({ searchTerm: "react" })
 *
 * @example
 * // Search with qualifiers
 * searchNpmPackages({
 *   searchTerm: "ui+components",
 *   qualifiers: { scope: "@mui", not: "deprecated" }
 * })
 */
export default async function searchNpmPackages(
  params: SearchNpmPackagesToolSchemaType
): Promise<McpResponse> {
  try {
    const tool = new SearchNpmPackagesTool();
    const response = await tool.searchPackages(params);
    return response;
  } catch (error) {
    const errorMessage = `Failed to search npm packages for "${params.searchTerm}". Error: ${error instanceof Error ? error.message : String(error)}`;
    logger.error(errorMessage);
    return {
      content: [textContent(errorMessage)],
      isError: true,
    };
  }
}

```

--------------------------------------------------------------------------------
/test/utils.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import {
  isRunningInDocker,
  isDockerRunning,
  preprocessDependencies,
} from '../src/utils.ts';
import { containerExists, isContainerRunning } from './utils.ts';
import * as childProcess from 'node:child_process';

vi.mock('fs');
vi.mock('node:child_process');

describe('utils', () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe('isRunningInDocker', () => {
    it('should return true when /.dockerenv exists', () => {
      vi.spyOn(fs, 'existsSync').mockImplementation((path) => {
        return path === '/.dockerenv';
      });

      expect(isRunningInDocker()).toBe(true);
      expect(fs.existsSync).toHaveBeenCalledWith('/.dockerenv');
    });

    it('should return true when /proc/1/cgroup exists and contains docker', () => {
      vi.spyOn(fs, 'existsSync').mockImplementation((path) => {
        return path === '/proc/1/cgroup';
      });

      vi.spyOn(fs, 'readFileSync').mockReturnValue(
        Buffer.from('12:memory:/docker/somecontainerid')
      );

      expect(isRunningInDocker()).toBe(true);
      expect(fs.existsSync).toHaveBeenCalledWith('/.dockerenv');
      expect(fs.existsSync).toHaveBeenCalledWith('/proc/1/cgroup');
      expect(fs.readFileSync).toHaveBeenCalledWith('/proc/1/cgroup', 'utf8');
    });

    it('should return true when /proc/1/cgroup exists and contains kubepods', () => {
      vi.spyOn(fs, 'existsSync').mockImplementation((path) => {
        return path === '/proc/1/cgroup';
      });

      vi.spyOn(fs, 'readFileSync').mockReturnValue(
        Buffer.from('12:memory:/kubepods/somecontainerid')
      );

      expect(isRunningInDocker()).toBe(true);
    });

    it('should return true when docker environment variables are set', () => {
      vi.spyOn(fs, 'existsSync').mockReturnValue(false);

      const originalEnv = process.env;
      process.env = { ...originalEnv, DOCKER_CONTAINER: 'true' };

      expect(isRunningInDocker()).toBe(true);

      process.env = originalEnv;
    });

    it('should handle file system errors gracefully', () => {
      vi.spyOn(fs, 'existsSync').mockImplementation((path) => {
        return path === '/proc/1/cgroup';
      });

      vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
        throw new Error('Permission denied');
      });

      expect(isRunningInDocker()).toBe(false);
    });

    it('should return false when no docker indicators are present', () => {
      vi.spyOn(fs, 'existsSync').mockReturnValue(false);

      const originalEnv = process.env;
      process.env = { ...originalEnv };
      delete process.env.DOCKER_CONTAINER;
      delete process.env.DOCKER_ENV;

      expect(isRunningInDocker()).toBe(false);

      process.env = originalEnv;
    });
  });

  describe('isDockerRunning', () => {
    it('should return true when docker info command succeeds', () => {
      vi.spyOn(childProcess, 'execFileSync').mockImplementation(() =>
        Buffer.from('')
      );

      expect(isDockerRunning()).toBe(true);
      expect(childProcess.execFileSync).toHaveBeenCalledWith('docker', [
        'info',
      ]);
    });

    it('should return false when docker info command fails', () => {
      vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => {
        throw new Error('docker daemon not running');
      });

      expect(isDockerRunning()).toBe(false);
      expect(childProcess.execFileSync).toHaveBeenCalledWith('docker', [
        'info',
      ]);
    });
  });

  describe('preprocessDependencies', () => {
    it('should convert dependency array to record format', () => {
      const dependencies = [
        { name: 'lodash', version: '4.17.21' },
        { name: 'express', version: '4.18.2' },
      ];

      const result = preprocessDependencies({ dependencies });

      expect(result).toEqual({
        lodash: '4.17.21',
        express: '4.18.2',
      });
    });

    it('should add chartjs-node-canvas for chartjs image', () => {
      const dependencies = [{ name: 'lodash', version: '4.17.21' }];

      const result = preprocessDependencies({
        dependencies,
        image: 'alfonsograziano/node-chartjs-canvas:latest',
      });

      expect(result).toEqual({
        lodash: '4.17.21',
        'chartjs-node-canvas': '4.0.0',
        '@mermaid-js/mermaid-cli': '^11.4.2',
      });
    });

    it('should not add chartjs-node-canvas for non-chartjs images', () => {
      const dependencies = [{ name: 'lodash', version: '4.17.21' }];

      const result = preprocessDependencies({
        dependencies,
        image: 'node:lts-slim',
      });

      expect(result).toEqual({
        lodash: '4.17.21',
      });
    });
  });
});

describe('containerExists', () => {
  it('should return true for a valid container ID', () => {
    vi.spyOn(childProcess, 'execFileSync').mockImplementation(() =>
      Buffer.from('')
    );
    expect(containerExists('js-sbx-valid')).toBe(true);
    expect(childProcess.execFileSync).toHaveBeenCalledWith('docker', [
      'inspect',
      'js-sbx-valid',
    ]);
  });

  it('should return false for a non-existent container ID', () => {
    vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => {
      throw new Error('No such container');
    });
    expect(containerExists('not-a-real-container')).toBe(false);
  });

  it('should return false for a malicious container ID', () => {
    vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => {
      throw new Error('Invalid container ID');
    });
    expect(containerExists('bad;id$(rm -rf /)')).toBe(false);
  });
});

describe('isContainerRunning', () => {
  it('should return true if docker inspect returns "true"', () => {
    vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => 'true');
    expect(isContainerRunning('js-sbx-valid')).toBe(true);
    expect(childProcess.execFileSync).toHaveBeenCalledWith(
      'docker',
      ['inspect', '-f', '{{.State.Running}}', 'js-sbx-valid'],
      { encoding: 'utf8' }
    );
  });

  it('should return false if docker inspect returns "false"', () => {
    vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => 'false');
    expect(isContainerRunning('js-sbx-valid')).toBe(false);
  });

  it('should return false for a non-existent container ID', () => {
    vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => {
      throw new Error('No such container');
    });
    expect(isContainerRunning('not-a-real-container')).toBe(false);
  });

  it('should return false for a malicious container ID', () => {
    vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => {
      throw new Error('Invalid container ID');
    });
    expect(isContainerRunning('bad;id$(rm -rf /)')).toBe(false);
  });
});

```

--------------------------------------------------------------------------------
/website/src/Components/Header.tsx:
--------------------------------------------------------------------------------

```typescript
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Menu, X, Terminal, Brain, Bot, GitBranch } from 'lucide-react';

const Header: React.FC = () => {
  const [isMenuOpen, setIsMenuOpen] = useState(false);
  const location = useLocation();

  const toggleMenu = () => {
    setIsMenuOpen(!isMenuOpen);
  };

  const closeMenu = () => {
    setIsMenuOpen(false);
  };

  const isActive = (path: string) => {
    return location.pathname === path;
  };

  return (
    <header className="bg-white shadow-sm sticky top-0 z-50">
      <div className="max-w-6xl mx-auto px-6">
        <div className="flex items-center justify-between h-16">
          {/* Logo */}
          <Link
            to="/"
            className="flex items-center gap-2 text-xl font-bold text-gray-900"
          >
            <div className="w-8 h-8 bg-gradient-to-r from-green-500 to-blue-600 rounded-lg flex items-center justify-center">
              <Brain size={20} className="text-white" />
            </div>
            <span>JSDevAI</span>
          </Link>

          {/* Desktop Navigation */}
          <nav className="hidden md:flex items-center gap-8">
            <Link
              to="/"
              className={`text-sm font-medium transition-colors ${
                isActive('/')
                  ? 'text-green-600 border-b-2 border-green-600'
                  : 'text-gray-600 hover:text-green-600'
              }`}
            >
              Home
            </Link>
            <Link
              to="/mcp"
              className={`text-sm font-medium transition-colors ${
                isActive('/mcp')
                  ? 'text-green-600 border-b-2 border-green-600'
                  : 'text-gray-600 hover:text-green-600'
              }`}
            >
              <div className="flex items-center gap-2">
                <Terminal size={16} />
                MCP Sandbox
              </div>
            </Link>
            <Link
              to="/tiny-agent"
              className={`text-sm font-medium transition-colors ${
                isActive('/tiny-agent')
                  ? 'text-green-600 border-b-2 border-green-600'
                  : 'text-gray-600 hover:text-green-600'
              }`}
            >
              <div className="flex items-center gap-2">
                <Bot size={16} />
                Tiny Agent
              </div>
            </Link>
            <Link
              to="/graph-gpt"
              className={`text-sm font-medium transition-colors ${
                isActive('/graph-gpt')
                  ? 'text-green-600 border-b-2 border-green-600'
                  : 'text-gray-600 hover:text-green-600'
              }`}
            >
              <div className="flex items-center gap-2">
                <GitBranch size={16} />
                GraphGPT
              </div>
            </Link>
            <a
              href="https://github.com/alfonsograziano/node-code-sandbox-mcp"
              target="_blank"
              rel="noopener noreferrer"
              className="text-sm font-medium text-gray-600 hover:text-green-600 transition-colors"
            >
              GitHub
            </a>
          </nav>

          {/* CTA Button */}
          <div className="hidden md:block">
            <Link
              to="/mcp"
              className="inline-flex items-center gap-2 px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 transition-colors"
            >
              <Terminal size={16} />
              Try Sandbox
            </Link>
          </div>

          {/* Mobile Menu Button */}
          <button
            onClick={toggleMenu}
            className="md:hidden p-2 text-gray-600 hover:text-green-600 transition-colors"
            aria-label="Toggle menu"
          >
            {isMenuOpen ? <X size={24} /> : <Menu size={24} />}
          </button>
        </div>

        {/* Mobile Navigation */}
        {isMenuOpen && (
          <div className="md:hidden border-t border-gray-200 py-4">
            <nav className="flex flex-col space-y-4">
              <Link
                to="/"
                onClick={closeMenu}
                className={`text-base font-medium transition-colors ${
                  isActive('/')
                    ? 'text-green-600'
                    : 'text-gray-600 hover:text-green-600'
                }`}
              >
                Home
              </Link>
              <Link
                to="/mcp"
                onClick={closeMenu}
                className={`text-base font-medium transition-colors ${
                  isActive('/mcp')
                    ? 'text-green-600'
                    : 'text-gray-600 hover:text-green-600'
                }`}
              >
                <div className="flex items-center gap-2">
                  <Terminal size={16} />
                  MCP Sandbox
                </div>
              </Link>
              <Link
                to="/tiny-agent"
                onClick={closeMenu}
                className={`text-base font-medium transition-colors ${
                  isActive('/tiny-agent')
                    ? 'text-green-600'
                    : 'text-gray-600 hover:text-green-600'
                }`}
              >
                <div className="flex items-center gap-2">
                  <Bot size={16} />
                  Tiny Agent
                </div>
              </Link>
              <Link
                to="/graph-gpt"
                onClick={closeMenu}
                className={`text-base font-medium transition-colors ${
                  isActive('/graph-gpt')
                    ? 'text-green-600'
                    : 'text-gray-600 hover:text-green-600'
                }`}
              >
                <div className="flex items-center gap-2">
                  <GitBranch size={16} />
                  GraphGPT
                </div>
              </Link>
              <a
                href="https://github.com/alfonsograziano/node-code-sandbox-mcp"
                target="_blank"
                rel="noopener noreferrer"
                onClick={closeMenu}
                className="text-base font-medium text-gray-600 hover:text-green-600 transition-colors"
              >
                GitHub
              </a>

              <div className="pt-2">
                <Link
                  to="/mcp"
                  onClick={closeMenu}
                  className="inline-flex items-center gap-2 px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 transition-colors w-full justify-center"
                >
                  <Terminal size={16} />
                  Try Sandbox
                </Link>
              </div>
            </nav>
          </div>
        )}
      </div>
    </header>
  );
};

export default Header;

```

--------------------------------------------------------------------------------
/USE_CASE.md:
--------------------------------------------------------------------------------

```markdown
# 📆 Use Cases Appendix

This document contains practical use cases to unlock the full power of the Node.js Sandbox MCP server. You can dynamically install any npm packages during execution, save files, and run completely isolated experiments in fresh Docker containers.

All the listed use cases have been tested.
If you find a use case you'd like to support but that doesn't currently work, please open an issue.
Or, if you want to add your own cool use case, feel free to open a Pull Request!

---

### Generate a QR Code

Create and run a JS script that generates a QR code for the URL `https://nodejs.org/en`, and save it as `qrcode.png`.

**Tip:** Use the `qrcode` package.

---

### Test Regular Expressions

Create and run a JavaScript script that defines a complex regular expression to match valid mathematical expressions containing nested parentheses (e.g., ((2+3)_(4-5))), allowing numbers, +, -, _, / operators, and properly nested parentheses.

Requirements:

- The regular expression must handle deep nesting (e.g., up - to 3-4 levels).
- Write at least 10 unit tests covering correct and - incorrect cases.
- Use assert or manually throw errors if the validation fails.
- Add a short comment explaining the structure of the regex.

---

### Create CSV files with random data

Create and execute a js script which generates 200 items in a csv. The CSV has full name, random number and random (but valid) email. Write it in a file called "fake_data.csv"

---

### Scrape a Webpage Title

Create and run a JS script that fetches `https://example.com`, saves the html file in "example.html", extracts the `<title>` tag, and shows it in the console.

**Tip:** Use `cheerio`.

---

### Create a PDF Report

Create a JavaScript script with Node.js that generates a PDF file containing a fun "Getting Started with JavaScript" tutorial for a 10-year-old kid.

The tutorial should be simple, playful, and colorful, explaining basic concepts like console.log(), variables, and how to write your first small program.
Save the PDF as getting-started-javascript.pdf with fs

Tip: Use `pdf-lib` or `pdfkit` for creating the PDF.

---

### Fetch an API and Save to JSON

Create and run a JS script that fetches data from the GitHub Node.js repo (`https://api.github.com/repos/nodejs/node`) and saves part of the response to `nodejs_info.json`.

---

### Markdown to HTML Converter

Write a JavaScript script that takes a Markdown string, converts it into HTML, and saves the result into a file named content_converted.html.

Use the following example Markdown string:

```markdown
# Welcome to My Page

This is a simple page created from **Markdown**!

- Learn JavaScript
- Learn Markdown
- Build Cool Stuff 🚀
```

Tip: Use a library like `marked` to perform the conversion.

---

### Generate Random Data

Create a JS script that generates a list of 100 fake users with names, emails, and addresses, then saves them to a JSON file called "fake_users.json".

**Tip:** Use `@faker-js/faker`.

---

### Evaluate a complex math expression

Create a JS script that evaluates this expression `((5 + 8) * (15 / 3) - (9 - (4 * 6)) + (10 / (2 + 6))) ^ 2 + sqrt(64) - factorial(6) + (24 / (5 + 7 * (3 ^ 2))) + log(1000) * sin(30 * pi / 180) - cos(60 * pi / 180) + tan(45 * pi / 180) + (4 ^ 3 - 2 ^ (5 - 2)) * (sqrt(81) / 9)`. Tip: use math.js

---

### Take a Screenshot with Playwright

Create and run a JS script that launches a Chromium browser, navigates to `https://example.com`, and takes a screenshot saved as `screenshot_test.png`.

**Tip:** Use the official Playwright Docker image (mcr.microsoft.com/playwright) and install the playwright npm package dynamically.

---

### Generate a chart

Write a JavaScript script that generates a bar chart using chartjs-node-canvas.
The chart should show Monthly Revenue Growth for the first 6 months of the year.
Use the following data:

-January: $12,000
-February: $15,500
-March: $14,200
-April: $18,300
-May: $21,000
-June: $24,500

Add the following details:

-Title: "Monthly Revenue Growth (2025)"
-X-axis label: "Month"
-Y-axis label: "Revenue (USD)"
-Save the resulting chart as chart.png.

---

### Summarize a Long Article

Fetch the content of https://en.wikipedia.org/wiki/Node.js, strip HTML tags, and send the plain text to the AI. Ask it to return a bullet-point summary of the most important sections in less than 300 words.

---

### Refactor and Optimize a JS Function

Here's an unoptimized JavaScript function:

```javascript
function getUniqueValues(arr) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    let exists = false;
    for (let j = 0; j < result.length; j++) {
      if (arr[i] === result[j]) {
        exists = true;
        break;
      }
    }
    if (!exists) {
      result.push(arr[i]);
    }
  }
  return result;
}
```

Please refactor and optimize this function for performance and readability.
Then, write and run basic tests with the Node.js test runner to make sure it works (covering common and edge cases).
As soon as all tests pass, return only the refactored function.

---

Here’s a complete and clear prompt that includes the schema and instructions for the AI:

---

### Create a Mock Book API from a JSON Schema

Here is a JSON Schema describing a `Book` entity:

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Book",
  "type": "object",
  "required": ["title", "author", "isbn"],
  "properties": {
    "title": {
      "type": "string",
      "minLength": 1
    },
    "author": {
      "type": "string",
      "minLength": 1
    },
    "isbn": {
      "type": "string",
      "pattern": "^(97(8|9))?\\d{9}(\\d|X)$"
    },
    "publishedYear": {
      "type": "integer",
      "minimum": 0,
      "maximum": 2100
    },
    "genres": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "available": {
      "type": "boolean",
      "default": true
    }
  },
  "additionalProperties": false
}
```

Using this schema:

1. Generate **mock data** for at least 5 different books.
2. Create a simple **Node.js REST API** (you can use Express or Fastify) that:
   - Serves a `GET /books` endpoint on **port 5007**, which returns all mock books.
   - Serves a `GET /books/:isbn` endpoint that returns a single book matching the provided ISBN (or a 404 if not found).
3. Run the server and print a message like:  
   `"Mock Book API is running on http://localhost:5007"`

---

### Files manipulation on the fly

**Prerequisites**: Create in your mounted folder a file called "books.json" with this content:

```json
[
  { "id": 1, "title": "The Silent Code", "author": "Jane Doe" },
  { "id": 2, "title": "Refactoring Legacy", "author": "John Smith" },
  { "id": 3, "title": "Async in Action", "author": "Jane Doe" },
  { "id": 4, "title": "The Pragmatic Stack", "author": "Emily Ray" },
  { "id": 5, "title": "Systems Unboxed", "author": "Mark Lee" }
]
```

Then run this prompt:

Run a JS script to read the file "books.json", filter all the books of the author "Jane Doe" and save the result in "books_filtered.json"

To read and write from files, you always need to use the "files" directory which is exposed on the host machine.

```
Page 1/2FirstPrevNextLast