@scratchrun/mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/index.js +160 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @scratchrun/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for [ScratchRun](https://scratchrun.dev) — ephemeral, MicroVM-isolated code execution for AI agents.
|
|
4
|
+
|
|
5
|
+
Each call runs in a fresh hardware-isolated MicroVM. The VM is hard-purged after execution. No state, no files, nothing persists between calls.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Add to your `claude_desktop_config.json`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"scratchrun": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["-y", "@scratchrun/mcp-server"],
|
|
17
|
+
"env": {
|
|
18
|
+
"SCRATCHRUN_API_KEY": "sr_live_your_key_here"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Get an API key at [scratchrun.dev](https://scratchrun.dev).
|
|
26
|
+
|
|
27
|
+
## Tool: `scratchrun_exec`
|
|
28
|
+
|
|
29
|
+
Executes code in an ephemeral sandbox and returns stdout, stderr, exit code, and any output files.
|
|
30
|
+
|
|
31
|
+
**Parameters:**
|
|
32
|
+
|
|
33
|
+
| Parameter | Type | Required | Description |
|
|
34
|
+
|-----------|------|----------|-------------|
|
|
35
|
+
| `runtime` | `python3.12` \| `python3.11` \| `node20` \| `bash` | yes | Runtime to use |
|
|
36
|
+
| `code` | string | yes | Code to execute |
|
|
37
|
+
| `timeout_ms` | integer | no | Timeout in ms (default 10000, max 30000) |
|
|
38
|
+
| `memory_mb` | integer | no | Memory limit in MB (default 256, max 512) |
|
|
39
|
+
| `env` | object | no | Environment variables — use for secrets, not code strings |
|
|
40
|
+
| `files` | object | no | Files to write before execution (path → content) |
|
|
41
|
+
| `return_files` | string[] | no | File paths to capture after execution (returned as base64) |
|
|
42
|
+
|
|
43
|
+
**Example — run Python and return a chart:**
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"runtime": "python3.12",
|
|
48
|
+
"code": "import matplotlib.pyplot as plt\nimport numpy as np\nx = np.linspace(0, 10, 100)\nplt.plot(x, np.sin(x))\nplt.savefig('/tmp/plot.png')",
|
|
49
|
+
"return_files": ["/tmp/plot.png"]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Output files are returned as base64. Image files (PNG, JPG, SVG) are returned as MCP image content blocks and render inline in Claude.
|
|
54
|
+
|
|
55
|
+
## Isolation
|
|
56
|
+
|
|
57
|
+
- Hardware-virtualized MicroVM per execution (own kernel, not a shared-kernel container)
|
|
58
|
+
- `TerminateMicroVM` called unconditionally after every run — VM destroyed, never reused
|
|
59
|
+
- RFC 1918 + cloud metadata (`169.254.x.x`) always blocked at the network layer
|
|
60
|
+
- Read-only system filesystem via OverlayFS; `/tmp` is RAM-backed and gone with the VM
|
|
61
|
+
|
|
62
|
+
## Latency
|
|
63
|
+
|
|
64
|
+
~400ms median (warm pool). 2–8s cold start if the pool is empty.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
const API_BASE = "https://api.scratchrun.dev";
|
|
6
|
+
const API_KEY = process.env.SCRATCHRUN_API_KEY;
|
|
7
|
+
if (!API_KEY) {
|
|
8
|
+
process.stderr.write("Error: SCRATCHRUN_API_KEY environment variable is required.\n");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const server = new Server({ name: "scratchrun", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
12
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
13
|
+
tools: [
|
|
14
|
+
{
|
|
15
|
+
name: "scratchrun_exec",
|
|
16
|
+
description: "Execute code in an ephemeral, MicroVM-isolated sandbox. " +
|
|
17
|
+
"The VM is hard-purged after execution — no state, no files, nothing persists between calls. " +
|
|
18
|
+
"Full internet egress (RFC 1918 + cloud metadata always blocked). " +
|
|
19
|
+
"Use return_files to capture output artifacts (charts, CSVs, data files) as base64. " +
|
|
20
|
+
"Keep secrets out of code strings — pass them via env instead.",
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: "object",
|
|
23
|
+
required: ["runtime", "code"],
|
|
24
|
+
properties: {
|
|
25
|
+
runtime: {
|
|
26
|
+
type: "string",
|
|
27
|
+
enum: ["python3.12", "python3.11", "node20", "bash"],
|
|
28
|
+
description: "Runtime. python3.12 includes numpy, pandas, matplotlib, requests, and more.",
|
|
29
|
+
},
|
|
30
|
+
code: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Code to execute.",
|
|
33
|
+
},
|
|
34
|
+
timeout_ms: {
|
|
35
|
+
type: "integer",
|
|
36
|
+
minimum: 1000,
|
|
37
|
+
maximum: 30000,
|
|
38
|
+
default: 10000,
|
|
39
|
+
description: "Execution timeout in milliseconds. Exit code 124 signals timeout.",
|
|
40
|
+
},
|
|
41
|
+
memory_mb: {
|
|
42
|
+
type: "integer",
|
|
43
|
+
minimum: 64,
|
|
44
|
+
maximum: 512,
|
|
45
|
+
default: 256,
|
|
46
|
+
description: "Memory limit in MB.",
|
|
47
|
+
},
|
|
48
|
+
env: {
|
|
49
|
+
type: "object",
|
|
50
|
+
additionalProperties: { type: "string" },
|
|
51
|
+
description: "Environment variables injected into the sandbox. Use for secrets and config.",
|
|
52
|
+
},
|
|
53
|
+
files: {
|
|
54
|
+
type: "object",
|
|
55
|
+
additionalProperties: { type: "string" },
|
|
56
|
+
description: "Files to write before execution. Keys are absolute paths, values are file contents.",
|
|
57
|
+
},
|
|
58
|
+
return_files: {
|
|
59
|
+
type: "array",
|
|
60
|
+
items: { type: "string" },
|
|
61
|
+
description: "File paths to capture after execution and return as base64. Use for charts, CSVs, or any output artifact.",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
}));
|
|
68
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
69
|
+
if (request.params.name !== "scratchrun_exec") {
|
|
70
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
71
|
+
}
|
|
72
|
+
const args = request.params.arguments;
|
|
73
|
+
const body = {
|
|
74
|
+
runtime: args.runtime,
|
|
75
|
+
code: args.code,
|
|
76
|
+
limits: {
|
|
77
|
+
timeout_ms: args.timeout_ms ?? 10000,
|
|
78
|
+
memory_mb: args.memory_mb ?? 256,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
if (args.env)
|
|
82
|
+
body.env = args.env;
|
|
83
|
+
if (args.files)
|
|
84
|
+
body.files = args.files;
|
|
85
|
+
if (args.return_files)
|
|
86
|
+
body.return_files = args.return_files;
|
|
87
|
+
let res;
|
|
88
|
+
try {
|
|
89
|
+
res = await fetch(`${API_BASE}/v1/exec`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify(body),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: `Network error: ${String(err)}` }],
|
|
101
|
+
isError: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (res.status === 401) {
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: "Error: Invalid API key. Check SCRATCHRUN_API_KEY." }],
|
|
107
|
+
isError: true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (res.status === 429) {
|
|
111
|
+
const retryAfter = res.headers.get("Retry-After") ?? "10";
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: `Sandbox capacity temporarily unavailable. Retry after ${retryAfter}s.` }],
|
|
114
|
+
isError: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
const text = await res.text();
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: "text", text: `Error ${res.status}: ${text}` }],
|
|
121
|
+
isError: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const result = await res.json();
|
|
125
|
+
const summary = [
|
|
126
|
+
`exit_code: ${result.exit_code}`,
|
|
127
|
+
`execution_ms: ${result.execution_ms}`,
|
|
128
|
+
`state_purged: ${result.state_purged}`,
|
|
129
|
+
];
|
|
130
|
+
if (result.stdout)
|
|
131
|
+
summary.push(`\nstdout:\n${result.stdout}`);
|
|
132
|
+
if (result.stderr)
|
|
133
|
+
summary.push(`\nstderr:\n${result.stderr}`);
|
|
134
|
+
if (result.note)
|
|
135
|
+
summary.push(`\nnote: ${result.note}`);
|
|
136
|
+
const content = [{ type: "text", text: summary.join("\n") }];
|
|
137
|
+
if (result.files) {
|
|
138
|
+
const imageTypes = {
|
|
139
|
+
png: "image/png",
|
|
140
|
+
jpg: "image/jpeg",
|
|
141
|
+
jpeg: "image/jpeg",
|
|
142
|
+
gif: "image/gif",
|
|
143
|
+
svg: "image/svg+xml",
|
|
144
|
+
webp: "image/webp",
|
|
145
|
+
};
|
|
146
|
+
for (const [path, b64] of Object.entries(result.files)) {
|
|
147
|
+
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
148
|
+
if (ext in imageTypes) {
|
|
149
|
+
content.push({ type: "image", data: b64, mimeType: imageTypes[ext] });
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const decoded = Buffer.from(b64, "base64").toString("utf-8");
|
|
153
|
+
content.push({ type: "text", text: `\n--- ${path} ---\n${decoded}` });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { content };
|
|
158
|
+
});
|
|
159
|
+
const transport = new StdioServerTransport();
|
|
160
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scratchrun/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for ScratchRun — ephemeral MicroVM code execution for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"scratchrun-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "node --loader ts-node/esm src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^26.1.2",
|
|
22
|
+
"typescript": "^5.0.0"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"scratchrun",
|
|
31
|
+
"code-execution",
|
|
32
|
+
"sandbox",
|
|
33
|
+
"microvm",
|
|
34
|
+
"ai-agents"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|