@zackbart/connecta 0.7.9 → 0.8.1

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 (199) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/README.md +47 -148
  3. package/dist/activity.d.ts +0 -8
  4. package/dist/activity.d.ts.map +1 -1
  5. package/dist/activity.js +0 -1
  6. package/dist/activity.js.map +1 -1
  7. package/dist/auth/bearer.d.ts +1 -7
  8. package/dist/auth/bearer.d.ts.map +1 -1
  9. package/dist/auth/bearer.js +3 -24
  10. package/dist/auth/bearer.js.map +1 -1
  11. package/dist/auth/clerk.d.ts +2 -11
  12. package/dist/auth/clerk.d.ts.map +1 -1
  13. package/dist/auth/clerk.js +31 -46
  14. package/dist/auth/clerk.js.map +1 -1
  15. package/dist/call-admission.d.ts.map +1 -1
  16. package/dist/call-admission.js +9 -4
  17. package/dist/call-admission.js.map +1 -1
  18. package/dist/catalog-fingerprint.d.ts +20 -0
  19. package/dist/catalog-fingerprint.d.ts.map +1 -0
  20. package/dist/catalog-fingerprint.js +34 -0
  21. package/dist/catalog-fingerprint.js.map +1 -0
  22. package/dist/catalog-limits.d.ts +10 -0
  23. package/dist/catalog-limits.d.ts.map +1 -0
  24. package/dist/catalog-limits.js +10 -0
  25. package/dist/catalog-limits.js.map +1 -0
  26. package/dist/catalog-service.d.ts +137 -0
  27. package/dist/catalog-service.d.ts.map +1 -0
  28. package/dist/catalog-service.js +401 -0
  29. package/dist/catalog-service.js.map +1 -0
  30. package/dist/catalog.d.ts +6 -0
  31. package/dist/catalog.d.ts.map +1 -1
  32. package/dist/catalog.js +53 -2
  33. package/dist/catalog.js.map +1 -1
  34. package/dist/concurrency.d.ts +4 -0
  35. package/dist/concurrency.d.ts.map +1 -0
  36. package/dist/concurrency.js +31 -0
  37. package/dist/concurrency.js.map +1 -0
  38. package/dist/connectors/api.d.ts.map +1 -1
  39. package/dist/connectors/api.js +22 -12
  40. package/dist/connectors/api.js.map +1 -1
  41. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  42. package/dist/connectors/remote-mcp.js +53 -41
  43. package/dist/connectors/remote-mcp.js.map +1 -1
  44. package/dist/credentials.d.ts +3 -2
  45. package/dist/credentials.d.ts.map +1 -1
  46. package/dist/errors.d.ts +19 -1
  47. package/dist/errors.d.ts.map +1 -1
  48. package/dist/errors.js.map +1 -1
  49. package/dist/execute.d.ts +10 -16
  50. package/dist/execute.d.ts.map +1 -1
  51. package/dist/execute.js +143 -330
  52. package/dist/execute.js.map +1 -1
  53. package/dist/executor-result.d.ts +0 -3
  54. package/dist/executor-result.d.ts.map +1 -1
  55. package/dist/executor-result.js +3 -3
  56. package/dist/executor-result.js.map +1 -1
  57. package/dist/executors/quickjs-child.js +3 -3
  58. package/dist/executors/quickjs-child.js.map +1 -1
  59. package/dist/executors/quickjs-protocol.d.ts +4 -1
  60. package/dist/executors/quickjs-protocol.d.ts.map +1 -1
  61. package/dist/executors/quickjs-runtime.d.ts.map +1 -1
  62. package/dist/executors/quickjs-runtime.js +7 -2
  63. package/dist/executors/quickjs-runtime.js.map +1 -1
  64. package/dist/executors/quickjs.d.ts.map +1 -1
  65. package/dist/executors/quickjs.js +4 -1
  66. package/dist/executors/quickjs.js.map +1 -1
  67. package/dist/favicon.js +2 -2
  68. package/dist/favicon.js.map +1 -1
  69. package/dist/index.d.ts +8 -82
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +58 -104
  72. package/dist/index.js.map +1 -1
  73. package/dist/invocation.d.ts +101 -0
  74. package/dist/invocation.d.ts.map +1 -0
  75. package/dist/invocation.js +405 -0
  76. package/dist/invocation.js.map +1 -0
  77. package/dist/meta-tools.d.ts +9 -95
  78. package/dist/meta-tools.d.ts.map +1 -1
  79. package/dist/meta-tools.js +235 -725
  80. package/dist/meta-tools.js.map +1 -1
  81. package/dist/operator-ui/generated.d.ts +3 -0
  82. package/dist/operator-ui/generated.d.ts.map +1 -0
  83. package/dist/operator-ui/generated.js +5 -0
  84. package/dist/operator-ui/generated.js.map +1 -0
  85. package/dist/operator-ui/model.d.ts +73 -0
  86. package/dist/operator-ui/model.d.ts.map +1 -0
  87. package/dist/operator-ui/model.js +28 -0
  88. package/dist/operator-ui/model.js.map +1 -0
  89. package/dist/registry.d.ts +33 -184
  90. package/dist/registry.d.ts.map +1 -1
  91. package/dist/registry.js +345 -330
  92. package/dist/registry.js.map +1 -1
  93. package/dist/retired-toolkits.d.ts +3 -0
  94. package/dist/retired-toolkits.d.ts.map +1 -0
  95. package/dist/retired-toolkits.js +9 -0
  96. package/dist/retired-toolkits.js.map +1 -0
  97. package/dist/routes/activity.d.ts +3 -0
  98. package/dist/routes/activity.d.ts.map +1 -0
  99. package/dist/routes/activity.js +170 -0
  100. package/dist/routes/activity.js.map +1 -0
  101. package/dist/routes/credentials.d.ts +3 -0
  102. package/dist/routes/credentials.d.ts.map +1 -0
  103. package/dist/routes/credentials.js +170 -0
  104. package/dist/routes/credentials.js.map +1 -0
  105. package/dist/routes/mcp.d.ts +8 -0
  106. package/dist/routes/mcp.d.ts.map +1 -0
  107. package/dist/routes/mcp.js +270 -0
  108. package/dist/routes/mcp.js.map +1 -0
  109. package/dist/routes/oauth.d.ts +4 -0
  110. package/dist/routes/oauth.d.ts.map +1 -0
  111. package/dist/routes/oauth.js +263 -0
  112. package/dist/routes/oauth.js.map +1 -0
  113. package/dist/routes/shared.d.ts +76 -0
  114. package/dist/routes/shared.d.ts.map +1 -0
  115. package/dist/routes/shared.js +140 -0
  116. package/dist/routes/shared.js.map +1 -0
  117. package/dist/routes/ui.d.ts +3 -0
  118. package/dist/routes/ui.d.ts.map +1 -0
  119. package/dist/routes/ui.js +116 -0
  120. package/dist/routes/ui.js.map +1 -0
  121. package/dist/server.d.ts +9 -40
  122. package/dist/server.d.ts.map +1 -1
  123. package/dist/server.js +62 -1322
  124. package/dist/server.js.map +1 -1
  125. package/dist/skills.d.ts +3 -21
  126. package/dist/skills.d.ts.map +1 -1
  127. package/dist/skills.js +15 -43
  128. package/dist/skills.js.map +1 -1
  129. package/dist/storage/file.d.ts.map +1 -1
  130. package/dist/storage/file.js +3 -1
  131. package/dist/storage/file.js.map +1 -1
  132. package/dist/storage/memory.d.ts.map +1 -1
  133. package/dist/storage/memory.js +3 -1
  134. package/dist/storage/memory.js.map +1 -1
  135. package/dist/timeout.d.ts.map +1 -1
  136. package/dist/timeout.js +2 -4
  137. package/dist/timeout.js.map +1 -1
  138. package/dist/types.d.ts +9 -59
  139. package/dist/types.d.ts.map +1 -1
  140. package/dist/ui.d.ts +3 -94
  141. package/dist/ui.d.ts.map +1 -1
  142. package/dist/ui.js +30 -1427
  143. package/dist/ui.js.map +1 -1
  144. package/dist/version.d.ts +1 -1
  145. package/dist/version.js +1 -1
  146. package/package.json +15 -8
  147. package/src/activity.ts +0 -9
  148. package/src/auth/bearer.ts +4 -33
  149. package/src/auth/clerk.ts +35 -48
  150. package/src/call-admission.ts +8 -6
  151. package/src/catalog-fingerprint.ts +52 -0
  152. package/src/catalog-limits.ts +11 -0
  153. package/src/catalog-service.ts +601 -0
  154. package/src/catalog.ts +57 -2
  155. package/src/concurrency.ts +39 -0
  156. package/src/connectors/api.ts +22 -12
  157. package/src/connectors/remote-mcp.ts +63 -45
  158. package/src/credentials.ts +2 -2
  159. package/src/errors.ts +21 -1
  160. package/src/execute.ts +193 -423
  161. package/src/executor-result.ts +3 -3
  162. package/src/executors/quickjs-child.ts +8 -4
  163. package/src/executors/quickjs-protocol.ts +1 -1
  164. package/src/executors/quickjs-runtime.ts +5 -1
  165. package/src/executors/quickjs.ts +11 -8
  166. package/src/favicon.ts +2 -2
  167. package/src/index.ts +70 -222
  168. package/src/invocation.ts +580 -0
  169. package/src/meta-tools.ts +393 -985
  170. package/src/operator-ui/browser.css +360 -0
  171. package/src/operator-ui/browser.ts +987 -0
  172. package/src/operator-ui/generated.ts +4 -0
  173. package/src/operator-ui/model.ts +105 -0
  174. package/src/registry.ts +459 -439
  175. package/src/retired-toolkits.ts +14 -0
  176. package/src/routes/activity.ts +215 -0
  177. package/src/routes/credentials.ts +254 -0
  178. package/src/routes/mcp.ts +339 -0
  179. package/src/routes/oauth.ts +322 -0
  180. package/src/routes/shared.ts +240 -0
  181. package/src/routes/ui.ts +143 -0
  182. package/src/server.ts +64 -1731
  183. package/src/skills.ts +15 -43
  184. package/src/storage/file.ts +3 -1
  185. package/src/storage/memory.ts +3 -1
  186. package/src/timeout.ts +2 -4
  187. package/src/types.ts +8 -60
  188. package/src/ui.ts +60 -1538
  189. package/src/version.ts +1 -1
  190. package/dist/credential-health.d.ts +0 -227
  191. package/dist/credential-health.d.ts.map +0 -1
  192. package/dist/credential-health.js +0 -606
  193. package/dist/credential-health.js.map +0 -1
  194. package/dist/toolkits.d.ts +0 -146
  195. package/dist/toolkits.d.ts.map +0 -1
  196. package/dist/toolkits.js +0 -322
  197. package/dist/toolkits.js.map +0 -1
  198. package/src/credential-health.ts +0 -832
  199. package/src/toolkits.ts +0 -461
package/src/meta-tools.ts CHANGED
@@ -1,23 +1,38 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
4
- import {
5
- recordToolActivity,
6
- type ActivityCallSource,
7
- type ActivityRequestContext,
3
+ import type {
4
+ ActivityCallSource,
5
+ ActivityRequestContext,
8
6
  } from "./activity.js";
7
+ import {
8
+ boundedDiscoveryText,
9
+ CatalogService,
10
+ DEFAULT_SEARCH_LIMIT,
11
+ DiscoveryPolicyError,
12
+ groupedSearchResult,
13
+ MAX_DESCRIBE_ADDRESSES,
14
+ MAX_DISCOVERY_RESULT_BYTES,
15
+ MAX_SEARCH_LIMIT,
16
+ } from "./catalog-service.js";
17
+ import {
18
+ mapSettledWithConcurrency,
19
+ resolveDiscoveryConcurrency,
20
+ } from "./concurrency.js";
9
21
  import {
10
22
  closeConnectorScope,
11
23
  type DeferredWork,
12
24
  } from "./connector-scope.js";
13
- import { unwrapMcpResult } from "./mcp-result.js";
14
25
  import {
15
26
  classifyCallError,
16
- ConnectorCallError,
17
27
  messageLooksRetryable,
18
28
  type CallErrorDetails,
19
29
  } from "./errors.js";
20
- import { isCallAdmissionError } from "./call-admission.js";
30
+ import {
31
+ InvocationService,
32
+ MAX_RETRY_BACKOFF_MS,
33
+ retryBackoffMs,
34
+ type InvocationTiming,
35
+ } from "./invocation.js";
21
36
  import {
22
37
  isValidMaxResultBytes,
23
38
  MIN_MAX_RESULT_BYTES,
@@ -25,8 +40,6 @@ import {
25
40
  type RegistryView,
26
41
  } from "./registry.js";
27
42
  import {
28
- connectorGuide,
29
- connectorSkillName,
30
43
  hasConnectorGuides,
31
44
  listSkills,
32
45
  resolveSkill,
@@ -36,8 +49,15 @@ import {
36
49
  normalizeTimeoutMs,
37
50
  withAbortableTimeout,
38
51
  } from "./timeout.js";
39
- import { credentialVerdictApplies } from "./credential-health.js";
40
- import type { ConnectorStatus, KVStorage, ToolDef } from "./types.js";
52
+ import type { ConnectorStatus, KVStorage } from "./types.js";
53
+
54
+ export {
55
+ MAX_DESCRIBE_ADDRESSES,
56
+ MAX_DISCOVERY_RESULT_BYTES,
57
+ MAX_RETRY_BACKOFF_MS,
58
+ MAX_SEARCH_LIMIT,
59
+ retryBackoffMs,
60
+ };
41
61
 
42
62
  interface TextContent {
43
63
  type: "text";
@@ -51,10 +71,12 @@ export interface ToolResult {
51
71
  }
52
72
 
53
73
  const RESULT_TTL_SECONDS = 900;
74
+ const enc = new TextEncoder();
75
+ const dec = new TextDecoder();
54
76
 
55
77
  export function jsonResult(obj: unknown): ToolResult {
56
78
  return {
57
- content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
79
+ content: [{ type: "text", text: JSON.stringify(obj) }],
58
80
  ...(obj !== null && typeof obj === "object" && !Array.isArray(obj)
59
81
  ? { structuredContent: obj as Record<string, unknown> }
60
82
  : {}),
@@ -69,95 +91,8 @@ function msg(err: unknown): string {
69
91
  return err instanceof Error ? err.message : String(err);
70
92
  }
71
93
 
72
- export const DEFAULT_SEARCH_LIMIT = 25;
73
- /**
74
- * A discovery page is for choosing the next tool, not exporting the catalog.
75
- * One hundred leaves room for broad browsing while keeping each deliberate
76
- * page far below the catalog sizes Connecta supports.
77
- */
78
- export const MAX_SEARCH_LIMIT = 100;
79
- /** Same one-request work bound for address-based discovery. */
80
- export const MAX_DESCRIBE_ADDRESSES = 100;
81
- /**
82
- * Final UTF-8 ceiling for a generated search/describe response. The count
83
- * limits are the ordinary guard; this catches unusually large full schemas or
84
- * descriptions that make even a bounded page expensive.
85
- */
86
- export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
87
- const enc = new TextEncoder();
88
- const dec = new TextDecoder();
89
-
90
- type ErrorDetails = CallErrorDetails;
91
-
92
- export class DiscoveryPolicyError extends Error {
93
- constructor(
94
- readonly code: "invalid_args" | "result_too_large",
95
- message: string,
96
- ) {
97
- super(message);
98
- this.name = "DiscoveryPolicyError";
99
- }
100
- }
101
-
102
- /** Validate before ranking so a huge page request does no proportional work. */
103
- export function discoverySearchLimit(value: unknown): number {
104
- if (value === undefined) return DEFAULT_SEARCH_LIMIT;
105
- if (
106
- typeof value !== "number" ||
107
- !Number.isInteger(value) ||
108
- value < 1 ||
109
- value > MAX_SEARCH_LIMIT
110
- ) {
111
- throw new DiscoveryPolicyError(
112
- "invalid_args",
113
- `limit must be a whole number from 1 through ${MAX_SEARCH_LIMIT}. Page through larger catalogs with offset.`,
114
- );
115
- }
116
- return value;
117
- }
118
-
119
- /** Validate the raw list so duplicate addresses consume the same bound. */
120
- export function discoveryAddresses(value: unknown): unknown[] {
121
- if (!Array.isArray(value)) {
122
- throw new DiscoveryPolicyError(
123
- "invalid_args",
124
- "addresses must be an array.",
125
- );
126
- }
127
- if (value.length > MAX_DESCRIBE_ADDRESSES) {
128
- throw new DiscoveryPolicyError(
129
- "invalid_args",
130
- `addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across describe_tools calls.`,
131
- );
132
- }
133
- return value;
134
- }
135
-
136
- /** Serialize once and count the exact bytes jsonResult would emit. */
137
- function boundedDiscoveryText(
138
- value: unknown,
139
- hint: string,
140
- ): string {
141
- const text = JSON.stringify(value, null, 2);
142
- if (text === undefined) {
143
- throw new TypeError("Discovery result is not JSON-serializable.");
144
- }
145
- const bytes = enc.encode(text).length;
146
- if (bytes > MAX_DISCOVERY_RESULT_BYTES) {
147
- throw new DiscoveryPolicyError(
148
- "result_too_large",
149
- `Discovery result is ${bytes} UTF-8 bytes, over the ${MAX_DISCOVERY_RESULT_BYTES}-byte ceiling. ${hint}`,
150
- );
151
- }
152
- return text;
153
- }
154
-
155
- /** Apply the same final result guard to code-mode discovery helpers. */
156
- export function assertDiscoveryResultSize(
157
- value: unknown,
158
- hint: string,
159
- ): void {
160
- boundedDiscoveryText(value, hint);
94
+ function errorDetails(code: string, message: string): CallErrorDetails {
95
+ return { code, message, retryable: messageLooksRetryable(message) };
161
96
  }
162
97
 
163
98
  function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
@@ -172,8 +107,12 @@ function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
172
107
  return result;
173
108
  }
174
109
 
175
- function discoveryResult(value: unknown, hint: string): ToolResult {
110
+ async function discoveryResult(
111
+ operation: () => unknown | Promise<unknown>,
112
+ hint: string,
113
+ ): Promise<ToolResult> {
176
114
  try {
115
+ const value = await operation();
177
116
  const text = boundedDiscoveryText(value, hint);
178
117
  return {
179
118
  content: [{ type: "text", text }],
@@ -189,71 +128,14 @@ function discoveryResult(value: unknown, hint: string): ToolResult {
189
128
  }
190
129
  }
191
130
 
192
- /**
193
- * The longest the engine will park a synchronous inbound request in *waiting
194
- * alone*. The engine already treats ~15 s as the outer bound of one reasonable
195
- * connector call (EXECUTE_HOST_CALL_TIMEOUT_MS), so sleeping for minutes trades
196
- * a fast, informative failure for a hung one. A connector-reported window this
197
- * long isn't truncated — it's declined (see `retryBackoffMs`) and reported
198
- * verbatim as `error.retryAfterMs`, so the agent, which can afford to wait,
199
- * decides when to re-issue.
200
- */
201
- export const MAX_RETRY_BACKOFF_MS = 10_000;
202
-
203
- /**
204
- * How long to wait before the next attempt, or `undefined` for "don't retry".
205
- *
206
- * A connector that read a `Retry-After` header knows the window exactly, so it
207
- * is honoured **exactly or not at all**: truncating an exponential *guess* is
208
- * harmless, but truncating a *known* window means deliberately retrying inside
209
- * a rate limit — the harm this channel exists to prevent. A window longer than
210
- * `MAX_RETRY_BACKOFF_MS` therefore declines the retry rather than shortening
211
- * it. (`retryAfterMs` is normalized non-negative, so `0` means "retry now".)
212
- * Connectors that report no window keep the historical exponential guess.
213
- *
214
- * Waits are per attempt, matching the per-attempt `timeoutMs` race in
215
- * `runCall`. Exported for direct testing.
216
- */
217
- export function retryBackoffMs(
218
- attempt: number,
219
- retryAfterMs: number | undefined,
220
- ): number | undefined {
221
- if (retryAfterMs === undefined) {
222
- return Math.min(250 * 2 ** (attempt - 1), 1_000);
223
- }
224
- return retryAfterMs <= MAX_RETRY_BACKOFF_MS ? retryAfterMs : undefined;
225
- }
226
-
227
- /** Details for failures that never reached a connector (no thrown value). */
228
- function errorDetails(code: string, message: string): ErrorDetails {
229
- return { code, message, retryable: messageLooksRetryable(message) };
230
- }
231
-
232
- function callerCancelledDetails(): ErrorDetails {
233
- return {
234
- code: "cancelled",
235
- message: "Tool call was cancelled by the caller.",
236
- retryable: false,
237
- };
238
- }
239
-
240
- function isCallerCancellation(
241
- error: unknown,
242
- signal: AbortSignal | undefined,
243
- ): boolean {
244
- return (
245
- signal?.aborted === true ||
246
- (isCallAdmissionError(error) && error.admissionKind === "cancelled")
247
- );
248
- }
249
131
 
250
132
  /** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
251
- function isContinuationByte(b: number): boolean {
252
- return (b & 0xc0) === 0x80;
133
+ function isContinuationByte(b: number | undefined): boolean {
134
+ return b !== undefined && (b & 0xc0) === 0x80;
253
135
  }
254
136
 
255
137
  /** Smallest accepted `get_result` byte offset. */
256
- export const MIN_RESULT_OFFSET = 0;
138
+ const MIN_RESULT_OFFSET = 0;
257
139
 
258
140
  /**
259
141
  * The one definition of a usable `get_result` offset: a whole number of bytes
@@ -269,7 +151,7 @@ export const MIN_RESULT_OFFSET = 0;
269
151
  * error. An offset past the end of the payload stays legal: it is a whole
270
152
  * number of bytes, and it answers with an empty final page.
271
153
  */
272
- export function isValidResultOffset(value: number): boolean {
154
+ function isValidResultOffset(value: number): boolean {
273
155
  return Number.isInteger(value) && value >= MIN_RESULT_OFFSET;
274
156
  }
275
157
 
@@ -333,8 +215,9 @@ export function alignEndToCharBoundary(
333
215
 
334
216
  /** Resolve a dot-path (segments) against a value; `key[]` maps the tail over an array. */
335
217
  function resolvePath(value: unknown, segments: string[]): unknown {
336
- if (segments.length === 0) return value;
337
- const [seg, ...rest] = segments;
218
+ const seg = segments[0];
219
+ if (seg === undefined) return value;
220
+ const rest = segments.slice(1);
338
221
  const isArr = seg.endsWith("[]");
339
222
  const key = isArr ? seg.slice(0, -2) : seg;
340
223
  let next: unknown = value;
@@ -375,7 +258,7 @@ function applyFieldsToContent(
375
258
  } catch {
376
259
  return b;
377
260
  }
378
- return { ...b, text: JSON.stringify(applyFields(parsed, fields), null, 2) };
261
+ return { ...b, text: JSON.stringify(applyFields(parsed, fields)) };
379
262
  });
380
263
  }
381
264
 
@@ -396,8 +279,8 @@ function applyFieldsToContent(
396
279
  * the three give one answer to the same question. A value JSON cannot serialize
397
280
  * at all (a BigInt) still throws, as before, and is reported as a failure.
398
281
  */
399
- export function serializeResultText(value: unknown): string {
400
- const serialized = JSON.stringify(value, null, 2);
282
+ function serializeResultText(value: unknown): string {
283
+ const serialized = JSON.stringify(value);
401
284
  return serialized === undefined ? String(value) : serialized;
402
285
  }
403
286
 
@@ -511,7 +394,7 @@ async function guardContent(
511
394
  ): Promise<ToolResult> {
512
395
  let text: string;
513
396
  try {
514
- text = JSON.stringify(content, null, 2);
397
+ text = JSON.stringify(content);
515
398
  } catch {
516
399
  // A block carrying a BigInt or a cycle cannot be serialized, so it cannot
517
400
  // be measured, stashed, or paged either — there is nothing this guard could
@@ -551,7 +434,7 @@ export interface ListArgs {
551
434
  /** When false, return cached/observed health without downstream I/O. */
552
435
  probe?: boolean;
553
436
  }
554
- export type ResultMode = "mcp" | "value";
437
+ type ResultMode = "mcp" | "value";
555
438
  export interface CallArgs {
556
439
  address: string;
557
440
  args?: Record<string, unknown>;
@@ -573,15 +456,7 @@ export interface GetResultArgs {
573
456
  /** Page size in bytes; a whole number >= 1. Defaults to the deployment cap. */
574
457
  maxBytes?: number;
575
458
  }
576
- export interface BatchCall {
577
- address: string;
578
- args?: Record<string, unknown>;
579
- fields?: string[];
580
- resultMode?: ResultMode;
581
- timeoutMs?: number;
582
- maxRetries?: number;
583
- diagnostics?: boolean;
584
- }
459
+ export type BatchCall = CallArgs;
585
460
  export interface BatchArgs {
586
461
  calls: BatchCall[];
587
462
  resultMode?: ResultMode;
@@ -616,6 +491,8 @@ export function createMetaTools(
616
491
  defaultToolTimeoutMs?: number;
617
492
  /** Per-connector deadline for the list/search/describe probe fan-out. Default 30_000. */
618
493
  probeTimeoutMs?: number;
494
+ /** Maximum simultaneous connector discovery operations. Default 4. */
495
+ discoveryConcurrency?: number;
619
496
  activity?: ActivityRequestContext;
620
497
  /** Inbound request cancellation shared by direct and batch child calls. */
621
498
  requestSignal?: AbortSignal;
@@ -629,10 +506,19 @@ export function createMetaTools(
629
506
  const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
630
507
  const probeTimeoutMs =
631
508
  normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
509
+ const discoveryConcurrency = resolveDiscoveryConcurrency(
510
+ opts.discoveryConcurrency,
511
+ );
632
512
  // createMetaTools() is called once per inbound MCP request. Sharing this
633
513
  // identity lets remote connectors reuse one downstream client inside that
634
514
  // request without leaking request-bound I/O into the next one.
635
515
  const requestScope = {};
516
+ const catalog = new CatalogService(registry, baseUrl, {
517
+ requestScope,
518
+ probeTimeoutMs,
519
+ concurrency: discoveryConcurrency,
520
+ });
521
+ const invocation = new InvocationService(registry, catalog, opts.activity);
636
522
  const withProbeDeadline = <T>(
637
523
  label: string,
638
524
  operation: (options: {
@@ -650,370 +536,120 @@ export function createMetaTools(
650
536
  toolResult: ToolResult;
651
537
  durationMs: number;
652
538
  attempts: number;
653
- timing: {
654
- catalogMs: number;
655
- admissionMs: number;
656
- connectorMs: number;
657
- backoffMs: number;
658
- resultProcessingMs: number;
659
- totalMs: number;
660
- };
539
+ timing: InvocationTiming;
661
540
  value?: unknown;
662
- error?: ErrorDetails;
541
+ error?: CallErrorDetails;
663
542
  }
664
543
 
665
- /** Shared call path used by call tools and batch_call: safety → fields → size guard. */
544
+ interface ProcessedCallResult {
545
+ toolResult: ToolResult;
546
+ value?: unknown;
547
+ }
548
+
549
+ /** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
666
550
  async function runCall(
667
551
  call: BatchCall,
668
552
  source: ActivityCallSource,
669
553
  options: { allowDestructive?: boolean } = {},
670
554
  ): Promise<RunCallOutcome> {
671
- const started = Date.now();
672
- let catalogMs = 0;
673
- let admissionMs = 0;
674
- let connectorMs = 0;
675
- let backoffMs = 0;
676
- let resultProcessingMs = 0;
677
- let attempts = 0;
678
- const timing = () => ({
679
- catalogMs,
680
- admissionMs,
681
- connectorMs,
682
- backoffMs,
683
- resultProcessingMs,
684
- totalMs: Date.now() - started,
685
- });
686
- const resolved = registry.resolveAddress(call.address);
687
- const record = (
688
- outcome: "success" | "error" | "timeout" | "cancelled",
689
- errorCode?: string,
690
- ) => {
691
- if (!resolved) return;
692
- recordToolActivity(opts.activity, {
693
- connectorId: resolved.connector.id,
694
- toolName: resolved.toolName,
695
- address: `${resolved.connector.id}.${resolved.toolName}`,
696
- source,
697
- outcome,
698
- durationMs: Date.now() - started,
699
- attempts,
700
- ...(errorCode ? { errorCode } : {}),
701
- });
702
- };
703
- const failed = (error: ErrorDetails): RunCallOutcome => {
704
- const durationMs = Date.now() - started;
705
- const diagnostics = timing();
706
- record(
707
- error.code === "timeout"
708
- ? "timeout"
709
- : error.code === "cancelled"
710
- ? "cancelled"
711
- : "error",
712
- error.code,
713
- );
714
- return {
715
- toolResult:
716
- call.resultMode === "value"
717
- ? jsonResult({
718
- ok: false,
719
- error,
720
- durationMs,
721
- attempts,
722
- ...(call.diagnostics ? { timing: diagnostics } : {}),
723
- })
724
- : errorResult(error.message),
725
- durationMs,
726
- attempts,
727
- timing: diagnostics,
728
- error,
729
- };
730
- };
731
- if (!resolved) {
732
- return failed(
733
- errorDetails("unknown_address", `Unknown address "${call.address}"`),
734
- );
735
- }
736
555
  const results = registry.resultsStorage();
737
- // Result-size cap for THIS call: the connector's own override wins, then
738
- // the deployment-wide value, then the built-in default (already folded
739
- // into `globalCap`). Resolved per call so one batch_call can mix a
740
- // tight-capped connector with siblings on the global cap. An override the
741
- // registry already warned about at startup is dropped here, so the
742
- // connector simply inherits `globalCap`.
743
- const cap = resolveMaxResultBytes(
744
- resolved.connector.maxResultBytes,
745
- globalCap,
746
- );
747
556
  const fields = call.fields && call.fields.length > 0 ? call.fields : null;
748
- // An explicit per-call deadline always wins; the config default only fills
749
- // the gap, and stays off entirely when the deployment sets none.
750
557
  const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
751
- const maxRetries = Math.min(
752
- 2,
753
- Math.max(0, Math.trunc(call.maxRetries ?? 0)),
754
- );
755
- const catalogStarted = Date.now();
756
- let definition: ToolDef | undefined;
757
- try {
758
- definition = (
759
- await registry.getTools(resolved.connector.id, baseUrl, requestScope)
760
- ).find((tool) => tool.name === resolved.toolName);
761
- } catch (err) {
762
- catalogMs += Date.now() - catalogStarted;
763
- if (opts.requestSignal?.aborted) {
764
- return failed(callerCancelledDetails());
765
- }
766
- // A connector whose catalog cannot be fetched is as unusable as one whose
767
- // execution fails, so it feeds health accounting the same way the
768
- // execution catch below does otherwise a connector every call_tool
769
- // fails against (a revoked downstream grant, say) still reads clean from
770
- // the cheap `list_connectors({ probe: false })` signal.
771
- //
772
- // Recorded HERE rather than inside the registry's catalog fetch on
773
- // purpose: `registry` is this connection's VIEW, so a toolkit-scoped
774
- // session records into its own log as well as the deployment-wide one,
775
- // which `Registry.refreshTools` could not reach. A cache hit that avoids
776
- // a live listTools call therefore records nothing either way — it is not
777
- // evidence of health, and success stays what it has always been: an
778
- // actual downstream call that returned.
779
- registry.recordFailure(resolved.connector.id, Date.now() - started, err);
780
- // classifyCallError so a typed auth_required thrown while listing tools
781
- // (e.g. a revoked downstream OAuth grant) keeps its code.
782
- return failed(classifyCallError(err, "catalog_lookup_failed"));
783
- }
784
- catalogMs += Date.now() - catalogStarted;
785
- if (!definition) {
786
- return failed(
787
- errorDetails(
788
- "unknown_tool",
789
- `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
790
- ),
791
- );
792
- }
793
- const explicitlyReadOnly =
794
- definition.annotations?.readOnlyHint === true &&
795
- definition.annotations?.destructiveHint !== true;
796
- if (!explicitlyReadOnly && !options.allowDestructive) {
797
- return failed(
798
- errorDetails(
799
- "destructive_tool_requires_approval",
800
- `Tool "${call.address}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`,
801
- ),
802
- );
803
- }
804
- const retrySafe =
805
- definition.annotations?.readOnlyHint === true ||
806
- definition.annotations?.idempotentHint === true;
807
-
808
- let result: unknown;
809
- while (true) {
810
- attempts++;
811
- let permit: Awaited<ReturnType<RegistryView["admitCall"]>> | undefined;
812
- const controller =
813
- timeoutMs || opts.requestSignal ? new AbortController() : undefined;
814
- const forwardAbort = () =>
815
- controller?.abort(opts.requestSignal?.reason);
816
- if (opts.requestSignal?.aborted) forwardAbort();
817
- else {
818
- opts.requestSignal?.addEventListener("abort", forwardAbort, {
819
- once: true,
820
- });
821
- }
822
- let timer: ReturnType<typeof setTimeout> | undefined;
823
- let onAbort: (() => void) | undefined;
824
- let attemptFailed = false;
825
- let attemptError: unknown;
826
- try {
827
- const admissionStarted = Date.now();
828
- try {
829
- permit = await registry.admitCall(resolved.connector.id, {
830
- toolName: resolved.toolName,
831
- args: call.args ?? {},
832
- signal: opts.requestSignal,
833
- });
834
- } finally {
835
- admissionMs += Date.now() - admissionStarted;
836
- }
837
- const ctx = registry.contextFor(
838
- resolved.connector.id,
839
- baseUrl,
840
- requestScope,
841
- { signal: controller?.signal, timeoutMs },
842
- );
843
- let rejectCancelled!: (reason: unknown) => void;
844
- const cancelled = controller
845
- ? new Promise<never>((_, reject) => {
846
- rejectCancelled = reject;
847
- })
848
- : undefined;
849
- onAbort = () => {
850
- rejectCancelled(
851
- controller?.signal.reason ??
852
- new ConnectorCallError("timeout", "Tool call was cancelled"),
853
- );
854
- };
855
- controller?.signal.addEventListener("abort", onAbort, { once: true });
856
- if (controller?.signal.aborted) onAbort();
857
- if (controller?.signal.aborted) await cancelled;
858
- if (timeoutMs) {
859
- timer = setTimeout(() => {
860
- controller?.abort(
861
- new ConnectorCallError(
862
- "timeout",
863
- `Tool call timed out after ${timeoutMs}ms`,
864
- ),
865
- );
866
- }, timeoutMs);
867
- }
868
- const connectorStarted = Date.now();
869
- try {
870
- const pending = resolved.connector.callTool(
871
- resolved.toolName,
872
- call.args ?? {},
873
- ctx,
874
- );
875
- result = cancelled
876
- ? await Promise.race([pending, cancelled])
877
- : await pending;
878
- } finally {
879
- connectorMs += Date.now() - connectorStarted;
880
- }
881
- const mcpResult = result as {
882
- content?: TextContent[];
883
- isError?: boolean;
884
- };
885
- if (resolved.connector.kind === "mcp" && mcpResult?.isError) {
886
- throw new Error(
887
- mcpResult.content?.map((block) => block.text).join("") ||
888
- "Downstream tool call failed",
558
+ const outcome = await invocation.invoke<ProcessedCallResult>(
559
+ call.address,
560
+ call.args ?? {},
561
+ {
562
+ source,
563
+ ...(options.allowDestructive !== undefined
564
+ ? { allowDestructive: options.allowDestructive }
565
+ : {}),
566
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
567
+ ...(call.maxRetries !== undefined
568
+ ? { maxRetries: call.maxRetries }
569
+ : {}),
570
+ ...(opts.requestSignal !== undefined
571
+ ? { requestSignal: opts.requestSignal }
572
+ : {}),
573
+ unwrapResult: call.resultMode === "value",
574
+ processResult: async (result, resolved) => {
575
+ // Result-size cap for THIS call: the connector's own override wins,
576
+ // then the deployment-wide value, then the built-in default (already
577
+ // folded into `globalCap`). Resolved per call so one batch_call can
578
+ // mix a tight-capped connector with siblings on the global cap. An
579
+ // override the registry already warned about at startup is dropped
580
+ // here, so the connector simply inherits `globalCap`.
581
+ const cap = resolveMaxResultBytes(
582
+ resolved.connector.maxResultBytes,
583
+ globalCap,
889
584
  );
890
- }
891
- } catch (err) {
892
- attemptFailed = true;
893
- attemptError = err;
894
- } finally {
895
- if (timer) clearTimeout(timer);
896
- if (onAbort) {
897
- controller?.signal.removeEventListener("abort", onAbort);
898
- }
899
- opts.requestSignal?.removeEventListener("abort", forwardAbort);
900
- permit?.release();
901
- }
902
- if (attemptFailed) {
903
- const callerCancelled = isCallerCancellation(
904
- attemptError,
905
- opts.requestSignal,
906
- );
907
- const details = callerCancelled
908
- ? callerCancelledDetails()
909
- : classifyCallError(attemptError);
910
- if (
911
- !callerCancelled &&
912
- attempts <= maxRetries &&
913
- retrySafe &&
914
- details.retryable
915
- ) {
916
- const wait = retryBackoffMs(attempts, details.retryAfterMs);
917
- if (wait !== undefined) {
918
- const backoffStarted = Date.now();
919
- if (wait > 0) {
920
- const completed = await new Promise<boolean>((resolve) => {
921
- let settled = false;
922
- const finish = (value: boolean) => {
923
- if (settled) return;
924
- settled = true;
925
- clearTimeout(timer);
926
- opts.requestSignal?.removeEventListener("abort", cancel);
927
- resolve(value);
928
- };
929
- const timer = setTimeout(() => finish(true), wait);
930
- const cancel = () => finish(false);
931
- opts.requestSignal?.addEventListener("abort", cancel, {
932
- once: true,
933
- });
934
- if (opts.requestSignal?.aborted) cancel();
935
- });
936
- backoffMs += Date.now() - backoffStarted;
937
- if (!completed) return failed(callerCancelledDetails());
938
- } else {
939
- backoffMs += Date.now() - backoffStarted;
940
- }
941
- continue;
585
+ if (call.resultMode === "value") {
586
+ let value = fields ? applyFields(result, fields) : result;
587
+ value = await guardValue(value, results, cap);
588
+ return {
589
+ toolResult: jsonResult({ ok: true, data: value }),
590
+ value,
591
+ };
942
592
  }
943
- // The reported window is longer than the engine will park a
944
- // synchronous request for. Fall through to failure with
945
- // retryAfterMs reported verbatim so the agent can re-issue.
946
- }
947
- if (!callerCancelled && !isCallAdmissionError(attemptError)) {
948
- registry.recordFailure(
949
- resolved.connector.id,
950
- Date.now() - started,
951
- attemptError,
952
- );
953
- }
954
- return failed(details);
955
- }
956
- break;
957
- }
958
-
959
- registry.recordSuccess(resolved.connector.id, Date.now() - started);
960
- const processingStarted = Date.now();
961
- try {
962
- const mr = result as { content?: TextContent[]; isError?: boolean };
963
- if (call.resultMode === "value") {
964
- let value = unwrapMcpResult(resolved.connector.kind, result);
965
- if (fields) value = applyFields(value, fields);
966
- value = await guardValue(value, results, cap);
967
- resultProcessingMs += Date.now() - processingStarted;
968
- const durationMs = Date.now() - started;
969
- const diagnostics = timing();
970
- record("success");
971
- return {
972
- toolResult: jsonResult({
973
- ok: true,
974
- data: value,
975
- durationMs,
976
- attempts,
977
- ...(call.diagnostics ? { timing: diagnostics } : {}),
978
- }),
979
- durationMs,
980
- attempts,
981
- timing: diagnostics,
982
- value,
983
- };
984
- }
985
- if (resolved.connector.kind === "mcp") {
986
- let content = mr?.content ?? [];
987
- if (fields) content = applyFieldsToContent(content, fields);
988
- const toolResult = await guardContent(content, results, cap);
989
- resultProcessingMs += Date.now() - processingStarted;
990
- record("success");
991
- return {
992
- toolResult,
993
- durationMs: Date.now() - started,
994
- attempts,
995
- timing: timing(),
996
- };
593
+ if (resolved.connector.kind === "mcp") {
594
+ const mcpResult = result as { content?: TextContent[] };
595
+ let content = mcpResult?.content ?? [];
596
+ if (fields) content = applyFieldsToContent(content, fields);
597
+ return { toolResult: await guardContent(content, results, cap) };
598
+ }
599
+ const value = fields ? applyFields(result, fields) : result;
600
+ return {
601
+ toolResult: await guardText(
602
+ serializeResultText(value),
603
+ results,
604
+ cap,
605
+ ),
606
+ value,
607
+ };
608
+ },
609
+ },
610
+ );
611
+ if (!outcome.ok) {
612
+ const failedResult =
613
+ outcome.error.code === "auth_required" ||
614
+ call.resultMode === "value"
615
+ ? jsonResult({
616
+ ok: false,
617
+ error: outcome.error,
618
+ durationMs: outcome.durationMs,
619
+ attempts: outcome.attempts,
620
+ ...(call.diagnostics ? { timing: outcome.timing } : {}),
621
+ })
622
+ : errorResult(outcome.error.message);
623
+ if (outcome.error.code === "auth_required") {
624
+ failedResult.isError = true;
997
625
  }
998
- const value = fields ? applyFields(result, fields) : result;
999
- const toolResult = await guardText(
1000
- serializeResultText(value),
1001
- results,
1002
- cap,
1003
- );
1004
- resultProcessingMs += Date.now() - processingStarted;
1005
- record("success");
1006
626
  return {
1007
- toolResult,
1008
- durationMs: Date.now() - started,
1009
- attempts,
1010
- timing: timing(),
1011
- value,
627
+ toolResult: failedResult,
628
+ durationMs: outcome.durationMs,
629
+ attempts: outcome.attempts,
630
+ timing: outcome.timing,
631
+ error: outcome.error,
1012
632
  };
1013
- } catch (err) {
1014
- resultProcessingMs += Date.now() - processingStarted;
1015
- return failed(errorDetails("result_processing_failed", msg(err)));
1016
633
  }
634
+ const valueModeResult =
635
+ call.resultMode === "value"
636
+ ? jsonResult({
637
+ ok: true,
638
+ data: outcome.value.value,
639
+ durationMs: outcome.durationMs,
640
+ attempts: outcome.attempts,
641
+ ...(call.diagnostics ? { timing: outcome.timing } : {}),
642
+ })
643
+ : outcome.value.toolResult;
644
+ return {
645
+ toolResult: valueModeResult,
646
+ durationMs: outcome.durationMs,
647
+ attempts: outcome.attempts,
648
+ timing: outcome.timing,
649
+ ...(Object.prototype.hasOwnProperty.call(outcome.value, "value")
650
+ ? { value: outcome.value.value }
651
+ : {}),
652
+ };
1017
653
  }
1018
654
 
1019
655
  return {
@@ -1044,181 +680,118 @@ export function createMetaTools(
1044
680
  // call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
1045
681
  const connectors = registry.listConnectors();
1046
682
  const scope = probe ? {} : requestScope;
1047
- const pending = connectors.map(
1048
- async (c) => {
1049
- const statusStarted = Date.now();
1050
- const observed = registry.healthFor(c.id);
1051
- const verdict = await registry.credentialHealthFor(c.id);
1052
- let status:
1053
- | ConnectorStatus
1054
- | { state: "ok" | "error" | "unknown"; message?: string };
1055
- if (probe) {
1056
- try {
1057
- status = await withProbeDeadline(
1058
- `list_connectors probe of "${c.id}"`,
1059
- (options) =>
1060
- registry.statusFor(c.id, baseUrl, scope, options),
1061
- );
1062
- } catch (err) {
1063
- // A probe that outran probeTimeoutMs (or otherwise threw)
1064
- // degrades this connector to an error status rather than
1065
- // hanging the whole list_connectors call.
1066
- status = { state: "error", message: msg(err) };
1067
- }
1068
- } else if (
1069
- verdict &&
1070
- // Deployment-wide, deliberately, like `hasObservedSuccess` beside
1071
- // it: a sibling toolkit's successful call proves the shared
1072
- // credential works, and a verdict retired for one view but not
1073
- // another would make the same connector read differently per scope
1074
- // for a reason that has nothing to do with scope.
1075
- credentialVerdictApplies(verdict, registry.observedSuccessAt(c.id))
1076
- ) {
1077
- // The proactive layer (issue #24): a liveness check already found
1078
- // the stored credential dead, so say so on the cheap path instead of
1079
- // waiting for an agent's real call to discover it. Only while it is
1080
- // the freshest evidence — a successful call since then retires it.
1081
- status = {
1082
- state: verdict.state,
1083
- ...(verdict.message ? { message: verdict.message } : {}),
1084
- ...(verdict.authorizationUrl
1085
- ? { authorizationUrl: verdict.authorizationUrl }
1086
- : {}),
1087
- };
1088
- } else {
1089
- // "error" comes from THIS view's own observations — a sibling
1090
- // toolkit's failure is not this session's experience — while
1091
- // ok/unknown may lean on the deployment-wide success signal, since
1092
- // "the connector answers at all" is a fact about the connector.
1093
- // Unscoped, the two are the same log, so this is unchanged there.
1094
- const derived =
1095
- observed?.consecutiveFailures && observed.consecutiveFailures > 0
1096
- ? ("error" as const)
1097
- : registry.hasObservedSuccess(c.id) || c.kind === "api"
1098
- ? ("ok" as const)
1099
- : ("unknown" as const);
1100
- status = {
1101
- // A successful liveness check upgrades "unknown" — nothing has
1102
- // been called yet, but the credential was verified, which is how
1103
- // re-authorization shows up here as ok rather than as an absence
1104
- // of evidence. It never DOWNgrades an observed failure: a real
1105
- // call that failed is stronger evidence than a background check.
1106
- state:
1107
- derived === "unknown" && verdict?.state === "ok"
1108
- ? ("ok" as const)
1109
- : derived,
1110
- ...(observed?.lastError ? { message: observed.lastError } : {}),
1111
- };
1112
- }
1113
- // Stamped where the observation actually happened — after the status
1114
- // probe, not before it. A 30-second probe stamped at its start would
1115
- // report a verdict older than it is, and would lose the race against a
1116
- // real call that succeeded WHILE it ran (that success must retire the
1117
- // verdict, and only an honest timestamp says so).
1118
- const checkedAt = new Date().toISOString();
1119
- // A live status probe IS a liveness observation of the stored
1120
- // credential, so it updates the same verdict a background check
1121
- // writes: the cached read afterwards agrees with what the operator
1122
- // just saw, and they are not swept again moments later. Recorded from
1123
- // the STATUS phase only, and only when the connector actually answered
1124
- // — a catalog refresh below is not a credential check (the sweep never
1125
- // fetches one), it is already counted in the health log, and letting
1126
- // its failure land here would spend the freshness budget on it. The
1127
- // registry ignores this for connectors storing no credential of ours.
1128
- if (probe && (status.state === "ok" || status.state === "auth_required")) {
1129
- await registry.recordCredentialHealth(c.id, {
1130
- state: status.state,
1131
- checkedAt,
1132
- ...(status.message ? { message: status.message } : {}),
1133
- ...("authorizationUrl" in status && status.authorizationUrl
1134
- ? { authorizationUrl: status.authorizationUrl }
1135
- : {}),
1136
- });
683
+ const inspect = async (c: (typeof connectors)[number]) => {
684
+ const statusStarted = Date.now();
685
+ const observed = registry.healthFor(c.id);
686
+ const drift = await registry.credentialDriftFor(c.id);
687
+ let status:
688
+ | ConnectorStatus
689
+ | { state: "ok" | "error" | "unknown"; message?: string };
690
+ if (drift) {
691
+ status = { state: "auth_required", message: drift };
692
+ } else if (probe) {
693
+ try {
694
+ status = await withProbeDeadline(
695
+ `list_connectors probe of "${c.id}"`,
696
+ (options) =>
697
+ registry.statusFor(c.id, baseUrl, scope, options),
698
+ );
699
+ } catch (err) {
700
+ // A probe that outran probeTimeoutMs (or otherwise threw)
701
+ // degrades this connector to an error status rather than
702
+ // hanging the whole list_connectors call.
703
+ status = { state: "error", message: msg(err) };
1137
704
  }
1138
- let tools = registry.peekTools(c.id);
1139
- // An auth_required status may have just started OAuth. A second
1140
- // listTools probe would overwrite its state/verifier while returning
1141
- // the first (now stale) authorization URL.
1142
- if (probe && status.state === "ok") {
1143
- try {
1144
- tools = await withProbeDeadline(
1145
- `list_connectors catalog refresh of "${c.id}"`,
1146
- (options) =>
1147
- registry.refreshTools(c.id, baseUrl, scope, options),
1148
- );
1149
- registry.recordSuccess(c.id, Date.now() - statusStarted);
1150
- } catch (err) {
1151
- const details = classifyCallError(err);
1152
- if (details.code === "auth_required") {
1153
- let authStatus: ConnectorStatus | undefined;
1154
- try {
1155
- authStatus = await withProbeDeadline(
1156
- `list_connectors authorization status of "${c.id}"`,
1157
- (options) =>
1158
- registry.statusFor(c.id, baseUrl, scope, options),
1159
- );
1160
- } catch {
1161
- // The typed auth verdict is still authoritative; this second
1162
- // read exists only to recover the connector's pending URL.
1163
- }
1164
- status =
1165
- authStatus?.state === "auth_required"
1166
- ? authStatus
1167
- : {
1168
- state: "auth_required" as const,
1169
- message: details.message,
1170
- };
1171
- await registry.recordCredentialHealth(c.id, {
1172
- state: "auth_required",
1173
- checkedAt,
1174
- ...(status.message ? { message: status.message } : {}),
1175
- ...("authorizationUrl" in status &&
1176
- status.authorizationUrl
1177
- ? { authorizationUrl: status.authorizationUrl }
1178
- : {}),
1179
- });
1180
- } else {
1181
- status = { state: "error" as const, message: msg(err) };
705
+ } else {
706
+ const derived =
707
+ observed?.consecutiveFailures && observed.consecutiveFailures > 0
708
+ ? ("error" as const)
709
+ : registry.hasObservedSuccess(c.id) || c.kind === "api"
710
+ ? ("ok" as const)
711
+ : ("unknown" as const);
712
+ status = {
713
+ state: derived,
714
+ ...(observed?.lastError ? { message: observed.lastError } : {}),
715
+ };
716
+ }
717
+ // Stamped after any live probe so the response reports when its
718
+ // observation completed, not when a potentially slow request began.
719
+ const checkedAt = new Date().toISOString();
720
+ let tools = registry.peekTools(c.id);
721
+ // An auth_required status may have just started OAuth. A second
722
+ // listTools probe would overwrite its state/verifier while returning
723
+ // the first (now stale) authorization URL.
724
+ if (probe && status.state === "ok") {
725
+ try {
726
+ tools = await withProbeDeadline(
727
+ `list_connectors catalog refresh of "${c.id}"`,
728
+ (options) =>
729
+ registry.refreshTools(c.id, baseUrl, scope, options),
730
+ );
731
+ registry.recordSuccess(c.id, Date.now() - statusStarted);
732
+ } catch (err) {
733
+ const details = classifyCallError(err);
734
+ if (details.code === "auth_required") {
735
+ let authStatus: ConnectorStatus | undefined;
736
+ try {
737
+ authStatus = await withProbeDeadline(
738
+ `list_connectors authorization status of "${c.id}"`,
739
+ (options) =>
740
+ registry.statusFor(c.id, baseUrl, scope, options),
741
+ );
742
+ } catch {
743
+ // The typed auth verdict is still authoritative; this second
744
+ // read exists only to recover the connector's pending URL.
1182
745
  }
1183
- registry.recordFailure(c.id, Date.now() - statusStarted, err);
746
+ status =
747
+ authStatus?.state === "auth_required"
748
+ ? authStatus
749
+ : {
750
+ state: "auth_required" as const,
751
+ message: details.message,
752
+ };
753
+ } else {
754
+ status = { state: "error" as const, message: msg(err) };
1184
755
  }
756
+ registry.recordFailure(c.id, Date.now() - statusStarted, err);
1185
757
  }
1186
- const latencyMs = Date.now() - statusStarted;
1187
- const latestObserved = registry.healthFor(c.id);
1188
- const credentialCheck = probe
1189
- ? await registry.credentialHealthFor(c.id)
1190
- : verdict;
1191
- return {
1192
- id: c.id,
1193
- ...(c.title ? { title: c.title } : {}),
1194
- description: c.description,
1195
- toolCount: tools?.length ?? 0,
1196
- status: status.state,
1197
- checkedAt,
1198
- latencyMs,
1199
- probe,
1200
- ...(latestObserved ?? observed ?? {}),
1201
- ...(credentialCheck ? { credentialCheck } : {}),
1202
- ...("authorizationUrl" in status && status.authorizationUrl
1203
- ? { authorizationUrl: status.authorizationUrl }
1204
- : {}),
1205
- ...(status.message ? { message: status.message } : {}),
1206
- };
1207
- },
758
+ }
759
+ const latencyMs = Date.now() - statusStarted;
760
+ const latestObserved = registry.healthFor(c.id);
761
+ return {
762
+ id: c.id,
763
+ ...(c.title ? { title: c.title } : {}),
764
+ description: c.description,
765
+ toolCount: tools?.length ?? 0,
766
+ status: status.state,
767
+ checkedAt,
768
+ latencyMs,
769
+ probe,
770
+ ...(latestObserved ?? observed),
771
+ ...("authorizationUrl" in status &&
772
+ status.authorizationUrl && {
773
+ authorizationUrl: status.authorizationUrl,
774
+ }),
775
+ ...(status.message && { message: status.message }),
776
+ };
777
+ };
778
+ const settled = await mapSettledWithConcurrency(
779
+ connectors,
780
+ discoveryConcurrency,
781
+ inspect,
1208
782
  );
1209
- if (!probe) {
1210
- return jsonResult({ connectors: await Promise.all(pending) });
783
+ if (probe) {
784
+ await mapSettledWithConcurrency(
785
+ connectors,
786
+ discoveryConcurrency,
787
+ (connector) =>
788
+ closeConnectorScope(
789
+ connector,
790
+ registry.contextFor(connector.id, baseUrl, scope),
791
+ opts.defer,
792
+ ),
793
+ );
1211
794
  }
1212
- const settled = await Promise.allSettled(pending);
1213
- await Promise.all(
1214
- connectors.map((connector) =>
1215
- closeConnectorScope(
1216
- connector,
1217
- registry.contextFor(connector.id, baseUrl, scope),
1218
- opts.defer,
1219
- ),
1220
- ),
1221
- );
1222
795
  const out = settled.map((result) => {
1223
796
  if (result.status === "rejected") throw result.reason;
1224
797
  return result.value;
@@ -1227,233 +800,16 @@ export function createMetaTools(
1227
800
  },
1228
801
 
1229
802
  async searchTools(args: SearchArgs): Promise<ToolResult> {
1230
- const q = args.query ?? "";
1231
- let limit: number;
1232
- try {
1233
- limit = discoverySearchLimit(args.limit);
1234
- } catch (err) {
1235
- if (err instanceof DiscoveryPolicyError) {
1236
- return discoveryErrorResult(err);
1237
- }
1238
- throw err;
1239
- }
1240
- const offset = Math.max(0, Math.trunc(args.offset ?? 0));
1241
- const conns = args.connector
1242
- ? [registry.getConnector(args.connector)].filter(
1243
- (c): c is NonNullable<typeof c> => Boolean(c),
1244
- )
1245
- : registry.listConnectors();
1246
- const matches: Array<{
1247
- connectorId: string;
1248
- connectorTitle?: string;
1249
- connectorDescription?: string;
1250
- connectorGuideSkill?: string;
1251
- tool: ToolDef;
1252
- score: number;
1253
- order: number;
1254
- }> = [];
1255
- const catalogs = await Promise.allSettled(
1256
- conns.map((c) =>
1257
- withProbeDeadline(
1258
- `search_tools probe of "${c.id}"`,
1259
- (options) =>
1260
- registry.getTools(c.id, baseUrl, requestScope, options),
1261
- ),
1262
- ),
1263
- );
1264
- let matchMode: "all" | "partial" = "all";
1265
- const collectMatches = (mode: "all" | "partial") => {
1266
- matches.length = 0;
1267
- let orderBase = 0;
1268
- catalogs.forEach((catalog, connectorIndex) => {
1269
- const c = conns[connectorIndex];
1270
- if (catalog.status === "fulfilled") {
1271
- for (const ranked of rankTools(catalog.value, q, mode)) {
1272
- matches.push({
1273
- connectorId: c.id,
1274
- connectorTitle: c.title,
1275
- connectorDescription: c.description,
1276
- ...(connectorGuide(c)
1277
- ? { connectorGuideSkill: connectorSkillName(c.id) }
1278
- : {}),
1279
- tool: ranked.tool,
1280
- score: ranked.score,
1281
- order: orderBase + ranked.order,
1282
- });
1283
- }
1284
- }
1285
- orderBase +=
1286
- catalog.status === "fulfilled" ? catalog.value.length : 1;
1287
- });
1288
- };
1289
- collectMatches("all");
1290
- if (q.trim() && matches.length === 0) {
1291
- matchMode = "partial";
1292
- collectMatches(matchMode);
1293
- }
1294
- matches.sort((a, b) => b.score - a.score || a.order - b.order);
1295
- const page = matches.slice(offset, offset + limit);
1296
- const groups: {
1297
- id: string;
1298
- title?: string;
1299
- description?: string;
1300
- /** Skill name of this connector's usage guide, when it has one. */
1301
- guide?: string;
1302
- tools: Array<{
1303
- name: string;
1304
- address: string;
1305
- description?: string;
1306
- inputSchema?: unknown;
1307
- outputSchema?: unknown;
1308
- annotations?: ToolDef["annotations"];
1309
- }>;
1310
- }[] = [];
1311
- const byConnector = new Map<string, (typeof groups)[number]>();
1312
- for (const match of page) {
1313
- let group = byConnector.get(match.connectorId);
1314
- if (!group) {
1315
- group = {
1316
- id: match.connectorId,
1317
- ...(match.connectorTitle ? { title: match.connectorTitle } : {}),
1318
- description: match.connectorDescription,
1319
- ...(match.connectorGuideSkill
1320
- ? { guide: match.connectorGuideSkill }
1321
- : {}),
1322
- tools: [],
1323
- };
1324
- byConnector.set(match.connectorId, group);
1325
- groups.push(group);
1326
- }
1327
- const schema = match.tool.inputSchema ?? { type: "object" };
1328
- group.tools.push({
1329
- name: match.tool.name,
1330
- address: `${match.connectorId}.${match.tool.name}`,
1331
- description: summarizeDescription(
1332
- match.tool.description,
1333
- args.fullDescriptions === true,
1334
- ),
1335
- ...(args.includeSchemas
1336
- ? {
1337
- inputSchema:
1338
- args.includeSchemas === "json"
1339
- ? schema
1340
- : compactSchema(schema),
1341
- }
1342
- : {}),
1343
- ...(args.includeSchemas && match.tool.outputSchema
1344
- ? {
1345
- outputSchema:
1346
- args.includeSchemas === "json"
1347
- ? match.tool.outputSchema
1348
- : compactSchema(match.tool.outputSchema),
1349
- }
1350
- : {}),
1351
- ...(match.tool.annotations
1352
- ? { annotations: match.tool.annotations }
1353
- : {}),
1354
- });
1355
- }
1356
- const nextOffset =
1357
- offset + page.length < matches.length
1358
- ? offset + page.length
1359
- : undefined;
1360
803
  return discoveryResult(
1361
- {
1362
- connectors: groups,
1363
- total: matches.length,
1364
- offset,
1365
- limit,
1366
- hasMore: nextOffset !== undefined,
1367
- ...(nextOffset !== undefined ? { nextOffset } : {}),
1368
- ...(matchMode === "partial" && matches.length > 0
1369
- ? { matchMode }
1370
- : {}),
1371
- },
804
+ async () =>
805
+ groupedSearchResult(await catalog.search(args)),
1372
806
  "Request a smaller limit, omit fullDescriptions, or use compact schemas.",
1373
807
  );
1374
808
  },
1375
809
 
1376
810
  async describeTools(args: DescribeArgs): Promise<ToolResult> {
1377
- try {
1378
- discoveryAddresses(args.addresses);
1379
- } catch (err) {
1380
- if (err instanceof DiscoveryPolicyError) {
1381
- return discoveryErrorResult(err);
1382
- }
1383
- throw err;
1384
- }
1385
- const format = args.format ?? "compact";
1386
- const resolved = args.addresses.map((address) => ({
1387
- address,
1388
- resolved: registry.resolveAddress(address),
1389
- }));
1390
- const connectorIds = [
1391
- ...new Set(
1392
- resolved
1393
- .map((entry) => entry.resolved?.connector.id)
1394
- .filter((id): id is string => Boolean(id)),
1395
- ),
1396
- ];
1397
- const loaded = await Promise.allSettled(
1398
- connectorIds.map((id) =>
1399
- withProbeDeadline(
1400
- `describe_tools probe of "${id}"`,
1401
- (options) =>
1402
- registry.getTools(id, baseUrl, requestScope, options),
1403
- ),
1404
- ),
1405
- );
1406
- const catalogs = new Map<string, ToolDef[] | Error>();
1407
- loaded.forEach((result, index) => {
1408
- catalogs.set(
1409
- connectorIds[index],
1410
- result.status === "fulfilled"
1411
- ? result.value
1412
- : result.reason instanceof Error
1413
- ? result.reason
1414
- : new Error(String(result.reason)),
1415
- );
1416
- });
1417
- const out = resolved.map(({ address, resolved }) => {
1418
- if (!resolved) {
1419
- return { address, error: `Unknown address "${address}"` };
1420
- }
1421
- const catalog = catalogs.get(resolved.connector.id);
1422
- if (catalog instanceof Error) {
1423
- return { address, error: catalog.message };
1424
- }
1425
- const tool = catalog?.find((t) => t.name === resolved.toolName);
1426
- if (!tool) {
1427
- return {
1428
- address,
1429
- error: `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
1430
- };
1431
- }
1432
- const schema = tool.inputSchema ?? { type: "object" };
1433
- return {
1434
- address,
1435
- name: tool.name,
1436
- description: summarizeDescription(
1437
- tool.description,
1438
- args.fullDescriptions === true,
1439
- ),
1440
- ...(connectorGuide(resolved.connector)
1441
- ? { guide: connectorSkillName(resolved.connector.id) }
1442
- : {}),
1443
- inputSchema: format === "json" ? schema : compactSchema(schema),
1444
- ...(tool.outputSchema
1445
- ? {
1446
- outputSchema:
1447
- format === "json"
1448
- ? tool.outputSchema
1449
- : compactSchema(tool.outputSchema),
1450
- }
1451
- : {}),
1452
- ...(tool.annotations ? { annotations: tool.annotations } : {}),
1453
- };
1454
- });
1455
811
  return discoveryResult(
1456
- { tools: out },
812
+ async () => ({ tools: await catalog.describe(args) }),
1457
813
  'Split the address list or use format: "compact".',
1458
814
  );
1459
815
  },
@@ -1534,17 +890,29 @@ export function createMetaTools(
1534
890
  runCall(
1535
891
  {
1536
892
  ...c,
1537
- resultMode: c.resultMode ?? args.resultMode,
1538
- timeoutMs: c.timeoutMs ?? args.timeoutMs,
1539
- maxRetries: c.maxRetries ?? args.maxRetries,
1540
- diagnostics: c.diagnostics ?? args.diagnostics,
893
+ ...((c.resultMode ?? args.resultMode) !== undefined
894
+ ? { resultMode: c.resultMode ?? args.resultMode }
895
+ : {}),
896
+ ...((c.timeoutMs ?? args.timeoutMs) !== undefined
897
+ ? { timeoutMs: c.timeoutMs ?? args.timeoutMs }
898
+ : {}),
899
+ ...((c.maxRetries ?? args.maxRetries) !== undefined
900
+ ? { maxRetries: c.maxRetries ?? args.maxRetries }
901
+ : {}),
902
+ ...((c.diagnostics ?? args.diagnostics) !== undefined
903
+ ? { diagnostics: c.diagnostics ?? args.diagnostics }
904
+ : {}),
1541
905
  },
1542
906
  "batch_call",
1543
907
  ),
1544
908
  ),
1545
909
  );
1546
910
  const results = settled.map((s, i) => {
1547
- const address = args.calls[i].address;
911
+ const call = args.calls[i];
912
+ if (!call) {
913
+ throw new Error("Batch result has no corresponding call");
914
+ }
915
+ const { address } = call;
1548
916
  if (s.status === "rejected") {
1549
917
  return {
1550
918
  address,
@@ -1562,19 +930,19 @@ export function createMetaTools(
1562
930
  errorDetails: r.error,
1563
931
  durationMs: r.durationMs,
1564
932
  attempts: r.attempts,
1565
- ...((args.calls[i].diagnostics ?? args.diagnostics)
933
+ ...((call.diagnostics ?? args.diagnostics)
1566
934
  ? { timing: r.timing }
1567
935
  : {}),
1568
936
  };
1569
937
  }
1570
- if ((args.calls[i].resultMode ?? args.resultMode) === "value") {
938
+ if ((call.resultMode ?? args.resultMode) === "value") {
1571
939
  return {
1572
940
  address,
1573
941
  ok: true,
1574
942
  data: r.value,
1575
943
  durationMs: r.durationMs,
1576
944
  attempts: r.attempts,
1577
- ...((args.calls[i].diagnostics ?? args.diagnostics)
945
+ ...((call.diagnostics ?? args.diagnostics)
1578
946
  ? { timing: r.timing }
1579
947
  : {}),
1580
948
  };
@@ -1585,7 +953,7 @@ export function createMetaTools(
1585
953
  result: r.toolResult.content,
1586
954
  durationMs: r.durationMs,
1587
955
  attempts: r.attempts,
1588
- ...((args.calls[i].diagnostics ?? args.diagnostics)
956
+ ...((call.diagnostics ?? args.diagnostics)
1589
957
  ? { timing: r.timing }
1590
958
  : {}),
1591
959
  };
@@ -1629,6 +997,21 @@ export function createMetaTools(
1629
997
  ...(details.retryAfterMs !== undefined
1630
998
  ? { retryAfterMs: details.retryAfterMs }
1631
999
  : {}),
1000
+ ...(details.connector !== undefined
1001
+ ? { connector: batchSummaryString(details.connector) }
1002
+ : {}),
1003
+ ...(details.operation !== undefined
1004
+ ? { operation: batchSummaryString(details.operation) }
1005
+ : {}),
1006
+ ...(details.recovery !== undefined
1007
+ ? { recovery: details.recovery }
1008
+ : {}),
1009
+ ...(details.nextAction !== undefined
1010
+ ? { nextAction: details.nextAction }
1011
+ : {}),
1012
+ ...(details.retry !== undefined
1013
+ ? { retry: batchSummaryString(details.retry) }
1014
+ : {}),
1632
1015
  },
1633
1016
  };
1634
1017
  }),
@@ -1643,24 +1026,62 @@ export function createMetaTools(
1643
1026
  return errorResult(`Unknown connector "${args.connector}"`);
1644
1027
  }
1645
1028
  if (!connector.startAuth) {
1646
- return errorResult(
1647
- `Connector "${args.connector}" does not use downstream OAuth — its auth is static (headers/none), so there is nothing to authorize.`,
1029
+ if (!connector.credential) {
1030
+ return jsonResult({
1031
+ connector: connector.id,
1032
+ recovery: "unavailable",
1033
+ message:
1034
+ `Connector "${connector.id}" declares neither downstream OAuth ` +
1035
+ "nor an operator-managed credential slot. Update the connector " +
1036
+ "or deployment configuration before retrying.",
1037
+ });
1038
+ }
1039
+ const ctx = registry.contextFor(
1040
+ connector.id,
1041
+ baseUrl,
1042
+ requestScope,
1648
1043
  );
1044
+ if (!ctx.credential) {
1045
+ return jsonResult({
1046
+ connector: connector.id,
1047
+ recovery: "unavailable",
1048
+ message:
1049
+ "Credential storage is not configured. Configure " +
1050
+ "credentials.encryptionKey, redeploy, then call " +
1051
+ "authorize_connector again.",
1052
+ });
1053
+ }
1054
+ const fields = connector.credential.fields?.map((field) => ({
1055
+ name: field.name,
1056
+ guidance: field.description ?? field.label,
1057
+ })) ?? [
1058
+ {
1059
+ name: "value",
1060
+ guidance:
1061
+ connector.credential.description ??
1062
+ connector.credential.label,
1063
+ },
1064
+ ];
1065
+ return jsonResult({
1066
+ connector: connector.id,
1067
+ recovery: "operator_config",
1068
+ credential: {
1069
+ label: connector.credential.label,
1070
+ fields,
1071
+ },
1072
+ operatorUrl: new URL("/credentials", baseUrl).toString(),
1073
+ instructions:
1074
+ "Have the operator open operatorUrl, set and test the credential, " +
1075
+ "then retry the original call. No redeploy is needed. Credential " +
1076
+ "mutation requires a Clerk-authenticated operator.",
1077
+ });
1649
1078
  }
1650
1079
  const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
1651
1080
  try {
1652
- const status = await connector.startAuth(ctx, { force: args.force });
1653
- // startAuth just spoke to the downstream about this exact credential, so
1654
- // its answer replaces any older liveness verdict including the stale
1655
- // `auth_required` that sent the agent here, once it reports ok.
1656
- await registry.recordCredentialHealth(connector.id, {
1657
- state: status.state,
1658
- checkedAt: new Date().toISOString(),
1659
- ...(status.message ? { message: status.message } : {}),
1660
- ...(status.authorizationUrl
1661
- ? { authorizationUrl: status.authorizationUrl }
1662
- : {}),
1663
- });
1081
+ const status = await connector.startAuth(
1082
+ ctx,
1083
+ args.force !== undefined ? { force: args.force } : {},
1084
+ );
1664
1085
  if (status.state === "auth_required" && !status.authorizationUrl) {
1665
1086
  // auth_required with nothing to open is a dead end for the operator.
1666
1087
  return errorResult(
@@ -1669,6 +1090,7 @@ export function createMetaTools(
1669
1090
  }
1670
1091
  return jsonResult({
1671
1092
  connector: connector.id,
1093
+ recovery: "oauth",
1672
1094
  status: status.state,
1673
1095
  ...(status.authorizationUrl
1674
1096
  ? {
@@ -1692,7 +1114,7 @@ export function createMetaTools(
1692
1114
 
1693
1115
  const LIST_DESC =
1694
1116
  "List connectors with status, cached tool count, and recent real-call health. Use probe=false for a fast inventory; use probe=true (default) only to diagnose live health or authorization.";
1695
- const SEARCH_DESC = `Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all. Pages contain at most ${MAX_SEARCH_LIMIT} tools. includeSchemas="compact" usually removes the describe_tools round trip.`;
1117
+ const SEARCH_DESC = `Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all. The default page has ${DEFAULT_SEARCH_LIMIT} tools; explicit limit can request up to ${MAX_SEARCH_LIMIT}. includeSchemas="compact" usually removes the describe_tools round trip.`;
1696
1118
  const DESCRIBE_DESC = `Inspect up to ${MAX_DESCRIBE_ADDRESSES} known tool addresses when search_tools did not include a sufficient schema. Returns descriptions, input/output schemas, and behavior annotations; format "compact" is the default.`;
1697
1119
  const CALL_DESC =
1698
1120
  'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths, resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
@@ -1703,7 +1125,7 @@ const GET_RESULT_DESC =
1703
1125
  const BATCH_DESC =
1704
1126
  "Use for 2–10 independent tools explicitly annotated readOnlyHint: true. Calls run in parallel with shared request-scoped clients; use execute_code when available instead for dependencies or in-sandbox reduction. Unannotated, write-capable, and destructive tools are refused. Batch timeout, safe retry, result mode, and diagnostics defaults may be overridden per call. An oversized final envelope returns ordered outcome summaries plus a get_result page handle.";
1705
1127
  const AUTHORIZE_DESC =
1706
- "Use after a connector reports auth_required. Starts downstream OAuth and returns an authorizationUrl for the operator to open. force=true wipes stored credentials first and restarts consent.";
1128
+ "Use after auth_required. Returns an OAuth or operator-credential handoff, or reports required deployment configuration. force=true restarts OAuth only; this tool never accepts credentials.";
1707
1129
  const SKILLS_DESC =
1708
1130
  'List or fetch concise guidance for choosing among Connecta meta-tools. Call skills({ name: "usage" }) once when the routing workflow is unfamiliar; do not refetch it in the same task.';
1709
1131
 
@@ -1713,10 +1135,7 @@ const SKILLS_DESC =
1713
1135
  * so a deployment with no guides gets every base description unchanged rather
1714
1136
  * than paying for text about a feature it does not use.
1715
1137
  *
1716
- * Registration is per connection and reads the connection's own registry view,
1717
- * so under a toolkit these sentences reflect the SCOPED connector set: a scoped
1718
- * session whose connectors carry no guides sees the base descriptions, and
1719
- * never learns from a tool description that guides exist out of scope.
1138
+ * Registration is per connection and reads the configured connector set.
1720
1139
  */
1721
1140
  const GUIDE_NOTES = {
1722
1141
  skills:
@@ -1758,6 +1177,16 @@ const READ_ONLY_LOCAL = {
1758
1177
  openWorldHint: false,
1759
1178
  } as const;
1760
1179
 
1180
+ const CALL_INPUT_SCHEMA = {
1181
+ address: z.string(),
1182
+ args: z.record(z.string(), z.unknown()).optional(),
1183
+ fields: z.array(z.string()).optional(),
1184
+ resultMode: z.enum(["mcp", "value"]).optional(),
1185
+ timeoutMs: z.number().int().positive().optional(),
1186
+ maxRetries: z.number().int().min(0).max(2).optional(),
1187
+ diagnostics: z.boolean().optional(),
1188
+ };
1189
+
1761
1190
  /** Register the nine meta-tools onto an McpServer instance. */
1762
1191
  export function registerMetaTools(
1763
1192
  server: McpServer,
@@ -1766,17 +1195,27 @@ export function registerMetaTools(
1766
1195
  baseUrl: string;
1767
1196
  defaultToolTimeoutMs?: number;
1768
1197
  probeTimeoutMs?: number;
1198
+ discoveryConcurrency?: number;
1769
1199
  activity?: ActivityRequestContext;
1770
1200
  requestSignal?: AbortSignal;
1771
1201
  defer?: DeferredWork;
1772
1202
  },
1773
1203
  ): void {
1774
1204
  const mt = createMetaTools(registry, ctx.baseUrl, {
1775
- defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
1776
- probeTimeoutMs: ctx.probeTimeoutMs,
1777
- activity: ctx.activity,
1778
- requestSignal: ctx.requestSignal,
1779
- defer: ctx.defer,
1205
+ ...(ctx.defaultToolTimeoutMs !== undefined
1206
+ ? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
1207
+ : {}),
1208
+ ...(ctx.probeTimeoutMs !== undefined
1209
+ ? { probeTimeoutMs: ctx.probeTimeoutMs }
1210
+ : {}),
1211
+ ...(ctx.discoveryConcurrency !== undefined
1212
+ ? { discoveryConcurrency: ctx.discoveryConcurrency }
1213
+ : {}),
1214
+ ...(ctx.activity !== undefined ? { activity: ctx.activity } : {}),
1215
+ ...(ctx.requestSignal !== undefined
1216
+ ? { requestSignal: ctx.requestSignal }
1217
+ : {}),
1218
+ ...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
1780
1219
  });
1781
1220
 
1782
1221
  server.registerTool(
@@ -1784,12 +1223,7 @@ export function registerMetaTools(
1784
1223
  {
1785
1224
  description: describedFor(registry, SKILLS_DESC, "skills"),
1786
1225
  inputSchema: { name: z.string().optional() },
1787
- annotations: {
1788
- readOnlyHint: true,
1789
- destructiveHint: false,
1790
- idempotentHint: true,
1791
- openWorldHint: false,
1792
- },
1226
+ annotations: READ_ONLY_LOCAL,
1793
1227
  },
1794
1228
  async (args) => mt.skills(args as SkillArgs),
1795
1229
  );
@@ -1839,15 +1273,7 @@ export function registerMetaTools(
1839
1273
  "call_tool",
1840
1274
  {
1841
1275
  description: CALL_DESC,
1842
- inputSchema: {
1843
- address: z.string(),
1844
- args: z.record(z.string(), z.unknown()).optional(),
1845
- fields: z.array(z.string()).optional(),
1846
- resultMode: z.enum(["mcp", "value"]).optional(),
1847
- timeoutMs: z.number().int().positive().optional(),
1848
- maxRetries: z.number().int().min(0).max(2).optional(),
1849
- diagnostics: z.boolean().optional(),
1850
- },
1276
+ inputSchema: CALL_INPUT_SCHEMA,
1851
1277
  // call_tool admits only tools that are themselves explicitly read-only;
1852
1278
  // anything else is refused and routed to call_destructive_tool.
1853
1279
  annotations: READ_ONLY_REMOTE,
@@ -1859,15 +1285,7 @@ export function registerMetaTools(
1859
1285
  "call_destructive_tool",
1860
1286
  {
1861
1287
  description: CALL_DESTRUCTIVE_DESC,
1862
- inputSchema: {
1863
- address: z.string(),
1864
- args: z.record(z.string(), z.unknown()).optional(),
1865
- fields: z.array(z.string()).optional(),
1866
- resultMode: z.enum(["mcp", "value"]).optional(),
1867
- timeoutMs: z.number().int().positive().optional(),
1868
- maxRetries: z.number().int().min(0).max(2).optional(),
1869
- diagnostics: z.boolean().optional(),
1870
- },
1288
+ inputSchema: CALL_INPUT_SCHEMA,
1871
1289
  annotations: {
1872
1290
  destructiveHint: true,
1873
1291
  readOnlyHint: false,
@@ -1920,17 +1338,7 @@ export function registerMetaTools(
1920
1338
  description: BATCH_DESC,
1921
1339
  inputSchema: {
1922
1340
  calls: z
1923
- .array(
1924
- z.object({
1925
- address: z.string(),
1926
- args: z.record(z.string(), z.unknown()).optional(),
1927
- fields: z.array(z.string()).optional(),
1928
- resultMode: z.enum(["mcp", "value"]).optional(),
1929
- timeoutMs: z.number().int().positive().optional(),
1930
- maxRetries: z.number().int().min(0).max(2).optional(),
1931
- diagnostics: z.boolean().optional(),
1932
- }),
1933
- )
1341
+ .array(z.object(CALL_INPUT_SCHEMA))
1934
1342
  .min(1)
1935
1343
  .max(10),
1936
1344
  resultMode: z.enum(["mcp", "value"]).optional(),