@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
package/src/meta-tools.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
2
  import { z } from "zod";
3
3
  import type {
4
4
  ActivityCallSource,
@@ -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,26 @@ export function createMetaTools(
614
609
  },
615
610
  );
616
611
  if (!outcome.ok) {
612
+ const failedResult =
613
+ outcome.error.code === "auth_required" ||
614
+ outcome.error.code === "input_required_unsupported" ||
615
+ call.resultMode === "value"
616
+ ? jsonResult({
617
+ ok: false,
618
+ error: outcome.error,
619
+ durationMs: outcome.durationMs,
620
+ attempts: outcome.attempts,
621
+ ...(call.diagnostics ? { timing: outcome.timing } : {}),
622
+ })
623
+ : errorResult(outcome.error.message);
624
+ if (
625
+ outcome.error.code === "auth_required" ||
626
+ outcome.error.code === "input_required_unsupported"
627
+ ) {
628
+ failedResult.isError = true;
629
+ }
617
630
  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),
631
+ toolResult: failedResult,
628
632
  durationMs: outcome.durationMs,
629
633
  attempts: outcome.attempts,
630
634
  timing: outcome.timing,
@@ -683,11 +687,13 @@ export function createMetaTools(
683
687
  const inspect = async (c: (typeof connectors)[number]) => {
684
688
  const statusStarted = Date.now();
685
689
  const observed = registry.healthFor(c.id);
686
- const verdict = await registry.credentialHealthFor(c.id);
690
+ const drift = await registry.credentialDriftFor(c.id);
687
691
  let status:
688
692
  | ConnectorStatus
689
693
  | { state: "ok" | "error" | "unknown"; message?: string };
690
- if (probe) {
694
+ if (drift) {
695
+ status = { state: "auth_required", message: drift };
696
+ } else if (probe) {
691
697
  try {
692
698
  status = await withProbeDeadline(
693
699
  `list_connectors probe of "${c.id}"`,
@@ -700,32 +706,7 @@ export function createMetaTools(
700
706
  // hanging the whole list_connectors call.
701
707
  status = { state: "error", message: msg(err) };
702
708
  }
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
709
  } 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
710
  const derived =
730
711
  observed?.consecutiveFailures && observed.consecutiveFailures > 0
731
712
  ? ("error" as const)
@@ -733,43 +714,13 @@ export function createMetaTools(
733
714
  ? ("ok" as const)
734
715
  : ("unknown" as const);
735
716
  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,
717
+ state: derived,
745
718
  ...(observed?.lastError ? { message: observed.lastError } : {}),
746
719
  };
747
720
  }
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).
721
+ // Stamped after any live probe so the response reports when its
722
+ // observation completed, not when a potentially slow request began.
753
723
  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
724
  let tools = registry.peekTools(c.id);
774
725
  // An auth_required status may have just started OAuth. A second
775
726
  // listTools probe would overwrite its state/verifier while returning
@@ -803,15 +754,6 @@ export function createMetaTools(
803
754
  state: "auth_required" as const,
804
755
  message: details.message,
805
756
  };
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
757
  } else {
816
758
  status = { state: "error" as const, message: msg(err) };
817
759
  }
@@ -820,9 +762,6 @@ export function createMetaTools(
820
762
  }
821
763
  const latencyMs = Date.now() - statusStarted;
822
764
  const latestObserved = registry.healthFor(c.id);
823
- const credentialCheck = probe
824
- ? await registry.credentialHealthFor(c.id)
825
- : verdict;
826
765
  return {
827
766
  id: c.id,
828
767
  ...(c.title ? { title: c.title } : {}),
@@ -833,7 +772,6 @@ export function createMetaTools(
833
772
  latencyMs,
834
773
  probe,
835
774
  ...(latestObserved ?? observed),
836
- ...(credentialCheck ? { credentialCheck } : {}),
837
775
  ...("authorizationUrl" in status &&
838
776
  status.authorizationUrl && {
839
777
  authorizationUrl: status.authorizationUrl,
@@ -866,31 +804,18 @@ export function createMetaTools(
866
804
  },
867
805
 
868
806
  async searchTools(args: SearchArgs): Promise<ToolResult> {
869
- try {
870
- return discoveryResult(
807
+ return discoveryResult(
808
+ async () =>
871
809
  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
- }
810
+ "Request a smaller limit, omit fullDescriptions, or use compact schemas.",
811
+ );
880
812
  },
881
813
 
882
814
  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
- }
815
+ return discoveryResult(
816
+ async () => ({ tools: await catalog.describe(args) }),
817
+ 'Split the address list or use format: "compact".',
818
+ );
894
819
  },
895
820
 
896
821
  async callTool(args: CallArgs): Promise<ToolResult> {
@@ -1076,6 +1001,21 @@ export function createMetaTools(
1076
1001
  ...(details.retryAfterMs !== undefined
1077
1002
  ? { retryAfterMs: details.retryAfterMs }
1078
1003
  : {}),
1004
+ ...(details.connector !== undefined
1005
+ ? { connector: batchSummaryString(details.connector) }
1006
+ : {}),
1007
+ ...(details.operation !== undefined
1008
+ ? { operation: batchSummaryString(details.operation) }
1009
+ : {}),
1010
+ ...(details.recovery !== undefined
1011
+ ? { recovery: details.recovery }
1012
+ : {}),
1013
+ ...(details.nextAction !== undefined
1014
+ ? { nextAction: details.nextAction }
1015
+ : {}),
1016
+ ...(details.retry !== undefined
1017
+ ? { retry: batchSummaryString(details.retry) }
1018
+ : {}),
1079
1019
  },
1080
1020
  };
1081
1021
  }),
@@ -1090,9 +1030,55 @@ export function createMetaTools(
1090
1030
  return errorResult(`Unknown connector "${args.connector}"`);
1091
1031
  }
1092
1032
  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.`,
1033
+ if (!connector.credential) {
1034
+ return jsonResult({
1035
+ connector: connector.id,
1036
+ recovery: "unavailable",
1037
+ message:
1038
+ `Connector "${connector.id}" declares neither downstream OAuth ` +
1039
+ "nor an operator-managed credential slot. Update the connector " +
1040
+ "or deployment configuration before retrying.",
1041
+ });
1042
+ }
1043
+ const ctx = registry.contextFor(
1044
+ connector.id,
1045
+ baseUrl,
1046
+ requestScope,
1095
1047
  );
1048
+ if (!ctx.credential) {
1049
+ return jsonResult({
1050
+ connector: connector.id,
1051
+ recovery: "unavailable",
1052
+ message:
1053
+ "Credential storage is not configured. Configure " +
1054
+ "credentials.encryptionKey, redeploy, then call " +
1055
+ "authorize_connector again.",
1056
+ });
1057
+ }
1058
+ const fields = connector.credential.fields?.map((field) => ({
1059
+ name: field.name,
1060
+ guidance: field.description ?? field.label,
1061
+ })) ?? [
1062
+ {
1063
+ name: "value",
1064
+ guidance:
1065
+ connector.credential.description ??
1066
+ connector.credential.label,
1067
+ },
1068
+ ];
1069
+ return jsonResult({
1070
+ connector: connector.id,
1071
+ recovery: "operator_config",
1072
+ credential: {
1073
+ label: connector.credential.label,
1074
+ fields,
1075
+ },
1076
+ operatorUrl: new URL("/credentials", baseUrl).toString(),
1077
+ instructions:
1078
+ "Have the operator open operatorUrl, set and test the credential, " +
1079
+ "then retry the original call. No redeploy is needed. Credential " +
1080
+ "mutation requires a Clerk-authenticated operator.",
1081
+ });
1096
1082
  }
1097
1083
  const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
1098
1084
  try {
@@ -1100,17 +1086,6 @@ export function createMetaTools(
1100
1086
  ctx,
1101
1087
  args.force !== undefined ? { force: args.force } : {},
1102
1088
  );
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
1089
  if (status.state === "auth_required" && !status.authorizationUrl) {
1115
1090
  // auth_required with nothing to open is a dead end for the operator.
1116
1091
  return errorResult(
@@ -1119,6 +1094,7 @@ export function createMetaTools(
1119
1094
  }
1120
1095
  return jsonResult({
1121
1096
  connector: connector.id,
1097
+ recovery: "oauth",
1122
1098
  status: status.state,
1123
1099
  ...(status.authorizationUrl
1124
1100
  ? {
@@ -1142,7 +1118,7 @@ export function createMetaTools(
1142
1118
 
1143
1119
  const LIST_DESC =
1144
1120
  "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.`;
1121
+ 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
1122
  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
1123
  const CALL_DESC =
1148
1124
  '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 +1129,7 @@ const GET_RESULT_DESC =
1153
1129
  const BATCH_DESC =
1154
1130
  "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
1131
  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.";
1132
+ "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
1133
  const SKILLS_DESC =
1158
1134
  '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
1135
 
@@ -1163,10 +1139,7 @@ const SKILLS_DESC =
1163
1139
  * so a deployment with no guides gets every base description unchanged rather
1164
1140
  * than paying for text about a feature it does not use.
1165
1141
  *
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.
1142
+ * Registration is per connection and reads the configured connector set.
1170
1143
  */
1171
1144
  const GUIDE_NOTES = {
1172
1145
  skills:
@@ -1208,6 +1181,16 @@ const READ_ONLY_LOCAL = {
1208
1181
  openWorldHint: false,
1209
1182
  } as const;
1210
1183
 
1184
+ const CALL_INPUT_SCHEMA = {
1185
+ address: z.string(),
1186
+ args: z.record(z.string(), z.unknown()).optional(),
1187
+ fields: z.array(z.string()).optional(),
1188
+ resultMode: z.enum(["mcp", "value"]).optional(),
1189
+ timeoutMs: z.number().int().positive().optional(),
1190
+ maxRetries: z.number().int().min(0).max(2).optional(),
1191
+ diagnostics: z.boolean().optional(),
1192
+ };
1193
+
1211
1194
  /** Register the nine meta-tools onto an McpServer instance. */
1212
1195
  export function registerMetaTools(
1213
1196
  server: McpServer,
@@ -1243,13 +1226,8 @@ export function registerMetaTools(
1243
1226
  "skills",
1244
1227
  {
1245
1228
  description: describedFor(registry, SKILLS_DESC, "skills"),
1246
- inputSchema: { name: z.string().optional() },
1247
- annotations: {
1248
- readOnlyHint: true,
1249
- destructiveHint: false,
1250
- idempotentHint: true,
1251
- openWorldHint: false,
1252
- },
1229
+ inputSchema: z.object({ name: z.string().optional() }),
1230
+ annotations: READ_ONLY_LOCAL,
1253
1231
  },
1254
1232
  async (args) => mt.skills(args as SkillArgs),
1255
1233
  );
@@ -1258,7 +1236,7 @@ export function registerMetaTools(
1258
1236
  "list_connectors",
1259
1237
  {
1260
1238
  description: LIST_DESC,
1261
- inputSchema: { probe: z.boolean().optional() },
1239
+ inputSchema: z.object({ probe: z.boolean().optional() }),
1262
1240
  annotations: READ_ONLY_REMOTE,
1263
1241
  },
1264
1242
  async (args) => mt.listConnectors(args as ListArgs),
@@ -1268,14 +1246,14 @@ export function registerMetaTools(
1268
1246
  "search_tools",
1269
1247
  {
1270
1248
  description: describedFor(registry, SEARCH_DESC, "search"),
1271
- inputSchema: {
1249
+ inputSchema: z.object({
1272
1250
  query: z.string().optional(),
1273
1251
  connector: z.string().optional(),
1274
1252
  limit: z.number().int().positive().max(MAX_SEARCH_LIMIT).optional(),
1275
1253
  offset: z.number().int().nonnegative().optional(),
1276
1254
  fullDescriptions: z.boolean().optional(),
1277
1255
  includeSchemas: z.enum(["compact", "json"]).optional(),
1278
- },
1256
+ }),
1279
1257
  annotations: READ_ONLY_REMOTE,
1280
1258
  },
1281
1259
  async (args) => mt.searchTools(args as SearchArgs),
@@ -1285,11 +1263,11 @@ export function registerMetaTools(
1285
1263
  "describe_tools",
1286
1264
  {
1287
1265
  description: describedFor(registry, DESCRIBE_DESC, "describe"),
1288
- inputSchema: {
1266
+ inputSchema: z.object({
1289
1267
  addresses: z.array(z.string()).max(MAX_DESCRIBE_ADDRESSES),
1290
1268
  format: z.enum(["compact", "json"]).optional(),
1291
1269
  fullDescriptions: z.boolean().optional(),
1292
- },
1270
+ }),
1293
1271
  annotations: READ_ONLY_REMOTE,
1294
1272
  },
1295
1273
  async (args) => mt.describeTools(args as DescribeArgs),
@@ -1299,15 +1277,7 @@ export function registerMetaTools(
1299
1277
  "call_tool",
1300
1278
  {
1301
1279
  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
- },
1280
+ inputSchema: z.object(CALL_INPUT_SCHEMA),
1311
1281
  // call_tool admits only tools that are themselves explicitly read-only;
1312
1282
  // anything else is refused and routed to call_destructive_tool.
1313
1283
  annotations: READ_ONLY_REMOTE,
@@ -1319,15 +1289,7 @@ export function registerMetaTools(
1319
1289
  "call_destructive_tool",
1320
1290
  {
1321
1291
  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
- },
1292
+ inputSchema: z.object(CALL_INPUT_SCHEMA),
1331
1293
  annotations: {
1332
1294
  destructiveHint: true,
1333
1295
  readOnlyHint: false,
@@ -1341,10 +1303,10 @@ export function registerMetaTools(
1341
1303
  "authorize_connector",
1342
1304
  {
1343
1305
  description: AUTHORIZE_DESC,
1344
- inputSchema: {
1306
+ inputSchema: z.object({
1345
1307
  connector: z.string(),
1346
1308
  force: z.boolean().optional(),
1347
- },
1309
+ }),
1348
1310
  // Starts (or with force, resets) a downstream OAuth flow — it changes
1349
1311
  // stored connector auth state, so it is deliberately not read-only.
1350
1312
  annotations: {
@@ -1360,7 +1322,7 @@ export function registerMetaTools(
1360
1322
  "get_result",
1361
1323
  {
1362
1324
  description: GET_RESULT_DESC,
1363
- inputSchema: {
1325
+ inputSchema: z.object({
1364
1326
  id: z.string(),
1365
1327
  // Both bounds are the shared rules (isValidResultOffset,
1366
1328
  // isValidMaxResultBytes) expressed for the wire: spelling them against
@@ -1368,7 +1330,7 @@ export function registerMetaTools(
1368
1330
  // in-handler checks if either floor ever moves.
1369
1331
  offset: z.number().int().min(MIN_RESULT_OFFSET).optional(),
1370
1332
  maxBytes: z.number().int().min(MIN_MAX_RESULT_BYTES).optional(),
1371
- },
1333
+ }),
1372
1334
  annotations: READ_ONLY_LOCAL,
1373
1335
  },
1374
1336
  async (args) => mt.getResult(args as GetResultArgs),
@@ -1378,26 +1340,16 @@ export function registerMetaTools(
1378
1340
  "batch_call",
1379
1341
  {
1380
1342
  description: BATCH_DESC,
1381
- inputSchema: {
1343
+ inputSchema: z.object({
1382
1344
  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
- )
1345
+ .array(z.object(CALL_INPUT_SCHEMA))
1394
1346
  .min(1)
1395
1347
  .max(10),
1396
1348
  resultMode: z.enum(["mcp", "value"]).optional(),
1397
1349
  timeoutMs: z.number().int().positive().optional(),
1398
1350
  maxRetries: z.number().int().min(0).max(2).optional(),
1399
1351
  diagnostics: z.boolean().optional(),
1400
- },
1352
+ }),
1401
1353
  // Same gate as call_tool: every call in the batch must be explicitly
1402
1354
  // read-only or the batch is refused.
1403
1355
  annotations: READ_ONLY_REMOTE,
@@ -236,57 +236,6 @@
236
236
  .connector-description { margin-top: .25rem; max-width: 40rem; }
237
237
  .connector-message,
238
238
  .connector-auth { margin-top: .75rem; }
239
- .connector-toolkits {
240
- align-items: baseline;
241
- display: flex;
242
- flex-wrap: wrap;
243
- gap: .25rem .5rem;
244
- margin-top: .5rem;
245
- }
246
- .scope-label {
247
- border: 1px solid var(--rule);
248
- display: inline-block;
249
- font-family: var(--mono);
250
- font-size: .72rem;
251
- padding: .05rem .35rem;
252
- }
253
-
254
- .toolkit-copy { margin-bottom: 1rem; max-width: 42rem; }
255
- .toolkit-ledger { border-bottom: 1px solid var(--rule); }
256
- .toolkit-card {
257
- border-top: 1px solid var(--rule);
258
- padding: .9rem 0 1rem;
259
- }
260
- .toolkit-head {
261
- display: grid;
262
- gap: var(--gap);
263
- grid-template-columns: minmax(0, 2fr) minmax(10rem, 1fr);
264
- }
265
- .toolkit-name {
266
- align-items: baseline;
267
- display: flex;
268
- flex-wrap: wrap;
269
- gap: .5rem;
270
- }
271
- .toolkit-name h3 { overflow-wrap: anywhere; }
272
- .toolkit-state { text-align: right; }
273
- .toolkit-connectors {
274
- display: flex;
275
- flex-wrap: wrap;
276
- gap: .5rem;
277
- margin-top: .75rem;
278
- }
279
- .toolkit-connector {
280
- border-left: 1px solid var(--ink);
281
- display: inline-flex;
282
- flex-wrap: wrap;
283
- gap: .4rem;
284
- padding-left: .5rem;
285
- }
286
- .toolkit-rules { margin-top: .75rem; }
287
- .toolkit-rules > * + * { margin-top: .25rem; }
288
- .toolkit-endpoint { margin-top: .75rem; }
289
-
290
239
  .credential-ledger { border-bottom: 1px solid var(--rule); }
291
240
  .credential-card { border-top: 1px solid var(--rule); padding-bottom: .75rem; padding-top: .75rem; }
292
241
  .credential-head {
@@ -403,11 +352,9 @@
403
352
  .section,
404
353
  .section + .section { margin-top: 2.5rem; }
405
354
  .connector-head,
406
- .toolkit-head,
407
355
  .tool,
408
356
  .activity-item { grid-template-columns: 1fr; }
409
- .connector-state,
410
- .toolkit-state { text-align: left; }
357
+ .connector-state { text-align: left; }
411
358
  .credential-field { align-items: start; grid-template-columns: 1fr; gap: .25rem; }
412
359
  input { min-height: 2.75rem; }
413
360
  }