@zackbart/connecta 0.7.9 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +135 -0
- package/README.md +47 -148
- package/dist/activity.d.ts +0 -8
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +0 -1
- package/dist/activity.js.map +1 -1
- package/dist/auth/bearer.d.ts +1 -7
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +3 -24
- package/dist/auth/bearer.js.map +1 -1
- package/dist/auth/clerk.d.ts +2 -11
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/auth/clerk.js +31 -46
- package/dist/auth/clerk.js.map +1 -1
- package/dist/call-admission.d.ts.map +1 -1
- package/dist/call-admission.js +9 -4
- package/dist/call-admission.js.map +1 -1
- package/dist/catalog-fingerprint.d.ts +20 -0
- package/dist/catalog-fingerprint.d.ts.map +1 -0
- package/dist/catalog-fingerprint.js +34 -0
- package/dist/catalog-fingerprint.js.map +1 -0
- package/dist/catalog-limits.d.ts +10 -0
- package/dist/catalog-limits.d.ts.map +1 -0
- package/dist/catalog-limits.js +10 -0
- package/dist/catalog-limits.js.map +1 -0
- package/dist/catalog-service.d.ts +137 -0
- package/dist/catalog-service.d.ts.map +1 -0
- package/dist/catalog-service.js +401 -0
- package/dist/catalog-service.js.map +1 -0
- package/dist/catalog.d.ts +6 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +53 -2
- package/dist/catalog.js.map +1 -1
- package/dist/concurrency.d.ts +4 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +31 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +22 -12
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +53 -41
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credentials.d.ts +3 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/errors.d.ts +19 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +10 -16
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +143 -330
- package/dist/execute.js.map +1 -1
- package/dist/executor-result.d.ts +0 -3
- package/dist/executor-result.d.ts.map +1 -1
- package/dist/executor-result.js +3 -3
- package/dist/executor-result.js.map +1 -1
- package/dist/executors/quickjs-child.js +3 -3
- package/dist/executors/quickjs-child.js.map +1 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -1
- package/dist/executors/quickjs-protocol.d.ts.map +1 -1
- package/dist/executors/quickjs-runtime.d.ts.map +1 -1
- package/dist/executors/quickjs-runtime.js +7 -2
- package/dist/executors/quickjs-runtime.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +4 -1
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/favicon.js +2 -2
- package/dist/favicon.js.map +1 -1
- package/dist/index.d.ts +8 -82
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -104
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +101 -0
- package/dist/invocation.d.ts.map +1 -0
- package/dist/invocation.js +405 -0
- package/dist/invocation.js.map +1 -0
- package/dist/meta-tools.d.ts +9 -95
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +235 -725
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +3 -0
- package/dist/operator-ui/generated.d.ts.map +1 -0
- package/dist/operator-ui/generated.js +5 -0
- package/dist/operator-ui/generated.js.map +1 -0
- package/dist/operator-ui/model.d.ts +73 -0
- package/dist/operator-ui/model.d.ts.map +1 -0
- package/dist/operator-ui/model.js +28 -0
- package/dist/operator-ui/model.js.map +1 -0
- package/dist/registry.d.ts +33 -184
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +345 -330
- package/dist/registry.js.map +1 -1
- package/dist/retired-toolkits.d.ts +3 -0
- package/dist/retired-toolkits.d.ts.map +1 -0
- package/dist/retired-toolkits.js +9 -0
- package/dist/retired-toolkits.js.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +170 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/credentials.d.ts +3 -0
- package/dist/routes/credentials.d.ts.map +1 -0
- package/dist/routes/credentials.js +170 -0
- package/dist/routes/credentials.js.map +1 -0
- package/dist/routes/mcp.d.ts +8 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +270 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/oauth.d.ts +4 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +263 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/shared.d.ts +76 -0
- package/dist/routes/shared.d.ts.map +1 -0
- package/dist/routes/shared.js +140 -0
- package/dist/routes/shared.js.map +1 -0
- package/dist/routes/ui.d.ts +3 -0
- package/dist/routes/ui.d.ts.map +1 -0
- package/dist/routes/ui.js +116 -0
- package/dist/routes/ui.js.map +1 -0
- package/dist/server.d.ts +9 -40
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +62 -1322
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +3 -21
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +15 -43
- package/dist/skills.js.map +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +3 -1
- package/dist/storage/file.js.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +3 -1
- package/dist/storage/memory.js.map +1 -1
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +2 -4
- package/dist/timeout.js.map +1 -1
- package/dist/types.d.ts +9 -59
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +3 -94
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +30 -1427
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +15 -8
- package/src/activity.ts +0 -9
- package/src/auth/bearer.ts +4 -33
- package/src/auth/clerk.ts +35 -48
- package/src/call-admission.ts +8 -6
- package/src/catalog-fingerprint.ts +52 -0
- package/src/catalog-limits.ts +11 -0
- package/src/catalog-service.ts +601 -0
- package/src/catalog.ts +57 -2
- package/src/concurrency.ts +39 -0
- package/src/connectors/api.ts +22 -12
- package/src/connectors/remote-mcp.ts +63 -45
- package/src/credentials.ts +2 -2
- package/src/errors.ts +21 -1
- package/src/execute.ts +193 -423
- package/src/executor-result.ts +3 -3
- package/src/executors/quickjs-child.ts +8 -4
- package/src/executors/quickjs-protocol.ts +1 -1
- package/src/executors/quickjs-runtime.ts +5 -1
- package/src/executors/quickjs.ts +11 -8
- package/src/favicon.ts +2 -2
- package/src/index.ts +70 -222
- package/src/invocation.ts +580 -0
- package/src/meta-tools.ts +393 -985
- package/src/operator-ui/browser.css +360 -0
- package/src/operator-ui/browser.ts +987 -0
- package/src/operator-ui/generated.ts +4 -0
- package/src/operator-ui/model.ts +105 -0
- package/src/registry.ts +459 -439
- package/src/retired-toolkits.ts +14 -0
- package/src/routes/activity.ts +215 -0
- package/src/routes/credentials.ts +254 -0
- package/src/routes/mcp.ts +339 -0
- package/src/routes/oauth.ts +322 -0
- package/src/routes/shared.ts +240 -0
- package/src/routes/ui.ts +143 -0
- package/src/server.ts +64 -1731
- package/src/skills.ts +15 -43
- package/src/storage/file.ts +3 -1
- package/src/storage/memory.ts +3 -1
- package/src/timeout.ts +2 -4
- package/src/types.ts +8 -60
- package/src/ui.ts +60 -1538
- package/src/version.ts +1 -1
- package/dist/credential-health.d.ts +0 -227
- package/dist/credential-health.d.ts.map +0 -1
- package/dist/credential-health.js +0 -606
- package/dist/credential-health.js.map +0 -1
- package/dist/toolkits.d.ts +0 -146
- package/dist/toolkits.d.ts.map +0 -1
- package/dist/toolkits.js +0 -322
- package/dist/toolkits.js.map +0 -1
- package/src/credential-health.ts +0 -832
- package/src/toolkits.ts +0 -461
package/src/meta-tools.ts
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
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
|
+
DEFAULT_SEARCH_LIMIT,
|
|
11
|
+
DiscoveryPolicyError,
|
|
12
|
+
groupedSearchResult,
|
|
13
|
+
MAX_DESCRIBE_ADDRESSES,
|
|
14
|
+
MAX_DISCOVERY_RESULT_BYTES,
|
|
15
|
+
MAX_SEARCH_LIMIT,
|
|
16
|
+
} from "./catalog-service.js";
|
|
17
|
+
import {
|
|
18
|
+
mapSettledWithConcurrency,
|
|
19
|
+
resolveDiscoveryConcurrency,
|
|
20
|
+
} from "./concurrency.js";
|
|
9
21
|
import {
|
|
10
22
|
closeConnectorScope,
|
|
11
23
|
type DeferredWork,
|
|
12
24
|
} from "./connector-scope.js";
|
|
13
|
-
import { unwrapMcpResult } from "./mcp-result.js";
|
|
14
25
|
import {
|
|
15
26
|
classifyCallError,
|
|
16
|
-
ConnectorCallError,
|
|
17
27
|
messageLooksRetryable,
|
|
18
28
|
type CallErrorDetails,
|
|
19
29
|
} from "./errors.js";
|
|
20
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
InvocationService,
|
|
32
|
+
MAX_RETRY_BACKOFF_MS,
|
|
33
|
+
retryBackoffMs,
|
|
34
|
+
type InvocationTiming,
|
|
35
|
+
} from "./invocation.js";
|
|
21
36
|
import {
|
|
22
37
|
isValidMaxResultBytes,
|
|
23
38
|
MIN_MAX_RESULT_BYTES,
|
|
@@ -25,8 +40,6 @@ import {
|
|
|
25
40
|
type RegistryView,
|
|
26
41
|
} from "./registry.js";
|
|
27
42
|
import {
|
|
28
|
-
connectorGuide,
|
|
29
|
-
connectorSkillName,
|
|
30
43
|
hasConnectorGuides,
|
|
31
44
|
listSkills,
|
|
32
45
|
resolveSkill,
|
|
@@ -36,8 +49,15 @@ import {
|
|
|
36
49
|
normalizeTimeoutMs,
|
|
37
50
|
withAbortableTimeout,
|
|
38
51
|
} from "./timeout.js";
|
|
39
|
-
import {
|
|
40
|
-
|
|
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
|
+
retryBackoffMs,
|
|
60
|
+
};
|
|
41
61
|
|
|
42
62
|
interface TextContent {
|
|
43
63
|
type: "text";
|
|
@@ -51,10 +71,12 @@ export interface ToolResult {
|
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
const RESULT_TTL_SECONDS = 900;
|
|
74
|
+
const enc = new TextEncoder();
|
|
75
|
+
const dec = new TextDecoder();
|
|
54
76
|
|
|
55
77
|
export function jsonResult(obj: unknown): ToolResult {
|
|
56
78
|
return {
|
|
57
|
-
content: [{ type: "text", text: JSON.stringify(obj
|
|
79
|
+
content: [{ type: "text", text: JSON.stringify(obj) }],
|
|
58
80
|
...(obj !== null && typeof obj === "object" && !Array.isArray(obj)
|
|
59
81
|
? { structuredContent: obj as Record<string, unknown> }
|
|
60
82
|
: {}),
|
|
@@ -69,95 +91,8 @@ function msg(err: unknown): string {
|
|
|
69
91
|
return err instanceof Error ? err.message : String(err);
|
|
70
92
|
}
|
|
71
93
|
|
|
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);
|
|
94
|
+
function errorDetails(code: string, message: string): CallErrorDetails {
|
|
95
|
+
return { code, message, retryable: messageLooksRetryable(message) };
|
|
161
96
|
}
|
|
162
97
|
|
|
163
98
|
function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
|
|
@@ -172,8 +107,12 @@ function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
|
|
|
172
107
|
return result;
|
|
173
108
|
}
|
|
174
109
|
|
|
175
|
-
function discoveryResult(
|
|
110
|
+
async function discoveryResult(
|
|
111
|
+
operation: () => unknown | Promise<unknown>,
|
|
112
|
+
hint: string,
|
|
113
|
+
): Promise<ToolResult> {
|
|
176
114
|
try {
|
|
115
|
+
const value = await operation();
|
|
177
116
|
const text = boundedDiscoveryText(value, hint);
|
|
178
117
|
return {
|
|
179
118
|
content: [{ type: "text", text }],
|
|
@@ -189,71 +128,14 @@ function discoveryResult(value: unknown, hint: string): ToolResult {
|
|
|
189
128
|
}
|
|
190
129
|
}
|
|
191
130
|
|
|
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
131
|
|
|
250
132
|
/** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
|
|
251
|
-
function isContinuationByte(b: number): boolean {
|
|
252
|
-
return (b & 0xc0) === 0x80;
|
|
133
|
+
function isContinuationByte(b: number | undefined): boolean {
|
|
134
|
+
return b !== undefined && (b & 0xc0) === 0x80;
|
|
253
135
|
}
|
|
254
136
|
|
|
255
137
|
/** Smallest accepted `get_result` byte offset. */
|
|
256
|
-
|
|
138
|
+
const MIN_RESULT_OFFSET = 0;
|
|
257
139
|
|
|
258
140
|
/**
|
|
259
141
|
* The one definition of a usable `get_result` offset: a whole number of bytes
|
|
@@ -269,7 +151,7 @@ export const MIN_RESULT_OFFSET = 0;
|
|
|
269
151
|
* error. An offset past the end of the payload stays legal: it is a whole
|
|
270
152
|
* number of bytes, and it answers with an empty final page.
|
|
271
153
|
*/
|
|
272
|
-
|
|
154
|
+
function isValidResultOffset(value: number): boolean {
|
|
273
155
|
return Number.isInteger(value) && value >= MIN_RESULT_OFFSET;
|
|
274
156
|
}
|
|
275
157
|
|
|
@@ -333,8 +215,9 @@ export function alignEndToCharBoundary(
|
|
|
333
215
|
|
|
334
216
|
/** Resolve a dot-path (segments) against a value; `key[]` maps the tail over an array. */
|
|
335
217
|
function resolvePath(value: unknown, segments: string[]): unknown {
|
|
336
|
-
|
|
337
|
-
|
|
218
|
+
const seg = segments[0];
|
|
219
|
+
if (seg === undefined) return value;
|
|
220
|
+
const rest = segments.slice(1);
|
|
338
221
|
const isArr = seg.endsWith("[]");
|
|
339
222
|
const key = isArr ? seg.slice(0, -2) : seg;
|
|
340
223
|
let next: unknown = value;
|
|
@@ -375,7 +258,7 @@ function applyFieldsToContent(
|
|
|
375
258
|
} catch {
|
|
376
259
|
return b;
|
|
377
260
|
}
|
|
378
|
-
return { ...b, text: JSON.stringify(applyFields(parsed, fields)
|
|
261
|
+
return { ...b, text: JSON.stringify(applyFields(parsed, fields)) };
|
|
379
262
|
});
|
|
380
263
|
}
|
|
381
264
|
|
|
@@ -396,8 +279,8 @@ function applyFieldsToContent(
|
|
|
396
279
|
* the three give one answer to the same question. A value JSON cannot serialize
|
|
397
280
|
* at all (a BigInt) still throws, as before, and is reported as a failure.
|
|
398
281
|
*/
|
|
399
|
-
|
|
400
|
-
const serialized = JSON.stringify(value
|
|
282
|
+
function serializeResultText(value: unknown): string {
|
|
283
|
+
const serialized = JSON.stringify(value);
|
|
401
284
|
return serialized === undefined ? String(value) : serialized;
|
|
402
285
|
}
|
|
403
286
|
|
|
@@ -511,7 +394,7 @@ async function guardContent(
|
|
|
511
394
|
): Promise<ToolResult> {
|
|
512
395
|
let text: string;
|
|
513
396
|
try {
|
|
514
|
-
text = JSON.stringify(content
|
|
397
|
+
text = JSON.stringify(content);
|
|
515
398
|
} catch {
|
|
516
399
|
// A block carrying a BigInt or a cycle cannot be serialized, so it cannot
|
|
517
400
|
// be measured, stashed, or paged either — there is nothing this guard could
|
|
@@ -551,7 +434,7 @@ export interface ListArgs {
|
|
|
551
434
|
/** When false, return cached/observed health without downstream I/O. */
|
|
552
435
|
probe?: boolean;
|
|
553
436
|
}
|
|
554
|
-
|
|
437
|
+
type ResultMode = "mcp" | "value";
|
|
555
438
|
export interface CallArgs {
|
|
556
439
|
address: string;
|
|
557
440
|
args?: Record<string, unknown>;
|
|
@@ -573,15 +456,7 @@ export interface GetResultArgs {
|
|
|
573
456
|
/** Page size in bytes; a whole number >= 1. Defaults to the deployment cap. */
|
|
574
457
|
maxBytes?: number;
|
|
575
458
|
}
|
|
576
|
-
export
|
|
577
|
-
address: string;
|
|
578
|
-
args?: Record<string, unknown>;
|
|
579
|
-
fields?: string[];
|
|
580
|
-
resultMode?: ResultMode;
|
|
581
|
-
timeoutMs?: number;
|
|
582
|
-
maxRetries?: number;
|
|
583
|
-
diagnostics?: boolean;
|
|
584
|
-
}
|
|
459
|
+
export type BatchCall = CallArgs;
|
|
585
460
|
export interface BatchArgs {
|
|
586
461
|
calls: BatchCall[];
|
|
587
462
|
resultMode?: ResultMode;
|
|
@@ -616,6 +491,8 @@ export function createMetaTools(
|
|
|
616
491
|
defaultToolTimeoutMs?: number;
|
|
617
492
|
/** Per-connector deadline for the list/search/describe probe fan-out. Default 30_000. */
|
|
618
493
|
probeTimeoutMs?: number;
|
|
494
|
+
/** Maximum simultaneous connector discovery operations. Default 4. */
|
|
495
|
+
discoveryConcurrency?: number;
|
|
619
496
|
activity?: ActivityRequestContext;
|
|
620
497
|
/** Inbound request cancellation shared by direct and batch child calls. */
|
|
621
498
|
requestSignal?: AbortSignal;
|
|
@@ -629,10 +506,19 @@ export function createMetaTools(
|
|
|
629
506
|
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
|
|
630
507
|
const probeTimeoutMs =
|
|
631
508
|
normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
509
|
+
const discoveryConcurrency = resolveDiscoveryConcurrency(
|
|
510
|
+
opts.discoveryConcurrency,
|
|
511
|
+
);
|
|
632
512
|
// createMetaTools() is called once per inbound MCP request. Sharing this
|
|
633
513
|
// identity lets remote connectors reuse one downstream client inside that
|
|
634
514
|
// request without leaking request-bound I/O into the next one.
|
|
635
515
|
const requestScope = {};
|
|
516
|
+
const catalog = new CatalogService(registry, baseUrl, {
|
|
517
|
+
requestScope,
|
|
518
|
+
probeTimeoutMs,
|
|
519
|
+
concurrency: discoveryConcurrency,
|
|
520
|
+
});
|
|
521
|
+
const invocation = new InvocationService(registry, catalog, opts.activity);
|
|
636
522
|
const withProbeDeadline = <T>(
|
|
637
523
|
label: string,
|
|
638
524
|
operation: (options: {
|
|
@@ -650,370 +536,120 @@ export function createMetaTools(
|
|
|
650
536
|
toolResult: ToolResult;
|
|
651
537
|
durationMs: number;
|
|
652
538
|
attempts: number;
|
|
653
|
-
timing:
|
|
654
|
-
catalogMs: number;
|
|
655
|
-
admissionMs: number;
|
|
656
|
-
connectorMs: number;
|
|
657
|
-
backoffMs: number;
|
|
658
|
-
resultProcessingMs: number;
|
|
659
|
-
totalMs: number;
|
|
660
|
-
};
|
|
539
|
+
timing: InvocationTiming;
|
|
661
540
|
value?: unknown;
|
|
662
|
-
error?:
|
|
541
|
+
error?: CallErrorDetails;
|
|
663
542
|
}
|
|
664
543
|
|
|
665
|
-
|
|
544
|
+
interface ProcessedCallResult {
|
|
545
|
+
toolResult: ToolResult;
|
|
546
|
+
value?: unknown;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
666
550
|
async function runCall(
|
|
667
551
|
call: BatchCall,
|
|
668
552
|
source: ActivityCallSource,
|
|
669
553
|
options: { allowDestructive?: boolean } = {},
|
|
670
554
|
): Promise<RunCallOutcome> {
|
|
671
|
-
const started = Date.now();
|
|
672
|
-
let catalogMs = 0;
|
|
673
|
-
let admissionMs = 0;
|
|
674
|
-
let connectorMs = 0;
|
|
675
|
-
let backoffMs = 0;
|
|
676
|
-
let resultProcessingMs = 0;
|
|
677
|
-
let attempts = 0;
|
|
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}`,
|
|
696
|
-
source,
|
|
697
|
-
outcome,
|
|
698
|
-
durationMs: Date.now() - started,
|
|
699
|
-
attempts,
|
|
700
|
-
...(errorCode ? { errorCode } : {}),
|
|
701
|
-
});
|
|
702
|
-
};
|
|
703
|
-
const failed = (error: ErrorDetails): RunCallOutcome => {
|
|
704
|
-
const durationMs = Date.now() - started;
|
|
705
|
-
const diagnostics = timing();
|
|
706
|
-
record(
|
|
707
|
-
error.code === "timeout"
|
|
708
|
-
? "timeout"
|
|
709
|
-
: error.code === "cancelled"
|
|
710
|
-
? "cancelled"
|
|
711
|
-
: "error",
|
|
712
|
-
error.code,
|
|
713
|
-
);
|
|
714
|
-
return {
|
|
715
|
-
toolResult:
|
|
716
|
-
call.resultMode === "value"
|
|
717
|
-
? jsonResult({
|
|
718
|
-
ok: false,
|
|
719
|
-
error,
|
|
720
|
-
durationMs,
|
|
721
|
-
attempts,
|
|
722
|
-
...(call.diagnostics ? { timing: diagnostics } : {}),
|
|
723
|
-
})
|
|
724
|
-
: errorResult(error.message),
|
|
725
|
-
durationMs,
|
|
726
|
-
attempts,
|
|
727
|
-
timing: diagnostics,
|
|
728
|
-
error,
|
|
729
|
-
};
|
|
730
|
-
};
|
|
731
|
-
if (!resolved) {
|
|
732
|
-
return failed(
|
|
733
|
-
errorDetails("unknown_address", `Unknown address "${call.address}"`),
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
555
|
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
556
|
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
557
|
const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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",
|
|
558
|
+
const outcome = await invocation.invoke<ProcessedCallResult>(
|
|
559
|
+
call.address,
|
|
560
|
+
call.args ?? {},
|
|
561
|
+
{
|
|
562
|
+
source,
|
|
563
|
+
...(options.allowDestructive !== undefined
|
|
564
|
+
? { allowDestructive: options.allowDestructive }
|
|
565
|
+
: {}),
|
|
566
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
567
|
+
...(call.maxRetries !== undefined
|
|
568
|
+
? { maxRetries: call.maxRetries }
|
|
569
|
+
: {}),
|
|
570
|
+
...(opts.requestSignal !== undefined
|
|
571
|
+
? { requestSignal: opts.requestSignal }
|
|
572
|
+
: {}),
|
|
573
|
+
unwrapResult: call.resultMode === "value",
|
|
574
|
+
processResult: async (result, resolved) => {
|
|
575
|
+
// Result-size cap for THIS call: the connector's own override wins,
|
|
576
|
+
// then the deployment-wide value, then the built-in default (already
|
|
577
|
+
// folded into `globalCap`). Resolved per call so one batch_call can
|
|
578
|
+
// mix a tight-capped connector with siblings on the global cap. An
|
|
579
|
+
// override the registry already warned about at startup is dropped
|
|
580
|
+
// here, so the connector simply inherits `globalCap`.
|
|
581
|
+
const cap = resolveMaxResultBytes(
|
|
582
|
+
resolved.connector.maxResultBytes,
|
|
583
|
+
globalCap,
|
|
889
584
|
);
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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;
|
|
585
|
+
if (call.resultMode === "value") {
|
|
586
|
+
let value = fields ? applyFields(result, fields) : result;
|
|
587
|
+
value = await guardValue(value, results, cap);
|
|
588
|
+
return {
|
|
589
|
+
toolResult: jsonResult({ ok: true, data: value }),
|
|
590
|
+
value,
|
|
591
|
+
};
|
|
942
592
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
durationMs,
|
|
976
|
-
attempts,
|
|
977
|
-
...(call.diagnostics ? { timing: diagnostics } : {}),
|
|
978
|
-
}),
|
|
979
|
-
durationMs,
|
|
980
|
-
attempts,
|
|
981
|
-
timing: diagnostics,
|
|
982
|
-
value,
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
if (resolved.connector.kind === "mcp") {
|
|
986
|
-
let content = mr?.content ?? [];
|
|
987
|
-
if (fields) content = applyFieldsToContent(content, fields);
|
|
988
|
-
const toolResult = await guardContent(content, results, cap);
|
|
989
|
-
resultProcessingMs += Date.now() - processingStarted;
|
|
990
|
-
record("success");
|
|
991
|
-
return {
|
|
992
|
-
toolResult,
|
|
993
|
-
durationMs: Date.now() - started,
|
|
994
|
-
attempts,
|
|
995
|
-
timing: timing(),
|
|
996
|
-
};
|
|
593
|
+
if (resolved.connector.kind === "mcp") {
|
|
594
|
+
const mcpResult = result as { content?: TextContent[] };
|
|
595
|
+
let content = mcpResult?.content ?? [];
|
|
596
|
+
if (fields) content = applyFieldsToContent(content, fields);
|
|
597
|
+
return { toolResult: await guardContent(content, results, cap) };
|
|
598
|
+
}
|
|
599
|
+
const value = fields ? applyFields(result, fields) : result;
|
|
600
|
+
return {
|
|
601
|
+
toolResult: await guardText(
|
|
602
|
+
serializeResultText(value),
|
|
603
|
+
results,
|
|
604
|
+
cap,
|
|
605
|
+
),
|
|
606
|
+
value,
|
|
607
|
+
};
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
);
|
|
611
|
+
if (!outcome.ok) {
|
|
612
|
+
const failedResult =
|
|
613
|
+
outcome.error.code === "auth_required" ||
|
|
614
|
+
call.resultMode === "value"
|
|
615
|
+
? jsonResult({
|
|
616
|
+
ok: false,
|
|
617
|
+
error: outcome.error,
|
|
618
|
+
durationMs: outcome.durationMs,
|
|
619
|
+
attempts: outcome.attempts,
|
|
620
|
+
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
621
|
+
})
|
|
622
|
+
: errorResult(outcome.error.message);
|
|
623
|
+
if (outcome.error.code === "auth_required") {
|
|
624
|
+
failedResult.isError = true;
|
|
997
625
|
}
|
|
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
626
|
return {
|
|
1007
|
-
toolResult,
|
|
1008
|
-
durationMs:
|
|
1009
|
-
attempts,
|
|
1010
|
-
timing: timing
|
|
1011
|
-
|
|
627
|
+
toolResult: failedResult,
|
|
628
|
+
durationMs: outcome.durationMs,
|
|
629
|
+
attempts: outcome.attempts,
|
|
630
|
+
timing: outcome.timing,
|
|
631
|
+
error: outcome.error,
|
|
1012
632
|
};
|
|
1013
|
-
} catch (err) {
|
|
1014
|
-
resultProcessingMs += Date.now() - processingStarted;
|
|
1015
|
-
return failed(errorDetails("result_processing_failed", msg(err)));
|
|
1016
633
|
}
|
|
634
|
+
const valueModeResult =
|
|
635
|
+
call.resultMode === "value"
|
|
636
|
+
? jsonResult({
|
|
637
|
+
ok: true,
|
|
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,118 @@ 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
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
});
|
|
683
|
+
const inspect = async (c: (typeof connectors)[number]) => {
|
|
684
|
+
const statusStarted = Date.now();
|
|
685
|
+
const observed = registry.healthFor(c.id);
|
|
686
|
+
const drift = await registry.credentialDriftFor(c.id);
|
|
687
|
+
let status:
|
|
688
|
+
| ConnectorStatus
|
|
689
|
+
| { state: "ok" | "error" | "unknown"; message?: string };
|
|
690
|
+
if (drift) {
|
|
691
|
+
status = { state: "auth_required", message: drift };
|
|
692
|
+
} else if (probe) {
|
|
693
|
+
try {
|
|
694
|
+
status = await withProbeDeadline(
|
|
695
|
+
`list_connectors probe of "${c.id}"`,
|
|
696
|
+
(options) =>
|
|
697
|
+
registry.statusFor(c.id, baseUrl, scope, options),
|
|
698
|
+
);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
// A probe that outran probeTimeoutMs (or otherwise threw)
|
|
701
|
+
// degrades this connector to an error status rather than
|
|
702
|
+
// hanging the whole list_connectors call.
|
|
703
|
+
status = { state: "error", message: msg(err) };
|
|
1137
704
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
: {}),
|
|
1179
|
-
});
|
|
1180
|
-
} else {
|
|
1181
|
-
status = { state: "error" as const, message: msg(err) };
|
|
705
|
+
} else {
|
|
706
|
+
const derived =
|
|
707
|
+
observed?.consecutiveFailures && observed.consecutiveFailures > 0
|
|
708
|
+
? ("error" as const)
|
|
709
|
+
: registry.hasObservedSuccess(c.id) || c.kind === "api"
|
|
710
|
+
? ("ok" as const)
|
|
711
|
+
: ("unknown" as const);
|
|
712
|
+
status = {
|
|
713
|
+
state: derived,
|
|
714
|
+
...(observed?.lastError ? { message: observed.lastError } : {}),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
// Stamped after any live probe so the response reports when its
|
|
718
|
+
// observation completed, not when a potentially slow request began.
|
|
719
|
+
const checkedAt = new Date().toISOString();
|
|
720
|
+
let tools = registry.peekTools(c.id);
|
|
721
|
+
// An auth_required status may have just started OAuth. A second
|
|
722
|
+
// listTools probe would overwrite its state/verifier while returning
|
|
723
|
+
// the first (now stale) authorization URL.
|
|
724
|
+
if (probe && status.state === "ok") {
|
|
725
|
+
try {
|
|
726
|
+
tools = await withProbeDeadline(
|
|
727
|
+
`list_connectors catalog refresh of "${c.id}"`,
|
|
728
|
+
(options) =>
|
|
729
|
+
registry.refreshTools(c.id, baseUrl, scope, options),
|
|
730
|
+
);
|
|
731
|
+
registry.recordSuccess(c.id, Date.now() - statusStarted);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
const details = classifyCallError(err);
|
|
734
|
+
if (details.code === "auth_required") {
|
|
735
|
+
let authStatus: ConnectorStatus | undefined;
|
|
736
|
+
try {
|
|
737
|
+
authStatus = await withProbeDeadline(
|
|
738
|
+
`list_connectors authorization status of "${c.id}"`,
|
|
739
|
+
(options) =>
|
|
740
|
+
registry.statusFor(c.id, baseUrl, scope, options),
|
|
741
|
+
);
|
|
742
|
+
} catch {
|
|
743
|
+
// The typed auth verdict is still authoritative; this second
|
|
744
|
+
// read exists only to recover the connector's pending URL.
|
|
1182
745
|
}
|
|
1183
|
-
|
|
746
|
+
status =
|
|
747
|
+
authStatus?.state === "auth_required"
|
|
748
|
+
? authStatus
|
|
749
|
+
: {
|
|
750
|
+
state: "auth_required" as const,
|
|
751
|
+
message: details.message,
|
|
752
|
+
};
|
|
753
|
+
} else {
|
|
754
|
+
status = { state: "error" as const, message: msg(err) };
|
|
1184
755
|
}
|
|
756
|
+
registry.recordFailure(c.id, Date.now() - statusStarted, err);
|
|
1185
757
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
758
|
+
}
|
|
759
|
+
const latencyMs = Date.now() - statusStarted;
|
|
760
|
+
const latestObserved = registry.healthFor(c.id);
|
|
761
|
+
return {
|
|
762
|
+
id: c.id,
|
|
763
|
+
...(c.title ? { title: c.title } : {}),
|
|
764
|
+
description: c.description,
|
|
765
|
+
toolCount: tools?.length ?? 0,
|
|
766
|
+
status: status.state,
|
|
767
|
+
checkedAt,
|
|
768
|
+
latencyMs,
|
|
769
|
+
probe,
|
|
770
|
+
...(latestObserved ?? observed),
|
|
771
|
+
...("authorizationUrl" in status &&
|
|
772
|
+
status.authorizationUrl && {
|
|
773
|
+
authorizationUrl: status.authorizationUrl,
|
|
774
|
+
}),
|
|
775
|
+
...(status.message && { message: status.message }),
|
|
776
|
+
};
|
|
777
|
+
};
|
|
778
|
+
const settled = await mapSettledWithConcurrency(
|
|
779
|
+
connectors,
|
|
780
|
+
discoveryConcurrency,
|
|
781
|
+
inspect,
|
|
1208
782
|
);
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
783
|
+
if (probe) {
|
|
784
|
+
await mapSettledWithConcurrency(
|
|
785
|
+
connectors,
|
|
786
|
+
discoveryConcurrency,
|
|
787
|
+
(connector) =>
|
|
788
|
+
closeConnectorScope(
|
|
789
|
+
connector,
|
|
790
|
+
registry.contextFor(connector.id, baseUrl, scope),
|
|
791
|
+
opts.defer,
|
|
792
|
+
),
|
|
793
|
+
);
|
|
1211
794
|
}
|
|
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
795
|
const out = settled.map((result) => {
|
|
1223
796
|
if (result.status === "rejected") throw result.reason;
|
|
1224
797
|
return result.value;
|
|
@@ -1227,233 +800,16 @@ export function createMetaTools(
|
|
|
1227
800
|
},
|
|
1228
801
|
|
|
1229
802
|
async searchTools(args: SearchArgs): Promise<ToolResult> {
|
|
1230
|
-
const q = args.query ?? "";
|
|
1231
|
-
let limit: number;
|
|
1232
|
-
try {
|
|
1233
|
-
limit = discoverySearchLimit(args.limit);
|
|
1234
|
-
} catch (err) {
|
|
1235
|
-
if (err instanceof DiscoveryPolicyError) {
|
|
1236
|
-
return discoveryErrorResult(err);
|
|
1237
|
-
}
|
|
1238
|
-
throw err;
|
|
1239
|
-
}
|
|
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
803
|
return discoveryResult(
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
},
|
|
804
|
+
async () =>
|
|
805
|
+
groupedSearchResult(await catalog.search(args)),
|
|
1372
806
|
"Request a smaller limit, omit fullDescriptions, or use compact schemas.",
|
|
1373
807
|
);
|
|
1374
808
|
},
|
|
1375
809
|
|
|
1376
810
|
async describeTools(args: DescribeArgs): Promise<ToolResult> {
|
|
1377
|
-
try {
|
|
1378
|
-
discoveryAddresses(args.addresses);
|
|
1379
|
-
} catch (err) {
|
|
1380
|
-
if (err instanceof DiscoveryPolicyError) {
|
|
1381
|
-
return discoveryErrorResult(err);
|
|
1382
|
-
}
|
|
1383
|
-
throw err;
|
|
1384
|
-
}
|
|
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
811
|
return discoveryResult(
|
|
1456
|
-
{ tools:
|
|
812
|
+
async () => ({ tools: await catalog.describe(args) }),
|
|
1457
813
|
'Split the address list or use format: "compact".',
|
|
1458
814
|
);
|
|
1459
815
|
},
|
|
@@ -1534,17 +890,29 @@ export function createMetaTools(
|
|
|
1534
890
|
runCall(
|
|
1535
891
|
{
|
|
1536
892
|
...c,
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
893
|
+
...((c.resultMode ?? args.resultMode) !== undefined
|
|
894
|
+
? { resultMode: c.resultMode ?? args.resultMode }
|
|
895
|
+
: {}),
|
|
896
|
+
...((c.timeoutMs ?? args.timeoutMs) !== undefined
|
|
897
|
+
? { timeoutMs: c.timeoutMs ?? args.timeoutMs }
|
|
898
|
+
: {}),
|
|
899
|
+
...((c.maxRetries ?? args.maxRetries) !== undefined
|
|
900
|
+
? { maxRetries: c.maxRetries ?? args.maxRetries }
|
|
901
|
+
: {}),
|
|
902
|
+
...((c.diagnostics ?? args.diagnostics) !== undefined
|
|
903
|
+
? { diagnostics: c.diagnostics ?? args.diagnostics }
|
|
904
|
+
: {}),
|
|
1541
905
|
},
|
|
1542
906
|
"batch_call",
|
|
1543
907
|
),
|
|
1544
908
|
),
|
|
1545
909
|
);
|
|
1546
910
|
const results = settled.map((s, i) => {
|
|
1547
|
-
const
|
|
911
|
+
const call = args.calls[i];
|
|
912
|
+
if (!call) {
|
|
913
|
+
throw new Error("Batch result has no corresponding call");
|
|
914
|
+
}
|
|
915
|
+
const { address } = call;
|
|
1548
916
|
if (s.status === "rejected") {
|
|
1549
917
|
return {
|
|
1550
918
|
address,
|
|
@@ -1562,19 +930,19 @@ export function createMetaTools(
|
|
|
1562
930
|
errorDetails: r.error,
|
|
1563
931
|
durationMs: r.durationMs,
|
|
1564
932
|
attempts: r.attempts,
|
|
1565
|
-
...((
|
|
933
|
+
...((call.diagnostics ?? args.diagnostics)
|
|
1566
934
|
? { timing: r.timing }
|
|
1567
935
|
: {}),
|
|
1568
936
|
};
|
|
1569
937
|
}
|
|
1570
|
-
if ((
|
|
938
|
+
if ((call.resultMode ?? args.resultMode) === "value") {
|
|
1571
939
|
return {
|
|
1572
940
|
address,
|
|
1573
941
|
ok: true,
|
|
1574
942
|
data: r.value,
|
|
1575
943
|
durationMs: r.durationMs,
|
|
1576
944
|
attempts: r.attempts,
|
|
1577
|
-
...((
|
|
945
|
+
...((call.diagnostics ?? args.diagnostics)
|
|
1578
946
|
? { timing: r.timing }
|
|
1579
947
|
: {}),
|
|
1580
948
|
};
|
|
@@ -1585,7 +953,7 @@ export function createMetaTools(
|
|
|
1585
953
|
result: r.toolResult.content,
|
|
1586
954
|
durationMs: r.durationMs,
|
|
1587
955
|
attempts: r.attempts,
|
|
1588
|
-
...((
|
|
956
|
+
...((call.diagnostics ?? args.diagnostics)
|
|
1589
957
|
? { timing: r.timing }
|
|
1590
958
|
: {}),
|
|
1591
959
|
};
|
|
@@ -1629,6 +997,21 @@ export function createMetaTools(
|
|
|
1629
997
|
...(details.retryAfterMs !== undefined
|
|
1630
998
|
? { retryAfterMs: details.retryAfterMs }
|
|
1631
999
|
: {}),
|
|
1000
|
+
...(details.connector !== undefined
|
|
1001
|
+
? { connector: batchSummaryString(details.connector) }
|
|
1002
|
+
: {}),
|
|
1003
|
+
...(details.operation !== undefined
|
|
1004
|
+
? { operation: batchSummaryString(details.operation) }
|
|
1005
|
+
: {}),
|
|
1006
|
+
...(details.recovery !== undefined
|
|
1007
|
+
? { recovery: details.recovery }
|
|
1008
|
+
: {}),
|
|
1009
|
+
...(details.nextAction !== undefined
|
|
1010
|
+
? { nextAction: details.nextAction }
|
|
1011
|
+
: {}),
|
|
1012
|
+
...(details.retry !== undefined
|
|
1013
|
+
? { retry: batchSummaryString(details.retry) }
|
|
1014
|
+
: {}),
|
|
1632
1015
|
},
|
|
1633
1016
|
};
|
|
1634
1017
|
}),
|
|
@@ -1643,24 +1026,62 @@ export function createMetaTools(
|
|
|
1643
1026
|
return errorResult(`Unknown connector "${args.connector}"`);
|
|
1644
1027
|
}
|
|
1645
1028
|
if (!connector.startAuth) {
|
|
1646
|
-
|
|
1647
|
-
|
|
1029
|
+
if (!connector.credential) {
|
|
1030
|
+
return jsonResult({
|
|
1031
|
+
connector: connector.id,
|
|
1032
|
+
recovery: "unavailable",
|
|
1033
|
+
message:
|
|
1034
|
+
`Connector "${connector.id}" declares neither downstream OAuth ` +
|
|
1035
|
+
"nor an operator-managed credential slot. Update the connector " +
|
|
1036
|
+
"or deployment configuration before retrying.",
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
const ctx = registry.contextFor(
|
|
1040
|
+
connector.id,
|
|
1041
|
+
baseUrl,
|
|
1042
|
+
requestScope,
|
|
1648
1043
|
);
|
|
1044
|
+
if (!ctx.credential) {
|
|
1045
|
+
return jsonResult({
|
|
1046
|
+
connector: connector.id,
|
|
1047
|
+
recovery: "unavailable",
|
|
1048
|
+
message:
|
|
1049
|
+
"Credential storage is not configured. Configure " +
|
|
1050
|
+
"credentials.encryptionKey, redeploy, then call " +
|
|
1051
|
+
"authorize_connector again.",
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
const fields = connector.credential.fields?.map((field) => ({
|
|
1055
|
+
name: field.name,
|
|
1056
|
+
guidance: field.description ?? field.label,
|
|
1057
|
+
})) ?? [
|
|
1058
|
+
{
|
|
1059
|
+
name: "value",
|
|
1060
|
+
guidance:
|
|
1061
|
+
connector.credential.description ??
|
|
1062
|
+
connector.credential.label,
|
|
1063
|
+
},
|
|
1064
|
+
];
|
|
1065
|
+
return jsonResult({
|
|
1066
|
+
connector: connector.id,
|
|
1067
|
+
recovery: "operator_config",
|
|
1068
|
+
credential: {
|
|
1069
|
+
label: connector.credential.label,
|
|
1070
|
+
fields,
|
|
1071
|
+
},
|
|
1072
|
+
operatorUrl: new URL("/credentials", baseUrl).toString(),
|
|
1073
|
+
instructions:
|
|
1074
|
+
"Have the operator open operatorUrl, set and test the credential, " +
|
|
1075
|
+
"then retry the original call. No redeploy is needed. Credential " +
|
|
1076
|
+
"mutation requires a Clerk-authenticated operator.",
|
|
1077
|
+
});
|
|
1649
1078
|
}
|
|
1650
1079
|
const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
|
|
1651
1080
|
try {
|
|
1652
|
-
const status = await connector.startAuth(
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
await registry.recordCredentialHealth(connector.id, {
|
|
1657
|
-
state: status.state,
|
|
1658
|
-
checkedAt: new Date().toISOString(),
|
|
1659
|
-
...(status.message ? { message: status.message } : {}),
|
|
1660
|
-
...(status.authorizationUrl
|
|
1661
|
-
? { authorizationUrl: status.authorizationUrl }
|
|
1662
|
-
: {}),
|
|
1663
|
-
});
|
|
1081
|
+
const status = await connector.startAuth(
|
|
1082
|
+
ctx,
|
|
1083
|
+
args.force !== undefined ? { force: args.force } : {},
|
|
1084
|
+
);
|
|
1664
1085
|
if (status.state === "auth_required" && !status.authorizationUrl) {
|
|
1665
1086
|
// auth_required with nothing to open is a dead end for the operator.
|
|
1666
1087
|
return errorResult(
|
|
@@ -1669,6 +1090,7 @@ export function createMetaTools(
|
|
|
1669
1090
|
}
|
|
1670
1091
|
return jsonResult({
|
|
1671
1092
|
connector: connector.id,
|
|
1093
|
+
recovery: "oauth",
|
|
1672
1094
|
status: status.state,
|
|
1673
1095
|
...(status.authorizationUrl
|
|
1674
1096
|
? {
|
|
@@ -1692,7 +1114,7 @@ export function createMetaTools(
|
|
|
1692
1114
|
|
|
1693
1115
|
const LIST_DESC =
|
|
1694
1116
|
"List connectors with status, cached tool count, and recent real-call health. Use probe=false for a fast inventory; use probe=true (default) only to diagnose live health or authorization.";
|
|
1695
|
-
const SEARCH_DESC = `Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all.
|
|
1117
|
+
const SEARCH_DESC = `Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all. The default page has ${DEFAULT_SEARCH_LIMIT} tools; explicit limit can request up to ${MAX_SEARCH_LIMIT}. includeSchemas="compact" usually removes the describe_tools round trip.`;
|
|
1696
1118
|
const DESCRIBE_DESC = `Inspect up to ${MAX_DESCRIBE_ADDRESSES} known tool addresses when search_tools did not include a sufficient schema. Returns descriptions, input/output schemas, and behavior annotations; format "compact" is the default.`;
|
|
1697
1119
|
const CALL_DESC =
|
|
1698
1120
|
'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths, resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
|
|
@@ -1703,7 +1125,7 @@ const GET_RESULT_DESC =
|
|
|
1703
1125
|
const BATCH_DESC =
|
|
1704
1126
|
"Use for 2–10 independent tools explicitly annotated readOnlyHint: true. Calls run in parallel with shared request-scoped clients; use execute_code when available instead for dependencies or in-sandbox reduction. Unannotated, write-capable, and destructive tools are refused. Batch timeout, safe retry, result mode, and diagnostics defaults may be overridden per call. An oversized final envelope returns ordered outcome summaries plus a get_result page handle.";
|
|
1705
1127
|
const AUTHORIZE_DESC =
|
|
1706
|
-
"Use after
|
|
1128
|
+
"Use after auth_required. Returns an OAuth or operator-credential handoff, or reports required deployment configuration. force=true restarts OAuth only; this tool never accepts credentials.";
|
|
1707
1129
|
const SKILLS_DESC =
|
|
1708
1130
|
'List or fetch concise guidance for choosing among Connecta meta-tools. Call skills({ name: "usage" }) once when the routing workflow is unfamiliar; do not refetch it in the same task.';
|
|
1709
1131
|
|
|
@@ -1713,10 +1135,7 @@ const SKILLS_DESC =
|
|
|
1713
1135
|
* so a deployment with no guides gets every base description unchanged rather
|
|
1714
1136
|
* than paying for text about a feature it does not use.
|
|
1715
1137
|
*
|
|
1716
|
-
* Registration is per connection and reads the
|
|
1717
|
-
* so under a toolkit these sentences reflect the SCOPED connector set: a scoped
|
|
1718
|
-
* session whose connectors carry no guides sees the base descriptions, and
|
|
1719
|
-
* never learns from a tool description that guides exist out of scope.
|
|
1138
|
+
* Registration is per connection and reads the configured connector set.
|
|
1720
1139
|
*/
|
|
1721
1140
|
const GUIDE_NOTES = {
|
|
1722
1141
|
skills:
|
|
@@ -1758,6 +1177,16 @@ const READ_ONLY_LOCAL = {
|
|
|
1758
1177
|
openWorldHint: false,
|
|
1759
1178
|
} as const;
|
|
1760
1179
|
|
|
1180
|
+
const CALL_INPUT_SCHEMA = {
|
|
1181
|
+
address: z.string(),
|
|
1182
|
+
args: z.record(z.string(), z.unknown()).optional(),
|
|
1183
|
+
fields: z.array(z.string()).optional(),
|
|
1184
|
+
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1185
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
1186
|
+
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1187
|
+
diagnostics: z.boolean().optional(),
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1761
1190
|
/** Register the nine meta-tools onto an McpServer instance. */
|
|
1762
1191
|
export function registerMetaTools(
|
|
1763
1192
|
server: McpServer,
|
|
@@ -1766,17 +1195,27 @@ export function registerMetaTools(
|
|
|
1766
1195
|
baseUrl: string;
|
|
1767
1196
|
defaultToolTimeoutMs?: number;
|
|
1768
1197
|
probeTimeoutMs?: number;
|
|
1198
|
+
discoveryConcurrency?: number;
|
|
1769
1199
|
activity?: ActivityRequestContext;
|
|
1770
1200
|
requestSignal?: AbortSignal;
|
|
1771
1201
|
defer?: DeferredWork;
|
|
1772
1202
|
},
|
|
1773
1203
|
): void {
|
|
1774
1204
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1205
|
+
...(ctx.defaultToolTimeoutMs !== undefined
|
|
1206
|
+
? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
|
|
1207
|
+
: {}),
|
|
1208
|
+
...(ctx.probeTimeoutMs !== undefined
|
|
1209
|
+
? { probeTimeoutMs: ctx.probeTimeoutMs }
|
|
1210
|
+
: {}),
|
|
1211
|
+
...(ctx.discoveryConcurrency !== undefined
|
|
1212
|
+
? { discoveryConcurrency: ctx.discoveryConcurrency }
|
|
1213
|
+
: {}),
|
|
1214
|
+
...(ctx.activity !== undefined ? { activity: ctx.activity } : {}),
|
|
1215
|
+
...(ctx.requestSignal !== undefined
|
|
1216
|
+
? { requestSignal: ctx.requestSignal }
|
|
1217
|
+
: {}),
|
|
1218
|
+
...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
|
|
1780
1219
|
});
|
|
1781
1220
|
|
|
1782
1221
|
server.registerTool(
|
|
@@ -1784,12 +1223,7 @@ export function registerMetaTools(
|
|
|
1784
1223
|
{
|
|
1785
1224
|
description: describedFor(registry, SKILLS_DESC, "skills"),
|
|
1786
1225
|
inputSchema: { name: z.string().optional() },
|
|
1787
|
-
annotations:
|
|
1788
|
-
readOnlyHint: true,
|
|
1789
|
-
destructiveHint: false,
|
|
1790
|
-
idempotentHint: true,
|
|
1791
|
-
openWorldHint: false,
|
|
1792
|
-
},
|
|
1226
|
+
annotations: READ_ONLY_LOCAL,
|
|
1793
1227
|
},
|
|
1794
1228
|
async (args) => mt.skills(args as SkillArgs),
|
|
1795
1229
|
);
|
|
@@ -1839,15 +1273,7 @@ export function registerMetaTools(
|
|
|
1839
1273
|
"call_tool",
|
|
1840
1274
|
{
|
|
1841
1275
|
description: CALL_DESC,
|
|
1842
|
-
inputSchema:
|
|
1843
|
-
address: z.string(),
|
|
1844
|
-
args: z.record(z.string(), z.unknown()).optional(),
|
|
1845
|
-
fields: z.array(z.string()).optional(),
|
|
1846
|
-
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1847
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
1848
|
-
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1849
|
-
diagnostics: z.boolean().optional(),
|
|
1850
|
-
},
|
|
1276
|
+
inputSchema: CALL_INPUT_SCHEMA,
|
|
1851
1277
|
// call_tool admits only tools that are themselves explicitly read-only;
|
|
1852
1278
|
// anything else is refused and routed to call_destructive_tool.
|
|
1853
1279
|
annotations: READ_ONLY_REMOTE,
|
|
@@ -1859,15 +1285,7 @@ export function registerMetaTools(
|
|
|
1859
1285
|
"call_destructive_tool",
|
|
1860
1286
|
{
|
|
1861
1287
|
description: CALL_DESTRUCTIVE_DESC,
|
|
1862
|
-
inputSchema:
|
|
1863
|
-
address: z.string(),
|
|
1864
|
-
args: z.record(z.string(), z.unknown()).optional(),
|
|
1865
|
-
fields: z.array(z.string()).optional(),
|
|
1866
|
-
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1867
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
1868
|
-
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1869
|
-
diagnostics: z.boolean().optional(),
|
|
1870
|
-
},
|
|
1288
|
+
inputSchema: CALL_INPUT_SCHEMA,
|
|
1871
1289
|
annotations: {
|
|
1872
1290
|
destructiveHint: true,
|
|
1873
1291
|
readOnlyHint: false,
|
|
@@ -1920,17 +1338,7 @@ export function registerMetaTools(
|
|
|
1920
1338
|
description: BATCH_DESC,
|
|
1921
1339
|
inputSchema: {
|
|
1922
1340
|
calls: z
|
|
1923
|
-
.array(
|
|
1924
|
-
z.object({
|
|
1925
|
-
address: z.string(),
|
|
1926
|
-
args: z.record(z.string(), z.unknown()).optional(),
|
|
1927
|
-
fields: z.array(z.string()).optional(),
|
|
1928
|
-
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1929
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
1930
|
-
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1931
|
-
diagnostics: z.boolean().optional(),
|
|
1932
|
-
}),
|
|
1933
|
-
)
|
|
1341
|
+
.array(z.object(CALL_INPUT_SCHEMA))
|
|
1934
1342
|
.min(1)
|
|
1935
1343
|
.max(10),
|
|
1936
1344
|
resultMode: z.enum(["mcp", "value"]).optional(),
|