@zackbart/connecta 0.7.0 → 0.7.3
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 +110 -0
- package/README.md +2 -1
- package/dist/connectors/remote-mcp.d.ts +24 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +208 -88
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credential-health.d.ts +8 -5
- package/dist/credential-health.d.ts.map +1 -1
- package/dist/credential-health.js +20 -13
- package/dist/credential-health.js.map +1 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +10 -8
- package/dist/execute.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +57 -5
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +25 -0
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +162 -20
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +18 -22
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +33 -21
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +18 -7
- package/dist/server.js.map +1 -1
- package/dist/timeout.d.ts +9 -4
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +34 -4
- package/dist/timeout.js.map +1 -1
- package/dist/toolkits.d.ts +8 -0
- package/dist/toolkits.d.ts.map +1 -1
- package/dist/toolkits.js +3 -0
- package/dist/toolkits.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +12 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +187 -6
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/connectors/remote-mcp.ts +269 -93
- package/src/credential-health.ts +20 -18
- package/src/execute.ts +18 -7
- package/src/executors/quickjs.ts +65 -5
- package/src/index.ts +7 -2
- package/src/meta-tools.ts +226 -43
- package/src/registry.ts +48 -20
- package/src/server.ts +20 -9
- package/src/timeout.ts +41 -4
- package/src/toolkits.ts +11 -0
- package/src/types.ts +2 -2
- package/src/ui.ts +212 -11
- package/src/version.ts +1 -1
package/src/execute.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { z } from "zod";
|
|
|
3
3
|
import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
|
|
4
4
|
import { recordToolActivity, type ActivityRequestContext } from "./activity.js";
|
|
5
5
|
import {
|
|
6
|
+
assertDiscoveryResultSize,
|
|
7
|
+
discoveryAddresses,
|
|
8
|
+
discoverySearchLimit,
|
|
6
9
|
errorResult,
|
|
7
10
|
jsonResult,
|
|
8
11
|
serializeResultText,
|
|
@@ -369,7 +372,7 @@ export async function buildSandboxProviders(
|
|
|
369
372
|
}
|
|
370
373
|
matches.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
371
374
|
const offset = Math.max(0, Math.trunc(args.offset ?? 0));
|
|
372
|
-
const limit =
|
|
375
|
+
const limit = discoverySearchLimit(args.limit);
|
|
373
376
|
const page = matches.slice(offset, offset + limit).map((match) => {
|
|
374
377
|
const input = match.tool.inputSchema ?? { type: "object" };
|
|
375
378
|
return {
|
|
@@ -404,7 +407,7 @@ export async function buildSandboxProviders(
|
|
|
404
407
|
offset + page.length < matches.length
|
|
405
408
|
? offset + page.length
|
|
406
409
|
: undefined;
|
|
407
|
-
|
|
410
|
+
const result = {
|
|
408
411
|
tools: page,
|
|
409
412
|
total: matches.length,
|
|
410
413
|
offset,
|
|
@@ -412,6 +415,11 @@ export async function buildSandboxProviders(
|
|
|
412
415
|
hasMore: nextOffset !== undefined,
|
|
413
416
|
...(nextOffset !== undefined ? { nextOffset } : {}),
|
|
414
417
|
};
|
|
418
|
+
assertDiscoveryResultSize(
|
|
419
|
+
result,
|
|
420
|
+
"Request a smaller limit, omit fullDescriptions, or use compact schemas.",
|
|
421
|
+
);
|
|
422
|
+
return result;
|
|
415
423
|
},
|
|
416
424
|
describe: async (raw: unknown) => {
|
|
417
425
|
const args = (raw ?? {}) as {
|
|
@@ -419,12 +427,10 @@ export async function buildSandboxProviders(
|
|
|
419
427
|
format?: "compact" | "json";
|
|
420
428
|
fullDescriptions?: boolean;
|
|
421
429
|
};
|
|
422
|
-
|
|
423
|
-
throw new Error("addresses must be an array");
|
|
424
|
-
}
|
|
430
|
+
const addresses = discoveryAddresses(args.addresses);
|
|
425
431
|
const format = args.format ?? "compact";
|
|
426
|
-
|
|
427
|
-
tools:
|
|
432
|
+
const result = {
|
|
433
|
+
tools: addresses.map((rawAddress) => {
|
|
428
434
|
const address = String(rawAddress);
|
|
429
435
|
const resolved = registry.resolveAddress(address);
|
|
430
436
|
if (!resolved) {
|
|
@@ -457,6 +463,11 @@ export async function buildSandboxProviders(
|
|
|
457
463
|
};
|
|
458
464
|
}),
|
|
459
465
|
};
|
|
466
|
+
assertDiscoveryResultSize(
|
|
467
|
+
result,
|
|
468
|
+
'Split the address list or use format: "compact".',
|
|
469
|
+
);
|
|
470
|
+
return result;
|
|
460
471
|
},
|
|
461
472
|
},
|
|
462
473
|
});
|
package/src/executors/quickjs.ts
CHANGED
|
@@ -40,11 +40,39 @@ const MAX_LOG_ENTRIES = 200;
|
|
|
40
40
|
// keeps the worst case — 200 maxed-out entries — bounded well under a MiB.
|
|
41
41
|
const MAX_LOG_ENTRY_CHARS = 8_000;
|
|
42
42
|
const MAX_LOG_TOTAL_CHARS = 256_000;
|
|
43
|
+
// Keep one host result below the range where quickjs-emscripten@0.32.0 can
|
|
44
|
+
// nondeterministically fail during runtime disposal under concurrent load.
|
|
45
|
+
// This still lets guest code reduce data more than ten times larger than
|
|
46
|
+
// connecta's final response budget.
|
|
47
|
+
const MAX_HOST_RESULT_BYTES = 256 * 1024;
|
|
43
48
|
|
|
44
49
|
function msg(err: unknown): string {
|
|
45
50
|
return err instanceof Error ? err.message : String(err);
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
function exceedsUtf8ByteLimit(value: string, limit: number): boolean {
|
|
54
|
+
let bytes = 0;
|
|
55
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
56
|
+
const code = value.charCodeAt(index);
|
|
57
|
+
if (code <= 0x7f) bytes += 1;
|
|
58
|
+
else if (code <= 0x7ff) bytes += 2;
|
|
59
|
+
else if (
|
|
60
|
+
code >= 0xd800 &&
|
|
61
|
+
code <= 0xdbff &&
|
|
62
|
+
index + 1 < value.length &&
|
|
63
|
+
value.charCodeAt(index + 1) >= 0xdc00 &&
|
|
64
|
+
value.charCodeAt(index + 1) <= 0xdfff
|
|
65
|
+
) {
|
|
66
|
+
bytes += 4;
|
|
67
|
+
index += 1;
|
|
68
|
+
} else {
|
|
69
|
+
bytes += 3;
|
|
70
|
+
}
|
|
71
|
+
if (bytes > limit) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
48
76
|
/** Normalize model output into an async-arrow expression: strip markdown fences, wrap bare bodies. */
|
|
49
77
|
export function normalizeCode(code: string): string {
|
|
50
78
|
let c = code.trim();
|
|
@@ -118,6 +146,25 @@ function armWake(bridge: HostBridge): void {
|
|
|
118
146
|
});
|
|
119
147
|
}
|
|
120
148
|
|
|
149
|
+
function waitForHostOrDeadline(
|
|
150
|
+
waitForSettle: Promise<void>,
|
|
151
|
+
remainingMs: number,
|
|
152
|
+
): Promise<boolean> {
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
let done = false;
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
done = true;
|
|
157
|
+
resolve(false);
|
|
158
|
+
}, remainingMs);
|
|
159
|
+
void waitForSettle.then(() => {
|
|
160
|
+
if (done) return;
|
|
161
|
+
done = true;
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
resolve(true);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
121
168
|
function installBridge(
|
|
122
169
|
ctx: QuickJSContext,
|
|
123
170
|
providers: ExecutorProvider[],
|
|
@@ -176,7 +223,20 @@ function installBridge(
|
|
|
176
223
|
if (!f) throw new Error(`Unknown function ${ns}.${fn}`);
|
|
177
224
|
const args = JSON.parse(argsJson) as unknown[];
|
|
178
225
|
const value = await f(...args);
|
|
179
|
-
|
|
226
|
+
let json: string;
|
|
227
|
+
try {
|
|
228
|
+
json = JSON.stringify({ ok: true, value });
|
|
229
|
+
} catch (err) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Host result from ${ns}.${fn} could not be serialized: ${msg(err)}`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (exceedsUtf8ByteLimit(json, MAX_HOST_RESULT_BYTES)) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Host result from ${ns}.${fn} exceeds the ${MAX_HOST_RESULT_BYTES}-byte serialized bridge limit.`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return json;
|
|
180
240
|
} catch (err) {
|
|
181
241
|
return JSON.stringify({ ok: false, error: msg(err) });
|
|
182
242
|
}
|
|
@@ -326,10 +386,10 @@ export function quickJsExecutor(
|
|
|
326
386
|
if (remaining <= 0) {
|
|
327
387
|
return { result: undefined, error: timeoutError };
|
|
328
388
|
}
|
|
329
|
-
const settled = await
|
|
330
|
-
bridge.waitForSettle
|
|
331
|
-
|
|
332
|
-
|
|
389
|
+
const settled = await waitForHostOrDeadline(
|
|
390
|
+
bridge.waitForSettle,
|
|
391
|
+
remaining,
|
|
392
|
+
);
|
|
333
393
|
if (settled) armWake(bridge);
|
|
334
394
|
else {
|
|
335
395
|
return { result: undefined, error: timeoutError };
|
package/src/index.ts
CHANGED
|
@@ -82,7 +82,8 @@ export interface ConnectaDiscoveryConfig {
|
|
|
82
82
|
* Deadline (ms) for each downstream probe/catalog call fanned out by
|
|
83
83
|
* `list_connectors`, `search_tools`, and `describe_tools`. Defaults to
|
|
84
84
|
* 30_000. A timed-out connector degrades independently; this does not apply
|
|
85
|
-
* to tool calls
|
|
85
|
+
* to tool calls. Catalog walks receive the same cancellation signal, which
|
|
86
|
+
* aborts an in-flight page where supported and prevents another from starting.
|
|
86
87
|
*/
|
|
87
88
|
probeTimeoutMs?: number;
|
|
88
89
|
}
|
|
@@ -574,7 +575,11 @@ export type {
|
|
|
574
575
|
CredentialHealthRecord,
|
|
575
576
|
} from "./credential-health.js";
|
|
576
577
|
|
|
577
|
-
export type {
|
|
578
|
+
export type {
|
|
579
|
+
RemoteMcpOptions,
|
|
580
|
+
RemoteMcpAuth,
|
|
581
|
+
RemoteMcpRedirectPolicy,
|
|
582
|
+
} from "./connectors/remote-mcp.js";
|
|
578
583
|
export type { ApiOptions, ApiTool } from "./connectors/api.js";
|
|
579
584
|
export type {
|
|
580
585
|
Connector,
|
package/src/meta-tools.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
DEFAULT_PROBE_TIMEOUT_MS,
|
|
32
32
|
normalizeTimeoutMs,
|
|
33
|
-
|
|
33
|
+
withAbortableTimeout,
|
|
34
34
|
} from "./timeout.js";
|
|
35
35
|
import { credentialVerdictApplies } from "./credential-health.js";
|
|
36
36
|
import type { ConnectorStatus, KVStorage, ToolDef } from "./types.js";
|
|
@@ -65,12 +65,126 @@ function msg(err: unknown): string {
|
|
|
65
65
|
return err instanceof Error ? err.message : String(err);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const DEFAULT_SEARCH_LIMIT = 25;
|
|
68
|
+
export const DEFAULT_SEARCH_LIMIT = 25;
|
|
69
|
+
/**
|
|
70
|
+
* A discovery page is for choosing the next tool, not exporting the catalog.
|
|
71
|
+
* One hundred leaves room for broad browsing while keeping each deliberate
|
|
72
|
+
* page far below the catalog sizes Connecta supports.
|
|
73
|
+
*/
|
|
74
|
+
export const MAX_SEARCH_LIMIT = 100;
|
|
75
|
+
/** Same one-request work bound for address-based discovery. */
|
|
76
|
+
export const MAX_DESCRIBE_ADDRESSES = 100;
|
|
77
|
+
/**
|
|
78
|
+
* Final UTF-8 ceiling for a generated search/describe response. The count
|
|
79
|
+
* limits are the ordinary guard; this catches unusually large full schemas or
|
|
80
|
+
* descriptions that make even a bounded page expensive.
|
|
81
|
+
*/
|
|
82
|
+
export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
|
|
69
83
|
const enc = new TextEncoder();
|
|
70
84
|
const dec = new TextDecoder();
|
|
71
85
|
|
|
72
86
|
type ErrorDetails = CallErrorDetails;
|
|
73
87
|
|
|
88
|
+
export class DiscoveryPolicyError extends Error {
|
|
89
|
+
constructor(
|
|
90
|
+
readonly code: "invalid_args" | "result_too_large",
|
|
91
|
+
message: string,
|
|
92
|
+
) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = "DiscoveryPolicyError";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Validate before ranking so a huge page request does no proportional work. */
|
|
99
|
+
export function discoverySearchLimit(value: unknown): number {
|
|
100
|
+
if (value === undefined) return DEFAULT_SEARCH_LIMIT;
|
|
101
|
+
if (
|
|
102
|
+
typeof value !== "number" ||
|
|
103
|
+
!Number.isInteger(value) ||
|
|
104
|
+
value < 1 ||
|
|
105
|
+
value > MAX_SEARCH_LIMIT
|
|
106
|
+
) {
|
|
107
|
+
throw new DiscoveryPolicyError(
|
|
108
|
+
"invalid_args",
|
|
109
|
+
`limit must be a whole number from 1 through ${MAX_SEARCH_LIMIT}. Page through larger catalogs with offset.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Validate the raw list so duplicate addresses consume the same bound. */
|
|
116
|
+
export function discoveryAddresses(value: unknown): unknown[] {
|
|
117
|
+
if (!Array.isArray(value)) {
|
|
118
|
+
throw new DiscoveryPolicyError(
|
|
119
|
+
"invalid_args",
|
|
120
|
+
"addresses must be an array.",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (value.length > MAX_DESCRIBE_ADDRESSES) {
|
|
124
|
+
throw new DiscoveryPolicyError(
|
|
125
|
+
"invalid_args",
|
|
126
|
+
`addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across describe_tools calls.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Serialize once and count the exact bytes jsonResult would emit. */
|
|
133
|
+
function boundedDiscoveryText(
|
|
134
|
+
value: unknown,
|
|
135
|
+
hint: string,
|
|
136
|
+
): string {
|
|
137
|
+
const text = JSON.stringify(value, null, 2);
|
|
138
|
+
if (text === undefined) {
|
|
139
|
+
throw new TypeError("Discovery result is not JSON-serializable.");
|
|
140
|
+
}
|
|
141
|
+
const bytes = enc.encode(text).length;
|
|
142
|
+
if (bytes > MAX_DISCOVERY_RESULT_BYTES) {
|
|
143
|
+
throw new DiscoveryPolicyError(
|
|
144
|
+
"result_too_large",
|
|
145
|
+
`Discovery result is ${bytes} UTF-8 bytes, over the ${MAX_DISCOVERY_RESULT_BYTES}-byte ceiling. ${hint}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return text;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Apply the same final result guard to code-mode discovery helpers. */
|
|
152
|
+
export function assertDiscoveryResultSize(
|
|
153
|
+
value: unknown,
|
|
154
|
+
hint: string,
|
|
155
|
+
): void {
|
|
156
|
+
boundedDiscoveryText(value, hint);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
|
|
160
|
+
const result = jsonResult({
|
|
161
|
+
error: {
|
|
162
|
+
code: error.code,
|
|
163
|
+
message: error.message,
|
|
164
|
+
retryable: false,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
result.isError = true;
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function discoveryResult(value: unknown, hint: string): ToolResult {
|
|
172
|
+
try {
|
|
173
|
+
const text = boundedDiscoveryText(value, hint);
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text }],
|
|
176
|
+
...(value !== null && typeof value === "object" && !Array.isArray(value)
|
|
177
|
+
? { structuredContent: value as Record<string, unknown> }
|
|
178
|
+
: {}),
|
|
179
|
+
};
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err instanceof DiscoveryPolicyError) {
|
|
182
|
+
return discoveryErrorResult(err);
|
|
183
|
+
}
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
74
188
|
/**
|
|
75
189
|
* The longest the engine will park a synchronous inbound request in *waiting
|
|
76
190
|
* alone*. The engine already treats ~15 s as the outer bound of one reasonable
|
|
@@ -483,6 +597,18 @@ export function createMetaTools(
|
|
|
483
597
|
// identity lets remote connectors reuse one downstream client inside that
|
|
484
598
|
// request without leaking request-bound I/O into the next one.
|
|
485
599
|
const requestScope = {};
|
|
600
|
+
const withProbeDeadline = <T>(
|
|
601
|
+
label: string,
|
|
602
|
+
operation: (options: {
|
|
603
|
+
signal: AbortSignal;
|
|
604
|
+
timeoutMs: number;
|
|
605
|
+
}) => Promise<T>,
|
|
606
|
+
) =>
|
|
607
|
+
withAbortableTimeout(
|
|
608
|
+
(signal) => operation({ signal, timeoutMs: probeTimeoutMs }),
|
|
609
|
+
probeTimeoutMs,
|
|
610
|
+
label,
|
|
611
|
+
);
|
|
486
612
|
|
|
487
613
|
interface RunCallOutcome {
|
|
488
614
|
toolResult: ToolResult;
|
|
@@ -793,8 +919,8 @@ export function createMetaTools(
|
|
|
793
919
|
// call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
|
|
794
920
|
const connectors = registry.listConnectors();
|
|
795
921
|
const scope = probe ? {} : requestScope;
|
|
796
|
-
const
|
|
797
|
-
|
|
922
|
+
const pending = connectors.map(
|
|
923
|
+
async (c) => {
|
|
798
924
|
const statusStarted = Date.now();
|
|
799
925
|
const observed = registry.healthFor(c.id);
|
|
800
926
|
const verdict = await registry.credentialHealthFor(c.id);
|
|
@@ -803,10 +929,10 @@ export function createMetaTools(
|
|
|
803
929
|
| { state: "ok" | "error" | "unknown"; message?: string };
|
|
804
930
|
if (probe) {
|
|
805
931
|
try {
|
|
806
|
-
status = await
|
|
807
|
-
registry.statusFor(c.id, baseUrl, scope),
|
|
808
|
-
probeTimeoutMs,
|
|
932
|
+
status = await withProbeDeadline(
|
|
809
933
|
`list_connectors probe of "${c.id}"`,
|
|
934
|
+
(options) =>
|
|
935
|
+
registry.statusFor(c.id, baseUrl, scope, options),
|
|
810
936
|
);
|
|
811
937
|
} catch (err) {
|
|
812
938
|
// A probe that outran probeTimeoutMs (or otherwise threw)
|
|
@@ -890,14 +1016,45 @@ export function createMetaTools(
|
|
|
890
1016
|
// the first (now stale) authorization URL.
|
|
891
1017
|
if (probe && status.state === "ok") {
|
|
892
1018
|
try {
|
|
893
|
-
tools = await
|
|
894
|
-
registry.refreshTools(c.id, baseUrl, scope),
|
|
895
|
-
probeTimeoutMs,
|
|
1019
|
+
tools = await withProbeDeadline(
|
|
896
1020
|
`list_connectors catalog refresh of "${c.id}"`,
|
|
1021
|
+
(options) =>
|
|
1022
|
+
registry.refreshTools(c.id, baseUrl, scope, options),
|
|
897
1023
|
);
|
|
898
1024
|
registry.recordSuccess(c.id, Date.now() - statusStarted);
|
|
899
1025
|
} catch (err) {
|
|
900
|
-
|
|
1026
|
+
const details = classifyCallError(err);
|
|
1027
|
+
if (details.code === "auth_required") {
|
|
1028
|
+
let authStatus: ConnectorStatus | undefined;
|
|
1029
|
+
try {
|
|
1030
|
+
authStatus = await withProbeDeadline(
|
|
1031
|
+
`list_connectors authorization status of "${c.id}"`,
|
|
1032
|
+
(options) =>
|
|
1033
|
+
registry.statusFor(c.id, baseUrl, scope, options),
|
|
1034
|
+
);
|
|
1035
|
+
} catch {
|
|
1036
|
+
// The typed auth verdict is still authoritative; this second
|
|
1037
|
+
// read exists only to recover the connector's pending URL.
|
|
1038
|
+
}
|
|
1039
|
+
status =
|
|
1040
|
+
authStatus?.state === "auth_required"
|
|
1041
|
+
? authStatus
|
|
1042
|
+
: {
|
|
1043
|
+
state: "auth_required" as const,
|
|
1044
|
+
message: details.message,
|
|
1045
|
+
};
|
|
1046
|
+
await registry.recordCredentialHealth(c.id, {
|
|
1047
|
+
state: "auth_required",
|
|
1048
|
+
checkedAt,
|
|
1049
|
+
...(status.message ? { message: status.message } : {}),
|
|
1050
|
+
...("authorizationUrl" in status &&
|
|
1051
|
+
status.authorizationUrl
|
|
1052
|
+
? { authorizationUrl: status.authorizationUrl }
|
|
1053
|
+
: {}),
|
|
1054
|
+
});
|
|
1055
|
+
} else {
|
|
1056
|
+
status = { state: "error" as const, message: msg(err) };
|
|
1057
|
+
}
|
|
901
1058
|
registry.recordFailure(c.id, Date.now() - statusStarted, err);
|
|
902
1059
|
}
|
|
903
1060
|
}
|
|
@@ -922,24 +1079,38 @@ export function createMetaTools(
|
|
|
922
1079
|
: {}),
|
|
923
1080
|
...(status.message ? { message: status.message } : {}),
|
|
924
1081
|
};
|
|
925
|
-
}
|
|
926
|
-
)
|
|
927
|
-
|
|
928
|
-
await Promise.all(
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1082
|
+
},
|
|
1083
|
+
);
|
|
1084
|
+
if (!probe) {
|
|
1085
|
+
return jsonResult({ connectors: await Promise.all(pending) });
|
|
1086
|
+
}
|
|
1087
|
+
const settled = await Promise.allSettled(pending);
|
|
1088
|
+
await Promise.all(
|
|
1089
|
+
connectors.map((connector) =>
|
|
1090
|
+
closeConnectorScope(
|
|
1091
|
+
connector,
|
|
1092
|
+
registry.contextFor(connector.id, baseUrl, scope),
|
|
934
1093
|
),
|
|
935
|
-
)
|
|
1094
|
+
),
|
|
1095
|
+
);
|
|
1096
|
+
const out = settled.map((result) => {
|
|
1097
|
+
if (result.status === "rejected") throw result.reason;
|
|
1098
|
+
return result.value;
|
|
936
1099
|
});
|
|
937
1100
|
return jsonResult({ connectors: out });
|
|
938
1101
|
},
|
|
939
1102
|
|
|
940
1103
|
async searchTools(args: SearchArgs): Promise<ToolResult> {
|
|
941
1104
|
const q = args.query ?? "";
|
|
942
|
-
|
|
1105
|
+
let limit: number;
|
|
1106
|
+
try {
|
|
1107
|
+
limit = discoverySearchLimit(args.limit);
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
if (err instanceof DiscoveryPolicyError) {
|
|
1110
|
+
return discoveryErrorResult(err);
|
|
1111
|
+
}
|
|
1112
|
+
throw err;
|
|
1113
|
+
}
|
|
943
1114
|
const offset = Math.max(0, Math.trunc(args.offset ?? 0));
|
|
944
1115
|
const conns = args.connector
|
|
945
1116
|
? [registry.getConnector(args.connector)].filter(
|
|
@@ -957,10 +1128,10 @@ export function createMetaTools(
|
|
|
957
1128
|
}> = [];
|
|
958
1129
|
const catalogs = await Promise.allSettled(
|
|
959
1130
|
conns.map((c) =>
|
|
960
|
-
|
|
961
|
-
registry.getTools(c.id, baseUrl, requestScope),
|
|
962
|
-
probeTimeoutMs,
|
|
1131
|
+
withProbeDeadline(
|
|
963
1132
|
`search_tools probe of "${c.id}"`,
|
|
1133
|
+
(options) =>
|
|
1134
|
+
registry.getTools(c.id, baseUrl, requestScope, options),
|
|
964
1135
|
),
|
|
965
1136
|
),
|
|
966
1137
|
);
|
|
@@ -1050,17 +1221,28 @@ export function createMetaTools(
|
|
|
1050
1221
|
offset + page.length < matches.length
|
|
1051
1222
|
? offset + page.length
|
|
1052
1223
|
: undefined;
|
|
1053
|
-
return
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1224
|
+
return discoveryResult(
|
|
1225
|
+
{
|
|
1226
|
+
connectors: groups,
|
|
1227
|
+
total: matches.length,
|
|
1228
|
+
offset,
|
|
1229
|
+
limit,
|
|
1230
|
+
hasMore: nextOffset !== undefined,
|
|
1231
|
+
...(nextOffset !== undefined ? { nextOffset } : {}),
|
|
1232
|
+
},
|
|
1233
|
+
"Request a smaller limit, omit fullDescriptions, or use compact schemas.",
|
|
1234
|
+
);
|
|
1061
1235
|
},
|
|
1062
1236
|
|
|
1063
1237
|
async describeTools(args: DescribeArgs): Promise<ToolResult> {
|
|
1238
|
+
try {
|
|
1239
|
+
discoveryAddresses(args.addresses);
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
if (err instanceof DiscoveryPolicyError) {
|
|
1242
|
+
return discoveryErrorResult(err);
|
|
1243
|
+
}
|
|
1244
|
+
throw err;
|
|
1245
|
+
}
|
|
1064
1246
|
const format = args.format ?? "compact";
|
|
1065
1247
|
const resolved = args.addresses.map((address) => ({
|
|
1066
1248
|
address,
|
|
@@ -1075,10 +1257,10 @@ export function createMetaTools(
|
|
|
1075
1257
|
];
|
|
1076
1258
|
const loaded = await Promise.allSettled(
|
|
1077
1259
|
connectorIds.map((id) =>
|
|
1078
|
-
|
|
1079
|
-
registry.getTools(id, baseUrl, requestScope),
|
|
1080
|
-
probeTimeoutMs,
|
|
1260
|
+
withProbeDeadline(
|
|
1081
1261
|
`describe_tools probe of "${id}"`,
|
|
1262
|
+
(options) =>
|
|
1263
|
+
registry.getTools(id, baseUrl, requestScope, options),
|
|
1082
1264
|
),
|
|
1083
1265
|
),
|
|
1084
1266
|
);
|
|
@@ -1131,7 +1313,10 @@ export function createMetaTools(
|
|
|
1131
1313
|
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
1132
1314
|
};
|
|
1133
1315
|
});
|
|
1134
|
-
return
|
|
1316
|
+
return discoveryResult(
|
|
1317
|
+
{ tools: out },
|
|
1318
|
+
'Split the address list or use format: "compact".',
|
|
1319
|
+
);
|
|
1135
1320
|
},
|
|
1136
1321
|
|
|
1137
1322
|
async callTool(args: CallArgs): Promise<ToolResult> {
|
|
@@ -1327,10 +1512,8 @@ export function createMetaTools(
|
|
|
1327
1512
|
|
|
1328
1513
|
const LIST_DESC =
|
|
1329
1514
|
"List connectors with status, cached tool count, and recent real-call health. Use probe=false for a fast inventory; use probe=true (default) only to diagnose live health or authorization.";
|
|
1330
|
-
const SEARCH_DESC =
|
|
1331
|
-
|
|
1332
|
-
const DESCRIBE_DESC =
|
|
1333
|
-
'Inspect known tool addresses when search_tools did not include a sufficient schema. Returns descriptions, input/output schemas, and behavior annotations; format "compact" is the default.';
|
|
1515
|
+
const SEARCH_DESC = `Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all. Pages contain at most ${MAX_SEARCH_LIMIT} tools. includeSchemas="compact" usually removes the describe_tools round trip.`;
|
|
1516
|
+
const DESCRIBE_DESC = `Inspect up to ${MAX_DESCRIBE_ADDRESSES} known tool addresses when search_tools did not include a sufficient schema. Returns descriptions, input/output schemas, and behavior annotations; format "compact" is the default.`;
|
|
1334
1517
|
const CALL_DESC =
|
|
1335
1518
|
'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths, resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
|
|
1336
1519
|
const CALL_DESTRUCTIVE_DESC =
|
|
@@ -1444,7 +1627,7 @@ export function registerMetaTools(
|
|
|
1444
1627
|
inputSchema: {
|
|
1445
1628
|
query: z.string().optional(),
|
|
1446
1629
|
connector: z.string().optional(),
|
|
1447
|
-
limit: z.number().int().positive().optional(),
|
|
1630
|
+
limit: z.number().int().positive().max(MAX_SEARCH_LIMIT).optional(),
|
|
1448
1631
|
offset: z.number().int().nonnegative().optional(),
|
|
1449
1632
|
fullDescriptions: z.boolean().optional(),
|
|
1450
1633
|
includeSchemas: z.enum(["compact", "json"]).optional(),
|
|
@@ -1459,7 +1642,7 @@ export function registerMetaTools(
|
|
|
1459
1642
|
{
|
|
1460
1643
|
description: describedFor(registry, DESCRIBE_DESC, "describe"),
|
|
1461
1644
|
inputSchema: {
|
|
1462
|
-
addresses: z.array(z.string()),
|
|
1645
|
+
addresses: z.array(z.string()).max(MAX_DESCRIBE_ADDRESSES),
|
|
1463
1646
|
format: z.enum(["compact", "json"]).optional(),
|
|
1464
1647
|
fullDescriptions: z.boolean().optional(),
|
|
1465
1648
|
},
|