@zackbart/connecta 0.7.9 → 0.8.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 (180) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +67 -144
  3. package/dist/auth/bearer.d.ts +1 -1
  4. package/dist/auth/bearer.js +4 -4
  5. package/dist/auth/bearer.js.map +1 -1
  6. package/dist/auth/clerk.d.ts +1 -1
  7. package/dist/auth/clerk.js +1 -1
  8. package/dist/call-admission.d.ts.map +1 -1
  9. package/dist/call-admission.js +9 -3
  10. package/dist/call-admission.js.map +1 -1
  11. package/dist/catalog-fingerprint.d.ts +20 -0
  12. package/dist/catalog-fingerprint.d.ts.map +1 -0
  13. package/dist/catalog-fingerprint.js +34 -0
  14. package/dist/catalog-fingerprint.js.map +1 -0
  15. package/dist/catalog-limits.d.ts +10 -0
  16. package/dist/catalog-limits.d.ts.map +1 -0
  17. package/dist/catalog-limits.js +10 -0
  18. package/dist/catalog-limits.js.map +1 -0
  19. package/dist/catalog-service.d.ts +138 -0
  20. package/dist/catalog-service.d.ts.map +1 -0
  21. package/dist/catalog-service.js +407 -0
  22. package/dist/catalog-service.js.map +1 -0
  23. package/dist/catalog.js +2 -2
  24. package/dist/catalog.js.map +1 -1
  25. package/dist/concurrency.d.ts +4 -0
  26. package/dist/concurrency.d.ts.map +1 -0
  27. package/dist/concurrency.js +31 -0
  28. package/dist/concurrency.js.map +1 -0
  29. package/dist/connectors/api.d.ts.map +1 -1
  30. package/dist/connectors/api.js +22 -12
  31. package/dist/connectors/api.js.map +1 -1
  32. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  33. package/dist/connectors/remote-mcp.js +44 -19
  34. package/dist/connectors/remote-mcp.js.map +1 -1
  35. package/dist/credential-health.d.ts +0 -22
  36. package/dist/credential-health.d.ts.map +1 -1
  37. package/dist/credential-health.js +14 -10
  38. package/dist/credential-health.js.map +1 -1
  39. package/dist/credentials.d.ts +3 -2
  40. package/dist/credentials.d.ts.map +1 -1
  41. package/dist/errors.d.ts +1 -1
  42. package/dist/errors.d.ts.map +1 -1
  43. package/dist/errors.js.map +1 -1
  44. package/dist/execute.d.ts +9 -8
  45. package/dist/execute.d.ts.map +1 -1
  46. package/dist/execute.js +144 -321
  47. package/dist/execute.js.map +1 -1
  48. package/dist/executor-result.d.ts +0 -3
  49. package/dist/executor-result.d.ts.map +1 -1
  50. package/dist/executor-result.js +2 -2
  51. package/dist/executor-result.js.map +1 -1
  52. package/dist/executors/quickjs-child.js +3 -3
  53. package/dist/executors/quickjs-child.js.map +1 -1
  54. package/dist/executors/quickjs-protocol.d.ts +4 -1
  55. package/dist/executors/quickjs-protocol.d.ts.map +1 -1
  56. package/dist/executors/quickjs-runtime.d.ts.map +1 -1
  57. package/dist/executors/quickjs-runtime.js +7 -2
  58. package/dist/executors/quickjs-runtime.js.map +1 -1
  59. package/dist/executors/quickjs.d.ts.map +1 -1
  60. package/dist/executors/quickjs.js +4 -1
  61. package/dist/executors/quickjs.js.map +1 -1
  62. package/dist/favicon.js +2 -2
  63. package/dist/favicon.js.map +1 -1
  64. package/dist/index.d.ts +5 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +44 -17
  67. package/dist/index.js.map +1 -1
  68. package/dist/invocation.d.ts +96 -0
  69. package/dist/invocation.d.ts.map +1 -0
  70. package/dist/invocation.js +369 -0
  71. package/dist/invocation.js.map +1 -0
  72. package/dist/meta-tools.d.ts +8 -86
  73. package/dist/meta-tools.d.ts.map +1 -1
  74. package/dist/meta-tools.js +145 -583
  75. package/dist/meta-tools.js.map +1 -1
  76. package/dist/operator-ui/generated.d.ts +3 -0
  77. package/dist/operator-ui/generated.d.ts.map +1 -0
  78. package/dist/operator-ui/generated.js +5 -0
  79. package/dist/operator-ui/generated.js.map +1 -0
  80. package/dist/operator-ui/model.d.ts +94 -0
  81. package/dist/operator-ui/model.d.ts.map +1 -0
  82. package/dist/operator-ui/model.js +28 -0
  83. package/dist/operator-ui/model.js.map +1 -0
  84. package/dist/registry.d.ts +22 -6
  85. package/dist/registry.d.ts.map +1 -1
  86. package/dist/registry.js +328 -35
  87. package/dist/registry.js.map +1 -1
  88. package/dist/routes/activity.d.ts +3 -0
  89. package/dist/routes/activity.d.ts.map +1 -0
  90. package/dist/routes/activity.js +173 -0
  91. package/dist/routes/activity.js.map +1 -0
  92. package/dist/routes/credentials.d.ts +3 -0
  93. package/dist/routes/credentials.d.ts.map +1 -0
  94. package/dist/routes/credentials.js +181 -0
  95. package/dist/routes/credentials.js.map +1 -0
  96. package/dist/routes/mcp.d.ts +8 -0
  97. package/dist/routes/mcp.d.ts.map +1 -0
  98. package/dist/routes/mcp.js +383 -0
  99. package/dist/routes/mcp.js.map +1 -0
  100. package/dist/routes/oauth.d.ts +4 -0
  101. package/dist/routes/oauth.d.ts.map +1 -0
  102. package/dist/routes/oauth.js +268 -0
  103. package/dist/routes/oauth.js.map +1 -0
  104. package/dist/routes/shared.d.ts +97 -0
  105. package/dist/routes/shared.d.ts.map +1 -0
  106. package/dist/routes/shared.js +199 -0
  107. package/dist/routes/shared.js.map +1 -0
  108. package/dist/routes/ui.d.ts +3 -0
  109. package/dist/routes/ui.d.ts.map +1 -0
  110. package/dist/routes/ui.js +122 -0
  111. package/dist/routes/ui.js.map +1 -0
  112. package/dist/server.d.ts +9 -40
  113. package/dist/server.d.ts.map +1 -1
  114. package/dist/server.js +63 -1293
  115. package/dist/server.js.map +1 -1
  116. package/dist/skills.d.ts +0 -15
  117. package/dist/skills.d.ts.map +1 -1
  118. package/dist/skills.js +6 -5
  119. package/dist/skills.js.map +1 -1
  120. package/dist/storage/file.d.ts.map +1 -1
  121. package/dist/storage/file.js +3 -1
  122. package/dist/storage/file.js.map +1 -1
  123. package/dist/storage/memory.d.ts.map +1 -1
  124. package/dist/storage/memory.js +3 -1
  125. package/dist/storage/memory.js.map +1 -1
  126. package/dist/toolkits.d.ts +0 -22
  127. package/dist/toolkits.d.ts.map +1 -1
  128. package/dist/toolkits.js +2 -2
  129. package/dist/toolkits.js.map +1 -1
  130. package/dist/types.d.ts +9 -2
  131. package/dist/types.d.ts.map +1 -1
  132. package/dist/ui.d.ts +3 -93
  133. package/dist/ui.d.ts.map +1 -1
  134. package/dist/ui.js +26 -1393
  135. package/dist/ui.js.map +1 -1
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.js +1 -1
  138. package/package.json +15 -8
  139. package/src/auth/bearer.ts +4 -4
  140. package/src/auth/clerk.ts +1 -1
  141. package/src/call-admission.ts +8 -5
  142. package/src/catalog-fingerprint.ts +52 -0
  143. package/src/catalog-limits.ts +11 -0
  144. package/src/catalog-service.ts +601 -0
  145. package/src/catalog.ts +2 -2
  146. package/src/concurrency.ts +39 -0
  147. package/src/connectors/api.ts +22 -12
  148. package/src/connectors/remote-mcp.ts +52 -22
  149. package/src/credential-health.ts +13 -10
  150. package/src/credentials.ts +2 -2
  151. package/src/errors.ts +1 -1
  152. package/src/execute.ts +194 -409
  153. package/src/executor-result.ts +2 -2
  154. package/src/executors/quickjs-child.ts +8 -4
  155. package/src/executors/quickjs-protocol.ts +1 -1
  156. package/src/executors/quickjs-runtime.ts +5 -1
  157. package/src/executors/quickjs.ts +11 -8
  158. package/src/favicon.ts +2 -2
  159. package/src/index.ts +49 -17
  160. package/src/invocation.ts +527 -0
  161. package/src/meta-tools.ts +366 -906
  162. package/src/operator-ui/browser.css +413 -0
  163. package/src/operator-ui/browser.ts +1121 -0
  164. package/src/operator-ui/generated.ts +4 -0
  165. package/src/operator-ui/model.ts +127 -0
  166. package/src/registry.ts +434 -44
  167. package/src/routes/activity.ts +220 -0
  168. package/src/routes/credentials.ts +266 -0
  169. package/src/routes/mcp.ts +485 -0
  170. package/src/routes/oauth.ts +328 -0
  171. package/src/routes/shared.ts +330 -0
  172. package/src/routes/ui.ts +153 -0
  173. package/src/server.ts +66 -1703
  174. package/src/skills.ts +6 -5
  175. package/src/storage/file.ts +3 -1
  176. package/src/storage/memory.ts +3 -1
  177. package/src/toolkits.ts +2 -2
  178. package/src/types.ts +9 -2
  179. package/src/ui.ts +52 -1493
  180. package/src/version.ts +1 -1
package/src/execute.ts CHANGED
@@ -1,40 +1,38 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
4
- import { recordToolActivity, type ActivityRequestContext } from "./activity.js";
3
+ import type { ActivityRequestContext } from "./activity.js";
5
4
  import {
6
5
  assertDiscoveryResultSize,
7
- discoveryAddresses,
8
- discoverySearchLimit,
9
- errorResult,
10
- jsonResult,
11
- type ToolResult,
12
- } from "./meta-tools.js";
6
+ CatalogService,
7
+ flatSearchResult,
8
+ } from "./catalog-service.js";
9
+ import { errorResult, jsonResult, type ToolResult } from "./meta-tools.js";
13
10
  import {
14
11
  guardExecuteResultValue,
15
12
  MAX_EXECUTE_LOG_CHARS,
16
13
  truncateExecuteText,
17
14
  } from "./executor-result.js";
18
- import { classifyCallError, ConnectorCallError } from "./errors.js";
19
- import { isCallAdmissionError } from "./call-admission.js";
20
15
  import {
21
16
  ExecutorAdmissionError,
22
17
  isAdmittingExecutor,
23
18
  } from "./executor-admission.js";
24
19
  import { unwrapMcpResult } from "./mcp-result.js";
20
+ import {
21
+ InvocationFailure,
22
+ InvocationService,
23
+ } from "./invocation.js";
25
24
  import type { RegistryView } from "./registry.js";
26
25
  import type {
27
26
  Connector,
28
27
  Executor,
29
28
  ExecutorProvider,
30
29
  Logger,
31
- ToolDef,
32
30
  } from "./types.js";
33
31
 
34
32
  /** Keep one model-written program from amplifying into an unbounded fan-out. */
35
- export const EXECUTE_MAX_HOST_CALLS = 20;
33
+ const EXECUTE_MAX_HOST_CALLS = 20;
36
34
  export const EXECUTE_MAX_BATCH_CALLS = 10;
37
- export const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
35
+ const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
38
36
 
39
37
  // deno-fmt-ignore
40
38
  const RESERVED = new Set([
@@ -89,10 +87,56 @@ export function sanitizeIdentifier(name: string): string {
89
87
  return id;
90
88
  }
91
89
 
90
+ const SANDBOX_RESERVED_NAMES = new Set([
91
+ "connecta",
92
+ "console",
93
+ "arguments",
94
+ "result",
95
+ "undefined",
96
+ "setTimeout",
97
+ "Promise",
98
+ "Error",
99
+ "WorkerEntrypoint",
100
+ "CodeExecutor",
101
+ "__invoke",
102
+ "__namespace",
103
+ "__call",
104
+ "__log",
105
+ "__dispatchers",
106
+ "__connectors",
107
+ "__logs",
108
+ "__CODEMODE_BINARY_TAG",
109
+ "__bytesToBase64",
110
+ "__base64ToBytes",
111
+ "__encodeCodemodeValue",
112
+ "__decodeCodemodeValue",
113
+ "__stringifyForCodemode",
114
+ "__parseForCodemode",
115
+ ]);
116
+
92
117
  function msg(err: unknown): string {
93
118
  return err instanceof Error ? err.message : String(err);
94
119
  }
95
120
 
121
+ function lazyNamespacePrelude(
122
+ connectors: Array<{ id: string; namespace: string }>,
123
+ ): string {
124
+ const declarations = connectors
125
+ .map(
126
+ ({ id, namespace }) =>
127
+ `globalThis[${JSON.stringify(namespace)}] = __makeConnectaNamespace(${JSON.stringify(id)});`,
128
+ )
129
+ .join("\n");
130
+ return `(() => {
131
+ const __makeConnectaNamespace = (connectorId) => Object.freeze(new Proxy(Object.create(null), {
132
+ get: (_target, toolName) => typeof toolName === "string"
133
+ ? (args) => connecta.__callNamespace(connectorId, toolName, args)
134
+ : undefined
135
+ }));
136
+ ${declarations}
137
+ })();`;
138
+ }
139
+
96
140
  /**
97
141
  * Unwrap an MCP CallToolResult so sandbox code sees plain values:
98
142
  * isError throws (a real exception the code can catch), structuredContent
@@ -107,9 +151,9 @@ export function unwrapForSandbox(
107
151
  }
108
152
 
109
153
  /**
110
- * Expose the registry to a sandbox: one provider (global namespace) per
111
- * connector with a function per tool, plus `connecta.call(address, args)` as
112
- * the raw-address escape hatch. Broken connectors are skipped, not fatal.
154
+ * Expose one fixed host provider plus trusted sandbox setup that creates a
155
+ * lazy proxy global per connector. No connector catalog is touched until code
156
+ * calls that namespace or explicitly asks search/describe.
113
157
  */
114
158
  export async function buildSandboxProviders(
115
159
  registry: RegistryView,
@@ -120,12 +164,19 @@ export async function buildSandboxProviders(
120
164
  signal?: AbortSignal;
121
165
  maxHostCalls?: number;
122
166
  hostCallTimeoutMs?: number;
167
+ discoveryConcurrency?: number;
123
168
  } = {},
124
169
  ): Promise<ExecutorProvider[]> {
125
- const providers: ExecutorProvider[] = [];
126
170
  // All host calls made by one execute_code invocation share a downstream
127
171
  // connection, while a later invocation receives a fresh request scope.
128
172
  const requestScope = {};
173
+ const catalog = new CatalogService(registry, baseUrl, {
174
+ requestScope,
175
+ ...(limits.discoveryConcurrency !== undefined
176
+ ? { concurrency: limits.discoveryConcurrency }
177
+ : {}),
178
+ });
179
+ const invocation = new InvocationService(registry, catalog, activity);
129
180
  const maxHostCalls = Math.max(
130
181
  1,
131
182
  Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS),
@@ -135,405 +186,129 @@ export async function buildSandboxProviders(
135
186
  Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS),
136
187
  );
137
188
  let hostCalls = 0;
138
- // Reserve the `connecta` namespace plus every global the sandbox bridge
139
- // installs (see setupScript/installBridge in quickjs.ts) — a connector id
140
- // sanitizing to one of these would clobber log capture or the call bridge.
141
- const used = new Set<string>([
142
- "connecta",
143
- "console",
144
- "__invoke",
145
- "__namespace",
146
- "__call",
147
- "__log",
148
- ]);
149
189
  const connectors = registry.listConnectors();
150
- const catalogStarted = Date.now();
151
- const loaded = await Promise.allSettled(
152
- connectors.map((connector) =>
153
- registry.getTools(connector.id, baseUrl, requestScope, {
154
- signal: limits.signal,
155
- }),
156
- ),
157
- );
158
- if (limits.signal?.aborted) {
159
- throw new ExecutorAdmissionError(
160
- "executor_cancelled",
161
- "Execution was cancelled during catalog construction.",
162
- );
163
- }
164
- const catalogs = new Map<string, ToolDef[]>();
165
- const callAddress = async (address: unknown, args: unknown) => {
166
- const resolved = registry.resolveAddress(String(address));
167
- if (!resolved) throw new Error(`Unknown address "${String(address)}"`);
168
- const definition = catalogs
169
- .get(resolved.connector.id)
170
- ?.find((tool) => tool.name === resolved.toolName);
171
- if (!definition) {
172
- throw new Error(
173
- `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
174
- );
175
- }
176
- if (
177
- definition.annotations?.readOnlyHint !== true ||
178
- definition.annotations?.destructiveHint === true
179
- ) {
180
- throw new Error(
181
- `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.`,
182
- );
183
- }
184
- hostCalls++;
185
- if (hostCalls > maxHostCalls) {
186
- throw new Error(
187
- `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`,
188
- );
189
- }
190
- if (limits.signal?.aborted) {
191
- throw new Error("execute_code host call cancelled");
190
+ const namespaces: Array<{ id: string; namespace: string }> = [];
191
+ const namespaceOwners = new Map<string, string>();
192
+ for (const connector of connectors) {
193
+ const namespace = sanitizeIdentifier(connector.id);
194
+ const owner = namespaceOwners.get(namespace);
195
+ const problem = SANDBOX_RESERVED_NAMES.has(namespace)
196
+ ? `Connector "${connector.id}" sanitizes to reserved execute_code namespace "${namespace}". Rename or exclude the connector before using execute_code.`
197
+ : owner
198
+ ? `Connector ids "${owner}" and "${connector.id}" both sanitize to execute_code namespace "${namespace}". Rename or exclude one connector before using execute_code.`
199
+ : undefined;
200
+ if (problem) {
201
+ logger.warn(`[connecta] execute_code: ${problem}`);
202
+ throw new Error(problem);
192
203
  }
193
- const controller = new AbortController();
194
- const cancel = () => controller.abort(limits.signal?.reason);
195
- limits.signal?.addEventListener("abort", cancel, { once: true });
196
- if (limits.signal?.aborted) cancel();
197
- let timer: ReturnType<typeof setTimeout> | undefined;
198
- let onAbort: (() => void) | undefined;
199
- const started = Date.now();
200
- let permit: Awaited<ReturnType<RegistryView["admitCall"]>> | undefined;
201
- try {
202
- permit = await registry.admitCall(resolved.connector.id, {
203
- toolName: resolved.toolName,
204
- args: args ?? {},
205
- signal: controller.signal,
206
- });
207
- let rejectCancelled!: (reason: Error) => void;
208
- const cancelled = new Promise<never>((_, reject) => {
209
- rejectCancelled = reject;
210
- });
211
- onAbort = () => {
212
- rejectCancelled(
213
- controller.signal.reason instanceof Error
214
- ? controller.signal.reason
215
- : new Error("execute_code host call cancelled"),
216
- );
217
- };
218
- controller.signal.addEventListener("abort", onAbort, { once: true });
219
- if (controller.signal.aborted) onAbort();
220
- if (controller.signal.aborted) await cancelled;
221
- const ctx = registry.contextFor(
222
- resolved.connector.id,
223
- baseUrl,
224
- requestScope,
225
- { signal: controller.signal, timeoutMs: hostCallTimeoutMs },
226
- );
227
- timer = setTimeout(() => {
228
- controller.abort(
229
- new ConnectorCallError(
230
- "timeout",
231
- `Tool call timed out after ${hostCallTimeoutMs}ms`,
232
- ),
233
- );
234
- }, hostCallTimeoutMs);
235
- const pending = resolved.connector.callTool(
236
- resolved.toolName,
237
- args ?? {},
238
- ctx,
239
- );
240
- const value = unwrapForSandbox(
241
- resolved.connector.kind,
242
- await Promise.race([pending, cancelled]),
243
- );
244
- registry.recordSuccess(resolved.connector.id, Date.now() - started);
245
- recordToolActivity(activity, {
246
- connectorId: resolved.connector.id,
247
- toolName: resolved.toolName,
248
- address: `${resolved.connector.id}.${resolved.toolName}`,
249
- source: "execute_code",
250
- outcome: "success",
251
- durationMs: Date.now() - started,
252
- attempts: 1,
253
- });
254
- return value;
255
- } catch (err) {
256
- const callerCancelled =
257
- limits.signal?.aborted === true ||
258
- (isCallAdmissionError(err) && err.admissionKind === "cancelled");
259
- if (!callerCancelled && !isCallAdmissionError(err)) {
260
- registry.recordFailure(
261
- resolved.connector.id,
262
- Date.now() - started,
263
- err,
204
+ namespaceOwners.set(namespace, connector.id);
205
+ namespaces.push({ id: connector.id, namespace });
206
+ }
207
+
208
+ const invocationContext = () => ({
209
+ source: "execute_code" as const,
210
+ timeoutMs: hostCallTimeoutMs,
211
+ ...(limits.signal !== undefined ? { requestSignal: limits.signal } : {}),
212
+ unwrapResult: true,
213
+ beforeDispatch: () => {
214
+ hostCalls++;
215
+ if (hostCalls > maxHostCalls) {
216
+ throw new Error(
217
+ `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`,
264
218
  );
265
219
  }
266
- const details = callerCancelled
267
- ? {
268
- code: "cancelled",
269
- message: "Tool call was cancelled by the caller.",
270
- retryable: false,
271
- }
272
- : classifyCallError(err);
273
- recordToolActivity(activity, {
274
- connectorId: resolved.connector.id,
275
- toolName: resolved.toolName,
276
- address: `${resolved.connector.id}.${resolved.toolName}`,
277
- source: "execute_code",
278
- outcome:
279
- details.code === "timeout"
280
- ? "timeout"
281
- : details.code === "cancelled"
282
- ? "cancelled"
283
- : "error",
284
- durationMs: Date.now() - started,
285
- attempts: 1,
286
- errorCode: details.code,
287
- });
288
- throw err;
289
- } finally {
290
- if (timer) clearTimeout(timer);
291
- if (onAbort) controller.signal.removeEventListener("abort", onAbort);
292
- limits.signal?.removeEventListener("abort", cancel);
293
- permit?.release();
294
- }
220
+ },
221
+ });
222
+ const callAddress = async (address: unknown, args: unknown) => {
223
+ const outcome = await invocation.invoke(
224
+ String(address),
225
+ args ?? {},
226
+ invocationContext(),
227
+ );
228
+ if (!outcome.ok) throw new InvocationFailure(outcome.error);
229
+ return outcome.value;
295
230
  };
296
- for (let i = 0; i < connectors.length; i++) {
297
- const connector = connectors[i];
298
- const ns = sanitizeIdentifier(connector.id);
299
- if (used.has(ns)) {
300
- logger.warn(
301
- `[connecta] execute_code: namespace "${ns}" (connector "${connector.id}") collides with a reserved name or an earlier connector — connector skipped`,
302
- );
303
- continue;
304
- }
305
- const loadedTools = loaded[i];
306
- if (loadedTools.status === "rejected") {
307
- // Same health accounting as the call_tool catalog catch: a connector whose
308
- // catalog cannot be fetched is unusable, and dropping its namespace with
309
- // only a warn would leave the cheap `list_connectors({ probe: false })`
310
- // signal clean for a code-mode deployment whose downstream grant was
311
- // revoked. Recorded through `registry` — this run's view — so a
312
- // toolkit-scoped execute_code lands in that toolkit's log as well.
313
- registry.recordFailure(
314
- connector.id,
315
- Date.now() - catalogStarted,
316
- loadedTools.reason,
317
- );
318
- // classifyCallError so a typed auth_required thrown while listing tools
319
- // keeps its code where an operator can see it; health stores the message.
320
- const details = classifyCallError(
321
- loadedTools.reason,
322
- "catalog_lookup_failed",
323
- );
324
- logger.warn(
325
- `[connecta] execute_code: connector "${connector.id}" skipped (${details.code}): ${msg(loadedTools.reason)}`,
326
- );
327
- continue;
328
- }
329
- const tools = loadedTools.value;
330
- catalogs.set(connector.id, tools);
331
- // Null-prototype map: tools named `toString`/`hasOwnProperty`/`constructor`
332
- // are legitimate and must not collide with inherited Object members.
333
- const fns: ExecutorProvider["fns"] = Object.create(null);
334
- for (const t of tools) {
335
- if (
336
- t.annotations?.readOnlyHint !== true ||
337
- t.annotations?.destructiveHint === true
338
- ) {
339
- continue;
340
- }
341
- const key = sanitizeIdentifier(t.name);
342
- if (Object.hasOwn(fns, key)) {
343
- logger.warn(
344
- `[connecta] execute_code: tool "${connector.id}.${t.name}" sanitizes to duplicate "${key}" — skipped`,
345
- );
346
- continue;
347
- }
348
- // `await` (not a bare promise return) so a synchronous throw inside
349
- // callAddress never sits handler-less for the thenable-adoption
350
- // microtask — workerd reports that gap as an unhandled rejection.
351
- fns[key] = async (args: unknown) =>
352
- await callAddress(`${connector.id}.${t.name}`, args);
353
- }
354
- if (Object.keys(fns).length > 0) {
355
- providers.push({ name: ns, fns });
356
- used.add(ns);
357
- }
358
- }
359
- providers.push({
360
- name: "connecta",
361
- fns: {
362
- call: callAddress,
363
- batch: async (calls: unknown) => {
364
- if (!Array.isArray(calls)) throw new Error("calls must be an array");
365
- if (calls.length > EXECUTE_MAX_BATCH_CALLS) {
366
- throw new Error(
367
- `connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS} calls`,
368
- );
369
- }
370
- return await Promise.all(
371
- calls.map(async (call) => {
372
- const item = call as { address?: unknown; args?: unknown };
373
- try {
374
- return {
375
- address: String(item.address),
376
- ok: true,
377
- data: await callAddress(item.address, item.args),
378
- };
379
- } catch (err) {
380
- return {
381
- address: String(item.address),
382
- ok: false,
383
- error: msg(err),
384
- };
385
- }
386
- }),
387
- );
388
- },
389
- search: async (raw: unknown) => {
390
- const args = (raw ?? {}) as {
391
- query?: string;
392
- connector?: string;
393
- limit?: number;
394
- offset?: number;
395
- fullDescriptions?: boolean;
396
- includeSchemas?: "compact" | "json";
397
- };
398
- const matches: Array<{
399
- connector: string;
400
- tool: ToolDef;
401
- score: number;
402
- order: number;
403
- }> = [];
404
- let matchMode: "all" | "partial" = "all";
405
- const collectMatches = (mode: "all" | "partial") => {
406
- matches.length = 0;
407
- let orderBase = 0;
408
- for (const connector of connectors) {
409
- if (args.connector && connector.id !== args.connector) continue;
410
- const tools = catalogs.get(connector.id);
411
- if (!tools) continue;
412
- for (const ranked of rankTools(
413
- tools,
414
- args.query ?? "",
415
- mode,
416
- )) {
417
- matches.push({
418
- connector: connector.id,
419
- tool: ranked.tool,
420
- score: ranked.score,
421
- order: orderBase + ranked.order,
422
- });
423
- }
424
- orderBase += tools.length;
231
+ const callNamespace = async (
232
+ connectorId: unknown,
233
+ toolAlias: unknown,
234
+ args: unknown,
235
+ ) => {
236
+ const outcome = await invocation.invokeToolAlias(
237
+ String(connectorId),
238
+ String(toolAlias),
239
+ sanitizeIdentifier,
240
+ args ?? {},
241
+ invocationContext(),
242
+ );
243
+ if (!outcome.ok) throw new InvocationFailure(outcome.error);
244
+ return outcome.value;
245
+ };
246
+
247
+ return [
248
+ {
249
+ name: "connecta",
250
+ prelude: lazyNamespacePrelude(namespaces),
251
+ fns: {
252
+ __callNamespace: callNamespace,
253
+ call: callAddress,
254
+ batch: async (calls: unknown) => {
255
+ if (!Array.isArray(calls)) throw new Error("calls must be an array");
256
+ if (calls.length > EXECUTE_MAX_BATCH_CALLS) {
257
+ throw new Error(
258
+ `connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS} calls`,
259
+ );
425
260
  }
426
- };
427
- collectMatches("all");
428
- if ((args.query ?? "").trim() && matches.length === 0) {
429
- matchMode = "partial";
430
- collectMatches(matchMode);
431
- }
432
- matches.sort((a, b) => b.score - a.score || a.order - b.order);
433
- const offset = Math.max(0, Math.trunc(args.offset ?? 0));
434
- const limit = discoverySearchLimit(args.limit);
435
- const page = matches.slice(offset, offset + limit).map((match) => {
436
- const input = match.tool.inputSchema ?? { type: "object" };
437
- return {
438
- name: match.tool.name,
439
- address: `${match.connector}.${match.tool.name}`,
440
- description: summarizeDescription(
441
- match.tool.description,
442
- args.fullDescriptions === true,
443
- ),
444
- ...(args.includeSchemas
445
- ? {
446
- inputSchema:
447
- args.includeSchemas === "json"
448
- ? input
449
- : compactSchema(input),
450
- }
451
- : {}),
452
- ...(args.includeSchemas && match.tool.outputSchema
453
- ? {
454
- outputSchema:
455
- args.includeSchemas === "json"
456
- ? match.tool.outputSchema
457
- : compactSchema(match.tool.outputSchema),
458
- }
459
- : {}),
460
- ...(match.tool.annotations
461
- ? { annotations: match.tool.annotations }
462
- : {}),
261
+ return await Promise.all(
262
+ calls.map(async (call) => {
263
+ const item = call as { address?: unknown; args?: unknown };
264
+ try {
265
+ return {
266
+ address: String(item.address),
267
+ ok: true,
268
+ data: await callAddress(item.address, item.args),
269
+ };
270
+ } catch (err) {
271
+ return {
272
+ address: String(item.address),
273
+ ok: false,
274
+ error: msg(err),
275
+ };
276
+ }
277
+ }),
278
+ );
279
+ },
280
+ search: async (raw: unknown) => {
281
+ const args = (raw ?? {}) as {
282
+ query?: string;
283
+ connector?: string;
284
+ limit?: number;
285
+ offset?: number;
286
+ fullDescriptions?: boolean;
287
+ includeSchemas?: "compact" | "json";
463
288
  };
464
- });
465
- const nextOffset =
466
- offset + page.length < matches.length
467
- ? offset + page.length
468
- : undefined;
469
- const result = {
470
- tools: page,
471
- total: matches.length,
472
- offset,
473
- limit,
474
- hasMore: nextOffset !== undefined,
475
- ...(nextOffset !== undefined ? { nextOffset } : {}),
476
- ...(matchMode === "partial" && matches.length > 0
477
- ? { matchMode }
478
- : {}),
479
- };
480
- assertDiscoveryResultSize(
481
- result,
482
- "Request a smaller limit, omit fullDescriptions, or use compact schemas.",
483
- );
484
- return result;
485
- },
486
- describe: async (raw: unknown) => {
487
- const args = (raw ?? {}) as {
488
- addresses?: unknown;
489
- format?: "compact" | "json";
490
- fullDescriptions?: boolean;
491
- };
492
- const addresses = discoveryAddresses(args.addresses);
493
- const format = args.format ?? "compact";
494
- const result = {
495
- tools: addresses.map((rawAddress) => {
496
- const address = String(rawAddress);
497
- const resolved = registry.resolveAddress(address);
498
- if (!resolved) {
499
- return { address, error: `Unknown address "${address}"` };
500
- }
501
- const tool = catalogs
502
- .get(resolved.connector.id)
503
- ?.find((item) => item.name === resolved.toolName);
504
- if (!tool) {
505
- return { address, error: `Unknown tool "${address}"` };
506
- }
507
- const input = tool.inputSchema ?? { type: "object" };
508
- return {
509
- address,
510
- name: tool.name,
511
- description: summarizeDescription(
512
- tool.description,
513
- args.fullDescriptions === true,
514
- ),
515
- inputSchema: format === "json" ? input : compactSchema(input),
516
- ...(tool.outputSchema
517
- ? {
518
- outputSchema:
519
- format === "json"
520
- ? tool.outputSchema
521
- : compactSchema(tool.outputSchema),
522
- }
523
- : {}),
524
- ...(tool.annotations ? { annotations: tool.annotations } : {}),
525
- };
526
- }),
527
- };
528
- assertDiscoveryResultSize(
529
- result,
530
- 'Split the address list or use format: "compact".',
531
- );
532
- return result;
289
+ const result = flatSearchResult(await catalog.search(args));
290
+ assertDiscoveryResultSize(
291
+ result,
292
+ "Request a smaller limit, omit fullDescriptions, or use compact schemas.",
293
+ );
294
+ return result;
295
+ },
296
+ describe: async (raw: unknown) => {
297
+ const args = (raw ?? {}) as {
298
+ addresses?: unknown;
299
+ format?: "compact" | "json";
300
+ fullDescriptions?: boolean;
301
+ };
302
+ const result = { tools: await catalog.describe(args) };
303
+ assertDiscoveryResultSize(
304
+ result,
305
+ 'Split the address list or use format: "compact".',
306
+ );
307
+ return result;
308
+ },
533
309
  },
534
310
  },
535
- });
536
- return providers;
311
+ ];
537
312
  }
538
313
 
539
314
  /** The execute_code handler. Exported for direct testing. */
@@ -543,6 +318,7 @@ export function createExecuteTool(
543
318
  executor: Executor,
544
319
  logger: Logger,
545
320
  activity?: ActivityRequestContext,
321
+ config: { discoveryConcurrency?: number } = {},
546
322
  ) {
547
323
  return async (
548
324
  { code }: { code: string },
@@ -572,7 +348,12 @@ export function createExecuteTool(
572
348
  baseUrl,
573
349
  logger,
574
350
  activity,
575
- { signal: controller.signal },
351
+ {
352
+ signal: controller.signal,
353
+ ...(config.discoveryConcurrency !== undefined
354
+ ? { discoveryConcurrency: config.discoveryConcurrency }
355
+ : {}),
356
+ },
576
357
  );
577
358
  if (controller.signal.aborted) {
578
359
  throw new ExecutorAdmissionError(
@@ -647,7 +428,7 @@ const EXECUTE_DESC = `Use for dependent multi-step calls, loops, joins, branchin
647
428
  Write an async arrow function. It runs with NO network, filesystem, timers, or imports — the only capabilities are:
648
429
  - 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.
649
430
  - connecta.call(address, args) and connecta.batch(calls) — call raw addresses.
650
- - connecta.search(args) and connecta.describe(args) — inspect the loaded catalog inside the same request.
431
+ - connecta.search(args) and connecta.describe(args) — load and inspect request-local catalogs on demand.
651
432
  - console.log(...) — captured and returned alongside the result.
652
433
 
653
434
  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.
@@ -665,6 +446,7 @@ export function registerExecuteTool(
665
446
  logger: Logger;
666
447
  activity?: ActivityRequestContext;
667
448
  requestSignal?: AbortSignal;
449
+ discoveryConcurrency?: number;
668
450
  },
669
451
  ): void {
670
452
  const handler = createExecuteTool(
@@ -673,6 +455,9 @@ export function registerExecuteTool(
673
455
  ctx.executor,
674
456
  ctx.logger,
675
457
  ctx.activity,
458
+ ctx.discoveryConcurrency !== undefined
459
+ ? { discoveryConcurrency: ctx.discoveryConcurrency }
460
+ : {},
676
461
  );
677
462
  server.registerTool(
678
463
  "execute_code",