@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
package/src/meta-tools.ts CHANGED
@@ -14,19 +14,8 @@ import {
14
14
  MAX_DISCOVERY_RESULT_BYTES,
15
15
  MAX_SEARCH_LIMIT,
16
16
  } from "./catalog-service.js";
17
- import {
18
- mapSettledWithConcurrency,
19
- resolveDiscoveryConcurrency,
20
- } from "./concurrency.js";
21
- import {
22
- closeConnectorScope,
23
- type DeferredWork,
24
- } from "./connector-scope.js";
25
- import {
26
- classifyCallError,
27
- messageLooksRetryable,
28
- type CallErrorDetails,
29
- } from "./errors.js";
17
+ import { resolveDiscoveryConcurrency } from "./concurrency.js";
18
+ import type { CallErrorDetails } from "./errors.js";
30
19
  import {
31
20
  InvocationService,
32
21
  MAX_RETRY_BACKOFF_MS,
@@ -47,13 +36,8 @@ import {
47
36
  import {
48
37
  DEFAULT_PROBE_TIMEOUT_MS,
49
38
  normalizeTimeoutMs,
50
- withAbortableTimeout,
51
39
  } from "./timeout.js";
52
- import type {
53
- ConnectaSurface,
54
- ConnectorStatus,
55
- KVStorage,
56
- } from "./types.js";
40
+ import type { KVStorage } from "./types.js";
57
41
 
58
42
  export {
59
43
  MAX_DESCRIBE_ADDRESSES,
@@ -95,10 +79,6 @@ function msg(err: unknown): string {
95
79
  return err instanceof Error ? err.message : String(err);
96
80
  }
97
81
 
98
- function errorDetails(code: string, message: string): CallErrorDetails {
99
- return { code, message, retryable: messageLooksRetryable(message) };
100
- }
101
-
102
82
  function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
103
83
  const result = jsonResult({
104
84
  error: {
@@ -132,7 +112,6 @@ async function discoveryResult(
132
112
  }
133
113
  }
134
114
 
135
-
136
115
  /** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
137
116
  function isContinuationByte(b: number | undefined): boolean {
138
117
  return b !== undefined && (b & 0xc0) === 0x80;
@@ -786,6 +765,10 @@ async function stashResult(
786
765
  resultId: string;
787
766
  totalBytes: number;
788
767
  hint: string;
768
+ nextAction: {
769
+ tool: "get_result";
770
+ arguments: { id: string; offset: 0 };
771
+ };
789
772
  }> {
790
773
  const id = crypto.randomUUID();
791
774
  await results.set(`result:${id}`, text, { ttlSeconds: RESULT_TTL_SECONDS });
@@ -794,16 +777,16 @@ async function stashResult(
794
777
  resultId: id,
795
778
  totalBytes,
796
779
  hint: "use get_result {id, offset} to page, or re-call with fields to select less",
780
+ nextAction: {
781
+ tool: "get_result",
782
+ arguments: { id, offset: 0 },
783
+ },
797
784
  };
798
785
  }
799
786
 
800
- /** Keep an oversized batch's inline outcome summary at fixed string overhead. */
801
- function batchSummaryString(value: string): string {
802
- const bytes = enc.encode(value);
803
- const maxBytes = 512;
804
- if (bytes.length <= maxBytes) return value;
805
- const end = alignEndToCharBoundary(bytes, 0, maxBytes, bytes.length);
806
- return `${dec.decode(bytes.slice(0, end))}…`;
787
+ interface GuardedResult<T> {
788
+ result: T;
789
+ truncated: boolean;
807
790
  }
808
791
 
809
792
  /**
@@ -817,16 +800,22 @@ async function guardEncoded(
817
800
  bytes: Uint8Array,
818
801
  results: KVStorage,
819
802
  cap: number,
820
- ): Promise<ToolResult> {
803
+ ): Promise<GuardedResult<ToolResult>> {
821
804
  if (bytes.length <= cap) {
822
- return { content: [{ type: "text", text }] };
805
+ return {
806
+ result: { content: [{ type: "text", text }] },
807
+ truncated: false,
808
+ };
823
809
  }
824
810
  const notice = await stashResult(text, results, bytes.length);
825
811
  const head = dec.decode(
826
812
  bytes.slice(0, alignEndToCharBoundary(bytes, 0, cap, bytes.length)),
827
813
  );
828
814
  return {
829
- content: [{ type: "text", text: `${head}\n${JSON.stringify(notice)}` }],
815
+ result: {
816
+ content: [{ type: "text", text: `${head}\n${JSON.stringify(notice)}` }],
817
+ },
818
+ truncated: true,
830
819
  };
831
820
  }
832
821
 
@@ -835,7 +824,7 @@ async function guardText(
835
824
  text: string,
836
825
  results: KVStorage,
837
826
  cap: number,
838
- ): Promise<ToolResult> {
827
+ ): Promise<GuardedResult<ToolResult>> {
839
828
  // `JSON.stringify`'s type says `string` where its behavior says `string |
840
829
  // undefined`, so TypeScript alone does not keep a non-string out of here.
841
830
  // Normalizing at the door means the size check below always measures exactly
@@ -851,11 +840,14 @@ async function guardValue(
851
840
  value: unknown,
852
841
  results: KVStorage,
853
842
  cap: number,
854
- ): Promise<unknown> {
843
+ ): Promise<GuardedResult<unknown>> {
855
844
  const text = serializeResultText(value);
856
845
  const bytes = enc.encode(text);
857
- if (bytes.length <= cap) return value;
858
- return stashResult(text, results, bytes.length);
846
+ if (bytes.length <= cap) return { result: value, truncated: false };
847
+ return {
848
+ result: await stashResult(text, results, bytes.length),
849
+ truncated: true,
850
+ };
859
851
  }
860
852
 
861
853
  /**
@@ -880,7 +872,7 @@ async function guardContent(
880
872
  content: TextContent[],
881
873
  results: KVStorage,
882
874
  cap: number,
883
- ): Promise<ToolResult> {
875
+ ): Promise<GuardedResult<ToolResult>> {
884
876
  let text: string;
885
877
  try {
886
878
  text = JSON.stringify(content);
@@ -889,17 +881,22 @@ async function guardContent(
889
881
  // be measured, stashed, or paged either — there is nothing this guard could
890
882
  // do with it. Pass it through as the old text-only measure did, rather than
891
883
  // turning a call that used to succeed into result_processing_failed.
892
- return { content };
884
+ return { result: { content }, truncated: false };
893
885
  }
894
886
  const bytes = enc.encode(text);
895
887
  // Under the cap the downstream blocks pass through untouched, non-text ones
896
888
  // included, in their original order.
897
- if (bytes.length <= cap) return { content };
889
+ if (bytes.length <= cap) {
890
+ return { result: { content }, truncated: false };
891
+ }
898
892
  if (content.every((b) => b.type === "text")) {
899
893
  return guardEncoded(text, bytes, results, cap);
900
894
  }
901
895
  const notice = await stashResult(text, results, bytes.length);
902
- return { content: [{ type: "text", text: JSON.stringify(notice) }] };
896
+ return {
897
+ result: { content: [{ type: "text", text: JSON.stringify(notice) }] },
898
+ truncated: true,
899
+ };
903
900
  }
904
901
 
905
902
  // --- compact schema rendering (feature 3a) --------------------------------
@@ -915,17 +912,6 @@ export interface SearchArgs {
915
912
  fullDescriptions?: boolean;
916
913
  includeSchemas?: "compact" | "json";
917
914
  }
918
- export type DescribeArgs = (
919
- | { address: string; addresses?: never }
920
- | { address?: never; addresses: string[] }
921
- ) & {
922
- format?: "compact" | "json";
923
- fullDescriptions?: boolean;
924
- };
925
- export interface ListArgs {
926
- /** When false, return cached/observed health without downstream I/O. */
927
- probe?: boolean;
928
- }
929
915
  type ResultMode = "mcp" | "value";
930
916
  export interface CallArgs {
931
917
  address: string;
@@ -938,6 +924,10 @@ export interface CallArgs {
938
924
  /** Include connector/catalog/result-processing timing segments. */
939
925
  diagnostics?: boolean;
940
926
  }
927
+ export interface DestructiveCallArgs extends CallArgs {
928
+ /** Short model-authored context for the host's approval UI; never downstream input. */
929
+ reason?: string;
930
+ }
941
931
  export interface GetResultArgs {
942
932
  id: string;
943
933
  /**
@@ -948,14 +938,6 @@ export interface GetResultArgs {
948
938
  /** Page size in bytes; a whole number >= 1. Defaults to the deployment cap. */
949
939
  maxBytes?: number;
950
940
  }
951
- export type BatchCall = CallArgs;
952
- export interface BatchArgs {
953
- calls: BatchCall[];
954
- resultMode?: ResultMode;
955
- timeoutMs?: number;
956
- maxRetries?: number;
957
- diagnostics?: boolean;
958
- }
959
941
  export interface AuthorizeArgs {
960
942
  connector: string;
961
943
  force?: boolean;
@@ -965,17 +947,27 @@ export interface SkillArgs {
965
947
  }
966
948
 
967
949
  /**
968
- * Every base meta-tool handler over a registry all nine, whichever surface is
969
- * advertised, since folding a tool away only skips its registration and never
970
- * its handler. Exported for direct testing; registerMetaTools() wires the ones
971
- * this surface advertises onto an McpServer. `opts.defaultToolTimeoutMs`
972
- * supplies a deadline for calls that don't carry one. (execute_code is
973
- * registered separately by registerExecuteTool.)
950
+ * The sentence that closes the OAuth handoff, telling the operator's agent how
951
+ * to confirm the flow landed through the one surface it can call.
952
+ */
953
+ function oauthFollowUp(connectorId: string): string {
954
+ return `Then retry the original call; connecta.search({ connector: ${JSON.stringify(connectorId)} }) inside execute_code confirms the catalog now loads.`;
955
+ }
956
+
957
+ /**
958
+ * Every meta-tool handler over a registry, one per registered tool. Exported for
959
+ * direct testing; registerMetaTools() wires the six explicit tools onto an
960
+ * McpServer. `opts.defaultToolTimeoutMs` supplies a deadline for calls that
961
+ * don't carry one. (execute_code is registered separately by
962
+ * registerExecuteTool, and builds its own services over the same registry.)
963
+ *
964
+ * What execute_code shares with these handlers is the services layer beneath
965
+ * them — `CatalogService` and `InvocationService` — not the handlers, which no
966
+ * in-program path calls.
974
967
  *
975
968
  * Deployment-wide result-size caps are read off the registry view rather than
976
- * passed in: `ConnectaConfig.calls.maxResultBytes`, its per-connector override,
977
- * and the independent `calls.maxBatchResultBytes` final-envelope boundary each
978
- * have one runtime source of truth.
969
+ * passed in: `ConnectaConfig.calls.maxResultBytes` and its per-connector
970
+ * override each have one runtime source of truth.
979
971
  */
980
972
  export function createMetaTools(
981
973
  registry: RegistryView,
@@ -983,27 +975,17 @@ export function createMetaTools(
983
975
  opts: {
984
976
  /** Deadline applied when a call passes no `timeoutMs`. Off when unset. */
985
977
  defaultToolTimeoutMs?: number;
986
- /** Per-connector deadline for the list/search/describe probe fan-out. Default 30_000. */
978
+ /** Per-connector deadline for the search/describe probe fan-out. Default 30_000. */
987
979
  probeTimeoutMs?: number;
988
980
  /** Maximum simultaneous connector discovery operations. Default 4. */
989
981
  discoveryConcurrency?: number;
990
982
  activity?: ActivityRequestContext;
991
- /** Inbound request cancellation shared by direct and batch child calls. */
983
+ /** Inbound request cancellation shared by every call this request makes. */
992
984
  requestSignal?: AbortSignal;
993
- /** Runtime continuation for the bounded tail of probe-owned teardown. */
994
- defer?: DeferredWork;
995
- /**
996
- * The advertised surface, which the `skills` guidance must match: a
997
- * code-first deployment never gets guidance naming a tool it does not
998
- * advertise. Default `classic`.
999
- */
1000
- surface?: ConnectaSurface;
1001
985
  } = {},
1002
986
  ) {
1003
- const surface: ConnectaSurface = opts.surface ?? "classic";
1004
987
  // Already normalized and warned about at registry construction.
1005
988
  const globalCap = registry.maxResultBytes;
1006
- const batchCap = registry.maxBatchResultBytes;
1007
989
  const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
1008
990
  const probeTimeoutMs =
1009
991
  normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
@@ -1018,20 +1000,10 @@ export function createMetaTools(
1018
1000
  requestScope,
1019
1001
  probeTimeoutMs,
1020
1002
  concurrency: discoveryConcurrency,
1003
+ // searchRoute keeps its top-level default. In-program callers use a
1004
+ // separate CatalogService configured for connecta.search.
1021
1005
  });
1022
1006
  const invocation = new InvocationService(registry, catalog, opts.activity);
1023
- const withProbeDeadline = <T>(
1024
- label: string,
1025
- operation: (options: {
1026
- signal: AbortSignal;
1027
- timeoutMs: number;
1028
- }) => Promise<T>,
1029
- ) =>
1030
- withAbortableTimeout(
1031
- (signal) => operation({ signal, timeoutMs: probeTimeoutMs }),
1032
- probeTimeoutMs,
1033
- label,
1034
- );
1035
1007
 
1036
1008
  interface RunCallOutcome {
1037
1009
  toolResult: ToolResult;
@@ -1045,11 +1017,17 @@ export function createMetaTools(
1045
1017
  interface ProcessedCallResult {
1046
1018
  toolResult: ToolResult;
1047
1019
  value?: unknown;
1020
+ /**
1021
+ * Friction on a call that *succeeded*. It travels as a friction class, not
1022
+ * as an `errorCode`, so persistence keyed on "this row has an error code"
1023
+ * keeps counting failures rather than truncations.
1024
+ */
1025
+ friction?: "result_too_large";
1048
1026
  }
1049
1027
 
1050
1028
  /** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
1051
1029
  async function runCall(
1052
- call: BatchCall,
1030
+ call: CallArgs,
1053
1031
  source: ActivityCallSource,
1054
1032
  options: { allowDestructive?: boolean } = {},
1055
1033
  ): Promise<RunCallOutcome> {
@@ -1075,7 +1053,7 @@ export function createMetaTools(
1075
1053
  processResult: async (result, resolved) => {
1076
1054
  // Result-size cap for THIS call: the connector's own override wins,
1077
1055
  // then the deployment-wide value, then the built-in default (already
1078
- // folded into `globalCap`). Resolved per call so one batch_call can
1056
+ // folded into `globalCap`). Resolved per call so one request can
1079
1057
  // mix a tight-capped connector with siblings on the global cap. An
1080
1058
  // override the registry already warned about at startup is dropped
1081
1059
  // here, so the connector simply inherits `globalCap`.
@@ -1091,10 +1069,14 @@ export function createMetaTools(
1091
1069
  resolved.definition.outputSchema,
1092
1070
  )
1093
1071
  : result;
1094
- value = await guardValue(value, results, cap);
1072
+ const guarded = await guardValue(value, results, cap);
1073
+ value = guarded.result;
1095
1074
  return {
1096
1075
  toolResult: jsonResult({ ok: true, data: value }),
1097
1076
  value,
1077
+ ...(guarded.truncated
1078
+ ? { friction: "result_too_large" as const }
1079
+ : {}),
1098
1080
  };
1099
1081
  }
1100
1082
  if (resolved.connector.kind === "mcp") {
@@ -1107,7 +1089,13 @@ export function createMetaTools(
1107
1089
  resolved.definition.outputSchema,
1108
1090
  );
1109
1091
  }
1110
- return { toolResult: await guardContent(content, results, cap) };
1092
+ const guarded = await guardContent(content, results, cap);
1093
+ return {
1094
+ toolResult: guarded.result,
1095
+ ...(guarded.truncated
1096
+ ? { friction: "result_too_large" as const }
1097
+ : {}),
1098
+ };
1111
1099
  }
1112
1100
  const value = fields
1113
1101
  ? projectionValue(
@@ -1116,19 +1104,26 @@ export function createMetaTools(
1116
1104
  resolved.definition.outputSchema,
1117
1105
  )
1118
1106
  : result;
1107
+ const guarded = await guardText(
1108
+ serializeResultText(value),
1109
+ results,
1110
+ cap,
1111
+ );
1119
1112
  return {
1120
- toolResult: await guardText(
1121
- serializeResultText(value),
1122
- results,
1123
- cap,
1124
- ),
1113
+ toolResult: guarded.result,
1125
1114
  value,
1115
+ ...(guarded.truncated
1116
+ ? { friction: "result_too_large" as const }
1117
+ : {}),
1126
1118
  };
1127
1119
  },
1120
+ activityFriction: (processed) => processed.friction,
1128
1121
  },
1129
1122
  );
1130
1123
  if (!outcome.ok) {
1124
+ const structuredRecovery = outcome.error.nextAction !== undefined;
1131
1125
  const failedResult =
1126
+ structuredRecovery ||
1132
1127
  outcome.error.code === "auth_required" ||
1133
1128
  outcome.error.code === "invalid_args" ||
1134
1129
  outcome.error.code === "input_required_unsupported" ||
@@ -1142,6 +1137,7 @@ export function createMetaTools(
1142
1137
  })
1143
1138
  : errorResult(outcome.error.message);
1144
1139
  if (
1140
+ structuredRecovery ||
1145
1141
  outcome.error.code === "auth_required" ||
1146
1142
  outcome.error.code === "invalid_args" ||
1147
1143
  outcome.error.code === "input_required_unsupported"
@@ -1187,143 +1183,18 @@ export function createMetaTools(
1187
1183
  type: "text",
1188
1184
  text:
1189
1185
  'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
1190
- listSkills(connectors, surface)
1186
+ listSkills(connectors)
1191
1187
  .map((skill) => `- \`${skill.name}\` — ${skill.description}`)
1192
1188
  .join("\n"),
1193
1189
  },
1194
1190
  ],
1195
1191
  };
1196
1192
  }
1197
- const skill = resolveSkill(args.name, connectors, surface);
1193
+ const skill = resolveSkill(args.name, connectors);
1198
1194
  if (!skill.found) return errorResult(skill.message);
1199
1195
  return { content: [{ type: "text", text: skill.content }] };
1200
1196
  },
1201
1197
 
1202
- async listConnectors(args: ListArgs = {}): Promise<ToolResult> {
1203
- const probe = args.probe ?? true;
1204
- // Live inventory owns a short-lived scope separate from the request's
1205
- // call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
1206
- const connectors = registry.listConnectors();
1207
- const scope = probe ? {} : requestScope;
1208
- const inspect = async (c: (typeof connectors)[number]) => {
1209
- const statusStarted = Date.now();
1210
- const observed = registry.healthFor(c.id);
1211
- const drift = await registry.credentialDriftFor(c.id);
1212
- let status:
1213
- | ConnectorStatus
1214
- | { state: "ok" | "error" | "unknown"; message?: string };
1215
- if (drift) {
1216
- status = { state: "auth_required", message: drift };
1217
- } else if (probe) {
1218
- try {
1219
- status = await withProbeDeadline(
1220
- `list_connectors probe of "${c.id}"`,
1221
- (options) =>
1222
- registry.statusFor(c.id, baseUrl, scope, options),
1223
- );
1224
- } catch (err) {
1225
- // A probe that outran probeTimeoutMs (or otherwise threw)
1226
- // degrades this connector to an error status rather than
1227
- // hanging the whole list_connectors call.
1228
- status = { state: "error", message: msg(err) };
1229
- }
1230
- } else {
1231
- const derived =
1232
- observed?.consecutiveFailures && observed.consecutiveFailures > 0
1233
- ? ("error" as const)
1234
- : registry.hasObservedSuccess(c.id) || c.kind === "api"
1235
- ? ("ok" as const)
1236
- : ("unknown" as const);
1237
- status = {
1238
- state: derived,
1239
- ...(observed?.lastError ? { message: observed.lastError } : {}),
1240
- };
1241
- }
1242
- // Stamped after any live probe so the response reports when its
1243
- // observation completed, not when a potentially slow request began.
1244
- const checkedAt = new Date().toISOString();
1245
- let tools = registry.peekTools(c.id);
1246
- // An auth_required status may have just started OAuth. A second
1247
- // listTools probe would overwrite its state/verifier while returning
1248
- // the first (now stale) authorization URL.
1249
- if (probe && status.state === "ok") {
1250
- try {
1251
- tools = await withProbeDeadline(
1252
- `list_connectors catalog refresh of "${c.id}"`,
1253
- (options) =>
1254
- registry.refreshTools(c.id, baseUrl, scope, options),
1255
- );
1256
- registry.recordSuccess(c.id, Date.now() - statusStarted);
1257
- } catch (err) {
1258
- const details = classifyCallError(err);
1259
- if (details.code === "auth_required") {
1260
- let authStatus: ConnectorStatus | undefined;
1261
- try {
1262
- authStatus = await withProbeDeadline(
1263
- `list_connectors authorization status of "${c.id}"`,
1264
- (options) =>
1265
- registry.statusFor(c.id, baseUrl, scope, options),
1266
- );
1267
- } catch {
1268
- // The typed auth verdict is still authoritative; this second
1269
- // read exists only to recover the connector's pending URL.
1270
- }
1271
- status =
1272
- authStatus?.state === "auth_required"
1273
- ? authStatus
1274
- : {
1275
- state: "auth_required" as const,
1276
- message: details.message,
1277
- };
1278
- } else {
1279
- status = { state: "error" as const, message: msg(err) };
1280
- }
1281
- registry.recordFailure(c.id, Date.now() - statusStarted, err);
1282
- }
1283
- }
1284
- const latencyMs = Date.now() - statusStarted;
1285
- const latestObserved = registry.healthFor(c.id);
1286
- return {
1287
- id: c.id,
1288
- ...(c.title ? { title: c.title } : {}),
1289
- description: c.description,
1290
- toolCount: tools?.length ?? 0,
1291
- status: status.state,
1292
- checkedAt,
1293
- latencyMs,
1294
- probe,
1295
- ...(latestObserved ?? observed),
1296
- ...("authorizationUrl" in status &&
1297
- status.authorizationUrl && {
1298
- authorizationUrl: status.authorizationUrl,
1299
- }),
1300
- ...(status.message && { message: status.message }),
1301
- };
1302
- };
1303
- const settled = await mapSettledWithConcurrency(
1304
- connectors,
1305
- discoveryConcurrency,
1306
- inspect,
1307
- );
1308
- if (probe) {
1309
- await mapSettledWithConcurrency(
1310
- connectors,
1311
- discoveryConcurrency,
1312
- (connector) =>
1313
- closeConnectorScope(
1314
- connector,
1315
- registry.contextFor(connector.id, baseUrl, scope),
1316
- opts.defer,
1317
- ),
1318
- );
1319
- }
1320
- const out = settled.map((result) => {
1321
- if (result.status === "rejected") throw result.reason;
1322
- return result.value;
1323
- });
1324
- return jsonResult({ connectors: out });
1325
- },
1326
-
1327
1198
  async searchTools(args: SearchArgs): Promise<ToolResult> {
1328
1199
  return discoveryResult(
1329
1200
  async () =>
@@ -1337,18 +1208,13 @@ export function createMetaTools(
1337
1208
  );
1338
1209
  },
1339
1210
 
1340
- async describeTools(args: DescribeArgs): Promise<ToolResult> {
1341
- return discoveryResult(
1342
- async () => ({ tools: await catalog.describe(args) }),
1343
- 'Split the address list or use format: "compact".',
1344
- );
1345
- },
1346
-
1347
1211
  async callTool(args: CallArgs): Promise<ToolResult> {
1348
1212
  return (await runCall(args, "call_tool")).toolResult;
1349
1213
  },
1350
1214
 
1351
- async callDestructiveTool(args: CallArgs): Promise<ToolResult> {
1215
+ async callDestructiveTool(args: DestructiveCallArgs): Promise<ToolResult> {
1216
+ // `reason` is read by the host's approval view and stops there — runCall
1217
+ // forwards only the call fields, so it never reaches the connector.
1352
1218
  return (
1353
1219
  await runCall(args, "call_destructive_tool", { allowDestructive: true })
1354
1220
  ).toolResult;
@@ -1413,143 +1279,6 @@ export function createMetaTools(
1413
1279
  });
1414
1280
  },
1415
1281
 
1416
- async batchCall(args: BatchArgs): Promise<ToolResult> {
1417
- const batchStarted = Date.now();
1418
- const settled = await Promise.allSettled(
1419
- args.calls.map((c) =>
1420
- runCall(
1421
- {
1422
- ...c,
1423
- ...((c.resultMode ?? args.resultMode) !== undefined
1424
- ? { resultMode: c.resultMode ?? args.resultMode }
1425
- : {}),
1426
- ...((c.timeoutMs ?? args.timeoutMs) !== undefined
1427
- ? { timeoutMs: c.timeoutMs ?? args.timeoutMs }
1428
- : {}),
1429
- ...((c.maxRetries ?? args.maxRetries) !== undefined
1430
- ? { maxRetries: c.maxRetries ?? args.maxRetries }
1431
- : {}),
1432
- ...((c.diagnostics ?? args.diagnostics) !== undefined
1433
- ? { diagnostics: c.diagnostics ?? args.diagnostics }
1434
- : {}),
1435
- },
1436
- "batch_call",
1437
- ),
1438
- ),
1439
- );
1440
- const results = settled.map((s, i) => {
1441
- const call = args.calls[i];
1442
- if (!call) {
1443
- throw new Error("Batch result has no corresponding call");
1444
- }
1445
- const { address } = call;
1446
- if (s.status === "rejected") {
1447
- return {
1448
- address,
1449
- ok: false,
1450
- error: msg(s.reason),
1451
- errorDetails: classifyCallError(s.reason, "batch_call_failed"),
1452
- };
1453
- }
1454
- const r = s.value;
1455
- if (r.error) {
1456
- return {
1457
- address,
1458
- ok: false,
1459
- error: r.error.message,
1460
- errorDetails: r.error,
1461
- durationMs: r.durationMs,
1462
- attempts: r.attempts,
1463
- ...((call.diagnostics ?? args.diagnostics)
1464
- ? { timing: r.timing }
1465
- : {}),
1466
- };
1467
- }
1468
- if ((call.resultMode ?? args.resultMode) === "value") {
1469
- return {
1470
- address,
1471
- ok: true,
1472
- data: r.value,
1473
- durationMs: r.durationMs,
1474
- attempts: r.attempts,
1475
- ...((call.diagnostics ?? args.diagnostics)
1476
- ? { timing: r.timing }
1477
- : {}),
1478
- };
1479
- }
1480
- return {
1481
- address,
1482
- ok: true,
1483
- result: r.toolResult.content,
1484
- durationMs: r.durationMs,
1485
- attempts: r.attempts,
1486
- ...((call.diagnostics ?? args.diagnostics)
1487
- ? { timing: r.timing }
1488
- : {}),
1489
- };
1490
- });
1491
- const envelope = {
1492
- results,
1493
- durationMs: Date.now() - batchStarted,
1494
- };
1495
- const text = serializeResultText(envelope);
1496
- const bytes = enc.encode(text);
1497
- if (bytes.length <= batchCap) return jsonResult(envelope);
1498
-
1499
- const notice = await stashResult(
1500
- text,
1501
- registry.resultsStorage(),
1502
- bytes.length,
1503
- );
1504
- return jsonResult({
1505
- results: results.map((result) => {
1506
- const common = {
1507
- address: batchSummaryString(result.address),
1508
- ok: !("error" in result),
1509
- ...("durationMs" in result
1510
- ? { durationMs: result.durationMs }
1511
- : {}),
1512
- ...("attempts" in result ? { attempts: result.attempts } : {}),
1513
- ...("timing" in result ? { timing: result.timing } : {}),
1514
- };
1515
- if (!("error" in result)) return common;
1516
- const error = result.error ?? "Batch call failed";
1517
- const details =
1518
- result.errorDetails ??
1519
- errorDetails("batch_call_failed", error);
1520
- return {
1521
- ...common,
1522
- error: batchSummaryString(error),
1523
- errorDetails: {
1524
- code: batchSummaryString(details.code),
1525
- message: batchSummaryString(details.message),
1526
- retryable: details.retryable,
1527
- ...(details.retryAfterMs !== undefined
1528
- ? { retryAfterMs: details.retryAfterMs }
1529
- : {}),
1530
- ...(details.connector !== undefined
1531
- ? { connector: batchSummaryString(details.connector) }
1532
- : {}),
1533
- ...(details.operation !== undefined
1534
- ? { operation: batchSummaryString(details.operation) }
1535
- : {}),
1536
- ...(details.recovery !== undefined
1537
- ? { recovery: details.recovery }
1538
- : {}),
1539
- ...(details.nextAction !== undefined
1540
- ? { nextAction: details.nextAction }
1541
- : {}),
1542
- ...(details.retry !== undefined
1543
- ? { retry: batchSummaryString(details.retry) }
1544
- : {}),
1545
- },
1546
- };
1547
- }),
1548
- durationMs: envelope.durationMs,
1549
- ...notice,
1550
- });
1551
- },
1552
-
1553
1282
  async authorizeConnector(args: AuthorizeArgs): Promise<ToolResult> {
1554
1283
  const connector = registry.getConnector(args.connector);
1555
1284
  if (!connector) {
@@ -1626,7 +1355,8 @@ export function createMetaTools(
1626
1355
  ? {
1627
1356
  authorizationUrl: status.authorizationUrl,
1628
1357
  instructions:
1629
- "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.",
1358
+ "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. " +
1359
+ oauthFollowUp(connector.id),
1630
1360
  }
1631
1361
  : {}),
1632
1362
  ...(status.message ? { message: status.message } : {}),
@@ -1642,39 +1372,19 @@ export function createMetaTools(
1642
1372
  };
1643
1373
  }
1644
1374
 
1645
- const LIST_DESC =
1646
- "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.";
1647
1375
  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.`;
1648
- 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.`;
1649
1376
  const CALL_DESC =
1650
- '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.';
1377
+ '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.';
1651
1378
  const CALL_DESTRUCTIVE_DESC =
1652
- "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.";
1379
+ "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.";
1653
1380
  const GET_RESULT_DESC =
1654
- "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.";
1655
- const BATCH_DESC =
1656
- "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.";
1381
+ "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.";
1657
1382
  const AUTHORIZE_DESC =
1658
1383
  "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.";
1659
1384
  const SKILLS_DESC =
1660
1385
  '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.';
1661
1386
 
1662
- /**
1663
- * Code-first replacements for the descriptions that route work between tools.
1664
- * Every one of these mentions a tool the consolidated surface removed, so on a
1665
- * code-first deployment the routing sentence has to point at the in-program
1666
- * function that took the work over — a description naming `batch_call` on a
1667
- * surface without one teaches a call that cannot succeed.
1668
- *
1669
- * The classic strings above are left byte-for-byte alone: classic is the
1670
- * compatibility surface and the eval's control arm, and rewording it would
1671
- * change what that control measures.
1672
- */
1673
- const CODE_FIRST_SEARCH_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
1674
- const CODE_FIRST_CALL_DESC =
1675
- '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.';
1676
- const CODE_FIRST_GET_RESULT_DESC =
1677
- "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.";
1387
+ const SEARCH_WITH_DESCRIBE_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
1678
1388
 
1679
1389
  /**
1680
1390
  * Sentences appended to a meta-tool description only when this connection
@@ -1689,8 +1399,6 @@ const GUIDE_NOTES = {
1689
1399
  ' 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.',
1690
1400
  search:
1691
1401
  " A connector group carrying `guide` has a usage guide; fetch it with skills({ name: <guide> }).",
1692
- describe:
1693
- " An entry carrying `guide` belongs to a connector with a usage guide; fetch it with skills({ name: <guide> }).",
1694
1402
  } as const;
1695
1403
 
1696
1404
  /** `base`, plus its guide note when any VISIBLE connector carries a guide. */
@@ -1735,15 +1443,12 @@ const CALL_INPUT_SCHEMA = {
1735
1443
  };
1736
1444
 
1737
1445
  /**
1738
- * Register the base meta-tools onto an McpServer instance: nine on the classic
1739
- * surface, six on the code-first one, where `list_connectors`,
1740
- * `describe_tools`, and `batch_call` have folded into the program surface
1741
- * (`registerExecuteTool` adds the seventh, `execute_code`).
1742
- *
1743
- * Only the registrations differ. Every handler still exists on the object
1744
- * `createMetaTools` returns, and a folded tool's behavior is reached through
1745
- * `connecta.search` / `connecta.describe` / `connecta.batch` inside a program —
1746
- * the same code paths, one layer down.
1446
+ * Register the six explicit meta-tools onto an McpServer instance.
1447
+ * `registerExecuteTool` adds the seventh, `execute_code`. Broad discovery and
1448
+ * multi-call work is reached through `connecta.search` / `connecta.describe` /
1449
+ * `connecta.batch` inside a program, which `execute_code` builds over the same
1450
+ * `CatalogService` and `InvocationService` these handlers use — one shared
1451
+ * services layer, two adapters above it.
1747
1452
  */
1748
1453
  export function registerMetaTools(
1749
1454
  server: McpServer,
@@ -1755,15 +1460,9 @@ export function registerMetaTools(
1755
1460
  discoveryConcurrency?: number;
1756
1461
  activity?: ActivityRequestContext;
1757
1462
  requestSignal?: AbortSignal;
1758
- defer?: DeferredWork;
1759
- /** The advertised surface. Default `classic`. */
1760
- surface?: ConnectaSurface;
1761
1463
  },
1762
1464
  ): void {
1763
- const surface: ConnectaSurface = ctx.surface ?? "classic";
1764
- const codeFirst = surface === "code-first";
1765
1465
  const mt = createMetaTools(registry, ctx.baseUrl, {
1766
- surface,
1767
1466
  ...(ctx.defaultToolTimeoutMs !== undefined
1768
1467
  ? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
1769
1468
  : {}),
@@ -1777,7 +1476,6 @@ export function registerMetaTools(
1777
1476
  ...(ctx.requestSignal !== undefined
1778
1477
  ? { requestSignal: ctx.requestSignal }
1779
1478
  : {}),
1780
- ...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
1781
1479
  });
1782
1480
 
1783
1481
  server.registerTool(
@@ -1790,28 +1488,12 @@ export function registerMetaTools(
1790
1488
  async (args) => mt.skills(args as SkillArgs),
1791
1489
  );
1792
1490
 
1793
- // Folded on the code-first surface: a program browses the same inventory with
1794
- // connecta.search({}) (every catalog) or connecta.search({ connector }) (one).
1795
- // Live connector probing is an operator concern, not a model one — it stays on
1796
- // the operator pages and /health, which is where the ethos puts observability.
1797
- if (!codeFirst) {
1798
- server.registerTool(
1799
- "list_connectors",
1800
- {
1801
- description: LIST_DESC,
1802
- inputSchema: z.object({ probe: z.boolean().optional() }),
1803
- annotations: READ_ONLY_REMOTE,
1804
- },
1805
- async (args) => mt.listConnectors(args as ListArgs),
1806
- );
1807
- }
1808
-
1809
1491
  server.registerTool(
1810
1492
  "search_tools",
1811
1493
  {
1812
1494
  description: describedFor(
1813
1495
  registry,
1814
- codeFirst ? CODE_FIRST_SEARCH_DESC : SEARCH_DESC,
1496
+ SEARCH_WITH_DESCRIBE_DESC,
1815
1497
  "search",
1816
1498
  ),
1817
1499
  inputSchema: z.object({
@@ -1830,28 +1512,10 @@ export function registerMetaTools(
1830
1512
  async (args) => mt.searchTools(args as SearchArgs),
1831
1513
  );
1832
1514
 
1833
- // Folded on the code-first surface: connecta.describe takes the same
1834
- // addresses, format, and per-address error reporting inside a program.
1835
- if (!codeFirst) {
1836
- server.registerTool(
1837
- "describe_tools",
1838
- {
1839
- description: describedFor(registry, DESCRIBE_DESC, "describe"),
1840
- inputSchema: z.object({
1841
- addresses: z.array(z.string()).max(MAX_DESCRIBE_ADDRESSES),
1842
- format: z.enum(["compact", "json"]).optional(),
1843
- fullDescriptions: z.boolean().optional(),
1844
- }),
1845
- annotations: READ_ONLY_REMOTE,
1846
- },
1847
- async (args) => mt.describeTools(args as DescribeArgs),
1848
- );
1849
- }
1850
-
1851
1515
  server.registerTool(
1852
1516
  "call_tool",
1853
1517
  {
1854
- description: codeFirst ? CODE_FIRST_CALL_DESC : CALL_DESC,
1518
+ description: CALL_DESC,
1855
1519
  inputSchema: z.object(CALL_INPUT_SCHEMA),
1856
1520
  // call_tool admits only tools that are themselves explicitly read-only;
1857
1521
  // anything else is refused and routed to call_destructive_tool.
@@ -1864,14 +1528,29 @@ export function registerMetaTools(
1864
1528
  "call_destructive_tool",
1865
1529
  {
1866
1530
  description: CALL_DESTRUCTIVE_DESC,
1867
- inputSchema: z.object(CALL_INPUT_SCHEMA),
1531
+ inputSchema: z.object({
1532
+ ...CALL_INPUT_SCHEMA,
1533
+ // Bounded above, but with no lower bound: a model that sends `""` or
1534
+ // whitespace has written no reason, and failing an entire consequential
1535
+ // call over a cosmetic field the host merely displays is the wrong
1536
+ // trade. It is normalized to absent below instead.
1537
+ reason: z.string().max(500).optional(),
1538
+ }),
1868
1539
  annotations: {
1869
1540
  destructiveHint: true,
1870
1541
  readOnlyHint: false,
1871
1542
  openWorldHint: true,
1872
1543
  },
1873
1544
  },
1874
- async (args) => mt.callDestructiveTool(args as CallArgs),
1545
+ async (args) => {
1546
+ // `reason` is the host's to display and connecta's to keep out of the
1547
+ // downstream call, so this destructuring is the whole of its handling:
1548
+ // nothing below reads it. Dropping it is also what makes an empty or
1549
+ // whitespace-only one "absent" rather than a validation failure — there
1550
+ // is no field left for it to be absent from.
1551
+ const { reason: _hostContext, ...call } = args as DestructiveCallArgs;
1552
+ return mt.callDestructiveTool(call);
1553
+ },
1875
1554
  );
1876
1555
 
1877
1556
  server.registerTool(
@@ -1896,7 +1575,7 @@ export function registerMetaTools(
1896
1575
  server.registerTool(
1897
1576
  "get_result",
1898
1577
  {
1899
- description: codeFirst ? CODE_FIRST_GET_RESULT_DESC : GET_RESULT_DESC,
1578
+ description: GET_RESULT_DESC,
1900
1579
  inputSchema: z.object({
1901
1580
  id: z.string(),
1902
1581
  // Both bounds are the shared rules (isValidResultOffset,
@@ -1910,29 +1589,4 @@ export function registerMetaTools(
1910
1589
  },
1911
1590
  async (args) => mt.getResult(args as GetResultArgs),
1912
1591
  );
1913
-
1914
- // Folded on the code-first surface: connecta.batch runs the same 1–10
1915
- // parallel read-only calls and returns the same typed per-call outcomes.
1916
- if (!codeFirst) {
1917
- server.registerTool(
1918
- "batch_call",
1919
- {
1920
- description: BATCH_DESC,
1921
- inputSchema: z.object({
1922
- calls: z
1923
- .array(z.object(CALL_INPUT_SCHEMA))
1924
- .min(1)
1925
- .max(10),
1926
- resultMode: z.enum(["mcp", "value"]).optional(),
1927
- timeoutMs: z.number().int().positive().optional(),
1928
- maxRetries: z.number().int().min(0).max(2).optional(),
1929
- diagnostics: z.boolean().optional(),
1930
- }),
1931
- // Same gate as call_tool: every call in the batch must be explicitly
1932
- // read-only or the batch is refused.
1933
- annotations: READ_ONLY_REMOTE,
1934
- },
1935
- async (args) => mt.batchCall(args as BatchArgs),
1936
- );
1937
- }
1938
1592
  }