@zackbart/connecta 0.7.9 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/README.md +47 -148
  3. package/dist/activity.d.ts +0 -8
  4. package/dist/activity.d.ts.map +1 -1
  5. package/dist/activity.js +0 -1
  6. package/dist/activity.js.map +1 -1
  7. package/dist/auth/bearer.d.ts +1 -7
  8. package/dist/auth/bearer.d.ts.map +1 -1
  9. package/dist/auth/bearer.js +3 -24
  10. package/dist/auth/bearer.js.map +1 -1
  11. package/dist/auth/clerk.d.ts +2 -11
  12. package/dist/auth/clerk.d.ts.map +1 -1
  13. package/dist/auth/clerk.js +31 -46
  14. package/dist/auth/clerk.js.map +1 -1
  15. package/dist/call-admission.d.ts.map +1 -1
  16. package/dist/call-admission.js +9 -4
  17. package/dist/call-admission.js.map +1 -1
  18. package/dist/catalog-fingerprint.d.ts +20 -0
  19. package/dist/catalog-fingerprint.d.ts.map +1 -0
  20. package/dist/catalog-fingerprint.js +34 -0
  21. package/dist/catalog-fingerprint.js.map +1 -0
  22. package/dist/catalog-limits.d.ts +10 -0
  23. package/dist/catalog-limits.d.ts.map +1 -0
  24. package/dist/catalog-limits.js +10 -0
  25. package/dist/catalog-limits.js.map +1 -0
  26. package/dist/catalog-service.d.ts +137 -0
  27. package/dist/catalog-service.d.ts.map +1 -0
  28. package/dist/catalog-service.js +401 -0
  29. package/dist/catalog-service.js.map +1 -0
  30. package/dist/catalog.d.ts +6 -0
  31. package/dist/catalog.d.ts.map +1 -1
  32. package/dist/catalog.js +53 -2
  33. package/dist/catalog.js.map +1 -1
  34. package/dist/concurrency.d.ts +4 -0
  35. package/dist/concurrency.d.ts.map +1 -0
  36. package/dist/concurrency.js +31 -0
  37. package/dist/concurrency.js.map +1 -0
  38. package/dist/connectors/api.d.ts.map +1 -1
  39. package/dist/connectors/api.js +22 -12
  40. package/dist/connectors/api.js.map +1 -1
  41. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  42. package/dist/connectors/remote-mcp.js +53 -41
  43. package/dist/connectors/remote-mcp.js.map +1 -1
  44. package/dist/credentials.d.ts +3 -2
  45. package/dist/credentials.d.ts.map +1 -1
  46. package/dist/errors.d.ts +19 -1
  47. package/dist/errors.d.ts.map +1 -1
  48. package/dist/errors.js.map +1 -1
  49. package/dist/execute.d.ts +10 -16
  50. package/dist/execute.d.ts.map +1 -1
  51. package/dist/execute.js +143 -330
  52. package/dist/execute.js.map +1 -1
  53. package/dist/executor-result.d.ts +0 -3
  54. package/dist/executor-result.d.ts.map +1 -1
  55. package/dist/executor-result.js +3 -3
  56. package/dist/executor-result.js.map +1 -1
  57. package/dist/executors/quickjs-child.js +3 -3
  58. package/dist/executors/quickjs-child.js.map +1 -1
  59. package/dist/executors/quickjs-protocol.d.ts +4 -1
  60. package/dist/executors/quickjs-protocol.d.ts.map +1 -1
  61. package/dist/executors/quickjs-runtime.d.ts.map +1 -1
  62. package/dist/executors/quickjs-runtime.js +7 -2
  63. package/dist/executors/quickjs-runtime.js.map +1 -1
  64. package/dist/executors/quickjs.d.ts.map +1 -1
  65. package/dist/executors/quickjs.js +4 -1
  66. package/dist/executors/quickjs.js.map +1 -1
  67. package/dist/favicon.js +2 -2
  68. package/dist/favicon.js.map +1 -1
  69. package/dist/index.d.ts +8 -82
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +58 -104
  72. package/dist/index.js.map +1 -1
  73. package/dist/invocation.d.ts +101 -0
  74. package/dist/invocation.d.ts.map +1 -0
  75. package/dist/invocation.js +405 -0
  76. package/dist/invocation.js.map +1 -0
  77. package/dist/meta-tools.d.ts +9 -95
  78. package/dist/meta-tools.d.ts.map +1 -1
  79. package/dist/meta-tools.js +235 -725
  80. package/dist/meta-tools.js.map +1 -1
  81. package/dist/operator-ui/generated.d.ts +3 -0
  82. package/dist/operator-ui/generated.d.ts.map +1 -0
  83. package/dist/operator-ui/generated.js +5 -0
  84. package/dist/operator-ui/generated.js.map +1 -0
  85. package/dist/operator-ui/model.d.ts +73 -0
  86. package/dist/operator-ui/model.d.ts.map +1 -0
  87. package/dist/operator-ui/model.js +28 -0
  88. package/dist/operator-ui/model.js.map +1 -0
  89. package/dist/registry.d.ts +33 -184
  90. package/dist/registry.d.ts.map +1 -1
  91. package/dist/registry.js +345 -330
  92. package/dist/registry.js.map +1 -1
  93. package/dist/retired-toolkits.d.ts +3 -0
  94. package/dist/retired-toolkits.d.ts.map +1 -0
  95. package/dist/retired-toolkits.js +9 -0
  96. package/dist/retired-toolkits.js.map +1 -0
  97. package/dist/routes/activity.d.ts +3 -0
  98. package/dist/routes/activity.d.ts.map +1 -0
  99. package/dist/routes/activity.js +170 -0
  100. package/dist/routes/activity.js.map +1 -0
  101. package/dist/routes/credentials.d.ts +3 -0
  102. package/dist/routes/credentials.d.ts.map +1 -0
  103. package/dist/routes/credentials.js +170 -0
  104. package/dist/routes/credentials.js.map +1 -0
  105. package/dist/routes/mcp.d.ts +8 -0
  106. package/dist/routes/mcp.d.ts.map +1 -0
  107. package/dist/routes/mcp.js +270 -0
  108. package/dist/routes/mcp.js.map +1 -0
  109. package/dist/routes/oauth.d.ts +4 -0
  110. package/dist/routes/oauth.d.ts.map +1 -0
  111. package/dist/routes/oauth.js +263 -0
  112. package/dist/routes/oauth.js.map +1 -0
  113. package/dist/routes/shared.d.ts +76 -0
  114. package/dist/routes/shared.d.ts.map +1 -0
  115. package/dist/routes/shared.js +140 -0
  116. package/dist/routes/shared.js.map +1 -0
  117. package/dist/routes/ui.d.ts +3 -0
  118. package/dist/routes/ui.d.ts.map +1 -0
  119. package/dist/routes/ui.js +116 -0
  120. package/dist/routes/ui.js.map +1 -0
  121. package/dist/server.d.ts +9 -40
  122. package/dist/server.d.ts.map +1 -1
  123. package/dist/server.js +62 -1322
  124. package/dist/server.js.map +1 -1
  125. package/dist/skills.d.ts +3 -21
  126. package/dist/skills.d.ts.map +1 -1
  127. package/dist/skills.js +15 -43
  128. package/dist/skills.js.map +1 -1
  129. package/dist/storage/file.d.ts.map +1 -1
  130. package/dist/storage/file.js +3 -1
  131. package/dist/storage/file.js.map +1 -1
  132. package/dist/storage/memory.d.ts.map +1 -1
  133. package/dist/storage/memory.js +3 -1
  134. package/dist/storage/memory.js.map +1 -1
  135. package/dist/timeout.d.ts.map +1 -1
  136. package/dist/timeout.js +2 -4
  137. package/dist/timeout.js.map +1 -1
  138. package/dist/types.d.ts +9 -59
  139. package/dist/types.d.ts.map +1 -1
  140. package/dist/ui.d.ts +3 -94
  141. package/dist/ui.d.ts.map +1 -1
  142. package/dist/ui.js +30 -1427
  143. package/dist/ui.js.map +1 -1
  144. package/dist/version.d.ts +1 -1
  145. package/dist/version.js +1 -1
  146. package/package.json +15 -8
  147. package/src/activity.ts +0 -9
  148. package/src/auth/bearer.ts +4 -33
  149. package/src/auth/clerk.ts +35 -48
  150. package/src/call-admission.ts +8 -6
  151. package/src/catalog-fingerprint.ts +52 -0
  152. package/src/catalog-limits.ts +11 -0
  153. package/src/catalog-service.ts +601 -0
  154. package/src/catalog.ts +57 -2
  155. package/src/concurrency.ts +39 -0
  156. package/src/connectors/api.ts +22 -12
  157. package/src/connectors/remote-mcp.ts +63 -45
  158. package/src/credentials.ts +2 -2
  159. package/src/errors.ts +21 -1
  160. package/src/execute.ts +193 -423
  161. package/src/executor-result.ts +3 -3
  162. package/src/executors/quickjs-child.ts +8 -4
  163. package/src/executors/quickjs-protocol.ts +1 -1
  164. package/src/executors/quickjs-runtime.ts +5 -1
  165. package/src/executors/quickjs.ts +11 -8
  166. package/src/favicon.ts +2 -2
  167. package/src/index.ts +70 -222
  168. package/src/invocation.ts +580 -0
  169. package/src/meta-tools.ts +393 -985
  170. package/src/operator-ui/browser.css +360 -0
  171. package/src/operator-ui/browser.ts +987 -0
  172. package/src/operator-ui/generated.ts +4 -0
  173. package/src/operator-ui/model.ts +105 -0
  174. package/src/registry.ts +459 -439
  175. package/src/retired-toolkits.ts +14 -0
  176. package/src/routes/activity.ts +215 -0
  177. package/src/routes/credentials.ts +254 -0
  178. package/src/routes/mcp.ts +339 -0
  179. package/src/routes/oauth.ts +322 -0
  180. package/src/routes/shared.ts +240 -0
  181. package/src/routes/ui.ts +143 -0
  182. package/src/server.ts +64 -1731
  183. package/src/skills.ts +15 -43
  184. package/src/storage/file.ts +3 -1
  185. package/src/storage/memory.ts +3 -1
  186. package/src/timeout.ts +2 -4
  187. package/src/types.ts +8 -60
  188. package/src/ui.ts +60 -1538
  189. package/src/version.ts +1 -1
  190. package/dist/credential-health.d.ts +0 -227
  191. package/dist/credential-health.d.ts.map +0 -1
  192. package/dist/credential-health.js +0 -606
  193. package/dist/credential-health.js.map +0 -1
  194. package/dist/toolkits.d.ts +0 -146
  195. package/dist/toolkits.d.ts.map +0 -1
  196. package/dist/toolkits.js +0 -322
  197. package/dist/toolkits.js.map +0 -1
  198. package/src/credential-health.ts +0 -832
  199. package/src/toolkits.ts +0 -461
@@ -0,0 +1,601 @@
1
+ import {
2
+ compactSchema,
3
+ lexicalSearchQuery,
4
+ rankTools,
5
+ summarizeDescription,
6
+ } from "./catalog.js";
7
+ import {
8
+ mapSettledWithConcurrency,
9
+ resolveDiscoveryConcurrency,
10
+ } from "./concurrency.js";
11
+ import { classifyCallError, messageLooksRetryable } from "./errors.js";
12
+ import type { CallErrorDetails } from "./errors.js";
13
+ import type {
14
+ ConnectorOperationOptions,
15
+ RegistryView,
16
+ } from "./registry.js";
17
+ import {
18
+ connectorGuide,
19
+ connectorSkillName,
20
+ } from "./skills.js";
21
+ import {
22
+ DEFAULT_PROBE_TIMEOUT_MS,
23
+ normalizeTimeoutMs,
24
+ withAbortableTimeout,
25
+ } from "./timeout.js";
26
+ import type {
27
+ Connector,
28
+ JsonSchema,
29
+ ToolDef,
30
+ } from "./types.js";
31
+
32
+ export const DEFAULT_SEARCH_LIMIT = 8;
33
+ export const MAX_SEARCH_LIMIT = 100;
34
+ export const MAX_DESCRIBE_ADDRESSES = 100;
35
+ export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
36
+
37
+ const encoder = new TextEncoder();
38
+
39
+ export class DiscoveryPolicyError extends Error {
40
+ constructor(
41
+ readonly code: "invalid_args" | "result_too_large",
42
+ message: string,
43
+ ) {
44
+ super(message);
45
+ this.name = "DiscoveryPolicyError";
46
+ }
47
+ }
48
+
49
+ /** Validate before ranking so a huge page request does no proportional work. */
50
+ function discoverySearchLimit(value: unknown): number {
51
+ if (value === undefined) return DEFAULT_SEARCH_LIMIT;
52
+ if (
53
+ typeof value !== "number" ||
54
+ !Number.isInteger(value) ||
55
+ value < 1 ||
56
+ value > MAX_SEARCH_LIMIT
57
+ ) {
58
+ throw new DiscoveryPolicyError(
59
+ "invalid_args",
60
+ `limit must be a whole number from 1 through ${MAX_SEARCH_LIMIT}. Page through larger catalogs with offset.`,
61
+ );
62
+ }
63
+ return value;
64
+ }
65
+
66
+ /** Validate the raw list so duplicate addresses consume the same bound. */
67
+ function discoveryAddresses(value: unknown): unknown[] {
68
+ if (!Array.isArray(value)) {
69
+ throw new DiscoveryPolicyError(
70
+ "invalid_args",
71
+ "addresses must be an array.",
72
+ );
73
+ }
74
+ if (value.length > MAX_DESCRIBE_ADDRESSES) {
75
+ throw new DiscoveryPolicyError(
76
+ "invalid_args",
77
+ `addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across describe_tools calls.`,
78
+ );
79
+ }
80
+ return value;
81
+ }
82
+
83
+ /** Serialize once and count the exact bytes the MCP adapter would emit. */
84
+ export function boundedDiscoveryText(value: unknown, hint: string): string {
85
+ const text = JSON.stringify(value);
86
+ if (text === undefined) {
87
+ throw new TypeError("Discovery result is not JSON-serializable.");
88
+ }
89
+ const bytes = encoder.encode(text).length;
90
+ if (bytes > MAX_DISCOVERY_RESULT_BYTES) {
91
+ throw new DiscoveryPolicyError(
92
+ "result_too_large",
93
+ `Discovery result is ${bytes} UTF-8 bytes, over the ${MAX_DISCOVERY_RESULT_BYTES}-byte ceiling. ${hint}`,
94
+ );
95
+ }
96
+ return text;
97
+ }
98
+
99
+ export interface CatalogSearchArgs {
100
+ query?: string;
101
+ connector?: string;
102
+ limit?: number;
103
+ offset?: number;
104
+ fullDescriptions?: boolean;
105
+ includeSchemas?: "compact" | "json";
106
+ }
107
+
108
+ export interface CatalogDescribeArgs {
109
+ addresses?: unknown;
110
+ format?: "compact" | "json";
111
+ fullDescriptions?: boolean;
112
+ }
113
+
114
+ interface CatalogSearchEntry {
115
+ connector: Connector;
116
+ guide?: string;
117
+ tool: {
118
+ name: string;
119
+ address: string;
120
+ description?: string;
121
+ inputSchema?: unknown;
122
+ outputSchema?: unknown;
123
+ annotations?: ToolDef["annotations"];
124
+ };
125
+ }
126
+
127
+ export interface CatalogSearchPage {
128
+ entries: CatalogSearchEntry[];
129
+ total: number;
130
+ offset: number;
131
+ limit: number;
132
+ hasMore: boolean;
133
+ nextOffset?: number;
134
+ matchMode?: "partial";
135
+ }
136
+
137
+ export interface CatalogDescription {
138
+ address: string;
139
+ name?: string;
140
+ description?: string;
141
+ guide?: string;
142
+ inputSchema?: unknown;
143
+ outputSchema?: unknown;
144
+ annotations?: ToolDef["annotations"];
145
+ error?: string;
146
+ }
147
+
148
+ export interface ResolvedCatalogTool {
149
+ connector: Connector;
150
+ toolName: string;
151
+ definition: ToolDef;
152
+ }
153
+
154
+ export type CatalogResolution =
155
+ | {
156
+ ok: true;
157
+ resolved: ResolvedCatalogTool;
158
+ catalogMs: number;
159
+ }
160
+ | {
161
+ ok: false;
162
+ error: CallErrorDetails;
163
+ catalogMs: number;
164
+ connector?: Connector;
165
+ toolName?: string;
166
+ cause?: unknown;
167
+ };
168
+
169
+ function framingError(code: string, message: string): CallErrorDetails {
170
+ return { code, message, retryable: messageLooksRetryable(message) };
171
+ }
172
+
173
+ function renderSchema(schema: JsonSchema, format: "compact" | "json"): unknown {
174
+ return format === "json" ? schema : compactSchema(schema);
175
+ }
176
+
177
+ /**
178
+ * Request-local catalog operations shared by MCP meta-tools and code mode.
179
+ * Successful catalogs may be reused inside this request; failures are not
180
+ * retained, and the service itself never escapes its request adapter.
181
+ */
182
+ export class CatalogService {
183
+ readonly requestScope: object;
184
+ private readonly probeTimeoutMs: number;
185
+ private readonly concurrency: number;
186
+ private readonly loaded = new Map<string, ToolDef[]>();
187
+ private readonly loading = new Map<string, Promise<ToolDef[]>>();
188
+
189
+ constructor(
190
+ private readonly registry: RegistryView,
191
+ readonly baseUrl: string,
192
+ options: {
193
+ requestScope?: object;
194
+ probeTimeoutMs?: number;
195
+ concurrency?: number;
196
+ } = {},
197
+ ) {
198
+ this.requestScope = options.requestScope ?? {};
199
+ this.probeTimeoutMs =
200
+ normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
201
+ this.concurrency = resolveDiscoveryConcurrency(options.concurrency);
202
+ }
203
+
204
+ async loadConnector(
205
+ id: string,
206
+ callOptions: ConnectorOperationOptions = {},
207
+ ): Promise<ToolDef[]> {
208
+ const cached = this.loaded.get(id);
209
+ if (cached) return cached;
210
+ const inFlight = this.loading.get(id);
211
+ if (inFlight) return inFlight;
212
+ const loading = this.registry
213
+ .getTools(id, this.baseUrl, this.requestScope, callOptions)
214
+ .then((tools) => {
215
+ this.loaded.set(id, tools);
216
+ return tools;
217
+ })
218
+ .finally(() => {
219
+ if (this.loading.get(id) === loading) this.loading.delete(id);
220
+ });
221
+ this.loading.set(id, loading);
222
+ return loading;
223
+ }
224
+
225
+ private loadForDiscovery(id: string, label: string): Promise<ToolDef[]> {
226
+ return withAbortableTimeout(
227
+ (signal) =>
228
+ this.loadConnector(id, {
229
+ signal,
230
+ timeoutMs: this.probeTimeoutMs,
231
+ }),
232
+ this.probeTimeoutMs,
233
+ label,
234
+ );
235
+ }
236
+
237
+ async resolveTool(
238
+ address: string,
239
+ callOptions: ConnectorOperationOptions = {},
240
+ ): Promise<CatalogResolution> {
241
+ const resolved = this.registry.resolveAddress(address);
242
+ if (!resolved) {
243
+ return {
244
+ ok: false,
245
+ error: framingError(
246
+ "unknown_address",
247
+ `Unknown address "${address}"`,
248
+ ),
249
+ catalogMs: 0,
250
+ };
251
+ }
252
+ const started = Date.now();
253
+ let tools: ToolDef[];
254
+ try {
255
+ tools = await this.loadConnector(resolved.connector.id, callOptions);
256
+ } catch (cause) {
257
+ return {
258
+ ok: false,
259
+ error: classifyCallError(cause, "catalog_lookup_failed"),
260
+ catalogMs: Date.now() - started,
261
+ connector: resolved.connector,
262
+ toolName: resolved.toolName,
263
+ cause,
264
+ };
265
+ }
266
+ const definition = tools.find((tool) => tool.name === resolved.toolName);
267
+ if (!definition) {
268
+ return {
269
+ ok: false,
270
+ error: framingError(
271
+ "unknown_tool",
272
+ `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
273
+ ),
274
+ catalogMs: Date.now() - started,
275
+ connector: resolved.connector,
276
+ toolName: resolved.toolName,
277
+ };
278
+ }
279
+ return {
280
+ ok: true,
281
+ resolved: {
282
+ connector: resolved.connector,
283
+ toolName: resolved.toolName,
284
+ definition,
285
+ },
286
+ catalogMs: Date.now() - started,
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Resolve the JavaScript-safe property used by a lazy code-mode namespace
292
+ * back to exactly one catalog tool. Ambiguous aliases fail with an explicit
293
+ * escape hatch instead of silently choosing the first tool.
294
+ */
295
+ async resolveToolAlias(
296
+ connectorId: string,
297
+ alias: string,
298
+ aliasFor: (toolName: string) => string,
299
+ callOptions: ConnectorOperationOptions = {},
300
+ ): Promise<CatalogResolution> {
301
+ const connector = this.registry.getConnector(connectorId);
302
+ if (!connector) {
303
+ return {
304
+ ok: false,
305
+ error: framingError(
306
+ "unknown_address",
307
+ `Unknown address "${connectorId}.${alias}"`,
308
+ ),
309
+ catalogMs: 0,
310
+ };
311
+ }
312
+ const started = Date.now();
313
+ let tools: ToolDef[];
314
+ try {
315
+ tools = await this.loadConnector(connector.id, callOptions);
316
+ } catch (cause) {
317
+ return {
318
+ ok: false,
319
+ error: classifyCallError(cause, "catalog_lookup_failed"),
320
+ catalogMs: Date.now() - started,
321
+ connector,
322
+ toolName: alias,
323
+ cause,
324
+ };
325
+ }
326
+ const [definition, ...collisions] = tools.filter(
327
+ (tool) => aliasFor(tool.name) === alias,
328
+ );
329
+ if (!definition) {
330
+ return {
331
+ ok: false,
332
+ error: framingError(
333
+ "unknown_tool",
334
+ `Unknown tool "${alias}" on connector "${connector.id}"`,
335
+ ),
336
+ catalogMs: Date.now() - started,
337
+ connector,
338
+ toolName: alias,
339
+ };
340
+ }
341
+ if (collisions.length > 0) {
342
+ const names = [definition, ...collisions]
343
+ .map((tool) => `"${tool.name}"`)
344
+ .join(", ");
345
+ return {
346
+ ok: false,
347
+ error: {
348
+ code: "ambiguous_tool_alias",
349
+ message: `Tool alias "${alias}" is ambiguous on connector "${connector.id}" because ${names} sanitize to the same name. Use connecta.call with an exact address.`,
350
+ retryable: false,
351
+ },
352
+ catalogMs: Date.now() - started,
353
+ connector,
354
+ toolName: alias,
355
+ };
356
+ }
357
+ return {
358
+ ok: true,
359
+ resolved: {
360
+ connector,
361
+ toolName: definition.name,
362
+ definition,
363
+ },
364
+ catalogMs: Date.now() - started,
365
+ };
366
+ }
367
+
368
+ async search(args: CatalogSearchArgs): Promise<CatalogSearchPage> {
369
+ const query = args.query ?? "";
370
+ const retrievalQuery = lexicalSearchQuery(query);
371
+ const limit = discoverySearchLimit(args.limit);
372
+ const offset = Math.max(0, Math.trunc(args.offset ?? 0));
373
+ const connectors = args.connector
374
+ ? [this.registry.getConnector(args.connector)].filter(
375
+ (connector): connector is Connector => Boolean(connector),
376
+ )
377
+ : this.registry.listConnectors();
378
+ const catalogs = await mapSettledWithConcurrency(
379
+ connectors,
380
+ this.concurrency,
381
+ (connector) =>
382
+ this.loadForDiscovery(
383
+ connector.id,
384
+ `search_tools probe of "${connector.id}"`,
385
+ ),
386
+ );
387
+ const matches: Array<{
388
+ connector: Connector;
389
+ tool: ToolDef;
390
+ score: number;
391
+ order: number;
392
+ }> = [];
393
+ let matchMode: "all" | "partial" = "all";
394
+ const collectMatches = (mode: "all" | "partial") => {
395
+ matches.length = 0;
396
+ let orderBase = 0;
397
+ catalogs.forEach((catalog, connectorIndex) => {
398
+ const connector = connectors[connectorIndex];
399
+ if (!connector) {
400
+ throw new Error("Catalog result has no corresponding connector");
401
+ }
402
+ if (catalog.status === "fulfilled") {
403
+ for (const ranked of rankTools(
404
+ catalog.value,
405
+ retrievalQuery,
406
+ mode,
407
+ )) {
408
+ matches.push({
409
+ connector,
410
+ tool: ranked.tool,
411
+ score: ranked.score,
412
+ order: orderBase + ranked.order,
413
+ });
414
+ }
415
+ }
416
+ orderBase +=
417
+ catalog.status === "fulfilled" ? catalog.value.length : 1;
418
+ });
419
+ };
420
+ collectMatches("all");
421
+ if (query.trim() && matches.length === 0) {
422
+ matchMode = "partial";
423
+ collectMatches(matchMode);
424
+ }
425
+ matches.sort((a, b) => b.score - a.score || a.order - b.order);
426
+ const entries = matches.slice(offset, offset + limit).map((match) => {
427
+ const input = match.tool.inputSchema ?? { type: "object" };
428
+ const description = summarizeDescription(
429
+ match.tool.description,
430
+ args.fullDescriptions === true,
431
+ );
432
+ return {
433
+ connector: match.connector,
434
+ ...(connectorGuide(match.connector)
435
+ ? { guide: connectorSkillName(match.connector.id) }
436
+ : {}),
437
+ tool: {
438
+ name: match.tool.name,
439
+ address: `${match.connector.id}.${match.tool.name}`,
440
+ ...(description !== undefined ? { description } : {}),
441
+ ...(args.includeSchemas
442
+ ? {
443
+ inputSchema:
444
+ renderSchema(input, args.includeSchemas),
445
+ }
446
+ : {}),
447
+ ...(args.includeSchemas && match.tool.outputSchema
448
+ ? {
449
+ outputSchema:
450
+ renderSchema(match.tool.outputSchema, args.includeSchemas),
451
+ }
452
+ : {}),
453
+ ...(match.tool.annotations
454
+ ? { annotations: match.tool.annotations }
455
+ : {}),
456
+ },
457
+ };
458
+ });
459
+ const nextOffset =
460
+ offset + entries.length < matches.length
461
+ ? offset + entries.length
462
+ : undefined;
463
+ return {
464
+ entries,
465
+ total: matches.length,
466
+ offset,
467
+ limit,
468
+ hasMore: nextOffset !== undefined,
469
+ ...(nextOffset !== undefined ? { nextOffset } : {}),
470
+ ...(matchMode === "partial" && matches.length > 0
471
+ ? { matchMode }
472
+ : {}),
473
+ };
474
+ }
475
+
476
+ async describe(args: CatalogDescribeArgs): Promise<CatalogDescription[]> {
477
+ const addresses = discoveryAddresses(args.addresses);
478
+ const format = args.format ?? "compact";
479
+ const resolved = addresses.map((rawAddress) => {
480
+ const address = String(rawAddress);
481
+ return { address, resolved: this.registry.resolveAddress(address) };
482
+ });
483
+ const connectorIds = [
484
+ ...new Set(
485
+ resolved
486
+ .map((entry) => entry.resolved?.connector.id)
487
+ .filter((id): id is string => Boolean(id)),
488
+ ),
489
+ ];
490
+ const loaded = await mapSettledWithConcurrency(
491
+ connectorIds,
492
+ this.concurrency,
493
+ (id) =>
494
+ this.loadForDiscovery(id, `describe_tools probe of "${id}"`),
495
+ );
496
+ const catalogs = new Map<string, ToolDef[] | Error>();
497
+ loaded.forEach((result, index) => {
498
+ const connectorId = connectorIds[index];
499
+ if (connectorId === undefined) {
500
+ throw new Error("Catalog result has no corresponding connector id");
501
+ }
502
+ catalogs.set(
503
+ connectorId,
504
+ result.status === "fulfilled"
505
+ ? result.value
506
+ : result.reason instanceof Error
507
+ ? result.reason
508
+ : new Error(String(result.reason)),
509
+ );
510
+ });
511
+ return resolved.map(({ address, resolved: addressResolution }) => {
512
+ if (!addressResolution) {
513
+ return { address, error: `Unknown address "${address}"` };
514
+ }
515
+ const catalog = catalogs.get(addressResolution.connector.id);
516
+ if (catalog instanceof Error) {
517
+ return { address, error: catalog.message };
518
+ }
519
+ const tool = catalog?.find(
520
+ (item) => item.name === addressResolution.toolName,
521
+ );
522
+ if (!tool) {
523
+ return {
524
+ address,
525
+ error: `Unknown tool "${addressResolution.toolName}" on connector "${addressResolution.connector.id}"`,
526
+ };
527
+ }
528
+ const input = tool.inputSchema ?? { type: "object" };
529
+ const description = summarizeDescription(
530
+ tool.description,
531
+ args.fullDescriptions === true,
532
+ );
533
+ return {
534
+ address,
535
+ name: tool.name,
536
+ ...(description !== undefined ? { description } : {}),
537
+ ...(connectorGuide(addressResolution.connector)
538
+ ? { guide: connectorSkillName(addressResolution.connector.id) }
539
+ : {}),
540
+ inputSchema: renderSchema(input, format),
541
+ ...(tool.outputSchema
542
+ ? {
543
+ outputSchema: renderSchema(tool.outputSchema, format),
544
+ }
545
+ : {}),
546
+ ...(tool.annotations ? { annotations: tool.annotations } : {}),
547
+ };
548
+ });
549
+ }
550
+ }
551
+
552
+ export function groupedSearchResult(page: CatalogSearchPage) {
553
+ const groups: Array<{
554
+ id: string;
555
+ title?: string;
556
+ description?: string;
557
+ guide?: string;
558
+ tools: CatalogSearchEntry["tool"][];
559
+ }> = [];
560
+ const byConnector = new Map<string, (typeof groups)[number]>();
561
+ for (const entry of page.entries) {
562
+ const existing = byConnector.get(entry.connector.id);
563
+ if (existing) {
564
+ existing.tools.push(entry.tool);
565
+ } else {
566
+ const group: (typeof groups)[number] = {
567
+ id: entry.connector.id,
568
+ ...(entry.connector.title ? { title: entry.connector.title } : {}),
569
+ ...(entry.connector.description !== undefined
570
+ ? { description: entry.connector.description }
571
+ : {}),
572
+ ...(entry.guide ? { guide: entry.guide } : {}),
573
+ tools: [],
574
+ };
575
+ byConnector.set(entry.connector.id, group);
576
+ groups.push(group);
577
+ group.tools.push(entry.tool);
578
+ }
579
+ }
580
+ return {
581
+ connectors: groups,
582
+ total: page.total,
583
+ offset: page.offset,
584
+ limit: page.limit,
585
+ hasMore: page.hasMore,
586
+ ...(page.nextOffset !== undefined ? { nextOffset: page.nextOffset } : {}),
587
+ ...(page.matchMode ? { matchMode: page.matchMode } : {}),
588
+ };
589
+ }
590
+
591
+ export function flatSearchResult(page: CatalogSearchPage) {
592
+ return {
593
+ tools: page.entries.map((entry) => entry.tool),
594
+ total: page.total,
595
+ offset: page.offset,
596
+ limit: page.limit,
597
+ hasMore: page.hasMore,
598
+ ...(page.nextOffset !== undefined ? { nextOffset: page.nextOffset } : {}),
599
+ ...(page.matchMode ? { matchMode: page.matchMode } : {}),
600
+ };
601
+ }
package/src/catalog.ts CHANGED
@@ -17,6 +17,61 @@ function normalized(text: string): string {
17
17
  return text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
18
18
  }
19
19
 
20
+ /**
21
+ * Conversational framing selected by the #188 research run before the #189
22
+ * holdout existed. Action-bearing terms such as get/list/search/find/create
23
+ * deliberately remain: arbitrary connector catalogs need those distinctions.
24
+ */
25
+ const CONVERSATIONAL_QUERY_WORDS = new Set([
26
+ "a",
27
+ "all",
28
+ "an",
29
+ "and",
30
+ "are",
31
+ "can",
32
+ "could",
33
+ "current",
34
+ "for",
35
+ "from",
36
+ "i",
37
+ "in",
38
+ "into",
39
+ "it",
40
+ "latest",
41
+ "let",
42
+ "me",
43
+ "most",
44
+ "of",
45
+ "on",
46
+ "our",
47
+ "please",
48
+ "right",
49
+ "show",
50
+ "that",
51
+ "the",
52
+ "this",
53
+ "to",
54
+ "up",
55
+ "want",
56
+ "when",
57
+ "which",
58
+ "with",
59
+ "you",
60
+ ]);
61
+
62
+ /**
63
+ * Remove conversational framing before the all-term/partial decision. If
64
+ * every term is framing, retain the original query rather than turning a
65
+ * search into an unfiltered catalog browse.
66
+ */
67
+ export function lexicalSearchQuery(query: string): string {
68
+ const terms = normalized(query).split(/\s+/).filter(Boolean);
69
+ const contentTerms = terms.filter(
70
+ (term) => !CONVERSATIONAL_QUERY_WORDS.has(term),
71
+ );
72
+ return contentTerms.length > 0 ? contentTerms.join(" ") : query;
73
+ }
74
+
20
75
  interface SearchDocument {
21
76
  tool: ToolDef;
22
77
  name: string;
@@ -233,8 +288,8 @@ export function compactSchema(schema: JsonSchema): string {
233
288
  const cached = compactSchemas.get(schema);
234
289
  if (cached) return cached;
235
290
  const defs = {
236
- ...((schema.$defs as Record<string, unknown>) ?? {}),
237
- ...((schema.definitions as Record<string, unknown>) ?? {}),
291
+ ...(schema.$defs as Record<string, unknown>),
292
+ ...(schema.definitions as Record<string, unknown>),
238
293
  };
239
294
  let rendered: string;
240
295
  try {
@@ -0,0 +1,39 @@
1
+ const DEFAULT_DISCOVERY_CONCURRENCY = 4;
2
+
3
+ export function resolveDiscoveryConcurrency(
4
+ value: number | undefined,
5
+ ): number {
6
+ return value !== undefined && Number.isInteger(value) && value > 0
7
+ ? value
8
+ : DEFAULT_DISCOVERY_CONCURRENCY;
9
+ }
10
+
11
+ /** Run `fn` over `items` with at most `limit` operations in flight. */
12
+ export async function mapSettledWithConcurrency<T, R>(
13
+ items: readonly T[],
14
+ limit: number,
15
+ fn: (item: T, index: number) => Promise<R>,
16
+ ): Promise<PromiseSettledResult<R>[]> {
17
+ const settled = Array<PromiseSettledResult<R>>(items.length);
18
+ const remaining = items.entries();
19
+ const workers = Array.from(
20
+ { length: Math.min(limit, items.length) },
21
+ async () => {
22
+ for (;;) {
23
+ const next = remaining.next();
24
+ if (next.done) return;
25
+ const [index, item] = next.value;
26
+ try {
27
+ settled[index] = {
28
+ status: "fulfilled",
29
+ value: await fn(item, index),
30
+ };
31
+ } catch (reason) {
32
+ settled[index] = { status: "rejected", reason };
33
+ }
34
+ }
35
+ },
36
+ );
37
+ await Promise.all(workers);
38
+ return settled;
39
+ }