@zackbart/connecta 0.7.0 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +2 -1
  3. package/dist/connectors/remote-mcp.d.ts +24 -1
  4. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  5. package/dist/connectors/remote-mcp.js +208 -88
  6. package/dist/connectors/remote-mcp.js.map +1 -1
  7. package/dist/credential-health.d.ts +8 -5
  8. package/dist/credential-health.d.ts.map +1 -1
  9. package/dist/credential-health.js +20 -13
  10. package/dist/credential-health.js.map +1 -1
  11. package/dist/execute.d.ts.map +1 -1
  12. package/dist/execute.js +10 -8
  13. package/dist/execute.js.map +1 -1
  14. package/dist/executors/quickjs.d.ts.map +1 -1
  15. package/dist/executors/quickjs.js +57 -5
  16. package/dist/executors/quickjs.js.map +1 -1
  17. package/dist/index.d.ts +3 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/meta-tools.d.ts +25 -0
  21. package/dist/meta-tools.d.ts.map +1 -1
  22. package/dist/meta-tools.js +162 -20
  23. package/dist/meta-tools.js.map +1 -1
  24. package/dist/registry.d.ts +18 -22
  25. package/dist/registry.d.ts.map +1 -1
  26. package/dist/registry.js +33 -21
  27. package/dist/registry.js.map +1 -1
  28. package/dist/server.d.ts.map +1 -1
  29. package/dist/server.js +18 -7
  30. package/dist/server.js.map +1 -1
  31. package/dist/timeout.d.ts +9 -4
  32. package/dist/timeout.d.ts.map +1 -1
  33. package/dist/timeout.js +34 -4
  34. package/dist/timeout.js.map +1 -1
  35. package/dist/toolkits.d.ts +8 -0
  36. package/dist/toolkits.d.ts.map +1 -1
  37. package/dist/toolkits.js +3 -0
  38. package/dist/toolkits.js.map +1 -1
  39. package/dist/types.d.ts +2 -2
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/ui.d.ts +12 -1
  42. package/dist/ui.d.ts.map +1 -1
  43. package/dist/ui.js +187 -6
  44. package/dist/ui.js.map +1 -1
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +1 -1
  48. package/src/connectors/remote-mcp.ts +269 -93
  49. package/src/credential-health.ts +20 -18
  50. package/src/execute.ts +18 -7
  51. package/src/executors/quickjs.ts +65 -5
  52. package/src/index.ts +7 -2
  53. package/src/meta-tools.ts +226 -43
  54. package/src/registry.ts +48 -20
  55. package/src/server.ts +20 -9
  56. package/src/timeout.ts +41 -4
  57. package/src/toolkits.ts +11 -0
  58. package/src/types.ts +2 -2
  59. package/src/ui.ts +212 -11
  60. package/src/version.ts +1 -1
package/src/execute.ts CHANGED
@@ -3,6 +3,9 @@ import { z } from "zod";
3
3
  import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
4
4
  import { recordToolActivity, type ActivityRequestContext } from "./activity.js";
5
5
  import {
6
+ assertDiscoveryResultSize,
7
+ discoveryAddresses,
8
+ discoverySearchLimit,
6
9
  errorResult,
7
10
  jsonResult,
8
11
  serializeResultText,
@@ -369,7 +372,7 @@ export async function buildSandboxProviders(
369
372
  }
370
373
  matches.sort((a, b) => b.score - a.score || a.order - b.order);
371
374
  const offset = Math.max(0, Math.trunc(args.offset ?? 0));
372
- const limit = Math.max(1, Math.trunc(args.limit ?? 25));
375
+ const limit = discoverySearchLimit(args.limit);
373
376
  const page = matches.slice(offset, offset + limit).map((match) => {
374
377
  const input = match.tool.inputSchema ?? { type: "object" };
375
378
  return {
@@ -404,7 +407,7 @@ export async function buildSandboxProviders(
404
407
  offset + page.length < matches.length
405
408
  ? offset + page.length
406
409
  : undefined;
407
- return {
410
+ const result = {
408
411
  tools: page,
409
412
  total: matches.length,
410
413
  offset,
@@ -412,6 +415,11 @@ export async function buildSandboxProviders(
412
415
  hasMore: nextOffset !== undefined,
413
416
  ...(nextOffset !== undefined ? { nextOffset } : {}),
414
417
  };
418
+ assertDiscoveryResultSize(
419
+ result,
420
+ "Request a smaller limit, omit fullDescriptions, or use compact schemas.",
421
+ );
422
+ return result;
415
423
  },
416
424
  describe: async (raw: unknown) => {
417
425
  const args = (raw ?? {}) as {
@@ -419,12 +427,10 @@ export async function buildSandboxProviders(
419
427
  format?: "compact" | "json";
420
428
  fullDescriptions?: boolean;
421
429
  };
422
- if (!Array.isArray(args.addresses)) {
423
- throw new Error("addresses must be an array");
424
- }
430
+ const addresses = discoveryAddresses(args.addresses);
425
431
  const format = args.format ?? "compact";
426
- return {
427
- tools: args.addresses.map((rawAddress) => {
432
+ const result = {
433
+ tools: addresses.map((rawAddress) => {
428
434
  const address = String(rawAddress);
429
435
  const resolved = registry.resolveAddress(address);
430
436
  if (!resolved) {
@@ -457,6 +463,11 @@ export async function buildSandboxProviders(
457
463
  };
458
464
  }),
459
465
  };
466
+ assertDiscoveryResultSize(
467
+ result,
468
+ 'Split the address list or use format: "compact".',
469
+ );
470
+ return result;
460
471
  },
461
472
  },
462
473
  });
@@ -40,11 +40,39 @@ const MAX_LOG_ENTRIES = 200;
40
40
  // keeps the worst case — 200 maxed-out entries — bounded well under a MiB.
41
41
  const MAX_LOG_ENTRY_CHARS = 8_000;
42
42
  const MAX_LOG_TOTAL_CHARS = 256_000;
43
+ // Keep one host result below the range where quickjs-emscripten@0.32.0 can
44
+ // nondeterministically fail during runtime disposal under concurrent load.
45
+ // This still lets guest code reduce data more than ten times larger than
46
+ // connecta's final response budget.
47
+ const MAX_HOST_RESULT_BYTES = 256 * 1024;
43
48
 
44
49
  function msg(err: unknown): string {
45
50
  return err instanceof Error ? err.message : String(err);
46
51
  }
47
52
 
53
+ function exceedsUtf8ByteLimit(value: string, limit: number): boolean {
54
+ let bytes = 0;
55
+ for (let index = 0; index < value.length; index += 1) {
56
+ const code = value.charCodeAt(index);
57
+ if (code <= 0x7f) bytes += 1;
58
+ else if (code <= 0x7ff) bytes += 2;
59
+ else if (
60
+ code >= 0xd800 &&
61
+ code <= 0xdbff &&
62
+ index + 1 < value.length &&
63
+ value.charCodeAt(index + 1) >= 0xdc00 &&
64
+ value.charCodeAt(index + 1) <= 0xdfff
65
+ ) {
66
+ bytes += 4;
67
+ index += 1;
68
+ } else {
69
+ bytes += 3;
70
+ }
71
+ if (bytes > limit) return true;
72
+ }
73
+ return false;
74
+ }
75
+
48
76
  /** Normalize model output into an async-arrow expression: strip markdown fences, wrap bare bodies. */
49
77
  export function normalizeCode(code: string): string {
50
78
  let c = code.trim();
@@ -118,6 +146,25 @@ function armWake(bridge: HostBridge): void {
118
146
  });
119
147
  }
120
148
 
149
+ function waitForHostOrDeadline(
150
+ waitForSettle: Promise<void>,
151
+ remainingMs: number,
152
+ ): Promise<boolean> {
153
+ return new Promise((resolve) => {
154
+ let done = false;
155
+ const timer = setTimeout(() => {
156
+ done = true;
157
+ resolve(false);
158
+ }, remainingMs);
159
+ void waitForSettle.then(() => {
160
+ if (done) return;
161
+ done = true;
162
+ clearTimeout(timer);
163
+ resolve(true);
164
+ });
165
+ });
166
+ }
167
+
121
168
  function installBridge(
122
169
  ctx: QuickJSContext,
123
170
  providers: ExecutorProvider[],
@@ -176,7 +223,20 @@ function installBridge(
176
223
  if (!f) throw new Error(`Unknown function ${ns}.${fn}`);
177
224
  const args = JSON.parse(argsJson) as unknown[];
178
225
  const value = await f(...args);
179
- return JSON.stringify({ ok: true, value });
226
+ let json: string;
227
+ try {
228
+ json = JSON.stringify({ ok: true, value });
229
+ } catch (err) {
230
+ throw new Error(
231
+ `Host result from ${ns}.${fn} could not be serialized: ${msg(err)}`,
232
+ );
233
+ }
234
+ if (exceedsUtf8ByteLimit(json, MAX_HOST_RESULT_BYTES)) {
235
+ throw new Error(
236
+ `Host result from ${ns}.${fn} exceeds the ${MAX_HOST_RESULT_BYTES}-byte serialized bridge limit.`,
237
+ );
238
+ }
239
+ return json;
180
240
  } catch (err) {
181
241
  return JSON.stringify({ ok: false, error: msg(err) });
182
242
  }
@@ -326,10 +386,10 @@ export function quickJsExecutor(
326
386
  if (remaining <= 0) {
327
387
  return { result: undefined, error: timeoutError };
328
388
  }
329
- const settled = await Promise.race([
330
- bridge.waitForSettle.then(() => true),
331
- new Promise<boolean>((r) => setTimeout(() => r(false), remaining)),
332
- ]);
389
+ const settled = await waitForHostOrDeadline(
390
+ bridge.waitForSettle,
391
+ remaining,
392
+ );
333
393
  if (settled) armWake(bridge);
334
394
  else {
335
395
  return { result: undefined, error: timeoutError };
package/src/index.ts CHANGED
@@ -82,7 +82,8 @@ export interface ConnectaDiscoveryConfig {
82
82
  * Deadline (ms) for each downstream probe/catalog call fanned out by
83
83
  * `list_connectors`, `search_tools`, and `describe_tools`. Defaults to
84
84
  * 30_000. A timed-out connector degrades independently; this does not apply
85
- * to tool calls or currently abort the underlying fetch.
85
+ * to tool calls. Catalog walks receive the same cancellation signal, which
86
+ * aborts an in-flight page where supported and prevents another from starting.
86
87
  */
87
88
  probeTimeoutMs?: number;
88
89
  }
@@ -574,7 +575,11 @@ export type {
574
575
  CredentialHealthRecord,
575
576
  } from "./credential-health.js";
576
577
 
577
- export type { RemoteMcpOptions, RemoteMcpAuth } from "./connectors/remote-mcp.js";
578
+ export type {
579
+ RemoteMcpOptions,
580
+ RemoteMcpAuth,
581
+ RemoteMcpRedirectPolicy,
582
+ } from "./connectors/remote-mcp.js";
578
583
  export type { ApiOptions, ApiTool } from "./connectors/api.js";
579
584
  export type {
580
585
  Connector,
package/src/meta-tools.ts CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  import {
31
31
  DEFAULT_PROBE_TIMEOUT_MS,
32
32
  normalizeTimeoutMs,
33
- withTimeout,
33
+ withAbortableTimeout,
34
34
  } from "./timeout.js";
35
35
  import { credentialVerdictApplies } from "./credential-health.js";
36
36
  import type { ConnectorStatus, KVStorage, ToolDef } from "./types.js";
@@ -65,12 +65,126 @@ function msg(err: unknown): string {
65
65
  return err instanceof Error ? err.message : String(err);
66
66
  }
67
67
 
68
- const DEFAULT_SEARCH_LIMIT = 25;
68
+ export const DEFAULT_SEARCH_LIMIT = 25;
69
+ /**
70
+ * A discovery page is for choosing the next tool, not exporting the catalog.
71
+ * One hundred leaves room for broad browsing while keeping each deliberate
72
+ * page far below the catalog sizes Connecta supports.
73
+ */
74
+ export const MAX_SEARCH_LIMIT = 100;
75
+ /** Same one-request work bound for address-based discovery. */
76
+ export const MAX_DESCRIBE_ADDRESSES = 100;
77
+ /**
78
+ * Final UTF-8 ceiling for a generated search/describe response. The count
79
+ * limits are the ordinary guard; this catches unusually large full schemas or
80
+ * descriptions that make even a bounded page expensive.
81
+ */
82
+ export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
69
83
  const enc = new TextEncoder();
70
84
  const dec = new TextDecoder();
71
85
 
72
86
  type ErrorDetails = CallErrorDetails;
73
87
 
88
+ export class DiscoveryPolicyError extends Error {
89
+ constructor(
90
+ readonly code: "invalid_args" | "result_too_large",
91
+ message: string,
92
+ ) {
93
+ super(message);
94
+ this.name = "DiscoveryPolicyError";
95
+ }
96
+ }
97
+
98
+ /** Validate before ranking so a huge page request does no proportional work. */
99
+ export function discoverySearchLimit(value: unknown): number {
100
+ if (value === undefined) return DEFAULT_SEARCH_LIMIT;
101
+ if (
102
+ typeof value !== "number" ||
103
+ !Number.isInteger(value) ||
104
+ value < 1 ||
105
+ value > MAX_SEARCH_LIMIT
106
+ ) {
107
+ throw new DiscoveryPolicyError(
108
+ "invalid_args",
109
+ `limit must be a whole number from 1 through ${MAX_SEARCH_LIMIT}. Page through larger catalogs with offset.`,
110
+ );
111
+ }
112
+ return value;
113
+ }
114
+
115
+ /** Validate the raw list so duplicate addresses consume the same bound. */
116
+ export function discoveryAddresses(value: unknown): unknown[] {
117
+ if (!Array.isArray(value)) {
118
+ throw new DiscoveryPolicyError(
119
+ "invalid_args",
120
+ "addresses must be an array.",
121
+ );
122
+ }
123
+ if (value.length > MAX_DESCRIBE_ADDRESSES) {
124
+ throw new DiscoveryPolicyError(
125
+ "invalid_args",
126
+ `addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across describe_tools calls.`,
127
+ );
128
+ }
129
+ return value;
130
+ }
131
+
132
+ /** Serialize once and count the exact bytes jsonResult would emit. */
133
+ function boundedDiscoveryText(
134
+ value: unknown,
135
+ hint: string,
136
+ ): string {
137
+ const text = JSON.stringify(value, null, 2);
138
+ if (text === undefined) {
139
+ throw new TypeError("Discovery result is not JSON-serializable.");
140
+ }
141
+ const bytes = enc.encode(text).length;
142
+ if (bytes > MAX_DISCOVERY_RESULT_BYTES) {
143
+ throw new DiscoveryPolicyError(
144
+ "result_too_large",
145
+ `Discovery result is ${bytes} UTF-8 bytes, over the ${MAX_DISCOVERY_RESULT_BYTES}-byte ceiling. ${hint}`,
146
+ );
147
+ }
148
+ return text;
149
+ }
150
+
151
+ /** Apply the same final result guard to code-mode discovery helpers. */
152
+ export function assertDiscoveryResultSize(
153
+ value: unknown,
154
+ hint: string,
155
+ ): void {
156
+ boundedDiscoveryText(value, hint);
157
+ }
158
+
159
+ function discoveryErrorResult(error: DiscoveryPolicyError): ToolResult {
160
+ const result = jsonResult({
161
+ error: {
162
+ code: error.code,
163
+ message: error.message,
164
+ retryable: false,
165
+ },
166
+ });
167
+ result.isError = true;
168
+ return result;
169
+ }
170
+
171
+ function discoveryResult(value: unknown, hint: string): ToolResult {
172
+ try {
173
+ const text = boundedDiscoveryText(value, hint);
174
+ return {
175
+ content: [{ type: "text", text }],
176
+ ...(value !== null && typeof value === "object" && !Array.isArray(value)
177
+ ? { structuredContent: value as Record<string, unknown> }
178
+ : {}),
179
+ };
180
+ } catch (err) {
181
+ if (err instanceof DiscoveryPolicyError) {
182
+ return discoveryErrorResult(err);
183
+ }
184
+ throw err;
185
+ }
186
+ }
187
+
74
188
  /**
75
189
  * The longest the engine will park a synchronous inbound request in *waiting
76
190
  * alone*. The engine already treats ~15 s as the outer bound of one reasonable
@@ -483,6 +597,18 @@ export function createMetaTools(
483
597
  // identity lets remote connectors reuse one downstream client inside that
484
598
  // request without leaking request-bound I/O into the next one.
485
599
  const requestScope = {};
600
+ const withProbeDeadline = <T>(
601
+ label: string,
602
+ operation: (options: {
603
+ signal: AbortSignal;
604
+ timeoutMs: number;
605
+ }) => Promise<T>,
606
+ ) =>
607
+ withAbortableTimeout(
608
+ (signal) => operation({ signal, timeoutMs: probeTimeoutMs }),
609
+ probeTimeoutMs,
610
+ label,
611
+ );
486
612
 
487
613
  interface RunCallOutcome {
488
614
  toolResult: ToolResult;
@@ -793,8 +919,8 @@ export function createMetaTools(
793
919
  // call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
794
920
  const connectors = registry.listConnectors();
795
921
  const scope = probe ? {} : requestScope;
796
- const out = await Promise.all(
797
- connectors.map(async (c) => {
922
+ const pending = connectors.map(
923
+ async (c) => {
798
924
  const statusStarted = Date.now();
799
925
  const observed = registry.healthFor(c.id);
800
926
  const verdict = await registry.credentialHealthFor(c.id);
@@ -803,10 +929,10 @@ export function createMetaTools(
803
929
  | { state: "ok" | "error" | "unknown"; message?: string };
804
930
  if (probe) {
805
931
  try {
806
- status = await withTimeout(
807
- registry.statusFor(c.id, baseUrl, scope),
808
- probeTimeoutMs,
932
+ status = await withProbeDeadline(
809
933
  `list_connectors probe of "${c.id}"`,
934
+ (options) =>
935
+ registry.statusFor(c.id, baseUrl, scope, options),
810
936
  );
811
937
  } catch (err) {
812
938
  // A probe that outran probeTimeoutMs (or otherwise threw)
@@ -890,14 +1016,45 @@ export function createMetaTools(
890
1016
  // the first (now stale) authorization URL.
891
1017
  if (probe && status.state === "ok") {
892
1018
  try {
893
- tools = await withTimeout(
894
- registry.refreshTools(c.id, baseUrl, scope),
895
- probeTimeoutMs,
1019
+ tools = await withProbeDeadline(
896
1020
  `list_connectors catalog refresh of "${c.id}"`,
1021
+ (options) =>
1022
+ registry.refreshTools(c.id, baseUrl, scope, options),
897
1023
  );
898
1024
  registry.recordSuccess(c.id, Date.now() - statusStarted);
899
1025
  } catch (err) {
900
- status = { state: "error" as const, message: msg(err) };
1026
+ const details = classifyCallError(err);
1027
+ if (details.code === "auth_required") {
1028
+ let authStatus: ConnectorStatus | undefined;
1029
+ try {
1030
+ authStatus = await withProbeDeadline(
1031
+ `list_connectors authorization status of "${c.id}"`,
1032
+ (options) =>
1033
+ registry.statusFor(c.id, baseUrl, scope, options),
1034
+ );
1035
+ } catch {
1036
+ // The typed auth verdict is still authoritative; this second
1037
+ // read exists only to recover the connector's pending URL.
1038
+ }
1039
+ status =
1040
+ authStatus?.state === "auth_required"
1041
+ ? authStatus
1042
+ : {
1043
+ state: "auth_required" as const,
1044
+ message: details.message,
1045
+ };
1046
+ await registry.recordCredentialHealth(c.id, {
1047
+ state: "auth_required",
1048
+ checkedAt,
1049
+ ...(status.message ? { message: status.message } : {}),
1050
+ ...("authorizationUrl" in status &&
1051
+ status.authorizationUrl
1052
+ ? { authorizationUrl: status.authorizationUrl }
1053
+ : {}),
1054
+ });
1055
+ } else {
1056
+ status = { state: "error" as const, message: msg(err) };
1057
+ }
901
1058
  registry.recordFailure(c.id, Date.now() - statusStarted, err);
902
1059
  }
903
1060
  }
@@ -922,24 +1079,38 @@ export function createMetaTools(
922
1079
  : {}),
923
1080
  ...(status.message ? { message: status.message } : {}),
924
1081
  };
925
- }),
926
- ).finally(async () => {
927
- if (!probe) return;
928
- await Promise.all(
929
- connectors.map((connector) =>
930
- closeConnectorScope(
931
- connector,
932
- registry.contextFor(connector.id, baseUrl, scope),
933
- ),
1082
+ },
1083
+ );
1084
+ if (!probe) {
1085
+ return jsonResult({ connectors: await Promise.all(pending) });
1086
+ }
1087
+ const settled = await Promise.allSettled(pending);
1088
+ await Promise.all(
1089
+ connectors.map((connector) =>
1090
+ closeConnectorScope(
1091
+ connector,
1092
+ registry.contextFor(connector.id, baseUrl, scope),
934
1093
  ),
935
- );
1094
+ ),
1095
+ );
1096
+ const out = settled.map((result) => {
1097
+ if (result.status === "rejected") throw result.reason;
1098
+ return result.value;
936
1099
  });
937
1100
  return jsonResult({ connectors: out });
938
1101
  },
939
1102
 
940
1103
  async searchTools(args: SearchArgs): Promise<ToolResult> {
941
1104
  const q = args.query ?? "";
942
- const limit = Math.max(1, Math.trunc(args.limit ?? DEFAULT_SEARCH_LIMIT));
1105
+ let limit: number;
1106
+ try {
1107
+ limit = discoverySearchLimit(args.limit);
1108
+ } catch (err) {
1109
+ if (err instanceof DiscoveryPolicyError) {
1110
+ return discoveryErrorResult(err);
1111
+ }
1112
+ throw err;
1113
+ }
943
1114
  const offset = Math.max(0, Math.trunc(args.offset ?? 0));
944
1115
  const conns = args.connector
945
1116
  ? [registry.getConnector(args.connector)].filter(
@@ -957,10 +1128,10 @@ export function createMetaTools(
957
1128
  }> = [];
958
1129
  const catalogs = await Promise.allSettled(
959
1130
  conns.map((c) =>
960
- withTimeout(
961
- registry.getTools(c.id, baseUrl, requestScope),
962
- probeTimeoutMs,
1131
+ withProbeDeadline(
963
1132
  `search_tools probe of "${c.id}"`,
1133
+ (options) =>
1134
+ registry.getTools(c.id, baseUrl, requestScope, options),
964
1135
  ),
965
1136
  ),
966
1137
  );
@@ -1050,17 +1221,28 @@ export function createMetaTools(
1050
1221
  offset + page.length < matches.length
1051
1222
  ? offset + page.length
1052
1223
  : undefined;
1053
- return jsonResult({
1054
- connectors: groups,
1055
- total: matches.length,
1056
- offset,
1057
- limit,
1058
- hasMore: nextOffset !== undefined,
1059
- ...(nextOffset !== undefined ? { nextOffset } : {}),
1060
- });
1224
+ return discoveryResult(
1225
+ {
1226
+ connectors: groups,
1227
+ total: matches.length,
1228
+ offset,
1229
+ limit,
1230
+ hasMore: nextOffset !== undefined,
1231
+ ...(nextOffset !== undefined ? { nextOffset } : {}),
1232
+ },
1233
+ "Request a smaller limit, omit fullDescriptions, or use compact schemas.",
1234
+ );
1061
1235
  },
1062
1236
 
1063
1237
  async describeTools(args: DescribeArgs): Promise<ToolResult> {
1238
+ try {
1239
+ discoveryAddresses(args.addresses);
1240
+ } catch (err) {
1241
+ if (err instanceof DiscoveryPolicyError) {
1242
+ return discoveryErrorResult(err);
1243
+ }
1244
+ throw err;
1245
+ }
1064
1246
  const format = args.format ?? "compact";
1065
1247
  const resolved = args.addresses.map((address) => ({
1066
1248
  address,
@@ -1075,10 +1257,10 @@ export function createMetaTools(
1075
1257
  ];
1076
1258
  const loaded = await Promise.allSettled(
1077
1259
  connectorIds.map((id) =>
1078
- withTimeout(
1079
- registry.getTools(id, baseUrl, requestScope),
1080
- probeTimeoutMs,
1260
+ withProbeDeadline(
1081
1261
  `describe_tools probe of "${id}"`,
1262
+ (options) =>
1263
+ registry.getTools(id, baseUrl, requestScope, options),
1082
1264
  ),
1083
1265
  ),
1084
1266
  );
@@ -1131,7 +1313,10 @@ export function createMetaTools(
1131
1313
  ...(tool.annotations ? { annotations: tool.annotations } : {}),
1132
1314
  };
1133
1315
  });
1134
- return jsonResult({ tools: out });
1316
+ return discoveryResult(
1317
+ { tools: out },
1318
+ 'Split the address list or use format: "compact".',
1319
+ );
1135
1320
  },
1136
1321
 
1137
1322
  async callTool(args: CallArgs): Promise<ToolResult> {
@@ -1327,10 +1512,8 @@ export function createMetaTools(
1327
1512
 
1328
1513
  const LIST_DESC =
1329
1514
  "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.";
1330
- const SEARCH_DESC =
1331
- 'Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all. includeSchemas="compact" usually removes the describe_tools round trip.';
1332
- const DESCRIBE_DESC =
1333
- 'Inspect known tool addresses when search_tools did not include a sufficient schema. Returns descriptions, input/output schemas, and behavior annotations; format "compact" is the default.';
1515
+ const SEARCH_DESC = `Start here when a tool address is unknown. Exact/name matches rank above description matches; an empty query browses all. Pages contain at most ${MAX_SEARCH_LIMIT} tools. includeSchemas="compact" usually removes the describe_tools round trip.`;
1516
+ const DESCRIBE_DESC = `Inspect up to ${MAX_DESCRIBE_ADDRESSES} known tool addresses when search_tools did not include a sufficient schema. Returns descriptions, input/output schemas, and behavior annotations; format "compact" is the default.`;
1334
1517
  const CALL_DESC =
1335
1518
  'Use for one tool explicitly annotated readOnlyHint: true. For 2–10 independent read-only calls use batch_call; for dependent steps or data reduction use execute_code when available. Unannotated, write-capable, and destructive tools are refused and require call_destructive_tool. fields selects JSON dot-paths, resultMode "value" unwraps results, timeoutMs sets a deadline, safe maxRetries are annotation-gated, diagnostics adds timing, and large results page through get_result.';
1336
1519
  const CALL_DESTRUCTIVE_DESC =
@@ -1444,7 +1627,7 @@ export function registerMetaTools(
1444
1627
  inputSchema: {
1445
1628
  query: z.string().optional(),
1446
1629
  connector: z.string().optional(),
1447
- limit: z.number().int().positive().optional(),
1630
+ limit: z.number().int().positive().max(MAX_SEARCH_LIMIT).optional(),
1448
1631
  offset: z.number().int().nonnegative().optional(),
1449
1632
  fullDescriptions: z.boolean().optional(),
1450
1633
  includeSchemas: z.enum(["compact", "json"]).optional(),
@@ -1459,7 +1642,7 @@ export function registerMetaTools(
1459
1642
  {
1460
1643
  description: describedFor(registry, DESCRIBE_DESC, "describe"),
1461
1644
  inputSchema: {
1462
- addresses: z.array(z.string()),
1645
+ addresses: z.array(z.string()).max(MAX_DESCRIBE_ADDRESSES),
1463
1646
  format: z.enum(["compact", "json"]).optional(),
1464
1647
  fullDescriptions: z.boolean().optional(),
1465
1648
  },