@scalebox/mcp 0.0.1
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/dist/cli.d.ts +7 -0
- package/dist/cli.js +34 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/load-guide.d.ts +3 -0
- package/dist/load-guide.js +15 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +331 -0
- package/package.json +39 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scalebox MCP Server CLI.
|
|
4
|
+
* Usage: npx -y @scalebox-dev/scalebox-mcp --api-key YOUR_API_KEY [--api-url URL]
|
|
5
|
+
* Or set env: SCALEBOX_API_KEY, SCALEBOX_API_URL
|
|
6
|
+
*/
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
let apiKey = process.env.SCALEBOX_API_KEY || process.env.SBX_API_KEY;
|
|
9
|
+
let apiUrl = process.env.SCALEBOX_API_URL;
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
if (args[i] === '--api-key' && args[i + 1]) {
|
|
12
|
+
apiKey = args[i + 1];
|
|
13
|
+
i++;
|
|
14
|
+
}
|
|
15
|
+
else if (args[i] === '--api-url' && args[i + 1]) {
|
|
16
|
+
apiUrl = args[i + 1];
|
|
17
|
+
i++;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
console.error('Error: API key is required. Set SCALEBOX_API_KEY or use --api-key YOUR_API_KEY');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
process.env.SCALEBOX_API_KEY = apiKey;
|
|
25
|
+
if (apiUrl) {
|
|
26
|
+
process.env.SCALEBOX_API_URL = apiUrl;
|
|
27
|
+
}
|
|
28
|
+
import('./server.js')
|
|
29
|
+
.then(({ runServer }) => runServer())
|
|
30
|
+
.catch((err) => {
|
|
31
|
+
console.error('Fatal error:', err);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load bundled Markdown guides from `packages/js/guides/` (copied from repo root `guides/` via sync-guides).
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
export const GUIDE_IDS = ['doc-index', 'api', 'js-sdk', 'python-sdk'];
|
|
8
|
+
export function loadGuide(id) {
|
|
9
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const file = path.join(dir, '..', 'guides', `${id}.md`);
|
|
11
|
+
if (!existsSync(file)) {
|
|
12
|
+
throw new Error(`Guide "${id}" not found at ${file}`);
|
|
13
|
+
}
|
|
14
|
+
return readFileSync(file, 'utf8');
|
|
15
|
+
}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scalebox MCP Server (stdio).
|
|
3
|
+
* Tools: create_sandbox, execute_code, list_files, read_file, write_file,
|
|
4
|
+
* install_packages, run_command, remove_file, make_dir, destroy_context, get_sandbox_info
|
|
5
|
+
*/
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { CodeInterpreter } from '@scalebox/sdk';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
/** Max length per stdout/stderr stream to keep MCP responses from overwhelming clients. */
|
|
11
|
+
const MAX_COMMAND_IO_LENGTH = 262144;
|
|
12
|
+
const sandboxes = new Map();
|
|
13
|
+
const contexts = new Map();
|
|
14
|
+
function getApiKey() {
|
|
15
|
+
const key = process.env.SCALEBOX_API_KEY || process.env.SBX_API_KEY;
|
|
16
|
+
if (!key) {
|
|
17
|
+
throw new Error('API key required. Set SCALEBOX_API_KEY or use --api-key');
|
|
18
|
+
}
|
|
19
|
+
return key;
|
|
20
|
+
}
|
|
21
|
+
function getSandbox(contextId) {
|
|
22
|
+
const sbx = sandboxes.get(contextId);
|
|
23
|
+
if (!sbx)
|
|
24
|
+
throw new Error(`Context ${contextId} not found. Call create_sandbox first.`);
|
|
25
|
+
return sbx;
|
|
26
|
+
}
|
|
27
|
+
function getContext(contextId) {
|
|
28
|
+
return contexts.get(contextId);
|
|
29
|
+
}
|
|
30
|
+
function jsonContent(obj) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function errContent(msg, isError = true) {
|
|
36
|
+
return { content: [{ type: 'text', text: msg }], isError };
|
|
37
|
+
}
|
|
38
|
+
/** Resolve Sandbox via the public SDK API (files, commands, etc.); do not touch private Interpreter fields. */
|
|
39
|
+
function getSandboxFor(interpreter) {
|
|
40
|
+
return interpreter.getSandbox();
|
|
41
|
+
}
|
|
42
|
+
/** Truncate long command output and record whether truncation occurred. */
|
|
43
|
+
function clipCommandIo(text) {
|
|
44
|
+
if (text.length <= MAX_COMMAND_IO_LENGTH) {
|
|
45
|
+
return { text, truncated: false };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
text: `${text.slice(0, MAX_COMMAND_IO_LENGTH)}\n... [truncated, max ${MAX_COMMAND_IO_LENGTH} chars per stream]`,
|
|
49
|
+
truncated: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const server = new McpServer({ name: 'scalebox-mcp', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
53
|
+
// Tool definitions with Zod input schemas (required by McpServer.registerTool)
|
|
54
|
+
const TOOLS = [
|
|
55
|
+
{
|
|
56
|
+
name: 'create_sandbox',
|
|
57
|
+
description: 'Create a sandbox and code execution context for stateful runs',
|
|
58
|
+
inputSchema: z.object({
|
|
59
|
+
language: z.string().describe('Language: python, nodejs, r, typescript, java, bash'),
|
|
60
|
+
cwd: z.string().optional().describe('Working directory (default /home/user)'),
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'execute_code',
|
|
65
|
+
description: 'Execute code in the sandbox',
|
|
66
|
+
inputSchema: z.object({
|
|
67
|
+
code: z.string().describe('Code to run'),
|
|
68
|
+
context_id: z.string().describe('Context ID (required)'),
|
|
69
|
+
language: z.string().optional().describe('Language (default python)'),
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'list_files',
|
|
74
|
+
description: 'List files and directories at path',
|
|
75
|
+
inputSchema: z.object({
|
|
76
|
+
path: z.string().describe('Path to list'),
|
|
77
|
+
context_id: z.string().describe('Context ID (required)'),
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'read_file',
|
|
82
|
+
description: 'Read file content from the sandbox',
|
|
83
|
+
inputSchema: z.object({
|
|
84
|
+
file_path: z.string().describe('File path'),
|
|
85
|
+
context_id: z.string().describe('Context ID (required)'),
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'write_file',
|
|
90
|
+
description: 'Write file content to the sandbox',
|
|
91
|
+
inputSchema: z.object({
|
|
92
|
+
file_path: z.string().describe('File path'),
|
|
93
|
+
content: z.string().describe('File content'),
|
|
94
|
+
context_id: z.string().describe('Context ID (required)'),
|
|
95
|
+
}),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'install_packages',
|
|
99
|
+
description: 'Install Python packages (pip)',
|
|
100
|
+
inputSchema: z.object({
|
|
101
|
+
packages: z.array(z.string()).describe('Package names to install'),
|
|
102
|
+
context_id: z.string().describe('Context ID (required)'),
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'run_command',
|
|
107
|
+
description: 'Run a shell command in the sandbox',
|
|
108
|
+
inputSchema: z.object({
|
|
109
|
+
command: z.string().describe('Command to run'),
|
|
110
|
+
context_id: z.string().describe('Context ID (required)'),
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'remove_file',
|
|
115
|
+
description: 'Remove file or directory in the sandbox',
|
|
116
|
+
inputSchema: z.object({
|
|
117
|
+
path: z.string().describe('Path to remove'),
|
|
118
|
+
context_id: z.string().describe('Context ID (required)'),
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'make_dir',
|
|
123
|
+
description: 'Create directory in the sandbox',
|
|
124
|
+
inputSchema: z.object({
|
|
125
|
+
path: z.string().describe('Directory path to create'),
|
|
126
|
+
context_id: z.string().describe('Context ID (required)'),
|
|
127
|
+
}),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'destroy_context',
|
|
131
|
+
description: 'Destroy a context and its sandbox',
|
|
132
|
+
inputSchema: z.object({
|
|
133
|
+
context_id: z.string().describe('Context ID to destroy'),
|
|
134
|
+
}),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'get_sandbox_info',
|
|
138
|
+
description: 'List all contexts and sandboxes',
|
|
139
|
+
inputSchema: z.object({}),
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
async function handleTool(name, a) {
|
|
143
|
+
switch (name) {
|
|
144
|
+
case 'create_sandbox': {
|
|
145
|
+
const language = (a.language ? String(a.language) : 'python');
|
|
146
|
+
const cwd = String(a.cwd ?? '/home/user');
|
|
147
|
+
const apiKey = getApiKey();
|
|
148
|
+
const interpreter = await CodeInterpreter.create({
|
|
149
|
+
apiKey,
|
|
150
|
+
apiUrl: process.env.SCALEBOX_API_URL,
|
|
151
|
+
});
|
|
152
|
+
const context = await interpreter.createCodeContext({ language, cwd });
|
|
153
|
+
sandboxes.set(context.id, interpreter);
|
|
154
|
+
contexts.set(context.id, context);
|
|
155
|
+
return jsonContent({
|
|
156
|
+
success: true,
|
|
157
|
+
context_id: context.id,
|
|
158
|
+
language: context.language,
|
|
159
|
+
cwd: context.cwd,
|
|
160
|
+
sandbox_id: getSandboxFor(interpreter).sandboxId,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
case 'execute_code': {
|
|
164
|
+
const code = String(a.code);
|
|
165
|
+
const contextId = String(a.context_id);
|
|
166
|
+
const language = (a.language ? String(a.language) : 'python');
|
|
167
|
+
const sbx = getSandbox(contextId);
|
|
168
|
+
const context = getContext(contextId);
|
|
169
|
+
if (!context)
|
|
170
|
+
return errContent(`Context ${contextId} not found`);
|
|
171
|
+
const execution = await sbx.runCode(code, { language, context });
|
|
172
|
+
const result = {
|
|
173
|
+
success: true,
|
|
174
|
+
language,
|
|
175
|
+
context_id: contextId,
|
|
176
|
+
stdout: execution.stdout ?? execution.logs?.stdout ?? '',
|
|
177
|
+
stderr: execution.stderr ?? execution.logs?.stderr ?? '',
|
|
178
|
+
};
|
|
179
|
+
if (execution.results?.length) {
|
|
180
|
+
result.results = execution.results.map((r) => ({
|
|
181
|
+
text: r.text,
|
|
182
|
+
data: r.data,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
return jsonContent(result);
|
|
186
|
+
}
|
|
187
|
+
case 'list_files': {
|
|
188
|
+
const path = String(a.path);
|
|
189
|
+
const contextId = String(a.context_id);
|
|
190
|
+
const sbx = getSandbox(contextId);
|
|
191
|
+
const sandbox = getSandboxFor(sbx);
|
|
192
|
+
const files = await sandbox.files.list(path);
|
|
193
|
+
const items = files.map((f) => ({
|
|
194
|
+
name: f.name,
|
|
195
|
+
type: f.type,
|
|
196
|
+
size: f.size ?? 0,
|
|
197
|
+
}));
|
|
198
|
+
return jsonContent({ success: true, path, total: items.length, items });
|
|
199
|
+
}
|
|
200
|
+
case 'read_file': {
|
|
201
|
+
const filePath = String(a.file_path);
|
|
202
|
+
const contextId = String(a.context_id);
|
|
203
|
+
const sbx = getSandbox(contextId);
|
|
204
|
+
const sandbox = getSandboxFor(sbx);
|
|
205
|
+
const content = await sandbox.files.read(filePath);
|
|
206
|
+
return jsonContent({ success: true, path: filePath, content });
|
|
207
|
+
}
|
|
208
|
+
case 'write_file': {
|
|
209
|
+
const filePath = String(a.file_path);
|
|
210
|
+
const content = String(a.content);
|
|
211
|
+
const contextId = String(a.context_id);
|
|
212
|
+
const sbx = getSandbox(contextId);
|
|
213
|
+
const sandbox = getSandboxFor(sbx);
|
|
214
|
+
await sandbox.files.write(filePath, content);
|
|
215
|
+
return jsonContent({ success: true, path: filePath, size: content.length });
|
|
216
|
+
}
|
|
217
|
+
case 'install_packages': {
|
|
218
|
+
const packages = Array.isArray(a.packages) ? a.packages : [];
|
|
219
|
+
const contextId = String(a.context_id);
|
|
220
|
+
const sbx = getSandbox(contextId);
|
|
221
|
+
const sandbox = getSandboxFor(sbx);
|
|
222
|
+
const cmd = `pip install ${packages.join(' ')} -q`;
|
|
223
|
+
const result = await sandbox.commands.run(cmd);
|
|
224
|
+
const stdoutClip = clipCommandIo(result.stdout ?? '');
|
|
225
|
+
const stderrClip = clipCommandIo(result.stderr ?? '');
|
|
226
|
+
return jsonContent({
|
|
227
|
+
success: result.exitCode === 0,
|
|
228
|
+
exit_code: result.exitCode,
|
|
229
|
+
packages,
|
|
230
|
+
stdout: stdoutClip.text,
|
|
231
|
+
stderr: stderrClip.text,
|
|
232
|
+
stdout_truncated: stdoutClip.truncated,
|
|
233
|
+
stderr_truncated: stderrClip.truncated,
|
|
234
|
+
...(result.error ? { command_error: String(result.error.message ?? result.error) } : {}),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
case 'run_command': {
|
|
238
|
+
const command = String(a.command);
|
|
239
|
+
const contextId = String(a.context_id);
|
|
240
|
+
const sbx = getSandbox(contextId);
|
|
241
|
+
const sandbox = getSandboxFor(sbx);
|
|
242
|
+
const result = await sandbox.commands.run(command);
|
|
243
|
+
const stdoutClip = clipCommandIo(result.stdout ?? '');
|
|
244
|
+
const stderrClip = clipCommandIo(result.stderr ?? '');
|
|
245
|
+
return jsonContent({
|
|
246
|
+
success: result.exitCode === 0,
|
|
247
|
+
command,
|
|
248
|
+
exit_code: result.exitCode,
|
|
249
|
+
stdout: stdoutClip.text,
|
|
250
|
+
stderr: stderrClip.text,
|
|
251
|
+
stdout_truncated: stdoutClip.truncated,
|
|
252
|
+
stderr_truncated: stderrClip.truncated,
|
|
253
|
+
...(result.error ? { command_error: String(result.error.message ?? result.error) } : {}),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
case 'remove_file': {
|
|
257
|
+
const path = String(a.path);
|
|
258
|
+
const contextId = String(a.context_id);
|
|
259
|
+
const sbx = getSandbox(contextId);
|
|
260
|
+
const sandbox = getSandboxFor(sbx);
|
|
261
|
+
await sandbox.files.remove(path);
|
|
262
|
+
return jsonContent({ success: true, message: `Removed: ${path}` });
|
|
263
|
+
}
|
|
264
|
+
case 'make_dir': {
|
|
265
|
+
const path = String(a.path);
|
|
266
|
+
const contextId = String(a.context_id);
|
|
267
|
+
const sbx = getSandbox(contextId);
|
|
268
|
+
const sandbox = getSandboxFor(sbx);
|
|
269
|
+
await sandbox.files.makeDir(path);
|
|
270
|
+
return jsonContent({ success: true, message: `Created directory: ${path}` });
|
|
271
|
+
}
|
|
272
|
+
case 'destroy_context': {
|
|
273
|
+
const contextId = String(a.context_id);
|
|
274
|
+
const context = contexts.get(contextId);
|
|
275
|
+
if (!context)
|
|
276
|
+
return errContent(`Context ${contextId} not found`);
|
|
277
|
+
const sbx = sandboxes.get(contextId);
|
|
278
|
+
if (sbx) {
|
|
279
|
+
await sbx.destroyContext(context);
|
|
280
|
+
sandboxes.delete(contextId);
|
|
281
|
+
}
|
|
282
|
+
contexts.delete(contextId);
|
|
283
|
+
return jsonContent({ success: true, message: `Context destroyed: ${contextId}` });
|
|
284
|
+
}
|
|
285
|
+
case 'get_sandbox_info': {
|
|
286
|
+
const contextList = [];
|
|
287
|
+
for (const [ctxId, ctx] of contexts) {
|
|
288
|
+
const sbx = sandboxes.get(ctxId);
|
|
289
|
+
let sandboxId = 'unknown';
|
|
290
|
+
if (sbx) {
|
|
291
|
+
try {
|
|
292
|
+
sandboxId = getSandboxFor(sbx).sandboxId ?? sandboxId;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// ignore
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
contextList.push({
|
|
299
|
+
context_id: ctxId,
|
|
300
|
+
sandbox_id: sandboxId,
|
|
301
|
+
language: ctx.language ?? 'unknown',
|
|
302
|
+
cwd: ctx.cwd ?? 'unknown',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return jsonContent({
|
|
306
|
+
strategy: 'Context ID is the single key for sandbox/context',
|
|
307
|
+
total_contexts: contexts.size,
|
|
308
|
+
total_sandboxes: sandboxes.size,
|
|
309
|
+
contexts: contextList,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
default:
|
|
313
|
+
return errContent(`Unknown tool: ${name}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const t of TOOLS) {
|
|
317
|
+
server.registerTool(t.name, { description: t.description, inputSchema: t.inputSchema }, async (args) => {
|
|
318
|
+
try {
|
|
319
|
+
return await handleTool(t.name, args ?? {});
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
323
|
+
return errContent(msg, true);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
export async function runServer() {
|
|
328
|
+
const transport = new StdioServerTransport();
|
|
329
|
+
await server.connect(transport);
|
|
330
|
+
console.error('Scalebox MCP Server running on stdio');
|
|
331
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scalebox/mcp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Scalebox MCP Server - run via npx",
|
|
5
|
+
"homepage": "https://github.com/scalebox-dev/scalebox-mcp#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/scalebox-dev/scalebox-mcp/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/scalebox-dev/scalebox-mcp.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"bin": {
|
|
18
|
+
"scalebox-mcp": "dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
26
|
+
"@scalebox/sdk": "^3.3.0",
|
|
27
|
+
"zod": "^3.23.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"typescript": "^5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
]
|
|
39
|
+
}
|