@zackbart/connecta 0.28.0 → 0.29.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 (320) hide show
  1. package/CHANGELOG.md +961 -5
  2. package/README.md +62 -39
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1632 -1509
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +526 -0
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +220 -0
  52. package/dist/connectors/api-connector.js +214 -0
  53. package/dist/connectors/api.d.ts +8 -71
  54. package/dist/connectors/api.js +13 -99
  55. package/dist/connectors/guarded-fetch.d.ts +144 -0
  56. package/dist/connectors/guarded-fetch.js +108 -25
  57. package/dist/connectors/negotiation-cache.js +46 -0
  58. package/dist/connectors/option-shapes.js +27 -0
  59. package/dist/connectors/remote-mcp.d.ts +30 -4
  60. package/dist/connectors/remote-mcp.js +1780 -351
  61. package/dist/connectors/resource-uri.js +335 -0
  62. package/dist/credential-contract.d.ts +10 -0
  63. package/dist/credential-rules.js +1 -3
  64. package/dist/credential-url.js +108 -0
  65. package/dist/credentials.d.ts +6 -1
  66. package/dist/credentials.js +27 -18
  67. package/dist/d1.d.ts +130 -0
  68. package/dist/d1.js +290 -0
  69. package/dist/describe-config.d.ts +165 -0
  70. package/dist/describe-config.js +315 -0
  71. package/dist/described.js +106 -0
  72. package/dist/downstream-input-context.js +119 -0
  73. package/dist/downstream-input.js +323 -0
  74. package/dist/errors.d.ts +97 -20
  75. package/dist/errors.js +158 -80
  76. package/dist/execute.js +453 -305
  77. package/dist/executor-admission.d.ts +1 -0
  78. package/dist/executor-admission.js +11 -15
  79. package/dist/executor-contract.d.ts +20 -0
  80. package/dist/executor-contract.js +48 -0
  81. package/dist/executor-result.js +24 -9
  82. package/dist/executors/quickjs-child.js +3 -4
  83. package/dist/executors/quickjs-runtime.js +147 -59
  84. package/dist/executors/quickjs.js +29 -35
  85. package/dist/favicon.js +1 -2
  86. package/dist/fix-prompt.js +1 -3
  87. package/dist/guest-runtime.js +188 -0
  88. package/dist/guest-types.d.ts +176 -0
  89. package/dist/guest-types.js +1 -0
  90. package/dist/html-security.js +24 -0
  91. package/dist/identity.js +1 -3
  92. package/dist/inbound-credential.js +52 -0
  93. package/dist/index.d.ts +25 -257
  94. package/dist/index.js +65 -478
  95. package/dist/invocation-auth.js +69 -0
  96. package/dist/invocation.js +230 -73
  97. package/dist/json-schema.d.ts +1 -1
  98. package/dist/mcp-client-context.js +35 -0
  99. package/dist/mcp-result.js +11 -7
  100. package/dist/meta-output.js +76 -0
  101. package/dist/meta-tool-names.d.ts +2 -0
  102. package/dist/meta-tool-names.js +3 -0
  103. package/dist/meta-tools.js +285 -209
  104. package/dist/module-contracts.d.ts +8 -23
  105. package/dist/node.d.ts +0 -2
  106. package/dist/node.js +2 -2
  107. package/dist/oauth-callback-outcome.js +1 -3
  108. package/dist/oauth-handoff.js +117 -0
  109. package/dist/oauth-partition.js +17 -0
  110. package/dist/oauth-sealing.js +20 -0
  111. package/dist/operator-record.d.ts +137 -0
  112. package/dist/operator-record.js +420 -0
  113. package/dist/operator-ui/assets.js +31 -0
  114. package/dist/operator-ui/contract.d.ts +72 -0
  115. package/dist/operator-ui/contract.js +1 -0
  116. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  117. package/dist/operator-ui/generated.js +5 -4
  118. package/dist/operator-ui/model.d.ts +12 -87
  119. package/dist/operator-ui/model.js +2 -9
  120. package/dist/operator-ui/setup-commands.js +1 -3
  121. package/dist/operator-ui/view.d.ts +323 -0
  122. package/dist/operator-ui/view.js +62 -189
  123. package/dist/page-styles.js +2 -3
  124. package/dist/program-source.js +14 -2
  125. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  126. package/dist/provider.d.ts +142 -0
  127. package/dist/provider.js +187 -0
  128. package/dist/providers/_shared/google/result-size.js +57 -0
  129. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  130. package/dist/providers/_shared/google/service-account.js +500 -0
  131. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  132. package/dist/providers/_shared/google/workspace.js +518 -0
  133. package/dist/providers/basecamp/index.d.ts +43 -0
  134. package/dist/providers/basecamp/index.js +997 -0
  135. package/dist/providers/basecamp/skill.generated.js +9 -0
  136. package/dist/providers/breeze/index.d.ts +34 -0
  137. package/dist/providers/breeze/index.js +1446 -0
  138. package/dist/providers/breeze/skill.generated.js +9 -0
  139. package/dist/providers/ccb/index.d.ts +103 -0
  140. package/dist/providers/ccb/index.js +1725 -0
  141. package/dist/providers/ccb/skill.generated.js +9 -0
  142. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  143. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  144. package/dist/providers/cloudflare/skill.generated.js +10 -0
  145. package/dist/providers/docs/index.d.ts +54 -0
  146. package/dist/providers/docs/index.js +1237 -0
  147. package/dist/providers/docs/skill.generated.js +9 -0
  148. package/dist/providers/drive/index.d.ts +53 -0
  149. package/dist/providers/drive/index.js +1523 -0
  150. package/dist/providers/drive/skill.generated.js +9 -0
  151. package/dist/providers/forms/index.d.ts +54 -0
  152. package/dist/providers/forms/index.js +1217 -0
  153. package/dist/providers/forms/skill.generated.js +11 -0
  154. package/dist/providers/github/auth.d.ts +32 -0
  155. package/dist/providers/github/auth.js +361 -0
  156. package/dist/providers/github/hosted.js +218 -0
  157. package/dist/providers/github/index.d.ts +13 -0
  158. package/dist/providers/github/index.js +140 -0
  159. package/dist/providers/github/key.js +100 -0
  160. package/dist/providers/github/rest.js +321 -0
  161. package/dist/providers/github/scope.d.ts +29 -0
  162. package/dist/providers/github/scope.js +124 -0
  163. package/dist/providers/github/skill.generated.js +8 -0
  164. package/dist/providers/gmail/index.d.ts +51 -0
  165. package/dist/providers/gmail/index.js +1436 -0
  166. package/dist/providers/gmail/skill.generated.js +9 -0
  167. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  168. package/dist/providers/linear/index.js +352 -0
  169. package/dist/providers/linear/skill.generated.js +8 -0
  170. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  171. package/dist/providers/mixpanel/index.js +416 -0
  172. package/dist/providers/mixpanel/skill.generated.js +9 -0
  173. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  174. package/dist/providers/{notion.js → notion/index.js} +226 -239
  175. package/dist/providers/notion/skill.generated.js +10 -0
  176. package/dist/providers/overflow/index.d.ts +60 -0
  177. package/dist/providers/overflow/index.js +1535 -0
  178. package/dist/providers/overflow/skill.generated.js +8 -0
  179. package/dist/providers/planning-center/index.d.ts +59 -0
  180. package/dist/providers/planning-center/index.js +2199 -0
  181. package/dist/providers/planning-center/skill.generated.js +10 -0
  182. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  183. package/dist/providers/revenuecat/index.js +557 -0
  184. package/dist/providers/revenuecat/skill.generated.js +11 -0
  185. package/dist/providers/sheets/index.d.ts +54 -0
  186. package/dist/providers/sheets/index.js +1237 -0
  187. package/dist/providers/sheets/skill.generated.js +10 -0
  188. package/dist/providers/slides/index.d.ts +53 -0
  189. package/dist/providers/slides/index.js +2645 -0
  190. package/dist/providers/slides/skill.generated.js +9 -0
  191. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  192. package/dist/providers/stripe/index.js +266 -0
  193. package/dist/providers/stripe/skill.generated.js +11 -0
  194. package/dist/providers/tithely/index.d.ts +49 -0
  195. package/dist/providers/tithely/index.js +1003 -0
  196. package/dist/providers/tithely/skill.generated.js +8 -0
  197. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  198. package/dist/providers/vercel/index.js +1038 -0
  199. package/dist/providers/vercel/mcp-ownership.js +212 -0
  200. package/dist/providers/vercel/skill.generated.js +11 -0
  201. package/dist/registry.d.ts +108 -171
  202. package/dist/registry.js +513 -925
  203. package/dist/request-state.js +21 -0
  204. package/dist/resource-read.js +100 -0
  205. package/dist/result-shapes.js +17 -49
  206. package/dist/retired-toolkits.js +1 -1
  207. package/dist/routes/access-tokens.js +3 -6
  208. package/dist/routes/activity-context.js +21 -0
  209. package/dist/routes/activity-disclosure.js +40 -0
  210. package/dist/routes/activity.js +59 -21
  211. package/dist/routes/actor-labels.js +3 -50
  212. package/dist/routes/connect.js +221 -0
  213. package/dist/routes/credentials.js +15 -27
  214. package/dist/routes/mcp.js +356 -130
  215. package/dist/routes/oauth-client-metadata.js +15 -0
  216. package/dist/routes/oauth-management.js +45 -107
  217. package/dist/routes/oauth.js +136 -57
  218. package/dist/routes/operator.js +3 -3
  219. package/dist/routes/shared.d.ts +25 -64
  220. package/dist/routes/shared.js +160 -80
  221. package/dist/routes/skills.js +30 -0
  222. package/dist/routes/ui-config.js +225 -0
  223. package/dist/routes/ui-data.js +40 -35
  224. package/dist/routes/ui.js +66 -56
  225. package/dist/runtime/admission.js +4 -2
  226. package/dist/runtime/call-admission.js +3 -1
  227. package/dist/runtime/connector-scope.js +1 -4
  228. package/dist/runtime/run.js +10 -39
  229. package/dist/runtime/services.js +12 -117
  230. package/dist/runtime/shared-read.js +1 -3
  231. package/dist/runtime/storage.js +8 -1
  232. package/dist/sent-secrets.d.ts +52 -0
  233. package/dist/sent-secrets.js +474 -0
  234. package/dist/server.js +28 -71
  235. package/dist/skills.js +406 -174
  236. package/dist/sqlite.d.ts +48 -0
  237. package/dist/sqlite.js +166 -0
  238. package/dist/storage/keys.js +428 -0
  239. package/dist/storage/memory.js +7 -0
  240. package/dist/storage/sql.d.ts +66 -0
  241. package/dist/storage/sql.js +534 -0
  242. package/dist/timeout.js +4 -2
  243. package/dist/tool-name.js +17 -0
  244. package/dist/tool-safety.d.ts +9 -35
  245. package/dist/tool-safety.js +20 -40
  246. package/dist/types.d.ts +320 -54
  247. package/dist/types.js +0 -1
  248. package/dist/ui.d.ts +7 -19
  249. package/dist/ui.js +76 -90
  250. package/dist/url-safety.js +3 -6
  251. package/dist/usage-guide.js +137 -0
  252. package/dist/validate.d.ts +7 -0
  253. package/dist/validate.js +309 -47
  254. package/dist/version.d.ts +1 -7
  255. package/dist/version.js +3 -7
  256. package/dist/worker.d.ts +11 -0
  257. package/dist/worker.js +434 -0
  258. package/documentation/architecture.md +665 -230
  259. package/documentation/auth.md +962 -458
  260. package/documentation/code-mode.md +501 -312
  261. package/documentation/meta-tools.md +449 -200
  262. package/documentation/operator-tests.md +93 -0
  263. package/documentation/provider-migration-0.29.md +60 -0
  264. package/examples/worker/AGENTS.md +34 -12
  265. package/examples/worker/README.md +286 -255
  266. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  267. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  268. package/examples/worker/src/connecta.config.ts +151 -0
  269. package/examples/worker/src/index.ts +14 -197
  270. package/examples/worker/wrangler.jsonc +15 -35
  271. package/package.json +125 -35
  272. package/templates/node/.dockerignore +1 -2
  273. package/templates/node/.env.example +20 -21
  274. package/templates/node/AGENTS.md +21 -17
  275. package/templates/node/Dockerfile +3 -5
  276. package/templates/node/README.md +133 -58
  277. package/templates/node/docker-compose.yml +11 -12
  278. package/templates/node/package.json +4 -2
  279. package/templates/node/src/connecta.config.ts +119 -0
  280. package/templates/node/src/index.ts +9 -154
  281. package/templates/node/src/provision-token.ts +17 -0
  282. package/AGENTS.md +0 -170
  283. package/dist/artifacts/connector.d.ts +0 -58
  284. package/dist/artifacts/connector.js +0 -730
  285. package/dist/artifacts/document.js +0 -86
  286. package/dist/artifacts/guide.js +0 -140
  287. package/dist/artifacts/html-scan.js +0 -329
  288. package/dist/artifacts/json.js +0 -78
  289. package/dist/artifacts/kv-store.d.ts +0 -20
  290. package/dist/artifacts/kv-store.js +0 -140
  291. package/dist/artifacts/markdown.js +0 -478
  292. package/dist/artifacts/module.d.ts +0 -37
  293. package/dist/artifacts/module.js +0 -66
  294. package/dist/artifacts/operations.d.ts +0 -197
  295. package/dist/artifacts/operations.js +0 -1002
  296. package/dist/artifacts/refresh.d.ts +0 -55
  297. package/dist/artifacts/refresh.js +0 -236
  298. package/dist/artifacts/routes.js +0 -206
  299. package/dist/artifacts/types.d.ts +0 -214
  300. package/dist/artifacts/types.js +0 -22
  301. package/dist/artifacts/validate.d.ts +0 -62
  302. package/dist/artifacts/validate.js +0 -651
  303. package/dist/artifacts.d.ts +0 -6
  304. package/dist/artifacts.js +0 -9
  305. package/dist/auth/bearer.d.ts +0 -11
  306. package/dist/auth/bearer.js +0 -50
  307. package/dist/providers/linear.js +0 -217
  308. package/dist/providers/mixpanel.js +0 -247
  309. package/dist/providers/revenuecat.js +0 -317
  310. package/dist/providers/stripe.js +0 -253
  311. package/dist/providers/vercel.js +0 -1253
  312. package/dist/storage/file.d.ts +0 -15
  313. package/dist/storage/file.js +0 -455
  314. package/ethos.md +0 -93
  315. package/examples/worker/src/cloudflare-kv.ts +0 -43
  316. package/examples/worker/src/d1-activity-row.ts +0 -155
  317. package/examples/worker/src/d1-activity.ts +0 -147
  318. package/examples/worker/src/d1-storage.ts +0 -138
  319. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  320. package/templates/node/src/file-activity.ts +0 -182
@@ -1,12 +1,13 @@
1
+ import { carryCatalogFreshness, catalogIsFresh } from "./catalog-freshness.js";
2
+ import { hasControlCharacters } from "./tool-name.js";
1
3
  import { Effect, Result } from "effect";
2
4
  import { compactDiscoverySchema, compactDescriptionSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, typescriptSignature, } from "./catalog.js";
3
5
  import { resolveDiscoveryConcurrency } from "./concurrency.js";
4
- import { boundedEchoText, classifyCallError, framingError, } from "./errors.js";
5
- import { connectorGuide, connectorGuideRequired, connectorGuideSummary, connectorSkillName, } from "./skills.js";
6
+ import { boundedEchoText, classifyCallError, ConnectorCallError, framingError } from "./errors.js";
7
+ import { connectorGuide, connectorGuideRequired, connectorGuideSummary, connectorSkillName } from "./skills.js";
6
8
  import { runEdge, withDeadlineEffect } from "./runtime/run.js";
7
9
  import { SharedRead } from "./runtime/shared-read.js";
8
- import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, } from "./timeout.js";
9
- import { isApprovalExempt, isExplicitlyReadOnly, NO_EXEMPTIONS, } from "./tool-safety.js";
10
+ import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs } from "./timeout.js";
10
11
  export const DEFAULT_SEARCH_LIMIT = 8;
11
12
  export const MAX_SEARCH_LIMIT = 100;
12
13
  export const MAX_DESCRIBE_ADDRESSES = 100;
@@ -20,6 +21,49 @@ const MAX_QUERY_TERM_LENGTH = 64;
20
21
  */
21
22
  const MAX_IDENTITY_CONNECTORS = 3;
22
23
  const MAX_DESCRIBE_SUGGESTIONS = 3;
24
+ // Recognized service names are query vocabulary, never a deployment inventory.
25
+ // Unknown/custom services use the explicit connector scope. Keep this independent
26
+ // of provider implementations so discovery cannot pull them into root imports.
27
+ const SERVICE_NAMES = [
28
+ "GitHub",
29
+ "GitLab",
30
+ "Supabase",
31
+ "PostHog",
32
+ "Linear",
33
+ "Mixpanel",
34
+ "RevenueCat",
35
+ "Stripe",
36
+ "Notion",
37
+ "Slack",
38
+ "Vercel",
39
+ "Cloudflare",
40
+ "Basecamp",
41
+ "Breeze",
42
+ "Tithely",
43
+ "Planning Center",
44
+ "Google Drive",
45
+ "Google Docs",
46
+ "Google Sheets",
47
+ "Google Slides",
48
+ "Gmail",
49
+ ];
50
+ function normalizedPhrase(text) {
51
+ return lexicalQueryTerms(text.toLowerCase()).join(" ");
52
+ }
53
+ function containsPhrase(query, phrase) {
54
+ const normalized = normalizedPhrase(phrase);
55
+ return normalized.length > 0 && ` ${normalizedPhrase(query)} `.includes(` ${normalized} `);
56
+ }
57
+ function connectorNames(connector) {
58
+ return [connector.id, ...(connector.title ? [connector.title] : [])];
59
+ }
60
+ function connectorMatchRank(query, connector) {
61
+ if (connectorNames(connector).some((name) => containsPhrase(query, name)))
62
+ return 2;
63
+ return SERVICE_NAMES.some((service) => containsPhrase(query, service) && connectorNames(connector).some((name) => containsPhrase(name, service)))
64
+ ? 1
65
+ : 0;
66
+ }
23
67
  const encoder = new TextEncoder();
24
68
  /** Clip one echoed query term without splitting a non-BMP code point. */
25
69
  function boundedQueryTerm(term) {
@@ -47,10 +91,7 @@ export class DiscoveryPolicyError extends Error {
47
91
  function discoverySearchLimit(value) {
48
92
  if (value === undefined)
49
93
  return DEFAULT_SEARCH_LIMIT;
50
- if (typeof value !== "number" ||
51
- !Number.isInteger(value) ||
52
- value < 1 ||
53
- value > MAX_SEARCH_LIMIT) {
94
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > MAX_SEARCH_LIMIT) {
54
95
  throw new DiscoveryPolicyError("invalid_args", `limit must be a whole number from 1 through ${MAX_SEARCH_LIMIT}. Page through larger catalogs with offset.`);
55
96
  }
56
97
  return value;
@@ -60,11 +101,7 @@ function discoveryAddresses(args) {
60
101
  if (args.address !== undefined && args.addresses !== undefined) {
61
102
  throw new DiscoveryPolicyError("invalid_args", "describe takes either address or addresses, not both.");
62
103
  }
63
- const value = args.address !== undefined
64
- ? typeof args.address === "string"
65
- ? [args.address]
66
- : undefined
67
- : args.addresses;
104
+ const value = args.address !== undefined ? (typeof args.address === "string" ? [args.address] : undefined) : args.addresses;
68
105
  if (!Array.isArray(value)) {
69
106
  throw new DiscoveryPolicyError("invalid_args", 'describe takes { address: "<connectorId>.<toolName>" } or { addresses: ["<connectorId>.<toolName>", ...] }.');
70
107
  }
@@ -89,8 +126,7 @@ function editDistance(left, right) {
89
126
  for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
90
127
  const current = [leftIndex + 1];
91
128
  for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
92
- current.push(Math.min(previous[rightIndex + 1] + 1, current[rightIndex] + 1, previous[rightIndex] +
93
- (left[leftIndex] === right[rightIndex] ? 0 : 1)));
129
+ current.push(Math.min(previous[rightIndex + 1] + 1, current[rightIndex] + 1, previous[rightIndex] + (left[leftIndex] === right[rightIndex] ? 0 : 1)));
94
130
  }
95
131
  previous = current;
96
132
  }
@@ -99,7 +135,7 @@ function editDistance(left, right) {
99
135
  /** Nearby names only; descriptions never influence describe-miss recovery. */
100
136
  function describeSuggestions(connectorId, attemptedName, tools) {
101
137
  const attempted = attemptedName.toLowerCase();
102
- return tools
138
+ return (tools
103
139
  .map((tool, order) => {
104
140
  const name = tool.name.toLowerCase();
105
141
  return { tool, order, distance: editDistance(attempted, name) };
@@ -115,7 +151,7 @@ function describeSuggestions(connectorId, attemptedName, tools) {
115
151
  // A clipped address would no longer be canonical. Omit an implausibly
116
152
  // large catalog name instead of letting one suggestion erase the page.
117
153
  .filter((address) => boundedEchoText(address) === address)
118
- .slice(0, MAX_DESCRIBE_SUGGESTIONS);
154
+ .slice(0, MAX_DESCRIBE_SUGGESTIONS));
119
155
  }
120
156
  /** Serialize once and count the exact bytes the MCP adapter would emit. */
121
157
  export function boundedDiscoveryText(value, hint) {
@@ -132,9 +168,7 @@ export function boundedDiscoveryText(value, hint) {
132
168
  function discoverySafety(value) {
133
169
  if (value === undefined)
134
170
  return "all";
135
- if (value !== "readOnly" &&
136
- value !== "approvalRequired" &&
137
- value !== "all") {
171
+ if (value !== "readOnly" && value !== "approvalRequired" && value !== "all") {
138
172
  throw new DiscoveryPolicyError("invalid_args", 'safety must be "readOnly", "approvalRequired", or "all".');
139
173
  }
140
174
  return value;
@@ -142,9 +176,7 @@ function discoverySafety(value) {
142
176
  function toolsForSafety(tools, safety) {
143
177
  if (safety === "all")
144
178
  return tools;
145
- return tools.filter((tool) => safety === "readOnly"
146
- ? isExplicitlyReadOnly(tool)
147
- : !isExplicitlyReadOnly(tool));
179
+ return tools.filter((tool) => safety === "readOnly" ? tool.classification === "read" : tool.classification !== "read");
148
180
  }
149
181
  /**
150
182
  * Reasons discovery can determine without reading arguments or guessing at a
@@ -157,7 +189,7 @@ function guideRequiredReasons(connector, tool, schemaTruncated) {
157
189
  const reasons = [];
158
190
  if (connectorGuideRequired(connector))
159
191
  reasons.push("connector_required");
160
- if (!isExplicitlyReadOnly(tool))
192
+ if (tool.classification !== "read")
161
193
  reasons.push("approval_required");
162
194
  if (schemaTruncated)
163
195
  reasons.push("schema_truncated");
@@ -178,11 +210,131 @@ function schemaKeyMetadata(input, output) {
178
210
  requiredInputKeys: inputKeys.required,
179
211
  }
180
212
  : {}),
181
- ...(outputKeys && outputKeys.properties.length > 0
182
- ? { outputKeys: outputKeys.properties }
183
- : {}),
213
+ ...(outputKeys && outputKeys.properties.length > 0 ? { outputKeys: outputKeys.properties } : {}),
184
214
  };
185
215
  }
216
+ const stringListSchema = { type: "array", items: { type: "string" } };
217
+ const guideProperties = {
218
+ guide: { type: "string" },
219
+ guideSummary: { type: "string" },
220
+ guideRequired: { const: true },
221
+ guideRequiredReasons: {
222
+ type: "array",
223
+ items: { enum: ["connector_required", "approval_required", "schema_truncated"] },
224
+ },
225
+ };
226
+ const catalogFailureProperties = {
227
+ code: { type: "string" },
228
+ message: { type: "string" },
229
+ retryable: { type: "boolean" },
230
+ retryAfterMs: { type: "integer", minimum: 0 },
231
+ };
232
+ /**
233
+ * The outputSchema contract for either search adapter. Guest API registration
234
+ * can reuse this value instead of maintaining a second page definition.
235
+ */
236
+ export const CATALOG_SEARCH_RESULT_SCHEMA = {
237
+ type: "object",
238
+ required: ["catalogErrors", "tools", "total", "offset", "limit", "hasMore"],
239
+ additionalProperties: false,
240
+ properties: {
241
+ catalogErrors: {
242
+ type: "array",
243
+ items: {
244
+ type: "object",
245
+ required: ["connector", "code", "message", "retryable"],
246
+ additionalProperties: false,
247
+ properties: {
248
+ connector: { type: "string" },
249
+ ...catalogFailureProperties,
250
+ recovery: { enum: ["oauth", "operator_config", "unavailable"] },
251
+ retry: { type: "string" },
252
+ nextAction: {
253
+ type: "object",
254
+ required: ["tool", "arguments", "operatorHandoff"],
255
+ additionalProperties: false,
256
+ properties: {
257
+ tool: { const: "authorize_connector" },
258
+ arguments: {
259
+ type: "object",
260
+ required: ["connector"],
261
+ additionalProperties: false,
262
+ properties: { connector: { type: "string" } },
263
+ },
264
+ operatorHandoff: { type: "string" },
265
+ },
266
+ },
267
+ },
268
+ },
269
+ },
270
+ absence: {
271
+ type: "object",
272
+ required: ["service", "message", "configuredConnectors"],
273
+ additionalProperties: false,
274
+ properties: {
275
+ service: { type: "string" },
276
+ message: { type: "string" },
277
+ configuredConnectors: stringListSchema,
278
+ },
279
+ },
280
+ tools: {
281
+ type: "array",
282
+ items: {
283
+ type: "object",
284
+ required: ["name", "address"],
285
+ additionalProperties: false,
286
+ properties: {
287
+ name: { type: "string" },
288
+ address: { type: "string" },
289
+ connectorTitle: { type: "string" },
290
+ description: { type: "string" },
291
+ inputSchema: {},
292
+ outputSchema: {},
293
+ signature: { type: "string" },
294
+ schemaFormat: { enum: ["json", "text"] },
295
+ outputSchemaSource: { const: "observed" },
296
+ inputSchemaTruncated: { const: true },
297
+ outputSchemaTruncated: { const: true },
298
+ inputKeys: stringListSchema,
299
+ requiredInputKeys: stringListSchema,
300
+ outputKeys: stringListSchema,
301
+ annotations: { type: "object" },
302
+ classification: { enum: ["read", "write"] },
303
+ ...guideProperties,
304
+ },
305
+ },
306
+ },
307
+ total: { type: "integer", minimum: 0 },
308
+ offset: { type: "integer", minimum: 0 },
309
+ limit: { type: "integer", minimum: 1, maximum: MAX_SEARCH_LIMIT },
310
+ hasMore: { type: "boolean" },
311
+ nextOffset: { type: "integer", minimum: 0 },
312
+ matchMode: { const: "partial" },
313
+ queryAnalysis: {
314
+ type: "object",
315
+ required: ["representedTerms", "otherResultTerms", "unmatchedTerms"],
316
+ additionalProperties: false,
317
+ properties: {
318
+ representedTerms: stringListSchema,
319
+ otherResultTerms: stringListSchema,
320
+ unmatchedTerms: stringListSchema,
321
+ truncated: { const: true },
322
+ connectorScope: { type: "string" },
323
+ unknownConnector: { const: true },
324
+ configuredConnectors: stringListSchema,
325
+ unavailableConnectorCount: { type: "integer", minimum: 0 },
326
+ catalogError: {
327
+ type: "object",
328
+ required: ["code", "message", "retryable"],
329
+ additionalProperties: false,
330
+ properties: catalogFailureProperties,
331
+ },
332
+ ...guideProperties,
333
+ guidance: { type: "string" },
334
+ },
335
+ },
336
+ },
337
+ };
186
338
  function renderSearchSchema(schema, format) {
187
339
  if (format === "json")
188
340
  return { schema, truncated: false };
@@ -198,16 +350,17 @@ export class CatalogService {
198
350
  registry;
199
351
  baseUrl;
200
352
  requestScope;
353
+ defer;
201
354
  probeTimeoutMs;
202
355
  concurrency;
203
356
  searchRoute;
204
- readOptions;
205
357
  requestSignal;
206
- approval;
207
358
  // The request-scoped catalog cache: one shared read per connector asked
208
359
  // about, started by the first asker and joined by every later one. A
209
360
  // success stays for the rest of the request, so a search and the call after
210
- // it see one catalog; a failure, or a read cancelled because every asker
361
+ // it see one catalog. The cache owns a deep copy and each asker receives
362
+ // another, so discovery, invocation, and guest code never hold its objects.
363
+ // A failure, or a read cancelled because every asker
211
364
  // left it, is dropped so the next ask reads again. The service lives and
212
365
  // dies with one request, so no read here is ever awaited from another.
213
366
  //
@@ -220,19 +373,12 @@ export class CatalogService {
220
373
  constructor(registry, baseUrl, options = {}) {
221
374
  this.registry = registry;
222
375
  this.baseUrl = baseUrl;
223
- this.approval = options.approval ?? NO_EXEMPTIONS;
224
376
  this.requestScope = options.requestScope ?? {};
225
- this.probeTimeoutMs =
226
- normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
377
+ this.probeTimeoutMs = normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
227
378
  this.concurrency = resolveDiscoveryConcurrency(options.concurrency);
228
379
  this.searchRoute = options.searchRoute ?? "search_tools";
229
380
  this.requestSignal = options.requestSignal;
230
- this.readOptions = options.defer
231
- ? {
232
- defer: options.defer,
233
- refreshTimeoutMs: this.probeTimeoutMs,
234
- }
235
- : undefined;
381
+ this.defer = options.defer;
236
382
  }
237
383
  /**
238
384
  * Send a caller back to discovery through the surface it can actually reach.
@@ -269,7 +415,13 @@ export class CatalogService {
269
415
  return Effect.suspend(() => {
270
416
  let read = this.catalogs.get(id);
271
417
  if (!read) {
272
- const started = new SharedRead((signal) => this.registry.getTools(id, this.baseUrl, this.requestScope, { signal, timeoutMs: this.probeTimeoutMs }, this.readOptions), (succeeded) => {
418
+ const started = new SharedRead((signal) => this.registry
419
+ .getTools(id, this.baseUrl, this.requestScope, {
420
+ signal,
421
+ timeoutMs: this.probeTimeoutMs,
422
+ ...(this.defer ? { defer: this.defer } : {}),
423
+ })
424
+ .then((tools) => carryCatalogFreshness(tools, structuredClone(tools))), (succeeded) => {
273
425
  // Evicted before the read resumes anyone, so an asker that
274
426
  // retries on hearing of the failure starts a fresh read.
275
427
  if (!succeeded && this.catalogs.get(id) === started) {
@@ -279,7 +431,7 @@ export class CatalogService {
279
431
  this.catalogs.set(id, started);
280
432
  read = started;
281
433
  }
282
- return read.join(callOptions.signal);
434
+ return Effect.map(read.join(callOptions.signal), (tools) => carryCatalogFreshness(tools, structuredClone(tools)));
283
435
  });
284
436
  }
285
437
  // Every named catalog under its own probe deadline, `concurrency` at a time,
@@ -291,22 +443,21 @@ export class CatalogService {
291
443
  return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.catalog(id, { signal }), {
292
444
  ...(this.requestSignal ? { signal: this.requestSignal } : {}),
293
445
  timeoutMs: this.probeTimeoutMs,
294
- timeoutError: new Error(`${route} probe of "${id}" timed out after ${this.probeTimeoutMs}ms`),
446
+ timeoutError: new ConnectorCallError("timeout", `${route} probe of "${id}" timed out after ${this.probeTimeoutMs}ms`),
295
447
  })), { concurrency: this.concurrency });
296
448
  }
297
449
  outputSchema(connectorId, tool) {
298
450
  if (tool.outputSchema)
299
451
  return { schema: tool.outputSchema };
300
452
  const observed = this.registry.observedOutputSchema(connectorId, tool);
301
- return observed
302
- ? { schema: observed, source: "observed" }
303
- : {};
453
+ return observed ? { schema: observed, source: "observed" } : {};
304
454
  }
305
455
  unknownAddressFailure(address, query) {
306
456
  return {
307
457
  ok: false,
308
458
  error: {
309
459
  ...framingError("unknown_address", `Unknown address "${boundedEchoText(address)}"`),
460
+ configuredConnectors: this.registry.listConnectors().map((connector) => connector.id),
310
461
  nextAction: this.searchRecovery({ query: recoveryQuery(query) }, "Find the configured canonical address before retrying."),
311
462
  },
312
463
  catalogMs: 0,
@@ -326,8 +477,10 @@ export class CatalogService {
326
477
  return {
327
478
  ok: false,
328
479
  error: {
329
- ...framingError("unknown_tool", `Unknown tool "${boundedEchoText(toolName)}" on connector "${connector.id}"`),
330
- nextAction: this.searchRecovery({ query: recoveryQuery(toolName), connector: connector.id }, "Find the connector's current canonical tool address."),
480
+ ...framingError("unknown_tool", hasControlCharacters(toolName)
481
+ ? `Unknown tool on connector "${connector.id}"`
482
+ : `Unknown tool "${boundedEchoText(toolName)}" on connector "${connector.id}"`),
483
+ nextAction: this.searchRecovery({ query: hasControlCharacters(toolName) ? "" : recoveryQuery(toolName), connector: connector.id }, "Find the connector's current canonical tool address."),
331
484
  },
332
485
  catalogMs: Date.now() - started,
333
486
  connector,
@@ -357,7 +510,7 @@ export class CatalogService {
357
510
  }
358
511
  return {
359
512
  ok: true,
360
- resolved: { connector, toolName, definition },
513
+ resolved: { connector, toolName, definition, classificationFresh: catalogIsFresh(tools) },
361
514
  catalogMs: Date.now() - started,
362
515
  };
363
516
  },
@@ -368,8 +521,7 @@ export class CatalogService {
368
521
  if (args.query !== undefined && typeof args.query !== "string") {
369
522
  throw new DiscoveryPolicyError("invalid_args", "query must be a string. Omit it or use an empty string to browse the catalog.");
370
523
  }
371
- if (args.connector !== undefined &&
372
- boundedEchoText(args.connector) !== args.connector) {
524
+ if (args.connector !== undefined && boundedEchoText(args.connector) !== args.connector) {
373
525
  // The scope is echoed back as `queryAnalysis.connectorScope`; a clipped
374
526
  // copy could name a different connector, so refuse instead of clamping.
375
527
  throw new DiscoveryPolicyError("invalid_args", "connector must be at most 512 UTF-8 bytes.");
@@ -378,38 +530,44 @@ export class CatalogService {
378
530
  const retrievalQuery = lexicalSearchQuery(query);
379
531
  const safety = discoverySafety(args.safety);
380
532
  const limit = discoverySearchLimit(args.limit);
381
- if (args.offset !== undefined && (typeof args.offset !== "number" ||
382
- !Number.isInteger(args.offset) ||
383
- args.offset < 0)) {
533
+ if (args.offset !== undefined &&
534
+ (typeof args.offset !== "number" || !Number.isInteger(args.offset) || args.offset < 0)) {
384
535
  throw new DiscoveryPolicyError("invalid_args", "offset must be a non-negative whole number. Start at 0 or use the previous page's nextOffset.");
385
536
  }
386
537
  const offset = args.offset ?? 0;
387
- const scopedConnector = args.connector
388
- ? this.registry.getConnector(args.connector)
538
+ const scopedConnector = args.connector ? this.registry.getConnector(args.connector) : undefined;
539
+ const visibleConnectors = this.registry.listConnectors();
540
+ const namedService = !args.connector ? SERVICE_NAMES.find((name) => containsPhrase(query, name)) : undefined;
541
+ const absentService = args.connector && !scopedConnector
542
+ ? args.connector
543
+ : namedService &&
544
+ !visibleConnectors.some((connector) => connectorNames(connector).some((name) => containsPhrase(name, namedService)))
545
+ ? namedService
546
+ : undefined;
547
+ const absence = absentService
548
+ ? {
549
+ service: absentService,
550
+ message: `No connector for "${absentService}" is configured for this endpoint.`,
551
+ configuredConnectors: visibleConnectors.map((connector) => connector.id),
552
+ }
389
553
  : undefined;
390
- const connectors = args.connector
391
- ? scopedConnector
392
- ? [scopedConnector]
393
- : []
394
- : this.registry.listConnectors();
395
- // Named for this service's route: a scoped search echoes a timed-out
396
- // probe's message back as `queryAnalysis.catalogError`, and a program
397
- // cannot call search_tools.
554
+ const connectors = absence ? [] : args.connector ? (scopedConnector ? [scopedConnector] : []) : visibleConnectors;
555
+ // Timeout messages name the route the caller used.
398
556
  const catalogs = await runEdge(this.discoveryCatalogs(connectors.map((connector) => connector.id), this.searchRoute));
399
557
  const searchableCatalogs = catalogs.map((catalog) => Result.map(catalog, (tools) => toolsForSafety(tools, safety)));
400
558
  const matches = [];
401
559
  let matchMode = "all";
402
- const statistics = lexicalCorpusStatistics(searchableCatalogs.flatMap((catalog) => Result.isSuccess(catalog) ? [catalog.success] : []), retrievalQuery);
560
+ const statistics = lexicalCorpusStatistics(searchableCatalogs.flatMap((catalog) => (Result.isSuccess(catalog) ? [catalog.success] : [])), retrievalQuery);
403
561
  const trimmedQuery = query.trim();
404
562
  const isBrowse = trimmedQuery.length === 0;
405
563
  const queryTerms = lexicalQueryTerms(retrievalQuery);
564
+ const rawPhrase = lexicalQueryTerms(query).join(" ");
406
565
  const queryTermCount = queryTerms.length;
407
566
  const unsearchableQuery = !isBrowse && queryTermCount === 0;
408
567
  const analysisTerms = unsearchableQuery ? [trimmedQuery] : queryTerms;
409
568
  const analyzedTerms = analysisTerms.slice(0, MAX_QUERY_TERMS);
410
569
  const displayTerm = (term) => boundedQueryTerm(term).text;
411
- const queryMetadataTruncated = analysisTerms.length > analyzedTerms.length ||
412
- analyzedTerms.some((term) => boundedQueryTerm(term).truncated);
570
+ const queryMetadataTruncated = analysisTerms.length > analyzedTerms.length || analyzedTerms.some((term) => boundedQueryTerm(term).truncated);
413
571
  const collectMatches = (mode) => {
414
572
  const collected = [];
415
573
  let orderBase = 0;
@@ -419,15 +577,43 @@ export class CatalogService {
419
577
  throw new Error("Catalog result has no corresponding connector");
420
578
  }
421
579
  if (Result.isSuccess(catalog)) {
422
- for (const ranked of rankTools(catalog.success, retrievalQuery, mode, statistics, query)) {
580
+ const connectorRank = args.connector ? 0 : connectorMatchRank(query, connector);
581
+ const exactConnector = connectorRank > 0;
582
+ const connectorTerms = new Set([
583
+ ...connectorNames(connector).flatMap(lexicalQueryTerms),
584
+ ...(namedService && connectorNames(connector).some((name) => containsPhrase(name, namedService))
585
+ ? lexicalQueryTerms(namedService)
586
+ : []),
587
+ ]);
588
+ const localStatistics = exactConnector
589
+ ? lexicalCorpusStatistics([catalog.success], retrievalQuery)
590
+ : undefined;
591
+ // An exact identity is a browse even when one tool repeats that
592
+ // identity in its name or description. Action queries still keep
593
+ // identity terms that also name an actual capability.
594
+ const identityBrowse = connectorNames(connector).some((name) => normalizedPhrase(query) === normalizedPhrase(name));
595
+ const toolQuery = identityBrowse
596
+ ? ""
597
+ : exactConnector
598
+ ? queryTerms
599
+ .filter((term) => !connectorTerms.has(term) ||
600
+ (localStatistics?.nameMatches.get(term)?.size ?? 0) > 0 ||
601
+ (localStatistics?.descriptionMatches.get(term)?.size ?? 0) > 0)
602
+ .join(" ")
603
+ : retrievalQuery;
604
+ const toolTermCount = lexicalQueryTerms(toolQuery).length;
605
+ for (const ranked of rankTools(catalog.success, toolQuery, mode, statistics, query)) {
423
606
  collected.push({
424
607
  connector,
425
608
  tool: ranked.tool,
426
609
  score: ranked.score,
427
610
  order: orderBase + ranked.order,
428
611
  exactName: ranked.exactName,
612
+ exactLookup: rawPhrase === lexicalQueryTerms(ranked.tool.name).join(" ") ||
613
+ rawPhrase === lexicalQueryTerms(`${connector.id}.${ranked.tool.name}`).join(" "),
614
+ connectorRank,
429
615
  matchedTermCount: ranked.matchedTermCount,
430
- complete: isBrowse || ranked.matchedTermCount === queryTermCount,
616
+ complete: isBrowse || ranked.matchedTermCount === toolTermCount,
431
617
  });
432
618
  }
433
619
  }
@@ -438,34 +624,33 @@ export class CatalogService {
438
624
  // A non-empty query that normalizes to no lexical terms is not a browse.
439
625
  // Ranking an empty phrase would otherwise return every tool as an
440
626
  // unrelated zero-score match, with no coverage to explain the result.
441
- const rankedMatches = unsearchableQuery
442
- ? []
443
- : collectMatches(isBrowse ? "all" : "partial");
627
+ const rankedMatches = unsearchableQuery ? [] : collectMatches(isBrowse ? "all" : "partial");
444
628
  const completeMatchCount = rankedMatches.filter((match) => match.complete).length;
445
629
  matches.push(...rankedMatches.filter((match) => match.complete ||
446
630
  completeMatchCount === 0 ||
447
631
  match.exactName ||
632
+ match.connectorRank > 0 ||
448
633
  match.matchedTermCount >= 2));
449
634
  if (!isBrowse && !unsearchableQuery && completeMatchCount === 0) {
450
635
  matchMode = "partial";
451
636
  }
452
- // Complete matches and exact tool-name phrases share the first rank tier.
453
- // This lets a strong action/object name beat a weak description-only
454
- // decoy. Other partial matches fill the remaining page only after every
455
- // complete match, regardless of a rare-term score spike.
637
+ // Exact tool names precede connector identity and lexical scoring. A
638
+ // longer name such as get_issue_status cannot beat an exact get_issue
639
+ // merely because its description repeats rare query terms.
456
640
  matches.sort((a, b) => {
457
641
  const aFirstTier = a.complete || a.exactName;
458
642
  const bFirstTier = b.complete || b.exactName;
459
- return (Number(bFirstTier) - Number(aFirstTier) ||
643
+ return (Number(b.exactLookup) - Number(a.exactLookup) ||
644
+ Number(b.exactName) - Number(a.exactName) ||
645
+ b.connectorRank - a.connectorRank ||
646
+ Number(bFirstTier) - Number(aFirstTier) ||
460
647
  b.score - a.score ||
461
648
  a.order - b.order);
462
649
  });
463
650
  const pageMatches = matches.slice(offset, offset + limit);
464
651
  const withSchemas = args.includeSchemas;
465
652
  const entries = pageMatches.map((match) => {
466
- const output = withSchemas
467
- ? this.outputSchema(match.connector.id, match.tool)
468
- : {};
653
+ const output = withSchemas ? this.outputSchema(match.connector.id, match.tool) : {};
469
654
  const input = match.tool.inputSchema ?? { type: "object" };
470
655
  const signature = withSchemas === "typescript"
471
656
  ? typescriptSignature(input, output.schema, {
@@ -484,9 +669,7 @@ export class CatalogService {
484
669
  : schemaFormat && output.schema
485
670
  ? renderSearchSchema(output.schema, schemaFormat)
486
671
  : undefined;
487
- const schemaKeys = withSchemas && args.includeSchemaKeys
488
- ? schemaKeyMetadata(input, output.schema)
489
- : undefined;
672
+ const schemaKeys = withSchemas && args.includeSchemaKeys ? schemaKeyMetadata(input, output.schema) : undefined;
490
673
  const description = summarizeDiscoveryDescription(match.tool.description, args.fullDescriptions === true);
491
674
  const requiredReasons = guideRequiredReasons(match.connector, match.tool, renderedInput?.truncated === true || renderedOutput?.truncated === true);
492
675
  const guideSummary = connectorGuideSummary(match.connector);
@@ -507,42 +690,25 @@ export class CatalogService {
507
690
  inputSchema: renderedInput?.schema,
508
691
  }
509
692
  : {}),
693
+ ...(withSchemas ? { schemaFormat: withSchemas === "json" ? "json" : "text" } : {}),
510
694
  ...(signature ? { signature: signature.text } : {}),
511
- ...(renderedInput?.truncated
512
- ? { inputSchemaTruncated: true }
513
- : {}),
695
+ ...(renderedInput?.truncated ? { inputSchemaTruncated: true } : {}),
514
696
  ...(schemaFormat && output.schema
515
697
  ? {
516
698
  outputSchema: renderedOutput?.schema,
517
699
  }
518
700
  : {}),
519
- ...(withSchemas && output.source
520
- ? { outputSchemaSource: output.source }
521
- : {}),
522
- ...(renderedOutput?.truncated
523
- ? { outputSchemaTruncated: true }
524
- : {}),
701
+ ...(withSchemas && output.source ? { outputSchemaSource: output.source } : {}),
702
+ ...(renderedOutput?.truncated ? { outputSchemaTruncated: true } : {}),
525
703
  ...(schemaKeys && !renderedInput?.truncated
526
704
  ? {
527
- ...(schemaKeys.inputKeys
528
- ? { inputKeys: schemaKeys.inputKeys }
529
- : {}),
530
- ...(schemaKeys.requiredInputKeys
531
- ? { requiredInputKeys: schemaKeys.requiredInputKeys }
532
- : {}),
705
+ ...(schemaKeys.inputKeys ? { inputKeys: schemaKeys.inputKeys } : {}),
706
+ ...(schemaKeys.requiredInputKeys ? { requiredInputKeys: schemaKeys.requiredInputKeys } : {}),
533
707
  }
534
708
  : {}),
535
- ...(schemaKeys?.outputKeys && !renderedOutput?.truncated
536
- ? { outputKeys: schemaKeys.outputKeys }
537
- : {}),
538
- ...(match.tool.annotations
539
- ? { annotations: match.tool.annotations }
540
- : {}),
541
- // Config lets a program call this write without asking (#566).
542
- // A marker, not a class: the tool stays approval-required.
543
- ...(isApprovalExempt(this.approval, match.connector, match.tool.name, match.tool)
544
- ? { approval: "exempt" }
545
- : {}),
709
+ ...(schemaKeys?.outputKeys && !renderedOutput?.truncated ? { outputKeys: schemaKeys.outputKeys } : {}),
710
+ ...(match.tool.annotations ? { annotations: match.tool.annotations } : {}),
711
+ classification: match.tool.classification,
546
712
  ...(requiredReasons
547
713
  ? {
548
714
  guideRequired: true,
@@ -552,14 +718,9 @@ export class CatalogService {
552
718
  },
553
719
  };
554
720
  });
555
- const nextOffset = offset + entries.length < matches.length
556
- ? offset + entries.length
557
- : undefined;
721
+ const nextOffset = offset + entries.length < matches.length ? offset + entries.length : undefined;
558
722
  const pageTools = new Set(pageMatches.map((match) => match.tool));
559
- const matchingTools = (term) => new Set([
560
- ...(statistics.nameMatches.get(term) ?? []),
561
- ...(statistics.descriptionMatches.get(term) ?? []),
562
- ]);
723
+ const matchingTools = (term) => new Set([...(statistics.nameMatches.get(term) ?? []), ...(statistics.descriptionMatches.get(term) ?? [])]);
563
724
  const representedTerms = [];
564
725
  const otherResultTerms = [];
565
726
  const unmatchedTerms = [];
@@ -576,32 +737,61 @@ export class CatalogService {
576
737
  }
577
738
  }
578
739
  const unavailableCatalogs = catalogs.filter(Result.isFailure).length;
579
- // Named field by field rather than spread: `CallErrorDetails` also carries
580
- // connector, operation, recovery, and nextAction, and a discovery read is
581
- // not a call — widening the classifier must not silently widen what a
582
- // catalog search hands back.
583
- const scopedCatalogError = (() => {
584
- const scopedCatalog = catalogs[0];
585
- if (!scopedConnector ||
586
- !scopedCatalog ||
587
- Result.isSuccess(scopedCatalog)) {
588
- return undefined;
589
- }
590
- const error = classifyCallError(scopedCatalog.failure, "catalog_lookup_failed");
591
- return {
592
- code: error.code,
593
- message: boundedEchoText(error.message),
594
- retryable: error.retryable,
595
- ...(error.retryAfterMs === undefined
596
- ? {}
597
- : { retryAfterMs: error.retryAfterMs }),
740
+ const catalogErrors = catalogs.flatMap((catalog, index) => {
741
+ if (Result.isSuccess(catalog))
742
+ return [];
743
+ const connector = connectors[index];
744
+ const classified = classifyCallError(catalog.failure, "catalog_lookup_failed");
745
+ const error = {
746
+ connector: connector.id,
747
+ code: classified.code,
748
+ // Catalog discovery reports recovery facts, never connector-authored
749
+ // error text. Clipping a downstream message does not make it safe.
750
+ message: classified.code === "downstream_oauth_required"
751
+ ? `Connector "${connector.id}" requires downstream OAuth authorization. Call authorize_connector and give its handoff to the operator.`
752
+ : classified.code === "auth_required"
753
+ ? `Connector "${connector.id}" requires operator-managed credentials or configuration. Call authorize_connector and give its handoff to the operator.`
754
+ : classified.code === "provider_permission_denied"
755
+ ? `Connector "${connector.id}" requires permission from the provider's resource owner or administrator. Reconnecting alone will not grant access.`
756
+ : `Connector "${connector.id}" catalog lookup failed (${classified.code}).`,
757
+ retryable: classified.retryable,
758
+ ...(classified.retryAfterMs === undefined ? {} : { retryAfterMs: classified.retryAfterMs }),
598
759
  };
599
- })();
600
- const safetyLabel = safety === "readOnly"
601
- ? "read-only "
602
- : safety === "approvalRequired"
603
- ? "approval-required "
604
- : "";
760
+ if (error.code === "auth_required" || error.code === "downstream_oauth_required") {
761
+ error.recovery = connector.startAuth
762
+ ? "oauth"
763
+ : this.registry.credentialUiAvailable() &&
764
+ connector.credential &&
765
+ this.registry.contextFor(connector.id, this.baseUrl, this.requestScope).credential
766
+ ? "operator_config"
767
+ : "unavailable";
768
+ error.nextAction = {
769
+ tool: "authorize_connector",
770
+ arguments: { connector: connector.id },
771
+ operatorHandoff: "Give the URL and instructions it returns to the operator.",
772
+ };
773
+ error.retry = `Retry discovery after the operator completes recovery for "${connector.id}".`;
774
+ }
775
+ else if (error.code === "provider_permission_denied") {
776
+ error.retry =
777
+ "Ask the provider's resource owner or administrator to grant the required permission or scope, then retry. Reconnecting alone will not grant access.";
778
+ }
779
+ return [error];
780
+ });
781
+ const needsAuth = (error) => error.code === "auth_required" ||
782
+ error.code === "downstream_oauth_required" ||
783
+ error.code === "provider_permission_denied";
784
+ catalogErrors.sort((a, b) => Number(needsAuth(b)) - Number(needsAuth(a)));
785
+ const scopedFailure = args.connector ? catalogErrors[0] : undefined;
786
+ const scopedCatalogError = scopedFailure
787
+ ? {
788
+ code: scopedFailure.code,
789
+ message: scopedFailure.message,
790
+ retryable: scopedFailure.retryable,
791
+ ...(scopedFailure.retryAfterMs === undefined ? {} : { retryAfterMs: scopedFailure.retryAfterMs }),
792
+ }
793
+ : undefined;
794
+ const safetyLabel = safety === "readOnly" ? "read-only " : safety === "approvalRequired" ? "approval-required " : "";
605
795
  const filterRecovery = safety === "all" ? "" : " Change safety to inspect the other tools.";
606
796
  const scopedGuide = matches.length === 0 && scopedConnector && connectorGuide(scopedConnector)
607
797
  ? {
@@ -610,15 +800,9 @@ export class CatalogService {
610
800
  required: connectorGuideRequired(scopedConnector),
611
801
  }
612
802
  : undefined;
613
- // A connector's own id — the address prefix the caller already has — and
614
- // the title it is displayed under are the most natural first query terms,
615
- // and neither is a document in the lexical index. Indexing them would move
616
- // ranking for every query that already matches tools, so instead a search
617
- // that matched nothing asks the same lexical question of connector
618
- // identity and corrects its own sentence: the deployment plainly has this
619
- // capability, and one scoped browse away are its tools. Unscoped only —
620
- // guidance for an explicit scope already names that connector rather than
621
- // claiming the deployment has nothing.
803
+ // Partial identity words can still explain a miss. Exact identities are
804
+ // already ranking inputs above; this guidance handles a title word such
805
+ // as "warehouse" when the full title is "Warehouse stock".
622
806
  const identityConnectorIds = matches.length === 0 && !isBrowse && !unsearchableQuery && !scopedConnector
623
807
  ? connectors
624
808
  // The full query, not `analyzedTerms`: that cap exists to bound
@@ -626,8 +810,7 @@ export class CatalogService {
626
810
  // term. A ninth term is a real search term, and a search that
627
811
  // ranked against it must not deny the connector it names.
628
812
  .filter((connector) => queryTerms.some((term) => matchesLexicalTerm(connector.id, term) ||
629
- (connector.title !== undefined &&
630
- matchesLexicalTerm(connector.title, term))))
813
+ (connector.title !== undefined && matchesLexicalTerm(connector.title, term))))
631
814
  .map((connector) => connector.id)
632
815
  : [];
633
816
  const namedIdentityConnectors = identityConnectorIds
@@ -637,17 +820,16 @@ export class CatalogService {
637
820
  const unnamedIdentityConnectors = identityConnectorIds.length - MAX_IDENTITY_CONNECTORS;
638
821
  const identityGuidance = identityConnectorIds.length === 0
639
822
  ? undefined
640
- : `No matching ${safetyLabel}capability was found${unavailableCatalogs === 0 ? "" : " in the catalogs that answered"}, but the query names configured connector${identityConnectorIds.length === 1 ? "" : "s"} ${namedIdentityConnectors}${unnamedIdentityConnectors > 0
641
- ? ` and ${unnamedIdentityConnectors} more`
642
- : ""}. Scope by connector and browse with an empty query to list the tools there.${filterRecovery}`;
823
+ : `No matching ${safetyLabel}capability was found${unavailableCatalogs === 0 ? "" : " in the catalogs that answered"}, but the query names configured connector${identityConnectorIds.length === 1 ? "" : "s"} ${namedIdentityConnectors}${unnamedIdentityConnectors > 0 ? ` and ${unnamedIdentityConnectors} more` : ""}. Scope by connector and browse with an empty query to list the tools there.${filterRecovery}`;
643
824
  // A scope that resolved to nothing is the same silence one step earlier in
644
825
  // the lookup: no connector resolved, so no catalog was even attempted, so
645
- // no catalog failed and the unavailable path below never fires. Echo only
646
- // the ID the caller already supplied — naming what else is configured
647
- // would answer a question they did not ask, past a filter they may not
648
- // pass.
826
+ // no catalog failed and the unavailable path below never fires. Name
827
+ // only configured connectors in the current identity/pool registry view.
649
828
  const unknownConnectorGuidance = args.connector && !scopedConnector
650
- ? `Connector "${args.connector}" is not configured in this deployment. Omit connector to search all configured tools.`
829
+ ? `Connector "${args.connector}" is not configured in this deployment. Omit connector to search all configured tools. Configured connectors for this endpoint: ${this.registry
830
+ .listConnectors()
831
+ .map((connector) => connector.id)
832
+ .join(", ") || "none"}.`
651
833
  : undefined;
652
834
  // Searchable queries report analysis when the scorer had to degrade. A
653
835
  // non-empty query with no searchable terms reports the bounded raw input
@@ -655,13 +837,16 @@ export class CatalogService {
655
837
  // no terms to analyse and normally reports none at all. Scope failures are
656
838
  // the exception, because an empty result alone looks like a connector that
657
839
  // correctly exposes no tools.
658
- const reportsQueryAnalysis = unsearchableQuery ||
840
+ const reportsQueryAnalysis = absence !== undefined ||
841
+ unsearchableQuery ||
659
842
  (queryTerms.length > 0
660
843
  ? matchMode === "partial"
661
844
  : unknownConnectorGuidance !== undefined || unavailableCatalogs > 0);
662
845
  const searchGuidance = () => {
663
846
  if (unknownConnectorGuidance)
664
847
  return unknownConnectorGuidance;
848
+ if (absence)
849
+ return absence.message;
665
850
  if (unsearchableQuery) {
666
851
  return scopedConnector && unavailableCatalogs > 0
667
852
  ? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
@@ -672,7 +857,7 @@ export class CatalogService {
672
857
  return undefined;
673
858
  return scopedConnector
674
859
  ? `Connector "${scopedConnector.id}" could not be browsed because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
675
- : `${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable, so this browse is incomplete. Scope by connector to see the typed reason.`;
860
+ : `${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable, so this browse is incomplete. Inspect catalogErrors for the typed reasons and recovery actions.`;
676
861
  }
677
862
  if (matches.length === 0) {
678
863
  if (scopedConnector) {
@@ -700,44 +885,39 @@ export class CatalogService {
700
885
  };
701
886
  const guidance = searchGuidance();
702
887
  return {
888
+ catalogErrors,
889
+ ...(absence ? { absence } : {}),
703
890
  entries,
704
891
  total: matches.length,
705
892
  offset,
706
893
  limit,
707
894
  hasMore: nextOffset !== undefined,
708
895
  ...(nextOffset !== undefined ? { nextOffset } : {}),
709
- ...(matchMode === "partial" && matches.length > 0
710
- ? { matchMode }
711
- : {}),
896
+ ...(matchMode === "partial" && matches.length > 0 ? { matchMode } : {}),
712
897
  ...(reportsQueryAnalysis
713
898
  ? {
714
899
  queryAnalysis: {
715
900
  representedTerms,
716
901
  otherResultTerms,
717
902
  unmatchedTerms,
718
- ...(queryMetadataTruncated
719
- ? { truncated: true }
720
- : {}),
903
+ ...(queryMetadataTruncated ? { truncated: true } : {}),
721
904
  ...(args.connector ? { connectorScope: args.connector } : {}),
722
905
  ...(args.connector && !scopedConnector
723
- ? { unknownConnector: true }
724
- : {}),
725
- ...(unavailableCatalogs > 0
726
- ? { unavailableConnectorCount: unavailableCatalogs }
906
+ ? {
907
+ unknownConnector: true,
908
+ configuredConnectors: this.registry.listConnectors().map((connector) => connector.id),
909
+ }
727
910
  : {}),
911
+ ...(unavailableCatalogs > 0 ? { unavailableConnectorCount: unavailableCatalogs } : {}),
728
912
  ...(scopedCatalogError ? { catalogError: scopedCatalogError } : {}),
729
913
  ...(scopedGuide
730
914
  ? {
731
915
  guide: scopedGuide.guide,
732
- ...(scopedGuide.guideSummary
733
- ? { guideSummary: scopedGuide.guideSummary }
734
- : {}),
916
+ ...(scopedGuide.guideSummary ? { guideSummary: scopedGuide.guideSummary } : {}),
735
917
  ...(scopedGuide.required
736
918
  ? {
737
919
  guideRequired: true,
738
- guideRequiredReasons: [
739
- "connector_required",
740
- ],
920
+ guideRequiredReasons: ["connector_required"],
741
921
  }
742
922
  : {}),
743
923
  }
@@ -756,9 +936,7 @@ export class CatalogService {
756
936
  return { address, resolved: this.registry.resolveAddress(address) };
757
937
  });
758
938
  const connectorIds = [
759
- ...new Set(resolved
760
- .map((entry) => entry.resolved?.connector.id)
761
- .filter((id) => Boolean(id))),
939
+ ...new Set(resolved.map((entry) => entry.resolved?.connector.id).filter((id) => Boolean(id))),
762
940
  ];
763
941
  const loaded = await runEdge(this.discoveryCatalogs(connectorIds, "connecta.describe"));
764
942
  const catalogs = new Map(connectorIds.map((id, index) => [id, loaded[index]]));
@@ -770,6 +948,7 @@ export class CatalogService {
770
948
  error: message,
771
949
  errorDetails: {
772
950
  ...framingError("unknown_address", message),
951
+ configuredConnectors: this.registry.listConnectors().map((connector) => connector.id),
773
952
  nextAction: this.searchRecovery({ query: recoveryQuery(address) }, "Find the configured canonical address before retrying."),
774
953
  },
775
954
  };
@@ -785,9 +964,7 @@ export class CatalogService {
785
964
  code: classified.code,
786
965
  message,
787
966
  retryable: classified.retryable,
788
- ...(classified.retryAfterMs === undefined
789
- ? {}
790
- : { retryAfterMs: classified.retryAfterMs }),
967
+ ...(classified.retryAfterMs === undefined ? {} : { retryAfterMs: classified.retryAfterMs }),
791
968
  },
792
969
  };
793
970
  }
@@ -818,16 +995,15 @@ export class CatalogService {
818
995
  description: true,
819
996
  })
820
997
  : undefined;
821
- const compactInput = format === "compact"
822
- ? compactDescriptionSchema(input) : undefined;
823
- const compactOutput = format === "compact" && output.schema
824
- ? compactDescriptionSchema(output.schema) : undefined;
998
+ const compactInput = format === "compact" ? compactDescriptionSchema(input) : undefined;
999
+ const compactOutput = format === "compact" && output.schema ? compactDescriptionSchema(output.schema) : undefined;
825
1000
  const inputTruncated = compactInput?.truncated === true || signature?.inputTruncated === true;
826
1001
  const outputTruncated = compactOutput?.truncated === true || signature?.outputTruncated === true;
827
1002
  const requiredReasons = guideRequiredReasons(addressResolution.connector, tool, inputTruncated || outputTruncated);
828
1003
  const guideSummary = connectorGuideSummary(addressResolution.connector);
829
1004
  return {
830
1005
  address,
1006
+ schemaFormat: format === "json" ? "json" : "text",
831
1007
  name: tool.name,
832
1008
  ...(description !== undefined ? { description } : {}),
833
1009
  ...(connectorGuide(addressResolution.connector)
@@ -842,9 +1018,7 @@ export class CatalogService {
842
1018
  guideRequiredReasons: requiredReasons,
843
1019
  }
844
1020
  : {}),
845
- ...(signature
846
- ? { signature: signature.text }
847
- : { inputSchema: compactInput?.text ?? input }),
1021
+ ...(signature ? { signature: signature.text } : { inputSchema: compactInput?.text ?? input }),
848
1022
  ...(inputTruncated ? { inputSchemaTruncated: true } : {}),
849
1023
  ...(outputTruncated ? { outputSchemaTruncated: true } : {}),
850
1024
  ...(output.schema && !signature
@@ -854,9 +1028,7 @@ export class CatalogService {
854
1028
  : {}),
855
1029
  ...(output.source ? { outputSchemaSource: output.source } : {}),
856
1030
  ...(tool.annotations ? { annotations: tool.annotations } : {}),
857
- ...(isApprovalExempt(this.approval, addressResolution.connector, tool.name, tool)
858
- ? { approval: "exempt" }
859
- : {}),
1031
+ classification: tool.classification,
860
1032
  };
861
1033
  });
862
1034
  }
@@ -872,45 +1044,17 @@ function pageTail(page) {
872
1044
  ...(page.queryAnalysis ? { queryAnalysis: page.queryAnalysis } : {}),
873
1045
  };
874
1046
  }
875
- export function groupedSearchResult(page) {
876
- const groups = [];
877
- const byConnector = new Map();
878
- for (const entry of page.entries) {
879
- const existing = byConnector.get(entry.connector.id);
880
- if (existing) {
881
- existing.tools.push(entry.tool);
882
- }
883
- else {
884
- const group = {
885
- id: entry.connector.id,
886
- ...(entry.connector.title ? { title: entry.connector.title } : {}),
887
- ...(entry.guide ? { guide: entry.guide } : {}),
888
- ...(entry.guideSummary
889
- ? { guideSummary: entry.guideSummary }
890
- : {}),
891
- tools: [],
892
- };
893
- byConnector.set(entry.connector.id, group);
894
- groups.push(group);
895
- group.tools.push(entry.tool);
896
- }
897
- }
898
- return {
899
- connectors: groups,
900
- ...pageTail(page),
901
- };
902
- }
903
1047
  export function flatSearchResult(page) {
904
1048
  return {
1049
+ catalogErrors: page.catalogErrors,
1050
+ ...(page.absence ? { absence: page.absence } : {}),
905
1051
  tools: page.entries.map((entry) => ({
906
1052
  ...entry.tool,
907
1053
  ...(entry.connector.title
908
1054
  ? { connectorTitle: boundedEchoText(entry.connector.title.replace(/\s+/g, " ").trim(), 117) }
909
1055
  : {}),
910
1056
  ...(entry.guide ? { guide: entry.guide } : {}),
911
- ...(entry.guideSummary
912
- ? { guideSummary: entry.guideSummary }
913
- : {}),
1057
+ ...(entry.guideSummary ? { guideSummary: entry.guideSummary } : {}),
914
1058
  })),
915
1059
  ...pageTail(page),
916
1060
  };