@j0hanz/filesystem-mcp 1.14.0 → 1.14.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/README.md +47 -47
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -13
- package/dist/config.js +12 -12
- package/dist/index.js +1 -1
- package/dist/lib/abort.d.ts +7 -0
- package/dist/lib/abort.js +81 -0
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +8 -2
- package/dist/lib/errors.d.ts +7 -3
- package/dist/lib/errors.js +59 -39
- package/dist/lib/file-operations/core.d.ts +3 -3
- package/dist/lib/file-operations/core.js +23 -20
- package/dist/lib/file-operations/metadata.d.ts +2 -2
- package/dist/lib/file-operations/metadata.js +60 -19
- package/dist/lib/file-operations/search.js +83 -84
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +20 -98
- package/dist/lib/globs.js +1 -1
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.js +91 -0
- package/dist/lib/observability.d.ts +7 -0
- package/dist/lib/observability.js +19 -9
- package/dist/lib/paths.js +55 -55
- package/dist/lib/resource-store.js +4 -4
- package/dist/lib/utils.d.ts +0 -12
- package/dist/lib/utils.js +0 -13
- package/dist/resources/generated-instructions.js +40 -31
- package/dist/resources/tool-catalog.js +32 -26
- package/dist/resources/tool-info.js +34 -29
- package/dist/resources/workflows.js +39 -18
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +66 -66
- package/dist/schemas.js +21 -44
- package/dist/server/bootstrap.d.ts +12 -11
- package/dist/server/bootstrap.js +95 -86
- package/dist/server/roots-manager.d.ts +5 -2
- package/dist/server/roots-manager.js +8 -6
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +26 -18
- package/dist/tools/calculate-hash.js +11 -22
- package/dist/tools/create-directory.js +10 -8
- package/dist/tools/delete-file.js +17 -15
- package/dist/tools/diff-files.js +15 -15
- package/dist/tools/edit-file.js +7 -4
- package/dist/tools/list-directory.js +6 -6
- package/dist/tools/move-file.js +98 -81
- package/dist/tools/read-multiple.js +4 -4
- package/dist/tools/read.js +5 -5
- package/dist/tools/replace-in-files.js +20 -23
- package/dist/tools/roots.js +1 -1
- package/dist/tools/search-content.js +32 -41
- package/dist/tools/search-files.js +54 -41
- package/dist/tools/shared.d.ts +3 -0
- package/dist/tools/shared.js +70 -28
- package/dist/tools/stat-many.js +11 -7
- package/dist/tools/stat.js +4 -4
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +3 -3
- package/dist/tools/write-file.js +10 -7
- package/package.json +9 -9
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { LoggingLevel } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
|
+
export interface SessionContextData {
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const SessionContext: AsyncLocalStorage<SessionContextData>;
|
|
8
|
+
export interface LogEvent {
|
|
9
|
+
level: LoggingLevel;
|
|
10
|
+
message: string;
|
|
11
|
+
data?: unknown;
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LoggingState {
|
|
15
|
+
minimumLevel: LoggingLevel;
|
|
16
|
+
}
|
|
17
|
+
export declare function createLoggingState(minimumLevel?: LoggingLevel): LoggingState;
|
|
18
|
+
export declare function canSendMcpLogs(server: McpServer): boolean;
|
|
19
|
+
export declare function logToMcp(server: McpServer | undefined, level: LoggingLevel, data: string, minLevel?: LoggingLevel): void;
|
|
20
|
+
export declare const Logger: {
|
|
21
|
+
emit(level: LoggingLevel, message: string, data?: unknown): void;
|
|
22
|
+
debug(message: string, data?: unknown): void;
|
|
23
|
+
info(message: string, data?: unknown): void;
|
|
24
|
+
notice(message: string, data?: unknown): void;
|
|
25
|
+
warn(message: string, data?: unknown): void;
|
|
26
|
+
error(message: string, data?: unknown): void;
|
|
27
|
+
critical(message: string, data?: unknown): void;
|
|
28
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { channel } from 'node:diagnostics_channel';
|
|
3
|
+
export const SessionContext = new AsyncLocalStorage();
|
|
4
|
+
const LOG_CHANNEL = channel('filesystem-mcp:log');
|
|
5
|
+
const MCP_LOGGER_NAME = 'filesystem-mcp';
|
|
6
|
+
const LOG_LEVEL_ORDER = {
|
|
7
|
+
debug: 0,
|
|
8
|
+
info: 1,
|
|
9
|
+
notice: 2,
|
|
10
|
+
warning: 3,
|
|
11
|
+
error: 4,
|
|
12
|
+
critical: 5,
|
|
13
|
+
alert: 6,
|
|
14
|
+
emergency: 7,
|
|
15
|
+
};
|
|
16
|
+
export function createLoggingState(minimumLevel = 'debug') {
|
|
17
|
+
return { minimumLevel };
|
|
18
|
+
}
|
|
19
|
+
export function canSendMcpLogs(server) {
|
|
20
|
+
const capabilities = server.server.getClientCapabilities();
|
|
21
|
+
if (!capabilities || typeof capabilities !== 'object')
|
|
22
|
+
return false;
|
|
23
|
+
return 'logging' in capabilities && Boolean(capabilities.logging);
|
|
24
|
+
}
|
|
25
|
+
export function logToMcp(server, level, data, minLevel = 'debug') {
|
|
26
|
+
if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[minLevel]) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!server || !canSendMcpLogs(server)) {
|
|
30
|
+
console.error(`[${level.toUpperCase()}] ${data}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const params = {
|
|
34
|
+
level,
|
|
35
|
+
logger: MCP_LOGGER_NAME,
|
|
36
|
+
data,
|
|
37
|
+
};
|
|
38
|
+
void server.sendLoggingMessage(params).catch((error) => {
|
|
39
|
+
console.error(`Failed to send MCP log: ${level} | ${data}`, formatTransportError(error));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function formatTransportError(error) {
|
|
43
|
+
if (error instanceof Error)
|
|
44
|
+
return error.message;
|
|
45
|
+
if (typeof error === 'string')
|
|
46
|
+
return error;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.stringify(error);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return String(error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export const Logger = {
|
|
55
|
+
emit(level, message, data) {
|
|
56
|
+
const session = SessionContext.getStore();
|
|
57
|
+
const event = {
|
|
58
|
+
level,
|
|
59
|
+
message,
|
|
60
|
+
...(data !== undefined ? { data } : {}),
|
|
61
|
+
...(session?.sessionId !== undefined
|
|
62
|
+
? { sessionId: session.sessionId }
|
|
63
|
+
: {}),
|
|
64
|
+
};
|
|
65
|
+
if (LOG_CHANNEL.hasSubscribers) {
|
|
66
|
+
LOG_CHANNEL.publish(event);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Fallback if no subscribers
|
|
70
|
+
console.error(`[${level.toUpperCase()}] ${message}`, data ?? '');
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
debug(message, data) {
|
|
74
|
+
this.emit('debug', message, data);
|
|
75
|
+
},
|
|
76
|
+
info(message, data) {
|
|
77
|
+
this.emit('info', message, data);
|
|
78
|
+
},
|
|
79
|
+
notice(message, data) {
|
|
80
|
+
this.emit('notice', message, data);
|
|
81
|
+
},
|
|
82
|
+
warn(message, data) {
|
|
83
|
+
this.emit('warning', message, data);
|
|
84
|
+
},
|
|
85
|
+
error(message, data) {
|
|
86
|
+
this.emit('error', message, data);
|
|
87
|
+
},
|
|
88
|
+
critical(message, data) {
|
|
89
|
+
this.emit('critical', message, data);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -5,6 +5,11 @@ interface OpsTraceContext {
|
|
|
5
5
|
path?: string | undefined;
|
|
6
6
|
[key: string]: unknown;
|
|
7
7
|
}
|
|
8
|
+
export interface TraceContext {
|
|
9
|
+
traceparent: string;
|
|
10
|
+
tracestate?: string;
|
|
11
|
+
baggage?: string;
|
|
12
|
+
}
|
|
8
13
|
interface ToolMetrics {
|
|
9
14
|
calls: number;
|
|
10
15
|
errors: number;
|
|
@@ -19,8 +24,10 @@ export declare function getToolContextSnapshot(): {
|
|
|
19
24
|
tool: string;
|
|
20
25
|
path?: string;
|
|
21
26
|
} | undefined;
|
|
27
|
+
export declare function getTraceContext(): TraceContext | undefined;
|
|
22
28
|
export declare function startPerfMeasure(name: string, detail?: Record<string, unknown>): ((ok?: boolean) => void) | undefined;
|
|
23
29
|
export declare function withToolDiagnostics<T>(tool: string, run: () => Promise<T>, options?: {
|
|
24
30
|
path?: string;
|
|
31
|
+
traceContext?: TraceContext;
|
|
25
32
|
}): Promise<T>;
|
|
26
33
|
export {};
|
|
@@ -3,16 +3,18 @@ import { hash } from 'node:crypto';
|
|
|
3
3
|
import { channel, tracingChannel } from 'node:diagnostics_channel';
|
|
4
4
|
import { monitorEventLoopDelay, performance, PerformanceObserver, } from 'node:perf_hooks';
|
|
5
5
|
import { parseTrueEnvFlag } from './constants.js';
|
|
6
|
+
import { Logger } from './logger.js';
|
|
6
7
|
import { isRecord } from './utils.js';
|
|
7
8
|
// --- Configuration ---
|
|
8
9
|
const ENV = process.env;
|
|
9
10
|
let _cachedConfig;
|
|
10
11
|
function readConfig() {
|
|
11
|
-
|
|
12
|
+
_cachedConfig ??= {
|
|
12
13
|
enabled: parseTrueEnvFlag(ENV['FS_CONTEXT_DIAGNOSTICS']),
|
|
13
14
|
detail: parseDetail(ENV['FS_CONTEXT_DIAGNOSTICS_DETAIL']),
|
|
14
15
|
logToolErrors: parseTrueEnvFlag(ENV['FS_CONTEXT_TOOL_LOG_ERRORS']),
|
|
15
|
-
}
|
|
16
|
+
};
|
|
17
|
+
return _cachedConfig;
|
|
16
18
|
}
|
|
17
19
|
function parseDetail(val) {
|
|
18
20
|
if (val === '2')
|
|
@@ -201,6 +203,9 @@ function applyToolContext(context) {
|
|
|
201
203
|
export function getToolContextSnapshot() {
|
|
202
204
|
return toolContext.getStore();
|
|
203
205
|
}
|
|
206
|
+
export function getTraceContext() {
|
|
207
|
+
return toolContext.getStore()?.traceContext;
|
|
208
|
+
}
|
|
204
209
|
function normalizeContext(ctx) {
|
|
205
210
|
if (!ctx.path)
|
|
206
211
|
return ctx;
|
|
@@ -254,16 +259,20 @@ export function startPerfMeasure(name, detail) {
|
|
|
254
259
|
}
|
|
255
260
|
};
|
|
256
261
|
}
|
|
257
|
-
function publishToolStart(tool, pathVal) {
|
|
262
|
+
function publishToolStart(tool, pathVal, traceparent) {
|
|
258
263
|
const event = { phase: 'start', tool };
|
|
259
264
|
if (pathVal)
|
|
260
265
|
event.path = pathVal;
|
|
266
|
+
if (traceparent)
|
|
267
|
+
event.traceparent = traceparent;
|
|
261
268
|
CHANNELS.tool.publish(event);
|
|
262
269
|
}
|
|
263
|
-
function publishToolEnd(tool, ok, durationMs, errorMsg) {
|
|
270
|
+
function publishToolEnd(tool, ok, durationMs, errorMsg, traceparent) {
|
|
264
271
|
const event = { phase: 'end', tool, ok, durationMs };
|
|
265
272
|
if (errorMsg)
|
|
266
273
|
event.error = errorMsg;
|
|
274
|
+
if (traceparent)
|
|
275
|
+
event.traceparent = traceparent;
|
|
267
276
|
CHANNELS.tool.publish(event);
|
|
268
277
|
}
|
|
269
278
|
function publishPerfEnd(tool, durationMs, eluStart, loopMonitor) {
|
|
@@ -281,13 +290,13 @@ function publishPerfEnd(tool, durationMs, eluStart, loopMonitor) {
|
|
|
281
290
|
}
|
|
282
291
|
CHANNELS.perf.publish(event);
|
|
283
292
|
}
|
|
284
|
-
async function runAndObserve(tool, run, pubTool, pubPerf, logErrors, pathVal) {
|
|
293
|
+
async function runAndObserve(tool, run, pubTool, pubPerf, logErrors, pathVal, traceparent) {
|
|
285
294
|
const startMs = performance.now();
|
|
286
295
|
const eluStart = pubPerf ? performance.eventLoopUtilization() : undefined;
|
|
287
296
|
const loopMonitor = pubPerf ? monitorEventLoopDelay() : undefined;
|
|
288
297
|
loopMonitor?.enable();
|
|
289
298
|
if (pubTool)
|
|
290
|
-
publishToolStart(tool, pathVal);
|
|
299
|
+
publishToolStart(tool, pathVal, traceparent);
|
|
291
300
|
let result;
|
|
292
301
|
const obs = { ok: false, errorMsg: undefined };
|
|
293
302
|
try {
|
|
@@ -306,7 +315,7 @@ async function runAndObserve(tool, run, pubTool, pubPerf, logErrors, pathVal) {
|
|
|
306
315
|
if (pubPerf && eluStart)
|
|
307
316
|
publishPerfEnd(tool, durationMs, eluStart, loopMonitor);
|
|
308
317
|
if (pubTool)
|
|
309
|
-
publishToolEnd(tool, obs.ok, durationMs, obs.errorMsg);
|
|
318
|
+
publishToolEnd(tool, obs.ok, durationMs, obs.errorMsg, traceparent);
|
|
310
319
|
updateMetrics(tool, obs.ok, durationMs);
|
|
311
320
|
if (logErrors && !obs.ok)
|
|
312
321
|
logError(tool, durationMs, obs.errorMsg);
|
|
@@ -319,6 +328,7 @@ export async function withToolDiagnostics(tool, run, options) {
|
|
|
319
328
|
const context = {
|
|
320
329
|
tool,
|
|
321
330
|
...(options?.path ? { path: options.path } : {}),
|
|
331
|
+
...(options?.traceContext ? { traceContext: options.traceContext } : {}),
|
|
322
332
|
};
|
|
323
333
|
return toolContext.run(context, async () => {
|
|
324
334
|
if (!config.enabled) {
|
|
@@ -354,10 +364,10 @@ export async function withToolDiagnostics(tool, run, options) {
|
|
|
354
364
|
throw e;
|
|
355
365
|
}
|
|
356
366
|
}
|
|
357
|
-
return runAndObserve(tool, run, pubTool, pubPerf, config.logToolErrors, normalizedPath);
|
|
367
|
+
return runAndObserve(tool, run, pubTool, pubPerf, config.logToolErrors, normalizedPath, options?.traceContext?.traceparent);
|
|
358
368
|
});
|
|
359
369
|
}
|
|
360
370
|
function logError(tool, durationMs, msg) {
|
|
361
371
|
const suffix = msg ? `: ${msg}` : '';
|
|
362
|
-
|
|
372
|
+
Logger.error(`[ToolError] ${tool} failed in ${durationMs.toFixed(1)}ms${suffix}`);
|
|
363
373
|
}
|
package/dist/lib/paths.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
5
|
-
import {
|
|
2
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import { homedir, platform } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, join, normalize, parse, posix, relative, resolve, sep, win32, } from 'node:path';
|
|
6
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { assertNotAborted, withAbort } from './abort.js';
|
|
7
7
|
import { SENSITIVE_FILE_ALLOWLIST, SENSITIVE_FILE_DENYLIST, } from './constants.js';
|
|
8
8
|
import { ErrorCode, isAbortError, isNodeError, McpError } from './errors.js';
|
|
9
|
-
import {
|
|
9
|
+
import { Logger } from './logger.js';
|
|
10
10
|
const WINDOWS_PATH_SEPARATOR = '\\';
|
|
11
11
|
const POSIX_PATH_SEPARATOR = '/';
|
|
12
12
|
export function toPosixPath(value) {
|
|
@@ -20,7 +20,7 @@ const HOME_PREFIX_LENGTH = 2;
|
|
|
20
20
|
const CHAR_CODE_SPACE = 32;
|
|
21
21
|
const CHAR_CODE_DOT = 46;
|
|
22
22
|
function normalizePathForMatch(input) {
|
|
23
|
-
return toPosixPath(
|
|
23
|
+
return toPosixPath(normalize(input));
|
|
24
24
|
}
|
|
25
25
|
function normalizeForMatch(input) {
|
|
26
26
|
const normalized = normalizePathForMatch(input);
|
|
@@ -84,7 +84,7 @@ function matchesAnyGlobs(globs, candidates) {
|
|
|
84
84
|
return false;
|
|
85
85
|
for (const candidate of candidates) {
|
|
86
86
|
for (const glob of globs) {
|
|
87
|
-
if (
|
|
87
|
+
if (posix.matchesGlob(candidate, glob))
|
|
88
88
|
return true;
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -100,7 +100,7 @@ export function isSensitivePath(requestedPath, resolvedPath) {
|
|
|
100
100
|
? normalizeForMatch(resolvedPath)
|
|
101
101
|
: undefined;
|
|
102
102
|
const pathCandidates = uniquePair(normalizedRequested, normalizedResolved);
|
|
103
|
-
const nameCandidates = uniquePair(
|
|
103
|
+
const nameCandidates = uniquePair(posix.basename(normalizedRequested), normalizedResolved ? posix.basename(normalizedResolved) : undefined);
|
|
104
104
|
if (matchesAnyGlobs(ALLOW_PATTERNS.pathGlobs, pathCandidates) ||
|
|
105
105
|
matchesAnyGlobs(ALLOW_PATTERNS.nameGlobs, nameCandidates)) {
|
|
106
106
|
return false;
|
|
@@ -111,11 +111,11 @@ export function isSensitivePath(requestedPath, resolvedPath) {
|
|
|
111
111
|
export function assertAllowedFileAccess(requestedPath, resolvedPath) {
|
|
112
112
|
if (!isSensitivePath(requestedPath, resolvedPath))
|
|
113
113
|
return;
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
Logger.warn(`Access denied: sensitive file blocked by policy (${requestedPath})`);
|
|
115
|
+
throw new McpError(ErrorCode.ACCESS_DENIED, 'Sensitive file blocked. Set FS_CONTEXT_ALLOW_SENSITIVE=1 to override.', requestedPath);
|
|
116
116
|
}
|
|
117
|
-
const HOMEDIR =
|
|
118
|
-
const PATH_SEPARATOR =
|
|
117
|
+
const HOMEDIR = homedir();
|
|
118
|
+
const PATH_SEPARATOR = sep;
|
|
119
119
|
const DRIVE_LETTER_REGEX = /^[A-Za-z]:/;
|
|
120
120
|
const WINDOWS_DRIVE_REL_REGEX = /^[A-Za-z]:$/u;
|
|
121
121
|
const LEADING_SEPARATORS_RE = /^[/\\]+/;
|
|
@@ -160,11 +160,11 @@ function expandHome(filepath) {
|
|
|
160
160
|
return HOMEDIR;
|
|
161
161
|
// Accept both "~/" and "~\\" for cross-platform UX.
|
|
162
162
|
if (filepath.startsWith('~/') || filepath.startsWith('~\\')) {
|
|
163
|
-
// Avoid `
|
|
163
|
+
// Avoid `join(HOMEDIR, "/foo")` resetting to the filesystem root.
|
|
164
164
|
const rest = filepath
|
|
165
165
|
.slice(HOME_PREFIX_LENGTH)
|
|
166
166
|
.replace(LEADING_SEPARATORS_RE, '');
|
|
167
|
-
return rest.length === 0 ? HOMEDIR :
|
|
167
|
+
return rest.length === 0 ? HOMEDIR : join(HOMEDIR, rest);
|
|
168
168
|
}
|
|
169
169
|
return filepath;
|
|
170
170
|
}
|
|
@@ -175,7 +175,7 @@ function expandHome(filepath) {
|
|
|
175
175
|
* - Lowercases Windows drive letter for stable comparisons.
|
|
176
176
|
*/
|
|
177
177
|
export function normalizePath(p) {
|
|
178
|
-
const resolved =
|
|
178
|
+
const resolved = resolve(expandHome(p));
|
|
179
179
|
if (IS_WINDOWS && DRIVE_LETTER_REGEX.test(resolved)) {
|
|
180
180
|
return resolved.charAt(0).toLowerCase() + resolved.slice(1);
|
|
181
181
|
}
|
|
@@ -194,8 +194,8 @@ function rethrowIfAborted(error) {
|
|
|
194
194
|
function isSamePath(left, right) {
|
|
195
195
|
if (left === right)
|
|
196
196
|
return true;
|
|
197
|
-
const leftResolved = normalizeCaseForComparison(
|
|
198
|
-
const rightResolved = normalizeCaseForComparison(
|
|
197
|
+
const leftResolved = normalizeCaseForComparison(resolve(left));
|
|
198
|
+
const rightResolved = normalizeCaseForComparison(resolve(right));
|
|
199
199
|
return leftResolved === rightResolved;
|
|
200
200
|
}
|
|
201
201
|
function stripTrailingSeparator(normalized) {
|
|
@@ -211,7 +211,7 @@ function normalizeAllowedDirectory(dir) {
|
|
|
211
211
|
if (trimmed.length === 0)
|
|
212
212
|
return '';
|
|
213
213
|
const normalized = normalizePath(trimmed);
|
|
214
|
-
const { root } =
|
|
214
|
+
const { root } = parse(normalized);
|
|
215
215
|
// Keep filesystem roots as-is ("/", "c:\\", "\\\\server\\share\\").
|
|
216
216
|
if (isFileSystemRootPath(normalized, root)) {
|
|
217
217
|
return root;
|
|
@@ -274,14 +274,12 @@ function isPathInsideDirectory(normalizedDirectory, normalizedCandidate) {
|
|
|
274
274
|
const candidate = normalizeForComparison(normalizedCandidate);
|
|
275
275
|
if (root === candidate)
|
|
276
276
|
return true;
|
|
277
|
-
const
|
|
278
|
-
if (
|
|
277
|
+
const rel = relative(root, candidate);
|
|
278
|
+
if (rel.length === 0)
|
|
279
279
|
return true;
|
|
280
|
-
if (
|
|
280
|
+
if (rel === '..')
|
|
281
281
|
return false;
|
|
282
|
-
return
|
|
283
|
-
!relative.startsWith('../') &&
|
|
284
|
-
!path.isAbsolute(relative));
|
|
282
|
+
return !rel.startsWith('..\\') && !rel.startsWith('../') && !isAbsolute(rel);
|
|
285
283
|
}
|
|
286
284
|
export function isPathWithinDirectories(normalizedPath, allowedDirs) {
|
|
287
285
|
for (const allowedDir of allowedDirs) {
|
|
@@ -293,7 +291,7 @@ export function isPathWithinDirectories(normalizedPath, allowedDirs) {
|
|
|
293
291
|
async function resolveRealPath(normalized, signal) {
|
|
294
292
|
try {
|
|
295
293
|
assertNotAborted(signal);
|
|
296
|
-
const realPath = await withAbort(
|
|
294
|
+
const realPath = await withAbort(realpath(normalized), signal);
|
|
297
295
|
return normalizeAllowedDirectory(realPath);
|
|
298
296
|
}
|
|
299
297
|
catch (error) {
|
|
@@ -327,12 +325,12 @@ export async function setAllowedDirectoriesResolved(dirs, signal) {
|
|
|
327
325
|
}
|
|
328
326
|
function ensureNonEmptyPath(requestedPath) {
|
|
329
327
|
if (!requestedPath || requestedPath.trim().length === 0) {
|
|
330
|
-
throw new McpError(ErrorCode.
|
|
328
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Path cannot be empty or whitespace', requestedPath);
|
|
331
329
|
}
|
|
332
330
|
}
|
|
333
331
|
function ensureNoNullBytes(requestedPath) {
|
|
334
332
|
if (requestedPath.includes('\0')) {
|
|
335
|
-
throw new McpError(ErrorCode.
|
|
333
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Path contains null bytes', requestedPath);
|
|
336
334
|
}
|
|
337
335
|
}
|
|
338
336
|
function getReservedDeviceName(segment) {
|
|
@@ -371,31 +369,31 @@ function ensureNoReservedWindowsNames(requestedPath) {
|
|
|
371
369
|
const reserved = getReservedDeviceNameForPath(requestedPath);
|
|
372
370
|
if (!reserved)
|
|
373
371
|
return;
|
|
374
|
-
throw new McpError(ErrorCode.
|
|
372
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `Windows reserved device name not allowed: ${reserved}`, requestedPath);
|
|
375
373
|
}
|
|
376
374
|
export function isWindowsDriveRelativePath(requestedPath) {
|
|
377
375
|
if (!IS_WINDOWS)
|
|
378
376
|
return false;
|
|
379
|
-
const parsed =
|
|
377
|
+
const parsed = win32.parse(requestedPath);
|
|
380
378
|
if (!WINDOWS_DRIVE_REL_REGEX.test(parsed.root))
|
|
381
379
|
return false;
|
|
382
|
-
return !
|
|
380
|
+
return !win32.isAbsolute(requestedPath);
|
|
383
381
|
}
|
|
384
382
|
function ensureNoWindowsDriveRelativePath(requestedPath) {
|
|
385
383
|
if (!isWindowsDriveRelativePath(requestedPath))
|
|
386
384
|
return;
|
|
387
|
-
throw new McpError(ErrorCode.
|
|
385
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Drive-relative path not allowed. Use C:\\path instead of C:path.', requestedPath);
|
|
388
386
|
}
|
|
389
387
|
function resolveRequestedPath(requestedPath) {
|
|
390
388
|
const expanded = expandHome(requestedPath);
|
|
391
|
-
if (!
|
|
389
|
+
if (!isAbsolute(expanded)) {
|
|
392
390
|
const roots = getAllowedDirectoriesForRelativeResolution();
|
|
393
391
|
if (roots.length > 1) {
|
|
394
|
-
throw new McpError(ErrorCode.
|
|
392
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Ambiguous relative path with multiple roots. Use an absolute path.', requestedPath);
|
|
395
393
|
}
|
|
396
394
|
const baseDir = roots[0];
|
|
397
395
|
if (baseDir) {
|
|
398
|
-
return normalizePath(
|
|
396
|
+
return normalizePath(resolve(baseDir, expanded));
|
|
399
397
|
}
|
|
400
398
|
}
|
|
401
399
|
return normalizePath(expanded);
|
|
@@ -409,24 +407,24 @@ function validateRequestedPath(requestedPath) {
|
|
|
409
407
|
}
|
|
410
408
|
const NODE_ERROR_MAP = {
|
|
411
409
|
ENOENT: {
|
|
412
|
-
code: ErrorCode.
|
|
413
|
-
message: (
|
|
410
|
+
code: ErrorCode.NOT_FOUND,
|
|
411
|
+
message: () => 'Path does not exist',
|
|
414
412
|
},
|
|
415
413
|
EACCES: {
|
|
416
|
-
code: ErrorCode.
|
|
417
|
-
message: (
|
|
414
|
+
code: ErrorCode.PERMISSION_DENIED,
|
|
415
|
+
message: () => 'Permission denied',
|
|
418
416
|
},
|
|
419
417
|
EPERM: {
|
|
420
|
-
code: ErrorCode.
|
|
421
|
-
message: (
|
|
418
|
+
code: ErrorCode.PERMISSION_DENIED,
|
|
419
|
+
message: () => 'Permission denied',
|
|
422
420
|
},
|
|
423
421
|
ELOOP: {
|
|
424
|
-
code: ErrorCode.
|
|
425
|
-
message: (
|
|
422
|
+
code: ErrorCode.SYMLINK_NOT_ALLOWED,
|
|
423
|
+
message: () => 'Too many symbolic links (circular reference)',
|
|
426
424
|
},
|
|
427
425
|
ENAMETOOLONG: {
|
|
428
|
-
code: ErrorCode.
|
|
429
|
-
message: (
|
|
426
|
+
code: ErrorCode.INVALID_INPUT,
|
|
427
|
+
message: () => 'Path name too long',
|
|
430
428
|
},
|
|
431
429
|
};
|
|
432
430
|
function buildAllowedDirectoriesHint() {
|
|
@@ -448,26 +446,28 @@ function toMcpError(requestedPath, error) {
|
|
|
448
446
|
else if (typeof error === 'string') {
|
|
449
447
|
originalMessage = error;
|
|
450
448
|
}
|
|
451
|
-
return new McpError(ErrorCode.
|
|
449
|
+
return new McpError(ErrorCode.NOT_FOUND, 'Path is not accessible', requestedPath, { originalCode: code, originalMessage }, error);
|
|
452
450
|
}
|
|
453
451
|
function toAccessDeniedWithHint(requestedPath, resolvedPath, normalizedResolved) {
|
|
454
452
|
const suggestion = buildAllowedDirectoriesHint();
|
|
455
|
-
return new McpError(ErrorCode.
|
|
453
|
+
return new McpError(ErrorCode.ACCESS_DENIED, `Outside allowed directories. ${suggestion}`, requestedPath, { resolvedPath, normalizedResolvedPath: normalizedResolved });
|
|
456
454
|
}
|
|
457
455
|
function ensureWithinAllowedDirectories(options) {
|
|
458
456
|
const { normalizedPath, requestedPath, allowedDirs, details } = options;
|
|
459
457
|
if (isPathWithinDirectories(normalizedPath, allowedDirs))
|
|
460
458
|
return;
|
|
461
459
|
if (allowedDirs.length === 0) {
|
|
462
|
-
|
|
460
|
+
Logger.warn('Access denied: no allowed directories configured');
|
|
461
|
+
throw new McpError(ErrorCode.ACCESS_DENIED, 'No allowed directories configured. Use --allow-cwd or configure roots.', requestedPath, details);
|
|
463
462
|
}
|
|
464
|
-
|
|
463
|
+
Logger.warn(`Access denied: path outside allowed directories (${requestedPath})`);
|
|
464
|
+
throw new McpError(ErrorCode.ACCESS_DENIED, 'Outside allowed directories', requestedPath, details);
|
|
465
465
|
}
|
|
466
466
|
async function resolveRealPathOrThrow(options) {
|
|
467
467
|
const { requestedPath, normalizedRequested, signal } = options;
|
|
468
468
|
try {
|
|
469
469
|
assertNotAborted(signal);
|
|
470
|
-
return await withAbort(
|
|
470
|
+
return await withAbort(realpath(normalizedRequested), signal);
|
|
471
471
|
}
|
|
472
472
|
catch (error) {
|
|
473
473
|
rethrowIfAborted(error);
|
|
@@ -494,7 +494,7 @@ function ensureResolvedPathAllowed(options) {
|
|
|
494
494
|
async function statPathOrThrow(requestedPath, resolvedPath, signal) {
|
|
495
495
|
try {
|
|
496
496
|
assertNotAborted(signal);
|
|
497
|
-
return await withAbort(
|
|
497
|
+
return await withAbort(stat(resolvedPath), signal);
|
|
498
498
|
}
|
|
499
499
|
catch (error) {
|
|
500
500
|
rethrowIfAborted(error);
|
|
@@ -507,7 +507,7 @@ async function resolveNearestExistingRealPathOrThrow(options) {
|
|
|
507
507
|
for (;;) {
|
|
508
508
|
try {
|
|
509
509
|
assertNotAborted(signal);
|
|
510
|
-
return await withAbort(
|
|
510
|
+
return await withAbort(realpath(current), signal);
|
|
511
511
|
}
|
|
512
512
|
catch (error) {
|
|
513
513
|
rethrowIfAborted(error);
|
|
@@ -515,7 +515,7 @@ async function resolveNearestExistingRealPathOrThrow(options) {
|
|
|
515
515
|
if (code !== 'ENOENT') {
|
|
516
516
|
throw toMcpError(requestedPath, error);
|
|
517
517
|
}
|
|
518
|
-
const parent =
|
|
518
|
+
const parent = dirname(current);
|
|
519
519
|
if (parent === current) {
|
|
520
520
|
throw toMcpError(requestedPath, error);
|
|
521
521
|
}
|
|
@@ -554,7 +554,7 @@ export async function validateExistingDirectory(requestedPath, signal) {
|
|
|
554
554
|
const details = await validateExistingPathDetailsInternal(requestedPath, signal);
|
|
555
555
|
const stats = await statPathOrThrow(requestedPath, details.resolvedPath, signal);
|
|
556
556
|
if (!stats.isDirectory()) {
|
|
557
|
-
throw new McpError(ErrorCode.
|
|
557
|
+
throw new McpError(ErrorCode.NOT_DIRECTORY, 'Not a directory', requestedPath);
|
|
558
558
|
}
|
|
559
559
|
return details.resolvedPath;
|
|
560
560
|
}
|
|
@@ -581,7 +581,7 @@ function isFileRoot(root) {
|
|
|
581
581
|
async function maybeAddRealPath(normalizedPath, validDirs, signal) {
|
|
582
582
|
try {
|
|
583
583
|
assertNotAborted(signal);
|
|
584
|
-
const realPath = await withAbort(
|
|
584
|
+
const realPath = await withAbort(realpath(normalizedPath), signal);
|
|
585
585
|
const normalizedReal = normalizePath(realPath);
|
|
586
586
|
if (!isSamePath(normalizedReal, normalizedPath)) {
|
|
587
587
|
validDirs.push(normalizedReal);
|
|
@@ -596,7 +596,7 @@ async function resolveRootDirectory(root, signal) {
|
|
|
596
596
|
const dirPath = fileURLToPath(root.uri);
|
|
597
597
|
const normalizedPath = normalizePath(dirPath);
|
|
598
598
|
assertNotAborted(signal);
|
|
599
|
-
const stats = await withAbort(
|
|
599
|
+
const stats = await withAbort(stat(normalizedPath), signal);
|
|
600
600
|
if (!stats.isDirectory())
|
|
601
601
|
return null;
|
|
602
602
|
return normalizedPath;
|
|
@@ -94,7 +94,7 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
94
94
|
bytes: entryBytes,
|
|
95
95
|
reason: 'entry_too_large',
|
|
96
96
|
});
|
|
97
|
-
throw new McpError(ErrorCode.
|
|
97
|
+
throw new McpError(ErrorCode.TOO_LARGE, `Resource too large to cache (${entryBytes} bytes).`);
|
|
98
98
|
}
|
|
99
99
|
const contentHash = computeSha256(params.text);
|
|
100
100
|
const indexKey = buildIndexKey(mimeType, contentHash);
|
|
@@ -146,7 +146,7 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
146
146
|
bytes: entry.size,
|
|
147
147
|
reason: 'evicted_immediately',
|
|
148
148
|
});
|
|
149
|
-
throw new McpError(ErrorCode.
|
|
149
|
+
throw new McpError(ErrorCode.TOO_LARGE, 'Cache full: entry evicted.');
|
|
150
150
|
}
|
|
151
151
|
return entry;
|
|
152
152
|
}
|
|
@@ -158,7 +158,7 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
158
158
|
uri,
|
|
159
159
|
reason: 'not_found',
|
|
160
160
|
});
|
|
161
|
-
throw new McpError(ErrorCode.
|
|
161
|
+
throw new McpError(ErrorCode.NOT_FOUND, `Resource not found: ${uri}. Re-run the tool to regenerate.`);
|
|
162
162
|
}
|
|
163
163
|
if (isExpired(existing)) {
|
|
164
164
|
removeEntry(uri, 'expired');
|
|
@@ -167,7 +167,7 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
167
167
|
uri,
|
|
168
168
|
reason: 'expired',
|
|
169
169
|
});
|
|
170
|
-
throw new McpError(ErrorCode.
|
|
170
|
+
throw new McpError(ErrorCode.NOT_FOUND, `Resource expired: ${uri}. Re-run the tool to regenerate.`);
|
|
171
171
|
}
|
|
172
172
|
publishResourceStoreDiagnostics({
|
|
173
173
|
phase: 'cache_hit',
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -5,15 +5,3 @@ export declare function debounce<Args extends unknown[]>(func: (...args: Args) =
|
|
|
5
5
|
};
|
|
6
6
|
export declare function mergeOptions<T extends object>(defaults: T, overrides: Partial<T>): T;
|
|
7
7
|
export declare function omitOptionKeys<T extends object, K extends keyof T>(input: T, keys: readonly K[]): Omit<T, K>;
|
|
8
|
-
interface ProgressPayload {
|
|
9
|
-
current: number;
|
|
10
|
-
total?: number;
|
|
11
|
-
}
|
|
12
|
-
type ProgressCallback = ((progress: ProgressPayload) => void) | undefined;
|
|
13
|
-
interface PeriodicProgressOptions {
|
|
14
|
-
total?: number;
|
|
15
|
-
throttleModulo?: number;
|
|
16
|
-
force?: boolean;
|
|
17
|
-
}
|
|
18
|
-
export declare function reportPeriodicProgress(onProgress: ProgressCallback, current: number, options?: PeriodicProgressOptions): void;
|
|
19
|
-
export {};
|
package/dist/lib/utils.js
CHANGED
|
@@ -35,16 +35,3 @@ export function omitOptionKeys(input, keys) {
|
|
|
35
35
|
}
|
|
36
36
|
return output;
|
|
37
37
|
}
|
|
38
|
-
export function reportPeriodicProgress(onProgress, current, options = {}) {
|
|
39
|
-
if (!onProgress || current === 0)
|
|
40
|
-
return;
|
|
41
|
-
const throttleModulo = options.throttleModulo ?? 1;
|
|
42
|
-
const force = options.force ?? false;
|
|
43
|
-
if (!force && throttleModulo > 1 && current % throttleModulo !== 0) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
onProgress({
|
|
47
|
-
current,
|
|
48
|
-
...(options.total !== undefined ? { total: options.total } : {}),
|
|
49
|
-
});
|
|
50
|
-
}
|