@zackbart/connecta 0.10.6 → 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 (80) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +5 -4
  4. package/bin/connecta.mjs +0 -7
  5. package/dist/activity.d.ts.map +1 -1
  6. package/dist/activity.js.map +1 -1
  7. package/dist/catalog-service.d.ts +2 -17
  8. package/dist/catalog-service.d.ts.map +1 -1
  9. package/dist/catalog-service.js +4 -6
  10. package/dist/catalog-service.js.map +1 -1
  11. package/dist/connectors/api.d.ts +2 -2
  12. package/dist/connectors/remote-mcp.d.ts +1 -1
  13. package/dist/errors.d.ts +1 -3
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +1 -1
  16. package/dist/errors.js.map +1 -1
  17. package/dist/execute.d.ts +1 -3
  18. package/dist/execute.d.ts.map +1 -1
  19. package/dist/execute.js +7 -28
  20. package/dist/execute.js.map +1 -1
  21. package/dist/index.d.ts +15 -31
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +23 -37
  24. package/dist/index.js.map +1 -1
  25. package/dist/invocation.js +2 -2
  26. package/dist/invocation.js.map +1 -1
  27. package/dist/meta-tools.d.ts +19 -58
  28. package/dist/meta-tools.d.ts.map +1 -1
  29. package/dist/meta-tools.js +34 -431
  30. package/dist/meta-tools.js.map +1 -1
  31. package/dist/registry.d.ts +1 -10
  32. package/dist/registry.d.ts.map +1 -1
  33. package/dist/registry.js +3 -15
  34. package/dist/registry.js.map +1 -1
  35. package/dist/routes/mcp.d.ts.map +1 -1
  36. package/dist/routes/mcp.js +19 -30
  37. package/dist/routes/mcp.js.map +1 -1
  38. package/dist/routes/shared.d.ts +5 -11
  39. package/dist/routes/shared.d.ts.map +1 -1
  40. package/dist/routes/shared.js.map +1 -1
  41. package/dist/server.js +5 -4
  42. package/dist/server.js.map +1 -1
  43. package/dist/skills.d.ts +8 -18
  44. package/dist/skills.d.ts.map +1 -1
  45. package/dist/skills.js +13 -60
  46. package/dist/skills.js.map +1 -1
  47. package/dist/types.d.ts +6 -20
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/documentation/code-first-exploration.md +16 -16
  52. package/documentation/code-mode.md +21 -35
  53. package/documentation/connectors.md +1 -1
  54. package/documentation/meta-tools.md +30 -43
  55. package/documentation/rich-output-design.md +4 -4
  56. package/examples/node/README.md +1 -2
  57. package/examples/node/src/index.ts +1 -3
  58. package/examples/worker/README.md +8 -13
  59. package/examples/worker/src/index.ts +6 -14
  60. package/examples/worker/wrangler.jsonc +3 -6
  61. package/package.json +1 -1
  62. package/src/activity.ts +5 -0
  63. package/src/catalog-service.ts +6 -26
  64. package/src/connectors/api.ts +2 -2
  65. package/src/connectors/remote-mcp.ts +1 -1
  66. package/src/errors.ts +2 -2
  67. package/src/execute.ts +7 -36
  68. package/src/index.ts +40 -69
  69. package/src/invocation.ts +2 -2
  70. package/src/meta-tools.ts +38 -565
  71. package/src/registry.ts +2 -33
  72. package/src/routes/mcp.ts +19 -30
  73. package/src/routes/shared.ts +4 -11
  74. package/src/server.ts +7 -7
  75. package/src/skills.ts +11 -74
  76. package/src/types.ts +6 -21
  77. package/src/version.ts +1 -1
  78. package/templates/node/README.md +2 -1
  79. package/templates/node/package.json +1 -1
  80. package/templates/node/src/index.ts +1 -1
package/src/meta-tools.ts CHANGED
@@ -14,21 +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
- boundedEchoText,
27
- classifyCallError,
28
- echoedCallArgs,
29
- messageLooksRetryable,
30
- type CallErrorDetails,
31
- } from "./errors.js";
17
+ import { resolveDiscoveryConcurrency } from "./concurrency.js";
18
+ import type { CallErrorDetails } from "./errors.js";
32
19
  import {
33
20
  InvocationService,
34
21
  MAX_RETRY_BACKOFF_MS,
@@ -49,13 +36,8 @@ import {
49
36
  import {
50
37
  DEFAULT_PROBE_TIMEOUT_MS,
51
38
  normalizeTimeoutMs,
52
- withAbortableTimeout,
53
39
  } from "./timeout.js";
54
- import type {
55
- ConnectaSurface,
56
- ConnectorStatus,
57
- KVStorage,
58
- } from "./types.js";
40
+ import type { KVStorage } from "./types.js";
59
41
 
60
42
  export {
61
43
  MAX_DESCRIBE_ADDRESSES,
@@ -97,10 +79,6 @@ function msg(err: unknown): string {
97
79
  return err instanceof Error ? err.message : String(err);
98
80
  }
99
81
 
100
- function errorDetails(code: string, message: string): CallErrorDetails {
101
- return { code, message, retryable: messageLooksRetryable(message) };
102
- }
103
-
104
82
  function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
105
83
  const result = jsonResult({
106
84
  error: {
@@ -134,7 +112,6 @@ async function discoveryResult(
134
112
  }
135
113
  }
136
114
 
137
-
138
115
  /** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
139
116
  function isContinuationByte(b: number | undefined): boolean {
140
117
  return b !== undefined && (b & 0xc0) === 0x80;
@@ -812,98 +789,6 @@ interface GuardedResult<T> {
812
789
  truncated: boolean;
813
790
  }
814
791
 
815
- /**
816
- * Keep an oversized batch's inline outcome summary at fixed string overhead.
817
- * The same clamp the error envelopes use — one budget, one marker, defined
818
- * once in `errors.ts` so the two cannot drift apart.
819
- */
820
- function batchSummaryString(value: string): string {
821
- return boundedEchoText(value);
822
- }
823
-
824
- /** Candidate addresses kept in an oversized batch's summary of one ambiguity. */
825
- const MAX_SUMMARY_ADDRESSES = 10;
826
-
827
- /**
828
- * A recovery route rebuilt field by field so the summary above keeps its
829
- * promise. Spreading `nextAction` through raw would reopen the hole this
830
- * function closes: every variant carries free-form strings, and one of them
831
- * carries the caller's arguments, which is exactly the payload an oversized
832
- * batch was already too large to hold.
833
- */
834
- function batchSummaryNextAction(
835
- nextAction: NonNullable<CallErrorDetails["nextAction"]>,
836
- ): NonNullable<CallErrorDetails["nextAction"]> {
837
- if ("function" in nextAction) {
838
- // A batch runs on the top-level catalog, whose search route is the tool, so
839
- // the function-keyed discovery variant does not arrive here today. Rebuild
840
- // it anyway: a guard that silently dropped an unrecognized route would turn
841
- // a recovery record into nothing at exactly the moment one is needed.
842
- if (nextAction.function === "connecta.search") {
843
- return {
844
- function: "connecta.search",
845
- arguments: batchSummarySearchArgs(nextAction.arguments),
846
- purpose: batchSummaryString(nextAction.purpose),
847
- };
848
- }
849
- // Say so when the candidate list is clipped. The unclipped purpose reads
850
- // "choose the intended canonical address", which is a lie about a list
851
- // that no longer contains every candidate — and the caller has no other
852
- // way to learn that the address it wants was the eleventh.
853
- const candidates = nextAction.addresses.slice(0, MAX_SUMMARY_ADDRESSES);
854
- return {
855
- function: nextAction.function,
856
- addresses: candidates.map(batchSummaryString),
857
- purpose: batchSummaryString(
858
- candidates.length < nextAction.addresses.length
859
- ? `${nextAction.purpose} Showing the first ${candidates.length} of ` +
860
- `${nextAction.addresses.length} candidates; re-run the call on its ` +
861
- "own to see them all."
862
- : nextAction.purpose,
863
- ),
864
- };
865
- }
866
- if (nextAction.tool === "authorize_connector") {
867
- return {
868
- tool: "authorize_connector",
869
- arguments: {
870
- connector: batchSummaryString(nextAction.arguments.connector),
871
- },
872
- operatorHandoff: batchSummaryString(nextAction.operatorHandoff),
873
- };
874
- }
875
- if (nextAction.tool === "call_destructive_tool") {
876
- return {
877
- tool: "call_destructive_tool",
878
- arguments: {
879
- address: batchSummaryString(nextAction.arguments.address),
880
- ...echoedCallArgs(nextAction.arguments.args),
881
- },
882
- purpose: batchSummaryString(nextAction.purpose),
883
- };
884
- }
885
- return {
886
- tool: "search_tools",
887
- arguments: batchSummarySearchArgs(nextAction.arguments),
888
- purpose: batchSummaryString(nextAction.purpose),
889
- };
890
- }
891
-
892
- /** The scoping arguments both discovery routes carry, bounded the same way. */
893
- function batchSummarySearchArgs(args: {
894
- query: string;
895
- connector?: string;
896
- includeSchemas: "compact";
897
- }): { query: string; connector?: string; includeSchemas: "compact" } {
898
- return {
899
- query: batchSummaryString(args.query),
900
- ...(args.connector !== undefined
901
- ? { connector: batchSummaryString(args.connector) }
902
- : {}),
903
- includeSchemas: "compact",
904
- };
905
- }
906
-
907
792
  /**
908
793
  * Return `text` as a single content block; if it exceeds `cap` bytes, stash the
909
794
  * full text and return the first `cap` bytes followed by a JSON truncation
@@ -1027,17 +912,6 @@ export interface SearchArgs {
1027
912
  fullDescriptions?: boolean;
1028
913
  includeSchemas?: "compact" | "json";
1029
914
  }
1030
- export type DescribeArgs = (
1031
- | { address: string; addresses?: never }
1032
- | { address?: never; addresses: string[] }
1033
- ) & {
1034
- format?: "compact" | "json";
1035
- fullDescriptions?: boolean;
1036
- };
1037
- export interface ListArgs {
1038
- /** When false, return cached/observed health without downstream I/O. */
1039
- probe?: boolean;
1040
- }
1041
915
  type ResultMode = "mcp" | "value";
1042
916
  export interface CallArgs {
1043
917
  address: string;
@@ -1064,14 +938,6 @@ export interface GetResultArgs {
1064
938
  /** Page size in bytes; a whole number >= 1. Defaults to the deployment cap. */
1065
939
  maxBytes?: number;
1066
940
  }
1067
- export type BatchCall = CallArgs;
1068
- export interface BatchArgs {
1069
- calls: BatchCall[];
1070
- resultMode?: ResultMode;
1071
- timeoutMs?: number;
1072
- maxRetries?: number;
1073
- diagnostics?: boolean;
1074
- }
1075
941
  export interface AuthorizeArgs {
1076
942
  connector: string;
1077
943
  force?: boolean;
@@ -1082,31 +948,26 @@ export interface SkillArgs {
1082
948
 
1083
949
  /**
1084
950
  * The sentence that closes the OAuth handoff, telling the operator's agent how
1085
- * to confirm the flow landed. `authorize_connector` is registered on both
1086
- * surfaces but `list_connectors` is not, so the classic status check cannot be
1087
- * the only one offered: a code-first agent handed that advice gets an
1088
- * unknown-tool error at exactly the moment it is trying to recover. It gets the
1089
- * check its own surface serves instead — the same folded-name defect as the
1090
- * describe path (#261), one tool result further along.
951
+ * to confirm the flow landed through the one surface it can call.
1091
952
  */
1092
- function oauthFollowUp(surface: ConnectaSurface, connectorId: string): string {
1093
- return surface === "code-first"
1094
- ? `Then retry the original call; connecta.search({ connector: ${JSON.stringify(connectorId)} }) inside execute_code confirms the catalog now loads.`
1095
- : "Re-run list_connectors afterwards to confirm status is ok.";
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.`;
1096
955
  }
1097
956
 
1098
957
  /**
1099
- * Every base meta-tool handler over a registry all nine, whichever surface is
1100
- * advertised, since folding a tool away only skips its registration and never
1101
- * its handler. Exported for direct testing; registerMetaTools() wires the ones
1102
- * this surface advertises onto an McpServer. `opts.defaultToolTimeoutMs`
1103
- * supplies a deadline for calls that don't carry one. (execute_code is
1104
- * registered separately by registerExecuteTool.)
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.
1105
967
  *
1106
968
  * Deployment-wide result-size caps are read off the registry view rather than
1107
- * passed in: `ConnectaConfig.calls.maxResultBytes`, its per-connector override,
1108
- * and the independent `calls.maxBatchResultBytes` final-envelope boundary each
1109
- * 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.
1110
971
  */
1111
972
  export function createMetaTools(
1112
973
  registry: RegistryView,
@@ -1114,27 +975,17 @@ export function createMetaTools(
1114
975
  opts: {
1115
976
  /** Deadline applied when a call passes no `timeoutMs`. Off when unset. */
1116
977
  defaultToolTimeoutMs?: number;
1117
- /** 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. */
1118
979
  probeTimeoutMs?: number;
1119
980
  /** Maximum simultaneous connector discovery operations. Default 4. */
1120
981
  discoveryConcurrency?: number;
1121
982
  activity?: ActivityRequestContext;
1122
- /** Inbound request cancellation shared by direct and batch child calls. */
983
+ /** Inbound request cancellation shared by every call this request makes. */
1123
984
  requestSignal?: AbortSignal;
1124
- /** Runtime continuation for the bounded tail of probe-owned teardown. */
1125
- defer?: DeferredWork;
1126
- /**
1127
- * The advertised surface, which the `skills` guidance must match: a
1128
- * code-first deployment never gets guidance naming a tool it does not
1129
- * advertise. Default `classic`.
1130
- */
1131
- surface?: ConnectaSurface;
1132
985
  } = {},
1133
986
  ) {
1134
- const surface: ConnectaSurface = opts.surface ?? "classic";
1135
987
  // Already normalized and warned about at registry construction.
1136
988
  const globalCap = registry.maxResultBytes;
1137
- const batchCap = registry.maxBatchResultBytes;
1138
989
  const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
1139
990
  const probeTimeoutMs =
1140
991
  normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
@@ -1149,28 +1000,10 @@ export function createMetaTools(
1149
1000
  requestScope,
1150
1001
  probeTimeoutMs,
1151
1002
  concurrency: discoveryConcurrency,
1152
- // These handlers are the top-level tools, so the route is the advertised
1153
- // one; in-program describes route through connecta.describe instead and
1154
- // are built with their own CatalogService in execute.ts.
1155
- describeRoute:
1156
- surface === "code-first" ? "connecta.describe" : "describe_tools",
1157
- // searchRoute keeps its default: unlike describe_tools, search_tools is
1158
- // served by both advertised surfaces, so a top-level handler has nothing to
1159
- // derive. Only an in-program caller needs to be sent to connecta.search.
1003
+ // searchRoute keeps its top-level default. In-program callers use a
1004
+ // separate CatalogService configured for connecta.search.
1160
1005
  });
1161
1006
  const invocation = new InvocationService(registry, catalog, opts.activity);
1162
- const withProbeDeadline = <T>(
1163
- label: string,
1164
- operation: (options: {
1165
- signal: AbortSignal;
1166
- timeoutMs: number;
1167
- }) => Promise<T>,
1168
- ) =>
1169
- withAbortableTimeout(
1170
- (signal) => operation({ signal, timeoutMs: probeTimeoutMs }),
1171
- probeTimeoutMs,
1172
- label,
1173
- );
1174
1007
 
1175
1008
  interface RunCallOutcome {
1176
1009
  toolResult: ToolResult;
@@ -1194,7 +1027,7 @@ export function createMetaTools(
1194
1027
 
1195
1028
  /** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
1196
1029
  async function runCall(
1197
- call: BatchCall,
1030
+ call: CallArgs,
1198
1031
  source: ActivityCallSource,
1199
1032
  options: { allowDestructive?: boolean } = {},
1200
1033
  ): Promise<RunCallOutcome> {
@@ -1220,7 +1053,7 @@ export function createMetaTools(
1220
1053
  processResult: async (result, resolved) => {
1221
1054
  // Result-size cap for THIS call: the connector's own override wins,
1222
1055
  // then the deployment-wide value, then the built-in default (already
1223
- // folded into `globalCap`). Resolved per call so one batch_call can
1056
+ // folded into `globalCap`). Resolved per call so one request can
1224
1057
  // mix a tight-capped connector with siblings on the global cap. An
1225
1058
  // override the registry already warned about at startup is dropped
1226
1059
  // here, so the connector simply inherits `globalCap`.
@@ -1350,143 +1183,18 @@ export function createMetaTools(
1350
1183
  type: "text",
1351
1184
  text:
1352
1185
  'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
1353
- listSkills(connectors, surface)
1186
+ listSkills(connectors)
1354
1187
  .map((skill) => `- \`${skill.name}\` — ${skill.description}`)
1355
1188
  .join("\n"),
1356
1189
  },
1357
1190
  ],
1358
1191
  };
1359
1192
  }
1360
- const skill = resolveSkill(args.name, connectors, surface);
1193
+ const skill = resolveSkill(args.name, connectors);
1361
1194
  if (!skill.found) return errorResult(skill.message);
1362
1195
  return { content: [{ type: "text", text: skill.content }] };
1363
1196
  },
1364
1197
 
1365
- async listConnectors(args: ListArgs = {}): Promise<ToolResult> {
1366
- const probe = args.probe ?? true;
1367
- // Live inventory owns a short-lived scope separate from the request's
1368
- // call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
1369
- const connectors = registry.listConnectors();
1370
- const scope = probe ? {} : requestScope;
1371
- const inspect = async (c: (typeof connectors)[number]) => {
1372
- const statusStarted = Date.now();
1373
- const observed = registry.healthFor(c.id);
1374
- const drift = await registry.credentialDriftFor(c.id);
1375
- let status:
1376
- | ConnectorStatus
1377
- | { state: "ok" | "error" | "unknown"; message?: string };
1378
- if (drift) {
1379
- status = { state: "auth_required", message: drift };
1380
- } else if (probe) {
1381
- try {
1382
- status = await withProbeDeadline(
1383
- `list_connectors probe of "${c.id}"`,
1384
- (options) =>
1385
- registry.statusFor(c.id, baseUrl, scope, options),
1386
- );
1387
- } catch (err) {
1388
- // A probe that outran probeTimeoutMs (or otherwise threw)
1389
- // degrades this connector to an error status rather than
1390
- // hanging the whole list_connectors call.
1391
- status = { state: "error", message: msg(err) };
1392
- }
1393
- } else {
1394
- const derived =
1395
- observed?.consecutiveFailures && observed.consecutiveFailures > 0
1396
- ? ("error" as const)
1397
- : registry.hasObservedSuccess(c.id) || c.kind === "api"
1398
- ? ("ok" as const)
1399
- : ("unknown" as const);
1400
- status = {
1401
- state: derived,
1402
- ...(observed?.lastError ? { message: observed.lastError } : {}),
1403
- };
1404
- }
1405
- // Stamped after any live probe so the response reports when its
1406
- // observation completed, not when a potentially slow request began.
1407
- const checkedAt = new Date().toISOString();
1408
- let tools = registry.peekTools(c.id);
1409
- // An auth_required status may have just started OAuth. A second
1410
- // listTools probe would overwrite its state/verifier while returning
1411
- // the first (now stale) authorization URL.
1412
- if (probe && status.state === "ok") {
1413
- try {
1414
- tools = await withProbeDeadline(
1415
- `list_connectors catalog refresh of "${c.id}"`,
1416
- (options) =>
1417
- registry.refreshTools(c.id, baseUrl, scope, options),
1418
- );
1419
- registry.recordSuccess(c.id, Date.now() - statusStarted);
1420
- } catch (err) {
1421
- const details = classifyCallError(err);
1422
- if (details.code === "auth_required") {
1423
- let authStatus: ConnectorStatus | undefined;
1424
- try {
1425
- authStatus = await withProbeDeadline(
1426
- `list_connectors authorization status of "${c.id}"`,
1427
- (options) =>
1428
- registry.statusFor(c.id, baseUrl, scope, options),
1429
- );
1430
- } catch {
1431
- // The typed auth verdict is still authoritative; this second
1432
- // read exists only to recover the connector's pending URL.
1433
- }
1434
- status =
1435
- authStatus?.state === "auth_required"
1436
- ? authStatus
1437
- : {
1438
- state: "auth_required" as const,
1439
- message: details.message,
1440
- };
1441
- } else {
1442
- status = { state: "error" as const, message: msg(err) };
1443
- }
1444
- registry.recordFailure(c.id, Date.now() - statusStarted, err);
1445
- }
1446
- }
1447
- const latencyMs = Date.now() - statusStarted;
1448
- const latestObserved = registry.healthFor(c.id);
1449
- return {
1450
- id: c.id,
1451
- ...(c.title ? { title: c.title } : {}),
1452
- description: c.description,
1453
- toolCount: tools?.length ?? 0,
1454
- status: status.state,
1455
- checkedAt,
1456
- latencyMs,
1457
- probe,
1458
- ...(latestObserved ?? observed),
1459
- ...("authorizationUrl" in status &&
1460
- status.authorizationUrl && {
1461
- authorizationUrl: status.authorizationUrl,
1462
- }),
1463
- ...(status.message && { message: status.message }),
1464
- };
1465
- };
1466
- const settled = await mapSettledWithConcurrency(
1467
- connectors,
1468
- discoveryConcurrency,
1469
- inspect,
1470
- );
1471
- if (probe) {
1472
- await mapSettledWithConcurrency(
1473
- connectors,
1474
- discoveryConcurrency,
1475
- (connector) =>
1476
- closeConnectorScope(
1477
- connector,
1478
- registry.contextFor(connector.id, baseUrl, scope),
1479
- opts.defer,
1480
- ),
1481
- );
1482
- }
1483
- const out = settled.map((result) => {
1484
- if (result.status === "rejected") throw result.reason;
1485
- return result.value;
1486
- });
1487
- return jsonResult({ connectors: out });
1488
- },
1489
-
1490
1198
  async searchTools(args: SearchArgs): Promise<ToolResult> {
1491
1199
  return discoveryResult(
1492
1200
  async () =>
@@ -1500,13 +1208,6 @@ export function createMetaTools(
1500
1208
  );
1501
1209
  },
1502
1210
 
1503
- async describeTools(args: DescribeArgs): Promise<ToolResult> {
1504
- return discoveryResult(
1505
- async () => ({ tools: await catalog.describe(args) }),
1506
- 'Split the address list or use format: "compact".',
1507
- );
1508
- },
1509
-
1510
1211
  async callTool(args: CallArgs): Promise<ToolResult> {
1511
1212
  return (await runCall(args, "call_tool")).toolResult;
1512
1213
  },
@@ -1578,143 +1279,6 @@ export function createMetaTools(
1578
1279
  });
1579
1280
  },
1580
1281
 
1581
- async batchCall(args: BatchArgs): Promise<ToolResult> {
1582
- const batchStarted = Date.now();
1583
- const settled = await Promise.allSettled(
1584
- args.calls.map((c) =>
1585
- runCall(
1586
- {
1587
- ...c,
1588
- ...((c.resultMode ?? args.resultMode) !== undefined
1589
- ? { resultMode: c.resultMode ?? args.resultMode }
1590
- : {}),
1591
- ...((c.timeoutMs ?? args.timeoutMs) !== undefined
1592
- ? { timeoutMs: c.timeoutMs ?? args.timeoutMs }
1593
- : {}),
1594
- ...((c.maxRetries ?? args.maxRetries) !== undefined
1595
- ? { maxRetries: c.maxRetries ?? args.maxRetries }
1596
- : {}),
1597
- ...((c.diagnostics ?? args.diagnostics) !== undefined
1598
- ? { diagnostics: c.diagnostics ?? args.diagnostics }
1599
- : {}),
1600
- },
1601
- "batch_call",
1602
- ),
1603
- ),
1604
- );
1605
- const results = settled.map((s, i) => {
1606
- const call = args.calls[i];
1607
- if (!call) {
1608
- throw new Error("Batch result has no corresponding call");
1609
- }
1610
- const { address } = call;
1611
- if (s.status === "rejected") {
1612
- return {
1613
- address,
1614
- ok: false,
1615
- error: msg(s.reason),
1616
- errorDetails: classifyCallError(s.reason, "batch_call_failed"),
1617
- };
1618
- }
1619
- const r = s.value;
1620
- if (r.error) {
1621
- return {
1622
- address,
1623
- ok: false,
1624
- error: r.error.message,
1625
- errorDetails: r.error,
1626
- durationMs: r.durationMs,
1627
- attempts: r.attempts,
1628
- ...((call.diagnostics ?? args.diagnostics)
1629
- ? { timing: r.timing }
1630
- : {}),
1631
- };
1632
- }
1633
- if ((call.resultMode ?? args.resultMode) === "value") {
1634
- return {
1635
- address,
1636
- ok: true,
1637
- data: r.value,
1638
- durationMs: r.durationMs,
1639
- attempts: r.attempts,
1640
- ...((call.diagnostics ?? args.diagnostics)
1641
- ? { timing: r.timing }
1642
- : {}),
1643
- };
1644
- }
1645
- return {
1646
- address,
1647
- ok: true,
1648
- result: r.toolResult.content,
1649
- durationMs: r.durationMs,
1650
- attempts: r.attempts,
1651
- ...((call.diagnostics ?? args.diagnostics)
1652
- ? { timing: r.timing }
1653
- : {}),
1654
- };
1655
- });
1656
- const envelope = {
1657
- results,
1658
- durationMs: Date.now() - batchStarted,
1659
- };
1660
- const text = serializeResultText(envelope);
1661
- const bytes = enc.encode(text);
1662
- if (bytes.length <= batchCap) return jsonResult(envelope);
1663
-
1664
- const notice = await stashResult(
1665
- text,
1666
- registry.resultsStorage(),
1667
- bytes.length,
1668
- );
1669
- return jsonResult({
1670
- results: results.map((result) => {
1671
- const common = {
1672
- address: batchSummaryString(result.address),
1673
- ok: !("error" in result),
1674
- ...("durationMs" in result
1675
- ? { durationMs: result.durationMs }
1676
- : {}),
1677
- ...("attempts" in result ? { attempts: result.attempts } : {}),
1678
- ...("timing" in result ? { timing: result.timing } : {}),
1679
- };
1680
- if (!("error" in result)) return common;
1681
- const error = result.error ?? "Batch call failed";
1682
- const details =
1683
- result.errorDetails ??
1684
- errorDetails("batch_call_failed", error);
1685
- return {
1686
- ...common,
1687
- error: batchSummaryString(error),
1688
- errorDetails: {
1689
- code: batchSummaryString(details.code),
1690
- message: batchSummaryString(details.message),
1691
- retryable: details.retryable,
1692
- ...(details.retryAfterMs !== undefined
1693
- ? { retryAfterMs: details.retryAfterMs }
1694
- : {}),
1695
- ...(details.connector !== undefined
1696
- ? { connector: batchSummaryString(details.connector) }
1697
- : {}),
1698
- ...(details.operation !== undefined
1699
- ? { operation: batchSummaryString(details.operation) }
1700
- : {}),
1701
- ...(details.recovery !== undefined
1702
- ? { recovery: details.recovery }
1703
- : {}),
1704
- ...(details.nextAction !== undefined
1705
- ? { nextAction: batchSummaryNextAction(details.nextAction) }
1706
- : {}),
1707
- ...(details.retry !== undefined
1708
- ? { retry: batchSummaryString(details.retry) }
1709
- : {}),
1710
- },
1711
- };
1712
- }),
1713
- durationMs: envelope.durationMs,
1714
- ...notice,
1715
- });
1716
- },
1717
-
1718
1282
  async authorizeConnector(args: AuthorizeArgs): Promise<ToolResult> {
1719
1283
  const connector = registry.getConnector(args.connector);
1720
1284
  if (!connector) {
@@ -1792,7 +1356,7 @@ export function createMetaTools(
1792
1356
  authorizationUrl: status.authorizationUrl,
1793
1357
  instructions:
1794
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. " +
1795
- oauthFollowUp(surface, connector.id),
1359
+ oauthFollowUp(connector.id),
1796
1360
  }
1797
1361
  : {}),
1798
1362
  ...(status.message ? { message: status.message } : {}),
@@ -1808,39 +1372,19 @@ export function createMetaTools(
1808
1372
  };
1809
1373
  }
1810
1374
 
1811
- const LIST_DESC =
1812
- "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.";
1813
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.`;
1814
- 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.`;
1815
1376
  const CALL_DESC =
1816
- '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.';
1817
1378
  const CALL_DESTRUCTIVE_DESC =
1818
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.";
1819
1380
  const GET_RESULT_DESC =
1820
- "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.";
1821
- const BATCH_DESC =
1822
- "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.";
1823
1382
  const AUTHORIZE_DESC =
1824
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.";
1825
1384
  const SKILLS_DESC =
1826
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.';
1827
1386
 
1828
- /**
1829
- * Code-first replacements for the descriptions that route work between tools.
1830
- * Every one of these mentions a tool the consolidated surface removed, so on a
1831
- * code-first deployment the routing sentence has to point at the in-program
1832
- * function that took the work over — a description naming `batch_call` on a
1833
- * surface without one teaches a call that cannot succeed.
1834
- *
1835
- * The classic strings above are left byte-for-byte alone: classic is the
1836
- * compatibility surface and the eval's control arm, and rewording it would
1837
- * change what that control measures.
1838
- */
1839
- const CODE_FIRST_SEARCH_DESC = `${SEARCH_DESC} Expand an ambiguous compact shape, or read exact JSON constraints, with connecta.describe inside execute_code.`;
1840
- const CODE_FIRST_CALL_DESC =
1841
- '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.';
1842
- const CODE_FIRST_GET_RESULT_DESC =
1843
- "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.`;
1844
1388
 
1845
1389
  /**
1846
1390
  * Sentences appended to a meta-tool description only when this connection
@@ -1855,8 +1399,6 @@ const GUIDE_NOTES = {
1855
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.',
1856
1400
  search:
1857
1401
  " A connector group carrying `guide` has a usage guide; fetch it with skills({ name: <guide> }).",
1858
- describe:
1859
- " An entry carrying `guide` belongs to a connector with a usage guide; fetch it with skills({ name: <guide> }).",
1860
1402
  } as const;
1861
1403
 
1862
1404
  /** `base`, plus its guide note when any VISIBLE connector carries a guide. */
@@ -1901,15 +1443,12 @@ const CALL_INPUT_SCHEMA = {
1901
1443
  };
1902
1444
 
1903
1445
  /**
1904
- * Register the base meta-tools onto an McpServer instance: nine on the classic
1905
- * surface, six on the code-first one, where `list_connectors`,
1906
- * `describe_tools`, and `batch_call` have folded into the program surface
1907
- * (`registerExecuteTool` adds the seventh, `execute_code`).
1908
- *
1909
- * Only the registrations differ. Every handler still exists on the object
1910
- * `createMetaTools` returns, and a folded tool's behavior is reached through
1911
- * `connecta.search` / `connecta.describe` / `connecta.batch` inside a program —
1912
- * 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.
1913
1452
  */
1914
1453
  export function registerMetaTools(
1915
1454
  server: McpServer,
@@ -1921,15 +1460,9 @@ export function registerMetaTools(
1921
1460
  discoveryConcurrency?: number;
1922
1461
  activity?: ActivityRequestContext;
1923
1462
  requestSignal?: AbortSignal;
1924
- defer?: DeferredWork;
1925
- /** The advertised surface. Default `classic`. */
1926
- surface?: ConnectaSurface;
1927
1463
  },
1928
1464
  ): void {
1929
- const surface: ConnectaSurface = ctx.surface ?? "classic";
1930
- const codeFirst = surface === "code-first";
1931
1465
  const mt = createMetaTools(registry, ctx.baseUrl, {
1932
- surface,
1933
1466
  ...(ctx.defaultToolTimeoutMs !== undefined
1934
1467
  ? { defaultToolTimeoutMs: ctx.defaultToolTimeoutMs }
1935
1468
  : {}),
@@ -1943,7 +1476,6 @@ export function registerMetaTools(
1943
1476
  ...(ctx.requestSignal !== undefined
1944
1477
  ? { requestSignal: ctx.requestSignal }
1945
1478
  : {}),
1946
- ...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
1947
1479
  });
1948
1480
 
1949
1481
  server.registerTool(
@@ -1956,28 +1488,12 @@ export function registerMetaTools(
1956
1488
  async (args) => mt.skills(args as SkillArgs),
1957
1489
  );
1958
1490
 
1959
- // Folded on the code-first surface: a program browses the same inventory with
1960
- // connecta.search({}) (every catalog) or connecta.search({ connector }) (one).
1961
- // Live connector probing is an operator concern, not a model one — it stays on
1962
- // the operator pages and /health, which is where the ethos puts observability.
1963
- if (!codeFirst) {
1964
- server.registerTool(
1965
- "list_connectors",
1966
- {
1967
- description: LIST_DESC,
1968
- inputSchema: z.object({ probe: z.boolean().optional() }),
1969
- annotations: READ_ONLY_REMOTE,
1970
- },
1971
- async (args) => mt.listConnectors(args as ListArgs),
1972
- );
1973
- }
1974
-
1975
1491
  server.registerTool(
1976
1492
  "search_tools",
1977
1493
  {
1978
1494
  description: describedFor(
1979
1495
  registry,
1980
- codeFirst ? CODE_FIRST_SEARCH_DESC : SEARCH_DESC,
1496
+ SEARCH_WITH_DESCRIBE_DESC,
1981
1497
  "search",
1982
1498
  ),
1983
1499
  inputSchema: z.object({
@@ -1996,28 +1512,10 @@ export function registerMetaTools(
1996
1512
  async (args) => mt.searchTools(args as SearchArgs),
1997
1513
  );
1998
1514
 
1999
- // Folded on the code-first surface: connecta.describe takes the same
2000
- // addresses, format, and per-address error reporting inside a program.
2001
- if (!codeFirst) {
2002
- server.registerTool(
2003
- "describe_tools",
2004
- {
2005
- description: describedFor(registry, DESCRIBE_DESC, "describe"),
2006
- inputSchema: z.object({
2007
- addresses: z.array(z.string()).max(MAX_DESCRIBE_ADDRESSES),
2008
- format: z.enum(["compact", "json"]).optional(),
2009
- fullDescriptions: z.boolean().optional(),
2010
- }),
2011
- annotations: READ_ONLY_REMOTE,
2012
- },
2013
- async (args) => mt.describeTools(args as DescribeArgs),
2014
- );
2015
- }
2016
-
2017
1515
  server.registerTool(
2018
1516
  "call_tool",
2019
1517
  {
2020
- description: codeFirst ? CODE_FIRST_CALL_DESC : CALL_DESC,
1518
+ description: CALL_DESC,
2021
1519
  inputSchema: z.object(CALL_INPUT_SCHEMA),
2022
1520
  // call_tool admits only tools that are themselves explicitly read-only;
2023
1521
  // anything else is refused and routed to call_destructive_tool.
@@ -2077,7 +1575,7 @@ export function registerMetaTools(
2077
1575
  server.registerTool(
2078
1576
  "get_result",
2079
1577
  {
2080
- description: codeFirst ? CODE_FIRST_GET_RESULT_DESC : GET_RESULT_DESC,
1578
+ description: GET_RESULT_DESC,
2081
1579
  inputSchema: z.object({
2082
1580
  id: z.string(),
2083
1581
  // Both bounds are the shared rules (isValidResultOffset,
@@ -2091,29 +1589,4 @@ export function registerMetaTools(
2091
1589
  },
2092
1590
  async (args) => mt.getResult(args as GetResultArgs),
2093
1591
  );
2094
-
2095
- // Folded on the code-first surface: connecta.batch runs the same 1–10
2096
- // parallel read-only calls and returns the same typed per-call outcomes.
2097
- if (!codeFirst) {
2098
- server.registerTool(
2099
- "batch_call",
2100
- {
2101
- description: BATCH_DESC,
2102
- inputSchema: z.object({
2103
- calls: z
2104
- .array(z.object(CALL_INPUT_SCHEMA))
2105
- .min(1)
2106
- .max(10),
2107
- resultMode: z.enum(["mcp", "value"]).optional(),
2108
- timeoutMs: z.number().int().positive().optional(),
2109
- maxRetries: z.number().int().min(0).max(2).optional(),
2110
- diagnostics: z.boolean().optional(),
2111
- }),
2112
- // Same gate as call_tool: every call in the batch must be explicitly
2113
- // read-only or the batch is refused.
2114
- annotations: READ_ONLY_REMOTE,
2115
- },
2116
- async (args) => mt.batchCall(args as BatchArgs),
2117
- );
2118
- }
2119
1592
  }