@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.
- package/CHANGELOG.md +150 -0
- package/dist/agents/cumulative-stats.d.ts +26 -0
- package/dist/agents/cumulative-stats.js +56 -0
- package/dist/agents/observer.d.ts +11 -7
- package/dist/agents/observer.js +2 -4
- package/dist/agents/subagent.d.ts +4 -0
- package/dist/agents/subagent.js +1 -1
- package/dist/agents/verify.d.ts +27 -3
- package/dist/agents/verify.js +7 -2
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.js +1 -1
- package/dist/core/lsp-diagnostics.d.ts +19 -17
- package/dist/core/lsp-diagnostics.js +11 -5
- package/dist/core/mcp.d.ts +214 -5
- package/dist/core/mcp.js +315 -30
- package/dist/core/memory-engine/consolidation.d.ts +378 -0
- package/dist/core/memory-engine/consolidation.js +342 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +237 -4
- package/dist/core/memory-engine/engine.js +1111 -4
- package/dist/core/memory-engine/export-bundle.js +9 -0
- package/dist/core/memory-engine/file-backend.js +27 -1
- package/dist/core/memory-engine/frontmatter.d.ts +20 -1
- package/dist/core/memory-engine/frontmatter.js +111 -0
- package/dist/core/memory-engine/index.d.ts +4 -2
- package/dist/core/memory-engine/index.js +3 -1
- package/dist/core/memory-engine/memory-backend-contract.js +131 -0
- package/dist/core/memory-engine/sync-client.js +26 -0
- package/dist/core/memory-engine/tools.d.ts +9 -0
- package/dist/core/memory-engine/tools.js +57 -13
- package/dist/core/memory-engine/types.d.ts +99 -0
- package/dist/core/memory-recall.js +4 -3
- package/dist/core/memory.d.ts +33 -3
- package/dist/core/memory.js +6 -4
- package/dist/core/permission-rule-model.d.ts +140 -21
- package/dist/core/permission-rule-model.js +76 -17
- package/dist/core/permission-rule-org.d.ts +4 -3
- package/dist/core/permission-rule-org.js +12 -3
- package/dist/core/permission-rules.d.ts +22 -0
- package/dist/core/permission-rules.js +60 -6
- package/dist/core/reminder-disclosure.d.ts +29 -4
- package/dist/core/reminder-disclosure.js +60 -12
- package/dist/core/runner/prepare-memory.js +7 -2
- package/dist/core/runner/prepare-task.d.ts +31 -1
- package/dist/core/runner/prepare-task.js +31 -14
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +12 -10
- package/dist/core/runner/session-rule-policy.js +5 -3
- package/dist/core/runner/synthetic-tools.js +4 -2
- package/dist/core/runner/turn-attachments.d.ts +16 -6
- package/dist/core/runner/turn-attachments.js +34 -20
- package/dist/core/tool-policy.d.ts +18 -0
- package/dist/core/tool-policy.js +19 -8
- package/dist/core/types.d.ts +89 -6
- package/dist/core/untrusted-egress.js +12 -2
- package/dist/core/untrusted-text.d.ts +189 -3
- package/dist/core/untrusted-text.js +416 -6
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/workflow-types.d.ts +16 -6
- package/dist/orchestration/workflow-types.js +10 -4
- package/dist/orchestration/workflow.js +32 -6
- package/dist/prompts/default.d.ts +12 -2
- package/dist/stores/file/background-agent-store.js +1 -0
- package/dist/stores/file/checkpoint-store.d.ts +6 -2
- package/dist/stores/file/checkpoint-store.js +1 -0
- package/dist/stores/file/fs-atomic.d.ts +151 -10
- package/dist/stores/file/fs-atomic.js +208 -32
- package/dist/stores/file/index.d.ts +26 -3
- package/dist/stores/file/index.js +25 -2
- package/dist/stores/file/shared-ledger.d.ts +40 -5
- package/dist/stores/file/shared-ledger.js +24 -8
- package/dist/stores/file/workflow-run-store.d.ts +8 -1
- package/dist/stores/file/workflow-run-store.js +1 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +71 -0
- package/dist/tools/fs/bash-readonly-classifier.js +58 -47
- package/dist/tools/fs/index.d.ts +3 -1
- package/package.json +1 -1
- 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,
|
|
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
|
-
|
|
785
|
-
const
|
|
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) => !
|
|
788
|
-
const removed =
|
|
789
|
-
h.
|
|
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.
|
|
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
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
?
|
|
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
|
|
1129
|
-
|
|
1130
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1276
|
-
let
|
|
1277
|
-
|
|
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
|
-
|
|
1560
|
+
const mintedBy = mintedNames.get(namespacedName);
|
|
1561
|
+
if (mintedBy !== undefined) {
|
|
1294
1562
|
dropped.push({
|
|
1295
1563
|
tool: inlineUntrusted(t.name),
|
|
1296
|
-
reason:
|
|
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.
|
|
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
|
+
}
|