@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +131 -4
  2. package/dist/apps-shell.d.ts +10 -12
  3. package/dist/apps-shell.js +29 -220
  4. package/dist/catalog-service.d.ts +16 -13
  5. package/dist/catalog-service.js +106 -115
  6. package/dist/catalog.js +29 -46
  7. package/dist/connector-scope.js +2 -7
  8. package/dist/connectors/api.d.ts +4 -16
  9. package/dist/connectors/api.js +19 -46
  10. package/dist/connectors/guarded-fetch.d.ts +9 -23
  11. package/dist/connectors/guarded-fetch.js +38 -76
  12. package/dist/connectors/remote-mcp.js +36 -79
  13. package/dist/errors.d.ts +6 -27
  14. package/dist/errors.js +8 -5
  15. package/dist/execute.d.ts +23 -28
  16. package/dist/execute.js +101 -257
  17. package/dist/executor-result.d.ts +1 -0
  18. package/dist/executor-result.js +4 -11
  19. package/dist/executors/quickjs-child.js +1 -3
  20. package/dist/executors/quickjs-runtime.js +1 -3
  21. package/dist/executors/quickjs.js +1 -3
  22. package/dist/index.js +134 -123
  23. package/dist/invocation.d.ts +1 -1
  24. package/dist/invocation.js +113 -183
  25. package/dist/meta-tools.d.ts +15 -29
  26. package/dist/meta-tools.js +41 -582
  27. package/dist/operator-ui/generated.d.ts +2 -2
  28. package/dist/providers/cloudflare.d.ts +2 -18
  29. package/dist/providers/cloudflare.js +1460 -2451
  30. package/dist/providers/linear.d.ts +4 -41
  31. package/dist/providers/linear.js +8 -39
  32. package/dist/providers/mixpanel.d.ts +3 -25
  33. package/dist/providers/mixpanel.js +7 -22
  34. package/dist/providers/notion.d.ts +1 -15
  35. package/dist/providers/notion.js +44 -173
  36. package/dist/providers/revenuecat.d.ts +4 -57
  37. package/dist/providers/revenuecat.js +10 -93
  38. package/dist/providers/stripe.d.ts +1 -12
  39. package/dist/providers/stripe.js +7 -45
  40. package/dist/registry.d.ts +9 -34
  41. package/dist/registry.js +9 -103
  42. package/dist/routes/mcp.js +1 -1
  43. package/dist/routes/oauth.js +3 -3
  44. package/dist/routes/shared.d.ts +15 -15
  45. package/dist/routes/shared.js +1 -3
  46. package/dist/skills.d.ts +1 -1
  47. package/dist/skills.js +5 -5
  48. package/dist/timeout.d.ts +8 -7
  49. package/dist/timeout.js +47 -38
  50. package/dist/types.d.ts +3 -3
  51. package/dist/ui.d.ts +1 -25
  52. package/dist/ui.js +18 -45
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/documentation/call-admission.md +1 -1
  56. package/documentation/cloudflare.md +1 -1
  57. package/documentation/code-mode.md +25 -25
  58. package/documentation/connectors.md +24 -1
  59. package/documentation/linear.md +1 -1
  60. package/documentation/meta-tools.md +4 -30
  61. package/documentation/mixpanel.md +1 -1
  62. package/documentation/notion.md +1 -1
  63. package/documentation/operations.md +29 -25
  64. package/documentation/provider-conventions.md +4 -5
  65. package/documentation/revenuecat.md +1 -1
  66. package/documentation/stripe.md +1 -1
  67. package/documentation/upgrading.md +43 -8
  68. package/ethos.md +75 -121
  69. package/package.json +3 -4
  70. package/templates/node/package.json +1 -1
  71. package/documentation/code-first-exploration.md +0 -292
  72. package/documentation/mcp-2026-07-28.md +0 -46
  73. package/documentation/mcp-ui-design.md +0 -382
  74. package/documentation/program-ui-read-calls.md +0 -213
  75. package/documentation/provider-audit.md +0 -198
  76. package/documentation/rich-output-design.md +0 -211
@@ -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: JSON.stringify(obj) }],
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
- * The one definition of a usable `get_result` offset: a whole number of bytes
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
- * Move a byte `end` back to the nearest UTF-8 codepoint boundary in
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, or re-call with fields to select less",
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
- ...(opts.defer ? { defer: opts.defer } : {}),
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 = fields
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
- let content = mcpResult?.content ?? [];
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 = fields
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 failedResult = structuredRecovery ||
840
- outcome.error.code === "auth_required" ||
841
- outcome.error.code === "invalid_args" ||
842
- outcome.error.code === "input_required_unsupported" ||
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 (structuredRecovery ||
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 fields, so it never reaches the connector.
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. fields projects JSON dot-paths; use [] through arrays. A truncated result carries a get_result action.';
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
- ...(ctx.defaultToolTimeoutMs !== undefined
1111
- ? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
1112
- : {}),
1113
- ...(ctx.probeTimeoutMs !== undefined
1114
- ? { probeTimeoutMs: ctx.probeTimeoutMs }
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, SEARCH_WITH_DESCRIBE_DESC, "search"),
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
- // The trusted program-view shell delegates bounded named reads here.
1154
- // It is already one of the seven model tools; app visibility adds no
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"),