@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.
- package/CHANGELOG.md +48 -0
- package/README.md +67 -144
- package/dist/auth/bearer.d.ts +1 -1
- package/dist/auth/bearer.js +4 -4
- package/dist/auth/bearer.js.map +1 -1
- package/dist/auth/clerk.d.ts +1 -1
- package/dist/auth/clerk.js +1 -1
- package/dist/call-admission.d.ts.map +1 -1
- package/dist/call-admission.js +9 -3
- package/dist/call-admission.js.map +1 -1
- package/dist/catalog-fingerprint.d.ts +20 -0
- package/dist/catalog-fingerprint.d.ts.map +1 -0
- package/dist/catalog-fingerprint.js +34 -0
- package/dist/catalog-fingerprint.js.map +1 -0
- package/dist/catalog-limits.d.ts +10 -0
- package/dist/catalog-limits.d.ts.map +1 -0
- package/dist/catalog-limits.js +10 -0
- package/dist/catalog-limits.js.map +1 -0
- package/dist/catalog-service.d.ts +138 -0
- package/dist/catalog-service.d.ts.map +1 -0
- package/dist/catalog-service.js +407 -0
- package/dist/catalog-service.js.map +1 -0
- package/dist/catalog.js +2 -2
- package/dist/catalog.js.map +1 -1
- package/dist/concurrency.d.ts +4 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +31 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +22 -12
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +44 -19
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credential-health.d.ts +0 -22
- package/dist/credential-health.d.ts.map +1 -1
- package/dist/credential-health.js +14 -10
- package/dist/credential-health.js.map +1 -1
- package/dist/credentials.d.ts +3 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +9 -8
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +144 -321
- package/dist/execute.js.map +1 -1
- package/dist/executor-result.d.ts +0 -3
- package/dist/executor-result.d.ts.map +1 -1
- package/dist/executor-result.js +2 -2
- package/dist/executor-result.js.map +1 -1
- package/dist/executors/quickjs-child.js +3 -3
- package/dist/executors/quickjs-child.js.map +1 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -1
- package/dist/executors/quickjs-protocol.d.ts.map +1 -1
- package/dist/executors/quickjs-runtime.d.ts.map +1 -1
- package/dist/executors/quickjs-runtime.js +7 -2
- package/dist/executors/quickjs-runtime.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +4 -1
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/favicon.js +2 -2
- package/dist/favicon.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -17
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +96 -0
- package/dist/invocation.d.ts.map +1 -0
- package/dist/invocation.js +369 -0
- package/dist/invocation.js.map +1 -0
- package/dist/meta-tools.d.ts +8 -86
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +145 -583
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +3 -0
- package/dist/operator-ui/generated.d.ts.map +1 -0
- package/dist/operator-ui/generated.js +5 -0
- package/dist/operator-ui/generated.js.map +1 -0
- package/dist/operator-ui/model.d.ts +94 -0
- package/dist/operator-ui/model.d.ts.map +1 -0
- package/dist/operator-ui/model.js +28 -0
- package/dist/operator-ui/model.js.map +1 -0
- package/dist/registry.d.ts +22 -6
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +328 -35
- package/dist/registry.js.map +1 -1
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +173 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/credentials.d.ts +3 -0
- package/dist/routes/credentials.d.ts.map +1 -0
- package/dist/routes/credentials.js +181 -0
- package/dist/routes/credentials.js.map +1 -0
- package/dist/routes/mcp.d.ts +8 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +383 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/oauth.d.ts +4 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +268 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/shared.d.ts +97 -0
- package/dist/routes/shared.d.ts.map +1 -0
- package/dist/routes/shared.js +199 -0
- package/dist/routes/shared.js.map +1 -0
- package/dist/routes/ui.d.ts +3 -0
- package/dist/routes/ui.d.ts.map +1 -0
- package/dist/routes/ui.js +122 -0
- package/dist/routes/ui.js.map +1 -0
- package/dist/server.d.ts +9 -40
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +63 -1293
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +0 -15
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +6 -5
- package/dist/skills.js.map +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +3 -1
- package/dist/storage/file.js.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +3 -1
- package/dist/storage/memory.js.map +1 -1
- package/dist/toolkits.d.ts +0 -22
- package/dist/toolkits.d.ts.map +1 -1
- package/dist/toolkits.js +2 -2
- package/dist/toolkits.js.map +1 -1
- package/dist/types.d.ts +9 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +3 -93
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +26 -1393
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +15 -8
- package/src/auth/bearer.ts +4 -4
- package/src/auth/clerk.ts +1 -1
- package/src/call-admission.ts +8 -5
- package/src/catalog-fingerprint.ts +52 -0
- package/src/catalog-limits.ts +11 -0
- package/src/catalog-service.ts +601 -0
- package/src/catalog.ts +2 -2
- package/src/concurrency.ts +39 -0
- package/src/connectors/api.ts +22 -12
- package/src/connectors/remote-mcp.ts +52 -22
- package/src/credential-health.ts +13 -10
- package/src/credentials.ts +2 -2
- package/src/errors.ts +1 -1
- package/src/execute.ts +194 -409
- package/src/executor-result.ts +2 -2
- package/src/executors/quickjs-child.ts +8 -4
- package/src/executors/quickjs-protocol.ts +1 -1
- package/src/executors/quickjs-runtime.ts +5 -1
- package/src/executors/quickjs.ts +11 -8
- package/src/favicon.ts +2 -2
- package/src/index.ts +49 -17
- package/src/invocation.ts +527 -0
- package/src/meta-tools.ts +366 -906
- package/src/operator-ui/browser.css +413 -0
- package/src/operator-ui/browser.ts +1121 -0
- package/src/operator-ui/generated.ts +4 -0
- package/src/operator-ui/model.ts +127 -0
- package/src/registry.ts +434 -44
- package/src/routes/activity.ts +220 -0
- package/src/routes/credentials.ts +266 -0
- package/src/routes/mcp.ts +485 -0
- package/src/routes/oauth.ts +328 -0
- package/src/routes/shared.ts +330 -0
- package/src/routes/ui.ts +153 -0
- package/src/server.ts +66 -1703
- package/src/skills.ts +6 -5
- package/src/storage/file.ts +3 -1
- package/src/storage/memory.ts +3 -1
- package/src/toolkits.ts +2 -2
- package/src/types.ts +9 -2
- package/src/ui.ts +52 -1493
- 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 {
|
|
4
|
-
import { recordToolActivity, type ActivityRequestContext } from "./activity.js";
|
|
3
|
+
import type { ActivityRequestContext } from "./activity.js";
|
|
5
4
|
import {
|
|
6
5
|
assertDiscoveryResultSize,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
33
|
+
const EXECUTE_MAX_HOST_CALLS = 20;
|
|
36
34
|
export const EXECUTE_MAX_BATCH_CALLS = 10;
|
|
37
|
-
|
|
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
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
{
|
|
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) —
|
|
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",
|