Skip to main content
Glama

Node Code Sandbox MCP

by mozicim
runJsEphemeral.test.ts19.9 kB
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as tmp from 'tmp'; import { z } from 'zod'; import runJsEphemeral, { argSchema } from '../src/tools/runJsEphemeral.ts'; import { DEFAULT_NODE_IMAGE } from '../src/utils.ts'; import { describeIfLocal } from './utils.ts'; import type { McpContentImage, McpContentResource, McpContentText, McpContentTextResource, } from '../src/types.ts'; let tmpDir: tmp.DirResult; describe('runJsEphemeral', () => { beforeEach(() => { tmpDir = tmp.dirSync({ unsafeCleanup: true }); process.env.FILES_DIR = tmpDir.name; }); afterEach(() => { tmpDir.removeCallback(); delete process.env.FILES_DIR; }); describe('argSchema', () => { it('should use default values for image and dependencies', () => { const parsed = z.object(argSchema).parse({ code: 'console.log(1);' }); expect(parsed.image).toBe(DEFAULT_NODE_IMAGE); expect(parsed.dependencies).toEqual([]); expect(parsed.code).toBe('console.log(1);'); }); it('should accept valid custom image and dependencies', () => { const input = { image: DEFAULT_NODE_IMAGE, dependencies: [ { name: 'lodash', version: '^4.17.21' }, { name: 'axios', version: '^1.0.0' }, ], code: "console.log('hi');", }; const parsed = z.object(argSchema).parse(input); expect(parsed.image).toBe(DEFAULT_NODE_IMAGE); expect(parsed.dependencies.length).toBe(2); expect(parsed.dependencies[0]).toEqual({ name: 'lodash', version: '^4.17.21', }); expect(parsed.code).toBe("console.log('hi');"); }); }); describe('should run runJsEphemeral', () => { it('shoud run runJsEphemeral base', async () => { const result = await runJsEphemeral({ code: "console.log('Hello, world!');", dependencies: [], }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); expect(result.content[0].type).toBe('text'); if (result.content[0].type === 'text') { expect(result.content[0].text).toContain('Hello, world!'); } else { throw new Error("Expected content type to be 'text'"); } }); it('should generate telemetry', async () => { const result = await runJsEphemeral({ code: "console.log('Hello telemetry!');", dependencies: [], }); const telemetryItem = result.content.find( (c) => c.type === 'text' && c.text.startsWith('Telemetry:') ); expect(telemetryItem).toBeDefined(); if (telemetryItem?.type === 'text') { const telemetry = JSON.parse( telemetryItem.text.replace('Telemetry:\n', '') ); expect(telemetry).toHaveProperty('installTimeMs'); expect(typeof telemetry.installTimeMs).toBe('number'); expect(telemetry).toHaveProperty('runTimeMs'); expect(typeof telemetry.runTimeMs).toBe('number'); expect(telemetry).toHaveProperty('installOutput'); expect(typeof telemetry.installOutput).toBe('string'); } else { throw new Error("Expected telemetry item to be of type 'text'"); } }); it('should hang indefinitely until a timeout error gets triggered', async () => { //Simulating a 10 seconds timeout process.env.RUN_SCRIPT_TIMEOUT = '10000'; const result = await runJsEphemeral({ code: ` (async () => { console.log("🕒 Hanging for 20 seconds…"); await new Promise((resolve) => setTimeout(resolve, 20_000)); console.log("✅ Done waiting 20 seconds, exiting now."); })(); `, }); //Cleanup delete process.env.RUN_SCRIPT_TIMEOUT; const execError = result.content.find( (item) => item.type === 'text' && item.text.startsWith('Error during execution:') ); expect(execError).toBeDefined(); expect((execError as McpContentText).text).toContain('ETIMEDOUT'); const telemetryText = result.content.find( (item) => item.type === 'text' && item.text.startsWith('Telemetry:') ); expect(telemetryText).toBeDefined(); }, 20_000); it('should report execution error for runtime exceptions', async () => { const result = await runJsEphemeral({ code: `throw new Error('boom');`, }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); // should hit our "other errors" branch const execError = result.content.find( (item) => item.type === 'text' && (item as McpContentText).text.startsWith('Error during execution:') ); expect(execError).toBeDefined(); expect((execError as McpContentText).text).toContain('Error: boom'); // telemetry should still be returned const telemetryText = result.content.find( (item) => item.type === 'text' && (item as McpContentText).text.startsWith('Telemetry:') ); expect(telemetryText).toBeDefined(); }); it('should skip npm install if no dependencies are provided', async () => { const result = await runJsEphemeral({ code: "console.log('No deps');", dependencies: [], }); const telemetryItem = result.content.find( (c) => c.type === 'text' && c.text.startsWith('Telemetry:') ); expect(telemetryItem).toBeDefined(); if (telemetryItem?.type === 'text') { const telemetry = JSON.parse( telemetryItem.text.replace('Telemetry:\n', '') ); expect(telemetry.installTimeMs).toBe(0); expect(telemetry.installOutput).toBe( 'Skipped npm install (no dependencies)' ); } }); it('should generate a valid QR code resource', async () => { const result = await runJsEphemeral({ code: ` import fs from 'fs'; import qrcode from 'qrcode'; const url = 'https://nodejs.org/en'; const outputFile = './files/qrcode.png'; qrcode.toFile(outputFile, url, { type: 'png', }, function(err) { if (err) throw err; console.log('QR code saved as PNG!'); }); `, dependencies: [ { name: 'qrcode', version: '^1.5.3', }, ], }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); // Find process output const processOutput = result.content.find( (item) => item.type === 'text' && item.text.startsWith('Node.js process output:') ); expect(processOutput).toBeDefined(); expect((processOutput as McpContentText).text).toContain( 'QR code saved as PNG!' ); // Find QR image const imageResource = result.content.find( (item) => item.type === 'image' && item.mimeType === 'image/png' ); expect(imageResource).toBeDefined(); }, 15_000); it('should save a hello.txt file and return it as a resource', async () => { const result = await runJsEphemeral({ code: ` import fs from 'fs/promises'; await fs.writeFile('./files/hello test.txt', 'Hello world!'); console.log('Saved hello test.txt'); `, }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); // Find process output const processOutput = result.content.find( (item) => item.type === 'text' && item.text.startsWith('Node.js process output:') ); expect(processOutput).toBeDefined(); expect((processOutput as McpContentText).text).toContain( 'Saved hello test.txt' ); // Find file change info const changeInfo = result.content.find( (item) => item.type === 'text' && item.text.startsWith('List of changed files:') ); expect(changeInfo).toBeDefined(); expect((changeInfo as McpContentText).text).toContain( '- hello test.txt was created' ); // Find the resource const resource = result.content.find((item) => item.type === 'resource'); expect(resource).toBeDefined(); expect((resource as McpContentResource).resource.mimeType).toBe( 'text/plain' ); expect((resource as McpContentResource).resource.uri).toContain( 'hello%20test.txt' ); expect((resource as McpContentResource).resource.uri).toContain( 'file://' ); if ('text' in (resource as McpContentResource).resource) { expect((resource as McpContentTextResource).resource.text).toBe( 'hello test.txt' ); } else { throw new Error("Expected resource to have a 'text' property"); } // Find telemetry info const telemetry = result.content.find( (item) => item.type === 'text' && item.text.startsWith('Telemetry:') ); expect(telemetry).toBeDefined(); expect((telemetry as McpContentText).text).toContain('"installTimeMs"'); expect((telemetry as McpContentText).text).toContain('"runTimeMs"'); }); }, 10_000); describe('runJsEphemeral error handling', () => { it('should return an execution error and telemetry when the code throws', async () => { const result = await runJsEphemeral({ code: "throw new Error('Test error');", }); const execError = result.content.find( (item) => item.type === 'text' && item.text.startsWith('Error during execution:') ); expect(execError).toBeDefined(); expect((execError as McpContentText).text).toContain('Test error'); const telemetryText = result.content.find( (item) => item.type === 'text' && item.text.startsWith('Telemetry:') ); expect(telemetryText).toBeDefined(); }); }); describe('runJsEphemeral multiple file outputs', () => { it('should handle saving both text and JPEG files correctly', async () => { const base64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxISEhUSEhIVFhUVFRUVFRUVFRUVFRUVFRUWFhUVFRUYHSggGBolGxUVITEhJSkrLi4uFx8zODMsNygtLisBCgoKDg0OGhAQGy0lHyYtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAJ8BPgMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAAEBgIDBQABB//EADkQAAIBAgQDBgQEBQUBAAAAAAECAwQRAAUSITFBBhTiUWEHFDJxgZEjQrHB0RUjYnLw8RUz/8QAGQEAAgMBAAAAAAAAAAAAAAAAAwQBAgAF/8QAJBEAAgEEAgEFAAAAAAAAAAAAAQIDBBESITFBBRMiUYGh/9oADAMBAAIRAxEAPwD9YKKKAP/Z'; const result = await runJsEphemeral({ code: ` import fs from 'fs/promises'; await fs.writeFile('./files/foo.txt', 'Hello Foo'); const img = Buffer.from('${base64}', 'base64'); await fs.writeFile('./files/bar.jpg', img); console.log('Done writing foo.txt and bar.jpg'); `, }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); // stdout const stdout = result.content.find( (c) => c.type === 'text' && c.text.includes('Done writing foo.txt and bar.jpg') ); expect(stdout).toBeDefined(); expect((stdout as McpContentText).text).toContain( 'Done writing foo.txt and bar.jpg' ); // resource names (foo.txt and bar.jpg) const savedResourceNames = result.content .filter((c) => c.type === 'resource') .map((c) => (c as McpContentTextResource).resource.text); expect(savedResourceNames).toEqual( expect.arrayContaining(['foo.txt', 'bar.jpg']) ); // JPEG image check const jpegImage = result.content.find( (c) => c.type === 'image' && c.mimeType === 'image/jpeg' ); expect(jpegImage).toBeDefined(); }); }); describe('runJsEphemeral screenshot with Playwright', () => { it('should take a screenshot of example.com using Playwright and the Playwright image', async () => { const result = await runJsEphemeral({ code: ` import { chromium } from 'playwright'; (async () => { const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto('https://example.com'); await page.screenshot({ path: './files/example_screenshot.png' }); await browser.close(); console.log('Screenshot saved'); })(); `, dependencies: [ { name: 'playwright', version: '^1.52.0', }, ], image: 'mcr.microsoft.com/playwright:v1.52.0-noble', }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); // stdout check const output = result.content.find( (item) => item.type === 'text' && item.text.includes('Screenshot saved') ); expect(output).toBeDefined(); expect((output as McpContentText).text).toContain('Screenshot saved'); // PNG image resource check const image = result.content.find( (item) => item.type === 'image' && item.mimeType === 'image/png' ); expect(image).toBeDefined(); }, 15_000); }); // Skipping this on the CI as it requires a lot of resources // and an image that is not available in the CI environment describeIfLocal( 'runJsEphemeral generate charts', () => { it('should correctly generate a chart', async () => { const result = await runJsEphemeral({ code: ` import { ChartJSNodeCanvas } from 'chartjs-node-canvas'; import fs from 'fs'; const width = 800; const height = 400; const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height }); const data = { labels: ['January', 'February', 'March', 'April', 'May', 'June'], datasets: [{ label: 'Monthly Revenue Growth (2025)', data: [12000, 15500, 14200, 18300, 21000, 24500], backgroundColor: 'rgba(75, 192, 192, 0.6)', borderColor: 'rgba(75, 192, 192, 1)', borderWidth: 1 }] }; const config = { type: 'bar', data: data, options: { responsive: true, plugins: { legend: { display: true, position: 'top', }, title: { display: true, text: 'Monthly Revenue Growth (2025)', } }, scales: { x: { title: { display: true, text: 'Month' } }, y: { title: { display: true, text: 'Revenue (USD)' }, beginAtZero: true } } } }; async function generateChart() { const image = await chartJSNodeCanvas.renderToBuffer(config); fs.writeFileSync('./files/chart_test.png', image); console.log('Chart saved as chart.png'); } generateChart(); `, image: 'alfonsograziano/node-chartjs-canvas:latest', }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); const output = result.content.find( (item) => item.type === 'text' && typeof item.text === 'string' && item.text.includes('Chart saved as chart.png') ); expect(output).toBeDefined(); expect((output as { type: 'text'; text: string }).text).toContain( 'Chart saved as chart.png' ); const image = result.content.find( (item) => item.type === 'image' && 'mimeType' in item && item.mimeType === 'image/png' ); expect(image).toBeDefined(); expect((image as McpContentImage).mimeType).toBe('image/png'); }); it('should still be able to add new dependencies with the node-chartjs-canvas image', async () => { const result = await runJsEphemeral({ code: ` import _ from 'lodash'; console.log('_.chunk([1,2,3,4,5], 2):', _.chunk([1,2,3,4,5], 2)); `, dependencies: [{ name: 'lodash', version: '^4.17.21' }], image: 'alfonsograziano/node-chartjs-canvas:latest', }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); const output = result.content.find( (item) => item.type === 'text' && typeof item.text === 'string' && item.text.includes('[ [ 1, 2 ], [ 3, 4 ], [ 5 ] ]') ); expect(output).toBeDefined(); expect((output as McpContentText).text).toContain( '[ [ 1, 2 ], [ 3, 4 ], [ 5 ] ]' ); }); it('should generate a Mermaid sequence diagram SVG file', async () => { const result = await runJsEphemeral({ code: ` import fs from "fs"; import { run } from "@mermaid-js/mermaid-cli"; const diagramDefinition = \` sequenceDiagram participant App as Application participant KC as Keycloak participant IDP as Identity Provider %% Initial Sign-In App->>KC: "Go authenticate!" KC->>App: Redirect to Keycloak login KC->>IDP: "Which IDP? (MyGovID / EntraID)" IDP-->>KC: ID Token + Refresh Token (1 day) KC-->>App: KC Tokens (1 day) %% After 24 Hours App->>KC: Request new tokens (expired refresh token) alt KC session still active (<14 days) KC-->>App: New tokens (1 day) else KC session expired (>14 days) KC->>IDP: Redirect to reauthenticate IDP-->>KC: Fresh ID + Refresh Tokens KC-->>App: New KC Tokens (1 day) end \`; fs.writeFileSync("./files/authDiagram.mmd", diagramDefinition, "utf8"); console.log("Mermaid definition saved to authDiagram.mmd"); console.time("test"); await run("./files/authDiagram.mmd", "output.svg"); console.timeEnd("test"); console.log("Diagram generated as output.svg"); `, dependencies: [ { name: '@mermaid-js/mermaid-cli', version: '^11.4.2' }, ], image: 'alfonsograziano/node-chartjs-canvas:latest', }); // Ensure result exists expect(result).toBeDefined(); expect(result.content).toBeDefined(); // Validate Mermaid diagram creation log const logOutput = result.content.find( (item) => item.type === 'text' && item.text.includes('Diagram generated as output.svg') ); expect(logOutput).toBeDefined(); // Validate .mmd file was created const mmdFile = result.content.find( (item) => item.type === 'resource' && item.resource?.uri?.endsWith('authDiagram.mmd') ); expect(mmdFile).toBeDefined(); // Optional: Validate that the SVG file was generated const svgLog = result.content.find( (item) => item.type === 'text' && item.text.includes('Diagram generated as output.svg') ); expect(svgLog).toBeDefined(); }); }, 50_000 ); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mozicim/node-code-sandbox-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server