@zackbart/connecta 0.10.5 → 0.11.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 (93) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +150 -0
  3. package/README.md +5 -4
  4. package/bin/connecta.mjs +0 -7
  5. package/dist/activity.d.ts +11 -1
  6. package/dist/activity.d.ts.map +1 -1
  7. package/dist/activity.js +44 -3
  8. package/dist/activity.js.map +1 -1
  9. package/dist/catalog-service.d.ts +24 -0
  10. package/dist/catalog-service.d.ts.map +1 -1
  11. package/dist/catalog-service.js +68 -9
  12. package/dist/catalog-service.js.map +1 -1
  13. package/dist/connectors/api.d.ts +2 -2
  14. package/dist/connectors/remote-mcp.d.ts +1 -1
  15. package/dist/errors.d.ts +49 -4
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +68 -1
  18. package/dist/errors.js.map +1 -1
  19. package/dist/execute.d.ts +73 -3
  20. package/dist/execute.d.ts.map +1 -1
  21. package/dist/execute.js +161 -29
  22. package/dist/execute.js.map +1 -1
  23. package/dist/index.d.ts +28 -30
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +29 -37
  26. package/dist/index.js.map +1 -1
  27. package/dist/invocation.d.ts +9 -2
  28. package/dist/invocation.d.ts.map +1 -1
  29. package/dist/invocation.js +61 -31
  30. package/dist/invocation.js.map +1 -1
  31. package/dist/meta-tools.d.ts +24 -59
  32. package/dist/meta-tools.d.ts.map +1 -1
  33. package/dist/meta-tools.js +107 -359
  34. package/dist/meta-tools.js.map +1 -1
  35. package/dist/operator-ui/generated.d.ts +1 -1
  36. package/dist/operator-ui/generated.d.ts.map +1 -1
  37. package/dist/operator-ui/generated.js +1 -1
  38. package/dist/operator-ui/generated.js.map +1 -1
  39. package/dist/registry.d.ts +12 -10
  40. package/dist/registry.d.ts.map +1 -1
  41. package/dist/registry.js +8 -17
  42. package/dist/registry.js.map +1 -1
  43. package/dist/routes/mcp.d.ts.map +1 -1
  44. package/dist/routes/mcp.js +19 -21
  45. package/dist/routes/mcp.js.map +1 -1
  46. package/dist/routes/shared.d.ts +9 -11
  47. package/dist/routes/shared.d.ts.map +1 -1
  48. package/dist/routes/shared.js.map +1 -1
  49. package/dist/server.js +5 -4
  50. package/dist/server.js.map +1 -1
  51. package/dist/skills.d.ts +8 -18
  52. package/dist/skills.d.ts.map +1 -1
  53. package/dist/skills.js +13 -60
  54. package/dist/skills.js.map +1 -1
  55. package/dist/types.d.ts +6 -20
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/documentation/code-first-exploration.md +16 -16
  60. package/documentation/code-mode.md +137 -63
  61. package/documentation/connectors.md +1 -1
  62. package/documentation/meta-tools.md +96 -33
  63. package/documentation/rich-output-design.md +212 -0
  64. package/ethos.md +17 -19
  65. package/examples/node/README.md +1 -2
  66. package/examples/node/src/index.ts +1 -3
  67. package/examples/worker/README.md +19 -16
  68. package/examples/worker/src/d1-activity-row.ts +40 -0
  69. package/examples/worker/src/d1-activity.ts +3 -2
  70. package/examples/worker/src/index.ts +6 -14
  71. package/examples/worker/wrangler.jsonc +3 -6
  72. package/package.json +1 -1
  73. package/src/activity.ts +69 -3
  74. package/src/catalog-service.ts +113 -20
  75. package/src/connectors/api.ts +2 -2
  76. package/src/connectors/remote-mcp.ts +1 -1
  77. package/src/errors.ts +104 -3
  78. package/src/execute.ts +237 -37
  79. package/src/index.ts +60 -67
  80. package/src/invocation.ts +61 -19
  81. package/src/meta-tools.ts +136 -482
  82. package/src/operator-ui/browser.ts +10 -2
  83. package/src/operator-ui/generated.ts +1 -1
  84. package/src/registry.ts +7 -35
  85. package/src/routes/mcp.ts +19 -21
  86. package/src/routes/shared.ts +8 -11
  87. package/src/server.ts +7 -7
  88. package/src/skills.ts +11 -74
  89. package/src/types.ts +6 -21
  90. package/src/version.ts +1 -1
  91. package/templates/node/README.md +2 -1
  92. package/templates/node/package.json +1 -1
  93. package/templates/node/src/index.ts +1 -1
@@ -1,12 +1,10 @@
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
- import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
4
- import { closeConnectorScope, } from "./connector-scope.js";
5
- import { classifyCallError, messageLooksRetryable, } from "./errors.js";
3
+ import { resolveDiscoveryConcurrency } from "./concurrency.js";
6
4
  import { InvocationService, MAX_RETRY_BACKOFF_MS, retryBackoffMs, } from "./invocation.js";
7
5
  import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
8
6
  import { hasConnectorGuides, listSkills, resolveSkill, } from "./skills.js";
9
- import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, withAbortableTimeout, } from "./timeout.js";
7
+ import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, } from "./timeout.js";
10
8
  export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_RETRY_BACKOFF_MS, MAX_SEARCH_LIMIT, retryBackoffMs, };
11
9
  const RESULT_TTL_SECONDS = 900;
12
10
  const enc = new TextEncoder();
@@ -25,9 +23,6 @@ export function errorResult(message) {
25
23
  function msg(err) {
26
24
  return err instanceof Error ? err.message : String(err);
27
25
  }
28
- function errorDetails(code, message) {
29
- return { code, message, retryable: messageLooksRetryable(message) };
30
- }
31
26
  function discoveryErrorResult(error) {
32
27
  const result = jsonResult({
33
28
  error: {
@@ -606,17 +601,12 @@ async function stashResult(text, results, totalBytes) {
606
601
  resultId: id,
607
602
  totalBytes,
608
603
  hint: "use get_result {id, offset} to page, or re-call with fields to select less",
604
+ nextAction: {
605
+ tool: "get_result",
606
+ arguments: { id, offset: 0 },
607
+ },
609
608
  };
610
609
  }
611
- /** Keep an oversized batch's inline outcome summary at fixed string overhead. */
612
- function batchSummaryString(value) {
613
- const bytes = enc.encode(value);
614
- const maxBytes = 512;
615
- if (bytes.length <= maxBytes)
616
- return value;
617
- const end = alignEndToCharBoundary(bytes, 0, maxBytes, bytes.length);
618
- return `${dec.decode(bytes.slice(0, end))}…`;
619
- }
620
610
  /**
621
611
  * Return `text` as a single content block; if it exceeds `cap` bytes, stash the
622
612
  * full text and return the first `cap` bytes followed by a JSON truncation
@@ -625,12 +615,18 @@ function batchSummaryString(value) {
625
615
  */
626
616
  async function guardEncoded(text, bytes, results, cap) {
627
617
  if (bytes.length <= cap) {
628
- return { content: [{ type: "text", text }] };
618
+ return {
619
+ result: { content: [{ type: "text", text }] },
620
+ truncated: false,
621
+ };
629
622
  }
630
623
  const notice = await stashResult(text, results, bytes.length);
631
624
  const head = dec.decode(bytes.slice(0, alignEndToCharBoundary(bytes, 0, cap, bytes.length)));
632
625
  return {
633
- content: [{ type: "text", text: `${head}\n${JSON.stringify(notice)}` }],
626
+ result: {
627
+ content: [{ type: "text", text: `${head}\n${JSON.stringify(notice)}` }],
628
+ },
629
+ truncated: true,
634
630
  };
635
631
  }
636
632
  /** {@link guardEncoded} over a string that has not been measured yet. */
@@ -648,8 +644,11 @@ async function guardValue(value, results, cap) {
648
644
  const text = serializeResultText(value);
649
645
  const bytes = enc.encode(text);
650
646
  if (bytes.length <= cap)
651
- return value;
652
- return stashResult(text, results, bytes.length);
647
+ return { result: value, truncated: false };
648
+ return {
649
+ result: await stashResult(text, results, bytes.length),
650
+ truncated: true,
651
+ };
653
652
  }
654
653
  /**
655
654
  * Bound a downstream MCP `content` array by `cap`, measuring the serialized
@@ -679,37 +678,48 @@ async function guardContent(content, results, cap) {
679
678
  // be measured, stashed, or paged either — there is nothing this guard could
680
679
  // do with it. Pass it through as the old text-only measure did, rather than
681
680
  // turning a call that used to succeed into result_processing_failed.
682
- return { content };
681
+ return { result: { content }, truncated: false };
683
682
  }
684
683
  const bytes = enc.encode(text);
685
684
  // Under the cap the downstream blocks pass through untouched, non-text ones
686
685
  // included, in their original order.
687
- if (bytes.length <= cap)
688
- return { content };
686
+ if (bytes.length <= cap) {
687
+ return { result: { content }, truncated: false };
688
+ }
689
689
  if (content.every((b) => b.type === "text")) {
690
690
  return guardEncoded(text, bytes, results, cap);
691
691
  }
692
692
  const notice = await stashResult(text, results, bytes.length);
693
- return { content: [{ type: "text", text: JSON.stringify(notice) }] };
693
+ return {
694
+ result: { content: [{ type: "text", text: JSON.stringify(notice) }] },
695
+ truncated: true,
696
+ };
697
+ }
698
+ /**
699
+ * The sentence that closes the OAuth handoff, telling the operator's agent how
700
+ * to confirm the flow landed through the one surface it can call.
701
+ */
702
+ function oauthFollowUp(connectorId) {
703
+ return `Then retry the original call; connecta.search({ connector: ${JSON.stringify(connectorId)} }) inside execute_code confirms the catalog now loads.`;
694
704
  }
695
705
  /**
696
- * Every base meta-tool handler over a registry all nine, whichever surface is
697
- * advertised, since folding a tool away only skips its registration and never
698
- * its handler. Exported for direct testing; registerMetaTools() wires the ones
699
- * this surface advertises onto an McpServer. `opts.defaultToolTimeoutMs`
700
- * supplies a deadline for calls that don't carry one. (execute_code is
701
- * registered separately by registerExecuteTool.)
706
+ * Every meta-tool handler over a registry, one per registered tool. Exported for
707
+ * direct testing; registerMetaTools() wires the six explicit tools onto an
708
+ * McpServer. `opts.defaultToolTimeoutMs` supplies a deadline for calls that
709
+ * don't carry one. (execute_code is registered separately by
710
+ * registerExecuteTool, and builds its own services over the same registry.)
711
+ *
712
+ * What execute_code shares with these handlers is the services layer beneath
713
+ * them — `CatalogService` and `InvocationService` — not the handlers, which no
714
+ * in-program path calls.
702
715
  *
703
716
  * Deployment-wide result-size caps are read off the registry view rather than
704
- * passed in: `ConnectaConfig.calls.maxResultBytes`, its per-connector override,
705
- * and the independent `calls.maxBatchResultBytes` final-envelope boundary each
706
- * have one runtime source of truth.
717
+ * passed in: `ConnectaConfig.calls.maxResultBytes` and its per-connector
718
+ * override each have one runtime source of truth.
707
719
  */
708
720
  export function createMetaTools(registry, baseUrl, opts = {}) {
709
- const surface = opts.surface ?? "classic";
710
721
  // Already normalized and warned about at registry construction.
711
722
  const globalCap = registry.maxResultBytes;
712
- const batchCap = registry.maxBatchResultBytes;
713
723
  const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
714
724
  const probeTimeoutMs = normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
715
725
  const discoveryConcurrency = resolveDiscoveryConcurrency(opts.discoveryConcurrency);
@@ -721,9 +731,10 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
721
731
  requestScope,
722
732
  probeTimeoutMs,
723
733
  concurrency: discoveryConcurrency,
734
+ // searchRoute keeps its top-level default. In-program callers use a
735
+ // separate CatalogService configured for connecta.search.
724
736
  });
725
737
  const invocation = new InvocationService(registry, catalog, opts.activity);
726
- const withProbeDeadline = (label, operation) => withAbortableTimeout((signal) => operation({ signal, timeoutMs: probeTimeoutMs }), probeTimeoutMs, label);
727
738
  /** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
728
739
  async function runCall(call, source, options = {}) {
729
740
  const results = registry.resultsStorage();
@@ -745,7 +756,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
745
756
  processResult: async (result, resolved) => {
746
757
  // Result-size cap for THIS call: the connector's own override wins,
747
758
  // then the deployment-wide value, then the built-in default (already
748
- // folded into `globalCap`). Resolved per call so one batch_call can
759
+ // folded into `globalCap`). Resolved per call so one request can
749
760
  // mix a tight-capped connector with siblings on the global cap. An
750
761
  // override the registry already warned about at startup is dropped
751
762
  // here, so the connector simply inherits `globalCap`.
@@ -754,10 +765,14 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
754
765
  let value = fields
755
766
  ? projectionValue(result, fields, resolved.definition.outputSchema)
756
767
  : result;
757
- value = await guardValue(value, results, cap);
768
+ const guarded = await guardValue(value, results, cap);
769
+ value = guarded.result;
758
770
  return {
759
771
  toolResult: jsonResult({ ok: true, data: value }),
760
772
  value,
773
+ ...(guarded.truncated
774
+ ? { friction: "result_too_large" }
775
+ : {}),
761
776
  };
762
777
  }
763
778
  if (resolved.connector.kind === "mcp") {
@@ -766,19 +781,32 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
766
781
  if (fields) {
767
782
  content = applyFieldsToContent(content, fields, resolved.definition.outputSchema);
768
783
  }
769
- return { toolResult: await guardContent(content, results, cap) };
784
+ const guarded = await guardContent(content, results, cap);
785
+ return {
786
+ toolResult: guarded.result,
787
+ ...(guarded.truncated
788
+ ? { friction: "result_too_large" }
789
+ : {}),
790
+ };
770
791
  }
771
792
  const value = fields
772
793
  ? projectionValue(result, fields, resolved.definition.outputSchema)
773
794
  : result;
795
+ const guarded = await guardText(serializeResultText(value), results, cap);
774
796
  return {
775
- toolResult: await guardText(serializeResultText(value), results, cap),
797
+ toolResult: guarded.result,
776
798
  value,
799
+ ...(guarded.truncated
800
+ ? { friction: "result_too_large" }
801
+ : {}),
777
802
  };
778
803
  },
804
+ activityFriction: (processed) => processed.friction,
779
805
  });
780
806
  if (!outcome.ok) {
781
- const failedResult = outcome.error.code === "auth_required" ||
807
+ const structuredRecovery = outcome.error.nextAction !== undefined;
808
+ const failedResult = structuredRecovery ||
809
+ outcome.error.code === "auth_required" ||
782
810
  outcome.error.code === "invalid_args" ||
783
811
  outcome.error.code === "input_required_unsupported" ||
784
812
  call.resultMode === "value"
@@ -790,7 +818,8 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
790
818
  ...(call.diagnostics ? { timing: outcome.timing } : {}),
791
819
  })
792
820
  : errorResult(outcome.error.message);
793
- if (outcome.error.code === "auth_required" ||
821
+ if (structuredRecovery ||
822
+ outcome.error.code === "auth_required" ||
794
823
  outcome.error.code === "invalid_args" ||
795
824
  outcome.error.code === "input_required_unsupported") {
796
825
  failedResult.isError = true;
@@ -831,134 +860,30 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
831
860
  {
832
861
  type: "text",
833
862
  text: 'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
834
- listSkills(connectors, surface)
863
+ listSkills(connectors)
835
864
  .map((skill) => `- \`${skill.name}\` — ${skill.description}`)
836
865
  .join("\n"),
837
866
  },
838
867
  ],
839
868
  };
840
869
  }
841
- const skill = resolveSkill(args.name, connectors, surface);
870
+ const skill = resolveSkill(args.name, connectors);
842
871
  if (!skill.found)
843
872
  return errorResult(skill.message);
844
873
  return { content: [{ type: "text", text: skill.content }] };
845
874
  },
846
- async listConnectors(args = {}) {
847
- const probe = args.probe ?? true;
848
- // Live inventory owns a short-lived scope separate from the request's
849
- // call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
850
- const connectors = registry.listConnectors();
851
- const scope = probe ? {} : requestScope;
852
- const inspect = async (c) => {
853
- const statusStarted = Date.now();
854
- const observed = registry.healthFor(c.id);
855
- const drift = await registry.credentialDriftFor(c.id);
856
- let status;
857
- if (drift) {
858
- status = { state: "auth_required", message: drift };
859
- }
860
- else if (probe) {
861
- try {
862
- status = await withProbeDeadline(`list_connectors probe of "${c.id}"`, (options) => registry.statusFor(c.id, baseUrl, scope, options));
863
- }
864
- catch (err) {
865
- // A probe that outran probeTimeoutMs (or otherwise threw)
866
- // degrades this connector to an error status rather than
867
- // hanging the whole list_connectors call.
868
- status = { state: "error", message: msg(err) };
869
- }
870
- }
871
- else {
872
- const derived = observed?.consecutiveFailures && observed.consecutiveFailures > 0
873
- ? "error"
874
- : registry.hasObservedSuccess(c.id) || c.kind === "api"
875
- ? "ok"
876
- : "unknown";
877
- status = {
878
- state: derived,
879
- ...(observed?.lastError ? { message: observed.lastError } : {}),
880
- };
881
- }
882
- // Stamped after any live probe so the response reports when its
883
- // observation completed, not when a potentially slow request began.
884
- const checkedAt = new Date().toISOString();
885
- let tools = registry.peekTools(c.id);
886
- // An auth_required status may have just started OAuth. A second
887
- // listTools probe would overwrite its state/verifier while returning
888
- // the first (now stale) authorization URL.
889
- if (probe && status.state === "ok") {
890
- try {
891
- tools = await withProbeDeadline(`list_connectors catalog refresh of "${c.id}"`, (options) => registry.refreshTools(c.id, baseUrl, scope, options));
892
- registry.recordSuccess(c.id, Date.now() - statusStarted);
893
- }
894
- catch (err) {
895
- const details = classifyCallError(err);
896
- if (details.code === "auth_required") {
897
- let authStatus;
898
- try {
899
- authStatus = await withProbeDeadline(`list_connectors authorization status of "${c.id}"`, (options) => registry.statusFor(c.id, baseUrl, scope, options));
900
- }
901
- catch {
902
- // The typed auth verdict is still authoritative; this second
903
- // read exists only to recover the connector's pending URL.
904
- }
905
- status =
906
- authStatus?.state === "auth_required"
907
- ? authStatus
908
- : {
909
- state: "auth_required",
910
- message: details.message,
911
- };
912
- }
913
- else {
914
- status = { state: "error", message: msg(err) };
915
- }
916
- registry.recordFailure(c.id, Date.now() - statusStarted, err);
917
- }
918
- }
919
- const latencyMs = Date.now() - statusStarted;
920
- const latestObserved = registry.healthFor(c.id);
921
- return {
922
- id: c.id,
923
- ...(c.title ? { title: c.title } : {}),
924
- description: c.description,
925
- toolCount: tools?.length ?? 0,
926
- status: status.state,
927
- checkedAt,
928
- latencyMs,
929
- probe,
930
- ...(latestObserved ?? observed),
931
- ...("authorizationUrl" in status &&
932
- status.authorizationUrl && {
933
- authorizationUrl: status.authorizationUrl,
934
- }),
935
- ...(status.message && { message: status.message }),
936
- };
937
- };
938
- const settled = await mapSettledWithConcurrency(connectors, discoveryConcurrency, inspect);
939
- if (probe) {
940
- await mapSettledWithConcurrency(connectors, discoveryConcurrency, (connector) => closeConnectorScope(connector, registry.contextFor(connector.id, baseUrl, scope), opts.defer));
941
- }
942
- const out = settled.map((result) => {
943
- if (result.status === "rejected")
944
- throw result.reason;
945
- return result.value;
946
- });
947
- return jsonResult({ connectors: out });
948
- },
949
875
  async searchTools(args) {
950
876
  return discoveryResult(async () => groupedSearchResult(await catalog.search({
951
877
  ...args,
952
878
  includeSchemaKeys: args.includeSchemas !== undefined,
953
879
  })), "Request a smaller limit, omit fullDescriptions, or use compact schemas.");
954
880
  },
955
- async describeTools(args) {
956
- return discoveryResult(async () => ({ tools: await catalog.describe(args) }), 'Split the address list or use format: "compact".');
957
- },
958
881
  async callTool(args) {
959
882
  return (await runCall(args, "call_tool")).toolResult;
960
883
  },
961
884
  async callDestructiveTool(args) {
885
+ // `reason` is read by the host's approval view and stops there — runCall
886
+ // forwards only the call fields, so it never reaches the connector.
962
887
  return (await runCall(args, "call_destructive_tool", { allowDestructive: true })).toolResult;
963
888
  },
964
889
  async getResult(args) {
@@ -1008,131 +933,6 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
1008
933
  text: slice,
1009
934
  });
1010
935
  },
1011
- async batchCall(args) {
1012
- const batchStarted = Date.now();
1013
- const settled = await Promise.allSettled(args.calls.map((c) => runCall({
1014
- ...c,
1015
- ...((c.resultMode ?? args.resultMode) !== undefined
1016
- ? { resultMode: c.resultMode ?? args.resultMode }
1017
- : {}),
1018
- ...((c.timeoutMs ?? args.timeoutMs) !== undefined
1019
- ? { timeoutMs: c.timeoutMs ?? args.timeoutMs }
1020
- : {}),
1021
- ...((c.maxRetries ?? args.maxRetries) !== undefined
1022
- ? { maxRetries: c.maxRetries ?? args.maxRetries }
1023
- : {}),
1024
- ...((c.diagnostics ?? args.diagnostics) !== undefined
1025
- ? { diagnostics: c.diagnostics ?? args.diagnostics }
1026
- : {}),
1027
- }, "batch_call")));
1028
- const results = settled.map((s, i) => {
1029
- const call = args.calls[i];
1030
- if (!call) {
1031
- throw new Error("Batch result has no corresponding call");
1032
- }
1033
- const { address } = call;
1034
- if (s.status === "rejected") {
1035
- return {
1036
- address,
1037
- ok: false,
1038
- error: msg(s.reason),
1039
- errorDetails: classifyCallError(s.reason, "batch_call_failed"),
1040
- };
1041
- }
1042
- const r = s.value;
1043
- if (r.error) {
1044
- return {
1045
- address,
1046
- ok: false,
1047
- error: r.error.message,
1048
- errorDetails: r.error,
1049
- durationMs: r.durationMs,
1050
- attempts: r.attempts,
1051
- ...((call.diagnostics ?? args.diagnostics)
1052
- ? { timing: r.timing }
1053
- : {}),
1054
- };
1055
- }
1056
- if ((call.resultMode ?? args.resultMode) === "value") {
1057
- return {
1058
- address,
1059
- ok: true,
1060
- data: r.value,
1061
- durationMs: r.durationMs,
1062
- attempts: r.attempts,
1063
- ...((call.diagnostics ?? args.diagnostics)
1064
- ? { timing: r.timing }
1065
- : {}),
1066
- };
1067
- }
1068
- return {
1069
- address,
1070
- ok: true,
1071
- result: r.toolResult.content,
1072
- durationMs: r.durationMs,
1073
- attempts: r.attempts,
1074
- ...((call.diagnostics ?? args.diagnostics)
1075
- ? { timing: r.timing }
1076
- : {}),
1077
- };
1078
- });
1079
- const envelope = {
1080
- results,
1081
- durationMs: Date.now() - batchStarted,
1082
- };
1083
- const text = serializeResultText(envelope);
1084
- const bytes = enc.encode(text);
1085
- if (bytes.length <= batchCap)
1086
- return jsonResult(envelope);
1087
- const notice = await stashResult(text, registry.resultsStorage(), bytes.length);
1088
- return jsonResult({
1089
- results: results.map((result) => {
1090
- const common = {
1091
- address: batchSummaryString(result.address),
1092
- ok: !("error" in result),
1093
- ...("durationMs" in result
1094
- ? { durationMs: result.durationMs }
1095
- : {}),
1096
- ...("attempts" in result ? { attempts: result.attempts } : {}),
1097
- ...("timing" in result ? { timing: result.timing } : {}),
1098
- };
1099
- if (!("error" in result))
1100
- return common;
1101
- const error = result.error ?? "Batch call failed";
1102
- const details = result.errorDetails ??
1103
- errorDetails("batch_call_failed", error);
1104
- return {
1105
- ...common,
1106
- error: batchSummaryString(error),
1107
- errorDetails: {
1108
- code: batchSummaryString(details.code),
1109
- message: batchSummaryString(details.message),
1110
- retryable: details.retryable,
1111
- ...(details.retryAfterMs !== undefined
1112
- ? { retryAfterMs: details.retryAfterMs }
1113
- : {}),
1114
- ...(details.connector !== undefined
1115
- ? { connector: batchSummaryString(details.connector) }
1116
- : {}),
1117
- ...(details.operation !== undefined
1118
- ? { operation: batchSummaryString(details.operation) }
1119
- : {}),
1120
- ...(details.recovery !== undefined
1121
- ? { recovery: details.recovery }
1122
- : {}),
1123
- ...(details.nextAction !== undefined
1124
- ? { nextAction: details.nextAction }
1125
- : {}),
1126
- ...(details.retry !== undefined
1127
- ? { retry: batchSummaryString(details.retry) }
1128
- : {}),
1129
- },
1130
- };
1131
- }),
1132
- durationMs: envelope.durationMs,
1133
- ...notice,
1134
- });
1135
- },
1136
936
  async authorizeConnector(args) {
1137
937
  const connector = registry.getConnector(args.connector);
1138
938
  if (!connector) {
@@ -1195,7 +995,8 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
1195
995
  ...(status.authorizationUrl
1196
996
  ? {
1197
997
  authorizationUrl: status.authorizationUrl,
1198
- instructions: "Have the operator open authorizationUrl in a browser and complete the consent flow. The provider then redirects back to this server's /oauth/callback/<connector> route, which finishes the flow automatically. Re-run list_connectors afterwards to confirm status is ok.",
998
+ instructions: "Have the operator open authorizationUrl in a browser and complete the consent flow. The provider then redirects back to this server's /oauth/callback/<connector> route, which finishes the flow automatically. " +
999
+ oauthFollowUp(connector.id),
1199
1000
  }
1200
1001
  : {}),
1201
1002
  ...(status.message ? { message: status.message } : {}),
@@ -1212,29 +1013,13 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
1212
1013
  },
1213
1014
  };
1214
1015
  }
1215
- const LIST_DESC = "List connectors with status, cached tool count, and recent real-call health. Use probe=false for a fast inventory; use probe=true (default) only to diagnose live health or authorization.";
1216
1016
  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; plain-object schemas also expose inputKeys, requiredInputKeys, and outputKeys, while inputSchemaTruncated/outputSchemaTruncated mark shapes that need exact retrieval; matches also carry declared annotations. Call directly when sufficient. Empty query browses all.`;
1217
- 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.`;
1218
- const CALL_DESC = 'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths; traverse arrays with [] (for example results[].id). Misses return data plus `$connecta` feedback. resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
1219
- const CALL_DESTRUCTIVE_DESC = "Invoke any tool that is not explicitly annotated readOnlyHint: true, including unannotated, write-capable, or destructive tools. The MCP destructiveHint on this meta-tool lets the host request human approval before execution. Use only after reviewing the downstream tool schema and consequences.";
1220
- const GET_RESULT_DESC = "Page a truncated result stashed by call_tool/batch_call. Input { id, offset?, maxBytes? } → { text, offset, nextOffset?, totalBytes } sliced by byte offset. maxBytes is a whole number of bytes >= 1 (omit for the deployment default) and offset a whole number of bytes >= 0; an offset inside a multi-byte character is moved back to that character's first byte and the offset served is returned. Unknown/expired id is an error.";
1221
- const BATCH_DESC = "Use for 2–10 independent tools explicitly annotated readOnlyHint: true. Calls run in parallel with shared request-scoped clients; use execute_code when available instead for dependencies or in-sandbox reduction. Unannotated, write-capable, and destructive tools are refused. Batch timeout, safe retry, result mode, and diagnostics defaults may be overridden per call. An oversized final envelope returns ordered outcome summaries plus a get_result page handle.";
1017
+ const CALL_DESC = '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; traverse arrays with [] (for example results[].id). Misses return data plus `$connecta` feedback. resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
1018
+ const CALL_DESTRUCTIVE_DESC = "Invoke any tool that is not explicitly annotated readOnlyHint: true, including unannotated, write-capable, or destructive tools. Include a short reason explaining the intended consequence for the human reviewer; it grants no authority and is never passed downstream. 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.";
1019
+ const GET_RESULT_DESC = "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.";
1222
1020
  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.";
1223
1021
  const SKILLS_DESC = 'List or fetch concise guidance for choosing among Connecta meta-tools. Call skills({ name: "usage" }) once when the routing workflow is unfamiliar; do not refetch it in the same task.';
1224
- /**
1225
- * Code-first replacements for the descriptions that route work between tools.
1226
- * Every one of these mentions a tool the consolidated surface removed, so on a
1227
- * code-first deployment the routing sentence has to point at the in-program
1228
- * function that took the work over — a description naming `batch_call` on a
1229
- * surface without one teaches a call that cannot succeed.
1230
- *
1231
- * The classic strings above are left byte-for-byte alone: classic is the
1232
- * compatibility surface and the eval's control arm, and rewording it would
1233
- * change what that control measures.
1234
- */
1235
- const CODE_FIRST_SEARCH_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
1236
- const CODE_FIRST_CALL_DESC = '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; traverse arrays with [] (for example results[].id). Misses return data plus `$connecta` feedback. resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
1237
- const CODE_FIRST_GET_RESULT_DESC = "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.";
1022
+ const SEARCH_WITH_DESCRIBE_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
1238
1023
  /**
1239
1024
  * Sentences appended to a meta-tool description only when this connection
1240
1025
  * actually has connector guides. Tool descriptions are always-loaded context,
@@ -1246,7 +1031,6 @@ const CODE_FIRST_GET_RESULT_DESC = "Page a truncated result stashed by call_tool
1246
1031
  const GUIDE_NOTES = {
1247
1032
  skills: ' skills({}) also lists this deployment\'s per-connector usage guides as "connector:<connectorId>"; fetch the guide for a connector before working with it for the first time.',
1248
1033
  search: " A connector group carrying `guide` has a usage guide; fetch it with skills({ name: <guide> }).",
1249
- describe: " An entry carrying `guide` belongs to a connector with a usage guide; fetch it with skills({ name: <guide> }).",
1250
1034
  };
1251
1035
  /** `base`, plus its guide note when any VISIBLE connector carries a guide. */
1252
1036
  function describedFor(registry, base, note) {
@@ -1282,21 +1066,15 @@ const CALL_INPUT_SCHEMA = {
1282
1066
  diagnostics: z.boolean().optional(),
1283
1067
  };
1284
1068
  /**
1285
- * Register the base meta-tools onto an McpServer instance: nine on the classic
1286
- * surface, six on the code-first one, where `list_connectors`,
1287
- * `describe_tools`, and `batch_call` have folded into the program surface
1288
- * (`registerExecuteTool` adds the seventh, `execute_code`).
1289
- *
1290
- * Only the registrations differ. Every handler still exists on the object
1291
- * `createMetaTools` returns, and a folded tool's behavior is reached through
1292
- * `connecta.search` / `connecta.describe` / `connecta.batch` inside a program —
1293
- * the same code paths, one layer down.
1069
+ * Register the six explicit meta-tools onto an McpServer instance.
1070
+ * `registerExecuteTool` adds the seventh, `execute_code`. Broad discovery and
1071
+ * multi-call work is reached through `connecta.search` / `connecta.describe` /
1072
+ * `connecta.batch` inside a program, which `execute_code` builds over the same
1073
+ * `CatalogService` and `InvocationService` these handlers use — one shared
1074
+ * services layer, two adapters above it.
1294
1075
  */
1295
1076
  export function registerMetaTools(server, registry, ctx) {
1296
- const surface = ctx.surface ?? "classic";
1297
- const codeFirst = surface === "code-first";
1298
1077
  const mt = createMetaTools(registry, ctx.baseUrl, {
1299
- surface,
1300
1078
  ...(ctx.defaultToolTimeoutMs !== undefined
1301
1079
  ? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
1302
1080
  : {}),
@@ -1310,26 +1088,14 @@ export function registerMetaTools(server, registry, ctx) {
1310
1088
  ...(ctx.requestSignal !== undefined
1311
1089
  ? { requestSignal: ctx.requestSignal }
1312
1090
  : {}),
1313
- ...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
1314
1091
  });
1315
1092
  server.registerTool("skills", {
1316
1093
  description: describedFor(registry, SKILLS_DESC, "skills"),
1317
1094
  inputSchema: z.object({ name: z.string().optional() }),
1318
1095
  annotations: READ_ONLY_LOCAL,
1319
1096
  }, async (args) => mt.skills(args));
1320
- // Folded on the code-first surface: a program browses the same inventory with
1321
- // connecta.search({}) (every catalog) or connecta.search({ connector }) (one).
1322
- // Live connector probing is an operator concern, not a model one — it stays on
1323
- // the operator pages and /health, which is where the ethos puts observability.
1324
- if (!codeFirst) {
1325
- server.registerTool("list_connectors", {
1326
- description: LIST_DESC,
1327
- inputSchema: z.object({ probe: z.boolean().optional() }),
1328
- annotations: READ_ONLY_REMOTE,
1329
- }, async (args) => mt.listConnectors(args));
1330
- }
1331
1097
  server.registerTool("search_tools", {
1332
- description: describedFor(registry, codeFirst ? CODE_FIRST_SEARCH_DESC : SEARCH_DESC, "search"),
1098
+ description: describedFor(registry, SEARCH_WITH_DESCRIBE_DESC, "search"),
1333
1099
  inputSchema: z.object({
1334
1100
  query: z.string().optional(),
1335
1101
  connector: z.string().optional(),
@@ -1343,21 +1109,8 @@ export function registerMetaTools(server, registry, ctx) {
1343
1109
  }),
1344
1110
  annotations: READ_ONLY_REMOTE,
1345
1111
  }, async (args) => mt.searchTools(args));
1346
- // Folded on the code-first surface: connecta.describe takes the same
1347
- // addresses, format, and per-address error reporting inside a program.
1348
- if (!codeFirst) {
1349
- server.registerTool("describe_tools", {
1350
- description: describedFor(registry, DESCRIBE_DESC, "describe"),
1351
- inputSchema: z.object({
1352
- addresses: z.array(z.string()).max(MAX_DESCRIBE_ADDRESSES),
1353
- format: z.enum(["compact", "json"]).optional(),
1354
- fullDescriptions: z.boolean().optional(),
1355
- }),
1356
- annotations: READ_ONLY_REMOTE,
1357
- }, async (args) => mt.describeTools(args));
1358
- }
1359
1112
  server.registerTool("call_tool", {
1360
- description: codeFirst ? CODE_FIRST_CALL_DESC : CALL_DESC,
1113
+ description: CALL_DESC,
1361
1114
  inputSchema: z.object(CALL_INPUT_SCHEMA),
1362
1115
  // call_tool admits only tools that are themselves explicitly read-only;
1363
1116
  // anything else is refused and routed to call_destructive_tool.
@@ -1365,13 +1118,28 @@ export function registerMetaTools(server, registry, ctx) {
1365
1118
  }, async (args) => mt.callTool(args));
1366
1119
  server.registerTool("call_destructive_tool", {
1367
1120
  description: CALL_DESTRUCTIVE_DESC,
1368
- inputSchema: z.object(CALL_INPUT_SCHEMA),
1121
+ inputSchema: z.object({
1122
+ ...CALL_INPUT_SCHEMA,
1123
+ // Bounded above, but with no lower bound: a model that sends `""` or
1124
+ // whitespace has written no reason, and failing an entire consequential
1125
+ // call over a cosmetic field the host merely displays is the wrong
1126
+ // trade. It is normalized to absent below instead.
1127
+ reason: z.string().max(500).optional(),
1128
+ }),
1369
1129
  annotations: {
1370
1130
  destructiveHint: true,
1371
1131
  readOnlyHint: false,
1372
1132
  openWorldHint: true,
1373
1133
  },
1374
- }, async (args) => mt.callDestructiveTool(args));
1134
+ }, async (args) => {
1135
+ // `reason` is the host's to display and connecta's to keep out of the
1136
+ // downstream call, so this destructuring is the whole of its handling:
1137
+ // nothing below reads it. Dropping it is also what makes an empty or
1138
+ // whitespace-only one "absent" rather than a validation failure — there
1139
+ // is no field left for it to be absent from.
1140
+ const { reason: _hostContext, ...call } = args;
1141
+ return mt.callDestructiveTool(call);
1142
+ });
1375
1143
  server.registerTool("authorize_connector", {
1376
1144
  description: AUTHORIZE_DESC,
1377
1145
  inputSchema: z.object({
@@ -1387,7 +1155,7 @@ export function registerMetaTools(server, registry, ctx) {
1387
1155
  },
1388
1156
  }, async (args) => mt.authorizeConnector(args));
1389
1157
  server.registerTool("get_result", {
1390
- description: codeFirst ? CODE_FIRST_GET_RESULT_DESC : GET_RESULT_DESC,
1158
+ description: GET_RESULT_DESC,
1391
1159
  inputSchema: z.object({
1392
1160
  id: z.string(),
1393
1161
  // Both bounds are the shared rules (isValidResultOffset,
@@ -1399,25 +1167,5 @@ export function registerMetaTools(server, registry, ctx) {
1399
1167
  }),
1400
1168
  annotations: READ_ONLY_LOCAL,
1401
1169
  }, async (args) => mt.getResult(args));
1402
- // Folded on the code-first surface: connecta.batch runs the same 1–10
1403
- // parallel read-only calls and returns the same typed per-call outcomes.
1404
- if (!codeFirst) {
1405
- server.registerTool("batch_call", {
1406
- description: BATCH_DESC,
1407
- inputSchema: z.object({
1408
- calls: z
1409
- .array(z.object(CALL_INPUT_SCHEMA))
1410
- .min(1)
1411
- .max(10),
1412
- resultMode: z.enum(["mcp", "value"]).optional(),
1413
- timeoutMs: z.number().int().positive().optional(),
1414
- maxRetries: z.number().int().min(0).max(2).optional(),
1415
- diagnostics: z.boolean().optional(),
1416
- }),
1417
- // Same gate as call_tool: every call in the batch must be explicitly
1418
- // read-only or the batch is refused.
1419
- annotations: READ_ONLY_REMOTE,
1420
- }, async (args) => mt.batchCall(args));
1421
- }
1422
1170
  }
1423
1171
  //# sourceMappingURL=meta-tools.js.map