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