@zackbart/connecta 0.24.3 → 0.25.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 (145) hide show
  1. package/AGENTS.md +58 -30
  2. package/CHANGELOG.md +317 -1
  3. package/README.md +5 -6
  4. package/dist/advertised-schema.js +53 -0
  5. package/dist/auth/downstream-oauth.js +490 -106
  6. package/dist/branding.d.ts +31 -2
  7. package/dist/branding.js +116 -8
  8. package/dist/call-admission.js +139 -113
  9. package/dist/catalog-service.js +141 -105
  10. package/dist/catalog.js +258 -33
  11. package/dist/concurrency.js +24 -23
  12. package/dist/connector-scope.js +6 -37
  13. package/dist/connectors/api.d.ts +3 -3
  14. package/dist/connectors/api.js +10 -2
  15. package/dist/connectors/guarded-fetch.js +34 -4
  16. package/dist/connectors/remote-mcp.d.ts +6 -5
  17. package/dist/connectors/remote-mcp.js +325 -245
  18. package/dist/credential-contract.d.ts +9 -0
  19. package/dist/credential-rules.js +2 -2
  20. package/dist/credentials.d.ts +6 -1
  21. package/dist/credentials.js +45 -3
  22. package/dist/errors.d.ts +11 -3
  23. package/dist/errors.js +2 -1
  24. package/dist/execute.js +363 -257
  25. package/dist/executor-admission.js +67 -50
  26. package/dist/executors/quickjs-runtime.js +4 -2
  27. package/dist/executors/quickjs.js +251 -210
  28. package/dist/fix-prompt.js +37 -0
  29. package/dist/index.d.ts +11 -2
  30. package/dist/index.js +63 -21
  31. package/dist/invocation.js +324 -285
  32. package/dist/meta-tools.js +304 -114
  33. package/dist/oauth-callback-outcome.js +94 -0
  34. package/dist/oauth-sealing.js +29 -0
  35. package/dist/operator-ui/fix-prompts.js +87 -0
  36. package/dist/operator-ui/generated.js +2 -2
  37. package/dist/operator-ui/model.d.ts +46 -1
  38. package/dist/operator-ui/setup-commands.js +53 -0
  39. package/dist/operator-ui/view.js +109 -2
  40. package/dist/program-source.js +205 -0
  41. package/dist/providers/cloudflare.d.ts +14 -2
  42. package/dist/providers/cloudflare.js +107 -16
  43. package/dist/providers/linear.d.ts +26 -4
  44. package/dist/providers/linear.js +19 -4
  45. package/dist/providers/mixpanel.d.ts +16 -3
  46. package/dist/providers/mixpanel.js +13 -2
  47. package/dist/providers/notion.d.ts +8 -1
  48. package/dist/providers/notion.js +85 -12
  49. package/dist/providers/revenuecat.d.ts +30 -4
  50. package/dist/providers/revenuecat.js +70 -5
  51. package/dist/providers/stripe.d.ts +10 -3
  52. package/dist/providers/stripe.js +30 -4
  53. package/dist/providers/vercel.js +12 -2
  54. package/dist/registry.d.ts +13 -5
  55. package/dist/registry.js +317 -258
  56. package/dist/routes/activity.js +120 -137
  57. package/dist/routes/credentials.js +174 -163
  58. package/dist/routes/mcp.js +129 -70
  59. package/dist/routes/oauth-management.js +60 -71
  60. package/dist/routes/oauth.js +44 -16
  61. package/dist/routes/operator.js +82 -0
  62. package/dist/routes/shared.d.ts +2 -0
  63. package/dist/routes/ui-data.js +210 -0
  64. package/dist/routes/ui.js +64 -32
  65. package/dist/runtime/admission.js +43 -0
  66. package/dist/runtime/call-admission.js +67 -0
  67. package/dist/runtime/connector-scope.js +72 -0
  68. package/dist/runtime/run.js +174 -0
  69. package/dist/runtime/services.js +130 -0
  70. package/dist/runtime/storage.js +66 -0
  71. package/dist/server.js +111 -89
  72. package/dist/skills.js +1 -1
  73. package/dist/storage/file.js +39 -2
  74. package/dist/storage/memory.js +28 -16
  75. package/dist/timeout.js +17 -47
  76. package/dist/types.d.ts +64 -7
  77. package/dist/ui.d.ts +16 -2
  78. package/dist/ui.js +68 -176
  79. package/dist/url-safety.js +157 -0
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/documentation/architecture.md +368 -182
  83. package/documentation/auth.md +277 -176
  84. package/documentation/code-mode.md +479 -329
  85. package/documentation/meta-tools.md +494 -417
  86. package/ethos.md +44 -47
  87. package/examples/worker/AGENTS.md +2 -1
  88. package/examples/worker/README.md +62 -14
  89. package/examples/worker/src/d1-storage.ts +116 -0
  90. package/examples/worker/src/index.ts +18 -21
  91. package/examples/worker/wrangler.jsonc +16 -4
  92. package/package.json +11 -4
  93. package/templates/node/.env.example +3 -3
  94. package/templates/node/AGENTS.md +5 -4
  95. package/templates/node/README.md +2 -1
  96. package/templates/node/package.json +1 -1
  97. package/templates/node/src/index.ts +23 -22
  98. package/dist/auth/downstream-oauth.d.ts +0 -200
  99. package/dist/catalog-fingerprint.d.ts +0 -19
  100. package/dist/catalog-limits.d.ts +0 -9
  101. package/dist/catalog-service.d.ts +0 -263
  102. package/dist/catalog.d.ts +0 -87
  103. package/dist/concurrency.d.ts +0 -3
  104. package/dist/connectors/guarded-fetch.d.ts +0 -92
  105. package/dist/credential-rules.d.ts +0 -85
  106. package/dist/execute.d.ts +0 -154
  107. package/dist/executor-result.d.ts +0 -10
  108. package/dist/executors/quickjs-child.d.ts +0 -1
  109. package/dist/executors/quickjs-protocol.d.ts +0 -67
  110. package/dist/favicon.d.ts +0 -1
  111. package/dist/identity.d.ts +0 -4
  112. package/dist/invocation.d.ts +0 -74
  113. package/dist/mcp-result.d.ts +0 -8
  114. package/dist/meta-tools.d.ts +0 -129
  115. package/dist/operator-ui/generated.d.ts +0 -2
  116. package/dist/operator-ui/view.d.ts +0 -151
  117. package/dist/result-shapes.d.ts +0 -13
  118. package/dist/retired-toolkits.d.ts +0 -2
  119. package/dist/routes/activity.d.ts +0 -2
  120. package/dist/routes/credentials.d.ts +0 -2
  121. package/dist/routes/mcp.d.ts +0 -10
  122. package/dist/routes/oauth-management.d.ts +0 -2
  123. package/dist/routes/oauth.d.ts +0 -2
  124. package/dist/routes/ui.d.ts +0 -2
  125. package/dist/server.d.ts +0 -9
  126. package/dist/skills.d.ts +0 -44
  127. package/dist/timeout.d.ts +0 -21
  128. package/dist/tool-safety.d.ts +0 -9
  129. package/documentation/call-admission.md +0 -158
  130. package/documentation/cloudflare.md +0 -471
  131. package/documentation/connector-guides.md +0 -176
  132. package/documentation/connectors.md +0 -431
  133. package/documentation/linear.md +0 -193
  134. package/documentation/mixpanel.md +0 -160
  135. package/documentation/notion.md +0 -308
  136. package/documentation/operations.md +0 -359
  137. package/documentation/operator-ui.md +0 -135
  138. package/documentation/optional-modules-upgrade.md +0 -243
  139. package/documentation/provider-conventions.md +0 -729
  140. package/documentation/request-admission.md +0 -204
  141. package/documentation/revenuecat.md +0 -305
  142. package/documentation/storage-and-credentials.md +0 -254
  143. package/documentation/stripe.md +0 -262
  144. package/documentation/upgrading.md +0 -768
  145. package/documentation/vercel.md +0 -241
package/dist/catalog.js CHANGED
@@ -35,11 +35,29 @@ class SchemaWork {
35
35
  const visit = this.visit.bind(this);
36
36
  const text = this.text.bind(this);
37
37
  const ancestors = [];
38
+ let serializedBytes = 0;
39
+ const addSerialized = (value) => {
40
+ serializedBytes += schemaEncoder.encode(value).length;
41
+ if (serializedBytes > this.byteLimit)
42
+ throw schemaSizeExceeded;
43
+ };
38
44
  return this.text(JSON.stringify(value, function (key, item) {
39
45
  visit();
40
46
  text(key);
41
47
  if (typeof item === "string")
42
48
  text(item);
49
+ const omitted = item === undefined || typeof item === "function" || typeof item === "symbol";
50
+ const root = key === "" && ancestors.length === 0;
51
+ if (!omitted) {
52
+ // Array indexes are implicit in JSON. Object keys and primitive values
53
+ // are the useful lower bound; the final text check remains authoritative
54
+ // for braces, commas, and values whose encoding is larger than this bound.
55
+ if (!root && !Array.isArray(this))
56
+ addSerialized(JSON.stringify(key));
57
+ if (typeof item !== "object" || item === null) {
58
+ addSerialized(JSON.stringify(item));
59
+ }
60
+ }
43
61
  if (item !== null && typeof item === "object") {
44
62
  while (ancestors.length && ancestors[ancestors.length - 1] !== this) {
45
63
  ancestors.pop();
@@ -481,17 +499,114 @@ function renderConstraints(base, schema, byteLimit, onTruncated) {
481
499
  ? base
482
500
  : `${grouped(base)} /* ${kept.join("; ")} */`;
483
501
  }
502
+ /**
503
+ * `grouped` for the TypeScript dialect, whose JSDoc prose and string literals
504
+ * may carry unbalanced brackets from a downstream description: skipping both
505
+ * keeps a stray `)` in prose from hiding a real top-level union.
506
+ */
507
+ function groupedType(part) {
508
+ let nesting = 0;
509
+ for (let i = 0; i < part.length; i += 1) {
510
+ const char = part[i];
511
+ if (part.startsWith("/*", i)) {
512
+ const end = part.indexOf("*/", i + 2);
513
+ if (end < 0)
514
+ break;
515
+ i = end + 1;
516
+ }
517
+ else if (char === '"') {
518
+ for (i += 1; i < part.length && part[i] !== '"'; i += 1) {
519
+ if (part[i] === "\\")
520
+ i += 1;
521
+ }
522
+ }
523
+ else if (char === "{" || char === "(" || char === "[" || char === "<") {
524
+ nesting += 1;
525
+ }
526
+ else if (char === "}" || char === ")" || char === "]" || char === ">") {
527
+ nesting -= 1;
528
+ }
529
+ else if (nesting === 0 &&
530
+ (part.startsWith(" | ", i) || part.startsWith(" & ", i))) {
531
+ return `(${part})`;
532
+ }
533
+ }
534
+ return part;
535
+ }
536
+ const TYPESCRIPT_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
537
+ function typescriptKey(key) {
538
+ return TYPESCRIPT_IDENTIFIER.test(key) ? key : JSON.stringify(key);
539
+ }
540
+ /** One-line JSDoc body; downstream prose can never close the comment early. */
541
+ function typescriptDoc(text) {
542
+ return text.replace(/\s+/g, " ").trim().replaceAll("*/", "*\\/");
543
+ }
544
+ function typescriptPrimitive(type) {
545
+ if (type === "integer")
546
+ return "number";
547
+ if (type === "array")
548
+ return "unknown[]";
549
+ if (type === "object")
550
+ return "Record<string, unknown>";
551
+ return ["string", "number", "boolean", "null"].includes(type)
552
+ ? type
553
+ : "unknown";
554
+ }
555
+ function boundedParts(work, separator, prefix, suffix) {
556
+ // Check each addition before joining so a rejected schema never creates a
557
+ // large intermediate string just to discover that the final result is over.
558
+ const parts = [];
559
+ let bytes = schemaEncoder.encode(prefix).length + schemaEncoder.encode(suffix).length;
560
+ const separatorBytes = schemaEncoder.encode(separator).length;
561
+ return {
562
+ get length() {
563
+ return parts.length;
564
+ },
565
+ add(part) {
566
+ const partBytes = schemaEncoder.encode(part).length;
567
+ const added = partBytes + (parts.length > 0 ? separatorBytes : 0);
568
+ if (bytes + added > work.byteLimit)
569
+ throw schemaSizeExceeded;
570
+ parts.push(part);
571
+ bytes += added;
572
+ },
573
+ finish() {
574
+ return `${prefix}${parts.join(separator)}${suffix}`;
575
+ },
576
+ };
577
+ }
484
578
  function renderSchema(schema, defs, seen, depth, options) {
485
579
  options.work.visit();
486
580
  return options.work.text(renderSchemaNode(schema, defs, seen, depth, options));
487
581
  }
488
582
  function renderSchemaNode(schema, defs, seen, depth, options) {
489
- if (depth > 4)
490
- return "…";
583
+ const ts = options.typescript === true;
584
+ const group = ts ? groupedType : grouped;
585
+ if (depth > 4) {
586
+ if (!ts)
587
+ return "…";
588
+ options.work.truncated = true;
589
+ return `unknown${COMPACT_DISCOVERY_TRUNCATION}`;
590
+ }
491
591
  if (schema === null || typeof schema !== "object") {
592
+ if (ts)
593
+ return schema === false ? "never" : "unknown";
492
594
  return options.work.json(schema);
493
595
  }
494
596
  const s = schema;
597
+ // OpenAPI 3.0's `nullable` has no JSON Schema meaning, so compact ignores
598
+ // it; a TypeScript reader writing `null` checks needs it in the type.
599
+ if (ts && s.nullable === true) {
600
+ const base = Object.create(null);
601
+ for (const key of propertyNames(s, options.work)) {
602
+ if (key !== "nullable")
603
+ base[key] = s[key];
604
+ }
605
+ const rendered = declaresShape(base)
606
+ ? renderSchema(base, defs, seen, depth, options)
607
+ : "unknown";
608
+ return rendered === "unknown" ? rendered : `${rendered} | null`;
609
+ }
495
610
  const constrain = (rendered) => options.renderConstraints
496
611
  ? renderConstraints(rendered, s, options.constraintByteLimit, options.onConstraintTruncated)
497
612
  : rendered;
@@ -524,29 +639,41 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
524
639
  if (key !== "allOf")
525
640
  own[key] = s[key];
526
641
  }
527
- const parts = declaresShape(own)
528
- ? [renderSchema(own, defs, seen, depth, options)]
529
- : [];
642
+ const parts = boundedParts(options.work, " & ", "", "");
643
+ const groupParts = declaresShape(own)
644
+ ? s.allOf.length > 0
645
+ : s.allOf.length > 1;
646
+ if (declaresShape(own)) {
647
+ const rendered = renderSchema(own, defs, seen, depth, options);
648
+ parts.add(groupParts ? group(rendered) : rendered);
649
+ }
530
650
  for (const member of s.allOf) {
531
- parts.push(renderSchema(member, defs, seen, depth + 1, options));
651
+ const rendered = renderSchema(member, defs, seen, depth + 1, options);
652
+ parts.add(groupParts ? group(rendered) : rendered);
532
653
  }
533
654
  if (parts.length === 0)
534
655
  return "unknown";
535
- if (parts.length === 1)
536
- return parts[0];
537
- return parts.map(grouped).join(" & ");
656
+ return parts.finish();
538
657
  }
539
658
  const reference = s.$ref ?? s.$dynamicRef;
540
659
  if (typeof reference === "string") {
541
660
  const dynamic = s.$ref === undefined;
542
661
  const rawName = refName(options.work.text(reference));
543
662
  const name = dynamic ? rawName.replace(/^#/, "") : rawName;
544
- if (seen.has(name))
545
- return name;
663
+ if (seen.has(name)) {
664
+ if (!ts)
665
+ return name;
666
+ // A cycle has no finite TypeScript expansion, and a bare definition
667
+ // name would be an undeclared type: degrade and send callers to JSON.
668
+ options.work.truncated = true;
669
+ return "unknown /* recursive */";
670
+ }
546
671
  const target = resolveDefinition(defs, name);
547
672
  if (target === undefined) {
548
- if (dynamic)
673
+ if (dynamic || ts)
549
674
  options.work.truncated = true;
675
+ if (ts)
676
+ return "unknown /* unresolved */";
550
677
  return dynamic ? "unknown" : name;
551
678
  }
552
679
  const cacheKey = JSON.stringify([name, depth, [...seen]]);
@@ -561,10 +688,11 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
561
688
  }
562
689
  const union = (s.oneOf ?? s.anyOf);
563
690
  if (Array.isArray(union)) {
564
- const rendered = union
565
- .map((u) => renderSchema(u, defs, seen, depth + 1, options))
566
- .join(" | ") ||
567
- "unknown";
691
+ const parts = boundedParts(options.work, " | ", "", "");
692
+ for (const member of union) {
693
+ parts.add(renderSchema(member, defs, seen, depth + 1, options));
694
+ }
695
+ const rendered = parts.finish() || "unknown";
568
696
  return constrain(rendered);
569
697
  }
570
698
  if (Array.isArray(s.enum)) {
@@ -580,21 +708,50 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
580
708
  return constrain(rendered);
581
709
  }
582
710
  const type = s.type;
711
+ // A type list renders member by member, so `["object", "null"]` beside
712
+ // `properties` keeps both its shape and its null rather than the object alone.
713
+ if (ts && Array.isArray(type) && type.length > 0) {
714
+ const members = new Set();
715
+ const parts = boundedParts(options.work, " | ", "", "");
716
+ for (const item of type) {
717
+ options.work.visit();
718
+ if (typeof item !== "string")
719
+ continue;
720
+ let rendered = "null";
721
+ if (item !== "null") {
722
+ const member = Object.create(null);
723
+ for (const key of propertyNames(s, options.work)) {
724
+ if (key !== "type")
725
+ member[key] = s[key];
726
+ }
727
+ member.type = item;
728
+ rendered = renderSchema(member, defs, seen, depth, options);
729
+ }
730
+ if (members.has(rendered))
731
+ continue;
732
+ members.add(rendered);
733
+ parts.add(rendered);
734
+ }
735
+ return parts.length === 0 ? "unknown" : parts.finish();
736
+ }
583
737
  if (Array.isArray(s.prefixItems)) {
584
- const parts = s.prefixItems.map((item) => renderSchema(item, defs, seen, depth + 1, options));
738
+ const parts = boundedParts(options.work, ", ", "[", "]");
739
+ for (const item of s.prefixItems) {
740
+ parts.add(renderSchema(item, defs, seen, depth + 1, options));
741
+ }
585
742
  if (s.items !== false) {
586
743
  const rest = s.items === undefined || s.items === true
587
744
  ? "unknown"
588
745
  : renderSchema(s.items, defs, seen, depth + 1, options);
589
- parts.push(`...${grouped(rest)}[]`);
746
+ parts.add(`...${group(rest)}[]`);
590
747
  }
591
- return `[${parts.join(", ")}]`;
748
+ return parts.finish();
592
749
  }
593
750
  if (type === "array" || s.items) {
594
751
  const items = s.items
595
752
  ? renderSchema(s.items, defs, seen, depth + 1, options)
596
753
  : "unknown";
597
- return `${items}[]`;
754
+ return ts ? `${groupedType(items)}[]` : `${items}[]`;
598
755
  }
599
756
  if (type === "object" || s.properties) {
600
757
  const props = (s.properties ?? {});
@@ -606,10 +763,17 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
606
763
  ...declaredKeys.filter((key) => !required.has(key)),
607
764
  ]
608
765
  : declaredKeys;
609
- if (keys.length === 0)
610
- return "{}";
611
- return `{ ${keys
612
- .map((key) => {
766
+ const additional = s.additionalProperties;
767
+ const index = ts && additional !== null && typeof additional === "object"
768
+ ? renderSchema(additional, defs, seen, depth + 1, options)
769
+ : undefined;
770
+ if (keys.length === 0 && index === undefined) {
771
+ // In TypeScript `{}` reads as "empty"; an object with no declared
772
+ // properties is an open map unless it closes itself explicitly.
773
+ return ts && additional !== false ? "Record<string, unknown>" : "{}";
774
+ }
775
+ const parts = boundedParts(options.work, ts ? "; " : ", ", "{ ", " }");
776
+ for (const key of keys) {
613
777
  const optional = required.has(key) ? "" : "?";
614
778
  const rendered = renderSchema(props[key], defs, seen, depth + 1, options);
615
779
  const description = props[key]?.description;
@@ -617,27 +781,40 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
617
781
  options.work.text(description);
618
782
  }
619
783
  options.work.text(key);
784
+ if (ts) {
785
+ const doc = options.propertyDescriptions && typeof description === "string" &&
786
+ description.trim()
787
+ ? `/** ${typescriptDoc(description)} */ `
788
+ : "";
789
+ parts.add(`${doc}${typescriptKey(key)}${optional}: ${rendered}`);
790
+ continue;
791
+ }
620
792
  const comment = options.propertyDescriptions && typeof description === "string"
621
793
  ? ` // ${description}`
622
794
  : "";
623
- return `${key}${optional}: ${rendered}${comment}`;
624
- })
625
- .join(", ")} }`;
795
+ parts.add(`${key}${optional}: ${rendered}${comment}`);
796
+ }
797
+ if (index !== undefined)
798
+ parts.add(`[key: string]: ${index}`);
799
+ return parts.finish();
626
800
  }
627
801
  if (typeof type === "string") {
628
- return constrain(options.work.text(type));
802
+ const text = options.work.text(type);
803
+ return constrain(ts ? typescriptPrimitive(text) : text);
629
804
  }
630
805
  if (Array.isArray(type)) {
631
- const rendered = type.map((item) => {
806
+ const parts = boundedParts(options.work, " | ", "", "");
807
+ for (const item of type) {
632
808
  options.work.visit();
633
- return options.work.text(String(item));
634
- }).join(" | ");
809
+ parts.add(options.work.text(String(item)));
810
+ }
811
+ const rendered = parts.finish();
635
812
  return constrain(rendered);
636
813
  }
637
814
  if (options.renderConstraints && constraintEntries(s).length > 0) {
638
815
  return renderConstraints("unknown", s, options.constraintByteLimit, options.onConstraintTruncated);
639
816
  }
640
- return options.work.json(schema);
817
+ return ts ? "unknown" : options.work.json(schema);
641
818
  }
642
819
  const compactSchemas = new WeakMap();
643
820
  function resolveDefinition(schema, name) {
@@ -713,10 +890,58 @@ export function compactDiscoverySchema(schema) {
713
890
  compactDiscoverySchemas.set(schema, result);
714
891
  return result;
715
892
  }
716
- function boundedCompactSchema(schema, description) {
893
+ const typescriptDiscoveryTypes = new WeakMap();
894
+ const typescriptDescriptionTypes = new WeakMap();
895
+ /**
896
+ * One schema as a TypeScript type, under exactly the compact budgets: search
897
+ * drops prose and orders required keys first within 1,024 bytes, describe
898
+ * keeps prose as JSDoc within 8,192, and both share the 2,000-visit walk.
899
+ */
900
+ function typescriptType(schema, description) {
901
+ const cache = description ? typescriptDescriptionTypes : typescriptDiscoveryTypes;
902
+ const cached = cache.get(schema);
903
+ if (cached)
904
+ return cached;
905
+ const result = boundedCompactSchema(schema, description, true);
906
+ cache.set(schema, result);
907
+ return result;
908
+ }
909
+ /** Marks runtime evidence inside the signature itself, not only beside it. */
910
+ const OBSERVED_OUTPUT_MARKER = "/* observed, not declared */ ";
911
+ /**
912
+ * Render a tool as the function `connecta.call(address, args)` resolves to,
913
+ * for an agent to read — never to execute: code mode still runs JavaScript.
914
+ *
915
+ * The input half comes from the declared input schema. The output half is the
916
+ * declared output schema, else a passively observed shape carrying a leading
917
+ * marker (runtime evidence is never presented as a contract), else `unknown`.
918
+ * A zero-field input renders as the optional `args?: {}` that call accepts:
919
+ * an MCP tool with no parameters rarely closes its schema, and reading its
920
+ * open `Record<string, unknown>` as "pass anything" would invite guessing.
921
+ */
922
+ export function typescriptSignature(input, output, options) {
923
+ const renderedInput = typescriptType(input, options.description);
924
+ const renderedOutput = output
925
+ ? typescriptType(output, options.description)
926
+ : undefined;
927
+ const args = renderedInput.text === "{}" ||
928
+ renderedInput.text === "Record<string, unknown>"
929
+ ? "args?: {}"
930
+ : `args: ${renderedInput.text}`;
931
+ const result = renderedOutput
932
+ ? `${options.observed ? OBSERVED_OUTPUT_MARKER : ""}${renderedOutput.text}`
933
+ : "unknown";
934
+ return {
935
+ text: `(${args}) => Promise<${result}>`,
936
+ inputTruncated: renderedInput.truncated,
937
+ outputTruncated: renderedOutput?.truncated === true,
938
+ };
939
+ }
940
+ function boundedCompactSchema(schema, description, typescript = false) {
717
941
  const work = new SchemaWork(description ? MAX_COMPACT_DESCRIPTION_SCHEMA_BYTES : MAX_COMPACT_DISCOVERY_SCHEMA_BYTES);
718
942
  const options = {
719
943
  work,
944
+ typescript,
720
945
  propertyDescriptions: description,
721
946
  requiredFirst: !description,
722
947
  enumByteLimit: description ? work.byteLimit : MAX_COMPACT_DISCOVERY_ENUM_BYTES,
@@ -1,30 +1,31 @@
1
+ import { Effect } from "effect";
2
+ import { runEdge } from "./runtime/run.js";
1
3
  const DEFAULT_DISCOVERY_CONCURRENCY = 4;
2
4
  export function resolveDiscoveryConcurrency(value) {
3
5
  return value !== undefined && Number.isInteger(value) && value > 0
4
6
  ? value
5
7
  : DEFAULT_DISCOVERY_CONCURRENCY;
6
8
  }
7
- /** Run `fn` over `items` with at most `limit` operations in flight. */
8
- export async function mapSettledWithConcurrency(items, limit, fn) {
9
- const settled = Array(items.length);
10
- const remaining = items.entries();
11
- const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
12
- for (;;) {
13
- const next = remaining.next();
14
- if (next.done)
15
- return;
16
- const [index, item] = next.value;
17
- try {
18
- settled[index] = {
19
- status: "fulfilled",
20
- value: await fn(item, index),
21
- };
22
- }
23
- catch (reason) {
24
- settled[index] = { status: "rejected", reason };
25
- }
26
- }
27
- });
28
- await Promise.all(workers);
29
- return settled;
9
+ /**
10
+ * Run `fn` over `items` with at most `limit` operations in flight, settling
11
+ * every one: a rejection lands in its slot instead of cancelling the rest.
12
+ * Results keep input order whatever order the work finishes in. A limit below
13
+ * one runs the items one at a time.
14
+ */
15
+ export function mapSettledWithConcurrency(items, limit, fn) {
16
+ return runEdge(Effect.forEach(items, (item, index) => Effect.tryPromise({
17
+ // Promise.resolve keeps a non-promise return from `fn` working, as
18
+ // `await` did; a synchronous throw is caught either way.
19
+ try: () => Promise.resolve(fn(item, index)),
20
+ catch: (reason) => reason,
21
+ }).pipe(Effect.match({
22
+ onSuccess: (value) => ({
23
+ status: "fulfilled",
24
+ value,
25
+ }),
26
+ onFailure: (reason) => ({
27
+ status: "rejected",
28
+ reason,
29
+ }),
30
+ })), { concurrency: limit >= 1 ? Math.floor(limit) : 1 }));
30
31
  }
@@ -1,19 +1,5 @@
1
- import { sleep } from "./timeout.js";
2
- /** Enough for local transport abort/close without letting cleanup own latency. */
3
- const CONNECTOR_SCOPE_CLOSE_BUDGET_MS = 100;
4
- /**
5
- * Bound on cleanup continued after the caller-facing window expires.
6
- *
7
- * `remoteMcp` spends at most one second asking the downstream to terminate its
8
- * session, leaving another second for the local close. Custom hooks still get a
9
- * finite background window: handing a never-settling promise to a Worker's
10
- * `waitUntil` would otherwise keep the invocation alive until the platform cap.
11
- */
12
- const CONNECTOR_SCOPE_DEFER_BUDGET_MS = 2_000;
13
- /** Resolve when `work` settles or `budgetMs` expires; never reject. */
14
- function waitAtMost(work, budgetMs) {
15
- return Promise.race([work, sleep(budgetMs)]).then(() => { });
16
- }
1
+ import { closeScope } from "./runtime/connector-scope.js";
2
+ import { runEdge } from "./runtime/run.js";
17
3
  /**
18
4
  * Tell a connector that a scope owned by the core has ended.
19
5
  *
@@ -25,25 +11,8 @@ function waitAtMost(work, budgetMs) {
25
11
  * window. Callers own the at-most-once guarantee and must not use the scope
26
12
  * again after this returns.
27
13
  */
28
- export async function closeConnectorScope(connector, ctx, defer) {
29
- try {
30
- const closing = connector.closeScope?.(ctx);
31
- if (!closing)
32
- return;
33
- // Attach both handlers before either timer can win, so a late rejection is
34
- // consumed rather than becoming an unhandled rejection.
35
- const settled = closing.then(() => { }, () => { });
36
- if (defer) {
37
- try {
38
- defer(waitAtMost(settled, CONNECTOR_SCOPE_DEFER_BUDGET_MS));
39
- }
40
- catch {
41
- // A runtime hook is best-effort too; the caller cap still applies.
42
- }
43
- }
44
- await waitAtMost(settled, CONNECTOR_SCOPE_CLOSE_BUDGET_MS);
45
- }
46
- catch {
47
- // The scope is over whether or not the connector managed to clean it up.
48
- }
14
+ export function closeConnectorScope(connector, ctx, defer) {
15
+ // The Promise face of closeScope (src/runtime/connector-scope.ts), which
16
+ // owns the budgets; an Effect caller registers it as a Scope finalizer.
17
+ return runEdge(closeScope(connector, ctx, defer));
49
18
  }
@@ -52,9 +52,9 @@ export interface ApiOptions {
52
52
  * discovery metadata. See `Connector.usageGuide`.
53
53
  */
54
54
  usageGuide?: string | ConnectorUsageGuide;
55
- /** Optional operator-managed credential exposed through ctx.credential and /credentials. */
55
+ /** Optional operator-managed credential exposed through ctx.credential and the connection in the operator UI. */
56
56
  credential?: ConnectorCredentialConfig;
57
- /** Optional validation behind /credentials' Test action. */
57
+ /** Optional validation behind the connection's Test action in the operator UI. */
58
58
  testCredential?: (value: string, ctx: ConnectorContext) => Promise<CredentialTestResult>;
59
59
  /** Optional validation for named multi-field credentials. */
60
60
  testCredentials?: (values: ConnectorCredentialValues, ctx: ConnectorContext) => Promise<CredentialTestResult>;
@@ -68,5 +68,5 @@ export interface ApiOptions {
68
68
  validateArgs?: boolean;
69
69
  tools: ApiTool[];
70
70
  }
71
- /** A static connector; see provider conventions' two construction-time checks. */
71
+ /** A static connector; every tool passes {@link checkToolContract} first. */
72
72
  export declare function api(id: string, opts: ApiOptions): Connector;
@@ -3,7 +3,15 @@ import { compileValidator, validateToolInput } from "../validate.js";
3
3
  export function defined(value) {
4
4
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
5
5
  }
6
- /** Enforce provider conventions' two construction-time checks. */
6
+ /**
7
+ * Three things a hand-written surface is refused for at construction rather
8
+ * than in production (#340): no description (discovery has nothing to route on
9
+ * and a guess costs a call), no explicit `readOnlyHint` (connecta never infers
10
+ * the safety class, so an unclassified tool is a deployment bug), and an
11
+ * `inputSchema` the validator cannot compile (declaring one is optional;
12
+ * declaring an unenforceable one is not). None of this reaches a proxied
13
+ * catalog — the contract binds the surfaces we write, not the ones we relay.
14
+ */
7
15
  function checkToolContract(id, tool) {
8
16
  const address = `${id}.${tool.name}`;
9
17
  if (typeof tool.description !== "string" || tool.description.trim() === "") {
@@ -20,7 +28,7 @@ function checkToolContract(id, tool) {
20
28
  if (tool.inputSchema)
21
29
  compileValidator(tool.inputSchema, { address });
22
30
  }
23
- /** A static connector; see provider conventions' two construction-time checks. */
31
+ /** A static connector; every tool passes {@link checkToolContract} first. */
24
32
  export function api(id, opts) {
25
33
  for (const t of opts.tools)
26
34
  checkToolContract(id, t);
@@ -1,4 +1,17 @@
1
- /** See documentation/connectors.md#the-guarded-fetch-transport. Web APIs only. */
1
+ /**
2
+ * The safety machinery every hand-written HTTP surface needs, extracted once
3
+ * (#341): path confinement, encoded query and framed bodies, per-request
4
+ * credential headers applied last, a refused redirect, a bounded read, and
5
+ * network-failure normalization. It owns no *meaning* — it never reads a status
6
+ * code and never invents an auth scheme, because the same 403 means an ungranted
7
+ * capability at one provider and a token scope at another, and those want
8
+ * opposite next moves. The provider's `authenticate` supplies headers and its
9
+ * mapper turns one `GuardedResponse` into a result or a typed failure.
10
+ *
11
+ * Not exported this release: the migrations proved the shape preserves behavior
12
+ * for connectors that already had this machinery, not that it is the right
13
+ * public surface. Web APIs only.
14
+ */
2
15
  import { ConnectorCallError, unavailableCallError } from "../errors.js";
3
16
  /** Parse delta-seconds or an HTTP-date into a non-negative wait window. */
4
17
  export function retryAfterMs(headers) {
@@ -123,7 +136,18 @@ async function drain(provider, stream, limit) {
123
136
  }
124
137
  return body;
125
138
  }
126
- /** See documentation/connectors.md#the-guarded-fetch-transport. */
139
+ /**
140
+ * Every read is capped at `maxResponseBytes`: a streaming body is abandoned at
141
+ * the ceiling rather than buffered past it, and where a runtime gives no body
142
+ * stream the text is measured in UTF-8 bytes before it is accepted, with JSON
143
+ * parsed from that same bounded text. Such a runtime still buffers internally,
144
+ * but cannot return an oversized body as a successful result.
145
+ *
146
+ * A mapper must re-throw `ConnectorCallError` and swallow only what it
147
+ * recognizes: a bare `catch` around `response.json()` eats this refusal along
148
+ * with a parse error and turns a response nobody was allowed to read into an
149
+ * empty success.
150
+ */
127
151
  function boundedResponse(provider, response, limit) {
128
152
  if (BODILESS_STATUSES.has(response.status)) {
129
153
  const emptyJson = async () => undefined;
@@ -186,7 +210,11 @@ async function jsonResult(read) {
186
210
  return { parseError: cause };
187
211
  }
188
212
  }
189
- /** Build the guarded transport described in documentation/connectors.md. */
213
+ /**
214
+ * Build the transport a connector sends every request through. `maxResponseBytes`
215
+ * is required rather than defaulted: what counts as an absurd response is a fact
216
+ * about the API, not about HTTP.
217
+ */
190
218
  export function guardedFetch(options) {
191
219
  const { provider, maxResponseBytes: limit } = options;
192
220
  if (!Number.isInteger(limit) || limit < 1) {
@@ -229,7 +257,9 @@ export function guardedFetch(options) {
229
257
  : request.rawBody !== undefined
230
258
  ? { body: request.rawBody }
231
259
  : {}),
232
- // Rationale: documentation/connectors.md#the-guarded-fetch-transport.
260
+ // A redirect is an instruction to re-send the credential to whatever
261
+ // origin `Location` names, and a confinement a redirect can undo was
262
+ // never one. Refused below rather than followed.
233
263
  redirect: "manual",
234
264
  ...(ctx.signal ? { signal: ctx.signal } : {}),
235
265
  });
@@ -2,8 +2,8 @@ import type { FetchLike, Transport } from "@modelcontextprotocol/client";
2
2
  import { ConnectorCallError } from "../errors.js";
3
3
  import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, ConnectorCredentialConfig, ConnectorUsageGuide, Logger } from "../types.js";
4
4
  /**
5
- * A static downstream credential the operator supplies at `/credentials`
6
- * rather than the deployment baking into its source.
5
+ * A static downstream credential supplied through the connection in the
6
+ * operator UI rather than baked into deployment source.
7
7
  *
8
8
  * The connector, its endpoint, and the credential *slot* stay declared in
9
9
  * code; only the secret arrives through the operator route, exactly as for
@@ -15,8 +15,8 @@ import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, Connect
15
15
  interface RemoteMcpCredentialAuth {
16
16
  type: "credential";
17
17
  /**
18
- * Operator-facing slot description rendered on `/credentials`. Defaults to
19
- * `{ label: "API key" }`; a maintained provider passes the name the provider
18
+ * Slot description rendered in the connection in the operator UI. Defaults
19
+ * to `{ label: "API key" }`; a maintained provider passes the name the provider
20
20
  * itself uses. Named `fields` are refused — this shape reads the reserved
21
21
  * `value` field only.
22
22
  */
@@ -122,7 +122,8 @@ export declare class RemoteMcpRedirectError extends ConnectorCallError {
122
122
  * Wrap fetch with explicit, bounded redirect handling.
123
123
  *
124
124
  * The starting URL of each fetch call is trusted by its caller (the configured
125
- * MCP endpoint, or an OAuth URL discovered by the pinned SDK). Only Location
125
+ * MCP endpoint, or an OAuth URL the pinned SDK discovered and
126
+ * `learnedUrlSafeFetch` already admitted). Only Location
126
127
  * values are policy-controlled here. No rejected target is ever fetched, so
127
128
  * arbitrary static header names receive the same protection as Authorization.
128
129
  */