@zackbart/connecta 0.15.1 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/AGENTS.md +40 -20
  2. package/CHANGELOG.md +653 -0
  3. package/README.md +25 -5
  4. package/bin/connecta.mjs +44 -3
  5. package/dist/access-tokens.d.ts +0 -1
  6. package/dist/access-tokens.js +0 -1
  7. package/dist/activity.d.ts +50 -2
  8. package/dist/activity.js +34 -1
  9. package/dist/apps-shell.d.ts +0 -1
  10. package/dist/apps-shell.js +0 -1
  11. package/dist/auth/bearer.d.ts +0 -1
  12. package/dist/auth/bearer.js +0 -1
  13. package/dist/auth/clerk.d.ts +0 -1
  14. package/dist/auth/clerk.js +45 -9
  15. package/dist/auth/downstream-oauth.d.ts +0 -1
  16. package/dist/auth/downstream-oauth.js +0 -1
  17. package/dist/call-admission.d.ts +0 -1
  18. package/dist/call-admission.js +0 -1
  19. package/dist/catalog-drift.d.ts +99 -0
  20. package/dist/catalog-drift.js +251 -0
  21. package/dist/catalog-fingerprint.d.ts +0 -1
  22. package/dist/catalog-fingerprint.js +0 -1
  23. package/dist/catalog-limits.d.ts +0 -1
  24. package/dist/catalog-limits.js +0 -1
  25. package/dist/catalog-service.d.ts +0 -1
  26. package/dist/catalog-service.js +41 -5
  27. package/dist/catalog.d.ts +11 -1
  28. package/dist/catalog.js +20 -1
  29. package/dist/concurrency.d.ts +0 -1
  30. package/dist/concurrency.js +0 -1
  31. package/dist/connector-scope.d.ts +0 -1
  32. package/dist/connector-scope.js +0 -1
  33. package/dist/connectors/api.d.ts +29 -21
  34. package/dist/connectors/api.js +45 -19
  35. package/dist/connectors/guarded-fetch.d.ts +106 -0
  36. package/dist/connectors/guarded-fetch.js +289 -0
  37. package/dist/connectors/remote-mcp.d.ts +0 -1
  38. package/dist/connectors/remote-mcp.js +0 -1
  39. package/dist/credentials.d.ts +0 -1
  40. package/dist/credentials.js +0 -1
  41. package/dist/errors.d.ts +30 -3
  42. package/dist/errors.js +1 -1
  43. package/dist/execute.d.ts +0 -1
  44. package/dist/execute.js +0 -1
  45. package/dist/executor-admission.d.ts +11 -1
  46. package/dist/executor-admission.js +28 -1
  47. package/dist/executor-result.d.ts +0 -1
  48. package/dist/executor-result.js +0 -1
  49. package/dist/executors/quickjs-child.d.ts +0 -1
  50. package/dist/executors/quickjs-child.js +0 -1
  51. package/dist/executors/quickjs-protocol.d.ts +0 -1
  52. package/dist/executors/quickjs-protocol.js +0 -1
  53. package/dist/executors/quickjs-runtime.d.ts +0 -1
  54. package/dist/executors/quickjs-runtime.js +0 -1
  55. package/dist/executors/quickjs.d.ts +0 -1
  56. package/dist/executors/quickjs.js +2 -1
  57. package/dist/favicon.d.ts +0 -1
  58. package/dist/favicon.js +0 -1
  59. package/dist/index.d.ts +2 -3
  60. package/dist/index.js +24 -7
  61. package/dist/invocation.d.ts +0 -1
  62. package/dist/invocation.js +0 -1
  63. package/dist/json-schema.d.ts +0 -1
  64. package/dist/json-schema.js +0 -1
  65. package/dist/mcp-result.d.ts +0 -1
  66. package/dist/mcp-result.js +0 -1
  67. package/dist/meta-tools.d.ts +0 -1
  68. package/dist/meta-tools.js +0 -1
  69. package/dist/node.d.ts +0 -1
  70. package/dist/node.js +0 -1
  71. package/dist/operator-ui/generated.d.ts +2 -3
  72. package/dist/operator-ui/generated.js +3 -4
  73. package/dist/operator-ui/model.d.ts +10 -1
  74. package/dist/operator-ui/model.js +0 -1
  75. package/dist/operator-ui/view.d.ts +167 -0
  76. package/dist/operator-ui/view.js +270 -0
  77. package/dist/providers/cloudflare.d.ts +0 -1
  78. package/dist/providers/cloudflare.js +261 -299
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -61
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -79
  83. package/dist/providers/notion.d.ts +0 -1
  84. package/dist/providers/notion.js +87 -42
  85. package/dist/providers/stripe.d.ts +13 -1
  86. package/dist/providers/stripe.js +31 -60
  87. package/dist/registry.d.ts +34 -2
  88. package/dist/registry.js +85 -5
  89. package/dist/retired-toolkits.d.ts +0 -1
  90. package/dist/retired-toolkits.js +0 -1
  91. package/dist/routes/access-tokens.d.ts +0 -1
  92. package/dist/routes/access-tokens.js +0 -1
  93. package/dist/routes/activity.d.ts +0 -1
  94. package/dist/routes/activity.js +0 -1
  95. package/dist/routes/credentials.d.ts +0 -1
  96. package/dist/routes/credentials.js +0 -1
  97. package/dist/routes/mcp.d.ts +0 -1
  98. package/dist/routes/mcp.js +0 -1
  99. package/dist/routes/oauth.d.ts +0 -1
  100. package/dist/routes/oauth.js +0 -1
  101. package/dist/routes/shared.d.ts +2 -1
  102. package/dist/routes/shared.js +0 -1
  103. package/dist/routes/ui.d.ts +0 -1
  104. package/dist/routes/ui.js +0 -1
  105. package/dist/server.d.ts +0 -1
  106. package/dist/server.js +11 -1
  107. package/dist/skills.d.ts +0 -1
  108. package/dist/skills.js +0 -1
  109. package/dist/storage/file.d.ts +0 -1
  110. package/dist/storage/file.js +0 -1
  111. package/dist/storage/memory.d.ts +0 -1
  112. package/dist/storage/memory.js +0 -1
  113. package/dist/timeout.d.ts +0 -1
  114. package/dist/timeout.js +0 -1
  115. package/dist/tool-safety.d.ts +0 -1
  116. package/dist/tool-safety.js +0 -1
  117. package/dist/types.d.ts +45 -1
  118. package/dist/types.js +0 -1
  119. package/dist/ui.d.ts +0 -1
  120. package/dist/ui.js +13 -137
  121. package/dist/validate.d.ts +13 -15
  122. package/dist/validate.js +24 -11
  123. package/dist/version.d.ts +1 -2
  124. package/dist/version.js +1 -2
  125. package/documentation/architecture.md +186 -5
  126. package/documentation/auth.md +11 -0
  127. package/documentation/call-admission.md +142 -6
  128. package/documentation/cloudflare.md +92 -10
  129. package/documentation/code-first-exploration.md +2 -2
  130. package/documentation/code-mode.md +14 -14
  131. package/documentation/connector-guides.md +172 -5
  132. package/documentation/connectors.md +89 -0
  133. package/documentation/linear.md +20 -2
  134. package/documentation/mcp-ui-design.md +2 -2
  135. package/documentation/meta-tools.md +7 -1
  136. package/documentation/mixpanel.md +42 -9
  137. package/documentation/notion.md +23 -2
  138. package/documentation/operations.md +325 -5
  139. package/documentation/operator-ui.md +120 -5
  140. package/documentation/provider-audit.md +160 -0
  141. package/documentation/provider-conventions.md +667 -0
  142. package/documentation/request-admission.md +167 -5
  143. package/documentation/rich-output-design.md +1 -2
  144. package/documentation/stripe.md +15 -0
  145. package/documentation/upgrading.md +454 -0
  146. package/ethos.md +14 -9
  147. package/examples/worker/README.md +102 -12
  148. package/examples/worker/src/index.ts +46 -1
  149. package/examples/worker/wrangler.jsonc +14 -1
  150. package/package.json +10 -6
  151. package/templates/node/.dockerignore +10 -0
  152. package/templates/node/.env.example +40 -3
  153. package/templates/node/AGENTS.md +16 -1
  154. package/templates/node/Dockerfile +49 -0
  155. package/templates/node/README.md +117 -2
  156. package/templates/node/docker-compose.yml +39 -0
  157. package/templates/node/package.json +1 -1
  158. package/templates/node/src/file-activity.ts +182 -0
  159. package/templates/node/src/index.ts +80 -4
  160. package/assets/connecta-clay-hero.png +0 -0
  161. package/dist/access-tokens.d.ts.map +0 -1
  162. package/dist/access-tokens.js.map +0 -1
  163. package/dist/activity.d.ts.map +0 -1
  164. package/dist/activity.js.map +0 -1
  165. package/dist/apps-shell.d.ts.map +0 -1
  166. package/dist/apps-shell.js.map +0 -1
  167. package/dist/auth/bearer.d.ts.map +0 -1
  168. package/dist/auth/bearer.js.map +0 -1
  169. package/dist/auth/clerk.d.ts.map +0 -1
  170. package/dist/auth/clerk.js.map +0 -1
  171. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  172. package/dist/auth/downstream-oauth.js.map +0 -1
  173. package/dist/call-admission.d.ts.map +0 -1
  174. package/dist/call-admission.js.map +0 -1
  175. package/dist/catalog-fingerprint.d.ts.map +0 -1
  176. package/dist/catalog-fingerprint.js.map +0 -1
  177. package/dist/catalog-limits.d.ts.map +0 -1
  178. package/dist/catalog-limits.js.map +0 -1
  179. package/dist/catalog-service.d.ts.map +0 -1
  180. package/dist/catalog-service.js.map +0 -1
  181. package/dist/catalog.d.ts.map +0 -1
  182. package/dist/catalog.js.map +0 -1
  183. package/dist/concurrency.d.ts.map +0 -1
  184. package/dist/concurrency.js.map +0 -1
  185. package/dist/connector-scope.d.ts.map +0 -1
  186. package/dist/connector-scope.js.map +0 -1
  187. package/dist/connectors/api.d.ts.map +0 -1
  188. package/dist/connectors/api.js.map +0 -1
  189. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  190. package/dist/connectors/remote-mcp.js.map +0 -1
  191. package/dist/credentials.d.ts.map +0 -1
  192. package/dist/credentials.js.map +0 -1
  193. package/dist/errors.d.ts.map +0 -1
  194. package/dist/errors.js.map +0 -1
  195. package/dist/execute.d.ts.map +0 -1
  196. package/dist/execute.js.map +0 -1
  197. package/dist/executor-admission.d.ts.map +0 -1
  198. package/dist/executor-admission.js.map +0 -1
  199. package/dist/executor-result.d.ts.map +0 -1
  200. package/dist/executor-result.js.map +0 -1
  201. package/dist/executors/quickjs-child.d.ts.map +0 -1
  202. package/dist/executors/quickjs-child.js.map +0 -1
  203. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  204. package/dist/executors/quickjs-protocol.js.map +0 -1
  205. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  206. package/dist/executors/quickjs-runtime.js.map +0 -1
  207. package/dist/executors/quickjs.d.ts.map +0 -1
  208. package/dist/executors/quickjs.js.map +0 -1
  209. package/dist/favicon.d.ts.map +0 -1
  210. package/dist/favicon.js.map +0 -1
  211. package/dist/index.d.ts.map +0 -1
  212. package/dist/index.js.map +0 -1
  213. package/dist/invocation.d.ts.map +0 -1
  214. package/dist/invocation.js.map +0 -1
  215. package/dist/json-schema.d.ts.map +0 -1
  216. package/dist/json-schema.js.map +0 -1
  217. package/dist/mcp-result.d.ts.map +0 -1
  218. package/dist/mcp-result.js.map +0 -1
  219. package/dist/meta-tools.d.ts.map +0 -1
  220. package/dist/meta-tools.js.map +0 -1
  221. package/dist/node.d.ts.map +0 -1
  222. package/dist/node.js.map +0 -1
  223. package/dist/operator-ui/generated.d.ts.map +0 -1
  224. package/dist/operator-ui/generated.js.map +0 -1
  225. package/dist/operator-ui/model.d.ts.map +0 -1
  226. package/dist/operator-ui/model.js.map +0 -1
  227. package/dist/providers/cloudflare.d.ts.map +0 -1
  228. package/dist/providers/cloudflare.js.map +0 -1
  229. package/dist/providers/linear.d.ts.map +0 -1
  230. package/dist/providers/linear.js.map +0 -1
  231. package/dist/providers/mixpanel.d.ts.map +0 -1
  232. package/dist/providers/mixpanel.js.map +0 -1
  233. package/dist/providers/notion.d.ts.map +0 -1
  234. package/dist/providers/notion.js.map +0 -1
  235. package/dist/providers/stripe.d.ts.map +0 -1
  236. package/dist/providers/stripe.js.map +0 -1
  237. package/dist/registry.d.ts.map +0 -1
  238. package/dist/registry.js.map +0 -1
  239. package/dist/retired-toolkits.d.ts.map +0 -1
  240. package/dist/retired-toolkits.js.map +0 -1
  241. package/dist/routes/access-tokens.d.ts.map +0 -1
  242. package/dist/routes/access-tokens.js.map +0 -1
  243. package/dist/routes/activity.d.ts.map +0 -1
  244. package/dist/routes/activity.js.map +0 -1
  245. package/dist/routes/credentials.d.ts.map +0 -1
  246. package/dist/routes/credentials.js.map +0 -1
  247. package/dist/routes/mcp.d.ts.map +0 -1
  248. package/dist/routes/mcp.js.map +0 -1
  249. package/dist/routes/oauth.d.ts.map +0 -1
  250. package/dist/routes/oauth.js.map +0 -1
  251. package/dist/routes/shared.d.ts.map +0 -1
  252. package/dist/routes/shared.js.map +0 -1
  253. package/dist/routes/ui.d.ts.map +0 -1
  254. package/dist/routes/ui.js.map +0 -1
  255. package/dist/server.d.ts.map +0 -1
  256. package/dist/server.js.map +0 -1
  257. package/dist/skills.d.ts.map +0 -1
  258. package/dist/skills.js.map +0 -1
  259. package/dist/storage/file.d.ts.map +0 -1
  260. package/dist/storage/file.js.map +0 -1
  261. package/dist/storage/memory.d.ts.map +0 -1
  262. package/dist/storage/memory.js.map +0 -1
  263. package/dist/timeout.d.ts.map +0 -1
  264. package/dist/timeout.js.map +0 -1
  265. package/dist/tool-safety.d.ts.map +0 -1
  266. package/dist/tool-safety.js.map +0 -1
  267. package/dist/types.d.ts.map +0 -1
  268. package/dist/types.js.map +0 -1
  269. package/dist/ui.d.ts.map +0 -1
  270. package/dist/ui.js.map +0 -1
  271. package/dist/validate.d.ts.map +0 -1
  272. package/dist/validate.js.map +0 -1
  273. package/dist/version.d.ts.map +0 -1
  274. package/dist/version.js.map +0 -1
  275. package/examples/node/README.md +0 -52
  276. package/examples/node/src/index.ts +0 -71
  277. package/src/access-tokens.ts +0 -289
  278. package/src/activity.ts +0 -234
  279. package/src/apps-shell.ts +0 -370
  280. package/src/auth/bearer.ts +0 -61
  281. package/src/auth/clerk.ts +0 -553
  282. package/src/auth/downstream-oauth.ts +0 -575
  283. package/src/call-admission.ts +0 -521
  284. package/src/catalog-fingerprint.ts +0 -52
  285. package/src/catalog-limits.ts +0 -11
  286. package/src/catalog-service.ts +0 -1153
  287. package/src/catalog.ts +0 -790
  288. package/src/concurrency.ts +0 -39
  289. package/src/connector-scope.ts +0 -66
  290. package/src/connectors/api.ts +0 -159
  291. package/src/connectors/remote-mcp.ts +0 -1090
  292. package/src/credentials.ts +0 -428
  293. package/src/errors.ts +0 -364
  294. package/src/execute.ts +0 -1406
  295. package/src/executor-admission.ts +0 -328
  296. package/src/executor-result.ts +0 -107
  297. package/src/executors/quickjs-child.ts +0 -172
  298. package/src/executors/quickjs-protocol.ts +0 -99
  299. package/src/executors/quickjs-runtime.ts +0 -470
  300. package/src/executors/quickjs.ts +0 -776
  301. package/src/favicon.ts +0 -53
  302. package/src/index.ts +0 -666
  303. package/src/invocation.ts +0 -655
  304. package/src/json-schema.ts +0 -11
  305. package/src/mcp-result.ts +0 -43
  306. package/src/meta-tools.ts +0 -1607
  307. package/src/node.ts +0 -206
  308. package/src/operator-ui/browser.css +0 -423
  309. package/src/operator-ui/browser.ts +0 -1281
  310. package/src/operator-ui/generated.ts +0 -4
  311. package/src/operator-ui/model.ts +0 -111
  312. package/src/providers/cloudflare.ts +0 -3999
  313. package/src/providers/linear.ts +0 -302
  314. package/src/providers/mixpanel.ts +0 -229
  315. package/src/providers/notion.ts +0 -1879
  316. package/src/providers/stripe.ts +0 -307
  317. package/src/registry.ts +0 -1085
  318. package/src/retired-toolkits.ts +0 -14
  319. package/src/routes/access-tokens.ts +0 -115
  320. package/src/routes/activity.ts +0 -215
  321. package/src/routes/credentials.ts +0 -254
  322. package/src/routes/mcp.ts +0 -429
  323. package/src/routes/oauth.ts +0 -322
  324. package/src/routes/shared.ts +0 -247
  325. package/src/routes/ui.ts +0 -152
  326. package/src/server.ts +0 -195
  327. package/src/skills.ts +0 -271
  328. package/src/storage/file.ts +0 -123
  329. package/src/storage/memory.ts +0 -44
  330. package/src/timeout.ts +0 -84
  331. package/src/tool-safety.ts +0 -15
  332. package/src/types.ts +0 -523
  333. package/src/ui.ts +0 -744
  334. package/src/validate.ts +0 -387
  335. package/src/version.ts +0 -7
@@ -1,1153 +0,0 @@
1
- import {
2
- compactDiscoverySchema,
3
- compactSchema,
4
- lexicalCorpusStatistics,
5
- lexicalQueryTerms,
6
- lexicalSearchQuery,
7
- rankTools,
8
- schemaObjectKeys,
9
- summarizeDiscoveryDescription,
10
- summarizeDescription,
11
- } from "./catalog.js";
12
- import {
13
- mapSettledWithConcurrency,
14
- resolveDiscoveryConcurrency,
15
- } from "./concurrency.js";
16
- import {
17
- boundedEchoText,
18
- classifyCallError,
19
- framingError,
20
- } from "./errors.js";
21
- import type { CallErrorDetails } from "./errors.js";
22
- import type {
23
- ConnectorOperationOptions,
24
- RegistryView,
25
- } from "./registry.js";
26
- import {
27
- connectorGuide,
28
- connectorGuideRequired,
29
- connectorGuideSummary,
30
- connectorSkillName,
31
- } from "./skills.js";
32
- import {
33
- DEFAULT_PROBE_TIMEOUT_MS,
34
- normalizeTimeoutMs,
35
- withAbortableTimeout,
36
- } from "./timeout.js";
37
- import { isExplicitlyReadOnly } from "./tool-safety.js";
38
- import type {
39
- Connector,
40
- JsonSchema,
41
- ToolDef,
42
- } from "./types.js";
43
-
44
- export const DEFAULT_SEARCH_LIMIT = 8;
45
- export const MAX_SEARCH_LIMIT = 100;
46
- export const MAX_DESCRIBE_ADDRESSES = 100;
47
- export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
48
- const MAX_QUERY_TERMS = 8;
49
- const MAX_QUERY_TERM_LENGTH = 64;
50
-
51
- const encoder = new TextEncoder();
52
-
53
- /** Clip one echoed query term without splitting a non-BMP code point. */
54
- function boundedQueryTerm(term: string): {
55
- text: string;
56
- truncated: boolean;
57
- } {
58
- const characters: string[] = [];
59
- for (const character of term) {
60
- characters.push(character);
61
- if (characters.length > MAX_QUERY_TERM_LENGTH) {
62
- return {
63
- text: `${characters.slice(0, MAX_QUERY_TERM_LENGTH - 1).join("")}…`,
64
- truncated: true,
65
- };
66
- }
67
- }
68
- return { text: characters.join(""), truncated: false };
69
- }
70
-
71
- /**
72
- * The discovery route a routing failure should send a caller back through. Same
73
- * catalog logic serves both the top-level `search_tools` path and the
74
- * in-program `connecta.search` path, so callers pass the route they own.
75
- */
76
- export type SearchRoute = "search_tools" | "connecta.search";
77
-
78
- export class DiscoveryPolicyError extends Error {
79
- constructor(
80
- readonly code: "invalid_args" | "result_too_large",
81
- message: string,
82
- ) {
83
- super(message);
84
- this.name = "DiscoveryPolicyError";
85
- }
86
- }
87
-
88
- /** Validate before ranking so a huge page request does no proportional work. */
89
- function discoverySearchLimit(value: unknown): number {
90
- if (value === undefined) return DEFAULT_SEARCH_LIMIT;
91
- if (
92
- typeof value !== "number" ||
93
- !Number.isInteger(value) ||
94
- value < 1 ||
95
- value > MAX_SEARCH_LIMIT
96
- ) {
97
- throw new DiscoveryPolicyError(
98
- "invalid_args",
99
- `limit must be a whole number from 1 through ${MAX_SEARCH_LIMIT}. Page through larger catalogs with offset.`,
100
- );
101
- }
102
- return value;
103
- }
104
-
105
- /** Normalize the single-address convenience form, then validate the bounded list. */
106
- function discoveryAddresses(args: CatalogDescribeArgs): unknown[] {
107
- if (args.address !== undefined && args.addresses !== undefined) {
108
- throw new DiscoveryPolicyError(
109
- "invalid_args",
110
- "describe takes either address or addresses, not both.",
111
- );
112
- }
113
- const value =
114
- args.address !== undefined
115
- ? typeof args.address === "string"
116
- ? [args.address]
117
- : undefined
118
- : args.addresses;
119
- if (!Array.isArray(value)) {
120
- throw new DiscoveryPolicyError(
121
- "invalid_args",
122
- 'describe takes { address: "<connectorId>.<toolName>" } or { addresses: ["<connectorId>.<toolName>", ...] }.',
123
- );
124
- }
125
- if (value.length > MAX_DESCRIBE_ADDRESSES) {
126
- throw new DiscoveryPolicyError(
127
- "invalid_args",
128
- `addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across connecta.describe calls.`,
129
- );
130
- }
131
- return value;
132
- }
133
-
134
- /**
135
- * Search terms derived from an address the catalog could not resolve. Bounded
136
- * because the address is entirely caller-authored: an invented one can be any
137
- * length, and this string is copied into a recovery record that is itself
138
- * copied into both halves of the result envelope.
139
- */
140
- function recoveryQuery(address: string): string {
141
- const separator = address.indexOf(".");
142
- const candidate = separator >= 0 ? address.slice(separator + 1) : address;
143
- return boundedEchoText(
144
- candidate.replaceAll(/[._-]+/g, " ").trim() || address,
145
- );
146
- }
147
-
148
- /** Serialize once and count the exact bytes the MCP adapter would emit. */
149
- export function boundedDiscoveryText(value: unknown, hint: string): string {
150
- const text = JSON.stringify(value);
151
- if (text === undefined) {
152
- throw new TypeError("Discovery result is not JSON-serializable.");
153
- }
154
- const bytes = encoder.encode(text).length;
155
- if (bytes > MAX_DISCOVERY_RESULT_BYTES) {
156
- throw new DiscoveryPolicyError(
157
- "result_too_large",
158
- `Discovery result is ${bytes} UTF-8 bytes, over the ${MAX_DISCOVERY_RESULT_BYTES}-byte ceiling. ${hint}`,
159
- );
160
- }
161
- return text;
162
- }
163
-
164
- export interface CatalogSearchArgs {
165
- query?: string;
166
- connector?: string;
167
- /**
168
- * Result classification only; never changes which tools exist or what may
169
- * execute. Omitted and "all" preserve the complete configured catalog.
170
- */
171
- safety?: "readOnly" | "approvalRequired" | "all";
172
- limit?: number;
173
- offset?: number;
174
- fullDescriptions?: boolean;
175
- includeSchemas?: "compact" | "json";
176
- /** Code-mode helper metadata; never exposed by the public search_tools schema. */
177
- includeSchemaKeys?: boolean;
178
- }
179
-
180
- function discoverySafety(
181
- value: unknown,
182
- ): "readOnly" | "approvalRequired" | "all" {
183
- if (value === undefined) return "all";
184
- if (
185
- value !== "readOnly" &&
186
- value !== "approvalRequired" &&
187
- value !== "all"
188
- ) {
189
- throw new DiscoveryPolicyError(
190
- "invalid_args",
191
- 'safety must be "readOnly", "approvalRequired", or "all".',
192
- );
193
- }
194
- return value;
195
- }
196
-
197
- function toolsForSafety(
198
- tools: ToolDef[],
199
- safety: "readOnly" | "approvalRequired" | "all",
200
- ): ToolDef[] {
201
- if (safety === "all") return tools;
202
- return tools.filter((tool) =>
203
- safety === "readOnly"
204
- ? isExplicitlyReadOnly(tool)
205
- : !isExplicitlyReadOnly(tool),
206
- );
207
- }
208
-
209
- export interface CatalogDescribeArgs {
210
- address?: unknown;
211
- addresses?: unknown;
212
- format?: "compact" | "json";
213
- fullDescriptions?: boolean;
214
- }
215
-
216
- interface CatalogSearchEntry {
217
- connector: Connector;
218
- guide?: string;
219
- guideSummary?: string;
220
- tool: {
221
- name: string;
222
- address: string;
223
- description?: string;
224
- inputSchema?: unknown;
225
- outputSchema?: unknown;
226
- inputSchemaTruncated?: true;
227
- outputSchemaTruncated?: true;
228
- inputKeys?: string[];
229
- requiredInputKeys?: string[];
230
- outputKeys?: string[];
231
- annotations?: ToolDef["annotations"];
232
- guideRequired?: true;
233
- guideRequiredReasons?: GuideRequiredReason[];
234
- };
235
- }
236
-
237
- type GuideRequiredReason =
238
- | "connector_required"
239
- | "approval_required"
240
- | "schema_truncated";
241
-
242
- /**
243
- * Reasons discovery can determine without reading arguments or guessing at a
244
- * task. Summary-only conventions remain an agent decision; hard requirements
245
- * are explicit and machine-readable.
246
- */
247
- function guideRequiredReasons(
248
- connector: Connector,
249
- tool: ToolDef,
250
- schemaTruncated: boolean,
251
- ): GuideRequiredReason[] | undefined {
252
- if (!connectorGuide(connector)) return undefined;
253
- const reasons: GuideRequiredReason[] = [];
254
- if (connectorGuideRequired(connector)) reasons.push("connector_required");
255
- if (!isExplicitlyReadOnly(tool)) reasons.push("approval_required");
256
- if (schemaTruncated) reasons.push("schema_truncated");
257
- return reasons.length > 0 ? reasons : undefined;
258
- }
259
-
260
- /**
261
- * Code-mode key metadata for one match. Each half is omitted when its schema
262
- * does not resolve to an object shape, so a program reads "no metadata, use the
263
- * rendered schema" rather than "this tool has no fields".
264
- */
265
- function schemaKeyMetadata(
266
- input: JsonSchema,
267
- output: JsonSchema | undefined,
268
- ): {
269
- inputKeys?: string[];
270
- requiredInputKeys?: string[];
271
- outputKeys?: string[];
272
- } {
273
- const inputKeys = schemaObjectKeys(input);
274
- const outputKeys = schemaObjectKeys(output);
275
- return {
276
- ...(inputKeys
277
- ? {
278
- inputKeys: inputKeys.properties,
279
- requiredInputKeys: inputKeys.required,
280
- }
281
- : {}),
282
- ...(outputKeys && outputKeys.properties.length > 0
283
- ? { outputKeys: outputKeys.properties }
284
- : {}),
285
- };
286
- }
287
-
288
- /**
289
- * The classified-failure subset a scoped search may echo: enough to tell a
290
- * transient outage from one an operator must clear, and nothing more. Kept as
291
- * its own type rather than `CallErrorDetails` so widening the call-path
292
- * classifier cannot widen this discovery-surface field by accident.
293
- */
294
- interface CatalogFailureDetail {
295
- code: string;
296
- message: string;
297
- retryable: boolean;
298
- retryAfterMs?: number;
299
- }
300
-
301
- export interface CatalogSearchPage {
302
- entries: CatalogSearchEntry[];
303
- total: number;
304
- offset: number;
305
- limit: number;
306
- hasMore: boolean;
307
- nextOffset?: number;
308
- matchMode?: "partial";
309
- queryAnalysis?: {
310
- representedTerms: string[];
311
- otherResultTerms: string[];
312
- unmatchedTerms: string[];
313
- truncated?: true;
314
- connectorScope?: string;
315
- unknownConnector?: true;
316
- unavailableConnectorCount?: number;
317
- /** Bounded typed failure for an explicitly scoped unavailable catalog. */
318
- catalogError?: CatalogFailureDetail;
319
- guide?: string;
320
- guideSummary?: string;
321
- guideRequired?: true;
322
- guideRequiredReasons?: GuideRequiredReason[];
323
- guidance?: string;
324
- };
325
- }
326
-
327
- export interface CatalogDescription {
328
- address: string;
329
- name?: string;
330
- description?: string;
331
- guide?: string;
332
- guideSummary?: string;
333
- guideRequired?: true;
334
- guideRequiredReasons?: GuideRequiredReason[];
335
- inputSchema?: unknown;
336
- outputSchema?: unknown;
337
- annotations?: ToolDef["annotations"];
338
- error?: string;
339
- }
340
-
341
- export interface ResolvedCatalogTool {
342
- connector: Connector;
343
- toolName: string;
344
- definition: ToolDef;
345
- }
346
-
347
- export type CatalogResolution =
348
- | {
349
- ok: true;
350
- resolved: ResolvedCatalogTool;
351
- catalogMs: number;
352
- }
353
- | {
354
- ok: false;
355
- error: CallErrorDetails;
356
- catalogMs: number;
357
- connector?: Connector;
358
- toolName?: string;
359
- cause?: unknown;
360
- };
361
-
362
- function renderSchema(schema: JsonSchema, format: "compact" | "json"): unknown {
363
- return format === "json" ? schema : compactSchema(schema);
364
- }
365
-
366
- function renderSearchSchema(
367
- schema: JsonSchema,
368
- format: "compact" | "json",
369
- ): { schema: unknown; truncated: boolean } {
370
- if (format === "json") return { schema, truncated: false };
371
- const compact = compactDiscoverySchema(schema);
372
- return { schema: compact.text, truncated: compact.truncated };
373
- }
374
-
375
- /**
376
- * Request-local catalog operations shared by MCP meta-tools and code mode.
377
- * Successful catalogs may be reused inside this request; failures are not
378
- * retained, and the service itself never escapes its request adapter.
379
- */
380
- export class CatalogService {
381
- readonly requestScope: object;
382
- private readonly probeTimeoutMs: number;
383
- private readonly concurrency: number;
384
- private readonly searchRoute: SearchRoute;
385
- private readonly loaded = new Map<string, ToolDef[]>();
386
- private readonly loading = new Map<string, Promise<ToolDef[]>>();
387
-
388
- constructor(
389
- private readonly registry: RegistryView,
390
- readonly baseUrl: string,
391
- options: {
392
- requestScope?: object;
393
- probeTimeoutMs?: number;
394
- concurrency?: number;
395
- /** The discovery route recovery records name. Default `search_tools`. */
396
- searchRoute?: SearchRoute;
397
- } = {},
398
- ) {
399
- this.requestScope = options.requestScope ?? {};
400
- this.probeTimeoutMs =
401
- normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
402
- this.concurrency = resolveDiscoveryConcurrency(options.concurrency);
403
- this.searchRoute = options.searchRoute ?? "search_tools";
404
- }
405
-
406
- /**
407
- * Send a caller back to discovery through the surface it can actually reach.
408
- * Both variants carry the same scoping arguments because `connecta.search`
409
- * takes the same ones `search_tools` does; only the key naming the callable
410
- * differs, the way the ambiguous-alias record already names a function.
411
- *
412
- * Not private: `InvocationService` builds the same class of record when a
413
- * call fails schema validation, and it is this catalog's route that decides
414
- * which key that record carries. Duplicating the branch there would let the
415
- * two drift.
416
- */
417
- searchRecovery(
418
- args: { query: string; connector?: string },
419
- purpose: string,
420
- ): NonNullable<CallErrorDetails["nextAction"]> {
421
- const searchArgs = {
422
- query: args.query,
423
- ...(args.connector !== undefined ? { connector: args.connector } : {}),
424
- includeSchemas: "compact" as const,
425
- };
426
- return this.searchRoute === "connecta.search"
427
- ? { function: "connecta.search", arguments: searchArgs, purpose }
428
- : { tool: "search_tools", arguments: searchArgs, purpose };
429
- }
430
-
431
- async loadConnector(
432
- id: string,
433
- callOptions: ConnectorOperationOptions = {},
434
- ): Promise<ToolDef[]> {
435
- const cached = this.loaded.get(id);
436
- if (cached) return cached;
437
- const inFlight = this.loading.get(id);
438
- if (inFlight) return inFlight;
439
- const loading = this.registry
440
- .getTools(id, this.baseUrl, this.requestScope, callOptions)
441
- .then((tools) => {
442
- this.loaded.set(id, tools);
443
- return tools;
444
- })
445
- .finally(() => {
446
- if (this.loading.get(id) === loading) this.loading.delete(id);
447
- });
448
- this.loading.set(id, loading);
449
- return loading;
450
- }
451
-
452
- private loadForDiscovery(id: string, label: string): Promise<ToolDef[]> {
453
- return withAbortableTimeout(
454
- (signal) =>
455
- this.loadConnector(id, {
456
- signal,
457
- timeoutMs: this.probeTimeoutMs,
458
- }),
459
- this.probeTimeoutMs,
460
- label,
461
- );
462
- }
463
-
464
- async resolveTool(
465
- address: string,
466
- callOptions: ConnectorOperationOptions = {},
467
- ): Promise<CatalogResolution> {
468
- const resolved = this.registry.resolveAddress(address);
469
- if (!resolved) {
470
- return {
471
- ok: false,
472
- error: {
473
- ...framingError(
474
- "unknown_address",
475
- `Unknown address "${boundedEchoText(address)}"`,
476
- ),
477
- nextAction: this.searchRecovery(
478
- { query: recoveryQuery(address) },
479
- "Find the configured canonical address before retrying.",
480
- ),
481
- },
482
- catalogMs: 0,
483
- };
484
- }
485
- const started = Date.now();
486
- let tools: ToolDef[];
487
- try {
488
- tools = await this.loadConnector(resolved.connector.id, callOptions);
489
- } catch (cause) {
490
- return {
491
- ok: false,
492
- error: classifyCallError(cause, "catalog_lookup_failed"),
493
- catalogMs: Date.now() - started,
494
- connector: resolved.connector,
495
- toolName: resolved.toolName,
496
- cause,
497
- };
498
- }
499
- const definition = tools.find((tool) => tool.name === resolved.toolName);
500
- if (!definition) {
501
- return {
502
- ok: false,
503
- error: {
504
- ...framingError(
505
- "unknown_tool",
506
- `Unknown tool "${boundedEchoText(resolved.toolName)}" on connector "${resolved.connector.id}"`,
507
- ),
508
- nextAction: this.searchRecovery(
509
- {
510
- query: recoveryQuery(resolved.toolName),
511
- connector: resolved.connector.id,
512
- },
513
- "Find the connector's current canonical tool address.",
514
- ),
515
- },
516
- catalogMs: Date.now() - started,
517
- connector: resolved.connector,
518
- toolName: resolved.toolName,
519
- };
520
- }
521
- return {
522
- ok: true,
523
- resolved: {
524
- connector: resolved.connector,
525
- toolName: resolved.toolName,
526
- definition,
527
- },
528
- catalogMs: Date.now() - started,
529
- };
530
- }
531
-
532
- /**
533
- * Resolve the JavaScript-safe property used by a lazy code-mode namespace
534
- * back to exactly one catalog tool. Ambiguous aliases fail with an explicit
535
- * escape hatch instead of silently choosing the first tool.
536
- */
537
- async resolveToolAlias(
538
- connectorId: string,
539
- alias: string,
540
- aliasFor: (toolName: string) => string,
541
- callOptions: ConnectorOperationOptions = {},
542
- ): Promise<CatalogResolution> {
543
- const connector = this.registry.getConnector(connectorId);
544
- if (!connector) {
545
- return {
546
- ok: false,
547
- error: {
548
- ...framingError(
549
- "unknown_address",
550
- `Unknown address "${boundedEchoText(`${connectorId}.${alias}`)}"`,
551
- ),
552
- nextAction: this.searchRecovery(
553
- { query: recoveryQuery(alias) },
554
- "Find the configured canonical address before retrying.",
555
- ),
556
- },
557
- catalogMs: 0,
558
- };
559
- }
560
- const started = Date.now();
561
- let tools: ToolDef[];
562
- try {
563
- tools = await this.loadConnector(connector.id, callOptions);
564
- } catch (cause) {
565
- return {
566
- ok: false,
567
- error: classifyCallError(cause, "catalog_lookup_failed"),
568
- catalogMs: Date.now() - started,
569
- connector,
570
- toolName: alias,
571
- cause,
572
- };
573
- }
574
- const [definition, ...collisions] = tools.filter(
575
- (tool) => aliasFor(tool.name) === alias,
576
- );
577
- if (!definition) {
578
- return {
579
- ok: false,
580
- error: {
581
- ...framingError(
582
- "unknown_tool",
583
- `Unknown tool "${boundedEchoText(alias)}" on connector "${connector.id}"`,
584
- ),
585
- nextAction: this.searchRecovery(
586
- { query: recoveryQuery(alias), connector: connector.id },
587
- "Find the connector's current canonical tool address.",
588
- ),
589
- },
590
- catalogMs: Date.now() - started,
591
- connector,
592
- toolName: alias,
593
- };
594
- }
595
- if (collisions.length > 0) {
596
- const names = [definition, ...collisions]
597
- .map((tool) => `"${tool.name}"`)
598
- .join(", ");
599
- return {
600
- ok: false,
601
- error: {
602
- code: "ambiguous_tool_alias",
603
- message: `Tool alias "${boundedEchoText(alias)}" is ambiguous on connector "${connector.id}" because ${names} sanitize to the same name. Use connecta.call with an exact address.`,
604
- retryable: false,
605
- nextAction: {
606
- function: "connecta.call",
607
- addresses: [definition, ...collisions].map(
608
- (tool) => `${connector.id}.${tool.name}`,
609
- ),
610
- purpose:
611
- "Choose the intended canonical address and call it with the original arguments.",
612
- },
613
- },
614
- catalogMs: Date.now() - started,
615
- connector,
616
- toolName: alias,
617
- };
618
- }
619
- return {
620
- ok: true,
621
- resolved: {
622
- connector,
623
- toolName: definition.name,
624
- definition,
625
- },
626
- catalogMs: Date.now() - started,
627
- };
628
- }
629
-
630
- async search(args: CatalogSearchArgs): Promise<CatalogSearchPage> {
631
- const query = args.query ?? "";
632
- const retrievalQuery = lexicalSearchQuery(query);
633
- const safety = discoverySafety(args.safety);
634
- const limit = discoverySearchLimit(args.limit);
635
- const offset = Math.max(0, Math.trunc(args.offset ?? 0));
636
- const scopedConnector = args.connector
637
- ? this.registry.getConnector(args.connector)
638
- : undefined;
639
- const connectors = args.connector
640
- ? scopedConnector
641
- ? [scopedConnector]
642
- : []
643
- : this.registry.listConnectors();
644
- const catalogs = await mapSettledWithConcurrency(
645
- connectors,
646
- this.concurrency,
647
- (connector) =>
648
- // Unlike the describe path, this label never reaches a caller: search
649
- // only counts rejected catalogs (`unavailableCatalogs` below) and
650
- // renders its own guidance, so the folded name here stays internal and
651
- // needs no surface awareness.
652
- this.loadForDiscovery(
653
- connector.id,
654
- `search_tools probe of "${connector.id}"`,
655
- ),
656
- );
657
- const searchableCatalogs = catalogs.map((catalog) =>
658
- catalog.status === "fulfilled"
659
- ? {
660
- status: "fulfilled" as const,
661
- value: toolsForSafety(catalog.value, safety),
662
- }
663
- : catalog,
664
- );
665
- const matches: Array<{
666
- connector: Connector;
667
- tool: ToolDef;
668
- score: number;
669
- order: number;
670
- exactName: boolean;
671
- matchedTermCount: number;
672
- complete: boolean;
673
- }> = [];
674
- let matchMode: "all" | "partial" = "all";
675
- const statistics = lexicalCorpusStatistics(
676
- searchableCatalogs.flatMap((catalog) =>
677
- catalog.status === "fulfilled" ? [catalog.value] : [],
678
- ),
679
- retrievalQuery,
680
- );
681
- const trimmedQuery = query.trim();
682
- const isBrowse = trimmedQuery.length === 0;
683
- const queryTerms = lexicalQueryTerms(retrievalQuery);
684
- const queryTermCount = queryTerms.length;
685
- const unsearchableQuery = !isBrowse && queryTermCount === 0;
686
- const analysisTerms = unsearchableQuery ? [trimmedQuery] : queryTerms;
687
- const analyzedTerms = analysisTerms.slice(0, MAX_QUERY_TERMS);
688
- const displayTerm = (term: string) => boundedQueryTerm(term).text;
689
- const queryMetadataTruncated =
690
- analysisTerms.length > analyzedTerms.length ||
691
- analyzedTerms.some((term) => boundedQueryTerm(term).truncated);
692
- const collectMatches = (mode: "all" | "partial") => {
693
- const collected: typeof matches = [];
694
- let orderBase = 0;
695
- searchableCatalogs.forEach((catalog, connectorIndex) => {
696
- const connector = connectors[connectorIndex];
697
- if (!connector) {
698
- throw new Error("Catalog result has no corresponding connector");
699
- }
700
- if (catalog.status === "fulfilled") {
701
- for (const ranked of rankTools(
702
- catalog.value,
703
- retrievalQuery,
704
- mode,
705
- statistics,
706
- query,
707
- )) {
708
- collected.push({
709
- connector,
710
- tool: ranked.tool,
711
- score: ranked.score,
712
- order: orderBase + ranked.order,
713
- exactName: ranked.exactName,
714
- matchedTermCount: ranked.matchedTermCount,
715
- complete:
716
- isBrowse || ranked.matchedTermCount === queryTermCount,
717
- });
718
- }
719
- }
720
- orderBase +=
721
- catalog.status === "fulfilled" ? catalog.value.length : 1;
722
- });
723
- return collected;
724
- };
725
- // A non-empty query that normalizes to no lexical terms is not a browse.
726
- // Ranking an empty phrase would otherwise return every tool as an
727
- // unrelated zero-score match, with no coverage to explain the result.
728
- const rankedMatches = unsearchableQuery
729
- ? []
730
- : collectMatches(isBrowse ? "all" : "partial");
731
- const completeMatchCount = rankedMatches.filter(
732
- (match) => match.complete,
733
- ).length;
734
- matches.push(
735
- ...rankedMatches.filter(
736
- (match) =>
737
- match.complete ||
738
- completeMatchCount === 0 ||
739
- match.exactName ||
740
- match.matchedTermCount >= 2,
741
- ),
742
- );
743
- if (!isBrowse && !unsearchableQuery && completeMatchCount === 0) {
744
- matchMode = "partial";
745
- }
746
- // Complete matches and exact tool-name phrases share the first rank tier.
747
- // This lets a strong action/object name beat a weak description-only
748
- // decoy. Other partial matches fill the remaining page only after every
749
- // complete match, regardless of a rare-term score spike.
750
- matches.sort((a, b) => {
751
- const aFirstTier = a.complete || a.exactName;
752
- const bFirstTier = b.complete || b.exactName;
753
- return (
754
- Number(bFirstTier) - Number(aFirstTier) ||
755
- b.score - a.score ||
756
- a.order - b.order
757
- );
758
- });
759
- const pageMatches = matches.slice(offset, offset + limit);
760
- const entries = pageMatches.map((match) => {
761
- const input = match.tool.inputSchema ?? { type: "object" };
762
- const renderedInput = args.includeSchemas
763
- ? renderSearchSchema(input, args.includeSchemas)
764
- : undefined;
765
- const renderedOutput =
766
- args.includeSchemas && match.tool.outputSchema
767
- ? renderSearchSchema(match.tool.outputSchema, args.includeSchemas)
768
- : undefined;
769
- const schemaKeys =
770
- args.includeSchemas && args.includeSchemaKeys
771
- ? schemaKeyMetadata(input, match.tool.outputSchema)
772
- : undefined;
773
- const description = summarizeDiscoveryDescription(
774
- match.tool.description,
775
- args.fullDescriptions === true,
776
- );
777
- const requiredReasons = guideRequiredReasons(
778
- match.connector,
779
- match.tool,
780
- renderedInput?.truncated === true || renderedOutput?.truncated === true,
781
- );
782
- const guideSummary = connectorGuideSummary(match.connector);
783
- return {
784
- connector: match.connector,
785
- ...(connectorGuide(match.connector)
786
- ? {
787
- guide: connectorSkillName(match.connector.id),
788
- ...(guideSummary ? { guideSummary } : {}),
789
- }
790
- : {}),
791
- tool: {
792
- name: match.tool.name,
793
- address: `${match.connector.id}.${match.tool.name}`,
794
- ...(description !== undefined ? { description } : {}),
795
- ...(args.includeSchemas
796
- ? {
797
- inputSchema: renderedInput?.schema,
798
- }
799
- : {}),
800
- ...(renderedInput?.truncated
801
- ? { inputSchemaTruncated: true as const }
802
- : {}),
803
- ...(args.includeSchemas && match.tool.outputSchema
804
- ? {
805
- outputSchema: renderedOutput?.schema,
806
- }
807
- : {}),
808
- ...(renderedOutput?.truncated
809
- ? { outputSchemaTruncated: true as const }
810
- : {}),
811
- ...(schemaKeys && !renderedInput?.truncated
812
- ? {
813
- ...(schemaKeys.inputKeys
814
- ? { inputKeys: schemaKeys.inputKeys }
815
- : {}),
816
- ...(schemaKeys.requiredInputKeys
817
- ? { requiredInputKeys: schemaKeys.requiredInputKeys }
818
- : {}),
819
- }
820
- : {}),
821
- ...(schemaKeys?.outputKeys && !renderedOutput?.truncated
822
- ? { outputKeys: schemaKeys.outputKeys }
823
- : {}),
824
- ...(match.tool.annotations
825
- ? { annotations: match.tool.annotations }
826
- : {}),
827
- ...(requiredReasons
828
- ? {
829
- guideRequired: true as const,
830
- guideRequiredReasons: requiredReasons,
831
- }
832
- : {}),
833
- },
834
- };
835
- });
836
- const nextOffset =
837
- offset + entries.length < matches.length
838
- ? offset + entries.length
839
- : undefined;
840
- const pageTools = new Set(pageMatches.map((match) => match.tool));
841
- const matchingTools = (term: string) =>
842
- new Set([
843
- ...(statistics.nameMatches.get(term) ?? []),
844
- ...(statistics.descriptionMatches.get(term) ?? []),
845
- ]);
846
- const representedTerms: string[] = [];
847
- const otherResultTerms: string[] = [];
848
- const unmatchedTerms: string[] = [];
849
- for (const term of analyzedTerms) {
850
- const termTools = matchingTools(term);
851
- if ([...termTools].some((tool) => pageTools.has(tool))) {
852
- representedTerms.push(displayTerm(term));
853
- } else if (termTools.size > 0) {
854
- otherResultTerms.push(displayTerm(term));
855
- } else {
856
- unmatchedTerms.push(displayTerm(term));
857
- }
858
- }
859
- const unavailableCatalogs = catalogs.filter(
860
- (catalog) => catalog.status === "rejected",
861
- ).length;
862
- // Named field by field rather than spread: `CallErrorDetails` also carries
863
- // connector, operation, recovery, and nextAction, and a discovery read is
864
- // not a call — widening the classifier must not silently widen what a
865
- // catalog search hands back.
866
- const scopedCatalogError = ((): CatalogFailureDetail | undefined => {
867
- if (!scopedConnector || catalogs[0]?.status !== "rejected") {
868
- return undefined;
869
- }
870
- const error = classifyCallError(
871
- catalogs[0].reason,
872
- "catalog_lookup_failed",
873
- );
874
- return {
875
- code: error.code,
876
- message: boundedEchoText(error.message),
877
- retryable: error.retryable,
878
- ...(error.retryAfterMs === undefined
879
- ? {}
880
- : { retryAfterMs: error.retryAfterMs }),
881
- };
882
- })();
883
- const safetyLabel =
884
- safety === "readOnly"
885
- ? "read-only "
886
- : safety === "approvalRequired"
887
- ? "approval-required "
888
- : "";
889
- const filterRecovery =
890
- safety === "all" ? "" : " Change safety to inspect the other tools.";
891
- const scopedGuide =
892
- matches.length === 0 && scopedConnector && connectorGuide(scopedConnector)
893
- ? {
894
- guide: connectorSkillName(scopedConnector.id),
895
- guideSummary: connectorGuideSummary(scopedConnector),
896
- required: connectorGuideRequired(scopedConnector),
897
- }
898
- : undefined;
899
- // A scope that resolved to nothing is the same silence one step earlier in
900
- // the lookup: no connector resolved, so no catalog was even attempted, so
901
- // no catalog failed and the unavailable path below never fires. Echo only
902
- // the ID the caller already supplied — naming what else is configured
903
- // would answer a question they did not ask, past a filter they may not
904
- // pass.
905
- const unknownConnectorGuidance =
906
- args.connector && !scopedConnector
907
- ? `Connector "${args.connector}" is not configured in this deployment. Omit connector to search all configured tools.`
908
- : undefined;
909
- // Searchable queries report analysis when the scorer had to degrade. A
910
- // non-empty query with no searchable terms reports the bounded raw input
911
- // as unmatched instead of silently becoming a browse. A real browse has
912
- // no terms to analyse and normally reports none at all. Scope failures are
913
- // the exception, because an empty result alone looks like a connector that
914
- // correctly exposes no tools.
915
- const reportsQueryAnalysis =
916
- unsearchableQuery ||
917
- (queryTerms.length > 0
918
- ? matchMode === "partial"
919
- : unknownConnectorGuidance !== undefined || unavailableCatalogs > 0);
920
- const guidance =
921
- unsearchableQuery
922
- ? (unknownConnectorGuidance ??
923
- (scopedConnector && unavailableCatalogs > 0
924
- ? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
925
- : "The query contained no searchable lexical terms. Use 2–4 ASCII action/object terms, or browse with an empty query."))
926
- : queryTerms.length === 0
927
- ? // A browse has no terms to advise about, so it stays silent unless
928
- // the scope itself failed: the guidance on a scoped miss recommends
929
- // browsing with an empty query, and that advice must not lead into a
930
- // dead end that looks like a connector with no tools.
931
- (unknownConnectorGuidance ??
932
- (unavailableCatalogs === 0
933
- ? undefined
934
- : scopedConnector
935
- ? `Connector "${scopedConnector.id}" could not be browsed because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
936
- : `${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable, so this browse is incomplete. Scope by connector to see the typed reason.`))
937
- : matches.length === 0
938
- ? (unknownConnectorGuidance ??
939
- (scopedConnector
940
- ? unavailableCatalogs > 0
941
- ? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
942
- : scopedGuide?.required
943
- ? `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Fetch queryAnalysis.guide before calling, then refine terms or browse with an empty query.${filterRecovery}`
944
- : `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
945
- : unavailableCatalogs === 0
946
- ? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
947
- : `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`))
948
- : matchMode === "partial"
949
- ? scopedConnector
950
- ? `No single tool on connector "${scopedConnector.id}" matched every term. Split distinct intents into separate searches.`
951
- : unavailableCatalogs === 0
952
- ? "No single tool matched every term. Split distinct intents into separate searches."
953
- : "No single tool matched every term in the catalogs that answered. Split distinct intents into separate searches."
954
- : undefined;
955
- return {
956
- entries,
957
- total: matches.length,
958
- offset,
959
- limit,
960
- hasMore: nextOffset !== undefined,
961
- ...(nextOffset !== undefined ? { nextOffset } : {}),
962
- ...(matchMode === "partial" && matches.length > 0
963
- ? { matchMode }
964
- : {}),
965
- ...(reportsQueryAnalysis
966
- ? {
967
- queryAnalysis: {
968
- representedTerms,
969
- otherResultTerms,
970
- unmatchedTerms,
971
- ...(queryMetadataTruncated
972
- ? { truncated: true as const }
973
- : {}),
974
- ...(args.connector ? { connectorScope: args.connector } : {}),
975
- ...(args.connector && !scopedConnector
976
- ? { unknownConnector: true as const }
977
- : {}),
978
- ...(unavailableCatalogs > 0
979
- ? { unavailableConnectorCount: unavailableCatalogs }
980
- : {}),
981
- ...(scopedCatalogError ? { catalogError: scopedCatalogError } : {}),
982
- ...(scopedGuide
983
- ? {
984
- guide: scopedGuide.guide,
985
- ...(scopedGuide.guideSummary
986
- ? { guideSummary: scopedGuide.guideSummary }
987
- : {}),
988
- ...(scopedGuide.required
989
- ? {
990
- guideRequired: true as const,
991
- guideRequiredReasons: [
992
- "connector_required" as const,
993
- ],
994
- }
995
- : {}),
996
- }
997
- : {}),
998
- ...(guidance ? { guidance } : {}),
999
- },
1000
- }
1001
- : {}),
1002
- };
1003
- }
1004
-
1005
- async describe(args: CatalogDescribeArgs): Promise<CatalogDescription[]> {
1006
- const addresses = discoveryAddresses(args);
1007
- const format = args.format ?? "compact";
1008
- const resolved = addresses.map((rawAddress) => {
1009
- const address = String(rawAddress);
1010
- return { address, resolved: this.registry.resolveAddress(address) };
1011
- });
1012
- const connectorIds = [
1013
- ...new Set(
1014
- resolved
1015
- .map((entry) => entry.resolved?.connector.id)
1016
- .filter((id): id is string => Boolean(id)),
1017
- ),
1018
- ];
1019
- const loaded = await mapSettledWithConcurrency(
1020
- connectorIds,
1021
- this.concurrency,
1022
- (id) =>
1023
- this.loadForDiscovery(id, `connecta.describe probe of "${id}"`),
1024
- );
1025
- const catalogs = new Map<string, ToolDef[] | Error>();
1026
- loaded.forEach((result, index) => {
1027
- const connectorId = connectorIds[index];
1028
- if (connectorId === undefined) {
1029
- throw new Error("Catalog result has no corresponding connector id");
1030
- }
1031
- catalogs.set(
1032
- connectorId,
1033
- result.status === "fulfilled"
1034
- ? result.value
1035
- : result.reason instanceof Error
1036
- ? result.reason
1037
- : new Error(String(result.reason)),
1038
- );
1039
- });
1040
- return resolved.map(({ address, resolved: addressResolution }) => {
1041
- if (!addressResolution) {
1042
- return { address, error: `Unknown address "${address}"` };
1043
- }
1044
- const catalog = catalogs.get(addressResolution.connector.id);
1045
- if (catalog instanceof Error) {
1046
- return { address, error: catalog.message };
1047
- }
1048
- const tool = catalog?.find(
1049
- (item) => item.name === addressResolution.toolName,
1050
- );
1051
- if (!tool) {
1052
- return {
1053
- address,
1054
- error: `Unknown tool "${addressResolution.toolName}" on connector "${addressResolution.connector.id}"`,
1055
- };
1056
- }
1057
- const input = tool.inputSchema ?? { type: "object" };
1058
- const description = summarizeDescription(
1059
- tool.description,
1060
- args.fullDescriptions === true,
1061
- );
1062
- const requiredReasons = guideRequiredReasons(
1063
- addressResolution.connector,
1064
- tool,
1065
- false,
1066
- );
1067
- const guideSummary = connectorGuideSummary(addressResolution.connector);
1068
- return {
1069
- address,
1070
- name: tool.name,
1071
- ...(description !== undefined ? { description } : {}),
1072
- ...(connectorGuide(addressResolution.connector)
1073
- ? {
1074
- guide: connectorSkillName(addressResolution.connector.id),
1075
- ...(guideSummary ? { guideSummary } : {}),
1076
- }
1077
- : {}),
1078
- ...(requiredReasons
1079
- ? {
1080
- guideRequired: true as const,
1081
- guideRequiredReasons: requiredReasons,
1082
- }
1083
- : {}),
1084
- inputSchema: renderSchema(input, format),
1085
- ...(tool.outputSchema
1086
- ? {
1087
- outputSchema: renderSchema(tool.outputSchema, format),
1088
- }
1089
- : {}),
1090
- ...(tool.annotations ? { annotations: tool.annotations } : {}),
1091
- };
1092
- });
1093
- }
1094
- }
1095
-
1096
- export function groupedSearchResult(page: CatalogSearchPage) {
1097
- const groups: Array<{
1098
- id: string;
1099
- title?: string;
1100
- guide?: string;
1101
- guideSummary?: string;
1102
- tools: CatalogSearchEntry["tool"][];
1103
- }> = [];
1104
- const byConnector = new Map<string, (typeof groups)[number]>();
1105
- for (const entry of page.entries) {
1106
- const existing = byConnector.get(entry.connector.id);
1107
- if (existing) {
1108
- existing.tools.push(entry.tool);
1109
- } else {
1110
- const group: (typeof groups)[number] = {
1111
- id: entry.connector.id,
1112
- ...(entry.connector.title ? { title: entry.connector.title } : {}),
1113
- ...(entry.guide ? { guide: entry.guide } : {}),
1114
- ...(entry.guideSummary
1115
- ? { guideSummary: entry.guideSummary }
1116
- : {}),
1117
- tools: [],
1118
- };
1119
- byConnector.set(entry.connector.id, group);
1120
- groups.push(group);
1121
- group.tools.push(entry.tool);
1122
- }
1123
- }
1124
- return {
1125
- connectors: groups,
1126
- total: page.total,
1127
- offset: page.offset,
1128
- limit: page.limit,
1129
- hasMore: page.hasMore,
1130
- ...(page.nextOffset !== undefined ? { nextOffset: page.nextOffset } : {}),
1131
- ...(page.matchMode ? { matchMode: page.matchMode } : {}),
1132
- ...(page.queryAnalysis ? { queryAnalysis: page.queryAnalysis } : {}),
1133
- };
1134
- }
1135
-
1136
- export function flatSearchResult(page: CatalogSearchPage) {
1137
- return {
1138
- tools: page.entries.map((entry) => ({
1139
- ...entry.tool,
1140
- ...(entry.guide ? { guide: entry.guide } : {}),
1141
- ...(entry.guideSummary
1142
- ? { guideSummary: entry.guideSummary }
1143
- : {}),
1144
- })),
1145
- total: page.total,
1146
- offset: page.offset,
1147
- limit: page.limit,
1148
- hasMore: page.hasMore,
1149
- ...(page.nextOffset !== undefined ? { nextOffset: page.nextOffset } : {}),
1150
- ...(page.matchMode ? { matchMode: page.matchMode } : {}),
1151
- ...(page.queryAnalysis ? { queryAnalysis: page.queryAnalysis } : {}),
1152
- };
1153
- }