@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/dist/meta-tools.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError, groupedSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
|
|
3
|
+
import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
|
|
4
4
|
import { closeConnectorScope, } from "./connector-scope.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { isCallAdmissionError } from "./call-admission.js";
|
|
5
|
+
import { classifyCallError, messageLooksRetryable, } from "./errors.js";
|
|
6
|
+
import { InvocationService, MAX_RETRY_BACKOFF_MS, retryBackoffMs, } from "./invocation.js";
|
|
8
7
|
import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
|
|
9
|
-
import {
|
|
8
|
+
import { hasConnectorGuides, listSkills, resolveSkill, } from "./skills.js";
|
|
10
9
|
import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, withAbortableTimeout, } from "./timeout.js";
|
|
11
|
-
|
|
10
|
+
export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_RETRY_BACKOFF_MS, MAX_SEARCH_LIMIT, retryBackoffMs, };
|
|
12
11
|
const RESULT_TTL_SECONDS = 900;
|
|
12
|
+
const enc = new TextEncoder();
|
|
13
|
+
const dec = new TextDecoder();
|
|
13
14
|
export function jsonResult(obj) {
|
|
14
15
|
return {
|
|
15
|
-
content: [{ type: "text", text: JSON.stringify(obj
|
|
16
|
+
content: [{ type: "text", text: JSON.stringify(obj) }],
|
|
16
17
|
...(obj !== null && typeof obj === "object" && !Array.isArray(obj)
|
|
17
18
|
? { structuredContent: obj }
|
|
18
19
|
: {}),
|
|
@@ -24,68 +25,8 @@ export function errorResult(message) {
|
|
|
24
25
|
function msg(err) {
|
|
25
26
|
return err instanceof Error ? err.message : String(err);
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* A discovery page is for choosing the next tool, not exporting the catalog.
|
|
30
|
-
* One hundred leaves room for broad browsing while keeping each deliberate
|
|
31
|
-
* page far below the catalog sizes Connecta supports.
|
|
32
|
-
*/
|
|
33
|
-
export const MAX_SEARCH_LIMIT = 100;
|
|
34
|
-
/** Same one-request work bound for address-based discovery. */
|
|
35
|
-
export const MAX_DESCRIBE_ADDRESSES = 100;
|
|
36
|
-
/**
|
|
37
|
-
* Final UTF-8 ceiling for a generated search/describe response. The count
|
|
38
|
-
* limits are the ordinary guard; this catches unusually large full schemas or
|
|
39
|
-
* descriptions that make even a bounded page expensive.
|
|
40
|
-
*/
|
|
41
|
-
export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
|
|
42
|
-
const enc = new TextEncoder();
|
|
43
|
-
const dec = new TextDecoder();
|
|
44
|
-
export class DiscoveryPolicyError extends Error {
|
|
45
|
-
code;
|
|
46
|
-
constructor(code, message) {
|
|
47
|
-
super(message);
|
|
48
|
-
this.code = code;
|
|
49
|
-
this.name = "DiscoveryPolicyError";
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
/** Validate before ranking so a huge page request does no proportional work. */
|
|
53
|
-
export function discoverySearchLimit(value) {
|
|
54
|
-
if (value === undefined)
|
|
55
|
-
return DEFAULT_SEARCH_LIMIT;
|
|
56
|
-
if (typeof value !== "number" ||
|
|
57
|
-
!Number.isInteger(value) ||
|
|
58
|
-
value < 1 ||
|
|
59
|
-
value > MAX_SEARCH_LIMIT) {
|
|
60
|
-
throw new DiscoveryPolicyError("invalid_args", `limit must be a whole number from 1 through ${MAX_SEARCH_LIMIT}. Page through larger catalogs with offset.`);
|
|
61
|
-
}
|
|
62
|
-
return value;
|
|
63
|
-
}
|
|
64
|
-
/** Validate the raw list so duplicate addresses consume the same bound. */
|
|
65
|
-
export function discoveryAddresses(value) {
|
|
66
|
-
if (!Array.isArray(value)) {
|
|
67
|
-
throw new DiscoveryPolicyError("invalid_args", "addresses must be an array.");
|
|
68
|
-
}
|
|
69
|
-
if (value.length > MAX_DESCRIBE_ADDRESSES) {
|
|
70
|
-
throw new DiscoveryPolicyError("invalid_args", `addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across describe_tools calls.`);
|
|
71
|
-
}
|
|
72
|
-
return value;
|
|
73
|
-
}
|
|
74
|
-
/** Serialize once and count the exact bytes jsonResult would emit. */
|
|
75
|
-
function boundedDiscoveryText(value, hint) {
|
|
76
|
-
const text = JSON.stringify(value, null, 2);
|
|
77
|
-
if (text === undefined) {
|
|
78
|
-
throw new TypeError("Discovery result is not JSON-serializable.");
|
|
79
|
-
}
|
|
80
|
-
const bytes = enc.encode(text).length;
|
|
81
|
-
if (bytes > MAX_DISCOVERY_RESULT_BYTES) {
|
|
82
|
-
throw new DiscoveryPolicyError("result_too_large", `Discovery result is ${bytes} UTF-8 bytes, over the ${MAX_DISCOVERY_RESULT_BYTES}-byte ceiling. ${hint}`);
|
|
83
|
-
}
|
|
84
|
-
return text;
|
|
85
|
-
}
|
|
86
|
-
/** Apply the same final result guard to code-mode discovery helpers. */
|
|
87
|
-
export function assertDiscoveryResultSize(value, hint) {
|
|
88
|
-
boundedDiscoveryText(value, hint);
|
|
28
|
+
function errorDetails(code, message) {
|
|
29
|
+
return { code, message, retryable: messageLooksRetryable(message) };
|
|
89
30
|
}
|
|
90
31
|
function discoveryErrorResult(error) {
|
|
91
32
|
const result = jsonResult({
|
|
@@ -98,8 +39,9 @@ function discoveryErrorResult(error) {
|
|
|
98
39
|
result.isError = true;
|
|
99
40
|
return result;
|
|
100
41
|
}
|
|
101
|
-
function discoveryResult(
|
|
42
|
+
async function discoveryResult(operation, hint) {
|
|
102
43
|
try {
|
|
44
|
+
const value = await operation();
|
|
103
45
|
const text = boundedDiscoveryText(value, hint);
|
|
104
46
|
return {
|
|
105
47
|
content: [{ type: "text", text }],
|
|
@@ -115,57 +57,12 @@ function discoveryResult(value, hint) {
|
|
|
115
57
|
throw err;
|
|
116
58
|
}
|
|
117
59
|
}
|
|
118
|
-
/**
|
|
119
|
-
* The longest the engine will park a synchronous inbound request in *waiting
|
|
120
|
-
* alone*. The engine already treats ~15 s as the outer bound of one reasonable
|
|
121
|
-
* connector call (EXECUTE_HOST_CALL_TIMEOUT_MS), so sleeping for minutes trades
|
|
122
|
-
* a fast, informative failure for a hung one. A connector-reported window this
|
|
123
|
-
* long isn't truncated — it's declined (see `retryBackoffMs`) and reported
|
|
124
|
-
* verbatim as `error.retryAfterMs`, so the agent, which can afford to wait,
|
|
125
|
-
* decides when to re-issue.
|
|
126
|
-
*/
|
|
127
|
-
export const MAX_RETRY_BACKOFF_MS = 10_000;
|
|
128
|
-
/**
|
|
129
|
-
* How long to wait before the next attempt, or `undefined` for "don't retry".
|
|
130
|
-
*
|
|
131
|
-
* A connector that read a `Retry-After` header knows the window exactly, so it
|
|
132
|
-
* is honoured **exactly or not at all**: truncating an exponential *guess* is
|
|
133
|
-
* harmless, but truncating a *known* window means deliberately retrying inside
|
|
134
|
-
* a rate limit — the harm this channel exists to prevent. A window longer than
|
|
135
|
-
* `MAX_RETRY_BACKOFF_MS` therefore declines the retry rather than shortening
|
|
136
|
-
* it. (`retryAfterMs` is normalized non-negative, so `0` means "retry now".)
|
|
137
|
-
* Connectors that report no window keep the historical exponential guess.
|
|
138
|
-
*
|
|
139
|
-
* Waits are per attempt, matching the per-attempt `timeoutMs` race in
|
|
140
|
-
* `runCall`. Exported for direct testing.
|
|
141
|
-
*/
|
|
142
|
-
export function retryBackoffMs(attempt, retryAfterMs) {
|
|
143
|
-
if (retryAfterMs === undefined) {
|
|
144
|
-
return Math.min(250 * 2 ** (attempt - 1), 1_000);
|
|
145
|
-
}
|
|
146
|
-
return retryAfterMs <= MAX_RETRY_BACKOFF_MS ? retryAfterMs : undefined;
|
|
147
|
-
}
|
|
148
|
-
/** Details for failures that never reached a connector (no thrown value). */
|
|
149
|
-
function errorDetails(code, message) {
|
|
150
|
-
return { code, message, retryable: messageLooksRetryable(message) };
|
|
151
|
-
}
|
|
152
|
-
function callerCancelledDetails() {
|
|
153
|
-
return {
|
|
154
|
-
code: "cancelled",
|
|
155
|
-
message: "Tool call was cancelled by the caller.",
|
|
156
|
-
retryable: false,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
function isCallerCancellation(error, signal) {
|
|
160
|
-
return (signal?.aborted === true ||
|
|
161
|
-
(isCallAdmissionError(error) && error.admissionKind === "cancelled"));
|
|
162
|
-
}
|
|
163
60
|
/** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
|
|
164
61
|
function isContinuationByte(b) {
|
|
165
|
-
return (b & 0xc0) === 0x80;
|
|
62
|
+
return b !== undefined && (b & 0xc0) === 0x80;
|
|
166
63
|
}
|
|
167
64
|
/** Smallest accepted `get_result` byte offset. */
|
|
168
|
-
|
|
65
|
+
const MIN_RESULT_OFFSET = 0;
|
|
169
66
|
/**
|
|
170
67
|
* The one definition of a usable `get_result` offset: a whole number of bytes
|
|
171
68
|
* at or past {@link MIN_RESULT_OFFSET}. Shared by the registered zod schema and
|
|
@@ -180,7 +77,7 @@ export const MIN_RESULT_OFFSET = 0;
|
|
|
180
77
|
* error. An offset past the end of the payload stays legal: it is a whole
|
|
181
78
|
* number of bytes, and it answers with an empty final page.
|
|
182
79
|
*/
|
|
183
|
-
|
|
80
|
+
function isValidResultOffset(value) {
|
|
184
81
|
return Number.isInteger(value) && value >= MIN_RESULT_OFFSET;
|
|
185
82
|
}
|
|
186
83
|
/**
|
|
@@ -236,9 +133,10 @@ export function alignEndToCharBoundary(bytes, offset, end, total) {
|
|
|
236
133
|
// --- fields selection (feature 2) -----------------------------------------
|
|
237
134
|
/** Resolve a dot-path (segments) against a value; `key[]` maps the tail over an array. */
|
|
238
135
|
function resolvePath(value, segments) {
|
|
239
|
-
|
|
136
|
+
const seg = segments[0];
|
|
137
|
+
if (seg === undefined)
|
|
240
138
|
return value;
|
|
241
|
-
const
|
|
139
|
+
const rest = segments.slice(1);
|
|
242
140
|
const isArr = seg.endsWith("[]");
|
|
243
141
|
const key = isArr ? seg.slice(0, -2) : seg;
|
|
244
142
|
let next = value;
|
|
@@ -276,7 +174,7 @@ function applyFieldsToContent(content, fields) {
|
|
|
276
174
|
catch {
|
|
277
175
|
return b;
|
|
278
176
|
}
|
|
279
|
-
return { ...b, text: JSON.stringify(applyFields(parsed, fields)
|
|
177
|
+
return { ...b, text: JSON.stringify(applyFields(parsed, fields)) };
|
|
280
178
|
});
|
|
281
179
|
}
|
|
282
180
|
// --- result-size guard + get_result (feature 1) ---------------------------
|
|
@@ -295,8 +193,8 @@ function applyFieldsToContent(content, fields) {
|
|
|
295
193
|
* the three give one answer to the same question. A value JSON cannot serialize
|
|
296
194
|
* at all (a BigInt) still throws, as before, and is reported as a failure.
|
|
297
195
|
*/
|
|
298
|
-
|
|
299
|
-
const serialized = JSON.stringify(value
|
|
196
|
+
function serializeResultText(value) {
|
|
197
|
+
const serialized = JSON.stringify(value);
|
|
300
198
|
return serialized === undefined ? String(value) : serialized;
|
|
301
199
|
}
|
|
302
200
|
/**
|
|
@@ -377,7 +275,7 @@ async function guardValue(value, results, cap) {
|
|
|
377
275
|
async function guardContent(content, results, cap) {
|
|
378
276
|
let text;
|
|
379
277
|
try {
|
|
380
|
-
text = JSON.stringify(content
|
|
278
|
+
text = JSON.stringify(content);
|
|
381
279
|
}
|
|
382
280
|
catch {
|
|
383
281
|
// A block carrying a BigInt or a cycle cannot be serialized, so it cannot
|
|
@@ -414,309 +312,106 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
414
312
|
const batchCap = registry.maxBatchResultBytes;
|
|
415
313
|
const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
|
|
416
314
|
const probeTimeoutMs = normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
315
|
+
const discoveryConcurrency = resolveDiscoveryConcurrency(opts.discoveryConcurrency);
|
|
417
316
|
// createMetaTools() is called once per inbound MCP request. Sharing this
|
|
418
317
|
// identity lets remote connectors reuse one downstream client inside that
|
|
419
318
|
// request without leaking request-bound I/O into the next one.
|
|
420
319
|
const requestScope = {};
|
|
320
|
+
const catalog = new CatalogService(registry, baseUrl, {
|
|
321
|
+
requestScope,
|
|
322
|
+
probeTimeoutMs,
|
|
323
|
+
concurrency: discoveryConcurrency,
|
|
324
|
+
});
|
|
325
|
+
const invocation = new InvocationService(registry, catalog, opts.activity);
|
|
421
326
|
const withProbeDeadline = (label, operation) => withAbortableTimeout((signal) => operation({ signal, timeoutMs: probeTimeoutMs }), probeTimeoutMs, label);
|
|
422
|
-
/**
|
|
327
|
+
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
423
328
|
async function runCall(call, source, options = {}) {
|
|
424
|
-
const started = Date.now();
|
|
425
|
-
let catalogMs = 0;
|
|
426
|
-
let admissionMs = 0;
|
|
427
|
-
let connectorMs = 0;
|
|
428
|
-
let backoffMs = 0;
|
|
429
|
-
let resultProcessingMs = 0;
|
|
430
|
-
let attempts = 0;
|
|
431
|
-
const timing = () => ({
|
|
432
|
-
catalogMs,
|
|
433
|
-
admissionMs,
|
|
434
|
-
connectorMs,
|
|
435
|
-
backoffMs,
|
|
436
|
-
resultProcessingMs,
|
|
437
|
-
totalMs: Date.now() - started,
|
|
438
|
-
});
|
|
439
|
-
const resolved = registry.resolveAddress(call.address);
|
|
440
|
-
const record = (outcome, errorCode) => {
|
|
441
|
-
if (!resolved)
|
|
442
|
-
return;
|
|
443
|
-
recordToolActivity(opts.activity, {
|
|
444
|
-
connectorId: resolved.connector.id,
|
|
445
|
-
toolName: resolved.toolName,
|
|
446
|
-
address: `${resolved.connector.id}.${resolved.toolName}`,
|
|
447
|
-
source,
|
|
448
|
-
outcome,
|
|
449
|
-
durationMs: Date.now() - started,
|
|
450
|
-
attempts,
|
|
451
|
-
...(errorCode ? { errorCode } : {}),
|
|
452
|
-
});
|
|
453
|
-
};
|
|
454
|
-
const failed = (error) => {
|
|
455
|
-
const durationMs = Date.now() - started;
|
|
456
|
-
const diagnostics = timing();
|
|
457
|
-
record(error.code === "timeout"
|
|
458
|
-
? "timeout"
|
|
459
|
-
: error.code === "cancelled"
|
|
460
|
-
? "cancelled"
|
|
461
|
-
: "error", error.code);
|
|
462
|
-
return {
|
|
463
|
-
toolResult: call.resultMode === "value"
|
|
464
|
-
? jsonResult({
|
|
465
|
-
ok: false,
|
|
466
|
-
error,
|
|
467
|
-
durationMs,
|
|
468
|
-
attempts,
|
|
469
|
-
...(call.diagnostics ? { timing: diagnostics } : {}),
|
|
470
|
-
})
|
|
471
|
-
: errorResult(error.message),
|
|
472
|
-
durationMs,
|
|
473
|
-
attempts,
|
|
474
|
-
timing: diagnostics,
|
|
475
|
-
error,
|
|
476
|
-
};
|
|
477
|
-
};
|
|
478
|
-
if (!resolved) {
|
|
479
|
-
return failed(errorDetails("unknown_address", `Unknown address "${call.address}"`));
|
|
480
|
-
}
|
|
481
329
|
const results = registry.resultsStorage();
|
|
482
|
-
// Result-size cap for THIS call: the connector's own override wins, then
|
|
483
|
-
// the deployment-wide value, then the built-in default (already folded
|
|
484
|
-
// into `globalCap`). Resolved per call so one batch_call can mix a
|
|
485
|
-
// tight-capped connector with siblings on the global cap. An override the
|
|
486
|
-
// registry already warned about at startup is dropped here, so the
|
|
487
|
-
// connector simply inherits `globalCap`.
|
|
488
|
-
const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
|
|
489
330
|
const fields = call.fields && call.fields.length > 0 ? call.fields : null;
|
|
490
|
-
// An explicit per-call deadline always wins; the config default only fills
|
|
491
|
-
// the gap, and stays off entirely when the deployment sets none.
|
|
492
331
|
const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
catalogMs += Date.now() - catalogStarted;
|
|
523
|
-
if (!definition) {
|
|
524
|
-
return failed(errorDetails("unknown_tool", `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`));
|
|
525
|
-
}
|
|
526
|
-
const explicitlyReadOnly = definition.annotations?.readOnlyHint === true &&
|
|
527
|
-
definition.annotations?.destructiveHint !== true;
|
|
528
|
-
if (!explicitlyReadOnly && !options.allowDestructive) {
|
|
529
|
-
return failed(errorDetails("destructive_tool_requires_approval", `Tool "${call.address}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`));
|
|
530
|
-
}
|
|
531
|
-
const retrySafe = definition.annotations?.readOnlyHint === true ||
|
|
532
|
-
definition.annotations?.idempotentHint === true;
|
|
533
|
-
let result;
|
|
534
|
-
while (true) {
|
|
535
|
-
attempts++;
|
|
536
|
-
let permit;
|
|
537
|
-
const controller = timeoutMs || opts.requestSignal ? new AbortController() : undefined;
|
|
538
|
-
const forwardAbort = () => controller?.abort(opts.requestSignal?.reason);
|
|
539
|
-
if (opts.requestSignal?.aborted)
|
|
540
|
-
forwardAbort();
|
|
541
|
-
else {
|
|
542
|
-
opts.requestSignal?.addEventListener("abort", forwardAbort, {
|
|
543
|
-
once: true,
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
let timer;
|
|
547
|
-
let onAbort;
|
|
548
|
-
let attemptFailed = false;
|
|
549
|
-
let attemptError;
|
|
550
|
-
try {
|
|
551
|
-
const admissionStarted = Date.now();
|
|
552
|
-
try {
|
|
553
|
-
permit = await registry.admitCall(resolved.connector.id, {
|
|
554
|
-
toolName: resolved.toolName,
|
|
555
|
-
args: call.args ?? {},
|
|
556
|
-
signal: opts.requestSignal,
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
finally {
|
|
560
|
-
admissionMs += Date.now() - admissionStarted;
|
|
561
|
-
}
|
|
562
|
-
const ctx = registry.contextFor(resolved.connector.id, baseUrl, requestScope, { signal: controller?.signal, timeoutMs });
|
|
563
|
-
let rejectCancelled;
|
|
564
|
-
const cancelled = controller
|
|
565
|
-
? new Promise((_, reject) => {
|
|
566
|
-
rejectCancelled = reject;
|
|
567
|
-
})
|
|
568
|
-
: undefined;
|
|
569
|
-
onAbort = () => {
|
|
570
|
-
rejectCancelled(controller?.signal.reason ??
|
|
571
|
-
new ConnectorCallError("timeout", "Tool call was cancelled"));
|
|
572
|
-
};
|
|
573
|
-
controller?.signal.addEventListener("abort", onAbort, { once: true });
|
|
574
|
-
if (controller?.signal.aborted)
|
|
575
|
-
onAbort();
|
|
576
|
-
if (controller?.signal.aborted)
|
|
577
|
-
await cancelled;
|
|
578
|
-
if (timeoutMs) {
|
|
579
|
-
timer = setTimeout(() => {
|
|
580
|
-
controller?.abort(new ConnectorCallError("timeout", `Tool call timed out after ${timeoutMs}ms`));
|
|
581
|
-
}, timeoutMs);
|
|
582
|
-
}
|
|
583
|
-
const connectorStarted = Date.now();
|
|
584
|
-
try {
|
|
585
|
-
const pending = resolved.connector.callTool(resolved.toolName, call.args ?? {}, ctx);
|
|
586
|
-
result = cancelled
|
|
587
|
-
? await Promise.race([pending, cancelled])
|
|
588
|
-
: await pending;
|
|
589
|
-
}
|
|
590
|
-
finally {
|
|
591
|
-
connectorMs += Date.now() - connectorStarted;
|
|
592
|
-
}
|
|
593
|
-
const mcpResult = result;
|
|
594
|
-
if (resolved.connector.kind === "mcp" && mcpResult?.isError) {
|
|
595
|
-
throw new Error(mcpResult.content?.map((block) => block.text).join("") ||
|
|
596
|
-
"Downstream tool call failed");
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
catch (err) {
|
|
600
|
-
attemptFailed = true;
|
|
601
|
-
attemptError = err;
|
|
602
|
-
}
|
|
603
|
-
finally {
|
|
604
|
-
if (timer)
|
|
605
|
-
clearTimeout(timer);
|
|
606
|
-
if (onAbort) {
|
|
607
|
-
controller?.signal.removeEventListener("abort", onAbort);
|
|
608
|
-
}
|
|
609
|
-
opts.requestSignal?.removeEventListener("abort", forwardAbort);
|
|
610
|
-
permit?.release();
|
|
611
|
-
}
|
|
612
|
-
if (attemptFailed) {
|
|
613
|
-
const callerCancelled = isCallerCancellation(attemptError, opts.requestSignal);
|
|
614
|
-
const details = callerCancelled
|
|
615
|
-
? callerCancelledDetails()
|
|
616
|
-
: classifyCallError(attemptError);
|
|
617
|
-
if (!callerCancelled &&
|
|
618
|
-
attempts <= maxRetries &&
|
|
619
|
-
retrySafe &&
|
|
620
|
-
details.retryable) {
|
|
621
|
-
const wait = retryBackoffMs(attempts, details.retryAfterMs);
|
|
622
|
-
if (wait !== undefined) {
|
|
623
|
-
const backoffStarted = Date.now();
|
|
624
|
-
if (wait > 0) {
|
|
625
|
-
const completed = await new Promise((resolve) => {
|
|
626
|
-
let settled = false;
|
|
627
|
-
const finish = (value) => {
|
|
628
|
-
if (settled)
|
|
629
|
-
return;
|
|
630
|
-
settled = true;
|
|
631
|
-
clearTimeout(timer);
|
|
632
|
-
opts.requestSignal?.removeEventListener("abort", cancel);
|
|
633
|
-
resolve(value);
|
|
634
|
-
};
|
|
635
|
-
const timer = setTimeout(() => finish(true), wait);
|
|
636
|
-
const cancel = () => finish(false);
|
|
637
|
-
opts.requestSignal?.addEventListener("abort", cancel, {
|
|
638
|
-
once: true,
|
|
639
|
-
});
|
|
640
|
-
if (opts.requestSignal?.aborted)
|
|
641
|
-
cancel();
|
|
642
|
-
});
|
|
643
|
-
backoffMs += Date.now() - backoffStarted;
|
|
644
|
-
if (!completed)
|
|
645
|
-
return failed(callerCancelledDetails());
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
backoffMs += Date.now() - backoffStarted;
|
|
649
|
-
}
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
// The reported window is longer than the engine will park a
|
|
653
|
-
// synchronous request for. Fall through to failure with
|
|
654
|
-
// retryAfterMs reported verbatim so the agent can re-issue.
|
|
332
|
+
const outcome = await invocation.invoke(call.address, call.args ?? {}, {
|
|
333
|
+
source,
|
|
334
|
+
...(options.allowDestructive !== undefined
|
|
335
|
+
? { allowDestructive: options.allowDestructive }
|
|
336
|
+
: {}),
|
|
337
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
338
|
+
...(call.maxRetries !== undefined
|
|
339
|
+
? { maxRetries: call.maxRetries }
|
|
340
|
+
: {}),
|
|
341
|
+
...(opts.requestSignal !== undefined
|
|
342
|
+
? { requestSignal: opts.requestSignal }
|
|
343
|
+
: {}),
|
|
344
|
+
unwrapResult: call.resultMode === "value",
|
|
345
|
+
processResult: async (result, resolved) => {
|
|
346
|
+
// Result-size cap for THIS call: the connector's own override wins,
|
|
347
|
+
// then the deployment-wide value, then the built-in default (already
|
|
348
|
+
// folded into `globalCap`). Resolved per call so one batch_call can
|
|
349
|
+
// mix a tight-capped connector with siblings on the global cap. An
|
|
350
|
+
// override the registry already warned about at startup is dropped
|
|
351
|
+
// here, so the connector simply inherits `globalCap`.
|
|
352
|
+
const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
|
|
353
|
+
if (call.resultMode === "value") {
|
|
354
|
+
let value = fields ? applyFields(result, fields) : result;
|
|
355
|
+
value = await guardValue(value, results, cap);
|
|
356
|
+
return {
|
|
357
|
+
toolResult: jsonResult({ ok: true, data: value }),
|
|
358
|
+
value,
|
|
359
|
+
};
|
|
655
360
|
}
|
|
656
|
-
if (
|
|
657
|
-
|
|
361
|
+
if (resolved.connector.kind === "mcp") {
|
|
362
|
+
const mcpResult = result;
|
|
363
|
+
let content = mcpResult?.content ?? [];
|
|
364
|
+
if (fields)
|
|
365
|
+
content = applyFieldsToContent(content, fields);
|
|
366
|
+
return { toolResult: await guardContent(content, results, cap) };
|
|
658
367
|
}
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
break;
|
|
662
|
-
}
|
|
663
|
-
registry.recordSuccess(resolved.connector.id, Date.now() - started);
|
|
664
|
-
const processingStarted = Date.now();
|
|
665
|
-
try {
|
|
666
|
-
const mr = result;
|
|
667
|
-
if (call.resultMode === "value") {
|
|
668
|
-
let value = unwrapMcpResult(resolved.connector.kind, result);
|
|
669
|
-
if (fields)
|
|
670
|
-
value = applyFields(value, fields);
|
|
671
|
-
value = await guardValue(value, results, cap);
|
|
672
|
-
resultProcessingMs += Date.now() - processingStarted;
|
|
673
|
-
const durationMs = Date.now() - started;
|
|
674
|
-
const diagnostics = timing();
|
|
675
|
-
record("success");
|
|
368
|
+
const value = fields ? applyFields(result, fields) : result;
|
|
676
369
|
return {
|
|
677
|
-
toolResult:
|
|
678
|
-
ok: true,
|
|
679
|
-
data: value,
|
|
680
|
-
durationMs,
|
|
681
|
-
attempts,
|
|
682
|
-
...(call.diagnostics ? { timing: diagnostics } : {}),
|
|
683
|
-
}),
|
|
684
|
-
durationMs,
|
|
685
|
-
attempts,
|
|
686
|
-
timing: diagnostics,
|
|
370
|
+
toolResult: await guardText(serializeResultText(value), results, cap),
|
|
687
371
|
value,
|
|
688
372
|
};
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
if (!outcome.ok) {
|
|
376
|
+
const failedResult = outcome.error.code === "auth_required" ||
|
|
377
|
+
call.resultMode === "value"
|
|
378
|
+
? jsonResult({
|
|
379
|
+
ok: false,
|
|
380
|
+
error: outcome.error,
|
|
381
|
+
durationMs: outcome.durationMs,
|
|
382
|
+
attempts: outcome.attempts,
|
|
383
|
+
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
384
|
+
})
|
|
385
|
+
: errorResult(outcome.error.message);
|
|
386
|
+
if (outcome.error.code === "auth_required") {
|
|
387
|
+
failedResult.isError = true;
|
|
689
388
|
}
|
|
690
|
-
if (resolved.connector.kind === "mcp") {
|
|
691
|
-
let content = mr?.content ?? [];
|
|
692
|
-
if (fields)
|
|
693
|
-
content = applyFieldsToContent(content, fields);
|
|
694
|
-
const toolResult = await guardContent(content, results, cap);
|
|
695
|
-
resultProcessingMs += Date.now() - processingStarted;
|
|
696
|
-
record("success");
|
|
697
|
-
return {
|
|
698
|
-
toolResult,
|
|
699
|
-
durationMs: Date.now() - started,
|
|
700
|
-
attempts,
|
|
701
|
-
timing: timing(),
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
const value = fields ? applyFields(result, fields) : result;
|
|
705
|
-
const toolResult = await guardText(serializeResultText(value), results, cap);
|
|
706
|
-
resultProcessingMs += Date.now() - processingStarted;
|
|
707
|
-
record("success");
|
|
708
389
|
return {
|
|
709
|
-
toolResult,
|
|
710
|
-
durationMs:
|
|
711
|
-
attempts,
|
|
712
|
-
timing: timing
|
|
713
|
-
|
|
390
|
+
toolResult: failedResult,
|
|
391
|
+
durationMs: outcome.durationMs,
|
|
392
|
+
attempts: outcome.attempts,
|
|
393
|
+
timing: outcome.timing,
|
|
394
|
+
error: outcome.error,
|
|
714
395
|
};
|
|
715
396
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
397
|
+
const valueModeResult = call.resultMode === "value"
|
|
398
|
+
? jsonResult({
|
|
399
|
+
ok: true,
|
|
400
|
+
data: outcome.value.value,
|
|
401
|
+
durationMs: outcome.durationMs,
|
|
402
|
+
attempts: outcome.attempts,
|
|
403
|
+
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
404
|
+
})
|
|
405
|
+
: outcome.value.toolResult;
|
|
406
|
+
return {
|
|
407
|
+
toolResult: valueModeResult,
|
|
408
|
+
durationMs: outcome.durationMs,
|
|
409
|
+
attempts: outcome.attempts,
|
|
410
|
+
timing: outcome.timing,
|
|
411
|
+
...(Object.prototype.hasOwnProperty.call(outcome.value, "value")
|
|
412
|
+
? { value: outcome.value.value }
|
|
413
|
+
: {}),
|
|
414
|
+
};
|
|
720
415
|
}
|
|
721
416
|
return {
|
|
722
417
|
async skills(args = {}) {
|
|
@@ -745,12 +440,15 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
745
440
|
// call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
|
|
746
441
|
const connectors = registry.listConnectors();
|
|
747
442
|
const scope = probe ? {} : requestScope;
|
|
748
|
-
const
|
|
443
|
+
const inspect = async (c) => {
|
|
749
444
|
const statusStarted = Date.now();
|
|
750
445
|
const observed = registry.healthFor(c.id);
|
|
751
|
-
const
|
|
446
|
+
const drift = await registry.credentialDriftFor(c.id);
|
|
752
447
|
let status;
|
|
753
|
-
if (
|
|
448
|
+
if (drift) {
|
|
449
|
+
status = { state: "auth_required", message: drift };
|
|
450
|
+
}
|
|
451
|
+
else if (probe) {
|
|
754
452
|
try {
|
|
755
453
|
status = await withProbeDeadline(`list_connectors probe of "${c.id}"`, (options) => registry.statusFor(c.id, baseUrl, scope, options));
|
|
756
454
|
}
|
|
@@ -761,73 +459,20 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
761
459
|
status = { state: "error", message: msg(err) };
|
|
762
460
|
}
|
|
763
461
|
}
|
|
764
|
-
else if (verdict &&
|
|
765
|
-
// Deployment-wide, deliberately, like `hasObservedSuccess` beside
|
|
766
|
-
// it: a sibling toolkit's successful call proves the shared
|
|
767
|
-
// credential works, and a verdict retired for one view but not
|
|
768
|
-
// another would make the same connector read differently per scope
|
|
769
|
-
// for a reason that has nothing to do with scope.
|
|
770
|
-
credentialVerdictApplies(verdict, registry.observedSuccessAt(c.id))) {
|
|
771
|
-
// The proactive layer (issue #24): a liveness check already found
|
|
772
|
-
// the stored credential dead, so say so on the cheap path instead of
|
|
773
|
-
// waiting for an agent's real call to discover it. Only while it is
|
|
774
|
-
// the freshest evidence — a successful call since then retires it.
|
|
775
|
-
status = {
|
|
776
|
-
state: verdict.state,
|
|
777
|
-
...(verdict.message ? { message: verdict.message } : {}),
|
|
778
|
-
...(verdict.authorizationUrl
|
|
779
|
-
? { authorizationUrl: verdict.authorizationUrl }
|
|
780
|
-
: {}),
|
|
781
|
-
};
|
|
782
|
-
}
|
|
783
462
|
else {
|
|
784
|
-
// "error" comes from THIS view's own observations — a sibling
|
|
785
|
-
// toolkit's failure is not this session's experience — while
|
|
786
|
-
// ok/unknown may lean on the deployment-wide success signal, since
|
|
787
|
-
// "the connector answers at all" is a fact about the connector.
|
|
788
|
-
// Unscoped, the two are the same log, so this is unchanged there.
|
|
789
463
|
const derived = observed?.consecutiveFailures && observed.consecutiveFailures > 0
|
|
790
464
|
? "error"
|
|
791
465
|
: registry.hasObservedSuccess(c.id) || c.kind === "api"
|
|
792
466
|
? "ok"
|
|
793
467
|
: "unknown";
|
|
794
468
|
status = {
|
|
795
|
-
|
|
796
|
-
// been called yet, but the credential was verified, which is how
|
|
797
|
-
// re-authorization shows up here as ok rather than as an absence
|
|
798
|
-
// of evidence. It never DOWNgrades an observed failure: a real
|
|
799
|
-
// call that failed is stronger evidence than a background check.
|
|
800
|
-
state: derived === "unknown" && verdict?.state === "ok"
|
|
801
|
-
? "ok"
|
|
802
|
-
: derived,
|
|
469
|
+
state: derived,
|
|
803
470
|
...(observed?.lastError ? { message: observed.lastError } : {}),
|
|
804
471
|
};
|
|
805
472
|
}
|
|
806
|
-
// Stamped
|
|
807
|
-
//
|
|
808
|
-
// report a verdict older than it is, and would lose the race against a
|
|
809
|
-
// real call that succeeded WHILE it ran (that success must retire the
|
|
810
|
-
// verdict, and only an honest timestamp says so).
|
|
473
|
+
// Stamped after any live probe so the response reports when its
|
|
474
|
+
// observation completed, not when a potentially slow request began.
|
|
811
475
|
const checkedAt = new Date().toISOString();
|
|
812
|
-
// A live status probe IS a liveness observation of the stored
|
|
813
|
-
// credential, so it updates the same verdict a background check
|
|
814
|
-
// writes: the cached read afterwards agrees with what the operator
|
|
815
|
-
// just saw, and they are not swept again moments later. Recorded from
|
|
816
|
-
// the STATUS phase only, and only when the connector actually answered
|
|
817
|
-
// — a catalog refresh below is not a credential check (the sweep never
|
|
818
|
-
// fetches one), it is already counted in the health log, and letting
|
|
819
|
-
// its failure land here would spend the freshness budget on it. The
|
|
820
|
-
// registry ignores this for connectors storing no credential of ours.
|
|
821
|
-
if (probe && (status.state === "ok" || status.state === "auth_required")) {
|
|
822
|
-
await registry.recordCredentialHealth(c.id, {
|
|
823
|
-
state: status.state,
|
|
824
|
-
checkedAt,
|
|
825
|
-
...(status.message ? { message: status.message } : {}),
|
|
826
|
-
...("authorizationUrl" in status && status.authorizationUrl
|
|
827
|
-
? { authorizationUrl: status.authorizationUrl }
|
|
828
|
-
: {}),
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
476
|
let tools = registry.peekTools(c.id);
|
|
832
477
|
// An auth_required status may have just started OAuth. A second
|
|
833
478
|
// listTools probe would overwrite its state/verifier while returning
|
|
@@ -855,15 +500,6 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
855
500
|
state: "auth_required",
|
|
856
501
|
message: details.message,
|
|
857
502
|
};
|
|
858
|
-
await registry.recordCredentialHealth(c.id, {
|
|
859
|
-
state: "auth_required",
|
|
860
|
-
checkedAt,
|
|
861
|
-
...(status.message ? { message: status.message } : {}),
|
|
862
|
-
...("authorizationUrl" in status &&
|
|
863
|
-
status.authorizationUrl
|
|
864
|
-
? { authorizationUrl: status.authorizationUrl }
|
|
865
|
-
: {}),
|
|
866
|
-
});
|
|
867
503
|
}
|
|
868
504
|
else {
|
|
869
505
|
status = { state: "error", message: msg(err) };
|
|
@@ -873,9 +509,6 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
873
509
|
}
|
|
874
510
|
const latencyMs = Date.now() - statusStarted;
|
|
875
511
|
const latestObserved = registry.healthFor(c.id);
|
|
876
|
-
const credentialCheck = probe
|
|
877
|
-
? await registry.credentialHealthFor(c.id)
|
|
878
|
-
: verdict;
|
|
879
512
|
return {
|
|
880
513
|
id: c.id,
|
|
881
514
|
...(c.title ? { title: c.title } : {}),
|
|
@@ -885,19 +518,18 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
885
518
|
checkedAt,
|
|
886
519
|
latencyMs,
|
|
887
520
|
probe,
|
|
888
|
-
...(latestObserved ?? observed
|
|
889
|
-
...(
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
...(status.message
|
|
521
|
+
...(latestObserved ?? observed),
|
|
522
|
+
...("authorizationUrl" in status &&
|
|
523
|
+
status.authorizationUrl && {
|
|
524
|
+
authorizationUrl: status.authorizationUrl,
|
|
525
|
+
}),
|
|
526
|
+
...(status.message && { message: status.message }),
|
|
894
527
|
};
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
|
|
528
|
+
};
|
|
529
|
+
const settled = await mapSettledWithConcurrency(connectors, discoveryConcurrency, inspect);
|
|
530
|
+
if (probe) {
|
|
531
|
+
await mapSettledWithConcurrency(connectors, discoveryConcurrency, (connector) => closeConnectorScope(connector, registry.contextFor(connector.id, baseUrl, scope), opts.defer));
|
|
898
532
|
}
|
|
899
|
-
const settled = await Promise.allSettled(pending);
|
|
900
|
-
await Promise.all(connectors.map((connector) => closeConnectorScope(connector, registry.contextFor(connector.id, baseUrl, scope), opts.defer)));
|
|
901
533
|
const out = settled.map((result) => {
|
|
902
534
|
if (result.status === "rejected")
|
|
903
535
|
throw result.reason;
|
|
@@ -906,175 +538,10 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
906
538
|
return jsonResult({ connectors: out });
|
|
907
539
|
},
|
|
908
540
|
async searchTools(args) {
|
|
909
|
-
|
|
910
|
-
let limit;
|
|
911
|
-
try {
|
|
912
|
-
limit = discoverySearchLimit(args.limit);
|
|
913
|
-
}
|
|
914
|
-
catch (err) {
|
|
915
|
-
if (err instanceof DiscoveryPolicyError) {
|
|
916
|
-
return discoveryErrorResult(err);
|
|
917
|
-
}
|
|
918
|
-
throw err;
|
|
919
|
-
}
|
|
920
|
-
const offset = Math.max(0, Math.trunc(args.offset ?? 0));
|
|
921
|
-
const conns = args.connector
|
|
922
|
-
? [registry.getConnector(args.connector)].filter((c) => Boolean(c))
|
|
923
|
-
: registry.listConnectors();
|
|
924
|
-
const matches = [];
|
|
925
|
-
const catalogs = await Promise.allSettled(conns.map((c) => withProbeDeadline(`search_tools probe of "${c.id}"`, (options) => registry.getTools(c.id, baseUrl, requestScope, options))));
|
|
926
|
-
let matchMode = "all";
|
|
927
|
-
const collectMatches = (mode) => {
|
|
928
|
-
matches.length = 0;
|
|
929
|
-
let orderBase = 0;
|
|
930
|
-
catalogs.forEach((catalog, connectorIndex) => {
|
|
931
|
-
const c = conns[connectorIndex];
|
|
932
|
-
if (catalog.status === "fulfilled") {
|
|
933
|
-
for (const ranked of rankTools(catalog.value, q, mode)) {
|
|
934
|
-
matches.push({
|
|
935
|
-
connectorId: c.id,
|
|
936
|
-
connectorTitle: c.title,
|
|
937
|
-
connectorDescription: c.description,
|
|
938
|
-
...(connectorGuide(c)
|
|
939
|
-
? { connectorGuideSkill: connectorSkillName(c.id) }
|
|
940
|
-
: {}),
|
|
941
|
-
tool: ranked.tool,
|
|
942
|
-
score: ranked.score,
|
|
943
|
-
order: orderBase + ranked.order,
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
orderBase +=
|
|
948
|
-
catalog.status === "fulfilled" ? catalog.value.length : 1;
|
|
949
|
-
});
|
|
950
|
-
};
|
|
951
|
-
collectMatches("all");
|
|
952
|
-
if (q.trim() && matches.length === 0) {
|
|
953
|
-
matchMode = "partial";
|
|
954
|
-
collectMatches(matchMode);
|
|
955
|
-
}
|
|
956
|
-
matches.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
957
|
-
const page = matches.slice(offset, offset + limit);
|
|
958
|
-
const groups = [];
|
|
959
|
-
const byConnector = new Map();
|
|
960
|
-
for (const match of page) {
|
|
961
|
-
let group = byConnector.get(match.connectorId);
|
|
962
|
-
if (!group) {
|
|
963
|
-
group = {
|
|
964
|
-
id: match.connectorId,
|
|
965
|
-
...(match.connectorTitle ? { title: match.connectorTitle } : {}),
|
|
966
|
-
description: match.connectorDescription,
|
|
967
|
-
...(match.connectorGuideSkill
|
|
968
|
-
? { guide: match.connectorGuideSkill }
|
|
969
|
-
: {}),
|
|
970
|
-
tools: [],
|
|
971
|
-
};
|
|
972
|
-
byConnector.set(match.connectorId, group);
|
|
973
|
-
groups.push(group);
|
|
974
|
-
}
|
|
975
|
-
const schema = match.tool.inputSchema ?? { type: "object" };
|
|
976
|
-
group.tools.push({
|
|
977
|
-
name: match.tool.name,
|
|
978
|
-
address: `${match.connectorId}.${match.tool.name}`,
|
|
979
|
-
description: summarizeDescription(match.tool.description, args.fullDescriptions === true),
|
|
980
|
-
...(args.includeSchemas
|
|
981
|
-
? {
|
|
982
|
-
inputSchema: args.includeSchemas === "json"
|
|
983
|
-
? schema
|
|
984
|
-
: compactSchema(schema),
|
|
985
|
-
}
|
|
986
|
-
: {}),
|
|
987
|
-
...(args.includeSchemas && match.tool.outputSchema
|
|
988
|
-
? {
|
|
989
|
-
outputSchema: args.includeSchemas === "json"
|
|
990
|
-
? match.tool.outputSchema
|
|
991
|
-
: compactSchema(match.tool.outputSchema),
|
|
992
|
-
}
|
|
993
|
-
: {}),
|
|
994
|
-
...(match.tool.annotations
|
|
995
|
-
? { annotations: match.tool.annotations }
|
|
996
|
-
: {}),
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
const nextOffset = offset + page.length < matches.length
|
|
1000
|
-
? offset + page.length
|
|
1001
|
-
: undefined;
|
|
1002
|
-
return discoveryResult({
|
|
1003
|
-
connectors: groups,
|
|
1004
|
-
total: matches.length,
|
|
1005
|
-
offset,
|
|
1006
|
-
limit,
|
|
1007
|
-
hasMore: nextOffset !== undefined,
|
|
1008
|
-
...(nextOffset !== undefined ? { nextOffset } : {}),
|
|
1009
|
-
...(matchMode === "partial" && matches.length > 0
|
|
1010
|
-
? { matchMode }
|
|
1011
|
-
: {}),
|
|
1012
|
-
}, "Request a smaller limit, omit fullDescriptions, or use compact schemas.");
|
|
541
|
+
return discoveryResult(async () => groupedSearchResult(await catalog.search(args)), "Request a smaller limit, omit fullDescriptions, or use compact schemas.");
|
|
1013
542
|
},
|
|
1014
543
|
async describeTools(args) {
|
|
1015
|
-
|
|
1016
|
-
discoveryAddresses(args.addresses);
|
|
1017
|
-
}
|
|
1018
|
-
catch (err) {
|
|
1019
|
-
if (err instanceof DiscoveryPolicyError) {
|
|
1020
|
-
return discoveryErrorResult(err);
|
|
1021
|
-
}
|
|
1022
|
-
throw err;
|
|
1023
|
-
}
|
|
1024
|
-
const format = args.format ?? "compact";
|
|
1025
|
-
const resolved = args.addresses.map((address) => ({
|
|
1026
|
-
address,
|
|
1027
|
-
resolved: registry.resolveAddress(address),
|
|
1028
|
-
}));
|
|
1029
|
-
const connectorIds = [
|
|
1030
|
-
...new Set(resolved
|
|
1031
|
-
.map((entry) => entry.resolved?.connector.id)
|
|
1032
|
-
.filter((id) => Boolean(id))),
|
|
1033
|
-
];
|
|
1034
|
-
const loaded = await Promise.allSettled(connectorIds.map((id) => withProbeDeadline(`describe_tools probe of "${id}"`, (options) => registry.getTools(id, baseUrl, requestScope, options))));
|
|
1035
|
-
const catalogs = new Map();
|
|
1036
|
-
loaded.forEach((result, index) => {
|
|
1037
|
-
catalogs.set(connectorIds[index], result.status === "fulfilled"
|
|
1038
|
-
? result.value
|
|
1039
|
-
: result.reason instanceof Error
|
|
1040
|
-
? result.reason
|
|
1041
|
-
: new Error(String(result.reason)));
|
|
1042
|
-
});
|
|
1043
|
-
const out = resolved.map(({ address, resolved }) => {
|
|
1044
|
-
if (!resolved) {
|
|
1045
|
-
return { address, error: `Unknown address "${address}"` };
|
|
1046
|
-
}
|
|
1047
|
-
const catalog = catalogs.get(resolved.connector.id);
|
|
1048
|
-
if (catalog instanceof Error) {
|
|
1049
|
-
return { address, error: catalog.message };
|
|
1050
|
-
}
|
|
1051
|
-
const tool = catalog?.find((t) => t.name === resolved.toolName);
|
|
1052
|
-
if (!tool) {
|
|
1053
|
-
return {
|
|
1054
|
-
address,
|
|
1055
|
-
error: `Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
const schema = tool.inputSchema ?? { type: "object" };
|
|
1059
|
-
return {
|
|
1060
|
-
address,
|
|
1061
|
-
name: tool.name,
|
|
1062
|
-
description: summarizeDescription(tool.description, args.fullDescriptions === true),
|
|
1063
|
-
...(connectorGuide(resolved.connector)
|
|
1064
|
-
? { guide: connectorSkillName(resolved.connector.id) }
|
|
1065
|
-
: {}),
|
|
1066
|
-
inputSchema: format === "json" ? schema : compactSchema(schema),
|
|
1067
|
-
...(tool.outputSchema
|
|
1068
|
-
? {
|
|
1069
|
-
outputSchema: format === "json"
|
|
1070
|
-
? tool.outputSchema
|
|
1071
|
-
: compactSchema(tool.outputSchema),
|
|
1072
|
-
}
|
|
1073
|
-
: {}),
|
|
1074
|
-
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
1075
|
-
};
|
|
1076
|
-
});
|
|
1077
|
-
return discoveryResult({ tools: out }, 'Split the address list or use format: "compact".');
|
|
544
|
+
return discoveryResult(async () => ({ tools: await catalog.describe(args) }), 'Split the address list or use format: "compact".');
|
|
1078
545
|
},
|
|
1079
546
|
async callTool(args) {
|
|
1080
547
|
return (await runCall(args, "call_tool")).toolResult;
|
|
@@ -1133,13 +600,25 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
1133
600
|
const batchStarted = Date.now();
|
|
1134
601
|
const settled = await Promise.allSettled(args.calls.map((c) => runCall({
|
|
1135
602
|
...c,
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
603
|
+
...((c.resultMode ?? args.resultMode) !== undefined
|
|
604
|
+
? { resultMode: c.resultMode ?? args.resultMode }
|
|
605
|
+
: {}),
|
|
606
|
+
...((c.timeoutMs ?? args.timeoutMs) !== undefined
|
|
607
|
+
? { timeoutMs: c.timeoutMs ?? args.timeoutMs }
|
|
608
|
+
: {}),
|
|
609
|
+
...((c.maxRetries ?? args.maxRetries) !== undefined
|
|
610
|
+
? { maxRetries: c.maxRetries ?? args.maxRetries }
|
|
611
|
+
: {}),
|
|
612
|
+
...((c.diagnostics ?? args.diagnostics) !== undefined
|
|
613
|
+
? { diagnostics: c.diagnostics ?? args.diagnostics }
|
|
614
|
+
: {}),
|
|
1140
615
|
}, "batch_call")));
|
|
1141
616
|
const results = settled.map((s, i) => {
|
|
1142
|
-
const
|
|
617
|
+
const call = args.calls[i];
|
|
618
|
+
if (!call) {
|
|
619
|
+
throw new Error("Batch result has no corresponding call");
|
|
620
|
+
}
|
|
621
|
+
const { address } = call;
|
|
1143
622
|
if (s.status === "rejected") {
|
|
1144
623
|
return {
|
|
1145
624
|
address,
|
|
@@ -1157,19 +636,19 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
1157
636
|
errorDetails: r.error,
|
|
1158
637
|
durationMs: r.durationMs,
|
|
1159
638
|
attempts: r.attempts,
|
|
1160
|
-
...((
|
|
639
|
+
...((call.diagnostics ?? args.diagnostics)
|
|
1161
640
|
? { timing: r.timing }
|
|
1162
641
|
: {}),
|
|
1163
642
|
};
|
|
1164
643
|
}
|
|
1165
|
-
if ((
|
|
644
|
+
if ((call.resultMode ?? args.resultMode) === "value") {
|
|
1166
645
|
return {
|
|
1167
646
|
address,
|
|
1168
647
|
ok: true,
|
|
1169
648
|
data: r.value,
|
|
1170
649
|
durationMs: r.durationMs,
|
|
1171
650
|
attempts: r.attempts,
|
|
1172
|
-
...((
|
|
651
|
+
...((call.diagnostics ?? args.diagnostics)
|
|
1173
652
|
? { timing: r.timing }
|
|
1174
653
|
: {}),
|
|
1175
654
|
};
|
|
@@ -1180,7 +659,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
1180
659
|
result: r.toolResult.content,
|
|
1181
660
|
durationMs: r.durationMs,
|
|
1182
661
|
attempts: r.attempts,
|
|
1183
|
-
...((
|
|
662
|
+
...((call.diagnostics ?? args.diagnostics)
|
|
1184
663
|
? { timing: r.timing }
|
|
1185
664
|
: {}),
|
|
1186
665
|
};
|
|
@@ -1220,6 +699,21 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
1220
699
|
...(details.retryAfterMs !== undefined
|
|
1221
700
|
? { retryAfterMs: details.retryAfterMs }
|
|
1222
701
|
: {}),
|
|
702
|
+
...(details.connector !== undefined
|
|
703
|
+
? { connector: batchSummaryString(details.connector) }
|
|
704
|
+
: {}),
|
|
705
|
+
...(details.operation !== undefined
|
|
706
|
+
? { operation: batchSummaryString(details.operation) }
|
|
707
|
+
: {}),
|
|
708
|
+
...(details.recovery !== undefined
|
|
709
|
+
? { recovery: details.recovery }
|
|
710
|
+
: {}),
|
|
711
|
+
...(details.nextAction !== undefined
|
|
712
|
+
? { nextAction: details.nextAction }
|
|
713
|
+
: {}),
|
|
714
|
+
...(details.retry !== undefined
|
|
715
|
+
? { retry: batchSummaryString(details.retry) }
|
|
716
|
+
: {}),
|
|
1223
717
|
},
|
|
1224
718
|
};
|
|
1225
719
|
}),
|
|
@@ -1233,28 +727,58 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
1233
727
|
return errorResult(`Unknown connector "${args.connector}"`);
|
|
1234
728
|
}
|
|
1235
729
|
if (!connector.startAuth) {
|
|
1236
|
-
|
|
730
|
+
if (!connector.credential) {
|
|
731
|
+
return jsonResult({
|
|
732
|
+
connector: connector.id,
|
|
733
|
+
recovery: "unavailable",
|
|
734
|
+
message: `Connector "${connector.id}" declares neither downstream OAuth ` +
|
|
735
|
+
"nor an operator-managed credential slot. Update the connector " +
|
|
736
|
+
"or deployment configuration before retrying.",
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
|
|
740
|
+
if (!ctx.credential) {
|
|
741
|
+
return jsonResult({
|
|
742
|
+
connector: connector.id,
|
|
743
|
+
recovery: "unavailable",
|
|
744
|
+
message: "Credential storage is not configured. Configure " +
|
|
745
|
+
"credentials.encryptionKey, redeploy, then call " +
|
|
746
|
+
"authorize_connector again.",
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
const fields = connector.credential.fields?.map((field) => ({
|
|
750
|
+
name: field.name,
|
|
751
|
+
guidance: field.description ?? field.label,
|
|
752
|
+
})) ?? [
|
|
753
|
+
{
|
|
754
|
+
name: "value",
|
|
755
|
+
guidance: connector.credential.description ??
|
|
756
|
+
connector.credential.label,
|
|
757
|
+
},
|
|
758
|
+
];
|
|
759
|
+
return jsonResult({
|
|
760
|
+
connector: connector.id,
|
|
761
|
+
recovery: "operator_config",
|
|
762
|
+
credential: {
|
|
763
|
+
label: connector.credential.label,
|
|
764
|
+
fields,
|
|
765
|
+
},
|
|
766
|
+
operatorUrl: new URL("/credentials", baseUrl).toString(),
|
|
767
|
+
instructions: "Have the operator open operatorUrl, set and test the credential, " +
|
|
768
|
+
"then retry the original call. No redeploy is needed. Credential " +
|
|
769
|
+
"mutation requires a Clerk-authenticated operator.",
|
|
770
|
+
});
|
|
1237
771
|
}
|
|
1238
772
|
const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
|
|
1239
773
|
try {
|
|
1240
|
-
const status = await connector.startAuth(ctx, { force: args.force });
|
|
1241
|
-
// startAuth just spoke to the downstream about this exact credential, so
|
|
1242
|
-
// its answer replaces any older liveness verdict — including the stale
|
|
1243
|
-
// `auth_required` that sent the agent here, once it reports ok.
|
|
1244
|
-
await registry.recordCredentialHealth(connector.id, {
|
|
1245
|
-
state: status.state,
|
|
1246
|
-
checkedAt: new Date().toISOString(),
|
|
1247
|
-
...(status.message ? { message: status.message } : {}),
|
|
1248
|
-
...(status.authorizationUrl
|
|
1249
|
-
? { authorizationUrl: status.authorizationUrl }
|
|
1250
|
-
: {}),
|
|
1251
|
-
});
|
|
774
|
+
const status = await connector.startAuth(ctx, args.force !== undefined ? { force: args.force } : {});
|
|
1252
775
|
if (status.state === "auth_required" && !status.authorizationUrl) {
|
|
1253
776
|
// auth_required with nothing to open is a dead end for the operator.
|
|
1254
777
|
return errorResult(`Connector "${connector.id}": authorization required but no URL is available — retry authorize_connector.`);
|
|
1255
778
|
}
|
|
1256
779
|
return jsonResult({
|
|
1257
780
|
connector: connector.id,
|
|
781
|
+
recovery: "oauth",
|
|
1258
782
|
status: status.state,
|
|
1259
783
|
...(status.authorizationUrl
|
|
1260
784
|
? {
|
|
@@ -1277,13 +801,13 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
1277
801
|
};
|
|
1278
802
|
}
|
|
1279
803
|
const LIST_DESC = "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.";
|
|
1280
|
-
const SEARCH_DESC = `Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all.
|
|
804
|
+
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.`;
|
|
1281
805
|
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.`;
|
|
1282
806
|
const CALL_DESC = '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.';
|
|
1283
807
|
const CALL_DESTRUCTIVE_DESC = "Invoke any tool that is not explicitly annotated readOnlyHint: true, including unannotated, write-capable, or destructive tools. The MCP destructiveHint on this meta-tool lets the host request human approval before execution. Use only after reviewing the downstream tool schema and consequences.";
|
|
1284
808
|
const GET_RESULT_DESC = "Page a truncated result stashed by call_tool/batch_call. Input { id, offset?, maxBytes? } → { text, offset, nextOffset?, totalBytes } sliced by byte offset. maxBytes is a whole number of bytes >= 1 (omit for the deployment default) and offset a whole number of bytes >= 0; an offset inside a multi-byte character is moved back to that character's first byte and the offset served is returned. Unknown/expired id is an error.";
|
|
1285
809
|
const BATCH_DESC = "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.";
|
|
1286
|
-
const AUTHORIZE_DESC = "Use after
|
|
810
|
+
const AUTHORIZE_DESC = "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.";
|
|
1287
811
|
const SKILLS_DESC = '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.';
|
|
1288
812
|
/**
|
|
1289
813
|
* Sentences appended to a meta-tool description only when this connection
|
|
@@ -1291,10 +815,7 @@ const SKILLS_DESC = 'List or fetch concise guidance for choosing among Connecta
|
|
|
1291
815
|
* so a deployment with no guides gets every base description unchanged rather
|
|
1292
816
|
* than paying for text about a feature it does not use.
|
|
1293
817
|
*
|
|
1294
|
-
* Registration is per connection and reads the
|
|
1295
|
-
* so under a toolkit these sentences reflect the SCOPED connector set: a scoped
|
|
1296
|
-
* session whose connectors carry no guides sees the base descriptions, and
|
|
1297
|
-
* never learns from a tool description that guides exist out of scope.
|
|
818
|
+
* Registration is per connection and reads the configured connector set.
|
|
1298
819
|
*/
|
|
1299
820
|
const GUIDE_NOTES = {
|
|
1300
821
|
skills: ' skills({}) also lists this deployment\'s per-connector usage guides as "connector:<connectorId>"; fetch the guide for a connector before working with it for the first time.',
|
|
@@ -1325,24 +846,37 @@ const READ_ONLY_LOCAL = {
|
|
|
1325
846
|
idempotentHint: true,
|
|
1326
847
|
openWorldHint: false,
|
|
1327
848
|
};
|
|
849
|
+
const CALL_INPUT_SCHEMA = {
|
|
850
|
+
address: z.string(),
|
|
851
|
+
args: z.record(z.string(), z.unknown()).optional(),
|
|
852
|
+
fields: z.array(z.string()).optional(),
|
|
853
|
+
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
854
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
855
|
+
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
856
|
+
diagnostics: z.boolean().optional(),
|
|
857
|
+
};
|
|
1328
858
|
/** Register the nine meta-tools onto an McpServer instance. */
|
|
1329
859
|
export function registerMetaTools(server, registry, ctx) {
|
|
1330
860
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
861
|
+
...(ctx.defaultToolTimeoutMs !== undefined
|
|
862
|
+
? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
|
|
863
|
+
: {}),
|
|
864
|
+
...(ctx.probeTimeoutMs !== undefined
|
|
865
|
+
? { probeTimeoutMs: ctx.probeTimeoutMs }
|
|
866
|
+
: {}),
|
|
867
|
+
...(ctx.discoveryConcurrency !== undefined
|
|
868
|
+
? { discoveryConcurrency: ctx.discoveryConcurrency }
|
|
869
|
+
: {}),
|
|
870
|
+
...(ctx.activity !== undefined ? { activity: ctx.activity } : {}),
|
|
871
|
+
...(ctx.requestSignal !== undefined
|
|
872
|
+
? { requestSignal: ctx.requestSignal }
|
|
873
|
+
: {}),
|
|
874
|
+
...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
|
|
1336
875
|
});
|
|
1337
876
|
server.registerTool("skills", {
|
|
1338
877
|
description: describedFor(registry, SKILLS_DESC, "skills"),
|
|
1339
878
|
inputSchema: { name: z.string().optional() },
|
|
1340
|
-
annotations:
|
|
1341
|
-
readOnlyHint: true,
|
|
1342
|
-
destructiveHint: false,
|
|
1343
|
-
idempotentHint: true,
|
|
1344
|
-
openWorldHint: false,
|
|
1345
|
-
},
|
|
879
|
+
annotations: READ_ONLY_LOCAL,
|
|
1346
880
|
}, async (args) => mt.skills(args));
|
|
1347
881
|
server.registerTool("list_connectors", {
|
|
1348
882
|
description: LIST_DESC,
|
|
@@ -1372,30 +906,14 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1372
906
|
}, async (args) => mt.describeTools(args));
|
|
1373
907
|
server.registerTool("call_tool", {
|
|
1374
908
|
description: CALL_DESC,
|
|
1375
|
-
inputSchema:
|
|
1376
|
-
address: z.string(),
|
|
1377
|
-
args: z.record(z.string(), z.unknown()).optional(),
|
|
1378
|
-
fields: z.array(z.string()).optional(),
|
|
1379
|
-
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1380
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
1381
|
-
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1382
|
-
diagnostics: z.boolean().optional(),
|
|
1383
|
-
},
|
|
909
|
+
inputSchema: CALL_INPUT_SCHEMA,
|
|
1384
910
|
// call_tool admits only tools that are themselves explicitly read-only;
|
|
1385
911
|
// anything else is refused and routed to call_destructive_tool.
|
|
1386
912
|
annotations: READ_ONLY_REMOTE,
|
|
1387
913
|
}, async (args) => mt.callTool(args));
|
|
1388
914
|
server.registerTool("call_destructive_tool", {
|
|
1389
915
|
description: CALL_DESTRUCTIVE_DESC,
|
|
1390
|
-
inputSchema:
|
|
1391
|
-
address: z.string(),
|
|
1392
|
-
args: z.record(z.string(), z.unknown()).optional(),
|
|
1393
|
-
fields: z.array(z.string()).optional(),
|
|
1394
|
-
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1395
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
1396
|
-
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1397
|
-
diagnostics: z.boolean().optional(),
|
|
1398
|
-
},
|
|
916
|
+
inputSchema: CALL_INPUT_SCHEMA,
|
|
1399
917
|
annotations: {
|
|
1400
918
|
destructiveHint: true,
|
|
1401
919
|
readOnlyHint: false,
|
|
@@ -1433,15 +951,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1433
951
|
description: BATCH_DESC,
|
|
1434
952
|
inputSchema: {
|
|
1435
953
|
calls: z
|
|
1436
|
-
.array(z.object(
|
|
1437
|
-
address: z.string(),
|
|
1438
|
-
args: z.record(z.string(), z.unknown()).optional(),
|
|
1439
|
-
fields: z.array(z.string()).optional(),
|
|
1440
|
-
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1441
|
-
timeoutMs: z.number().int().positive().optional(),
|
|
1442
|
-
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
1443
|
-
diagnostics: z.boolean().optional(),
|
|
1444
|
-
}))
|
|
954
|
+
.array(z.object(CALL_INPUT_SCHEMA))
|
|
1445
955
|
.min(1)
|
|
1446
956
|
.max(10),
|
|
1447
957
|
resultMode: z.enum(["mcp", "value"]).optional(),
|