@scratchrun/mcp-server 0.1.1 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +6 -3
  2. package/dist/index.js +73 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,12 +56,15 @@ Output files are returned as base64. Image files (PNG, JPG, SVG) are returned as
56
56
 
57
57
  - Hardware-virtualized MicroVM per execution (own kernel, not a shared-kernel container)
58
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
59
+ - RFC 1918 private ranges are blocked. Link-local `169.254.x.x` is **not**: an
60
+ instance-metadata service is reachable. It exposes no credentials (no IAM
61
+ role is attached), only placement data and a per-VM key.
62
+ - No in-guest hardening: code runs as root on a writable filesystem, and `/tmp`
63
+ is ordinary disk inside the VM. All of it is destroyed with the VM.
61
64
 
62
65
  ## Latency
63
66
 
64
- ~400ms median (warm pool). 2–8s cold start if the pool is empty.
67
+ ~3.4s median end to end, ~4.2s p95 for trivial code (measured over 100 sequential runs). Every call boots a fresh hardware-isolated MicroVM and destroys it afterwards; real workloads take as long as the work takes.
65
68
 
66
69
  ## License
67
70
 
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
15
15
  name: "scratchrun_exec",
16
16
  description: "Execute code in an ephemeral, MicroVM-isolated sandbox. " +
17
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). " +
18
+ "Full internet egress; RFC 1918 private ranges are blocked, link-local 169.254.x.x is not. " +
19
19
  "Use return_files to capture output artifacts (charts, CSVs, data files) as base64. " +
20
20
  "Keep secrets out of code strings — pass them via env instead.",
21
21
  inputSchema: {
@@ -84,22 +84,79 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
84
84
  body.files = args.files;
85
85
  if (args.return_files)
86
86
  body.return_files = args.return_files;
87
+ // Capacity retry.
88
+ //
89
+ // ScratchRun is currently limited to roughly one execution per second across
90
+ // the service, so a 429 under load is expected rather than exceptional. The
91
+ // agent should not have to handle that, so we absorb it here.
92
+ //
93
+ // Three deliberate choices:
94
+ // BOUNDED — an unbounded silent retry is worse than a clear error, because
95
+ // the caller has its own timeout and token budget and cannot tell
96
+ // "waiting" apart from "hung". We stop at RETRY_BUDGET_MS and
97
+ // return a structured error it can act on.
98
+ // JITTERED — full jitter, not fixed backoff. Without it, callers that
99
+ // collide once collide again on every retry, and the unlucky one
100
+ // starves. Jitter dissolves that without needing a queue.
101
+ // QUIET — no output for short waits. One progress line once the wait gets
102
+ // long, which is for the human watching the agent, not the agent.
103
+ const RETRY_BUDGET_MS = 45_000;
104
+ const BASE_DELAY_MS = 500;
105
+ const MAX_DELAY_MS = 8_000;
106
+ const NOTIFY_AFTER_MS = 2_000;
107
+ const started = Date.now();
108
+ let attempt = 0;
109
+ let waited = 0;
110
+ let notified = false;
87
111
  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
- };
112
+ for (;;) {
113
+ try {
114
+ res = await fetch(`${API_BASE}/v1/exec`, {
115
+ method: "POST",
116
+ headers: {
117
+ Authorization: `Bearer ${API_KEY}`,
118
+ "Content-Type": "application/json",
119
+ },
120
+ body: JSON.stringify(body),
121
+ });
122
+ }
123
+ catch (err) {
124
+ return {
125
+ content: [{ type: "text", text: `Network error: ${String(err)}` }],
126
+ isError: true,
127
+ };
128
+ }
129
+ // 429 = capacity. 503 = service-wide daily ceiling; retrying will not help.
130
+ if (res.status !== 429)
131
+ break;
132
+ const elapsed = Date.now() - started;
133
+ // Full jitter: pick uniformly from [0, capped exponential backoff].
134
+ const ceiling = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
135
+ let delay = Math.random() * ceiling;
136
+ // Honour a server Retry-After when it asks for longer than our own backoff.
137
+ const ra = Number(res.headers.get("Retry-After"));
138
+ if (Number.isFinite(ra) && ra * 1000 > delay)
139
+ delay = ra * 1000;
140
+ if (elapsed + delay > RETRY_BUDGET_MS) {
141
+ return {
142
+ content: [{
143
+ type: "text",
144
+ text: `Sandbox capacity unavailable after ${Math.round(elapsed / 1000)}s of retrying. ` +
145
+ `ScratchRun serves roughly one execution per second across all users, so this ` +
146
+ `means sustained contention rather than an outage. Retry in a minute, or reduce ` +
147
+ `how many executions you issue in parallel.`,
148
+ }],
149
+ isError: true,
150
+ };
151
+ }
152
+ if (!notified && elapsed + delay > NOTIFY_AFTER_MS) {
153
+ notified = true;
154
+ // stderr, not the tool result — visible to the operator, invisible to the model
155
+ console.error(`[scratchrun] waiting for sandbox capacity (~${Math.round(delay / 1000)}s)...`);
156
+ }
157
+ await new Promise((r) => setTimeout(r, delay));
158
+ waited += delay;
159
+ attempt++;
103
160
  }
104
161
  if (res.status === 401) {
105
162
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scratchrun/mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "mcpName": "dev.scratchrun/mcp",
5
5
  "description": "MCP server for ScratchRun — ephemeral MicroVM code execution for AI agents",
6
6
  "type": "module",