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