@scratchrun/mcp-server 0.1.0 → 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.
- package/README.md +6 -3
- package/dist/index.js +73 -16
- package/package.json +2 -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
|
|
60
|
-
-
|
|
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
|
-
~
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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