@zackbart/connecta 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +31 -55
  3. package/SECURITY.md +5 -11
  4. package/dist/activity.d.ts +0 -8
  5. package/dist/activity.d.ts.map +1 -1
  6. package/dist/activity.js +0 -1
  7. package/dist/activity.js.map +1 -1
  8. package/dist/auth/bearer.d.ts +1 -7
  9. package/dist/auth/bearer.d.ts.map +1 -1
  10. package/dist/auth/bearer.js +2 -23
  11. package/dist/auth/bearer.js.map +1 -1
  12. package/dist/auth/clerk.d.ts +2 -11
  13. package/dist/auth/clerk.d.ts.map +1 -1
  14. package/dist/auth/clerk.js +31 -46
  15. package/dist/auth/clerk.js.map +1 -1
  16. package/dist/auth/downstream-oauth.d.ts +15 -6
  17. package/dist/auth/downstream-oauth.d.ts.map +1 -1
  18. package/dist/auth/downstream-oauth.js +60 -11
  19. package/dist/auth/downstream-oauth.js.map +1 -1
  20. package/dist/call-admission.d.ts.map +1 -1
  21. package/dist/call-admission.js +0 -1
  22. package/dist/call-admission.js.map +1 -1
  23. package/dist/catalog-service.d.ts +1 -2
  24. package/dist/catalog-service.d.ts.map +1 -1
  25. package/dist/catalog-service.js +12 -18
  26. package/dist/catalog-service.js.map +1 -1
  27. package/dist/catalog.d.ts +6 -0
  28. package/dist/catalog.d.ts.map +1 -1
  29. package/dist/catalog.js +51 -0
  30. package/dist/catalog.js.map +1 -1
  31. package/dist/connectors/remote-mcp.d.ts +1 -1
  32. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  33. package/dist/connectors/remote-mcp.js +73 -71
  34. package/dist/connectors/remote-mcp.js.map +1 -1
  35. package/dist/errors.d.ts +19 -1
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js +1 -0
  38. package/dist/errors.js.map +1 -1
  39. package/dist/execute.d.ts +4 -9
  40. package/dist/execute.d.ts.map +1 -1
  41. package/dist/execute.js +41 -20
  42. package/dist/execute.js.map +1 -1
  43. package/dist/executor-result.js +1 -1
  44. package/dist/executor-result.js.map +1 -1
  45. package/dist/index.d.ts +3 -82
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +17 -90
  48. package/dist/index.js.map +1 -1
  49. package/dist/invocation.d.ts +5 -0
  50. package/dist/invocation.d.ts.map +1 -1
  51. package/dist/invocation.js +46 -10
  52. package/dist/invocation.js.map +1 -1
  53. package/dist/meta-tools.d.ts +4 -12
  54. package/dist/meta-tools.d.ts.map +1 -1
  55. package/dist/meta-tools.js +119 -169
  56. package/dist/meta-tools.js.map +1 -1
  57. package/dist/operator-ui/generated.d.ts +2 -2
  58. package/dist/operator-ui/generated.d.ts.map +1 -1
  59. package/dist/operator-ui/generated.js +2 -2
  60. package/dist/operator-ui/generated.js.map +1 -1
  61. package/dist/operator-ui/model.d.ts +1 -22
  62. package/dist/operator-ui/model.d.ts.map +1 -1
  63. package/dist/operator-ui/model.js.map +1 -1
  64. package/dist/registry.d.ts +11 -178
  65. package/dist/registry.d.ts.map +1 -1
  66. package/dist/registry.js +52 -330
  67. package/dist/registry.js.map +1 -1
  68. package/dist/retired-toolkits.d.ts +3 -0
  69. package/dist/retired-toolkits.d.ts.map +1 -0
  70. package/dist/retired-toolkits.js +9 -0
  71. package/dist/retired-toolkits.js.map +1 -0
  72. package/dist/routes/activity.d.ts.map +1 -1
  73. package/dist/routes/activity.js +2 -5
  74. package/dist/routes/activity.js.map +1 -1
  75. package/dist/routes/credentials.d.ts.map +1 -1
  76. package/dist/routes/credentials.js +1 -12
  77. package/dist/routes/credentials.js.map +1 -1
  78. package/dist/routes/mcp.d.ts.map +1 -1
  79. package/dist/routes/mcp.js +94 -188
  80. package/dist/routes/mcp.js.map +1 -1
  81. package/dist/routes/oauth.d.ts.map +1 -1
  82. package/dist/routes/oauth.js +3 -8
  83. package/dist/routes/oauth.js.map +1 -1
  84. package/dist/routes/shared.d.ts +9 -30
  85. package/dist/routes/shared.d.ts.map +1 -1
  86. package/dist/routes/shared.js +10 -69
  87. package/dist/routes/shared.js.map +1 -1
  88. package/dist/routes/ui.d.ts.map +1 -1
  89. package/dist/routes/ui.js +4 -10
  90. package/dist/routes/ui.js.map +1 -1
  91. package/dist/server.d.ts.map +1 -1
  92. package/dist/server.js +1 -31
  93. package/dist/server.js.map +1 -1
  94. package/dist/skills.d.ts +3 -6
  95. package/dist/skills.d.ts.map +1 -1
  96. package/dist/skills.js +9 -38
  97. package/dist/skills.js.map +1 -1
  98. package/dist/timeout.d.ts.map +1 -1
  99. package/dist/timeout.js +2 -4
  100. package/dist/timeout.js.map +1 -1
  101. package/dist/types.d.ts +8 -61
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/ui.d.ts +1 -2
  104. package/dist/ui.d.ts.map +1 -1
  105. package/dist/ui.js +5 -35
  106. package/dist/ui.js.map +1 -1
  107. package/dist/version.d.ts +1 -1
  108. package/dist/version.js +1 -1
  109. package/package.json +3 -2
  110. package/src/activity.ts +0 -9
  111. package/src/auth/bearer.ts +3 -32
  112. package/src/auth/clerk.ts +35 -48
  113. package/src/auth/downstream-oauth.ts +106 -24
  114. package/src/call-admission.ts +0 -1
  115. package/src/catalog-service.ts +23 -23
  116. package/src/catalog.ts +55 -0
  117. package/src/connectors/remote-mcp.ts +106 -86
  118. package/src/errors.ts +22 -0
  119. package/src/execute.ts +43 -24
  120. package/src/executor-result.ts +1 -1
  121. package/src/index.ts +24 -208
  122. package/src/invocation.ts +63 -10
  123. package/src/meta-tools.ts +139 -187
  124. package/src/operator-ui/browser.css +1 -54
  125. package/src/operator-ui/browser.ts +16 -150
  126. package/src/operator-ui/generated.ts +2 -2
  127. package/src/operator-ui/model.ts +1 -23
  128. package/src/registry.ts +59 -429
  129. package/src/retired-toolkits.ts +14 -0
  130. package/src/routes/activity.ts +1 -6
  131. package/src/routes/credentials.ts +0 -12
  132. package/src/routes/mcp.ts +110 -230
  133. package/src/routes/oauth.ts +2 -8
  134. package/src/routes/shared.ts +10 -100
  135. package/src/routes/ui.ts +2 -12
  136. package/src/server.ts +1 -31
  137. package/src/skills.ts +9 -38
  138. package/src/timeout.ts +2 -4
  139. package/src/types.ts +11 -62
  140. package/src/ui.ts +9 -46
  141. package/src/version.ts +1 -1
  142. package/dist/credential-health.d.ts +0 -205
  143. package/dist/credential-health.d.ts.map +0 -1
  144. package/dist/credential-health.js +0 -610
  145. package/dist/credential-health.js.map +0 -1
  146. package/dist/toolkits.d.ts +0 -124
  147. package/dist/toolkits.d.ts.map +0 -1
  148. package/dist/toolkits.js +0 -322
  149. package/dist/toolkits.js.map +0 -1
  150. package/src/credential-health.ts +0 -835
  151. package/src/toolkits.ts +0 -461
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, groupedSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
2
+ import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError, groupedSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
3
3
  import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
4
4
  import { closeConnectorScope, } from "./connector-scope.js";
5
5
  import { classifyCallError, messageLooksRetryable, } from "./errors.js";
@@ -7,14 +7,13 @@ import { InvocationService, MAX_RETRY_BACKOFF_MS, retryBackoffMs, } from "./invo
7
7
  import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
8
8
  import { hasConnectorGuides, listSkills, resolveSkill, } from "./skills.js";
9
9
  import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, withAbortableTimeout, } from "./timeout.js";
10
- import { credentialVerdictApplies } from "./credential-health.js";
11
- export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_RETRY_BACKOFF_MS, MAX_SEARCH_LIMIT, DiscoveryPolicyError, retryBackoffMs, };
10
+ export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_RETRY_BACKOFF_MS, MAX_SEARCH_LIMIT, retryBackoffMs, };
12
11
  const RESULT_TTL_SECONDS = 900;
13
12
  const enc = new TextEncoder();
14
13
  const dec = new TextDecoder();
15
14
  export function jsonResult(obj) {
16
15
  return {
17
- content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
16
+ content: [{ type: "text", text: JSON.stringify(obj) }],
18
17
  ...(obj !== null && typeof obj === "object" && !Array.isArray(obj)
19
18
  ? { structuredContent: obj }
20
19
  : {}),
@@ -40,8 +39,9 @@ function discoveryErrorResult(error) {
40
39
  result.isError = true;
41
40
  return result;
42
41
  }
43
- function discoveryResult(value, hint) {
42
+ async function discoveryResult(operation, hint) {
44
43
  try {
44
+ const value = await operation();
45
45
  const text = boundedDiscoveryText(value, hint);
46
46
  return {
47
47
  content: [{ type: "text", text }],
@@ -174,7 +174,7 @@ function applyFieldsToContent(content, fields) {
174
174
  catch {
175
175
  return b;
176
176
  }
177
- return { ...b, text: JSON.stringify(applyFields(parsed, fields), null, 2) };
177
+ return { ...b, text: JSON.stringify(applyFields(parsed, fields)) };
178
178
  });
179
179
  }
180
180
  // --- result-size guard + get_result (feature 1) ---------------------------
@@ -194,7 +194,7 @@ function applyFieldsToContent(content, fields) {
194
194
  * at all (a BigInt) still throws, as before, and is reported as a failure.
195
195
  */
196
196
  function serializeResultText(value) {
197
- const serialized = JSON.stringify(value, null, 2);
197
+ const serialized = JSON.stringify(value);
198
198
  return serialized === undefined ? String(value) : serialized;
199
199
  }
200
200
  /**
@@ -275,7 +275,7 @@ async function guardValue(value, results, cap) {
275
275
  async function guardContent(content, results, cap) {
276
276
  let text;
277
277
  try {
278
- text = JSON.stringify(content, null, 2);
278
+ text = JSON.stringify(content);
279
279
  }
280
280
  catch {
281
281
  // A block carrying a BigInt or a cycle cannot be serialized, so it cannot
@@ -373,16 +373,23 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
373
373
  },
374
374
  });
375
375
  if (!outcome.ok) {
376
+ const failedResult = outcome.error.code === "auth_required" ||
377
+ outcome.error.code === "input_required_unsupported" ||
378
+ call.resultMode === "value"
379
+ ? jsonResult({
380
+ ok: false,
381
+ error: outcome.error,
382
+ durationMs: outcome.durationMs,
383
+ attempts: outcome.attempts,
384
+ ...(call.diagnostics ? { timing: outcome.timing } : {}),
385
+ })
386
+ : errorResult(outcome.error.message);
387
+ if (outcome.error.code === "auth_required" ||
388
+ outcome.error.code === "input_required_unsupported") {
389
+ failedResult.isError = true;
390
+ }
376
391
  return {
377
- toolResult: call.resultMode === "value"
378
- ? jsonResult({
379
- ok: false,
380
- error: outcome.error,
381
- durationMs: outcome.durationMs,
382
- attempts: outcome.attempts,
383
- ...(call.diagnostics ? { timing: outcome.timing } : {}),
384
- })
385
- : errorResult(outcome.error.message),
392
+ toolResult: failedResult,
386
393
  durationMs: outcome.durationMs,
387
394
  attempts: outcome.attempts,
388
395
  timing: outcome.timing,
@@ -438,9 +445,12 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
438
445
  const inspect = async (c) => {
439
446
  const statusStarted = Date.now();
440
447
  const observed = registry.healthFor(c.id);
441
- const verdict = await registry.credentialHealthFor(c.id);
448
+ const drift = await registry.credentialDriftFor(c.id);
442
449
  let status;
443
- if (probe) {
450
+ if (drift) {
451
+ status = { state: "auth_required", message: drift };
452
+ }
453
+ else if (probe) {
444
454
  try {
445
455
  status = await withProbeDeadline(`list_connectors probe of "${c.id}"`, (options) => registry.statusFor(c.id, baseUrl, scope, options));
446
456
  }
@@ -451,73 +461,20 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
451
461
  status = { state: "error", message: msg(err) };
452
462
  }
453
463
  }
454
- else if (verdict &&
455
- // Deployment-wide, deliberately, like `hasObservedSuccess` beside
456
- // it: a sibling toolkit's successful call proves the shared
457
- // credential works, and a verdict retired for one view but not
458
- // another would make the same connector read differently per scope
459
- // for a reason that has nothing to do with scope.
460
- credentialVerdictApplies(verdict, registry.observedSuccessAt(c.id))) {
461
- // The proactive layer (issue #24): a liveness check already found
462
- // the stored credential dead, so say so on the cheap path instead of
463
- // waiting for an agent's real call to discover it. Only while it is
464
- // the freshest evidence — a successful call since then retires it.
465
- status = {
466
- state: verdict.state,
467
- ...(verdict.message ? { message: verdict.message } : {}),
468
- ...(verdict.authorizationUrl
469
- ? { authorizationUrl: verdict.authorizationUrl }
470
- : {}),
471
- };
472
- }
473
464
  else {
474
- // "error" comes from THIS view's own observations — a sibling
475
- // toolkit's failure is not this session's experience — while
476
- // ok/unknown may lean on the deployment-wide success signal, since
477
- // "the connector answers at all" is a fact about the connector.
478
- // Unscoped, the two are the same log, so this is unchanged there.
479
465
  const derived = observed?.consecutiveFailures && observed.consecutiveFailures > 0
480
466
  ? "error"
481
467
  : registry.hasObservedSuccess(c.id) || c.kind === "api"
482
468
  ? "ok"
483
469
  : "unknown";
484
470
  status = {
485
- // A successful liveness check upgrades "unknown" — nothing has
486
- // been called yet, but the credential was verified, which is how
487
- // re-authorization shows up here as ok rather than as an absence
488
- // of evidence. It never DOWNgrades an observed failure: a real
489
- // call that failed is stronger evidence than a background check.
490
- state: derived === "unknown" && verdict?.state === "ok"
491
- ? "ok"
492
- : derived,
471
+ state: derived,
493
472
  ...(observed?.lastError ? { message: observed.lastError } : {}),
494
473
  };
495
474
  }
496
- // Stamped where the observation actually happened after the status
497
- // probe, not before it. A 30-second probe stamped at its start would
498
- // report a verdict older than it is, and would lose the race against a
499
- // real call that succeeded WHILE it ran (that success must retire the
500
- // verdict, and only an honest timestamp says so).
475
+ // Stamped after any live probe so the response reports when its
476
+ // observation completed, not when a potentially slow request began.
501
477
  const checkedAt = new Date().toISOString();
502
- // A live status probe IS a liveness observation of the stored
503
- // credential, so it updates the same verdict a background check
504
- // writes: the cached read afterwards agrees with what the operator
505
- // just saw, and they are not swept again moments later. Recorded from
506
- // the STATUS phase only, and only when the connector actually answered
507
- // — a catalog refresh below is not a credential check (the sweep never
508
- // fetches one), it is already counted in the health log, and letting
509
- // its failure land here would spend the freshness budget on it. The
510
- // registry ignores this for connectors storing no credential of ours.
511
- if (probe && (status.state === "ok" || status.state === "auth_required")) {
512
- await registry.recordCredentialHealth(c.id, {
513
- state: status.state,
514
- checkedAt,
515
- ...(status.message ? { message: status.message } : {}),
516
- ...("authorizationUrl" in status && status.authorizationUrl
517
- ? { authorizationUrl: status.authorizationUrl }
518
- : {}),
519
- });
520
- }
521
478
  let tools = registry.peekTools(c.id);
522
479
  // An auth_required status may have just started OAuth. A second
523
480
  // listTools probe would overwrite its state/verifier while returning
@@ -545,15 +502,6 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
545
502
  state: "auth_required",
546
503
  message: details.message,
547
504
  };
548
- await registry.recordCredentialHealth(c.id, {
549
- state: "auth_required",
550
- checkedAt,
551
- ...(status.message && { message: status.message }),
552
- ...("authorizationUrl" in status &&
553
- status.authorizationUrl
554
- ? { authorizationUrl: status.authorizationUrl }
555
- : {}),
556
- });
557
505
  }
558
506
  else {
559
507
  status = { state: "error", message: msg(err) };
@@ -563,9 +511,6 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
563
511
  }
564
512
  const latencyMs = Date.now() - statusStarted;
565
513
  const latestObserved = registry.healthFor(c.id);
566
- const credentialCheck = probe
567
- ? await registry.credentialHealthFor(c.id)
568
- : verdict;
569
514
  return {
570
515
  id: c.id,
571
516
  ...(c.title ? { title: c.title } : {}),
@@ -576,7 +521,6 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
576
521
  latencyMs,
577
522
  probe,
578
523
  ...(latestObserved ?? observed),
579
- ...(credentialCheck ? { credentialCheck } : {}),
580
524
  ...("authorizationUrl" in status &&
581
525
  status.authorizationUrl && {
582
526
  authorizationUrl: status.authorizationUrl,
@@ -596,26 +540,10 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
596
540
  return jsonResult({ connectors: out });
597
541
  },
598
542
  async searchTools(args) {
599
- try {
600
- return discoveryResult(groupedSearchResult(await catalog.search(args)), "Request a smaller limit, omit fullDescriptions, or use compact schemas.");
601
- }
602
- catch (err) {
603
- if (err instanceof DiscoveryPolicyError) {
604
- return discoveryErrorResult(err);
605
- }
606
- throw err;
607
- }
543
+ return discoveryResult(async () => groupedSearchResult(await catalog.search(args)), "Request a smaller limit, omit fullDescriptions, or use compact schemas.");
608
544
  },
609
545
  async describeTools(args) {
610
- try {
611
- return discoveryResult({ tools: await catalog.describe(args) }, 'Split the address list or use format: "compact".');
612
- }
613
- catch (err) {
614
- if (err instanceof DiscoveryPolicyError) {
615
- return discoveryErrorResult(err);
616
- }
617
- throw err;
618
- }
546
+ return discoveryResult(async () => ({ tools: await catalog.describe(args) }), 'Split the address list or use format: "compact".');
619
547
  },
620
548
  async callTool(args) {
621
549
  return (await runCall(args, "call_tool")).toolResult;
@@ -773,6 +701,21 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
773
701
  ...(details.retryAfterMs !== undefined
774
702
  ? { retryAfterMs: details.retryAfterMs }
775
703
  : {}),
704
+ ...(details.connector !== undefined
705
+ ? { connector: batchSummaryString(details.connector) }
706
+ : {}),
707
+ ...(details.operation !== undefined
708
+ ? { operation: batchSummaryString(details.operation) }
709
+ : {}),
710
+ ...(details.recovery !== undefined
711
+ ? { recovery: details.recovery }
712
+ : {}),
713
+ ...(details.nextAction !== undefined
714
+ ? { nextAction: details.nextAction }
715
+ : {}),
716
+ ...(details.retry !== undefined
717
+ ? { retry: batchSummaryString(details.retry) }
718
+ : {}),
776
719
  },
777
720
  };
778
721
  }),
@@ -786,28 +729,58 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
786
729
  return errorResult(`Unknown connector "${args.connector}"`);
787
730
  }
788
731
  if (!connector.startAuth) {
789
- return errorResult(`Connector "${args.connector}" does not use downstream OAuth — its auth is static (headers/none), so there is nothing to authorize.`);
732
+ if (!connector.credential) {
733
+ return jsonResult({
734
+ connector: connector.id,
735
+ recovery: "unavailable",
736
+ message: `Connector "${connector.id}" declares neither downstream OAuth ` +
737
+ "nor an operator-managed credential slot. Update the connector " +
738
+ "or deployment configuration before retrying.",
739
+ });
740
+ }
741
+ const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
742
+ if (!ctx.credential) {
743
+ return jsonResult({
744
+ connector: connector.id,
745
+ recovery: "unavailable",
746
+ message: "Credential storage is not configured. Configure " +
747
+ "credentials.encryptionKey, redeploy, then call " +
748
+ "authorize_connector again.",
749
+ });
750
+ }
751
+ const fields = connector.credential.fields?.map((field) => ({
752
+ name: field.name,
753
+ guidance: field.description ?? field.label,
754
+ })) ?? [
755
+ {
756
+ name: "value",
757
+ guidance: connector.credential.description ??
758
+ connector.credential.label,
759
+ },
760
+ ];
761
+ return jsonResult({
762
+ connector: connector.id,
763
+ recovery: "operator_config",
764
+ credential: {
765
+ label: connector.credential.label,
766
+ fields,
767
+ },
768
+ operatorUrl: new URL("/credentials", baseUrl).toString(),
769
+ instructions: "Have the operator open operatorUrl, set and test the credential, " +
770
+ "then retry the original call. No redeploy is needed. Credential " +
771
+ "mutation requires a Clerk-authenticated operator.",
772
+ });
790
773
  }
791
774
  const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
792
775
  try {
793
776
  const status = await connector.startAuth(ctx, args.force !== undefined ? { force: args.force } : {});
794
- // startAuth just spoke to the downstream about this exact credential, so
795
- // its answer replaces any older liveness verdict — including the stale
796
- // `auth_required` that sent the agent here, once it reports ok.
797
- await registry.recordCredentialHealth(connector.id, {
798
- state: status.state,
799
- checkedAt: new Date().toISOString(),
800
- ...(status.message ? { message: status.message } : {}),
801
- ...(status.authorizationUrl
802
- ? { authorizationUrl: status.authorizationUrl }
803
- : {}),
804
- });
805
777
  if (status.state === "auth_required" && !status.authorizationUrl) {
806
778
  // auth_required with nothing to open is a dead end for the operator.
807
779
  return errorResult(`Connector "${connector.id}": authorization required but no URL is available — retry authorize_connector.`);
808
780
  }
809
781
  return jsonResult({
810
782
  connector: connector.id,
783
+ recovery: "oauth",
811
784
  status: status.state,
812
785
  ...(status.authorizationUrl
813
786
  ? {
@@ -830,13 +803,13 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
830
803
  };
831
804
  }
832
805
  const LIST_DESC = "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.";
833
- 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.`;
806
+ 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.`;
834
807
  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.`;
835
808
  const CALL_DESC = '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.';
836
809
  const CALL_DESTRUCTIVE_DESC = "Invoke any tool that is not explicitly annotated readOnlyHint: true, including unannotated, write-capable, or destructive tools. The MCP destructiveHint on this meta-tool lets the host request human approval before execution. Use only after reviewing the downstream tool schema and consequences.";
837
810
  const GET_RESULT_DESC = "Page a truncated result stashed by call_tool/batch_call. Input { id, offset?, maxBytes? } → { text, offset, nextOffset?, totalBytes } sliced by byte offset. maxBytes is a whole number of bytes >= 1 (omit for the deployment default) and offset a whole number of bytes >= 0; an offset inside a multi-byte character is moved back to that character's first byte and the offset served is returned. Unknown/expired id is an error.";
838
811
  const BATCH_DESC = "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.";
839
- const AUTHORIZE_DESC = "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.";
812
+ const AUTHORIZE_DESC = "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.";
840
813
  const SKILLS_DESC = '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.';
841
814
  /**
842
815
  * Sentences appended to a meta-tool description only when this connection
@@ -844,10 +817,7 @@ const SKILLS_DESC = 'List or fetch concise guidance for choosing among Connecta
844
817
  * so a deployment with no guides gets every base description unchanged rather
845
818
  * than paying for text about a feature it does not use.
846
819
  *
847
- * Registration is per connection and reads the connection's own registry view,
848
- * so under a toolkit these sentences reflect the SCOPED connector set: a scoped
849
- * session whose connectors carry no guides sees the base descriptions, and
850
- * never learns from a tool description that guides exist out of scope.
820
+ * Registration is per connection and reads the configured connector set.
851
821
  */
852
822
  const GUIDE_NOTES = {
853
823
  skills: ' skills({}) also lists this deployment\'s per-connector usage guides as "connector:<connectorId>"; fetch the guide for a connector before working with it for the first time.',
@@ -878,6 +848,15 @@ const READ_ONLY_LOCAL = {
878
848
  idempotentHint: true,
879
849
  openWorldHint: false,
880
850
  };
851
+ const CALL_INPUT_SCHEMA = {
852
+ address: z.string(),
853
+ args: z.record(z.string(), z.unknown()).optional(),
854
+ fields: z.array(z.string()).optional(),
855
+ resultMode: z.enum(["mcp", "value"]).optional(),
856
+ timeoutMs: z.number().int().positive().optional(),
857
+ maxRetries: z.number().int().min(0).max(2).optional(),
858
+ diagnostics: z.boolean().optional(),
859
+ };
881
860
  /** Register the nine meta-tools onto an McpServer instance. */
882
861
  export function registerMetaTools(server, registry, ctx) {
883
862
  const mt = createMetaTools(registry, ctx.baseUrl, {
@@ -898,66 +877,45 @@ export function registerMetaTools(server, registry, ctx) {
898
877
  });
899
878
  server.registerTool("skills", {
900
879
  description: describedFor(registry, SKILLS_DESC, "skills"),
901
- inputSchema: { name: z.string().optional() },
902
- annotations: {
903
- readOnlyHint: true,
904
- destructiveHint: false,
905
- idempotentHint: true,
906
- openWorldHint: false,
907
- },
880
+ inputSchema: z.object({ name: z.string().optional() }),
881
+ annotations: READ_ONLY_LOCAL,
908
882
  }, async (args) => mt.skills(args));
909
883
  server.registerTool("list_connectors", {
910
884
  description: LIST_DESC,
911
- inputSchema: { probe: z.boolean().optional() },
885
+ inputSchema: z.object({ probe: z.boolean().optional() }),
912
886
  annotations: READ_ONLY_REMOTE,
913
887
  }, async (args) => mt.listConnectors(args));
914
888
  server.registerTool("search_tools", {
915
889
  description: describedFor(registry, SEARCH_DESC, "search"),
916
- inputSchema: {
890
+ inputSchema: z.object({
917
891
  query: z.string().optional(),
918
892
  connector: z.string().optional(),
919
893
  limit: z.number().int().positive().max(MAX_SEARCH_LIMIT).optional(),
920
894
  offset: z.number().int().nonnegative().optional(),
921
895
  fullDescriptions: z.boolean().optional(),
922
896
  includeSchemas: z.enum(["compact", "json"]).optional(),
923
- },
897
+ }),
924
898
  annotations: READ_ONLY_REMOTE,
925
899
  }, async (args) => mt.searchTools(args));
926
900
  server.registerTool("describe_tools", {
927
901
  description: describedFor(registry, DESCRIBE_DESC, "describe"),
928
- inputSchema: {
902
+ inputSchema: z.object({
929
903
  addresses: z.array(z.string()).max(MAX_DESCRIBE_ADDRESSES),
930
904
  format: z.enum(["compact", "json"]).optional(),
931
905
  fullDescriptions: z.boolean().optional(),
932
- },
906
+ }),
933
907
  annotations: READ_ONLY_REMOTE,
934
908
  }, async (args) => mt.describeTools(args));
935
909
  server.registerTool("call_tool", {
936
910
  description: CALL_DESC,
937
- inputSchema: {
938
- address: z.string(),
939
- args: z.record(z.string(), z.unknown()).optional(),
940
- fields: z.array(z.string()).optional(),
941
- resultMode: z.enum(["mcp", "value"]).optional(),
942
- timeoutMs: z.number().int().positive().optional(),
943
- maxRetries: z.number().int().min(0).max(2).optional(),
944
- diagnostics: z.boolean().optional(),
945
- },
911
+ inputSchema: z.object(CALL_INPUT_SCHEMA),
946
912
  // call_tool admits only tools that are themselves explicitly read-only;
947
913
  // anything else is refused and routed to call_destructive_tool.
948
914
  annotations: READ_ONLY_REMOTE,
949
915
  }, async (args) => mt.callTool(args));
950
916
  server.registerTool("call_destructive_tool", {
951
917
  description: CALL_DESTRUCTIVE_DESC,
952
- inputSchema: {
953
- address: z.string(),
954
- args: z.record(z.string(), z.unknown()).optional(),
955
- fields: z.array(z.string()).optional(),
956
- resultMode: z.enum(["mcp", "value"]).optional(),
957
- timeoutMs: z.number().int().positive().optional(),
958
- maxRetries: z.number().int().min(0).max(2).optional(),
959
- diagnostics: z.boolean().optional(),
960
- },
918
+ inputSchema: z.object(CALL_INPUT_SCHEMA),
961
919
  annotations: {
962
920
  destructiveHint: true,
963
921
  readOnlyHint: false,
@@ -966,10 +924,10 @@ export function registerMetaTools(server, registry, ctx) {
966
924
  }, async (args) => mt.callDestructiveTool(args));
967
925
  server.registerTool("authorize_connector", {
968
926
  description: AUTHORIZE_DESC,
969
- inputSchema: {
927
+ inputSchema: z.object({
970
928
  connector: z.string(),
971
929
  force: z.boolean().optional(),
972
- },
930
+ }),
973
931
  // Starts (or with force, resets) a downstream OAuth flow — it changes
974
932
  // stored connector auth state, so it is deliberately not read-only.
975
933
  annotations: {
@@ -980,7 +938,7 @@ export function registerMetaTools(server, registry, ctx) {
980
938
  }, async (args) => mt.authorizeConnector(args));
981
939
  server.registerTool("get_result", {
982
940
  description: GET_RESULT_DESC,
983
- inputSchema: {
941
+ inputSchema: z.object({
984
942
  id: z.string(),
985
943
  // Both bounds are the shared rules (isValidResultOffset,
986
944
  // isValidMaxResultBytes) expressed for the wire: spelling them against
@@ -988,29 +946,21 @@ export function registerMetaTools(server, registry, ctx) {
988
946
  // in-handler checks if either floor ever moves.
989
947
  offset: z.number().int().min(MIN_RESULT_OFFSET).optional(),
990
948
  maxBytes: z.number().int().min(MIN_MAX_RESULT_BYTES).optional(),
991
- },
949
+ }),
992
950
  annotations: READ_ONLY_LOCAL,
993
951
  }, async (args) => mt.getResult(args));
994
952
  server.registerTool("batch_call", {
995
953
  description: BATCH_DESC,
996
- inputSchema: {
954
+ inputSchema: z.object({
997
955
  calls: z
998
- .array(z.object({
999
- address: z.string(),
1000
- args: z.record(z.string(), z.unknown()).optional(),
1001
- fields: z.array(z.string()).optional(),
1002
- resultMode: z.enum(["mcp", "value"]).optional(),
1003
- timeoutMs: z.number().int().positive().optional(),
1004
- maxRetries: z.number().int().min(0).max(2).optional(),
1005
- diagnostics: z.boolean().optional(),
1006
- }))
956
+ .array(z.object(CALL_INPUT_SCHEMA))
1007
957
  .min(1)
1008
958
  .max(10),
1009
959
  resultMode: z.enum(["mcp", "value"]).optional(),
1010
960
  timeoutMs: z.number().int().positive().optional(),
1011
961
  maxRetries: z.number().int().min(0).max(2).optional(),
1012
962
  diagnostics: z.boolean().optional(),
1013
- },
963
+ }),
1014
964
  // Same gate as call_tool: every call in the batch must be explicitly
1015
965
  // read-only or the batch is refused.
1016
966
  annotations: READ_ONLY_REMOTE,