@zackbart/connecta 0.7.9 → 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 +48 -0
- package/README.md +67 -144
- 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.map +1 -1
- package/dist/call-admission.js +9 -3
- 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 +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.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 +44 -19
- 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 -321
- 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 +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -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 +8 -86
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +145 -583
- 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 +22 -6
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +328 -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 +63 -1293
- 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 +9 -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 -1393
- 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/auth/bearer.ts +4 -4
- package/src/auth/clerk.ts +1 -1
- package/src/call-admission.ts +8 -5
- 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 +22 -12
- package/src/connectors/remote-mcp.ts +52 -22
- package/src/credential-health.ts +13 -10
- package/src/credentials.ts +2 -2
- package/src/errors.ts +1 -1
- package/src/execute.ts +194 -409
- 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 +49 -17
- package/src/invocation.ts +527 -0
- package/src/meta-tools.ts +366 -906
- 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 +434 -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 +66 -1703
- 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 +9 -2
- package/src/ui.ts +52 -1493
- package/src/version.ts +1 -1
package/src/meta-tools.ts
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type ActivityCallSource,
|
|
7
|
-
type ActivityRequestContext,
|
|
3
|
+
import type {
|
|
4
|
+
ActivityCallSource,
|
|
5
|
+
ActivityRequestContext,
|
|
8
6
|
} from "./activity.js";
|
|
7
|
+
import {
|
|
8
|
+
boundedDiscoveryText,
|
|
9
|
+
CatalogService,
|
|
10
|
+
DiscoveryPolicyError,
|
|
11
|
+
groupedSearchResult,
|
|
12
|
+
MAX_DESCRIBE_ADDRESSES,
|
|
13
|
+
MAX_DISCOVERY_RESULT_BYTES,
|
|
14
|
+
MAX_SEARCH_LIMIT,
|
|
15
|
+
} from "./catalog-service.js";
|
|
16
|
+
import {
|
|
17
|
+
mapSettledWithConcurrency,
|
|
18
|
+
resolveDiscoveryConcurrency,
|
|
19
|
+
} from "./concurrency.js";
|
|
9
20
|
import {
|
|
10
21
|
closeConnectorScope,
|
|
11
22
|
type DeferredWork,
|
|
12
23
|
} from "./connector-scope.js";
|
|
13
|
-
import { unwrapMcpResult } from "./mcp-result.js";
|
|
14
24
|
import {
|
|
15
25
|
classifyCallError,
|
|
16
|
-
ConnectorCallError,
|
|
17
26
|
messageLooksRetryable,
|
|
18
27
|
type CallErrorDetails,
|
|
19
28
|
} from "./errors.js";
|
|
20
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
InvocationService,
|
|
31
|
+
MAX_RETRY_BACKOFF_MS,
|
|
32
|
+
retryBackoffMs,
|
|
33
|
+
type InvocationTiming,
|
|
34
|
+
} from "./invocation.js";
|
|
21
35
|
import {
|
|
22
36
|
isValidMaxResultBytes,
|
|
23
37
|
MIN_MAX_RESULT_BYTES,
|
|
@@ -25,8 +39,6 @@ import {
|
|
|
25
39
|
type RegistryView,
|
|
26
40
|
} from "./registry.js";
|
|
27
41
|
import {
|
|
28
|
-
connectorGuide,
|
|
29
|
-
connectorSkillName,
|
|
30
42
|
hasConnectorGuides,
|
|
31
43
|
listSkills,
|
|
32
44
|
resolveSkill,
|
|
@@ -37,7 +49,16 @@ import {
|
|
|
37
49
|
withAbortableTimeout,
|
|
38
50
|
} from "./timeout.js";
|
|
39
51
|
import { credentialVerdictApplies } from "./credential-health.js";
|
|
40
|
-
import type { ConnectorStatus, KVStorage
|
|
52
|
+
import type { ConnectorStatus, KVStorage } from "./types.js";
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
MAX_DESCRIBE_ADDRESSES,
|
|
56
|
+
MAX_DISCOVERY_RESULT_BYTES,
|
|
57
|
+
MAX_RETRY_BACKOFF_MS,
|
|
58
|
+
MAX_SEARCH_LIMIT,
|
|
59
|
+
DiscoveryPolicyError,
|
|
60
|
+
retryBackoffMs,
|
|
61
|
+
};
|
|
41
62
|
|
|
42
63
|
interface TextContent {
|
|
43
64
|
type: "text";
|
|
@@ -51,6 +72,8 @@ export interface ToolResult {
|
|
|
51
72
|
}
|
|
52
73
|
|
|
53
74
|
const RESULT_TTL_SECONDS = 900;
|
|
75
|
+
const enc = new TextEncoder();
|
|
76
|
+
const dec = new TextDecoder();
|
|
54
77
|
|
|
55
78
|
export function jsonResult(obj: unknown): ToolResult {
|
|
56
79
|
return {
|
|
@@ -69,95 +92,8 @@ function msg(err: unknown): string {
|
|
|
69
92
|
return err instanceof Error ? err.message : String(err);
|
|
70
93
|
}
|
|
71
94
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
* A discovery page is for choosing the next tool, not exporting the catalog.
|
|
75
|
-
* One hundred leaves room for broad browsing while keeping each deliberate
|
|
76
|
-
* page far below the catalog sizes Connecta supports.
|
|
77
|
-
*/
|
|
78
|
-
export const MAX_SEARCH_LIMIT = 100;
|
|
79
|
-
/** Same one-request work bound for address-based discovery. */
|
|
80
|
-
export const MAX_DESCRIBE_ADDRESSES = 100;
|
|
81
|
-
/**
|
|
82
|
-
* Final UTF-8 ceiling for a generated search/describe response. The count
|
|
83
|
-
* limits are the ordinary guard; this catches unusually large full schemas or
|
|
84
|
-
* descriptions that make even a bounded page expensive.
|
|
85
|
-
*/
|
|
86
|
-
export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
|
|
87
|
-
const enc = new TextEncoder();
|
|
88
|
-
const dec = new TextDecoder();
|
|
89
|
-
|
|
90
|
-
type ErrorDetails = CallErrorDetails;
|
|
91
|
-
|
|
92
|
-
export class DiscoveryPolicyError extends Error {
|
|
93
|
-
constructor(
|
|
94
|
-
readonly code: "invalid_args" | "result_too_large",
|
|
95
|
-
message: string,
|
|
96
|
-
) {
|
|
97
|
-
super(message);
|
|
98
|
-
this.name = "DiscoveryPolicyError";
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Validate before ranking so a huge page request does no proportional work. */
|
|
103
|
-
export function discoverySearchLimit(value: unknown): number {
|
|
104
|
-
if (value === undefined) return DEFAULT_SEARCH_LIMIT;
|
|
105
|
-
if (
|
|
106
|
-
typeof value !== "number" ||
|
|
107
|
-
!Number.isInteger(value) ||
|
|
108
|
-
value < 1 ||
|
|
109
|
-
value > MAX_SEARCH_LIMIT
|
|
110
|
-
) {
|
|
111
|
-
throw new DiscoveryPolicyError(
|
|
112
|
-
"invalid_args",
|
|
113
|
-
`limit must be a whole number from 1 through ${MAX_SEARCH_LIMIT}. Page through larger catalogs with offset.`,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
return value;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Validate the raw list so duplicate addresses consume the same bound. */
|
|
120
|
-
export function discoveryAddresses(value: unknown): unknown[] {
|
|
121
|
-
if (!Array.isArray(value)) {
|
|
122
|
-
throw new DiscoveryPolicyError(
|
|
123
|
-
"invalid_args",
|
|
124
|
-
"addresses must be an array.",
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
if (value.length > MAX_DESCRIBE_ADDRESSES) {
|
|
128
|
-
throw new DiscoveryPolicyError(
|
|
129
|
-
"invalid_args",
|
|
130
|
-
`addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across describe_tools calls.`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
return value;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Serialize once and count the exact bytes jsonResult would emit. */
|
|
137
|
-
function boundedDiscoveryText(
|
|
138
|
-
value: unknown,
|
|
139
|
-
hint: string,
|
|
140
|
-
): string {
|
|
141
|
-
const text = JSON.stringify(value, null, 2);
|
|
142
|
-
if (text === undefined) {
|
|
143
|
-
throw new TypeError("Discovery result is not JSON-serializable.");
|
|
144
|
-
}
|
|
145
|
-
const bytes = enc.encode(text).length;
|
|
146
|
-
if (bytes > MAX_DISCOVERY_RESULT_BYTES) {
|
|
147
|
-
throw new DiscoveryPolicyError(
|
|
148
|
-
"result_too_large",
|
|
149
|
-
`Discovery result is ${bytes} UTF-8 bytes, over the ${MAX_DISCOVERY_RESULT_BYTES}-byte ceiling. ${hint}`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
return text;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Apply the same final result guard to code-mode discovery helpers. */
|
|
156
|
-
export function assertDiscoveryResultSize(
|
|
157
|
-
value: unknown,
|
|
158
|
-
hint: string,
|
|
159
|
-
): void {
|
|
160
|
-
boundedDiscoveryText(value, hint);
|
|
95
|
+
function errorDetails(code: string, message: string): CallErrorDetails {
|
|
96
|
+
return { code, message, retryable: messageLooksRetryable(message) };
|
|
161
97
|
}
|
|
162
98
|
|
|
163
99
|
function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
|
|
@@ -189,71 +125,14 @@ function discoveryResult(value: unknown, hint: string): ToolResult {
|
|
|
189
125
|
}
|
|
190
126
|
}
|
|
191
127
|
|
|
192
|
-
/**
|
|
193
|
-
* The longest the engine will park a synchronous inbound request in *waiting
|
|
194
|
-
* alone*. The engine already treats ~15 s as the outer bound of one reasonable
|
|
195
|
-
* connector call (EXECUTE_HOST_CALL_TIMEOUT_MS), so sleeping for minutes trades
|
|
196
|
-
* a fast, informative failure for a hung one. A connector-reported window this
|
|
197
|
-
* long isn't truncated — it's declined (see `retryBackoffMs`) and reported
|
|
198
|
-
* verbatim as `error.retryAfterMs`, so the agent, which can afford to wait,
|
|
199
|
-
* decides when to re-issue.
|
|
200
|
-
*/
|
|
201
|
-
export const MAX_RETRY_BACKOFF_MS = 10_000;
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* How long to wait before the next attempt, or `undefined` for "don't retry".
|
|
205
|
-
*
|
|
206
|
-
* A connector that read a `Retry-After` header knows the window exactly, so it
|
|
207
|
-
* is honoured **exactly or not at all**: truncating an exponential *guess* is
|
|
208
|
-
* harmless, but truncating a *known* window means deliberately retrying inside
|
|
209
|
-
* a rate limit — the harm this channel exists to prevent. A window longer than
|
|
210
|
-
* `MAX_RETRY_BACKOFF_MS` therefore declines the retry rather than shortening
|
|
211
|
-
* it. (`retryAfterMs` is normalized non-negative, so `0` means "retry now".)
|
|
212
|
-
* Connectors that report no window keep the historical exponential guess.
|
|
213
|
-
*
|
|
214
|
-
* Waits are per attempt, matching the per-attempt `timeoutMs` race in
|
|
215
|
-
* `runCall`. Exported for direct testing.
|
|
216
|
-
*/
|
|
217
|
-
export function retryBackoffMs(
|
|
218
|
-
attempt: number,
|
|
219
|
-
retryAfterMs: number | undefined,
|
|
220
|
-
): number | undefined {
|
|
221
|
-
if (retryAfterMs === undefined) {
|
|
222
|
-
return Math.min(250 * 2 ** (attempt - 1), 1_000);
|
|
223
|
-
}
|
|
224
|
-
return retryAfterMs <= MAX_RETRY_BACKOFF_MS ? retryAfterMs : undefined;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** Details for failures that never reached a connector (no thrown value). */
|
|
228
|
-
function errorDetails(code: string, message: string): ErrorDetails {
|
|
229
|
-
return { code, message, retryable: messageLooksRetryable(message) };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function callerCancelledDetails(): ErrorDetails {
|
|
233
|
-
return {
|
|
234
|
-
code: "cancelled",
|
|
235
|
-
message: "Tool call was cancelled by the caller.",
|
|
236
|
-
retryable: false,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function isCallerCancellation(
|
|
241
|
-
error: unknown,
|
|
242
|
-
signal: AbortSignal | undefined,
|
|
243
|
-
): boolean {
|
|
244
|
-
return (
|
|
245
|
-
signal?.aborted === true ||
|
|
246
|
-
(isCallAdmissionError(error) && error.admissionKind === "cancelled")
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
128
|
|
|
250
129
|
/** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
|
|
251
|
-
function isContinuationByte(b: number): boolean {
|
|
252
|
-
return (b & 0xc0) === 0x80;
|
|
130
|
+
function isContinuationByte(b: number | undefined): boolean {
|
|
131
|
+
return b !== undefined && (b & 0xc0) === 0x80;
|
|
253
132
|
}
|
|
254
133
|
|
|
255
134
|
/** Smallest accepted `get_result` byte offset. */
|
|
256
|
-
|
|
135
|
+
const MIN_RESULT_OFFSET = 0;
|
|
257
136
|
|
|
258
137
|
/**
|
|
259
138
|
* The one definition of a usable `get_result` offset: a whole number of bytes
|
|
@@ -269,7 +148,7 @@ export const MIN_RESULT_OFFSET = 0;
|
|
|
269
148
|
* error. An offset past the end of the payload stays legal: it is a whole
|
|
270
149
|
* number of bytes, and it answers with an empty final page.
|
|
271
150
|
*/
|
|
272
|
-
|
|
151
|
+
function isValidResultOffset(value: number): boolean {
|
|
273
152
|
return Number.isInteger(value) && value >= MIN_RESULT_OFFSET;
|
|
274
153
|
}
|
|
275
154
|
|
|
@@ -333,8 +212,9 @@ export function alignEndToCharBoundary(
|
|
|
333
212
|
|
|
334
213
|
/** Resolve a dot-path (segments) against a value; `key[]` maps the tail over an array. */
|
|
335
214
|
function resolvePath(value: unknown, segments: string[]): unknown {
|
|
336
|
-
|
|
337
|
-
|
|
215
|
+
const seg = segments[0];
|
|
216
|
+
if (seg === undefined) return value;
|
|
217
|
+
const rest = segments.slice(1);
|
|
338
218
|
const isArr = seg.endsWith("[]");
|
|
339
219
|
const key = isArr ? seg.slice(0, -2) : seg;
|
|
340
220
|
let next: unknown = value;
|
|
@@ -396,7 +276,7 @@ function applyFieldsToContent(
|
|
|
396
276
|
* the three give one answer to the same question. A value JSON cannot serialize
|
|
397
277
|
* at all (a BigInt) still throws, as before, and is reported as a failure.
|
|
398
278
|
*/
|
|
399
|
-
|
|
279
|
+
function serializeResultText(value: unknown): string {
|
|
400
280
|
const serialized = JSON.stringify(value, null, 2);
|
|
401
281
|
return serialized === undefined ? String(value) : serialized;
|
|
402
282
|
}
|
|
@@ -551,7 +431,7 @@ export interface ListArgs {
|
|
|
551
431
|
/** When false, return cached/observed health without downstream I/O. */
|
|
552
432
|
probe?: boolean;
|
|
553
433
|
}
|
|
554
|
-
|
|
434
|
+
type ResultMode = "mcp" | "value";
|
|
555
435
|
export interface CallArgs {
|
|
556
436
|
address: string;
|
|
557
437
|
args?: Record<string, unknown>;
|
|
@@ -616,6 +496,8 @@ export function createMetaTools(
|
|
|
616
496
|
defaultToolTimeoutMs?: number;
|
|
617
497
|
/** Per-connector deadline for the list/search/describe probe fan-out. Default 30_000. */
|
|
618
498
|
probeTimeoutMs?: number;
|
|
499
|
+
/** Maximum simultaneous connector discovery operations. Default 4. */
|
|
500
|
+
discoveryConcurrency?: number;
|
|
619
501
|
activity?: ActivityRequestContext;
|
|
620
502
|
/** Inbound request cancellation shared by direct and batch child calls. */
|
|
621
503
|
requestSignal?: AbortSignal;
|
|
@@ -629,10 +511,19 @@ export function createMetaTools(
|
|
|
629
511
|
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
|
|
630
512
|
const probeTimeoutMs =
|
|
631
513
|
normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
514
|
+
const discoveryConcurrency = resolveDiscoveryConcurrency(
|
|
515
|
+
opts.discoveryConcurrency,
|
|
516
|
+
);
|
|
632
517
|
// createMetaTools() is called once per inbound MCP request. Sharing this
|
|
633
518
|
// identity lets remote connectors reuse one downstream client inside that
|
|
634
519
|
// request without leaking request-bound I/O into the next one.
|
|
635
520
|
const requestScope = {};
|
|
521
|
+
const catalog = new CatalogService(registry, baseUrl, {
|
|
522
|
+
requestScope,
|
|
523
|
+
probeTimeoutMs,
|
|
524
|
+
concurrency: discoveryConcurrency,
|
|
525
|
+
});
|
|
526
|
+
const invocation = new InvocationService(registry, catalog, opts.activity);
|
|
636
527
|
const withProbeDeadline = <T>(
|
|
637
528
|
label: string,
|
|
638
529
|
operation: (options: {
|
|
@@ -650,370 +541,115 @@ export function createMetaTools(
|
|
|
650
541
|
toolResult: ToolResult;
|
|
651
542
|
durationMs: number;
|
|
652
543
|
attempts: number;
|
|
653
|
-
timing:
|
|
654
|
-
catalogMs: number;
|
|
655
|
-
admissionMs: number;
|
|
656
|
-
connectorMs: number;
|
|
657
|
-
backoffMs: number;
|
|
658
|
-
resultProcessingMs: number;
|
|
659
|
-
totalMs: number;
|
|
660
|
-
};
|
|
544
|
+
timing: InvocationTiming;
|
|
661
545
|
value?: unknown;
|
|
662
|
-
error?:
|
|
546
|
+
error?: CallErrorDetails;
|
|
663
547
|
}
|
|
664
548
|
|
|
665
|
-
|
|
549
|
+
interface ProcessedCallResult {
|
|
550
|
+
toolResult: ToolResult;
|
|
551
|
+
value?: unknown;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
666
555
|
async function runCall(
|
|
667
556
|
call: BatchCall,
|
|
668
557
|
source: ActivityCallSource,
|
|
669
558
|
options: { allowDestructive?: boolean } = {},
|
|
670
559
|
): Promise<RunCallOutcome> {
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const timing = () => ({
|
|
679
|
-
catalogMs,
|
|
680
|
-
admissionMs,
|
|
681
|
-
connectorMs,
|
|
682
|
-
backoffMs,
|
|
683
|
-
resultProcessingMs,
|
|
684
|
-
totalMs: Date.now() - started,
|
|
685
|
-
});
|
|
686
|
-
const resolved = registry.resolveAddress(call.address);
|
|
687
|
-
const record = (
|
|
688
|
-
outcome: "success" | "error" | "timeout" | "cancelled",
|
|
689
|
-
errorCode?: string,
|
|
690
|
-
) => {
|
|
691
|
-
if (!resolved) return;
|
|
692
|
-
recordToolActivity(opts.activity, {
|
|
693
|
-
connectorId: resolved.connector.id,
|
|
694
|
-
toolName: resolved.toolName,
|
|
695
|
-
address: `${resolved.connector.id}.${resolved.toolName}`,
|
|
560
|
+
const results = registry.resultsStorage();
|
|
561
|
+
const fields = call.fields && call.fields.length > 0 ? call.fields : null;
|
|
562
|
+
const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
|
|
563
|
+
const outcome = await invocation.invoke<ProcessedCallResult>(
|
|
564
|
+
call.address,
|
|
565
|
+
call.args ?? {},
|
|
566
|
+
{
|
|
696
567
|
source,
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
...(
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
:
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
568
|
+
...(options.allowDestructive !== undefined
|
|
569
|
+
? { allowDestructive: options.allowDestructive }
|
|
570
|
+
: {}),
|
|
571
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
572
|
+
...(call.maxRetries !== undefined
|
|
573
|
+
? { maxRetries: call.maxRetries }
|
|
574
|
+
: {}),
|
|
575
|
+
...(opts.requestSignal !== undefined
|
|
576
|
+
? { requestSignal: opts.requestSignal }
|
|
577
|
+
: {}),
|
|
578
|
+
unwrapResult: call.resultMode === "value",
|
|
579
|
+
processResult: async (result, resolved) => {
|
|
580
|
+
// Result-size cap for THIS call: the connector's own override wins,
|
|
581
|
+
// then the deployment-wide value, then the built-in default (already
|
|
582
|
+
// folded into `globalCap`). Resolved per call so one batch_call can
|
|
583
|
+
// mix a tight-capped connector with siblings on the global cap. An
|
|
584
|
+
// override the registry already warned about at startup is dropped
|
|
585
|
+
// here, so the connector simply inherits `globalCap`.
|
|
586
|
+
const cap = resolveMaxResultBytes(
|
|
587
|
+
resolved.connector.maxResultBytes,
|
|
588
|
+
globalCap,
|
|
589
|
+
);
|
|
590
|
+
if (call.resultMode === "value") {
|
|
591
|
+
let value = fields ? applyFields(result, fields) : result;
|
|
592
|
+
value = await guardValue(value, results, cap);
|
|
593
|
+
return {
|
|
594
|
+
toolResult: jsonResult({ ok: true, data: value }),
|
|
595
|
+
value,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
if (resolved.connector.kind === "mcp") {
|
|
599
|
+
const mcpResult = result as { content?: TextContent[] };
|
|
600
|
+
let content = mcpResult?.content ?? [];
|
|
601
|
+
if (fields) content = applyFieldsToContent(content, fields);
|
|
602
|
+
return { toolResult: await guardContent(content, results, cap) };
|
|
603
|
+
}
|
|
604
|
+
const value = fields ? applyFields(result, fields) : result;
|
|
605
|
+
return {
|
|
606
|
+
toolResult: await guardText(
|
|
607
|
+
serializeResultText(value),
|
|
608
|
+
results,
|
|
609
|
+
cap,
|
|
610
|
+
),
|
|
611
|
+
value,
|
|
612
|
+
};
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
);
|
|
616
|
+
if (!outcome.ok) {
|
|
714
617
|
return {
|
|
715
618
|
toolResult:
|
|
716
619
|
call.resultMode === "value"
|
|
717
620
|
? jsonResult({
|
|
718
621
|
ok: false,
|
|
719
|
-
error,
|
|
720
|
-
durationMs,
|
|
721
|
-
attempts,
|
|
722
|
-
...(call.diagnostics ? { timing:
|
|
622
|
+
error: outcome.error,
|
|
623
|
+
durationMs: outcome.durationMs,
|
|
624
|
+
attempts: outcome.attempts,
|
|
625
|
+
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
723
626
|
})
|
|
724
|
-
: errorResult(error.message),
|
|
725
|
-
durationMs,
|
|
726
|
-
attempts,
|
|
727
|
-
timing:
|
|
728
|
-
error,
|
|
627
|
+
: errorResult(outcome.error.message),
|
|
628
|
+
durationMs: outcome.durationMs,
|
|
629
|
+
attempts: outcome.attempts,
|
|
630
|
+
timing: outcome.timing,
|
|
631
|
+
error: outcome.error,
|
|
729
632
|
};
|
|
730
|
-
};
|
|
731
|
-
if (!resolved) {
|
|
732
|
-
return failed(
|
|
733
|
-
errorDetails("unknown_address", `Unknown address "${call.address}"`),
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
const results = registry.resultsStorage();
|
|
737
|
-
// Result-size cap for THIS call: the connector's own override wins, then
|
|
738
|
-
// the deployment-wide value, then the built-in default (already folded
|
|
739
|
-
// into `globalCap`). Resolved per call so one batch_call can mix a
|
|
740
|
-
// tight-capped connector with siblings on the global cap. An override the
|
|
741
|
-
// registry already warned about at startup is dropped here, so the
|
|
742
|
-
// connector simply inherits `globalCap`.
|
|
743
|
-
const cap = resolveMaxResultBytes(
|
|
744
|
-
resolved.connector.maxResultBytes,
|
|
745
|
-
globalCap,
|
|
746
|
-
);
|
|
747
|
-
const fields = call.fields && call.fields.length > 0 ? call.fields : null;
|
|
748
|
-
// An explicit per-call deadline always wins; the config default only fills
|
|
749
|
-
// the gap, and stays off entirely when the deployment sets none.
|
|
750
|
-
const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
|
|
751
|
-
const maxRetries = Math.min(
|
|
752
|
-
2,
|
|
753
|
-
Math.max(0, Math.trunc(call.maxRetries ?? 0)),
|
|
754
|
-
);
|
|
755
|
-
const catalogStarted = Date.now();
|
|
756
|
-
let definition: ToolDef | undefined;
|
|
757
|
-
try {
|
|
758
|
-
definition = (
|
|
759
|
-
await registry.getTools(resolved.connector.id, baseUrl, requestScope)
|
|
760
|
-
).find((tool) => tool.name === resolved.toolName);
|
|
761
|
-
} catch (err) {
|
|
762
|
-
catalogMs += Date.now() - catalogStarted;
|
|
763
|
-
if (opts.requestSignal?.aborted) {
|
|
764
|
-
return failed(callerCancelledDetails());
|
|
765
|
-
}
|
|
766
|
-
// A connector whose catalog cannot be fetched is as unusable as one whose
|
|
767
|
-
// execution fails, so it feeds health accounting the same way the
|
|
768
|
-
// execution catch below does — otherwise a connector every call_tool
|
|
769
|
-
// fails against (a revoked downstream grant, say) still reads clean from
|
|
770
|
-
// the cheap `list_connectors({ probe: false })` signal.
|
|
771
|
-
//
|
|
772
|
-
// Recorded HERE rather than inside the registry's catalog fetch on
|
|
773
|
-
// purpose: `registry` is this connection's VIEW, so a toolkit-scoped
|
|
774
|
-
// session records into its own log as well as the deployment-wide one,
|
|
775
|
-
// which `Registry.refreshTools` could not reach. A cache hit that avoids
|
|
776
|
-
// a live listTools call therefore records nothing either way — it is not
|
|
777
|
-
// evidence of health, and success stays what it has always been: an
|
|
778
|
-
// actual downstream call that returned.
|
|
779
|
-
registry.recordFailure(resolved.connector.id, Date.now() - started, err);
|
|
780
|
-
// classifyCallError so a typed auth_required thrown while listing tools
|
|
781
|
-
// (e.g. a revoked downstream OAuth grant) keeps its code.
|
|
782
|
-
return failed(classifyCallError(err, "catalog_lookup_failed"));
|
|
783
|
-
}
|
|
784
|
-
catalogMs += Date.now() - catalogStarted;
|
|
785
|
-
if (!definition) {
|
|
786
|
-
return failed(
|
|
787
|
-
errorDetails(
|
|
788
|
-
"unknown_tool",
|
|
789
|
-
`Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
|
|
790
|
-
),
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
const explicitlyReadOnly =
|
|
794
|
-
definition.annotations?.readOnlyHint === true &&
|
|
795
|
-
definition.annotations?.destructiveHint !== true;
|
|
796
|
-
if (!explicitlyReadOnly && !options.allowDestructive) {
|
|
797
|
-
return failed(
|
|
798
|
-
errorDetails(
|
|
799
|
-
"destructive_tool_requires_approval",
|
|
800
|
-
`Tool "${call.address}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`,
|
|
801
|
-
),
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
const retrySafe =
|
|
805
|
-
definition.annotations?.readOnlyHint === true ||
|
|
806
|
-
definition.annotations?.idempotentHint === true;
|
|
807
|
-
|
|
808
|
-
let result: unknown;
|
|
809
|
-
while (true) {
|
|
810
|
-
attempts++;
|
|
811
|
-
let permit: Awaited<ReturnType<RegistryView["admitCall"]>> | undefined;
|
|
812
|
-
const controller =
|
|
813
|
-
timeoutMs || opts.requestSignal ? new AbortController() : undefined;
|
|
814
|
-
const forwardAbort = () =>
|
|
815
|
-
controller?.abort(opts.requestSignal?.reason);
|
|
816
|
-
if (opts.requestSignal?.aborted) forwardAbort();
|
|
817
|
-
else {
|
|
818
|
-
opts.requestSignal?.addEventListener("abort", forwardAbort, {
|
|
819
|
-
once: true,
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
823
|
-
let onAbort: (() => void) | undefined;
|
|
824
|
-
let attemptFailed = false;
|
|
825
|
-
let attemptError: unknown;
|
|
826
|
-
try {
|
|
827
|
-
const admissionStarted = Date.now();
|
|
828
|
-
try {
|
|
829
|
-
permit = await registry.admitCall(resolved.connector.id, {
|
|
830
|
-
toolName: resolved.toolName,
|
|
831
|
-
args: call.args ?? {},
|
|
832
|
-
signal: opts.requestSignal,
|
|
833
|
-
});
|
|
834
|
-
} finally {
|
|
835
|
-
admissionMs += Date.now() - admissionStarted;
|
|
836
|
-
}
|
|
837
|
-
const ctx = registry.contextFor(
|
|
838
|
-
resolved.connector.id,
|
|
839
|
-
baseUrl,
|
|
840
|
-
requestScope,
|
|
841
|
-
{ signal: controller?.signal, timeoutMs },
|
|
842
|
-
);
|
|
843
|
-
let rejectCancelled!: (reason: unknown) => void;
|
|
844
|
-
const cancelled = controller
|
|
845
|
-
? new Promise<never>((_, reject) => {
|
|
846
|
-
rejectCancelled = reject;
|
|
847
|
-
})
|
|
848
|
-
: undefined;
|
|
849
|
-
onAbort = () => {
|
|
850
|
-
rejectCancelled(
|
|
851
|
-
controller?.signal.reason ??
|
|
852
|
-
new ConnectorCallError("timeout", "Tool call was cancelled"),
|
|
853
|
-
);
|
|
854
|
-
};
|
|
855
|
-
controller?.signal.addEventListener("abort", onAbort, { once: true });
|
|
856
|
-
if (controller?.signal.aborted) onAbort();
|
|
857
|
-
if (controller?.signal.aborted) await cancelled;
|
|
858
|
-
if (timeoutMs) {
|
|
859
|
-
timer = setTimeout(() => {
|
|
860
|
-
controller?.abort(
|
|
861
|
-
new ConnectorCallError(
|
|
862
|
-
"timeout",
|
|
863
|
-
`Tool call timed out after ${timeoutMs}ms`,
|
|
864
|
-
),
|
|
865
|
-
);
|
|
866
|
-
}, timeoutMs);
|
|
867
|
-
}
|
|
868
|
-
const connectorStarted = Date.now();
|
|
869
|
-
try {
|
|
870
|
-
const pending = resolved.connector.callTool(
|
|
871
|
-
resolved.toolName,
|
|
872
|
-
call.args ?? {},
|
|
873
|
-
ctx,
|
|
874
|
-
);
|
|
875
|
-
result = cancelled
|
|
876
|
-
? await Promise.race([pending, cancelled])
|
|
877
|
-
: await pending;
|
|
878
|
-
} finally {
|
|
879
|
-
connectorMs += Date.now() - connectorStarted;
|
|
880
|
-
}
|
|
881
|
-
const mcpResult = result as {
|
|
882
|
-
content?: TextContent[];
|
|
883
|
-
isError?: boolean;
|
|
884
|
-
};
|
|
885
|
-
if (resolved.connector.kind === "mcp" && mcpResult?.isError) {
|
|
886
|
-
throw new Error(
|
|
887
|
-
mcpResult.content?.map((block) => block.text).join("") ||
|
|
888
|
-
"Downstream tool call failed",
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
} catch (err) {
|
|
892
|
-
attemptFailed = true;
|
|
893
|
-
attemptError = err;
|
|
894
|
-
} finally {
|
|
895
|
-
if (timer) clearTimeout(timer);
|
|
896
|
-
if (onAbort) {
|
|
897
|
-
controller?.signal.removeEventListener("abort", onAbort);
|
|
898
|
-
}
|
|
899
|
-
opts.requestSignal?.removeEventListener("abort", forwardAbort);
|
|
900
|
-
permit?.release();
|
|
901
|
-
}
|
|
902
|
-
if (attemptFailed) {
|
|
903
|
-
const callerCancelled = isCallerCancellation(
|
|
904
|
-
attemptError,
|
|
905
|
-
opts.requestSignal,
|
|
906
|
-
);
|
|
907
|
-
const details = callerCancelled
|
|
908
|
-
? callerCancelledDetails()
|
|
909
|
-
: classifyCallError(attemptError);
|
|
910
|
-
if (
|
|
911
|
-
!callerCancelled &&
|
|
912
|
-
attempts <= maxRetries &&
|
|
913
|
-
retrySafe &&
|
|
914
|
-
details.retryable
|
|
915
|
-
) {
|
|
916
|
-
const wait = retryBackoffMs(attempts, details.retryAfterMs);
|
|
917
|
-
if (wait !== undefined) {
|
|
918
|
-
const backoffStarted = Date.now();
|
|
919
|
-
if (wait > 0) {
|
|
920
|
-
const completed = await new Promise<boolean>((resolve) => {
|
|
921
|
-
let settled = false;
|
|
922
|
-
const finish = (value: boolean) => {
|
|
923
|
-
if (settled) return;
|
|
924
|
-
settled = true;
|
|
925
|
-
clearTimeout(timer);
|
|
926
|
-
opts.requestSignal?.removeEventListener("abort", cancel);
|
|
927
|
-
resolve(value);
|
|
928
|
-
};
|
|
929
|
-
const timer = setTimeout(() => finish(true), wait);
|
|
930
|
-
const cancel = () => finish(false);
|
|
931
|
-
opts.requestSignal?.addEventListener("abort", cancel, {
|
|
932
|
-
once: true,
|
|
933
|
-
});
|
|
934
|
-
if (opts.requestSignal?.aborted) cancel();
|
|
935
|
-
});
|
|
936
|
-
backoffMs += Date.now() - backoffStarted;
|
|
937
|
-
if (!completed) return failed(callerCancelledDetails());
|
|
938
|
-
} else {
|
|
939
|
-
backoffMs += Date.now() - backoffStarted;
|
|
940
|
-
}
|
|
941
|
-
continue;
|
|
942
|
-
}
|
|
943
|
-
// The reported window is longer than the engine will park a
|
|
944
|
-
// synchronous request for. Fall through to failure with
|
|
945
|
-
// retryAfterMs reported verbatim so the agent can re-issue.
|
|
946
|
-
}
|
|
947
|
-
if (!callerCancelled && !isCallAdmissionError(attemptError)) {
|
|
948
|
-
registry.recordFailure(
|
|
949
|
-
resolved.connector.id,
|
|
950
|
-
Date.now() - started,
|
|
951
|
-
attemptError,
|
|
952
|
-
);
|
|
953
|
-
}
|
|
954
|
-
return failed(details);
|
|
955
|
-
}
|
|
956
|
-
break;
|
|
957
633
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
try {
|
|
962
|
-
const mr = result as { content?: TextContent[]; isError?: boolean };
|
|
963
|
-
if (call.resultMode === "value") {
|
|
964
|
-
let value = unwrapMcpResult(resolved.connector.kind, result);
|
|
965
|
-
if (fields) value = applyFields(value, fields);
|
|
966
|
-
value = await guardValue(value, results, cap);
|
|
967
|
-
resultProcessingMs += Date.now() - processingStarted;
|
|
968
|
-
const durationMs = Date.now() - started;
|
|
969
|
-
const diagnostics = timing();
|
|
970
|
-
record("success");
|
|
971
|
-
return {
|
|
972
|
-
toolResult: jsonResult({
|
|
634
|
+
const valueModeResult =
|
|
635
|
+
call.resultMode === "value"
|
|
636
|
+
? jsonResult({
|
|
973
637
|
ok: true,
|
|
974
|
-
data: value,
|
|
975
|
-
durationMs,
|
|
976
|
-
attempts,
|
|
977
|
-
...(call.diagnostics ? { timing:
|
|
978
|
-
})
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
resultProcessingMs += Date.now() - processingStarted;
|
|
990
|
-
record("success");
|
|
991
|
-
return {
|
|
992
|
-
toolResult,
|
|
993
|
-
durationMs: Date.now() - started,
|
|
994
|
-
attempts,
|
|
995
|
-
timing: timing(),
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
const value = fields ? applyFields(result, fields) : result;
|
|
999
|
-
const toolResult = await guardText(
|
|
1000
|
-
serializeResultText(value),
|
|
1001
|
-
results,
|
|
1002
|
-
cap,
|
|
1003
|
-
);
|
|
1004
|
-
resultProcessingMs += Date.now() - processingStarted;
|
|
1005
|
-
record("success");
|
|
1006
|
-
return {
|
|
1007
|
-
toolResult,
|
|
1008
|
-
durationMs: Date.now() - started,
|
|
1009
|
-
attempts,
|
|
1010
|
-
timing: timing(),
|
|
1011
|
-
value,
|
|
1012
|
-
};
|
|
1013
|
-
} catch (err) {
|
|
1014
|
-
resultProcessingMs += Date.now() - processingStarted;
|
|
1015
|
-
return failed(errorDetails("result_processing_failed", msg(err)));
|
|
1016
|
-
}
|
|
638
|
+
data: outcome.value.value,
|
|
639
|
+
durationMs: outcome.durationMs,
|
|
640
|
+
attempts: outcome.attempts,
|
|
641
|
+
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
642
|
+
})
|
|
643
|
+
: outcome.value.toolResult;
|
|
644
|
+
return {
|
|
645
|
+
toolResult: valueModeResult,
|
|
646
|
+
durationMs: outcome.durationMs,
|
|
647
|
+
attempts: outcome.attempts,
|
|
648
|
+
timing: outcome.timing,
|
|
649
|
+
...(Object.prototype.hasOwnProperty.call(outcome.value, "value")
|
|
650
|
+
? { value: outcome.value.value }
|
|
651
|
+
: {}),
|
|
652
|
+
};
|
|
1017
653
|
}
|
|
1018
654
|
|
|
1019
655
|
return {
|
|
@@ -1044,181 +680,184 @@ export function createMetaTools(
|
|
|
1044
680
|
// call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
|
|
1045
681
|
const connectors = registry.listConnectors();
|
|
1046
682
|
const scope = probe ? {} : requestScope;
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
(options)
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
status = { state: "error", message: msg(err) };
|
|
1067
|
-
}
|
|
1068
|
-
} else if (
|
|
1069
|
-
verdict &&
|
|
1070
|
-
// Deployment-wide, deliberately, like `hasObservedSuccess` beside
|
|
1071
|
-
// it: a sibling toolkit's successful call proves the shared
|
|
1072
|
-
// credential works, and a verdict retired for one view but not
|
|
1073
|
-
// another would make the same connector read differently per scope
|
|
1074
|
-
// for a reason that has nothing to do with scope.
|
|
1075
|
-
credentialVerdictApplies(verdict, registry.observedSuccessAt(c.id))
|
|
1076
|
-
) {
|
|
1077
|
-
// The proactive layer (issue #24): a liveness check already found
|
|
1078
|
-
// the stored credential dead, so say so on the cheap path instead of
|
|
1079
|
-
// waiting for an agent's real call to discover it. Only while it is
|
|
1080
|
-
// the freshest evidence — a successful call since then retires it.
|
|
1081
|
-
status = {
|
|
1082
|
-
state: verdict.state,
|
|
1083
|
-
...(verdict.message ? { message: verdict.message } : {}),
|
|
1084
|
-
...(verdict.authorizationUrl
|
|
1085
|
-
? { authorizationUrl: verdict.authorizationUrl }
|
|
1086
|
-
: {}),
|
|
1087
|
-
};
|
|
1088
|
-
} else {
|
|
1089
|
-
// "error" comes from THIS view's own observations — a sibling
|
|
1090
|
-
// toolkit's failure is not this session's experience — while
|
|
1091
|
-
// ok/unknown may lean on the deployment-wide success signal, since
|
|
1092
|
-
// "the connector answers at all" is a fact about the connector.
|
|
1093
|
-
// Unscoped, the two are the same log, so this is unchanged there.
|
|
1094
|
-
const derived =
|
|
1095
|
-
observed?.consecutiveFailures && observed.consecutiveFailures > 0
|
|
1096
|
-
? ("error" as const)
|
|
1097
|
-
: registry.hasObservedSuccess(c.id) || c.kind === "api"
|
|
1098
|
-
? ("ok" as const)
|
|
1099
|
-
: ("unknown" as const);
|
|
1100
|
-
status = {
|
|
1101
|
-
// A successful liveness check upgrades "unknown" — nothing has
|
|
1102
|
-
// been called yet, but the credential was verified, which is how
|
|
1103
|
-
// re-authorization shows up here as ok rather than as an absence
|
|
1104
|
-
// of evidence. It never DOWNgrades an observed failure: a real
|
|
1105
|
-
// call that failed is stronger evidence than a background check.
|
|
1106
|
-
state:
|
|
1107
|
-
derived === "unknown" && verdict?.state === "ok"
|
|
1108
|
-
? ("ok" as const)
|
|
1109
|
-
: derived,
|
|
1110
|
-
...(observed?.lastError ? { message: observed.lastError } : {}),
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
// Stamped where the observation actually happened — after the status
|
|
1114
|
-
// probe, not before it. A 30-second probe stamped at its start would
|
|
1115
|
-
// report a verdict older than it is, and would lose the race against a
|
|
1116
|
-
// real call that succeeded WHILE it ran (that success must retire the
|
|
1117
|
-
// verdict, and only an honest timestamp says so).
|
|
1118
|
-
const checkedAt = new Date().toISOString();
|
|
1119
|
-
// A live status probe IS a liveness observation of the stored
|
|
1120
|
-
// credential, so it updates the same verdict a background check
|
|
1121
|
-
// writes: the cached read afterwards agrees with what the operator
|
|
1122
|
-
// just saw, and they are not swept again moments later. Recorded from
|
|
1123
|
-
// the STATUS phase only, and only when the connector actually answered
|
|
1124
|
-
// — a catalog refresh below is not a credential check (the sweep never
|
|
1125
|
-
// fetches one), it is already counted in the health log, and letting
|
|
1126
|
-
// its failure land here would spend the freshness budget on it. The
|
|
1127
|
-
// registry ignores this for connectors storing no credential of ours.
|
|
1128
|
-
if (probe && (status.state === "ok" || status.state === "auth_required")) {
|
|
1129
|
-
await registry.recordCredentialHealth(c.id, {
|
|
1130
|
-
state: status.state,
|
|
1131
|
-
checkedAt,
|
|
1132
|
-
...(status.message ? { message: status.message } : {}),
|
|
1133
|
-
...("authorizationUrl" in status && status.authorizationUrl
|
|
1134
|
-
? { authorizationUrl: status.authorizationUrl }
|
|
1135
|
-
: {}),
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
let tools = registry.peekTools(c.id);
|
|
1139
|
-
// An auth_required status may have just started OAuth. A second
|
|
1140
|
-
// listTools probe would overwrite its state/verifier while returning
|
|
1141
|
-
// the first (now stale) authorization URL.
|
|
1142
|
-
if (probe && status.state === "ok") {
|
|
1143
|
-
try {
|
|
1144
|
-
tools = await withProbeDeadline(
|
|
1145
|
-
`list_connectors catalog refresh of "${c.id}"`,
|
|
1146
|
-
(options) =>
|
|
1147
|
-
registry.refreshTools(c.id, baseUrl, scope, options),
|
|
1148
|
-
);
|
|
1149
|
-
registry.recordSuccess(c.id, Date.now() - statusStarted);
|
|
1150
|
-
} catch (err) {
|
|
1151
|
-
const details = classifyCallError(err);
|
|
1152
|
-
if (details.code === "auth_required") {
|
|
1153
|
-
let authStatus: ConnectorStatus | undefined;
|
|
1154
|
-
try {
|
|
1155
|
-
authStatus = await withProbeDeadline(
|
|
1156
|
-
`list_connectors authorization status of "${c.id}"`,
|
|
1157
|
-
(options) =>
|
|
1158
|
-
registry.statusFor(c.id, baseUrl, scope, options),
|
|
1159
|
-
);
|
|
1160
|
-
} catch {
|
|
1161
|
-
// The typed auth verdict is still authoritative; this second
|
|
1162
|
-
// read exists only to recover the connector's pending URL.
|
|
1163
|
-
}
|
|
1164
|
-
status =
|
|
1165
|
-
authStatus?.state === "auth_required"
|
|
1166
|
-
? authStatus
|
|
1167
|
-
: {
|
|
1168
|
-
state: "auth_required" as const,
|
|
1169
|
-
message: details.message,
|
|
1170
|
-
};
|
|
1171
|
-
await registry.recordCredentialHealth(c.id, {
|
|
1172
|
-
state: "auth_required",
|
|
1173
|
-
checkedAt,
|
|
1174
|
-
...(status.message ? { message: status.message } : {}),
|
|
1175
|
-
...("authorizationUrl" in status &&
|
|
1176
|
-
status.authorizationUrl
|
|
1177
|
-
? { authorizationUrl: status.authorizationUrl }
|
|
1178
|
-
: {}),
|
|
1179
|
-
});
|
|
1180
|
-
} else {
|
|
1181
|
-
status = { state: "error" as const, message: msg(err) };
|
|
1182
|
-
}
|
|
1183
|
-
registry.recordFailure(c.id, Date.now() - statusStarted, err);
|
|
1184
|
-
}
|
|
683
|
+
const inspect = async (c: (typeof connectors)[number]) => {
|
|
684
|
+
const statusStarted = Date.now();
|
|
685
|
+
const observed = registry.healthFor(c.id);
|
|
686
|
+
const verdict = await registry.credentialHealthFor(c.id);
|
|
687
|
+
let status:
|
|
688
|
+
| ConnectorStatus
|
|
689
|
+
| { state: "ok" | "error" | "unknown"; message?: string };
|
|
690
|
+
if (probe) {
|
|
691
|
+
try {
|
|
692
|
+
status = await withProbeDeadline(
|
|
693
|
+
`list_connectors probe of "${c.id}"`,
|
|
694
|
+
(options) =>
|
|
695
|
+
registry.statusFor(c.id, baseUrl, scope, options),
|
|
696
|
+
);
|
|
697
|
+
} catch (err) {
|
|
698
|
+
// A probe that outran probeTimeoutMs (or otherwise threw)
|
|
699
|
+
// degrades this connector to an error status rather than
|
|
700
|
+
// hanging the whole list_connectors call.
|
|
701
|
+
status = { state: "error", message: msg(err) };
|
|
1185
702
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
703
|
+
} else if (
|
|
704
|
+
verdict &&
|
|
705
|
+
// Deployment-wide, deliberately, like `hasObservedSuccess` beside
|
|
706
|
+
// it: a sibling toolkit's successful call proves the shared
|
|
707
|
+
// credential works, and a verdict retired for one view but not
|
|
708
|
+
// another would make the same connector read differently per scope
|
|
709
|
+
// for a reason that has nothing to do with scope.
|
|
710
|
+
credentialVerdictApplies(verdict, registry.observedSuccessAt(c.id))
|
|
711
|
+
) {
|
|
712
|
+
// The proactive layer (issue #24): a liveness check already found
|
|
713
|
+
// the stored credential dead, so say so on the cheap path instead of
|
|
714
|
+
// waiting for an agent's real call to discover it. Only while it is
|
|
715
|
+
// the freshest evidence — a successful call since then retires it.
|
|
716
|
+
status = {
|
|
717
|
+
state: verdict.state,
|
|
718
|
+
...(verdict.message ? { message: verdict.message } : {}),
|
|
719
|
+
...(verdict.authorizationUrl
|
|
720
|
+
? { authorizationUrl: verdict.authorizationUrl }
|
|
721
|
+
: {}),
|
|
722
|
+
};
|
|
723
|
+
} else {
|
|
724
|
+
// "error" comes from THIS view's own observations — a sibling
|
|
725
|
+
// toolkit's failure is not this session's experience — while
|
|
726
|
+
// ok/unknown may lean on the deployment-wide success signal, since
|
|
727
|
+
// "the connector answers at all" is a fact about the connector.
|
|
728
|
+
// Unscoped, the two are the same log, so this is unchanged there.
|
|
729
|
+
const derived =
|
|
730
|
+
observed?.consecutiveFailures && observed.consecutiveFailures > 0
|
|
731
|
+
? ("error" as const)
|
|
732
|
+
: registry.hasObservedSuccess(c.id) || c.kind === "api"
|
|
733
|
+
? ("ok" as const)
|
|
734
|
+
: ("unknown" as const);
|
|
735
|
+
status = {
|
|
736
|
+
// A successful liveness check upgrades "unknown" — nothing has
|
|
737
|
+
// been called yet, but the credential was verified, which is how
|
|
738
|
+
// re-authorization shows up here as ok rather than as an absence
|
|
739
|
+
// of evidence. It never DOWNgrades an observed failure: a real
|
|
740
|
+
// call that failed is stronger evidence than a background check.
|
|
741
|
+
state:
|
|
742
|
+
derived === "unknown" && verdict?.state === "ok"
|
|
743
|
+
? ("ok" as const)
|
|
744
|
+
: derived,
|
|
745
|
+
...(observed?.lastError ? { message: observed.lastError } : {}),
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
// Stamped where the observation actually happened — after the status
|
|
749
|
+
// probe, not before it. A 30-second probe stamped at its start would
|
|
750
|
+
// report a verdict older than it is, and would lose the race against a
|
|
751
|
+
// real call that succeeded WHILE it ran (that success must retire the
|
|
752
|
+
// verdict, and only an honest timestamp says so).
|
|
753
|
+
const checkedAt = new Date().toISOString();
|
|
754
|
+
// A live status probe IS a liveness observation of the stored
|
|
755
|
+
// credential, so it updates the same verdict a background check
|
|
756
|
+
// writes: the cached read afterwards agrees with what the operator
|
|
757
|
+
// just saw, and they are not swept again moments later. Recorded from
|
|
758
|
+
// the STATUS phase only, and only when the connector actually answered
|
|
759
|
+
// — a catalog refresh below is not a credential check (the sweep never
|
|
760
|
+
// fetches one), it is already counted in the health log, and letting
|
|
761
|
+
// its failure land here would spend the freshness budget on it. The
|
|
762
|
+
// registry ignores this for connectors storing no credential of ours.
|
|
763
|
+
if (probe && (status.state === "ok" || status.state === "auth_required")) {
|
|
764
|
+
await registry.recordCredentialHealth(c.id, {
|
|
765
|
+
state: status.state,
|
|
1197
766
|
checkedAt,
|
|
1198
|
-
|
|
1199
|
-
probe,
|
|
1200
|
-
...(latestObserved ?? observed ?? {}),
|
|
1201
|
-
...(credentialCheck ? { credentialCheck } : {}),
|
|
767
|
+
...(status.message ? { message: status.message } : {}),
|
|
1202
768
|
...("authorizationUrl" in status && status.authorizationUrl
|
|
1203
769
|
? { authorizationUrl: status.authorizationUrl }
|
|
1204
770
|
: {}),
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
let tools = registry.peekTools(c.id);
|
|
774
|
+
// An auth_required status may have just started OAuth. A second
|
|
775
|
+
// listTools probe would overwrite its state/verifier while returning
|
|
776
|
+
// the first (now stale) authorization URL.
|
|
777
|
+
if (probe && status.state === "ok") {
|
|
778
|
+
try {
|
|
779
|
+
tools = await withProbeDeadline(
|
|
780
|
+
`list_connectors catalog refresh of "${c.id}"`,
|
|
781
|
+
(options) =>
|
|
782
|
+
registry.refreshTools(c.id, baseUrl, scope, options),
|
|
783
|
+
);
|
|
784
|
+
registry.recordSuccess(c.id, Date.now() - statusStarted);
|
|
785
|
+
} catch (err) {
|
|
786
|
+
const details = classifyCallError(err);
|
|
787
|
+
if (details.code === "auth_required") {
|
|
788
|
+
let authStatus: ConnectorStatus | undefined;
|
|
789
|
+
try {
|
|
790
|
+
authStatus = await withProbeDeadline(
|
|
791
|
+
`list_connectors authorization status of "${c.id}"`,
|
|
792
|
+
(options) =>
|
|
793
|
+
registry.statusFor(c.id, baseUrl, scope, options),
|
|
794
|
+
);
|
|
795
|
+
} catch {
|
|
796
|
+
// The typed auth verdict is still authoritative; this second
|
|
797
|
+
// read exists only to recover the connector's pending URL.
|
|
798
|
+
}
|
|
799
|
+
status =
|
|
800
|
+
authStatus?.state === "auth_required"
|
|
801
|
+
? authStatus
|
|
802
|
+
: {
|
|
803
|
+
state: "auth_required" as const,
|
|
804
|
+
message: details.message,
|
|
805
|
+
};
|
|
806
|
+
await registry.recordCredentialHealth(c.id, {
|
|
807
|
+
state: "auth_required",
|
|
808
|
+
checkedAt,
|
|
809
|
+
...(status.message && { message: status.message }),
|
|
810
|
+
...("authorizationUrl" in status &&
|
|
811
|
+
status.authorizationUrl
|
|
812
|
+
? { authorizationUrl: status.authorizationUrl }
|
|
813
|
+
: {}),
|
|
814
|
+
});
|
|
815
|
+
} else {
|
|
816
|
+
status = { state: "error" as const, message: msg(err) };
|
|
817
|
+
}
|
|
818
|
+
registry.recordFailure(c.id, Date.now() - statusStarted, err);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const latencyMs = Date.now() - statusStarted;
|
|
822
|
+
const latestObserved = registry.healthFor(c.id);
|
|
823
|
+
const credentialCheck = probe
|
|
824
|
+
? await registry.credentialHealthFor(c.id)
|
|
825
|
+
: verdict;
|
|
826
|
+
return {
|
|
827
|
+
id: c.id,
|
|
828
|
+
...(c.title ? { title: c.title } : {}),
|
|
829
|
+
description: c.description,
|
|
830
|
+
toolCount: tools?.length ?? 0,
|
|
831
|
+
status: status.state,
|
|
832
|
+
checkedAt,
|
|
833
|
+
latencyMs,
|
|
834
|
+
probe,
|
|
835
|
+
...(latestObserved ?? observed),
|
|
836
|
+
...(credentialCheck ? { credentialCheck } : {}),
|
|
837
|
+
...("authorizationUrl" in status &&
|
|
838
|
+
status.authorizationUrl && {
|
|
839
|
+
authorizationUrl: status.authorizationUrl,
|
|
840
|
+
}),
|
|
841
|
+
...(status.message && { message: status.message }),
|
|
842
|
+
};
|
|
843
|
+
};
|
|
844
|
+
const settled = await mapSettledWithConcurrency(
|
|
845
|
+
connectors,
|
|
846
|
+
discoveryConcurrency,
|
|
847
|
+
inspect,
|
|
1208
848
|
);
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
849
|
+
if (probe) {
|
|
850
|
+
await mapSettledWithConcurrency(
|
|
851
|
+
connectors,
|
|
852
|
+
discoveryConcurrency,
|
|
853
|
+
(connector) =>
|
|
854
|
+
closeConnectorScope(
|
|
855
|
+
connector,
|
|
856
|
+
registry.contextFor(connector.id, baseUrl, scope),
|
|
857
|
+
opts.defer,
|
|
858
|
+
),
|
|
859
|
+
);
|
|
1211
860
|
}
|
|
1212
|
-
const settled = await Promise.allSettled(pending);
|
|
1213
|
-
await Promise.all(
|
|
1214
|
-
connectors.map((connector) =>
|
|
1215
|
-
closeConnectorScope(
|
|
1216
|
-
connector,
|
|
1217
|
-
registry.contextFor(connector.id, baseUrl, scope),
|
|
1218
|
-
opts.defer,
|
|
1219
|
-
),
|
|
1220
|
-
),
|
|
1221
|
-
);
|
|
1222
861
|
const out = settled.map((result) => {
|
|
1223
862
|
if (result.status === "rejected") throw result.reason;
|
|
1224
863
|
return result.value;
|
|
@@ -1227,235 +866,31 @@ export function createMetaTools(
|
|
|
1227
866
|
},
|
|
1228
867
|
|
|
1229
868
|
async searchTools(args: SearchArgs): Promise<ToolResult> {
|
|
1230
|
-
const q = args.query ?? "";
|
|
1231
|
-
let limit: number;
|
|
1232
869
|
try {
|
|
1233
|
-
|
|
870
|
+
return discoveryResult(
|
|
871
|
+
groupedSearchResult(await catalog.search(args)),
|
|
872
|
+
"Request a smaller limit, omit fullDescriptions, or use compact schemas.",
|
|
873
|
+
);
|
|
1234
874
|
} catch (err) {
|
|
1235
875
|
if (err instanceof DiscoveryPolicyError) {
|
|
1236
876
|
return discoveryErrorResult(err);
|
|
1237
877
|
}
|
|
1238
878
|
throw err;
|
|
1239
879
|
}
|
|
1240
|
-
const offset = Math.max(0, Math.trunc(args.offset ?? 0));
|
|
1241
|
-
const conns = args.connector
|
|
1242
|
-
? [registry.getConnector(args.connector)].filter(
|
|
1243
|
-
(c): c is NonNullable<typeof c> => Boolean(c),
|
|
1244
|
-
)
|
|
1245
|
-
: registry.listConnectors();
|
|
1246
|
-
const matches: Array<{
|
|
1247
|
-
connectorId: string;
|
|
1248
|
-
connectorTitle?: string;
|
|
1249
|
-
connectorDescription?: string;
|
|
1250
|
-
connectorGuideSkill?: string;
|
|
1251
|
-
tool: ToolDef;
|
|
1252
|
-
score: number;
|
|
1253
|
-
order: number;
|
|
1254
|
-
}> = [];
|
|
1255
|
-
const catalogs = await Promise.allSettled(
|
|
1256
|
-
conns.map((c) =>
|
|
1257
|
-
withProbeDeadline(
|
|
1258
|
-
`search_tools probe of "${c.id}"`,
|
|
1259
|
-
(options) =>
|
|
1260
|
-
registry.getTools(c.id, baseUrl, requestScope, options),
|
|
1261
|
-
),
|
|
1262
|
-
),
|
|
1263
|
-
);
|
|
1264
|
-
let matchMode: "all" | "partial" = "all";
|
|
1265
|
-
const collectMatches = (mode: "all" | "partial") => {
|
|
1266
|
-
matches.length = 0;
|
|
1267
|
-
let orderBase = 0;
|
|
1268
|
-
catalogs.forEach((catalog, connectorIndex) => {
|
|
1269
|
-
const c = conns[connectorIndex];
|
|
1270
|
-
if (catalog.status === "fulfilled") {
|
|
1271
|
-
for (const ranked of rankTools(catalog.value, q, mode)) {
|
|
1272
|
-
matches.push({
|
|
1273
|
-
connectorId: c.id,
|
|
1274
|
-
connectorTitle: c.title,
|
|
1275
|
-
connectorDescription: c.description,
|
|
1276
|
-
...(connectorGuide(c)
|
|
1277
|
-
? { connectorGuideSkill: connectorSkillName(c.id) }
|
|
1278
|
-
: {}),
|
|
1279
|
-
tool: ranked.tool,
|
|
1280
|
-
score: ranked.score,
|
|
1281
|
-
order: orderBase + ranked.order,
|
|
1282
|
-
});
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
orderBase +=
|
|
1286
|
-
catalog.status === "fulfilled" ? catalog.value.length : 1;
|
|
1287
|
-
});
|
|
1288
|
-
};
|
|
1289
|
-
collectMatches("all");
|
|
1290
|
-
if (q.trim() && matches.length === 0) {
|
|
1291
|
-
matchMode = "partial";
|
|
1292
|
-
collectMatches(matchMode);
|
|
1293
|
-
}
|
|
1294
|
-
matches.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
1295
|
-
const page = matches.slice(offset, offset + limit);
|
|
1296
|
-
const groups: {
|
|
1297
|
-
id: string;
|
|
1298
|
-
title?: string;
|
|
1299
|
-
description?: string;
|
|
1300
|
-
/** Skill name of this connector's usage guide, when it has one. */
|
|
1301
|
-
guide?: string;
|
|
1302
|
-
tools: Array<{
|
|
1303
|
-
name: string;
|
|
1304
|
-
address: string;
|
|
1305
|
-
description?: string;
|
|
1306
|
-
inputSchema?: unknown;
|
|
1307
|
-
outputSchema?: unknown;
|
|
1308
|
-
annotations?: ToolDef["annotations"];
|
|
1309
|
-
}>;
|
|
1310
|
-
}[] = [];
|
|
1311
|
-
const byConnector = new Map<string, (typeof groups)[number]>();
|
|
1312
|
-
for (const match of page) {
|
|
1313
|
-
let group = byConnector.get(match.connectorId);
|
|
1314
|
-
if (!group) {
|
|
1315
|
-
group = {
|
|
1316
|
-
id: match.connectorId,
|
|
1317
|
-
...(match.connectorTitle ? { title: match.connectorTitle } : {}),
|
|
1318
|
-
description: match.connectorDescription,
|
|
1319
|
-
...(match.connectorGuideSkill
|
|
1320
|
-
? { guide: match.connectorGuideSkill }
|
|
1321
|
-
: {}),
|
|
1322
|
-
tools: [],
|
|
1323
|
-
};
|
|
1324
|
-
byConnector.set(match.connectorId, group);
|
|
1325
|
-
groups.push(group);
|
|
1326
|
-
}
|
|
1327
|
-
const schema = match.tool.inputSchema ?? { type: "object" };
|
|
1328
|
-
group.tools.push({
|
|
1329
|
-
name: match.tool.name,
|
|
1330
|
-
address: `${match.connectorId}.${match.tool.name}`,
|
|
1331
|
-
description: summarizeDescription(
|
|
1332
|
-
match.tool.description,
|
|
1333
|
-
args.fullDescriptions === true,
|
|
1334
|
-
),
|
|
1335
|
-
...(args.includeSchemas
|
|
1336
|
-
? {
|
|
1337
|
-
inputSchema:
|
|
1338
|
-
args.includeSchemas === "json"
|
|
1339
|
-
? schema
|
|
1340
|
-
: compactSchema(schema),
|
|
1341
|
-
}
|
|
1342
|
-
: {}),
|
|
1343
|
-
...(args.includeSchemas && match.tool.outputSchema
|
|
1344
|
-
? {
|
|
1345
|
-
outputSchema:
|
|
1346
|
-
args.includeSchemas === "json"
|
|
1347
|
-
? match.tool.outputSchema
|
|
1348
|
-
: compactSchema(match.tool.outputSchema),
|
|
1349
|
-
}
|
|
1350
|
-
: {}),
|
|
1351
|
-
...(match.tool.annotations
|
|
1352
|
-
? { annotations: match.tool.annotations }
|
|
1353
|
-
: {}),
|
|
1354
|
-
});
|
|
1355
|
-
}
|
|
1356
|
-
const nextOffset =
|
|
1357
|
-
offset + page.length < matches.length
|
|
1358
|
-
? offset + page.length
|
|
1359
|
-
: undefined;
|
|
1360
|
-
return discoveryResult(
|
|
1361
|
-
{
|
|
1362
|
-
connectors: groups,
|
|
1363
|
-
total: matches.length,
|
|
1364
|
-
offset,
|
|
1365
|
-
limit,
|
|
1366
|
-
hasMore: nextOffset !== undefined,
|
|
1367
|
-
...(nextOffset !== undefined ? { nextOffset } : {}),
|
|
1368
|
-
...(matchMode === "partial" && matches.length > 0
|
|
1369
|
-
? { matchMode }
|
|
1370
|
-
: {}),
|
|
1371
|
-
},
|
|
1372
|
-
"Request a smaller limit, omit fullDescriptions, or use compact schemas.",
|
|
1373
|
-
);
|
|
1374
880
|
},
|
|
1375
881
|
|
|
1376
882
|
async describeTools(args: DescribeArgs): Promise<ToolResult> {
|
|
1377
883
|
try {
|
|
1378
|
-
|
|
884
|
+
return discoveryResult(
|
|
885
|
+
{ tools: await catalog.describe(args) },
|
|
886
|
+
'Split the address list or use format: "compact".',
|
|
887
|
+
);
|
|
1379
888
|
} catch (err) {
|
|
1380
889
|
if (err instanceof DiscoveryPolicyError) {
|
|
1381
890
|
return discoveryErrorResult(err);
|
|
1382
891
|
}
|
|
1383
892
|
throw err;
|
|
1384
893
|
}
|
|
1385
|
-
const format = args.format ?? "compact";
|
|
1386
|
-
const resolved = args.addresses.map((address) => ({
|
|
1387
|
-
address,
|
|
1388
|
-
resolved: registry.resolveAddress(address),
|
|
1389
|
-
}));
|
|
1390
|
-
const connectorIds = [
|
|
1391
|
-
...new Set(
|
|
1392
|
-
resolved
|
|
1393
|
-
.map((entry) => entry.resolved?.connector.id)
|
|
1394
|
-
.filter((id): id is string => Boolean(id)),
|
|
1395
|
-
),
|
|
1396
|
-
];
|
|
1397
|
-
const loaded = await Promise.allSettled(
|
|
1398
|
-
connectorIds.map((id) =>
|
|
1399
|
-
withProbeDeadline(
|
|
1400
|
-
`describe_tools probe of "${id}"`,
|
|
1401
|
-
(options) =>
|
|
1402
|
-
registry.getTools(id, baseUrl, requestScope, options),
|
|
1403
|
-
),
|
|
1404
|
-
),
|
|
1405
|
-
);
|
|
1406
|
-
const catalogs = new Map<string, ToolDef[] | Error>();
|
|
1407
|
-
loaded.forEach((result, index) => {
|
|
1408
|
-
catalogs.set(
|
|
1409
|
-
connectorIds[index],
|
|
1410
|
-
result.status === "fulfilled"
|
|
1411
|
-
? result.value
|
|
1412
|
-
: result.reason instanceof Error
|
|
1413
|
-
? result.reason
|
|
1414
|
-
: new Error(String(result.reason)),
|
|
1415
|
-
);
|
|
1416
|
-
});
|
|
1417
|
-
const out = resolved.map(({ address, resolved }) => {
|
|
1418
|
-
if (!resolved) {
|
|
1419
|
-
return { address, error: `Unknown address "${address}"` };
|
|
1420
|
-
}
|
|
1421
|
-
const catalog = catalogs.get(resolved.connector.id);
|
|
1422
|
-
if (catalog instanceof Error) {
|
|
1423
|
-
return { address, error: catalog.message };
|
|
1424
|
-
}
|
|
1425
|
-
const tool = catalog?.find((t) => t.name === resolved.toolName);
|
|
1426
|
-
if (!tool) {
|
|
1427
|
-
return {
|
|
1428
|
-
address,
|
|
1429
|
-
error: `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
|
|
1430
|
-
};
|
|
1431
|
-
}
|
|
1432
|
-
const schema = tool.inputSchema ?? { type: "object" };
|
|
1433
|
-
return {
|
|
1434
|
-
address,
|
|
1435
|
-
name: tool.name,
|
|
1436
|
-
description: summarizeDescription(
|
|
1437
|
-
tool.description,
|
|
1438
|
-
args.fullDescriptions === true,
|
|
1439
|
-
),
|
|
1440
|
-
...(connectorGuide(resolved.connector)
|
|
1441
|
-
? { guide: connectorSkillName(resolved.connector.id) }
|
|
1442
|
-
: {}),
|
|
1443
|
-
inputSchema: format === "json" ? schema : compactSchema(schema),
|
|
1444
|
-
...(tool.outputSchema
|
|
1445
|
-
? {
|
|
1446
|
-
outputSchema:
|
|
1447
|
-
format === "json"
|
|
1448
|
-
? tool.outputSchema
|
|
1449
|
-
: compactSchema(tool.outputSchema),
|
|
1450
|
-
}
|
|
1451
|
-
: {}),
|
|
1452
|
-
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
1453
|
-
};
|
|
1454
|
-
});
|
|
1455
|
-
return discoveryResult(
|
|
1456
|
-
{ tools: out },
|
|
1457
|
-
'Split the address list or use format: "compact".',
|
|
1458
|
-
);
|
|
1459
894
|
},
|
|
1460
895
|
|
|
1461
896
|
async callTool(args: CallArgs): Promise<ToolResult> {
|
|
@@ -1534,17 +969,29 @@ export function createMetaTools(
|
|
|
1534
969
|
runCall(
|
|
1535
970
|
{
|
|
1536
971
|
...c,
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
972
|
+
...((c.resultMode ?? args.resultMode) !== undefined
|
|
973
|
+
? { resultMode: c.resultMode ?? args.resultMode }
|
|
974
|
+
: {}),
|
|
975
|
+
...((c.timeoutMs ?? args.timeoutMs) !== undefined
|
|
976
|
+
? { timeoutMs: c.timeoutMs ?? args.timeoutMs }
|
|
977
|
+
: {}),
|
|
978
|
+
...((c.maxRetries ?? args.maxRetries) !== undefined
|
|
979
|
+
? { maxRetries: c.maxRetries ?? args.maxRetries }
|
|
980
|
+
: {}),
|
|
981
|
+
...((c.diagnostics ?? args.diagnostics) !== undefined
|
|
982
|
+
? { diagnostics: c.diagnostics ?? args.diagnostics }
|
|
983
|
+
: {}),
|
|
1541
984
|
},
|
|
1542
985
|
"batch_call",
|
|
1543
986
|
),
|
|
1544
987
|
),
|
|
1545
988
|
);
|
|
1546
989
|
const results = settled.map((s, i) => {
|
|
1547
|
-
const
|
|
990
|
+
const call = args.calls[i];
|
|
991
|
+
if (!call) {
|
|
992
|
+
throw new Error("Batch result has no corresponding call");
|
|
993
|
+
}
|
|
994
|
+
const { address } = call;
|
|
1548
995
|
if (s.status === "rejected") {
|
|
1549
996
|
return {
|
|
1550
997
|
address,
|
|
@@ -1562,19 +1009,19 @@ export function createMetaTools(
|
|
|
1562
1009
|
errorDetails: r.error,
|
|
1563
1010
|
durationMs: r.durationMs,
|
|
1564
1011
|
attempts: r.attempts,
|
|
1565
|
-
...((
|
|
1012
|
+
...((call.diagnostics ?? args.diagnostics)
|
|
1566
1013
|
? { timing: r.timing }
|
|
1567
1014
|
: {}),
|
|
1568
1015
|
};
|
|
1569
1016
|
}
|
|
1570
|
-
if ((
|
|
1017
|
+
if ((call.resultMode ?? args.resultMode) === "value") {
|
|
1571
1018
|
return {
|
|
1572
1019
|
address,
|
|
1573
1020
|
ok: true,
|
|
1574
1021
|
data: r.value,
|
|
1575
1022
|
durationMs: r.durationMs,
|
|
1576
1023
|
attempts: r.attempts,
|
|
1577
|
-
...((
|
|
1024
|
+
...((call.diagnostics ?? args.diagnostics)
|
|
1578
1025
|
? { timing: r.timing }
|
|
1579
1026
|
: {}),
|
|
1580
1027
|
};
|
|
@@ -1585,7 +1032,7 @@ export function createMetaTools(
|
|
|
1585
1032
|
result: r.toolResult.content,
|
|
1586
1033
|
durationMs: r.durationMs,
|
|
1587
1034
|
attempts: r.attempts,
|
|
1588
|
-
...((
|
|
1035
|
+
...((call.diagnostics ?? args.diagnostics)
|
|
1589
1036
|
? { timing: r.timing }
|
|
1590
1037
|
: {}),
|
|
1591
1038
|
};
|
|
@@ -1649,7 +1096,10 @@ export function createMetaTools(
|
|
|
1649
1096
|
}
|
|
1650
1097
|
const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
|
|
1651
1098
|
try {
|
|
1652
|
-
const status = await connector.startAuth(
|
|
1099
|
+
const status = await connector.startAuth(
|
|
1100
|
+
ctx,
|
|
1101
|
+
args.force !== undefined ? { force: args.force } : {},
|
|
1102
|
+
);
|
|
1653
1103
|
// startAuth just spoke to the downstream about this exact credential, so
|
|
1654
1104
|
// its answer replaces any older liveness verdict — including the stale
|
|
1655
1105
|
// `auth_required` that sent the agent here, once it reports ok.
|
|
@@ -1766,17 +1216,27 @@ export function registerMetaTools(
|
|
|
1766
1216
|
baseUrl: string;
|
|
1767
1217
|
defaultToolTimeoutMs?: number;
|
|
1768
1218
|
probeTimeoutMs?: number;
|
|
1219
|
+
discoveryConcurrency?: number;
|
|
1769
1220
|
activity?: ActivityRequestContext;
|
|
1770
1221
|
requestSignal?: AbortSignal;
|
|
1771
1222
|
defer?: DeferredWork;
|
|
1772
1223
|
},
|
|
1773
1224
|
): void {
|
|
1774
1225
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1226
|
+
...(ctx.defaultToolTimeoutMs !== undefined
|
|
1227
|
+
? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
|
|
1228
|
+
: {}),
|
|
1229
|
+
...(ctx.probeTimeoutMs !== undefined
|
|
1230
|
+
? { probeTimeoutMs: ctx.probeTimeoutMs }
|
|
1231
|
+
: {}),
|
|
1232
|
+
...(ctx.discoveryConcurrency !== undefined
|
|
1233
|
+
? { discoveryConcurrency: ctx.discoveryConcurrency }
|
|
1234
|
+
: {}),
|
|
1235
|
+
...(ctx.activity !== undefined ? { activity: ctx.activity } : {}),
|
|
1236
|
+
...(ctx.requestSignal !== undefined
|
|
1237
|
+
? { requestSignal: ctx.requestSignal }
|
|
1238
|
+
: {}),
|
|
1239
|
+
...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
|
|
1780
1240
|
});
|
|
1781
1241
|
|
|
1782
1242
|
server.registerTool(
|