@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.
- package/AGENTS.md +113 -0
- package/CHANGELOG.md +83 -0
- package/README.md +62 -12
- package/bin/connecta.mjs +272 -0
- package/dist/access-tokens.d.ts +32 -0
- package/dist/access-tokens.d.ts.map +1 -0
- package/dist/access-tokens.js +225 -0
- package/dist/access-tokens.js.map +1 -0
- package/dist/catalog-service.d.ts +39 -1
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +133 -11
- package/dist/catalog-service.js.map +1 -1
- package/dist/catalog.d.ts +17 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +113 -13
- package/dist/catalog.js.map +1 -1
- package/dist/execute.d.ts +45 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +265 -68
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +1 -5
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +1 -0
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +410 -12
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +2 -2
- package/dist/operator-ui/generated.d.ts.map +1 -1
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/generated.js.map +1 -1
- package/dist/operator-ui/model.d.ts +2 -0
- package/dist/operator-ui/model.d.ts.map +1 -1
- package/dist/operator-ui/model.js.map +1 -1
- package/dist/routes/access-tokens.d.ts +7 -0
- package/dist/routes/access-tokens.d.ts.map +1 -0
- package/dist/routes/access-tokens.js +84 -0
- package/dist/routes/access-tokens.js.map +1 -0
- package/dist/routes/shared.d.ts +3 -0
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js.map +1 -1
- package/dist/routes/ui.d.ts.map +1 -1
- package/dist/routes/ui.js +9 -1
- package/dist/routes/ui.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +5 -0
- package/dist/storage/file.js.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +8 -0
- package/dist/storage/memory.js.map +1 -1
- package/dist/tool-safety.d.ts +10 -0
- package/dist/tool-safety.d.ts.map +1 -0
- package/dist/tool-safety.js +12 -0
- package/dist/tool-safety.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +4 -4
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +44 -1
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +7 -0
- package/documentation/auth.md +58 -0
- package/documentation/call-admission.md +7 -0
- package/documentation/code-first-exploration.md +292 -0
- package/documentation/code-mode.md +697 -0
- package/documentation/connector-guides.md +7 -0
- package/documentation/connectors.md +63 -0
- package/documentation/mcp-2026-07-28.md +46 -0
- package/documentation/meta-tools.md +167 -0
- package/documentation/operations.md +7 -0
- package/documentation/operator-ui.md +7 -0
- package/documentation/request-admission.md +7 -0
- package/documentation/storage-and-credentials.md +54 -0
- package/ethos.md +132 -0
- package/examples/node/README.md +53 -0
- package/examples/node/src/index.ts +73 -0
- package/examples/worker/README.md +160 -0
- package/examples/worker/src/cloudflare-kv.ts +43 -0
- package/examples/worker/src/d1-activity-row.ts +100 -0
- package/examples/worker/src/d1-activity.ts +144 -0
- package/examples/worker/src/index.ts +136 -0
- package/examples/worker/wrangler.jsonc +26 -0
- package/package.json +11 -1
- package/src/access-tokens.ts +289 -0
- package/src/catalog-service.ts +177 -15
- package/src/catalog.ts +143 -12
- package/src/execute.ts +372 -96
- package/src/index.ts +33 -1
- package/src/invocation.ts +1 -8
- package/src/meta-tools.ts +504 -11
- package/src/operator-ui/browser.css +63 -0
- package/src/operator-ui/browser.ts +288 -2
- package/src/operator-ui/generated.ts +2 -2
- package/src/operator-ui/model.ts +6 -0
- package/src/routes/access-tokens.ts +115 -0
- package/src/routes/shared.ts +3 -0
- package/src/routes/ui.ts +9 -0
- package/src/server.ts +5 -0
- package/src/skills.ts +1 -1
- package/src/storage/file.ts +5 -0
- package/src/storage/memory.ts +8 -0
- package/src/tool-safety.ts +15 -0
- package/src/types.ts +5 -0
- package/src/ui.ts +50 -1
- package/src/version.ts +1 -1
- package/templates/node/.env.example +5 -0
- package/templates/node/AGENTS.md +19 -0
- package/templates/node/README.md +33 -0
- package/templates/node/package.json +23 -0
- package/templates/node/src/index.ts +43 -0
- 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
|
+
}
|
package/src/catalog-service.ts
CHANGED
|
@@ -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
|
-
?
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|