@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.
- package/CHANGELOG.md +135 -0
- package/README.md +47 -148
- package/dist/activity.d.ts +0 -8
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +0 -1
- package/dist/activity.js.map +1 -1
- package/dist/auth/bearer.d.ts +1 -7
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +3 -24
- package/dist/auth/bearer.js.map +1 -1
- package/dist/auth/clerk.d.ts +2 -11
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/auth/clerk.js +31 -46
- package/dist/auth/clerk.js.map +1 -1
- package/dist/call-admission.d.ts.map +1 -1
- package/dist/call-admission.js +9 -4
- package/dist/call-admission.js.map +1 -1
- package/dist/catalog-fingerprint.d.ts +20 -0
- package/dist/catalog-fingerprint.d.ts.map +1 -0
- package/dist/catalog-fingerprint.js +34 -0
- package/dist/catalog-fingerprint.js.map +1 -0
- package/dist/catalog-limits.d.ts +10 -0
- package/dist/catalog-limits.d.ts.map +1 -0
- package/dist/catalog-limits.js +10 -0
- package/dist/catalog-limits.js.map +1 -0
- package/dist/catalog-service.d.ts +137 -0
- package/dist/catalog-service.d.ts.map +1 -0
- package/dist/catalog-service.js +401 -0
- package/dist/catalog-service.js.map +1 -0
- package/dist/catalog.d.ts +6 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +53 -2
- package/dist/catalog.js.map +1 -1
- package/dist/concurrency.d.ts +4 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +31 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +22 -12
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +53 -41
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credentials.d.ts +3 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/errors.d.ts +19 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +10 -16
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +143 -330
- package/dist/execute.js.map +1 -1
- package/dist/executor-result.d.ts +0 -3
- package/dist/executor-result.d.ts.map +1 -1
- package/dist/executor-result.js +3 -3
- package/dist/executor-result.js.map +1 -1
- package/dist/executors/quickjs-child.js +3 -3
- package/dist/executors/quickjs-child.js.map +1 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -1
- package/dist/executors/quickjs-protocol.d.ts.map +1 -1
- package/dist/executors/quickjs-runtime.d.ts.map +1 -1
- package/dist/executors/quickjs-runtime.js +7 -2
- package/dist/executors/quickjs-runtime.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +4 -1
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/favicon.js +2 -2
- package/dist/favicon.js.map +1 -1
- package/dist/index.d.ts +8 -82
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -104
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +101 -0
- package/dist/invocation.d.ts.map +1 -0
- package/dist/invocation.js +405 -0
- package/dist/invocation.js.map +1 -0
- package/dist/meta-tools.d.ts +9 -95
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +235 -725
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +3 -0
- package/dist/operator-ui/generated.d.ts.map +1 -0
- package/dist/operator-ui/generated.js +5 -0
- package/dist/operator-ui/generated.js.map +1 -0
- package/dist/operator-ui/model.d.ts +73 -0
- package/dist/operator-ui/model.d.ts.map +1 -0
- package/dist/operator-ui/model.js +28 -0
- package/dist/operator-ui/model.js.map +1 -0
- package/dist/registry.d.ts +33 -184
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +345 -330
- package/dist/registry.js.map +1 -1
- package/dist/retired-toolkits.d.ts +3 -0
- package/dist/retired-toolkits.d.ts.map +1 -0
- package/dist/retired-toolkits.js +9 -0
- package/dist/retired-toolkits.js.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +170 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/credentials.d.ts +3 -0
- package/dist/routes/credentials.d.ts.map +1 -0
- package/dist/routes/credentials.js +170 -0
- package/dist/routes/credentials.js.map +1 -0
- package/dist/routes/mcp.d.ts +8 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +270 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/oauth.d.ts +4 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +263 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/shared.d.ts +76 -0
- package/dist/routes/shared.d.ts.map +1 -0
- package/dist/routes/shared.js +140 -0
- package/dist/routes/shared.js.map +1 -0
- package/dist/routes/ui.d.ts +3 -0
- package/dist/routes/ui.d.ts.map +1 -0
- package/dist/routes/ui.js +116 -0
- package/dist/routes/ui.js.map +1 -0
- package/dist/server.d.ts +9 -40
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +62 -1322
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +3 -21
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +15 -43
- package/dist/skills.js.map +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +3 -1
- package/dist/storage/file.js.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +3 -1
- package/dist/storage/memory.js.map +1 -1
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +2 -4
- package/dist/timeout.js.map +1 -1
- package/dist/types.d.ts +9 -59
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +3 -94
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +30 -1427
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +15 -8
- package/src/activity.ts +0 -9
- package/src/auth/bearer.ts +4 -33
- package/src/auth/clerk.ts +35 -48
- package/src/call-admission.ts +8 -6
- package/src/catalog-fingerprint.ts +52 -0
- package/src/catalog-limits.ts +11 -0
- package/src/catalog-service.ts +601 -0
- package/src/catalog.ts +57 -2
- package/src/concurrency.ts +39 -0
- package/src/connectors/api.ts +22 -12
- package/src/connectors/remote-mcp.ts +63 -45
- package/src/credentials.ts +2 -2
- package/src/errors.ts +21 -1
- package/src/execute.ts +193 -423
- package/src/executor-result.ts +3 -3
- package/src/executors/quickjs-child.ts +8 -4
- package/src/executors/quickjs-protocol.ts +1 -1
- package/src/executors/quickjs-runtime.ts +5 -1
- package/src/executors/quickjs.ts +11 -8
- package/src/favicon.ts +2 -2
- package/src/index.ts +70 -222
- package/src/invocation.ts +580 -0
- package/src/meta-tools.ts +393 -985
- package/src/operator-ui/browser.css +360 -0
- package/src/operator-ui/browser.ts +987 -0
- package/src/operator-ui/generated.ts +4 -0
- package/src/operator-ui/model.ts +105 -0
- package/src/registry.ts +459 -439
- package/src/retired-toolkits.ts +14 -0
- package/src/routes/activity.ts +215 -0
- package/src/routes/credentials.ts +254 -0
- package/src/routes/mcp.ts +339 -0
- package/src/routes/oauth.ts +322 -0
- package/src/routes/shared.ts +240 -0
- package/src/routes/ui.ts +143 -0
- package/src/server.ts +64 -1731
- package/src/skills.ts +15 -43
- package/src/storage/file.ts +3 -1
- package/src/storage/memory.ts +3 -1
- package/src/timeout.ts +2 -4
- package/src/types.ts +8 -60
- package/src/ui.ts +60 -1538
- package/src/version.ts +1 -1
- package/dist/credential-health.d.ts +0 -227
- package/dist/credential-health.d.ts.map +0 -1
- package/dist/credential-health.js +0 -606
- package/dist/credential-health.js.map +0 -1
- package/dist/toolkits.d.ts +0 -146
- package/dist/toolkits.d.ts.map +0 -1
- package/dist/toolkits.js +0 -322
- package/dist/toolkits.js.map +0 -1
- package/src/credential-health.ts +0 -832
- 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
|
-
...(
|
|
237
|
-
...(
|
|
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
|
+
}
|