@zackbart/connecta 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.
Files changed (116) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +249 -0
  4. package/SECURITY.md +25 -0
  5. package/assets/connecta-clay-hero.png +0 -0
  6. package/dist/activity.d.ts +82 -0
  7. package/dist/activity.d.ts.map +1 -0
  8. package/dist/activity.js +51 -0
  9. package/dist/activity.js.map +1 -0
  10. package/dist/auth/bearer.d.ts +10 -0
  11. package/dist/auth/bearer.d.ts.map +1 -0
  12. package/dist/auth/bearer.js +47 -0
  13. package/dist/auth/bearer.js.map +1 -0
  14. package/dist/auth/clerk.d.ts +20 -0
  15. package/dist/auth/clerk.d.ts.map +1 -0
  16. package/dist/auth/clerk.js +167 -0
  17. package/dist/auth/clerk.js.map +1 -0
  18. package/dist/auth/downstream-oauth.d.ts +78 -0
  19. package/dist/auth/downstream-oauth.d.ts.map +1 -0
  20. package/dist/auth/downstream-oauth.js +180 -0
  21. package/dist/auth/downstream-oauth.js.map +1 -0
  22. package/dist/catalog.d.ts +11 -0
  23. package/dist/catalog.d.ts.map +1 -0
  24. package/dist/catalog.js +144 -0
  25. package/dist/catalog.js.map +1 -0
  26. package/dist/connectors/api.d.ts +34 -0
  27. package/dist/connectors/api.d.ts.map +1 -0
  28. package/dist/connectors/api.js +36 -0
  29. package/dist/connectors/api.js.map +1 -0
  30. package/dist/connectors/remote-mcp.d.ts +33 -0
  31. package/dist/connectors/remote-mcp.d.ts.map +1 -0
  32. package/dist/connectors/remote-mcp.js +255 -0
  33. package/dist/connectors/remote-mcp.js.map +1 -0
  34. package/dist/credentials.d.ts +33 -0
  35. package/dist/credentials.d.ts.map +1 -0
  36. package/dist/credentials.js +177 -0
  37. package/dist/credentials.js.map +1 -0
  38. package/dist/execute.d.ts +40 -0
  39. package/dist/execute.d.ts.map +1 -0
  40. package/dist/execute.js +424 -0
  41. package/dist/execute.js.map +1 -0
  42. package/dist/executors/quickjs.d.ts +26 -0
  43. package/dist/executors/quickjs.d.ts.map +1 -0
  44. package/dist/executors/quickjs.js +267 -0
  45. package/dist/executors/quickjs.js.map +1 -0
  46. package/dist/favicon.d.ts +2 -0
  47. package/dist/favicon.d.ts.map +1 -0
  48. package/dist/favicon.js +49 -0
  49. package/dist/favicon.js.map +1 -0
  50. package/dist/index.d.ts +85 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +73 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/mcp-result.d.ts +9 -0
  55. package/dist/mcp-result.d.ts.map +1 -0
  56. package/dist/mcp-result.js +35 -0
  57. package/dist/mcp-result.js.map +1 -0
  58. package/dist/meta-tools.d.ts +100 -0
  59. package/dist/meta-tools.d.ts.map +1 -0
  60. package/dist/meta-tools.js +797 -0
  61. package/dist/meta-tools.js.map +1 -0
  62. package/dist/node.d.ts +9 -0
  63. package/dist/node.d.ts.map +1 -0
  64. package/dist/node.js +57 -0
  65. package/dist/node.js.map +1 -0
  66. package/dist/registry.d.ts +78 -0
  67. package/dist/registry.d.ts.map +1 -0
  68. package/dist/registry.js +299 -0
  69. package/dist/registry.js.map +1 -0
  70. package/dist/server.d.ts +31 -0
  71. package/dist/server.d.ts.map +1 -0
  72. package/dist/server.js +543 -0
  73. package/dist/server.js.map +1 -0
  74. package/dist/skills.d.ts +8 -0
  75. package/dist/skills.d.ts.map +1 -0
  76. package/dist/skills.js +60 -0
  77. package/dist/skills.js.map +1 -0
  78. package/dist/storage/file.d.ts +8 -0
  79. package/dist/storage/file.d.ts.map +1 -0
  80. package/dist/storage/file.js +53 -0
  81. package/dist/storage/file.js.map +1 -0
  82. package/dist/storage/memory.d.ts +4 -0
  83. package/dist/storage/memory.d.ts.map +1 -0
  84. package/dist/storage/memory.js +29 -0
  85. package/dist/storage/memory.js.map +1 -0
  86. package/dist/types.d.ts +219 -0
  87. package/dist/types.d.ts.map +1 -0
  88. package/dist/types.js +3 -0
  89. package/dist/types.js.map +1 -0
  90. package/dist/ui.d.ts +91 -0
  91. package/dist/ui.d.ts.map +1 -0
  92. package/dist/ui.js +1031 -0
  93. package/dist/ui.js.map +1 -0
  94. package/package.json +96 -0
  95. package/src/activity.ts +144 -0
  96. package/src/auth/bearer.ts +54 -0
  97. package/src/auth/clerk.ts +226 -0
  98. package/src/auth/downstream-oauth.ts +202 -0
  99. package/src/catalog.ts +166 -0
  100. package/src/connectors/api.ts +80 -0
  101. package/src/connectors/remote-mcp.ts +324 -0
  102. package/src/credentials.ts +259 -0
  103. package/src/execute.ts +550 -0
  104. package/src/executors/quickjs.ts +321 -0
  105. package/src/favicon.ts +53 -0
  106. package/src/index.ts +197 -0
  107. package/src/mcp-result.ts +43 -0
  108. package/src/meta-tools.ts +1120 -0
  109. package/src/node.ts +62 -0
  110. package/src/registry.ts +400 -0
  111. package/src/server.ts +758 -0
  112. package/src/skills.ts +63 -0
  113. package/src/storage/file.ts +62 -0
  114. package/src/storage/memory.ts +34 -0
  115. package/src/types.ts +254 -0
  116. package/src/ui.ts +1121 -0
package/src/execute.ts ADDED
@@ -0,0 +1,550 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
4
+ import { recordToolActivity, type ActivityRequestContext } from "./activity.js";
5
+ import { errorResult, jsonResult, type ToolResult } from "./meta-tools.js";
6
+ import { unwrapMcpResult } from "./mcp-result.js";
7
+ import type { Registry } from "./registry.js";
8
+ import type {
9
+ Connector,
10
+ Executor,
11
+ ExecutorProvider,
12
+ Logger,
13
+ ToolDef,
14
+ } from "./types.js";
15
+
16
+ /** ~6k tokens. Sandbox code should filter data down before returning. */
17
+ const MAX_RESULT_CHARS = 24_000;
18
+ const MAX_LOG_CHARS = 4_000;
19
+ /** Keep one model-written program from amplifying into an unbounded fan-out. */
20
+ export const EXECUTE_MAX_HOST_CALLS = 20;
21
+ export const EXECUTE_MAX_BATCH_CALLS = 10;
22
+ export const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
23
+
24
+ // deno-fmt-ignore
25
+ const RESERVED = new Set([
26
+ "break",
27
+ "case",
28
+ "catch",
29
+ "class",
30
+ "const",
31
+ "continue",
32
+ "debugger",
33
+ "default",
34
+ "delete",
35
+ "do",
36
+ "else",
37
+ "enum",
38
+ "export",
39
+ "extends",
40
+ "false",
41
+ "finally",
42
+ "for",
43
+ "function",
44
+ "if",
45
+ "import",
46
+ "in",
47
+ "instanceof",
48
+ "let",
49
+ "new",
50
+ "null",
51
+ "return",
52
+ "static",
53
+ "super",
54
+ "switch",
55
+ "this",
56
+ "throw",
57
+ "true",
58
+ "try",
59
+ "typeof",
60
+ "var",
61
+ "void",
62
+ "while",
63
+ "with",
64
+ "yield",
65
+ "await",
66
+ "async",
67
+ ]);
68
+
69
+ /** Convert a connector/tool name into a valid JS identifier. */
70
+ export function sanitizeIdentifier(name: string): string {
71
+ let id = name.replace(/[^A-Za-z0-9_$]/g, "_");
72
+ if (/^[0-9]/.test(id)) id = `_${id}`;
73
+ if (RESERVED.has(id)) id = `${id}_`;
74
+ return id;
75
+ }
76
+
77
+ function msg(err: unknown): string {
78
+ return err instanceof Error ? err.message : String(err);
79
+ }
80
+
81
+ /**
82
+ * Unwrap an MCP CallToolResult so sandbox code sees plain values:
83
+ * isError throws (a real exception the code can catch), structuredContent
84
+ * wins when present, all-text content is JSON.parsed when possible.
85
+ * Non-MCP connectors already return plain values.
86
+ */
87
+ export function unwrapForSandbox(
88
+ kind: Connector["kind"],
89
+ result: unknown,
90
+ ): unknown {
91
+ return unwrapMcpResult(kind, result);
92
+ }
93
+
94
+ /**
95
+ * Expose the registry to a sandbox: one provider (global namespace) per
96
+ * connector with a function per tool, plus `connecta.call(address, args)` as
97
+ * the raw-address escape hatch. Broken connectors are skipped, not fatal.
98
+ */
99
+ export async function buildSandboxProviders(
100
+ registry: Registry,
101
+ baseUrl: string,
102
+ logger: Logger,
103
+ activity?: ActivityRequestContext,
104
+ limits: {
105
+ signal?: AbortSignal;
106
+ maxHostCalls?: number;
107
+ hostCallTimeoutMs?: number;
108
+ } = {},
109
+ ): Promise<ExecutorProvider[]> {
110
+ const providers: ExecutorProvider[] = [];
111
+ // All host calls made by one execute_code invocation share a downstream
112
+ // connection, while a later invocation receives a fresh request scope.
113
+ const requestScope = {};
114
+ const maxHostCalls = Math.max(
115
+ 1,
116
+ Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS),
117
+ );
118
+ const hostCallTimeoutMs = Math.max(
119
+ 1,
120
+ Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS),
121
+ );
122
+ let hostCalls = 0;
123
+ // Reserve the `connecta` namespace plus every global the sandbox bridge
124
+ // installs (see setupScript/installBridge in quickjs.ts) — a connector id
125
+ // sanitizing to one of these would clobber log capture or the call bridge.
126
+ const used = new Set<string>([
127
+ "connecta",
128
+ "console",
129
+ "__invoke",
130
+ "__call",
131
+ "__log",
132
+ ]);
133
+ const connectors = registry.listConnectors();
134
+ const loaded = await Promise.allSettled(
135
+ connectors.map((connector) =>
136
+ registry.getTools(connector.id, baseUrl, requestScope),
137
+ ),
138
+ );
139
+ const catalogs = new Map<string, ToolDef[]>();
140
+ const callAddress = async (address: unknown, args: unknown) => {
141
+ const resolved = registry.resolveAddress(String(address));
142
+ if (!resolved) throw new Error(`Unknown address "${String(address)}"`);
143
+ const definition = catalogs
144
+ .get(resolved.connector.id)
145
+ ?.find((tool) => tool.name === resolved.toolName);
146
+ if (!definition) {
147
+ throw new Error(
148
+ `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
149
+ );
150
+ }
151
+ if (
152
+ definition.annotations?.readOnlyHint !== true ||
153
+ definition.annotations?.destructiveHint === true
154
+ ) {
155
+ throw new Error(
156
+ `Tool "${String(address)}" is not explicitly read-only and cannot run inside execute_code. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`,
157
+ );
158
+ }
159
+ hostCalls++;
160
+ if (hostCalls > maxHostCalls) {
161
+ throw new Error(
162
+ `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`,
163
+ );
164
+ }
165
+ if (limits.signal?.aborted) {
166
+ throw new Error("execute_code host call cancelled");
167
+ }
168
+ const controller = new AbortController();
169
+ const cancel = () => controller.abort(limits.signal?.reason);
170
+ limits.signal?.addEventListener("abort", cancel, { once: true });
171
+ let timer: ReturnType<typeof setTimeout> | undefined;
172
+ let rejectCancelled!: (reason: Error) => void;
173
+ const cancelled = new Promise<never>((_, reject) => {
174
+ rejectCancelled = reject;
175
+ });
176
+ const onAbort = () => {
177
+ rejectCancelled(
178
+ controller.signal.reason instanceof Error
179
+ ? controller.signal.reason
180
+ : new Error("execute_code host call cancelled"),
181
+ );
182
+ };
183
+ controller.signal.addEventListener("abort", onAbort, { once: true });
184
+ const ctx = registry.contextFor(
185
+ resolved.connector.id,
186
+ baseUrl,
187
+ requestScope,
188
+ { signal: controller.signal, timeoutMs: hostCallTimeoutMs },
189
+ );
190
+ const started = Date.now();
191
+ try {
192
+ timer = setTimeout(() => {
193
+ controller.abort(
194
+ new Error(`Tool call timed out after ${hostCallTimeoutMs}ms`),
195
+ );
196
+ }, hostCallTimeoutMs);
197
+ const pending = resolved.connector.callTool(
198
+ resolved.toolName,
199
+ args ?? {},
200
+ ctx,
201
+ );
202
+ const value = unwrapForSandbox(
203
+ resolved.connector.kind,
204
+ await Promise.race([pending, cancelled]),
205
+ );
206
+ registry.recordSuccess(resolved.connector.id, Date.now() - started);
207
+ recordToolActivity(activity, {
208
+ connectorId: resolved.connector.id,
209
+ toolName: resolved.toolName,
210
+ address: `${resolved.connector.id}.${resolved.toolName}`,
211
+ source: "execute_code",
212
+ outcome: "success",
213
+ durationMs: Date.now() - started,
214
+ attempts: 1,
215
+ });
216
+ return value;
217
+ } catch (err) {
218
+ registry.recordFailure(resolved.connector.id, Date.now() - started, err);
219
+ const timedOut = /timed out|timeout/i.test(msg(err));
220
+ recordToolActivity(activity, {
221
+ connectorId: resolved.connector.id,
222
+ toolName: resolved.toolName,
223
+ address: `${resolved.connector.id}.${resolved.toolName}`,
224
+ source: "execute_code",
225
+ outcome: timedOut ? "timeout" : "error",
226
+ durationMs: Date.now() - started,
227
+ attempts: 1,
228
+ errorCode: timedOut ? "timeout" : "connector_call_failed",
229
+ });
230
+ throw err;
231
+ } finally {
232
+ if (timer) clearTimeout(timer);
233
+ controller.signal.removeEventListener("abort", onAbort);
234
+ limits.signal?.removeEventListener("abort", cancel);
235
+ }
236
+ };
237
+ for (let i = 0; i < connectors.length; i++) {
238
+ const connector = connectors[i];
239
+ const ns = sanitizeIdentifier(connector.id);
240
+ if (used.has(ns)) {
241
+ logger.warn(
242
+ `[connecta] execute_code: namespace "${ns}" (connector "${connector.id}") collides with a reserved name or an earlier connector — connector skipped`,
243
+ );
244
+ continue;
245
+ }
246
+ const loadedTools = loaded[i];
247
+ if (loadedTools.status === "rejected") {
248
+ logger.warn(
249
+ `[connecta] execute_code: connector "${connector.id}" skipped: ${msg(loadedTools.reason)}`,
250
+ );
251
+ continue;
252
+ }
253
+ const tools = loadedTools.value;
254
+ catalogs.set(connector.id, tools);
255
+ // Null-prototype map: tools named `toString`/`hasOwnProperty`/`constructor`
256
+ // are legitimate and must not collide with inherited Object members.
257
+ const fns: ExecutorProvider["fns"] = Object.create(null);
258
+ for (const t of tools) {
259
+ if (
260
+ t.annotations?.readOnlyHint !== true ||
261
+ t.annotations?.destructiveHint === true
262
+ ) {
263
+ continue;
264
+ }
265
+ const key = sanitizeIdentifier(t.name);
266
+ if (Object.hasOwn(fns, key)) {
267
+ logger.warn(
268
+ `[connecta] execute_code: tool "${connector.id}.${t.name}" sanitizes to duplicate "${key}" — skipped`,
269
+ );
270
+ continue;
271
+ }
272
+ fns[key] = async (args: unknown) =>
273
+ callAddress(`${connector.id}.${t.name}`, args);
274
+ }
275
+ if (Object.keys(fns).length > 0) {
276
+ providers.push({ name: ns, fns });
277
+ used.add(ns);
278
+ }
279
+ }
280
+ providers.push({
281
+ name: "connecta",
282
+ fns: {
283
+ call: callAddress,
284
+ batch: async (calls: unknown) => {
285
+ if (!Array.isArray(calls)) throw new Error("calls must be an array");
286
+ if (calls.length > EXECUTE_MAX_BATCH_CALLS) {
287
+ throw new Error(
288
+ `connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS} calls`,
289
+ );
290
+ }
291
+ return Promise.all(
292
+ calls.map(async (call) => {
293
+ const item = call as { address?: unknown; args?: unknown };
294
+ try {
295
+ return {
296
+ address: String(item.address),
297
+ ok: true,
298
+ data: await callAddress(item.address, item.args),
299
+ };
300
+ } catch (err) {
301
+ return {
302
+ address: String(item.address),
303
+ ok: false,
304
+ error: msg(err),
305
+ };
306
+ }
307
+ }),
308
+ );
309
+ },
310
+ search: async (raw: unknown) => {
311
+ const args = (raw ?? {}) as {
312
+ query?: string;
313
+ connector?: string;
314
+ limit?: number;
315
+ offset?: number;
316
+ fullDescriptions?: boolean;
317
+ includeSchemas?: "compact" | "json";
318
+ };
319
+ const matches: Array<{
320
+ connector: string;
321
+ tool: ToolDef;
322
+ score: number;
323
+ order: number;
324
+ }> = [];
325
+ let orderBase = 0;
326
+ for (const connector of connectors) {
327
+ if (args.connector && connector.id !== args.connector) continue;
328
+ const tools = catalogs.get(connector.id);
329
+ if (!tools) continue;
330
+ for (const ranked of rankTools(tools, args.query ?? "")) {
331
+ matches.push({
332
+ connector: connector.id,
333
+ tool: ranked.tool,
334
+ score: ranked.score,
335
+ order: orderBase + ranked.order,
336
+ });
337
+ }
338
+ orderBase += tools.length;
339
+ }
340
+ matches.sort((a, b) => b.score - a.score || a.order - b.order);
341
+ const offset = Math.max(0, Math.trunc(args.offset ?? 0));
342
+ const limit = Math.max(1, Math.trunc(args.limit ?? 25));
343
+ const page = matches.slice(offset, offset + limit).map((match) => {
344
+ const input = match.tool.inputSchema ?? { type: "object" };
345
+ return {
346
+ name: match.tool.name,
347
+ address: `${match.connector}.${match.tool.name}`,
348
+ description: summarizeDescription(
349
+ match.tool.description,
350
+ args.fullDescriptions === true,
351
+ ),
352
+ ...(args.includeSchemas
353
+ ? {
354
+ inputSchema:
355
+ args.includeSchemas === "json"
356
+ ? input
357
+ : compactSchema(input),
358
+ }
359
+ : {}),
360
+ ...(args.includeSchemas && match.tool.outputSchema
361
+ ? {
362
+ outputSchema:
363
+ args.includeSchemas === "json"
364
+ ? match.tool.outputSchema
365
+ : compactSchema(match.tool.outputSchema),
366
+ }
367
+ : {}),
368
+ ...(match.tool.annotations
369
+ ? { annotations: match.tool.annotations }
370
+ : {}),
371
+ };
372
+ });
373
+ const nextOffset =
374
+ offset + page.length < matches.length
375
+ ? offset + page.length
376
+ : undefined;
377
+ return {
378
+ tools: page,
379
+ total: matches.length,
380
+ offset,
381
+ limit,
382
+ hasMore: nextOffset !== undefined,
383
+ ...(nextOffset !== undefined ? { nextOffset } : {}),
384
+ };
385
+ },
386
+ describe: async (raw: unknown) => {
387
+ const args = (raw ?? {}) as {
388
+ addresses?: unknown;
389
+ format?: "compact" | "json";
390
+ fullDescriptions?: boolean;
391
+ };
392
+ if (!Array.isArray(args.addresses)) {
393
+ throw new Error("addresses must be an array");
394
+ }
395
+ const format = args.format ?? "compact";
396
+ return {
397
+ tools: args.addresses.map((rawAddress) => {
398
+ const address = String(rawAddress);
399
+ const resolved = registry.resolveAddress(address);
400
+ if (!resolved) {
401
+ return { address, error: `Unknown address "${address}"` };
402
+ }
403
+ const tool = catalogs
404
+ .get(resolved.connector.id)
405
+ ?.find((item) => item.name === resolved.toolName);
406
+ if (!tool) {
407
+ return { address, error: `Unknown tool "${address}"` };
408
+ }
409
+ const input = tool.inputSchema ?? { type: "object" };
410
+ return {
411
+ address,
412
+ name: tool.name,
413
+ description: summarizeDescription(
414
+ tool.description,
415
+ args.fullDescriptions === true,
416
+ ),
417
+ inputSchema: format === "json" ? input : compactSchema(input),
418
+ ...(tool.outputSchema
419
+ ? {
420
+ outputSchema:
421
+ format === "json"
422
+ ? tool.outputSchema
423
+ : compactSchema(tool.outputSchema),
424
+ }
425
+ : {}),
426
+ ...(tool.annotations ? { annotations: tool.annotations } : {}),
427
+ };
428
+ }),
429
+ };
430
+ },
431
+ },
432
+ });
433
+ return providers;
434
+ }
435
+
436
+ function truncate(text: string, max: number): string {
437
+ if (text.length <= max) return text;
438
+ return `${text.slice(0, max)}\n--- TRUNCATED (${text.length} chars total) — filter/map/slice data inside your code and return only what you need ---`;
439
+ }
440
+
441
+ function guardResultValue(value: unknown): unknown {
442
+ const serialized = JSON.stringify(value, null, 2);
443
+ const text = serialized === undefined ? String(value) : serialized;
444
+ if (text.length <= MAX_RESULT_CHARS) return value;
445
+ return {
446
+ truncated: true,
447
+ preview: text.slice(0, MAX_RESULT_CHARS),
448
+ totalChars: text.length,
449
+ hint: "filter/map/slice data inside execute_code and return only what you need",
450
+ };
451
+ }
452
+
453
+ /** The execute_code handler. Exported for direct testing. */
454
+ export function createExecuteTool(
455
+ registry: Registry,
456
+ baseUrl: string,
457
+ executor: Executor,
458
+ logger: Logger,
459
+ activity?: ActivityRequestContext,
460
+ ) {
461
+ return async ({ code }: { code: string }): Promise<ToolResult> => {
462
+ const controller = new AbortController();
463
+ const providers = await buildSandboxProviders(
464
+ registry,
465
+ baseUrl,
466
+ logger,
467
+ activity,
468
+ { signal: controller.signal },
469
+ );
470
+ let outcome;
471
+ try {
472
+ outcome = await executor.execute(code, providers);
473
+ } catch (err) {
474
+ return errorResult(`Executor failed: ${msg(err)}`);
475
+ } finally {
476
+ // A sandbox timeout or early return must also release any outstanding
477
+ // host waits and signal cooperative connectors to stop their work.
478
+ controller.abort();
479
+ }
480
+ const logs =
481
+ outcome.logs && outcome.logs.length > 0
482
+ ? truncate(outcome.logs.join("\n"), MAX_LOG_CHARS)
483
+ : undefined;
484
+ if (outcome.error) {
485
+ return errorResult(
486
+ `Error: ${outcome.error}${logs ? `\n\nLogs:\n${logs}` : ""}`,
487
+ );
488
+ }
489
+ // A result crossing back as a host BigInt (or otherwise unserializable
490
+ // value) makes JSON.stringify throw — keep that inside the structured
491
+ // error path so captured logs survive instead of a raw SDK 500.
492
+ let result: unknown;
493
+ try {
494
+ result = guardResultValue(outcome.result);
495
+ } catch (err) {
496
+ return errorResult(
497
+ `Error: result is not JSON-serializable: ${msg(err)}${logs ? `\n\nLogs:\n${logs}` : ""}`,
498
+ );
499
+ }
500
+ return jsonResult({
501
+ result,
502
+ ...(logs ? { logs } : {}),
503
+ });
504
+ };
505
+ }
506
+
507
+ const EXECUTE_DESC = `Use for dependent multi-step calls, loops, joins, branching, or reducing large results in a sandbox. Only tools explicitly annotated readOnlyHint: true are available. For one straightforward call use call_tool; for 2–10 independent calls use batch_call. Each run is limited to ${EXECUTE_MAX_HOST_CALLS} host calls; connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS}; each host call has a ${EXECUTE_HOST_CALL_TIMEOUT_MS / 1_000}-second deadline.
508
+
509
+ Write an async arrow function. It runs with NO network, filesystem, timers, or imports — the only capabilities are:
510
+ - One global per connector: every address <connectorId>.<toolName> from search_tools is callable as <connectorId>.<toolName>(args) with a single args object matching the schema from describe_tools. Names are sanitized to JS identifiers: characters outside [A-Za-z0-9_$] become "_" (e.g. my-service.get.thing → my_service.get_thing), leading digits get "_" prefixed, reserved words get "_" appended.
511
+ - connecta.call(address, args) and connecta.batch(calls) — call raw addresses.
512
+ - connecta.search(args) and connecta.describe(args) — inspect the loaded catalog inside the same request.
513
+ - console.log(...) — captured and returned alongside the result.
514
+
515
+ Tool calls return plain values (MCP text content is JSON-parsed when possible) and throw on downstream errors — use try/catch to handle them. Return a JSON-serializable value; large results are truncated, so reduce data in code instead of returning raw payloads.
516
+
517
+ Workflow: search_tools → describe_tools (schemas) → execute_code. Plain JavaScript only — no TypeScript syntax.
518
+ Example: async () => { const r = await crm.search({ query: "roadmap" }); return r.results.map((item) => item.title); }`;
519
+
520
+ /** Register the execute_code meta-tool. Only called when an executor is configured. */
521
+ export function registerExecuteTool(
522
+ server: McpServer,
523
+ registry: Registry,
524
+ ctx: {
525
+ baseUrl: string;
526
+ executor: Executor;
527
+ logger: Logger;
528
+ activity?: ActivityRequestContext;
529
+ },
530
+ ): void {
531
+ const handler = createExecuteTool(
532
+ registry,
533
+ ctx.baseUrl,
534
+ ctx.executor,
535
+ ctx.logger,
536
+ ctx.activity,
537
+ );
538
+ server.registerTool(
539
+ "execute_code",
540
+ {
541
+ description: EXECUTE_DESC,
542
+ inputSchema: {
543
+ code: z
544
+ .string()
545
+ .describe("A JavaScript async arrow function to execute."),
546
+ },
547
+ },
548
+ async (args) => handler(args as { code: string }),
549
+ );
550
+ }