@sema-agent/core 5.53.0 → 5.55.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/CHANGELOG.md +150 -0
  2. package/dist/agents/cumulative-stats.d.ts +26 -0
  3. package/dist/agents/cumulative-stats.js +56 -0
  4. package/dist/agents/observer.d.ts +11 -7
  5. package/dist/agents/observer.js +2 -4
  6. package/dist/agents/subagent.d.ts +4 -0
  7. package/dist/agents/subagent.js +1 -1
  8. package/dist/agents/verify.d.ts +27 -3
  9. package/dist/agents/verify.js +7 -2
  10. package/dist/core/governance-codes.js +14 -0
  11. package/dist/core/hooks.js +1 -1
  12. package/dist/core/lsp-diagnostics.d.ts +19 -17
  13. package/dist/core/lsp-diagnostics.js +11 -5
  14. package/dist/core/mcp.d.ts +214 -5
  15. package/dist/core/mcp.js +315 -30
  16. package/dist/core/memory-engine/consolidation.d.ts +378 -0
  17. package/dist/core/memory-engine/consolidation.js +342 -0
  18. package/dist/core/memory-engine/dual-root.js +3 -0
  19. package/dist/core/memory-engine/engine.d.ts +237 -4
  20. package/dist/core/memory-engine/engine.js +1111 -4
  21. package/dist/core/memory-engine/export-bundle.js +9 -0
  22. package/dist/core/memory-engine/file-backend.js +27 -1
  23. package/dist/core/memory-engine/frontmatter.d.ts +20 -1
  24. package/dist/core/memory-engine/frontmatter.js +111 -0
  25. package/dist/core/memory-engine/index.d.ts +4 -2
  26. package/dist/core/memory-engine/index.js +3 -1
  27. package/dist/core/memory-engine/memory-backend-contract.js +131 -0
  28. package/dist/core/memory-engine/sync-client.js +26 -0
  29. package/dist/core/memory-engine/tools.d.ts +9 -0
  30. package/dist/core/memory-engine/tools.js +57 -13
  31. package/dist/core/memory-engine/types.d.ts +99 -0
  32. package/dist/core/memory-recall.js +4 -3
  33. package/dist/core/memory.d.ts +33 -3
  34. package/dist/core/memory.js +6 -4
  35. package/dist/core/permission-rule-model.d.ts +140 -21
  36. package/dist/core/permission-rule-model.js +76 -17
  37. package/dist/core/permission-rule-org.d.ts +4 -3
  38. package/dist/core/permission-rule-org.js +12 -3
  39. package/dist/core/permission-rules.d.ts +22 -0
  40. package/dist/core/permission-rules.js +60 -6
  41. package/dist/core/reminder-disclosure.d.ts +29 -4
  42. package/dist/core/reminder-disclosure.js +60 -12
  43. package/dist/core/runner/prepare-memory.js +7 -2
  44. package/dist/core/runner/prepare-task.d.ts +31 -1
  45. package/dist/core/runner/prepare-task.js +31 -14
  46. package/dist/core/runner/runtask.d.ts +8 -1
  47. package/dist/core/runner/runtask.js +12 -10
  48. package/dist/core/runner/session-rule-policy.js +5 -3
  49. package/dist/core/runner/synthetic-tools.js +4 -2
  50. package/dist/core/runner/turn-attachments.d.ts +16 -6
  51. package/dist/core/runner/turn-attachments.js +34 -20
  52. package/dist/core/tool-policy.d.ts +18 -0
  53. package/dist/core/tool-policy.js +19 -8
  54. package/dist/core/types.d.ts +89 -6
  55. package/dist/core/untrusted-egress.js +12 -2
  56. package/dist/core/untrusted-text.d.ts +189 -3
  57. package/dist/core/untrusted-text.js +416 -6
  58. package/dist/engine/loop/types.d.ts +7 -0
  59. package/dist/index.d.ts +2 -2
  60. package/dist/index.js +2 -2
  61. package/dist/orchestration/workflow-types.d.ts +16 -6
  62. package/dist/orchestration/workflow-types.js +10 -4
  63. package/dist/orchestration/workflow.js +32 -6
  64. package/dist/prompts/default.d.ts +12 -2
  65. package/dist/stores/file/background-agent-store.js +1 -0
  66. package/dist/stores/file/checkpoint-store.d.ts +6 -2
  67. package/dist/stores/file/checkpoint-store.js +1 -0
  68. package/dist/stores/file/fs-atomic.d.ts +151 -10
  69. package/dist/stores/file/fs-atomic.js +208 -32
  70. package/dist/stores/file/index.d.ts +26 -3
  71. package/dist/stores/file/index.js +25 -2
  72. package/dist/stores/file/shared-ledger.d.ts +40 -5
  73. package/dist/stores/file/shared-ledger.js +24 -8
  74. package/dist/stores/file/workflow-run-store.d.ts +8 -1
  75. package/dist/stores/file/workflow-run-store.js +1 -0
  76. package/dist/tools/fs/bash-readonly-classifier.d.ts +71 -0
  77. package/dist/tools/fs/bash-readonly-classifier.js +58 -47
  78. package/dist/tools/fs/index.d.ts +3 -1
  79. package/package.json +1 -1
  80. package/test/export-surface.snapshot.json +30 -1
package/dist/core/mcp.js CHANGED
@@ -7,6 +7,7 @@ import { lstat, mkdir, writeFile } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { CallToolResultSchema, ElicitRequestSchema, ErrorCode, ListResourcesResultSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
10
+ import { DEFAULT_REQUEST_TIMEOUT_MSEC } from "@modelcontextprotocol/sdk/shared/protocol.js";
10
11
  import { MCP_IMAGE_MAX_BASE64, sharpImageResizer } from "./image-downsample.js";
11
12
  import { sliceHeadSafe, sliceTailSafe } from "./surrogate-safe-slice.js";
12
13
  import { truncateError } from "./tool-errors.js";
@@ -536,6 +537,82 @@ export async function mapContent(content, serverName, imageResizer) {
536
537
  }
537
538
  return out;
538
539
  }
540
+ export const MCP_INVISIBLE_TEXT_RE = /[\p{Cf}\p{Co}\p{Cn}]/gu;
541
+ const MCP_SANITIZE_MAX_ROUNDS = 10;
542
+ function sanitizeMcpModelFacingText(text) {
543
+ let out = text;
544
+ for (let round = 0; round < MCP_SANITIZE_MAX_ROUNDS; round++) {
545
+ const next = out.normalize("NFKC").replace(MCP_INVISIBLE_TEXT_RE, "");
546
+ if (next === out)
547
+ return out;
548
+ out = next;
549
+ }
550
+ return out.replace(MCP_INVISIBLE_TEXT_RE, "");
551
+ }
552
+ const MCP_SCHEMA_PROSE_KEYS = new Set(["description", "title", "$comment"]);
553
+ const MCP_SCHEMA_CHILD_SEAT = new Map([
554
+ ...["properties", "definitions", "$defs", "dependentSchemas"].map((k) => [k, "properties"]),
555
+ ...["patternProperties"].map((k) => [k, "schemaMap"]),
556
+ ...["items", "prefixItems", "additionalItems", "additionalProperties", "unevaluatedItems", "unevaluatedProperties", "contains", "propertyNames", "contentSchema", "not", "if", "then", "else", "allOf", "anyOf", "oneOf"].map((k) => [k, "schema"]),
557
+ ...["required", "dependentRequired", "dependencies", "$ref", "$dynamicRef", "$id", "$anchor", "$dynamicAnchor"].map((k) => [k, "nameRefs"]),
558
+ ]);
559
+ class McpPayloadKeyCollision extends Error {
560
+ }
561
+ function sanitizeMcpSchemaNameRefs(value) {
562
+ if (typeof value === "string")
563
+ return sanitizeMcpModelFacingText(value);
564
+ if (Array.isArray(value))
565
+ return value.map(sanitizeMcpSchemaNameRefs);
566
+ if (value !== null && typeof value === "object") {
567
+ const out = Object.create(null);
568
+ for (const [key, v] of Object.entries(value)) {
569
+ const neutralized = sanitizeMcpModelFacingText(key);
570
+ if (Object.hasOwn(out, neutralized))
571
+ throw new McpPayloadKeyCollision(neutralized);
572
+ out[neutralized] = Array.isArray(v) ? sanitizeMcpSchemaNameRefs(v) : sanitizeMcpModelFacingValue(v, "schema");
573
+ }
574
+ return out;
575
+ }
576
+ return value;
577
+ }
578
+ function sanitizeMcpModelFacingValue(value, seat = "schema") {
579
+ if (seat === "data")
580
+ return value;
581
+ if (seat === "nameRefs")
582
+ return sanitizeMcpSchemaNameRefs(value);
583
+ if (Array.isArray(value))
584
+ return value.map((v) => sanitizeMcpModelFacingValue(v, seat === "schema" ? "schema" : "data"));
585
+ if (value !== null && typeof value === "object") {
586
+ const out = Object.create(null);
587
+ for (const [key, v] of Object.entries(value)) {
588
+ if (seat === "schemaMap") {
589
+ out[key] = sanitizeMcpModelFacingValue(v, "schema");
590
+ continue;
591
+ }
592
+ const neutralized = sanitizeMcpModelFacingText(key);
593
+ if (Object.hasOwn(out, neutralized))
594
+ throw new McpPayloadKeyCollision(neutralized);
595
+ if (seat === "properties") {
596
+ out[neutralized] = sanitizeMcpModelFacingValue(v, "schema");
597
+ continue;
598
+ }
599
+ if (MCP_SCHEMA_PROSE_KEYS.has(neutralized)) {
600
+ out[neutralized] = typeof v === "string" ? sanitizeMcpModelFacingText(v) : v;
601
+ continue;
602
+ }
603
+ out[neutralized] = sanitizeMcpModelFacingValue(v, MCP_SCHEMA_CHILD_SEAT.get(neutralized) ?? "data");
604
+ }
605
+ return out;
606
+ }
607
+ return value;
608
+ }
609
+ const MCP_TOOL_DESCRIPTION_MAX_CHARS = 2048;
610
+ const MCP_TOOL_DESCRIPTION_TRUNCATION_MARK = "… [truncated]";
611
+ function capMcpToolDescription(description) {
612
+ if (description.length <= MCP_TOOL_DESCRIPTION_MAX_CHARS)
613
+ return description;
614
+ return sliceHeadSafe(description, MCP_TOOL_DESCRIPTION_MAX_CHARS) + MCP_TOOL_DESCRIPTION_TRUNCATION_MARK;
615
+ }
539
616
  const MCP_SCHEMA_COMBINATOR_KEYS = ["anyOf", "oneOf", "allOf"];
540
617
  const MCP_SCHEMA_PROP_NAME_RE = /^[a-zA-Z0-9_.-]{1,64}$/;
541
618
  const MCP_SCHEMA_CARRY_KEYS = ["$defs", "definitions", "$schema", "additionalProperties", "description", "title"];
@@ -642,6 +719,19 @@ export function normalizeMcpToolSchema(schema) {
642
719
  return { outcome: "drop", reason: `input schema uses top-level ${combinatorsPresent.join("/")} and could not be normalized` };
643
720
  }
644
721
  }
722
+ export function mcpToolSchemaAdvisory(schema) {
723
+ if (schema === undefined || !isPlainSchemaObject(schema))
724
+ return undefined;
725
+ const properties = schema.properties;
726
+ if (!isPlainSchemaObject(properties))
727
+ return undefined;
728
+ for (const key of Object.keys(properties)) {
729
+ if (!MCP_SCHEMA_PROP_NAME_RE.test(key)) {
730
+ return `its parameter name ${inlineUntrusted(JSON.stringify(key), 80)} is outside the character set some providers accept for tool parameters (${MCP_SCHEMA_PROP_NAME_RE.source}); requests carrying this tool may be rejected by such a provider`;
731
+ }
732
+ }
733
+ return undefined;
734
+ }
645
735
  export function mcpToolSchemaProblem(schema) {
646
736
  if (schema === undefined)
647
737
  return undefined;
@@ -719,7 +809,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
719
809
  if (r.status === "fulfilled") {
720
810
  const s = r.value;
721
811
  clients.push(s.client);
722
- serverHandles.push({ name: spec.name, spec, client: s.client, health: s.health, toolNames: s.tools.map((t) => t.name) });
812
+ serverHandles.push({ name: spec.name, spec, client: s.client, health: s.health, tools: s.tools, listedRaw: s.listedTools });
723
813
  tools.push(...s.tools);
724
814
  toolAxes.push(...s.axes);
725
815
  if (s.instructions) {
@@ -740,6 +830,13 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
740
830
  resourceServers.push(s.resourceServer);
741
831
  for (const d of s.dropped)
742
832
  droppedTools.push({ server: inlineUntrusted(spec.name, 160), ...d });
833
+ for (const a of s.schemaAdvisories)
834
+ warnings.push(schemaAdvisoryWarning(spec.name, a.tool, a.reason));
835
+ if (s.toolsCapabilityAbsent === true && spec.allowTools !== undefined && spec.allowTools.length > 0) {
836
+ warnings.push(toolsCapabilityAbsentWarning(spec.name));
837
+ }
838
+ if (s.listingIncomplete)
839
+ warnings.push(listingIncompleteWarning(spec.name, s.listingIncomplete));
743
840
  statuses.push(s.status);
744
841
  }
745
842
  else {
@@ -781,12 +878,17 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
781
878
  }
782
879
  try {
783
880
  const listed = await listToolsLenient(h.client);
784
- cacheMcpToolMetadata(h.client, listed.tools);
785
- const { serverTools, serverAxes, dropped } = intakeListedTools(listed, h.spec, h.client, h.health, imageResizer, mcpDisclosure, isServerRevoked);
881
+ const advertised = new Set(listed.tools.map((t) => t.name));
882
+ const retainedRaw = listed.incomplete !== undefined ? h.listedRaw.filter((t) => !advertised.has(t.name)) : [];
883
+ const mergedRaw = retainedRaw.length > 0 ? [...listed.tools, ...retainedRaw] : listed.tools;
884
+ cacheMcpToolMetadata(h.client, retainedRaw.length > 0 ? [...retainedRaw, ...listed.tools] : listed.tools);
885
+ const { serverTools, serverAxes, dropped } = intakeListedTools({ tools: mergedRaw }, h.spec, h.client, h.health, imageResizer, mcpDisclosure, isServerRevoked);
886
+ const priorNames = h.tools.map((t) => t.name);
786
887
  const newNames = serverTools.map((t) => t.name);
787
- const added = newNames.filter((n) => !h.toolNames.includes(n));
788
- const removed = h.toolNames.filter((n) => !newNames.includes(n));
789
- h.toolNames = newNames;
888
+ const added = newNames.filter((n) => !priorNames.includes(n));
889
+ const removed = priorNames.filter((n) => !newNames.includes(n));
890
+ h.tools = serverTools;
891
+ h.listedRaw = mergedRaw;
790
892
  results.push({
791
893
  server: h.name,
792
894
  prefix: prefixOf(h.name),
@@ -797,6 +899,9 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
797
899
  tools: serverTools,
798
900
  axes: serverAxes,
799
901
  dropped: dropped.map((d) => ({ server: inlineUntrusted(h.name, 160), ...d })),
902
+ ...(listed.incomplete !== undefined
903
+ ? { listingIncomplete: listed.incomplete, error: `tool listing incomplete — ${listingIncompleteNote(listed.incomplete)}` }
904
+ : {}),
800
905
  });
801
906
  }
802
907
  catch (err) {
@@ -804,7 +909,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
804
909
  server: h.name,
805
910
  prefix: prefixOf(h.name),
806
911
  status: "failed",
807
- toolCount: h.toolNames.length,
912
+ toolCount: h.tools.length,
808
913
  added: [],
809
914
  removed: [],
810
915
  error: inlineUntrusted(namedMcpFailureText(err), 240),
@@ -875,6 +980,110 @@ function resourceLine(r) {
875
980
  function serverNames(servers) {
876
981
  return servers.map((r) => r.server).join(", ") || "(none)";
877
982
  }
983
+ const MAX_MCP_LIST_PAGES = 20;
984
+ const MIN_MCP_PAGE_BUDGET_MS = 250;
985
+ function continuationPageFloorMs(budgetMs) {
986
+ return Math.max(1, Math.min(MIN_MCP_PAGE_BUDGET_MS, Math.floor(budgetMs / 10)));
987
+ }
988
+ export function listEntryFingerprint(entry) {
989
+ try {
990
+ const json = JSON.stringify(entry);
991
+ return typeof json === "string" ? json : undefined;
992
+ }
993
+ catch {
994
+ return undefined;
995
+ }
996
+ }
997
+ const LIST_ENTRY_FINGERPRINT_MAX_CHARS = 8 * 1024;
998
+ const LOOP_REWIND_FINGERPRINT_MAX_CHARS = 1024 * 1024;
999
+ export async function walkMcpListPages(fetchPage, opts) {
1000
+ const items = [];
1001
+ const used = new Set();
1002
+ const deadline = Date.now() + opts.budgetMs;
1003
+ let cursor;
1004
+ let pages = 0;
1005
+ for (;;) {
1006
+ const remainingMs = pages === 0 ? opts.budgetMs : deadline - Date.now();
1007
+ if (pages > 0 && remainingMs < continuationPageFloorMs(opts.budgetMs)) {
1008
+ return { items, incomplete: { reason: "budget_exhausted", pages, budgetMs: opts.budgetMs } };
1009
+ }
1010
+ let page;
1011
+ try {
1012
+ page = await fetchPage(cursor, remainingMs);
1013
+ }
1014
+ catch (err) {
1015
+ if (pages === 0 || opts.signal?.aborted)
1016
+ throw err;
1017
+ return { items, incomplete: { reason: "page_error", pages, error: err instanceof Error ? err.message : String(err) } };
1018
+ }
1019
+ opts.onPage?.();
1020
+ const pageStart = items.length;
1021
+ items.push(...page.items);
1022
+ pages++;
1023
+ if (page.cursorInvalid === true)
1024
+ return { items, incomplete: { reason: "cursor_invalid", pages } };
1025
+ const next = page.nextCursor;
1026
+ if (next === undefined)
1027
+ return { items };
1028
+ if (used.has(next)) {
1029
+ const fresh = [];
1030
+ const fingerprintOf = opts.fingerprintOf;
1031
+ if (fingerprintOf !== undefined) {
1032
+ const before = new Set();
1033
+ let keying = LOOP_REWIND_FINGERPRINT_MAX_CHARS;
1034
+ const keyOf = (item) => {
1035
+ if (keying <= 0)
1036
+ return undefined;
1037
+ const fp = fingerprintOf(item);
1038
+ if (fp === undefined) {
1039
+ keying -= LIST_ENTRY_FINGERPRINT_MAX_CHARS;
1040
+ return undefined;
1041
+ }
1042
+ keying -= fp.length;
1043
+ return fp.length <= LIST_ENTRY_FINGERPRINT_MAX_CHARS ? fp : undefined;
1044
+ };
1045
+ for (let i = 0; i < pageStart; i++) {
1046
+ const fp = keyOf(items[i]);
1047
+ if (fp !== undefined)
1048
+ before.add(fp);
1049
+ }
1050
+ for (const item of page.items) {
1051
+ const fp = keyOf(item);
1052
+ if (fp !== undefined && before.has(fp))
1053
+ continue;
1054
+ fresh.push(item);
1055
+ }
1056
+ }
1057
+ items.length = pageStart;
1058
+ items.push(...fresh);
1059
+ return { items, incomplete: { reason: "cursor_loop", pages } };
1060
+ }
1061
+ if (pages >= MAX_MCP_LIST_PAGES)
1062
+ return { items, incomplete: { reason: "page_cap", pages } };
1063
+ used.add(next);
1064
+ cursor = next;
1065
+ }
1066
+ }
1067
+ function idleWatchdogTripText(idleMs) {
1068
+ return `received no response for ${idleMs}ms (idle watchdog; set MCP_IDLE_TIMEOUT_STDIO / MCP_IDLE_TIMEOUT_HTTP to change this bound)`;
1069
+ }
1070
+ function listingIncompleteNote(flag) {
1071
+ const pages = `${flag.pages} page${flag.pages === 1 ? "" : "s"}`;
1072
+ if (flag.reason === "page_cap") {
1073
+ return `[listing truncated: the ${MAX_MCP_LIST_PAGES}-page pagination limit was reached — more entries may exist beyond this listing]`;
1074
+ }
1075
+ if (flag.reason === "cursor_loop") {
1076
+ return `[listing truncated: after ${pages} the server handed back a pagination cursor it had already used, so the walk stopped — more entries may exist beyond this listing]`;
1077
+ }
1078
+ if (flag.reason === "cursor_invalid") {
1079
+ return `[listing truncated: after ${pages} the server sent a pagination cursor that is not a string, so the walk could not continue — more entries may exist beyond this listing]`;
1080
+ }
1081
+ if (flag.reason === "budget_exhausted") {
1082
+ const budget = flag.budgetMs !== undefined ? `${flag.budgetMs}ms ` : "";
1083
+ return `[listing truncated: after ${pages} the ${budget}time budget for this listing was too low to give the next page a usable request, so the client stopped without sending one — this is a client-side bound, not a server failure; more entries may exist beyond this listing]`;
1084
+ }
1085
+ return `[listing incomplete: the server failed mid-pagination after ${pages} (${inlineUntrusted(flag.error ?? "unknown error", 240)}) — only the pages retrieved before the failure are shown]`;
1086
+ }
878
1087
  async function readDirViaExtension(rs, uri, signal, timeoutMs, watchdog) {
879
1088
  const resources = [];
880
1089
  let cursor;
@@ -918,10 +1127,13 @@ function renderDirChildren(server, uri, children, flags) {
918
1127
  if (flags?.cursorInvalid) {
919
1128
  notes.push(`[listing incomplete: the server rejected the pagination cursor (invalid or expired) — only the pages retrieved before the rejection are shown; more entries may exist beyond this listing]`);
920
1129
  }
1130
+ if (flags?.listing)
1131
+ notes.push(listingIncompleteNote(flags.listing));
921
1132
  const detailFlags = {
922
1133
  ...(flags?.truncated ? { truncated: true } : {}),
923
1134
  ...(flags?.incomplete !== undefined ? { incomplete: true, error: flags.incomplete } : {}),
924
1135
  ...(flags?.cursorInvalid ? { incomplete: true, cursorInvalid: true } : {}),
1136
+ ...(flags?.listing ? { incomplete: true, listing: flags.listing } : {}),
925
1137
  };
926
1138
  const listing = children.length === 0
927
1139
  ? `(Empty directory: ${inlineUntrusted(uri)})`
@@ -959,6 +1171,7 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
959
1171
  const sections = [];
960
1172
  const all = [];
961
1173
  const errors = [];
1174
+ const incomplete = [];
962
1175
  for (const rs of targets) {
963
1176
  if (isServerRevoked(rs.server)) {
964
1177
  errors.push({ server: rs.server, error: "server revoked by the operator mid-session (request not sent)" });
@@ -973,16 +1186,30 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
973
1186
  const idleMs = mcpIdleTimeoutMs(rs.transportKind);
974
1187
  const watchdog = armMcpIdleWatchdog(rs.health, idleMs, signal);
975
1188
  try {
976
- const res = await rs.client.listResources(undefined, { signal: watchdog.combinedSignal, timeout: mcpToolTimeoutMs() });
977
- all.push(...res.resources.map((r) => ({ server: rs.server, ...r })));
978
- const lines = res.resources.map((r) => resourceLine(r));
979
- sections.push(`[${rs.server}]\n${lines.length ? lines.join("\n") : "(no resources)"}`);
1189
+ const walk = await walkMcpListPages(async (cursor, remainingMs) => {
1190
+ const page = await rs.client
1191
+ .listResources(cursor === undefined ? undefined : { cursor }, { signal: watchdog.combinedSignal, timeout: remainingMs })
1192
+ .catch((err) => {
1193
+ throw watchdog.idleSignal.reason === IDLE_WATCHDOG_ABORT_REASON ? new Error(idleWatchdogTripText(idleMs)) : err;
1194
+ });
1195
+ return { items: page.resources, ...(page.nextCursor !== undefined ? { nextCursor: page.nextCursor } : {}) };
1196
+ }, { budgetMs: mcpToolTimeoutMs(), onPage: () => watchdog.rearm(), fingerprintOf: listEntryFingerprint, ...(signal !== undefined ? { signal } : {}) });
1197
+ all.push(...walk.items.map((r) => ({ server: rs.server, ...r })));
1198
+ const lines = walk.items.map((r) => resourceLine(r));
1199
+ const body = lines.length ? lines.join("\n") : "(no resources)";
1200
+ if (walk.incomplete) {
1201
+ incomplete.push({ server: rs.server, ...walk.incomplete });
1202
+ sections.push(`[${rs.server}]\n${body}\n${listingIncompleteNote(walk.incomplete)}`);
1203
+ }
1204
+ else {
1205
+ sections.push(`[${rs.server}]\n${body}`);
1206
+ }
980
1207
  }
981
1208
  catch (err) {
982
1209
  if (signal?.aborted)
983
1210
  throw err;
984
1211
  const msg = watchdog.idleSignal.reason === IDLE_WATCHDOG_ABORT_REASON
985
- ? `received no response for ${idleMs}ms (idle watchdog; set MCP_IDLE_TIMEOUT_STDIO / MCP_IDLE_TIMEOUT_HTTP to change this bound)`
1212
+ ? idleWatchdogTripText(idleMs)
986
1213
  : err instanceof Error
987
1214
  ? err.message
988
1215
  : String(err);
@@ -996,7 +1223,7 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
996
1223
  const body = sections.join("\n\n");
997
1224
  return {
998
1225
  content: [{ type: "text", text: sections.length ? delimitUntrusted("mcp resources", body) : "(no MCP resources)" }],
999
- details: { resources: all, ...(errors.length ? { errors } : {}) },
1226
+ details: { resources: all, ...(errors.length ? { errors } : {}), ...(incomplete.length ? { incomplete } : {}) },
1000
1227
  terminate: false,
1001
1228
  };
1002
1229
  },
@@ -1125,12 +1352,15 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
1125
1352
  return renderDirChildren(server, uri, r.resources, { truncated: r.truncated, incomplete: r.incomplete, cursorInvalid: r.cursorInvalid });
1126
1353
  watchdog.rearm();
1127
1354
  }
1128
- const res = await rs.client
1129
- .listResources(undefined, { signal: watchdog.combinedSignal, timeout: timeoutMs })
1130
- .catch((err) => rethrowHonestMcpError(err, { server, what, timeoutMs, writeEffect: false, signal, attributeServer: true, idle: { signal: watchdog.idleSignal, idleMs } }));
1355
+ const walk = await walkMcpListPages(async (cursor, remainingMs) => {
1356
+ const page = await rs.client
1357
+ .listResources(cursor === undefined ? undefined : { cursor }, { signal: watchdog.combinedSignal, timeout: remainingMs })
1358
+ .catch((err) => rethrowHonestMcpError(err, { server, what, timeoutMs: remainingMs, writeEffect: false, signal, attributeServer: true, idle: { signal: watchdog.idleSignal, idleMs } }));
1359
+ return { items: page.resources, ...(page.nextCursor !== undefined ? { nextCursor: page.nextCursor } : {}) };
1360
+ }, { budgetMs: timeoutMs, onPage: () => watchdog.rearm(), fingerprintOf: listEntryFingerprint, ...(signal !== undefined ? { signal } : {}) });
1131
1361
  const dirPrefix = uri.endsWith("/") ? uri : `${uri}/`;
1132
- const children = res.resources.filter((r) => typeof r.uri === "string" && r.uri !== uri && r.uri.startsWith(dirPrefix));
1133
- return renderDirChildren(server, uri, children);
1362
+ const children = walk.items.filter((r) => typeof r.uri === "string" && r.uri !== uri && r.uri.startsWith(dirPrefix));
1363
+ return renderDirChildren(server, uri, children, walk.incomplete ? { listing: walk.incomplete } : undefined);
1134
1364
  }
1135
1365
  finally {
1136
1366
  watchdog.dispose();
@@ -1177,7 +1407,8 @@ const LenientListToolsResultSchema = {
1177
1407
  });
1178
1408
  }
1179
1409
  const nextCursor = data.nextCursor;
1180
- return { success: true, data: { tools, ...(typeof nextCursor === "string" ? { nextCursor } : {}) } };
1410
+ const cursorInvalid = nextCursor !== undefined && nextCursor !== null && typeof nextCursor !== "string";
1411
+ return { success: true, data: { tools, ...(typeof nextCursor === "string" ? { nextCursor } : {}), ...(cursorInvalid ? { cursorInvalid: true } : {}) } };
1181
1412
  },
1182
1413
  };
1183
1414
  export function parseCallToolResultLenient(data) {
@@ -1192,7 +1423,21 @@ export function parseCallToolResultLenient(data) {
1192
1423
  }
1193
1424
  const LENIENT_CALL_TOOL_RESULT_SCHEMA = { safeParse: parseCallToolResultLenient };
1194
1425
  async function listToolsLenient(client, options) {
1195
- return client.request({ method: "tools/list", params: {} }, LenientListToolsResultSchema, options);
1426
+ if (!client.getServerCapabilities()?.tools)
1427
+ return { tools: [], toolsCapabilityAbsent: true };
1428
+ const walk = await walkMcpListPages(async (cursor, remainingMs) => {
1429
+ const page = await client.request({ method: "tools/list", params: cursor === undefined ? {} : { cursor } }, LenientListToolsResultSchema, { ...options, timeout: remainingMs });
1430
+ return {
1431
+ items: page.tools,
1432
+ ...(page.nextCursor !== undefined ? { nextCursor: page.nextCursor } : {}),
1433
+ ...(page.cursorInvalid === true ? { cursorInvalid: true } : {}),
1434
+ };
1435
+ }, {
1436
+ budgetMs: options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC,
1437
+ fingerprintOf: listEntryFingerprint,
1438
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
1439
+ });
1440
+ return { tools: walk.items, ...(walk.incomplete !== undefined ? { incomplete: walk.incomplete } : {}) };
1196
1441
  }
1197
1442
  async function connectServer(spec, principal, onElicit, imageResizer, reminderDisclosure, isServerRevoked) {
1198
1443
  const elicitOn = spec.elicitation === true && onElicit !== undefined;
@@ -1230,7 +1475,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
1230
1475
  };
1231
1476
  const listed = await listToolsLenient(client, startupOpts);
1232
1477
  cacheMcpToolMetadata(client, listed.tools);
1233
- const { serverTools, serverAxes, dropped } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked);
1478
+ const { serverTools, serverAxes, dropped, advisories } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked);
1234
1479
  const caps = client.getServerCapabilities();
1235
1480
  const resourceInfo = caps?.resources
1236
1481
  ? {
@@ -1254,7 +1499,11 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
1254
1499
  tools: serverTools,
1255
1500
  axes: serverAxes,
1256
1501
  dropped,
1502
+ schemaAdvisories: advisories,
1503
+ ...(listed.toolsCapabilityAbsent === true ? { toolsCapabilityAbsent: true } : {}),
1504
+ listedTools: listed.tools,
1257
1505
  ...(instructions ? { instructions } : {}),
1506
+ ...(listed.incomplete !== undefined ? { listingIncomplete: listed.incomplete } : {}),
1258
1507
  ...(resourceInfo && (resourceInfo.listAllowed || resourceInfo.readAllowed) ? { resourceServer: { server: spec.name, client, health, transportKind: idleKindOf(spec), ...resourceInfo } } : {}),
1259
1508
  };
1260
1509
  }
@@ -1267,14 +1516,28 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1267
1516
  const serverTools = [];
1268
1517
  const serverAxes = [];
1269
1518
  const dropped = [];
1270
- const mintedNames = new Set();
1519
+ const advisories = [];
1520
+ const mintedNames = new Map();
1271
1521
  for (const t of listed.tools) {
1272
1522
  if (spec.allowTools && !spec.allowTools.includes(t.name)) {
1273
1523
  continue;
1274
1524
  }
1275
- const normalized = normalizeMcpToolSchema(t.inputSchema);
1276
- let effectiveInputSchema = t.inputSchema;
1277
- let effectiveDescription = t.description !== undefined ? sanitizeUntrustedText(t.description) : undefined;
1525
+ let modelFacingSchema;
1526
+ let sanitizedDescription;
1527
+ try {
1528
+ modelFacingSchema = sanitizeMcpModelFacingValue(t.inputSchema);
1529
+ sanitizedDescription = t.description !== undefined ? sanitizeMcpModelFacingText(t.description) : undefined;
1530
+ }
1531
+ catch (err) {
1532
+ const reason = err instanceof McpPayloadKeyCollision
1533
+ ? `two of its advertised names neutralize to the same model-facing spelling ${inlineUntrusted(JSON.stringify(err.message), 80)}; one parameter would have vanished from the schema the model is given while the server still expects it`
1534
+ : "advertised tool payload could not be neutralized for model-facing display (pathological nesting)";
1535
+ dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(reason, 240) });
1536
+ continue;
1537
+ }
1538
+ const normalized = normalizeMcpToolSchema(modelFacingSchema);
1539
+ let effectiveInputSchema = modelFacingSchema;
1540
+ let effectiveDescription = sanitizedDescription !== undefined ? sanitizeUntrustedText(sanitizedDescription) : undefined;
1278
1541
  if (normalized.outcome === "drop") {
1279
1542
  dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(normalized.reason, 240) });
1280
1543
  continue;
@@ -1288,16 +1551,23 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1288
1551
  dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(schemaProblem, 240) });
1289
1552
  continue;
1290
1553
  }
1554
+ const schemaAdvisory = mcpToolSchemaAdvisory(effectiveInputSchema);
1555
+ if (schemaAdvisory !== undefined) {
1556
+ advisories.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(schemaAdvisory, 240) });
1557
+ }
1291
1558
  const remoteName = t.name;
1292
1559
  const namespacedName = mintNamespacedToolName(MCP_NAMESPACE, spec.name, remoteName);
1293
- if (mintedNames.has(namespacedName)) {
1560
+ const mintedBy = mintedNames.get(namespacedName);
1561
+ if (mintedBy !== undefined) {
1294
1562
  dropped.push({
1295
1563
  tool: inlineUntrusted(t.name),
1296
- reason: `name collides with an earlier tool of this server: both mount as "${namespacedName}" after namespacing (only [a-zA-Z0-9_-] survives), and one name cannot denote two tools`,
1564
+ reason: mintedBy === t.name
1565
+ ? `this server listed this tool name more than once in one listing; the first entry is mounted as "${namespacedName}" and the repeat is dropped (one name cannot denote two tools)`
1566
+ : `name collides with an earlier tool of this server: both mount as "${namespacedName}" after namespacing (only [a-zA-Z0-9_-] survives), and one name cannot denote two tools`,
1297
1567
  });
1298
1568
  continue;
1299
1569
  }
1300
- mintedNames.add(namespacedName);
1570
+ mintedNames.set(namespacedName, t.name);
1301
1571
  const hintAxis = mcpAxisFor(namespacedName, t.annotations);
1302
1572
  const axis = applyCallerAxisOverride(namespacedName, hintAxis, spec.toolAxes?.[remoteName]);
1303
1573
  if (axis)
@@ -1308,7 +1578,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1308
1578
  const mcpAlwaysLoad = mcpToolMeta?.["anthropic/alwaysLoad"] === true;
1309
1579
  serverTools.push({
1310
1580
  name: namespacedName,
1311
- description: effectiveDescription ?? `MCP tool ${remoteName} from ${spec.name}`,
1581
+ description: capMcpToolDescription(effectiveDescription ?? `MCP tool ${inlineUntrusted(sanitizeMcpModelFacingText(remoteName))} from ${spec.name}`),
1312
1582
  label: `${spec.name}:${remoteName}`,
1313
1583
  parameters: (effectiveInputSchema ?? { type: "object" }),
1314
1584
  ...(mcpMaxResultSizeChars !== undefined ? { mcpMaxResultSizeChars } : {}),
@@ -1410,10 +1680,25 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1410
1680
  },
1411
1681
  });
1412
1682
  }
1413
- return { serverTools, serverAxes, dropped };
1683
+ return { serverTools, serverAxes, dropped, advisories };
1414
1684
  }
1415
1685
  function asServerWarning(spec, err) {
1416
1686
  const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${namedMcpFailureText(err)})`, { cause: err });
1417
1687
  warning.code = "mcp.server_unavailable";
1418
1688
  return warning;
1419
1689
  }
1690
+ function schemaAdvisoryWarning(server, tool, reason) {
1691
+ const warning = new Error(`mcp: server "${inlineUntrusted(server, 160)}" tool "${tool}" is mounted, but ${reason}.`);
1692
+ warning.code = "mcp.tool_schema_advisory";
1693
+ return warning;
1694
+ }
1695
+ function toolsCapabilityAbsentWarning(server) {
1696
+ const warning = new Error(`mcp: server "${inlineUntrusted(server, 160)}" declared no "tools" capability, so no tool listing was requested and none of the tools named in this server's allowTools can be mounted. If the server does serve tools, it must declare the capability at initialize.`);
1697
+ warning.code = "mcp.tools_capability_absent";
1698
+ return warning;
1699
+ }
1700
+ function listingIncompleteWarning(server, flag) {
1701
+ const warning = new Error(`mcp: server "${inlineUntrusted(server, 160)}" listed its tools INCOMPLETELY — ${listingIncompleteNote(flag)}. The tools beyond the ${flag.pages} page${flag.pages === 1 ? "" : "s"} retrieved are NOT mounted for this task.`);
1702
+ warning.code = "mcp.listing_incomplete";
1703
+ return warning;
1704
+ }