@zackbart/connecta 0.24.2 → 0.24.4
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/AGENTS.md +18 -20
- package/CHANGELOG.md +205 -1
- package/README.md +5 -6
- package/dist/auth/bearer.js +2 -0
- package/dist/auth/downstream-oauth.d.ts +12 -1
- package/dist/auth/downstream-oauth.js +147 -35
- package/dist/branding.d.ts +31 -2
- package/dist/branding.js +116 -8
- package/dist/call-admission.d.ts +4 -0
- package/dist/call-admission.js +26 -0
- package/dist/catalog-drift.js +9 -4
- package/dist/catalog-service.d.ts +2 -0
- package/dist/catalog-service.js +25 -8
- package/dist/catalog.d.ts +2 -0
- package/dist/catalog.js +246 -121
- package/dist/connectors/api.d.ts +1 -1
- package/dist/connectors/api.js +21 -3
- package/dist/connectors/guarded-fetch.d.ts +6 -2
- package/dist/connectors/guarded-fetch.js +61 -24
- package/dist/connectors/remote-mcp.js +92 -57
- package/dist/errors.d.ts +28 -3
- package/dist/errors.js +60 -1
- package/dist/execute.js +85 -23
- package/dist/executor-result.js +3 -1
- package/dist/executors/quickjs-child.js +5 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -0
- package/dist/executors/quickjs-runtime.d.ts +1 -1
- package/dist/executors/quickjs-runtime.js +38 -21
- package/dist/executors/quickjs.js +68 -27
- package/dist/index.d.ts +15 -1
- package/dist/index.js +36 -4
- package/dist/invocation.js +134 -93
- package/dist/mcp-result.js +3 -2
- package/dist/meta-tools.js +191 -36
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +38 -1
- package/dist/operator-ui/view.js +71 -0
- package/dist/providers/cloudflare.d.ts +14 -2
- package/dist/providers/cloudflare.js +107 -16
- package/dist/providers/linear.d.ts +26 -4
- package/dist/providers/linear.js +19 -4
- package/dist/providers/mixpanel.d.ts +16 -3
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/notion.d.ts +8 -1
- package/dist/providers/notion.js +83 -10
- package/dist/providers/revenuecat.d.ts +30 -4
- package/dist/providers/revenuecat.js +42 -4
- package/dist/providers/stripe.d.ts +7 -1
- package/dist/providers/stripe.js +30 -4
- package/dist/providers/vercel.js +11 -1
- package/dist/registry.d.ts +22 -2
- package/dist/registry.js +101 -13
- package/dist/routes/mcp.d.ts +4 -1
- package/dist/routes/mcp.js +84 -13
- package/dist/routes/oauth.js +4 -0
- package/dist/routes/shared.d.ts +1 -0
- package/dist/routes/shared.js +4 -4
- package/dist/server.js +15 -3
- package/dist/skills.js +6 -5
- package/dist/storage/file.d.ts +6 -2
- package/dist/storage/file.js +312 -34
- package/dist/storage/memory.js +12 -1
- package/dist/types.d.ts +37 -0
- package/dist/ui.js +18 -10
- package/dist/validate.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +193 -165
- package/documentation/auth.md +199 -145
- package/documentation/code-mode.md +433 -316
- package/documentation/meta-tools.md +363 -355
- package/examples/worker/AGENTS.md +2 -1
- package/examples/worker/README.md +12 -10
- package/examples/worker/src/index.ts +12 -15
- package/package.json +1 -2
- package/templates/node/.env.example +3 -3
- package/templates/node/AGENTS.md +5 -4
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +23 -22
- package/documentation/call-admission.md +0 -142
- package/documentation/cloudflare.md +0 -471
- package/documentation/connector-guides.md +0 -176
- package/documentation/connectors.md +0 -389
- package/documentation/linear.md +0 -193
- package/documentation/mixpanel.md +0 -160
- package/documentation/notion.md +0 -308
- package/documentation/operations.md +0 -359
- package/documentation/operator-ui.md +0 -135
- package/documentation/optional-modules-upgrade.md +0 -243
- package/documentation/provider-conventions.md +0 -722
- package/documentation/request-admission.md +0 -170
- package/documentation/revenuecat.md +0 -305
- package/documentation/storage-and-credentials.md +0 -201
- package/documentation/stripe.md +0 -262
- package/documentation/upgrading.md +0 -754
- package/documentation/vercel.md +0 -241
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { credentialTestRule, describeCredentialTestMismatch, } from "./credentia
|
|
|
2
2
|
import { Registry } from "./registry.js";
|
|
3
3
|
import { parseConnectorAccess, POOL_NAME_RE } from "./connector-access.js";
|
|
4
4
|
import { createFetchHandler } from "./server.js";
|
|
5
|
-
import { droppedBrandingUrls, droppedUiAuthUrls } from "./branding.js";
|
|
5
|
+
import { droppedBrandingUrls, droppedThemeTokens, droppedUiAuthUrls, } from "./branding.js";
|
|
6
6
|
import { memoryStorage } from "./storage/memory.js";
|
|
7
7
|
import { CONNECTA_VERSION } from "./version.js";
|
|
8
8
|
import { AdmissionController, executorName, isAdmittingExecutor, withExecutorAdmission, } from "./executor-admission.js";
|
|
@@ -60,6 +60,7 @@ const CONFIG_SCHEMA = {
|
|
|
60
60
|
pools: null,
|
|
61
61
|
storage: null,
|
|
62
62
|
publicUrl: null,
|
|
63
|
+
allowedOrigins: null,
|
|
63
64
|
activity: null,
|
|
64
65
|
vault: null,
|
|
65
66
|
ui: null,
|
|
@@ -70,6 +71,10 @@ const CONFIG_SCHEMA = {
|
|
|
70
71
|
staleCatalogSeconds: null,
|
|
71
72
|
probeTimeoutMs: null,
|
|
72
73
|
},
|
|
74
|
+
results: {
|
|
75
|
+
maxStashBytes: null,
|
|
76
|
+
maxStashEntries: null,
|
|
77
|
+
},
|
|
73
78
|
calls: {
|
|
74
79
|
defaultTimeoutMs: null,
|
|
75
80
|
maxResultBytes: null,
|
|
@@ -135,6 +140,17 @@ function rejectUnknownOptions(paths) {
|
|
|
135
140
|
/** Reject JavaScript typos and removed options before construction does work. */
|
|
136
141
|
function assertKnownConfig(config) {
|
|
137
142
|
rejectUnknownOptions(unknownOptionPaths(config, "ConnectaConfig", CONFIG_SCHEMA));
|
|
143
|
+
if (config.results !== undefined) {
|
|
144
|
+
if (!config.results || typeof config.results !== "object" || Array.isArray(config.results)) {
|
|
145
|
+
throw new Error("ConnectaConfig.results must be an object");
|
|
146
|
+
}
|
|
147
|
+
for (const key of ["maxStashBytes", "maxStashEntries"]) {
|
|
148
|
+
const value = config.results[key];
|
|
149
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
150
|
+
throw new Error(`ConnectaConfig.results.${key} must be a non-negative safe integer`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
138
154
|
if (config.vault && ["get", "getAll", "set", "setAll", "metadata", "delete"].some(key => typeof config.vault[key] !== "function"))
|
|
139
155
|
throw new Error("ConnectaConfig.vault must implement CredentialVault");
|
|
140
156
|
if (config.ui && (typeof config.ui.handle !== "function" || typeof config.ui.credentialHandoffUrl !== "function" || !Array.isArray(config.ui.reservedPaths)))
|
|
@@ -217,12 +233,15 @@ function resolvePools(pools, registry) {
|
|
|
217
233
|
function warnInsecureConfig(config, inboundAuth, logger) {
|
|
218
234
|
const oauthConnectors = config.connectors.filter((c) => c.finishAuth);
|
|
219
235
|
const hasCredentialConnector = config.connectors.some((c) => c.credential);
|
|
220
|
-
//
|
|
221
|
-
//
|
|
236
|
+
// Static API headers can carry secrets without declaring credential hooks.
|
|
237
|
+
// Any configured connector warrants the open-deployment warning.
|
|
222
238
|
if (inboundAuth.length === 0 &&
|
|
223
|
-
|
|
239
|
+
config.connectors.length > 0) {
|
|
224
240
|
logger.warn("[connecta] running with no inbound authentication: any caller can " +
|
|
225
241
|
"invoke every shared connector. " +
|
|
242
|
+
(hasCredentialConnector || oauthConnectors.length > 0
|
|
243
|
+
? "Configured credentials and downstream OAuth grants are exposed to those calls. "
|
|
244
|
+
: "") +
|
|
226
245
|
"Configure `auth` (for example bearerToken(...) or Clerk) to gate access.");
|
|
227
246
|
}
|
|
228
247
|
// Unset publicUrl with OAuth connectors: the downstream redirect_uri is
|
|
@@ -243,6 +262,17 @@ function warnInsecureConfig(config, inboundAuth, logger) {
|
|
|
243
262
|
"used as an href, so it must be an absolute http(s) URL (favicon.href " +
|
|
244
263
|
"may also be a root-relative path). The default is rendered instead.");
|
|
245
264
|
}
|
|
265
|
+
// Theme tokens are written into a `:root` block, so each is gated
|
|
266
|
+
// syntactically and a rejected value takes the stylesheet's default. Same
|
|
267
|
+
// reason as the branding URLs above: the page still renders, so without this
|
|
268
|
+
// line the only evidence is that the operator's color never showed up.
|
|
269
|
+
const droppedTheme = droppedThemeTokens(config.ui?.branding?.theme);
|
|
270
|
+
if (droppedTheme.length > 0) {
|
|
271
|
+
logger.warn(`[connecta] branding ${droppedTheme.join(", ")} dropped: accent must be ` +
|
|
272
|
+
"a hex color, radius a CSS length, the font families a plain " +
|
|
273
|
+
"font-family list, and colorScheme one of system/light/dark. The " +
|
|
274
|
+
"default is rendered instead.");
|
|
275
|
+
}
|
|
246
276
|
// Operator shells render exactly one provider's browser sign-in config — the
|
|
247
277
|
// first that offers one, matching the server route's `find` — and that
|
|
248
278
|
// provider's URLs reach the browser: frontendApiUrl as the loader's
|
|
@@ -330,6 +360,7 @@ export function createConnecta(config) {
|
|
|
330
360
|
persistToolCatalog: config.discovery?.persistCatalog,
|
|
331
361
|
toolCatalogStaleSeconds: config.discovery?.staleCatalogSeconds,
|
|
332
362
|
maxResultBytes: config.calls?.maxResultBytes,
|
|
363
|
+
results: config.results,
|
|
333
364
|
});
|
|
334
365
|
const inboundAuth = configuredAuth;
|
|
335
366
|
const pools = resolvePools(config.pools, registry);
|
|
@@ -356,6 +387,7 @@ export function createConnecta(config) {
|
|
|
356
387
|
identity: config.identity,
|
|
357
388
|
pools,
|
|
358
389
|
publicUrl: config.publicUrl,
|
|
390
|
+
allowedOrigins: config.allowedOrigins,
|
|
359
391
|
serverInfo,
|
|
360
392
|
logger,
|
|
361
393
|
activity: config.activity?.store,
|
package/dist/invocation.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isCallAdmissionError } from "./call-admission.js";
|
|
2
|
-
import { classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
|
|
2
|
+
import { boundedEchoText, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
|
|
3
3
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
4
4
|
import { splitAddress } from "./registry.js";
|
|
5
5
|
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
@@ -43,10 +43,10 @@ function assertRawMcpSuccess(kind, result) {
|
|
|
43
43
|
const mcpResult = result;
|
|
44
44
|
if (!mcpResult.isError)
|
|
45
45
|
return;
|
|
46
|
-
throw new Error(mcpResult.content
|
|
46
|
+
throw new Error(boundedEchoText(mcpResult.content
|
|
47
47
|
?.filter((block) => block.type === "text")
|
|
48
48
|
.map((block) => block.text ?? "")
|
|
49
|
-
.join("") || "Downstream tool call failed");
|
|
49
|
+
.join("") || "Downstream tool call failed"));
|
|
50
50
|
}
|
|
51
51
|
export class InvocationFailure extends Error {
|
|
52
52
|
details;
|
|
@@ -196,6 +196,8 @@ export class InvocationService {
|
|
|
196
196
|
tool: target.toolName,
|
|
197
197
|
source: context.source,
|
|
198
198
|
code: details.code,
|
|
199
|
+
// Sanitized transport diagnostics (origin and errno only, #539).
|
|
200
|
+
...(details.details ? { details: details.details } : {}),
|
|
199
201
|
attempts,
|
|
200
202
|
durationMs: Date.now() - started,
|
|
201
203
|
message: String(details.message ?? "").slice(0, 300),
|
|
@@ -215,111 +217,150 @@ export class InvocationService {
|
|
|
215
217
|
error: details,
|
|
216
218
|
};
|
|
217
219
|
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
activityTarget = {
|
|
223
|
-
connector: resolution.connector,
|
|
224
|
-
toolName: resolution.toolName,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
if (resolution.cause && context.requestSignal?.aborted) {
|
|
228
|
-
return failed(callerCancelledDetails());
|
|
229
|
-
}
|
|
230
|
-
return failed(resolution.error);
|
|
231
|
-
}
|
|
232
|
-
resolved = resolution.resolved;
|
|
233
|
-
activityTarget = resolved;
|
|
234
|
-
if (!isExplicitlyReadOnly(resolved.definition) && !context.allowDestructive) {
|
|
235
|
-
const canonicalAddress = `${resolved.connector.id}.${resolved.toolName}`;
|
|
236
|
-
return failed(framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`));
|
|
237
|
-
}
|
|
238
|
-
// Remote MCP tools advertise their input schema in the catalog. Validate
|
|
239
|
-
// against that same request-local definition before admission or provider
|
|
240
|
-
// dispatch, so a predictable mismatch stays structured instead of being
|
|
241
|
-
// flattened into provider-specific error prose. Unsupported schemas retain
|
|
242
|
-
// validateToolInput's fail-open behavior and reach the downstream normally.
|
|
243
|
-
if (resolved.connector.kind === "mcp" &&
|
|
244
|
-
resolved.definition.inputSchema) {
|
|
245
|
-
const invalid = validateToolInput(resolved.definition.inputSchema, args ?? {}, {
|
|
246
|
-
address: `${resolved.connector.id}.${resolved.toolName}`,
|
|
247
|
-
logger: this.registry.contextFor(resolved.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger,
|
|
248
|
-
});
|
|
249
|
-
if (invalid)
|
|
250
|
-
return failed(classifyCallError(invalid));
|
|
251
|
-
}
|
|
252
|
-
try {
|
|
253
|
-
context.beforeDispatch?.();
|
|
254
|
-
}
|
|
255
|
-
catch (error) {
|
|
256
|
-
return failed(error instanceof InvocationFailure
|
|
257
|
-
? error.details
|
|
258
|
-
: classifyCallError(error));
|
|
220
|
+
if (context.requestSignal?.aborted) {
|
|
221
|
+
// Preserve the cancelled admission-attempt count without starting discovery.
|
|
222
|
+
attempts = 1;
|
|
223
|
+
return failed(callerCancelledDetails());
|
|
259
224
|
}
|
|
260
225
|
let result;
|
|
261
226
|
let observedResult;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
227
|
+
// One deadline owns discovery, queue admission, and provider dispatch.
|
|
228
|
+
// Only the caller of this function records the final outcome, so a late
|
|
229
|
+
// cancellation cannot append a second activity event after the deadline.
|
|
230
|
+
const dispatch = async (callSignal) => {
|
|
231
|
+
const resolution = await this.catalog.resolveTool(address, defined({ signal: callSignal }));
|
|
232
|
+
catalogMs += resolution.catalogMs;
|
|
233
|
+
if (callSignal?.aborted)
|
|
234
|
+
throw callSignal.reason;
|
|
235
|
+
if (!resolution.ok) {
|
|
236
|
+
if (resolution.connector && resolution.toolName) {
|
|
237
|
+
activityTarget = {
|
|
238
|
+
connector: resolution.connector,
|
|
239
|
+
toolName: resolution.toolName,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (resolution.cause && context.requestSignal?.aborted) {
|
|
243
|
+
return callerCancelledDetails();
|
|
244
|
+
}
|
|
245
|
+
return resolution.error;
|
|
246
|
+
}
|
|
247
|
+
const target = resolution.resolved;
|
|
248
|
+
resolved = target;
|
|
249
|
+
activityTarget = target;
|
|
250
|
+
if (!isExplicitlyReadOnly(target.definition) && !context.allowDestructive) {
|
|
251
|
+
const canonicalAddress = `${target.connector.id}.${target.toolName}`;
|
|
252
|
+
return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`);
|
|
253
|
+
}
|
|
254
|
+
// Remote MCP tools advertise their input schema in the catalog. Validate
|
|
255
|
+
// against that same request-local definition before admission or provider
|
|
256
|
+
// dispatch, so a predictable mismatch stays structured instead of being
|
|
257
|
+
// flattened into provider-specific error prose. Unsupported schemas retain
|
|
258
|
+
// validateToolInput's fail-open behavior and reach the downstream normally.
|
|
259
|
+
if (target.connector.kind === "mcp" &&
|
|
260
|
+
target.definition.inputSchema) {
|
|
261
|
+
const invalid = validateToolInput(target.definition.inputSchema, args ?? {}, {
|
|
262
|
+
address: `${target.connector.id}.${target.toolName}`,
|
|
263
|
+
logger: this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope).logger,
|
|
264
|
+
});
|
|
265
|
+
if (invalid)
|
|
266
|
+
return classifyCallError(invalid);
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
context.beforeDispatch?.();
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
return error instanceof InvocationFailure
|
|
273
|
+
? error.details
|
|
274
|
+
: classifyCallError(error);
|
|
275
|
+
}
|
|
276
|
+
if (callSignal?.aborted)
|
|
277
|
+
throw callSignal.reason;
|
|
278
|
+
attempts = 1;
|
|
279
|
+
let permit;
|
|
280
|
+
let attemptError;
|
|
281
|
+
let attemptFailed = false;
|
|
282
|
+
try {
|
|
283
|
+
permit = await timed((elapsed) => { admissionMs += elapsed; }, () => this.registry.admitCall(target.connector.id, {
|
|
284
|
+
toolName: target.toolName,
|
|
285
|
+
args: args ?? {},
|
|
286
|
+
...defined({ signal: callSignal }),
|
|
287
|
+
}));
|
|
288
|
+
const raw = await timed((elapsed) => { connectorMs += elapsed; }, () => {
|
|
289
|
+
const call = () => {
|
|
290
|
+
const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs }));
|
|
291
|
+
if (target.connector.credential &&
|
|
292
|
+
!connectorContext.credential) {
|
|
293
|
+
throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
|
|
294
|
+
`authorize_connector({ connector: "${target.connector.id}" }).`);
|
|
295
|
+
}
|
|
296
|
+
// Cancellation can arrive during admission or context construction.
|
|
297
|
+
if (callSignal?.aborted)
|
|
298
|
+
throw callSignal.reason;
|
|
299
|
+
return target.connector.callTool(target.toolName, args ?? {}, connectorContext);
|
|
300
|
+
};
|
|
301
|
+
// Race cancellation here too, so an uncooperative connector cannot
|
|
302
|
+
// retain its admission permit after the enclosing deadline expires.
|
|
303
|
+
return callSignal
|
|
304
|
+
? withDeadline(call, {
|
|
305
|
+
signal: callSignal,
|
|
306
|
+
timeoutError: new ConnectorCallError("timeout", "Tool call timed out"),
|
|
307
|
+
})
|
|
308
|
+
: call();
|
|
309
|
+
});
|
|
310
|
+
// isError is checked here for BOTH result shapes so every adapter
|
|
311
|
+
// reports the same downstream-failure wording, and the throw lands
|
|
312
|
+
// inside the attempt where it feeds health.
|
|
313
|
+
assertRawMcpSuccess(target.connector.kind, raw);
|
|
314
|
+
observedResult = unwrapMcpResult(target.connector.kind, raw);
|
|
315
|
+
result = context.unwrapResult ? observedResult : raw;
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
attemptFailed = true;
|
|
319
|
+
attemptError = error;
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
permit?.release();
|
|
323
|
+
}
|
|
324
|
+
if (attemptFailed) {
|
|
325
|
+
if (callSignal?.aborted)
|
|
326
|
+
throw callSignal.reason;
|
|
327
|
+
const callerCancelled = isCallerCancellation(attemptError, context.requestSignal);
|
|
328
|
+
const details = callerCancelled
|
|
329
|
+
? callerCancelledDetails()
|
|
330
|
+
: classifyCallError(attemptError);
|
|
331
|
+
return details;
|
|
332
|
+
}
|
|
333
|
+
return undefined;
|
|
334
|
+
};
|
|
266
335
|
try {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
args: args ?? {},
|
|
270
|
-
...defined({ signal: context.requestSignal }),
|
|
271
|
-
}));
|
|
272
|
-
const raw = await timed((elapsed) => { connectorMs += elapsed; }, () => {
|
|
273
|
-
const call = (callSignal) => {
|
|
274
|
-
const connectorContext = this.registry.contextFor(resolved.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs }));
|
|
275
|
-
if (resolved.connector.credential &&
|
|
276
|
-
!connectorContext.credential) {
|
|
277
|
-
throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
|
|
278
|
-
`authorize_connector({ connector: "${resolved.connector.id}" }).`);
|
|
279
|
-
}
|
|
280
|
-
// Cancellation can arrive during admission or context construction.
|
|
281
|
-
if (callSignal?.aborted)
|
|
282
|
-
throw callSignal.reason;
|
|
283
|
-
return resolved.connector.callTool(resolved.toolName, args ?? {}, connectorContext);
|
|
284
|
-
};
|
|
285
|
-
if (!context.timeoutMs && !context.requestSignal)
|
|
286
|
-
return call();
|
|
287
|
-
return withDeadline(call, {
|
|
336
|
+
const error = context.timeoutMs || context.requestSignal
|
|
337
|
+
? await withDeadline(dispatch, {
|
|
288
338
|
...defined({
|
|
289
339
|
timeoutMs: context.timeoutMs,
|
|
290
340
|
signal: context.requestSignal,
|
|
291
341
|
}),
|
|
292
342
|
timeoutError: new ConnectorCallError("timeout", `Tool call timed out after ${context.timeoutMs}ms`),
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// inside the attempt where it feeds health.
|
|
298
|
-
assertRawMcpSuccess(resolved.connector.kind, raw);
|
|
299
|
-
observedResult = unwrapMcpResult(resolved.connector.kind, raw);
|
|
300
|
-
result = context.unwrapResult ? observedResult : raw;
|
|
343
|
+
})
|
|
344
|
+
: await dispatch();
|
|
345
|
+
if (error)
|
|
346
|
+
return failed(error);
|
|
301
347
|
}
|
|
302
348
|
catch (error) {
|
|
303
|
-
|
|
304
|
-
attemptError = error;
|
|
305
|
-
}
|
|
306
|
-
finally {
|
|
307
|
-
permit?.release();
|
|
308
|
-
}
|
|
309
|
-
if (attemptFailed) {
|
|
310
|
-
const callerCancelled = isCallerCancellation(attemptError, context.requestSignal);
|
|
311
|
-
const details = callerCancelled
|
|
349
|
+
return failed(context.requestSignal?.aborted
|
|
312
350
|
? callerCancelledDetails()
|
|
313
|
-
: classifyCallError(
|
|
314
|
-
return failed(details);
|
|
351
|
+
: classifyCallError(error));
|
|
315
352
|
}
|
|
353
|
+
// A dispatch that returned no refusal resolved a concrete tool.
|
|
354
|
+
const completed = resolved;
|
|
355
|
+
if (!completed)
|
|
356
|
+
throw new Error("Invocation completed without a resolved tool");
|
|
316
357
|
try {
|
|
317
358
|
const value = await timed((elapsed) => { resultProcessingMs += elapsed; }, async () => {
|
|
318
359
|
const processed = context.processResult
|
|
319
|
-
? await context.processResult(result,
|
|
360
|
+
? await context.processResult(result, completed)
|
|
320
361
|
: result;
|
|
321
362
|
try {
|
|
322
|
-
this.registry.observeOutputShape(
|
|
363
|
+
this.registry.observeOutputShape(completed.connector.id, completed.definition, observedResult);
|
|
323
364
|
}
|
|
324
365
|
catch {
|
|
325
366
|
// Shape learning is advisory. It cannot change a completed call.
|
|
@@ -332,14 +373,14 @@ export class InvocationService {
|
|
|
332
373
|
return {
|
|
333
374
|
ok: true,
|
|
334
375
|
value,
|
|
335
|
-
resolved,
|
|
376
|
+
resolved: completed,
|
|
336
377
|
durationMs: Date.now() - started,
|
|
337
378
|
attempts,
|
|
338
379
|
timing: diagnostics,
|
|
339
380
|
};
|
|
340
381
|
}
|
|
341
|
-
catch
|
|
342
|
-
return failed(framingError("result_processing_failed",
|
|
382
|
+
catch {
|
|
383
|
+
return failed(framingError("result_processing_failed", "The downstream call completed, but its result could not be processed. Do not repeat the call to recover its result."));
|
|
343
384
|
}
|
|
344
385
|
}
|
|
345
386
|
}
|
package/dist/mcp-result.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { boundedEchoText } from "./errors.js";
|
|
1
2
|
/**
|
|
2
3
|
* Unwrap an MCP CallToolResult into an ordinary JavaScript value:
|
|
3
4
|
* `toolResult` wins when present, then `structuredContent`; all-text content is
|
|
@@ -17,9 +18,9 @@ export function unwrapMcpResult(kind, result) {
|
|
|
17
18
|
.filter((c) => c.type === "text")
|
|
18
19
|
.map((c) => c.text ?? "")
|
|
19
20
|
.join("\n");
|
|
20
|
-
throw new Error(text || "Tool call failed");
|
|
21
|
+
throw new Error(boundedEchoText(text || "Tool call failed"));
|
|
21
22
|
}
|
|
22
|
-
if (r.structuredContent
|
|
23
|
+
if (r.structuredContent !== undefined)
|
|
23
24
|
return r.structuredContent;
|
|
24
25
|
if (content.length > 0 && content.every((c) => c.type === "text")) {
|
|
25
26
|
const text = content.map((c) => c.text ?? "").join("\n");
|