@j0hanz/filesystem-mcp 2.7.6 → 2.8.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 (108) hide show
  1. package/dist/cli.d.ts +1 -9
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/errors.d.ts +4 -3
  5. package/dist/core/errors.d.ts.map +1 -1
  6. package/dist/core/errors.js +5 -16
  7. package/dist/core/errors.js.map +1 -1
  8. package/dist/core/fs.d.ts +11 -11
  9. package/dist/core/fs.d.ts.map +1 -1
  10. package/dist/core/fs.js +108 -43
  11. package/dist/core/fs.js.map +1 -1
  12. package/dist/core/glob.d.ts +8 -0
  13. package/dist/core/glob.d.ts.map +1 -1
  14. package/dist/core/glob.js +192 -29
  15. package/dist/core/glob.js.map +1 -1
  16. package/dist/core/input-required.d.ts +12 -0
  17. package/dist/core/input-required.d.ts.map +1 -1
  18. package/dist/core/input-required.js +7 -2
  19. package/dist/core/input-required.js.map +1 -1
  20. package/dist/core/observability.d.ts +2 -0
  21. package/dist/core/observability.d.ts.map +1 -1
  22. package/dist/core/observability.js +7 -0
  23. package/dist/core/observability.js.map +1 -1
  24. package/dist/core/path-completer.d.ts.map +1 -1
  25. package/dist/core/path-completer.js +11 -30
  26. package/dist/core/path-completer.js.map +1 -1
  27. package/dist/core/path.d.ts +0 -2
  28. package/dist/core/path.d.ts.map +1 -1
  29. package/dist/core/path.js +12 -29
  30. package/dist/core/path.js.map +1 -1
  31. package/dist/core/read.d.ts +1 -1
  32. package/dist/core/read.d.ts.map +1 -1
  33. package/dist/core/read.js +13 -14
  34. package/dist/core/read.js.map +1 -1
  35. package/dist/core/schema.d.ts +6 -1
  36. package/dist/core/schema.d.ts.map +1 -1
  37. package/dist/core/schema.js +17 -3
  38. package/dist/core/schema.js.map +1 -1
  39. package/dist/core/search.d.ts.map +1 -1
  40. package/dist/core/search.js +104 -51
  41. package/dist/core/search.js.map +1 -1
  42. package/dist/core/store.d.ts +36 -1
  43. package/dist/core/store.d.ts.map +1 -1
  44. package/dist/core/store.js +71 -6
  45. package/dist/core/store.js.map +1 -1
  46. package/dist/core/util.d.ts +34 -5
  47. package/dist/core/util.d.ts.map +1 -1
  48. package/dist/core/util.js +59 -5
  49. package/dist/core/util.js.map +1 -1
  50. package/dist/index.js +5 -16
  51. package/dist/index.js.map +1 -1
  52. package/dist/resources.d.ts +5 -1
  53. package/dist/resources.d.ts.map +1 -1
  54. package/dist/resources.js +45 -22
  55. package/dist/resources.js.map +1 -1
  56. package/dist/server.d.ts +9 -6
  57. package/dist/server.d.ts.map +1 -1
  58. package/dist/server.js +7 -8
  59. package/dist/server.js.map +1 -1
  60. package/dist/tools/create.d.ts.map +1 -1
  61. package/dist/tools/create.js +8 -19
  62. package/dist/tools/create.js.map +1 -1
  63. package/dist/tools/define.d.ts +8 -7
  64. package/dist/tools/define.d.ts.map +1 -1
  65. package/dist/tools/define.js +48 -16
  66. package/dist/tools/define.js.map +1 -1
  67. package/dist/tools/delete.d.ts.map +1 -1
  68. package/dist/tools/delete.js +39 -21
  69. package/dist/tools/delete.js.map +1 -1
  70. package/dist/tools/edit.d.ts.map +1 -1
  71. package/dist/tools/edit.js +24 -39
  72. package/dist/tools/edit.js.map +1 -1
  73. package/dist/tools/find-files.d.ts.map +1 -1
  74. package/dist/tools/find-files.js +49 -60
  75. package/dist/tools/find-files.js.map +1 -1
  76. package/dist/tools/index.d.ts +2 -11
  77. package/dist/tools/index.d.ts.map +1 -1
  78. package/dist/tools/index.js +1 -9
  79. package/dist/tools/index.js.map +1 -1
  80. package/dist/tools/list.d.ts.map +1 -1
  81. package/dist/tools/list.js +59 -69
  82. package/dist/tools/list.js.map +1 -1
  83. package/dist/tools/move.d.ts.map +1 -1
  84. package/dist/tools/move.js +5 -1
  85. package/dist/tools/move.js.map +1 -1
  86. package/dist/tools/patch.d.ts.map +1 -1
  87. package/dist/tools/patch.js +3 -6
  88. package/dist/tools/patch.js.map +1 -1
  89. package/dist/tools/read.d.ts.map +1 -1
  90. package/dist/tools/read.js +31 -40
  91. package/dist/tools/read.js.map +1 -1
  92. package/dist/tools/replace-text.d.ts.map +1 -1
  93. package/dist/tools/replace-text.js +23 -32
  94. package/dist/tools/replace-text.js.map +1 -1
  95. package/dist/tools/search-text.d.ts.map +1 -1
  96. package/dist/tools/search-text.js +48 -64
  97. package/dist/tools/search-text.js.map +1 -1
  98. package/dist/transport/http.js +1 -1
  99. package/dist/transport/http.js.map +1 -1
  100. package/dist/transport/shared.d.ts +7 -5
  101. package/dist/transport/shared.d.ts.map +1 -1
  102. package/dist/transport/shared.js +8 -2
  103. package/dist/transport/shared.js.map +1 -1
  104. package/dist/transport/stdio.d.ts +4 -1
  105. package/dist/transport/stdio.d.ts.map +1 -1
  106. package/dist/transport/stdio.js +45 -12
  107. package/dist/transport/stdio.js.map +1 -1
  108. package/package.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/core/search.ts"],"names":[],"mappings":"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","sourcesContent":["import { stat as fsStat, readFile } from 'node:fs/promises';\nimport { basename } from 'node:path';\n\nimport type { RE2ExecArray } from '@adguard/re2-wasm';\nimport { RE2 } from '@adguard/re2-wasm';\n\nimport { processEntriesConcurrently, resolveStopReason } from './concurrency.ts';\nimport { globEntries, type GlobEntry } from './glob.ts';\nimport type { PathGuard } from './path.ts';\nimport { escapeRegExp, getMaxTextFileSize, MIB, PARALLEL_CONCURRENCY } from './util.ts';\n\n/**\n * A fresh, flat copy of `s`. V8 keeps a substring of a long string as a slice\n * that references its parent, so a kept line would otherwise pin the whole\n * file's text for as long as the match list lives (60 s in the page store).\n */\nconst own = (s: string): string => Buffer.from(s, 'utf8').toString('utf8');\n\ninterface SearchResult {\n file: string;\n line: number;\n /** 0-indexed column of the first occurrence on the line. */\n column: number;\n content: string;\n matchCount: number;\n /** Up to `context` lines either side of the match; absent when context is 0. */\n before?: string[];\n after?: string[];\n}\n\nexport type Regex = RE2;\n\n/** Occurrence-counting bound, so one pathological line cannot spin forever. */\nconst MAX_MATCHES_PER_LINE = 100_000;\n\n/**\n * re2-wasm copies the whole input on every exec, so draining a dense pattern is\n * O(matches x input) and fully synchronous: the loop pins and the tool's\n * AbortSignal.timeout (a timer) can never fire. The returned `tick` yields a\n * macrotask every ~10ms and reports whether `signal` has aborted.\n */\nexport function createYielder(signal: AbortSignal | undefined): () => Promise<boolean> {\n let last = Date.now();\n return async () => {\n if (Date.now() - last > 10) {\n await new Promise((resolve) => {\n setImmediate(resolve);\n });\n last = Date.now();\n }\n return signal?.aborted ?? false;\n };\n}\n\n/**\n * Largest input one RE2 call may scan as a single string. The input is copied\n * into the same fixed 16 MB wasm heap as the patterns; about 10 MB of input\n * aborts the module. 4 MiB leaves room for the copy and the pattern set.\n */\nexport const RE2_MAX_INPUT_BYTES = 4 * MIB;\n\nconst BUFFER_ANCHOR_RE = /\\\\[AzZ]/u;\n/** `(?i)`, `(?-m)`, `(?s:…)`: inline flags can undo the `m` the prefilter relies on. */\nconst INLINE_FLAGS_RE = /\\(\\?[a-zA-Z-]+[:)]/u;\nconst CR = 13;\n\n/**\n * Compile a pattern on RE2 rather than on V8's irregexp.\n *\n * Patterns arrive from the MCP client, so a backtracking engine would let one\n * request pin the event loop with no way out: an abort signal cannot preempt a\n * synchronous `exec`, and on stdio that wedges the whole server. RE2 matches in\n * time linear in the input and cannot backtrack at all, so the hazard is gone\n * rather than bounded.\n *\n * RE2 rejects the constructs the tool schema documents as unsupported —\n * lookahead, lookbehind, backreferences — with its own {@link SyntaxError},\n * which the tool layer turns into a normal tool error. It always matches in\n * Unicode mode and requires the `u` flag to say so.\n *\n * Every compiled pattern owns memory in re2-wasm's fixed 16 MB heap, which\n * `ALLOW_MEMORY_GROWTH` is off for. re2-wasm never frees it and a\n * FinalizationRegistry does not keep up (V8 sees no pressure from the wasm\n * heap), so exhaustion is an emscripten `abort()` that kills regex search for\n * the rest of the process. Every caller MUST pass the result to\n * {@link freeRegex} when it is done with it.\n */\nexport function compileRegex(\n pattern: string,\n options: { caseSensitive?: boolean; multiline?: boolean } = {},\n): Regex {\n const flags = `g${options.multiline ? 'm' : ''}${options.caseSensitive ? '' : 'i'}u`;\n try {\n return new RE2(pattern, flags);\n } catch (error) {\n if (!(error instanceof SyntaxError)) throw error;\n throw new SyntaxError(\n `${error.message} — lookahead, lookbehind, and backreferences are not supported.`,\n { cause: error },\n );\n }\n}\n\n/**\n * Release a compiled pattern's wasm memory. re2-wasm exposes no disposal of its\n * own, so this reaches the embind handle it holds privately; a version that\n * renames the field degrades to the pre-existing leak rather than throwing.\n * Idempotent — a second call on an already-freed handle is swallowed. Using a\n * {@link Regex} after freeing it is undefined behaviour in the wasm heap, so\n * free only in a `finally` that owns the compile.\n */\nexport function freeRegex(regex: Regex | undefined): void {\n const handle = (regex as unknown as { wrapper?: { delete?: () => void } } | undefined)?.wrapper;\n try {\n handle?.delete?.();\n } catch {\n // already deleted, or a re2-wasm build without embind disposal\n }\n}\n\nexport interface RegexMatch {\n /** The matched text. */\n text: string;\n /** Capture groups, 1-indexed in the pattern, 0-indexed here. */\n groups: (string | undefined)[];\n /** Named capture groups, when the pattern declares any. */\n named: Record<string, string> | undefined;\n /** Start offset in UTF-16 code units — usable with `String.prototype.slice`. */\n start: number;\n /** End offset in UTF-16 code units. */\n end: number;\n}\n\n/** Code-point length of `text`, i.e. its UTF-16 length minus its surrogate pairs. */\nconst codePointLength = (text: string): number =>\n // eslint-disable-next-line @typescript-eslint/no-misused-spread -- code points are the intended unit (re2-wasm offsets count code points); grapheme segmentation would change the count, not preserve it.\n [...text].length;\n\n/**\n * Iterate a global pattern's non-overlapping matches with offsets JS can use.\n *\n * re2-wasm reports `index` and `lastIndex` in **code points**, while every JS\n * string operation indexes in UTF-16 code units. The two agree only until the\n * first astral character (most emoji), past which every offset is short by one\n * per surrogate pair — slicing on the raw index cuts into the middle of\n * neighbouring text. Its `lastIndex` bookkeeping is worse still: it adds the\n * match's UTF-16 length to a code-point index, so an astral character inside a\n * match can skip the next one. This is the single place that corrects both; no\n * caller should read `match.index` or `regex.lastIndex` directly.\n *\n * Zero-length matches (e.g. `a*`) advance by one code point so they cannot loop\n * forever. The regex is global and shared across calls, so `lastIndex` is reset\n * on entry.\n */\nexport function* execMatches(regex: Regex, text: string): Generator<RegexMatch, undefined> {\n regex.lastIndex = 0;\n // Cursor into `text`, carried forward across matches: RE2 reports them in\n // ascending order, so the walk is O(text) in total rather than per match.\n let cursorCp = 0;\n let cursorU16 = 0;\n let match: RE2ExecArray | null;\n while ((match = regex.exec(text)) !== null) {\n while (cursorCp < match.index && cursorU16 < text.length) {\n const isPair =\n (text.charCodeAt(cursorU16) & 0xfc00) === 0xd800 &&\n (text.charCodeAt(cursorU16 + 1) & 0xfc00) === 0xdc00;\n cursorU16 += isPair ? 2 : 1;\n cursorCp++;\n }\n const matched = match[0] ?? '';\n const start = cursorU16;\n yield {\n text: matched,\n groups: match.slice(1),\n named: match.groups,\n start,\n end: start + matched.length,\n };\n // Own the advance rather than trusting the wrapper's mixed-unit arithmetic.\n regex.lastIndex = match.index + (matched.length === 0 ? 1 : codePointLength(matched));\n }\n}\n\n/**\n * Find non-overlapping occurrences of a global regex in a single line, reporting\n * the first match's column alongside the count.\n */\nasync function findLineMatches(\n regex: Regex,\n line: string,\n tick: () => Promise<boolean>,\n): Promise<{ count: number; column: number } | undefined> {\n let count = 0;\n let column = -1;\n // Awaiting mid-drain is safe with a shared regex: execMatches re-writes\n // lastIndex from its own match before every exec, with no await inside.\n for (const match of execMatches(regex, line)) {\n if (column === -1) column = match.start;\n count++;\n if (count >= MAX_MATCHES_PER_LINE || (await tick())) break;\n }\n return count > 0 ? { count, column } : undefined;\n}\n\nexport interface SearchContentOptions {\n caseSensitive?: boolean;\n isRegex?: boolean;\n maxResults?: number;\n filePattern?: string;\n skipIgnored?: boolean;\n includeHidden?: boolean;\n maxDepth?: number;\n /**\n * Search exactly this file (absolute, already guard-validated) instead of\n * walking `directory` with `filePattern`. Set when the caller named a file:\n * its name must not be interpreted as a glob.\n */\n explicitFile?: string;\n /** Lines of context to carry either side of each match; 0 (default) carries none. */\n context?: number;\n signal?: AbortSignal;\n}\n\nexport interface SearchContentOutcome {\n basePath: string;\n matches: SearchResult[];\n summary: {\n /**\n * Matching *lines*, one per entry in `matches` — not pattern occurrences.\n * A line with three occurrences counts once here and reports 3 in its own\n * `SearchResult.matchCount`.\n */\n matchingLines: number;\n filesScanned: number;\n filesMatched: number;\n truncated: boolean;\n /** Files the guard rejected or that could not be stat'd. */\n skippedInaccessible: number;\n /** Files skipped unread because they exceed maxFileSize. */\n skippedTooLarge: number;\n /** Files skipped because they contain a NUL byte (binary). */\n skippedBinary: number;\n /**\n * `StoppedReason` narrowed to the stops these scans can produce: both\n * resolve the stop through `resolveStopReason` (concurrency.ts) — never\n * `maxFiles`, which belongs to `replace_text`'s per-file cap.\n */\n stoppedReason?: 'maxResults' | 'timeout';\n };\n}\n\ntype FileScan =\n | { kind: 'inaccessible' }\n | { kind: 'tooLarge' }\n | { kind: 'binary' }\n | { kind: 'scanned'; matches: SearchResult[] };\n\ninterface ScanContext {\n pathGuard: PathGuard;\n signal: AbortSignal | undefined;\n maxFileSize: number;\n maxResults: number;\n regex: Regex;\n prefilter: Regex;\n source: string;\n context: number;\n}\n\nasync function scanFile(entryPath: string, scanCtx: ScanContext): Promise<FileScan> {\n try {\n await scanCtx.pathGuard.validateExistingPath(entryPath);\n } catch {\n return { kind: 'inaccessible' };\n }\n\n try {\n const stats = await fsStat(entryPath);\n if (stats.size > scanCtx.maxFileSize) return { kind: 'tooLarge' };\n } catch {\n return { kind: 'inaccessible' };\n }\n\n let buffer: Buffer;\n try {\n buffer = await readFile(entryPath, { signal: scanCtx.signal });\n } catch (error) {\n if (scanCtx.signal?.aborted) throw error;\n return { kind: 'inaccessible' };\n }\n\n // A NUL byte marks a binary file, as it does for `read` and `grep -I`.\n // Non-UTF-8 text is still searched: a lossy decode only affects the\n // bytes a pattern could not have matched anyway.\n if (buffer.includes(0)) return { kind: 'binary' };\n\n const content = buffer.toString('utf-8');\n if (canPrefilter(scanCtx.source, buffer)) {\n scanCtx.prefilter.lastIndex = 0;\n if (scanCtx.prefilter.exec(content) === null) return { kind: 'scanned', matches: [] };\n }\n\n const lines = content.split(/\\r?\\n/u);\n // A trailing newline splits into a phantom empty last element, and an\n // empty file splits into one empty element: neither is a line the\n // file has, for matching or for context.\n const lineCount =\n content.length === 0 ? 0 : content.endsWith('\\n') ? lines.length - 1 : lines.length;\n // A single line past RE2_MAX_INPUT_BYTES OOMs the wasm module for the rest of\n // the process; report the file tooLarge instead. Only a buffer over the bound\n // can hold such a line, so ordinary files skip the per-line measure.\n const checkLineSize = buffer.length > RE2_MAX_INPUT_BYTES;\n const matches: SearchResult[] = [];\n const tick = createYielder(scanCtx.signal);\n for (let i = 0; i < lineCount; i++) {\n const line = lines[i];\n if (line === undefined) continue;\n if (checkLineSize && Buffer.byteLength(line, 'utf8') > RE2_MAX_INPUT_BYTES) {\n return { kind: 'tooLarge' };\n }\n // Yield between lines too: millions of short lines never yield per-match.\n if (await tick()) break;\n // One scan per line: the helper resets lastIndex itself, so it\n // doubles as the \"does this line match\" test.\n const found = await findLineMatches(scanCtx.regex, line, tick);\n if (found) {\n matches.push({\n file: entryPath,\n line: i + 1,\n column: found.column,\n content: own(line),\n matchCount: found.count,\n ...(scanCtx.context > 0\n ? {\n before: lines.slice(Math.max(0, i - scanCtx.context), i).map(own),\n after: lines.slice(i + 1, Math.min(lineCount, i + 1 + scanCtx.context)).map(own),\n }\n : {}),\n });\n if (matches.length >= scanCtx.maxResults) break;\n }\n }\n return { kind: 'scanned', matches };\n}\n\nexport async function searchContent(\n directory: string,\n pattern: string,\n options: SearchContentOptions,\n pathGuard: PathGuard,\n): Promise<SearchContentOutcome> {\n const source = options.isRegex ? pattern || '' : escapeRegExp(pattern || '');\n // Compiles live inside the try so a failing second compile does not leak the\n // first's wasm memory; freeRegex is null-safe.\n let regex: Regex | undefined;\n let prefilter: Regex | undefined;\n try {\n regex = compileRegex(source, {\n caseSensitive: Boolean(options.caseSensitive),\n });\n // Whole-buffer test: `m` makes ^/$ line-relative so a per-line hit is always a\n // buffer hit (the converse can be false — a false positive just falls through\n // to the per-line scan). \\A/\\z/\\Z are buffer-relative even under `m`, inline\n // flag groups can undo `m`, and $ cannot see past a \\r, so those inputs skip\n // the prefilter (see canPrefilter).\n prefilter = compileRegex(source, {\n caseSensitive: Boolean(options.caseSensitive),\n multiline: true,\n });\n const matches: SearchResult[] = [];\n const maxResults = options.maxResults ?? 100;\n const maxFileSize = getMaxTextFileSize();\n const context = options.context ?? 0;\n const ceiling = pathGuard.allowedRootContaining(directory);\n\n const entries = options.explicitFile\n ? [{ path: options.explicitFile }]\n : globEntries({\n cwd: directory,\n pattern: options.filePattern ?? '**/*',\n // Same rule as replace_text, so a search with the same glob previews the\n // files a replace would touch: a slash-free glob matches at any depth.\n baseNameMatch: true,\n includeHidden: Boolean(options.includeHidden),\n skipIgnored: Boolean(options.skipIgnored),\n ...(ceiling !== undefined ? { ignoreCeiling: ceiling } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.maxDepth !== undefined ? { maxDepth: options.maxDepth } : {}),\n suppressErrors: true,\n });\n\n let filesScanned = 0;\n let filesMatched = 0;\n let matchingLines = 0;\n let skippedTooLarge = 0;\n let skippedBinary = 0;\n const scanCtx: ScanContext = {\n pathGuard,\n signal: options.signal,\n maxFileSize,\n maxResults,\n regex,\n prefilter,\n source,\n context,\n };\n const perFile = new Map<number, FileScan>();\n let dispatched = 0;\n let collected = 0;\n const stopped = await processEntriesConcurrently(entries, {\n signal: options.signal,\n concurrency: PARALLEL_CONCURRENCY,\n shouldStop: () => collected >= maxResults,\n onEntry: () => {\n dispatched++;\n },\n onError: () => undefined,\n runEntry: async (entryPath) => {\n const index = dispatched - 1;\n const scan = await scanFile(entryPath, scanCtx);\n perFile.set(index, scan);\n if (scan.kind === 'scanned') collected += scan.matches.length;\n },\n });\n\n let skippedInaccessible = 0;\n for (let i = 0; i < dispatched; i++) {\n const scan = perFile.get(i);\n if (!scan) continue;\n switch (scan.kind) {\n case 'inaccessible':\n skippedInaccessible++;\n break;\n case 'tooLarge':\n skippedTooLarge++;\n break;\n case 'binary':\n filesScanned++;\n skippedBinary++;\n break;\n case 'scanned':\n filesScanned++;\n if (scan.matches.length > 0) filesMatched++;\n for (const match of scan.matches) {\n if (matches.length >= maxResults) break;\n matches.push(match);\n matchingLines++;\n }\n break;\n }\n }\n\n const stoppedReason = resolveStopReason(\n matches.length >= maxResults,\n stopped === 'timeout' || Boolean(options.signal?.aborted),\n );\n\n return {\n basePath: directory,\n matches,\n summary: {\n matchingLines,\n filesScanned,\n filesMatched,\n truncated: stoppedReason !== undefined,\n skippedInaccessible,\n skippedTooLarge,\n skippedBinary,\n ...(stoppedReason ? { stoppedReason } : {}),\n },\n };\n } finally {\n freeRegex(prefilter);\n freeRegex(regex);\n }\n}\n\n/** Whether one whole-buffer RE2 test is a sound \"cannot match\" check for this file. */\nfunction canPrefilter(source: string, buffer: Buffer): boolean {\n if (buffer.length > RE2_MAX_INPUT_BYTES) return false;\n if (BUFFER_ANCHOR_RE.test(source)) return false;\n if (INLINE_FLAGS_RE.test(source)) return false;\n if (source.includes('$') && buffer.includes(CR)) return false;\n return true;\n}\n\nasync function* guardedEntries(\n entries: AsyncIterable<GlobEntry>,\n pathGuard: PathGuard,\n signal: AbortSignal | undefined,\n counters: { skippedInaccessible: number; stoppedByAbort: boolean },\n): AsyncGenerator<GlobEntry> {\n // The signal carries both client cancellation and the tool's search timeout,\n // so an abort means \"return what we have, marked incomplete\" rather than\n // throw — but it must never be reported as a finished scan. The walk itself\n // throws when the signal stops it before a single entry is yielded.\n try {\n for await (const entry of entries) {\n if (signal?.aborted) {\n counters.stoppedByAbort = true;\n return;\n }\n try {\n await pathGuard.validateExistingPath(entry.path);\n } catch {\n counters.skippedInaccessible++;\n continue;\n }\n yield entry;\n }\n } catch (error) {\n if (!signal?.aborted) throw error;\n counters.stoppedByAbort = true;\n }\n}\n\nexport async function searchFiles(\n directory: string,\n pattern: string,\n options: {\n maxResults?: number;\n includeHidden?: boolean;\n sortBy?: 'name' | 'path';\n skipIgnored?: boolean;\n maxDepth?: number;\n signal?: AbortSignal;\n },\n pathGuard: PathGuard,\n): Promise<{\n basePath: string;\n results: { path: string }[];\n summary: {\n matched: number;\n filesScanned: number;\n truncated: boolean;\n skippedInaccessible: number;\n /** Narrowed like SearchContentOutcome's — scans never `maxFiles`. */\n stoppedReason?: 'maxResults' | 'timeout';\n };\n}> {\n const maxResults = options.maxResults ?? 100;\n const ceiling = pathGuard.allowedRootContaining(directory);\n const entries = globEntries({\n cwd: directory,\n pattern,\n baseNameMatch: true,\n includeHidden: Boolean(options.includeHidden),\n skipIgnored: Boolean(options.skipIgnored),\n ...(ceiling !== undefined ? { ignoreCeiling: ceiling } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.maxDepth !== undefined ? { maxDepth: options.maxDepth } : {}),\n suppressErrors: true,\n });\n const results: { path: string }[] = [];\n let filesScanned = 0;\n const counters = { skippedInaccessible: 0, stoppedByAbort: false };\n\n for await (const entry of guardedEntries(entries, pathGuard, options.signal, counters)) {\n if (results.length >= maxResults) break;\n filesScanned++;\n results.push({ path: entry.path });\n }\n\n // Sorting — only name / path are supported; size / modified were removed\n // (the glob never collected stats, so they were always undefined).\n if (options.sortBy === 'name') {\n results.sort((a, b) => basename(a.path).localeCompare(basename(b.path)));\n } else {\n results.sort((a, b) => a.path.localeCompare(b.path));\n }\n\n // Same precedence as above: the result cap wins over a same-iteration abort.\n const stoppedReason = resolveStopReason(results.length >= maxResults, counters.stoppedByAbort);\n\n return {\n basePath: directory,\n results,\n summary: {\n matched: results.length,\n filesScanned,\n truncated: stoppedReason !== undefined,\n skippedInaccessible: counters.skippedInaccessible,\n ...(stoppedReason ? { stoppedReason } : {}),\n },\n };\n}\n"]}
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/core/search.ts"],"names":[],"mappings":"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","sourcesContent":["import { basename } from 'node:path';\n\nimport type { RE2ExecArray } from '@adguard/re2-wasm';\nimport { RE2 } from '@adguard/re2-wasm';\n\nimport { processEntriesConcurrently, resolveStopReason } from './concurrency.ts';\nimport { GuardedFileSystem } from './fs.ts';\nimport { globEntries } from './glob.ts';\nimport type { PathGuard } from './path.ts';\nimport { escapeRegExp, getMaxTextFileSize, MIB, PARALLEL_CONCURRENCY } from './util.ts';\n\n/**\n * A fresh, flat copy of `s`. V8 keeps a substring of a long string as a slice\n * that references its parent, so a kept line would otherwise pin the whole\n * file's text for as long as the match list lives (60 s in the page store).\n */\nconst own = (s: string): string => Buffer.from(s, 'utf8').toString('utf8');\n\ninterface SearchResult {\n file: string;\n line: number;\n /** 0-indexed column of the first occurrence on the line. */\n column: number;\n content: string;\n matchCount: number;\n /** Up to `context` lines either side of the match; absent when context is 0. */\n before?: string[];\n after?: string[];\n}\n\nexport type Regex = RE2;\n\n/** Occurrence-counting bound, so one pathological line cannot spin forever. */\nconst MAX_MATCHES_PER_LINE = 100_000;\n\n/**\n * re2-wasm copies the whole input on every exec, so draining a dense pattern is\n * O(matches x input) and fully synchronous: the loop pins and the tool's\n * AbortSignal.timeout (a timer) can never fire. The returned `tick` yields a\n * macrotask every ~10ms and reports whether `signal` has aborted.\n */\nexport function createYielder(signal: AbortSignal | undefined): () => Promise<boolean> {\n let last = Date.now();\n return async () => {\n if (Date.now() - last > 10) {\n await new Promise((resolve) => {\n setImmediate(resolve);\n });\n last = Date.now();\n }\n return signal?.aborted ?? false;\n };\n}\n\n/**\n * Largest input one RE2 call may scan as a single string. The input is copied\n * into the same fixed 16 MB wasm heap as the patterns; about 10 MB of input\n * aborts the module. 4 MiB leaves room for the copy and the pattern set.\n */\nexport const RE2_MAX_INPUT_BYTES = 4 * MIB;\n\nconst BUFFER_ANCHOR_RE = /\\\\[AzZ]/u;\n/** `(?i)`, `(?-m)`, `(?s:…)`: inline flags can undo the `m` the prefilter relies on. */\nconst INLINE_FLAGS_RE = /\\(\\?[a-zA-Z-]+[:)]/u;\nconst CR = 13;\n\n/**\n * Compile a pattern on RE2 rather than on V8's irregexp.\n *\n * Patterns arrive from the MCP client, so a backtracking engine would let one\n * request pin the event loop with no way out: an abort signal cannot preempt a\n * synchronous `exec`, and on stdio that wedges the whole server. RE2 matches in\n * time linear in the input and cannot backtrack at all, so the hazard is gone\n * rather than bounded.\n *\n * RE2 rejects the constructs the tool schema documents as unsupported —\n * lookahead, lookbehind, backreferences — with its own {@link SyntaxError},\n * which the tool layer turns into a normal tool error. It always matches in\n * Unicode mode and requires the `u` flag to say so.\n *\n * Every compiled pattern owns memory in re2-wasm's fixed 16 MB heap, which\n * `ALLOW_MEMORY_GROWTH` is off for. re2-wasm never frees it and a\n * FinalizationRegistry does not keep up (V8 sees no pressure from the wasm\n * heap), so exhaustion is an emscripten `abort()` that kills regex search for\n * the rest of the process. Every caller MUST pass the result to\n * {@link freeRegex} when it is done with it.\n */\nexport function compileRegex(\n pattern: string,\n options: { caseSensitive?: boolean; multiline?: boolean } = {},\n): Regex {\n const flags = `g${options.multiline ? 'm' : ''}${options.caseSensitive ? '' : 'i'}u`;\n try {\n return new RE2(pattern, flags);\n } catch (error) {\n if (!(error instanceof SyntaxError)) throw error;\n throw new SyntaxError(\n `${error.message} — lookahead, lookbehind, and backreferences are not supported.`,\n { cause: error },\n );\n }\n}\n\n/**\n * Release a compiled pattern's wasm memory. re2-wasm exposes no disposal of its\n * own, so this reaches the embind handle it holds privately; a version that\n * renames the field degrades to the pre-existing leak rather than throwing.\n * Idempotent — a second call on an already-freed handle is swallowed. Using a\n * {@link Regex} after freeing it is undefined behaviour in the wasm heap, so\n * free only in a `finally` that owns the compile.\n */\nexport function freeRegex(regex: Regex | undefined): void {\n const handle = (regex as unknown as { wrapper?: { delete?: () => void } } | undefined)?.wrapper;\n try {\n handle?.delete?.();\n } catch {\n // already deleted, or a re2-wasm build without embind disposal\n }\n}\n\nexport interface RegexMatch {\n /** The matched text. */\n text: string;\n /** Capture groups, 1-indexed in the pattern, 0-indexed here. */\n groups: (string | undefined)[];\n /** Named capture groups, when the pattern declares any. */\n named: Record<string, string> | undefined;\n /** Start offset in UTF-16 code units — usable with `String.prototype.slice`. */\n start: number;\n /** End offset in UTF-16 code units. */\n end: number;\n}\n\n/** Code-point length of `text`, i.e. its UTF-16 length minus its surrogate pairs. */\nconst codePointLength = (text: string): number =>\n // eslint-disable-next-line @typescript-eslint/no-misused-spread -- code points are the intended unit (re2-wasm offsets count code points); grapheme segmentation would change the count, not preserve it.\n [...text].length;\n\n/**\n * Iterate a global pattern's non-overlapping matches with offsets JS can use.\n *\n * re2-wasm reports `index` and `lastIndex` in **code points**, while every JS\n * string operation indexes in UTF-16 code units. The two agree only until the\n * first astral character (most emoji), past which every offset is short by one\n * per surrogate pair — slicing on the raw index cuts into the middle of\n * neighbouring text. Its `lastIndex` bookkeeping is worse still: it adds the\n * match's UTF-16 length to a code-point index, so an astral character inside a\n * match can skip the next one. This is the single place that corrects both; no\n * caller should read `match.index` or `regex.lastIndex` directly.\n *\n * Zero-length matches (e.g. `a*`) advance by one code point so they cannot loop\n * forever. The regex is global and shared across calls, so `lastIndex` is reset\n * on entry.\n */\nexport function* execMatches(regex: Regex, text: string): Generator<RegexMatch, undefined> {\n regex.lastIndex = 0;\n // Cursor into `text`, carried forward across matches: RE2 reports them in\n // ascending order, so the walk is O(text) in total rather than per match.\n let cursorCp = 0;\n let cursorU16 = 0;\n let match: RE2ExecArray | null;\n while ((match = regex.exec(text)) !== null) {\n while (cursorCp < match.index && cursorU16 < text.length) {\n const isPair =\n (text.charCodeAt(cursorU16) & 0xfc00) === 0xd800 &&\n (text.charCodeAt(cursorU16 + 1) & 0xfc00) === 0xdc00;\n cursorU16 += isPair ? 2 : 1;\n cursorCp++;\n }\n const matched = match[0] ?? '';\n const start = cursorU16;\n yield {\n text: matched,\n groups: match.slice(1),\n named: match.groups,\n start,\n end: start + matched.length,\n };\n // Own the advance rather than trusting the wrapper's mixed-unit arithmetic.\n regex.lastIndex = match.index + (matched.length === 0 ? 1 : codePointLength(matched));\n }\n}\n\n/**\n * Find non-overlapping occurrences of a global regex in a single line, reporting\n * the first match's column alongside the count.\n */\nasync function findLineMatches(\n regex: Regex,\n line: string,\n tick: () => Promise<boolean>,\n): Promise<{ count: number; column: number } | undefined> {\n let count = 0;\n let column = -1;\n // Awaiting mid-drain is safe with a shared regex: execMatches re-writes\n // lastIndex from its own match before every exec, with no await inside.\n for (const match of execMatches(regex, line)) {\n if (column === -1) column = match.start;\n count++;\n if (count >= MAX_MATCHES_PER_LINE || (await tick())) break;\n }\n return count > 0 ? { count, column } : undefined;\n}\n\nexport interface SearchContentOptions {\n caseSensitive?: boolean;\n isRegex?: boolean;\n maxResults?: number;\n filePattern?: string;\n skipIgnored?: boolean;\n includeHidden?: boolean;\n maxDepth?: number;\n /**\n * Search exactly this file (absolute, already guard-validated) instead of\n * walking `directory` with `filePattern`. Set when the caller named a file:\n * its name must not be interpreted as a glob.\n */\n explicitFile?: string;\n /** Lines of context to carry either side of each match; 0 (default) carries none. */\n context?: number;\n signal?: AbortSignal;\n}\n\nexport interface SearchContentOutcome {\n basePath: string;\n matches: SearchResult[];\n summary: {\n /**\n * Matching *lines*, one per entry in `matches` — not pattern occurrences.\n * A line with three occurrences counts once here and reports 3 in its own\n * `SearchResult.matchCount`.\n */\n matchingLines: number;\n filesScanned: number;\n filesMatched: number;\n truncated: boolean;\n /** Files the guard rejected or that could not be stat'd. */\n skippedInaccessible: number;\n /** Files skipped unread because they exceed maxFileSize. */\n skippedTooLarge: number;\n /** Files skipped because they contain a NUL byte (binary). */\n skippedBinary: number;\n /**\n * `StoppedReason` narrowed to the stops these scans can produce: both\n * resolve the stop through `resolveStopReason` (concurrency.ts) — never\n * `maxFiles`, which belongs to `replace_text`'s per-file cap.\n */\n stoppedReason?: 'maxResults' | 'timeout';\n };\n}\n\ntype FileScan =\n | { kind: 'inaccessible' }\n | { kind: 'tooLarge' }\n | { kind: 'binary' }\n | { kind: 'scanned'; matches: SearchResult[] };\n\ninterface ScanContext {\n fs: GuardedFileSystem;\n signal: AbortSignal | undefined;\n maxFileSize: number;\n maxResults: number;\n regex: Regex;\n prefilter: Regex;\n source: string;\n context: number;\n}\n\nasync function scanFile(entryPath: string, scanCtx: ScanContext): Promise<FileScan> {\n let buffer: Buffer;\n try {\n // open() validates, refuses a swapped final symlink (O_NOFOLLOW) and\n // re-checks the descriptor's real path (H4), so these bytes are the\n // validated file's, not whatever the name points at by now.\n await using handle = await scanCtx.fs.open(entryPath);\n const stats = await handle.stat();\n if (stats.size > scanCtx.maxFileSize) return { kind: 'tooLarge' };\n buffer = await handle.readFile({ signal: scanCtx.signal });\n } catch (error) {\n if (scanCtx.signal?.aborted) throw error;\n return { kind: 'inaccessible' };\n }\n\n // A NUL byte marks a binary file, as it does for `read` and `grep -I`.\n // Non-UTF-8 text is still searched: a lossy decode only affects the\n // bytes a pattern could not have matched anyway.\n if (buffer.includes(0)) return { kind: 'binary' };\n\n const content = buffer.toString('utf-8');\n if (canPrefilter(scanCtx.source, buffer)) {\n scanCtx.prefilter.lastIndex = 0;\n if (scanCtx.prefilter.exec(content) === null) return { kind: 'scanned', matches: [] };\n }\n\n const lines = content.split(/\\r?\\n/u);\n // A trailing newline splits into a phantom empty last element, and an\n // empty file splits into one empty element: neither is a line the\n // file has, for matching or for context.\n const lineCount =\n content.length === 0 ? 0 : content.endsWith('\\n') ? lines.length - 1 : lines.length;\n // A single line past RE2_MAX_INPUT_BYTES OOMs the wasm module for the rest of\n // the process; report the file tooLarge instead. Only a buffer over the bound\n // can hold such a line, so ordinary files skip the per-line measure.\n const checkLineSize = buffer.length > RE2_MAX_INPUT_BYTES;\n const matches: SearchResult[] = [];\n const tick = createYielder(scanCtx.signal);\n for (let i = 0; i < lineCount; i++) {\n const line = lines[i];\n if (line === undefined) continue;\n if (checkLineSize && Buffer.byteLength(line, 'utf8') > RE2_MAX_INPUT_BYTES) {\n return { kind: 'tooLarge' };\n }\n // Yield between lines too: millions of short lines never yield per-match.\n if (await tick()) break;\n // One scan per line: the helper resets lastIndex itself, so it\n // doubles as the \"does this line match\" test.\n const found = await findLineMatches(scanCtx.regex, line, tick);\n if (found) {\n matches.push({\n file: entryPath,\n line: i + 1,\n column: found.column,\n content: own(line),\n matchCount: found.count,\n ...(scanCtx.context > 0\n ? {\n before: lines.slice(Math.max(0, i - scanCtx.context), i).map(own),\n after: lines.slice(i + 1, Math.min(lineCount, i + 1 + scanCtx.context)).map(own),\n }\n : {}),\n });\n if (matches.length >= scanCtx.maxResults) break;\n }\n }\n return { kind: 'scanned', matches };\n}\n\nexport async function searchContent(\n directory: string,\n pattern: string,\n options: SearchContentOptions,\n pathGuard: PathGuard,\n): Promise<SearchContentOutcome> {\n const source = options.isRegex ? pattern || '' : escapeRegExp(pattern || '');\n // Compiles live inside the try so a failing second compile does not leak the\n // first's wasm memory; freeRegex is null-safe.\n let regex: Regex | undefined;\n let prefilter: Regex | undefined;\n try {\n regex = compileRegex(source, {\n caseSensitive: Boolean(options.caseSensitive),\n });\n // Whole-buffer test: `m` makes ^/$ line-relative so a per-line hit is always a\n // buffer hit (the converse can be false — a false positive just falls through\n // to the per-line scan). \\A/\\z/\\Z are buffer-relative even under `m`, inline\n // flag groups can undo `m`, and $ cannot see past a \\r, so those inputs skip\n // the prefilter (see canPrefilter).\n prefilter = compileRegex(source, {\n caseSensitive: Boolean(options.caseSensitive),\n multiline: true,\n });\n const matches: SearchResult[] = [];\n const maxResults = options.maxResults ?? 100;\n const maxFileSize = getMaxTextFileSize();\n const context = options.context ?? 0;\n const ceiling = pathGuard.allowedRootContaining(directory);\n\n const entries = options.explicitFile\n ? [{ path: options.explicitFile }]\n : globEntries({\n cwd: directory,\n pattern: options.filePattern ?? '**/*',\n // Same rule as replace_text, so a search with the same glob previews the\n // files a replace would touch: a slash-free glob matches at any depth.\n baseNameMatch: true,\n includeHidden: Boolean(options.includeHidden),\n skipIgnored: Boolean(options.skipIgnored),\n ...(ceiling !== undefined ? { ignoreCeiling: ceiling } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.maxDepth !== undefined ? { maxDepth: options.maxDepth } : {}),\n suppressErrors: true,\n });\n\n let filesScanned = 0;\n let filesMatched = 0;\n let matchingLines = 0;\n let skippedTooLarge = 0;\n let skippedBinary = 0;\n const scanCtx: ScanContext = {\n fs: new GuardedFileSystem(pathGuard),\n signal: options.signal,\n maxFileSize,\n maxResults,\n regex,\n prefilter,\n source,\n context,\n };\n const perFile = new Map<number, FileScan>();\n let dispatched = 0;\n let collected = 0;\n const stopped = await processEntriesConcurrently(entries, {\n signal: options.signal,\n concurrency: PARALLEL_CONCURRENCY,\n shouldStop: () => collected >= maxResults,\n onEntry: () => {\n dispatched++;\n },\n onError: () => undefined,\n runEntry: async (entryPath) => {\n const index = dispatched - 1;\n const scan = await scanFile(entryPath, scanCtx);\n perFile.set(index, scan);\n if (scan.kind === 'scanned') collected += scan.matches.length;\n },\n });\n\n let skippedInaccessible = 0;\n for (let i = 0; i < dispatched; i++) {\n const scan = perFile.get(i);\n if (!scan) continue;\n switch (scan.kind) {\n case 'inaccessible':\n skippedInaccessible++;\n break;\n case 'tooLarge':\n skippedTooLarge++;\n break;\n case 'binary':\n filesScanned++;\n skippedBinary++;\n break;\n case 'scanned':\n filesScanned++;\n if (scan.matches.length > 0) filesMatched++;\n for (const match of scan.matches) {\n if (matches.length >= maxResults) break;\n matches.push(match);\n matchingLines++;\n }\n break;\n }\n }\n\n const stoppedReason = resolveStopReason(\n matches.length >= maxResults,\n stopped === 'timeout' || Boolean(options.signal?.aborted),\n );\n\n return {\n basePath: directory,\n matches,\n summary: {\n matchingLines,\n filesScanned,\n filesMatched,\n truncated: stoppedReason !== undefined,\n skippedInaccessible,\n skippedTooLarge,\n skippedBinary,\n ...(stoppedReason ? { stoppedReason } : {}),\n },\n };\n } finally {\n freeRegex(prefilter);\n freeRegex(regex);\n }\n}\n\n/** Whether one whole-buffer RE2 test is a sound \"cannot match\" check for this file. */\nfunction canPrefilter(source: string, buffer: Buffer): boolean {\n if (buffer.length > RE2_MAX_INPUT_BYTES) return false;\n if (BUFFER_ANCHOR_RE.test(source)) return false;\n if (INLINE_FLAGS_RE.test(source)) return false;\n if (source.includes('$') && buffer.includes(CR)) return false;\n return true;\n}\n\nexport async function searchFiles(\n directory: string,\n pattern: string,\n options: {\n maxResults?: number;\n includeHidden?: boolean;\n sortBy?: 'name' | 'path';\n skipIgnored?: boolean;\n maxDepth?: number;\n signal?: AbortSignal;\n },\n pathGuard: PathGuard,\n): Promise<{\n basePath: string;\n results: { path: string }[];\n summary: {\n matched: number;\n filesScanned: number;\n truncated: boolean;\n skippedInaccessible: number;\n /** Narrowed like SearchContentOutcome's — scans never `maxFiles`. */\n stoppedReason?: 'maxResults' | 'timeout';\n };\n}> {\n const maxResults = options.maxResults ?? 100;\n const ceiling = pathGuard.allowedRootContaining(directory);\n const entries = globEntries({\n cwd: directory,\n pattern,\n baseNameMatch: true,\n includeHidden: Boolean(options.includeHidden),\n skipIgnored: Boolean(options.skipIgnored),\n ...(ceiling !== undefined ? { ignoreCeiling: ceiling } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.maxDepth !== undefined ? { maxDepth: options.maxDepth } : {}),\n suppressErrors: true,\n });\n const results: { path: string }[] = [];\n let filesScanned = 0;\n let skippedInaccessible = 0;\n let stoppedByAbort = false;\n\n // The signal carries both client cancellation and the tool's search timeout,\n // so an abort means \"return what we have, marked incomplete\" rather than\n // throw — but it must never be reported as a finished scan. The walk itself\n // throws when the signal stops it before a single entry is yielded.\n try {\n for await (const entry of entries) {\n if (options.signal?.aborted) {\n stoppedByAbort = true;\n break;\n }\n try {\n await pathGuard.validateExistingPath(entry.path);\n } catch {\n skippedInaccessible++;\n continue;\n }\n if (results.length >= maxResults) break;\n filesScanned++;\n results.push({ path: entry.path });\n }\n } catch (error) {\n if (!options.signal?.aborted) throw error;\n stoppedByAbort = true;\n }\n\n // Sorting — only name / path are supported; size / modified were removed\n // (the glob never collected stats, so they were always undefined).\n if (options.sortBy === 'name') {\n results.sort((a, b) => basename(a.path).localeCompare(basename(b.path)));\n } else {\n results.sort((a, b) => a.path.localeCompare(b.path));\n }\n\n // Same precedence as above: the result cap wins over a same-iteration abort.\n const stoppedReason = resolveStopReason(results.length >= maxResults, stoppedByAbort);\n\n return {\n basePath: directory,\n results,\n summary: {\n matched: results.length,\n filesScanned,\n truncated: stoppedReason !== undefined,\n skippedInaccessible,\n ...(stoppedReason ? { stoppedReason } : {}),\n },\n };\n}\n"]}
@@ -9,9 +9,37 @@ interface ResourceEntry {
9
9
  expiresAt: string;
10
10
  text: string;
11
11
  }
12
+ /** The `resources/read` result that serves `entry`, and what the store measures it by. */
13
+ export declare function entryReadResult(entry: ResourceEntry): {
14
+ contents: [{
15
+ uri: string;
16
+ mimeType: string;
17
+ text: string;
18
+ }];
19
+ };
20
+ interface ResourceStoreOptions {
21
+ /**
22
+ * Refuse an entry whose `resources/read` result encodes past this many
23
+ * bytes. stdio sets it: the entry is re-encoded there as one JSON string,
24
+ * where escaping can outgrow the bytes stored, and a link the connection
25
+ * can never carry back is worth no more than an entry over the cap.
26
+ */
27
+ readonly maxReadResultBytes?: number;
28
+ }
12
29
  export declare class ResourceStore {
13
30
  #private;
14
- constructor(onListChanged?: () => void);
31
+ constructor(onListChanged?: () => void, options?: ResourceStoreOptions);
32
+ /**
33
+ * A view sharing this store's entries that also remembers the URIs put
34
+ * through it, so {@link discard} can take one tool call's entries back.
35
+ */
36
+ forCall(): ResourceStore;
37
+ /**
38
+ * Drop what was put through this view and is still cached. The put already
39
+ * announced the list change (and may have pruned or evicted other entries),
40
+ * so taking the entry back is announced too.
41
+ */
42
+ discard(): void;
15
43
  getEntry(uri: string): ResourceEntry;
16
44
  putText(params: {
17
45
  name: string;
@@ -64,6 +92,13 @@ export declare function invalidCursor(): FsError;
64
92
  */
65
93
  export declare class PageSnapshotStore {
66
94
  #private;
95
+ /**
96
+ * A view sharing this store's snapshots that also remembers the ones
97
+ * created through it, so {@link discard} can take one tool call's back.
98
+ */
99
+ forCall(): PageSnapshotStore;
100
+ /** Drop the snapshots created through this view. */
101
+ discard(): void;
67
102
  create(params: {
68
103
  queryKey: string;
69
104
  items: readonly unknown[];
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/core/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAIjE,OAAO,EAAa,OAAO,EAAE,MAAM,aAAa,CAAC;AAmFjD,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAOD,qBAAa,aAAa;;gBAQZ,aAAa,CAAC,EAAE,MAAM,IAAI;IAItC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa;IAYpC,OAAO,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,aAAa;IAqBjF;;;;OAIG;IACH,IAAI,IAAI,MAAM,EAAE;CAGjB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,IAAI,EAAE,YAAY,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,MAAM,EAIZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,OAAO,EAAE,GAClD,kBAAkB,CAwBpB;AAID,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO;IACpD,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAKvC;AAID;;;;;;;;;;GAUG;AACH,qBAAa,iBAAiB;;IAM5B,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM;IAU3F,IAAI,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;IAMhF,KAAK,IAAI,IAAI;CAGd"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/core/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAIjE,OAAO,EAAa,OAAO,EAAE,MAAM,aAAa,CAAC;AAqFjD,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAOD,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG;IACrD,QAAQ,EAAE,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC7D,CAEA;AAED,UAAU,oBAAoB;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC;AAED,qBAAa,aAAa;;gBAYZ,aAAa,CAAC,EAAE,MAAM,IAAI,EAAE,OAAO,GAAE,oBAAyB;IAK1E;;;OAGG;IACH,OAAO,IAAI,aAAa;IAOxB;;;;OAIG;IACH,OAAO,IAAI,IAAI;IAQf,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa;IAYpC,OAAO,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,aAAa;IAkCjF;;;;OAIG;IACH,IAAI,IAAI,MAAM,EAAE;CAGjB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,IAAI,EAAE,YAAY,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,MAAM,EAIZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,OAAO,EAAE,GAClD,kBAAkB,CAwBpB;AAID,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO;IACpD,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAKvC;AAID;;;;;;;;;;GAUG;AACH,qBAAa,iBAAiB;;IAS5B;;;OAGG;IACH,OAAO,IAAI,iBAAiB;IAO5B,oDAAoD;IACpD,OAAO,IAAI,IAAI;IAIf,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM;IAW3F,IAAI,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;IAMhF,KAAK,IAAI,IAAI;CAGd"}
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { ErrorCode, FsError } from "./errors.js";
3
- import { MIB } from "./util.js";
3
+ import { jsonBytesOver, MIB } from "./util.js";
4
4
  // ─── TTL + LRU map ───────────────────────────────────────────────────────────
5
5
  /**
6
6
  * A bounded, expiring map. `Map` keeps insertion order and a read re-inserts,
@@ -55,12 +55,14 @@ class TtlLru {
55
55
  this.delete(oldest.value);
56
56
  }
57
57
  }
58
+ /** True when `key` was present. */
58
59
  delete(key) {
59
60
  const entry = this.#byKey.get(key);
60
61
  if (!entry)
61
- return;
62
+ return false;
62
63
  this.#weight -= entry.weight;
63
64
  this.#byKey.delete(key);
65
+ return true;
64
66
  }
65
67
  /** Live keys, least recently used first. Prunes expired entries first. */
66
68
  keys() {
@@ -76,15 +78,48 @@ const MAX_ENTRIES = 64;
76
78
  const MAX_TOTAL_BYTES = 25 * MIB;
77
79
  const MAX_ENTRY_BYTES = 10 * MIB;
78
80
  const ENTRY_TTL_MS = 60 * 1000;
81
+ /** The `resources/read` result that serves `entry`, and what the store measures it by. */
82
+ export function entryReadResult(entry) {
83
+ return { contents: [{ uri: entry.uri, mimeType: entry.mimeType, text: entry.text }] };
84
+ }
79
85
  export class ResourceStore {
86
+ // Not readonly: a call view (`forCall`) shares its store's entries.
80
87
  #entries = new TtlLru({
81
88
  maxEntries: MAX_ENTRIES,
82
89
  maxWeight: MAX_TOTAL_BYTES,
83
90
  ttlMs: ENTRY_TTL_MS,
84
91
  });
85
92
  #onListChanged;
86
- constructor(onListChanged) {
93
+ #options;
94
+ /** A call view's own puts; `undefined` on the store itself. */
95
+ #putUris;
96
+ constructor(onListChanged, options = {}) {
87
97
  this.#onListChanged = onListChanged;
98
+ this.#options = options;
99
+ }
100
+ /**
101
+ * A view sharing this store's entries that also remembers the URIs put
102
+ * through it, so {@link discard} can take one tool call's entries back.
103
+ */
104
+ forCall() {
105
+ const view = new ResourceStore(this.#onListChanged, this.#options);
106
+ view.#entries = this.#entries;
107
+ view.#putUris = [];
108
+ return view;
109
+ }
110
+ /**
111
+ * Drop what was put through this view and is still cached. The put already
112
+ * announced the list change (and may have pruned or evicted other entries),
113
+ * so taking the entry back is announced too.
114
+ */
115
+ discard() {
116
+ let dropped = false;
117
+ for (const uri of this.#putUris?.splice(0) ?? []) {
118
+ if (this.#entries.delete(uri))
119
+ dropped = true;
120
+ }
121
+ if (dropped)
122
+ this.#onListChanged?.();
88
123
  }
89
124
  getEntry(uri) {
90
125
  const entry = this.#entries.get(uri);
@@ -99,9 +134,7 @@ export class ResourceStore {
99
134
  putText(params) {
100
135
  const entryBytes = Buffer.byteLength(params.text, 'utf8');
101
136
  if (entryBytes > MAX_ENTRY_BYTES) {
102
- if (this.#entries.prune())
103
- this.#onListChanged?.();
104
- throw new FsError(ErrorCode.TOO_LARGE, `Resource too large to cache (${entryBytes} bytes).`);
137
+ throw this.#refuse(`Resource too large to cache (${entryBytes} bytes).`);
105
138
  }
106
139
  const now = Date.now();
107
140
  const entry = {
@@ -113,10 +146,23 @@ export class ResourceStore {
113
146
  expiresAt: new Date(now + ENTRY_TTL_MS).toISOString(),
114
147
  text: params.text,
115
148
  };
149
+ const { maxReadResultBytes } = this.#options;
150
+ const readBytes = maxReadResultBytes === undefined
151
+ ? undefined
152
+ : jsonBytesOver(entryReadResult(entry), maxReadResultBytes);
153
+ if (readBytes !== undefined) {
154
+ throw this.#refuse(`Resource too large to read back (${readBytes} bytes once JSON-encoded).`);
155
+ }
116
156
  this.#entries.set(entry.uri, entry, entryBytes);
157
+ this.#putUris?.push(entry.uri);
117
158
  this.#onListChanged?.();
118
159
  return entry;
119
160
  }
161
+ #refuse(message) {
162
+ if (this.#entries.prune())
163
+ this.#onListChanged?.();
164
+ return new FsError(ErrorCode.TOO_LARGE, message);
165
+ }
120
166
  /**
121
167
  * Live URIs, oldest first. Deliberately silent about the prune it runs:
122
168
  * this is the read path `resources/list` runs through, and the caller is
@@ -181,10 +227,28 @@ const MAX_SNAPSHOTS = 32;
181
227
  * the way `ResourceStore` does.
182
228
  */
183
229
  export class PageSnapshotStore {
230
+ // Not readonly: a call view (`forCall`) shares its store's snapshots.
184
231
  #byId = new TtlLru({
185
232
  maxEntries: MAX_SNAPSHOTS,
186
233
  ttlMs: ENTRY_TTL_MS,
187
234
  });
235
+ /** A call view's own snapshots; `undefined` on the store itself. */
236
+ #createdIds;
237
+ /**
238
+ * A view sharing this store's snapshots that also remembers the ones
239
+ * created through it, so {@link discard} can take one tool call's back.
240
+ */
241
+ forCall() {
242
+ const view = new PageSnapshotStore();
243
+ view.#byId = this.#byId;
244
+ view.#createdIds = [];
245
+ return view;
246
+ }
247
+ /** Drop the snapshots created through this view. */
248
+ discard() {
249
+ for (const id of this.#createdIds?.splice(0) ?? [])
250
+ this.#byId.delete(id);
251
+ }
188
252
  create(params) {
189
253
  const snapshotId = randomUUID();
190
254
  this.#byId.set(snapshotId, {
@@ -192,6 +256,7 @@ export class PageSnapshotStore {
192
256
  items: params.items,
193
257
  metadata: params.metadata,
194
258
  });
259
+ this.#createdIds?.push(snapshotId);
195
260
  return snapshotId;
196
261
  }
197
262
  read(snapshotId, queryKey) {
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/core/store.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ContentBlock } from '@modelcontextprotocol/server';\n\nimport { randomUUID } from 'node:crypto';\n\nimport { ErrorCode, FsError } from './errors.ts';\nimport { MIB } from './util.ts';\n\n// ─── TTL + LRU map ───────────────────────────────────────────────────────────\n\n/**\n * A bounded, expiring map. `Map` keeps insertion order and a read re-inserts,\n * so the first key is always the least recently used one. Entries carry an\n * optional weight so a byte budget can be enforced next to the entry cap.\n */\nclass TtlLru<V> {\n readonly #byKey = new Map<string, { value: V; expiresAt: number; weight: number }>();\n readonly #maxEntries: number;\n readonly #maxWeight: number;\n readonly #ttlMs: number;\n #weight = 0;\n\n constructor(opts: { maxEntries: number; ttlMs: number; maxWeight?: number }) {\n this.#maxEntries = opts.maxEntries;\n this.#maxWeight = opts.maxWeight ?? Number.POSITIVE_INFINITY;\n this.#ttlMs = opts.ttlMs;\n }\n\n /** Drop every expired entry; true when at least one went. */\n prune(): boolean {\n const now = Date.now();\n const before = this.#byKey.size;\n for (const [key, entry] of this.#byKey) {\n if (entry.expiresAt <= now) this.delete(key);\n }\n return this.#byKey.size !== before;\n }\n\n /** The live value, bumped to most recently used; `undefined` when missing or expired. */\n get(key: string): V | undefined {\n const entry = this.#byKey.get(key);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n this.delete(key);\n return undefined;\n }\n this.#byKey.delete(key);\n this.#byKey.set(key, entry);\n return entry.value;\n }\n\n /** Insert (pruning expired first) and evict least recently used entries past either cap. */\n set(key: string, value: V, weight = 0): void {\n this.prune();\n this.delete(key);\n this.#byKey.set(key, { value, expiresAt: Date.now() + this.#ttlMs, weight });\n this.#weight += weight;\n while (\n this.#byKey.size > 0 &&\n (this.#byKey.size > this.#maxEntries || this.#weight > this.#maxWeight)\n ) {\n const oldest = this.#byKey.keys().next();\n if (oldest.done) break;\n this.delete(oldest.value);\n }\n }\n\n delete(key: string): void {\n const entry = this.#byKey.get(key);\n if (!entry) return;\n this.#weight -= entry.weight;\n this.#byKey.delete(key);\n }\n\n /** Live keys, least recently used first. Prunes expired entries first. */\n keys(): string[] {\n this.prune();\n return [...this.#byKey.keys()];\n }\n\n clear(): void {\n this.#byKey.clear();\n this.#weight = 0;\n }\n}\n\n// ─── Resource store: externalized tool results ───────────────────────────────\n\ninterface ResourceEntry {\n uri: string;\n name: string;\n mimeType: string;\n size: number;\n createdAt: string;\n expiresAt: string;\n text: string;\n}\n\nconst MAX_ENTRIES = 64;\nconst MAX_TOTAL_BYTES = 25 * MIB;\nconst MAX_ENTRY_BYTES = 10 * MIB;\nconst ENTRY_TTL_MS = 60 * 1000;\n\nexport class ResourceStore {\n readonly #entries = new TtlLru<ResourceEntry>({\n maxEntries: MAX_ENTRIES,\n maxWeight: MAX_TOTAL_BYTES,\n ttlMs: ENTRY_TTL_MS,\n });\n readonly #onListChanged: (() => void) | undefined;\n\n constructor(onListChanged?: () => void) {\n this.#onListChanged = onListChanged;\n }\n\n getEntry(uri: string): ResourceEntry {\n const entry = this.#entries.get(uri);\n if (entry) return entry;\n // Not found and expired share a remedy; only the wording differs, and an\n // expiry seen here changed the list a client may hold.\n if (this.#entries.prune()) this.#onListChanged?.();\n throw new FsError(\n ErrorCode.NOT_FOUND,\n `Resource not found: ${uri}. Re-run the tool to regenerate.`,\n );\n }\n\n putText(params: { name: string; mimeType?: string; text: string }): ResourceEntry {\n const entryBytes = Buffer.byteLength(params.text, 'utf8');\n if (entryBytes > MAX_ENTRY_BYTES) {\n if (this.#entries.prune()) this.#onListChanged?.();\n throw new FsError(ErrorCode.TOO_LARGE, `Resource too large to cache (${entryBytes} bytes).`);\n }\n const now = Date.now();\n const entry: ResourceEntry = {\n uri: `filesystem-mcp://result/${randomUUID()}`,\n name: params.name,\n mimeType: params.mimeType ?? 'text/plain',\n size: entryBytes,\n createdAt: new Date(now).toISOString(),\n expiresAt: new Date(now + ENTRY_TTL_MS).toISOString(),\n text: params.text,\n };\n this.#entries.set(entry.uri, entry, entryBytes);\n this.#onListChanged?.();\n return entry;\n }\n\n /**\n * Live URIs, oldest first. Deliberately silent about the prune it runs:\n * this is the read path `resources/list` runs through, and the caller is\n * already receiving the post-prune list.\n */\n keys(): string[] {\n return this.#entries.keys();\n }\n}\n\nexport interface JsonResourceResult {\n entry: {\n uri: string;\n size: number;\n mimeType: string;\n expiresAt: string;\n };\n link: ContentBlock;\n}\n\n/**\n * Owner of the externalize-a-payload rule: a tool whose inline response is\n * truncated publishes the full value to the resource store as pretty-printed\n * JSON and hands back the URI to reach it by, plus the link block that offers\n * it to the user.\n */\nexport function putJsonResource(\n store: ResourceStore,\n name: string,\n // `Record<string, unknown> | readonly unknown[]`, not `unknown` or `object`:\n // JSON.stringify returns undefined for undefined and for functions, and `null`\n // is excluded so non-string payloads cannot enter the store's text entry.\n value: Record<string, unknown> | readonly unknown[],\n): JsonResourceResult {\n const entry = store.putText({\n name,\n mimeType: 'application/json',\n text: JSON.stringify(value, null, 2),\n });\n\n return {\n entry: {\n uri: entry.uri,\n size: entry.size,\n mimeType: entry.mimeType,\n expiresAt: entry.expiresAt,\n },\n link: {\n type: 'resource_link',\n uri: entry.uri,\n name: entry.name,\n description: `Cached tool result; expires ${entry.expiresAt}`,\n mimeType: entry.mimeType,\n size: entry.size,\n annotations: { audience: ['user'], lastModified: entry.createdAt },\n },\n };\n}\n\n// ─── Page snapshots: paginated result sets ───────────────────────────────────\n\nexport interface PageSnapshot<T = unknown, M = unknown> {\n readonly items: readonly T[];\n readonly metadata: M;\n}\n\n/**\n * The one cursor rejection: a cursor whose snapshot expired, was evicted, was\n * already consumed past its end, or belongs to a different query. Every case\n * has the same remedy, so they share one message.\n */\nexport function invalidCursor(): FsError {\n return new FsError(\n ErrorCode.INVALID_INPUT,\n 'Invalid cursor. Request the first page without a cursor.',\n );\n}\n\nconst MAX_SNAPSHOTS = 32;\n\n/**\n * Short-lived snapshots of a completed query's full result set, so later pages\n * slice a stored array instead of re-scanning and re-sorting the filesystem.\n *\n * ponytail: bounded by snapshot count and TTL, not by bytes — MAX_SNAPSHOTS x\n * 20,000 `list` entries or MAX_SNAPSHOTS x 10,000 `search_text` matches, each\n * match retaining up to 20 more line strings at `context: 10`, order of a few\n * hundred MB held for 60s, and the HTTP leg shares one store so any caller can\n * drive it. If that shows up as memory pressure, pass a byte weight to `set`\n * the way `ResourceStore` does.\n */\nexport class PageSnapshotStore {\n readonly #byId = new TtlLru<PageSnapshot & { queryKey: string }>({\n maxEntries: MAX_SNAPSHOTS,\n ttlMs: ENTRY_TTL_MS,\n });\n\n create(params: { queryKey: string; items: readonly unknown[]; metadata?: unknown }): string {\n const snapshotId = randomUUID();\n this.#byId.set(snapshotId, {\n queryKey: params.queryKey,\n items: params.items,\n metadata: params.metadata,\n });\n return snapshotId;\n }\n\n read<T, M = undefined>(snapshotId: string, queryKey: string): PageSnapshot<T, M> {\n const entry = this.#byId.get(snapshotId);\n if (entry?.queryKey !== queryKey) throw invalidCursor();\n return { items: entry.items as readonly T[], metadata: entry.metadata as M };\n }\n\n clear(): void {\n this.#byId.clear();\n }\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/core/store.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ContentBlock } from '@modelcontextprotocol/server';\n\nimport { randomUUID } from 'node:crypto';\n\nimport { ErrorCode, FsError } from './errors.ts';\nimport { jsonBytesOver, MIB } from './util.ts';\n\n// ─── TTL + LRU map ───────────────────────────────────────────────────────────\n\n/**\n * A bounded, expiring map. `Map` keeps insertion order and a read re-inserts,\n * so the first key is always the least recently used one. Entries carry an\n * optional weight so a byte budget can be enforced next to the entry cap.\n */\nclass TtlLru<V> {\n readonly #byKey = new Map<string, { value: V; expiresAt: number; weight: number }>();\n readonly #maxEntries: number;\n readonly #maxWeight: number;\n readonly #ttlMs: number;\n #weight = 0;\n\n constructor(opts: { maxEntries: number; ttlMs: number; maxWeight?: number }) {\n this.#maxEntries = opts.maxEntries;\n this.#maxWeight = opts.maxWeight ?? Number.POSITIVE_INFINITY;\n this.#ttlMs = opts.ttlMs;\n }\n\n /** Drop every expired entry; true when at least one went. */\n prune(): boolean {\n const now = Date.now();\n const before = this.#byKey.size;\n for (const [key, entry] of this.#byKey) {\n if (entry.expiresAt <= now) this.delete(key);\n }\n return this.#byKey.size !== before;\n }\n\n /** The live value, bumped to most recently used; `undefined` when missing or expired. */\n get(key: string): V | undefined {\n const entry = this.#byKey.get(key);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n this.delete(key);\n return undefined;\n }\n this.#byKey.delete(key);\n this.#byKey.set(key, entry);\n return entry.value;\n }\n\n /** Insert (pruning expired first) and evict least recently used entries past either cap. */\n set(key: string, value: V, weight = 0): void {\n this.prune();\n this.delete(key);\n this.#byKey.set(key, { value, expiresAt: Date.now() + this.#ttlMs, weight });\n this.#weight += weight;\n while (\n this.#byKey.size > 0 &&\n (this.#byKey.size > this.#maxEntries || this.#weight > this.#maxWeight)\n ) {\n const oldest = this.#byKey.keys().next();\n if (oldest.done) break;\n this.delete(oldest.value);\n }\n }\n\n /** True when `key` was present. */\n delete(key: string): boolean {\n const entry = this.#byKey.get(key);\n if (!entry) return false;\n this.#weight -= entry.weight;\n this.#byKey.delete(key);\n return true;\n }\n\n /** Live keys, least recently used first. Prunes expired entries first. */\n keys(): string[] {\n this.prune();\n return [...this.#byKey.keys()];\n }\n\n clear(): void {\n this.#byKey.clear();\n this.#weight = 0;\n }\n}\n\n// ─── Resource store: externalized tool results ───────────────────────────────\n\ninterface ResourceEntry {\n uri: string;\n name: string;\n mimeType: string;\n size: number;\n createdAt: string;\n expiresAt: string;\n text: string;\n}\n\nconst MAX_ENTRIES = 64;\nconst MAX_TOTAL_BYTES = 25 * MIB;\nconst MAX_ENTRY_BYTES = 10 * MIB;\nconst ENTRY_TTL_MS = 60 * 1000;\n\n/** The `resources/read` result that serves `entry`, and what the store measures it by. */\nexport function entryReadResult(entry: ResourceEntry): {\n contents: [{ uri: string; mimeType: string; text: string }];\n} {\n return { contents: [{ uri: entry.uri, mimeType: entry.mimeType, text: entry.text }] };\n}\n\ninterface ResourceStoreOptions {\n /**\n * Refuse an entry whose `resources/read` result encodes past this many\n * bytes. stdio sets it: the entry is re-encoded there as one JSON string,\n * where escaping can outgrow the bytes stored, and a link the connection\n * can never carry back is worth no more than an entry over the cap.\n */\n readonly maxReadResultBytes?: number;\n}\n\nexport class ResourceStore {\n // Not readonly: a call view (`forCall`) shares its store's entries.\n #entries = new TtlLru<ResourceEntry>({\n maxEntries: MAX_ENTRIES,\n maxWeight: MAX_TOTAL_BYTES,\n ttlMs: ENTRY_TTL_MS,\n });\n readonly #onListChanged: (() => void) | undefined;\n readonly #options: ResourceStoreOptions;\n /** A call view's own puts; `undefined` on the store itself. */\n #putUris: string[] | undefined;\n\n constructor(onListChanged?: () => void, options: ResourceStoreOptions = {}) {\n this.#onListChanged = onListChanged;\n this.#options = options;\n }\n\n /**\n * A view sharing this store's entries that also remembers the URIs put\n * through it, so {@link discard} can take one tool call's entries back.\n */\n forCall(): ResourceStore {\n const view = new ResourceStore(this.#onListChanged, this.#options);\n view.#entries = this.#entries;\n view.#putUris = [];\n return view;\n }\n\n /**\n * Drop what was put through this view and is still cached. The put already\n * announced the list change (and may have pruned or evicted other entries),\n * so taking the entry back is announced too.\n */\n discard(): void {\n let dropped = false;\n for (const uri of this.#putUris?.splice(0) ?? []) {\n if (this.#entries.delete(uri)) dropped = true;\n }\n if (dropped) this.#onListChanged?.();\n }\n\n getEntry(uri: string): ResourceEntry {\n const entry = this.#entries.get(uri);\n if (entry) return entry;\n // Not found and expired share a remedy; only the wording differs, and an\n // expiry seen here changed the list a client may hold.\n if (this.#entries.prune()) this.#onListChanged?.();\n throw new FsError(\n ErrorCode.NOT_FOUND,\n `Resource not found: ${uri}. Re-run the tool to regenerate.`,\n );\n }\n\n putText(params: { name: string; mimeType?: string; text: string }): ResourceEntry {\n const entryBytes = Buffer.byteLength(params.text, 'utf8');\n if (entryBytes > MAX_ENTRY_BYTES) {\n throw this.#refuse(`Resource too large to cache (${entryBytes} bytes).`);\n }\n const now = Date.now();\n const entry: ResourceEntry = {\n uri: `filesystem-mcp://result/${randomUUID()}`,\n name: params.name,\n mimeType: params.mimeType ?? 'text/plain',\n size: entryBytes,\n createdAt: new Date(now).toISOString(),\n expiresAt: new Date(now + ENTRY_TTL_MS).toISOString(),\n text: params.text,\n };\n const { maxReadResultBytes } = this.#options;\n const readBytes =\n maxReadResultBytes === undefined\n ? undefined\n : jsonBytesOver(entryReadResult(entry), maxReadResultBytes);\n if (readBytes !== undefined) {\n throw this.#refuse(`Resource too large to read back (${readBytes} bytes once JSON-encoded).`);\n }\n this.#entries.set(entry.uri, entry, entryBytes);\n this.#putUris?.push(entry.uri);\n this.#onListChanged?.();\n return entry;\n }\n\n #refuse(message: string): FsError {\n if (this.#entries.prune()) this.#onListChanged?.();\n return new FsError(ErrorCode.TOO_LARGE, message);\n }\n\n /**\n * Live URIs, oldest first. Deliberately silent about the prune it runs:\n * this is the read path `resources/list` runs through, and the caller is\n * already receiving the post-prune list.\n */\n keys(): string[] {\n return this.#entries.keys();\n }\n}\n\nexport interface JsonResourceResult {\n entry: {\n uri: string;\n size: number;\n mimeType: string;\n expiresAt: string;\n };\n link: ContentBlock;\n}\n\n/**\n * Owner of the externalize-a-payload rule: a tool whose inline response is\n * truncated publishes the full value to the resource store as pretty-printed\n * JSON and hands back the URI to reach it by, plus the link block that offers\n * it to the user.\n */\nexport function putJsonResource(\n store: ResourceStore,\n name: string,\n // `Record<string, unknown> | readonly unknown[]`, not `unknown` or `object`:\n // JSON.stringify returns undefined for undefined and for functions, and `null`\n // is excluded so non-string payloads cannot enter the store's text entry.\n value: Record<string, unknown> | readonly unknown[],\n): JsonResourceResult {\n const entry = store.putText({\n name,\n mimeType: 'application/json',\n text: JSON.stringify(value, null, 2),\n });\n\n return {\n entry: {\n uri: entry.uri,\n size: entry.size,\n mimeType: entry.mimeType,\n expiresAt: entry.expiresAt,\n },\n link: {\n type: 'resource_link',\n uri: entry.uri,\n name: entry.name,\n description: `Cached tool result; expires ${entry.expiresAt}`,\n mimeType: entry.mimeType,\n size: entry.size,\n annotations: { audience: ['user'], lastModified: entry.createdAt },\n },\n };\n}\n\n// ─── Page snapshots: paginated result sets ───────────────────────────────────\n\nexport interface PageSnapshot<T = unknown, M = unknown> {\n readonly items: readonly T[];\n readonly metadata: M;\n}\n\n/**\n * The one cursor rejection: a cursor whose snapshot expired, was evicted, was\n * already consumed past its end, or belongs to a different query. Every case\n * has the same remedy, so they share one message.\n */\nexport function invalidCursor(): FsError {\n return new FsError(\n ErrorCode.INVALID_INPUT,\n 'Invalid cursor. Request the first page without a cursor.',\n );\n}\n\nconst MAX_SNAPSHOTS = 32;\n\n/**\n * Short-lived snapshots of a completed query's full result set, so later pages\n * slice a stored array instead of re-scanning and re-sorting the filesystem.\n *\n * ponytail: bounded by snapshot count and TTL, not by bytes — MAX_SNAPSHOTS x\n * 20,000 `list` entries or MAX_SNAPSHOTS x 10,000 `search_text` matches, each\n * match retaining up to 20 more line strings at `context: 10`, order of a few\n * hundred MB held for 60s, and the HTTP leg shares one store so any caller can\n * drive it. If that shows up as memory pressure, pass a byte weight to `set`\n * the way `ResourceStore` does.\n */\nexport class PageSnapshotStore {\n // Not readonly: a call view (`forCall`) shares its store's snapshots.\n #byId = new TtlLru<PageSnapshot & { queryKey: string }>({\n maxEntries: MAX_SNAPSHOTS,\n ttlMs: ENTRY_TTL_MS,\n });\n /** A call view's own snapshots; `undefined` on the store itself. */\n #createdIds: string[] | undefined;\n\n /**\n * A view sharing this store's snapshots that also remembers the ones\n * created through it, so {@link discard} can take one tool call's back.\n */\n forCall(): PageSnapshotStore {\n const view = new PageSnapshotStore();\n view.#byId = this.#byId;\n view.#createdIds = [];\n return view;\n }\n\n /** Drop the snapshots created through this view. */\n discard(): void {\n for (const id of this.#createdIds?.splice(0) ?? []) this.#byId.delete(id);\n }\n\n create(params: { queryKey: string; items: readonly unknown[]; metadata?: unknown }): string {\n const snapshotId = randomUUID();\n this.#byId.set(snapshotId, {\n queryKey: params.queryKey,\n items: params.items,\n metadata: params.metadata,\n });\n this.#createdIds?.push(snapshotId);\n return snapshotId;\n }\n\n read<T, M = undefined>(snapshotId: string, queryKey: string): PageSnapshot<T, M> {\n const entry = this.#byId.get(snapshotId);\n if (entry?.queryKey !== queryKey) throw invalidCursor();\n return { items: entry.items as readonly T[], metadata: entry.metadata as M };\n }\n\n clear(): void {\n this.#byId.clear();\n }\n}\n"]}
@@ -37,13 +37,42 @@ export declare function getMaxInboundMessageBytes(): number;
37
37
  /** Combined byte budget for one batched `read`. */
38
38
  export declare const READ_MANY_MAX_TOTAL_BYTES: number;
39
39
  /**
40
- * Ceiling on the JSON-encoded size of one `read` result. The MCP SDK's stdio
41
- * transport caps a message at a fixed 10 MiB ReadBuffer regardless of
42
- * `--max-file-size`, so a file under the size limit can still overflow it once
43
- * escaped; `read` returns TOO_LARGE instead. ~256 KiB covers the JSON-RPC
44
- * envelope this measure omits.
40
+ * Ceiling on JSON-encoded tool and resource results over stdio. The SDK's
41
+ * fixed 10 MiB ReadBuffer applies regardless of `--max-file-size`; escaping,
42
+ * duplicated metadata, and base64 can overflow it even when the source fits.
43
+ * A tool call that changed nothing (read-only, or a dry run) and a resource
44
+ * read return TOO_LARGE instead, and the stdio result store refuses an entry
45
+ * whose read would overflow it; HTTP is unaffected. ~256 KiB covers the
46
+ * JSON-RPC envelope this measure omits, including the bounded request ID.
47
+ * Mutating tools need pre-commit safeguards rather than a post-commit output
48
+ * refusal.
45
49
  */
46
50
  export declare const MAX_OUTBOUND_RESULT_BYTES: number;
51
+ /**
52
+ * JSON-encoded stdio request IDs and string progress tokens, including
53
+ * quotes/escapes, must fit the envelope reserve: each is echoed into frames.
54
+ */
55
+ export declare const MAX_STDIO_REQUEST_ID_BYTES = 1024;
56
+ /**
57
+ * An upper bound on `Buffer.byteLength(JSON.stringify(value))` for the plain
58
+ * JSON data tool and resource results are made of, from a walk that reads
59
+ * string lengths but never encodes a string. JSON spends at most six bytes on
60
+ * one UTF-16 code unit (`\uXXXX` for a control character or a lone surrogate;
61
+ * any other BMP character is at most three UTF-8 bytes, a surrogate pair four
62
+ * for its two units), so a string or key costs at most 6 × length + 2 quotes.
63
+ * A finite number costs its `String()` form, which is what JSON writes, and a
64
+ * non-finite one `null`; `true`, `false` and `null` at most five. Each array
65
+ * element and object member adds one separator, and a member one colon, on top
66
+ * of the two brackets. A member JSON would omit (undefined, a function) only
67
+ * overcounts. `toJSON` is followed the way JSON.stringify follows it.
68
+ */
69
+ export declare function jsonByteBound(value: unknown): number;
70
+ /**
71
+ * `value`'s JSON-encoded size in bytes when it is over `limit`, else
72
+ * `undefined`. {@link jsonByteBound} clears most values without encoding them;
73
+ * only one it cannot clear pays for the exact `JSON.stringify` measure.
74
+ */
75
+ export declare function jsonBytesOver(value: unknown, limit: number): number | undefined;
47
76
  /** Default line chunk size for read continuation when no explicit range was given. */
48
77
  export declare const DEFAULT_CONTINUATION_CHUNK_SIZE = 200;
49
78
  export declare const DEFAULT_SEARCH_TIMEOUT_MS = 5000;
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/core/util.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,GAAG,QAAa,CAAC;AAE9B,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,GACV,MAAM,CAER;AA6BD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAKhE;AA6BD,qEAAqE;AACrE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS1D;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MACgD,CAAC;AAE/F,eAAO,MAAM,oBAAoB,QAAoD,CAAC;AAEtF,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,wBAAgB,kBAAkB,IAAI,MAAM,CAQ3C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AACD,mDAAmD;AACnD,eAAO,MAAM,yBAAyB,QAAY,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,QAAuB,CAAC;AAE9D,sFAAsF;AACtF,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,eAAO,MAAM,gBAAgB,QAAQ,CAAC;AAEtC,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AACxC,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../src/core/util.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,GAAG,QAAa,CAAC;AAE9B,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,GACV,MAAM,CAER;AA6BD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAKhE;AA6BD,qEAAqE;AACrE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS1D;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MACgD,CAAC;AAE/F,eAAO,MAAM,oBAAoB,QAAoD,CAAC;AAEtF,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,wBAAgB,kBAAkB,IAAI,MAAM,CAQ3C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AACD,mDAAmD;AACnD,eAAO,MAAM,yBAAyB,QAAY,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,QAAuB,CAAC;AAE9D;;;GAGG;AACH,eAAO,MAAM,0BAA0B,OAAM,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAgBpD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI/E;AAED,sFAAsF;AACtF,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,eAAO,MAAM,gBAAgB,QAAQ,CAAC;AAEtC,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AACxC,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC"}
package/dist/core/util.js CHANGED
@@ -105,13 +105,67 @@ export function getMaxInboundMessageBytes() {
105
105
  /** Combined byte budget for one batched `read`. */
106
106
  export const READ_MANY_MAX_TOTAL_BYTES = 512 * KIB;
107
107
  /**
108
- * Ceiling on the JSON-encoded size of one `read` result. The MCP SDK's stdio
109
- * transport caps a message at a fixed 10 MiB ReadBuffer regardless of
110
- * `--max-file-size`, so a file under the size limit can still overflow it once
111
- * escaped; `read` returns TOO_LARGE instead. ~256 KiB covers the JSON-RPC
112
- * envelope this measure omits.
108
+ * Ceiling on JSON-encoded tool and resource results over stdio. The SDK's
109
+ * fixed 10 MiB ReadBuffer applies regardless of `--max-file-size`; escaping,
110
+ * duplicated metadata, and base64 can overflow it even when the source fits.
111
+ * A tool call that changed nothing (read-only, or a dry run) and a resource
112
+ * read return TOO_LARGE instead, and the stdio result store refuses an entry
113
+ * whose read would overflow it; HTTP is unaffected. ~256 KiB covers the
114
+ * JSON-RPC envelope this measure omits, including the bounded request ID.
115
+ * Mutating tools need pre-commit safeguards rather than a post-commit output
116
+ * refusal.
113
117
  */
114
118
  export const MAX_OUTBOUND_RESULT_BYTES = 10 * MIB - 256 * KIB;
119
+ /**
120
+ * JSON-encoded stdio request IDs and string progress tokens, including
121
+ * quotes/escapes, must fit the envelope reserve: each is echoed into frames.
122
+ */
123
+ export const MAX_STDIO_REQUEST_ID_BYTES = KIB;
124
+ /**
125
+ * An upper bound on `Buffer.byteLength(JSON.stringify(value))` for the plain
126
+ * JSON data tool and resource results are made of, from a walk that reads
127
+ * string lengths but never encodes a string. JSON spends at most six bytes on
128
+ * one UTF-16 code unit (`\uXXXX` for a control character or a lone surrogate;
129
+ * any other BMP character is at most three UTF-8 bytes, a surrogate pair four
130
+ * for its two units), so a string or key costs at most 6 × length + 2 quotes.
131
+ * A finite number costs its `String()` form, which is what JSON writes, and a
132
+ * non-finite one `null`; `true`, `false` and `null` at most five. Each array
133
+ * element and object member adds one separator, and a member one colon, on top
134
+ * of the two brackets. A member JSON would omit (undefined, a function) only
135
+ * overcounts. `toJSON` is followed the way JSON.stringify follows it.
136
+ */
137
+ export function jsonByteBound(value) {
138
+ if (typeof value === 'string')
139
+ return 6 * value.length + 2;
140
+ if (typeof value === 'number')
141
+ return Number.isFinite(value) ? String(value).length : 4;
142
+ if (typeof value !== 'object' || value === null)
143
+ return 5;
144
+ if (typeof value.toJSON === 'function') {
145
+ return jsonByteBound(value.toJSON());
146
+ }
147
+ let bytes = 2;
148
+ if (Array.isArray(value)) {
149
+ for (const item of value)
150
+ bytes += jsonByteBound(item) + 1;
151
+ return bytes;
152
+ }
153
+ for (const [key, item] of Object.entries(value)) {
154
+ bytes += 6 * key.length + 2 + 1 + jsonByteBound(item) + 1;
155
+ }
156
+ return bytes;
157
+ }
158
+ /**
159
+ * `value`'s JSON-encoded size in bytes when it is over `limit`, else
160
+ * `undefined`. {@link jsonByteBound} clears most values without encoding them;
161
+ * only one it cannot clear pays for the exact `JSON.stringify` measure.
162
+ */
163
+ export function jsonBytesOver(value, limit) {
164
+ if (jsonByteBound(value) <= limit)
165
+ return undefined;
166
+ const bytes = Buffer.byteLength(JSON.stringify(value), 'utf8');
167
+ return bytes > limit ? bytes : undefined;
168
+ }
115
169
  /** Default line chunk size for read continuation when no explicit range was given. */
116
170
  export const DEFAULT_CONTINUATION_CHUNK_SIZE = 200;
117
171
  export const DEFAULT_SEARCH_TIMEOUT_MS = 5000;