@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.
- package/CHANGELOG.md +94 -0
- package/README.md +67 -144
- package/dist/activity.d.ts +1 -1
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js.map +1 -1
- 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 +81 -0
- package/dist/call-admission.d.ts.map +1 -0
- package/dist/call-admission.js +345 -0
- package/dist/call-admission.js.map +1 -0
- 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 +3 -1
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +22 -11
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +3 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +44 -18
- 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 -291
- 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 +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -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 +11 -86
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +145 -485
- 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 +47 -6
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +356 -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 +67 -1292
- 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 +64 -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 -1389
- 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/activity.ts +5 -1
- package/src/auth/bearer.ts +4 -4
- package/src/auth/clerk.ts +1 -1
- package/src/call-admission.ts +522 -0
- 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 +25 -11
- package/src/connectors/remote-mcp.ts +55 -21
- package/src/credential-health.ts +13 -10
- package/src/credentials.ts +2 -2
- package/src/errors.ts +1 -1
- package/src/execute.ts +194 -377
- 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 +54 -17
- package/src/invocation.ts +527 -0
- package/src/meta-tools.ts +369 -797
- 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 +489 -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 +70 -1702
- 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 +70 -2
- package/src/ui.ts +52 -1489
- 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 {
|
|
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
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
|
-
|
|
33
|
+
const EXECUTE_MAX_HOST_CALLS = 20;
|
|
35
34
|
export const EXECUTE_MAX_BATCH_CALLS = 10;
|
|
36
|
-
|
|
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
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
{
|
|
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) —
|
|
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",
|