@zackbart/connecta 0.10.1 → 0.10.3

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 (78) hide show
  1. package/AGENTS.md +113 -0
  2. package/CHANGELOG.md +70 -0
  3. package/README.md +53 -9
  4. package/bin/connecta.mjs +272 -0
  5. package/dist/catalog-service.d.ts +40 -1
  6. package/dist/catalog-service.d.ts.map +1 -1
  7. package/dist/catalog-service.js +137 -12
  8. package/dist/catalog-service.js.map +1 -1
  9. package/dist/catalog.d.ts +17 -0
  10. package/dist/catalog.d.ts.map +1 -1
  11. package/dist/catalog.js +113 -13
  12. package/dist/catalog.js.map +1 -1
  13. package/dist/errors.d.ts +28 -0
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +40 -0
  16. package/dist/errors.js.map +1 -1
  17. package/dist/execute.d.ts +45 -1
  18. package/dist/execute.d.ts.map +1 -1
  19. package/dist/execute.js +265 -68
  20. package/dist/execute.js.map +1 -1
  21. package/dist/invocation.d.ts.map +1 -1
  22. package/dist/invocation.js +34 -6
  23. package/dist/invocation.js.map +1 -1
  24. package/dist/meta-tools.d.ts +1 -0
  25. package/dist/meta-tools.d.ts.map +1 -1
  26. package/dist/meta-tools.js +412 -12
  27. package/dist/meta-tools.js.map +1 -1
  28. package/dist/skills.d.ts +1 -1
  29. package/dist/skills.d.ts.map +1 -1
  30. package/dist/skills.js +1 -1
  31. package/dist/tool-safety.d.ts +10 -0
  32. package/dist/tool-safety.d.ts.map +1 -0
  33. package/dist/tool-safety.js +12 -0
  34. package/dist/tool-safety.js.map +1 -0
  35. package/dist/validate.d.ts.map +1 -1
  36. package/dist/validate.js +100 -1
  37. package/dist/validate.js.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/documentation/architecture.md +7 -0
  41. package/documentation/auth.md +58 -0
  42. package/documentation/call-admission.md +7 -0
  43. package/documentation/code-first-exploration.md +292 -0
  44. package/documentation/code-mode.md +696 -0
  45. package/documentation/connector-guides.md +7 -0
  46. package/documentation/connectors.md +69 -0
  47. package/documentation/mcp-2026-07-28.md +46 -0
  48. package/documentation/meta-tools.md +185 -0
  49. package/documentation/operations.md +7 -0
  50. package/documentation/operator-ui.md +7 -0
  51. package/documentation/request-admission.md +7 -0
  52. package/documentation/storage-and-credentials.md +54 -0
  53. package/ethos.md +132 -0
  54. package/examples/node/README.md +53 -0
  55. package/examples/node/src/index.ts +73 -0
  56. package/examples/worker/README.md +160 -0
  57. package/examples/worker/src/cloudflare-kv.ts +43 -0
  58. package/examples/worker/src/d1-activity-row.ts +100 -0
  59. package/examples/worker/src/d1-activity.ts +144 -0
  60. package/examples/worker/src/index.ts +136 -0
  61. package/examples/worker/wrangler.jsonc +26 -0
  62. package/package.json +11 -1
  63. package/src/catalog-service.ts +181 -16
  64. package/src/catalog.ts +143 -12
  65. package/src/errors.ts +88 -1
  66. package/src/execute.ts +372 -96
  67. package/src/invocation.ts +45 -8
  68. package/src/meta-tools.ts +506 -11
  69. package/src/skills.ts +1 -1
  70. package/src/tool-safety.ts +15 -0
  71. package/src/validate.ts +128 -0
  72. package/src/version.ts +1 -1
  73. package/templates/node/.env.example +5 -0
  74. package/templates/node/AGENTS.md +19 -0
  75. package/templates/node/README.md +33 -0
  76. package/templates/node/package.json +23 -0
  77. package/templates/node/src/index.ts +43 -0
  78. package/templates/node/tsconfig.json +12 -0
package/src/meta-tools.ts CHANGED
@@ -236,23 +236,491 @@ function resolvePath(value: unknown, segments: string[]): unknown {
236
236
  return resolvePath(next, rest);
237
237
  }
238
238
 
239
- /** Select the given dot-paths from a value → `{ "<path>": value }` (omitting misses). */
239
+ const MAX_PROJECTION_AVAILABLE_FIELDS = 20;
240
+ const MAX_PROJECTION_SCHEMA_DEPTH = 12;
241
+ const MAX_PROJECTION_SCHEMA_NODES = 200;
242
+ const MAX_PROJECTION_SCHEMA_PATHS = 100;
243
+ const MAX_PROJECTION_PATH_CHARS = 256;
244
+ const MAX_PROJECTION_PATH_BYTES = 512;
245
+ const MAX_PROJECTION_TOTAL_PATH_CHARS = 512;
246
+ const MAX_PROJECTION_TOTAL_PATH_BYTES = 768;
247
+ const JSON_SCHEMA_TYPES = new Set([
248
+ "array",
249
+ "boolean",
250
+ "integer",
251
+ "null",
252
+ "number",
253
+ "object",
254
+ "string",
255
+ ]);
256
+ const NON_SEMANTIC_REF_SIBLINGS = new Set([
257
+ "$anchor",
258
+ "$comment",
259
+ "$defs",
260
+ "$id",
261
+ "$ref",
262
+ "$schema",
263
+ "default",
264
+ "definitions",
265
+ "deprecated",
266
+ "description",
267
+ "examples",
268
+ "readOnly",
269
+ "title",
270
+ "writeOnly",
271
+ ]);
272
+
273
+ interface FieldProjection {
274
+ data: Record<string, unknown>;
275
+ unmatchedFields: string[];
276
+ }
277
+
278
+ interface ProjectionFeedback {
279
+ data: Record<string, unknown>;
280
+ $connecta: {
281
+ type: "field_projection";
282
+ unmatchedFields: string[];
283
+ schemaDeclared?: true;
284
+ schemaCoverage?: "complete" | "partial";
285
+ invalidFields?: string[];
286
+ availableFields?: string[];
287
+ availableFieldsTruncated?: true;
288
+ };
289
+ }
290
+
291
+ interface SchemaFieldAnalysis {
292
+ paths: string[];
293
+ complete: boolean;
294
+ truncated: boolean;
295
+ }
296
+
297
+ function localSchemaRef(root: unknown, ref: string): unknown | undefined {
298
+ if (ref === "#") return root;
299
+ if (
300
+ !ref.startsWith("#/") ||
301
+ ref.length > 2_048 ||
302
+ /~(?![01])/.test(ref)
303
+ ) {
304
+ return undefined;
305
+ }
306
+ const segments = ref.slice(2).split("/");
307
+ if (segments.length > MAX_PROJECTION_SCHEMA_DEPTH) return undefined;
308
+ let current = root;
309
+ for (const encoded of segments) {
310
+ if (current === null || typeof current !== "object") return undefined;
311
+ const key = encoded.replaceAll("~1", "/").replaceAll("~0", "~");
312
+ if (!Object.prototype.hasOwnProperty.call(current, key)) return undefined;
313
+ current = (current as Record<string, unknown>)[key];
314
+ }
315
+ return current;
316
+ }
317
+
318
+ function hasSchemaType(
319
+ schema: Record<string, unknown>,
320
+ wanted: string,
321
+ ): boolean {
322
+ return (
323
+ schema.type === wanted ||
324
+ (Array.isArray(schema.type) && schema.type.includes(wanted))
325
+ );
326
+ }
327
+
328
+ function selectableFieldName(name: string): boolean {
329
+ return name.length > 0 && !name.includes(".") && !name.endsWith("[]");
330
+ }
331
+
332
+ /**
333
+ * Collect selectable output paths without trusting a schema more than JSON
334
+ * Schema permits. Traversal is iterative and budgeted before sorting or
335
+ * rendering, so a cyclic, extremely deep, or extremely broad downstream
336
+ * schema cannot turn projection feedback into unbounded host work.
337
+ */
338
+ function analyzeSchemaFields(root: unknown): SchemaFieldAnalysis {
339
+ interface PendingSchema {
340
+ schema: unknown;
341
+ prefix: string;
342
+ depth: number;
343
+ ancestors: Set<unknown>;
344
+ }
345
+ const pending: PendingSchema[] = [
346
+ { schema: root, prefix: "", depth: 0, ancestors: new Set() },
347
+ ];
348
+ const paths = new Set<string>();
349
+ let nodes = 0;
350
+ let totalPathChars = 0;
351
+ let totalPathBytes = 0;
352
+ let complete = true;
353
+ let truncated = false;
354
+
355
+ const addPath = (path: string): boolean => {
356
+ if (paths.has(path)) return true;
357
+ // Check UTF-16 length before encoding, so a hostile multi-megabyte key
358
+ // never causes a same-sized temporary allocation merely to reject it.
359
+ if (
360
+ path.length > MAX_PROJECTION_PATH_CHARS ||
361
+ totalPathChars + path.length > MAX_PROJECTION_TOTAL_PATH_CHARS
362
+ ) {
363
+ complete = false;
364
+ truncated = true;
365
+ return false;
366
+ }
367
+ const pathBytes = enc.encode(path).length;
368
+ if (
369
+ pathBytes > MAX_PROJECTION_PATH_BYTES ||
370
+ totalPathBytes + pathBytes > MAX_PROJECTION_TOTAL_PATH_BYTES ||
371
+ paths.size >= MAX_PROJECTION_SCHEMA_PATHS
372
+ ) {
373
+ complete = false;
374
+ truncated = true;
375
+ return false;
376
+ }
377
+ paths.add(path);
378
+ totalPathChars += path.length;
379
+ totalPathBytes += pathBytes;
380
+ return true;
381
+ };
382
+ const enqueue = (item: PendingSchema): boolean => {
383
+ if (nodes + pending.length >= MAX_PROJECTION_SCHEMA_NODES) {
384
+ complete = false;
385
+ truncated = true;
386
+ return false;
387
+ }
388
+ pending.push(item);
389
+ return true;
390
+ };
391
+
392
+ while (pending.length > 0) {
393
+ const item = pending.pop()!;
394
+ if (item.depth > MAX_PROJECTION_SCHEMA_DEPTH) {
395
+ complete = false;
396
+ truncated = true;
397
+ continue;
398
+ }
399
+ if (++nodes > MAX_PROJECTION_SCHEMA_NODES) {
400
+ complete = false;
401
+ truncated = true;
402
+ break;
403
+ }
404
+ if (item.schema === false) continue;
405
+ if (
406
+ item.schema === true ||
407
+ item.schema === null ||
408
+ typeof item.schema !== "object" ||
409
+ item.ancestors.has(item.schema)
410
+ ) {
411
+ complete = false;
412
+ continue;
413
+ }
414
+
415
+ const schema = item.schema as Record<string, unknown>;
416
+ const ancestors = new Set(item.ancestors).add(item.schema);
417
+ let recognized = false;
418
+ if (
419
+ schema.type !== undefined &&
420
+ !(
421
+ (typeof schema.type === "string" &&
422
+ JSON_SCHEMA_TYPES.has(schema.type)) ||
423
+ (Array.isArray(schema.type) &&
424
+ schema.type.length > 0 &&
425
+ schema.type.every(
426
+ (type) =>
427
+ typeof type === "string" && JSON_SCHEMA_TYPES.has(type),
428
+ ))
429
+ )
430
+ ) {
431
+ complete = false;
432
+ }
433
+
434
+ if (schema.$ref !== undefined) {
435
+ recognized = true;
436
+ let hasSemanticSiblings = false;
437
+ for (const key in schema) {
438
+ if (
439
+ Object.prototype.hasOwnProperty.call(schema, key) &&
440
+ !NON_SEMANTIC_REF_SIBLINGS.has(key)
441
+ ) {
442
+ hasSemanticSiblings = true;
443
+ break;
444
+ }
445
+ }
446
+ if (hasSemanticSiblings) {
447
+ // Modern JSON Schema applies $ref siblings as an intersection. A
448
+ // compact field walker cannot prove that intersection's selectable
449
+ // paths, so do not publish paths from either half as available.
450
+ complete = false;
451
+ continue;
452
+ }
453
+ const target =
454
+ typeof schema.$ref === "string"
455
+ ? localSchemaRef(root, schema.$ref)
456
+ : undefined;
457
+ if (target === undefined) complete = false;
458
+ else {
459
+ enqueue({
460
+ schema: target,
461
+ prefix: item.prefix,
462
+ depth: item.depth + 1,
463
+ ancestors,
464
+ });
465
+ }
466
+ }
467
+
468
+ for (const keyword of ["allOf", "anyOf", "oneOf"]) {
469
+ const variants = schema[keyword];
470
+ if (!Array.isArray(variants)) continue;
471
+ recognized = true;
472
+ // Combining schemas can close or conditionally expose fields in ways
473
+ // this compact recovery walker intentionally does not prove.
474
+ complete = false;
475
+ for (const variant of variants) {
476
+ if (
477
+ !enqueue({
478
+ schema: variant,
479
+ prefix: item.prefix,
480
+ depth: item.depth + 1,
481
+ ancestors,
482
+ })
483
+ ) {
484
+ break;
485
+ }
486
+ }
487
+ }
488
+ if (truncated) break;
489
+
490
+ const properties = schema.properties;
491
+ const propertyRecord =
492
+ properties !== null &&
493
+ typeof properties === "object" &&
494
+ !Array.isArray(properties)
495
+ ? (properties as Record<string, unknown>)
496
+ : undefined;
497
+ if (properties !== undefined && propertyRecord === undefined) {
498
+ complete = false;
499
+ }
500
+ const objectShape =
501
+ hasSchemaType(schema, "object") || propertyRecord !== undefined;
502
+ if (objectShape) {
503
+ recognized = true;
504
+ const patterns = schema.patternProperties;
505
+ let hasPatterns = false;
506
+ if (
507
+ patterns !== undefined &&
508
+ (patterns === null ||
509
+ typeof patterns !== "object" ||
510
+ Array.isArray(patterns))
511
+ ) {
512
+ complete = false;
513
+ } else if (patterns !== undefined) {
514
+ for (const key in patterns as Record<string, unknown>) {
515
+ if (Object.prototype.hasOwnProperty.call(patterns, key)) {
516
+ hasPatterns = true;
517
+ break;
518
+ }
519
+ }
520
+ }
521
+ if (schema.additionalProperties !== false || hasPatterns) {
522
+ complete = false;
523
+ }
524
+ if (propertyRecord) {
525
+ for (const key in propertyRecord) {
526
+ if (!Object.prototype.hasOwnProperty.call(propertyRecord, key)) {
527
+ continue;
528
+ }
529
+ if (++nodes > MAX_PROJECTION_SCHEMA_NODES) {
530
+ complete = false;
531
+ truncated = true;
532
+ break;
533
+ }
534
+ if (key.length > MAX_PROJECTION_PATH_CHARS) {
535
+ complete = false;
536
+ truncated = true;
537
+ break;
538
+ }
539
+ if (!selectableFieldName(key)) {
540
+ complete = false;
541
+ continue;
542
+ }
543
+ const child = propertyRecord[key];
544
+ // A false property schema forbids the property; advertising its name
545
+ // as selectable would turn an impossible value into a valid hint.
546
+ if (child === false) continue;
547
+ const path = item.prefix ? `${item.prefix}.${key}` : key;
548
+ if (!addPath(path)) break;
549
+ if (
550
+ !enqueue({
551
+ schema: child,
552
+ prefix: path,
553
+ depth: item.depth + 1,
554
+ ancestors,
555
+ })
556
+ ) {
557
+ break;
558
+ }
559
+ }
560
+ }
561
+ }
562
+ if (truncated) break;
563
+
564
+ const arrayShape =
565
+ hasSchemaType(schema, "array") ||
566
+ schema.items !== undefined ||
567
+ schema.prefixItems !== undefined;
568
+ if (arrayShape) {
569
+ recognized = true;
570
+ const arrayPath = `${item.prefix}[]`;
571
+ if (addPath(arrayPath)) {
572
+ if (Array.isArray(schema.prefixItems)) {
573
+ complete = false;
574
+ for (const child of schema.prefixItems) {
575
+ if (
576
+ !enqueue({
577
+ schema: child,
578
+ prefix: arrayPath,
579
+ depth: item.depth + 1,
580
+ ancestors,
581
+ })
582
+ ) {
583
+ break;
584
+ }
585
+ }
586
+ }
587
+ if (schema.items === undefined) {
588
+ if (!Array.isArray(schema.prefixItems)) complete = false;
589
+ } else if (schema.items === true || Array.isArray(schema.items)) {
590
+ complete = false;
591
+ const children = Array.isArray(schema.items)
592
+ ? schema.items
593
+ : [];
594
+ for (const child of children) {
595
+ if (
596
+ !enqueue({
597
+ schema: child,
598
+ prefix: arrayPath,
599
+ depth: item.depth + 1,
600
+ ancestors,
601
+ })
602
+ ) {
603
+ break;
604
+ }
605
+ }
606
+ } else if (schema.items !== false) {
607
+ enqueue({
608
+ schema: schema.items,
609
+ prefix: arrayPath,
610
+ depth: item.depth + 1,
611
+ ancestors,
612
+ });
613
+ }
614
+ }
615
+ }
616
+
617
+ const types = Array.isArray(schema.type)
618
+ ? schema.type
619
+ : schema.type === undefined
620
+ ? []
621
+ : [schema.type];
622
+ const primitiveOnly =
623
+ types.length > 0 &&
624
+ types.every(
625
+ (type) =>
626
+ type === "string" ||
627
+ type === "number" ||
628
+ type === "integer" ||
629
+ type === "boolean" ||
630
+ type === "null",
631
+ );
632
+ if (!recognized && !primitiveOnly) complete = false;
633
+ if (truncated) break;
634
+ }
635
+
636
+ return {
637
+ paths: [...paths].sort(),
638
+ complete,
639
+ truncated,
640
+ };
641
+ }
642
+
643
+ function schemaProjectionFeedback(
644
+ outputSchema: unknown,
645
+ unmatchedFields: string[],
646
+ ): Omit<ProjectionFeedback["$connecta"], "type" | "unmatchedFields"> {
647
+ const analysis = analyzeSchemaFields(outputSchema);
648
+ const available = new Set(analysis.paths);
649
+ const invalidFields = analysis.complete
650
+ ? unmatchedFields.filter((field) => !available.has(field))
651
+ : [];
652
+ return {
653
+ schemaDeclared: true,
654
+ schemaCoverage: analysis.complete ? "complete" : "partial",
655
+ ...(invalidFields.length > 0 ? { invalidFields } : {}),
656
+ ...(analysis.paths.length > 0
657
+ ? {
658
+ availableFields: analysis.paths.slice(
659
+ 0,
660
+ MAX_PROJECTION_AVAILABLE_FIELDS,
661
+ ),
662
+ }
663
+ : {}),
664
+ ...(analysis.truncated ||
665
+ analysis.paths.length > MAX_PROJECTION_AVAILABLE_FIELDS
666
+ ? { availableFieldsTruncated: true as const }
667
+ : {}),
668
+ };
669
+ }
670
+
671
+ /** Select the given dot-paths, retaining both matches and exact misses. */
240
672
  function applyFields(
241
673
  value: unknown,
242
674
  fields: string[],
243
- ): Record<string, unknown> {
675
+ ): FieldProjection {
244
676
  const out: Record<string, unknown> = {};
677
+ const unmatchedFields: string[] = [];
245
678
  for (const path of fields) {
246
679
  const resolved = resolvePath(value, path.split("."));
247
- if (resolved !== undefined) out[path] = resolved;
680
+ if (resolved === undefined) unmatchedFields.push(path);
681
+ else out[path] = resolved;
248
682
  }
249
- return out;
683
+ return { data: out, unmatchedFields };
684
+ }
685
+
686
+ /**
687
+ * Keep the historical flat projection when every path resolves. A miss gets a
688
+ * wrapper with a reserved discriminator so neither `{}` nor downstream fields
689
+ * named `data` / `projection` can be mistaken for projection feedback.
690
+ */
691
+ function projectionValue(
692
+ value: unknown,
693
+ fields: string[],
694
+ outputSchema?: unknown,
695
+ ): Record<string, unknown> | ProjectionFeedback {
696
+ const projected = applyFields(value, fields);
697
+ // `$connecta` is reserved at the top level of a projection. Even a fully
698
+ // matched downstream field with that exact name is escaped below `data`, so
699
+ // no user-controlled value can impersonate Connecta's discriminator.
700
+ const reservedCollision = Object.prototype.hasOwnProperty.call(
701
+ projected.data,
702
+ "$connecta",
703
+ );
704
+ if (projected.unmatchedFields.length === 0 && !reservedCollision) {
705
+ return projected.data;
706
+ }
707
+ return {
708
+ data: projected.data,
709
+ $connecta: {
710
+ type: "field_projection",
711
+ unmatchedFields: projected.unmatchedFields,
712
+ ...(outputSchema
713
+ ? schemaProjectionFeedback(outputSchema, projected.unmatchedFields)
714
+ : {}),
715
+ },
716
+ };
250
717
  }
251
718
 
252
719
  /** Apply fields to each JSON-parseable text block; non-JSON blocks pass through. */
253
720
  function applyFieldsToContent(
254
721
  content: TextContent[],
255
722
  fields: string[],
723
+ outputSchema?: unknown,
256
724
  ): TextContent[] {
257
725
  return content.map((b) => {
258
726
  if (b.type !== "text") return b;
@@ -262,7 +730,10 @@ function applyFieldsToContent(
262
730
  } catch {
263
731
  return b;
264
732
  }
265
- return { ...b, text: JSON.stringify(applyFields(parsed, fields)) };
733
+ return {
734
+ ...b,
735
+ text: JSON.stringify(projectionValue(parsed, fields, outputSchema)),
736
+ };
266
737
  });
267
738
  }
268
739
 
@@ -424,6 +895,7 @@ async function guardContent(
424
895
  export interface SearchArgs {
425
896
  query?: string;
426
897
  connector?: string;
898
+ safety?: "readOnly" | "approvalRequired" | "all";
427
899
  limit?: number;
428
900
  offset?: number;
429
901
  fullDescriptions?: boolean;
@@ -596,7 +1068,13 @@ export function createMetaTools(
596
1068
  globalCap,
597
1069
  );
598
1070
  if (call.resultMode === "value") {
599
- let value = fields ? applyFields(result, fields) : result;
1071
+ let value = fields
1072
+ ? projectionValue(
1073
+ result,
1074
+ fields,
1075
+ resolved.definition.outputSchema,
1076
+ )
1077
+ : result;
600
1078
  value = await guardValue(value, results, cap);
601
1079
  return {
602
1080
  toolResult: jsonResult({ ok: true, data: value }),
@@ -606,10 +1084,22 @@ export function createMetaTools(
606
1084
  if (resolved.connector.kind === "mcp") {
607
1085
  const mcpResult = result as { content?: TextContent[] };
608
1086
  let content = mcpResult?.content ?? [];
609
- if (fields) content = applyFieldsToContent(content, fields);
1087
+ if (fields) {
1088
+ content = applyFieldsToContent(
1089
+ content,
1090
+ fields,
1091
+ resolved.definition.outputSchema,
1092
+ );
1093
+ }
610
1094
  return { toolResult: await guardContent(content, results, cap) };
611
1095
  }
612
- const value = fields ? applyFields(result, fields) : result;
1096
+ const value = fields
1097
+ ? projectionValue(
1098
+ result,
1099
+ fields,
1100
+ resolved.definition.outputSchema,
1101
+ )
1102
+ : result;
613
1103
  return {
614
1104
  toolResult: await guardText(
615
1105
  serializeResultText(value),
@@ -624,6 +1114,7 @@ export function createMetaTools(
624
1114
  if (!outcome.ok) {
625
1115
  const failedResult =
626
1116
  outcome.error.code === "auth_required" ||
1117
+ outcome.error.code === "invalid_args" ||
627
1118
  outcome.error.code === "input_required_unsupported" ||
628
1119
  call.resultMode === "value"
629
1120
  ? jsonResult({
@@ -636,6 +1127,7 @@ export function createMetaTools(
636
1127
  : errorResult(outcome.error.message);
637
1128
  if (
638
1129
  outcome.error.code === "auth_required" ||
1130
+ outcome.error.code === "invalid_args" ||
639
1131
  outcome.error.code === "input_required_unsupported"
640
1132
  ) {
641
1133
  failedResult.isError = true;
@@ -1131,10 +1623,10 @@ export function createMetaTools(
1131
1623
 
1132
1624
  const LIST_DESC =
1133
1625
  "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.";
1134
- const SEARCH_DESC = `Unknown address: use 2–4 distinctive action/object terms, not the full request; omit limit initially (default ${DEFAULT_SEARCH_LIMIT}) and page only if needed, up to ${MAX_SEARCH_LIMIT}. includeSchemas="compact" adds the input and any declared output shape; matches also carry declared annotations. Call directly when sufficient. Empty query browses all.`;
1626
+ const SEARCH_DESC = `Unknown address: use 2–4 distinctive action/object terms, not the full request; omit limit initially (default ${DEFAULT_SEARCH_LIMIT}) and page only if needed, up to ${MAX_SEARCH_LIMIT}. Partial and no-match searches report term coverage and next-step guidance. safety="readOnly" returns only calls available to call_tool and generated code; "approvalRequired" returns everything else; omitted or "all" preserves the complete catalog. This filters results, not authority. includeSchemas="compact" adds the input and any declared output shape, each bounded; inputSchemaTruncated/outputSchemaTruncated mark shapes that need exact retrieval; matches also carry declared annotations. Call directly when sufficient. Empty query browses all.`;
1135
1627
  const DESCRIBE_DESC = `Only when search_tools omitted schemas, a compact shape is ambiguous, or exact JSON constraints are needed. Inspects up to ${MAX_DESCRIBE_ADDRESSES} addresses with schemas and annotations; "compact" is default, while "json" preserves exact constraints.`;
1136
1628
  const CALL_DESC =
1137
- '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.';
1629
+ '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; any misses return data plus `$connecta` field-projection feedback. resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
1138
1630
  const CALL_DESTRUCTIVE_DESC =
1139
1631
  "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.";
1140
1632
  const GET_RESULT_DESC =
@@ -1159,7 +1651,7 @@ const SKILLS_DESC =
1159
1651
  */
1160
1652
  const CODE_FIRST_SEARCH_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
1161
1653
  const CODE_FIRST_CALL_DESC =
1162
- 'Use for ONE tool explicitly annotated readOnlyHint: true — the cheapest path for a single cold call. For two or more calls, dependent steps, loops, joins, or data reduction use execute_code, whose connecta.call and connecta.batch reach the same tools. 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.';
1654
+ 'Use for ONE tool explicitly annotated readOnlyHint: true — the cheapest path for a single cold call. For two or more calls, dependent steps, loops, joins, or data reduction use execute_code, whose connecta.call and connecta.batch reach the same tools. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths; any misses return data plus `$connecta` field-projection feedback. resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
1163
1655
  const CODE_FIRST_GET_RESULT_DESC =
1164
1656
  "Page a truncated result stashed by call_tool or call_destructive_tool; a program's oversized return is not paged, so reduce it in code instead. 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.";
1165
1657
 
@@ -1304,6 +1796,9 @@ export function registerMetaTools(
1304
1796
  inputSchema: z.object({
1305
1797
  query: z.string().optional(),
1306
1798
  connector: z.string().optional(),
1799
+ safety: z
1800
+ .enum(["readOnly", "approvalRequired", "all"])
1801
+ .optional(),
1307
1802
  limit: z.number().int().positive().max(MAX_SEARCH_LIMIT).optional(),
1308
1803
  offset: z.number().int().nonnegative().optional(),
1309
1804
  fullDescriptions: z.boolean().optional(),
package/src/skills.ts CHANGED
@@ -60,7 +60,7 @@ Use exact addresses returned by discovery; never invent one. Search with 2–4 d
60
60
 
61
61
  One async arrow function. The only capabilities are one global per connector (\`<connectorId>.<toolName>(args)\`), the four \`connecta\` functions, and \`console.log\`.
62
62
 
63
- - What exists: \`connecta.search({})\` browses every catalog and \`connecta.search({ connector: "<id>" })\` browses one that inventory is what a program discovers with, and each match carries its \`address\` and annotations.
63
+ - What exists: \`connecta.search({})\` browses every catalog; add \`safety: "readOnly"\` for only calls the program can execute, and \`connector: "<id>"\` to browse one. This filters discovery results, not authority, and each match carries its \`address\` and annotations.
64
64
  - Exact schemas for known addresses: \`connecta.describe({ addresses: [...] })\`; \`format: "json"\` only for exact constraints.
65
65
  - Two to ten independent calls: \`connecta.batch([...])\`. Each outcome is \`{ address, ok: true, data }\` or \`{ address, ok: false, error, errorDetails: { code, retryable } }\`, which is also how a program tells a policy refusal from a transient failure.
66
66
  - Search inside the run rather than searching first, and return only the reduction the answer needs — never raw payloads.
@@ -0,0 +1,15 @@
1
+ import type { ToolDef } from "./types.js";
2
+
3
+ /**
4
+ * The one fail-closed classification shared by discovery and invocation.
5
+ *
6
+ * A tool is read-only only when it says so without also saying it is
7
+ * destructive. Missing, false, and contradictory annotations all require the
8
+ * approval-visible call path.
9
+ */
10
+ export function isExplicitlyReadOnly(definition: ToolDef): boolean {
11
+ return (
12
+ definition.annotations?.readOnlyHint === true &&
13
+ definition.annotations?.destructiveHint !== true
14
+ );
15
+ }