@zackbart/connecta 0.18.3 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +131 -4
- package/dist/apps-shell.d.ts +10 -12
- package/dist/apps-shell.js +29 -220
- package/dist/catalog-service.d.ts +16 -13
- package/dist/catalog-service.js +106 -115
- package/dist/catalog.js +29 -46
- package/dist/connector-scope.js +2 -7
- package/dist/connectors/api.d.ts +4 -16
- package/dist/connectors/api.js +19 -46
- package/dist/connectors/guarded-fetch.d.ts +9 -23
- package/dist/connectors/guarded-fetch.js +38 -76
- package/dist/connectors/remote-mcp.js +36 -79
- package/dist/errors.d.ts +6 -27
- package/dist/errors.js +8 -5
- package/dist/execute.d.ts +23 -28
- package/dist/execute.js +101 -257
- package/dist/executor-result.d.ts +1 -0
- package/dist/executor-result.js +4 -11
- package/dist/executors/quickjs-child.js +1 -3
- package/dist/executors/quickjs-runtime.js +1 -3
- package/dist/executors/quickjs.js +1 -3
- package/dist/index.js +134 -123
- package/dist/invocation.d.ts +1 -1
- package/dist/invocation.js +113 -183
- package/dist/meta-tools.d.ts +15 -29
- package/dist/meta-tools.js +41 -582
- package/dist/operator-ui/generated.d.ts +2 -2
- package/dist/providers/cloudflare.d.ts +2 -18
- package/dist/providers/cloudflare.js +1460 -2451
- package/dist/providers/linear.d.ts +4 -41
- package/dist/providers/linear.js +8 -39
- package/dist/providers/mixpanel.d.ts +3 -25
- package/dist/providers/mixpanel.js +7 -22
- package/dist/providers/notion.d.ts +1 -15
- package/dist/providers/notion.js +44 -173
- package/dist/providers/revenuecat.d.ts +4 -57
- package/dist/providers/revenuecat.js +10 -93
- package/dist/providers/stripe.d.ts +1 -12
- package/dist/providers/stripe.js +7 -45
- package/dist/registry.d.ts +9 -34
- package/dist/registry.js +9 -103
- package/dist/routes/mcp.js +1 -1
- package/dist/routes/oauth.js +3 -3
- package/dist/routes/shared.d.ts +15 -15
- package/dist/routes/shared.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -5
- package/dist/timeout.d.ts +8 -7
- package/dist/timeout.js +47 -38
- package/dist/types.d.ts +3 -3
- package/dist/ui.d.ts +1 -25
- package/dist/ui.js +18 -45
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/call-admission.md +1 -1
- package/documentation/cloudflare.md +1 -1
- package/documentation/code-mode.md +25 -25
- package/documentation/connectors.md +24 -1
- package/documentation/linear.md +1 -1
- package/documentation/meta-tools.md +4 -30
- package/documentation/mixpanel.md +1 -1
- package/documentation/notion.md +1 -1
- package/documentation/operations.md +29 -25
- package/documentation/provider-conventions.md +4 -5
- package/documentation/revenuecat.md +1 -1
- package/documentation/stripe.md +1 -1
- package/documentation/upgrading.md +43 -8
- package/ethos.md +75 -121
- package/package.json +3 -4
- package/templates/node/package.json +1 -1
- package/documentation/code-first-exploration.md +0 -292
- package/documentation/mcp-2026-07-28.md +0 -46
- package/documentation/mcp-ui-design.md +0 -382
- package/documentation/program-ui-read-calls.md +0 -213
- package/documentation/provider-audit.md +0 -198
- package/documentation/rich-output-design.md +0 -211
package/dist/meta-tools.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError, groupedSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
|
|
3
3
|
import { resolveDiscoveryConcurrency } from "./concurrency.js";
|
|
4
|
+
import { msg } from "./errors.js";
|
|
5
|
+
import { serializeResultText } from "./executor-result.js";
|
|
4
6
|
import { InvocationService, MAX_RETRY_BACKOFF_MS, retryBackoffMs, } from "./invocation.js";
|
|
5
7
|
import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
|
|
6
8
|
import { hasConnectorGuides, listSkills, resolveSkill, } from "./skills.js";
|
|
@@ -9,9 +11,9 @@ export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_RETRY_BACKOFF_M
|
|
|
9
11
|
const RESULT_TTL_SECONDS = 900;
|
|
10
12
|
const enc = new TextEncoder();
|
|
11
13
|
const dec = new TextDecoder();
|
|
12
|
-
export function jsonResult(obj) {
|
|
14
|
+
export function jsonResult(obj, text = JSON.stringify(obj)) {
|
|
13
15
|
return {
|
|
14
|
-
content: [{ type: "text", text
|
|
16
|
+
content: [{ type: "text", text }],
|
|
15
17
|
...(obj !== null && typeof obj === "object" && !Array.isArray(obj)
|
|
16
18
|
? { structuredContent: obj }
|
|
17
19
|
: {}),
|
|
@@ -20,9 +22,6 @@ export function jsonResult(obj) {
|
|
|
20
22
|
export function errorResult(message) {
|
|
21
23
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
22
24
|
}
|
|
23
|
-
function msg(err) {
|
|
24
|
-
return err instanceof Error ? err.message : String(err);
|
|
25
|
-
}
|
|
26
25
|
function discoveryErrorResult(error) {
|
|
27
26
|
const result = jsonResult({
|
|
28
27
|
error: {
|
|
@@ -38,12 +37,7 @@ async function discoveryResult(operation, hint) {
|
|
|
38
37
|
try {
|
|
39
38
|
const value = await operation();
|
|
40
39
|
const text = boundedDiscoveryText(value, hint);
|
|
41
|
-
return
|
|
42
|
-
content: [{ type: "text", text }],
|
|
43
|
-
...(value !== null && typeof value === "object" && !Array.isArray(value)
|
|
44
|
-
? { structuredContent: value }
|
|
45
|
-
: {}),
|
|
46
|
-
};
|
|
40
|
+
return jsonResult(value, text);
|
|
47
41
|
}
|
|
48
42
|
catch (err) {
|
|
49
43
|
if (err instanceof DiscoveryPolicyError) {
|
|
@@ -58,20 +52,8 @@ function isContinuationByte(b) {
|
|
|
58
52
|
}
|
|
59
53
|
/** Smallest accepted `get_result` byte offset. */
|
|
60
54
|
const MIN_RESULT_OFFSET = 0;
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
* at or past {@link MIN_RESULT_OFFSET}. Shared by the registered zod schema and
|
|
64
|
-
* the handler's own check, the way `isValidMaxResultBytes` is shared across the
|
|
65
|
-
* cap's intake points (issue #32) — so a value valid at the wire is valid in
|
|
66
|
-
* process, and the two cannot drift.
|
|
67
|
-
*
|
|
68
|
-
* Everything else is rejected rather than coerced, because coercion is how an
|
|
69
|
-
* out-of-domain offset used to void a result silently: `Math.max(0, NaN)` is
|
|
70
|
-
* `NaN`, which slices to nothing, serializes as `"offset": null`, and reports
|
|
71
|
-
* no `nextOffset` — a caller sees a successful, empty result instead of an
|
|
72
|
-
* error. An offset past the end of the payload stays legal: it is a whole
|
|
73
|
-
* number of bytes, and it answers with an empty final page.
|
|
74
|
-
*/
|
|
55
|
+
/** Whole-byte offset accepted by the result representation documented in
|
|
56
|
+
* documentation/meta-tools.md#result-representation. */
|
|
75
57
|
function isValidResultOffset(value) {
|
|
76
58
|
return Number.isInteger(value) && value >= MIN_RESULT_OFFSET;
|
|
77
59
|
}
|
|
@@ -93,21 +75,8 @@ export function alignStartToCharBoundary(bytes, offset) {
|
|
|
93
75
|
o--;
|
|
94
76
|
return o;
|
|
95
77
|
}
|
|
96
|
-
/**
|
|
97
|
-
*
|
|
98
|
-
* `(offset, total]`, so decoding `bytes[offset, end)` never splits a codepoint
|
|
99
|
-
* (which would emit U+FFFD and break byte-exact reassembly). If backing up
|
|
100
|
-
* would make no progress — a single codepoint wider than the window — extend
|
|
101
|
-
* forward to the end of that codepoint instead so paging always advances.
|
|
102
|
-
* Assumes `offset` is itself a codepoint boundary (offsets are the prior
|
|
103
|
-
* `nextOffset`, which this function guarantees, and 0 is always a boundary).
|
|
104
|
-
*
|
|
105
|
-
* The return is always `> offset` while `offset < total`, whatever `end` is
|
|
106
|
-
* asked for. That is the belt-and-braces half of issue #32: cap validation
|
|
107
|
-
* keeps an empty window from arising in the first place, and this keeps an
|
|
108
|
-
* empty window from turning into a `nextOffset === offset` paging loop if one
|
|
109
|
-
* ever does. Exported for direct testing of that invariant.
|
|
110
|
-
*/
|
|
78
|
+
/** End boundary for UTF-8-safe, forward-progressing result pages. See
|
|
79
|
+
* documentation/meta-tools.md#result-representation. */
|
|
111
80
|
export function alignEndToCharBoundary(bytes, offset, end, total) {
|
|
112
81
|
if (end >= total)
|
|
113
82
|
return total;
|
|
@@ -125,480 +94,6 @@ export function alignEndToCharBoundary(bytes, offset, end, total) {
|
|
|
125
94
|
}
|
|
126
95
|
return e;
|
|
127
96
|
}
|
|
128
|
-
/** Resolve a dot-path and retain misses below every `[]` boundary. */
|
|
129
|
-
function resolvePath(value, segments) {
|
|
130
|
-
const seg = segments[0];
|
|
131
|
-
if (seg === undefined) {
|
|
132
|
-
return value === undefined
|
|
133
|
-
? { status: "unmatched" }
|
|
134
|
-
: { status: "matched", value };
|
|
135
|
-
}
|
|
136
|
-
const rest = segments.slice(1);
|
|
137
|
-
const isArr = seg.endsWith("[]");
|
|
138
|
-
const key = isArr ? seg.slice(0, -2) : seg;
|
|
139
|
-
let next = value;
|
|
140
|
-
if (key !== "") {
|
|
141
|
-
if (value === null || typeof value !== "object") {
|
|
142
|
-
return { status: "unmatched" };
|
|
143
|
-
}
|
|
144
|
-
next = value[key];
|
|
145
|
-
}
|
|
146
|
-
if (isArr) {
|
|
147
|
-
if (!Array.isArray(next))
|
|
148
|
-
return { status: "unmatched" };
|
|
149
|
-
if (next.length === 0)
|
|
150
|
-
return { status: "matched", value: [] };
|
|
151
|
-
const elements = next.map((el) => resolvePath(el, rest));
|
|
152
|
-
const matched = elements.some((element) => element.status !== "unmatched");
|
|
153
|
-
const missed = elements.some((element) => element.status !== "matched");
|
|
154
|
-
if (!matched)
|
|
155
|
-
return { status: "unmatched" };
|
|
156
|
-
return {
|
|
157
|
-
status: missed ? "partial" : "matched",
|
|
158
|
-
// Undefined placeholders retain the historical array positions in the
|
|
159
|
-
// partial value. JSON renders them as null; partialFields says they are
|
|
160
|
-
// unresolved rather than genuine downstream nulls.
|
|
161
|
-
value: elements.map((element) => element.status === "unmatched" ? undefined : element.value),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return resolvePath(next, rest);
|
|
165
|
-
}
|
|
166
|
-
const MAX_PROJECTION_AVAILABLE_FIELDS = 20;
|
|
167
|
-
const MAX_PROJECTION_SCHEMA_DEPTH = 12;
|
|
168
|
-
const MAX_PROJECTION_SCHEMA_NODES = 200;
|
|
169
|
-
const MAX_PROJECTION_SCHEMA_PATHS = 100;
|
|
170
|
-
const MAX_PROJECTION_PATH_CHARS = 256;
|
|
171
|
-
const MAX_PROJECTION_PATH_BYTES = 512;
|
|
172
|
-
const MAX_PROJECTION_TOTAL_PATH_CHARS = 512;
|
|
173
|
-
const MAX_PROJECTION_TOTAL_PATH_BYTES = 768;
|
|
174
|
-
const JSON_SCHEMA_TYPES = new Set([
|
|
175
|
-
"array",
|
|
176
|
-
"boolean",
|
|
177
|
-
"integer",
|
|
178
|
-
"null",
|
|
179
|
-
"number",
|
|
180
|
-
"object",
|
|
181
|
-
"string",
|
|
182
|
-
]);
|
|
183
|
-
const NON_SEMANTIC_REF_SIBLINGS = new Set([
|
|
184
|
-
"$anchor",
|
|
185
|
-
"$comment",
|
|
186
|
-
"$defs",
|
|
187
|
-
"$id",
|
|
188
|
-
"$ref",
|
|
189
|
-
"$schema",
|
|
190
|
-
"default",
|
|
191
|
-
"definitions",
|
|
192
|
-
"deprecated",
|
|
193
|
-
"description",
|
|
194
|
-
"examples",
|
|
195
|
-
"readOnly",
|
|
196
|
-
"title",
|
|
197
|
-
"writeOnly",
|
|
198
|
-
]);
|
|
199
|
-
function localSchemaRef(root, ref) {
|
|
200
|
-
if (ref === "#")
|
|
201
|
-
return root;
|
|
202
|
-
if (!ref.startsWith("#/") ||
|
|
203
|
-
ref.length > 2_048 ||
|
|
204
|
-
/~(?![01])/.test(ref)) {
|
|
205
|
-
return undefined;
|
|
206
|
-
}
|
|
207
|
-
const segments = ref.slice(2).split("/");
|
|
208
|
-
if (segments.length > MAX_PROJECTION_SCHEMA_DEPTH)
|
|
209
|
-
return undefined;
|
|
210
|
-
let current = root;
|
|
211
|
-
for (const encoded of segments) {
|
|
212
|
-
if (current === null || typeof current !== "object")
|
|
213
|
-
return undefined;
|
|
214
|
-
const key = encoded.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
215
|
-
if (!Object.prototype.hasOwnProperty.call(current, key))
|
|
216
|
-
return undefined;
|
|
217
|
-
current = current[key];
|
|
218
|
-
}
|
|
219
|
-
return current;
|
|
220
|
-
}
|
|
221
|
-
function hasSchemaType(schema, wanted) {
|
|
222
|
-
return (schema.type === wanted ||
|
|
223
|
-
(Array.isArray(schema.type) && schema.type.includes(wanted)));
|
|
224
|
-
}
|
|
225
|
-
function selectableFieldName(name) {
|
|
226
|
-
return name.length > 0 && !name.includes(".") && !name.endsWith("[]");
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Collect selectable output paths without trusting a schema more than JSON
|
|
230
|
-
* Schema permits. Traversal is iterative and budgeted before sorting or
|
|
231
|
-
* rendering, so a cyclic, extremely deep, or extremely broad downstream
|
|
232
|
-
* schema cannot turn projection feedback into unbounded host work.
|
|
233
|
-
*/
|
|
234
|
-
function analyzeSchemaFields(root) {
|
|
235
|
-
const pending = [
|
|
236
|
-
{ schema: root, prefix: "", depth: 0, ancestors: new Set() },
|
|
237
|
-
];
|
|
238
|
-
const paths = new Set();
|
|
239
|
-
let nodes = 0;
|
|
240
|
-
let totalPathChars = 0;
|
|
241
|
-
let totalPathBytes = 0;
|
|
242
|
-
let complete = true;
|
|
243
|
-
let truncated = false;
|
|
244
|
-
const addPath = (path) => {
|
|
245
|
-
if (paths.has(path))
|
|
246
|
-
return true;
|
|
247
|
-
// Check UTF-16 length before encoding, so a hostile multi-megabyte key
|
|
248
|
-
// never causes a same-sized temporary allocation merely to reject it.
|
|
249
|
-
if (path.length > MAX_PROJECTION_PATH_CHARS ||
|
|
250
|
-
totalPathChars + path.length > MAX_PROJECTION_TOTAL_PATH_CHARS) {
|
|
251
|
-
complete = false;
|
|
252
|
-
truncated = true;
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
const pathBytes = enc.encode(path).length;
|
|
256
|
-
if (pathBytes > MAX_PROJECTION_PATH_BYTES ||
|
|
257
|
-
totalPathBytes + pathBytes > MAX_PROJECTION_TOTAL_PATH_BYTES ||
|
|
258
|
-
paths.size >= MAX_PROJECTION_SCHEMA_PATHS) {
|
|
259
|
-
complete = false;
|
|
260
|
-
truncated = true;
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
paths.add(path);
|
|
264
|
-
totalPathChars += path.length;
|
|
265
|
-
totalPathBytes += pathBytes;
|
|
266
|
-
return true;
|
|
267
|
-
};
|
|
268
|
-
const enqueue = (item) => {
|
|
269
|
-
if (nodes + pending.length >= MAX_PROJECTION_SCHEMA_NODES) {
|
|
270
|
-
complete = false;
|
|
271
|
-
truncated = true;
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
274
|
-
pending.push(item);
|
|
275
|
-
return true;
|
|
276
|
-
};
|
|
277
|
-
while (pending.length > 0) {
|
|
278
|
-
const item = pending.pop();
|
|
279
|
-
if (item.depth > MAX_PROJECTION_SCHEMA_DEPTH) {
|
|
280
|
-
complete = false;
|
|
281
|
-
truncated = true;
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (++nodes > MAX_PROJECTION_SCHEMA_NODES) {
|
|
285
|
-
complete = false;
|
|
286
|
-
truncated = true;
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
if (item.schema === false)
|
|
290
|
-
continue;
|
|
291
|
-
if (item.schema === true ||
|
|
292
|
-
item.schema === null ||
|
|
293
|
-
typeof item.schema !== "object" ||
|
|
294
|
-
item.ancestors.has(item.schema)) {
|
|
295
|
-
complete = false;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
const schema = item.schema;
|
|
299
|
-
const ancestors = new Set(item.ancestors).add(item.schema);
|
|
300
|
-
let recognized = false;
|
|
301
|
-
if (schema.type !== undefined &&
|
|
302
|
-
!((typeof schema.type === "string" &&
|
|
303
|
-
JSON_SCHEMA_TYPES.has(schema.type)) ||
|
|
304
|
-
(Array.isArray(schema.type) &&
|
|
305
|
-
schema.type.length > 0 &&
|
|
306
|
-
schema.type.every((type) => typeof type === "string" && JSON_SCHEMA_TYPES.has(type))))) {
|
|
307
|
-
complete = false;
|
|
308
|
-
}
|
|
309
|
-
if (schema.$ref !== undefined) {
|
|
310
|
-
recognized = true;
|
|
311
|
-
let hasSemanticSiblings = false;
|
|
312
|
-
for (const key in schema) {
|
|
313
|
-
if (Object.prototype.hasOwnProperty.call(schema, key) &&
|
|
314
|
-
!NON_SEMANTIC_REF_SIBLINGS.has(key)) {
|
|
315
|
-
hasSemanticSiblings = true;
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
if (hasSemanticSiblings) {
|
|
320
|
-
// Modern JSON Schema applies $ref siblings as an intersection. A
|
|
321
|
-
// compact field walker cannot prove that intersection's selectable
|
|
322
|
-
// paths, so do not publish paths from either half as available.
|
|
323
|
-
complete = false;
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
const target = typeof schema.$ref === "string"
|
|
327
|
-
? localSchemaRef(root, schema.$ref)
|
|
328
|
-
: undefined;
|
|
329
|
-
if (target === undefined)
|
|
330
|
-
complete = false;
|
|
331
|
-
else {
|
|
332
|
-
enqueue({
|
|
333
|
-
schema: target,
|
|
334
|
-
prefix: item.prefix,
|
|
335
|
-
depth: item.depth + 1,
|
|
336
|
-
ancestors,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
|
|
341
|
-
const variants = schema[keyword];
|
|
342
|
-
if (!Array.isArray(variants))
|
|
343
|
-
continue;
|
|
344
|
-
recognized = true;
|
|
345
|
-
// Combining schemas can close or conditionally expose fields in ways
|
|
346
|
-
// this compact recovery walker intentionally does not prove.
|
|
347
|
-
complete = false;
|
|
348
|
-
for (const variant of variants) {
|
|
349
|
-
if (!enqueue({
|
|
350
|
-
schema: variant,
|
|
351
|
-
prefix: item.prefix,
|
|
352
|
-
depth: item.depth + 1,
|
|
353
|
-
ancestors,
|
|
354
|
-
})) {
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
if (truncated)
|
|
360
|
-
break;
|
|
361
|
-
const properties = schema.properties;
|
|
362
|
-
const propertyRecord = properties !== null &&
|
|
363
|
-
typeof properties === "object" &&
|
|
364
|
-
!Array.isArray(properties)
|
|
365
|
-
? properties
|
|
366
|
-
: undefined;
|
|
367
|
-
if (properties !== undefined && propertyRecord === undefined) {
|
|
368
|
-
complete = false;
|
|
369
|
-
}
|
|
370
|
-
const objectShape = hasSchemaType(schema, "object") || propertyRecord !== undefined;
|
|
371
|
-
if (objectShape) {
|
|
372
|
-
recognized = true;
|
|
373
|
-
const patterns = schema.patternProperties;
|
|
374
|
-
let hasPatterns = false;
|
|
375
|
-
if (patterns !== undefined &&
|
|
376
|
-
(patterns === null ||
|
|
377
|
-
typeof patterns !== "object" ||
|
|
378
|
-
Array.isArray(patterns))) {
|
|
379
|
-
complete = false;
|
|
380
|
-
}
|
|
381
|
-
else if (patterns !== undefined) {
|
|
382
|
-
for (const key in patterns) {
|
|
383
|
-
if (Object.prototype.hasOwnProperty.call(patterns, key)) {
|
|
384
|
-
hasPatterns = true;
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
if (schema.additionalProperties !== false || hasPatterns) {
|
|
390
|
-
complete = false;
|
|
391
|
-
}
|
|
392
|
-
if (propertyRecord) {
|
|
393
|
-
for (const key in propertyRecord) {
|
|
394
|
-
if (!Object.prototype.hasOwnProperty.call(propertyRecord, key)) {
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
if (++nodes > MAX_PROJECTION_SCHEMA_NODES) {
|
|
398
|
-
complete = false;
|
|
399
|
-
truncated = true;
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
|
-
if (key.length > MAX_PROJECTION_PATH_CHARS) {
|
|
403
|
-
complete = false;
|
|
404
|
-
truncated = true;
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
|
-
if (!selectableFieldName(key)) {
|
|
408
|
-
complete = false;
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
const child = propertyRecord[key];
|
|
412
|
-
// A false property schema forbids the property; advertising its name
|
|
413
|
-
// as selectable would turn an impossible value into a valid hint.
|
|
414
|
-
if (child === false)
|
|
415
|
-
continue;
|
|
416
|
-
const path = item.prefix ? `${item.prefix}.${key}` : key;
|
|
417
|
-
if (!addPath(path))
|
|
418
|
-
break;
|
|
419
|
-
if (!enqueue({
|
|
420
|
-
schema: child,
|
|
421
|
-
prefix: path,
|
|
422
|
-
depth: item.depth + 1,
|
|
423
|
-
ancestors,
|
|
424
|
-
})) {
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
if (truncated)
|
|
431
|
-
break;
|
|
432
|
-
const arrayShape = hasSchemaType(schema, "array") ||
|
|
433
|
-
schema.items !== undefined ||
|
|
434
|
-
schema.prefixItems !== undefined;
|
|
435
|
-
if (arrayShape) {
|
|
436
|
-
recognized = true;
|
|
437
|
-
const arrayPath = `${item.prefix}[]`;
|
|
438
|
-
if (addPath(arrayPath)) {
|
|
439
|
-
if (Array.isArray(schema.prefixItems)) {
|
|
440
|
-
complete = false;
|
|
441
|
-
for (const child of schema.prefixItems) {
|
|
442
|
-
if (!enqueue({
|
|
443
|
-
schema: child,
|
|
444
|
-
prefix: arrayPath,
|
|
445
|
-
depth: item.depth + 1,
|
|
446
|
-
ancestors,
|
|
447
|
-
})) {
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
if (schema.items === undefined) {
|
|
453
|
-
if (!Array.isArray(schema.prefixItems))
|
|
454
|
-
complete = false;
|
|
455
|
-
}
|
|
456
|
-
else if (schema.items === true || Array.isArray(schema.items)) {
|
|
457
|
-
complete = false;
|
|
458
|
-
const children = Array.isArray(schema.items)
|
|
459
|
-
? schema.items
|
|
460
|
-
: [];
|
|
461
|
-
for (const child of children) {
|
|
462
|
-
if (!enqueue({
|
|
463
|
-
schema: child,
|
|
464
|
-
prefix: arrayPath,
|
|
465
|
-
depth: item.depth + 1,
|
|
466
|
-
ancestors,
|
|
467
|
-
})) {
|
|
468
|
-
break;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
else if (schema.items !== false) {
|
|
473
|
-
enqueue({
|
|
474
|
-
schema: schema.items,
|
|
475
|
-
prefix: arrayPath,
|
|
476
|
-
depth: item.depth + 1,
|
|
477
|
-
ancestors,
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
const types = Array.isArray(schema.type)
|
|
483
|
-
? schema.type
|
|
484
|
-
: schema.type === undefined
|
|
485
|
-
? []
|
|
486
|
-
: [schema.type];
|
|
487
|
-
const primitiveOnly = types.length > 0 &&
|
|
488
|
-
types.every((type) => type === "string" ||
|
|
489
|
-
type === "number" ||
|
|
490
|
-
type === "integer" ||
|
|
491
|
-
type === "boolean" ||
|
|
492
|
-
type === "null");
|
|
493
|
-
if (!recognized && !primitiveOnly)
|
|
494
|
-
complete = false;
|
|
495
|
-
if (truncated)
|
|
496
|
-
break;
|
|
497
|
-
}
|
|
498
|
-
return {
|
|
499
|
-
paths: [...paths].sort(),
|
|
500
|
-
complete,
|
|
501
|
-
truncated,
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
function schemaProjectionFeedback(outputSchema, unmatchedFields) {
|
|
505
|
-
const analysis = analyzeSchemaFields(outputSchema);
|
|
506
|
-
const available = new Set(analysis.paths);
|
|
507
|
-
const invalidFields = analysis.complete
|
|
508
|
-
? unmatchedFields.filter((field) => !available.has(field))
|
|
509
|
-
: [];
|
|
510
|
-
const missingArrayMarker = unmatchedFields.some((field) => analysis.paths.some((availableField) => availableField.includes("[]") &&
|
|
511
|
-
availableField.replaceAll("[]", "") === field));
|
|
512
|
-
return {
|
|
513
|
-
...(missingArrayMarker
|
|
514
|
-
? {
|
|
515
|
-
hint: 'Traverse arrays with [] after the array field name, for example "results[].id".',
|
|
516
|
-
}
|
|
517
|
-
: {}),
|
|
518
|
-
schemaDeclared: true,
|
|
519
|
-
schemaCoverage: analysis.complete ? "complete" : "partial",
|
|
520
|
-
...(invalidFields.length > 0 ? { invalidFields } : {}),
|
|
521
|
-
...(analysis.paths.length > 0
|
|
522
|
-
? {
|
|
523
|
-
availableFields: analysis.paths.slice(0, MAX_PROJECTION_AVAILABLE_FIELDS),
|
|
524
|
-
}
|
|
525
|
-
: {}),
|
|
526
|
-
...(analysis.truncated ||
|
|
527
|
-
analysis.paths.length > MAX_PROJECTION_AVAILABLE_FIELDS
|
|
528
|
-
? { availableFieldsTruncated: true }
|
|
529
|
-
: {}),
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
/** Select the given dot-paths, retaining both matches and exact misses. */
|
|
533
|
-
function applyFields(value, fields) {
|
|
534
|
-
const out = {};
|
|
535
|
-
const unmatchedFields = [];
|
|
536
|
-
const partialFields = [];
|
|
537
|
-
for (const path of fields) {
|
|
538
|
-
const resolved = resolvePath(value, path.split("."));
|
|
539
|
-
if (resolved.status === "unmatched") {
|
|
540
|
-
unmatchedFields.push(path);
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
out[path] = resolved.value;
|
|
544
|
-
if (resolved.status === "partial")
|
|
545
|
-
partialFields.push(path);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
return { data: out, unmatchedFields, partialFields };
|
|
549
|
-
}
|
|
550
|
-
/**
|
|
551
|
-
* Keep the historical flat projection when every path resolves. A miss gets a
|
|
552
|
-
* wrapper with a reserved discriminator so neither `{}` nor downstream fields
|
|
553
|
-
* named `data` / `projection` can be mistaken for projection feedback.
|
|
554
|
-
*/
|
|
555
|
-
function projectionValue(value, fields, outputSchema) {
|
|
556
|
-
const projected = applyFields(value, fields);
|
|
557
|
-
// `$connecta` is reserved at the top level of a projection. Even a fully
|
|
558
|
-
// matched downstream field with that exact name is escaped below `data`, so
|
|
559
|
-
// no user-controlled value can impersonate Connecta's discriminator.
|
|
560
|
-
const reservedCollision = Object.prototype.hasOwnProperty.call(projected.data, "$connecta");
|
|
561
|
-
if (projected.unmatchedFields.length === 0 &&
|
|
562
|
-
projected.partialFields.length === 0 &&
|
|
563
|
-
!reservedCollision) {
|
|
564
|
-
return projected.data;
|
|
565
|
-
}
|
|
566
|
-
const problemFields = [
|
|
567
|
-
...projected.unmatchedFields,
|
|
568
|
-
...projected.partialFields,
|
|
569
|
-
];
|
|
570
|
-
return {
|
|
571
|
-
data: projected.data,
|
|
572
|
-
$connecta: {
|
|
573
|
-
type: "field_projection",
|
|
574
|
-
unmatchedFields: projected.unmatchedFields,
|
|
575
|
-
...(projected.partialFields.length > 0
|
|
576
|
-
? { partialFields: projected.partialFields }
|
|
577
|
-
: {}),
|
|
578
|
-
...(outputSchema
|
|
579
|
-
? schemaProjectionFeedback(outputSchema, problemFields)
|
|
580
|
-
: {}),
|
|
581
|
-
},
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
/** Apply fields to each JSON-parseable text block; non-JSON blocks pass through. */
|
|
585
|
-
function applyFieldsToContent(content, fields, outputSchema) {
|
|
586
|
-
return content.map((b) => {
|
|
587
|
-
if (b.type !== "text")
|
|
588
|
-
return b;
|
|
589
|
-
let parsed;
|
|
590
|
-
try {
|
|
591
|
-
parsed = JSON.parse(b.text);
|
|
592
|
-
}
|
|
593
|
-
catch {
|
|
594
|
-
return b;
|
|
595
|
-
}
|
|
596
|
-
return {
|
|
597
|
-
...b,
|
|
598
|
-
text: JSON.stringify(projectionValue(parsed, fields, outputSchema)),
|
|
599
|
-
};
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
97
|
// --- result-size guard + get_result (feature 1) ---------------------------
|
|
603
98
|
/**
|
|
604
99
|
* The one serialization every result guard measures, stashes, and pages: JSON
|
|
@@ -615,10 +110,6 @@ function applyFieldsToContent(content, fields, outputSchema) {
|
|
|
615
110
|
* the three give one answer to the same question. A value JSON cannot serialize
|
|
616
111
|
* at all (a BigInt) still throws, as before, and is reported as a failure.
|
|
617
112
|
*/
|
|
618
|
-
function serializeResultText(value) {
|
|
619
|
-
const serialized = JSON.stringify(value);
|
|
620
|
-
return serialized === undefined ? String(value) : serialized;
|
|
621
|
-
}
|
|
622
113
|
/**
|
|
623
114
|
* Stash `text` under `result:<uuid>` (ttl 900s) and describe it as the
|
|
624
115
|
* truncation notice every over-cap path hands back.
|
|
@@ -630,7 +121,7 @@ async function stashResult(text, results, totalBytes) {
|
|
|
630
121
|
truncated: true,
|
|
631
122
|
resultId: id,
|
|
632
123
|
totalBytes,
|
|
633
|
-
hint: "use get_result {id, offset} to page
|
|
124
|
+
hint: "use get_result {id, offset} to page the complete direct-call result",
|
|
634
125
|
nextAction: {
|
|
635
126
|
tool: "get_result",
|
|
636
127
|
arguments: { id, offset: 0 },
|
|
@@ -761,7 +252,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
761
252
|
requestScope,
|
|
762
253
|
probeTimeoutMs,
|
|
763
254
|
concurrency: discoveryConcurrency,
|
|
764
|
-
|
|
255
|
+
defer: opts.defer,
|
|
765
256
|
// searchRoute keeps its top-level default. In-program callers use a
|
|
766
257
|
// separate CatalogService configured for connecta.search.
|
|
767
258
|
});
|
|
@@ -769,7 +260,6 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
769
260
|
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
770
261
|
async function runCall(call, source, options = {}) {
|
|
771
262
|
const results = registry.resultsStorage();
|
|
772
|
-
const fields = call.fields && call.fields.length > 0 ? call.fields : null;
|
|
773
263
|
const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
|
|
774
264
|
const outcome = await invocation.invoke(call.address, call.args ?? {}, {
|
|
775
265
|
source,
|
|
@@ -792,54 +282,38 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
792
282
|
// override the registry already warned about at startup is dropped
|
|
793
283
|
// here, so the connector simply inherits `globalCap`.
|
|
794
284
|
const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
|
|
285
|
+
const processed = (toolResult, truncated, value) => ({
|
|
286
|
+
toolResult,
|
|
287
|
+
...value,
|
|
288
|
+
...(truncated ? { friction: "result_too_large" } : {}),
|
|
289
|
+
});
|
|
795
290
|
if (call.resultMode === "value") {
|
|
796
|
-
let value =
|
|
797
|
-
? projectionValue(result, fields, resolved.definition.outputSchema)
|
|
798
|
-
: result;
|
|
291
|
+
let value = result;
|
|
799
292
|
const guarded = await guardValue(value, results, cap);
|
|
800
293
|
value = guarded.result;
|
|
801
|
-
return {
|
|
802
|
-
toolResult: jsonResult({ ok: true, data: value }),
|
|
803
|
-
value,
|
|
804
|
-
...(guarded.truncated
|
|
805
|
-
? { friction: "result_too_large" }
|
|
806
|
-
: {}),
|
|
807
|
-
};
|
|
294
|
+
return processed(jsonResult({ ok: true, data: value }), guarded.truncated, { value });
|
|
808
295
|
}
|
|
809
296
|
if (resolved.connector.kind === "mcp") {
|
|
810
297
|
const mcpResult = result;
|
|
811
|
-
|
|
812
|
-
if (fields) {
|
|
813
|
-
content = applyFieldsToContent(content, fields, resolved.definition.outputSchema);
|
|
814
|
-
}
|
|
298
|
+
const content = mcpResult?.content ?? [];
|
|
815
299
|
const guarded = await guardContent(content, results, cap);
|
|
816
|
-
return
|
|
817
|
-
toolResult: guarded.result,
|
|
818
|
-
...(guarded.truncated
|
|
819
|
-
? { friction: "result_too_large" }
|
|
820
|
-
: {}),
|
|
821
|
-
};
|
|
300
|
+
return processed(guarded.result, guarded.truncated);
|
|
822
301
|
}
|
|
823
|
-
const value =
|
|
824
|
-
? projectionValue(result, fields, resolved.definition.outputSchema)
|
|
825
|
-
: result;
|
|
302
|
+
const value = result;
|
|
826
303
|
const guarded = await guardText(serializeResultText(value), results, cap);
|
|
827
|
-
return {
|
|
828
|
-
toolResult: guarded.result,
|
|
829
|
-
value,
|
|
830
|
-
...(guarded.truncated
|
|
831
|
-
? { friction: "result_too_large" }
|
|
832
|
-
: {}),
|
|
833
|
-
};
|
|
304
|
+
return processed(guarded.result, guarded.truncated, { value });
|
|
834
305
|
},
|
|
835
306
|
activityFriction: (processed) => processed.friction,
|
|
836
307
|
});
|
|
837
308
|
if (!outcome.ok) {
|
|
838
309
|
const structuredRecovery = outcome.error.nextAction !== undefined;
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
310
|
+
const recoveryRequired = structuredRecovery ||
|
|
311
|
+
[
|
|
312
|
+
"auth_required",
|
|
313
|
+
"invalid_args",
|
|
314
|
+
"input_required_unsupported",
|
|
315
|
+
].includes(outcome.error.code);
|
|
316
|
+
const failedResult = recoveryRequired ||
|
|
843
317
|
call.resultMode === "value"
|
|
844
318
|
? jsonResult({
|
|
845
319
|
ok: false,
|
|
@@ -849,10 +323,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
849
323
|
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
850
324
|
})
|
|
851
325
|
: errorResult(outcome.error.message);
|
|
852
|
-
if (
|
|
853
|
-
outcome.error.code === "auth_required" ||
|
|
854
|
-
outcome.error.code === "invalid_args" ||
|
|
855
|
-
outcome.error.code === "input_required_unsupported") {
|
|
326
|
+
if (recoveryRequired) {
|
|
856
327
|
failedResult.isError = true;
|
|
857
328
|
}
|
|
858
329
|
return {
|
|
@@ -914,7 +385,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
914
385
|
},
|
|
915
386
|
async callDestructiveTool(args) {
|
|
916
387
|
// `reason` is read by the host's approval view and stops there — runCall
|
|
917
|
-
// forwards only the call
|
|
388
|
+
// forwards only the call arguments, so it never reaches the connector.
|
|
918
389
|
return (await runCall(args, "call_destructive_tool", { allowDestructive: true })).toolResult;
|
|
919
390
|
},
|
|
920
391
|
async getResult(args) {
|
|
@@ -1045,12 +516,11 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
1045
516
|
};
|
|
1046
517
|
}
|
|
1047
518
|
const SEARCH_DESC = `Use top-level search for one unknown-address read before call_tool, or for approval-required work before call_destructive_tool. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" finds direct or program calls; "approvalRequired" finds the fail-closed complement. These filters grant no authority. For multiple, dependent, or reduced read-only calls, use one execute_code program instead. Empty query browses.`;
|
|
1048
|
-
const CALL_DESC = 'Call one tool explicitly annotated readOnlyHint: true. Use execute_code for multiple, dependent, or reduced read-only calls. Unannotated or write-capable tools fail closed to call_destructive_tool.
|
|
519
|
+
const CALL_DESC = 'Call one tool explicitly annotated readOnlyHint: true. Use execute_code for multiple, dependent, or reduced read-only calls. Unannotated or write-capable tools fail closed to call_destructive_tool. A truncated result carries a get_result action.';
|
|
1049
520
|
const CALL_DESTRUCTIVE_DESC = "Call any tool not explicitly annotated readOnlyHint: true. Include a short reason for the human reviewer after checking the schema and consequences. The reason grants no authority and is not sent downstream.";
|
|
1050
521
|
const GET_RESULT_DESC = "Page a truncated direct-call result by id and byte offset. A program result is never paged; reduce it inside execute_code. Returns text, offset, nextOffset when more remains, and totalBytes.";
|
|
1051
522
|
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.";
|
|
1052
523
|
const SKILLS_DESC = 'List or fetch on-demand guidance. Fetch usage once per task for program syntax, selection, repair, examples, and runtime details.';
|
|
1053
|
-
const SEARCH_WITH_DESCRIBE_DESC = SEARCH_DESC;
|
|
1054
524
|
/**
|
|
1055
525
|
* Sentences appended to a meta-tool description only when this connection
|
|
1056
526
|
* actually has connector guides. Tool descriptions are always-loaded context,
|
|
@@ -1091,7 +561,6 @@ const READ_ONLY_LOCAL = {
|
|
|
1091
561
|
const CALL_INPUT_SCHEMA = {
|
|
1092
562
|
address: z.string(),
|
|
1093
563
|
args: z.record(z.string(), z.unknown()).optional(),
|
|
1094
|
-
fields: z.array(z.string()).optional(),
|
|
1095
564
|
resultMode: z.enum(["mcp", "value"]).optional(),
|
|
1096
565
|
timeoutMs: z.number().int().positive().optional(),
|
|
1097
566
|
maxRetries: z.number().int().min(0).max(2).optional(),
|
|
@@ -1107,20 +576,12 @@ const CALL_INPUT_SCHEMA = {
|
|
|
1107
576
|
*/
|
|
1108
577
|
export function registerMetaTools(server, registry, ctx) {
|
|
1109
578
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
...(ctx.discoveryConcurrency !== undefined
|
|
1117
|
-
? { discoveryConcurrency: ctx.discoveryConcurrency }
|
|
1118
|
-
: {}),
|
|
1119
|
-
...(ctx.activity !== undefined ? { activity: ctx.activity } : {}),
|
|
1120
|
-
...(ctx.requestSignal !== undefined
|
|
1121
|
-
? { requestSignal: ctx.requestSignal }
|
|
1122
|
-
: {}),
|
|
1123
|
-
...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
|
|
579
|
+
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
580
|
+
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
581
|
+
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
582
|
+
activity: ctx.activity,
|
|
583
|
+
requestSignal: ctx.requestSignal,
|
|
584
|
+
defer: ctx.defer,
|
|
1124
585
|
});
|
|
1125
586
|
server.registerTool("skills", {
|
|
1126
587
|
description: describedFor(registry, SKILLS_DESC, "skills"),
|
|
@@ -1129,7 +590,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1129
590
|
_meta: { ui: { visibility: ["model"] } },
|
|
1130
591
|
}, async (args) => mt.skills(args));
|
|
1131
592
|
server.registerTool("search_tools", {
|
|
1132
|
-
description: describedFor(registry,
|
|
593
|
+
description: describedFor(registry, SEARCH_DESC, "search"),
|
|
1133
594
|
inputSchema: z.object({
|
|
1134
595
|
query: z.string().optional(),
|
|
1135
596
|
connector: z.string().optional(),
|
|
@@ -1150,10 +611,8 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1150
611
|
// call_tool admits only tools that are themselves explicitly read-only;
|
|
1151
612
|
// anything else is refused and routed to call_destructive_tool.
|
|
1152
613
|
annotations: READ_ONLY_REMOTE,
|
|
1153
|
-
//
|
|
1154
|
-
|
|
1155
|
-
// tool and this handler repeats ordinary fail-closed read admission.
|
|
1156
|
-
_meta: { ui: { visibility: ["model", "app"] } },
|
|
614
|
+
// Omission defaults to model + app. Display-only views may call no tool.
|
|
615
|
+
_meta: { ui: { visibility: ["model"] } },
|
|
1157
616
|
}, async (args) => mt.callTool(args));
|
|
1158
617
|
server.registerTool("call_destructive_tool", {
|
|
1159
618
|
description: describedFor(registry, CALL_DESTRUCTIVE_DESC, "destructive"),
|