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