@zackbart/connecta 0.7.8 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +67 -144
  3. package/dist/activity.d.ts +1 -1
  4. package/dist/activity.d.ts.map +1 -1
  5. package/dist/activity.js.map +1 -1
  6. package/dist/auth/bearer.d.ts +1 -1
  7. package/dist/auth/bearer.js +4 -4
  8. package/dist/auth/bearer.js.map +1 -1
  9. package/dist/auth/clerk.d.ts +1 -1
  10. package/dist/auth/clerk.js +1 -1
  11. package/dist/call-admission.d.ts +81 -0
  12. package/dist/call-admission.d.ts.map +1 -0
  13. package/dist/call-admission.js +345 -0
  14. package/dist/call-admission.js.map +1 -0
  15. package/dist/catalog-fingerprint.d.ts +20 -0
  16. package/dist/catalog-fingerprint.d.ts.map +1 -0
  17. package/dist/catalog-fingerprint.js +34 -0
  18. package/dist/catalog-fingerprint.js.map +1 -0
  19. package/dist/catalog-limits.d.ts +10 -0
  20. package/dist/catalog-limits.d.ts.map +1 -0
  21. package/dist/catalog-limits.js +10 -0
  22. package/dist/catalog-limits.js.map +1 -0
  23. package/dist/catalog-service.d.ts +138 -0
  24. package/dist/catalog-service.d.ts.map +1 -0
  25. package/dist/catalog-service.js +407 -0
  26. package/dist/catalog-service.js.map +1 -0
  27. package/dist/catalog.js +2 -2
  28. package/dist/catalog.js.map +1 -1
  29. package/dist/concurrency.d.ts +4 -0
  30. package/dist/concurrency.d.ts.map +1 -0
  31. package/dist/concurrency.js +31 -0
  32. package/dist/concurrency.js.map +1 -0
  33. package/dist/connectors/api.d.ts +3 -1
  34. package/dist/connectors/api.d.ts.map +1 -1
  35. package/dist/connectors/api.js +22 -11
  36. package/dist/connectors/api.js.map +1 -1
  37. package/dist/connectors/remote-mcp.d.ts +3 -1
  38. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  39. package/dist/connectors/remote-mcp.js +44 -18
  40. package/dist/connectors/remote-mcp.js.map +1 -1
  41. package/dist/credential-health.d.ts +0 -22
  42. package/dist/credential-health.d.ts.map +1 -1
  43. package/dist/credential-health.js +14 -10
  44. package/dist/credential-health.js.map +1 -1
  45. package/dist/credentials.d.ts +3 -2
  46. package/dist/credentials.d.ts.map +1 -1
  47. package/dist/errors.d.ts +1 -1
  48. package/dist/errors.d.ts.map +1 -1
  49. package/dist/errors.js.map +1 -1
  50. package/dist/execute.d.ts +9 -8
  51. package/dist/execute.d.ts.map +1 -1
  52. package/dist/execute.js +144 -291
  53. package/dist/execute.js.map +1 -1
  54. package/dist/executor-result.d.ts +0 -3
  55. package/dist/executor-result.d.ts.map +1 -1
  56. package/dist/executor-result.js +2 -2
  57. package/dist/executor-result.js.map +1 -1
  58. package/dist/executors/quickjs-child.js +3 -3
  59. package/dist/executors/quickjs-child.js.map +1 -1
  60. package/dist/executors/quickjs-protocol.d.ts +4 -1
  61. package/dist/executors/quickjs-protocol.d.ts.map +1 -1
  62. package/dist/executors/quickjs-runtime.d.ts.map +1 -1
  63. package/dist/executors/quickjs-runtime.js +7 -2
  64. package/dist/executors/quickjs-runtime.js.map +1 -1
  65. package/dist/executors/quickjs.d.ts.map +1 -1
  66. package/dist/executors/quickjs.js +4 -1
  67. package/dist/executors/quickjs.js.map +1 -1
  68. package/dist/favicon.js +2 -2
  69. package/dist/favicon.js.map +1 -1
  70. package/dist/index.d.ts +6 -1
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +45 -17
  73. package/dist/index.js.map +1 -1
  74. package/dist/invocation.d.ts +96 -0
  75. package/dist/invocation.d.ts.map +1 -0
  76. package/dist/invocation.js +369 -0
  77. package/dist/invocation.js.map +1 -0
  78. package/dist/meta-tools.d.ts +11 -86
  79. package/dist/meta-tools.d.ts.map +1 -1
  80. package/dist/meta-tools.js +145 -485
  81. package/dist/meta-tools.js.map +1 -1
  82. package/dist/operator-ui/generated.d.ts +3 -0
  83. package/dist/operator-ui/generated.d.ts.map +1 -0
  84. package/dist/operator-ui/generated.js +5 -0
  85. package/dist/operator-ui/generated.js.map +1 -0
  86. package/dist/operator-ui/model.d.ts +94 -0
  87. package/dist/operator-ui/model.d.ts.map +1 -0
  88. package/dist/operator-ui/model.js +28 -0
  89. package/dist/operator-ui/model.js.map +1 -0
  90. package/dist/registry.d.ts +47 -6
  91. package/dist/registry.d.ts.map +1 -1
  92. package/dist/registry.js +356 -35
  93. package/dist/registry.js.map +1 -1
  94. package/dist/routes/activity.d.ts +3 -0
  95. package/dist/routes/activity.d.ts.map +1 -0
  96. package/dist/routes/activity.js +173 -0
  97. package/dist/routes/activity.js.map +1 -0
  98. package/dist/routes/credentials.d.ts +3 -0
  99. package/dist/routes/credentials.d.ts.map +1 -0
  100. package/dist/routes/credentials.js +181 -0
  101. package/dist/routes/credentials.js.map +1 -0
  102. package/dist/routes/mcp.d.ts +8 -0
  103. package/dist/routes/mcp.d.ts.map +1 -0
  104. package/dist/routes/mcp.js +383 -0
  105. package/dist/routes/mcp.js.map +1 -0
  106. package/dist/routes/oauth.d.ts +4 -0
  107. package/dist/routes/oauth.d.ts.map +1 -0
  108. package/dist/routes/oauth.js +268 -0
  109. package/dist/routes/oauth.js.map +1 -0
  110. package/dist/routes/shared.d.ts +97 -0
  111. package/dist/routes/shared.d.ts.map +1 -0
  112. package/dist/routes/shared.js +199 -0
  113. package/dist/routes/shared.js.map +1 -0
  114. package/dist/routes/ui.d.ts +3 -0
  115. package/dist/routes/ui.d.ts.map +1 -0
  116. package/dist/routes/ui.js +122 -0
  117. package/dist/routes/ui.js.map +1 -0
  118. package/dist/server.d.ts +9 -40
  119. package/dist/server.d.ts.map +1 -1
  120. package/dist/server.js +67 -1292
  121. package/dist/server.js.map +1 -1
  122. package/dist/skills.d.ts +0 -15
  123. package/dist/skills.d.ts.map +1 -1
  124. package/dist/skills.js +6 -5
  125. package/dist/skills.js.map +1 -1
  126. package/dist/storage/file.d.ts.map +1 -1
  127. package/dist/storage/file.js +3 -1
  128. package/dist/storage/file.js.map +1 -1
  129. package/dist/storage/memory.d.ts.map +1 -1
  130. package/dist/storage/memory.js +3 -1
  131. package/dist/storage/memory.js.map +1 -1
  132. package/dist/toolkits.d.ts +0 -22
  133. package/dist/toolkits.d.ts.map +1 -1
  134. package/dist/toolkits.js +2 -2
  135. package/dist/toolkits.js.map +1 -1
  136. package/dist/types.d.ts +64 -2
  137. package/dist/types.d.ts.map +1 -1
  138. package/dist/ui.d.ts +3 -93
  139. package/dist/ui.d.ts.map +1 -1
  140. package/dist/ui.js +26 -1389
  141. package/dist/ui.js.map +1 -1
  142. package/dist/version.d.ts +1 -1
  143. package/dist/version.js +1 -1
  144. package/package.json +15 -8
  145. package/src/activity.ts +5 -1
  146. package/src/auth/bearer.ts +4 -4
  147. package/src/auth/clerk.ts +1 -1
  148. package/src/call-admission.ts +522 -0
  149. package/src/catalog-fingerprint.ts +52 -0
  150. package/src/catalog-limits.ts +11 -0
  151. package/src/catalog-service.ts +601 -0
  152. package/src/catalog.ts +2 -2
  153. package/src/concurrency.ts +39 -0
  154. package/src/connectors/api.ts +25 -11
  155. package/src/connectors/remote-mcp.ts +55 -21
  156. package/src/credential-health.ts +13 -10
  157. package/src/credentials.ts +2 -2
  158. package/src/errors.ts +1 -1
  159. package/src/execute.ts +194 -377
  160. package/src/executor-result.ts +2 -2
  161. package/src/executors/quickjs-child.ts +8 -4
  162. package/src/executors/quickjs-protocol.ts +1 -1
  163. package/src/executors/quickjs-runtime.ts +5 -1
  164. package/src/executors/quickjs.ts +11 -8
  165. package/src/favicon.ts +2 -2
  166. package/src/index.ts +54 -17
  167. package/src/invocation.ts +527 -0
  168. package/src/meta-tools.ts +369 -797
  169. package/src/operator-ui/browser.css +413 -0
  170. package/src/operator-ui/browser.ts +1121 -0
  171. package/src/operator-ui/generated.ts +4 -0
  172. package/src/operator-ui/model.ts +127 -0
  173. package/src/registry.ts +489 -44
  174. package/src/routes/activity.ts +220 -0
  175. package/src/routes/credentials.ts +266 -0
  176. package/src/routes/mcp.ts +485 -0
  177. package/src/routes/oauth.ts +328 -0
  178. package/src/routes/shared.ts +330 -0
  179. package/src/routes/ui.ts +153 -0
  180. package/src/server.ts +70 -1702
  181. package/src/skills.ts +6 -5
  182. package/src/storage/file.ts +3 -1
  183. package/src/storage/memory.ts +3 -1
  184. package/src/toolkits.ts +2 -2
  185. package/src/types.ts +70 -2
  186. package/src/ui.ts +52 -1489
  187. package/src/version.ts +1 -1
@@ -1,14 +1,17 @@
1
1
  import { z } from "zod";
2
- import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
3
- import { recordToolActivity, } from "./activity.js";
2
+ import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, groupedSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
3
+ import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
4
4
  import { closeConnectorScope, } from "./connector-scope.js";
5
- import { unwrapMcpResult } from "./mcp-result.js";
6
- import { classifyCallError, ConnectorCallError, messageLooksRetryable, } from "./errors.js";
5
+ import { classifyCallError, messageLooksRetryable, } from "./errors.js";
6
+ import { InvocationService, MAX_RETRY_BACKOFF_MS, retryBackoffMs, } from "./invocation.js";
7
7
  import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
8
- import { connectorGuide, connectorSkillName, hasConnectorGuides, listSkills, resolveSkill, } from "./skills.js";
8
+ import { hasConnectorGuides, listSkills, resolveSkill, } from "./skills.js";
9
9
  import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, withAbortableTimeout, } from "./timeout.js";
10
10
  import { credentialVerdictApplies } from "./credential-health.js";
11
+ export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_RETRY_BACKOFF_MS, MAX_SEARCH_LIMIT, DiscoveryPolicyError, retryBackoffMs, };
11
12
  const RESULT_TTL_SECONDS = 900;
13
+ const enc = new TextEncoder();
14
+ const dec = new TextDecoder();
12
15
  export function jsonResult(obj) {
13
16
  return {
14
17
  content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
@@ -23,68 +26,8 @@ export function errorResult(message) {
23
26
  function msg(err) {
24
27
  return err instanceof Error ? err.message : String(err);
25
28
  }
26
- export const DEFAULT_SEARCH_LIMIT = 25;
27
- /**
28
- * A discovery page is for choosing the next tool, not exporting the catalog.
29
- * One hundred leaves room for broad browsing while keeping each deliberate
30
- * page far below the catalog sizes Connecta supports.
31
- */
32
- export const MAX_SEARCH_LIMIT = 100;
33
- /** Same one-request work bound for address-based discovery. */
34
- export const MAX_DESCRIBE_ADDRESSES = 100;
35
- /**
36
- * Final UTF-8 ceiling for a generated search/describe response. The count
37
- * limits are the ordinary guard; this catches unusually large full schemas or
38
- * descriptions that make even a bounded page expensive.
39
- */
40
- export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
41
- const enc = new TextEncoder();
42
- const dec = new TextDecoder();
43
- export class DiscoveryPolicyError extends Error {
44
- code;
45
- constructor(code, message) {
46
- super(message);
47
- this.code = code;
48
- this.name = "DiscoveryPolicyError";
49
- }
50
- }
51
- /** Validate before ranking so a huge page request does no proportional work. */
52
- export function discoverySearchLimit(value) {
53
- if (value === undefined)
54
- return DEFAULT_SEARCH_LIMIT;
55
- if (typeof value !== "number" ||
56
- !Number.isInteger(value) ||
57
- value < 1 ||
58
- value > MAX_SEARCH_LIMIT) {
59
- throw new DiscoveryPolicyError("invalid_args", `limit must be a whole number from 1 through ${MAX_SEARCH_LIMIT}. Page through larger catalogs with offset.`);
60
- }
61
- return value;
62
- }
63
- /** Validate the raw list so duplicate addresses consume the same bound. */
64
- export function discoveryAddresses(value) {
65
- if (!Array.isArray(value)) {
66
- throw new DiscoveryPolicyError("invalid_args", "addresses must be an array.");
67
- }
68
- if (value.length > MAX_DESCRIBE_ADDRESSES) {
69
- throw new DiscoveryPolicyError("invalid_args", `addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across describe_tools calls.`);
70
- }
71
- return value;
72
- }
73
- /** Serialize once and count the exact bytes jsonResult would emit. */
74
- function boundedDiscoveryText(value, hint) {
75
- const text = JSON.stringify(value, null, 2);
76
- if (text === undefined) {
77
- throw new TypeError("Discovery result is not JSON-serializable.");
78
- }
79
- const bytes = enc.encode(text).length;
80
- if (bytes > MAX_DISCOVERY_RESULT_BYTES) {
81
- throw new DiscoveryPolicyError("result_too_large", `Discovery result is ${bytes} UTF-8 bytes, over the ${MAX_DISCOVERY_RESULT_BYTES}-byte ceiling. ${hint}`);
82
- }
83
- return text;
84
- }
85
- /** Apply the same final result guard to code-mode discovery helpers. */
86
- export function assertDiscoveryResultSize(value, hint) {
87
- boundedDiscoveryText(value, hint);
29
+ function errorDetails(code, message) {
30
+ return { code, message, retryable: messageLooksRetryable(message) };
88
31
  }
89
32
  function discoveryErrorResult(error) {
90
33
  const result = jsonResult({
@@ -114,46 +57,12 @@ function discoveryResult(value, hint) {
114
57
  throw err;
115
58
  }
116
59
  }
117
- /**
118
- * The longest the engine will park a synchronous inbound request in *waiting
119
- * alone*. The engine already treats ~15 s as the outer bound of one reasonable
120
- * connector call (EXECUTE_HOST_CALL_TIMEOUT_MS), so sleeping for minutes trades
121
- * a fast, informative failure for a hung one. A connector-reported window this
122
- * long isn't truncated — it's declined (see `retryBackoffMs`) and reported
123
- * verbatim as `error.retryAfterMs`, so the agent, which can afford to wait,
124
- * decides when to re-issue.
125
- */
126
- export const MAX_RETRY_BACKOFF_MS = 10_000;
127
- /**
128
- * How long to wait before the next attempt, or `undefined` for "don't retry".
129
- *
130
- * A connector that read a `Retry-After` header knows the window exactly, so it
131
- * is honoured **exactly or not at all**: truncating an exponential *guess* is
132
- * harmless, but truncating a *known* window means deliberately retrying inside
133
- * a rate limit — the harm this channel exists to prevent. A window longer than
134
- * `MAX_RETRY_BACKOFF_MS` therefore declines the retry rather than shortening
135
- * it. (`retryAfterMs` is normalized non-negative, so `0` means "retry now".)
136
- * Connectors that report no window keep the historical exponential guess.
137
- *
138
- * Waits are per attempt, matching the per-attempt `timeoutMs` race in
139
- * `runCall`. Exported for direct testing.
140
- */
141
- export function retryBackoffMs(attempt, retryAfterMs) {
142
- if (retryAfterMs === undefined) {
143
- return Math.min(250 * 2 ** (attempt - 1), 1_000);
144
- }
145
- return retryAfterMs <= MAX_RETRY_BACKOFF_MS ? retryAfterMs : undefined;
146
- }
147
- /** Details for failures that never reached a connector (no thrown value). */
148
- function errorDetails(code, message) {
149
- return { code, message, retryable: messageLooksRetryable(message) };
150
- }
151
60
  /** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
152
61
  function isContinuationByte(b) {
153
- return (b & 0xc0) === 0x80;
62
+ return b !== undefined && (b & 0xc0) === 0x80;
154
63
  }
155
64
  /** Smallest accepted `get_result` byte offset. */
156
- export const MIN_RESULT_OFFSET = 0;
65
+ const MIN_RESULT_OFFSET = 0;
157
66
  /**
158
67
  * The one definition of a usable `get_result` offset: a whole number of bytes
159
68
  * at or past {@link MIN_RESULT_OFFSET}. Shared by the registered zod schema and
@@ -168,7 +77,7 @@ export const MIN_RESULT_OFFSET = 0;
168
77
  * error. An offset past the end of the payload stays legal: it is a whole
169
78
  * number of bytes, and it answers with an empty final page.
170
79
  */
171
- export function isValidResultOffset(value) {
80
+ function isValidResultOffset(value) {
172
81
  return Number.isInteger(value) && value >= MIN_RESULT_OFFSET;
173
82
  }
174
83
  /**
@@ -224,9 +133,10 @@ export function alignEndToCharBoundary(bytes, offset, end, total) {
224
133
  // --- fields selection (feature 2) -----------------------------------------
225
134
  /** Resolve a dot-path (segments) against a value; `key[]` maps the tail over an array. */
226
135
  function resolvePath(value, segments) {
227
- if (segments.length === 0)
136
+ const seg = segments[0];
137
+ if (seg === undefined)
228
138
  return value;
229
- const [seg, ...rest] = segments;
139
+ const rest = segments.slice(1);
230
140
  const isArr = seg.endsWith("[]");
231
141
  const key = isArr ? seg.slice(0, -2) : seg;
232
142
  let next = value;
@@ -283,7 +193,7 @@ function applyFieldsToContent(content, fields) {
283
193
  * the three give one answer to the same question. A value JSON cannot serialize
284
194
  * at all (a BigInt) still throws, as before, and is reported as a failure.
285
195
  */
286
- export function serializeResultText(value) {
196
+ function serializeResultText(value) {
287
197
  const serialized = JSON.stringify(value, null, 2);
288
198
  return serialized === undefined ? String(value) : serialized;
289
199
  }
@@ -402,224 +312,101 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
402
312
  const batchCap = registry.maxBatchResultBytes;
403
313
  const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
404
314
  const probeTimeoutMs = normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
315
+ const discoveryConcurrency = resolveDiscoveryConcurrency(opts.discoveryConcurrency);
405
316
  // createMetaTools() is called once per inbound MCP request. Sharing this
406
317
  // identity lets remote connectors reuse one downstream client inside that
407
318
  // request without leaking request-bound I/O into the next one.
408
319
  const requestScope = {};
320
+ const catalog = new CatalogService(registry, baseUrl, {
321
+ requestScope,
322
+ probeTimeoutMs,
323
+ concurrency: discoveryConcurrency,
324
+ });
325
+ const invocation = new InvocationService(registry, catalog, opts.activity);
409
326
  const withProbeDeadline = (label, operation) => withAbortableTimeout((signal) => operation({ signal, timeoutMs: probeTimeoutMs }), probeTimeoutMs, label);
410
- /** Shared call path used by call tools and batch_call: safety → fields → size guard. */
327
+ /** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
411
328
  async function runCall(call, source, options = {}) {
412
- const started = Date.now();
413
- let catalogMs = 0;
414
- let connectorMs = 0;
415
- let backoffMs = 0;
416
- let resultProcessingMs = 0;
417
- let attempts = 0;
418
- const timing = () => ({
419
- catalogMs,
420
- connectorMs,
421
- backoffMs,
422
- resultProcessingMs,
423
- totalMs: Date.now() - started,
424
- });
425
- const resolved = registry.resolveAddress(call.address);
426
- const record = (outcome, errorCode) => {
427
- if (!resolved)
428
- return;
429
- recordToolActivity(opts.activity, {
430
- connectorId: resolved.connector.id,
431
- toolName: resolved.toolName,
432
- address: `${resolved.connector.id}.${resolved.toolName}`,
433
- source,
434
- outcome,
435
- durationMs: Date.now() - started,
436
- attempts,
437
- ...(errorCode ? { errorCode } : {}),
438
- });
439
- };
440
- const failed = (error) => {
441
- const durationMs = Date.now() - started;
442
- const diagnostics = timing();
443
- record(error.code === "timeout" ? "timeout" : "error", error.code);
444
- return {
445
- toolResult: call.resultMode === "value"
446
- ? jsonResult({
447
- ok: false,
448
- error,
449
- durationMs,
450
- attempts,
451
- ...(call.diagnostics ? { timing: diagnostics } : {}),
452
- })
453
- : errorResult(error.message),
454
- durationMs,
455
- attempts,
456
- timing: diagnostics,
457
- error,
458
- };
459
- };
460
- if (!resolved) {
461
- return failed(errorDetails("unknown_address", `Unknown address "${call.address}"`));
462
- }
463
329
  const results = registry.resultsStorage();
464
- // Result-size cap for THIS call: the connector's own override wins, then
465
- // the deployment-wide value, then the built-in default (already folded
466
- // into `globalCap`). Resolved per call so one batch_call can mix a
467
- // tight-capped connector with siblings on the global cap. An override the
468
- // registry already warned about at startup is dropped here, so the
469
- // connector simply inherits `globalCap`.
470
- const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
471
330
  const fields = call.fields && call.fields.length > 0 ? call.fields : null;
472
- // An explicit per-call deadline always wins; the config default only fills
473
- // the gap, and stays off entirely when the deployment sets none.
474
331
  const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
475
- const maxRetries = Math.min(2, Math.max(0, Math.trunc(call.maxRetries ?? 0)));
476
- const catalogStarted = Date.now();
477
- let definition;
478
- try {
479
- definition = (await registry.getTools(resolved.connector.id, baseUrl, requestScope)).find((tool) => tool.name === resolved.toolName);
480
- }
481
- catch (err) {
482
- catalogMs += Date.now() - catalogStarted;
483
- // A connector whose catalog cannot be fetched is as unusable as one whose
484
- // execution fails, so it feeds health accounting the same way the
485
- // execution catch below does — otherwise a connector every call_tool
486
- // fails against (a revoked downstream grant, say) still reads clean from
487
- // the cheap `list_connectors({ probe: false })` signal.
488
- //
489
- // Recorded HERE rather than inside the registry's catalog fetch on
490
- // purpose: `registry` is this connection's VIEW, so a toolkit-scoped
491
- // session records into its own log as well as the deployment-wide one,
492
- // which `Registry.refreshTools` could not reach. A cache hit that avoids
493
- // a live listTools call therefore records nothing either way — it is not
494
- // evidence of health, and success stays what it has always been: an
495
- // actual downstream call that returned.
496
- registry.recordFailure(resolved.connector.id, Date.now() - started, err);
497
- // classifyCallError so a typed auth_required thrown while listing tools
498
- // (e.g. a revoked downstream OAuth grant) keeps its code.
499
- return failed(classifyCallError(err, "catalog_lookup_failed"));
500
- }
501
- catalogMs += Date.now() - catalogStarted;
502
- if (!definition) {
503
- return failed(errorDetails("unknown_tool", `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`));
504
- }
505
- const explicitlyReadOnly = definition.annotations?.readOnlyHint === true &&
506
- definition.annotations?.destructiveHint !== true;
507
- if (!explicitlyReadOnly && !options.allowDestructive) {
508
- return failed(errorDetails("destructive_tool_requires_approval", `Tool "${call.address}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`));
509
- }
510
- const retrySafe = definition.annotations?.readOnlyHint === true ||
511
- definition.annotations?.idempotentHint === true;
512
- let result;
513
- while (true) {
514
- attempts++;
515
- const controller = timeoutMs ? new AbortController() : undefined;
516
- let timer;
517
- const ctx = registry.contextFor(resolved.connector.id, baseUrl, requestScope, { signal: controller?.signal, timeoutMs });
518
- const connectorStarted = Date.now();
519
- try {
520
- const pending = resolved.connector.callTool(resolved.toolName, call.args ?? {}, ctx);
521
- result = timeoutMs
522
- ? await Promise.race([
523
- pending,
524
- new Promise((_, reject) => {
525
- timer = setTimeout(() => {
526
- reject(new ConnectorCallError("timeout", `Tool call timed out after ${timeoutMs}ms`));
527
- controller?.abort();
528
- }, timeoutMs);
529
- }),
530
- ])
531
- : await pending;
532
- connectorMs += Date.now() - connectorStarted;
533
- const mcpResult = result;
534
- if (resolved.connector.kind === "mcp" && mcpResult?.isError) {
535
- throw new Error(mcpResult.content?.map((block) => block.text).join("") ||
536
- "Downstream tool call failed");
332
+ const outcome = await invocation.invoke(call.address, call.args ?? {}, {
333
+ source,
334
+ ...(options.allowDestructive !== undefined
335
+ ? { allowDestructive: options.allowDestructive }
336
+ : {}),
337
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
338
+ ...(call.maxRetries !== undefined
339
+ ? { maxRetries: call.maxRetries }
340
+ : {}),
341
+ ...(opts.requestSignal !== undefined
342
+ ? { requestSignal: opts.requestSignal }
343
+ : {}),
344
+ unwrapResult: call.resultMode === "value",
345
+ processResult: async (result, resolved) => {
346
+ // Result-size cap for THIS call: the connector's own override wins,
347
+ // then the deployment-wide value, then the built-in default (already
348
+ // folded into `globalCap`). Resolved per call so one batch_call can
349
+ // mix a tight-capped connector with siblings on the global cap. An
350
+ // override the registry already warned about at startup is dropped
351
+ // here, so the connector simply inherits `globalCap`.
352
+ const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
353
+ if (call.resultMode === "value") {
354
+ let value = fields ? applyFields(result, fields) : result;
355
+ value = await guardValue(value, results, cap);
356
+ return {
357
+ toolResult: jsonResult({ ok: true, data: value }),
358
+ value,
359
+ };
537
360
  }
538
- break;
539
- }
540
- catch (err) {
541
- // Includes connector setup, downstream execution, and timeout wait.
542
- connectorMs += Date.now() - connectorStarted;
543
- const details = classifyCallError(err);
544
- if (attempts <= maxRetries && retrySafe && details.retryable) {
545
- const wait = retryBackoffMs(attempts, details.retryAfterMs);
546
- if (wait !== undefined) {
547
- const backoffStarted = Date.now();
548
- if (wait > 0) {
549
- await new Promise((resolve) => setTimeout(resolve, wait));
550
- }
551
- backoffMs += Date.now() - backoffStarted;
552
- continue;
553
- }
554
- // The reported window is longer than the engine will park a
555
- // synchronous request for. Fall through to failure with
556
- // retryAfterMs reported verbatim so the agent can re-issue.
361
+ if (resolved.connector.kind === "mcp") {
362
+ const mcpResult = result;
363
+ let content = mcpResult?.content ?? [];
364
+ if (fields)
365
+ content = applyFieldsToContent(content, fields);
366
+ return { toolResult: await guardContent(content, results, cap) };
557
367
  }
558
- registry.recordFailure(resolved.connector.id, Date.now() - started, err);
559
- return failed(details);
560
- }
561
- finally {
562
- if (timer)
563
- clearTimeout(timer);
564
- }
565
- }
566
- registry.recordSuccess(resolved.connector.id, Date.now() - started);
567
- const processingStarted = Date.now();
568
- try {
569
- const mr = result;
570
- if (call.resultMode === "value") {
571
- let value = unwrapMcpResult(resolved.connector.kind, result);
572
- if (fields)
573
- value = applyFields(value, fields);
574
- value = await guardValue(value, results, cap);
575
- resultProcessingMs += Date.now() - processingStarted;
576
- const durationMs = Date.now() - started;
577
- const diagnostics = timing();
578
- record("success");
368
+ const value = fields ? applyFields(result, fields) : result;
579
369
  return {
580
- toolResult: jsonResult({
581
- ok: true,
582
- data: value,
583
- durationMs,
584
- attempts,
585
- ...(call.diagnostics ? { timing: diagnostics } : {}),
586
- }),
587
- durationMs,
588
- attempts,
589
- timing: diagnostics,
370
+ toolResult: await guardText(serializeResultText(value), results, cap),
590
371
  value,
591
372
  };
592
- }
593
- if (resolved.connector.kind === "mcp") {
594
- let content = mr?.content ?? [];
595
- if (fields)
596
- content = applyFieldsToContent(content, fields);
597
- const toolResult = await guardContent(content, results, cap);
598
- resultProcessingMs += Date.now() - processingStarted;
599
- record("success");
600
- return {
601
- toolResult,
602
- durationMs: Date.now() - started,
603
- attempts,
604
- timing: timing(),
605
- };
606
- }
607
- const value = fields ? applyFields(result, fields) : result;
608
- const toolResult = await guardText(serializeResultText(value), results, cap);
609
- resultProcessingMs += Date.now() - processingStarted;
610
- record("success");
373
+ },
374
+ });
375
+ if (!outcome.ok) {
611
376
  return {
612
- toolResult,
613
- durationMs: Date.now() - started,
614
- attempts,
615
- timing: timing(),
616
- value,
377
+ toolResult: call.resultMode === "value"
378
+ ? jsonResult({
379
+ ok: false,
380
+ error: outcome.error,
381
+ durationMs: outcome.durationMs,
382
+ attempts: outcome.attempts,
383
+ ...(call.diagnostics ? { timing: outcome.timing } : {}),
384
+ })
385
+ : errorResult(outcome.error.message),
386
+ durationMs: outcome.durationMs,
387
+ attempts: outcome.attempts,
388
+ timing: outcome.timing,
389
+ error: outcome.error,
617
390
  };
618
391
  }
619
- catch (err) {
620
- resultProcessingMs += Date.now() - processingStarted;
621
- return failed(errorDetails("result_processing_failed", msg(err)));
622
- }
392
+ const valueModeResult = call.resultMode === "value"
393
+ ? jsonResult({
394
+ ok: true,
395
+ data: outcome.value.value,
396
+ durationMs: outcome.durationMs,
397
+ attempts: outcome.attempts,
398
+ ...(call.diagnostics ? { timing: outcome.timing } : {}),
399
+ })
400
+ : outcome.value.toolResult;
401
+ return {
402
+ toolResult: valueModeResult,
403
+ durationMs: outcome.durationMs,
404
+ attempts: outcome.attempts,
405
+ timing: outcome.timing,
406
+ ...(Object.prototype.hasOwnProperty.call(outcome.value, "value")
407
+ ? { value: outcome.value.value }
408
+ : {}),
409
+ };
623
410
  }
624
411
  return {
625
412
  async skills(args = {}) {
@@ -648,7 +435,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
648
435
  // call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
649
436
  const connectors = registry.listConnectors();
650
437
  const scope = probe ? {} : requestScope;
651
- const pending = connectors.map(async (c) => {
438
+ const inspect = async (c) => {
652
439
  const statusStarted = Date.now();
653
440
  const observed = registry.healthFor(c.id);
654
441
  const verdict = await registry.credentialHealthFor(c.id);
@@ -761,7 +548,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
761
548
  await registry.recordCredentialHealth(c.id, {
762
549
  state: "auth_required",
763
550
  checkedAt,
764
- ...(status.message ? { message: status.message } : {}),
551
+ ...(status.message && { message: status.message }),
765
552
  ...("authorizationUrl" in status &&
766
553
  status.authorizationUrl
767
554
  ? { authorizationUrl: status.authorizationUrl }
@@ -788,19 +575,19 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
788
575
  checkedAt,
789
576
  latencyMs,
790
577
  probe,
791
- ...(latestObserved ?? observed ?? {}),
578
+ ...(latestObserved ?? observed),
792
579
  ...(credentialCheck ? { credentialCheck } : {}),
793
- ...("authorizationUrl" in status && status.authorizationUrl
794
- ? { authorizationUrl: status.authorizationUrl }
795
- : {}),
796
- ...(status.message ? { message: status.message } : {}),
580
+ ...("authorizationUrl" in status &&
581
+ status.authorizationUrl && {
582
+ authorizationUrl: status.authorizationUrl,
583
+ }),
584
+ ...(status.message && { message: status.message }),
797
585
  };
798
- });
799
- if (!probe) {
800
- return jsonResult({ connectors: await Promise.all(pending) });
586
+ };
587
+ const settled = await mapSettledWithConcurrency(connectors, discoveryConcurrency, inspect);
588
+ if (probe) {
589
+ await mapSettledWithConcurrency(connectors, discoveryConcurrency, (connector) => closeConnectorScope(connector, registry.contextFor(connector.id, baseUrl, scope), opts.defer));
801
590
  }
802
- const settled = await Promise.allSettled(pending);
803
- await Promise.all(connectors.map((connector) => closeConnectorScope(connector, registry.contextFor(connector.id, baseUrl, scope), opts.defer)));
804
591
  const out = settled.map((result) => {
805
592
  if (result.status === "rejected")
806
593
  throw result.reason;
@@ -809,10 +596,8 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
809
596
  return jsonResult({ connectors: out });
810
597
  },
811
598
  async searchTools(args) {
812
- const q = args.query ?? "";
813
- let limit;
814
599
  try {
815
- limit = discoverySearchLimit(args.limit);
600
+ return discoveryResult(groupedSearchResult(await catalog.search(args)), "Request a smaller limit, omit fullDescriptions, or use compact schemas.");
816
601
  }
817
602
  catch (err) {
818
603
  if (err instanceof DiscoveryPolicyError) {
@@ -820,103 +605,10 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
820
605
  }
821
606
  throw err;
822
607
  }
823
- const offset = Math.max(0, Math.trunc(args.offset ?? 0));
824
- const conns = args.connector
825
- ? [registry.getConnector(args.connector)].filter((c) => Boolean(c))
826
- : registry.listConnectors();
827
- const matches = [];
828
- const catalogs = await Promise.allSettled(conns.map((c) => withProbeDeadline(`search_tools probe of "${c.id}"`, (options) => registry.getTools(c.id, baseUrl, requestScope, options))));
829
- let matchMode = "all";
830
- const collectMatches = (mode) => {
831
- matches.length = 0;
832
- let orderBase = 0;
833
- catalogs.forEach((catalog, connectorIndex) => {
834
- const c = conns[connectorIndex];
835
- if (catalog.status === "fulfilled") {
836
- for (const ranked of rankTools(catalog.value, q, mode)) {
837
- matches.push({
838
- connectorId: c.id,
839
- connectorTitle: c.title,
840
- connectorDescription: c.description,
841
- ...(connectorGuide(c)
842
- ? { connectorGuideSkill: connectorSkillName(c.id) }
843
- : {}),
844
- tool: ranked.tool,
845
- score: ranked.score,
846
- order: orderBase + ranked.order,
847
- });
848
- }
849
- }
850
- orderBase +=
851
- catalog.status === "fulfilled" ? catalog.value.length : 1;
852
- });
853
- };
854
- collectMatches("all");
855
- if (q.trim() && matches.length === 0) {
856
- matchMode = "partial";
857
- collectMatches(matchMode);
858
- }
859
- matches.sort((a, b) => b.score - a.score || a.order - b.order);
860
- const page = matches.slice(offset, offset + limit);
861
- const groups = [];
862
- const byConnector = new Map();
863
- for (const match of page) {
864
- let group = byConnector.get(match.connectorId);
865
- if (!group) {
866
- group = {
867
- id: match.connectorId,
868
- ...(match.connectorTitle ? { title: match.connectorTitle } : {}),
869
- description: match.connectorDescription,
870
- ...(match.connectorGuideSkill
871
- ? { guide: match.connectorGuideSkill }
872
- : {}),
873
- tools: [],
874
- };
875
- byConnector.set(match.connectorId, group);
876
- groups.push(group);
877
- }
878
- const schema = match.tool.inputSchema ?? { type: "object" };
879
- group.tools.push({
880
- name: match.tool.name,
881
- address: `${match.connectorId}.${match.tool.name}`,
882
- description: summarizeDescription(match.tool.description, args.fullDescriptions === true),
883
- ...(args.includeSchemas
884
- ? {
885
- inputSchema: args.includeSchemas === "json"
886
- ? schema
887
- : compactSchema(schema),
888
- }
889
- : {}),
890
- ...(args.includeSchemas && match.tool.outputSchema
891
- ? {
892
- outputSchema: args.includeSchemas === "json"
893
- ? match.tool.outputSchema
894
- : compactSchema(match.tool.outputSchema),
895
- }
896
- : {}),
897
- ...(match.tool.annotations
898
- ? { annotations: match.tool.annotations }
899
- : {}),
900
- });
901
- }
902
- const nextOffset = offset + page.length < matches.length
903
- ? offset + page.length
904
- : undefined;
905
- return discoveryResult({
906
- connectors: groups,
907
- total: matches.length,
908
- offset,
909
- limit,
910
- hasMore: nextOffset !== undefined,
911
- ...(nextOffset !== undefined ? { nextOffset } : {}),
912
- ...(matchMode === "partial" && matches.length > 0
913
- ? { matchMode }
914
- : {}),
915
- }, "Request a smaller limit, omit fullDescriptions, or use compact schemas.");
916
608
  },
917
609
  async describeTools(args) {
918
610
  try {
919
- discoveryAddresses(args.addresses);
611
+ return discoveryResult({ tools: await catalog.describe(args) }, 'Split the address list or use format: "compact".');
920
612
  }
921
613
  catch (err) {
922
614
  if (err instanceof DiscoveryPolicyError) {
@@ -924,60 +616,6 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
924
616
  }
925
617
  throw err;
926
618
  }
927
- const format = args.format ?? "compact";
928
- const resolved = args.addresses.map((address) => ({
929
- address,
930
- resolved: registry.resolveAddress(address),
931
- }));
932
- const connectorIds = [
933
- ...new Set(resolved
934
- .map((entry) => entry.resolved?.connector.id)
935
- .filter((id) => Boolean(id))),
936
- ];
937
- const loaded = await Promise.allSettled(connectorIds.map((id) => withProbeDeadline(`describe_tools probe of "${id}"`, (options) => registry.getTools(id, baseUrl, requestScope, options))));
938
- const catalogs = new Map();
939
- loaded.forEach((result, index) => {
940
- catalogs.set(connectorIds[index], result.status === "fulfilled"
941
- ? result.value
942
- : result.reason instanceof Error
943
- ? result.reason
944
- : new Error(String(result.reason)));
945
- });
946
- const out = resolved.map(({ address, resolved }) => {
947
- if (!resolved) {
948
- return { address, error: `Unknown address "${address}"` };
949
- }
950
- const catalog = catalogs.get(resolved.connector.id);
951
- if (catalog instanceof Error) {
952
- return { address, error: catalog.message };
953
- }
954
- const tool = catalog?.find((t) => t.name === resolved.toolName);
955
- if (!tool) {
956
- return {
957
- address,
958
- error: `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
959
- };
960
- }
961
- const schema = tool.inputSchema ?? { type: "object" };
962
- return {
963
- address,
964
- name: tool.name,
965
- description: summarizeDescription(tool.description, args.fullDescriptions === true),
966
- ...(connectorGuide(resolved.connector)
967
- ? { guide: connectorSkillName(resolved.connector.id) }
968
- : {}),
969
- inputSchema: format === "json" ? schema : compactSchema(schema),
970
- ...(tool.outputSchema
971
- ? {
972
- outputSchema: format === "json"
973
- ? tool.outputSchema
974
- : compactSchema(tool.outputSchema),
975
- }
976
- : {}),
977
- ...(tool.annotations ? { annotations: tool.annotations } : {}),
978
- };
979
- });
980
- return discoveryResult({ tools: out }, 'Split the address list or use format: "compact".');
981
619
  },
982
620
  async callTool(args) {
983
621
  return (await runCall(args, "call_tool")).toolResult;
@@ -1036,13 +674,25 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
1036
674
  const batchStarted = Date.now();
1037
675
  const settled = await Promise.allSettled(args.calls.map((c) => runCall({
1038
676
  ...c,
1039
- resultMode: c.resultMode ?? args.resultMode,
1040
- timeoutMs: c.timeoutMs ?? args.timeoutMs,
1041
- maxRetries: c.maxRetries ?? args.maxRetries,
1042
- diagnostics: c.diagnostics ?? args.diagnostics,
677
+ ...((c.resultMode ?? args.resultMode) !== undefined
678
+ ? { resultMode: c.resultMode ?? args.resultMode }
679
+ : {}),
680
+ ...((c.timeoutMs ?? args.timeoutMs) !== undefined
681
+ ? { timeoutMs: c.timeoutMs ?? args.timeoutMs }
682
+ : {}),
683
+ ...((c.maxRetries ?? args.maxRetries) !== undefined
684
+ ? { maxRetries: c.maxRetries ?? args.maxRetries }
685
+ : {}),
686
+ ...((c.diagnostics ?? args.diagnostics) !== undefined
687
+ ? { diagnostics: c.diagnostics ?? args.diagnostics }
688
+ : {}),
1043
689
  }, "batch_call")));
1044
690
  const results = settled.map((s, i) => {
1045
- const address = args.calls[i].address;
691
+ const call = args.calls[i];
692
+ if (!call) {
693
+ throw new Error("Batch result has no corresponding call");
694
+ }
695
+ const { address } = call;
1046
696
  if (s.status === "rejected") {
1047
697
  return {
1048
698
  address,
@@ -1060,19 +710,19 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
1060
710
  errorDetails: r.error,
1061
711
  durationMs: r.durationMs,
1062
712
  attempts: r.attempts,
1063
- ...((args.calls[i].diagnostics ?? args.diagnostics)
713
+ ...((call.diagnostics ?? args.diagnostics)
1064
714
  ? { timing: r.timing }
1065
715
  : {}),
1066
716
  };
1067
717
  }
1068
- if ((args.calls[i].resultMode ?? args.resultMode) === "value") {
718
+ if ((call.resultMode ?? args.resultMode) === "value") {
1069
719
  return {
1070
720
  address,
1071
721
  ok: true,
1072
722
  data: r.value,
1073
723
  durationMs: r.durationMs,
1074
724
  attempts: r.attempts,
1075
- ...((args.calls[i].diagnostics ?? args.diagnostics)
725
+ ...((call.diagnostics ?? args.diagnostics)
1076
726
  ? { timing: r.timing }
1077
727
  : {}),
1078
728
  };
@@ -1083,7 +733,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
1083
733
  result: r.toolResult.content,
1084
734
  durationMs: r.durationMs,
1085
735
  attempts: r.attempts,
1086
- ...((args.calls[i].diagnostics ?? args.diagnostics)
736
+ ...((call.diagnostics ?? args.diagnostics)
1087
737
  ? { timing: r.timing }
1088
738
  : {}),
1089
739
  };
@@ -1140,7 +790,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
1140
790
  }
1141
791
  const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
1142
792
  try {
1143
- const status = await connector.startAuth(ctx, { force: args.force });
793
+ const status = await connector.startAuth(ctx, args.force !== undefined ? { force: args.force } : {});
1144
794
  // startAuth just spoke to the downstream about this exact credential, so
1145
795
  // its answer replaces any older liveness verdict — including the stale
1146
796
  // `auth_required` that sent the agent here, once it reports ok.
@@ -1231,10 +881,20 @@ const READ_ONLY_LOCAL = {
1231
881
  /** Register the nine meta-tools onto an McpServer instance. */
1232
882
  export function registerMetaTools(server, registry, ctx) {
1233
883
  const mt = createMetaTools(registry, ctx.baseUrl, {
1234
- defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
1235
- probeTimeoutMs: ctx.probeTimeoutMs,
1236
- activity: ctx.activity,
1237
- defer: ctx.defer,
884
+ ...(ctx.defaultToolTimeoutMs !== undefined
885
+ ? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
886
+ : {}),
887
+ ...(ctx.probeTimeoutMs !== undefined
888
+ ? { probeTimeoutMs: ctx.probeTimeoutMs }
889
+ : {}),
890
+ ...(ctx.discoveryConcurrency !== undefined
891
+ ? { discoveryConcurrency: ctx.discoveryConcurrency }
892
+ : {}),
893
+ ...(ctx.activity !== undefined ? { activity: ctx.activity } : {}),
894
+ ...(ctx.requestSignal !== undefined
895
+ ? { requestSignal: ctx.requestSignal }
896
+ : {}),
897
+ ...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
1238
898
  });
1239
899
  server.registerTool("skills", {
1240
900
  description: describedFor(registry, SKILLS_DESC, "skills"),