@zackbart/connecta 0.8.0 → 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 (143) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +31 -55
  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 +2 -23
  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 +0 -1
  17. package/dist/call-admission.js.map +1 -1
  18. package/dist/catalog-service.d.ts +1 -2
  19. package/dist/catalog-service.d.ts.map +1 -1
  20. package/dist/catalog-service.js +12 -18
  21. package/dist/catalog-service.js.map +1 -1
  22. package/dist/catalog.d.ts +6 -0
  23. package/dist/catalog.d.ts.map +1 -1
  24. package/dist/catalog.js +51 -0
  25. package/dist/catalog.js.map +1 -1
  26. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  27. package/dist/connectors/remote-mcp.js +9 -22
  28. package/dist/connectors/remote-mcp.js.map +1 -1
  29. package/dist/errors.d.ts +18 -0
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/execute.d.ts +1 -8
  33. package/dist/execute.d.ts.map +1 -1
  34. package/dist/execute.js +3 -13
  35. package/dist/execute.js.map +1 -1
  36. package/dist/executor-result.js +1 -1
  37. package/dist/executor-result.js.map +1 -1
  38. package/dist/index.d.ts +3 -82
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +17 -90
  41. package/dist/index.js.map +1 -1
  42. package/dist/invocation.d.ts +5 -0
  43. package/dist/invocation.d.ts.map +1 -1
  44. package/dist/invocation.js +46 -10
  45. package/dist/invocation.js.map +1 -1
  46. package/dist/meta-tools.d.ts +3 -11
  47. package/dist/meta-tools.d.ts.map +1 -1
  48. package/dist/meta-tools.js +105 -157
  49. package/dist/meta-tools.js.map +1 -1
  50. package/dist/operator-ui/generated.d.ts +2 -2
  51. package/dist/operator-ui/generated.d.ts.map +1 -1
  52. package/dist/operator-ui/generated.js +2 -2
  53. package/dist/operator-ui/generated.js.map +1 -1
  54. package/dist/operator-ui/model.d.ts +1 -22
  55. package/dist/operator-ui/model.d.ts.map +1 -1
  56. package/dist/operator-ui/model.js.map +1 -1
  57. package/dist/registry.d.ts +11 -178
  58. package/dist/registry.d.ts.map +1 -1
  59. package/dist/registry.js +52 -330
  60. package/dist/registry.js.map +1 -1
  61. package/dist/retired-toolkits.d.ts +3 -0
  62. package/dist/retired-toolkits.d.ts.map +1 -0
  63. package/dist/retired-toolkits.js +9 -0
  64. package/dist/retired-toolkits.js.map +1 -0
  65. package/dist/routes/activity.d.ts.map +1 -1
  66. package/dist/routes/activity.js +2 -5
  67. package/dist/routes/activity.js.map +1 -1
  68. package/dist/routes/credentials.d.ts.map +1 -1
  69. package/dist/routes/credentials.js +1 -12
  70. package/dist/routes/credentials.js.map +1 -1
  71. package/dist/routes/mcp.d.ts.map +1 -1
  72. package/dist/routes/mcp.js +32 -145
  73. package/dist/routes/mcp.js.map +1 -1
  74. package/dist/routes/oauth.d.ts.map +1 -1
  75. package/dist/routes/oauth.js +2 -7
  76. package/dist/routes/oauth.js.map +1 -1
  77. package/dist/routes/shared.d.ts +7 -28
  78. package/dist/routes/shared.d.ts.map +1 -1
  79. package/dist/routes/shared.js +10 -69
  80. package/dist/routes/shared.js.map +1 -1
  81. package/dist/routes/ui.d.ts.map +1 -1
  82. package/dist/routes/ui.js +4 -10
  83. package/dist/routes/ui.js.map +1 -1
  84. package/dist/server.d.ts.map +1 -1
  85. package/dist/server.js +1 -31
  86. package/dist/server.js.map +1 -1
  87. package/dist/skills.d.ts +3 -6
  88. package/dist/skills.d.ts.map +1 -1
  89. package/dist/skills.js +9 -38
  90. package/dist/skills.js.map +1 -1
  91. package/dist/timeout.d.ts.map +1 -1
  92. package/dist/timeout.js +2 -4
  93. package/dist/timeout.js.map +1 -1
  94. package/dist/types.d.ts +2 -59
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/ui.d.ts +1 -2
  97. package/dist/ui.d.ts.map +1 -1
  98. package/dist/ui.js +5 -35
  99. package/dist/ui.js.map +1 -1
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/package.json +1 -1
  103. package/src/activity.ts +0 -9
  104. package/src/auth/bearer.ts +3 -32
  105. package/src/auth/clerk.ts +35 -48
  106. package/src/call-admission.ts +0 -1
  107. package/src/catalog-service.ts +23 -23
  108. package/src/catalog.ts +55 -0
  109. package/src/connectors/remote-mcp.ts +11 -23
  110. package/src/errors.ts +20 -0
  111. package/src/execute.ts +3 -18
  112. package/src/executor-result.ts +1 -1
  113. package/src/index.ts +24 -208
  114. package/src/invocation.ts +63 -10
  115. package/src/meta-tools.ts +122 -174
  116. package/src/operator-ui/browser.css +1 -54
  117. package/src/operator-ui/browser.ts +16 -150
  118. package/src/operator-ui/generated.ts +2 -2
  119. package/src/operator-ui/model.ts +1 -23
  120. package/src/registry.ts +59 -429
  121. package/src/retired-toolkits.ts +14 -0
  122. package/src/routes/activity.ts +1 -6
  123. package/src/routes/credentials.ts +0 -12
  124. package/src/routes/mcp.ts +40 -186
  125. package/src/routes/oauth.ts +1 -7
  126. package/src/routes/shared.ts +8 -98
  127. package/src/routes/ui.ts +2 -12
  128. package/src/server.ts +1 -31
  129. package/src/skills.ts +9 -38
  130. package/src/timeout.ts +2 -4
  131. package/src/types.ts +1 -60
  132. package/src/ui.ts +9 -46
  133. package/src/version.ts +1 -1
  134. package/dist/credential-health.d.ts +0 -205
  135. package/dist/credential-health.d.ts.map +0 -1
  136. package/dist/credential-health.js +0 -610
  137. package/dist/credential-health.js.map +0 -1
  138. package/dist/toolkits.d.ts +0 -124
  139. package/dist/toolkits.d.ts.map +0 -1
  140. package/dist/toolkits.js +0 -322
  141. package/dist/toolkits.js.map +0 -1
  142. package/src/credential-health.ts +0 -835
  143. package/src/toolkits.ts +0 -461
package/src/invocation.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  classifyCallError,
14
14
  ConnectorCallError,
15
15
  messageLooksRetryable,
16
+ type AuthRecoveryMode,
16
17
  type CallErrorDetails,
17
18
  } from "./errors.js";
18
19
  import { unwrapMcpResult } from "./mcp-result.js";
@@ -81,6 +82,21 @@ function callerCancelledDetails(): CallErrorDetails {
81
82
  };
82
83
  }
83
84
 
85
+ function recoveryMode(
86
+ registry: RegistryView,
87
+ connector: ResolvedCatalogTool["connector"],
88
+ baseUrl: string,
89
+ ): AuthRecoveryMode {
90
+ if (connector.startAuth) return "oauth";
91
+ if (
92
+ connector.credential &&
93
+ registry.contextFor(connector.id, baseUrl).credential
94
+ ) {
95
+ return "operator_config";
96
+ }
97
+ return "unavailable";
98
+ }
99
+
84
100
  function isCallerCancellation(
85
101
  error: unknown,
86
102
  signal: AbortSignal | undefined,
@@ -155,6 +171,11 @@ export class InvocationFailure extends Error {
155
171
  readonly code: string;
156
172
  readonly retryable: boolean;
157
173
  readonly retryAfterMs: number | undefined;
174
+ readonly connector: string | undefined;
175
+ readonly operation: string | undefined;
176
+ readonly recovery: CallErrorDetails["recovery"];
177
+ readonly nextAction: CallErrorDetails["nextAction"];
178
+ readonly retry: string | undefined;
158
179
 
159
180
  constructor(readonly details: CallErrorDetails) {
160
181
  super(details.message);
@@ -162,6 +183,11 @@ export class InvocationFailure extends Error {
162
183
  this.code = details.code;
163
184
  this.retryable = details.retryable;
164
185
  this.retryAfterMs = details.retryAfterMs;
186
+ this.connector = details.connector;
187
+ this.operation = details.operation;
188
+ this.recovery = details.recovery;
189
+ this.nextAction = details.nextAction;
190
+ this.retry = details.retry;
165
191
  }
166
192
  }
167
193
 
@@ -262,13 +288,36 @@ export class InvocationService {
262
288
  };
263
289
  const failed = (error: CallErrorDetails): InvocationOutcome<T> => {
264
290
  const diagnostics = timing();
291
+ const target = resolved ?? activityTarget;
292
+ const details =
293
+ error.code === "auth_required" && target
294
+ ? {
295
+ ...error,
296
+ connector: target.connector.id,
297
+ operation: `${target.connector.id}.${target.toolName}`,
298
+ recovery: recoveryMode(
299
+ this.registry,
300
+ target.connector,
301
+ this.catalog.baseUrl,
302
+ ),
303
+ nextAction: {
304
+ tool: "authorize_connector" as const,
305
+ arguments: { connector: target.connector.id },
306
+ operatorHandoff:
307
+ "Give the URL and instructions it returns to the operator.",
308
+ },
309
+ retry:
310
+ `Retry ${target.connector.id}.${target.toolName} after ` +
311
+ "the operator completes recovery.",
312
+ }
313
+ : error;
265
314
  record(
266
- error.code === "timeout"
315
+ details.code === "timeout"
267
316
  ? "timeout"
268
- : error.code === "cancelled"
317
+ : details.code === "cancelled"
269
318
  ? "cancelled"
270
319
  : "error",
271
- error.code,
320
+ details.code,
272
321
  );
273
322
  return {
274
323
  ok: false,
@@ -276,7 +325,7 @@ export class InvocationService {
276
325
  attempts,
277
326
  timing: diagnostics,
278
327
  ...(resolved ? { resolved } : {}),
279
- error,
328
+ error: details,
280
329
  };
281
330
  };
282
331
 
@@ -299,12 +348,9 @@ export class InvocationService {
299
348
  // against (a revoked downstream grant, say) still reads clean from the
300
349
  // cheap `list_connectors({ probe: false })` signal.
301
350
  //
302
- // Recorded HERE rather than inside the registry's catalog fetch on
303
- // purpose: `registry` is this connection's VIEW, so a toolkit-scoped
304
- // session records into its own log as well as the deployment-wide one,
305
- // which `Registry.refreshTools` could not reach. A cache hit that
306
- // avoids a live listTools call records nothing either way — it is not
307
- // evidence of health, and success stays what it has always been: an
351
+ // Recorded HERE rather than inside the registry's catalog fetch because
352
+ // a cache hit that avoids a live listTools call records nothing — it is
353
+ // not evidence of health. Success stays what it has always been: an
308
354
  // actual downstream call that returned.
309
355
  this.registry.recordFailure(
310
356
  resolution.connector.id,
@@ -383,6 +429,13 @@ export class InvocationService {
383
429
  : {}),
384
430
  },
385
431
  );
432
+ if (resolved.connector.credential && !connectorContext.credential) {
433
+ throw new ConnectorCallError(
434
+ "auth_required",
435
+ "Operator-managed credential storage is not configured. Call " +
436
+ `authorize_connector({ connector: "${resolved.connector.id}" }).`,
437
+ );
438
+ }
386
439
  let rejectCancelled!: (reason: unknown) => void;
387
440
  const cancelled = controller
388
441
  ? new Promise<never>((_, reject) => {
package/src/meta-tools.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  import {
8
8
  boundedDiscoveryText,
9
9
  CatalogService,
10
+ DEFAULT_SEARCH_LIMIT,
10
11
  DiscoveryPolicyError,
11
12
  groupedSearchResult,
12
13
  MAX_DESCRIBE_ADDRESSES,
@@ -48,7 +49,6 @@ import {
48
49
  normalizeTimeoutMs,
49
50
  withAbortableTimeout,
50
51
  } from "./timeout.js";
51
- import { credentialVerdictApplies } from "./credential-health.js";
52
52
  import type { ConnectorStatus, KVStorage } from "./types.js";
53
53
 
54
54
  export {
@@ -56,7 +56,6 @@ export {
56
56
  MAX_DISCOVERY_RESULT_BYTES,
57
57
  MAX_RETRY_BACKOFF_MS,
58
58
  MAX_SEARCH_LIMIT,
59
- DiscoveryPolicyError,
60
59
  retryBackoffMs,
61
60
  };
62
61
 
@@ -77,7 +76,7 @@ const dec = new TextDecoder();
77
76
 
78
77
  export function jsonResult(obj: unknown): ToolResult {
79
78
  return {
80
- content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
79
+ content: [{ type: "text", text: JSON.stringify(obj) }],
81
80
  ...(obj !== null && typeof obj === "object" && !Array.isArray(obj)
82
81
  ? { structuredContent: obj as Record<string, unknown> }
83
82
  : {}),
@@ -108,8 +107,12 @@ function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
108
107
  return result;
109
108
  }
110
109
 
111
- function discoveryResult(value: unknown, hint: string): ToolResult {
110
+ async function discoveryResult(
111
+ operation: () => unknown | Promise<unknown>,
112
+ hint: string,
113
+ ): Promise<ToolResult> {
112
114
  try {
115
+ const value = await operation();
113
116
  const text = boundedDiscoveryText(value, hint);
114
117
  return {
115
118
  content: [{ type: "text", text }],
@@ -255,7 +258,7 @@ function applyFieldsToContent(
255
258
  } catch {
256
259
  return b;
257
260
  }
258
- return { ...b, text: JSON.stringify(applyFields(parsed, fields), null, 2) };
261
+ return { ...b, text: JSON.stringify(applyFields(parsed, fields)) };
259
262
  });
260
263
  }
261
264
 
@@ -277,7 +280,7 @@ function applyFieldsToContent(
277
280
  * at all (a BigInt) still throws, as before, and is reported as a failure.
278
281
  */
279
282
  function serializeResultText(value: unknown): string {
280
- const serialized = JSON.stringify(value, null, 2);
283
+ const serialized = JSON.stringify(value);
281
284
  return serialized === undefined ? String(value) : serialized;
282
285
  }
283
286
 
@@ -391,7 +394,7 @@ async function guardContent(
391
394
  ): Promise<ToolResult> {
392
395
  let text: string;
393
396
  try {
394
- text = JSON.stringify(content, null, 2);
397
+ text = JSON.stringify(content);
395
398
  } catch {
396
399
  // A block carrying a BigInt or a cycle cannot be serialized, so it cannot
397
400
  // be measured, stashed, or paged either — there is nothing this guard could
@@ -453,15 +456,7 @@ export interface GetResultArgs {
453
456
  /** Page size in bytes; a whole number >= 1. Defaults to the deployment cap. */
454
457
  maxBytes?: number;
455
458
  }
456
- export interface BatchCall {
457
- address: string;
458
- args?: Record<string, unknown>;
459
- fields?: string[];
460
- resultMode?: ResultMode;
461
- timeoutMs?: number;
462
- maxRetries?: number;
463
- diagnostics?: boolean;
464
- }
459
+ export type BatchCall = CallArgs;
465
460
  export interface BatchArgs {
466
461
  calls: BatchCall[];
467
462
  resultMode?: ResultMode;
@@ -614,17 +609,22 @@ export function createMetaTools(
614
609
  },
615
610
  );
616
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;
625
+ }
617
626
  return {
618
- toolResult:
619
- call.resultMode === "value"
620
- ? jsonResult({
621
- ok: false,
622
- error: outcome.error,
623
- durationMs: outcome.durationMs,
624
- attempts: outcome.attempts,
625
- ...(call.diagnostics ? { timing: outcome.timing } : {}),
626
- })
627
- : errorResult(outcome.error.message),
627
+ toolResult: failedResult,
628
628
  durationMs: outcome.durationMs,
629
629
  attempts: outcome.attempts,
630
630
  timing: outcome.timing,
@@ -683,11 +683,13 @@ export function createMetaTools(
683
683
  const inspect = async (c: (typeof connectors)[number]) => {
684
684
  const statusStarted = Date.now();
685
685
  const observed = registry.healthFor(c.id);
686
- const verdict = await registry.credentialHealthFor(c.id);
686
+ const drift = await registry.credentialDriftFor(c.id);
687
687
  let status:
688
688
  | ConnectorStatus
689
689
  | { state: "ok" | "error" | "unknown"; message?: string };
690
- if (probe) {
690
+ if (drift) {
691
+ status = { state: "auth_required", message: drift };
692
+ } else if (probe) {
691
693
  try {
692
694
  status = await withProbeDeadline(
693
695
  `list_connectors probe of "${c.id}"`,
@@ -700,32 +702,7 @@ export function createMetaTools(
700
702
  // hanging the whole list_connectors call.
701
703
  status = { state: "error", message: msg(err) };
702
704
  }
703
- } else if (
704
- verdict &&
705
- // Deployment-wide, deliberately, like `hasObservedSuccess` beside
706
- // it: a sibling toolkit's successful call proves the shared
707
- // credential works, and a verdict retired for one view but not
708
- // another would make the same connector read differently per scope
709
- // for a reason that has nothing to do with scope.
710
- credentialVerdictApplies(verdict, registry.observedSuccessAt(c.id))
711
- ) {
712
- // The proactive layer (issue #24): a liveness check already found
713
- // the stored credential dead, so say so on the cheap path instead of
714
- // waiting for an agent's real call to discover it. Only while it is
715
- // the freshest evidence — a successful call since then retires it.
716
- status = {
717
- state: verdict.state,
718
- ...(verdict.message ? { message: verdict.message } : {}),
719
- ...(verdict.authorizationUrl
720
- ? { authorizationUrl: verdict.authorizationUrl }
721
- : {}),
722
- };
723
705
  } else {
724
- // "error" comes from THIS view's own observations — a sibling
725
- // toolkit's failure is not this session's experience — while
726
- // ok/unknown may lean on the deployment-wide success signal, since
727
- // "the connector answers at all" is a fact about the connector.
728
- // Unscoped, the two are the same log, so this is unchanged there.
729
706
  const derived =
730
707
  observed?.consecutiveFailures && observed.consecutiveFailures > 0
731
708
  ? ("error" as const)
@@ -733,43 +710,13 @@ export function createMetaTools(
733
710
  ? ("ok" as const)
734
711
  : ("unknown" as const);
735
712
  status = {
736
- // A successful liveness check upgrades "unknown" — nothing has
737
- // been called yet, but the credential was verified, which is how
738
- // re-authorization shows up here as ok rather than as an absence
739
- // of evidence. It never DOWNgrades an observed failure: a real
740
- // call that failed is stronger evidence than a background check.
741
- state:
742
- derived === "unknown" && verdict?.state === "ok"
743
- ? ("ok" as const)
744
- : derived,
713
+ state: derived,
745
714
  ...(observed?.lastError ? { message: observed.lastError } : {}),
746
715
  };
747
716
  }
748
- // Stamped where the observation actually happened after the status
749
- // probe, not before it. A 30-second probe stamped at its start would
750
- // report a verdict older than it is, and would lose the race against a
751
- // real call that succeeded WHILE it ran (that success must retire the
752
- // verdict, and only an honest timestamp says so).
717
+ // Stamped after any live probe so the response reports when its
718
+ // observation completed, not when a potentially slow request began.
753
719
  const checkedAt = new Date().toISOString();
754
- // A live status probe IS a liveness observation of the stored
755
- // credential, so it updates the same verdict a background check
756
- // writes: the cached read afterwards agrees with what the operator
757
- // just saw, and they are not swept again moments later. Recorded from
758
- // the STATUS phase only, and only when the connector actually answered
759
- // — a catalog refresh below is not a credential check (the sweep never
760
- // fetches one), it is already counted in the health log, and letting
761
- // its failure land here would spend the freshness budget on it. The
762
- // registry ignores this for connectors storing no credential of ours.
763
- if (probe && (status.state === "ok" || status.state === "auth_required")) {
764
- await registry.recordCredentialHealth(c.id, {
765
- state: status.state,
766
- checkedAt,
767
- ...(status.message ? { message: status.message } : {}),
768
- ...("authorizationUrl" in status && status.authorizationUrl
769
- ? { authorizationUrl: status.authorizationUrl }
770
- : {}),
771
- });
772
- }
773
720
  let tools = registry.peekTools(c.id);
774
721
  // An auth_required status may have just started OAuth. A second
775
722
  // listTools probe would overwrite its state/verifier while returning
@@ -803,15 +750,6 @@ export function createMetaTools(
803
750
  state: "auth_required" as const,
804
751
  message: details.message,
805
752
  };
806
- await registry.recordCredentialHealth(c.id, {
807
- state: "auth_required",
808
- checkedAt,
809
- ...(status.message && { message: status.message }),
810
- ...("authorizationUrl" in status &&
811
- status.authorizationUrl
812
- ? { authorizationUrl: status.authorizationUrl }
813
- : {}),
814
- });
815
753
  } else {
816
754
  status = { state: "error" as const, message: msg(err) };
817
755
  }
@@ -820,9 +758,6 @@ export function createMetaTools(
820
758
  }
821
759
  const latencyMs = Date.now() - statusStarted;
822
760
  const latestObserved = registry.healthFor(c.id);
823
- const credentialCheck = probe
824
- ? await registry.credentialHealthFor(c.id)
825
- : verdict;
826
761
  return {
827
762
  id: c.id,
828
763
  ...(c.title ? { title: c.title } : {}),
@@ -833,7 +768,6 @@ export function createMetaTools(
833
768
  latencyMs,
834
769
  probe,
835
770
  ...(latestObserved ?? observed),
836
- ...(credentialCheck ? { credentialCheck } : {}),
837
771
  ...("authorizationUrl" in status &&
838
772
  status.authorizationUrl && {
839
773
  authorizationUrl: status.authorizationUrl,
@@ -866,31 +800,18 @@ export function createMetaTools(
866
800
  },
867
801
 
868
802
  async searchTools(args: SearchArgs): Promise<ToolResult> {
869
- try {
870
- return discoveryResult(
803
+ return discoveryResult(
804
+ async () =>
871
805
  groupedSearchResult(await catalog.search(args)),
872
- "Request a smaller limit, omit fullDescriptions, or use compact schemas.",
873
- );
874
- } catch (err) {
875
- if (err instanceof DiscoveryPolicyError) {
876
- return discoveryErrorResult(err);
877
- }
878
- throw err;
879
- }
806
+ "Request a smaller limit, omit fullDescriptions, or use compact schemas.",
807
+ );
880
808
  },
881
809
 
882
810
  async describeTools(args: DescribeArgs): Promise<ToolResult> {
883
- try {
884
- return discoveryResult(
885
- { tools: await catalog.describe(args) },
886
- 'Split the address list or use format: "compact".',
887
- );
888
- } catch (err) {
889
- if (err instanceof DiscoveryPolicyError) {
890
- return discoveryErrorResult(err);
891
- }
892
- throw err;
893
- }
811
+ return discoveryResult(
812
+ async () => ({ tools: await catalog.describe(args) }),
813
+ 'Split the address list or use format: "compact".',
814
+ );
894
815
  },
895
816
 
896
817
  async callTool(args: CallArgs): Promise<ToolResult> {
@@ -1076,6 +997,21 @@ export function createMetaTools(
1076
997
  ...(details.retryAfterMs !== undefined
1077
998
  ? { retryAfterMs: details.retryAfterMs }
1078
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
+ : {}),
1079
1015
  },
1080
1016
  };
1081
1017
  }),
@@ -1090,9 +1026,55 @@ export function createMetaTools(
1090
1026
  return errorResult(`Unknown connector "${args.connector}"`);
1091
1027
  }
1092
1028
  if (!connector.startAuth) {
1093
- return errorResult(
1094
- `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,
1095
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
+ });
1096
1078
  }
1097
1079
  const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
1098
1080
  try {
@@ -1100,17 +1082,6 @@ export function createMetaTools(
1100
1082
  ctx,
1101
1083
  args.force !== undefined ? { force: args.force } : {},
1102
1084
  );
1103
- // startAuth just spoke to the downstream about this exact credential, so
1104
- // its answer replaces any older liveness verdict — including the stale
1105
- // `auth_required` that sent the agent here, once it reports ok.
1106
- await registry.recordCredentialHealth(connector.id, {
1107
- state: status.state,
1108
- checkedAt: new Date().toISOString(),
1109
- ...(status.message ? { message: status.message } : {}),
1110
- ...(status.authorizationUrl
1111
- ? { authorizationUrl: status.authorizationUrl }
1112
- : {}),
1113
- });
1114
1085
  if (status.state === "auth_required" && !status.authorizationUrl) {
1115
1086
  // auth_required with nothing to open is a dead end for the operator.
1116
1087
  return errorResult(
@@ -1119,6 +1090,7 @@ export function createMetaTools(
1119
1090
  }
1120
1091
  return jsonResult({
1121
1092
  connector: connector.id,
1093
+ recovery: "oauth",
1122
1094
  status: status.state,
1123
1095
  ...(status.authorizationUrl
1124
1096
  ? {
@@ -1142,7 +1114,7 @@ export function createMetaTools(
1142
1114
 
1143
1115
  const LIST_DESC =
1144
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.";
1145
- 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.`;
1146
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.`;
1147
1119
  const CALL_DESC =
1148
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.';
@@ -1153,7 +1125,7 @@ const GET_RESULT_DESC =
1153
1125
  const BATCH_DESC =
1154
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.";
1155
1127
  const AUTHORIZE_DESC =
1156
- "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.";
1157
1129
  const SKILLS_DESC =
1158
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.';
1159
1131
 
@@ -1163,10 +1135,7 @@ const SKILLS_DESC =
1163
1135
  * so a deployment with no guides gets every base description unchanged rather
1164
1136
  * than paying for text about a feature it does not use.
1165
1137
  *
1166
- * Registration is per connection and reads the connection's own registry view,
1167
- * so under a toolkit these sentences reflect the SCOPED connector set: a scoped
1168
- * session whose connectors carry no guides sees the base descriptions, and
1169
- * never learns from a tool description that guides exist out of scope.
1138
+ * Registration is per connection and reads the configured connector set.
1170
1139
  */
1171
1140
  const GUIDE_NOTES = {
1172
1141
  skills:
@@ -1208,6 +1177,16 @@ const READ_ONLY_LOCAL = {
1208
1177
  openWorldHint: false,
1209
1178
  } as const;
1210
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
+
1211
1190
  /** Register the nine meta-tools onto an McpServer instance. */
1212
1191
  export function registerMetaTools(
1213
1192
  server: McpServer,
@@ -1244,12 +1223,7 @@ export function registerMetaTools(
1244
1223
  {
1245
1224
  description: describedFor(registry, SKILLS_DESC, "skills"),
1246
1225
  inputSchema: { name: z.string().optional() },
1247
- annotations: {
1248
- readOnlyHint: true,
1249
- destructiveHint: false,
1250
- idempotentHint: true,
1251
- openWorldHint: false,
1252
- },
1226
+ annotations: READ_ONLY_LOCAL,
1253
1227
  },
1254
1228
  async (args) => mt.skills(args as SkillArgs),
1255
1229
  );
@@ -1299,15 +1273,7 @@ export function registerMetaTools(
1299
1273
  "call_tool",
1300
1274
  {
1301
1275
  description: CALL_DESC,
1302
- inputSchema: {
1303
- address: z.string(),
1304
- args: z.record(z.string(), z.unknown()).optional(),
1305
- fields: z.array(z.string()).optional(),
1306
- resultMode: z.enum(["mcp", "value"]).optional(),
1307
- timeoutMs: z.number().int().positive().optional(),
1308
- maxRetries: z.number().int().min(0).max(2).optional(),
1309
- diagnostics: z.boolean().optional(),
1310
- },
1276
+ inputSchema: CALL_INPUT_SCHEMA,
1311
1277
  // call_tool admits only tools that are themselves explicitly read-only;
1312
1278
  // anything else is refused and routed to call_destructive_tool.
1313
1279
  annotations: READ_ONLY_REMOTE,
@@ -1319,15 +1285,7 @@ export function registerMetaTools(
1319
1285
  "call_destructive_tool",
1320
1286
  {
1321
1287
  description: CALL_DESTRUCTIVE_DESC,
1322
- inputSchema: {
1323
- address: z.string(),
1324
- args: z.record(z.string(), z.unknown()).optional(),
1325
- fields: z.array(z.string()).optional(),
1326
- resultMode: z.enum(["mcp", "value"]).optional(),
1327
- timeoutMs: z.number().int().positive().optional(),
1328
- maxRetries: z.number().int().min(0).max(2).optional(),
1329
- diagnostics: z.boolean().optional(),
1330
- },
1288
+ inputSchema: CALL_INPUT_SCHEMA,
1331
1289
  annotations: {
1332
1290
  destructiveHint: true,
1333
1291
  readOnlyHint: false,
@@ -1380,17 +1338,7 @@ export function registerMetaTools(
1380
1338
  description: BATCH_DESC,
1381
1339
  inputSchema: {
1382
1340
  calls: z
1383
- .array(
1384
- z.object({
1385
- address: z.string(),
1386
- args: z.record(z.string(), z.unknown()).optional(),
1387
- fields: z.array(z.string()).optional(),
1388
- resultMode: z.enum(["mcp", "value"]).optional(),
1389
- timeoutMs: z.number().int().positive().optional(),
1390
- maxRetries: z.number().int().min(0).max(2).optional(),
1391
- diagnostics: z.boolean().optional(),
1392
- }),
1393
- )
1341
+ .array(z.object(CALL_INPUT_SCHEMA))
1394
1342
  .min(1)
1395
1343
  .max(10),
1396
1344
  resultMode: z.enum(["mcp", "value"]).optional(),