#
tokens: 49148/50000 15/90 files (page 2/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 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

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

```typescript
  1 | import { existsSync, readFileSync } from 'fs';
  2 | import { execFileSync } from 'node:child_process';
  3 | import { getConfig } from './config.ts';
  4 | 
  5 | export function isRunningInDocker() {
  6 |   // 1. The "/.dockerenv" sentinel file
  7 |   if (existsSync('/.dockerenv')) return true;
  8 | 
  9 |   // 2. cgroup data often embeds "docker" or "kubepods"
 10 |   try {
 11 |     if (existsSync('/proc/1/cgroup')) {
 12 |       const cgroup = readFileSync('/proc/1/cgroup', 'utf8');
 13 |       if (cgroup.includes('docker') || cgroup.includes('kubepods')) {
 14 |         return true;
 15 |       }
 16 |     }
 17 |   } catch {
 18 |     // unreadable or missing → assume "not"
 19 |   }
 20 | 
 21 |   // 3. Check for environment variables commonly set in Docker
 22 |   if (process.env.DOCKER_CONTAINER || process.env.DOCKER_ENV) {
 23 |     return true;
 24 |   }
 25 | 
 26 |   // On macOS or Windows for tests, just return false
 27 |   return false;
 28 | }
 29 | 
 30 | export function preprocessDependencies({
 31 |   dependencies,
 32 |   image,
 33 | }: {
 34 |   dependencies: Array<{ name: string; version: string }>;
 35 |   image?: string;
 36 | }): Record<string, string> {
 37 |   const dependenciesRecord: Record<string, string> = Object.fromEntries(
 38 |     dependencies.map(({ name, version }) => [name, version])
 39 |   );
 40 | 
 41 |   // This image has a pre-cached version of chartjs-node-canvas,
 42 |   // but we still need to explicitly declare it in package.json
 43 |   if (image?.includes('alfonsograziano/node-chartjs-canvas')) {
 44 |     dependenciesRecord['chartjs-node-canvas'] = '4.0.0';
 45 |     dependenciesRecord['@mermaid-js/mermaid-cli'] = '^11.4.2';
 46 |   }
 47 | 
 48 |   return dependenciesRecord;
 49 | }
 50 | 
 51 | export const DEFAULT_NODE_IMAGE = 'node:lts-slim';
 52 | 
 53 | export const PLAYWRIGHT_IMAGE = 'mcr.microsoft.com/playwright:v1.55.0-noble';
 54 | 
 55 | export const suggestedImages = {
 56 |   'node:lts-slim': {
 57 |     description: 'Node.js LTS version, slim variant.',
 58 |     reason: 'Lightweight and fast for JavaScript execution tasks.',
 59 |   },
 60 |   [PLAYWRIGHT_IMAGE]: {
 61 |     description: 'Playwright image for browser automation.',
 62 |     reason: 'Preconfigured for running Playwright scripts.',
 63 |   },
 64 |   'alfonsograziano/node-chartjs-canvas:latest': {
 65 |     description:
 66 |       'Chart.js image for chart generation and mermaid charts generation.',
 67 |     reason: `'Preconfigured for generating charts with chartjs-node-canvas and Mermaid. Minimal Mermaid example:
 68 |     import fs from "fs";
 69 |     import { run } from "@mermaid-js/mermaid-cli";
 70 |     fs.writeFileSync("./files/diagram.mmd", "graph LR; A-->B;", "utf8");
 71 |     await run("./files/diagram.mmd", "./files/diagram.svg");`,
 72 |   },
 73 | };
 74 | 
 75 | export const generateSuggestedImages = () => {
 76 |   return Object.entries(suggestedImages)
 77 |     .map(([image, { description, reason }]) => {
 78 |       return `- **${image}**: ${description} (${reason})`;
 79 |     })
 80 |     .join('\n');
 81 | };
 82 | 
 83 | export async function waitForPortHttp(
 84 |   port: number,
 85 |   timeoutMs = 10_000,
 86 |   intervalMs = 250
 87 | ): Promise<void> {
 88 |   const start = Date.now();
 89 | 
 90 |   while (Date.now() - start < timeoutMs) {
 91 |     try {
 92 |       const res = await fetch(`http://localhost:${port}`);
 93 |       if (res.ok || res.status === 404) return; // server is up
 94 |     } catch {
 95 |       // server not ready
 96 |     }
 97 | 
 98 |     await new Promise((r) => setTimeout(r, intervalMs));
 99 |   }
100 | 
101 |   throw new Error(
102 |     `Timeout: Server did not respond on http://localhost:${port} within ${timeoutMs}ms`
103 |   );
104 | }
105 | 
106 | export function isDockerRunning() {
107 |   try {
108 |     execFileSync('docker', ['info']);
109 |     return true;
110 |     // eslint-disable-next-line @typescript-eslint/no-unused-vars
111 |   } catch (e) {
112 |     return false;
113 |   }
114 | }
115 | export const DOCKER_NOT_RUNNING_ERROR =
116 |   'Error: Docker is not running. Please start Docker and try again.';
117 | 
118 | export interface Limits {
119 |   memory?: string;
120 |   cpus?: string;
121 | }
122 | 
123 | export const IMAGE_DEFAULTS: Record<string, Limits> = {
124 |   'node:lts-slim': { memory: '512m', cpus: '1' },
125 |   'alfonsograziano/node-chartjs': { memory: '2g', cpus: '2' },
126 |   'mcr.microsoft.com/playwright': { memory: '2g', cpus: '2' },
127 | };
128 | 
129 | export function computeResourceLimits(image: string) {
130 |   const base = { memFlag: '', cpuFlag: '' };
131 |   if (!image) return base;
132 | 
133 |   const def =
134 |     Object.entries(IMAGE_DEFAULTS).find(([key]) => image.includes(key))?.[1] ??
135 |     {};
136 | 
137 |   const memory = getConfig().rawMemoryLimit ?? def.memory;
138 |   const cpus = getConfig().rawCpuLimit ?? def.cpus;
139 | 
140 |   return {
141 |     ...base,
142 |     memFlag: memory ? `--memory ${memory}` : '',
143 |     cpuFlag: cpus ? `--cpus ${cpus}` : '',
144 |   };
145 | }
146 | 
147 | /**
148 |  * Sanitizes and validates a Docker container ID or name.
149 |  * Docker container names/IDs must match [a-zA-Z0-9][a-zA-Z0-9_.-]*
150 |  * @param id The container ID or name to validate
151 |  * @returns The sanitized ID if valid, otherwise null
152 |  */
153 | export function sanitizeContainerId(id: string): string | null {
154 |   // Docker container names/IDs: https://docs.docker.com/engine/reference/commandline/run/#container-name
155 |   // Allow alphanumerics, underscores, periods, dashes. Must start with alphanumeric.
156 |   if (typeof id !== 'string') return null;
157 |   if (/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(id)) return id;
158 |   return null;
159 | }
160 | 
161 | /**
162 |  * Sanitizes and validates a Docker image name (optionally with tag).
163 |  * @param image The image name to validate
164 |  * @returns The sanitized image name if valid, otherwise null
165 |  */
166 | export function sanitizeImageName(image: string): string | null {
167 |   // Docker image names: [registry/][user/]repo[:tag]
168 |   // Allow alphanumerics, underscores, periods, dashes, slashes, colons
169 |   if (typeof image !== 'string') return null;
170 |   if (/^[a-zA-Z0-9_.:/-]+$/.test(image)) return image;
171 |   return null;
172 | }
173 | 
174 | /**
175 |  * Sanitizes a shell command to be run inside a container. This is a basic check;
176 |  * for more advanced needs, consider whitelisting allowed commands.
177 |  * @param cmd The command string
178 |  * @returns The sanitized command if valid, otherwise null
179 |  */
180 | export function sanitizeShellCommand(cmd: string): string | null {
181 |   // For now, just check it's a non-empty string and doesn't contain dangerous metacharacters
182 |   if (typeof cmd !== 'string' || !cmd.trim()) return null;
183 |   // Disallow command substitution (backticks and $()) which are most dangerous
184 |   if (/[`]|\$\([^)]+\)/.test(cmd)) return null;
185 |   // Allow >, <, &, | for redirection and backgrounding, as needed for listenOnPort
186 |   // Still block backticks and $() for command substitution
187 |   return cmd;
188 | }
189 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  2 | import { tmpdir } from 'os';
  3 | import { mkdtempSync, rmSync, readFileSync } from 'fs';
  4 | import path from 'path';
  5 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  6 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
  7 | import dotenv from 'dotenv';
  8 | import { type McpResponse } from '../src/types.ts';
  9 | import fs from 'fs';
 10 | import { execSync } from 'child_process';
 11 | import { normalizeMountPath } from './utils.ts';
 12 | 
 13 | dotenv.config();
 14 | const __dirname = path.dirname(new URL(import.meta.url).pathname);
 15 | 
 16 | describe('Node.js Code Sandbox MCP Tests', () => {
 17 |   let hostWorkspaceDir: string;
 18 |   let containerWorkspaceDir: string;
 19 |   let client: Client;
 20 | 
 21 |   beforeAll(async () => {
 22 |     hostWorkspaceDir = mkdtempSync(path.join(tmpdir(), 'ws-'));
 23 |     containerWorkspaceDir = normalizeMountPath(hostWorkspaceDir);
 24 | 
 25 |     // Build the latest version of the Docker image
 26 |     execSync('docker build -t alfonsograziano/node-code-sandbox-mcp .', {
 27 |       stdio: 'inherit',
 28 |     });
 29 | 
 30 |     client = new Client({ name: 'node_js_sandbox_test', version: '1.0.0' });
 31 | 
 32 |     await client.connect(
 33 |       new StdioClientTransport({
 34 |         command: 'docker',
 35 |         args: [
 36 |           'run',
 37 |           '-i',
 38 |           '--rm',
 39 |           '-v',
 40 |           '/var/run/docker.sock:/var/run/docker.sock',
 41 |           '-v',
 42 |           `${hostWorkspaceDir}:/root`,
 43 |           '-e',
 44 |           `FILES_DIR=${containerWorkspaceDir}`,
 45 |           'alfonsograziano/node-code-sandbox-mcp',
 46 |         ],
 47 |       })
 48 |     );
 49 |   }, 200_000);
 50 | 
 51 |   afterAll(() => {
 52 |     rmSync(hostWorkspaceDir, { recursive: true, force: true });
 53 |     // Optional: Stop any potentially lingering client connections
 54 |     client?.close();
 55 |   });
 56 | 
 57 |   it('should run a console.log', async () => {
 58 |     const code = `console.log("Hello from workspace!");`;
 59 | 
 60 |     const result = (await client.callTool({
 61 |       name: 'run_js_ephemeral',
 62 |       arguments: { code, dependencies: [] },
 63 |     })) as { content: Array<{ type: string; text: string }> };
 64 | 
 65 |     expect(result).toBeDefined();
 66 |     expect(result.content).toBeInstanceOf(Array);
 67 |     expect(result.content[0]).toMatchObject({
 68 |       type: 'text',
 69 |     });
 70 | 
 71 |     const outputText = result.content[0].text;
 72 |     expect(outputText).toContain('Hello from workspace!');
 73 |     expect(outputText).toContain('Node.js process output');
 74 |   });
 75 | 
 76 |   describe('runJsEphemeral via MCP client (files)', () => {
 77 |     it('should read and write files using the host-mounted /files', async () => {
 78 |       const inputFileName = 'text.txt';
 79 |       const inputFilePath = path.join(hostWorkspaceDir, inputFileName);
 80 |       const inputContent = 'This is a file from the host.';
 81 |       fs.writeFileSync(inputFilePath, inputContent, 'utf-8');
 82 | 
 83 |       const outputFileName = 'output-host.txt';
 84 |       const outputContent = 'This file was created in the sandbox.';
 85 | 
 86 |       const code = `
 87 |         import fs from 'fs';
 88 |         const input = fs.readFileSync('./files/${inputFileName}', 'utf-8');
 89 |         console.log('Input file content:', input);
 90 |         fs.writeFileSync('./files/${outputFileName}', input + ' | ${outputContent}');
 91 |         console.log('Files processed.');
 92 |       `;
 93 | 
 94 |       const result = (await client.callTool({
 95 |         name: 'run_js_ephemeral',
 96 |         arguments: { code, dependencies: [] },
 97 |       })) as McpResponse;
 98 | 
 99 |       expect(result).toBeDefined();
100 |       expect(result.content).toBeDefined();
101 | 
102 |       // Process output
103 |       const processOutput = result.content.find(
104 |         (item) =>
105 |           item.type === 'text' &&
106 |           item.text.startsWith('Node.js process output:')
107 |       );
108 |       expect(processOutput).toBeDefined();
109 |       expect((processOutput as { text: string }).text).toContain(
110 |         'Input file content: This is a file from the host.'
111 |       );
112 |       expect((processOutput as { text: string }).text).toContain(
113 |         'Files processed.'
114 |       );
115 | 
116 |       // File creation message
117 |       const fileChangeInfo = result.content.find(
118 |         (item) =>
119 |           item.type === 'text' && item.text.startsWith('List of changed files:')
120 |       );
121 |       expect(fileChangeInfo).toBeDefined();
122 |       expect((fileChangeInfo as { text: string }).text).toContain(
123 |         '- output-host.txt was created'
124 |       );
125 | 
126 |       // Resource
127 |       const resource = result.content.find(
128 |         (item) =>
129 |           item.type === 'resource' &&
130 |           'resource' in item &&
131 |           typeof item.resource?.uri === 'string'
132 |       );
133 |       expect(resource).toBeDefined();
134 |       const resourceData = (
135 |         resource as {
136 |           resource: { mimeType: string; uri: string; text: string };
137 |         }
138 |       ).resource;
139 |       expect(resourceData.mimeType).toBe('text/plain');
140 |       expect(resourceData.uri).toContain('output-host.txt');
141 |       expect(resourceData.uri).toContain('file://');
142 |       expect(resourceData.text).toBe('output-host.txt');
143 | 
144 |       // Telemetry
145 |       const telemetry = result.content.find(
146 |         (item) => item.type === 'text' && item.text.startsWith('Telemetry:')
147 |       );
148 |       expect(telemetry).toBeDefined();
149 |       expect((telemetry as { text: string }).text).toContain('"installTimeMs"');
150 |       expect((telemetry as { text: string }).text).toContain('"runTimeMs"');
151 |     });
152 |   });
153 | 
154 |   describe('run-node-js-script prompt', () => {
155 |     it('should include NODE_GUIDELINES.md in the prompt response', async () => {
156 |       const expectedGuidelines = readFileSync(
157 |         path.join(__dirname, '../NODE_GUIDELINES.md'),
158 |         'utf-8'
159 |       );
160 | 
161 |       const userPrompt = "Please create a file named 'test.txt'.";
162 | 
163 |       // Get the prompt
164 |       const result = await client.getPrompt({
165 |         name: 'run-node-js-script',
166 |         arguments: { prompt: userPrompt },
167 |       });
168 | 
169 |       expect(result).toBeDefined();
170 |       expect(result.messages).toBeInstanceOf(Array);
171 |       expect(result.messages.length).toBeGreaterThan(0);
172 | 
173 |       const messageContent = result.messages[0].content;
174 |       expect(messageContent.type).toBe('text');
175 | 
176 |       const responseText = messageContent.text;
177 | 
178 |       // Check that the response text includes both the user's prompt and the full guidelines
179 |       expect(responseText).toContain(userPrompt);
180 |       expect(responseText).toContain(expectedGuidelines);
181 |     });
182 |   });
183 | }, 200_000);
184 | 
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import {
  4 |   McpServer,
  5 |   ResourceTemplate,
  6 | } from '@modelcontextprotocol/sdk/server/mcp.js';
  7 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  8 | import { randomUUID } from 'crypto';
  9 | import initializeSandbox, {
 10 |   argSchema as initializeSchema,
 11 |   setServerRunId,
 12 | } from './tools/initialize.ts';
 13 | import execInSandbox, { argSchema as execSchema } from './tools/exec.ts';
 14 | import runJs, { argSchema as runJsSchema } from './tools/runJs.ts';
 15 | import stopSandbox, { argSchema as stopSchema } from './tools/stop.ts';
 16 | import runJsEphemeral, {
 17 |   argSchema as ephemeralSchema,
 18 | } from './tools/runJsEphemeral.ts';
 19 | import mime from 'mime-types';
 20 | import fs from 'fs/promises';
 21 | import path from 'path';
 22 | import { fileURLToPath } from 'url';
 23 | import { z } from 'zod';
 24 | import { getConfig } from './config.ts';
 25 | import { startScavenger, cleanActiveContainers } from './containerUtils.ts';
 26 | import { setServerInstance, logger } from './logger.ts';
 27 | import getDependencyTypes, {
 28 |   argSchema as getDependencyTypesSchema,
 29 | } from './tools/getDependencyTypes.ts';
 30 | import searchNpmPackages, {
 31 |   SearchNpmPackagesToolSchema,
 32 | } from './tools/searchNpmPackages.ts';
 33 | const __filename = fileURLToPath(import.meta.url);
 34 | const __dirname = path.dirname(__filename);
 35 | 
 36 | import packageJson from '../package.json' with { type: 'json' };
 37 | 
 38 | export const serverRunId = randomUUID();
 39 | setServerRunId(serverRunId);
 40 | 
 41 | const nodeGuidelines = await fs.readFile(
 42 |   path.join(__dirname, '..', 'NODE_GUIDELINES.md'),
 43 |   'utf-8'
 44 | );
 45 | 
 46 | // Create the server with logging capability enabled
 47 | const server = new McpServer(
 48 |   {
 49 |     name: packageJson.name,
 50 |     version: packageJson.version,
 51 |     description: packageJson.description,
 52 |   },
 53 |   {
 54 |     capabilities: {
 55 |       logging: {},
 56 |       tools: {},
 57 |     },
 58 |   }
 59 | );
 60 | 
 61 | // Set the server instance for logging
 62 | setServerInstance(server);
 63 | 
 64 | // Configure server tools and resources
 65 | server.tool(
 66 |   'sandbox_initialize',
 67 |   'Start a new isolated Docker container running Node.js. Used to set up a sandbox session for multiple commands and scripts.',
 68 |   initializeSchema,
 69 |   initializeSandbox
 70 | );
 71 | 
 72 | server.tool(
 73 |   'sandbox_exec',
 74 |   'Execute one or more shell commands inside a running sandbox container. Requires a sandbox initialized beforehand.',
 75 |   execSchema,
 76 |   execInSandbox
 77 | );
 78 | 
 79 | server.tool(
 80 |   'run_js',
 81 |   `Install npm dependencies and run JavaScript code inside a running sandbox container.
 82 |   After running, you must manually stop the sandbox to free resources.
 83 |   The code must be valid ESModules (import/export syntax). Best for complex workflows where you want to reuse the environment across multiple executions.
 84 |   When reading and writing from the Node.js processes, you always need to read from and write to the "./files" directory to ensure persistence on the mounted volume.`,
 85 |   runJsSchema,
 86 |   runJs
 87 | );
 88 | 
 89 | server.tool(
 90 |   'sandbox_stop',
 91 |   'Terminate and remove a running sandbox container. Should be called after finishing work in a sandbox initialized with sandbox_initialize.',
 92 |   stopSchema,
 93 |   stopSandbox
 94 | );
 95 | 
 96 | server.tool(
 97 |   'run_js_ephemeral',
 98 |   `Run a JavaScript snippet in a temporary disposable container with optional npm dependencies, then automatically clean up. 
 99 |   The code must be valid ESModules (import/export syntax). Ideal for simple one-shot executions without maintaining a sandbox or managing cleanup manually.
100 |   When reading and writing from the Node.js processes, you always need to read from and write to the "./files" directory to ensure persistence on the mounted volume.
101 |   This includes images (e.g., PNG, JPEG) and other files (e.g., text, JSON, binaries).
102 | 
103 |   Example:
104 |   \`\`\`js
105 |   import fs from "fs/promises";
106 |   await fs.writeFile("./files/hello.txt", "Hello world!");
107 |   console.log("Saved ./files/hello.txt");
108 |   \`\`\`
109 | `,
110 |   ephemeralSchema,
111 |   runJsEphemeral
112 | );
113 | 
114 | server.tool(
115 |   'get_dependency_types',
116 |   `
117 |   Given an array of npm package names (and optional versions), 
118 |   fetch whether each package ships its own TypeScript definitions 
119 |   or has a corresponding @types/… package, and return the raw .d.ts text.
120 |   
121 |   Useful whenwhen you're about to run a Node.js script against an unfamiliar dependency 
122 |   and want to inspect what APIs and types it exposes.
123 |   `,
124 |   getDependencyTypesSchema,
125 |   getDependencyTypes
126 | );
127 | 
128 | server.tool(
129 |   'search_npm_packages',
130 |   'Search for npm packages by a search term and get their name, description, and a README snippet.',
131 |   SearchNpmPackagesToolSchema.shape,
132 |   searchNpmPackages
133 | );
134 | 
135 | server.resource(
136 |   'file',
137 |   new ResourceTemplate('file://{+filepath}', { list: undefined }),
138 |   async (uri) => {
139 |     const filepath = new URL(uri).pathname;
140 |     const data = await fs.readFile(filepath);
141 |     const mimeType = mime.lookup(filepath) || 'application/octet-stream';
142 |     return {
143 |       contents: [
144 |         {
145 |           uri: uri.toString(),
146 |           mimeType,
147 |           blob: data.toString('base64'),
148 |         },
149 |       ],
150 |     };
151 |   }
152 | );
153 | 
154 | server.prompt('run-node-js-script', { prompt: z.string() }, ({ prompt }) => ({
155 |   messages: [
156 |     {
157 |       role: 'user',
158 |       content: {
159 |         type: 'text',
160 |         text:
161 |           `Here is my prompt:\n\n${prompt}\n\n` +
162 |           nodeGuidelines +
163 |           `\n---\n\n` +
164 |           `Please write and run a Node.js script that fulfills my prompt.`,
165 |       },
166 |     },
167 |   ],
168 | }));
169 | 
170 | const scavengerIntervalHandle = startScavenger(
171 |   getConfig().containerTimeoutMilliseconds,
172 |   getConfig().containerTimeoutSeconds
173 | );
174 | 
175 | async function gracefulShutdown(signal: string) {
176 |   logger.info(`Received ${signal}. Starting graceful shutdown...`);
177 | 
178 |   clearInterval(scavengerIntervalHandle);
179 |   logger.info('Stopped container scavenger.');
180 | 
181 |   await cleanActiveContainers();
182 | 
183 |   setTimeout(() => {
184 |     logger.info('Exiting.');
185 |     process.exit(0);
186 |   }, 500);
187 | }
188 | 
189 | process.on('SIGINT', () => gracefulShutdown('SIGINT'));
190 | process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
191 | process.on('SIGUSR2', () => gracefulShutdown('SIGUSR2'));
192 | 
193 | // Set up the transport
194 | const transport = new StdioServerTransport();
195 | 
196 | // Connect the server to start receiving and sending messages
197 | logger.info('Initializing server...');
198 | await server.connect(transport);
199 | logger.info('Server started and connected successfully');
200 | logger.info(
201 |   `Container timeout set to: ${getConfig().containerTimeoutSeconds} seconds (${getConfig().containerTimeoutMilliseconds}ms)`
202 | );
203 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  2 | import type { Mock } from 'vitest';
  3 | import { z } from 'zod';
  4 | import getDependencyTypes, {
  5 |   argSchema,
  6 | } from '../src/tools/getDependencyTypes.ts';
  7 | import type { McpContentText } from '../src/types.ts';
  8 | 
  9 | // Schema validation tests
 10 | describe('argSchema', () => {
 11 |   it('should accept valid dependencies array with version', () => {
 12 |     const input = { dependencies: [{ name: 'foo', version: '1.2.3' }] };
 13 |     const parsed = z.object(argSchema).parse(input);
 14 |     expect(parsed).toEqual(input);
 15 |   });
 16 | 
 17 |   it('should accept valid dependencies array without version', () => {
 18 |     const input = { dependencies: [{ name: 'bar' }] };
 19 |     const parsed = z.object(argSchema).parse(input);
 20 |     expect(parsed).toEqual({
 21 |       dependencies: [{ name: 'bar', version: undefined }],
 22 |     });
 23 |   });
 24 | 
 25 |   it('should reject when dependencies is missing', () => {
 26 |     expect(() => z.object(argSchema).parse({} as any)).toThrow();
 27 |   });
 28 | });
 29 | 
 30 | // Tool behavior tests
 31 | describe('getDependencyTypes', () => {
 32 |   beforeEach(() => {
 33 |     // Stub global.fetch
 34 |     vi.stubGlobal('fetch', vi.fn());
 35 |   });
 36 | 
 37 |   afterEach(() => {
 38 |     vi.unstubAllGlobals();
 39 |   });
 40 | 
 41 |   it('should return in-package types when available', async () => {
 42 |     // Mock registry metadata fetch
 43 |     (fetch as Mock).mockImplementation((url: string) => {
 44 |       if (url === 'https://registry.npmjs.org/foo') {
 45 |         return Promise.resolve({
 46 |           ok: true,
 47 |           json: async () => ({
 48 |             'dist-tags': { latest: '1.0.0' },
 49 |             versions: { '1.0.0': { types: 'index.d.ts' } },
 50 |           }),
 51 |         });
 52 |       }
 53 |       if (url === 'https://unpkg.com/[email protected]/index.d.ts') {
 54 |         return Promise.resolve({
 55 |           ok: true,
 56 |           text: async () => '/* foo types */',
 57 |         });
 58 |       }
 59 |       return Promise.resolve({ ok: false });
 60 |     });
 61 | 
 62 |     const response = await getDependencyTypes({
 63 |       dependencies: [{ name: 'foo' }],
 64 |     });
 65 |     // Verify structure
 66 |     expect(response.content).toHaveLength(1);
 67 |     const item = response.content[0] as McpContentText;
 68 |     expect(item.type).toBe('text');
 69 | 
 70 |     const parsed = JSON.parse(item.text) as any[];
 71 |     expect(parsed).toEqual([
 72 |       {
 73 |         name: 'foo',
 74 |         hasTypes: true,
 75 |         types: '/* foo types */',
 76 |         version: '1.0.0',
 77 |       },
 78 |     ]);
 79 |   });
 80 | 
 81 |   it('should fallback to @types package when in-package types are missing', async () => {
 82 |     (fetch as Mock).mockImplementation((url: string) => {
 83 |       if (url === 'https://registry.npmjs.org/bar') {
 84 |         return Promise.resolve({
 85 |           ok: true,
 86 |           json: async () => ({
 87 |             'dist-tags': { latest: '2.0.0' },
 88 |             versions: { '2.0.0': {} },
 89 |           }),
 90 |         });
 91 |       }
 92 |       if (url === 'https://registry.npmjs.org/%40types%2Fbar') {
 93 |         return Promise.resolve({
 94 |           ok: true,
 95 |           json: async () => ({
 96 |             'dist-tags': { latest: '3.1.4' },
 97 |             versions: { '3.1.4': { typings: 'index.d.ts' } },
 98 |           }),
 99 |         });
100 |       }
101 |       if (url === 'https://unpkg.com/@types/[email protected]/index.d.ts') {
102 |         return Promise.resolve({
103 |           ok: true,
104 |           text: async () => '/* bar external types */',
105 |         });
106 |       }
107 |       return Promise.resolve({ ok: false });
108 |     });
109 | 
110 |     const response = await getDependencyTypes({
111 |       dependencies: [{ name: 'bar' }],
112 |     });
113 |     expect(response.content).toHaveLength(1);
114 |     const item = response.content[0] as McpContentText;
115 |     expect(item.type).toBe('text');
116 | 
117 |     const parsed = JSON.parse(item.text) as any[];
118 |     expect(parsed).toEqual([
119 |       {
120 |         name: 'bar',
121 |         hasTypes: true,
122 |         types: '/* bar external types */',
123 |         typesPackage: '@types/bar',
124 |         version: '3.1.4',
125 |       },
126 |     ]);
127 |   });
128 | 
129 |   it('should return hasTypes false when no types are available', async () => {
130 |     (fetch as Mock).mockImplementation((url: string) => {
131 |       if (url.startsWith('https://registry.npmjs.org/')) {
132 |         return Promise.resolve({
133 |           ok: true,
134 |           json: async () => ({
135 |             'dist-tags': { latest: '0.1.0' },
136 |             versions: { '0.1.0': {} },
137 |           }),
138 |         });
139 |       }
140 |       // @types lookup fails
141 |       return Promise.resolve({ ok: false });
142 |     });
143 | 
144 |     const response = await getDependencyTypes({
145 |       dependencies: [{ name: 'baz' }],
146 |     });
147 |     const item = response.content[0] as McpContentText;
148 | 
149 |     const parsed = JSON.parse(item.text) as any[];
150 |     expect(parsed).toEqual([{ name: 'baz', hasTypes: false }]);
151 |   });
152 | 
153 |   it('should handle fetch errors gracefully', async () => {
154 |     (fetch as Mock).mockImplementation(() => {
155 |       throw new Error('Network failure');
156 |     });
157 | 
158 |     const response = await getDependencyTypes({
159 |       dependencies: [{ name: 'qux' }],
160 |     });
161 |     const item = response.content[0] as McpContentText;
162 | 
163 |     const parsed = JSON.parse(item.text) as any[];
164 |     expect(parsed).toEqual([{ name: 'qux', hasTypes: false }]);
165 |   });
166 | });
167 | 
168 | describe('getDependencyTypes integration test', () => {
169 |   it('should fetch real types for dayjs', async () => {
170 |     // Restore real fetch to allow network requests
171 |     vi.restoreAllMocks();
172 | 
173 |     const response = await getDependencyTypes({
174 |       dependencies: [{ name: 'dayjs', version: '1.11.7' }],
175 |     });
176 | 
177 |     expect(response.content).toHaveLength(1);
178 |     const item = response.content[0] as McpContentText;
179 |     expect(item.type).toBe('text');
180 | 
181 |     const parsed = JSON.parse(item.text) as any[];
182 | 
183 |     expect(parsed[0].name).toBe('dayjs');
184 |     expect(parsed[0].hasTypes).toBe(true);
185 |     expect(parsed[0].types).toBeDefined();
186 |     expect(parsed[0].types.length).toBeGreaterThan(0);
187 |     expect(parsed[0].version).toBe('1.11.7');
188 |   }, 15_000);
189 | 
190 |   it('should fetch external types from @types for express', async () => {
191 |     // Restore real fetch to allow network requests
192 |     vi.restoreAllMocks();
193 | 
194 |     const response = await getDependencyTypes({
195 |       dependencies: [{ name: 'express', version: '4.17.1' }],
196 |     });
197 | 
198 |     expect(response.content).toHaveLength(1);
199 |     const item = response.content[0] as McpContentText;
200 |     expect(item.type).toBe('text');
201 | 
202 |     const parsed = JSON.parse(item.text) as any[];
203 | 
204 |     expect(parsed[0].name).toBe('express');
205 |     expect(parsed[0].hasTypes).toBe(true);
206 |     expect(parsed[0].typesPackage).toBe('@types/express');
207 |     expect(parsed[0].types).toBeDefined();
208 |     expect(parsed[0].types.length).toBeGreaterThan(0);
209 |     expect(typeof parsed[0].version).toBe('string');
210 |   }, 15_000);
211 | });
212 | 
```

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

```typescript
  1 | import { NpmRegistry, type PackageInfo } from 'npm-registry-sdk';
  2 | import { z } from 'zod';
  3 | 
  4 | import { logger } from '../logger.ts';
  5 | import { type McpResponse, textContent } from '../types.ts';
  6 | 
  7 | /**
  8 |  * Zod schema for validating npm package search parameters
  9 |  */
 10 | export const SearchNpmPackagesToolSchema = z.object({
 11 |   searchTerm: z
 12 |     .string()
 13 |     .min(1, { message: 'Search term cannot be empty.' })
 14 |     .regex(/^\S+$/, { message: 'Search term cannot contain spaces.' })
 15 |     .describe(
 16 |       'The term to search for in npm packages. Should contain all relevant context. Should ideally be text that might appear in the package name, description, or keywords. Use plus signs (+) to combine related terms (e.g., "react+components" for React component libraries). For filtering by author, maintainer, or scope, use the qualifiers field instead of including them in the search term. Examples: "express" for Express.js, "ui+components" for UI component packages, "testing+jest" for Jest testing utilities.'
 17 |     ),
 18 |   qualifiers: z
 19 |     .object({
 20 |       author: z.string().optional().describe('Filter by package author name'),
 21 |       maintainer: z
 22 |         .string()
 23 |         .optional()
 24 |         .describe('Filter by package maintainer name'),
 25 |       scope: z
 26 |         .string()
 27 |         .optional()
 28 |         .describe('Filter by npm scope (e.g., "@vue" for Vue.js packages)'),
 29 |       keywords: z.string().optional().describe('Filter by package keywords'),
 30 |       not: z
 31 |         .string()
 32 |         .optional()
 33 |         .describe('Exclude packages matching this criteria (e.g., "insecure")'),
 34 |       is: z
 35 |         .string()
 36 |         .optional()
 37 |         .describe(
 38 |           'Include only packages matching this criteria (e.g., "unstable")'
 39 |         ),
 40 |       boostExact: z
 41 |         .string()
 42 |         .optional()
 43 |         .describe('Boost exact matches for this term in search results'),
 44 |     })
 45 |     .optional()
 46 |     .describe(
 47 |       'Optional qualifiers to filter the search results. For example, { not: "insecure" } will exclude insecure packages, { author: "sindresorhus" } will only show packages by that author, { scope: "@vue" } will only show Vue.js scoped packages.'
 48 |     ),
 49 | });
 50 | 
 51 | type SearchNpmPackagesToolSchemaType = z.infer<
 52 |   typeof SearchNpmPackagesToolSchema
 53 | >;
 54 | 
 55 | type PackageDetails = {
 56 |   /** The name of the package */
 57 |   name: string;
 58 |   /** A brief description of the package */
 59 |   description: string;
 60 |   /** A snippet from the package's README file */
 61 |   readmeSnippet: string;
 62 | };
 63 | 
 64 | class SearchNpmPackagesTool {
 65 |   private readonly registry: NpmRegistry;
 66 |   private readonly maxResults = 5;
 67 |   private readonly maxReadmeLength = 500;
 68 | 
 69 |   constructor() {
 70 |     this.registry = new NpmRegistry();
 71 |   }
 72 | 
 73 |   /**
 74 |    * Searches for npm packages based on the provided search term and qualifiers
 75 |    * @param {SearchNpmPackagesToolSchemaType} params - Search parameters including search term and optional qualifiers
 76 |    * @returns {Promise<McpResponse>} A response containing the search results or an error message
 77 |    */
 78 |   public async searchPackages({
 79 |     searchTerm,
 80 |     qualifiers,
 81 |   }: SearchNpmPackagesToolSchemaType): Promise<McpResponse> {
 82 |     const searchResults = await this.registry.search(searchTerm, {
 83 |       qualifiers,
 84 |     });
 85 | 
 86 |     if (!searchResults.total) {
 87 |       return {
 88 |         content: [textContent('No packages found.')],
 89 |       };
 90 |     }
 91 | 
 92 |     const packages = searchResults.objects
 93 |       .sort((a, b) => b.score.detail.popularity - a.score.detail.popularity)
 94 |       .slice(0, this.maxResults)
 95 |       .map((result) => result.package.name);
 96 | 
 97 |     const packagesInfos = await this.getPackagesDetails(packages);
 98 | 
 99 |     return {
100 |       content: [textContent(JSON.stringify(packagesInfos, null, 2))],
101 |     };
102 |   }
103 | 
104 |   /**
105 |    * Retrieves detailed information for multiple packages
106 |    * @param {string[]} packages - Array of package names to get details for
107 |    * @returns {Promise<PackageDetails[]>} Array of package details
108 |    * @private
109 |    */
110 |   private async getPackagesDetails(
111 |     packages: string[]
112 |   ): Promise<PackageDetails[]> {
113 |     const multiPackageInfo: PackageInfo[] = await Promise.all(
114 |       packages.map((pkg) => this.registry.getPackage(pkg))
115 |     );
116 | 
117 |     const packagesDetails: PackageDetails[] = [];
118 | 
119 |     for (const packageInfo of Object.values(multiPackageInfo)) {
120 |       packagesDetails.push({
121 |         name: packageInfo.name,
122 |         description: packageInfo.description || 'No description available.',
123 |         readmeSnippet: this.extractReadmeSnippet(packageInfo.readme),
124 |       });
125 |     }
126 | 
127 |     return packagesDetails;
128 |   }
129 | 
130 |   /**
131 |    * Extracts a snippet from a package's README file
132 |    * @param {string | undefined} readme - The full README content
133 |    * @returns {string} A truncated snippet of the README or a default message if README is not available
134 |    * @private
135 |    */
136 |   private extractReadmeSnippet(readme: string | undefined): string {
137 |     if (!readme) {
138 |       return 'README not available.';
139 |     }
140 | 
141 |     const snippet = readme.substring(0, this.maxReadmeLength);
142 |     return snippet.length === this.maxReadmeLength ? snippet + '...' : snippet;
143 |   }
144 | }
145 | 
146 | /**
147 |  * Search for npm packages by a search term and get their name, description, and a README snippet.
148 |  * This is an MCP (Model Context Protocol) tool that allows LLMs to discover and analyze npm packages.
149 |  *
150 |  * Returns up to 5 packages sorted by popularity, each containing:
151 |  * - Package name
152 |  * - Description
153 |  * - README snippet (first 500 characters)
154 |  *
155 |  * Use qualifiers to filter results by author, scope, keywords, or exclude unwanted packages.
156 |  *
157 |  * @param {SearchNpmPackagesToolSchemaType} params - Search parameters including search term and optional qualifiers
158 |  * @returns {Promise<McpResponse>} A response containing the search results formatted as JSON, or an error message
159 |  *
160 |  * @example
161 |  * // Basic search
162 |  * searchNpmPackages({ searchTerm: "react" })
163 |  *
164 |  * @example
165 |  * // Search with qualifiers
166 |  * searchNpmPackages({
167 |  *   searchTerm: "ui+components",
168 |  *   qualifiers: { scope: "@mui", not: "deprecated" }
169 |  * })
170 |  */
171 | export default async function searchNpmPackages(
172 |   params: SearchNpmPackagesToolSchemaType
173 | ): Promise<McpResponse> {
174 |   try {
175 |     const tool = new SearchNpmPackagesTool();
176 |     const response = await tool.searchPackages(params);
177 |     return response;
178 |   } catch (error) {
179 |     const errorMessage = `Failed to search npm packages for "${params.searchTerm}". Error: ${error instanceof Error ? error.message : String(error)}`;
180 |     logger.error(errorMessage);
181 |     return {
182 |       content: [textContent(errorMessage)],
183 |       isError: true,
184 |     };
185 |   }
186 | }
187 | 
```

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

```typescript
  1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
  2 | import * as fs from 'fs';
  3 | import {
  4 |   isRunningInDocker,
  5 |   isDockerRunning,
  6 |   preprocessDependencies,
  7 | } from '../src/utils.ts';
  8 | import { containerExists, isContainerRunning } from './utils.ts';
  9 | import * as childProcess from 'node:child_process';
 10 | 
 11 | vi.mock('fs');
 12 | vi.mock('node:child_process');
 13 | 
 14 | describe('utils', () => {
 15 |   beforeEach(() => {
 16 |     vi.resetAllMocks();
 17 |   });
 18 | 
 19 |   afterEach(() => {
 20 |     vi.clearAllMocks();
 21 |   });
 22 | 
 23 |   describe('isRunningInDocker', () => {
 24 |     it('should return true when /.dockerenv exists', () => {
 25 |       vi.spyOn(fs, 'existsSync').mockImplementation((path) => {
 26 |         return path === '/.dockerenv';
 27 |       });
 28 | 
 29 |       expect(isRunningInDocker()).toBe(true);
 30 |       expect(fs.existsSync).toHaveBeenCalledWith('/.dockerenv');
 31 |     });
 32 | 
 33 |     it('should return true when /proc/1/cgroup exists and contains docker', () => {
 34 |       vi.spyOn(fs, 'existsSync').mockImplementation((path) => {
 35 |         return path === '/proc/1/cgroup';
 36 |       });
 37 | 
 38 |       vi.spyOn(fs, 'readFileSync').mockReturnValue(
 39 |         Buffer.from('12:memory:/docker/somecontainerid')
 40 |       );
 41 | 
 42 |       expect(isRunningInDocker()).toBe(true);
 43 |       expect(fs.existsSync).toHaveBeenCalledWith('/.dockerenv');
 44 |       expect(fs.existsSync).toHaveBeenCalledWith('/proc/1/cgroup');
 45 |       expect(fs.readFileSync).toHaveBeenCalledWith('/proc/1/cgroup', 'utf8');
 46 |     });
 47 | 
 48 |     it('should return true when /proc/1/cgroup exists and contains kubepods', () => {
 49 |       vi.spyOn(fs, 'existsSync').mockImplementation((path) => {
 50 |         return path === '/proc/1/cgroup';
 51 |       });
 52 | 
 53 |       vi.spyOn(fs, 'readFileSync').mockReturnValue(
 54 |         Buffer.from('12:memory:/kubepods/somecontainerid')
 55 |       );
 56 | 
 57 |       expect(isRunningInDocker()).toBe(true);
 58 |     });
 59 | 
 60 |     it('should return true when docker environment variables are set', () => {
 61 |       vi.spyOn(fs, 'existsSync').mockReturnValue(false);
 62 | 
 63 |       const originalEnv = process.env;
 64 |       process.env = { ...originalEnv, DOCKER_CONTAINER: 'true' };
 65 | 
 66 |       expect(isRunningInDocker()).toBe(true);
 67 | 
 68 |       process.env = originalEnv;
 69 |     });
 70 | 
 71 |     it('should handle file system errors gracefully', () => {
 72 |       vi.spyOn(fs, 'existsSync').mockImplementation((path) => {
 73 |         return path === '/proc/1/cgroup';
 74 |       });
 75 | 
 76 |       vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
 77 |         throw new Error('Permission denied');
 78 |       });
 79 | 
 80 |       expect(isRunningInDocker()).toBe(false);
 81 |     });
 82 | 
 83 |     it('should return false when no docker indicators are present', () => {
 84 |       vi.spyOn(fs, 'existsSync').mockReturnValue(false);
 85 | 
 86 |       const originalEnv = process.env;
 87 |       process.env = { ...originalEnv };
 88 |       delete process.env.DOCKER_CONTAINER;
 89 |       delete process.env.DOCKER_ENV;
 90 | 
 91 |       expect(isRunningInDocker()).toBe(false);
 92 | 
 93 |       process.env = originalEnv;
 94 |     });
 95 |   });
 96 | 
 97 |   describe('isDockerRunning', () => {
 98 |     it('should return true when docker info command succeeds', () => {
 99 |       vi.spyOn(childProcess, 'execFileSync').mockImplementation(() =>
100 |         Buffer.from('')
101 |       );
102 | 
103 |       expect(isDockerRunning()).toBe(true);
104 |       expect(childProcess.execFileSync).toHaveBeenCalledWith('docker', [
105 |         'info',
106 |       ]);
107 |     });
108 | 
109 |     it('should return false when docker info command fails', () => {
110 |       vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => {
111 |         throw new Error('docker daemon not running');
112 |       });
113 | 
114 |       expect(isDockerRunning()).toBe(false);
115 |       expect(childProcess.execFileSync).toHaveBeenCalledWith('docker', [
116 |         'info',
117 |       ]);
118 |     });
119 |   });
120 | 
121 |   describe('preprocessDependencies', () => {
122 |     it('should convert dependency array to record format', () => {
123 |       const dependencies = [
124 |         { name: 'lodash', version: '4.17.21' },
125 |         { name: 'express', version: '4.18.2' },
126 |       ];
127 | 
128 |       const result = preprocessDependencies({ dependencies });
129 | 
130 |       expect(result).toEqual({
131 |         lodash: '4.17.21',
132 |         express: '4.18.2',
133 |       });
134 |     });
135 | 
136 |     it('should add chartjs-node-canvas for chartjs image', () => {
137 |       const dependencies = [{ name: 'lodash', version: '4.17.21' }];
138 | 
139 |       const result = preprocessDependencies({
140 |         dependencies,
141 |         image: 'alfonsograziano/node-chartjs-canvas:latest',
142 |       });
143 | 
144 |       expect(result).toEqual({
145 |         lodash: '4.17.21',
146 |         'chartjs-node-canvas': '4.0.0',
147 |         '@mermaid-js/mermaid-cli': '^11.4.2',
148 |       });
149 |     });
150 | 
151 |     it('should not add chartjs-node-canvas for non-chartjs images', () => {
152 |       const dependencies = [{ name: 'lodash', version: '4.17.21' }];
153 | 
154 |       const result = preprocessDependencies({
155 |         dependencies,
156 |         image: 'node:lts-slim',
157 |       });
158 | 
159 |       expect(result).toEqual({
160 |         lodash: '4.17.21',
161 |       });
162 |     });
163 |   });
164 | });
165 | 
166 | describe('containerExists', () => {
167 |   it('should return true for a valid container ID', () => {
168 |     vi.spyOn(childProcess, 'execFileSync').mockImplementation(() =>
169 |       Buffer.from('')
170 |     );
171 |     expect(containerExists('js-sbx-valid')).toBe(true);
172 |     expect(childProcess.execFileSync).toHaveBeenCalledWith('docker', [
173 |       'inspect',
174 |       'js-sbx-valid',
175 |     ]);
176 |   });
177 | 
178 |   it('should return false for a non-existent container ID', () => {
179 |     vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => {
180 |       throw new Error('No such container');
181 |     });
182 |     expect(containerExists('not-a-real-container')).toBe(false);
183 |   });
184 | 
185 |   it('should return false for a malicious container ID', () => {
186 |     vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => {
187 |       throw new Error('Invalid container ID');
188 |     });
189 |     expect(containerExists('bad;id$(rm -rf /)')).toBe(false);
190 |   });
191 | });
192 | 
193 | describe('isContainerRunning', () => {
194 |   it('should return true if docker inspect returns "true"', () => {
195 |     vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => 'true');
196 |     expect(isContainerRunning('js-sbx-valid')).toBe(true);
197 |     expect(childProcess.execFileSync).toHaveBeenCalledWith(
198 |       'docker',
199 |       ['inspect', '-f', '{{.State.Running}}', 'js-sbx-valid'],
200 |       { encoding: 'utf8' }
201 |     );
202 |   });
203 | 
204 |   it('should return false if docker inspect returns "false"', () => {
205 |     vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => 'false');
206 |     expect(isContainerRunning('js-sbx-valid')).toBe(false);
207 |   });
208 | 
209 |   it('should return false for a non-existent container ID', () => {
210 |     vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => {
211 |       throw new Error('No such container');
212 |     });
213 |     expect(isContainerRunning('not-a-real-container')).toBe(false);
214 |   });
215 | 
216 |   it('should return false for a malicious container ID', () => {
217 |     vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => {
218 |       throw new Error('Invalid container ID');
219 |     });
220 |     expect(isContainerRunning('bad;id$(rm -rf /)')).toBe(false);
221 |   });
222 | });
223 | 
```

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

```typescript
  1 | import React, { useState } from 'react';
  2 | import { Link, useLocation } from 'react-router-dom';
  3 | import { Menu, X, Terminal, Brain, Bot, GitBranch } from 'lucide-react';
  4 | 
  5 | const Header: React.FC = () => {
  6 |   const [isMenuOpen, setIsMenuOpen] = useState(false);
  7 |   const location = useLocation();
  8 | 
  9 |   const toggleMenu = () => {
 10 |     setIsMenuOpen(!isMenuOpen);
 11 |   };
 12 | 
 13 |   const closeMenu = () => {
 14 |     setIsMenuOpen(false);
 15 |   };
 16 | 
 17 |   const isActive = (path: string) => {
 18 |     return location.pathname === path;
 19 |   };
 20 | 
 21 |   return (
 22 |     <header className="bg-white shadow-sm sticky top-0 z-50">
 23 |       <div className="max-w-6xl mx-auto px-6">
 24 |         <div className="flex items-center justify-between h-16">
 25 |           {/* Logo */}
 26 |           <Link
 27 |             to="/"
 28 |             className="flex items-center gap-2 text-xl font-bold text-gray-900"
 29 |           >
 30 |             <div className="w-8 h-8 bg-gradient-to-r from-green-500 to-blue-600 rounded-lg flex items-center justify-center">
 31 |               <Brain size={20} className="text-white" />
 32 |             </div>
 33 |             <span>JSDevAI</span>
 34 |           </Link>
 35 | 
 36 |           {/* Desktop Navigation */}
 37 |           <nav className="hidden md:flex items-center gap-8">
 38 |             <Link
 39 |               to="/"
 40 |               className={`text-sm font-medium transition-colors ${
 41 |                 isActive('/')
 42 |                   ? 'text-green-600 border-b-2 border-green-600'
 43 |                   : 'text-gray-600 hover:text-green-600'
 44 |               }`}
 45 |             >
 46 |               Home
 47 |             </Link>
 48 |             <Link
 49 |               to="/mcp"
 50 |               className={`text-sm font-medium transition-colors ${
 51 |                 isActive('/mcp')
 52 |                   ? 'text-green-600 border-b-2 border-green-600'
 53 |                   : 'text-gray-600 hover:text-green-600'
 54 |               }`}
 55 |             >
 56 |               <div className="flex items-center gap-2">
 57 |                 <Terminal size={16} />
 58 |                 MCP Sandbox
 59 |               </div>
 60 |             </Link>
 61 |             <Link
 62 |               to="/tiny-agent"
 63 |               className={`text-sm font-medium transition-colors ${
 64 |                 isActive('/tiny-agent')
 65 |                   ? 'text-green-600 border-b-2 border-green-600'
 66 |                   : 'text-gray-600 hover:text-green-600'
 67 |               }`}
 68 |             >
 69 |               <div className="flex items-center gap-2">
 70 |                 <Bot size={16} />
 71 |                 Tiny Agent
 72 |               </div>
 73 |             </Link>
 74 |             <Link
 75 |               to="/graph-gpt"
 76 |               className={`text-sm font-medium transition-colors ${
 77 |                 isActive('/graph-gpt')
 78 |                   ? 'text-green-600 border-b-2 border-green-600'
 79 |                   : 'text-gray-600 hover:text-green-600'
 80 |               }`}
 81 |             >
 82 |               <div className="flex items-center gap-2">
 83 |                 <GitBranch size={16} />
 84 |                 GraphGPT
 85 |               </div>
 86 |             </Link>
 87 |             <a
 88 |               href="https://github.com/alfonsograziano/node-code-sandbox-mcp"
 89 |               target="_blank"
 90 |               rel="noopener noreferrer"
 91 |               className="text-sm font-medium text-gray-600 hover:text-green-600 transition-colors"
 92 |             >
 93 |               GitHub
 94 |             </a>
 95 |           </nav>
 96 | 
 97 |           {/* CTA Button */}
 98 |           <div className="hidden md:block">
 99 |             <Link
100 |               to="/mcp"
101 |               className="inline-flex items-center gap-2 px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 transition-colors"
102 |             >
103 |               <Terminal size={16} />
104 |               Try Sandbox
105 |             </Link>
106 |           </div>
107 | 
108 |           {/* Mobile Menu Button */}
109 |           <button
110 |             onClick={toggleMenu}
111 |             className="md:hidden p-2 text-gray-600 hover:text-green-600 transition-colors"
112 |             aria-label="Toggle menu"
113 |           >
114 |             {isMenuOpen ? <X size={24} /> : <Menu size={24} />}
115 |           </button>
116 |         </div>
117 | 
118 |         {/* Mobile Navigation */}
119 |         {isMenuOpen && (
120 |           <div className="md:hidden border-t border-gray-200 py-4">
121 |             <nav className="flex flex-col space-y-4">
122 |               <Link
123 |                 to="/"
124 |                 onClick={closeMenu}
125 |                 className={`text-base font-medium transition-colors ${
126 |                   isActive('/')
127 |                     ? 'text-green-600'
128 |                     : 'text-gray-600 hover:text-green-600'
129 |                 }`}
130 |               >
131 |                 Home
132 |               </Link>
133 |               <Link
134 |                 to="/mcp"
135 |                 onClick={closeMenu}
136 |                 className={`text-base font-medium transition-colors ${
137 |                   isActive('/mcp')
138 |                     ? 'text-green-600'
139 |                     : 'text-gray-600 hover:text-green-600'
140 |                 }`}
141 |               >
142 |                 <div className="flex items-center gap-2">
143 |                   <Terminal size={16} />
144 |                   MCP Sandbox
145 |                 </div>
146 |               </Link>
147 |               <Link
148 |                 to="/tiny-agent"
149 |                 onClick={closeMenu}
150 |                 className={`text-base font-medium transition-colors ${
151 |                   isActive('/tiny-agent')
152 |                     ? 'text-green-600'
153 |                     : 'text-gray-600 hover:text-green-600'
154 |                 }`}
155 |               >
156 |                 <div className="flex items-center gap-2">
157 |                   <Bot size={16} />
158 |                   Tiny Agent
159 |                 </div>
160 |               </Link>
161 |               <Link
162 |                 to="/graph-gpt"
163 |                 onClick={closeMenu}
164 |                 className={`text-base font-medium transition-colors ${
165 |                   isActive('/graph-gpt')
166 |                     ? 'text-green-600'
167 |                     : 'text-gray-600 hover:text-green-600'
168 |                 }`}
169 |               >
170 |                 <div className="flex items-center gap-2">
171 |                   <GitBranch size={16} />
172 |                   GraphGPT
173 |                 </div>
174 |               </Link>
175 |               <a
176 |                 href="https://github.com/alfonsograziano/node-code-sandbox-mcp"
177 |                 target="_blank"
178 |                 rel="noopener noreferrer"
179 |                 onClick={closeMenu}
180 |                 className="text-base font-medium text-gray-600 hover:text-green-600 transition-colors"
181 |               >
182 |                 GitHub
183 |               </a>
184 | 
185 |               <div className="pt-2">
186 |                 <Link
187 |                   to="/mcp"
188 |                   onClick={closeMenu}
189 |                   className="inline-flex items-center gap-2 px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 transition-colors w-full justify-center"
190 |                 >
191 |                   <Terminal size={16} />
192 |                   Try Sandbox
193 |                 </Link>
194 |               </div>
195 |             </nav>
196 |           </div>
197 |         )}
198 |       </div>
199 |     </header>
200 |   );
201 | };
202 | 
203 | export default Header;
204 | 
```

--------------------------------------------------------------------------------
/USE_CASE.md:
--------------------------------------------------------------------------------

```markdown
  1 | # 📆 Use Cases Appendix
  2 | 
  3 | This document contains practical use cases to unlock the full power of the Node.js Sandbox MCP server. You can dynamically install any npm packages during execution, save files, and run completely isolated experiments in fresh Docker containers.
  4 | 
  5 | All the listed use cases have been tested.
  6 | If you find a use case you'd like to support but that doesn't currently work, please open an issue.
  7 | Or, if you want to add your own cool use case, feel free to open a Pull Request!
  8 | 
  9 | ---
 10 | 
 11 | ### Generate a QR Code
 12 | 
 13 | Create and run a JS script that generates a QR code for the URL `https://nodejs.org/en`, and save it as `qrcode.png`.
 14 | 
 15 | **Tip:** Use the `qrcode` package.
 16 | 
 17 | ---
 18 | 
 19 | ### Test Regular Expressions
 20 | 
 21 | 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.
 22 | 
 23 | Requirements:
 24 | 
 25 | - The regular expression must handle deep nesting (e.g., up - to 3-4 levels).
 26 | - Write at least 10 unit tests covering correct and - incorrect cases.
 27 | - Use assert or manually throw errors if the validation fails.
 28 | - Add a short comment explaining the structure of the regex.
 29 | 
 30 | ---
 31 | 
 32 | ### Create CSV files with random data
 33 | 
 34 | Create and execute a js script which generates 200 items in a csv. The CSV has full name, random number and random (but valid) email. Write it in a file called "fake_data.csv"
 35 | 
 36 | ---
 37 | 
 38 | ### Scrape a Webpage Title
 39 | 
 40 | Create and run a JS script that fetches `https://example.com`, saves the html file in "example.html", extracts the `<title>` tag, and shows it in the console.
 41 | 
 42 | **Tip:** Use `cheerio`.
 43 | 
 44 | ---
 45 | 
 46 | ### Create a PDF Report
 47 | 
 48 | Create a JavaScript script with Node.js that generates a PDF file containing a fun "Getting Started with JavaScript" tutorial for a 10-year-old kid.
 49 | 
 50 | The tutorial should be simple, playful, and colorful, explaining basic concepts like console.log(), variables, and how to write your first small program.
 51 | Save the PDF as getting-started-javascript.pdf with fs
 52 | 
 53 | Tip: Use `pdf-lib` or `pdfkit` for creating the PDF.
 54 | 
 55 | ---
 56 | 
 57 | ### Fetch an API and Save to JSON
 58 | 
 59 | Create and run a JS script that fetches data from the GitHub Node.js repo (`https://api.github.com/repos/nodejs/node`) and saves part of the response to `nodejs_info.json`.
 60 | 
 61 | ---
 62 | 
 63 | ### Markdown to HTML Converter
 64 | 
 65 | Write a JavaScript script that takes a Markdown string, converts it into HTML, and saves the result into a file named content_converted.html.
 66 | 
 67 | Use the following example Markdown string:
 68 | 
 69 | ```markdown
 70 | # Welcome to My Page
 71 | 
 72 | This is a simple page created from **Markdown**!
 73 | 
 74 | - Learn JavaScript
 75 | - Learn Markdown
 76 | - Build Cool Stuff 🚀
 77 | ```
 78 | 
 79 | Tip: Use a library like `marked` to perform the conversion.
 80 | 
 81 | ---
 82 | 
 83 | ### Generate Random Data
 84 | 
 85 | 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".
 86 | 
 87 | **Tip:** Use `@faker-js/faker`.
 88 | 
 89 | ---
 90 | 
 91 | ### Evaluate a complex math expression
 92 | 
 93 | Create a JS script that evaluates this expression `((5 + 8) * (15 / 3) - (9 - (4 * 6)) + (10 / (2 + 6))) ^ 2 + sqrt(64) - factorial(6) + (24 / (5 + 7 * (3 ^ 2))) + log(1000) * sin(30 * pi / 180) - cos(60 * pi / 180) + tan(45 * pi / 180) + (4 ^ 3 - 2 ^ (5 - 2)) * (sqrt(81) / 9)`. Tip: use math.js
 94 | 
 95 | ---
 96 | 
 97 | ### Take a Screenshot with Playwright
 98 | 
 99 | 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`.
100 | 
101 | **Tip:** Use the official Playwright Docker image (mcr.microsoft.com/playwright) and install the playwright npm package dynamically.
102 | 
103 | ---
104 | 
105 | ### Generate a chart
106 | 
107 | Write a JavaScript script that generates a bar chart using chartjs-node-canvas.
108 | The chart should show Monthly Revenue Growth for the first 6 months of the year.
109 | Use the following data:
110 | 
111 | -January: $12,000
112 | -February: $15,500
113 | -March: $14,200
114 | -April: $18,300
115 | -May: $21,000
116 | -June: $24,500
117 | 
118 | Add the following details:
119 | 
120 | -Title: "Monthly Revenue Growth (2025)"
121 | -X-axis label: "Month"
122 | -Y-axis label: "Revenue (USD)"
123 | -Save the resulting chart as chart.png.
124 | 
125 | ---
126 | 
127 | ### Summarize a Long Article
128 | 
129 | Fetch the content of https://en.wikipedia.org/wiki/Node.js, strip HTML tags, and send the plain text to the AI. Ask it to return a bullet-point summary of the most important sections in less than 300 words.
130 | 
131 | ---
132 | 
133 | ### Refactor and Optimize a JS Function
134 | 
135 | Here's an unoptimized JavaScript function:
136 | 
137 | ```javascript
138 | function getUniqueValues(arr) {
139 |   let result = [];
140 |   for (let i = 0; i < arr.length; i++) {
141 |     let exists = false;
142 |     for (let j = 0; j < result.length; j++) {
143 |       if (arr[i] === result[j]) {
144 |         exists = true;
145 |         break;
146 |       }
147 |     }
148 |     if (!exists) {
149 |       result.push(arr[i]);
150 |     }
151 |   }
152 |   return result;
153 | }
154 | ```
155 | 
156 | Please refactor and optimize this function for performance and readability.
157 | Then, write and run basic tests with the Node.js test runner to make sure it works (covering common and edge cases).
158 | As soon as all tests pass, return only the refactored function.
159 | 
160 | ---
161 | 
162 | Here’s a complete and clear prompt that includes the schema and instructions for the AI:
163 | 
164 | ---
165 | 
166 | ### Create a Mock Book API from a JSON Schema
167 | 
168 | Here is a JSON Schema describing a `Book` entity:
169 | 
170 | ```json
171 | {
172 |   "$schema": "https://json-schema.org/draft/2020-12/schema",
173 |   "title": "Book",
174 |   "type": "object",
175 |   "required": ["title", "author", "isbn"],
176 |   "properties": {
177 |     "title": {
178 |       "type": "string",
179 |       "minLength": 1
180 |     },
181 |     "author": {
182 |       "type": "string",
183 |       "minLength": 1
184 |     },
185 |     "isbn": {
186 |       "type": "string",
187 |       "pattern": "^(97(8|9))?\\d{9}(\\d|X)$"
188 |     },
189 |     "publishedYear": {
190 |       "type": "integer",
191 |       "minimum": 0,
192 |       "maximum": 2100
193 |     },
194 |     "genres": {
195 |       "type": "array",
196 |       "items": {
197 |         "type": "string"
198 |       }
199 |     },
200 |     "available": {
201 |       "type": "boolean",
202 |       "default": true
203 |     }
204 |   },
205 |   "additionalProperties": false
206 | }
207 | ```
208 | 
209 | Using this schema:
210 | 
211 | 1. Generate **mock data** for at least 5 different books.
212 | 2. Create a simple **Node.js REST API** (you can use Express or Fastify) that:
213 |    - Serves a `GET /books` endpoint on **port 5007**, which returns all mock books.
214 |    - Serves a `GET /books/:isbn` endpoint that returns a single book matching the provided ISBN (or a 404 if not found).
215 | 3. Run the server and print a message like:  
216 |    `"Mock Book API is running on http://localhost:5007"`
217 | 
218 | ---
219 | 
220 | ### Files manipulation on the fly
221 | 
222 | **Prerequisites**: Create in your mounted folder a file called "books.json" with this content:
223 | 
224 | ```json
225 | [
226 |   { "id": 1, "title": "The Silent Code", "author": "Jane Doe" },
227 |   { "id": 2, "title": "Refactoring Legacy", "author": "John Smith" },
228 |   { "id": 3, "title": "Async in Action", "author": "Jane Doe" },
229 |   { "id": 4, "title": "The Pragmatic Stack", "author": "Emily Ray" },
230 |   { "id": 5, "title": "Systems Unboxed", "author": "Mark Lee" }
231 | ]
232 | ```
233 | 
234 | Then run this prompt:
235 | 
236 | Run a JS script to read the file "books.json", filter all the books of the author "Jane Doe" and save the result in "books_filtered.json"
237 | 
238 | To read and write from files, you always need to use the "files" directory which is exposed on the host machine.
239 | 
```

--------------------------------------------------------------------------------
/test/unit/linterUtils.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
  2 | import { lintAndRefactorCode } from '../../src/linterUtils.ts';
  3 | import { ESLint } from 'eslint';
  4 | 
  5 | vi.mock('eslint', () => {
  6 |   const ESLint = vi.fn();
  7 |   ESLint.prototype.lintText = vi.fn();
  8 |   return { ESLint };
  9 | });
 10 | 
 11 | // Get a typed reference to the mocked class and its method
 12 | const mockedESLint = vi.mocked(ESLint);
 13 | const mockedLintText = vi.mocked(ESLint.prototype.lintText);
 14 | 
 15 | describe('lintAndRefactorCode', () => {
 16 |   beforeEach(() => {
 17 |     // Clear mock history before each test
 18 |     mockedESLint.mockClear();
 19 |     mockedLintText.mockClear();
 20 |   });
 21 | 
 22 |   it('should return original code and no errors for clean code', async () => {
 23 |     const code = `const x = 1;`;
 24 |     mockedLintText.mockResolvedValue([
 25 |       {
 26 |         messages: [],
 27 |         output: undefined, // No fixes, so output is undefined
 28 |         errorCount: 0,
 29 |         warningCount: 0,
 30 |         fixableErrorCount: 0,
 31 |         fixableWarningCount: 0,
 32 |         usedDeprecatedRules: [],
 33 |         filePath: '',
 34 |         suppressedMessages: [],
 35 |         fatalErrorCount: 0,
 36 |       },
 37 |     ]);
 38 | 
 39 |     const { fixedCode, errorReport } = await lintAndRefactorCode(code);
 40 | 
 41 |     expect(fixedCode).toBe(code);
 42 |     expect(errorReport).toBeNull();
 43 |     expect(mockedLintText).toHaveBeenCalledWith(code);
 44 |   });
 45 | 
 46 |   it('should return fixed code and no errors for auto-fixable issues', async () => {
 47 |     const originalCode = `var x=1`;
 48 |     const expectedFixedCode = `const x = 1;`;
 49 |     mockedLintText.mockResolvedValue([
 50 |       {
 51 |         messages: [], // Assuming no remaining errors after fix
 52 |         output: expectedFixedCode,
 53 |         errorCount: 0,
 54 |         warningCount: 0,
 55 |         fixableErrorCount: 0,
 56 |         fixableWarningCount: 0,
 57 |         usedDeprecatedRules: [],
 58 |         filePath: '',
 59 |         suppressedMessages: [],
 60 |         fatalErrorCount: 0,
 61 |       },
 62 |     ]);
 63 | 
 64 |     const { fixedCode, errorReport } = await lintAndRefactorCode(originalCode);
 65 | 
 66 |     expect(fixedCode).toBe(expectedFixedCode);
 67 |     expect(errorReport).toBeNull();
 68 |     expect(mockedLintText).toHaveBeenCalledWith(originalCode);
 69 |   });
 70 | 
 71 |   it('should return an error report for non-fixable errors', async () => {
 72 |     const codeWithErrors = `const x = y;`; // ReferenceError
 73 |     const errorMessages = [
 74 |       {
 75 |         severity: 2 as const,
 76 |         line: 1,
 77 |         column: 11,
 78 |         message: "'y' is not defined.",
 79 |         ruleId: 'no-undef',
 80 |       },
 81 |     ];
 82 |     mockedLintText.mockResolvedValue([
 83 |       {
 84 |         messages: errorMessages,
 85 |         output: undefined, // No fixes applied
 86 |         errorCount: 1,
 87 |         warningCount: 0,
 88 |         fixableErrorCount: 0,
 89 |         fixableWarningCount: 0,
 90 |         usedDeprecatedRules: [],
 91 |         filePath: '',
 92 |         suppressedMessages: [],
 93 |         fatalErrorCount: 0,
 94 |       },
 95 |     ]);
 96 | 
 97 |     const { fixedCode, errorReport } =
 98 |       await lintAndRefactorCode(codeWithErrors);
 99 | 
100 |     expect(fixedCode).toBe(codeWithErrors);
101 |     expect(errorReport).not.toBeNull();
102 |     expect(errorReport).toBe("L1:11: 'y' is not defined. (no-undef)");
103 |     expect(mockedLintText).toHaveBeenCalledWith(codeWithErrors);
104 |   });
105 | 
106 |   it('should return fixed code and an error report for mixed issues', async () => {
107 |     const originalCode = `var x=y`; // fixable `var` and `spacing`, unfixable `y`
108 |     const partiallyFixedCode = `const x = y;`;
109 |     const errorMessages = [
110 |       {
111 |         severity: 2 as const,
112 |         line: 1,
113 |         column: 9,
114 |         message: "'y' is not defined.",
115 |         ruleId: 'no-undef',
116 |       },
117 |     ];
118 |     mockedLintText.mockResolvedValue([
119 |       {
120 |         messages: errorMessages,
121 |         output: partiallyFixedCode,
122 |         errorCount: 1,
123 |         warningCount: 0,
124 |         fixableErrorCount: 0,
125 |         fixableWarningCount: 0,
126 |         usedDeprecatedRules: [],
127 |         filePath: '',
128 |         suppressedMessages: [],
129 |         fatalErrorCount: 0,
130 |       },
131 |     ]);
132 | 
133 |     const { fixedCode, errorReport } = await lintAndRefactorCode(originalCode);
134 | 
135 |     expect(fixedCode).toBe(partiallyFixedCode);
136 |     expect(errorReport).not.toBeNull();
137 |     expect(errorReport).toBe("L1:9: 'y' is not defined. (no-undef)");
138 |     expect(mockedLintText).toHaveBeenCalledWith(originalCode);
139 |   });
140 | 
141 |   it('should auto-fix `let` to `const` for non-reassigned variables', async () => {
142 |     const originalCode = `let x = 5; console.log(x);`;
143 |     const expectedFixedCode = `const x = 5; console.log(x);`;
144 |     mockedLintText.mockResolvedValue([
145 |       {
146 |         messages: [],
147 |         output: expectedFixedCode,
148 |         errorCount: 0,
149 |         warningCount: 0,
150 |         fixableErrorCount: 0,
151 |         fixableWarningCount: 0,
152 |         usedDeprecatedRules: [],
153 |         filePath: '',
154 |         suppressedMessages: [],
155 |         fatalErrorCount: 0,
156 |       },
157 |     ]);
158 | 
159 |     const { fixedCode, errorReport } = await lintAndRefactorCode(originalCode);
160 | 
161 |     expect(fixedCode).toBe(expectedFixedCode);
162 |     expect(errorReport).toBeNull();
163 |     expect(mockedLintText).toHaveBeenCalledWith(originalCode);
164 |   });
165 | 
166 |   it('should auto-fix to use object shorthand', async () => {
167 |     const originalCode = `const name = 'test'; const obj = { name: name };`;
168 |     const expectedFixedCode = `const name = 'test'; const obj = { name };`;
169 |     mockedLintText.mockResolvedValue([
170 |       {
171 |         messages: [],
172 |         output: expectedFixedCode,
173 |         errorCount: 0,
174 |         warningCount: 0,
175 |         fixableErrorCount: 0,
176 |         fixableWarningCount: 0,
177 |         usedDeprecatedRules: [],
178 |         filePath: '',
179 |         suppressedMessages: [],
180 |         fatalErrorCount: 0,
181 |       },
182 |     ]);
183 | 
184 |     const { fixedCode, errorReport } = await lintAndRefactorCode(originalCode);
185 | 
186 |     expect(fixedCode).toBe(expectedFixedCode);
187 |     expect(errorReport).toBeNull();
188 |     expect(mockedLintText).toHaveBeenCalledWith(originalCode);
189 |   });
190 | 
191 |   it('should auto-fix to use template literals', async () => {
192 |     const originalCode = `const name = 'world'; const greeting = 'Hello ' + name;`;
193 |     const expectedFixedCode =
194 |       "const name = 'world'; const greeting = `Hello ${name}`;";
195 |     mockedLintText.mockResolvedValue([
196 |       {
197 |         messages: [],
198 |         output: expectedFixedCode,
199 |         errorCount: 0,
200 |         warningCount: 0,
201 |         fixableErrorCount: 0,
202 |         fixableWarningCount: 0,
203 |         usedDeprecatedRules: [],
204 |         filePath: '',
205 |         suppressedMessages: [],
206 |         fatalErrorCount: 0,
207 |       },
208 |     ]);
209 | 
210 |     const { fixedCode, errorReport } = await lintAndRefactorCode(originalCode);
211 | 
212 |     expect(fixedCode).toBe(expectedFixedCode);
213 |     expect(errorReport).toBeNull();
214 |     expect(mockedLintText).toHaveBeenCalledWith(originalCode);
215 |   });
216 | 
217 |   it('should report an error for using == instead of ===', async () => {
218 |     const codeWithErrors = `if (x == 1) {}`;
219 |     const errorMessages = [
220 |       {
221 |         severity: 2 as const,
222 |         line: 1,
223 |         column: 5,
224 |         message: "Expected '===' and instead saw '=='.",
225 |         ruleId: 'eqeqeq',
226 |       },
227 |     ];
228 |     mockedLintText.mockResolvedValue([
229 |       {
230 |         messages: errorMessages,
231 |         output: codeWithErrors, // No fix applied
232 |         errorCount: 1,
233 |         warningCount: 0,
234 |         fixableErrorCount: 0,
235 |         fixableWarningCount: 0,
236 |         usedDeprecatedRules: [],
237 |         filePath: '',
238 |         suppressedMessages: [],
239 |         fatalErrorCount: 0,
240 |       },
241 |     ]);
242 | 
243 |     const { fixedCode, errorReport } =
244 |       await lintAndRefactorCode(codeWithErrors);
245 | 
246 |     expect(fixedCode).toBe(codeWithErrors);
247 |     expect(errorReport).not.toBeNull();
248 |     expect(errorReport).toBe(
249 |       "L1:5: Expected '===' and instead saw '=='. (eqeqeq)"
250 |     );
251 |     expect(mockedLintText).toHaveBeenCalledWith(codeWithErrors);
252 |   });
253 | });
254 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  2 | import * as tmp from 'tmp';
  3 | import { z } from 'zod';
  4 | import runJs, { argSchema } from '../src/tools/runJs.ts';
  5 | import initializeSandbox from '../src/tools/initialize.ts';
  6 | import stopSandbox from '../src/tools/stop.ts';
  7 | import type { McpContentText } from '../src/types.ts';
  8 | 
  9 | describe('argSchema', () => {
 10 |   it('should accept code and container_id and set defaults', () => {
 11 |     const parsed = z.object(argSchema).parse({
 12 |       code: "console.log('hi');",
 13 |       container_id: 'dummy',
 14 |     });
 15 |     expect(parsed.container_id).toBe('dummy');
 16 |     expect(parsed.dependencies).toEqual([]);
 17 |     expect(parsed.code).toBe("console.log('hi');");
 18 |   });
 19 | });
 20 | 
 21 | describe('runJs basic execution', () => {
 22 |   let containerId: string;
 23 |   let tmpDir: tmp.DirResult;
 24 | 
 25 |   beforeEach(async () => {
 26 |     tmpDir = tmp.dirSync({ unsafeCleanup: true });
 27 |     process.env.FILES_DIR = tmpDir.name;
 28 | 
 29 |     const result = await initializeSandbox({});
 30 |     if (result.content[0].type === 'text') {
 31 |       containerId = result.content[0].text;
 32 |     } else {
 33 |       throw new Error("Expected the first content item to be of type 'text'");
 34 |     }
 35 |   });
 36 | 
 37 |   afterEach(() => {
 38 |     tmpDir.removeCallback();
 39 |     delete process.env.FILES_DIR;
 40 | 
 41 |     if (containerId) {
 42 |       stopSandbox({ container_id: containerId });
 43 |     }
 44 |   });
 45 | 
 46 |   it('should run simple JS in container', async () => {
 47 |     const result = await runJs({
 48 |       container_id: containerId,
 49 |       code: `console.log("Hello from runJs")`,
 50 |     });
 51 | 
 52 |     expect(result).toBeDefined();
 53 |     expect(result.content.length).toBeGreaterThan(0);
 54 | 
 55 |     const output = result.content[0];
 56 |     expect(output.type).toBe('text');
 57 |     if (output.type === 'text') {
 58 |       expect(output.text).toContain('Hello from runJs');
 59 |     }
 60 |   });
 61 | 
 62 |   it('should generate telemetry', async () => {
 63 |     const result = await runJs({
 64 |       container_id: containerId,
 65 |       code: "console.log('Hello telemetry!');",
 66 |     });
 67 | 
 68 |     const telemetryItem = result.content.find(
 69 |       (c) => c.type === 'text' && c.text.startsWith('Telemetry:')
 70 |     );
 71 | 
 72 |     expect(telemetryItem).toBeDefined();
 73 |     if (telemetryItem?.type === 'text') {
 74 |       const telemetry = JSON.parse(
 75 |         telemetryItem.text.replace('Telemetry:\n', '')
 76 |       );
 77 | 
 78 |       expect(telemetry).toHaveProperty('installTimeMs');
 79 |       expect(typeof telemetry.installTimeMs).toBe('number');
 80 |       expect(telemetry).toHaveProperty('runTimeMs');
 81 |       expect(typeof telemetry.runTimeMs).toBe('number');
 82 |       expect(telemetry).toHaveProperty('installOutput');
 83 |       expect(typeof telemetry.installOutput).toBe('string');
 84 |     } else {
 85 |       throw new Error("Expected telemetry item to be of type 'text'");
 86 |     }
 87 |   });
 88 |   it('should write and retrieve a file', async () => {
 89 |     const result = await runJs({
 90 |       container_id: containerId,
 91 |       code: `
 92 |         import fs from 'fs/promises';
 93 |         await fs.writeFile('./files/hello test.txt', 'Hello world!');
 94 |         console.log('Saved hello test.txt');
 95 |       `,
 96 |     });
 97 | 
 98 |     // Assert stdout contains the save confirmation
 99 |     const stdoutEntry = result.content.find(
100 |       (c) => c.type === 'text' && c.text.includes('Saved hello test.txt')
101 |     );
102 |     expect(stdoutEntry).toBeDefined();
103 | 
104 |     // Assert the change list mentions the created file
105 |     const changeList = result.content.find(
106 |       (c) =>
107 |         c.type === 'text' &&
108 |         c.text.includes('List of changed files') &&
109 |         c.text.includes('- hello test.txt was created')
110 |     );
111 |     expect(changeList).toBeDefined();
112 | 
113 |     // Assert the resource entry has the correct text and URI
114 |     const resourceEntry = result.content.find(
115 |       (c) =>
116 |         c.type === 'resource' &&
117 |         'text' in c.resource &&
118 |         c.resource.text === 'hello test.txt'
119 |     );
120 |     expect(resourceEntry).toBeDefined();
121 |     if (resourceEntry?.type === 'resource') {
122 |       // The URI should include the filename (URL-encoded)
123 |       expect(resourceEntry.resource.uri).toContain('hello%20test.txt');
124 |     }
125 |   });
126 | 
127 |   it('should skip npm install if no dependencies are provided', async () => {
128 |     const result = await runJs({
129 |       container_id: containerId,
130 |       code: "console.log('No deps');",
131 |       dependencies: [],
132 |     });
133 | 
134 |     const telemetryItem = result.content.find(
135 |       (c) => c.type === 'text' && c.text.startsWith('Telemetry:')
136 |     );
137 | 
138 |     expect(telemetryItem).toBeDefined();
139 |     if (telemetryItem?.type === 'text') {
140 |       const telemetry = JSON.parse(
141 |         telemetryItem.text.replace('Telemetry:\n', '')
142 |       );
143 | 
144 |       expect(telemetry.installTimeMs).toBe(0);
145 |       expect(telemetry.installOutput).toBe(
146 |         'Skipped npm install (no dependencies)'
147 |       );
148 |     }
149 |   });
150 | 
151 |   it('should install lodash and use it', async () => {
152 |     const result = await runJs({
153 |       container_id: containerId,
154 |       code: `
155 |         import _ from 'lodash';
156 |         console.log(_.join(['Hello', 'lodash'], ' '));
157 |       `,
158 |       dependencies: [{ name: 'lodash', version: '^4.17.21' }],
159 |     });
160 | 
161 |     const stdout = result.content.find((c) => c.type === 'text');
162 |     expect(stdout).toBeDefined();
163 |     if (stdout?.type === 'text') {
164 |       expect(stdout.text).toContain('Hello lodash');
165 |     }
166 |   });
167 | 
168 |   it('should hang indefinitely until a timeout error gets triggered', async () => {
169 |     //Simulating a 10 seconds timeout
170 |     process.env.RUN_SCRIPT_TIMEOUT = '10000';
171 |     const result = await runJs({
172 |       container_id: containerId,
173 |       code: `
174 |         (async () => {
175 |           console.log("🕒 Hanging for 20 seconds…");
176 |           await new Promise((resolve) => setTimeout(resolve, 20_000));
177 |           console.log("✅ Done waiting 20 seconds, exiting now.");
178 |         })();
179 |       `,
180 |     });
181 | 
182 |     //Cleanup
183 |     delete process.env.RUN_SCRIPT_TIMEOUT;
184 | 
185 |     const execError = result.content.find(
186 |       (item) =>
187 |         item.type === 'text' && item.text.startsWith('Error during execution:')
188 |     );
189 |     expect(execError).toBeDefined();
190 |     expect((execError as McpContentText).text).toContain('ETIMEDOUT');
191 | 
192 |     const telemetryText = result.content.find(
193 |       (item) => item.type === 'text' && item.text.startsWith('Telemetry:')
194 |     );
195 |     expect(telemetryText).toBeDefined();
196 |   }, 20_000);
197 | 
198 |   it('should report execution error for runtime exceptions', async () => {
199 |     const result = await runJs({
200 |       container_id: containerId,
201 |       code: `throw new Error('boom');`,
202 |     });
203 | 
204 |     expect(result).toBeDefined();
205 |     expect(result.content).toBeDefined();
206 | 
207 |     const execError = result.content.find(
208 |       (item) =>
209 |         item.type === 'text' && item.text.startsWith('Error during execution:')
210 |     );
211 |     expect(execError).toBeDefined();
212 |     expect((execError as McpContentText).text).toContain('Error: boom');
213 | 
214 |     const telemetryText = result.content.find(
215 |       (item) => item.type === 'text' && item.text.startsWith('Telemetry:')
216 |     );
217 |     expect(telemetryText).toBeDefined();
218 |   });
219 | 
220 |   it('should auto-fix linting issues and run the corrected code', async () => {
221 |     const result = await runJs({
222 |       container_id: containerId,
223 |       // This code has fixable issues: `var` instead of `const`, and extra spacing.
224 |       code: `var msg = "hello auto-fixed world"  ; console.log(msg)`,
225 |     });
226 | 
227 |     // 1. Check that no linting report was returned, as it should be auto-fixed.
228 |     const lintReport = result.content.find(
229 |       (c) => c.type === 'text' && c.text.startsWith('Linting issues found')
230 |     );
231 |     expect(lintReport).toBeUndefined();
232 | 
233 |     // 2. Check that the execution was successful and the output is correct.
234 |     const execOutput = result.content.find(
235 |       (c) => c.type === 'text' && c.text.startsWith('Node.js process output:')
236 |     );
237 |     expect(execOutput).toBeDefined();
238 |     if (execOutput?.type === 'text') {
239 |       expect(execOutput.text).toContain('hello auto-fixed world');
240 |     }
241 | 
242 |     // 3. Check that there was no execution error.
243 |     const execError = result.content.find(
244 |       (c) => c.type === 'text' && c.text.startsWith('Error during execution:')
245 |     );
246 |     expect(execError).toBeUndefined();
247 |   });
248 | 
249 |   it('should report unfixable linting issues and the subsequent execution error', async () => {
250 |     const result = await runJs({
251 |       container_id: containerId,
252 |       // This code has an unfixable issue: using an undefined variable.
253 |       code: `console.log(someUndefinedVariable);`,
254 |     });
255 | 
256 |     expect(result).toBeDefined();
257 | 
258 |     // 1. Check that a linting report was returned.
259 |     const lintReport = result.content.find(
260 |       (c) => c.type === 'text' && c.text.startsWith('Linting issues found')
261 |     );
262 |     expect(lintReport).toBeDefined();
263 |     if (lintReport?.type === 'text') {
264 |       expect(lintReport.text).toContain(
265 |         "'someUndefinedVariable' is not defined."
266 |       );
267 |       expect(lintReport.text).toContain('(no-undef)');
268 |     }
269 | 
270 |     // 2. Check that the execution also failed and was reported.
271 |     const execError = result.content.find(
272 |       (c) => c.type === 'text' && c.text.startsWith('Error during execution:')
273 |     );
274 |     expect(execError).toBeDefined();
275 |     if (execError?.type === 'text') {
276 |       expect(execError.text).toContain(
277 |         'ReferenceError: someUndefinedVariable is not defined'
278 |       );
279 |     }
280 |   });
281 | }, 10_000);
282 | 
283 | describe('Command injection prevention', () => {
284 |   beforeEach(() => {
285 |     vi.doMock('node:child_process', () => ({
286 |       execFileSync: vi.fn(() => Buffer.from('')),
287 |       execFile: vi.fn(() => Buffer.from('')),
288 |     }));
289 |   });
290 | 
291 |   afterEach(() => {
292 |     vi.resetModules();
293 |     vi.resetAllMocks();
294 |     vi.restoreAllMocks();
295 |   });
296 | 
297 |   const dangerousIds = [
298 |     '$(touch /tmp/pwned)',
299 |     '`touch /tmp/pwned`',
300 |     'bad;id',
301 |     'js-sbx-123 && rm -rf /',
302 |     'js-sbx-123 | echo hacked',
303 |     'js-sbx-123 > /tmp/pwned',
304 |     'js-sbx-123 $(id)',
305 |     'js-sbx-123; echo pwned',
306 |     'js-sbx-123`echo pwned`',
307 |     'js-sbx-123/../../etc/passwd',
308 |     'js-sbx-123\nrm -rf /',
309 |     '',
310 |     ' ',
311 |     'js-sbx-123$',
312 |     'js-sbx-123#',
313 |   ];
314 | 
315 |   dangerousIds.forEach((payload) => {
316 |     it(`should reject dangerous container_id: "${payload}"`, async () => {
317 |       const { default: runJs } = await import('../src/tools/runJs.ts');
318 |       const childProcess = await import('node:child_process');
319 |       const result = await runJs({
320 |         container_id: payload,
321 |         code: 'console.log("test")',
322 |       });
323 |       expect(result).toEqual({
324 |         content: [
325 |           {
326 |             type: 'text',
327 |             text: 'Invalid container ID',
328 |           },
329 |         ],
330 |       });
331 |       const execFileSyncCall = vi.mocked(childProcess.execFileSync).mock.calls;
332 |       expect(execFileSyncCall.length).toBe(0);
333 |     });
334 |   });
335 | });
336 | 
```

--------------------------------------------------------------------------------
/website/src/pages/NodeMCPServer.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React, { useEffect, useState } from 'react';
  2 | import {
  3 |   X,
  4 |   Terminal,
  5 |   ShieldCheck,
  6 |   Cpu,
  7 |   Package,
  8 |   Code,
  9 |   Settings,
 10 |   Server,
 11 |   Star,
 12 | } from 'lucide-react';
 13 | 
 14 | import { UseCase, useCases } from '../useCases';
 15 | import GettingStarted from '../Components/GettingStarted';
 16 | import Footer from '../Components/Footer';
 17 | import Header from '../Components/Header';
 18 | 
 19 | interface Feature {
 20 |   title: string;
 21 |   description: string;
 22 |   icon: React.ElementType;
 23 | }
 24 | 
 25 | // Extract unique categories
 26 | const allCategories: string[] = [
 27 |   ...new Set(useCases.flatMap((u) => u.category)),
 28 | ].sort();
 29 | 
 30 | const features: Feature[] = [
 31 |   {
 32 |     title: 'Ephemeral Containers',
 33 |     description:
 34 |       'Run JavaScript in isolated Docker containers that clean up automatically after execution.',
 35 |     icon: Terminal,
 36 |   },
 37 |   {
 38 |     title: 'Secure Execution',
 39 |     description:
 40 |       'Sandboxed execution with CPU/memory limits and safe, controlled environments.',
 41 |     icon: ShieldCheck,
 42 |   },
 43 |   {
 44 |     title: 'Detached Mode',
 45 |     description:
 46 |       'Keep containers alive after execution to host servers or persistent processes.',
 47 |     icon: Cpu,
 48 |   },
 49 |   {
 50 |     title: 'Optimized Docker Images',
 51 |     description:
 52 |       'Use prebuilt Docker images with popular dependencies already installed—perfect for advanced use cases like Playwright, Chart.js, and more.',
 53 |     icon: Package,
 54 |   },
 55 |   {
 56 |     title: 'NPM Dependencies',
 57 |     description:
 58 |       'Install dependencies on-the-fly per job using package name and version.',
 59 |     icon: Settings,
 60 |   },
 61 |   {
 62 |     title: 'Multi-Tool Support',
 63 |     description:
 64 |       'Use ephemeral runs or long-lived sandbox sessions, depending on your use case.',
 65 |     icon: Server,
 66 |   },
 67 | ];
 68 | 
 69 | const App: React.FC = () => {
 70 |   const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
 71 |   const [modalOpen, setModalOpen] = useState<boolean>(false);
 72 |   const [selectedCase, setSelectedCase] = useState<UseCase | null>(null);
 73 | 
 74 |   const toggleCategory = (cat: string) => {
 75 |     setSelectedCategories((prev) =>
 76 |       prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat]
 77 |     );
 78 |   };
 79 | 
 80 |   const openModal = (useCase: UseCase) => {
 81 |     setSelectedCase(useCase);
 82 |     setModalOpen(true);
 83 |   };
 84 | 
 85 |   const closeModal = () => {
 86 |     setModalOpen(false);
 87 |     setSelectedCase(null);
 88 |   };
 89 | 
 90 |   useEffect(() => {
 91 |     if (modalOpen) {
 92 |       document.body.classList.add('overflow-hidden');
 93 |     } else {
 94 |       document.body.classList.remove('overflow-hidden');
 95 |     }
 96 | 
 97 |     return () => {
 98 |       document.body.classList.remove('overflow-hidden');
 99 |     };
100 |   }, [modalOpen]);
101 | 
102 |   const filteredCases =
103 |     selectedCategories.length === 0
104 |       ? useCases
105 |       : useCases.filter((u) =>
106 |           u.category.some((c) => selectedCategories.includes(c))
107 |         );
108 | 
109 |   const gridBg: React.CSSProperties = {
110 |     backgroundImage:
111 |       'linear-gradient(to right, rgba(0,0,0,0.03) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,0,0,0.03) 1px, transparent 1px)',
112 |     backgroundSize: '20px 20px',
113 |   };
114 | 
115 |   return (
116 |     <div style={gridBg} className="min-h-screen bg-gray-50 text-gray-900">
117 |       {/* Header */}
118 |       <Header />
119 | 
120 |       {/* Hero Section */}
121 |       <header className="max-w-6xl mx-auto text-center py-16">
122 |         <h1 className="text-5xl font-extrabold mb-4">
123 |           🐢🚀 Node.js Sandbox MCP Server
124 |         </h1>
125 |         {/* Compact MCP Info Banner */}
126 |         <div className="max-w-3xl mx-auto mb-8 px-4 py-3 bg-green-50 border border-green-200 text-sm rounded-lg flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 text-center sm:text-left">
127 |           <div className="text-gray-800">
128 |             🧠 MCP is a protocol that lets AI models access tools and data
129 |             through a standardized interface.
130 |           </div>
131 |           <a
132 |             href="https://modelcontextprotocol.io/"
133 |             target="_blank"
134 |             rel="noopener noreferrer"
135 |             className="text-green-700 font-medium hover:underline"
136 |           >
137 |             Learn more →
138 |           </a>
139 |         </div>
140 | 
141 |         <p className="text-lg text-gray-700 mb-8">
142 |           Run JavaScript in secure, disposable Docker containers via the Model
143 |           Context Protocol (MCP). Automatic dependency installation included.
144 |         </p>
145 | 
146 |         <div className="flex flex-col sm:flex-row justify-center gap-4 px-4 sm:px-0">
147 |           <a
148 |             href="#use-cases"
149 |             className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition text-center"
150 |           >
151 |             Explore Use Cases
152 |           </a>
153 |           <a
154 |             href="https://github.com/alfonsograziano/node-code-sandbox-mcp"
155 |             target="_blank"
156 |             rel="noopener noreferrer"
157 |             className="flex items-center justify-center gap-2 px-6 py-3 bg-white border border-gray-300 text-gray-800 rounded-lg hover:bg-gray-100 transition"
158 |           >
159 |             <Star size={16} className="text-yellow-500" fill="yellow" /> Star on
160 |             GitHub
161 |           </a>
162 |           <div className="relative">
163 |             <a
164 |               href="https://hub.docker.com/r/mcp/node-code-sandbox"
165 |               target="_blank"
166 |               rel="noopener noreferrer"
167 |               className="flex items-center justify-center gap-2 px-6 py-3 bg-white border border-gray-300 text-gray-800 rounded-lg hover:bg-gray-100 transition"
168 |             >
169 |               <Package size={16} className="text-blue-500" /> Docker Hub
170 |             </a>
171 |             <div className="absolute -top-2 -right-2 bg-green-500 text-white text-xs font-bold px-2 py-1 rounded-full shadow-lg">
172 |               10K+
173 |             </div>
174 |           </div>
175 |         </div>
176 |       </header>
177 | 
178 |       {/* Features */}
179 |       <section id="features" className="max-w-6xl mx-auto">
180 |         <h2 className="text-3xl font-bold text-center mb-8">Core Features</h2>
181 |         <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
182 |           {features.map(({ title, description, icon: Icon }, i) => (
183 |             <div
184 |               key={i}
185 |               className="bg-white bg-opacity-80 p-6 rounded-xl shadow-md"
186 |             >
187 |               <Icon width={32} height={32} className="text-green-600 mb-4" />
188 |               <h3 className="text-xl font-semibold mb-2">{title}</h3>
189 |               <p className="text-gray-700">{description}</p>
190 |             </div>
191 |           ))}
192 |         </div>
193 |       </section>
194 | 
195 |       {/* Getting Started Section */}
196 |       <GettingStarted />
197 | 
198 |       {/* Contribute Section */}
199 |       <section id="contribute" className="max-w-4xl mx-auto my-10">
200 |         <div className="bg-green-50 border border-green-200 p-6 rounded-xl shadow-md">
201 |           <h2 className="text-2xl font-bold mb-2">
202 |             🤝 Want to contribute to Node.js + AI?
203 |           </h2>
204 |           <p className="text-gray-800 mb-4">
205 |             Brilliant! If you want to help, I’m opening a few issues on my new
206 |             MCP server project. Since the project is still in its early stages,
207 |             it’s the <strong>perfect time to jump in</strong> and become a core
208 |             collaborator. 🚀
209 |           </p>
210 |           <p className="text-gray-800 mb-4">
211 |             If you're excited about bringing <strong>Node.js</strong> into the
212 |             world of <strong>AI applications</strong>, leave a comment or DM me,
213 |             I'd love to have a chat!
214 |           </p>
215 |           <a
216 |             href="https://github.com/alfonsograziano/node-code-sandbox-mcp/issues"
217 |             target="_blank"
218 |             rel="noopener noreferrer"
219 |             className="inline-block px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition"
220 |           >
221 |             View Open Issues →
222 |           </a>
223 |         </div>
224 |       </section>
225 | 
226 |       {/* Use Cases Grid */}
227 |       <section id="use-cases" className="max-w-6xl mx-auto py-8">
228 |         <h2 className="text-3xl font-bold text-center mb-8">Use Cases</h2>
229 |         <p className="text-center text-gray-600 max-w-3xl mx-auto mb-8">
230 |           Discover powerful, real-world examples you can run in seconds. From
231 |           file generation to web scraping and AI workflows—this sandbox unlocks
232 |           serious JavaScript potential in isolated containers.
233 |         </p>
234 | 
235 |         <section id="filter" className="max-w-6xl mx-auto py-8">
236 |           <h3 className="text-2xl font-semibold mb-4">Filter by Category</h3>
237 |           <div className="flex flex-wrap gap-2">
238 |             {allCategories.map((cat) => (
239 |               <button
240 |                 key={cat}
241 |                 onClick={() => toggleCategory(cat)}
242 |                 className={`flex items-center gap-1 px-3 py-1 rounded-full text-sm font-medium transition-all ${
243 |                   selectedCategories.includes(cat)
244 |                     ? 'bg-green-600 text-white'
245 |                     : 'bg-white border border-gray-300 text-gray-800 hover:bg-gray-100'
246 |                 }`}
247 |               >
248 |                 <span>{cat}</span>
249 |               </button>
250 |             ))}
251 |           </div>
252 |         </section>
253 | 
254 |         <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
255 |           {filteredCases.map((u, index) => (
256 |             <div
257 |               key={index}
258 |               onClick={() => openModal(u)}
259 |               className="bg-white bg-opacity-80 p-6 rounded-xl shadow hover:shadow-lg cursor-pointer transition"
260 |             >
261 |               <h3 className="text-xl font-semibold mb-2 flex items-center gap-2">
262 |                 {u.title}
263 |               </h3>
264 |               <p className="text-gray-700 mb-4">{u.description}</p>
265 |               <div className="flex flex-wrap gap-2">
266 |                 {u.category.map((cat) => (
267 |                   <span
268 |                     key={cat}
269 |                     className="flex items-center gap-1 bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs"
270 |                   >
271 |                     <span>{cat}</span>
272 |                   </span>
273 |                 ))}
274 |               </div>
275 |             </div>
276 |           ))}
277 |         </div>
278 |       </section>
279 | 
280 |       {/* Modal */}
281 |       {modalOpen && selectedCase && (
282 |         <div
283 |           className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
284 |           onClick={closeModal}
285 |         >
286 |           <div
287 |             className="bg-white rounded-xl w-full max-w-3xl max-h-[90vh] overflow-y-auto"
288 |             onClick={(e) => e.stopPropagation()}
289 |           >
290 |             <div className="sticky top-0 bg-white p-4 flex justify-between items-center border-b">
291 |               <h2 className="text-2xl font-bold">{selectedCase.title}</h2>
292 |               <button
293 |                 onClick={closeModal}
294 |                 aria-label="Close"
295 |                 className="p-2 hover:bg-gray-100 rounded-full"
296 |               >
297 |                 <X size={20} />
298 |               </button>
299 |             </div>
300 |             <div className="p-6">
301 |               <div className="flex flex-wrap gap-2 mb-4">
302 |                 {selectedCase.category.map((cat) => (
303 |                   <span
304 |                     key={cat}
305 |                     className="flex items-center gap-1 bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm"
306 |                   >
307 |                     <span>{cat}</span>
308 |                   </span>
309 |                 ))}
310 |               </div>
311 |               <div className="mb-6">
312 |                 <h3 className="flex items-center gap-2 text-lg font-semibold mb-2 text-gray-800">
313 |                   <Code size={20} /> Prompt for AI
314 |                 </h3>
315 |                 <pre className="bg-gray-100 rounded-lg p-4 whitespace-pre-wrap text-sm text-gray-800">
316 |                   {selectedCase.prompt}
317 |                 </pre>
318 |               </div>
319 |               <div>
320 |                 <h3 className="text-lg font-semibold mb-2 text-gray-800">
321 |                   Expected Result
322 |                 </h3>
323 |                 <div className="bg-gray-100 rounded-lg p-4 text-sm text-gray-800">
324 |                   {selectedCase.result}
325 |                 </div>
326 |               </div>
327 |             </div>
328 |           </div>
329 |         </div>
330 |       )}
331 | 
332 |       {/* Footer */}
333 |       <Footer />
334 |     </div>
335 |   );
336 | };
337 | 
338 | export default App;
339 | 
```

--------------------------------------------------------------------------------
/website/src/useCases.ts:
--------------------------------------------------------------------------------

```typescript
  1 | export const categories = {
  2 |   FILE_GENERATION: '📄 File Generation',
  3 |   IMAGES: '🖼️ Images',
  4 |   DEVELOPMENT: '💻 Development',
  5 |   TESTING: '🧪 Testing',
  6 |   DATA: '📊 Data',
  7 |   WEB: '🌐 Web',
  8 |   SCRAPING: '🕷️ Scraping',
  9 |   EDUCATION: '🎓 Education',
 10 |   API: '🔌 API',
 11 |   CONVERSION: '🔄 Conversion',
 12 |   DATA_VISUALIZATION: '📈 Data Visualization',
 13 |   AI: '🤖 AI',
 14 |   MATH: '➗ Math',
 15 |   FILE_PROCESSING: '📄 File Processing',
 16 |   AUTOMATION: '⚙️ Automation',
 17 |   SIMULATION: '🧪 Simulation',
 18 |   FUN: '🎉 Fun & Games',
 19 |   SECURITY: '🔐 Security',
 20 |   AUDIO: '🔊 Audio',
 21 | } as const;
 22 | 
 23 | export type CategoryKey = keyof typeof categories;
 24 | export interface UseCase {
 25 |   title: string;
 26 |   description: string;
 27 |   category: string[];
 28 |   prompt: string;
 29 |   result: string;
 30 |   prerequisites?: string;
 31 | }
 32 | 
 33 | export const useCases: UseCase[] = [
 34 |   {
 35 |     title: 'Generate a QR Code',
 36 |     description: 'Create a QR code from a URL and save it as an image file.',
 37 |     category: [categories.FILE_GENERATION, categories.IMAGES],
 38 |     prompt: `Create a Node.js script that installs the 'qrcode' package, generates a QR code for the URL "https://nodejs.org/en", and saves it to a file named "qrcode.png".`,
 39 |     result:
 40 |       'An image file "qrcode.png" will be generated in the output folder, containing the QR code.',
 41 |   },
 42 |   {
 43 |     title: 'Test Regular Expressions',
 44 |     description:
 45 |       'Create and test a complex regular expression with unit tests.',
 46 |     category: [categories.DEVELOPMENT, categories.TESTING],
 47 |     prompt: `Create a Node.js script that defines a complex regular expression to match valid mathematical expressions with nested parentheses (e.g. ((2+3)_(4-5))). The regex should support +, -, _, /, numbers, and nesting up to 4 levels. Write at least 10 unit tests, throwing errors if validation fails. Comment the regex logic.`,
 48 |     result:
 49 |       'Console output confirming all regex test cases passed and regex logic validated.',
 50 |   },
 51 |   {
 52 |     title: 'Create CSV with Random Data',
 53 |     description: 'Generate a CSV file with random names, numbers, and emails.',
 54 |     category: [categories.FILE_GENERATION, categories.DATA],
 55 |     prompt:
 56 |       'Create and execute a js script which generates 200 items in a csv. The CSV has full name, random number and random (but valid) email. Write it in a file called "fake_data.csv"',
 57 |     result:
 58 |       'A CSV file "fake_data.csv" with 200 rows of randomized data will appear in the output.',
 59 |   },
 60 |   {
 61 |     title: 'Scrape a Webpage Title',
 62 |     description: 'Fetch a webpage, save HTML, and extract the title.',
 63 |     category: [categories.WEB, categories.SCRAPING],
 64 |     prompt: `Use Node.js with the "cheerio" package to fetch https://example.com, save the HTML to "example.html", and extract the content of the <title> tag. Log it to the console.`,
 65 |     result:
 66 |       'A file "example.html" will be saved and the page title will be printed to the console.',
 67 |   },
 68 |   {
 69 |     title: 'Create a PDF Report',
 70 |     description: 'Generate a playful JavaScript tutorial for kids as a PDF.',
 71 |     category: [categories.FILE_GENERATION, categories.EDUCATION],
 72 |     prompt:
 73 |       'Create a JavaScript script with Node.js that generates a PDF file containing a fun "Getting Started with JavaScript" tutorial for a 10-year-old kid.\n\nThe tutorial should be simple, playful, and colorful, explaining basic concepts like console.log(), variables, and how to write your first small program.\nSave the PDF as getting-started-javascript.pdf with fs\n\nTip: Use `pdf-lib` or `pdfkit` for creating the PDF.',
 74 |     result:
 75 |       'A child-friendly PDF tutorial named "getting-started-javascript.pdf" will be created.',
 76 |   },
 77 |   {
 78 |     title: 'Fetch an API and Save to JSON',
 79 |     description: 'Fetch GitHub API data and save it locally.',
 80 |     category: [categories.WEB, categories.API, categories.FILE_GENERATION],
 81 |     prompt: `Create a Node.js script that fetches repository data from https://api.github.com/repos/nodejs/node and saves the name, description, and star count to a file called "nodejs_info.json".`,
 82 |     result:
 83 |       'A JSON file "nodejs_info.json" will be saved with details from the Node.js GitHub repository.',
 84 |   },
 85 |   {
 86 |     title: 'Markdown to HTML Converter',
 87 |     description: 'Convert Markdown content to HTML using a library.',
 88 |     category: [categories.FILE_GENERATION, categories.CONVERSION],
 89 |     prompt: `Use Node.js and the "marked" package to convert the following Markdown to HTML and save it to "content_converted.html":\n\n# Welcome to My Page\n\nThis is a simple page created from **Markdown**!\n\n- Learn JavaScript\n- Learn Markdown\n- Build Cool Stuff 🚀`,
 90 |     result:
 91 |       'The HTML version of the Markdown content will be saved in "content_converted.html".',
 92 |   },
 93 |   {
 94 |     title: 'Generate Random Data',
 95 |     description: 'Generate fake user data and save to JSON.',
 96 |     category: [categories.FILE_GENERATION, categories.DATA],
 97 |     prompt: `Use Node.js with the "@faker-js/faker" package to create a list of 100 fake users (name, email, address) and save to "fake_users.json".`,
 98 |     result:
 99 |       'A JSON file "fake_users.json" containing 100 fake users will be saved.',
100 |   },
101 |   {
102 |     title: 'Evaluate Complex Math Expression',
103 |     description: 'Use math.js to evaluate a very complex expression.',
104 |     category: [categories.DEVELOPMENT, categories.MATH],
105 |     prompt: `Use Node.js and the "mathjs" package to evaluate the following expression accurately: ((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).`,
106 |     result: 'The result of the full expression will be logged to the console.',
107 |   },
108 |   {
109 |     title: 'Take a Screenshot with Playwright',
110 |     description: 'Launch Chromium and save a screenshot.',
111 |     category: [categories.WEB, categories.IMAGES],
112 |     prompt: `Use Node.js with the "playwright" package and use the optimized Docker image to launch a Chromium browser, visit https://example.com, and save a screenshot to "screenshot_test.png".`,
113 |     result:
114 |       'A PNG screenshot named "screenshot_test.png" will be saved to the output.',
115 |   },
116 |   {
117 |     title: 'Generate a Chart',
118 |     description: 'Create a revenue chart using Chart.js.',
119 |     category: [categories.DATA_VISUALIZATION, categories.IMAGES],
120 |     prompt:
121 |       'Write a JavaScript script that generates a bar chart using chartjs-node-canvas.\nThe chart should show Monthly Revenue Growth for the first 6 months of the year.\nUse the following data:\n\n-January: $12,000\n-February: $15,500\n-March: $14,200\n-April: $18,300\n-May: $21,000\n-June: $24,500\n\nAdd the following details:\n\n-Title: "Monthly Revenue Growth (2025)"\n-X-axis label: "Month"\n-Y-axis label: "Revenue (USD)"\n-Save the resulting chart as chart.png.',
122 |     result:
123 |       'An image file "chart.png" with the rendered revenue chart will be generated.',
124 |   },
125 |   {
126 |     title: 'Summarize a Long Article',
127 |     description: 'Extract Wikipedia article text and summarize it.',
128 |     category: [categories.WEB, categories.AI, categories.SCRAPING],
129 |     prompt: `Fetch and extract plain text from https://en.wikipedia.org/wiki/Node.js. Strip HTML tags and send the text to an AI to summarize in bullet points (max 300 words).`,
130 |     result:
131 |       'A bullet-point summary of the Node.js Wikipedia page will be returned via AI.',
132 |   },
133 |   {
134 |     title: 'Refactor and Optimize JS Code',
135 |     description: 'Refactor a loop-based function using modern JS.',
136 |     category: [categories.DEVELOPMENT, categories.TESTING],
137 |     prompt:
138 |       "Here's an unoptimized JavaScript function:\n```javascript\nfunction getUniqueValues(arr) {\n  let result = [];\n  for (let i = 0; i < arr.length; i++) {\n    let exists = false;\n    for (let j = 0; j < result.length; j++) {\n      if (arr[i] === result[j]) {\n        exists = true;\n        break;\n      }\n    }\n    if (!exists) {\n      result.push(arr[i]);\n    }\n  }\n  return result;\n}\n```\n\nPlease refactor and optimize this function for performance and readability. Then, write and run basic tests with the Node.js test runner to make sure it works (covering common and edge cases). As soon as all tests pass, return only the refactored function.",
139 |     result:
140 |       'A clean, optimized function using modern JS and tests will be logged.',
141 |   },
142 |   {
143 |     title: 'Create a Mock Book API',
144 |     description: 'Build an API from a schema with mock data.',
145 |     category: [categories.WEB, categories.API, categories.DEVELOPMENT],
146 |     prompt: `
147 | Here is a JSON Schema describing a "Book" entity:
148 | 
149 | {
150 |   "$schema": "https://json-schema.org/draft/2020-12/schema",
151 |   "title": "Book",
152 |   "type": "object",
153 |   "required": ["title", "author", "isbn"],
154 |   "properties": {
155 |     "title": {
156 |       "type": "string",
157 |       "minLength": 1
158 |     },
159 |     "author": {
160 |       "type": "string",
161 |       "minLength": 1
162 |     },
163 |     "isbn": {
164 |       "type": "string",
165 |       "pattern": "^(97(8|9))?\\d{9}(\\d|X)$"
166 |     },
167 |     "publishedYear": {
168 |       "type": "integer",
169 |       "minimum": 0,
170 |       "maximum": 2100
171 |     },
172 |     "genres": {
173 |       "type": "array",
174 |       "items": {
175 |         "type": "string"
176 |       }
177 |     },
178 |     "available": {
179 |       "type": "boolean",
180 |       "default": true
181 |     }
182 |   },
183 |   "additionalProperties": false
184 | }
185 | 
186 | Using this schema:
187 | 
188 | 1. Generate **mock data** for at least 5 different books.
189 | 2. Create a simple **Node.js REST API** (you can use Express or Fastify) that:
190 |    - Serves a GET /books endpoint on **port 5007**, which returns all mock books.
191 |    - Serves a GET /books/:isbn endpoint that returns a single book matching the provided ISBN (or a 404 if not found).
192 | 3. Run the server and print a message like:  
193 |    "Mock Book API is running on http://localhost:5007"`,
194 |     result:
195 |       'A local API will be running with endpoints to fetch all or individual mock books.',
196 |   },
197 |   {
198 |     title: 'File Manipulation',
199 |     description: 'Read, filter, and write a JSON file.',
200 |     category: [categories.FILE_PROCESSING, categories.DATA],
201 |     prerequisites:
202 |       ' Create in your mounted folder a file called "books.json" with this content:\n\n```json\n[\n  { "id": 1, "title": "The Silent Code", "author": "Jane Doe" },\n  { "id": 2, "title": "Refactoring Legacy", "author": "John Smith" },\n  { "id": 3, "title": "Async in Action", "author": "Jane Doe" },\n  { "id": 4, "title": "The Pragmatic Stack", "author": "Emily Ray" },\n  { "id": 5, "title": "Systems Unboxed", "author": "Mark Lee" }\n]\n```',
203 |     prompt: `Create a Node.js script to read "books.json", filter books by author "Jane Doe", and save them to "books_filtered.json".`,
204 |     result:
205 |       'A new file "books_filtered.json" will contain only books written by Jane Doe.',
206 |   },
207 |   {
208 |     title: 'Simulate Dice Rolls for a Game',
209 |     description: 'Run thousands of dice rolls and calculate probabilities.',
210 |     category: [categories.SIMULATION, categories.FUN],
211 |     prompt: `Write a Node.js script that simulates 100,000 rolls of two six-sided dice. Count and print the probability of each possible sum (2 to 12), rounded to 4 decimals.`,
212 |     result: 'Console output showing empirical probabilities for each dice sum.',
213 |   },
214 |   {
215 |     title: 'Create a Password Strength Checker',
216 |     description:
217 |       'Use zxcvbn to analyze password strength and suggest improvements.',
218 |     category: [categories.SECURITY, categories.DEVELOPMENT],
219 |     prompt: `Install and use the "zxcvbn" package to check the strength of this password "?p{4t5#z+oJh", and provide suggestions for improvement. Print the feedback to the console.`,
220 |     result: 'Console logs show password strength score and actionable tips.',
221 |   },
222 |   {
223 |     title: 'Explore NPM Package API Surface',
224 |     description:
225 |       'Analyze and extract the top-level functions, types, and exports of any npm package.',
226 |     category: [categories.DEVELOPMENT, categories.AI],
227 |     prompt: `Explore and explain the surface API and exported types of the npm package "lodash".`,
228 |     result:
229 |       'Console output summarizing the functions, classes, and type exports of lodash, including usage hints.',
230 |   },
231 |   {
232 |     title: 'Create Dependency Tree Diagram',
233 |     description:
234 |       'Visualize a local Node.js project’s internal dependency tree.',
235 |     category: [categories.DATA, categories.DEVELOPMENT],
236 |     prompt: `Run madge on the local ./src directory and output the dependency graph as a JSON or SVG.`,
237 |     result:
238 |       'An image or JSON structure representing the project’s internal module dependencies.',
239 |   },
240 |   {
241 |     title: 'Convert CSV to JSON',
242 |     description: 'Read a CSV file and output a clean, structured JSON version.',
243 |     category: [categories.FILE_PROCESSING, categories.CONVERSION],
244 |     prompt: `Read the file "data.csv", convert it to JSON format, and save it as "data.json".`,
245 |     result:
246 |       'File "data.json" created with structured data matching the CSV rows.',
247 |   },
248 |   {
249 |     title: 'Markdown Slide Deck Generator',
250 |     description: 'Convert a markdown document into HTML slides.',
251 |     category: [categories.FILE_GENERATION, categories.EDUCATION],
252 |     prompt: `Take "slides.md" and use "reveal.js" to generate an HTML slide deck in "slides.html".`,
253 |     result:
254 |       'An interactive slide deck HTML file is saved and ready to present.',
255 |   },
256 |   {
257 |     title: 'Generate a Changelog',
258 |     description:
259 |       'Fetch the Git diff between two tags or branches and automatically generate a Markdown changelog.',
260 |     category: [
261 |       categories.AUTOMATION,
262 |       categories.DEVELOPMENT,
263 |       categories.FILE_GENERATION,
264 |     ],
265 |     prompt: `Write a Node.js script that uses the GitHub API to get the commit diff between v1.0.2 and master of the repo "alfonsograziano/node-code-sandbox-mcp". Summarize the changes and generate a Markdown changelog for the upcoming v1.1 release.`,
266 |     result:
267 |       'A Markdown changelog with categorized features, fixes, and improvements based on commit history and diff.',
268 |   },
269 |   {
270 |     title: 'Generate a PR Description from a Diff',
271 |     description:
272 |       'Fetch and analyze the file changes in a GitHub PR and generate a structured PR description in Markdown.',
273 |     category: [categories.AUTOMATION, categories.DEVELOPMENT],
274 |     prompt: `Use the GitHub API to fetch the diff from PR #71 on the "alfonsograziano/node-code-sandbox-mcp" repository. Analyze what was changed, added, or removed, and create a well-formatted PR description with sections like "What’s Changed", "Why", and "Additional Context".`,
275 |     result:
276 |       'A ready-to-use Markdown PR description summarizing the intent and scope of the pull request.',
277 |   },
278 | ];
279 | 
```

--------------------------------------------------------------------------------
/website/src/pages/GraphGPT.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React from 'react';
  2 | import {
  3 |   Brain,
  4 |   GitBranch,
  5 |   Zap,
  6 |   Play,
  7 |   Github,
  8 |   CheckCircle,
  9 |   Database,
 10 |   Layers,
 11 |   Users,
 12 |   ExternalLink,
 13 | } from 'lucide-react';
 14 | import Footer from '../Components/Footer';
 15 | import Header from '../Components/Header';
 16 | 
 17 | const GraphGPT: React.FC = () => {
 18 |   const gridBg: React.CSSProperties = {
 19 |     backgroundImage:
 20 |       'linear-gradient(to right, rgba(0,0,0,0.03) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,0,0,0.03) 1px, transparent 1px)',
 21 |     backgroundSize: '20px 20px',
 22 |   };
 23 | 
 24 |   const features = [
 25 |     {
 26 |       icon: GitBranch,
 27 |       title: 'Non-linear Conversations',
 28 |       description:
 29 |         'Create multiple conversation branches from any point in your interaction',
 30 |     },
 31 |     {
 32 |       icon: Layers,
 33 |       title: 'Visual Graph Interface',
 34 |       description:
 35 |         'Intuitive node-based conversation management using React Flow',
 36 |     },
 37 |     {
 38 |       icon: Zap,
 39 |       title: 'Real-time Streaming',
 40 |       description: 'Live markdown rendering as the AI responds',
 41 |     },
 42 |     {
 43 |       icon: Brain,
 44 |       title: 'Contextual Branching',
 45 |       description: 'Create new nodes from specific parts of AI responses',
 46 |     },
 47 |     {
 48 |       icon: Database,
 49 |       title: 'Conversation Persistence',
 50 |       description: 'Save and manage multiple conversation graphs',
 51 |     },
 52 |     {
 53 |       icon: Users,
 54 |       title: 'Interactive Node Management',
 55 |       description:
 56 |         'Click to activate conversation paths, drag to reposition nodes',
 57 |     },
 58 |   ];
 59 | 
 60 |   return (
 61 |     <div style={gridBg} className="min-h-screen bg-gray-50 text-gray-900">
 62 |       {/* Header */}
 63 |       <Header />
 64 | 
 65 |       {/* Hero Section */}
 66 |       <header className="relative overflow-hidden">
 67 |         <div className="max-w-6xl mx-auto px-6 py-20 text-center">
 68 |           <h1 className="text-4xl md:text-6xl font-extrabold mb-6 leading-tight">
 69 |             GraphGPT
 70 |             <span className="block text-3xl md:text-4xl font-normal text-gray-600 mt-4">
 71 |               A graph-based interface for LLM interactions
 72 |             </span>
 73 |           </h1>
 74 | 
 75 |           <p className="text-lg md:text-xl text-gray-700 mb-8 max-w-3xl mx-auto">
 76 |             Mirror human thinking patterns with non-linear conversations.
 77 |             Instead of linear chat, explore multiple conversation paths
 78 |             simultaneously, creating a visual knowledge graph of your AI
 79 |             interactions.
 80 |           </p>
 81 | 
 82 |           <div className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
 83 |             <a
 84 |               href="https://github.com/alfonsograziano/graph-gpt"
 85 |               target="_blank"
 86 |               rel="noopener noreferrer"
 87 |               className="inline-flex items-center gap-2 px-8 py-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition text-lg font-semibold"
 88 |             >
 89 |               <Github size={20} />
 90 |               View on GitHub
 91 |             </a>
 92 |             <a
 93 |               href="https://www.youtube.com/watch?v=AGMuGlKxO3w"
 94 |               target="_blank"
 95 |               rel="noopener noreferrer"
 96 |               className="inline-flex items-center gap-2 px-8 py-4 bg-white border border-gray-300 text-gray-800 rounded-lg hover:bg-gray-100 transition text-lg font-semibold"
 97 |             >
 98 |               <Play size={20} />
 99 |               Watch Demo
100 |             </a>
101 |           </div>
102 | 
103 |           {/* Hero Image */}
104 |           <div className="relative max-w-4xl mx-auto">
105 |             <div className="bg-gradient-to-r from-purple-400 to-blue-500 rounded-2xl p-2 shadow-2xl">
106 |               <img
107 |                 src="/images/graph-gpt.png"
108 |                 alt="GraphGPT Demo Interface - Interactive graph visualization of AI conversations with nodes and branches"
109 |                 className="w-full h-auto rounded-xl shadow-lg"
110 |               />
111 |             </div>
112 |           </div>
113 |         </div>
114 |       </header>
115 | 
116 |       {/* Demo Video Section */}
117 |       <section className="py-20 bg-white">
118 |         <div className="max-w-6xl mx-auto px-6">
119 |           <div className="text-center mb-12">
120 |             <h2 className="text-3xl md:text-4xl font-bold mb-6">
121 |               📹 See GraphGPT in Action
122 |             </h2>
123 |             <p className="text-lg text-gray-600 max-w-2xl mx-auto">
124 |               Watch how GraphGPT transforms traditional linear conversations
125 |               into dynamic, explorative knowledge graphs.
126 |             </p>
127 |           </div>
128 | 
129 |           <div className="relative max-w-4xl mx-auto">
130 |             <div className="bg-gradient-to-r from-red-500 to-pink-500 rounded-2xl p-2 shadow-2xl">
131 |               <div className="bg-black rounded-xl overflow-hidden">
132 |                 <iframe
133 |                   width="100%"
134 |                   height="400"
135 |                   src="https://www.youtube.com/embed/AGMuGlKxO3w"
136 |                   title="GraphGPT Demo Video"
137 |                   frameBorder="0"
138 |                   allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
139 |                   allowFullScreen
140 |                   className="w-full aspect-video rounded-lg"
141 |                 ></iframe>
142 |               </div>
143 |             </div>
144 |           </div>
145 |         </div>
146 |       </section>
147 | 
148 |       {/* Key Features Section */}
149 |       <section className="py-20 bg-gray-50">
150 |         <div className="max-w-6xl mx-auto px-6">
151 |           <div className="text-center mb-16">
152 |             <h2 className="text-3xl md:text-4xl font-bold mb-6">
153 |               🌟 Key Features
154 |             </h2>
155 |             <p className="text-lg text-gray-600 max-w-2xl mx-auto">
156 |               Experience the future of AI conversations with these powerful
157 |               features
158 |             </p>
159 |           </div>
160 | 
161 |           <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
162 |             {features.map((feature, index) => (
163 |               <div
164 |                 key={index}
165 |                 className="bg-white p-6 rounded-xl shadow-md hover:shadow-lg transition"
166 |               >
167 |                 <feature.icon
168 |                   width={32}
169 |                   height={32}
170 |                   className="text-purple-600 mb-4"
171 |                 />
172 |                 <h3 className="text-xl font-semibold mb-3">{feature.title}</h3>
173 |                 <p className="text-gray-600">{feature.description}</p>
174 |               </div>
175 |             ))}
176 |           </div>
177 |         </div>
178 |       </section>
179 | 
180 |       {/* Reference Parts of Conversation Section */}
181 |       <section className="py-20 bg-white">
182 |         <div className="max-w-6xl mx-auto px-6">
183 |           <div className="text-center mb-16">
184 |             <h2 className="text-3xl md:text-4xl font-bold mb-6">
185 |               🔗 Reference Parts of a Conversation
186 |             </h2>
187 |             <p className="text-lg text-gray-600 max-w-2xl mx-auto">
188 |               Link new chat inputs to specific parts of your conversation for
189 |               more contextual and focused AI interactions
190 |             </p>
191 |           </div>
192 | 
193 |           <div className="bg-gradient-to-br from-blue-50 to-purple-50 rounded-2xl p-8 md:p-12">
194 |             <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
195 |               <div>
196 |                 <h3 className="text-2xl md:text-3xl font-bold mb-6 text-gray-900">
197 |                   Contextual Branching
198 |                 </h3>
199 |                 <p className="text-lg text-gray-700 mb-6">
200 |                   Select any part of your conversation and create a new branch
201 |                   from that specific point. This allows you to explore different
202 |                   directions while maintaining the context of your original
203 |                   discussion.
204 |                 </p>
205 |                 <div className="space-y-4">
206 |                   <div className="flex items-center gap-3">
207 |                     <div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
208 |                       <CheckCircle size={16} className="text-blue-600" />
209 |                     </div>
210 |                     <span className="text-gray-700 font-medium">
211 |                       Click on any message to create a branch
212 |                     </span>
213 |                   </div>
214 |                   <div className="flex items-center gap-3">
215 |                     <div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
216 |                       <CheckCircle size={16} className="text-blue-600" />
217 |                     </div>
218 |                     <span className="text-gray-700 font-medium">
219 |                       Maintain conversation context automatically
220 |                     </span>
221 |                   </div>
222 |                   <div className="flex items-center gap-3">
223 |                     <div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
224 |                       <CheckCircle size={16} className="text-blue-600" />
225 |                     </div>
226 |                     <span className="text-gray-700 font-medium">
227 |                       Explore multiple conversation paths simultaneously
228 |                     </span>
229 |                   </div>
230 |                 </div>
231 |               </div>
232 | 
233 |               {/* Reference Parts Screenshot */}
234 |               <div className="bg-white rounded-xl shadow-lg">
235 |                 <img
236 |                   src="/images/graph-gpt_reference_section.png"
237 |                   alt="GraphGPT Reference Parts - Visual demonstration of contextual branching and conversation referencing"
238 |                   className="w-full h-auto rounded-lg"
239 |                 />
240 |               </div>
241 |             </div>
242 |           </div>
243 |         </div>
244 |       </section>
245 | 
246 |       {/* Markdown Support Section */}
247 |       <section className="py-20 bg-gray-50">
248 |         <div className="max-w-6xl mx-auto px-6">
249 |           <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
250 |             {/* Markdown Support Screenshot */}
251 |             <div className="bg-white rounded-xl shadow-lg">
252 |               <img
253 |                 src="/images/graph-gpt_markdown.png"
254 |                 alt="GraphGPT Markdown Support - Real-time markdown rendering demonstration with code highlighting and formatting"
255 |                 className="w-full h-auto rounded-lg"
256 |               />
257 |             </div>
258 | 
259 |             <div>
260 |               <div>
261 |                 <h2 className="text-3xl md:text-4xl font-bold mb-6">
262 |                   📝 Markdown Support
263 |                 </h2>
264 |                 <p className="text-lg text-gray-600 max-w-2xl mx-auto">
265 |                   Rich text formatting and real-time rendering for enhanced
266 |                   conversation readability and structure
267 |                 </p>
268 |               </div>
269 | 
270 |               <p className="text-lg text-gray-700 mb-6">
271 |                 GraphGPT supports full Markdown formatting, allowing you to
272 |                 create structured, readable conversations with headers, lists,
273 |                 code blocks, and more. All formatting is rendered in real-time
274 |                 as the AI responds.
275 |               </p>
276 |               <div className="space-y-4">
277 |                 <div className="flex items-center gap-3">
278 |                   <div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
279 |                     <CheckCircle size={16} className="text-green-600" />
280 |                   </div>
281 |                   <span className="text-gray-700 font-medium">
282 |                     Real-time markdown rendering
283 |                   </span>
284 |                 </div>
285 |                 <div className="flex items-center gap-3">
286 |                   <div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
287 |                     <CheckCircle size={16} className="text-green-600" />
288 |                   </div>
289 |                   <span className="text-gray-700 font-medium">
290 |                     Code syntax highlighting
291 |                   </span>
292 |                 </div>
293 |                 <div className="flex items-center gap-3">
294 |                   <div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
295 |                     <CheckCircle size={16} className="text-green-600" />
296 |                   </div>
297 |                   <span className="text-gray-700 font-medium">
298 |                     Tables, lists, and formatting support
299 |                   </span>
300 |                 </div>
301 |               </div>
302 |             </div>
303 |           </div>
304 |         </div>
305 |       </section>
306 | 
307 |       {/* Quick Start Section */}
308 |       <section className="py-20 bg-gradient-to-br from-blue-50 to-indigo-100">
309 |         <div className="max-w-6xl mx-auto px-6">
310 |           <div className="text-center mb-16">
311 |             <h2 className="text-3xl md:text-4xl font-bold mb-6">
312 |               🚀 Quick Start
313 |             </h2>
314 |             <p className="text-lg text-gray-600 max-w-2xl mx-auto">
315 |               Get GraphGPT running on your machine in just a few steps
316 |             </p>
317 |           </div>
318 | 
319 |           <div className="bg-white rounded-2xl shadow-lg p-8 md:p-12">
320 |             <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
321 |               {/* Prerequisites */}
322 |               <div>
323 |                 <h3 className="text-2xl font-bold mb-6">Prerequisites</h3>
324 |                 <div className="space-y-4">
325 |                   <div className="flex items-center gap-3">
326 |                     <div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
327 |                       <CheckCircle size={16} className="text-green-600" />
328 |                     </div>
329 |                     <span className="text-gray-700 font-medium">
330 |                       Node.js v20+
331 |                     </span>
332 |                   </div>
333 |                   <div className="flex items-center gap-3">
334 |                     <div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
335 |                       <CheckCircle size={16} className="text-green-600" />
336 |                     </div>
337 |                     <span className="text-gray-700 font-medium">
338 |                       MongoDB (local with Docker, or cloud instance)
339 |                     </span>
340 |                   </div>
341 |                   <div className="flex items-center gap-3">
342 |                     <div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
343 |                       <CheckCircle size={16} className="text-green-600" />
344 |                     </div>
345 |                     <span className="text-gray-700 font-medium">
346 |                       OpenAI API key
347 |                     </span>
348 |                   </div>
349 |                 </div>
350 |               </div>
351 | 
352 |               {/* Installation Steps */}
353 |               <div>
354 |                 <h3 className="text-2xl font-bold mb-6">Installation</h3>
355 |                 <div className="space-y-4">
356 |                   <div className="bg-gray-900 rounded-lg p-4 overflow-x-auto">
357 |                     <code className="text-green-400 text-sm whitespace-nowrap">
358 |                       git clone https://github.com/alfonsograziano/graph-gpt.git
359 |                       <br />
360 |                       cd graph-gpt
361 |                       <br />
362 |                       npm install
363 |                     </code>
364 |                   </div>
365 |                   <div className="bg-gray-900 rounded-lg p-4 overflow-x-auto">
366 |                     <code className="text-green-400 text-sm whitespace-nowrap">
367 |                       cp .env.example .env.local
368 |                       <br /># Add your OpenAI API key and MongoDB URI
369 |                     </code>
370 |                   </div>
371 |                   <div className="bg-gray-900 rounded-lg p-4 overflow-x-auto">
372 |                     <code className="text-green-400 text-sm whitespace-nowrap">
373 |                       npm run start:mongodb
374 |                       <br />
375 |                       npm run dev
376 |                     </code>
377 |                   </div>
378 |                 </div>
379 |               </div>
380 |             </div>
381 | 
382 |             <div className="mt-12 text-center">
383 |               <a
384 |                 href="https://github.com/alfonsograziano/graph-gpt"
385 |                 target="_blank"
386 |                 rel="noopener noreferrer"
387 |                 className="inline-flex items-center gap-2 px-8 py-4 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition text-lg font-semibold"
388 |               >
389 |                 <Github size={20} />
390 |                 Get Started on GitHub
391 |                 <ExternalLink size={16} />
392 |               </a>
393 |             </div>
394 |           </div>
395 |         </div>
396 |       </section>
397 | 
398 |       {/* Contributing Section */}
399 |       <section className="py-20 bg-white">
400 |         <div className="max-w-4xl mx-auto px-6 text-center">
401 |           <h2 className="text-3xl md:text-4xl font-bold mb-6">
402 |             🤝 Contributing
403 |           </h2>
404 |           <p className="text-lg text-gray-600 mb-8">
405 |             We welcome contributions! Please feel free to open a PR or an issue
406 |             to request features :D
407 |           </p>
408 |           <div className="flex justify-center">
409 |             <a
410 |               href="https://github.com/alfonsograziano/graph-gpt"
411 |               target="_blank"
412 |               rel="noopener noreferrer"
413 |               className="inline-flex items-center gap-2 px-6 py-3 bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition"
414 |             >
415 |               <Github size={20} />
416 |               Contribute on GitHub
417 |             </a>
418 |           </div>
419 |         </div>
420 |       </section>
421 | 
422 |       {/* CTA Section */}
423 |       <section className="py-20 bg-gradient-to-r from-purple-600 to-blue-600 text-white">
424 |         <div className="max-w-4xl mx-auto px-6 text-center">
425 |           <h2 className="text-3xl md:text-4xl font-bold mb-6">
426 |             Ready to Transform Your AI Conversations?
427 |           </h2>
428 |           <p className="text-xl mb-8 opacity-90">
429 |             Experience the future of AI interaction with GraphGPT's innovative
430 |             graph-based conversation interface.
431 |           </p>
432 |           <div className="flex flex-col sm:flex-row gap-4 justify-center">
433 |             <a
434 |               href="https://github.com/alfonsograziano/graph-gpt"
435 |               target="_blank"
436 |               rel="noopener noreferrer"
437 |               className="inline-flex items-center gap-2 px-8 py-4 bg-white text-purple-600 rounded-lg hover:bg-gray-100 transition text-lg font-semibold"
438 |             >
439 |               <Github size={20} />
440 |               View on GitHub
441 |             </a>
442 |             <a
443 |               href="https://www.youtube.com/watch?v=AGMuGlKxO3w"
444 |               target="_blank"
445 |               rel="noopener noreferrer"
446 |               className="inline-flex items-center gap-2 px-8 py-4 bg-transparent border-2 border-white text-white rounded-lg hover:bg-white hover:text-purple-600 transition text-lg font-semibold"
447 |             >
448 |               <Play size={20} />
449 |               Watch Demo
450 |             </a>
451 |           </div>
452 |         </div>
453 |       </section>
454 | 
455 |       {/* Footer */}
456 |       <Footer />
457 |     </div>
458 |   );
459 | };
460 | 
461 | export default GraphGPT;
462 | 
```

--------------------------------------------------------------------------------
/website/src/pages/TinyAgent.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React from 'react';
  2 | import {
  3 |   Brain,
  4 |   Code,
  5 |   Shield,
  6 |   Terminal,
  7 |   ArrowRight,
  8 |   CheckCircle,
  9 |   Play,
 10 |   Github,
 11 |   Database,
 12 |   FileText,
 13 |   Cpu,
 14 |   Server,
 15 |   HardDrive,
 16 |   Search,
 17 |   GitBranch,
 18 |   Package,
 19 |   Globe,
 20 |   ArrowUpRight,
 21 |   Settings,
 22 | } from 'lucide-react';
 23 | import Footer from '../Components/Footer';
 24 | import Header from '../Components/Header';
 25 | 
 26 | const TinyAgent: React.FC = () => {
 27 |   const gridBg: React.CSSProperties = {
 28 |     backgroundImage:
 29 |       'linear-gradient(to right, rgba(0,0,0,0.03) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,0,0,0.03) 1px, transparent 1px)',
 30 |     backgroundSize: '20px 20px',
 31 |   };
 32 | 
 33 |   const features = [
 34 |     {
 35 |       icon: Brain,
 36 |       title: 'Intelligent Agent',
 37 |       description: 'LLM-powered agent with context-aware decision making',
 38 |       color: 'from-green-500 to-green-700',
 39 |     },
 40 |     {
 41 |       icon: Server,
 42 |       title: 'Server-Client Architecture',
 43 |       description:
 44 |         'Once the server is running, multiple clients can be used to interact with the agent',
 45 |       color: 'from-green-500 to-green-700',
 46 |     },
 47 |     {
 48 |       icon: Search,
 49 |       title: 'RAG System',
 50 |       description:
 51 |         'Automatic file indexing of your workspace and retrieval with semantic search',
 52 |       color: 'from-green-500 to-green-700',
 53 |     },
 54 |     {
 55 |       icon: Terminal,
 56 |       title: 'Built-in Tools',
 57 |       description:
 58 |         'Integrated MCP servers including code interpreter and Playwright',
 59 |       color: 'from-green-500 to-green-700',
 60 |     },
 61 |     {
 62 |       icon: Shield,
 63 |       title: 'Node.js Sandbox',
 64 |       description: 'Safe code execution environment for dynamic tool creation',
 65 |       color: 'from-green-500 to-green-700',
 66 |     },
 67 |     {
 68 |       icon: Globe,
 69 |       title: 'Smart Web Content Fetching',
 70 |       description:
 71 |         'Fetch and extract clean, LLM-optimized text content from webpages.',
 72 |       color: 'from-green-500 to-green-700',
 73 |     },
 74 |   ];
 75 | 
 76 |   const architecture = [
 77 |     {
 78 |       title: 'Server',
 79 |       description:
 80 |         'Handles RAG operations, memory storage, MCP server management, and tool execution',
 81 |       icon: Server,
 82 |       features: [
 83 |         'File indexing',
 84 |         'Vector search',
 85 |         'Memory storage',
 86 |         'Tool registry',
 87 |       ],
 88 |     },
 89 |     {
 90 |       title: 'Client',
 91 |       description:
 92 |         'Provides command line interface, goal setting, tool invocation, and memory context management',
 93 |       icon: Cpu,
 94 |       features: [
 95 |         'User interaction',
 96 |         'Task management',
 97 |         'Tool handling',
 98 |         'Context management',
 99 |       ],
100 |     },
101 |   ];
102 | 
103 |   const tools = [
104 |     {
105 |       title: 'Code Interpreter',
106 |       description:
107 |         'Execute Node.js code in a sandboxed environment with resource limits',
108 |       icon: Code,
109 |       benefits: ['Safe execution', 'Resource limits', 'Dynamic tools'],
110 |     },
111 |     {
112 |       title: 'Playwright Integration',
113 |       description:
114 |         'Web automation and scraping with browser control and interaction',
115 |       icon: Globe,
116 |       benefits: ['Web automation', 'Browser control', 'Screenshot analysis'],
117 |     },
118 |     {
119 |       title: 'Filesystem Access',
120 |       description: 'File reading, writing, and manipulation operations',
121 |       icon: FileText,
122 |       benefits: [
123 |         'File operations',
124 |         'Directory traversal',
125 |         'Content manipulation',
126 |       ],
127 |     },
128 |   ];
129 | 
130 |   const quickStart = [
131 |     {
132 |       step: '1',
133 |       title: 'Prerequisites',
134 |       description: 'Node.js 18+, PostgreSQL with pgvector, OpenAI API key',
135 |       icon: CheckCircle,
136 |     },
137 |     {
138 |       step: '2',
139 |       title: 'Installation',
140 |       description: 'Run npm install to get all dependencies',
141 |       icon: Package,
142 |     },
143 |     {
144 |       step: '3',
145 |       title: 'Configuration',
146 |       description: 'Set API keys and configure agent settings',
147 |       icon: Settings,
148 |     },
149 |     {
150 |       step: '4',
151 |       title: 'Run',
152 |       description: 'Start server and client processes',
153 |       icon: Play,
154 |     },
155 |   ];
156 | 
157 |   return (
158 |     <div style={gridBg} className="min-h-screen bg-gray-50 text-gray-900">
159 |       {/* Header */}
160 |       <Header />
161 | 
162 |       {/* Hero Section */}
163 |       <header className="relative overflow-hidden">
164 |         <div className="max-w-6xl mx-auto px-6 py-20 grid grid-cols-1 md:grid-cols-2 items-center gap-12">
165 |           {/* Left Column - Content */}
166 |           <div className="text-center md:text-left order-2 md:order-1">
167 |             <h1 className="text-4xl md:text-6xl font-extrabold mb-6 leading-tight">
168 |               <span className="block text-transparent bg-clip-text bg-gradient-to-r from-green-600 to-green-700 leading-[1.2]">
169 |                 Tiny Agent
170 |               </span>
171 | 
172 |               <span className="block text-3xl md:text-4xl font-normal text-gray-600 mt-1">
173 |                 A cute agent written in TypeScript
174 |               </span>
175 |             </h1>
176 | 
177 |             <p className="text-lg md:text-xl text-gray-700 mb-8 max-w-2xl">
178 |               An opinionated AI agent that comes with "batteries included":
179 |               built-in tools, RAG capabilities, memory persistence, and powerful
180 |               integrations.
181 |             </p>
182 | 
183 |             <div className="flex flex-col sm:flex-row gap-4 justify-center md:justify-start mb-12">
184 |               <a
185 |                 href="https://github.com/alfonsograziano/meta-tiny-agents"
186 |                 target="_blank"
187 |                 rel="noopener noreferrer"
188 |                 className="inline-flex items-center gap-2 px-8 py-4 bg-gradient-to-r from-green-600 to-green-700 text-white rounded-lg hover:from-green-700 hover:to-green-800 transition-all text-lg font-semibold shadow-lg hover:shadow-xl transform hover:-translate-y-1"
189 |               >
190 |                 <Github size={20} />
191 |                 View on GitHub
192 |               </a>
193 |               <a
194 |                 href="#features"
195 |                 className="inline-flex items-center gap-2 px-8 py-4 bg-white border border-gray-300 text-gray-800 rounded-lg hover:bg-gray-100 transition text-lg font-semibold"
196 |               >
197 |                 <ArrowRight size={20} />
198 |                 Explore Features
199 |               </a>
200 |             </div>
201 |           </div>
202 | 
203 |           {/* Right Column - Image */}
204 |           <div className="flex justify-center md:justify-end order-1 md:order-2">
205 |             <img
206 |               src="/images/client.png"
207 |               alt="Client illustration"
208 |               className="max-w-full h-auto rounded-2xl shadow-xl"
209 |             />
210 |           </div>
211 |         </div>
212 |       </header>
213 | 
214 |       {/* Features Section */}
215 |       <section id="features" className="py-20 bg-white">
216 |         <div className="max-w-6xl mx-auto px-6">
217 |           <div className="text-center mb-16">
218 |             <h2 className="text-3xl md:text-4xl font-bold mb-4">
219 |               Everything You Need, Built Right In
220 |             </h2>
221 |             <p className="text-lg text-gray-600 max-w-2xl mx-auto">
222 |               No need to hunt for tools or build integrations - everything is
223 |               already included and designed to work together seamlessly
224 |             </p>
225 |           </div>
226 | 
227 |           <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
228 |             {features.map((feature, index) => (
229 |               <div
230 |                 key={index}
231 |                 className={`group relative p-6 rounded-xl border-2 transition-all duration-300 hover:shadow-xl transform hover:-translate-y-2 border-gray-200 bg-white hover:border-green-200`}
232 |               >
233 |                 <div
234 |                   className={`w-12 h-12 rounded-lg bg-gradient-to-r ${feature.color} flex items-center justify-center mb-4 group-hover:scale-110 transition-transform`}
235 |                 >
236 |                   <feature.icon size={24} className="text-white" />
237 |                 </div>
238 |                 <h3 className="text-xl font-semibold mb-2">{feature.title}</h3>
239 |                 <p className="text-gray-600">{feature.description}</p>
240 |               </div>
241 |             ))}
242 |           </div>
243 |         </div>
244 |       </section>
245 | 
246 |       {/* Architecture Section */}
247 |       <section className="py-20 bg-gray-50">
248 |         <div className="max-w-6xl mx-auto px-6">
249 |           <div className="text-center mb-16">
250 |             <h2 className="text-3xl md:text-4xl font-bold mb-4">
251 |               Scalable Architecture
252 |             </h2>
253 |             <p className="text-lg text-gray-600 max-w-2xl mx-auto">
254 |               Built for production with server-client separation and intelligent
255 |               resource management
256 |             </p>
257 |           </div>
258 | 
259 |           <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
260 |             {architecture.map((arch, index) => (
261 |               <div
262 |                 key={index}
263 |                 className="bg-white p-8 rounded-xl shadow-lg border border-gray-200"
264 |               >
265 |                 <div className="flex items-center gap-4 mb-6">
266 |                   <div className="w-12 h-12 bg-gradient-to-r from-green-500 to-green-700 rounded-lg flex items-center justify-center">
267 |                     <arch.icon size={24} className="text-white" />
268 |                   </div>
269 |                   <h3 className="text-2xl font-bold">{arch.title}</h3>
270 |                 </div>
271 |                 <p className="text-gray-600 mb-6">{arch.description}</p>
272 |                 <ul className="space-y-2">
273 |                   {arch.features.map((feature, idx) => (
274 |                     <li
275 |                       key={idx}
276 |                       className="flex items-center gap-2 text-gray-700"
277 |                     >
278 |                       <CheckCircle size={16} className="text-green-500" />
279 |                       {feature}
280 |                     </li>
281 |                   ))}
282 |                 </ul>
283 |               </div>
284 |             ))}
285 |           </div>
286 |         </div>
287 |       </section>
288 | 
289 |       {/* Tools Section */}
290 |       <section className="py-20 bg-white">
291 |         <div className="max-w-6xl mx-auto px-6">
292 |           <div className="text-center mb-16">
293 |             <h2 className="text-3xl md:text-4xl font-bold mb-4">
294 |               Built-in Tools & Capabilities
295 |             </h2>
296 |             <p className="text-lg text-gray-600 max-w-2xl mx-auto">
297 |               Comprehensive toolset for AI development and automation
298 |             </p>
299 |           </div>
300 | 
301 |           <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
302 |             {tools.map((tool, index) => (
303 |               <div
304 |                 key={index}
305 |                 className="bg-gradient-to-br from-gray-50 to-white p-6 rounded-xl border border-gray-200 hover:shadow-lg transition-all duration-300"
306 |               >
307 |                 <div className="w-12 h-12 bg-gradient-to-r from-green-500 to-green-700 rounded-lg flex items-center justify-center mb-4">
308 |                   <tool.icon size={24} className="text-white" />
309 |                 </div>
310 |                 <h3 className="text-xl font-semibold mb-3">{tool.title}</h3>
311 |                 <p className="text-gray-600 mb-4">{tool.description}</p>
312 |                 <ul className="space-y-2">
313 |                   {tool.benefits.map((benefit, idx) => (
314 |                     <li
315 |                       key={idx}
316 |                       className="flex items-center gap-2 text-sm text-gray-600"
317 |                     >
318 |                       <ArrowRight size={14} className="text-purple-500" />
319 |                       {benefit}
320 |                     </li>
321 |                   ))}
322 |                 </ul>
323 |               </div>
324 |             ))}
325 |           </div>
326 |         </div>
327 |       </section>
328 | 
329 |       {/* Quick Start Section */}
330 |       <section className="py-20 bg-gradient-to-br from-green-50 to-green-100">
331 |         <div className="max-w-6xl mx-auto px-6">
332 |           <div className="text-center mb-16">
333 |             <h2 className="text-3xl md:text-4xl font-bold mb-4">
334 |               Get Started in Minutes
335 |             </h2>
336 |             <p className="text-lg text-gray-600 max-w-2xl mx-auto">
337 |               Simple setup process to get your AI agent running quickly
338 |             </p>
339 |           </div>
340 | 
341 |           <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
342 |             {quickStart.map((step, index) => (
343 |               <div
344 |                 key={index}
345 |                 className="bg-white p-6 rounded-xl shadow-lg border border-gray-200 text-center"
346 |               >
347 |                 <div className="w-16 h-16 bg-gradient-to-r from-green-500 to-green-700 rounded-full flex items-center justify-center mx-auto mb-4">
348 |                   <span className="text-white font-bold text-xl">
349 |                     {step.step}
350 |                   </span>
351 |                 </div>
352 |                 <h3 className="text-lg font-semibold mb-2">{step.title}</h3>
353 |                 <p className="text-gray-600 text-sm">{step.description}</p>
354 |               </div>
355 |             ))}
356 |           </div>
357 | 
358 |           <div className="text-center mt-12">
359 |             <a
360 |               href="https://github.com/alfonsograziano/meta-tiny-agents"
361 |               target="_blank"
362 |               rel="noopener noreferrer"
363 |               className="inline-flex items-center gap-2 px-8 py-4 bg-gradient-to-r from-green-600 to-green-700 text-white rounded-lg hover:from-green-700 hover:to-green-800 transition-all text-lg font-semibold shadow-lg hover:shadow-xl transform hover:-translate-y-1"
364 |             >
365 |               <Github size={20} />
366 |               Start Building
367 |               <ArrowUpRight size={20} />
368 |             </a>
369 |           </div>
370 |         </div>
371 |       </section>
372 | 
373 |       {/* RAG & Memory Section */}
374 |       <section className="py-20 bg-white">
375 |         <div className="max-w-6xl mx-auto px-6">
376 |           <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
377 |             <div>
378 |               <h2 className="text-3xl md:text-4xl font-bold mb-6">
379 |                 Advanced RAG & Memory System
380 |               </h2>
381 |               <p className="text-lg text-gray-600 mb-8">
382 |                 Tiny Agent automatically indexes your workspace and provides
383 |                 intelligent context retrieval for seamless AI workflows.
384 |               </p>
385 | 
386 |               <div className="space-y-4">
387 |                 <div className="flex items-start gap-3">
388 |                   <div className="w-6 h-6 bg-green-500 rounded-full flex items-center justify-center mt-1">
389 |                     <CheckCircle size={14} className="text-white" />
390 |                   </div>
391 |                   <div>
392 |                     <h4 className="font-semibold">Automatic File Indexing</h4>
393 |                     <p className="text-gray-600 text-sm">
394 |                       Files are automatically processed and indexed for semantic
395 |                       search
396 |                     </p>
397 |                   </div>
398 |                 </div>
399 | 
400 |                 <div className="flex items-start gap-3">
401 |                   <div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center mt-1">
402 |                     <CheckCircle size={14} className="text-white" />
403 |                   </div>
404 |                   <div>
405 |                     <h4 className="font-semibold">Vector Storage</h4>
406 |                     <p className="text-gray-600 text-sm">
407 |                       Uses pgvector for efficient similarity search and
408 |                       retrieval
409 |                     </p>
410 |                   </div>
411 |                 </div>
412 | 
413 |                 <div className="flex items-start gap-3">
414 |                   <div className="w-6 h-6 bg-purple-500 rounded-full flex items-center justify-center mt-1">
415 |                     <CheckCircle size={14} className="text-white" />
416 |                   </div>
417 |                   <div>
418 |                     <h4 className="font-semibold">Memory Persistence</h4>
419 |                     <p className="text-gray-600 text-sm">
420 |                       Memories survive restarts and are stored as semantic
421 |                       vectors
422 |                     </p>
423 |                   </div>
424 |                 </div>
425 |               </div>
426 |             </div>
427 | 
428 |             <div className="relative">
429 |               <div className="bg-gradient-to-br from-green-100 to-green-200 p-8 rounded-2xl">
430 |                 <div className="space-y-4">
431 |                   <div className="bg-white p-4 rounded-lg shadow-sm">
432 |                     <div className="flex items-center gap-2 mb-2">
433 |                       <Search size={16} className="text-purple-500" />
434 |                       <span className="font-mono text-sm text-gray-600">
435 |                         RAG Query
436 |                       </span>
437 |                     </div>
438 |                     <p className="text-sm text-gray-800">
439 |                       Find relevant context for AI tasks
440 |                     </p>
441 |                   </div>
442 | 
443 |                   <div className="bg-white p-4 rounded-lg shadow-sm">
444 |                     <div className="flex items-center gap-2 mb-2">
445 |                       <Database size={16} className="text-blue-500" />
446 |                       <span className="font-mono text-sm text-gray-600">
447 |                         Vector Search
448 |                       </span>
449 |                     </div>
450 |                     <p className="text-sm text-gray-800">
451 |                       Semantic similarity matching
452 |                     </p>
453 |                   </div>
454 | 
455 |                   <div className="bg-white p-4 rounded-lg shadow-sm">
456 |                     <div className="flex items-center gap-2 mb-2">
457 |                       <HardDrive size={16} className="text-green-500" />
458 |                       <span className="font-mono text-sm text-gray-600">
459 |                         Memory Store
460 |                       </span>
461 |                     </div>
462 |                     <p className="text-sm text-gray-800">
463 |                       Persistent context across sessions
464 |                     </p>
465 |                   </div>
466 |                 </div>
467 |               </div>
468 |             </div>
469 |           </div>
470 |         </div>
471 |       </section>
472 | 
473 |       {/* CTA Section */}
474 |       <section className="py-20 bg-gradient-to-r from-green-600 to-green-700">
475 |         <div className="max-w-4xl mx-auto px-6 text-center">
476 |           <h2 className="text-3xl md:text-4xl font-bold text-white mb-6">
477 |             Ready to Build AI Agents the Right Way?
478 |           </h2>
479 |           <p className="text-xl text-green-100 mb-8">
480 |             Skip the setup headaches and start building with our opinionated,
481 |             batteries-included framework
482 |           </p>
483 | 
484 |           <div className="flex flex-col sm:flex-row gap-4 justify-center">
485 |             <a
486 |               href="https://github.com/alfonsograziano/meta-tiny-agents"
487 |               target="_blank"
488 |               rel="noopener noreferrer"
489 |               className="inline-flex items-center gap-2 px-8 py-4 bg-white text-green-600 rounded-lg hover:bg-gray-100 transition text-lg font-semibold shadow-lg"
490 |             >
491 |               <Github size={20} />
492 |               Star on GitHub
493 |             </a>
494 |             <a
495 |               href="https://github.com/alfonsograziano/meta-tiny-agents"
496 |               target="_blank"
497 |               rel="noopener noreferrer"
498 |               className="inline-flex items-center gap-2 px-8 py-4 border-2 border-white text-white rounded-lg hover:bg-white hover:text-green-600 transition text-lg font-semibold"
499 |             >
500 |               <GitBranch size={20} />
501 |               Fork & Contribute
502 |             </a>
503 |           </div>
504 |         </div>
505 |       </section>
506 | 
507 |       {/* Footer */}
508 |       <Footer />
509 |     </div>
510 |   );
511 | };
512 | 
513 | export default TinyAgent;
514 | 
```

--------------------------------------------------------------------------------
/test/runJsEphemeral.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import * as tmp from 'tmp';
  3 | import { z } from 'zod';
  4 | import runJsEphemeral, { argSchema } from '../src/tools/runJsEphemeral.ts';
  5 | import { DEFAULT_NODE_IMAGE, PLAYWRIGHT_IMAGE } from '../src/utils.ts';
  6 | import { describeIfLocal } from './utils.ts';
  7 | import type {
  8 |   McpContentImage,
  9 |   McpContentResource,
 10 |   McpContentText,
 11 |   McpContentTextResource,
 12 | } from '../src/types.ts';
 13 | 
 14 | let tmpDir: tmp.DirResult;
 15 | 
 16 | describe('runJsEphemeral', () => {
 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 |   describe('argSchema', () => {
 27 |     it('should use default values for image and dependencies', () => {
 28 |       const parsed = z.object(argSchema).parse({ code: 'console.log(1);' });
 29 |       expect(parsed.image).toBe(DEFAULT_NODE_IMAGE);
 30 |       expect(parsed.dependencies).toEqual([]);
 31 |       expect(parsed.code).toBe('console.log(1);');
 32 |     });
 33 | 
 34 |     it('should accept valid custom image and dependencies', () => {
 35 |       const input = {
 36 |         image: DEFAULT_NODE_IMAGE,
 37 |         dependencies: [
 38 |           { name: 'lodash', version: '^4.17.21' },
 39 |           { name: 'axios', version: '^1.0.0' },
 40 |         ],
 41 |         code: "console.log('hi');",
 42 |       };
 43 |       const parsed = z.object(argSchema).parse(input);
 44 |       expect(parsed.image).toBe(DEFAULT_NODE_IMAGE);
 45 |       expect(parsed.dependencies.length).toBe(2);
 46 |       expect(parsed.dependencies[0]).toEqual({
 47 |         name: 'lodash',
 48 |         version: '^4.17.21',
 49 |       });
 50 |       expect(parsed.code).toBe("console.log('hi');");
 51 |     });
 52 |   });
 53 | 
 54 |   describe('should run runJsEphemeral', () => {
 55 |     it('shoud run runJsEphemeral base', async () => {
 56 |       const result = await runJsEphemeral({
 57 |         code: "console.log('Hello, world!');",
 58 |         dependencies: [],
 59 |       });
 60 |       expect(result).toBeDefined();
 61 |       expect(result.content).toBeDefined();
 62 |       expect(result.content.length).toBeGreaterThan(0);
 63 |       expect(result.content[0].type).toBe('text');
 64 | 
 65 |       if (result.content[0].type === 'text') {
 66 |         expect(result.content[0].text).toContain('Hello, world!');
 67 |       } else {
 68 |         throw new Error("Expected content type to be 'text'");
 69 |       }
 70 |     });
 71 | 
 72 |     it('should generate telemetry', async () => {
 73 |       const result = await runJsEphemeral({
 74 |         code: "console.log('Hello telemetry!');",
 75 |         dependencies: [],
 76 |       });
 77 | 
 78 |       const telemetryItem = result.content.find(
 79 |         (c) => c.type === 'text' && c.text.startsWith('Telemetry:')
 80 |       );
 81 |       expect(telemetryItem).toBeDefined();
 82 |       if (telemetryItem?.type === 'text') {
 83 |         const telemetry = JSON.parse(
 84 |           telemetryItem.text.replace('Telemetry:\n', '')
 85 |         );
 86 |         expect(telemetry).toHaveProperty('installTimeMs');
 87 |         expect(typeof telemetry.installTimeMs).toBe('number');
 88 |         expect(telemetry).toHaveProperty('runTimeMs');
 89 |         expect(typeof telemetry.runTimeMs).toBe('number');
 90 |         expect(telemetry).toHaveProperty('installOutput');
 91 |         expect(typeof telemetry.installOutput).toBe('string');
 92 |       } else {
 93 |         throw new Error("Expected telemetry item to be of type 'text'");
 94 |       }
 95 |     });
 96 | 
 97 |     it('should hang indefinitely until a timeout error gets triggered', async () => {
 98 |       //Simulating a 10 seconds timeout
 99 |       process.env.RUN_SCRIPT_TIMEOUT = '10000';
100 |       const result = await runJsEphemeral({
101 |         code: `
102 |           (async () => {
103 |             console.log("🕒 Hanging for 20 seconds…");
104 |             await new Promise((resolve) => setTimeout(resolve, 20_000));
105 |             console.log("✅ Done waiting 20 seconds, exiting now.");
106 |           })();
107 |            `,
108 |       });
109 | 
110 |       //Cleanup
111 |       delete process.env.RUN_SCRIPT_TIMEOUT;
112 | 
113 |       const execError = result.content.find(
114 |         (item) =>
115 |           item.type === 'text' &&
116 |           item.text.startsWith('Error during execution:')
117 |       );
118 |       expect(execError).toBeDefined();
119 |       expect((execError as McpContentText).text).toContain('ETIMEDOUT');
120 | 
121 |       const telemetryText = result.content.find(
122 |         (item) => item.type === 'text' && item.text.startsWith('Telemetry:')
123 |       );
124 |       expect(telemetryText).toBeDefined();
125 |     }, 20_000);
126 | 
127 |     it('should report execution error for runtime exceptions', async () => {
128 |       const result = await runJsEphemeral({
129 |         code: `throw new Error('boom');`,
130 |       });
131 | 
132 |       expect(result).toBeDefined();
133 |       expect(result.content).toBeDefined();
134 | 
135 |       // should hit our "other errors" branch
136 |       const execError = result.content.find(
137 |         (item) =>
138 |           item.type === 'text' &&
139 |           (item as McpContentText).text.startsWith('Error during execution:')
140 |       );
141 |       expect(execError).toBeDefined();
142 |       expect((execError as McpContentText).text).toContain('Error: boom');
143 | 
144 |       // telemetry should still be returned
145 |       const telemetryText = result.content.find(
146 |         (item) =>
147 |           item.type === 'text' &&
148 |           (item as McpContentText).text.startsWith('Telemetry:')
149 |       );
150 |       expect(telemetryText).toBeDefined();
151 |     });
152 | 
153 |     it('should skip npm install if no dependencies are provided', async () => {
154 |       const result = await runJsEphemeral({
155 |         code: "console.log('No deps');",
156 |         dependencies: [],
157 |       });
158 | 
159 |       const telemetryItem = result.content.find(
160 |         (c) => c.type === 'text' && c.text.startsWith('Telemetry:')
161 |       );
162 | 
163 |       expect(telemetryItem).toBeDefined();
164 |       if (telemetryItem?.type === 'text') {
165 |         const telemetry = JSON.parse(
166 |           telemetryItem.text.replace('Telemetry:\n', '')
167 |         );
168 | 
169 |         expect(telemetry.installTimeMs).toBe(0);
170 |         expect(telemetry.installOutput).toBe(
171 |           'Skipped npm install (no dependencies)'
172 |         );
173 |       }
174 |     });
175 | 
176 |     it('should generate a valid QR code resource', async () => {
177 |       const result = await runJsEphemeral({
178 |         code: `
179 |           import fs from 'fs';
180 |           import qrcode from 'qrcode';
181 |     
182 |           const url = 'https://nodejs.org/en';
183 |           const outputFile = './files/qrcode.png';
184 |     
185 |           qrcode.toFile(outputFile, url, {
186 |             type: 'png',
187 |           }, function(err) {
188 |             if (err) throw err;
189 |             console.log('QR code saved as PNG!');
190 |           });
191 |         `,
192 |         dependencies: [
193 |           {
194 |             name: 'qrcode',
195 |             version: '^1.5.3',
196 |           },
197 |         ],
198 |       });
199 | 
200 |       expect(result).toBeDefined();
201 |       expect(result.content).toBeDefined();
202 | 
203 |       // Find process output
204 |       const processOutput = result.content.find(
205 |         (item) =>
206 |           item.type === 'text' &&
207 |           item.text.startsWith('Node.js process output:')
208 |       );
209 |       expect(processOutput).toBeDefined();
210 |       expect((processOutput as McpContentText).text).toContain(
211 |         'QR code saved as PNG!'
212 |       );
213 | 
214 |       // Find QR image
215 |       const imageResource = result.content.find(
216 |         (item) => item.type === 'image' && item.mimeType === 'image/png'
217 |       );
218 |       expect(imageResource).toBeDefined();
219 |     }, 15_000);
220 | 
221 |     it('should save a hello.txt file and return it as a resource', async () => {
222 |       const result = await runJsEphemeral({
223 |         code: `
224 |           import fs from 'fs/promises';
225 |           await fs.writeFile('./files/hello test.txt', 'Hello world!');
226 |           console.log('Saved hello test.txt');
227 |         `,
228 |       });
229 | 
230 |       expect(result).toBeDefined();
231 |       expect(result.content).toBeDefined();
232 | 
233 |       // Find process output
234 |       const processOutput = result.content.find(
235 |         (item) =>
236 |           item.type === 'text' &&
237 |           item.text.startsWith('Node.js process output:')
238 |       );
239 |       expect(processOutput).toBeDefined();
240 |       expect((processOutput as McpContentText).text).toContain(
241 |         'Saved hello test.txt'
242 |       );
243 | 
244 |       // Find file change info
245 |       const changeInfo = result.content.find(
246 |         (item) =>
247 |           item.type === 'text' && item.text.startsWith('List of changed files:')
248 |       );
249 |       expect(changeInfo).toBeDefined();
250 |       expect((changeInfo as McpContentText).text).toContain(
251 |         '- hello test.txt was created'
252 |       );
253 | 
254 |       // Find the resource
255 |       const resource = result.content.find((item) => item.type === 'resource');
256 |       expect(resource).toBeDefined();
257 |       expect((resource as McpContentResource).resource.mimeType).toBe(
258 |         'text/plain'
259 |       );
260 |       expect((resource as McpContentResource).resource.uri).toContain(
261 |         'hello%20test.txt'
262 |       );
263 |       expect((resource as McpContentResource).resource.uri).toContain(
264 |         'file://'
265 |       );
266 |       if ('text' in (resource as McpContentResource).resource) {
267 |         expect((resource as McpContentTextResource).resource.text).toBe(
268 |           'hello test.txt'
269 |         );
270 |       } else {
271 |         throw new Error("Expected resource to have a 'text' property");
272 |       }
273 | 
274 |       // Find telemetry info
275 |       const telemetry = result.content.find(
276 |         (item) => item.type === 'text' && item.text.startsWith('Telemetry:')
277 |       );
278 |       expect(telemetry).toBeDefined();
279 |       expect((telemetry as McpContentText).text).toContain('"installTimeMs"');
280 |       expect((telemetry as McpContentText).text).toContain('"runTimeMs"');
281 |     });
282 |   }, 10_000);
283 | 
284 |   describe('runJsEphemeral error handling', () => {
285 |     it('should return an execution error and telemetry when the code throws', async () => {
286 |       const result = await runJsEphemeral({
287 |         code: "throw new Error('Test error');",
288 |       });
289 | 
290 |       const execError = result.content.find(
291 |         (item) =>
292 |           item.type === 'text' &&
293 |           item.text.startsWith('Error during execution:')
294 |       );
295 |       expect(execError).toBeDefined();
296 |       expect((execError as McpContentText).text).toContain('Test error');
297 | 
298 |       const telemetryText = result.content.find(
299 |         (item) => item.type === 'text' && item.text.startsWith('Telemetry:')
300 |       );
301 |       expect(telemetryText).toBeDefined();
302 |     });
303 |   });
304 | 
305 |   describe('runJsEphemeral multiple file outputs', () => {
306 |     it('should handle saving both text and JPEG files correctly', async () => {
307 |       const base64 =
308 |         '/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxISEhUSEhIVFhUVFRUVFRUVFRUVFRUVFRUWFhUVFRUYHSggGBolGxUVITEhJSkrLi4uFx8zODMsNygtLisBCgoKDg0OGhAQGy0lHyYtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAJ8BPgMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAAEBgIDBQABB//EADkQAAIBAgQDBgQEBQUBAAAAAAECAwQRAAUSITFBBhTiUWEHFDJxgZEjQrHB0RUjYnLw8RUz/8QAGQEAAgMBAAAAAAAAAAAAAAAAAwQBAgAF/8QAJBEAAgEEAgEFAAAAAAAAAAAAAQIDBBESITFBBRMiUYGh/9oADAMBAAIRAxEAPwD9YKKKAP/Z';
309 | 
310 |       const result = await runJsEphemeral({
311 |         code: `
312 |           import fs from 'fs/promises';
313 |           await fs.writeFile('./files/foo.txt', 'Hello Foo');
314 |           const img = Buffer.from('${base64}', 'base64');
315 |           await fs.writeFile('./files/bar.jpg', img);
316 |           console.log('Done writing foo.txt and bar.jpg');
317 |         `,
318 |       });
319 | 
320 |       expect(result).toBeDefined();
321 |       expect(result.content).toBeDefined();
322 | 
323 |       // stdout
324 |       const stdout = result.content.find(
325 |         (c) =>
326 |           c.type === 'text' &&
327 |           c.text.includes('Done writing foo.txt and bar.jpg')
328 |       );
329 |       expect(stdout).toBeDefined();
330 |       expect((stdout as McpContentText).text).toContain(
331 |         'Done writing foo.txt and bar.jpg'
332 |       );
333 | 
334 |       // resource names (foo.txt and bar.jpg)
335 |       const savedResourceNames = result.content
336 |         .filter((c) => c.type === 'resource')
337 |         .map((c) => (c as McpContentTextResource).resource.text);
338 | 
339 |       expect(savedResourceNames).toEqual(
340 |         expect.arrayContaining(['foo.txt', 'bar.jpg'])
341 |       );
342 | 
343 |       // JPEG image check
344 |       const jpegImage = result.content.find(
345 |         (c) => c.type === 'image' && c.mimeType === 'image/jpeg'
346 |       );
347 |       expect(jpegImage).toBeDefined();
348 |     });
349 |   });
350 | 
351 |   describe('runJsEphemeral screenshot with Playwright', () => {
352 |     it('should take a screenshot of example.com using Playwright and the Playwright image', async () => {
353 |       process.env.RUN_SCRIPT_TIMEOUT = '80000';
354 | 
355 |       const playwrightVersion =
356 |         PLAYWRIGHT_IMAGE.match(/:v(\d+\.\d+\.\d+)/)?.[1];
357 |       if (!playwrightVersion) {
358 |         throw new Error(
359 |           `Could not extract Playwright version from image: ${PLAYWRIGHT_IMAGE}`
360 |         );
361 |       }
362 | 
363 |       const result = await runJsEphemeral({
364 |         code: `
365 |             import { chromium } from 'playwright';
366 |     
367 |             (async () => {
368 |               const browser = await chromium.launch({ args: ['--no-sandbox'] });
369 |               const page = await browser.newPage();
370 |               await page.goto('https://example.com', { timeout: 70000 });
371 |               await page.screenshot({ path: './files/example_screenshot.png' });
372 |               await browser.close();
373 |               console.log('Screenshot saved');
374 |             })();
375 |           `,
376 |         dependencies: [
377 |           {
378 |             name: 'playwright',
379 |             version: `^${playwrightVersion}`,
380 |           },
381 |         ],
382 |         image: PLAYWRIGHT_IMAGE,
383 |       });
384 | 
385 |       // Cleanup
386 |       delete process.env.RUN_SCRIPT_TIMEOUT;
387 | 
388 |       expect(result).toBeDefined();
389 |       expect(result.content).toBeDefined();
390 | 
391 |       // stdout check
392 |       const output = result.content.find(
393 |         (item) => item.type === 'text' && item.text.includes('Screenshot saved')
394 |       );
395 |       expect(output).toBeDefined();
396 |       expect((output as McpContentText).text).toContain('Screenshot saved');
397 | 
398 |       // PNG image resource check
399 |       const image = result.content.find(
400 |         (item) => item.type === 'image' && item.mimeType === 'image/png'
401 |       );
402 |       expect(image).toBeDefined();
403 |     }, 100_000);
404 |   });
405 | 
406 |   describe('runJsEphemeral linting', () => {
407 |     it('should auto-fix linting issues and run the corrected code', async () => {
408 |       const result = await runJsEphemeral({
409 |         // This code has fixable issues: `var` instead of `const`, and extra spacing.
410 |         code: `var msg = "hello auto-fixed world"  ; console.log(msg)`,
411 |       });
412 | 
413 |       // 1. Check that no linting report was returned, as it should be auto-fixed.
414 |       const lintReport = result.content.find(
415 |         (c) => c.type === 'text' && c.text.startsWith('Linting issues found')
416 |       );
417 |       expect(lintReport).toBeUndefined();
418 | 
419 |       // 2. Check that the execution was successful and the output is correct.
420 |       const execOutput = result.content.find(
421 |         (c) => c.type === 'text' && c.text.startsWith('Node.js process output:')
422 |       );
423 |       expect(execOutput).toBeDefined();
424 |       if (execOutput?.type === 'text') {
425 |         expect(execOutput.text).toContain('hello auto-fixed world');
426 |       }
427 | 
428 |       // 3. Check that there was no execution error.
429 |       const execError = result.content.find(
430 |         (c) => c.type === 'text' && c.text.startsWith('Error during execution:')
431 |       );
432 |       expect(execError).toBeUndefined();
433 |     });
434 | 
435 |     it('should report unfixable linting issues and the subsequent execution error', async () => {
436 |       const result = await runJsEphemeral({
437 |         // This code has an unfixable issue: using an undefined variable.
438 |         code: `console.log(someUndefinedVariable);`,
439 |       });
440 | 
441 |       expect(result).toBeDefined();
442 | 
443 |       // 1. Check that a linting report was returned.
444 |       const lintReport = result.content.find(
445 |         (c) => c.type === 'text' && c.text.startsWith('Linting issues found')
446 |       );
447 |       expect(lintReport).toBeDefined();
448 |       if (lintReport?.type === 'text') {
449 |         expect(lintReport.text).toContain(
450 |           "'someUndefinedVariable' is not defined."
451 |         );
452 |         expect(lintReport.text).toContain('(no-undef)');
453 |       }
454 | 
455 |       // 2. Check that the execution also failed and was reported.
456 |       const execError = result.content.find(
457 |         (c) => c.type === 'text' && c.text.startsWith('Error during execution:')
458 |       );
459 |       expect(execError).toBeDefined();
460 |       if (execError?.type === 'text') {
461 |         expect(execError.text).toContain(
462 |           'ReferenceError: someUndefinedVariable is not defined'
463 |         );
464 |       }
465 |     });
466 |   });
467 | 
468 |   // Skipping this on the CI as it requires a lot of resources
469 |   // and an image that is not available in the CI environment
470 |   describeIfLocal(
471 |     'runJsEphemeral generate charts',
472 |     () => {
473 |       it('should correctly generate a chart', async () => {
474 |         const result = await runJsEphemeral({
475 |           code: `
476 |           import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
477 |           import fs from 'fs';
478 |   
479 |           const width = 800;
480 |           const height = 400;
481 |           const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height });
482 |   
483 |           const data = {
484 |             labels: ['January', 'February', 'March', 'April', 'May', 'June'],
485 |             datasets: [{
486 |               label: 'Monthly Revenue Growth (2025)',
487 |               data: [12000, 15500, 14200, 18300, 21000, 24500],
488 |               backgroundColor: 'rgba(75, 192, 192, 0.6)',
489 |               borderColor: 'rgba(75, 192, 192, 1)',
490 |               borderWidth: 1
491 |             }]
492 |           };
493 |   
494 |           const config = {
495 |             type: 'bar',
496 |             data: data,
497 |             options: {
498 |               responsive: true,
499 |               plugins: {
500 |                 legend: {
501 |                   display: true,
502 |                   position: 'top',
503 |                 },
504 |                 title: {
505 |                   display: true,
506 |                   text: 'Monthly Revenue Growth (2025)',
507 |                 }
508 |               },
509 |               scales: {
510 |                 x: {
511 |                   title: {
512 |                     display: true,
513 |                     text: 'Month'
514 |                   }
515 |                 },
516 |                 y: {
517 |                   title: {
518 |                     display: true,
519 |                     text: 'Revenue (USD)'
520 |                   },
521 |                   beginAtZero: true
522 |                 }
523 |               }
524 |             }
525 |           };
526 |   
527 |           async function generateChart() {
528 |             const image = await chartJSNodeCanvas.renderToBuffer(config);
529 |             fs.writeFileSync('./files/chart_test.png', image);
530 |             console.log('Chart saved as chart.png');
531 |           }
532 |   
533 |           generateChart();
534 |         `,
535 |           image: 'alfonsograziano/node-chartjs-canvas:latest',
536 |         });
537 | 
538 |         expect(result).toBeDefined();
539 |         expect(result.content).toBeDefined();
540 | 
541 |         const output = result.content.find(
542 |           (item) =>
543 |             item.type === 'text' &&
544 |             typeof item.text === 'string' &&
545 |             item.text.includes('Chart saved as chart.png')
546 |         );
547 |         expect(output).toBeDefined();
548 |         expect((output as { type: 'text'; text: string }).text).toContain(
549 |           'Chart saved as chart.png'
550 |         );
551 | 
552 |         const image = result.content.find(
553 |           (item) =>
554 |             item.type === 'image' &&
555 |             'mimeType' in item &&
556 |             item.mimeType === 'image/png'
557 |         );
558 |         expect(image).toBeDefined();
559 |         expect((image as McpContentImage).mimeType).toBe('image/png');
560 |       });
561 | 
562 |       it('should still be able to add new dependencies with the node-chartjs-canvas image', async () => {
563 |         const result = await runJsEphemeral({
564 |           code: `
565 |           import _ from 'lodash';
566 |           console.log('_.chunk([1,2,3,4,5], 2):', _.chunk([1,2,3,4,5], 2));
567 |         `,
568 |           dependencies: [{ name: 'lodash', version: '^4.17.21' }],
569 |           image: 'alfonsograziano/node-chartjs-canvas:latest',
570 |         });
571 | 
572 |         expect(result).toBeDefined();
573 |         expect(result.content).toBeDefined();
574 | 
575 |         const output = result.content.find(
576 |           (item) =>
577 |             item.type === 'text' &&
578 |             typeof item.text === 'string' &&
579 |             item.text.includes('[ [ 1, 2 ], [ 3, 4 ], [ 5 ] ]')
580 |         );
581 |         expect(output).toBeDefined();
582 |         expect((output as McpContentText).text).toContain(
583 |           '[ [ 1, 2 ], [ 3, 4 ], [ 5 ] ]'
584 |         );
585 |       });
586 | 
587 |       it('should generate a Mermaid sequence diagram SVG file', async () => {
588 |         const result = await runJsEphemeral({
589 |           code: `
590 |             import fs from "fs";
591 |             import { run } from "@mermaid-js/mermaid-cli";
592 |       
593 |             const diagramDefinition = \`
594 |             sequenceDiagram
595 |                 participant App as Application
596 |                 participant KC as Keycloak
597 |                 participant IDP as Identity Provider
598 |       
599 |                 %% Initial Sign-In
600 |                 App->>KC: "Go authenticate!"
601 |                 KC->>App: Redirect to Keycloak login
602 |                 KC->>IDP: "Which IDP? (MyGovID / EntraID)"
603 |                 IDP-->>KC: ID Token + Refresh Token (1 day)
604 |                 KC-->>App: KC Tokens (1 day)
605 |       
606 |                 %% After 24 Hours
607 |                 App->>KC: Request new tokens (expired refresh token)
608 |                 alt KC session still active (<14 days)
609 |                     KC-->>App: New tokens (1 day)
610 |                 else KC session expired (>14 days)
611 |                     KC->>IDP: Redirect to reauthenticate
612 |                     IDP-->>KC: Fresh ID + Refresh Tokens
613 |                     KC-->>App: New KC Tokens (1 day)
614 |                 end
615 |             \`;
616 |       
617 |             fs.writeFileSync("./files/authDiagram.mmd", diagramDefinition, "utf8");
618 |             console.log("Mermaid definition saved to authDiagram.mmd");
619 |       
620 |             console.time("test");
621 |             await run("./files/authDiagram.mmd", "./files/output.svg", {
622 |               puppeteerConfig: { args: ['--no-sandbox'] },
623 |             });
624 |             console.timeEnd("test");
625 |             console.log("Diagram generated as output.svg");
626 |           `,
627 |           dependencies: [
628 |             { name: '@mermaid-js/mermaid-cli', version: '^11.4.2' },
629 |           ],
630 |           image: 'alfonsograziano/node-chartjs-canvas:latest',
631 |         });
632 | 
633 |         // Ensure result exists
634 |         expect(result).toBeDefined();
635 |         expect(result.content).toBeDefined();
636 | 
637 |         // Validate Mermaid diagram creation log
638 |         const logOutput = result.content.find(
639 |           (item) =>
640 |             item.type === 'text' &&
641 |             item.text.includes('Diagram generated as output.svg')
642 |         );
643 |         expect(logOutput).toBeDefined();
644 | 
645 |         // Validate .mmd file was created
646 |         const mmdFile = result.content.find(
647 |           (item) =>
648 |             item.type === 'resource' &&
649 |             item.resource?.uri?.endsWith('authDiagram.mmd')
650 |         );
651 |         expect(mmdFile).toBeDefined();
652 | 
653 |         // Validate that the SVG file was generated and returned as a resource
654 |         const svgFile = result.content.find(
655 |           (item) =>
656 |             item.type === 'resource' &&
657 |             item.resource?.uri?.endsWith('output.svg')
658 |         );
659 |         expect(svgFile).toBeDefined();
660 |       });
661 |     },
662 |     50_000
663 |   );
664 | });
665 | 
```
Page 2/3FirstPrevNextLast