@zackbart/connecta 0.18.3 → 0.19.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 (69) hide show
  1. package/CHANGELOG.md +73 -4
  2. package/dist/catalog-service.d.ts +16 -13
  3. package/dist/catalog-service.js +106 -115
  4. package/dist/catalog.js +29 -46
  5. package/dist/connector-scope.js +2 -7
  6. package/dist/connectors/api.d.ts +4 -16
  7. package/dist/connectors/api.js +19 -46
  8. package/dist/connectors/guarded-fetch.d.ts +9 -23
  9. package/dist/connectors/guarded-fetch.js +38 -76
  10. package/dist/connectors/remote-mcp.js +36 -79
  11. package/dist/errors.d.ts +6 -27
  12. package/dist/errors.js +8 -5
  13. package/dist/execute.d.ts +24 -22
  14. package/dist/execute.js +98 -145
  15. package/dist/executor-result.d.ts +1 -0
  16. package/dist/executor-result.js +4 -11
  17. package/dist/executors/quickjs-child.js +1 -3
  18. package/dist/executors/quickjs-runtime.js +1 -3
  19. package/dist/executors/quickjs.js +1 -3
  20. package/dist/index.js +27 -57
  21. package/dist/invocation.js +113 -183
  22. package/dist/meta-tools.d.ts +15 -28
  23. package/dist/meta-tools.js +33 -89
  24. package/dist/providers/cloudflare.d.ts +2 -18
  25. package/dist/providers/cloudflare.js +1460 -2451
  26. package/dist/providers/linear.d.ts +4 -41
  27. package/dist/providers/linear.js +8 -39
  28. package/dist/providers/mixpanel.d.ts +3 -25
  29. package/dist/providers/mixpanel.js +7 -22
  30. package/dist/providers/notion.d.ts +1 -15
  31. package/dist/providers/notion.js +44 -173
  32. package/dist/providers/revenuecat.d.ts +4 -57
  33. package/dist/providers/revenuecat.js +10 -93
  34. package/dist/providers/stripe.d.ts +1 -12
  35. package/dist/providers/stripe.js +7 -45
  36. package/dist/registry.d.ts +9 -34
  37. package/dist/registry.js +9 -103
  38. package/dist/routes/mcp.js +1 -1
  39. package/dist/routes/oauth.js +3 -3
  40. package/dist/routes/shared.d.ts +15 -15
  41. package/dist/routes/shared.js +1 -3
  42. package/dist/timeout.d.ts +8 -7
  43. package/dist/timeout.js +47 -38
  44. package/dist/types.d.ts +3 -3
  45. package/dist/ui.d.ts +1 -25
  46. package/dist/ui.js +18 -45
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/documentation/call-admission.md +1 -1
  50. package/documentation/cloudflare.md +1 -1
  51. package/documentation/code-mode.md +7 -7
  52. package/documentation/connectors.md +24 -1
  53. package/documentation/linear.md +1 -1
  54. package/documentation/mixpanel.md +1 -1
  55. package/documentation/notion.md +1 -1
  56. package/documentation/operations.md +18 -14
  57. package/documentation/provider-conventions.md +1 -1
  58. package/documentation/revenuecat.md +1 -1
  59. package/documentation/stripe.md +1 -1
  60. package/documentation/upgrading.md +13 -4
  61. package/ethos.md +75 -121
  62. package/package.json +3 -4
  63. package/templates/node/package.json +1 -1
  64. package/documentation/code-first-exploration.md +0 -292
  65. package/documentation/mcp-2026-07-28.md +0 -46
  66. package/documentation/mcp-ui-design.md +0 -382
  67. package/documentation/program-ui-read-calls.md +0 -213
  68. package/documentation/provider-audit.md +0 -198
  69. package/documentation/rich-output-design.md +0 -211
@@ -4,7 +4,20 @@ import { classifyCallError, ConnectorCallError, echoedCallArgs, framingError, }
4
4
  import { unwrapMcpResult } from "./mcp-result.js";
5
5
  import { splitAddress } from "./registry.js";
6
6
  import { isExplicitlyReadOnly } from "./tool-safety.js";
7
+ import { sleep, withDeadline } from "./timeout.js";
7
8
  import { validateToolInput } from "./validate.js";
9
+ function defined(values) {
10
+ return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
11
+ }
12
+ async function timed(bucket, fn) {
13
+ const started = Date.now();
14
+ try {
15
+ return await fn();
16
+ }
17
+ finally {
18
+ bucket(Date.now() - started);
19
+ }
20
+ }
8
21
  /**
9
22
  * The longest the engine will park a synchronous inbound request in *waiting
10
23
  * alone*. The engine already treats ~15 s as the outer bound of one reasonable
@@ -109,18 +122,16 @@ export class InvocationService {
109
122
  this.activity = activity;
110
123
  }
111
124
  async invoke(address, args, context) {
112
- return this.invokeWithResolution(address, args, context, () => this.catalog.resolveTool(address, context.requestSignal !== undefined
113
- ? { signal: context.requestSignal }
114
- : {}));
125
+ const options = defined({ signal: context.requestSignal });
126
+ return this.invokeWithResolution(address, args, context, () => this.catalog.resolveTool(address, options));
115
127
  }
116
128
  /**
117
129
  * Code-mode namespace dispatch preserves JavaScript-safe tool aliases while
118
130
  * still feeding the resolved catalog entry through the one invocation path.
119
131
  */
120
132
  async invokeToolAlias(connectorId, toolAlias, aliasFor, args, context) {
121
- return this.invokeWithResolution(`${connectorId}.${toolAlias}`, args, context, () => this.catalog.resolveToolAlias(connectorId, toolAlias, aliasFor, context.requestSignal !== undefined
122
- ? { signal: context.requestSignal }
123
- : {}));
133
+ const options = defined({ signal: context.requestSignal });
134
+ return this.invokeWithResolution(`${connectorId}.${toolAlias}`, args, context, () => this.catalog.resolveToolAlias(connectorId, toolAlias, aliasFor, options));
124
135
  }
125
136
  async invokeWithResolution(address, args, context, resolve) {
126
137
  const started = Date.now();
@@ -161,37 +172,35 @@ export class InvocationService {
161
172
  outcome,
162
173
  durationMs: Date.now() - started,
163
174
  attempts,
164
- ...(classification.errorCode
165
- ? { errorCode: classification.errorCode }
166
- : {}),
167
- ...(classification.friction
168
- ? { friction: classification.friction }
169
- : {}),
175
+ ...defined({
176
+ errorCode: classification.errorCode,
177
+ friction: classification.friction,
178
+ }),
170
179
  });
171
180
  };
172
- const failed = (error) => {
173
- const diagnostics = timing();
174
- const target = resolved ?? activityTarget;
175
- const echoed = error.code === "destructive_tool_requires_approval"
176
- ? echoedCallArgs(args)
177
- : {};
178
- const details = error.code === "destructive_tool_requires_approval" && target
179
- ? {
180
- ...error,
181
- nextAction: {
182
- tool: "call_destructive_tool",
183
- arguments: {
184
- address: `${target.connector.id}.${target.toolName}`,
185
- ...echoed,
181
+ const enrich = (error, target) => {
182
+ if (!target)
183
+ return error;
184
+ switch (error.code) {
185
+ case "destructive_tool_requires_approval": {
186
+ const echoed = echoedCallArgs(args);
187
+ return {
188
+ ...error,
189
+ nextAction: {
190
+ tool: "call_destructive_tool",
191
+ arguments: {
192
+ address: `${target.connector.id}.${target.toolName}`,
193
+ ...echoed,
194
+ },
195
+ purpose: "Ask the MCP host to approve this consequential call. " +
196
+ ("args" in echoed
197
+ ? "Re-send these arguments and add a short reason for the human reviewer."
198
+ : "Re-send the arguments you just sent — they are too large to echo back — and add a short reason for the human reviewer."),
186
199
  },
187
- purpose: "Ask the MCP host to approve this consequential call. " +
188
- ("args" in echoed
189
- ? "Re-send these arguments and add a short reason for the human reviewer."
190
- : "Re-send the arguments you just sent — they are too large to echo back — and add a short reason for the human reviewer."),
191
- },
200
+ };
192
201
  }
193
- : error.code === "auth_required" && target
194
- ? {
202
+ case "auth_required":
203
+ return {
195
204
  ...error,
196
205
  connector: target.connector.id,
197
206
  operation: `${target.connector.id}.${target.toolName}`,
@@ -203,20 +212,28 @@ export class InvocationService {
203
212
  },
204
213
  retry: `Retry ${target.connector.id}.${target.toolName} after ` +
205
214
  "the operator completes recovery.",
206
- }
207
- : error.code === "invalid_args" && error.validation && target
208
- ? {
209
- ...error,
215
+ };
216
+ case "invalid_args":
217
+ if (!error.validation)
218
+ return error;
219
+ return {
220
+ ...error,
221
+ connector: target.connector.id,
222
+ operation: `${target.connector.id}.${target.toolName}`,
223
+ nextAction: this.catalog.searchRecovery({
224
+ query: target.toolName,
210
225
  connector: target.connector.id,
211
- operation: `${target.connector.id}.${target.toolName}`,
212
- nextAction: this.catalog.searchRecovery({
213
- query: target.toolName,
214
- connector: target.connector.id,
215
- }, "Inspect the current input shape if the validation findings are not sufficient."),
216
- retry: `Correct the listed arguments and retry ` +
217
- `${target.connector.id}.${target.toolName}.`,
218
- }
219
- : error;
226
+ }, "Inspect the current input shape if the validation findings are not sufficient."),
227
+ retry: `Correct the listed arguments and retry ` +
228
+ `${target.connector.id}.${target.toolName}.`,
229
+ };
230
+ default:
231
+ return error;
232
+ }
233
+ };
234
+ const failed = (error) => {
235
+ const diagnostics = timing();
236
+ const details = enrich(error, resolved ?? activityTarget);
220
237
  record(details.code === "timeout"
221
238
  ? "timeout"
222
239
  : details.code === "cancelled"
@@ -227,7 +244,7 @@ export class InvocationService {
227
244
  durationMs: Date.now() - started,
228
245
  attempts,
229
246
  timing: diagnostics,
230
- ...(resolved ? { resolved } : {}),
247
+ ...defined({ resolved }),
231
248
  error: details,
232
249
  };
233
250
  };
@@ -240,21 +257,8 @@ export class InvocationService {
240
257
  toolName: resolution.toolName,
241
258
  };
242
259
  }
243
- if (resolution.cause && resolution.connector) {
244
- if (context.requestSignal?.aborted) {
245
- return failed(callerCancelledDetails());
246
- }
247
- // A connector whose catalog cannot be fetched is as unusable as one
248
- // whose execution fails, so it feeds health accounting the same way the
249
- // attempt catch below does — otherwise a connector every call fails
250
- // against (a revoked downstream grant, say) still reads clean in the
251
- // deployment's health log.
252
- //
253
- // Recorded HERE rather than inside the registry's catalog fetch because
254
- // a cache hit that avoids a live listTools call records nothing — it is
255
- // not evidence of health. Success stays what it has always been: an
256
- // actual downstream call that returned.
257
- this.registry.recordFailure(resolution.connector.id, Date.now() - started, resolution.cause);
260
+ if (resolution.cause && context.requestSignal?.aborted) {
261
+ return failed(callerCancelledDetails());
258
262
  }
259
263
  return failed(resolution.error);
260
264
  }
@@ -292,95 +296,49 @@ export class InvocationService {
292
296
  while (true) {
293
297
  attempts++;
294
298
  let permit;
295
- const controller = context.timeoutMs || context.requestSignal
296
- ? new AbortController()
297
- : undefined;
298
- const forwardAbort = () => controller?.abort(context.requestSignal?.reason);
299
- if (context.requestSignal?.aborted)
300
- forwardAbort();
301
- else {
302
- context.requestSignal?.addEventListener("abort", forwardAbort, {
303
- once: true,
304
- });
305
- }
306
- let timer;
307
- let onAbort;
308
299
  let attemptError;
309
300
  let attemptFailed = false;
310
301
  try {
311
- const admissionStarted = Date.now();
312
- try {
313
- permit = await this.registry.admitCall(resolved.connector.id, {
314
- toolName: resolved.toolName,
315
- args: args ?? {},
316
- ...(context.requestSignal !== undefined
317
- ? { signal: context.requestSignal }
318
- : {}),
302
+ permit = await timed((elapsed) => { admissionMs += elapsed; }, () => this.registry.admitCall(resolved.connector.id, {
303
+ toolName: resolved.toolName,
304
+ args: args ?? {},
305
+ ...defined({ signal: context.requestSignal }),
306
+ }));
307
+ const raw = await timed((elapsed) => { connectorMs += elapsed; }, () => {
308
+ const call = (callSignal) => {
309
+ const connectorContext = this.registry.contextFor(resolved.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs }));
310
+ if (resolved.connector.credential &&
311
+ !connectorContext.credential) {
312
+ throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
313
+ `authorize_connector({ connector: "${resolved.connector.id}" }).`);
314
+ }
315
+ // Cancellation can arrive during admission or context construction.
316
+ if (callSignal?.aborted)
317
+ throw callSignal.reason;
318
+ return resolved.connector.callTool(resolved.toolName, args ?? {}, connectorContext);
319
+ };
320
+ if (!context.timeoutMs && !context.requestSignal)
321
+ return call();
322
+ return withDeadline(call, {
323
+ ...defined({
324
+ timeoutMs: context.timeoutMs,
325
+ signal: context.requestSignal,
326
+ }),
327
+ timeoutError: new ConnectorCallError("timeout", `Tool call timed out after ${context.timeoutMs}ms`),
319
328
  });
320
- }
321
- finally {
322
- admissionMs += Date.now() - admissionStarted;
323
- }
324
- const connectorContext = this.registry.contextFor(resolved.connector.id, this.catalog.baseUrl, this.catalog.requestScope, {
325
- ...(controller?.signal !== undefined
326
- ? { signal: controller.signal }
327
- : {}),
328
- ...(context.timeoutMs !== undefined
329
- ? { timeoutMs: context.timeoutMs }
330
- : {}),
331
329
  });
332
- if (resolved.connector.credential && !connectorContext.credential) {
333
- throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
334
- `authorize_connector({ connector: "${resolved.connector.id}" }).`);
335
- }
336
- let rejectCancelled;
337
- const cancelled = controller
338
- ? new Promise((_, reject) => {
339
- rejectCancelled = reject;
340
- })
341
- : undefined;
342
- onAbort = () => {
343
- rejectCancelled(controller?.signal.reason ??
344
- new ConnectorCallError("timeout", "Tool call was cancelled"));
345
- };
346
- controller?.signal.addEventListener("abort", onAbort, { once: true });
347
- if (controller?.signal.aborted)
348
- onAbort();
349
- if (controller?.signal.aborted)
350
- await cancelled;
351
- if (context.timeoutMs) {
352
- timer = setTimeout(() => {
353
- controller?.abort(new ConnectorCallError("timeout", `Tool call timed out after ${context.timeoutMs}ms`));
354
- }, context.timeoutMs);
355
- }
356
- const connectorStarted = Date.now();
357
- try {
358
- const pending = resolved.connector.callTool(resolved.toolName, args ?? {}, connectorContext);
359
- const raw = cancelled
360
- ? await Promise.race([pending, cancelled])
361
- : await pending;
362
- // isError is checked here for BOTH result shapes so every adapter
363
- // reports the same downstream-failure wording, and the throw lands
364
- // inside the attempt where it stays retry-eligible and feeds health.
365
- assertRawMcpSuccess(resolved.connector.kind, raw);
366
- observedResult = unwrapMcpResult(resolved.connector.kind, raw);
367
- result = context.unwrapResult ? observedResult : raw;
368
- }
369
- finally {
370
- connectorMs += Date.now() - connectorStarted;
371
- }
330
+ // isError is checked here for BOTH result shapes so every adapter
331
+ // reports the same downstream-failure wording, and the throw lands
332
+ // inside the attempt where it stays retry-eligible and feeds health.
333
+ assertRawMcpSuccess(resolved.connector.kind, raw);
334
+ observedResult = unwrapMcpResult(resolved.connector.kind, raw);
335
+ result = context.unwrapResult ? observedResult : raw;
372
336
  }
373
337
  catch (error) {
374
338
  attemptFailed = true;
375
339
  attemptError = error;
376
340
  }
377
341
  finally {
378
- if (timer)
379
- clearTimeout(timer);
380
- if (onAbort) {
381
- controller?.signal.removeEventListener("abort", onAbort);
382
- }
383
- context.requestSignal?.removeEventListener("abort", forwardAbort);
384
342
  permit?.release();
385
343
  }
386
344
  if (attemptFailed) {
@@ -394,59 +352,32 @@ export class InvocationService {
394
352
  details.retryable) {
395
353
  const wait = retryBackoffMs(attempts, details.retryAfterMs);
396
354
  if (wait !== undefined) {
397
- const backoffStarted = Date.now();
398
- if (wait > 0) {
399
- const completed = await new Promise((resolve) => {
400
- let settled = false;
401
- const finish = (value) => {
402
- if (settled)
403
- return;
404
- settled = true;
405
- clearTimeout(timer);
406
- context.requestSignal?.removeEventListener("abort", cancel);
407
- resolve(value);
408
- };
409
- const timer = setTimeout(() => finish(true), wait);
410
- const cancel = () => finish(false);
411
- context.requestSignal?.addEventListener("abort", cancel, {
412
- once: true,
413
- });
414
- if (context.requestSignal?.aborted)
415
- cancel();
416
- });
417
- backoffMs += Date.now() - backoffStarted;
418
- if (!completed)
419
- return failed(callerCancelledDetails());
420
- }
421
- else {
422
- backoffMs += Date.now() - backoffStarted;
423
- }
355
+ const completed = await timed((elapsed) => { backoffMs += elapsed; }, () => sleep(wait, context.requestSignal));
356
+ if (!completed)
357
+ return failed(callerCancelledDetails());
424
358
  continue;
425
359
  }
426
360
  // The reported window is longer than the engine will park a
427
361
  // synchronous request for. Fall through to failure with
428
362
  // retryAfterMs reported verbatim so the agent can re-issue.
429
363
  }
430
- if (!callerCancelled && !isCallAdmissionError(attemptError)) {
431
- this.registry.recordFailure(resolved.connector.id, Date.now() - started, attemptError);
432
- }
433
364
  return failed(details);
434
365
  }
435
366
  break;
436
367
  }
437
- this.registry.recordSuccess(resolved.connector.id, Date.now() - started);
438
- const processingStarted = Date.now();
439
368
  try {
440
- const value = context.processResult
441
- ? await context.processResult(result, resolved)
442
- : result;
443
- try {
444
- this.registry.observeOutputShape(resolved.connector.id, resolved.definition, observedResult);
445
- }
446
- catch {
447
- // Shape learning is advisory. It cannot change a completed call.
448
- }
449
- resultProcessingMs += Date.now() - processingStarted;
369
+ const value = await timed((elapsed) => { resultProcessingMs += elapsed; }, async () => {
370
+ const processed = context.processResult
371
+ ? await context.processResult(result, resolved)
372
+ : result;
373
+ try {
374
+ this.registry.observeOutputShape(resolved.connector.id, resolved.definition, observedResult);
375
+ }
376
+ catch {
377
+ // Shape learning is advisory. It cannot change a completed call.
378
+ }
379
+ return processed;
380
+ });
450
381
  const diagnostics = timing();
451
382
  const friction = context.activityFriction?.(value);
452
383
  record("success", friction ? { friction } : {});
@@ -460,7 +391,6 @@ export class InvocationService {
460
391
  };
461
392
  }
462
393
  catch (error) {
463
- resultProcessingMs += Date.now() - processingStarted;
464
394
  return failed(framingError("result_processing_failed", error instanceof Error ? error.message : String(error)));
465
395
  }
466
396
  }
@@ -15,7 +15,7 @@ export interface ToolResult {
15
15
  structuredContent?: Record<string, unknown>;
16
16
  [x: string]: unknown;
17
17
  }
18
- export declare function jsonResult(obj: unknown): ToolResult;
18
+ export declare function jsonResult(obj: unknown, text?: string): ToolResult;
19
19
  export declare function errorResult(message: string): ToolResult;
20
20
  /**
21
21
  * Move a byte `offset` back to the nearest UTF-8 codepoint boundary in
@@ -30,21 +30,8 @@ export declare function errorResult(message: string): ToolResult;
30
30
  * character there to split.
31
31
  */
32
32
  export declare function alignStartToCharBoundary(bytes: Uint8Array, offset: number): number;
33
- /**
34
- * Move a byte `end` back to the nearest UTF-8 codepoint boundary in
35
- * `(offset, total]`, so decoding `bytes[offset, end)` never splits a codepoint
36
- * (which would emit U+FFFD and break byte-exact reassembly). If backing up
37
- * would make no progress — a single codepoint wider than the window — extend
38
- * forward to the end of that codepoint instead so paging always advances.
39
- * Assumes `offset` is itself a codepoint boundary (offsets are the prior
40
- * `nextOffset`, which this function guarantees, and 0 is always a boundary).
41
- *
42
- * The return is always `> offset` while `offset < total`, whatever `end` is
43
- * asked for. That is the belt-and-braces half of issue #32: cap validation
44
- * keeps an empty window from arising in the first place, and this keeps an
45
- * empty window from turning into a `nextOffset === offset` paging loop if one
46
- * ever does. Exported for direct testing of that invariant.
47
- */
33
+ /** End boundary for UTF-8-safe, forward-progressing result pages. See
34
+ * documentation/meta-tools.md#result-representation. */
48
35
  export declare function alignEndToCharBoundary(bytes: Uint8Array, offset: number, end: number, total: number): number;
49
36
  export interface SearchArgs {
50
37
  query?: string;
@@ -105,16 +92,16 @@ export interface SkillArgs {
105
92
  */
106
93
  export declare function createMetaTools(registry: RegistryView, baseUrl: string, opts?: {
107
94
  /** Deadline applied when a call passes no `timeoutMs`. Off when unset. */
108
- defaultToolTimeoutMs?: number;
95
+ defaultToolTimeoutMs?: number | undefined;
109
96
  /** Per-connector deadline for the search/describe probe fan-out. Default 30_000. */
110
- probeTimeoutMs?: number;
97
+ probeTimeoutMs?: number | undefined;
111
98
  /** Maximum simultaneous connector discovery operations. Default 4. */
112
- discoveryConcurrency?: number;
113
- activity?: ActivityRequestContext;
99
+ discoveryConcurrency?: number | undefined;
100
+ activity?: ActivityRequestContext | undefined;
114
101
  /** Inbound request cancellation shared by every call this request makes. */
115
- requestSignal?: AbortSignal;
102
+ requestSignal?: AbortSignal | undefined;
116
103
  /** Runtime-owned tail for stale catalog refreshes. */
117
- defer?: DeferredWork;
104
+ defer?: DeferredWork | undefined;
118
105
  }): {
119
106
  skills(args?: SkillArgs): Promise<ToolResult>;
120
107
  searchTools(args: SearchArgs): Promise<ToolResult>;
@@ -133,10 +120,10 @@ export declare function createMetaTools(registry: RegistryView, baseUrl: string,
133
120
  */
134
121
  export declare function registerMetaTools(server: McpServer, registry: RegistryView, ctx: {
135
122
  baseUrl: string;
136
- defaultToolTimeoutMs?: number;
137
- probeTimeoutMs?: number;
138
- discoveryConcurrency?: number;
139
- activity?: ActivityRequestContext;
140
- requestSignal?: AbortSignal;
141
- defer?: DeferredWork;
123
+ defaultToolTimeoutMs?: number | undefined;
124
+ probeTimeoutMs?: number | undefined;
125
+ discoveryConcurrency?: number | undefined;
126
+ activity?: ActivityRequestContext | undefined;
127
+ requestSignal?: AbortSignal | undefined;
128
+ defer?: DeferredWork | undefined;
142
129
  }): void;