@j0hanz/filesystem-mcp 1.17.0 → 1.17.1
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/dist/completions.js +83 -16
- package/dist/lib/constants.d.ts +1 -1
- package/dist/lib/constants.js +3 -1
- package/dist/lib/fs-helpers.d.ts +2 -1
- package/dist/lib/fs-helpers.js +39 -22
- package/dist/server/bootstrap.js +16 -6
- package/dist/server/roots-manager.js +5 -4
- package/dist/tools/apply-patch.js +8 -3
- package/dist/tools/edit-file.js +8 -3
- package/dist/tools/read.js +2 -5
- package/dist/tools/task-support.js +26 -6
- package/package.json +1 -1
package/dist/completions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readdir, stat } from 'node:fs/promises';
|
|
1
|
+
import { readdir, realpath, stat } from 'node:fs/promises';
|
|
2
2
|
import { basename, dirname, isAbsolute, join, parse, resolve, sep, } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { getAllowedDirectories, isPathWithinDirectories, normalizePath, toPosixPath, } from './lib/paths.js';
|
|
4
5
|
import { isRecord } from './lib/utils.js';
|
|
5
6
|
import { getSortedToolContracts } from './resources/tool-info.js';
|
|
@@ -46,9 +47,63 @@ const PATH_ARGUMENTS = new Set([
|
|
|
46
47
|
const DESTINATION_CONTEXT_KEYS = ['source', 'path', 'cwd', 'root'];
|
|
47
48
|
const PRIMARY_PATH_CONTEXT_KEYS = ['path', 'cwd', 'root'];
|
|
48
49
|
const DEFAULT_CONTEXT_KEYS = ['path', 'source', 'cwd', 'root'];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
function extractEnumValuesFromSchema(argumentName, schema) {
|
|
51
|
+
const properties = getInputSchemaProperties(schema);
|
|
52
|
+
if (!properties || typeof properties !== 'object')
|
|
53
|
+
return undefined;
|
|
54
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
55
|
+
if (key.toLowerCase() !== argumentName)
|
|
56
|
+
continue;
|
|
57
|
+
if (value === null || typeof value !== 'object' || !('enum' in value)) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const enumValues = value.enum;
|
|
61
|
+
if (!Array.isArray(enumValues))
|
|
62
|
+
return undefined;
|
|
63
|
+
const stringValues = enumValues.filter((entry) => typeof entry === 'string');
|
|
64
|
+
return stringValues.length > 0 ? stringValues : undefined;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
function getInputSchemaProperties(schema) {
|
|
69
|
+
const { properties } = z.toJSONSchema(schema, { io: 'input' });
|
|
70
|
+
if (!properties || typeof properties !== 'object')
|
|
71
|
+
return undefined;
|
|
72
|
+
return properties;
|
|
73
|
+
}
|
|
74
|
+
function intersectEnumValueSets(valueSets) {
|
|
75
|
+
const [firstSet, ...restSets] = valueSets;
|
|
76
|
+
if (!firstSet)
|
|
77
|
+
return [];
|
|
78
|
+
return firstSet.filter((value) => restSets.every((candidateSet) => candidateSet.includes(value)));
|
|
79
|
+
}
|
|
80
|
+
function buildEnumArgumentValues() {
|
|
81
|
+
const valuesByArgument = new Map();
|
|
82
|
+
for (const contract of getSortedToolContracts()) {
|
|
83
|
+
const properties = getInputSchemaProperties(contract.inputSchema);
|
|
84
|
+
if (!properties || typeof properties !== 'object')
|
|
85
|
+
continue;
|
|
86
|
+
for (const key of Object.keys(properties)) {
|
|
87
|
+
const normalizedKey = key.toLowerCase();
|
|
88
|
+
const values = extractEnumValuesFromSchema(normalizedKey, contract.inputSchema);
|
|
89
|
+
if (!values)
|
|
90
|
+
continue;
|
|
91
|
+
const existing = valuesByArgument.get(normalizedKey) ?? [];
|
|
92
|
+
existing.push(values);
|
|
93
|
+
valuesByArgument.set(normalizedKey, existing);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const result = new Map();
|
|
97
|
+
for (const [argumentName, valueSets] of valuesByArgument) {
|
|
98
|
+
const values = valueSets.length === 1
|
|
99
|
+
? (valueSets[0] ?? [])
|
|
100
|
+
: intersectEnumValueSets(valueSets);
|
|
101
|
+
if (values.length > 0) {
|
|
102
|
+
result.set(argumentName, values);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
52
107
|
function isPathLikeArgumentName(argName) {
|
|
53
108
|
return (PATH_ARGUMENTS.has(argName) ||
|
|
54
109
|
argName.endsWith('paths') ||
|
|
@@ -58,8 +113,8 @@ function isPathLikeArgumentName(argName) {
|
|
|
58
113
|
argName.endsWith('dirs') ||
|
|
59
114
|
argName.endsWith('dir'));
|
|
60
115
|
}
|
|
61
|
-
function getEnumCompletions(argName, currentValue) {
|
|
62
|
-
const values =
|
|
116
|
+
function getEnumCompletions(argName, currentValue, enumArgumentValues) {
|
|
117
|
+
const values = enumArgumentValues.get(argName);
|
|
63
118
|
if (!values)
|
|
64
119
|
return undefined;
|
|
65
120
|
const prefix = currentValue.toLowerCase();
|
|
@@ -267,18 +322,29 @@ function resolveContextCandidatePath(candidate, allowed) {
|
|
|
267
322
|
return resolveNamedRootPath(candidate, allowed);
|
|
268
323
|
}
|
|
269
324
|
async function toAllowedContextDirectory(resolved, allowed) {
|
|
270
|
-
|
|
271
|
-
|
|
325
|
+
const parent = dirname(resolved);
|
|
326
|
+
if (await isAllowedCompletionDirectory(resolved, allowed)) {
|
|
327
|
+
return resolved;
|
|
328
|
+
}
|
|
329
|
+
return (await isAllowedCompletionDirectory(parent, allowed))
|
|
330
|
+
? parent
|
|
331
|
+
: undefined;
|
|
332
|
+
}
|
|
333
|
+
async function isAllowedCompletionDirectory(path, allowed) {
|
|
334
|
+
if (!isPathWithinDirectories(path, allowed))
|
|
335
|
+
return false;
|
|
272
336
|
try {
|
|
273
|
-
const stats = await
|
|
274
|
-
|
|
275
|
-
|
|
337
|
+
const [stats, resolvedRealPath] = await Promise.all([
|
|
338
|
+
stat(path),
|
|
339
|
+
realpath(path),
|
|
340
|
+
]);
|
|
341
|
+
if (!stats.isDirectory())
|
|
342
|
+
return false;
|
|
343
|
+
return isPathWithinDirectories(normalizePath(resolvedRealPath), allowed);
|
|
276
344
|
}
|
|
277
345
|
catch {
|
|
278
|
-
|
|
346
|
+
return false;
|
|
279
347
|
}
|
|
280
|
-
const parent = dirname(resolved);
|
|
281
|
-
return isPathWithinDirectories(parent, allowed) ? parent : undefined;
|
|
282
348
|
}
|
|
283
349
|
async function resolveContextBaseDirectory(argumentName, contextArguments, allowed) {
|
|
284
350
|
if (!hasContextArguments(contextArguments)) {
|
|
@@ -371,7 +437,7 @@ function getSearchContext(currentValue, allowed, contextBase) {
|
|
|
371
437
|
}
|
|
372
438
|
async function findMatchesInDirectory(searchDir, prefix, allowed) {
|
|
373
439
|
const matches = [];
|
|
374
|
-
if (!
|
|
440
|
+
if (!(await isAllowedCompletionDirectory(searchDir, allowed))) {
|
|
375
441
|
return matches;
|
|
376
442
|
}
|
|
377
443
|
try {
|
|
@@ -460,6 +526,7 @@ function handleTopicAndToolCompletions(ref, argName, argumentValue, topicValues,
|
|
|
460
526
|
export function registerCompletions(server, instructions = '') {
|
|
461
527
|
const topicValues = extractTopicCompletions(instructions);
|
|
462
528
|
const toolNameValues = extractToolNameCompletions();
|
|
529
|
+
const enumArgumentValues = buildEnumArgumentValues();
|
|
463
530
|
server.server.setRequestHandler('completion/complete', async (request) => {
|
|
464
531
|
const { params } = request;
|
|
465
532
|
const { argument, ref } = params;
|
|
@@ -467,7 +534,7 @@ export function registerCompletions(server, instructions = '') {
|
|
|
467
534
|
const predef = handleTopicAndToolCompletions(ref, argName, argument.value, topicValues, toolNameValues);
|
|
468
535
|
if (predef)
|
|
469
536
|
return predef;
|
|
470
|
-
const enumResult = getEnumCompletions(argName, argument.value);
|
|
537
|
+
const enumResult = getEnumCompletions(argName, argument.value, enumArgumentValues);
|
|
471
538
|
if (enumResult) {
|
|
472
539
|
return buildCompletionResponse(enumResult);
|
|
473
540
|
}
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const DEFAULT_TASK_TTL_MS: number;
|
|
|
5
5
|
export declare const MAX_TASK_TTL_MS: number;
|
|
6
6
|
export declare const MAX_CONCURRENT_TASKS: number;
|
|
7
7
|
export declare const TASK_CANCEL_POLL_MS = 2000;
|
|
8
|
-
export declare
|
|
8
|
+
export declare function getInitHandshakeTimeoutMs(): number;
|
|
9
9
|
export declare const INIT_TIMEOUT_CLOSE: boolean;
|
|
10
10
|
export declare const TASK_POLL_INTERVAL_MS = 100;
|
|
11
11
|
export declare const PARALLEL_CONCURRENCY: number;
|
package/dist/lib/constants.js
CHANGED
|
@@ -76,7 +76,9 @@ export const DEFAULT_TASK_TTL_MS = 5 * 60 * 1000;
|
|
|
76
76
|
export const MAX_TASK_TTL_MS = parseEnvInt('FILESYSTEM_MCP_MAX_TASK_TTL_MS', 60 * 60 * 1000, 1_000, 24 * 60 * 60 * 1000);
|
|
77
77
|
export const MAX_CONCURRENT_TASKS = parseEnvInt('FILESYSTEM_MCP_MAX_CONCURRENT_TASKS', 100, 1, 10_000);
|
|
78
78
|
export const TASK_CANCEL_POLL_MS = 2_000;
|
|
79
|
-
export
|
|
79
|
+
export function getInitHandshakeTimeoutMs() {
|
|
80
|
+
return parseEnvInt('FS_INIT_HANDSHAKE_TIMEOUT_MS', 30_000, 1_000, 300_000);
|
|
81
|
+
}
|
|
80
82
|
export const INIT_TIMEOUT_CLOSE = parseTrueEnvFlag(process.env['FS_INIT_TIMEOUT_CLOSE']);
|
|
81
83
|
export const TASK_POLL_INTERVAL_MS = 100;
|
|
82
84
|
// Auto-tuned parallelism based on CPU cores (no env override)
|
package/dist/lib/fs-helpers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Stats } from 'node:fs';
|
|
2
2
|
import { type FileHandle } from 'node:fs/promises';
|
|
3
3
|
import type { FileType } from '../config.js';
|
|
4
4
|
interface ParallelResult<R> {
|
|
@@ -12,6 +12,7 @@ export declare function processInParallel<T, R>(items: T[], processor: (item: T)
|
|
|
12
12
|
export declare function getFileType(stats: Stats): FileType;
|
|
13
13
|
export declare function isHidden(name: string): boolean;
|
|
14
14
|
export declare function isProbablyBinary(filePath: string, existingHandle?: FileHandle, signal?: AbortSignal): Promise<boolean>;
|
|
15
|
+
export declare function calculateFileContentHash(filePath: string, signal?: AbortSignal): Promise<string>;
|
|
15
16
|
type ReadMode = 'head' | 'full' | 'range' | 'tail';
|
|
16
17
|
interface ReadFileOptions {
|
|
17
18
|
encoding?: BufferEncoding;
|
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -51,9 +51,11 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
53
|
import { isUtf8 } from 'node:buffer';
|
|
54
|
-
import { randomUUID } from 'node:crypto';
|
|
54
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
55
|
+
import { createReadStream } from 'node:fs';
|
|
55
56
|
import { open, rename, stat, unlink, writeFile, } from 'node:fs/promises';
|
|
56
57
|
import { extname } from 'node:path';
|
|
58
|
+
import { pipeline } from 'node:stream/promises';
|
|
57
59
|
import { assertNotAborted, withAbort } from './abort.js';
|
|
58
60
|
import { BINARY_CHECK_BUFFER_SIZE, KNOWN_BINARY_EXTENSIONS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from './constants.js';
|
|
59
61
|
import { ErrorCode, McpError, normalizeUnknownError } from './errors.js';
|
|
@@ -192,6 +194,11 @@ function isBinarySlice(slice) {
|
|
|
192
194
|
return true;
|
|
193
195
|
return !isUtf8(slice);
|
|
194
196
|
}
|
|
197
|
+
export async function calculateFileContentHash(filePath, signal) {
|
|
198
|
+
const hasher = createHash('sha256');
|
|
199
|
+
await pipeline(createReadStream(filePath, { signal, highWaterMark: STREAM_CHUNK_SIZE }), hasher, { signal });
|
|
200
|
+
return hasher.digest('hex');
|
|
201
|
+
}
|
|
195
202
|
function validateReadOptions(options) {
|
|
196
203
|
const hasHead = options.head !== undefined;
|
|
197
204
|
const hasTail = options.tail !== undefined;
|
|
@@ -298,24 +305,6 @@ async function readFileBufferWithLimit(handle, maxSize, requestedPath, signal) {
|
|
|
298
305
|
}
|
|
299
306
|
return Buffer.concat(chunks, totalSize);
|
|
300
307
|
}
|
|
301
|
-
async function headFile(handle, numLines, encoding = 'utf-8', maxBytesRead, signal) {
|
|
302
|
-
assertNotAborted(signal);
|
|
303
|
-
const lines = [];
|
|
304
|
-
let estimatedBytes = 0;
|
|
305
|
-
const hasMaxBytes = maxBytesRead !== undefined;
|
|
306
|
-
const newlineBytes = Buffer.byteLength('\n', encoding);
|
|
307
|
-
for await (const line of handle.readLines({ encoding, signal })) {
|
|
308
|
-
lines.push(line);
|
|
309
|
-
if (lines.length >= numLines)
|
|
310
|
-
break;
|
|
311
|
-
if (!hasMaxBytes)
|
|
312
|
-
continue;
|
|
313
|
-
estimatedBytes += Buffer.byteLength(line, encoding) + newlineBytes;
|
|
314
|
-
if (estimatedBytes >= maxBytesRead)
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
return lines.join('\n');
|
|
318
|
-
}
|
|
319
308
|
function countLines(content) {
|
|
320
309
|
if (content.length === 0)
|
|
321
310
|
return 0;
|
|
@@ -327,9 +316,37 @@ function countLines(content) {
|
|
|
327
316
|
return count;
|
|
328
317
|
}
|
|
329
318
|
async function readHeadContent(handle, head, options) {
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
319
|
+
assertNotAborted(options.signal);
|
|
320
|
+
const lines = [];
|
|
321
|
+
let estimatedBytes = 0;
|
|
322
|
+
const newlineBytes = Buffer.byteLength('\n', options.encoding);
|
|
323
|
+
const iterator = handle
|
|
324
|
+
.readLines({ encoding: options.encoding, signal: options.signal })[Symbol.asyncIterator]();
|
|
325
|
+
let hasMoreLines = false;
|
|
326
|
+
let next = await iterator.next();
|
|
327
|
+
try {
|
|
328
|
+
while (!next.done) {
|
|
329
|
+
const line = next.value;
|
|
330
|
+
lines.push(line);
|
|
331
|
+
estimatedBytes +=
|
|
332
|
+
Buffer.byteLength(line, options.encoding) + newlineBytes;
|
|
333
|
+
if (estimatedBytes >= options.maxSize) {
|
|
334
|
+
hasMoreLines = true;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
if (lines.length === head) {
|
|
338
|
+
const peek = await iterator.next();
|
|
339
|
+
hasMoreLines = !peek.done;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
next = await iterator.next();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
await iterator.return?.();
|
|
347
|
+
}
|
|
348
|
+
const content = lines.join('\n');
|
|
349
|
+
const linesRead = lines.length;
|
|
333
350
|
return {
|
|
334
351
|
content,
|
|
335
352
|
truncated: hasMoreLines,
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -4,7 +4,8 @@ import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
|
4
4
|
import { channel } from 'node:diagnostics_channel';
|
|
5
5
|
import { readFile } from 'node:fs/promises';
|
|
6
6
|
import { createServer as createHttpServer, } from 'node:http';
|
|
7
|
-
import {
|
|
7
|
+
import { inspect } from 'node:util';
|
|
8
|
+
import { DEFAULT_LOG_LEVEL, getInitHandshakeTimeoutMs, INIT_TIMEOUT_CLOSE, parseEnvInt, } from '../lib/constants.js';
|
|
8
9
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
9
10
|
import { createLoggingState, Logger, logToMcp, SessionContext, } from '../lib/logger.js';
|
|
10
11
|
import { withAllowedDirectoriesState } from '../lib/paths.js';
|
|
@@ -44,9 +45,17 @@ const activeServers = new Map();
|
|
|
44
45
|
// For stdio (single session without a specific ID)
|
|
45
46
|
let stdioServer;
|
|
46
47
|
function stringifyData(data) {
|
|
47
|
-
if (
|
|
48
|
+
if (data === undefined)
|
|
48
49
|
return '';
|
|
49
|
-
|
|
50
|
+
if (typeof data === 'string')
|
|
51
|
+
return ` ${data}`;
|
|
52
|
+
if (data === null ||
|
|
53
|
+
typeof data === 'number' ||
|
|
54
|
+
typeof data === 'boolean' ||
|
|
55
|
+
typeof data === 'bigint') {
|
|
56
|
+
return ` ${String(data)}`;
|
|
57
|
+
}
|
|
58
|
+
return ` ${inspect(data, { depth: 4, colors: false, compact: 3 })}`;
|
|
50
59
|
}
|
|
51
60
|
channel('filesystem-mcp:log').subscribe((message) => {
|
|
52
61
|
const event = message;
|
|
@@ -533,14 +542,15 @@ export async function startHttpServer(port, options) {
|
|
|
533
542
|
handleHttpRequestError(error, res);
|
|
534
543
|
}
|
|
535
544
|
}
|
|
536
|
-
const
|
|
545
|
+
const initHandshakeTimeoutMs = getInitHandshakeTimeoutMs();
|
|
546
|
+
const SWEEP_INTERVAL_MS = initHandshakeTimeoutMs * 2;
|
|
537
547
|
const sweepTimer = setInterval(() => {
|
|
538
548
|
const now = Date.now();
|
|
539
549
|
for (const [sessionId, session] of sessions) {
|
|
540
550
|
if (!session.rootsManager.isInitialized() &&
|
|
541
|
-
now - session.createdAt >
|
|
551
|
+
now - session.createdAt > initHandshakeTimeoutMs) {
|
|
542
552
|
Logger.warn(`[HTTP] Evicting stale session ${sessionId}: client never sent notifications/initialized`);
|
|
543
|
-
session.
|
|
553
|
+
session.close().catch((err) => {
|
|
544
554
|
Logger.error(`[HTTP] Error closing stale session ${sessionId}:`, formatUnknownErrorMessage(err));
|
|
545
555
|
});
|
|
546
556
|
}
|
|
@@ -3,7 +3,7 @@ import { channel } from 'node:diagnostics_channel';
|
|
|
3
3
|
import { realpath } from 'node:fs/promises';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { assertNotAborted, createTimedAbortSignal, withAbort, } from '../lib/abort.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getInitHandshakeTimeoutMs } from '../lib/constants.js';
|
|
7
7
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
8
8
|
import { Logger, logToMcp } from '../lib/logger.js';
|
|
9
9
|
import { getValidRootDirectories, isPathWithinDirectories, normalizePath, resolveAllowedDirectoriesState, setAllowedDirectoriesStateResolved, } from '../lib/paths.js';
|
|
@@ -128,6 +128,7 @@ export class RootsManager {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
registerHandlers(server, onInitTimeout) {
|
|
131
|
+
const initHandshakeTimeoutMs = getInitHandshakeTimeoutMs();
|
|
131
132
|
server.server.setNotificationHandler('notifications/initialized', async () => {
|
|
132
133
|
if (this.initTimer) {
|
|
133
134
|
clearTimeout(this.initTimer);
|
|
@@ -146,14 +147,14 @@ export class RootsManager {
|
|
|
146
147
|
if (LIFECYCLE_CHANNEL.hasSubscribers) {
|
|
147
148
|
LIFECYCLE_CHANNEL.publish({
|
|
148
149
|
phase: 'init_timeout',
|
|
149
|
-
timeoutMs:
|
|
150
|
+
timeoutMs: initHandshakeTimeoutMs,
|
|
150
151
|
});
|
|
151
152
|
}
|
|
152
|
-
logToMcp(server, 'warning', `Client did not send notifications/initialized within ${String(
|
|
153
|
+
logToMcp(server, 'warning', `Client did not send notifications/initialized within ${String(initHandshakeTimeoutMs)}ms`, this.loggingState.minimumLevel);
|
|
153
154
|
onInitTimeout?.();
|
|
154
155
|
}
|
|
155
156
|
this.initTimer = undefined;
|
|
156
|
-
},
|
|
157
|
+
}, initHandshakeTimeoutMs);
|
|
157
158
|
this.initTimer.unref();
|
|
158
159
|
}
|
|
159
160
|
async recomputeAllowedDirectories() {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
2
|
import { basename, resolve } from 'node:path';
|
|
3
3
|
import { applyPatch, parsePatch } from 'diff';
|
|
4
4
|
import { withAbort } from '../lib/abort.js';
|
|
5
5
|
import { MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
6
6
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
7
|
-
import { atomicWriteFile, processInParallel } from '../lib/fs-helpers.js';
|
|
7
|
+
import { atomicWriteFile, processInParallel, readFileWithStats, } from '../lib/fs-helpers.js';
|
|
8
8
|
import { Logger } from '../lib/logger.js';
|
|
9
9
|
import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
|
|
10
10
|
import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
|
|
@@ -62,7 +62,12 @@ async function applyDiff(filePath, diff, options, signal) {
|
|
|
62
62
|
assertAllowedFileAccess(filePath, validPath);
|
|
63
63
|
const stats = await withAbort(stat(validPath), signal);
|
|
64
64
|
assertPatchTargetSizeWithinLimit(validPath, stats.size, MAX_TEXT_FILE_SIZE);
|
|
65
|
-
const content = await
|
|
65
|
+
const { content } = await readFileWithStats(filePath, validPath, stats, {
|
|
66
|
+
encoding: 'utf-8',
|
|
67
|
+
maxSize: MAX_TEXT_FILE_SIZE,
|
|
68
|
+
skipBinary: true,
|
|
69
|
+
...(signal ? { signal } : {}),
|
|
70
|
+
});
|
|
66
71
|
const patched = applyPatch(content, diff, {
|
|
67
72
|
fuzzFactor: options.fuzzFactor,
|
|
68
73
|
autoConvertLineEndings: options.autoConvertLineEndings,
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { createTwoFilesPatch, diffLines } from 'diff';
|
|
4
4
|
import RE2 from 're2';
|
|
5
5
|
import { withAbort } from '../lib/abort.js';
|
|
6
6
|
import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
7
7
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
8
|
-
import { atomicWriteFile } from '../lib/fs-helpers.js';
|
|
8
|
+
import { atomicWriteFile, readFileWithStats } from '../lib/fs-helpers.js';
|
|
9
9
|
import { Logger } from '../lib/logger.js';
|
|
10
10
|
import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
|
|
11
11
|
import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
|
|
@@ -167,7 +167,12 @@ async function loadEditableFile(requestedPath, signal) {
|
|
|
167
167
|
if (stats.size > MAX_TEXT_FILE_SIZE) {
|
|
168
168
|
throw new McpError(ErrorCode.TOO_LARGE, `File too large for edit (${stats.size} bytes > ${MAX_TEXT_FILE_SIZE} bytes)`, requestedPath, { size: stats.size, maxFileSize: MAX_TEXT_FILE_SIZE });
|
|
169
169
|
}
|
|
170
|
-
const content = await
|
|
170
|
+
const { content } = await readFileWithStats(requestedPath, validPath, stats, {
|
|
171
|
+
encoding: 'utf-8',
|
|
172
|
+
maxSize: MAX_TEXT_FILE_SIZE,
|
|
173
|
+
skipBinary: true,
|
|
174
|
+
...(signal ? { signal } : {}),
|
|
175
|
+
});
|
|
171
176
|
return { validPath, content };
|
|
172
177
|
}
|
|
173
178
|
function buildEditProgressMessage(args) {
|
package/dist/tools/read.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { basename } from 'node:path';
|
|
3
2
|
import { DEFAULT_SEARCH_TIMEOUT_MS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
|
|
4
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
|
-
import { readFile } from '../lib/fs-helpers.js';
|
|
4
|
+
import { calculateFileContentHash, readFile } from '../lib/fs-helpers.js';
|
|
6
5
|
import { ReadFileInputSchema, ReadFileOutputSchema } from '../schemas.js';
|
|
7
6
|
import { FILE_READ_ICONS } from './icons.js';
|
|
8
7
|
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, } from './shared.js';
|
|
@@ -144,9 +143,7 @@ async function handleReadFile(args, signal, resourceStore) {
|
|
|
144
143
|
const result = await readFile(args.path, options);
|
|
145
144
|
const structured = toStructuredReadFileResult(args, result);
|
|
146
145
|
if (args.includeHash) {
|
|
147
|
-
structured.contentHash =
|
|
148
|
-
.update(result.content, 'utf-8')
|
|
149
|
-
.digest('hex');
|
|
146
|
+
structured.contentHash = await calculateFileContentHash(result.path, signal);
|
|
150
147
|
}
|
|
151
148
|
const externalizedResponse = maybeBuildExternalizedReadResponse(args.path, result.content, structured, resourceStore);
|
|
152
149
|
if (externalizedResponse) {
|
|
@@ -263,6 +263,19 @@ const TERMINAL_TASK_STATUSES = new Set([
|
|
|
263
263
|
'cancelled',
|
|
264
264
|
'unknown',
|
|
265
265
|
]);
|
|
266
|
+
const taskCreationLocks = new WeakMap();
|
|
267
|
+
async function acquireTaskCreationLock(taskStore) {
|
|
268
|
+
const previous = taskCreationLocks.get(taskStore) ?? Promise.resolve();
|
|
269
|
+
let release;
|
|
270
|
+
const next = new Promise((resolve) => {
|
|
271
|
+
release = resolve;
|
|
272
|
+
});
|
|
273
|
+
taskCreationLocks.set(taskStore, previous.catch(() => { }).then(() => next));
|
|
274
|
+
await previous.catch(() => { });
|
|
275
|
+
return () => {
|
|
276
|
+
release();
|
|
277
|
+
};
|
|
278
|
+
}
|
|
266
279
|
async function isTaskAlreadyTerminal(taskStore, taskId) {
|
|
267
280
|
try {
|
|
268
281
|
const task = await taskStore.getTask(taskId);
|
|
@@ -443,13 +456,20 @@ export function createToolTaskHandler(run, options) {
|
|
|
443
456
|
throw new McpError(ErrorCode.INVALID_INPUT, 'Client not initialized; wait for notifications/initialized');
|
|
444
457
|
}
|
|
445
458
|
const taskStore = getTaskStore(ctx);
|
|
446
|
-
|
|
447
|
-
|
|
459
|
+
const releaseCreationLock = await acquireTaskCreationLock(taskStore);
|
|
460
|
+
let task;
|
|
461
|
+
try {
|
|
462
|
+
if ((await countActiveTasks(taskStore)) >= MAX_CONCURRENT_TASKS) {
|
|
463
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `Too many active tasks (limit: ${String(MAX_CONCURRENT_TASKS)}).`);
|
|
464
|
+
}
|
|
465
|
+
task = await taskStore.createTask({
|
|
466
|
+
ttl: resolveRequestedTaskTtl(ctx.taskRequestedTtl),
|
|
467
|
+
pollInterval: options?.pollIntervalMs ?? TASK_POLL_INTERVAL_MS,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
releaseCreationLock();
|
|
448
472
|
}
|
|
449
|
-
const task = await taskStore.createTask({
|
|
450
|
-
ttl: resolveRequestedTaskTtl(ctx.taskRequestedTtl),
|
|
451
|
-
pollInterval: options?.pollIntervalMs ?? TASK_POLL_INTERVAL_MS,
|
|
452
|
-
});
|
|
453
473
|
const toolLabel = options?.toolName ?? 'tool';
|
|
454
474
|
try {
|
|
455
475
|
await taskStore.updateTaskStatus(task.taskId, 'working', `${toolLabel}: starting`);
|
package/package.json
CHANGED