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