#
tokens: 47965/50000 74/90 files (page 1/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 3. Use http://codebase.md/alfonsograziano/node-code-sandbox-mcp?lines=true&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:
--------------------------------------------------------------------------------

```
1 | 23
2 | 
```

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

```
1 | engine-strict=true
2 | 
```

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

```
1 | {
2 |   "extends": [
3 |     "@commitlint/config-conventional"
4 |   ]
5 | }
6 | 
```

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

```
1 | node_modules
2 | dist
3 | *.log
4 | coverage
5 | .env
6 | package-lock.json
7 | yarn.lock
8 | pnpm-lock.yaml 
```

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

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

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

```
1 | OPENAI_API_KEY=ADD_HERE_YOUR_OPENAI_API_KEY
2 | OPENAI_MODEL=ENTER_HERE_THE_CHOSEN_MODEL
3 | FILES_DIR=/Users/youruser/Desktop
4 | SANDBOX_MEMORY_LIMIT=
5 | SANDBOX_CPU_LIMIT=
```

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

```
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | pnpm-debug.log*
 8 | lerna-debug.log*
 9 | 
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | 
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | 
26 | .vercel
27 | 
```

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

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

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

```
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | 
 82 | # parcel-bundler cache (https://parceljs.org/)
 83 | .cache
 84 | .parcel-cache
 85 | 
 86 | # Next.js build output
 87 | .next
 88 | out
 89 | 
 90 | # Nuxt.js build / generate output
 91 | .nuxt
 92 | dist
 93 | 
 94 | # Gatsby files
 95 | .cache/
 96 | # Comment in the public line in if your project uses Gatsby and not Next.js
 97 | # https://nextjs.org/blog/next-9-1#public-directory-support
 98 | # public
 99 | 
100 | # vuepress build output
101 | .vuepress/dist
102 | 
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 | 
107 | # vitepress build output
108 | **/.vitepress/dist
109 | 
110 | # vitepress cache directory
111 | **/.vitepress/cache
112 | 
113 | # Docusaurus cache and generated files
114 | .docusaurus
115 | 
116 | # Serverless directories
117 | .serverless/
118 | 
119 | # FuseBox cache
120 | .fusebox/
121 | 
122 | # DynamoDB Local files
123 | .dynamodb/
124 | 
125 | # TernJS port file
126 | .tern-port
127 | 
128 | # Stores VSCode versions used for testing VSCode extensions
129 | .vscode-test
130 | 
131 | # yarn v2
132 | .yarn/cache
133 | .yarn/unplugged
134 | .yarn/build-state.yml
135 | .yarn/install-state.gz
136 | .pnp.*
137 | 
138 | # General
139 | .DS_Store
140 | 
141 | # Thumbnails
142 | ._*
143 | 
144 | # Files that might appear in the root of a volume
145 | .DocumentRevisions-V100
146 | .fseventsd
147 | .Spotlight-V100
148 | .TemporaryItems
149 | .Trashes
150 | .VolumeIcon.icns
151 | .com.apple.timemachine.donotpresent
152 | 
153 | # Directories potentially created on remote AFP share
154 | .AppleDB
155 | .AppleDesktop
156 | Network Trash Folder
157 | Temporary Items
158 | .apdisk
159 | 
```

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

```markdown
  1 | # 🐢🚀 Node.js Sandbox MCP Server
  2 | 
  3 | Node.js server implementing the Model Context Protocol (MCP) for running arbitrary JavaScript in ephemeral Docker containers with on‑the‑fly npm dependency installation.
  4 | 
  5 | ![Website Preview](https://raw.githubusercontent.com/alfonsograziano/node-code-sandbox-mcp/master/assets/images/website_homepage.png)
  6 | 
  7 | 👉 [Look at the official website](https://jsdevai.com/)
  8 | 
  9 | 📦 [Available on Docker Hub](https://hub.docker.com/r/mcp/node-code-sandbox)
 10 | 
 11 | ## Features
 12 | 
 13 | - Start and manage isolated Node.js sandbox containers
 14 | - Execute arbitrary shell commands inside containers
 15 | - Install specified npm dependencies per job
 16 | - Run ES module JavaScript snippets and capture stdout
 17 | - Tear down containers cleanly
 18 | - **Detached Mode:** Keep the container alive after script execution (e.g. for long-running servers)
 19 | 
 20 | > Note: Containers run with controlled CPU/memory limits.
 21 | 
 22 | ## Explore Cool Use Cases
 23 | 
 24 | 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)
 25 | It contains a curated list of prompts, examples, and creative experiments you can try with the Node.js Sandbox MCP Server.
 26 | 
 27 | ## ⚠️ Prerequisites
 28 | 
 29 | To use this MCP server, Docker must be installed and running on your machine.
 30 | 
 31 | **Tip:** Pre-pull any Docker images you'll need to avoid delays during first execution.
 32 | 
 33 | Example recommended images:
 34 | 
 35 | - node:lts-slim
 36 | - mcr.microsoft.com/playwright:v1.55.0-noble
 37 | - alfonsograziano/node-chartjs-canvas:latest
 38 | 
 39 | ## Getting started
 40 | 
 41 | In order to get started with this MCP server, first of all you need to connect it to a client (for example Claude Desktop).
 42 | 
 43 | Once it's running, you can test that it's fully working with a couple of test prompts:
 44 | 
 45 | - Validate that the tool can run:
 46 | 
 47 |   ```markdown
 48 |   Create and run a JS script with a console.log("Hello World")
 49 |   ```
 50 | 
 51 |   This should run a console.log and in the tool response you should be able to see Hello World.
 52 | 
 53 | - Validate that you can install dependencies and save files
 54 | 
 55 |   ```markdown
 56 |   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.
 57 |   ```
 58 | 
 59 |   This should create a file in your mounted directory (for example the Desktop) called "qrcode.png"
 60 | 
 61 | ### Usage with Claude Desktop
 62 | 
 63 | Add this to your `claude_desktop_config.json`:
 64 | You can follow the [Official Guide](https://modelcontextprotocol.io/quickstart/user) to install this MCP server
 65 | 
 66 | ```json
 67 | {
 68 |   "mcpServers": {
 69 |     "js-sandbox": {
 70 |       "command": "docker",
 71 |       "args": [
 72 |         "run",
 73 |         "-i",
 74 |         "--rm",
 75 |         "-v",
 76 |         "/var/run/docker.sock:/var/run/docker.sock",
 77 |         "-v",
 78 |         "$HOME/Desktop/sandbox-output:/root",
 79 |         "-e",
 80 |         "FILES_DIR=$HOME/Desktop/sandbox-output",
 81 |         "-e",
 82 |         "SANDBOX_MEMORY_LIMIT=512m", // optional
 83 |         "-e",
 84 |         "SANDBOX_CPU_LIMIT=0.75", // optional
 85 |         "mcp/node-code-sandbox"
 86 |       ]
 87 |     }
 88 |   }
 89 | }
 90 | ```
 91 | 
 92 | or with NPX:
 93 | 
 94 | ```json
 95 | {
 96 |   "mcpServers": {
 97 |     "node-code-sandbox-mcp": {
 98 |       "type": "stdio",
 99 |       "command": "npx",
100 |       "args": ["-y", "node-code-sandbox-mcp"],
101 |       "env": {
102 |         "FILES_DIR": "/Users/alfonsograziano/Desktop/node-sandbox",
103 |         "SANDBOX_MEMORY_LIMIT": "512m", // optional
104 |         "SANDBOX_CPU_LIMIT": "0.75" // optional
105 |       }
106 |     }
107 |   }
108 | }
109 | ```
110 | 
111 | > Note: Ensure your working directory points to the built server, and Docker is installed/running.
112 | 
113 | ### Docker
114 | 
115 | Run the server in a container (mount Docker socket if needed), and pass through your desired host output directory as an env var:
116 | 
117 | ```shell
118 | # Build locally if necessary
119 | # docker build -t mcp/node-code-sandbox .
120 | 
121 | docker run --rm -it \
122 |   -v /var/run/docker.sock:/var/run/docker.sock \
123 |   -v "$HOME/Desktop/sandbox-output":"/root" \
124 |   -e FILES_DIR="$HOME/Desktop/sandbox-output" \
125 |   -e SANDBOX_MEMORY_LIMIT="512m" \
126 |   -e SANDBOX_CPU_LIMIT="0.5" \
127 |   mcp/node-code-sandbox stdio
128 | ```
129 | 
130 | This bind-mounts your host folder into the container at the **same absolute path** and makes `FILES_DIR` available inside the MCP server.
131 | 
132 | #### Ephemeral usage – **no persistent storage**
133 | 
134 | ```bash
135 | docker run --rm -it \
136 |   -v /var/run/docker.sock:/var/run/docker.sock \
137 |   alfonsograziano/node-code-sandbox-mcp stdio
138 | ```
139 | 
140 | ### Usage with VS Code
141 | 
142 | **Quick install** buttons (VS Code & Insiders):
143 | 
144 | Install js-sandbox-mcp (NPX) Install js-sandbox-mcp (Docker)
145 | 
146 | **Manual configuration**: Add to your VS Code `settings.json` or `.vscode/mcp.json`:
147 | 
148 | ```json
149 | "mcp": {
150 |     "servers": {
151 |         "js-sandbox": {
152 |             "command": "docker",
153 |             "args": [
154 |                 "run",
155 |                 "-i",
156 |                 "--rm",
157 |                 "-v", "/var/run/docker.sock:/var/run/docker.sock",
158 |                 "-v", "$HOME/Desktop/sandbox-output:/root", // optional
159 |                 "-e", "FILES_DIR=$HOME/Desktop/sandbox-output",  // optional
160 |                 "-e", "SANDBOX_MEMORY_LIMIT=512m",
161 |                 "-e", "SANDBOX_CPU_LIMIT=1",
162 |                 "mcp/node-code-sandbox"
163 |               ]
164 |         }
165 |     }
166 | }
167 | ```
168 | 
169 | ## API
170 | 
171 | ## Tools
172 | 
173 | ### run_js_ephemeral
174 | 
175 | Run a one-off JS script in a brand-new disposable container.
176 | 
177 | **Inputs:**
178 | 
179 | - `image` (string, optional): Docker image to use (default: `node:lts-slim`).
180 | - `code` (string, required): JavaScript source to execute.
181 | - `dependencies` (array of `{ name, version }`, optional): NPM packages and versions to install (default: `[]`).
182 | 
183 | **Behavior:**
184 | 
185 | 1. Creates a fresh container.
186 | 2. Writes your `index.js` and a minimal `package.json`.
187 | 3. Installs the specified dependencies.
188 | 4. Executes the script.
189 | 5. Tears down (removes) the container.
190 | 6. Returns the captured stdout.
191 | 7. If your code saves any files in the current directory, these files will be returned automatically.
192 |    - Images (e.g., PNG, JPEG) are returned as `image` content.
193 |    - Other files (e.g., `.txt`, `.json`) are returned as `resource` content.
194 |    - Note: the file saving feature is currently available only in the ephemeral tool.
195 | 
196 | > **Tip:** To get files back, simply save them during your script execution.
197 | 
198 | **Example Call:**
199 | 
200 | ```jsonc
201 | {
202 |   "name": "run_js_ephemeral",
203 |   "arguments": {
204 |     "image": "node:lts-slim",
205 |     "code": "console.log('One-shot run!');",
206 |     "dependencies": [{ "name": "lodash", "version": "^4.17.21" }],
207 |   },
208 | }
209 | ```
210 | 
211 | **Example to save a file:**
212 | 
213 | ```javascript
214 | import fs from 'fs/promises';
215 | 
216 | await fs.writeFile('hello.txt', 'Hello world!');
217 | console.log('Saved hello.txt');
218 | ```
219 | 
220 | This will return the console output **and** the `hello.txt` file.
221 | 
222 | ### sandbox_initialize
223 | 
224 | Start a fresh sandbox container.
225 | 
226 | - **Input**:
227 |   - `image` (_string_, optional, default: `node:lts-slim`): Docker image for the sandbox
228 |   - `port` (_number_, optional): If set, maps this container port to the host
229 | - **Output**: Container ID string
230 | 
231 | ### sandbox_exec
232 | 
233 | Run shell commands inside the running sandbox.
234 | 
235 | - **Input**:
236 |   - `container_id` (_string_): ID from `sandbox_initialize`
237 |   - `commands` (_string[]_): Array of shell commands to execute
238 | - **Output**: Combined stdout of each command
239 | 
240 | ### run_js
241 | 
242 | Install npm dependencies and execute JavaScript code.
243 | 
244 | - **Input**:
245 |   - `container_id` (_string_): ID from `sandbox_initialize`
246 |   - `code` (_string_): JS source to run (ES modules supported)
247 |   - `dependencies` (_array of `{ name, version }`_, optional, default: `[]`): npm package names → semver versions
248 |   - `listenOnPort` (_number_, optional): If set, leaves the process running and exposes this port to the host (**Detached Mode**)
249 | 
250 | - **Behavior:**
251 |   1. Creates a temp workspace inside the container
252 |   2. Writes `index.js` and a minimal `package.json`
253 |   3. Runs `npm install --omit=dev --ignore-scripts --no-audit --loglevel=error`
254 |   4. Executes `node index.js` and captures stdout, or leaves process running in background if `listenOnPort` is set
255 |   5. Cleans up workspace unless running in detached mode
256 | 
257 | - **Output**: Script stdout or background execution notice
258 | 
259 | ### sandbox_stop
260 | 
261 | Terminate and remove the sandbox container.
262 | 
263 | - **Input**:
264 |   - `container_id` (_string_): ID from `sandbox_initialize`
265 | - **Output**: Confirmation message
266 | 
267 | ### search_npm_packages
268 | 
269 | Search for npm packages by a search term and get their name, description, and a README snippet.
270 | 
271 | - **Input**:
272 |   - `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).
273 |   - `qualifiers` (_object_, optional): Optional qualifiers to filter the search results:
274 |     - `author` (_string_, optional): Filter by package author name
275 |     - `maintainer` (_string_, optional): Filter by package maintainer name
276 |     - `scope` (_string_, optional): Filter by npm scope (e.g., "@vue" for Vue.js packages)
277 |     - `keywords` (_string_, optional): Filter by package keywords
278 |     - `not` (_string_, optional): Exclude packages matching this criteria (e.g., "insecure")
279 |     - `is` (_string_, optional): Include only packages matching this criteria (e.g., "unstable")
280 |     - `boostExact` (_string_, optional): Boost exact matches for this term in search results
281 | 
282 | - **Behavior:**
283 |   1. Searches the npm registry using the provided search term and qualifiers
284 |   2. Returns up to 5 packages sorted by popularity
285 |   3. For each package, provides name, description, and README snippet (first 500 characters)
286 | 
287 | - **Output**: JSON array containing package details with name, description, and README snippet
288 | 
289 | ## Usage Tips
290 | 
291 | - **Session-based tools** (`sandbox_initialize` ➔ `run_js` ➔ `sandbox_stop`) are ideal when you want to:
292 |   - Keep a long-lived sandbox container open.
293 |   - Run multiple commands or scripts in the same environment.
294 |   - Incrementally install and reuse dependencies.
295 | - **One-shot execution** with `run_js_ephemeral` is perfect for:
296 |   - Quick experiments or simple scripts.
297 |   - Cases where you don't need to maintain state or cache dependencies.
298 |   - Clean, atomic runs without worrying about manual teardown.
299 | - **Detached mode** is useful when you want to:
300 |   - Spin up servers or long-lived services on-the-fly
301 |   - Expose and test endpoints from running containers
302 | 
303 | Choose the workflow that best fits your use-case!
304 | 
305 | ## Build
306 | 
307 | Compile and bundle:
308 | 
309 | ```shell
310 | npm install
311 | npm run build
312 | ```
313 | 
314 | ## License
315 | 
316 | MIT License
317 | 
318 | 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:
319 | 
320 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
321 | 
322 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
323 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
324 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
325 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
326 | 
```

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

```markdown
 1 | MIT License
 2 | 
 3 | Copyright (c) 2022 joshcs.eth | jcs.sol
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 
```

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

```typescript
1 | /// <reference types="vite/client" />
2 | 
```

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

```typescript
1 | /// <reference types="vite/client" />
2 | 
```

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

```css
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 | 
```

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

```javascript
1 | module.exports = {
2 |   plugins: {
3 |     tailwindcss: {},
4 |     autoprefixer: {},
5 |   },
6 | }
7 | 
```

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

```typescript
1 | import { Buffer } from "buffer";
2 | 
3 | window.global = window.global ?? window;
4 | window.Buffer = window.Buffer ?? Buffer;
5 | 
6 | export {};
7 | 
```

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

```json
1 | {
2 |   "recommendations": [
3 |     "esbenp.prettier-vscode",
4 |     "dbaeumer.vscode-eslint",
5 |     "ms-azuretools.vscode-containers"
6 |   ]
7 | }
8 | 
```

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

```json
1 | {
2 |   "extends": "./tsconfig.json",
3 |   "compilerOptions": {
4 |     "rootDir": "src"
5 |   },
6 |   "exclude": ["test/**/*.ts", "evals/**/*.ts"]
7 | }
8 | 
```

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

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

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

```typescript
1 | import { defineConfig } from 'vitest/config';
2 | 
3 | export default defineConfig({
4 |   test: {
5 |     include: ['**/*.{test,spec}.{js,ts}'],
6 |   },
7 | });
8 | 
```

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

```json
1 | {
2 |   "compilerOptions": {
3 |     "composite": true,
4 |     "module": "esnext",
5 |     "moduleResolution": "node"
6 |   },
7 |   "include": ["vite.config.ts"]
8 | }
9 | 
```

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

```typescript
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 | 
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 |   plugins: [react()]
7 | })
8 | 
```

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

```javascript
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 |   content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
4 |   theme: {
5 |     extend: {},
6 |   },
7 |   plugins: [],
8 | };
9 | 
```

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

```typescript
 1 | import './polyfills';
 2 | import React from 'react';
 3 | import ReactDOM from 'react-dom/client';
 4 | import App from './App';
 5 | import './index.css';
 6 | ReactDOM.createRoot(document.getElementById('root')!).render(
 7 |   <React.StrictMode>
 8 |     <App />
 9 |   </React.StrictMode>
10 | );
11 | 
```

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

```html
 1 | <!DOCTYPE html>
 2 | <html lang="en">
 3 |   <head>
 4 |     <meta charset="UTF-8" />
 5 |     <link rel="icon" type="image/svg+xml" href="/src/favicon.ico" />
 6 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 7 |     <title>Node.js MCP Server</title>
 8 |    
 9 |   </head>
10 |   <body>
11 |     <div id="root"></div>
12 |     <script type="module" src="/src/main.tsx"></script>
13 |   </body>
14 | </html>
15 | 
```

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

```json
 1 | {
 2 |   "inputs": [
 3 |     {
 4 |       "type": "promptString",
 5 |       "id": "id_files_dir_node_code_sandbox_mcp",
 6 |       "description": "Files directory for the Node Code Sandbox MCP",
 7 |       "password": false
 8 |     }
 9 |   ],
10 |   "servers": {
11 |     "node-code-sandbox-mcp (dev)": {
12 |       "type": "stdio",
13 |       "command": "node",
14 |       "args": ["${workspaceFolder}/src/server.ts"],
15 |       "env": {
16 |         "FILES_DIR": "${input:id_files_dir_node_code_sandbox_mcp}"
17 |       }
18 |     }
19 |   }
20 | }
21 | 
```

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

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ESNext",
 4 |     "useDefineForClassFields": true,
 5 |     "lib": ["DOM", "DOM.Iterable", "ESNext"],
 6 |     "allowJs": false,
 7 |     "skipLibCheck": true,
 8 |     "esModuleInterop": false,
 9 |     "allowSyntheticDefaultImports": true,
10 |     "strict": true,
11 |     "forceConsistentCasingInFileNames": true,
12 |     "module": "ESNext",
13 |     "moduleResolution": "Node",
14 |     "resolveJsonModule": true,
15 |     "isolatedModules": true,
16 |     "noEmit": true,
17 |     "jsx": "react-jsx"
18 |   },
19 |   "include": ["src"],
20 |   "references": [{ "path": "./tsconfig.node.json" }]
21 | }
22 | 
```

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

```yaml
 1 | name: Publish Package
 2 | 
 3 | on:
 4 |   push:
 5 |     # only fire on tag pushes matching semver-style vMAJOR.MINOR.PATCH
 6 |     tags:
 7 |       - 'v*.*.*'
 8 | 
 9 | jobs:
10 |   publish:
11 |     runs-on: ubuntu-latest
12 |     steps:
13 |       - name: Checkout repository
14 |         uses: actions/checkout@v3
15 | 
16 |       - name: Setup Node.js
17 |         uses: actions/setup-node@v3
18 |         with:
19 |           node-version: '23'
20 |           registry-url: 'https://registry.npmjs.org'
21 | 
22 |       - name: Install dependencies
23 |         run: npm ci
24 | 
25 |       - name: Publish to npm
26 |         run: npm publish
27 |         env:
28 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
29 | 
```

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

```json
 1 | {
 2 |   "[javascript]": {
 3 |     "editor.defaultFormatter": "esbenp.prettier-vscode"
 4 |   },
 5 |   "[json]": {
 6 |     "editor.defaultFormatter": "esbenp.prettier-vscode"
 7 |   },
 8 |   "[jsonc]": {
 9 |     "editor.defaultFormatter": "esbenp.prettier-vscode"
10 |   },
11 |   "[typescript]": {
12 |     "editor.defaultFormatter": "esbenp.prettier-vscode"
13 |   },
14 |   "editor.codeActionsOnSave": {
15 |     "quickfix.biome": "explicit",
16 |     "source.organizeImports.biome": "explicit"
17 |   },
18 |   "editor.defaultFormatter": "esbenp.prettier-vscode",
19 |   "editor.formatOnPaste": true,
20 |   "editor.formatOnSave": true,
21 |   "typescript.tsdk": "node_modules/typescript/lib"
22 | }
23 | 
```

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

```typescript
 1 | import React from 'react';
 2 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
 3 | import Home from './pages/Home';
 4 | import NodeMCPServer from './pages/NodeMCPServer';
 5 | import TinyAgent from './pages/TinyAgent';
 6 | import GraphGPT from './pages/GraphGPT';
 7 | 
 8 | const App: React.FC = () => {
 9 |   return (
10 |     <Router>
11 |       <Routes>
12 |         <Route path="/" element={<Home />} />
13 |         <Route path="/mcp" element={<NodeMCPServer />} />
14 |         <Route path="/tiny-agent" element={<TinyAgent />} />
15 |         <Route path="/graph-gpt" element={<GraphGPT />} />
16 |       </Routes>
17 |     </Router>
18 |   );
19 | };
20 | 
21 | export default App;
22 | 
```

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

```json
 1 | {
 2 |   "name": "my-project",
 3 |   "private": true,
 4 |   "version": "0.0.0",
 5 |   "scripts": {
 6 |     "dev": "vite",
 7 |     "build": "tsc && vite build",
 8 |     "preview": "vite preview"
 9 |   },
10 |   "dependencies": {
11 |     "@types/react-router-dom": "^5.3.3",
12 |     "buffer": "^6.0.3",
13 |     "clsx": "^2.1.1",
14 |     "ethers": "^5.6.9",
15 |     "lucide-react": "^0.509.0",
16 |     "react": "^18.0.0",
17 |     "react-dom": "^18.0.0",
18 |     "react-icons": "^4.4.0",
19 |     "react-router-dom": "^7.8.1"
20 |   },
21 |   "devDependencies": {
22 |     "@types/react": "^18.0.0",
23 |     "@types/react-dom": "^18.0.0",
24 |     "@vitejs/plugin-react": "^1.3.0",
25 |     "autoprefixer": "^10.4.7",
26 |     "postcss": "^8.4.14",
27 |     "tailwindcss": "^3.1.4",
28 |     "typescript": "^4.6.3",
29 |     "vite": "^3.0.0"
30 |   }
31 | }
32 | 
```

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

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "allowImportingTsExtensions": true,
 4 |     "baseUrl": ".",
 5 |     "erasableSyntaxOnly": true,
 6 |     "esModuleInterop": true,
 7 |     "forceConsistentCasingInFileNames": true,
 8 |     "isolatedModules": true,
 9 |     "module": "NodeNext",
10 |     "moduleResolution": "NodeNext",
11 |     "newLine": "lf",
12 |     "noFallthroughCasesInSwitch": true,
13 |     "noUnusedLocals": true,
14 |     "outDir": "dist",
15 |     "removeComments": true,
16 |     "resolveJsonModule": true,
17 |     "rewriteRelativeImportExtensions": true,
18 |     "skipLibCheck": true,
19 |     "sourceMap": true,
20 |     "strict": true,
21 |     "target": "ESNext",
22 |     "verbatimModuleSyntax": true
23 |   },
24 |   "include": ["src/**/*.ts", "test/**/*.ts", "evals/**/*.ts"],
25 |   "exclude": ["node_modules"]
26 | }
27 | 
```

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

```yaml
 1 | name: Run Docker-Based Tests
 2 | 
 3 | on:
 4 |   pull_request:
 5 |     branches:
 6 |       - master
 7 |   workflow_dispatch: {}
 8 | 
 9 | jobs:
10 |   test:
11 |     runs-on: ubuntu-latest
12 |     steps:
13 |       - name: Checkout code
14 |         uses: actions/checkout@v4
15 | 
16 |       - name: Set up Node.js 23
17 |         uses: actions/setup-node@v4
18 |         with:
19 |           node-version: '23'
20 | 
21 |       - name: Install dependencies
22 |         run: npm ci
23 |       - name: Pull required Docker images
24 |         run: |
25 |           docker pull --platform=linux/amd64 node:lts-slim
26 |           docker pull --platform=linux/amd64 mcr.microsoft.com/playwright:v1.53.2-noble
27 |           docker pull --platform=linux/amd64 alfonsograziano/node-code-sandbox-mcp:latest
28 | 
29 |       - name: Run tests
30 |         run: npm test
31 | 
```

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

```yaml
 1 | name: Publish Multi-Arch Image
 2 | on:
 3 |   push:
 4 |     # only fire on tag pushes matching vMAJOR.MINOR.PATCH
 5 |     tags:
 6 |       - 'v*.*.*'
 7 |   workflow_dispatch: {}
 8 | jobs:
 9 |   build:
10 |     runs-on: ubuntu-latest
11 |     steps:
12 |       - name: Checkout code
13 |         uses: actions/checkout@v3
14 | 
15 |       - name: Set up QEMU for cross-build
16 |         uses: docker/setup-qemu-action@v3
17 | 
18 |       - name: Set up Docker Buildx
19 |         uses: docker/setup-buildx-action@v3
20 | 
21 |       - name: Log in to Docker Hub
22 |         uses: docker/login-action@v3
23 |         with:
24 |           username: ${{ secrets.DOCKER_USERNAME }}
25 |           password: ${{ secrets.DOCKER_PASSWORD }}
26 | 
27 |       - name: Build & push multi-arch image
28 |         uses: docker/build-push-action@v6
29 |         with:
30 |           context: .
31 |           platforms: linux/amd64,linux/arm64
32 | 
```

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

```typescript
 1 | export type McpContentText = {
 2 |   type: 'text';
 3 |   text: string;
 4 | };
 5 | 
 6 | export type McpContentImage = {
 7 |   type: 'image';
 8 |   data: string;
 9 |   mimeType: string;
10 | };
11 | 
12 | export type McpContentAudio = {
13 |   type: 'audio';
14 |   data: string;
15 |   mimeType: string;
16 | };
17 | 
18 | export type McpContentTextResource = {
19 |   type: 'resource';
20 |   resource: {
21 |     text: string;
22 |     uri: string;
23 |     mimeType?: string;
24 |   };
25 | };
26 | 
27 | export type McpContentResource = {
28 |   type: 'resource';
29 |   resource:
30 |     | {
31 |         text: string;
32 |         uri: string;
33 |         mimeType?: string;
34 |       }
35 |     | {
36 |         uri: string;
37 |         blob: string;
38 |         mimeType?: string;
39 |       };
40 | };
41 | 
42 | export type McpContent =
43 |   | McpContentText
44 |   | McpContentImage
45 |   | McpContentAudio
46 |   | McpContentResource;
47 | 
48 | export type McpResponse = {
49 |   content: McpContent[];
50 |   _meta?: Record<string, unknown>;
51 |   isError?: boolean;
52 | };
53 | 
54 | export const textContent = (text: string): McpContent => ({
55 |   type: 'text',
56 |   text,
57 | });
58 | 
```

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

```yaml
 1 | name: Publish Node-Chartjs-Canvas Multi-Arch Image
 2 | on:
 3 |   push:
 4 |     # only fire on tag pushes matching vMAJOR.MINOR.PATCH
 5 |     tags:
 6 |       - 'v*.*.*'
 7 |     paths:
 8 |       # Trigger only when Dockerfile changes
 9 |       - 'images/node-chartjs-canvas/Dockerfile'
10 |   workflow_dispatch: {}
11 | jobs:
12 |   build:
13 |     runs-on: ubuntu-latest
14 |     steps:
15 |       - name: Checkout code
16 |         uses: actions/checkout@v3
17 | 
18 |       - name: Set up QEMU for cross-build
19 |         uses: docker/setup-qemu-action@v3
20 | 
21 |       - name: Set up Docker Buildx
22 |         uses: docker/setup-buildx-action@v3
23 | 
24 |       - name: Log in to Docker Hub
25 |         uses: docker/login-action@v3
26 |         with:
27 |           username: ${{ secrets.DOCKER_USERNAME }}
28 |           password: ${{ secrets.DOCKER_PASSWORD }}
29 | 
30 |       - name: Build & push multi-arch image
31 |         uses: docker/build-push-action@v6
32 |         with:
33 |           context: ./images/node-chartjs-canvas
34 |           platforms: linux/amd64,linux/arm64
35 |           push: true
36 |           tags: |
37 |             ${{ secrets.DOCKER_USERNAME }}/node-chartjs-canvas:latest
38 | 
```

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

```javascript
 1 | import path from 'path';
 2 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 3 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 4 | 
 5 | async function main() {
 6 |   // 1️⃣ Create the MCP client
 7 |   const client = new Client({
 8 |     name: 'ephemeral-example',
 9 |     version: '1.0.0',
10 |   });
11 | 
12 |   // 2️⃣ Connect to your js-sandbox-mcp server
13 |   await client.connect(
14 |     new StdioClientTransport({
15 |       command: 'npm',
16 |       args: ['run', 'dev'],
17 |       cwd: path.resolve('..'),
18 |       env: { ...process.env },
19 |     })
20 |   );
21 | 
22 |   console.log('✅ Connected to js-sandbox-mcp');
23 | 
24 |   // 3️⃣ Use the new run_js_ephemeral tool in one step
25 |   const result = await client.callTool({
26 |     name: 'run_js_ephemeral',
27 |     arguments: {
28 |       image: 'node:lts-slim',
29 |       code: `
30 |         import { randomUUID } from 'node:crypto';
31 |         console.log('Ephemeral run! Your UUID is', randomUUID());
32 |       `,
33 |       dependencies: [],
34 |     },
35 |   });
36 | 
37 |   console.log('▶️ run_js_ephemeral output:\n', result.content[0].text);
38 | 
39 |   process.exit(0);
40 | }
41 | 
42 | main().catch((err) => {
43 |   console.error('❌ Error in ephemeral example:', err);
44 |   process.exit(1);
45 | });
46 | 
```

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

```typescript
 1 | import { execFileSync } from 'node:child_process';
 2 | import { describe, it } from 'vitest';
 3 | import path from 'path';
 4 | /**
 5 |  * Utility to check if a Docker container is running
 6 |  */
 7 | export function isContainerRunning(containerId: string): boolean {
 8 |   try {
 9 |     const output = execFileSync(
10 |       'docker',
11 |       ['inspect', '-f', '{{.State.Running}}', containerId],
12 |       { encoding: 'utf8' }
13 |     ).trim();
14 |     return output === 'true';
15 |   } catch {
16 |     return false;
17 |   }
18 | }
19 | 
20 | /**
21 |  * Utility to check if a Docker container exists
22 |  */
23 | export function containerExists(containerId: string): boolean {
24 |   try {
25 |     execFileSync('docker', ['inspect', containerId]);
26 |     return true;
27 |   } catch {
28 |     return false;
29 |   }
30 | }
31 | 
32 | export const describeIfLocal = process.env.CI ? describe.skip : describe;
33 | export const testIfLocal = process.env.CI ? it.skip : it;
34 | 
35 | export function normalizeMountPath(hostPath: string) {
36 |   if (process.platform === 'win32') {
37 |     // e.g. C:\Users\alfon\Temp\ws-abc  →  /c/Users/alfon/Temp/ws-abc
38 |     const drive = hostPath[0].toLowerCase();
39 |     const rest = hostPath.slice(2).split(path.sep).join('/');
40 |     return `/${drive}/${rest}`;
41 |   }
42 |   return hostPath;
43 | }
44 | 
```

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

```typescript
 1 | import { z } from 'zod';
 2 | import { execFileSync } from 'node:child_process';
 3 | import { type McpResponse, textContent } from '../types.ts';
 4 | import {
 5 |   DOCKER_NOT_RUNNING_ERROR,
 6 |   isDockerRunning,
 7 |   sanitizeContainerId,
 8 |   sanitizeShellCommand,
 9 | } from '../utils.ts';
10 | 
11 | export const argSchema = {
12 |   container_id: z.string(),
13 |   commands: z.array(z.string().min(1)),
14 | };
15 | 
16 | export default async function execInSandbox({
17 |   container_id,
18 |   commands,
19 | }: {
20 |   container_id: string;
21 |   commands: string[];
22 | }): Promise<McpResponse> {
23 |   const validId = sanitizeContainerId(container_id);
24 |   if (!validId) {
25 |     return {
26 |       content: [textContent('Invalid container ID')],
27 |     };
28 |   }
29 | 
30 |   if (!isDockerRunning()) {
31 |     return {
32 |       content: [textContent(DOCKER_NOT_RUNNING_ERROR)],
33 |     };
34 |   }
35 | 
36 |   const output: string[] = [];
37 |   for (const cmd of commands) {
38 |     const sanitizedCmd = sanitizeShellCommand(cmd);
39 |     if (!sanitizedCmd)
40 |       throw new Error(
41 |         'Cannot run command as it contains dangerous metacharacters'
42 |       );
43 |     output.push(
44 |       execFileSync('docker', ['exec', validId, '/bin/sh', '-c', sanitizedCmd], {
45 |         encoding: 'utf8',
46 |       })
47 |     );
48 |   }
49 |   return { content: [textContent(output.join('\n'))] };
50 | }
51 | 
```

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

```dockerfile
 1 | # Build dependencies
 2 | FROM node:lts AS builder
 3 | 
 4 | RUN apt-get update && \
 5 |     apt-get install -y --no-install-recommends debian-archive-keyring && \
 6 |     apt-get update && \
 7 |     apt-get install -y --no-install-recommends \
 8 |     build-essential python3 pkg-config \
 9 |     libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev && \
10 |     rm -rf /var/lib/apt/lists/*
11 | 
12 | WORKDIR /build
13 | RUN npm install --omit=dev --prefer-offline --no-audit [email protected] @mermaid-js/mermaid-cli
14 | 
15 | # Chromium stage
16 | FROM node:lts-slim
17 | 
18 | # Runtime dependencies only
19 | RUN apt-get update && \
20 |     apt-get install -y --no-install-recommends debian-archive-keyring && \
21 |     apt-get update && \
22 |     apt-get install -y --no-install-recommends \
23 |     libcairo2 libpango1.0-0 libjpeg62-turbo libgif7 librsvg2-2 \
24 |     chromium && \
25 |     rm -rf /var/lib/apt/lists/*
26 | 
27 | RUN groupadd -r chromium && \
28 |     useradd -r -g chromium -G audio,video chromium && \
29 |     mkdir -p /home/chromium /workspace && \
30 |     chown -R chromium:chromium /home/chromium /workspace
31 | 
32 | WORKDIR /workspace
33 | COPY --from=builder --chown=chromium:chromium /build/node_modules ./node_modules
34 | 
35 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 \
36 |     PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
37 |     HOME=/home/chromium
38 | 
39 | USER chromium
40 | 
41 | RUN ./node_modules/.bin/mmdc -h
42 | 
```

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

```javascript
 1 | import path from 'path';
 2 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 3 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 4 | 
 5 | async function main() {
 6 |   // 1️⃣ Create the MCP client
 7 |   const client = new Client({
 8 |     name: 'ephemeral-example',
 9 |     version: '1.0.0',
10 |   });
11 | 
12 |   // 2️⃣ Connect to your js-sandbox-mcp server
13 |   await client.connect(
14 |     new StdioClientTransport({
15 |       command: 'docker',
16 |       args: [
17 |         'run',
18 |         '-i',
19 |         '--rm',
20 |         '-v',
21 |         '/var/run/docker.sock:/var/run/docker.sock',
22 |         'alfonsograziano/node-code-sandbox-mcp',
23 |       ],
24 |       env: {
25 |         PATH: process.env.PATH,
26 |       },
27 |     })
28 |   );
29 | 
30 |   console.log('✅ Connected to js-sandbox-mcp');
31 | 
32 |   // 3️⃣ Use the new run_js_ephemeral tool in one step
33 |   const result = await client.callTool({
34 |     name: 'run_js_ephemeral',
35 |     arguments: {
36 |       image: 'node:lts-slim',
37 |       code: `
38 |         import { randomUUID } from 'node:crypto';
39 |         console.log('Ephemeral run! Your UUID is', randomUUID());
40 |       `,
41 |       dependencies: [],
42 |     },
43 |   });
44 | 
45 |   console.log('▶️ run_js_ephemeral output:\n', result.content[0].text);
46 | 
47 |   process.exit(0);
48 | }
49 | 
50 | main().catch((err) => {
51 |   console.error('❌ Error in ephemeral example:', err);
52 |   process.exit(1);
53 | });
54 | 
```

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

```javascript
 1 | import path from 'path';
 2 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 3 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 4 | 
 5 | async function main() {
 6 |   // 1️⃣ Create the MCP client
 7 |   const client = new Client({
 8 |     name: 'ephemeral-with-deps-example',
 9 |     version: '1.0.0',
10 |   });
11 | 
12 |   // 2️⃣ Connect to your js-sandbox-mcp server
13 |   await client.connect(
14 |     new StdioClientTransport({
15 |       command: 'npm',
16 |       args: ['run', 'dev'],
17 |       cwd: path.resolve('..'),
18 |       env: { ...process.env },
19 |     })
20 |   );
21 | 
22 |   console.log('✅ Connected to js-sandbox-mcp');
23 | 
24 |   // 3️⃣ Use the run_js_ephemeral tool with a dependency (lodash)
25 |   const result = await client.callTool({
26 |     name: 'run_js_ephemeral',
27 |     arguments: {
28 |       image: 'node:lts-slim',
29 |       code: `
30 |         import _ from 'lodash';
31 |         const names = ['Alice', 'Bob', 'Carol', 'Dave'];
32 |         const shuffled = _.shuffle(names);
33 |         console.log('Shuffled names:', shuffled.join(', '));
34 |       `,
35 |       dependencies: [
36 |         {
37 |           name: 'lodash',
38 |           version: '^4.17.21',
39 |         },
40 |       ],
41 |     },
42 |   });
43 | 
44 |   console.log('▶️ run_js_ephemeral output:\n', result.content[0].text);
45 | 
46 |   process.exit(0);
47 | }
48 | 
49 | main().catch((err) => {
50 |   console.error('❌ Error in ephemeral-with-deps example:', err);
51 |   process.exit(1);
52 | });
53 | 
```

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

```typescript
 1 | import { describe, it, expect } from 'vitest';
 2 | import initializeSandbox from '../src/tools/initialize.ts';
 3 | import stopSandbox from '../src/tools/stop.ts';
 4 | import execInSandbox from '../src/tools/exec.ts';
 5 | import { containerExists, isContainerRunning } from './utils.ts';
 6 | 
 7 | describe('sandbox full lifecycle', () => {
 8 |   it('should create, exec in, and remove a Docker sandbox container', async () => {
 9 |     // Step 1: Start container
10 |     const start = await initializeSandbox({});
11 |     const content = start.content[0];
12 |     if (content.type !== 'text') throw new Error('Unexpected content type');
13 |     const containerId = content.text;
14 | 
15 |     expect(containerId).toMatch(/^js-sbx-/);
16 |     expect(isContainerRunning(containerId)).toBe(true);
17 | 
18 |     // Step 2: Execute command
19 |     const execResult = await execInSandbox({
20 |       container_id: containerId,
21 |       commands: ['echo Hello World', 'uname -a'],
22 |     });
23 | 
24 |     const execOutput = execResult.content[0];
25 |     if (execOutput.type !== 'text') throw new Error('Unexpected content type');
26 | 
27 |     expect(execOutput.text).toContain('Hello World');
28 |     expect(execOutput.text).toMatch(/Linux|Unix/); // should match OS output
29 | 
30 |     // Step 3: Stop container
31 |     const stop = await stopSandbox({ container_id: containerId });
32 |     const stopMsg = stop.content[0];
33 |     if (stopMsg.type !== 'text') throw new Error('Unexpected content type');
34 | 
35 |     expect(stopMsg.text).toContain(`Container ${containerId} removed.`);
36 |     expect(containerExists(containerId)).toBe(false);
37 |   });
38 | });
39 | 
```

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

```javascript
 1 | // eslint.config.js
 2 | 
 3 | import js from '@eslint/js'; // for built‑in configs
 4 | import { FlatCompat } from '@eslint/eslintrc'; // to translate shareables
 5 | import tsParser from '@typescript-eslint/parser';
 6 | import tsPlugin from '@typescript-eslint/eslint-plugin';
 7 | import prettierPlugin from 'eslint-plugin-prettier';
 8 | import { fileURLToPath } from 'url';
 9 | import { dirname } from 'path';
10 | 
11 | // reproduce __dirname in ESM
12 | const __filename = fileURLToPath(import.meta.url);
13 | const __dirname = dirname(__filename);
14 | 
15 | // Tell FlatCompat about eslint:recommended (and eslint:all, if you ever need it)
16 | const compat = new FlatCompat({
17 |   baseDirectory: __dirname,
18 |   recommendedConfig: js.configs.recommended,
19 |   allConfig: js.configs.all,
20 | });
21 | 
22 | export default [
23 |   // --- ignore patterns ---
24 |   {
25 |     ignores: [
26 |       'node_modules',
27 |       'dist',
28 |       '*.log',
29 |       'coverage',
30 |       '.env',
31 |       'package-lock.json',
32 |       'yarn.lock',
33 |       'pnpm-lock.yaml',
34 |       'examples',
35 |     ],
36 |   },
37 | 
38 |   // bring in eslint:recommended, plugin:@typescript-eslint/recommended & prettier
39 |   ...compat.extends(
40 |     'eslint:recommended',
41 |     'plugin:@typescript-eslint/recommended',
42 |     'prettier'
43 |   ),
44 | 
45 |   // our overrides for TypeScript files
46 |   {
47 |     files: ['*.ts', '*.tsx'],
48 |     languageOptions: {
49 |       parser: tsParser,
50 |       parserOptions: {
51 |         ecmaVersion: 12,
52 |         sourceType: 'module',
53 |       },
54 |     },
55 |     plugins: {
56 |       '@typescript-eslint': tsPlugin,
57 |       prettier: prettierPlugin,
58 |     },
59 |     rules: {
60 |       'prettier/prettier': 'error',
61 |     },
62 |   },
63 | ];
64 | 
```

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

```typescript
 1 | import { z } from 'zod';
 2 | import { execFileSync } from 'node:child_process';
 3 | import { type McpResponse, textContent } from '../types.ts';
 4 | import {
 5 |   DOCKER_NOT_RUNNING_ERROR,
 6 |   isDockerRunning,
 7 |   sanitizeContainerId,
 8 | } from '../utils.ts';
 9 | import { activeSandboxContainers } from '../containerUtils.ts';
10 | 
11 | export const argSchema = {
12 |   container_id: z.string().regex(/^[a-zA-Z0-9_.-]+$/, 'Invalid container ID'),
13 | };
14 | 
15 | export default async function stopSandbox({
16 |   container_id,
17 | }: {
18 |   container_id: string;
19 | }): Promise<McpResponse> {
20 |   if (!isDockerRunning()) {
21 |     return {
22 |       content: [textContent(DOCKER_NOT_RUNNING_ERROR)],
23 |     };
24 |   }
25 | 
26 |   const validId = sanitizeContainerId(container_id);
27 |   if (!validId) {
28 |     return {
29 |       content: [textContent('Invalid container ID')],
30 |     };
31 |   }
32 | 
33 |   try {
34 |     // Use execFileSync with validated container_id
35 |     execFileSync('docker', ['rm', '-f', validId]);
36 |     activeSandboxContainers.delete(validId);
37 | 
38 |     return {
39 |       content: [textContent(`Container ${container_id} removed.`)],
40 |     };
41 |   } catch (error) {
42 |     // Handle any errors that occur during container removal
43 |     const errorMessage = error instanceof Error ? error.message : String(error);
44 |     console.error(
45 |       `[stopSandbox] Error removing container ${container_id}: ${errorMessage}`
46 |     );
47 | 
48 |     // Still remove from our registry even if Docker command failed
49 |     activeSandboxContainers.delete(validId);
50 | 
51 |     return {
52 |       content: [
53 |         textContent(
54 |           `Error removing container ${container_id}: ${errorMessage}`
55 |         ),
56 |       ],
57 |     };
58 |   }
59 | }
60 | 
```

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

```javascript
 1 | import path from 'path';
 2 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 3 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 4 | 
 5 | async function main() {
 6 |   // 1️⃣ Create the MCP client
 7 |   const client = new Client({
 8 |     name: 'ephemeral-example',
 9 |     version: '1.0.0',
10 |   });
11 | 
12 |   // 2️⃣ Connect to your js-sandbox-mcp server
13 |   await client.connect(
14 |     new StdioClientTransport({
15 |       command: 'npm',
16 |       args: ['run', 'dev'],
17 |       cwd: path.resolve('..'),
18 |       env: {
19 |         ...process.env,
20 |         //TODO: Change this with your user!
21 |         FILES_DIR: '/Users/your_user/Desktop',
22 |       },
23 |     })
24 |   );
25 | 
26 |   console.log('✅ Connected to js-sandbox-mcp');
27 | 
28 |   // 3️⃣ Use the new run_js_ephemeral tool in one step
29 |   const result = await client.callTool({
30 |     name: 'run_js_ephemeral',
31 |     arguments: {
32 |       // Use the ofcicial MS playwright image
33 |       image: 'mcr.microsoft.com/playwright:v1.53.2-noble',
34 |       code: `
35 |         import { chromium } from 'playwright';
36 |   
37 |         (async () => {
38 |           const browser = await chromium.launch();
39 |           const page = await browser.newPage();
40 |           await page.goto('https://example.com');
41 |           await page.screenshot({ path: 'screenshot_test.png' });
42 |           await browser.close();
43 |         })();
44 |       `,
45 |       dependencies: [
46 |         {
47 |           name: 'playwright',
48 |           version: '^1.52.0',
49 |         },
50 |       ],
51 |     },
52 |   });
53 | 
54 |   console.log('▶️ run_js_ephemeral output:\n', result.content[0].text);
55 | 
56 |   process.exit(0);
57 | }
58 | 
59 | main().catch((err) => {
60 |   console.error('❌ Error in ephemeral example:', err);
61 |   process.exit(1);
62 | });
63 | 
```

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

```typescript
 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | 
 3 | // Will be set once the server is initialized
 4 | let serverInstance: McpServer | null = null;
 5 | 
 6 | /**
 7 |  * Set the server instance for logging
 8 |  * @param server MCP server instance
 9 |  */
10 | export function setServerInstance(server: McpServer): void {
11 |   serverInstance = server;
12 | }
13 | 
14 | /**
15 |  * Log levels supported by MCP
16 |  */
17 | export type LogLevel = 'debug' | 'info' | 'warning' | 'error';
18 | 
19 | /**
20 |  * Send a logging message using the MCP protocol
21 |  * Falls back to console.error if server is not initialized
22 |  * @param level Log level
23 |  * @param message Message to log
24 |  * @param data Optional data to include
25 |  */
26 | export function log(level: LogLevel, message: string, data?: unknown): void {
27 |   if (serverInstance) {
28 |     // Access the server through the internal server property
29 |     // @ts-expect-error - _server is not documented in the public API but is available
30 |     const internalServer = serverInstance._server;
31 |     if (
32 |       internalServer &&
33 |       typeof internalServer.sendLoggingMessage === 'function'
34 |     ) {
35 |       internalServer.sendLoggingMessage({
36 |         level,
37 |         data: data ? `${message}: ${JSON.stringify(data)}` : message,
38 |       });
39 |       return;
40 |     }
41 |   }
42 | 
43 |   // Fallback if server is not initialized yet or doesn't support logging
44 |   console.error(`[${level.toUpperCase()}] ${message}`, data || '');
45 | }
46 | 
47 | /**
48 |  * Convenience methods for different log levels
49 |  */
50 | export const logger = {
51 |   debug: (message: string, data?: unknown) => log('debug', message, data),
52 |   info: (message: string, data?: unknown) => log('info', message, data),
53 |   warning: (message: string, data?: unknown) => log('warning', message, data),
54 |   error: (message: string, data?: unknown) => log('error', message, data),
55 | };
56 | 
```

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

```javascript
 1 | import path from 'path';
 2 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 3 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 4 | 
 5 | async function main() {
 6 |   // 1️⃣ Create the MCP client
 7 |   const client = new Client({
 8 |     name: 'example-client',
 9 |     version: '1.0.0',
10 |   });
11 | 
12 |   // 2️⃣ Launch & connect to the js-sandbox-mcp server
13 |   await client.connect(
14 |     new StdioClientTransport({
15 |       command: 'npm',
16 |       args: ['run', 'dev'], // runs `npm run dev` in the sandbox folder
17 |       cwd: path.resolve('..'),
18 |     })
19 |   );
20 | 
21 |   console.log('✅ Connected to js-sandbox-mcp');
22 | 
23 |   // 3️⃣ List available tools
24 |   const tools = await client.listTools();
25 |   console.log('🔧 Available tools:');
26 |   console.dir(tools, { depth: null });
27 | 
28 |   // 4️⃣ Initialize a fresh sandbox container
29 |   const initResult = await client.callTool({
30 |     name: 'sandbox_initialize',
31 |     arguments: {
32 |       /* no args = uses default node:lts-slim */
33 |     },
34 |   });
35 |   const containerId = initResult.content[0].text;
36 |   console.log(`🐳 Container started: ${containerId}`);
37 | 
38 |   // 5️⃣ Run a JS snippet inside the container
39 |   const runResult = await client.callTool({
40 |     name: 'run_js',
41 |     arguments: {
42 |       container_id: containerId,
43 |       code: `
44 |         import { randomUUID } from 'node:crypto';
45 |         console.log('Hello from sandbox! Your UUID is', randomUUID());
46 |       `,
47 |       dependencies: [],
48 |     },
49 |   });
50 |   console.log('▶️ run_js output:\n', runResult.content[0].text);
51 | 
52 |   // 6️⃣ Tear down the container
53 |   const stopResult = await client.callTool({
54 |     name: 'sandbox_stop',
55 |     arguments: { container_id: containerId },
56 |   });
57 |   console.log('🛑', stopResult.content[0].text);
58 | 
59 |   process.exit(0);
60 | }
61 | 
62 | main().catch((err) => {
63 |   console.error('❌ Error in example:', err);
64 |   process.exit(1);
65 | });
66 | 
```

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

```typescript
 1 | import React from 'react';
 2 | import { Link } from 'react-router-dom';
 3 | 
 4 | const Footer: React.FC = () => {
 5 |   return (
 6 |     <footer className="bg-gray-900 text-white py-12">
 7 |       <div className="max-w-6xl mx-auto px-6">
 8 |         <div className="grid grid-cols-1 md:grid-cols-4 gap-8">
 9 |           <div>
10 |             <h3 className="text-xl font-bold mb-4">jsdevai.com</h3>
11 |             <p className="text-gray-400">
12 |               The ultimate destination for JavaScript developers building
13 |               AI-powered applications.
14 |             </p>
15 |           </div>
16 |           <div>
17 |             <h4 className="font-semibold mb-4">Tools</h4>
18 |             <ul className="space-y-2 text-gray-400">
19 |               <li>
20 |                 <Link to="/mcp" className="hover:text-white transition">
21 |                   Node.js Sandbox MCP
22 |                 </Link>
23 |               </li>
24 |               <li>
25 |                 <Link to="/tiny-agent" className="hover:text-white transition">
26 |                   Tiny Agent
27 |                 </Link>
28 |               </li>
29 |               <li>
30 |                 <Link to="/graph-gpt" className="hover:text-white transition">
31 |                   GraphGPT
32 |                 </Link>
33 |               </li>
34 |             </ul>
35 |           </div>
36 | 
37 |           <div>
38 |             <h4 className="font-semibold mb-4">Community</h4>
39 |             <ul className="space-y-2 text-gray-400">
40 |               <li>
41 |                 <a href="#" className="hover:text-white transition">
42 |                   GitHub
43 |                 </a>
44 |               </li>
45 |             </ul>
46 |           </div>
47 |         </div>
48 |         <div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
49 |           <p>
50 |             &copy; 2025 jsdevai.com • Empowering JavaScript developers in the AI
51 |             revolution
52 |           </p>
53 |         </div>
54 |       </div>
55 |     </footer>
56 |   );
57 | };
58 | 
59 | export default Footer;
60 | 
```

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

```json
 1 | {
 2 |   "name": "node-code-sandbox-mcp",
 3 |   "description": "Run arbitrary JavaScript inside disposable Docker containers and install npm dependencies on the fly.",
 4 |   "version": "1.3.0",
 5 |   "type": "module",
 6 |   "bin": {
 7 |     "node-code-sandbox-mcp": "dist/server.js"
 8 |   },
 9 |   "files": [
10 |     "dist",
11 |     "NODE_GUIDELINES.md"
12 |   ],
13 |   "scripts": {
14 |     "dev": "node --env-file .env --watch src/server.ts",
15 |     "dev:evals": "node evals/index.ts",
16 |     "build": "rimraf dist && tsc -p tsconfig.build.json && shx chmod +x dist/*.js",
17 |     "start": "node dist/server.js",
18 |     "test": "vitest",
19 |     "test:coverage": "vitest --coverage",
20 |     "inspector": "npx @modelcontextprotocol/inspector npm run dev",
21 |     "lint": "eslint . --ext .ts --report-unused-disable-directives --max-warnings 0",
22 |     "format": "prettier --write .",
23 |     "check": "npm run lint && npm run format",
24 |     "pre-commit": "lint-staged",
25 |     "prepublishOnly": "npm run build",
26 |     "prepare": "husky",
27 |     "release": "standard-version",
28 |     "major": "npm run release -- --release-as major",
29 |     "minor": "npm run release -- --release-as minor",
30 |     "patch": "npm run release -- --release-as patch",
31 |     "push-release": "git push --follow-tags origin master"
32 |   },
33 |   "dependencies": {
34 |     "@eslint/eslintrc": "^3.1.0",
35 |     "@modelcontextprotocol/sdk": "^1.17.3",
36 |     "@typescript-eslint/eslint-plugin": "^8.40.0",
37 |     "@typescript-eslint/parser": "^8.40.0",
38 |     "dotenv": "^17.2.1",
39 |     "eslint": "^9.33.0",
40 |     "eslint-config-prettier": "^10.1.8",
41 |     "eslint-plugin-n": "^17.21.3",
42 |     "eslint-plugin-prettier": "^5.5.4",
43 |     "mime-types": "^3.0.1",
44 |     "npm-registry-sdk": "^1.2.1",
45 |     "openai": "^5.13.1",
46 |     "tmp": "^0.2.5"
47 |   },
48 |   "devDependencies": {
49 |     "@commitlint/cli": "^19.8.1",
50 |     "@commitlint/config-conventional": "^19.8.1",
51 |     "@types/lint-staged": "^14.0.0",
52 |     "@types/mime-types": "^3.0.1",
53 |     "@types/node": "^24.3.0",
54 |     "@types/tmp": "^0.2.6",
55 |     "@vitest/coverage-v8": "^3.2.4",
56 |     "husky": "^9.1.7",
57 |     "lint-staged": "^16.1.5",
58 |     "prettier": "^3.6.2",
59 |     "rimraf": "^6.0.1",
60 |     "shx": "^0.4.0",
61 |     "standard-version": "^9.5.0",
62 |     "typescript": "^5.9.2",
63 |     "vitest": "^3.2.4"
64 |   },
65 |   "engines": {
66 |     "node": ">=23.10.0"
67 |   }
68 | }
69 | 
```

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

```typescript
 1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
 2 | import initializeSandbox from '../src/tools/initialize.ts';
 3 | import * as childProcess from 'node:child_process';
 4 | import * as crypto from 'node:crypto';
 5 | import * as utils from '../src/utils.ts';
 6 | import * as types from '../src/types.ts';
 7 | 
 8 | vi.mock('node:child_process');
 9 | vi.mock('node:crypto');
10 | vi.mock('../types');
11 | 
12 | describe('initializeSandbox', () => {
13 |   const fakeUUID = '123e4567-e89b-12d3-a456-426614174000';
14 |   const fakeContainerName = `js-sbx-${fakeUUID}`;
15 | 
16 |   beforeEach(() => {
17 |     vi.resetAllMocks();
18 |     vi.spyOn(crypto, 'randomUUID').mockReturnValue(fakeUUID);
19 |     vi.spyOn(childProcess, 'execFileSync').mockImplementation(() =>
20 |       Buffer.from('')
21 |     );
22 |     vi.spyOn(types, 'textContent').mockImplementation((name) => ({
23 |       type: 'text',
24 |       text: name,
25 |     }));
26 |   });
27 | 
28 |   it('should return an error message if Docker is not running', async () => {
29 |     vi.spyOn(utils, 'isDockerRunning').mockReturnValue(false);
30 |     const result = await initializeSandbox({});
31 |     expect(result).toEqual({
32 |       content: [
33 |         {
34 |           type: 'text',
35 |           text: 'Error: Docker is not running. Please start Docker and try again.',
36 |         },
37 |       ],
38 |     });
39 |   });
40 | 
41 |   it('should use the default image when none is provided', async () => {
42 |     const result = await initializeSandbox({});
43 |     expect(childProcess.execFileSync).toHaveBeenCalledWith(
44 |       'docker',
45 |       expect.arrayContaining([
46 |         '--name',
47 |         fakeContainerName,
48 |         utils.DEFAULT_NODE_IMAGE,
49 |       ]),
50 |       expect.any(Object)
51 |     );
52 |     expect(result).toEqual({
53 |       content: [{ type: 'text', text: fakeContainerName }],
54 |     });
55 |   });
56 | 
57 |   it('should use the provided image', async () => {
58 |     const customImage = 'node:20-alpine';
59 |     const result = await initializeSandbox({ image: customImage });
60 |     expect(childProcess.execFileSync).toHaveBeenCalledWith(
61 |       'docker',
62 |       expect.arrayContaining(['--name', fakeContainerName, customImage]),
63 |       expect.any(Object)
64 |     );
65 |     if (result.content[0].type === 'text') {
66 |       expect(result.content[0].text).toBe(fakeContainerName);
67 |     } else {
68 |       throw new Error('Unexpected content type');
69 |     }
70 |   });
71 | });
72 | 
```

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

```typescript
 1 | import { z } from 'zod';
 2 | 
 3 | const DEFAULT_TIMEOUT_SECONDS = 3600;
 4 | const DEFAULT_RUN_SCRIPT_TIMEOUT = 30_000;
 5 | 
 6 | const envSchema = z.object({
 7 |   NODE_CONTAINER_TIMEOUT: z.string().optional(),
 8 |   RUN_SCRIPT_TIMEOUT: z.string().optional(),
 9 |   SANDBOX_MEMORY_LIMIT: z
10 |     .string()
11 |     .regex(/^\d+(\.\d+)?[mMgG]?$/, {
12 |       message: 'SANDBOX_MEMORY_LIMIT must be like "512m", "1g", or bytes',
13 |     })
14 |     .optional()
15 |     .nullable(),
16 |   SANDBOX_CPU_LIMIT: z
17 |     .string()
18 |     .regex(/^\d+(\.\d+)?$/, {
19 |       message: 'SANDBOX_CPU_LIMIT must be numeric (e.g. "0.5", "2")',
20 |     })
21 |     .optional()
22 |     .nullable(),
23 |   FILES_DIR: z.string().optional().nullable(),
24 | });
25 | 
26 | // Schema for the final config object with transformations and defaults
27 | const configSchema = z.object({
28 |   containerTimeoutSeconds: z.number().positive(),
29 |   containerTimeoutMilliseconds: z.number().positive(),
30 |   runScriptTimeoutMilliseconds: z.number().positive(),
31 |   rawMemoryLimit: z.string().optional(),
32 |   rawCpuLimit: z.string().optional(),
33 |   filesDir: z.string().optional(),
34 | });
35 | 
36 | function loadConfig() {
37 |   const parsedEnv = envSchema.safeParse(process.env);
38 | 
39 |   if (!parsedEnv.success) {
40 |     throw new Error('Invalid environment variables');
41 |   }
42 | 
43 |   const timeoutString = parsedEnv.data.NODE_CONTAINER_TIMEOUT;
44 |   let seconds = DEFAULT_TIMEOUT_SECONDS;
45 | 
46 |   if (timeoutString) {
47 |     const parsedSeconds = parseInt(timeoutString, 10);
48 |     if (!isNaN(parsedSeconds) && parsedSeconds > 0) {
49 |       seconds = parsedSeconds;
50 |     }
51 |   }
52 | 
53 |   const runScriptTimeoutMillisecondsString = parsedEnv.data.RUN_SCRIPT_TIMEOUT;
54 |   let runScriptTimeoutMilliseconds = DEFAULT_RUN_SCRIPT_TIMEOUT;
55 | 
56 |   if (runScriptTimeoutMillisecondsString) {
57 |     const parsedSeconds = parseInt(runScriptTimeoutMillisecondsString, 10);
58 |     if (!isNaN(parsedSeconds) && parsedSeconds > 0) {
59 |       runScriptTimeoutMilliseconds = parsedSeconds;
60 |     }
61 |   }
62 | 
63 |   const milliseconds = seconds * 1000;
64 |   const memRaw = parsedEnv.data.SANDBOX_MEMORY_LIMIT;
65 |   const cpuRaw = parsedEnv.data.SANDBOX_CPU_LIMIT;
66 |   const filesDir = parsedEnv.data.FILES_DIR;
67 | 
68 |   return configSchema.parse({
69 |     containerTimeoutSeconds: seconds,
70 |     containerTimeoutMilliseconds: milliseconds,
71 |     runScriptTimeoutMilliseconds: runScriptTimeoutMilliseconds,
72 |     rawMemoryLimit: memRaw,
73 |     rawCpuLimit: cpuRaw,
74 |     filesDir: filesDir,
75 |   });
76 | }
77 | 
78 | export const getConfig = loadConfig;
79 | 
```

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

```typescript
  1 | import fs from 'fs/promises';
  2 | import path from 'path';
  3 | import tmp from 'tmp';
  4 | import { pathToFileURL } from 'url';
  5 | import mime from 'mime-types';
  6 | import { textContent, type McpContent } from './types.ts';
  7 | import { isRunningInDocker } from './utils.ts';
  8 | import { getConfig } from './config.ts';
  9 | 
 10 | export async function prepareWorkspace({
 11 |   code,
 12 |   dependenciesRecord,
 13 | }: {
 14 |   code: string;
 15 |   dependenciesRecord: Record<string, string>;
 16 | }) {
 17 |   const localTmp = tmp.dirSync({ unsafeCleanup: true });
 18 | 
 19 |   await fs.writeFile(path.join(localTmp.name, 'index.js'), code);
 20 |   await fs.writeFile(
 21 |     path.join(localTmp.name, 'package.json'),
 22 |     JSON.stringify(
 23 |       { type: 'module', dependencies: dependenciesRecord },
 24 |       null,
 25 |       2
 26 |     )
 27 |   );
 28 | 
 29 |   return localTmp;
 30 | }
 31 | 
 32 | export async function extractOutputsFromDir({
 33 |   dirPath,
 34 |   outputDir,
 35 | }: {
 36 |   dirPath: string;
 37 |   outputDir: string;
 38 | }): Promise<McpContent[]> {
 39 |   const contents: McpContent[] = [];
 40 |   const imageTypes = new Set(['image/jpeg', 'image/png']);
 41 | 
 42 |   await fs.mkdir(outputDir, { recursive: true });
 43 | 
 44 |   const dirents = await fs.readdir(dirPath, { withFileTypes: true });
 45 | 
 46 |   for (const dirent of dirents) {
 47 |     if (!dirent.isFile()) continue;
 48 | 
 49 |     const fname = dirent.name;
 50 |     if (
 51 |       fname === 'index.js' ||
 52 |       fname === 'package.json' ||
 53 |       fname === 'package-lock.json'
 54 |     )
 55 |       continue;
 56 | 
 57 |     const fullPath = path.join(dirPath, fname);
 58 |     const destPath = path.join(outputDir, fname);
 59 |     await fs.copyFile(fullPath, destPath);
 60 | 
 61 |     const hostPath = path.join(getFilesDir(), fname);
 62 |     contents.push(textContent(`I saved the file ${fname} at ${hostPath}`));
 63 | 
 64 |     const mimeType = mime.lookup(fname) || 'application/octet-stream';
 65 | 
 66 |     if (imageTypes.has(mimeType)) {
 67 |       const b64 = await fs.readFile(fullPath, { encoding: 'base64' });
 68 |       contents.push({
 69 |         type: 'image',
 70 |         data: b64,
 71 |         mimeType,
 72 |       });
 73 |     }
 74 | 
 75 |     contents.push({
 76 |       type: 'resource',
 77 |       resource: {
 78 |         uri: pathToFileURL(hostPath).href,
 79 |         mimeType,
 80 |         text: fname,
 81 |       },
 82 |     });
 83 |   }
 84 | 
 85 |   return contents;
 86 | }
 87 | 
 88 | export function getHostOutputDir(): string {
 89 |   const isContainer = isRunningInDocker();
 90 |   return isContainer
 91 |     ? path.resolve(process.env.HOME || process.cwd())
 92 |     : getFilesDir();
 93 | }
 94 | 
 95 | // This FILES_DIR is an env var coming from the user
 96 | export const getFilesDir = (): string   => {
 97 |   return getConfig().filesDir!;             
 98 | };
 99 | 
100 | export const getMountFlag = (): string  => {
101 |   const dir = getFilesDir();
102 |   return dir ? `-v ${dir}:/workspace/files` : '';
103 | };
```

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

```typescript
 1 | import { ESLint } from 'eslint';
 2 | import globals from 'globals';
 3 | import nPlugin from 'eslint-plugin-n';
 4 | import prettierPlugin from 'eslint-plugin-prettier';
 5 | 
 6 | const eslint = new ESLint({
 7 |   fix: true,
 8 |   overrideConfigFile: 'eslint.config.js',
 9 | 
10 |   overrideConfig: {
11 |     plugins: {
12 |       n: nPlugin,
13 |       prettier: prettierPlugin,
14 |     },
15 |     languageOptions: {
16 |       ecmaVersion: 'latest',
17 |       sourceType: 'module',
18 |       globals: {
19 |         ...globals.node,
20 |       },
21 |     },
22 |     rules: {
23 |       // --- Best Practices & Bug Prevention for JS ---
24 |       eqeqeq: ['error', 'always'],
25 |       'no-unused-vars': ['warn', { args: 'none', ignoreRestSiblings: true }],
26 |       'no-console': ['warn', { allow: ['warn', 'error'] }],
27 |       'no-return-await': 'error',
28 |       'no-throw-literal': 'error',
29 | 
30 |       // --- Modern JavaScript & Code Style ---
31 |       'no-var': 'error',
32 |       'prefer-const': 'error',
33 |       'object-shorthand': 'error',
34 |       'prefer-template': 'error',
35 |       'prefer-arrow-callback': 'error',
36 |       'prefer-destructuring': ['warn', { object: true, array: false }],
37 | 
38 |       // Re-apply prettier rule to ensure it has priority
39 |       'prettier/prettier': 'error',
40 | 
41 |       // --- Node.js Specific Rules ---
42 |       'n/handle-callback-err': 'error',
43 |       'n/no-deprecated-api': 'error',
44 |       'n/no-new-require': 'error',
45 |       'n/no-unpublished-import': 'off', // Disabled because at linting stage we have not yet run npm i
46 |       'n/no-missing-import': 'off',
47 |     },
48 |   },
49 | });
50 | 
51 | /**
52 |  * Lints and auto-fixes the given code string.
53 |  * @param code The source code generated by the LLM.
54 |  * @returns An object with the fixed code and a formatted string of remaining errors.
55 |  */
56 | export async function lintAndRefactorCode(code: string): Promise<{
57 |   fixedCode: string;
58 |   errorReport: string | null;
59 | }> {
60 |   const results = await eslint.lintText(code);
61 |   const result = results[0]; // We are only linting one string
62 | 
63 |   // The 'output' property contains the fixed code if fixes were applied.
64 |   // If no fixes were needed, it's undefined, so we fall back to the original code.
65 |   const fixedCode = result.output ?? code;
66 | 
67 |   // Filter for errors that could not be auto-fixed
68 |   const remainingErrors = result.messages.filter(
69 |     (msg) => msg.severity === 2 // 2 for 'error'
70 |   );
71 | 
72 |   if (remainingErrors.length > 0) {
73 |     // Format the remaining errors for feedback
74 |     const errorReport = remainingErrors
75 |       .map(
76 |         (err) => `L${err.line}:${err.column}: ${err.message} (${err.ruleId})`
77 |       )
78 |       .join('\n');
79 |     return { fixedCode, errorReport };
80 |   }
81 | 
82 |   return { fixedCode, errorReport: null };
83 | }
84 | 
```

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

```typescript
 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
 2 | import * as tmp from 'tmp';
 3 | import { execSync } from 'node:child_process';
 4 | import runJs from '../src/tools/runJs.ts';
 5 | import { DEFAULT_NODE_IMAGE } from '../src/utils.ts';
 6 | import { forceStopContainer } from '../src/dockerUtils.ts';
 7 | 
 8 | function startSandboxContainer(): string {
 9 |   return execSync(
10 |     `docker run -d --network host --memory 512m --cpus 1 --workdir /workspace ${DEFAULT_NODE_IMAGE} tail -f /dev/null`,
11 |     { encoding: 'utf-8' }
12 |   ).trim();
13 | }
14 | let tmpDir: tmp.DirResult;
15 | 
16 | describe('runJs npm install benchmarking', () => {
17 |   beforeEach(() => {
18 |     tmpDir = tmp.dirSync({ unsafeCleanup: true });
19 |     process.env.FILES_DIR = tmpDir.name;
20 |   });
21 | 
22 |   afterEach(() => {
23 |     tmpDir.removeCallback();
24 |     delete process.env.FILES_DIR;
25 |   });
26 | 
27 |   it('should install dependency faster on second run due to caching', async () => {
28 |     const containerId = startSandboxContainer();
29 | 
30 |     try {
31 |       const dependency = { name: 'lodash', version: '^4.17.21' };
32 | 
33 |       // First run: benchmark install
34 |       const result1 = await runJs({
35 |         container_id: containerId,
36 |         code: "console.log('Hello')",
37 |         dependencies: [dependency],
38 |       });
39 | 
40 |       const telemetryItem1 = result1.content.find(
41 |         (c) => c.type === 'text' && c.text.startsWith('Telemetry:')
42 |       );
43 |       expect(telemetryItem1).toBeDefined();
44 |       const telemetry1 = JSON.parse(
45 |         (telemetryItem1 && telemetryItem1.type === 'text'
46 |           ? telemetryItem1.text
47 |           : ''
48 |         ).replace('Telemetry:\n', '')
49 |       );
50 |       const installTimeMs1 = telemetry1.installTimeMs;
51 | 
52 |       // Second run: same install again, expect faster
53 |       const result2 = await runJs({
54 |         container_id: containerId,
55 |         code: "console.log('Hello')",
56 |         dependencies: [dependency],
57 |       });
58 | 
59 |       const telemetryItem2 = result2.content.find(
60 |         (c) => c.type === 'text' && c.text.startsWith('Telemetry:')
61 |       );
62 |       expect(telemetryItem2).toBeDefined();
63 |       const telemetry2 = JSON.parse(
64 |         (telemetryItem2 && telemetryItem2.type === 'text'
65 |           ? telemetryItem2.text
66 |           : ''
67 |         ).replace('Telemetry:\n', '')
68 |       );
69 |       const installTimeMs2 = telemetry2.installTimeMs;
70 |       // Assert that second install is faster
71 |       try {
72 |         expect(installTimeMs2).toBeLessThan(installTimeMs1);
73 |       } catch (error) {
74 |         console.error('Error in assertion:', error);
75 |         console.log(`First install time: ${installTimeMs1}ms`);
76 |         console.log(`Second install time: ${installTimeMs2}ms`);
77 |         throw error; // Re-throw the error to fail the test
78 |       }
79 |     } finally {
80 |       forceStopContainer(containerId);
81 |     }
82 |   }, 20_000);
83 | });
84 | 
```

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

```javascript
 1 | import path from 'path';
 2 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 3 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 4 | 
 5 | async function main() {
 6 |   // 1️⃣ Create the MCP client
 7 |   const client = new Client({
 8 |     name: 'ephemeral-with-deps-example',
 9 |     version: '1.0.0',
10 |   });
11 | 
12 |   // Host path where you want outputs to land
13 |   const FILES_DIR = '/Users/alfonsograziano/Desktop';
14 | 
15 |   // Resolve it against $HOME (in case you ever switch to a relative subfolder)
16 |   const hostOutput = path.resolve(process.env.HOME, FILES_DIR);
17 | 
18 |   // Where we’ll mount that folder _inside_ the MCP‐server container
19 |   const containerOutput = '/root';
20 | 
21 |   // 2️⃣ Connect to your js-sandbox-mcp server
22 | 
23 |   await client.connect(
24 |     new StdioClientTransport({
25 |       command: 'npm',
26 |       args: ['run', 'dev'],
27 |       cwd: path.resolve('..'),
28 |       env: { ...process.env, FILES_DIR },
29 |     })
30 |   );
31 | 
32 |   // await client.connect(
33 |   //   new StdioClientTransport({
34 |   //     command: "docker",
35 |   //     args: [
36 |   //       // 1) Start a new container
37 |   //       "run",
38 |   //       // 2) Keep STDIN open and allocate a pseudo-TTY (required for MCP over stdio)
39 |   //       "-i",
40 |   //       // 3) Remove the container automatically when it exits
41 |   //       "--rm",
42 | 
43 |   //       // 4) Give the MCP-server access to the Docker socket
44 |   //       //    so it can spin up inner “ephemeral” containers
45 |   //       "-v",
46 |   //       "/var/run/docker.sock:/var/run/docker.sock",
47 | 
48 |   //       // 5) Bind-mount your Desktop folder into the container at /root
49 |   //       "-v",
50 |   //       `${hostOutput}:${containerOutput}`,
51 | 
52 |   //       // 6) Pass your host’s output-dir env var _into_ the MCP-server
53 |   //       "-e",
54 |   //       `FILES_DIR=${hostOutput}`,
55 | 
56 |   //       // 7) The MCP-server image that will manage your ephemeral sandboxes
57 |   //       "alfonsograziano/node-code-sandbox-mcp",
58 |   //     ],
59 |   //     env: {
60 |   //       // inherit your shell’s env
61 |   //       ...process.env,
62 |   //       // also set FILES_DIR inside the MCP-server process
63 |   //       FILES_DIR,
64 |   //     },
65 |   //   })
66 |   // );
67 | 
68 |   console.log('✅ Connected to js-sandbox-mcp');
69 | 
70 |   // 3️⃣ Use the run_js_ephemeral tool with a dependency (lodash)
71 |   const result = await client.callTool({
72 |     name: 'run_js_ephemeral',
73 |     arguments: {
74 |       image: 'node:lts-slim',
75 |       code: `
76 |           import fs from 'fs/promises';  
77 |           await fs.writeFile('hello_world.txt', 'Hello world!');
78 |       `,
79 |       dependencies: [
80 |         {
81 |           name: 'lodash',
82 |           version: '^4.17.21',
83 |         },
84 |       ],
85 |     },
86 |   });
87 | 
88 |   console.log('▶️ run_js_ephemeral output:\n', JSON.stringify(result, null, 2));
89 | 
90 |   process.exit(0);
91 | }
92 | 
93 | main().catch((err) => {
94 |   console.error('❌ Error in ephemeral-with-deps example:', err);
95 |   process.exit(1);
96 | });
97 | 
```

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

```typescript
 1 | import { z } from 'zod';
 2 | import type { McpResponse } from '../types.ts';
 3 | import { textContent } from '../types.ts';
 4 | import { logger } from '../logger.ts';
 5 | 
 6 | export const argSchema = {
 7 |   dependencies: z.array(
 8 |     z.object({
 9 |       name: z.string(),
10 |       version: z.string().optional(),
11 |     })
12 |   ),
13 | };
14 | 
15 | export default async function getDependencyTypes({
16 |   dependencies,
17 | }: {
18 |   dependencies: { name: string; version?: string }[];
19 | }): Promise<McpResponse> {
20 |   const results: {
21 |     name: string;
22 |     hasTypes: boolean;
23 |     types?: string;
24 |     typesPackage?: string;
25 |     version?: string;
26 |   }[] = [];
27 | 
28 |   for (const dep of dependencies) {
29 |     const info: (typeof results)[number] = { name: dep.name, hasTypes: false };
30 |     try {
31 |       const pkgRes = await fetch(`https://registry.npmjs.org/${dep.name}`);
32 |       if (pkgRes.ok) {
33 |         const pkgMeta = (await pkgRes.json()) as any;
34 |         const latestTag = pkgMeta['dist-tags']?.latest as string;
35 |         const versionToUse = dep.version || latestTag;
36 |         const versionData = pkgMeta.versions?.[versionToUse];
37 |         // Check for in-package types
38 |         if (versionData) {
39 |           const typesField = versionData.types || versionData.typings;
40 |           if (typesField) {
41 |             const url = `https://unpkg.com/${dep.name}@${versionToUse}/${typesField}`;
42 |             const contentRes = await fetch(url);
43 |             if (contentRes.ok) {
44 |               info.hasTypes = true;
45 |               info.types = await contentRes.text();
46 |               info.version = versionToUse;
47 |               results.push(info);
48 |               continue;
49 |             }
50 |           }
51 |         }
52 | 
53 |         // Fallback to @types package
54 |         const sanitized = dep.name.replace('@', '').replace('/', '__');
55 |         const typesName = `@types/${sanitized}`;
56 |         const typesRes = await fetch(
57 |           `https://registry.npmjs.org/${encodeURIComponent(typesName)}`
58 |         );
59 |         if (typesRes.ok) {
60 |           const typesMeta = (await typesRes.json()) as any;
61 |           const typesVersion = typesMeta['dist-tags']?.latest as string;
62 |           const typesVersionData = typesMeta.versions?.[typesVersion];
63 |           const typesField =
64 |             typesVersionData?.types ||
65 |             typesVersionData?.typings ||
66 |             'index.d.ts';
67 |           const url = `https://unpkg.com/${typesName}@${typesVersion}/${typesField}`;
68 |           const contentRes = await fetch(url);
69 |           if (contentRes.ok) {
70 |             info.hasTypes = true;
71 |             info.typesPackage = typesName;
72 |             info.version = typesVersion;
73 |             info.types = await contentRes.text();
74 |           }
75 |         }
76 |       }
77 |     } catch (e) {
78 |       logger.info(`Failed to fetch type info for ${dep.name}: ${e}`);
79 |     }
80 |     results.push(info);
81 |   }
82 | 
83 |   return { content: [textContent(JSON.stringify(results))] };
84 | }
85 | 
```

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

```typescript
 1 | import { execFile, execFileSync } from 'child_process';
 2 | import util from 'util';
 3 | import { logger } from './logger.ts';
 4 | import { getConfig } from './config.ts';
 5 | import { textContent } from './types.ts';
 6 | import { sanitizeContainerId, sanitizeShellCommand } from './utils.ts';
 7 | 
 8 | const execFilePromise = util.promisify(execFile);
 9 | 
10 | /**
11 |  * Attempts to forcefully stop and remove a Docker container by its ID.
12 |  * Logs errors but does not throw them to allow cleanup flows to continue.
13 |  * Does NOT manage any external container registry/map.
14 |  * @param containerId The ID of the container to stop and remove.
15 |  */
16 | export async function forceStopContainer(containerId: string): Promise<void> {
17 |   logger.info(
18 |     `Attempting to stop and remove container via dockerUtils: ${containerId}`
19 |   );
20 |   try {
21 |     // Sanitize containerId
22 |     const safeId = sanitizeContainerId(containerId);
23 |     if (!safeId) throw new Error('Invalid containerId');
24 |     // Force stop the container (ignores errors if already stopped)
25 |     await execFilePromise('docker', ['stop', safeId]);
26 |     // Force remove the container (ignores errors if already removed)
27 |     await execFilePromise('docker', ['rm', '-f', safeId]);
28 |     logger.info(
29 |       `Successfully issued stop/remove commands for container: ${containerId}`
30 |     );
31 |   } catch (error) {
32 |     // Log errors but don't throw
33 |     logger.error(
34 |       `Error during docker stop/remove commands for container ${containerId}`,
35 |       typeof error === 'object' &&
36 |         error !== null &&
37 |         ('stderr' in error || 'message' in error)
38 |         ? (error as { stderr?: string; message?: string }).stderr ||
39 |             (error as { message: string }).message
40 |         : String(error)
41 |     );
42 |   }
43 | }
44 | 
45 | export type NodeExecResult = {
46 |   output: string | null;
47 |   error: Error | null;
48 |   duration: number;
49 | };
50 | 
51 | export function safeExecNodeInContainer({
52 |   containerId,
53 |   timeoutMs = getConfig().runScriptTimeoutMilliseconds,
54 |   command = 'node index.js',
55 | }: {
56 |   containerId: string;
57 |   timeoutMs?: number;
58 |   command?: string;
59 | }): NodeExecResult {
60 |   const runStart = Date.now();
61 |   // Sanitize command
62 |   const safeCmd = sanitizeShellCommand(command);
63 |   if (!safeCmd) {
64 |     return { output: null, error: new Error('Invalid command'), duration: 0 };
65 |   }
66 |   try {
67 |     const output = execFileSync(
68 |       'docker',
69 |       ['exec', containerId, '/bin/sh', '-c', safeCmd],
70 |       { encoding: 'utf8', timeout: timeoutMs }
71 |     );
72 |     return { output, error: null, duration: Date.now() - runStart };
73 |   } catch (err) {
74 |     const error = err instanceof Error ? err : new Error(String(err));
75 |     return { output: null, error, duration: Date.now() - runStart };
76 |   }
77 | }
78 | 
79 | export const getContentFromError = (
80 |   error: Error,
81 |   telemetry: Record<string, unknown>
82 | ) => {
83 |   return {
84 |     content: [
85 |       textContent(`Error during execution: ${error.message}`),
86 |       textContent(`Telemetry:\n${JSON.stringify(telemetry, null, 2)}`),
87 |     ],
88 |   };
89 | };
90 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import * as tmp from 'tmp';
  3 | import * as fs from 'fs';
  4 | import * as path from 'path';
  5 | import { getSnapshot, detectChanges } from '../src/snapshotUtils.ts';
  6 | 
  7 | let tmpDir: tmp.DirResult;
  8 | 
  9 | function createFile(filePath: string, content = '') {
 10 |   fs.writeFileSync(filePath, content);
 11 | }
 12 | 
 13 | function createDir(dirPath: string) {
 14 |   fs.mkdirSync(dirPath);
 15 | }
 16 | 
 17 | describe('Filesystem snapshot and change detection', () => {
 18 |   beforeEach(() => {
 19 |     tmpDir = tmp.dirSync({ unsafeCleanup: true });
 20 |   });
 21 | 
 22 |   afterEach(() => {
 23 |     tmpDir.removeCallback();
 24 |   });
 25 | 
 26 |   it('getSnapshot returns correct structure for files and directories', async () => {
 27 |     const file1 = path.join(tmpDir.name, 'file1.txt');
 28 |     const subDir = path.join(tmpDir.name, 'sub');
 29 |     const file2 = path.join(subDir, 'file2.txt');
 30 | 
 31 |     createFile(file1, 'Hello');
 32 |     createDir(subDir);
 33 |     createFile(file2, 'World');
 34 | 
 35 |     const snapshot = await getSnapshot(tmpDir.name);
 36 | 
 37 |     expect(Object.keys(snapshot)).toContain(file1);
 38 |     expect(Object.keys(snapshot)).toContain(subDir);
 39 |     expect(Object.keys(snapshot)).toContain(file2);
 40 | 
 41 |     expect(snapshot[file1].isDirectory).toBe(false);
 42 |     expect(snapshot[subDir].isDirectory).toBe(true);
 43 |     expect(snapshot[file2].isDirectory).toBe(false);
 44 |   });
 45 | 
 46 |   it('detectChanges detects created files', async () => {
 47 |     const initialSnapshot = await getSnapshot(tmpDir.name);
 48 | 
 49 |     const newFile = path.join(tmpDir.name, 'newFile.txt');
 50 |     createFile(newFile, 'New content');
 51 | 
 52 |     const changes = await detectChanges(
 53 |       initialSnapshot,
 54 |       tmpDir.name,
 55 |       Date.now() - 1000
 56 |     );
 57 | 
 58 |     expect(changes).toEqual([
 59 |       {
 60 |         type: 'created',
 61 |         path: newFile,
 62 |         isDirectory: false,
 63 |       },
 64 |     ]);
 65 |   });
 66 | 
 67 |   it('detectChanges detects deleted files', async () => {
 68 |     const fileToDelete = path.join(tmpDir.name, 'toDelete.txt');
 69 |     createFile(fileToDelete, 'To be deleted');
 70 | 
 71 |     const snapshotBeforeDelete = await getSnapshot(tmpDir.name);
 72 |     fs.unlinkSync(fileToDelete);
 73 | 
 74 |     const changes = await detectChanges(
 75 |       snapshotBeforeDelete,
 76 |       tmpDir.name,
 77 |       Date.now() - 1000
 78 |     );
 79 | 
 80 |     expect(changes).toEqual([
 81 |       {
 82 |         type: 'deleted',
 83 |         path: fileToDelete,
 84 |         isDirectory: false,
 85 |       },
 86 |     ]);
 87 |   });
 88 | 
 89 |   it('detectChanges detects updated files', async () => {
 90 |     const fileToUpdate = path.join(tmpDir.name, 'update.txt');
 91 |     createFile(fileToUpdate, 'Original');
 92 | 
 93 |     const snapshot = await getSnapshot(tmpDir.name);
 94 | 
 95 |     // Wait to ensure mtimeMs changes
 96 |     await new Promise((resolve) => setTimeout(resolve, 20));
 97 |     fs.writeFileSync(fileToUpdate, 'Updated');
 98 | 
 99 |     const changes = await detectChanges(
100 |       snapshot,
101 |       tmpDir.name,
102 |       Date.now() - 1000
103 |     );
104 | 
105 |     expect(changes).toEqual([
106 |       {
107 |         type: 'updated',
108 |         path: fileToUpdate,
109 |         isDirectory: false,
110 |       },
111 |     ]);
112 |   });
113 | });
114 | 
```

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

```typescript
  1 | import { z } from 'zod';
  2 | import { execFileSync } from 'node:child_process';
  3 | import { randomUUID } from 'node:crypto';
  4 | import { type McpResponse, textContent } from '../types.ts';
  5 | import {
  6 |   DEFAULT_NODE_IMAGE,
  7 |   DOCKER_NOT_RUNNING_ERROR,
  8 |   isDockerRunning,
  9 |   computeResourceLimits,
 10 | } from '../utils.ts';
 11 | import { getMountFlag } from '../runUtils.ts';
 12 | import { activeSandboxContainers } from '../containerUtils.ts';
 13 | import { logger } from '../logger.ts';
 14 | import stopSandbox from './stop.ts';
 15 | 
 16 | // Instead of importing serverRunId directly, we'll have a variable that gets set
 17 | let serverRunId = 'unknown';
 18 | 
 19 | // Function to set the serverRunId from the server.ts file
 20 | export function setServerRunId(id: string) {
 21 |   serverRunId = id;
 22 | }
 23 | 
 24 | export const argSchema = {
 25 |   image: z.string().optional(),
 26 |   port: z
 27 |     .number()
 28 |     .optional()
 29 |     .describe('If set, maps this container port to the host'),
 30 | };
 31 | 
 32 | export default async function initializeSandbox({
 33 |   image = DEFAULT_NODE_IMAGE,
 34 |   port,
 35 | }: {
 36 |   image?: string;
 37 |   port?: number;
 38 | }): Promise<McpResponse> {
 39 |   if (!isDockerRunning()) {
 40 |     return {
 41 |       content: [textContent(DOCKER_NOT_RUNNING_ERROR)],
 42 |     };
 43 |   }
 44 | 
 45 |   const containerId = `js-sbx-${randomUUID()}`;
 46 |   const creationTimestamp = Date.now();
 47 | 
 48 |   const portOption = port ? `-p ${port}:${port}` : `--network host`; // prefer --network host if no explicit port mapping
 49 | 
 50 |   // Construct labels
 51 |   const labels = [
 52 |     `mcp-sandbox=true`,
 53 |     `mcp-server-run-id=${serverRunId}`,
 54 |     `mcp-creation-timestamp=${creationTimestamp}`,
 55 |   ];
 56 |   const { memFlag, cpuFlag } = computeResourceLimits(image);
 57 |   const mountFlag = getMountFlag();
 58 | 
 59 |   try {
 60 |     const args = [
 61 |       'run',
 62 |       '-d',
 63 |       ...portOption.split(' '),
 64 |       ...memFlag.split(' '),
 65 |       ...cpuFlag.split(' '),
 66 |       '--workdir',
 67 |       '/workspace',
 68 |       ...mountFlag.split(' '),
 69 |       ...labels.flatMap((label) => ['--label', label]),
 70 |       '--name',
 71 |       containerId,
 72 |       image,
 73 |       'tail',
 74 |       '-f',
 75 |       '/dev/null',
 76 |     ].filter(Boolean);
 77 | 
 78 |     execFileSync('docker', args, { stdio: 'ignore' });
 79 | 
 80 |     // Register the container only after successful creation
 81 |     activeSandboxContainers.set(containerId, creationTimestamp);
 82 |     logger.info(`Registered container ${containerId}`);
 83 | 
 84 |     return {
 85 |       content: [textContent(containerId)],
 86 |     };
 87 |   } catch (error) {
 88 |     logger.error(`Failed to initialize container ${containerId}`, error);
 89 |     // Ensure partial cleanup if execFileSync fails after container might be created but before registration
 90 |     try {
 91 |       stopSandbox({ container_id: containerId });
 92 |     } catch (cleanupError: unknown) {
 93 |       // Ignore cleanup errors - log it just in case
 94 |       logger.warning(
 95 |         `Ignoring error during cleanup attempt for ${containerId}: ${String(cleanupError)}`
 96 |       );
 97 |     }
 98 |     return {
 99 |       content: [
100 |         textContent(
101 |           `Failed to initialize sandbox container: ${error instanceof Error ? error.message : String(error)}`
102 |         ),
103 |       ],
104 |     };
105 |   }
106 | }
107 | 
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import fs from 'fs';
  4 | import path from 'path';
  5 | import dotenv from 'dotenv';
  6 | import { OpenAIAuditClient } from './auditClient.ts';
  7 | 
  8 | dotenv.config();
  9 | 
 10 | /**
 11 |  * evalRunner Configuration
 12 |  *
 13 |  * - evalsPath:   Path to a JSON file containing an array of eval definitions {
 14 |  *                   id: string,
 15 |  *                   prompt: string
 16 |  *               }
 17 |  * - batchSize:   Number of evals to process per batch.
 18 |  * - outputPath:  Path to write results in JSONL format.
 19 |  */
 20 | const config = {
 21 |   evalsPath: './evals/basicEvals.json',
 22 |   batchSize: 5,
 23 |   outputPath: './evalResults.jsonl',
 24 | };
 25 | 
 26 | async function run() {
 27 |   const { evalsPath, batchSize, outputPath } = config;
 28 | 
 29 |   // Load eval definitions
 30 |   if (!fs.existsSync(evalsPath)) {
 31 |     console.error(`Evals file not found at ${evalsPath}`);
 32 |     process.exit(1);
 33 |   }
 34 |   const evals = JSON.parse(fs.readFileSync(evalsPath, 'utf-8'));
 35 |   if (!Array.isArray(evals)) {
 36 |     console.error('Evals file must export an array of {id, prompt} objects.');
 37 |     process.exit(1);
 38 |   }
 39 | 
 40 |   // Initialize OpenAI Audit Client
 41 |   const client = new OpenAIAuditClient({
 42 |     apiKey: process.env.OPENAI_API_KEY!,
 43 |     model: process.env.OPENAI_MODEL || 'gpt-4o-mini',
 44 |   });
 45 |   await client.initializeClient();
 46 |   console.log('OpenAI Audit Client initialized');
 47 | 
 48 |   // Ensure output directory exists
 49 |   const outDir = path.dirname(outputPath);
 50 |   if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
 51 | 
 52 |   // Process in batches
 53 |   for (let i = 0; i < evals.length; i += batchSize) {
 54 |     const batch = evals.slice(i, i + batchSize);
 55 |     console.log(
 56 |       `Processing batch ${i / batchSize + 1} (${batch.length} evals)...`
 57 |     );
 58 | 
 59 |     const promises = batch.map(async ({ id, prompt }) => {
 60 |       const startTimeInMillis = new Date().getTime();
 61 |       const startHumanRadableTime = new Date().toISOString();
 62 |       try {
 63 |         const fullResponse = await client.chat({
 64 |           messages: [{ role: 'user', content: prompt }],
 65 |         });
 66 |         const endTimeInMillis = new Date().getTime();
 67 |         const endHumanRadableTime = new Date().toISOString();
 68 |         const durationInMillis = endTimeInMillis - startTimeInMillis;
 69 |         const humanRadableDuration = `${startHumanRadableTime} - ${endHumanRadableTime}`;
 70 | 
 71 |         return {
 72 |           id,
 73 |           fullResponse,
 74 |           timing: {
 75 |             startTimeInMillis,
 76 |             endTimeInMillis,
 77 |             startHumanRadableTime,
 78 |             endHumanRadableTime,
 79 |             durationInMillis,
 80 |             humanRadableDuration,
 81 |           },
 82 |         };
 83 |       } catch (err) {
 84 |         const errorMessage =
 85 |           err instanceof Error ? err.message : `Unknown error: ${err}`;
 86 |         return { id, prompt, error: errorMessage };
 87 |       }
 88 |     });
 89 | 
 90 |     const results = await Promise.all(promises);
 91 | 
 92 |     // Append each result as a JSON line
 93 |     for (const result of results) {
 94 |       fs.appendFileSync(outputPath, JSON.stringify(result, null, 2) + '\n');
 95 |     }
 96 |     console.log(`Batch ${i / batchSize + 1} done.`);
 97 |   }
 98 | 
 99 |   console.log('All evals processed. Results saved to', config.outputPath);
100 | }
101 | 
102 | run().catch((err) => {
103 |   console.error('Error running evalRunner:', err);
104 |   process.exit(1);
105 | });
106 | 
```

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

```json
 1 | [
 2 |   {
 3 |     "id": "generate-qr-code",
 4 |     "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."
 5 |   },
 6 |   {
 7 |     "id": "test-regular-expressions",
 8 |     "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."
 9 |   },
10 |   {
11 |     "id": "create-csv-random-data",
12 |     "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'."
13 |   },
14 |   {
15 |     "id": "scrape-webpage-title",
16 |     "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'."
17 |   },
18 |   {
19 |     "id": "create-pdf-report",
20 |     "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."
21 |   },
22 |   {
23 |     "id": "fetch-api-save-json",
24 |     "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'."
25 |   },
26 |   {
27 |     "id": "markdown-to-html-converter",
28 |     "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'."
29 |   },
30 |   {
31 |     "id": "generate-random-data",
32 |     "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'."
33 |   },
34 |   {
35 |     "id": "evaluate-complex-math-expression",
36 |     "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'."
37 |   },
38 |   {
39 |     "id": "take-screenshot-with-playwright",
40 |     "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."
41 |   },
42 |   {
43 |     "id": "generate-chart",
44 |     "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'."
45 |   },
46 |   {
47 |     "id": "hello-world",
48 |     "prompt": "Create and run a simple Node.js script that prints \"Hello, World!\" to the console."
49 |   }
50 | ]
51 | 
```

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

```typescript
  1 | import path from 'node:path';
  2 | import { glob, stat, readFile } from 'node:fs/promises';
  3 | import { pathToFileURL } from 'node:url';
  4 | 
  5 | import mime from 'mime-types';
  6 | 
  7 | import { getFilesDir } from './runUtils.ts';
  8 | import { type McpContent, textContent } from './types.ts';
  9 | import { isRunningInDocker } from './utils.ts';
 10 | import type { Dirent } from 'node:fs';
 11 | 
 12 | type ChangeType = 'created' | 'updated' | 'deleted';
 13 | type Change = {
 14 |   type: ChangeType;
 15 |   path: string;
 16 |   isDirectory: boolean;
 17 | };
 18 | 
 19 | type FileSnapshot = Record<string, { mtimeMs: number; isDirectory: boolean }>;
 20 | 
 21 | export const getMountPointDir = () => {
 22 |   if (isRunningInDocker()) {
 23 |     return '/root';
 24 |   }
 25 |   return getFilesDir();
 26 | };
 27 | 
 28 | export async function getSnapshot(dir: string): Promise<FileSnapshot> {
 29 |   const snapshot: FileSnapshot = {};
 30 | 
 31 |   const executor = glob('**/*', {
 32 |     cwd: dir,
 33 |     withFileTypes: true,
 34 |     exclude: (file: string | Dirent): boolean => {
 35 |       const name = typeof file === 'string' ? file : file.name;
 36 |       return ['.git', 'node_modules'].includes(name);
 37 |     },
 38 |   });
 39 | 
 40 |   for await (const entry of executor) {
 41 |     const fullPath = path.join(entry.parentPath, entry.name);
 42 |     const stats = await stat(fullPath);
 43 |     snapshot[fullPath] = {
 44 |       mtimeMs: stats.mtimeMs,
 45 |       isDirectory: entry.isDirectory(),
 46 |     };
 47 |   }
 48 | 
 49 |   return snapshot;
 50 | }
 51 | 
 52 | export async function detectChanges(
 53 |   prevSnapshot: FileSnapshot,
 54 |   dir: string,
 55 |   sinceTimeMs: number
 56 | ): Promise<Change[]> {
 57 |   const changes: Change[] = [];
 58 |   const currentSnapshot = await getSnapshot(dir);
 59 | 
 60 |   const allPaths = new Set([
 61 |     ...Object.keys(prevSnapshot),
 62 |     ...Object.keys(currentSnapshot),
 63 |   ]);
 64 | 
 65 |   for (const filePath of allPaths) {
 66 |     const prev = prevSnapshot[filePath];
 67 |     const curr = currentSnapshot[filePath];
 68 | 
 69 |     if (!prev && curr && curr.mtimeMs >= sinceTimeMs) {
 70 |       changes.push({
 71 |         type: 'created',
 72 |         path: filePath,
 73 |         isDirectory: curr.isDirectory,
 74 |       });
 75 |     } else if (prev && !curr) {
 76 |       changes.push({
 77 |         type: 'deleted',
 78 |         path: filePath,
 79 |         isDirectory: prev.isDirectory,
 80 |       });
 81 |     } else if (
 82 |       prev &&
 83 |       curr &&
 84 |       curr.mtimeMs > prev.mtimeMs &&
 85 |       curr.mtimeMs >= sinceTimeMs
 86 |     ) {
 87 |       changes.push({
 88 |         type: 'updated',
 89 |         path: filePath,
 90 |         isDirectory: curr.isDirectory,
 91 |       });
 92 |     }
 93 |   }
 94 | 
 95 |   return changes;
 96 | }
 97 | 
 98 | export async function changesToMcpContent(
 99 |   changes: Change[]
100 | ): Promise<McpContent[]> {
101 |   const contents: McpContent[] = [];
102 |   const imageTypes = new Set(['image/jpeg', 'image/png']);
103 | 
104 |   // Build single summary message
105 |   const summaryLines = changes.map((change) => {
106 |     const fname = path.basename(change.path);
107 |     return `- ${fname} was ${change.type}`;
108 |   });
109 | 
110 |   if (summaryLines.length > 0) {
111 |     contents.push(
112 |       textContent(`List of changed files:\n${summaryLines.join('\n')}`)
113 |     );
114 |   }
115 | 
116 |   // Add image/resource entries for created/updated (not deleted)
117 |   for (const change of changes) {
118 |     if (change.type === 'deleted') continue;
119 | 
120 |     const mimeType = mime.lookup(change.path) || 'application/octet-stream';
121 | 
122 |     if (imageTypes.has(mimeType)) {
123 |       const b64 = await readFile(change.path, {
124 |         encoding: 'base64',
125 |       });
126 |       contents.push({
127 |         type: 'image',
128 |         data: b64,
129 |         mimeType,
130 |       });
131 |     }
132 | 
133 |     const hostPath = path.join(getFilesDir(), path.basename(change.path));
134 | 
135 |     contents.push({
136 |       type: 'resource',
137 |       resource: {
138 |         uri: pathToFileURL(hostPath).href,
139 |         mimeType,
140 |         text: path.basename(change.path),
141 |       },
142 |     });
143 |   }
144 | 
145 |   return contents;
146 | }
147 | 
```

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

```typescript
  1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
  2 | import stopSandbox from '../src/tools/stop.ts';
  3 | import * as childProcess from 'node:child_process';
  4 | import * as utils from '../src/utils.ts';
  5 | 
  6 | vi.mock('node:child_process');
  7 | vi.mock('../src/types', () => ({
  8 |   textContent: (text: string) => ({ type: 'text', text }),
  9 | }));
 10 | vi.mock('../src/utils.ts', async () => {
 11 |   const actual =
 12 |     await vi.importActual<typeof import('../src/utils.ts')>('../src/utils.ts');
 13 |   return {
 14 |     ...actual,
 15 |     DOCKER_NOT_RUNNING_ERROR: actual.DOCKER_NOT_RUNNING_ERROR,
 16 |     isDockerRunning: vi.fn(() => true),
 17 |     sanitizeContainerId: actual.sanitizeContainerId,
 18 |   };
 19 | });
 20 | 
 21 | beforeEach(() => {
 22 |   vi.resetAllMocks();
 23 |   vi.spyOn(childProcess, 'execFileSync').mockImplementation(() =>
 24 |     Buffer.from('')
 25 |   );
 26 | });
 27 | 
 28 | afterEach(() => {
 29 |   vi.restoreAllMocks();
 30 | });
 31 | 
 32 | describe('stopSandbox', () => {
 33 |   const fakeContainerId = 'js-sbx-test123'; // valid container ID
 34 | 
 35 |   it('should remove the container with the given ID', async () => {
 36 |     const result = await stopSandbox({ container_id: fakeContainerId });
 37 |     expect(childProcess.execFileSync).toHaveBeenCalledWith('docker', [
 38 |       'rm',
 39 |       '-f',
 40 |       fakeContainerId,
 41 |     ]);
 42 |     expect(result).toEqual({
 43 |       content: [
 44 |         { type: 'text', text: `Container ${fakeContainerId} removed.` },
 45 |       ],
 46 |     });
 47 |   });
 48 | 
 49 |   it('should return an error message when Docker is not running', async () => {
 50 |     vi.mocked(utils.isDockerRunning).mockReturnValue(false);
 51 |     const result = await stopSandbox({ container_id: fakeContainerId });
 52 |     expect(childProcess.execFileSync).not.toHaveBeenCalled();
 53 |     expect(result).toEqual({
 54 |       content: [{ type: 'text', text: utils.DOCKER_NOT_RUNNING_ERROR }],
 55 |     });
 56 |   });
 57 | 
 58 |   it('should handle errors when removing the container', async () => {
 59 |     vi.spyOn(console, 'error').mockImplementation(() => {});
 60 |     const errorMessage = 'Container not found';
 61 |     vi.mocked(childProcess.execFileSync).mockImplementation(() => {
 62 |       throw new Error(errorMessage);
 63 |     });
 64 |     const result = await stopSandbox({ container_id: fakeContainerId });
 65 |     expect(childProcess.execFileSync).toHaveBeenCalledWith('docker', [
 66 |       'rm',
 67 |       '-f',
 68 |       fakeContainerId,
 69 |     ]);
 70 |     expect(result).toEqual({
 71 |       content: [
 72 |         {
 73 |           type: 'text',
 74 |           text: `Error removing container ${fakeContainerId}: ${errorMessage}`,
 75 |         },
 76 |       ],
 77 |     });
 78 |   });
 79 | 
 80 |   it('should reject invalid container_id', async () => {
 81 |     const result = await stopSandbox({ container_id: 'bad;id$(rm -rf /)' });
 82 |     expect(result).toEqual({
 83 |       content: [
 84 |         {
 85 |           type: 'text',
 86 |           text: 'Invalid container ID',
 87 |         },
 88 |       ],
 89 |     });
 90 |     expect(childProcess.execFileSync).not.toHaveBeenCalled();
 91 |   });
 92 | });
 93 | 
 94 | describe('Command injection prevention', () => {
 95 |   const dangerousIds = [
 96 |     '$(touch /tmp/pwned)',
 97 |     '`touch /tmp/pwned`',
 98 |     'bad;id',
 99 |     'js-sbx-123 && rm -rf /',
100 |     'js-sbx-123 | echo hacked',
101 |     'js-sbx-123 > /tmp/pwned',
102 |     'js-sbx-123 $(id)',
103 |     'js-sbx-123; echo pwned',
104 |     'js-sbx-123`echo pwned`',
105 |     'js-sbx-123/../../etc/passwd',
106 |     'js-sbx-123\nrm -rf /',
107 |     '',
108 |     ' ',
109 |     'js-sbx-123$',
110 |     'js-sbx-123#',
111 |   ];
112 | 
113 |   dangerousIds.forEach((payload) => {
114 |     it(`should reject dangerous container_id: "${payload}"`, async () => {
115 |       const result = await stopSandbox({ container_id: payload });
116 |       expect(result).toEqual({
117 |         content: [
118 |           {
119 |             type: 'text',
120 |             text: 'Invalid container ID',
121 |           },
122 |         ],
123 |       });
124 |       expect(childProcess.execFileSync).not.toHaveBeenCalled();
125 |     });
126 |   });
127 | });
128 | 
```

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

```typescript
  1 | import {
  2 |   describe,
  3 |   it,
  4 |   expect,
  5 |   beforeAll,
  6 |   afterAll,
  7 |   beforeEach,
  8 |   afterEach,
  9 | } from 'vitest';
 10 | import initializeSandbox from '../src/tools/initialize.ts';
 11 | import execInSandbox from '../src/tools/exec.ts';
 12 | import stopSandbox from '../src/tools/stop.ts';
 13 | import * as utils from '../src/utils.ts';
 14 | import { vi } from 'vitest';
 15 | 
 16 | let containerId: string;
 17 | 
 18 | beforeAll(async () => {
 19 |   const result = await initializeSandbox({});
 20 |   const content = result.content[0];
 21 |   if (content.type !== 'text') throw new Error('Unexpected content type');
 22 |   containerId = content.text;
 23 | });
 24 | 
 25 | afterAll(async () => {
 26 |   await stopSandbox({ container_id: containerId });
 27 | });
 28 | 
 29 | describe('execInSandbox', () => {
 30 |   it('should return an error if Docker is not running', async () => {
 31 |     vi.spyOn(utils, 'isDockerRunning').mockReturnValue(false);
 32 | 
 33 |     const result = await initializeSandbox({});
 34 |     expect(result).toEqual({
 35 |       content: [
 36 |         {
 37 |           type: 'text',
 38 |           text: 'Error: Docker is not running. Please start Docker and try again.',
 39 |         },
 40 |       ],
 41 |     });
 42 | 
 43 |     vi.restoreAllMocks();
 44 |   });
 45 |   it('should execute a single command and return its output', async () => {
 46 |     const result = await execInSandbox({
 47 |       container_id: containerId,
 48 |       commands: ['echo Hello'],
 49 |     });
 50 | 
 51 |     expect(result.content[0].type).toBe('text');
 52 |     if (result.content[0].type === 'text') {
 53 |       expect(result.content[0].text.trim()).toBe('Hello');
 54 |     } else {
 55 |       throw new Error('Unexpected content type');
 56 |     }
 57 |   });
 58 | 
 59 |   it('should execute multiple commands and join their outputs', async () => {
 60 |     const result = await execInSandbox({
 61 |       container_id: containerId,
 62 |       commands: ['echo First', 'echo Second'],
 63 |     });
 64 | 
 65 |     let output: string[] = [];
 66 |     if (result.content[0].type === 'text') {
 67 |       output = result.content[0].text.trim().split('\n');
 68 |       expect(output).toEqual(['First', '', 'Second']);
 69 |     } else {
 70 |       throw new Error('Unexpected content type');
 71 |     }
 72 |   });
 73 | 
 74 |   it('should handle command with special characters', async () => {
 75 |     const result = await execInSandbox({
 76 |       container_id: containerId,
 77 |       commands: ['echo "Special: $HOME"'],
 78 |     });
 79 | 
 80 |     if (result.content[0].type === 'text') {
 81 |       expect(result.content[0].text.trim()).toContain('Special:');
 82 |     } else {
 83 |       throw new Error('Unexpected content type');
 84 |     }
 85 |   });
 86 | });
 87 | 
 88 | describe('Command injection prevention', () => {
 89 |   beforeEach(() => {
 90 |     vi.doMock('node:child_process', () => ({
 91 |       execFileSync: vi.fn(() => Buffer.from('')),
 92 |       execFile: vi.fn(() => Buffer.from('')),
 93 |     }));
 94 |   });
 95 | 
 96 |   afterEach(() => {
 97 |     vi.resetModules();
 98 |     vi.resetAllMocks();
 99 |     vi.restoreAllMocks();
100 |   });
101 | 
102 |   const dangerousIds = [
103 |     '$(touch /tmp/pwned)',
104 |     '`touch /tmp/pwned`',
105 |     'bad;id',
106 |     'js-sbx-123 && rm -rf /',
107 |     'js-sbx-123 | echo hacked',
108 |     'js-sbx-123 > /tmp/pwned',
109 |     'js-sbx-123 $(id)',
110 |     'js-sbx-123; echo pwned',
111 |     'js-sbx-123`echo pwned`',
112 |     'js-sbx-123/../../etc/passwd',
113 |     'js-sbx-123\nrm -rf /',
114 |     '',
115 |     ' ',
116 |     'js-sbx-123$',
117 |     'js-sbx-123#',
118 |   ];
119 | 
120 |   dangerousIds.forEach((payload) => {
121 |     it(`should reject dangerous container_id: "${payload}"`, async () => {
122 |       const { default: execInSandbox } = await import('../src/tools/exec.ts');
123 |       const childProcess = await import('node:child_process');
124 |       const result = await execInSandbox({
125 |         container_id: payload,
126 |         commands: ['echo test'],
127 |       });
128 |       expect(result).toEqual({
129 |         content: [
130 |           {
131 |             type: 'text',
132 |             text: 'Invalid container ID',
133 |           },
134 |         ],
135 |       });
136 |       const execFileSyncCall = vi.mocked(childProcess.execFileSync).mock.calls;
137 |       expect(execFileSyncCall.length).toBe(0);
138 |     });
139 |   });
140 | });
141 | 
```

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

```typescript
  1 | import { forceStopContainer as dockerForceStopContainer } from './dockerUtils.ts';
  2 | import { logger } from './logger.ts';
  3 | 
  4 | // Registry for active sandbox containers: Map<containerId, creationTimestamp>
  5 | export const activeSandboxContainers = new Map<string, number>();
  6 | 
  7 | /**
  8 |  * Starts the periodic scavenger task to clean up timed-out containers.
  9 |  * @param containerTimeoutMilliseconds The maximum allowed age for a container in milliseconds.
 10 |  * @param containerTimeoutSeconds The timeout in seconds (for logging).
 11 |  * @param checkIntervalMilliseconds How often the scavenger should check (defaults to 60000ms).
 12 |  * @returns The interval handle returned by setInterval.
 13 |  */
 14 | export function startScavenger(
 15 |   containerTimeoutMilliseconds: number,
 16 |   containerTimeoutSeconds: number,
 17 |   checkIntervalMilliseconds = 60 * 1000
 18 | ): NodeJS.Timeout {
 19 |   logger.info(
 20 |     `Starting container scavenger. Timeout: ${containerTimeoutSeconds}s, Check Interval: ${checkIntervalMilliseconds / 1000}s`
 21 |   );
 22 | 
 23 |   const scavengerInterval = setInterval(() => {
 24 |     const now = Date.now();
 25 |     if (activeSandboxContainers.size > 0) {
 26 |       logger.debug(
 27 |         `Checking ${activeSandboxContainers.size} active containers for timeout (${containerTimeoutSeconds}s)...`
 28 |       );
 29 |     }
 30 |     for (const [
 31 |       containerId,
 32 |       creationTimestamp,
 33 |     ] of activeSandboxContainers.entries()) {
 34 |       if (now - creationTimestamp > containerTimeoutMilliseconds) {
 35 |         logger.warning(
 36 |           `Container ${containerId} timed out (created at ${new Date(creationTimestamp).toISOString()}). Forcing removal.`
 37 |         );
 38 | 
 39 |         dockerForceStopContainer(containerId)
 40 |           .then(() => {
 41 |             // Remove from registry AFTER docker command attempt
 42 |             activeSandboxContainers.delete(containerId);
 43 |             logger.info(`Removed container ${containerId} from registry.`);
 44 |           })
 45 |           .catch((error) => {
 46 |             // Log error from force stop attempt but continue scavenger
 47 |             logger.error(`Error during forced stop of ${containerId}`, error);
 48 |             // Still attempt to remove from registry if Docker failed
 49 |             activeSandboxContainers.delete(containerId);
 50 |             logger.info(
 51 |               `Removed container ${containerId} from registry after error.`
 52 |             );
 53 |           });
 54 |       }
 55 |     }
 56 |   }, checkIntervalMilliseconds);
 57 | 
 58 |   return scavengerInterval;
 59 | }
 60 | 
 61 | /**
 62 |  * Attempts to stop and remove all containers currently listed in the
 63 |  * activeSandboxContainers registry.
 64 |  * Should be called during graceful shutdown.
 65 |  */
 66 | export async function cleanActiveContainers(): Promise<void> {
 67 |   const containersToClean = Array.from(activeSandboxContainers.keys());
 68 | 
 69 |   if (containersToClean.length === 0) {
 70 |     logger.info('[Shutdown Cleanup] No active containers to clean up.');
 71 |     return;
 72 |   }
 73 | 
 74 |   logger.info(
 75 |     `[Shutdown Cleanup] Cleaning up ${containersToClean.length} active containers...`
 76 |   );
 77 | 
 78 |   const cleanupPromises = containersToClean.map(async (id) => {
 79 |     try {
 80 |       await dockerForceStopContainer(id); // Attempt to stop/remove via Docker
 81 |     } catch (error) {
 82 |       // Log error but continue, registry removal happens regardless
 83 |       logger.error(`[Shutdown Cleanup] Error stopping container ${id}`, error);
 84 |     } finally {
 85 |       activeSandboxContainers.delete(id); // Always remove from registry
 86 |       logger.info(`[Shutdown Cleanup] Removed container ${id} from registry.`);
 87 |     }
 88 |   });
 89 | 
 90 |   const results = await Promise.allSettled(cleanupPromises);
 91 |   logger.info('[Shutdown Cleanup] Container cleanup finished.');
 92 | 
 93 |   results.forEach((result, index) => {
 94 |     if (result.status === 'rejected') {
 95 |       logger.error(
 96 |         `[Shutdown Cleanup] Promise for container ${containersToClean[index]} rejected`,
 97 |         result.reason
 98 |       );
 99 |     }
100 |   });
101 | }
102 | 
```

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

```typescript
  1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
  2 | import * as childProcess from 'node:child_process';
  3 | import * as utils from '../src/utils.ts';
  4 | 
  5 | vi.mock('node:child_process', () => ({
  6 |   execFileSync: vi.fn(() => Buffer.from('')),
  7 | }));
  8 | vi.mock('../src/utils');
  9 | vi.mocked(utils).computeResourceLimits = vi
 10 |   .fn()
 11 |   .mockReturnValue({ memFlag: '', cpuFlag: '' });
 12 | 
 13 | vi.mock('../src/containerUtils', () => ({
 14 |   activeSandboxContainers: new Map(),
 15 | }));
 16 | 
 17 | describe('initialize module', () => {
 18 |   beforeEach(() => {
 19 |     vi.resetAllMocks();
 20 |     vi.spyOn(utils, 'isDockerRunning').mockReturnValue(true);
 21 |     vi.spyOn(utils, 'computeResourceLimits').mockReturnValue({
 22 |       memFlag: '',
 23 |       cpuFlag: '',
 24 |     });
 25 |   });
 26 | 
 27 |   afterEach(() => {
 28 |     vi.clearAllMocks();
 29 |   });
 30 | 
 31 |   describe('setServerRunId', () => {
 32 |     it('should set the server run ID correctly', async () => {
 33 |       vi.doMock('../src/runUtils', () => ({
 34 |         getFilesDir: vi.fn().mockReturnValue(''),
 35 |         getMountFlag: vi.fn().mockReturnValue(''),
 36 |       }));
 37 |       vi.resetModules();
 38 |       const mod = await import('../src/tools/initialize.ts');
 39 |       const initializeSandbox = mod.default;
 40 |       const setServerRunId = mod.setServerRunId;
 41 | 
 42 |       // Set a test server run ID
 43 |       const testId = 'test-server-run-id';
 44 |       setServerRunId(testId);
 45 | 
 46 |       // Call initialize function to create a container
 47 |       await initializeSandbox({});
 48 | 
 49 |       // Verify that execFileSync was called with the correct label containing our test ID
 50 |       expect(childProcess.execFileSync).toHaveBeenCalled();
 51 |       const execFileSyncCall = vi.mocked(childProcess.execFileSync).mock
 52 |         .calls[0][1] as string[];
 53 |       // Join the args array to a string for easier matching
 54 |       expect(execFileSyncCall.join(' ')).toContain(
 55 |         `--label mcp-server-run-id=${testId}`
 56 |       );
 57 |     });
 58 | 
 59 |     it('should use unknown as the default server run ID if not set', async () => {
 60 |       vi.doMock('../src/runUtils', () => ({
 61 |         getFilesDir: vi.fn().mockReturnValue(''),
 62 |         getMountFlag: vi.fn().mockReturnValue(''),
 63 |       }));
 64 |       vi.resetModules();
 65 |       const { default: initializeSandbox } = await import(
 66 |         '../src/tools/initialize.ts'
 67 |       );
 68 | 
 69 |       // Call initialize without setting the server run ID
 70 |       await initializeSandbox({});
 71 | 
 72 |       // Verify that execFileSync was called with the default "unknown" ID
 73 |       expect(childProcess.execFileSync).toHaveBeenCalled();
 74 |       const execFileSyncCall = vi.mocked(childProcess.execFileSync).mock
 75 |         .calls[0][1] as string[];
 76 |       expect(execFileSyncCall.join(' ')).toContain(
 77 |         '--label mcp-server-run-id=unknown'
 78 |       );
 79 |     });
 80 |   });
 81 | 
 82 |   describe('volume mount behaviour', () => {
 83 |     it('does NOT include a -v flag when FILES_DIR is unset', async () => {
 84 |       vi.doMock('../src/runUtils', () => ({
 85 |         getFilesDir: vi.fn().mockReturnValue(''),
 86 |         getMountFlag: vi.fn().mockReturnValue(''),
 87 |       }));
 88 |       vi.resetModules();
 89 |       const { default: initializeSandbox } = await import(
 90 |         '../src/tools/initialize.ts'
 91 |       );
 92 | 
 93 |       await initializeSandbox({});
 94 | 
 95 |       const args = vi.mocked(childProcess.execFileSync).mock
 96 |         .calls[0][1] as string[];
 97 |       expect(args.join(' ')).not.toContain('-v ');
 98 |     });
 99 | 
100 |     it('includes the -v flag when getMountFlag returns one', async () => {
101 |       vi.doMock('../src/runUtils', () => ({
102 |         getFilesDir: vi.fn().mockReturnValue('/host/dir'),
103 |         getMountFlag: vi.fn().mockReturnValue('-v /host/dir:/workspace/files'),
104 |       }));
105 |       vi.resetModules();
106 |       const { default: initializeSandbox } = await import(
107 |         '../src/tools/initialize.ts'
108 |       );
109 | 
110 |       await initializeSandbox({});
111 | 
112 |       const args = vi.mocked(childProcess.execFileSync).mock
113 |         .calls[0][1] as string[];
114 |       expect(args.join(' ')).toContain('-v /host/dir:/workspace/files');
115 |     });
116 |   });
117 | });
118 | 
```

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

```typescript
  1 | import { OpenAI } from 'openai';
  2 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  3 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
  4 | 
  5 | /**
  6 |  * Settings for the OpenAIAuditClient
  7 |  */
  8 | export interface AuditClientSettings {
  9 |   apiKey?: string; // OpenAI API key
 10 |   model: string; // Model to use for chat completions
 11 | }
 12 | 
 13 | /**
 14 |  * A client wrapper that calls OpenAI chat completions with tool support and returns detailed audit entries,
 15 |  * including timing for each tool invocation.
 16 |  */
 17 | export class OpenAIAuditClient {
 18 |   private openai: OpenAI;
 19 |   private model: string;
 20 |   private client: Client;
 21 |   private availableTools: OpenAI.Chat.ChatCompletionTool[] = [];
 22 | 
 23 |   constructor(settings: AuditClientSettings) {
 24 |     const { apiKey, model } = settings;
 25 |     this.openai = new OpenAI({ apiKey });
 26 |     this.model = model;
 27 |     this.client = new Client({ name: 'node_js_sandbox', version: '1.0.0' });
 28 |   }
 29 | 
 30 |   /**
 31 |    * Initializes the sandbox client by launching the Docker-based MCP server and loading available tools.
 32 |    */
 33 |   public async initializeClient() {
 34 |     const userOutputDir = process.env.FILES_DIR;
 35 |     await this.client.connect(
 36 |       new StdioClientTransport({
 37 |         command: 'docker',
 38 |         args: [
 39 |           'run',
 40 |           '-i',
 41 |           '--rm',
 42 |           '-v',
 43 |           '/var/run/docker.sock:/var/run/docker.sock',
 44 |           '-v',
 45 |           `${userOutputDir}:/root`,
 46 |           '-e',
 47 |           `FILES_DIR=${userOutputDir}`,
 48 |           'alfonsograziano/node-code-sandbox-mcp',
 49 |         ],
 50 |       })
 51 |     );
 52 | 
 53 |     const { tools } = await this.client.listTools();
 54 |     this.availableTools = tools.map((tool) => ({
 55 |       type: 'function',
 56 |       function: {
 57 |         parameters: tool.inputSchema,
 58 |         ...tool,
 59 |       },
 60 |     }));
 61 |   }
 62 | 
 63 |   /**
 64 |    * Call OpenAI's chat completions with automatic tool usage.
 65 |    * Returns the sequence of messages, responses, and timing details for each tool invocation.
 66 |    * @param requestOptions - Includes messages to send
 67 |    */
 68 |   public async chat(
 69 |     requestOptions: Omit<OpenAI.Chat.ChatCompletionCreateParams, 'model'>
 70 |   ): Promise<{
 71 |     responses: OpenAI.Chat.Completions.ChatCompletion[];
 72 |     messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
 73 |     toolRuns: Array<{
 74 |       toolName: string;
 75 |       toolCallId: string;
 76 |       params: unknown;
 77 |       durationMs: number;
 78 |     }>;
 79 |   }> {
 80 |     const messages = [...requestOptions.messages];
 81 |     const responses: OpenAI.Chat.Completions.ChatCompletion[] = [];
 82 |     const toolRuns: Array<{
 83 |       toolName: string;
 84 |       toolCallId: string;
 85 |       params: unknown;
 86 |       durationMs: number;
 87 |     }> = [];
 88 |     let interactionCount = 0;
 89 |     const maxInteractions = 10;
 90 | 
 91 |     while (interactionCount++ < maxInteractions) {
 92 |       const response = await this.openai.chat.completions.create({
 93 |         model: this.model,
 94 |         messages,
 95 |         tools: this.availableTools,
 96 |         tool_choice: 'auto',
 97 |       });
 98 |       responses.push(response);
 99 |       const message = response.choices[0].message;
100 |       messages.push(message);
101 | 
102 |       if (message.tool_calls) {
103 |         for (const toolCall of message.tool_calls) {
104 |           const functionName = toolCall.function.name;
105 |           const params = JSON.parse(toolCall.function.arguments || '{}');
106 |           const start = Date.now();
107 |           const result = await this.client.callTool({
108 |             name: functionName,
109 |             arguments: params,
110 |           });
111 |           const durationMs = Date.now() - start;
112 | 
113 |           // record tool invocation details
114 |           toolRuns.push({
115 |             toolName: functionName,
116 |             toolCallId: toolCall.id,
117 |             params,
118 |             durationMs,
119 |           });
120 | 
121 |           messages.push({
122 |             role: 'tool',
123 |             tool_call_id: toolCall.id,
124 |             content: JSON.stringify(result),
125 |           });
126 |         }
127 |       } else {
128 |         break;
129 |       }
130 |     }
131 | 
132 |     return { responses, messages, toolRuns };
133 |   }
134 | 
135 |   /**
136 |    * Exposes the list of available tools for inspection.
137 |    */
138 |   public getAvailableTools() {
139 |     return this.availableTools;
140 |   }
141 | }
142 | 
```

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

```typescript
  1 | import {
  2 |   describe,
  3 |   it,
  4 |   expect,
  5 |   vi,
  6 |   beforeEach,
  7 |   afterEach,
  8 |   type Mock,
  9 | } from 'vitest';
 10 | import searchNpmPackages from '../src/tools/searchNpmPackages.ts';
 11 | import { NpmRegistry } from 'npm-registry-sdk';
 12 | import type { McpContentText } from '../src/types.ts';
 13 | 
 14 | vi.mock('npm-registry-sdk');
 15 | 
 16 | describe('searchNpmPackages', () => {
 17 |   let searchMock: ReturnType<typeof vi.fn>;
 18 |   let getPackageMock: ReturnType<typeof vi.fn>;
 19 | 
 20 |   beforeEach(() => {
 21 |     searchMock = vi.fn();
 22 |     getPackageMock = vi.fn();
 23 | 
 24 |     // Configure the mocked NpmsIO constructor to return our specific mock methods
 25 |     (NpmRegistry as Mock).mockImplementation(() => {
 26 |       return {
 27 |         search: searchMock,
 28 |         getPackage: getPackageMock,
 29 |       };
 30 |     });
 31 |   });
 32 | 
 33 |   afterEach(() => {
 34 |     vi.resetAllMocks();
 35 |   });
 36 | 
 37 |   it('should return packages with their details when search is successful', async () => {
 38 |     const mockSearchResults = {
 39 |       total: 2,
 40 |       objects: [
 41 |         {
 42 |           package: { name: 'package1' },
 43 |           score: { detail: { popularity: 1 } },
 44 |         },
 45 |         {
 46 |           package: { name: 'package2' },
 47 |           score: { detail: { popularity: 0.5 } },
 48 |         },
 49 |       ],
 50 |     };
 51 | 
 52 |     const mockPackageInfos = [
 53 |       {
 54 |         name: 'package1',
 55 |         description: 'Test package 1',
 56 |         readme: 'Package 1 readme content',
 57 |       },
 58 |       {
 59 |         name: 'package2',
 60 |         description: 'Test package 2',
 61 |         readme: 'Package 2 readme content',
 62 |       },
 63 |     ];
 64 | 
 65 |     searchMock.mockResolvedValue(mockSearchResults);
 66 |     getPackageMock.mockImplementation((pkg) =>
 67 |       Promise.resolve(mockPackageInfos.find((p) => p.name === pkg))
 68 |     );
 69 | 
 70 |     const result = await searchNpmPackages({
 71 |       searchTerm: 'test-package',
 72 |     });
 73 | 
 74 |     expect(searchMock).toHaveBeenCalledWith('test-package', {
 75 |       qualifiers: undefined,
 76 |     });
 77 |     expect(getPackageMock).toHaveBeenCalledTimes(2);
 78 |     expect(JSON.parse((result.content[0] as McpContentText).text)).toEqual([
 79 |       {
 80 |         name: 'package1',
 81 |         description: 'Test package 1',
 82 |         readmeSnippet: 'Package 1 readme content',
 83 |       },
 84 |       {
 85 |         name: 'package2',
 86 |         description: 'Test package 2',
 87 |         readmeSnippet: 'Package 2 readme content',
 88 |       },
 89 |     ]);
 90 |   });
 91 | 
 92 |   it('should return "No packages found" when search returns no results', async () => {
 93 |     searchMock.mockResolvedValue({ total: 0, objects: [] });
 94 | 
 95 |     const result = await searchNpmPackages({
 96 |       searchTerm: 'nonexistent-package',
 97 |     });
 98 | 
 99 |     expect(searchMock).toHaveBeenCalledWith('nonexistent-package', {
100 |       qualifiers: undefined,
101 |     });
102 |     expect(getPackageMock).not.toHaveBeenCalled();
103 |     expect((result.content[0] as McpContentText).text).toBe(
104 |       'No packages found.'
105 |     );
106 |   });
107 | 
108 |   it('should apply search qualifiers when provided', async () => {
109 |     const mockSearchResults = {
110 |       total: 1,
111 |       objects: [
112 |         {
113 |           package: { name: 'qualified-package' },
114 |           score: { detail: { popularity: 1 } },
115 |         },
116 |       ],
117 |     };
118 | 
119 |     const mockPackageInfo = {
120 |       name: 'qualified-package',
121 |       description: 'Qualified package',
122 |       readme: 'Qualified package readme',
123 |     };
124 | 
125 |     searchMock.mockResolvedValue(mockSearchResults);
126 |     getPackageMock.mockResolvedValue(mockPackageInfo);
127 | 
128 |     const qualifiers = {
129 |       author: 'test-author',
130 |       keywords: 'test',
131 |     };
132 | 
133 |     const result = await searchNpmPackages({
134 |       searchTerm: 'test-package',
135 |       qualifiers,
136 |     });
137 | 
138 |     expect(searchMock).toHaveBeenCalledWith('test-package', { qualifiers });
139 |     expect(getPackageMock).toHaveBeenCalledWith('qualified-package');
140 |     expect(JSON.parse((result.content[0] as McpContentText).text)).toEqual([
141 |       {
142 |         name: 'qualified-package',
143 |         description: 'Qualified package',
144 |         readmeSnippet: 'Qualified package readme',
145 |       },
146 |     ]);
147 |   });
148 | 
149 |   it('should handle packages with missing description or readme', async () => {
150 |     const mockSearchResults = {
151 |       total: 1,
152 |       objects: [
153 |         {
154 |           package: { name: 'incomplete-package' },
155 |           score: { detail: { popularity: 1 } },
156 |         },
157 |       ],
158 |     };
159 | 
160 |     const mockPackageInfo = {
161 |       name: 'incomplete-package',
162 |       description: undefined,
163 |       readme: undefined,
164 |     };
165 | 
166 |     searchMock.mockResolvedValue(mockSearchResults);
167 |     getPackageMock.mockResolvedValue(mockPackageInfo);
168 | 
169 |     const result = await searchNpmPackages({
170 |       searchTerm: 'incomplete-package',
171 |     });
172 | 
173 |     expect(JSON.parse((result.content[0] as McpContentText).text)).toEqual([
174 |       {
175 |         name: 'incomplete-package',
176 |         description: 'No description available.',
177 |         readmeSnippet: 'README not available.',
178 |       },
179 |     ]);
180 |   });
181 | 
182 |   it('should handle search errors gracefully', async () => {
183 |     const error = new Error('Search failed');
184 |     searchMock.mockRejectedValue(error);
185 | 
186 |     const result = await searchNpmPackages({
187 |       searchTerm: 'test-package',
188 |     });
189 | 
190 |     expect(result).toEqual({
191 |       content: [
192 |         {
193 |           text: 'Failed to search npm packages for "test-package". Error: Search failed',
194 |           type: 'text',
195 |         },
196 |       ],
197 |       isError: true,
198 |     });
199 |     expect(getPackageMock).not.toHaveBeenCalled();
200 |   });
201 | });
202 | 
```

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

```typescript
  1 | import { z } from 'zod';
  2 | import { execFileSync } from 'child_process';
  3 | import tmp from 'tmp';
  4 | import { randomUUID } from 'crypto';
  5 | import { type McpResponse, textContent, type McpContent } from '../types.ts';
  6 | import {
  7 |   DEFAULT_NODE_IMAGE,
  8 |   DOCKER_NOT_RUNNING_ERROR,
  9 |   generateSuggestedImages,
 10 |   isDockerRunning,
 11 |   preprocessDependencies,
 12 |   computeResourceLimits,
 13 | } from '../utils.ts';
 14 | import { prepareWorkspace, getMountFlag } from '../runUtils.ts';
 15 | import {
 16 |   changesToMcpContent,
 17 |   detectChanges,
 18 |   getSnapshot,
 19 |   getMountPointDir,
 20 | } from '../snapshotUtils.ts';
 21 | import {
 22 |   getContentFromError,
 23 |   safeExecNodeInContainer,
 24 | } from '../dockerUtils.ts';
 25 | import { lintAndRefactorCode } from '../linterUtils.ts';
 26 | 
 27 | const NodeDependency = z.object({
 28 |   name: z.string().describe('npm package name, e.g. lodash'),
 29 |   version: z.string().describe('npm package version range, e.g. ^4.17.21'),
 30 | });
 31 | 
 32 | export const argSchema = {
 33 |   image: z
 34 |     .string()
 35 |     .optional()
 36 |     .default(DEFAULT_NODE_IMAGE)
 37 |     .describe(
 38 |       'Docker image to use for ephemeral execution. e.g. ' +
 39 |         generateSuggestedImages()
 40 |     ),
 41 |   // We use an array of { name, version } items instead of a record
 42 |   // because the OpenAI function-calling schema doesn’t reliably support arbitrary
 43 |   // object keys. An explicit array ensures each dependency has a clear, uniform
 44 |   // structure the model can populate.
 45 |   // Schema for a single dependency item
 46 |   dependencies: z
 47 |     .array(NodeDependency)
 48 |     .default([])
 49 |     .describe(
 50 |       'A list of npm dependencies to install before running the code. ' +
 51 |         'Each item must have a `name` (package) and `version` (range). ' +
 52 |         'If none, returns an empty array.'
 53 |     ),
 54 |   code: z
 55 |     .string()
 56 |     .describe('JavaScript code to run inside the ephemeral container.'),
 57 | };
 58 | 
 59 | type NodeDependenciesArray = Array<{ name: string; version: string }>;
 60 | 
 61 | export default async function runJsEphemeral({
 62 |   image = DEFAULT_NODE_IMAGE,
 63 |   code,
 64 |   dependencies = [],
 65 | }: {
 66 |   image?: string;
 67 |   code: string;
 68 |   dependencies?: NodeDependenciesArray;
 69 | }): Promise<McpResponse> {
 70 |   if (!isDockerRunning()) {
 71 |     return { content: [textContent(DOCKER_NOT_RUNNING_ERROR)] };
 72 |   }
 73 | 
 74 |   // Lint and refactor the code first.
 75 |   const { fixedCode, errorReport } = await lintAndRefactorCode(code);
 76 | 
 77 |   const telemetry: Record<string, unknown> = {};
 78 |   const dependenciesRecord = preprocessDependencies({ dependencies, image });
 79 |   const containerId = `js-ephemeral-${randomUUID()}`;
 80 |   const tmpDir = tmp.dirSync({ unsafeCleanup: true });
 81 |   const { memFlag, cpuFlag } = computeResourceLimits(image);
 82 |   const mountFlag = getMountFlag();
 83 | 
 84 |   try {
 85 |     // Start an ephemeral container
 86 |     execFileSync('docker', [
 87 |       'run',
 88 |       '-d',
 89 |       '--network',
 90 |       'host',
 91 |       ...memFlag.split(' ').filter(Boolean),
 92 |       ...cpuFlag.split(' ').filter(Boolean),
 93 |       '--workdir',
 94 |       '/workspace',
 95 |       ...mountFlag.split(' ').filter(Boolean),
 96 |       '--name',
 97 |       containerId,
 98 |       image,
 99 |       'tail',
100 |       '-f',
101 |       '/dev/null',
102 |     ]);
103 | 
104 |     // Prepare workspace locally
105 |     const localWorkspace = await prepareWorkspace({
106 |       code: fixedCode,
107 |       dependenciesRecord,
108 |     });
109 |     execFileSync('docker', [
110 |       'cp',
111 |       `${localWorkspace.name}/.`,
112 |       `${containerId}:/workspace`,
113 |     ]);
114 | 
115 |     // Generate snapshot of the workspace
116 |     const snapshotStartTime = Date.now();
117 |     const snapshot = await getSnapshot(getMountPointDir());
118 | 
119 |     // Run install and script inside container
120 |     const installCmd =
121 |       'npm install --omit=dev --prefer-offline --no-audit --loglevel=error';
122 | 
123 |     if (dependencies.length > 0) {
124 |       const installStart = Date.now();
125 |       const installOutput = execFileSync(
126 |         'docker',
127 |         ['exec', containerId, '/bin/sh', '-c', installCmd],
128 |         { encoding: 'utf8' }
129 |       );
130 |       telemetry.installTimeMs = Date.now() - installStart;
131 |       telemetry.installOutput = installOutput;
132 |     } else {
133 |       telemetry.installTimeMs = 0;
134 |       telemetry.installOutput = 'Skipped npm install (no dependencies)';
135 |     }
136 | 
137 |     const { output, error, duration } = safeExecNodeInContainer({
138 |       containerId,
139 |     });
140 |     telemetry.runTimeMs = duration;
141 |     if (error) {
142 |       const errorResponse = getContentFromError(error, telemetry);
143 |       if (errorReport) {
144 |         errorResponse.content.unshift(
145 |           textContent(
146 |             `Linting issues found (some may have been auto-fixed):\n${errorReport}`
147 |           )
148 |         );
149 |       }
150 |       return errorResponse;
151 |     }
152 | 
153 |     // Detect the file changed during the execution of the tool in the mounted workspace
154 |     // and report the changes to the user
155 |     const changes = await detectChanges(
156 |       snapshot,
157 |       getMountPointDir(),
158 |       snapshotStartTime
159 |     );
160 | 
161 |     const extractedContents = await changesToMcpContent(changes);
162 | 
163 |     const responseContent: McpContent[] = [];
164 |     if (errorReport) {
165 |       responseContent.push(
166 |         textContent(
167 |           `Linting issues found (some may have been auto-fixed):\n${errorReport}`
168 |         )
169 |       );
170 |     }
171 | 
172 |     return {
173 |       content: [
174 |         ...(responseContent.length ? responseContent : []),
175 |         textContent(`Node.js process output:\n${output}`),
176 |         ...extractedContents,
177 |         textContent(`Telemetry:\n${JSON.stringify(telemetry, null, 2)}`),
178 |       ],
179 |     };
180 |   } finally {
181 |     execFileSync('docker', ['rm', '-f', containerId]);
182 |     tmpDir.removeCallback();
183 |   }
184 | }
185 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import * as tmp from 'tmp';
  3 | 
  4 | import runJs from '../src/tools/runJs.ts';
  5 | import initializeSandbox from '../src/tools/initialize.ts';
  6 | import stopSandbox from '../src/tools/stop.ts';
  7 | 
  8 | let tmpDir: tmp.DirResult;
  9 | 
 10 | describe('runJs with listenOnPort using Node.js http module', () => {
 11 |   beforeEach(() => {
 12 |     tmpDir = tmp.dirSync({ unsafeCleanup: true });
 13 |     process.env.FILES_DIR = tmpDir.name;
 14 |   });
 15 | 
 16 |   afterEach(() => {
 17 |     tmpDir.removeCallback();
 18 |     delete process.env.FILES_DIR;
 19 |   });
 20 | 
 21 |   it('should start a basic HTTP server in the container and expose it on the given port', async () => {
 22 |     const port = 20000 + Math.floor(Math.random() * 10000);
 23 |     const start = await initializeSandbox({ port });
 24 |     const content = start.content[0];
 25 |     if (content.type !== 'text') throw new Error('Unexpected content type');
 26 |     const containerId = content.text;
 27 | 
 28 |     try {
 29 |       // RunJS returns a promise that resolves when the server is started and listening
 30 |       // on the specified port. The server will run in the background.
 31 |       const result = await runJs({
 32 |         container_id: containerId,
 33 |         code: `
 34 |           import http from 'http';
 35 | 
 36 |           const server = http.createServer((req, res) => {
 37 |             res.writeHead(200, { 'Content-Type': 'text/plain' });
 38 |             res.end('ok');
 39 |           });
 40 | 
 41 |           server.listen(${port}, '0.0.0.0', () => {
 42 |             console.log('Server started');
 43 |           });
 44 |         `,
 45 |         dependencies: [],
 46 |         listenOnPort: port,
 47 |       });
 48 | 
 49 |       expect(result).toBeDefined();
 50 |       expect(result.content[0].type).toBe('text');
 51 | 
 52 |       if (result.content[0].type === 'text') {
 53 |         expect(result.content[0].text).toContain(
 54 |           'Server started in background'
 55 |         );
 56 |       }
 57 | 
 58 |       const res = await fetch(`http://localhost:${port}`);
 59 |       const body = await res.text();
 60 |       expect(body).toBe('ok');
 61 |     } finally {
 62 |       await stopSandbox({ container_id: containerId });
 63 |     }
 64 |   }, 10_000);
 65 | 
 66 |   it('should start an Express server and return book data from endpoints', async () => {
 67 |     const port = 20000 + Math.floor(Math.random() * 10000);
 68 |     const start = await initializeSandbox({ port });
 69 |     const content = start.content[0];
 70 |     if (content.type !== 'text') throw new Error('Unexpected content type');
 71 |     const containerId = content.text;
 72 | 
 73 |     try {
 74 |       const result = await runJs({
 75 |         container_id: containerId,
 76 |         code: `
 77 |           import express from 'express';
 78 |           const app = express();
 79 |           const port = ${port};
 80 | 
 81 |           const books = [
 82 |               {
 83 |                   title: 'The Great Gatsby',
 84 |                   author: 'F. Scott Fitzgerald',
 85 |                   isbn: '9780743273565',
 86 |                   publishedYear: 1925,
 87 |                   genres: ['Fiction', 'Classic'],
 88 |                   available: true
 89 |               },
 90 |               {
 91 |                   title: '1984',
 92 |                   author: 'George Orwell',
 93 |                   isbn: '9780451524935',
 94 |                   publishedYear: 1949,
 95 |                   genres: ['Fiction', 'Dystopian'],
 96 |                   available: true
 97 |               },
 98 |               {
 99 |                   title: 'To Kill a Mockingbird',
100 |                   author: 'Harper Lee',
101 |                   isbn: '9780061120084',
102 |                   publishedYear: 1960,
103 |                   genres: ['Fiction', 'Classic'],
104 |                   available: false
105 |               },
106 |               {
107 |                   title: 'The Catcher in the Rye',
108 |                   author: 'J.D. Salinger',
109 |                   isbn: '9780316769488',
110 |                   publishedYear: 1951,
111 |                   genres: ['Fiction', 'Classic'],
112 |                   available: true
113 |               },
114 |               {
115 |                   title: 'The Hobbit',
116 |                   author: 'J.R.R. Tolkien',
117 |                   isbn: '9780547928227',
118 |                   publishedYear: 1937,
119 |                   genres: ['Fantasy', 'Adventure'],
120 |                   available: true
121 |               }
122 |           ];
123 | 
124 |           app.get('/books', (req, res) => {
125 |               res.json(books);
126 |           });
127 | 
128 |           app.get('/books/:isbn', (req, res) => {
129 |               const book = books.find(b => b.isbn === req.params.isbn);
130 |               if (book) {
131 |                   res.json(book);
132 |               } else {
133 |                   res.sendStatus(404);
134 |               }
135 |           });
136 | 
137 |           app.listen(port, '0.0.0.0', () => {
138 |               console.log('Server started');
139 |           });
140 |         `,
141 |         dependencies: [
142 |           {
143 |             name: 'express',
144 |             version: '4.18.2',
145 |           },
146 |         ],
147 |         listenOnPort: port,
148 |       });
149 | 
150 |       expect(result).toBeDefined();
151 |       expect(result.content[0].type).toBe('text');
152 |       // expect(result.content[0].text).toContain("Server started in background");
153 | 
154 |       const resAll = await fetch(`http://localhost:${port}/books`);
155 |       expect(resAll.status).toBe(200);
156 |       const books = await resAll.json();
157 |       expect(Array.isArray(books)).toBe(true);
158 |       expect(books.length).toBeGreaterThanOrEqual(5);
159 | 
160 |       const resSingle = await fetch(
161 |         `http://localhost:${port}/books/9780451524935`
162 |       );
163 |       expect(resSingle.status).toBe(200);
164 |       const book = await resSingle.json();
165 |       expect(book.title).toBe('1984');
166 | 
167 |       const resNotFound = await fetch(
168 |         `http://localhost:${port}/books/nonexistent`
169 |       );
170 |       expect(resNotFound.status).toBe(404);
171 |     } finally {
172 |       await stopSandbox({ container_id: containerId });
173 |     }
174 |   }, 30_000);
175 | });
176 | 
```

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

```typescript
  1 | import React, { useState } from 'react';
  2 | import clsx from 'clsx';
  3 | 
  4 | const GettingStarted: React.FC = () => {
  5 |   const [selectedClient, setSelectedClient] = useState<'claude' | 'vscode'>(
  6 |     'claude'
  7 |   );
  8 |   const [variant, setVariant] = useState<'docker' | 'npx'>('docker');
  9 | 
 10 |   const codeSnippets = {
 11 |     claude: {
 12 |       docker: {
 13 |         mcpServers: {
 14 |           'js-sandbox': {
 15 |             command: 'docker',
 16 |             args: [
 17 |               'run',
 18 |               '-i',
 19 |               '--rm',
 20 |               '-v',
 21 |               '/var/run/docker.sock:/var/run/docker.sock',
 22 |               '-v',
 23 |               '$HOME/Desktop/sandbox-output:/root',
 24 |               '-e',
 25 |               'FILES_DIR=$HOME/Desktop/sandbox-output',
 26 |               'mcp/node-code-sandbox',
 27 |             ],
 28 |           },
 29 |         },
 30 |       },
 31 |       npx: {
 32 |         mcpServers: {
 33 |           'node-code-sandbox-mcp': {
 34 |             type: 'stdio',
 35 |             command: 'npx',
 36 |             args: ['-y', 'node-code-sandbox-mcp'],
 37 |             env: {
 38 |               FILES_DIR: '/Users/your_user/Desktop/node-sandbox',
 39 |             },
 40 |           },
 41 |         },
 42 |       },
 43 |     },
 44 |     vscode: {
 45 |       docker: {
 46 |         mcp: {
 47 |           servers: {
 48 |             'js-sandbox': {
 49 |               command: 'docker',
 50 |               args: [
 51 |                 'run',
 52 |                 '-i',
 53 |                 '--rm',
 54 |                 '-v',
 55 |                 '/var/run/docker.sock:/var/run/docker.sock',
 56 |                 '-v',
 57 |                 '$HOME/Desktop/sandbox-output:/root',
 58 |                 '-e',
 59 |                 'FILES_DIR=$HOME/Desktop/sandbox-output',
 60 |                 'mcp/node-code-sandbox',
 61 |               ],
 62 |             },
 63 |           },
 64 |         },
 65 |       },
 66 |       npx: {
 67 |         mcp: {
 68 |           servers: {
 69 |             'js-sandbox': {
 70 |               type: 'stdio',
 71 |               command: 'npx',
 72 |               args: ['-y', 'node-code-sandbox-mcp'],
 73 |               env: {
 74 |                 FILES_DIR: '/Users/your_user/Desktop/node-sandbox',
 75 |               },
 76 |             },
 77 |           },
 78 |         },
 79 |       },
 80 |     },
 81 |   };
 82 | 
 83 |   const tabs = [
 84 |     { key: 'claude', label: 'Claude Desktop' },
 85 |     { key: 'vscode', label: 'VS Code' },
 86 |   ];
 87 | 
 88 |   return (
 89 |     <section id="getting-started" className="max-w-6xl mx-auto py-16">
 90 |       <h2 className="text-3xl font-bold text-center mb-4">Getting Started</h2>
 91 |       <p className="text-center text-gray-700 mb-8">
 92 |         To get started, first of all you need to import the server in your MCP
 93 |         client.
 94 |       </p>
 95 | 
 96 |       <div className="bg-white rounded-xl shadow p-6 space-y-6">
 97 |         {/* Docker Hub Info */}
 98 |         <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
 99 |           <h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
100 |             <span className="text-blue-700">📦</span> Docker Hub Available
101 |           </h3>
102 |           <p className="text-sm text-gray-700">
103 |             The MCP server is now available on Docker Hub. You can pull it
104 |             directly using:
105 |             <code className="block bg-gray-100 p-2 rounded mt-2 text-xs">
106 |               docker pull mcp/node-code-sandbox
107 |             </code>
108 |             <a
109 |               href="https://hub.docker.com/r/mcp/node-code-sandbox"
110 |               target="_blank"
111 |               rel="noopener noreferrer"
112 |               className="text-blue-600 hover:underline text-sm inline-block mt-2"
113 |             >
114 |               View on Docker Hub →
115 |             </a>
116 |           </p>
117 |         </div>
118 | 
119 |         {/* Tabs + Switch */}
120 |         <div className="flex flex-wrap justify-between items-center gap-4">
121 |           <div className="flex gap-2">
122 |             {tabs.map((tab) => (
123 |               <button
124 |                 key={tab.key}
125 |                 onClick={() =>
126 |                   setSelectedClient(tab.key as typeof selectedClient)
127 |                 }
128 |                 className={clsx(
129 |                   'px-4 py-2 rounded-full text-sm font-medium border transition-all',
130 |                   selectedClient === tab.key
131 |                     ? 'bg-green-600 text-white'
132 |                     : 'bg-gray-100 text-gray-800 hover:bg-gray-200'
133 |                 )}
134 |               >
135 |                 {tab.label}
136 |               </button>
137 |             ))}
138 |           </div>
139 |           <div className="flex items-center gap-2 text-sm">
140 |             <span className="text-gray-600">Variant:</span>
141 |             <button
142 |               onClick={() => setVariant('docker')}
143 |               className={clsx(
144 |                 'px-3 py-1 rounded-full border text-sm font-medium',
145 |                 variant === 'docker'
146 |                   ? 'bg-green-600 text-white'
147 |                   : 'bg-white text-gray-800 hover:bg-gray-100'
148 |               )}
149 |             >
150 |               Docker
151 |             </button>
152 |             <button
153 |               onClick={() => setVariant('npx')}
154 |               className={clsx(
155 |                 'px-3 py-1 rounded-full border text-sm font-medium',
156 |                 variant === 'npx'
157 |                   ? 'bg-green-600 text-white'
158 |                   : 'bg-white text-gray-800 hover:bg-gray-100'
159 |               )}
160 |             >
161 |               NPX
162 |             </button>
163 |           </div>
164 |         </div>
165 | 
166 |         {/* Config Code */}
167 |         <pre className="bg-gray-100 p-4 rounded-lg text-xs overflow-x-auto whitespace-pre">
168 |           {JSON.stringify(codeSnippets[selectedClient][variant], null, 2)}
169 |         </pre>
170 | 
171 |         <p className="text-sm text-gray-700">
172 |           Copy this snippet into your configuration file. Ensure Docker is
173 |           running and your volumes are mounted correctly.
174 |         </p>
175 |       </div>
176 |     </section>
177 |   );
178 | };
179 | 
180 | export default GettingStarted;
181 | 
```

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

```markdown
 1 | You are an expert Node.js developer. Your purpose is to write modern, efficient, and secure JavaScript code for the Node.js runtime.
 2 | 
 3 | 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.
 4 | 
 5 | ---
 6 | 
 7 | ### **1. Core Principles: Modern Syntax and APIs**
 8 | 
 9 | #### **1.1. Embrace ES Modules (ESM)**
10 | 
11 | - **Default to ESM:** Write all code using ES Modules (`import`/`export` syntax). This is the modern standard.
12 | - **No CommonJS:** **DO NOT** use CommonJS (`require()`/`module.exports`).
13 | - **Top-Level Await:** Use top-level `await` for asynchronous initialization in your main application file.
14 | 
15 | #### **1.2. Use Native APIs First**
16 | 
17 | - **HTTP Requests:** Use the global `fetch` API for all HTTP requests. **DO NOT** use `node-fetch`, `axios`, or the deprecated `request` package.
18 | - **Testing:** Use the `node:test` module and `node:assert` for writing tests. **DO NOT** use Jest, Mocha, or Chai unless specifically requested.
19 | - **URL Parsing:** Use the global `URL` constructor (`new URL(...)`). **DO NOT** use the legacy `url.parse()` API.
20 | 
21 | #### **1.3. Master Asynchronous Patterns**
22 | 
23 | - **`async/await` is Mandatory:** Use `async/await` for all asynchronous operations. It is non-negotiable for clarity and error handling.
24 | - **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`.
25 | - **Avoid Raw Promises Where Possible:** Do not chain `.then()` and `.catch()` when `async/await` provides a cleaner, linear control flow.
26 | 
27 | ---
28 | 
29 | ### **2. Performance and Concurrency**
30 | 
31 | #### **2.1. Never Block the Event Loop**
32 | 
33 | - **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`).
34 | - **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.
35 | 
36 | #### **2.2. Implement Streaming and Backpressure**
37 | 
38 | - **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.
39 | - **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.
40 | 
41 | ---
42 | 
43 | ### **3. Error Handling and Resilience**
44 | 
45 | #### **3.1. Handle Errors Robustly**
46 | 
47 | - **Comprehensive `try...catch`:** Wrap all `await` calls in `try...catch` blocks to handle potential runtime errors gracefully.
48 | - **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.
49 | - **Centralized Error Handling:** In server applications (like Express), use centralized error-handling middleware to catch and process all errors consistently.
50 | 
51 | #### **3.2. Build Resilient Services**
52 | 
53 | - **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.
54 | - **Implement Graceful Shutdown:** Your application must handle `SIGINT` and `SIGTERM` signals. On shutdown, you must:
55 |   1.  Stop accepting new requests.
56 |   2.  Finish processing in-flight requests.
57 |   3.  Close database connections and other resources.
58 |   4.  Exit the process with `process.exit(0)`.
59 | 
60 | ---
61 | 
62 | ### **4. Security First**
63 | 
64 | #### **4.1. Avoid Common Vulnerabilities**
65 | 
66 | - **Validate All Inputs:** Never trust user input. Use a schema validation library like `zod` or `joi` to validate request bodies, query parameters, and headers.
67 | - **Prevent Injection:** Use parameterized queries or ORMs to prevent SQL injection. Never construct database queries with string concatenation.
68 | - **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.
69 | - **Secure Dependencies:** Always use a lockfile (`package-lock.json`). Regularly audit dependencies with `npm audit`.
70 | 
71 | #### **4.2. Secure Coding Practices**
72 | 
73 | - **No Unsafe Execution:** **NEVER** use `eval()` or `new Function('...')` with dynamic strings. It is a massive security risk.
74 | - **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.
75 | - **Manage Secrets:** **NEVER** hardcode secrets (API keys, passwords) in the source code. Load them from environment variables (e.g., using `dotenv` in development).
76 | 
77 | ---
78 | 
79 | ### **5. Code Style and Structure**
80 | 
81 | #### **5.1. Modern JavaScript Syntax**
82 | 
83 | - **`const` Over `let`:** Use `const` by default. Only use `let` if a variable must be reassigned. **NEVER** use `var`.
84 | - **Strict Equality:** Always use strict equality (`===` and `!==`). **DO NOT** use loose equality (`==` and `!=`).
85 | - **No Prototype Extension:** **NEVER** modify the prototypes of built-in objects like `Object.prototype` or `Array.prototype`.
86 | 
87 | #### **5.2. Maintain Clean Code**
88 | 
89 | - **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.
90 | - **Pure Functions:** Prefer pure functions that do not have side effects. Avoid modifying function arguments directly.
91 | - **Prevent Circular Dependencies:** Structure your files and modules to avoid circular `import` statements, which can cause runtime errors.
92 | 
```

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

```typescript
  1 | import { z } from 'zod';
  2 | import { execFileSync } from 'node:child_process';
  3 | import { type McpResponse, textContent, type McpContent } from '../types.ts';
  4 | import { prepareWorkspace } from '../runUtils.ts';
  5 | import {
  6 |   DOCKER_NOT_RUNNING_ERROR,
  7 |   isDockerRunning,
  8 |   waitForPortHttp,
  9 |   sanitizeContainerId,
 10 | } from '../utils.ts';
 11 | import {
 12 |   changesToMcpContent,
 13 |   detectChanges,
 14 |   getMountPointDir,
 15 |   getSnapshot,
 16 | } from '../snapshotUtils.ts';
 17 | import {
 18 |   getContentFromError,
 19 |   safeExecNodeInContainer,
 20 | } from '../dockerUtils.ts';
 21 | import { lintAndRefactorCode } from '../linterUtils.ts';
 22 | 
 23 | const NodeDependency = z.object({
 24 |   name: z.string().describe('npm package name, e.g. lodash'),
 25 |   version: z.string().describe('npm package version range, e.g. ^4.17.21'),
 26 | });
 27 | 
 28 | export const argSchema = {
 29 |   container_id: z.string().describe('Docker container identifier'),
 30 |   // We use an array of { name, version } items instead of a record
 31 |   // because the OpenAI function-calling schema doesn’t reliably support arbitrary
 32 |   // object keys. An explicit array ensures each dependency has a clear, uniform
 33 |   // structure the model can populate.
 34 |   // Schema for a single dependency item
 35 |   dependencies: z
 36 |     .array(NodeDependency)
 37 |     .default([])
 38 |     .describe(
 39 |       'A list of npm dependencies to install before running the code. ' +
 40 |         'Each item must have a `name` (package) and `version` (range). ' +
 41 |         'If none, returns an empty array.'
 42 |     ),
 43 |   code: z.string().describe('JavaScript code to run inside the container.'),
 44 |   listenOnPort: z
 45 |     .number()
 46 |     .optional()
 47 |     .describe(
 48 |       'If set, leaves the process running and exposes this port to the host.'
 49 |     ),
 50 | };
 51 | 
 52 | type DependenciesArray = Array<{ name: string; version: string }>;
 53 | 
 54 | export default async function runJs({
 55 |   container_id,
 56 |   code,
 57 |   dependencies = [],
 58 |   listenOnPort,
 59 | }: {
 60 |   container_id: string;
 61 |   code: string;
 62 |   dependencies?: DependenciesArray;
 63 |   listenOnPort?: number;
 64 | }): Promise<McpResponse> {
 65 |   const validId = sanitizeContainerId(container_id);
 66 |   if (!validId) {
 67 |     return { content: [textContent('Invalid container ID')] };
 68 |   }
 69 | 
 70 |   if (!isDockerRunning()) {
 71 |     return { content: [textContent(DOCKER_NOT_RUNNING_ERROR)] };
 72 |   }
 73 | 
 74 |   // Lint and refactor the code first.
 75 |   const { fixedCode, errorReport } = await lintAndRefactorCode(code);
 76 | 
 77 |   const telemetry: Record<string, unknown> = {};
 78 |   const dependenciesRecord: Record<string, string> = Object.fromEntries(
 79 |     dependencies.map(({ name, version }) => [name, version])
 80 |   );
 81 | 
 82 |   // Create workspace in container
 83 |   const localWorkspace = await prepareWorkspace({
 84 |     code: fixedCode,
 85 |     dependenciesRecord,
 86 |   });
 87 |   execFileSync('docker', [
 88 |     'cp',
 89 |     `${localWorkspace.name}/.`,
 90 |     `${validId}:/workspace`,
 91 |   ]);
 92 | 
 93 |   let rawOutput: string = '';
 94 | 
 95 |   // Generate snapshot of the workspace
 96 |   const snapshotStartTime = Date.now();
 97 |   const snapshot = await getSnapshot(getMountPointDir());
 98 | 
 99 |   if (listenOnPort) {
100 |     if (dependencies.length > 0) {
101 |       const installStart = Date.now();
102 |       const installOutput = execFileSync(
103 |         'docker',
104 |         [
105 |           'exec',
106 |           validId,
107 |           '/bin/sh',
108 |           '-c',
109 |           'npm install --omit=dev --prefer-offline --no-audit --loglevel=error',
110 |         ],
111 |         { encoding: 'utf8' }
112 |       );
113 |       telemetry.installTimeMs = Date.now() - installStart;
114 |       telemetry.installOutput = installOutput;
115 |     } else {
116 |       telemetry.installTimeMs = 0;
117 |       telemetry.installOutput = 'Skipped npm install (no dependencies)';
118 |     }
119 | 
120 |     const { error, duration } = safeExecNodeInContainer({
121 |       containerId: validId,
122 |       command: `nohup node index.js > output.log 2>&1 &`,
123 |     });
124 |     telemetry.runTimeMs = duration;
125 |     if (error) {
126 |       const errorResponse = getContentFromError(error, telemetry);
127 |       if (errorReport) {
128 |         errorResponse.content.unshift(
129 |           textContent(
130 |             `Linting issues found (some may have been auto-fixed):\n${errorReport}`
131 |           )
132 |         );
133 |       }
134 |       return errorResponse;
135 |     }
136 | 
137 |     await waitForPortHttp(listenOnPort);
138 |     rawOutput = `Server started in background; logs at /output.log`;
139 |   } else {
140 |     if (dependencies.length > 0) {
141 |       const installStart = Date.now();
142 |       const fullCmd =
143 |         'npm install --omit=dev --prefer-offline --no-audit --loglevel=error';
144 |       const installOutput = execFileSync(
145 |         'docker',
146 |         ['exec', validId, '/bin/sh', '-c', fullCmd],
147 |         { encoding: 'utf8' }
148 |       );
149 |       telemetry.installTimeMs = Date.now() - installStart;
150 |       telemetry.installOutput = installOutput;
151 |     } else {
152 |       telemetry.installTimeMs = 0;
153 |       telemetry.installOutput = 'Skipped npm install (no dependencies)';
154 |     }
155 | 
156 |     const { output, error, duration } = safeExecNodeInContainer({
157 |       containerId: validId,
158 |     });
159 | 
160 |     if (output) rawOutput = output;
161 |     telemetry.runTimeMs = duration;
162 |     if (error) {
163 |       const errorResponse = getContentFromError(error, telemetry);
164 |       if (errorReport) {
165 |         errorResponse.content.unshift(
166 |           textContent(
167 |             `Linting issues found (some may have been auto-fixed):\n${errorReport}`
168 |           )
169 |         );
170 |       }
171 |       return errorResponse;
172 |     }
173 |   }
174 | 
175 |   // Detect the file changed during the execution of the tool in the mounted workspace
176 |   // and report the changes to the user
177 |   const changes = await detectChanges(
178 |     snapshot,
179 |     getMountPointDir(),
180 |     snapshotStartTime
181 |   );
182 | 
183 |   const extractedContents = await changesToMcpContent(changes);
184 |   localWorkspace.removeCallback();
185 | 
186 |   const responseContent: McpContent[] = [];
187 |   if (errorReport) {
188 |     responseContent.push(
189 |       textContent(
190 |         `Linting issues found (some may have been auto-fixed):\n${errorReport}`
191 |       )
192 |     );
193 |   }
194 | 
195 |   return {
196 |     content: [
197 |       ...(responseContent.length ? responseContent : []),
198 |       textContent(`Node.js process output:\n${rawOutput}`),
199 |       ...extractedContents,
200 |       textContent(`Telemetry:\n${JSON.stringify(telemetry, null, 2)}`),
201 |     ],
202 |   };
203 | }
204 | 
```
Page 1/3FirstPrevNextLast