@toolsdk.ai/registry 1.0.112 → 1.0.113

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.
@@ -0,0 +1,428 @@
1
+ import { Sandbox } from "@e2b/code-interpreter";
2
+ import { getPackageConfigByKey } from "../helper";
3
+ export class MCPSandboxClient {
4
+ constructor(apiKey) {
5
+ this.sandbox = null;
6
+ this.initializing = null;
7
+ this.toolCache = new Map();
8
+ this.E2B_SANDBOX_TIMEOUT_MS = 300000;
9
+ this.TOOL_CACHE_TTL = 30 * 60 * 1000;
10
+ this.TOOL_EXECUTION_TIMEOUT = 300000;
11
+ this.lastTouchTime = null;
12
+ this.THROTTLE_DELAY_MS = 10 * 1000;
13
+ // Lifecycle and Auto-Recovery
14
+ this.createdAt = null;
15
+ this.lastUsedAt = null;
16
+ this.ttlTimer = null;
17
+ this.autoCloseOnIdleTimer = null;
18
+ this.idleCloseMs = null;
19
+ this.apiKey = apiKey || process.env.E2B_API_KEY || "e2b-api-key-placeholder";
20
+ }
21
+ // Safe initialize: ensures concurrent calls don't create duplicate sandboxes
22
+ async initialize() {
23
+ if (this.sandbox) {
24
+ // this.touch();
25
+ // console.log("[MCPSandboxClient] Sandbox already initialized");
26
+ return;
27
+ }
28
+ if (this.initializing) {
29
+ // Wait for existing initialization to complete
30
+ await this.initializing;
31
+ // this.touch();
32
+ return;
33
+ }
34
+ const initLabel = `[MCPSandboxClient] Sandbox initialization ${Date.now()}-${(Math.random() * 1000000) | 0}`;
35
+ console.time(initLabel);
36
+ this.initializing = (async () => {
37
+ try {
38
+ this.sandbox = await Sandbox.create(`mcp-sandbox-01`, {
39
+ apiKey: this.apiKey,
40
+ timeoutMs: this.E2B_SANDBOX_TIMEOUT_MS,
41
+ });
42
+ this.createdAt = Date.now();
43
+ this.touch();
44
+ // After 1-hour forced session termination
45
+ this.setupTTLTimer();
46
+ console.log("[MCPSandboxClient] Sandbox created successfully");
47
+ }
48
+ finally {
49
+ this.initializing = null;
50
+ }
51
+ })();
52
+ await this.initializing;
53
+ console.timeEnd(initLabel);
54
+ }
55
+ setupTTLTimer() {
56
+ // Clean up existing timer
57
+ if (this.ttlTimer) {
58
+ clearTimeout(this.ttlTimer);
59
+ this.ttlTimer = null;
60
+ }
61
+ // E2B supports maximum 1-hour sessions, force close at 59m 30s for safety
62
+ const TTL_MS = 60 * 60 * 1000;
63
+ const safetyMs = 30 * 1000;
64
+ const remaining = Math.max(0, TTL_MS - (Date.now() - (this.createdAt || Date.now())));
65
+ this.ttlTimer = setTimeout(async () => {
66
+ console.warn("[MCPSandboxClient] Sandbox TTL reached, closing sandbox to avoid exceeding 1 hour.");
67
+ try {
68
+ await this.kill();
69
+ }
70
+ catch (err) {
71
+ console.error("[MCPSandboxClient] Error while killing sandbox after TTL:", err);
72
+ }
73
+ }, remaining - safetyMs);
74
+ }
75
+ /**
76
+ * Update sandbox last used time
77
+ */
78
+ touch() {
79
+ const now = Date.now();
80
+ // If this is the first call, or more than 10 seconds have passed since the last call
81
+ if (!this.lastTouchTime || now - this.lastTouchTime >= this.THROTTLE_DELAY_MS) {
82
+ this.lastTouchTime = now;
83
+ this.performTouch();
84
+ }
85
+ }
86
+ /**
87
+ * Actually perform the touch operation
88
+ */
89
+ async performTouch() {
90
+ this.lastUsedAt = Date.now();
91
+ console.log(`[MCPSandboxClient] Sandbox touched at ${this.lastUsedAt}`);
92
+ // Reset E2B sandbox timeout
93
+ if (this.sandbox) {
94
+ console.log("[MCPSandboxClient] Resetting E2B sandbox timeout");
95
+ // const info = await this.sandbox.getInfo();
96
+ // console.log(`[MCPSandboxClient] E2B sandbox info: ${JSON.stringify(info, null, 2)}`);
97
+ this.sandbox.setTimeout(this.E2B_SANDBOX_TIMEOUT_MS).catch((err) => {
98
+ console.error("[MCPSandboxClient] Failed to reset E2B sandbox timeout:", err);
99
+ });
100
+ }
101
+ // Refresh timer if waiting for idle close
102
+ if (this.autoCloseOnIdleTimer && this.idleCloseMs) {
103
+ clearTimeout(this.autoCloseOnIdleTimer);
104
+ // Reschedule idle close (refresh timer)
105
+ this.autoCloseOnIdleTimer = setTimeout(async () => {
106
+ console.log("[MCPSandboxClient] Idle TTL reached, closing sandbox due to inactivity.");
107
+ try {
108
+ await this.kill();
109
+ }
110
+ catch (err) {
111
+ console.error("[MCPSandboxClient] Error while killing sandbox on idle:", err);
112
+ }
113
+ }, this.idleCloseMs);
114
+ }
115
+ }
116
+ // Schedule idle auto-close (called by PackageSO when refCount reaches 0)
117
+ scheduleIdleClose(idleMs) {
118
+ // Save idle close time for refresh
119
+ this.idleCloseMs = idleMs;
120
+ // Clear existing idle timer
121
+ if (this.autoCloseOnIdleTimer) {
122
+ clearTimeout(this.autoCloseOnIdleTimer);
123
+ }
124
+ // Set new idle timer
125
+ this.autoCloseOnIdleTimer = setTimeout(async () => {
126
+ console.log("[MCPSandboxClient] Idle TTL reached, closing sandbox due to inactivity.");
127
+ try {
128
+ await this.kill();
129
+ }
130
+ catch (err) {
131
+ console.error("[MCPSandboxClient] Error while killing sandbox on idle:", err);
132
+ }
133
+ }, idleMs);
134
+ }
135
+ // Force close and cleanup timers & cache
136
+ async kill() {
137
+ const killLabel = `[MCPSandboxClient] Sandbox closing ${Date.now()}-${(Math.random() * 1000000) | 0}`;
138
+ console.time(killLabel);
139
+ try {
140
+ if (this.ttlTimer) {
141
+ clearTimeout(this.ttlTimer);
142
+ this.ttlTimer = null;
143
+ }
144
+ if (this.autoCloseOnIdleTimer) {
145
+ clearTimeout(this.autoCloseOnIdleTimer);
146
+ this.autoCloseOnIdleTimer = null;
147
+ }
148
+ if (this.sandbox) {
149
+ try {
150
+ await this.sandbox.kill();
151
+ }
152
+ catch (err) {
153
+ // sandbox.kill may throw, log error but continue cleaning local state
154
+ console.error("[MCPSandboxClient] Error during sandbox.kill():", err);
155
+ }
156
+ this.sandbox = null;
157
+ }
158
+ else {
159
+ console.log("[MCPSandboxClient] No sandbox to close");
160
+ }
161
+ }
162
+ finally {
163
+ // clear cache and reset timestamps
164
+ this.toolCache.clear();
165
+ this.createdAt = null;
166
+ this.lastUsedAt = null;
167
+ console.timeEnd(killLabel);
168
+ }
169
+ }
170
+ // Clear cache for specific package
171
+ clearPackageCache(packageKey) {
172
+ this.toolCache.delete(packageKey);
173
+ }
174
+ // Clear all tool caches
175
+ clearAllCache() {
176
+ this.toolCache.clear();
177
+ }
178
+ async listTools(packageKey) {
179
+ var _a;
180
+ const cached = this.toolCache.get(packageKey);
181
+ if (cached) {
182
+ const now = Date.now();
183
+ if (now - cached.timestamp < this.TOOL_CACHE_TTL) {
184
+ console.log(`[MCPSandboxClient] Returning cached tools for package: ${packageKey}`);
185
+ // Refresh cache expiration time
186
+ this.toolCache.set(packageKey, {
187
+ tools: cached.tools,
188
+ timestamp: Date.now(),
189
+ });
190
+ this.touch();
191
+ return cached.tools;
192
+ }
193
+ else {
194
+ // Cache expired, remove it
195
+ this.toolCache.delete(packageKey);
196
+ }
197
+ }
198
+ if (!this.sandbox) {
199
+ throw new Error("Sandbox not initialized. Call initialize() first.");
200
+ }
201
+ const mcpServerConfig = await getPackageConfigByKey(packageKey);
202
+ const testCode = this.generateMCPTestCode(mcpServerConfig, "listTools");
203
+ const testResult = await this.runCodeWithTimeout(testCode, { language: "javascript" });
204
+ if (testResult.error) {
205
+ console.error("[MCPSandboxClient] Failed to list tools:", testResult.error);
206
+ throw new Error(`Failed to list tools: ${testResult.error}`);
207
+ }
208
+ const stdout = ((_a = testResult.logs) === null || _a === void 0 ? void 0 : _a.stdout) || [];
209
+ const last = stdout[stdout.length - 1] || "{}";
210
+ const result = JSON.parse(last);
211
+ this.toolCache.set(packageKey, {
212
+ tools: result.tools,
213
+ timestamp: Date.now(),
214
+ });
215
+ this.touch();
216
+ return result.tools;
217
+ }
218
+ async executeTool(packageKey, toolName, argumentsObj, envs) {
219
+ if (!this.sandbox) {
220
+ throw new Error("Sandbox not initialized. Call initialize() first.");
221
+ }
222
+ const execLabel = `[MCPSandboxClient] Execute tool: ${toolName} from package: ${packageKey} ${Date.now()}-${(Math.random() * 1000000) | 0}`;
223
+ console.time(execLabel);
224
+ try {
225
+ const mcpServerConfig = await getPackageConfigByKey(packageKey);
226
+ const testCode = this.generateMCPTestCode(mcpServerConfig, "executeTool", toolName, argumentsObj, envs);
227
+ const testResult = await this.runCodeWithTimeout(testCode, { language: "javascript" });
228
+ if (testResult.error) {
229
+ console.error("[MCPSandboxClient] Failed to execute tool:", testResult.error);
230
+ throw new Error(`Failed to execute tool: ${testResult.error}`);
231
+ }
232
+ // Handle stderr output, log if there are error messages
233
+ if (testResult.logs.stderr && testResult.logs.stderr.length > 0) {
234
+ const stderrOutput = testResult.logs.stderr.join("\n");
235
+ console.error("[MCPSandboxClient] Tool execution stderr output:", stderrOutput);
236
+ }
237
+ const result = JSON.parse(testResult.logs.stdout[testResult.logs.stdout.length - 1] || "{}");
238
+ if (result.isError) {
239
+ console.error("[MCPSandboxClient] Tool execution error:", result.errorMessage);
240
+ throw new Error(result.errorMessage);
241
+ }
242
+ this.touch();
243
+ return result;
244
+ }
245
+ catch (err) {
246
+ if (err instanceof Error &&
247
+ (err.message.includes("sandbox was not found") || err.message.includes("terminated"))) {
248
+ console.warn("[MCPSandboxClient] Sandbox not found, cleaning up state and reinitializing");
249
+ await this.kill();
250
+ throw new Error("Sandbox was not found. Please retry the operation.");
251
+ }
252
+ console.error("[MCPSandboxClient] Error executing tool:", err);
253
+ throw err;
254
+ }
255
+ finally {
256
+ console.timeEnd(execLabel);
257
+ }
258
+ }
259
+ // Add timeout protection to sandbox.runCode, kill sandbox on timeout
260
+ async runCodeWithTimeout(code, options) {
261
+ if (!this.sandbox)
262
+ throw new Error("Sandbox not initialized.");
263
+ const execPromise = this.sandbox.runCode(code, options);
264
+ const timeoutMs = this.TOOL_EXECUTION_TIMEOUT;
265
+ let timeoutHandle = null;
266
+ const timeoutPromise = new Promise((_, reject) => {
267
+ timeoutHandle = setTimeout(async () => {
268
+ const msg = `[MCPSandboxClient] runCode timeout after ${timeoutMs}ms`;
269
+ console.error(msg);
270
+ // After timeout, try to force kill sandbox to recycle resources and avoid subsequent calls using this stuck instance
271
+ try {
272
+ await this.kill();
273
+ }
274
+ catch (err) {
275
+ console.error("[MCPSandboxClient] Error killing sandbox after run timeout:", err);
276
+ }
277
+ reject(new Error(msg));
278
+ }, timeoutMs);
279
+ });
280
+ const label = `[MCPSandboxClient] Tool execution time ${Date.now()}-${(Math.random() * 1000000) | 0}`;
281
+ try {
282
+ console.time(label);
283
+ const result = (await Promise.race([execPromise, timeoutPromise]));
284
+ return result;
285
+ }
286
+ catch (err) {
287
+ console.error("[MCPSandboxClient] runCodeWithTimeout error:", err);
288
+ throw err;
289
+ }
290
+ finally {
291
+ if (timeoutHandle)
292
+ clearTimeout(timeoutHandle);
293
+ console.timeEnd(label);
294
+ }
295
+ }
296
+ generateMCPTestCode(mcpServerConfig, operation, toolName, argumentsObj, envs) {
297
+ const commonCode = `
298
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
299
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
300
+ import fs from "node:fs";
301
+
302
+ function getPackageJSON(packageName) {
303
+ const packageJSONFilePath = \`/home/node_modules/\${packageName}/package.json\`;
304
+
305
+ if (!fs.existsSync(packageJSONFilePath)) {
306
+ throw new Error(\`Package '\${packageName}' not found in node_modules.\`);
307
+ }
308
+
309
+ const packageJSONStr = fs.readFileSync(packageJSONFilePath, "utf8");
310
+ const packageJSON = JSON.parse(packageJSONStr);
311
+ return packageJSON;
312
+ }
313
+
314
+ async function runMCP() {
315
+ let client;
316
+ try {
317
+ const packageName = "${mcpServerConfig.packageName}";
318
+ const packageJSON = getPackageJSON(packageName);
319
+
320
+ let binPath;
321
+ if (typeof packageJSON.bin === "string") {
322
+ binPath = packageJSON.bin;
323
+ } else if (typeof packageJSON.bin === "object") {
324
+ binPath = Object.values(packageJSON.bin)[0];
325
+ } else {
326
+ binPath = packageJSON.main;
327
+ }
328
+
329
+ if (!binPath) {
330
+ throw new Error(\`Package \${packageName} does not have a valid bin path in package.json.\`);
331
+ }
332
+
333
+ const binFilePath = \`/home/node_modules/\${packageName}/\${binPath}\`;
334
+ const binArgs = ${JSON.stringify(mcpServerConfig.binArgs || [])};
335
+
336
+ const transport = new StdioClientTransport({
337
+ command: "node",
338
+ args: [binFilePath, ...binArgs],
339
+ env: {
340
+ ...(Object.fromEntries(
341
+ Object.entries(process.env).filter(([_, v]) => v !== undefined)
342
+ ) as Record<string, string>),
343
+ ${this.generateEnvVariables(mcpServerConfig.env, envs)}
344
+ },
345
+ });
346
+
347
+ client = new Client(
348
+ {
349
+ name: "mcp-server-${mcpServerConfig.packageName}-client",
350
+ version: "1.0.0",
351
+ },
352
+ {
353
+ capabilities: {
354
+ tools: {},
355
+ },
356
+ },
357
+ );
358
+
359
+ await client.connect(transport);
360
+ `;
361
+ if (operation === "listTools") {
362
+ return `${commonCode}
363
+ const toolsObj = await client.listTools();
364
+
365
+ const result = {
366
+ toolCount: toolsObj.tools.length,
367
+ tools: toolsObj.tools
368
+ };
369
+
370
+ console.log(JSON.stringify(result));
371
+ if (client) {
372
+ client.close();
373
+ }
374
+ return;
375
+ } catch (error) {
376
+ console.error("Error in MCP test:", error);
377
+ if (client) {
378
+ client.close();
379
+ }
380
+ throw error;
381
+ }
382
+ }
383
+
384
+ runMCP();
385
+ `;
386
+ }
387
+ else {
388
+ return `${commonCode}
389
+
390
+ const result = await client.callTool({
391
+ name: "${toolName}",
392
+ arguments: ${JSON.stringify(argumentsObj)}
393
+ });
394
+
395
+ console.log(JSON.stringify(result))
396
+ if (client) {
397
+ client.close();
398
+ }
399
+ return;
400
+ } catch (error) {
401
+ if (client) {
402
+ client.close();
403
+ }
404
+ console.log(JSON.stringify({
405
+ result: null,
406
+ isError: true,
407
+ errorMessage: error.message
408
+ }));
409
+ }
410
+ }
411
+
412
+ runMCP();
413
+ `;
414
+ }
415
+ }
416
+ generateEnvVariables(env, realEnvs) {
417
+ if (!env) {
418
+ return "";
419
+ }
420
+ const envEntries = Object.entries(env).map(([key, _]) => {
421
+ if (realEnvs === null || realEnvs === void 0 ? void 0 : realEnvs[key]) {
422
+ return `${JSON.stringify(key)}: ${JSON.stringify(realEnvs[key])}`;
423
+ }
424
+ return `${JSON.stringify(key)}: "mock_value"`;
425
+ });
426
+ return envEntries.join(",\n ");
427
+ }
428
+ }