@zackbart/connecta 0.10.0 → 0.10.2

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 (123) hide show
  1. package/AGENTS.md +113 -0
  2. package/CHANGELOG.md +83 -0
  3. package/README.md +62 -12
  4. package/bin/connecta.mjs +272 -0
  5. package/dist/access-tokens.d.ts +32 -0
  6. package/dist/access-tokens.d.ts.map +1 -0
  7. package/dist/access-tokens.js +225 -0
  8. package/dist/access-tokens.js.map +1 -0
  9. package/dist/catalog-service.d.ts +39 -1
  10. package/dist/catalog-service.d.ts.map +1 -1
  11. package/dist/catalog-service.js +133 -11
  12. package/dist/catalog-service.js.map +1 -1
  13. package/dist/catalog.d.ts +17 -0
  14. package/dist/catalog.d.ts.map +1 -1
  15. package/dist/catalog.js +113 -13
  16. package/dist/catalog.js.map +1 -1
  17. package/dist/execute.d.ts +45 -1
  18. package/dist/execute.d.ts.map +1 -1
  19. package/dist/execute.js +265 -68
  20. package/dist/execute.js.map +1 -1
  21. package/dist/index.d.ts +11 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +12 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/invocation.d.ts.map +1 -1
  26. package/dist/invocation.js +1 -5
  27. package/dist/invocation.js.map +1 -1
  28. package/dist/meta-tools.d.ts +1 -0
  29. package/dist/meta-tools.d.ts.map +1 -1
  30. package/dist/meta-tools.js +410 -12
  31. package/dist/meta-tools.js.map +1 -1
  32. package/dist/operator-ui/generated.d.ts +2 -2
  33. package/dist/operator-ui/generated.d.ts.map +1 -1
  34. package/dist/operator-ui/generated.js +2 -2
  35. package/dist/operator-ui/generated.js.map +1 -1
  36. package/dist/operator-ui/model.d.ts +2 -0
  37. package/dist/operator-ui/model.d.ts.map +1 -1
  38. package/dist/operator-ui/model.js.map +1 -1
  39. package/dist/routes/access-tokens.d.ts +7 -0
  40. package/dist/routes/access-tokens.d.ts.map +1 -0
  41. package/dist/routes/access-tokens.js +84 -0
  42. package/dist/routes/access-tokens.js.map +1 -0
  43. package/dist/routes/shared.d.ts +3 -0
  44. package/dist/routes/shared.d.ts.map +1 -1
  45. package/dist/routes/shared.js.map +1 -1
  46. package/dist/routes/ui.d.ts.map +1 -1
  47. package/dist/routes/ui.js +9 -1
  48. package/dist/routes/ui.js.map +1 -1
  49. package/dist/server.d.ts.map +1 -1
  50. package/dist/server.js +5 -0
  51. package/dist/server.js.map +1 -1
  52. package/dist/skills.d.ts +1 -1
  53. package/dist/skills.d.ts.map +1 -1
  54. package/dist/skills.js +1 -1
  55. package/dist/storage/file.d.ts.map +1 -1
  56. package/dist/storage/file.js +5 -0
  57. package/dist/storage/file.js.map +1 -1
  58. package/dist/storage/memory.d.ts.map +1 -1
  59. package/dist/storage/memory.js +8 -0
  60. package/dist/storage/memory.js.map +1 -1
  61. package/dist/tool-safety.d.ts +10 -0
  62. package/dist/tool-safety.d.ts.map +1 -0
  63. package/dist/tool-safety.js +12 -0
  64. package/dist/tool-safety.js.map +1 -0
  65. package/dist/types.d.ts +5 -0
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/ui.d.ts +4 -4
  68. package/dist/ui.d.ts.map +1 -1
  69. package/dist/ui.js +44 -1
  70. package/dist/ui.js.map +1 -1
  71. package/dist/version.d.ts +1 -1
  72. package/dist/version.js +1 -1
  73. package/documentation/architecture.md +7 -0
  74. package/documentation/auth.md +58 -0
  75. package/documentation/call-admission.md +7 -0
  76. package/documentation/code-first-exploration.md +292 -0
  77. package/documentation/code-mode.md +697 -0
  78. package/documentation/connector-guides.md +7 -0
  79. package/documentation/connectors.md +63 -0
  80. package/documentation/mcp-2026-07-28.md +46 -0
  81. package/documentation/meta-tools.md +167 -0
  82. package/documentation/operations.md +7 -0
  83. package/documentation/operator-ui.md +7 -0
  84. package/documentation/request-admission.md +7 -0
  85. package/documentation/storage-and-credentials.md +54 -0
  86. package/ethos.md +132 -0
  87. package/examples/node/README.md +53 -0
  88. package/examples/node/src/index.ts +73 -0
  89. package/examples/worker/README.md +160 -0
  90. package/examples/worker/src/cloudflare-kv.ts +43 -0
  91. package/examples/worker/src/d1-activity-row.ts +100 -0
  92. package/examples/worker/src/d1-activity.ts +144 -0
  93. package/examples/worker/src/index.ts +136 -0
  94. package/examples/worker/wrangler.jsonc +26 -0
  95. package/package.json +11 -1
  96. package/src/access-tokens.ts +289 -0
  97. package/src/catalog-service.ts +177 -15
  98. package/src/catalog.ts +143 -12
  99. package/src/execute.ts +372 -96
  100. package/src/index.ts +33 -1
  101. package/src/invocation.ts +1 -8
  102. package/src/meta-tools.ts +504 -11
  103. package/src/operator-ui/browser.css +63 -0
  104. package/src/operator-ui/browser.ts +288 -2
  105. package/src/operator-ui/generated.ts +2 -2
  106. package/src/operator-ui/model.ts +6 -0
  107. package/src/routes/access-tokens.ts +115 -0
  108. package/src/routes/shared.ts +3 -0
  109. package/src/routes/ui.ts +9 -0
  110. package/src/server.ts +5 -0
  111. package/src/skills.ts +1 -1
  112. package/src/storage/file.ts +5 -0
  113. package/src/storage/memory.ts +8 -0
  114. package/src/tool-safety.ts +15 -0
  115. package/src/types.ts +5 -0
  116. package/src/ui.ts +50 -1
  117. package/src/version.ts +1 -1
  118. package/templates/node/.env.example +5 -0
  119. package/templates/node/AGENTS.md +19 -0
  120. package/templates/node/README.md +33 -0
  121. package/templates/node/package.json +23 -0
  122. package/templates/node/src/index.ts +43 -0
  123. package/templates/node/tsconfig.json +12 -0
@@ -0,0 +1,289 @@
1
+ import type { AuthResult, InboundAuth, KVStorage } from "./types.js";
2
+
3
+ const TOKEN_PREFIX = "cta_";
4
+ const TOKEN_BYTES = 32;
5
+ const TOKEN_VALUE_RE = /^cta_[A-Za-z0-9_-]{43}$/;
6
+ const RECORD_PREFIX = "access-token:v1:record:";
7
+ const LOOKUP_PREFIX = "access-token:v1:lookup:";
8
+ const MAX_NAME_CHARACTERS = 80;
9
+ const DEFAULT_MAX_ACTIVE = 100;
10
+ const MAX_CONFIGURED_ACTIVE = 1_000;
11
+ const encoder = new TextEncoder();
12
+
13
+ interface StoredAccessToken {
14
+ version: 1;
15
+ id: string;
16
+ name: string;
17
+ tokenHash: string;
18
+ tokenPrefix: string;
19
+ createdAt: string;
20
+ createdBy: string;
21
+ revokedAt?: string;
22
+ revokedBy?: string;
23
+ }
24
+
25
+ interface TokenLookup {
26
+ version: 1;
27
+ id: string;
28
+ }
29
+
30
+ export interface AccessTokenMetadata {
31
+ id: string;
32
+ name: string;
33
+ tokenPrefix: string;
34
+ createdAt: string;
35
+ revokedAt?: string;
36
+ }
37
+
38
+ export interface CreatedAccessToken {
39
+ token: string;
40
+ accessToken: AccessTokenMetadata;
41
+ }
42
+
43
+ function recordKey(id: string): string {
44
+ return `${RECORD_PREFIX}${id}`;
45
+ }
46
+
47
+ function lookupKey(hash: string): string {
48
+ return `${LOOKUP_PREFIX}${hash}`;
49
+ }
50
+
51
+ function bytesToBase64Url(bytes: Uint8Array): string {
52
+ let binary = "";
53
+ for (const byte of bytes) binary += String.fromCharCode(byte);
54
+ return btoa(binary)
55
+ .replaceAll("+", "-")
56
+ .replaceAll("/", "_")
57
+ .replace(/=+$/u, "");
58
+ }
59
+
60
+ function bytesToHex(bytes: Uint8Array): string {
61
+ return [...bytes]
62
+ .map((byte) => byte.toString(16).padStart(2, "0"))
63
+ .join("");
64
+ }
65
+
66
+ async function hashToken(token: string): Promise<string> {
67
+ return bytesToHex(
68
+ new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(token))),
69
+ );
70
+ }
71
+
72
+ function normalizeName(value: unknown): string {
73
+ if (typeof value !== "string") {
74
+ throw new Error("Token name must be a string");
75
+ }
76
+ const compact = value.replace(/\s+/gu, " ").trim();
77
+ if (!compact) throw new Error("Token name cannot be empty");
78
+ if (Array.from(compact).length > MAX_NAME_CHARACTERS) {
79
+ throw new Error(
80
+ `Token name cannot exceed ${MAX_NAME_CHARACTERS} characters`,
81
+ );
82
+ }
83
+ return compact;
84
+ }
85
+
86
+ function parseRecord(raw: string): StoredAccessToken {
87
+ try {
88
+ const value = JSON.parse(raw) as Partial<StoredAccessToken>;
89
+ if (
90
+ value.version !== 1 ||
91
+ typeof value.id !== "string" ||
92
+ !/^[0-9a-f-]{36}$/u.test(value.id) ||
93
+ typeof value.name !== "string" ||
94
+ typeof value.tokenHash !== "string" ||
95
+ !/^[0-9a-f]{64}$/u.test(value.tokenHash) ||
96
+ typeof value.tokenPrefix !== "string" ||
97
+ typeof value.createdAt !== "string" ||
98
+ typeof value.createdBy !== "string" ||
99
+ (value.revokedAt !== undefined &&
100
+ typeof value.revokedAt !== "string") ||
101
+ (value.revokedBy !== undefined &&
102
+ typeof value.revokedBy !== "string")
103
+ ) {
104
+ throw new Error("invalid token record");
105
+ }
106
+ return value as StoredAccessToken;
107
+ } catch {
108
+ throw new Error("Stored access token metadata is invalid or corrupted");
109
+ }
110
+ }
111
+
112
+ function parseLookup(raw: string): TokenLookup | null {
113
+ try {
114
+ const value = JSON.parse(raw) as Partial<TokenLookup>;
115
+ return value.version === 1 && typeof value.id === "string"
116
+ ? { version: 1, id: value.id }
117
+ : null;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ function metadata(record: StoredAccessToken): AccessTokenMetadata {
124
+ return {
125
+ id: record.id,
126
+ name: record.name,
127
+ tokenPrefix: record.tokenPrefix,
128
+ createdAt: record.createdAt,
129
+ ...(record.revokedAt ? { revokedAt: record.revokedAt } : {}),
130
+ };
131
+ }
132
+
133
+ function unauthorized(): AuthResult {
134
+ return {
135
+ ok: false,
136
+ response: new Response(JSON.stringify({ error: "unauthorized" }), {
137
+ status: 401,
138
+ headers: {
139
+ "Content-Type": "application/json",
140
+ "WWW-Authenticate": "Bearer",
141
+ },
142
+ }),
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Deployment-scoped personal access tokens. Secret material is never
148
+ * recoverable: authentication indexes a SHA-256 digest of a random 256-bit
149
+ * token, while separately enumerable metadata powers operator management.
150
+ */
151
+ export class AccessTokenManager {
152
+ readonly auth: InboundAuth;
153
+ private readonly maxActive: number;
154
+
155
+ constructor(
156
+ private readonly storage: KVStorage,
157
+ options: { maxActive?: number } = {},
158
+ ) {
159
+ if (!storage.list) {
160
+ throw new Error(
161
+ "accessTokens requires a storage adapter that implements list(prefix)",
162
+ );
163
+ }
164
+ const maxActive = options.maxActive ?? DEFAULT_MAX_ACTIVE;
165
+ if (
166
+ !Number.isInteger(maxActive) ||
167
+ maxActive < 1 ||
168
+ maxActive > MAX_CONFIGURED_ACTIVE
169
+ ) {
170
+ throw new Error(
171
+ `accessTokens.maxActive must be a whole number from 1 to ${MAX_CONFIGURED_ACTIVE}`,
172
+ );
173
+ }
174
+ this.maxActive = maxActive;
175
+ this.auth = {
176
+ kind: "access_token",
177
+ activityActorNamespace: "connecta:access-tokens:v1",
178
+ activityActorLabel: async (id) => {
179
+ try {
180
+ return (await this.read(id))?.name;
181
+ } catch {
182
+ return undefined;
183
+ }
184
+ },
185
+ authorize: (request) => this.authorize(request),
186
+ };
187
+ }
188
+
189
+ private async read(id: string): Promise<StoredAccessToken | null> {
190
+ const raw = await this.storage.get(recordKey(id));
191
+ return raw ? parseRecord(raw) : null;
192
+ }
193
+
194
+ async list(): Promise<AccessTokenMetadata[]> {
195
+ const keys = await this.storage.list!(RECORD_PREFIX);
196
+ const records = await Promise.all(
197
+ keys.map(async (key) => {
198
+ const raw = await this.storage.get(key);
199
+ return raw ? parseRecord(raw) : null;
200
+ }),
201
+ );
202
+ return records
203
+ .filter((record): record is StoredAccessToken => Boolean(record))
204
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
205
+ .map(metadata);
206
+ }
207
+
208
+ async create(name: unknown, createdBy: string): Promise<CreatedAccessToken> {
209
+ const normalizedName = normalizeName(name);
210
+ const active = (await this.list()).filter((token) => !token.revokedAt);
211
+ if (active.length >= this.maxActive) {
212
+ throw new Error(
213
+ `This deployment already has the maximum of ${this.maxActive} active access tokens`,
214
+ );
215
+ }
216
+ const secretBytes = crypto.getRandomValues(new Uint8Array(TOKEN_BYTES));
217
+ const token = TOKEN_PREFIX + bytesToBase64Url(secretBytes);
218
+ const hash = await hashToken(token);
219
+ if (await this.storage.get(lookupKey(hash))) {
220
+ throw new Error("Access token collision; create another token");
221
+ }
222
+ const record: StoredAccessToken = {
223
+ version: 1,
224
+ id: crypto.randomUUID(),
225
+ name: normalizedName,
226
+ tokenHash: hash,
227
+ tokenPrefix: token.slice(0, 12),
228
+ createdAt: new Date().toISOString(),
229
+ createdBy,
230
+ };
231
+ await this.storage.set(recordKey(record.id), JSON.stringify(record));
232
+ try {
233
+ await this.storage.set(
234
+ lookupKey(hash),
235
+ JSON.stringify({ version: 1, id: record.id } satisfies TokenLookup),
236
+ );
237
+ } catch (error) {
238
+ await this.storage.delete(recordKey(record.id)).catch(() => {});
239
+ throw error;
240
+ }
241
+ return { token, accessToken: metadata(record) };
242
+ }
243
+
244
+ async rename(
245
+ id: string,
246
+ name: unknown,
247
+ ): Promise<AccessTokenMetadata | null> {
248
+ const record = await this.read(id);
249
+ if (!record) return null;
250
+ record.name = normalizeName(name);
251
+ await this.storage.set(recordKey(id), JSON.stringify(record));
252
+ return metadata(record);
253
+ }
254
+
255
+ async revoke(
256
+ id: string,
257
+ revokedBy: string,
258
+ ): Promise<AccessTokenMetadata | null> {
259
+ const record = await this.read(id);
260
+ if (!record) return null;
261
+ if (!record.revokedAt) {
262
+ // Admission disappears first. A metadata-write failure may leave the UI
263
+ // calling the record active, but can never leave a token labelled
264
+ // revoked while its lookup still admits requests.
265
+ await this.storage.delete(lookupKey(record.tokenHash));
266
+ record.revokedAt = new Date().toISOString();
267
+ record.revokedBy = revokedBy;
268
+ await this.storage.set(recordKey(id), JSON.stringify(record));
269
+ }
270
+ return metadata(record);
271
+ }
272
+
273
+ private async authorize(request: Request): Promise<AuthResult> {
274
+ const header = request.headers.get("authorization") ?? "";
275
+ const match = /^Bearer\s+(.+)$/iu.exec(header);
276
+ const token = match?.[1];
277
+ if (!token || !TOKEN_VALUE_RE.test(token)) return unauthorized();
278
+ const hash = await hashToken(token);
279
+ const lookupRaw = await this.storage.get(lookupKey(hash));
280
+ if (!lookupRaw) return unauthorized();
281
+ const lookup = parseLookup(lookupRaw);
282
+ if (!lookup) return unauthorized();
283
+ const record = await this.read(lookup.id);
284
+ if (!record || record.revokedAt || record.tokenHash !== hash) {
285
+ return unauthorized();
286
+ }
287
+ return { ok: true, subjectId: record.id };
288
+ }
289
+ }
@@ -1,9 +1,12 @@
1
1
  import {
2
+ compactDiscoverySchema,
2
3
  compactSchema,
3
4
  lexicalCorpusStatistics,
5
+ lexicalQueryTerms,
4
6
  lexicalSearchQuery,
5
7
  rankTools,
6
8
  schemaObjectKeys,
9
+ summarizeDiscoveryDescription,
7
10
  summarizeDescription,
8
11
  } from "./catalog.js";
9
12
  import {
@@ -25,6 +28,7 @@ import {
25
28
  normalizeTimeoutMs,
26
29
  withAbortableTimeout,
27
30
  } from "./timeout.js";
31
+ import { isExplicitlyReadOnly } from "./tool-safety.js";
28
32
  import type {
29
33
  Connector,
30
34
  JsonSchema,
@@ -35,6 +39,8 @@ export const DEFAULT_SEARCH_LIMIT = 8;
35
39
  export const MAX_SEARCH_LIMIT = 100;
36
40
  export const MAX_DESCRIBE_ADDRESSES = 100;
37
41
  export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
42
+ const MAX_QUERY_ANALYSIS_TERMS = 8;
43
+ const MAX_QUERY_ANALYSIS_TERM_LENGTH = 64;
38
44
 
39
45
  const encoder = new TextEncoder();
40
46
 
@@ -101,6 +107,11 @@ export function boundedDiscoveryText(value: unknown, hint: string): string {
101
107
  export interface CatalogSearchArgs {
102
108
  query?: string;
103
109
  connector?: string;
110
+ /**
111
+ * Result classification only; never changes which tools exist or what may
112
+ * execute. Omitted and "all" preserve the complete configured catalog.
113
+ */
114
+ safety?: "readOnly" | "approvalRequired" | "all";
104
115
  limit?: number;
105
116
  offset?: number;
106
117
  fullDescriptions?: boolean;
@@ -109,6 +120,35 @@ export interface CatalogSearchArgs {
109
120
  includeSchemaKeys?: boolean;
110
121
  }
111
122
 
123
+ function discoverySafety(
124
+ value: unknown,
125
+ ): "readOnly" | "approvalRequired" | "all" {
126
+ if (value === undefined) return "all";
127
+ if (
128
+ value !== "readOnly" &&
129
+ value !== "approvalRequired" &&
130
+ value !== "all"
131
+ ) {
132
+ throw new DiscoveryPolicyError(
133
+ "invalid_args",
134
+ 'safety must be "readOnly", "approvalRequired", or "all".',
135
+ );
136
+ }
137
+ return value;
138
+ }
139
+
140
+ function toolsForSafety(
141
+ tools: ToolDef[],
142
+ safety: "readOnly" | "approvalRequired" | "all",
143
+ ): ToolDef[] {
144
+ if (safety === "all") return tools;
145
+ return tools.filter((tool) =>
146
+ safety === "readOnly"
147
+ ? isExplicitlyReadOnly(tool)
148
+ : !isExplicitlyReadOnly(tool),
149
+ );
150
+ }
151
+
112
152
  export interface CatalogDescribeArgs {
113
153
  addresses?: unknown;
114
154
  format?: "compact" | "json";
@@ -124,6 +164,8 @@ interface CatalogSearchEntry {
124
164
  description?: string;
125
165
  inputSchema?: unknown;
126
166
  outputSchema?: unknown;
167
+ inputSchemaTruncated?: true;
168
+ outputSchemaTruncated?: true;
127
169
  inputKeys?: string[];
128
170
  requiredInputKeys?: string[];
129
171
  outputKeys?: string[];
@@ -165,6 +207,16 @@ export interface CatalogSearchPage {
165
207
  hasMore: boolean;
166
208
  nextOffset?: number;
167
209
  matchMode?: "partial";
210
+ queryAnalysis?: {
211
+ representedTerms: string[];
212
+ otherResultTerms: string[];
213
+ unmatchedTerms: string[];
214
+ truncated?: true;
215
+ connectorScope?: string;
216
+ unknownConnector?: true;
217
+ unavailableConnectorCount?: number;
218
+ guidance?: string;
219
+ };
168
220
  }
169
221
 
170
222
  export interface CatalogDescription {
@@ -203,6 +255,15 @@ function renderSchema(schema: JsonSchema, format: "compact" | "json"): unknown {
203
255
  return format === "json" ? schema : compactSchema(schema);
204
256
  }
205
257
 
258
+ function renderSearchSchema(
259
+ schema: JsonSchema,
260
+ format: "compact" | "json",
261
+ ): { schema: unknown; truncated: boolean } {
262
+ if (format === "json") return { schema, truncated: false };
263
+ const compact = compactDiscoverySchema(schema);
264
+ return { schema: compact.text, truncated: compact.truncated };
265
+ }
266
+
206
267
  /**
207
268
  * Request-local catalog operations shared by MCP meta-tools and code mode.
208
269
  * Successful catalogs may be reused inside this request; failures are not
@@ -397,12 +458,16 @@ export class CatalogService {
397
458
  async search(args: CatalogSearchArgs): Promise<CatalogSearchPage> {
398
459
  const query = args.query ?? "";
399
460
  const retrievalQuery = lexicalSearchQuery(query);
461
+ const safety = discoverySafety(args.safety);
400
462
  const limit = discoverySearchLimit(args.limit);
401
463
  const offset = Math.max(0, Math.trunc(args.offset ?? 0));
464
+ const scopedConnector = args.connector
465
+ ? this.registry.getConnector(args.connector)
466
+ : undefined;
402
467
  const connectors = args.connector
403
- ? [this.registry.getConnector(args.connector)].filter(
404
- (connector): connector is Connector => Boolean(connector),
405
- )
468
+ ? scopedConnector
469
+ ? [scopedConnector]
470
+ : []
406
471
  : this.registry.listConnectors();
407
472
  const catalogs = await mapSettledWithConcurrency(
408
473
  connectors,
@@ -413,6 +478,14 @@ export class CatalogService {
413
478
  `search_tools probe of "${connector.id}"`,
414
479
  ),
415
480
  );
481
+ const searchableCatalogs = catalogs.map((catalog) =>
482
+ catalog.status === "fulfilled"
483
+ ? {
484
+ status: "fulfilled" as const,
485
+ value: toolsForSafety(catalog.value, safety),
486
+ }
487
+ : catalog,
488
+ );
416
489
  const matches: Array<{
417
490
  connector: Connector;
418
491
  tool: ToolDef;
@@ -421,7 +494,7 @@ export class CatalogService {
421
494
  }> = [];
422
495
  let matchMode: "all" | "partial" = "all";
423
496
  const statistics = lexicalCorpusStatistics(
424
- catalogs.flatMap((catalog) =>
497
+ searchableCatalogs.flatMap((catalog) =>
425
498
  catalog.status === "fulfilled" ? [catalog.value] : [],
426
499
  ),
427
500
  retrievalQuery,
@@ -429,7 +502,7 @@ export class CatalogService {
429
502
  const collectMatches = (mode: "all" | "partial") => {
430
503
  matches.length = 0;
431
504
  let orderBase = 0;
432
- catalogs.forEach((catalog, connectorIndex) => {
505
+ searchableCatalogs.forEach((catalog, connectorIndex) => {
433
506
  const connector = connectors[connectorIndex];
434
507
  if (!connector) {
435
508
  throw new Error("Catalog result has no corresponding connector");
@@ -459,9 +532,17 @@ export class CatalogService {
459
532
  collectMatches(matchMode);
460
533
  }
461
534
  matches.sort((a, b) => b.score - a.score || a.order - b.order);
462
- const entries = matches.slice(offset, offset + limit).map((match) => {
535
+ const pageMatches = matches.slice(offset, offset + limit);
536
+ const entries = pageMatches.map((match) => {
463
537
  const input = match.tool.inputSchema ?? { type: "object" };
464
- const description = summarizeDescription(
538
+ const renderedInput = args.includeSchemas
539
+ ? renderSearchSchema(input, args.includeSchemas)
540
+ : undefined;
541
+ const renderedOutput =
542
+ args.includeSchemas && match.tool.outputSchema
543
+ ? renderSearchSchema(match.tool.outputSchema, args.includeSchemas)
544
+ : undefined;
545
+ const description = summarizeDiscoveryDescription(
465
546
  match.tool.description,
466
547
  args.fullDescriptions === true,
467
548
  );
@@ -476,16 +557,20 @@ export class CatalogService {
476
557
  ...(description !== undefined ? { description } : {}),
477
558
  ...(args.includeSchemas
478
559
  ? {
479
- inputSchema:
480
- renderSchema(input, args.includeSchemas),
560
+ inputSchema: renderedInput?.schema,
481
561
  }
482
562
  : {}),
563
+ ...(renderedInput?.truncated
564
+ ? { inputSchemaTruncated: true as const }
565
+ : {}),
483
566
  ...(args.includeSchemas && match.tool.outputSchema
484
567
  ? {
485
- outputSchema:
486
- renderSchema(match.tool.outputSchema, args.includeSchemas),
568
+ outputSchema: renderedOutput?.schema,
487
569
  }
488
570
  : {}),
571
+ ...(renderedOutput?.truncated
572
+ ? { outputSchemaTruncated: true as const }
573
+ : {}),
489
574
  ...(args.includeSchemas && args.includeSchemaKeys
490
575
  ? schemaKeyMetadata(input, match.tool.outputSchema)
491
576
  : {}),
@@ -499,6 +584,62 @@ export class CatalogService {
499
584
  offset + entries.length < matches.length
500
585
  ? offset + entries.length
501
586
  : undefined;
587
+ const queryTerms = lexicalQueryTerms(retrievalQuery);
588
+ const analyzedTerms = queryTerms.slice(0, MAX_QUERY_ANALYSIS_TERMS);
589
+ const displayTerm = (term: string) =>
590
+ term.length <= MAX_QUERY_ANALYSIS_TERM_LENGTH
591
+ ? term
592
+ : `${term.slice(0, MAX_QUERY_ANALYSIS_TERM_LENGTH - 1)}…`;
593
+ const pageTools = new Set(pageMatches.map((match) => match.tool));
594
+ const matchingTools = (term: string) =>
595
+ new Set([
596
+ ...(statistics.nameMatches.get(term) ?? []),
597
+ ...(statistics.descriptionMatches.get(term) ?? []),
598
+ ]);
599
+ const representedTerms: string[] = [];
600
+ const otherResultTerms: string[] = [];
601
+ const unmatchedTerms: string[] = [];
602
+ for (const term of analyzedTerms) {
603
+ const termTools = matchingTools(term);
604
+ if ([...termTools].some((tool) => pageTools.has(tool))) {
605
+ representedTerms.push(displayTerm(term));
606
+ } else if (termTools.size > 0) {
607
+ otherResultTerms.push(displayTerm(term));
608
+ } else {
609
+ unmatchedTerms.push(displayTerm(term));
610
+ }
611
+ }
612
+ const unavailableCatalogs = catalogs.filter(
613
+ (catalog) => catalog.status === "rejected",
614
+ ).length;
615
+ const safetyLabel =
616
+ safety === "readOnly"
617
+ ? "read-only "
618
+ : safety === "approvalRequired"
619
+ ? "approval-required "
620
+ : "";
621
+ const filterRecovery =
622
+ safety === "all" ? "" : " Change safety to inspect the other tools.";
623
+ const guidance =
624
+ queryTerms.length === 0
625
+ ? undefined
626
+ : matches.length === 0
627
+ ? args.connector && !scopedConnector
628
+ ? `Connector "${args.connector}" is not configured in this deployment. Omit connector to search all configured tools.`
629
+ : scopedConnector
630
+ ? unavailableCatalogs > 0
631
+ ? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Retry later.`
632
+ : `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
633
+ : unavailableCatalogs === 0
634
+ ? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
635
+ : `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}`
636
+ : matchMode === "partial"
637
+ ? scopedConnector
638
+ ? `No single tool on connector "${scopedConnector.id}" matched every term. Split distinct intents into separate searches.`
639
+ : unavailableCatalogs === 0
640
+ ? "No single tool matched every term. Split distinct intents into separate searches."
641
+ : "No single tool matched every term in the catalogs that answered. Split distinct intents into separate searches."
642
+ : undefined;
502
643
  return {
503
644
  entries,
504
645
  total: matches.length,
@@ -509,6 +650,29 @@ export class CatalogService {
509
650
  ...(matchMode === "partial" && matches.length > 0
510
651
  ? { matchMode }
511
652
  : {}),
653
+ ...(queryTerms.length > 0 && matchMode === "partial"
654
+ ? {
655
+ queryAnalysis: {
656
+ representedTerms,
657
+ otherResultTerms,
658
+ unmatchedTerms,
659
+ ...(queryTerms.length > analyzedTerms.length ||
660
+ analyzedTerms.some(
661
+ (term) => term.length > MAX_QUERY_ANALYSIS_TERM_LENGTH,
662
+ )
663
+ ? { truncated: true as const }
664
+ : {}),
665
+ ...(args.connector ? { connectorScope: args.connector } : {}),
666
+ ...(args.connector && !scopedConnector
667
+ ? { unknownConnector: true as const }
668
+ : {}),
669
+ ...(unavailableCatalogs > 0
670
+ ? { unavailableConnectorCount: unavailableCatalogs }
671
+ : {}),
672
+ ...(guidance ? { guidance } : {}),
673
+ },
674
+ }
675
+ : {}),
512
676
  };
513
677
  }
514
678
 
@@ -592,7 +756,6 @@ export function groupedSearchResult(page: CatalogSearchPage) {
592
756
  const groups: Array<{
593
757
  id: string;
594
758
  title?: string;
595
- description?: string;
596
759
  guide?: string;
597
760
  tools: CatalogSearchEntry["tool"][];
598
761
  }> = [];
@@ -605,9 +768,6 @@ export function groupedSearchResult(page: CatalogSearchPage) {
605
768
  const group: (typeof groups)[number] = {
606
769
  id: entry.connector.id,
607
770
  ...(entry.connector.title ? { title: entry.connector.title } : {}),
608
- ...(entry.connector.description !== undefined
609
- ? { description: entry.connector.description }
610
- : {}),
611
771
  ...(entry.guide ? { guide: entry.guide } : {}),
612
772
  tools: [],
613
773
  };
@@ -624,6 +784,7 @@ export function groupedSearchResult(page: CatalogSearchPage) {
624
784
  hasMore: page.hasMore,
625
785
  ...(page.nextOffset !== undefined ? { nextOffset: page.nextOffset } : {}),
626
786
  ...(page.matchMode ? { matchMode: page.matchMode } : {}),
787
+ ...(page.queryAnalysis ? { queryAnalysis: page.queryAnalysis } : {}),
627
788
  };
628
789
  }
629
790
 
@@ -636,5 +797,6 @@ export function flatSearchResult(page: CatalogSearchPage) {
636
797
  hasMore: page.hasMore,
637
798
  ...(page.nextOffset !== undefined ? { nextOffset: page.nextOffset } : {}),
638
799
  ...(page.matchMode ? { matchMode: page.matchMode } : {}),
800
+ ...(page.queryAnalysis ? { queryAnalysis: page.queryAnalysis } : {}),
639
801
  };
640
802
  }