@j0hanz/filesystem-mcp 1.7.3 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +635 -377
- package/dist/cli.js +2 -2
- package/dist/completions.js +2 -3
- package/dist/index.js +2 -2
- package/dist/lib/file-operations/{common.d.ts → core.d.ts} +6 -0
- package/dist/lib/file-operations/{common.js → core.js} +45 -0
- package/dist/lib/file-operations/metadata.d.ts +73 -0
- package/dist/lib/file-operations/metadata.js +889 -0
- package/dist/lib/file-operations/{search-content.d.ts → search.d.ts} +29 -3
- package/dist/lib/file-operations/{search-content.js → search.js} +409 -82
- package/dist/lib/file-operations/{glob-engine.d.ts → traversal.d.ts} +18 -1
- package/dist/lib/file-operations/{glob-engine.js → traversal.js} +25 -2
- package/dist/lib/fs-helpers.d.ts +1 -0
- package/dist/lib/fs-helpers.js +10 -2
- package/dist/lib/observability.js +1 -1
- package/dist/lib/{path-validation.d.ts → paths.d.ts} +4 -0
- package/dist/lib/{path-validation.js → paths.js} +112 -1
- package/dist/lib/utils.d.ts +15 -0
- package/dist/lib/utils.js +33 -0
- package/dist/resources/generated-instructions.js +6 -6
- package/dist/resources/tool-catalog.js +9 -9
- package/dist/resources/tool-info.js +3 -3
- package/dist/resources/workflows.js +10 -23
- package/dist/schemas.js +15 -15
- package/dist/server/bootstrap.d.ts +19 -1
- package/dist/server/bootstrap.js +103 -9
- package/dist/server/roots-manager.d.ts +2 -2
- package/dist/server/roots-manager.js +3 -3
- package/dist/tools/apply-patch.js +7 -3
- package/dist/tools/calculate-hash.js +15 -15
- package/dist/tools/create-directory.js +1 -1
- package/dist/tools/delete-file.js +7 -4
- package/dist/tools/diff-files.js +2 -2
- package/dist/tools/edit-file.js +14 -13
- package/dist/tools/list-directory.js +5 -7
- package/dist/tools/move-file.js +2 -1
- package/dist/tools/read-multiple.js +13 -26
- package/dist/tools/read.js +3 -3
- package/dist/tools/replace-in-files.js +17 -20
- package/dist/tools/roots.js +4 -6
- package/dist/tools/search-content.js +9 -11
- package/dist/tools/search-files.js +4 -6
- package/dist/tools/shared.d.ts +18 -1
- package/dist/tools/shared.js +39 -19
- package/dist/tools/stat-many.js +14 -24
- package/dist/tools/stat.js +4 -3
- package/dist/tools/task-support.js +1 -1
- package/dist/tools/tree.js +3 -4
- package/dist/tools/write-file.js +1 -1
- package/package.json +9 -5
- package/dist/lib/file-operations/file-info.d.ts +0 -10
- package/dist/lib/file-operations/file-info.js +0 -143
- package/dist/lib/file-operations/gitignore.d.ts +0 -6
- package/dist/lib/file-operations/gitignore.js +0 -45
- package/dist/lib/file-operations/list-directory.d.ts +0 -14
- package/dist/lib/file-operations/list-directory.js +0 -256
- package/dist/lib/file-operations/read-multiple-files.d.ts +0 -25
- package/dist/lib/file-operations/read-multiple-files.js +0 -252
- package/dist/lib/file-operations/search-files.d.ts +0 -27
- package/dist/lib/file-operations/search-files.js +0 -218
- package/dist/lib/file-operations/search-worker.d.ts +0 -2
- package/dist/lib/file-operations/search-worker.js +0 -129
- package/dist/lib/file-operations/tree.d.ts +0 -28
- package/dist/lib/file-operations/tree.js +0 -269
- package/dist/lib/path-format.d.ts +0 -1
- package/dist/lib/path-format.js +0 -7
- package/dist/lib/path-policy.d.ts +0 -2
- package/dist/lib/path-policy.js +0 -100
- package/dist/lib/type-guards.d.ts +0 -1
- package/dist/lib/type-guards.js +0 -3
- package/dist/server/capabilities.d.ts +0 -10
- package/dist/server/capabilities.js +0 -48
- package/dist/server/logging.d.ts +0 -7
- package/dist/server/logging.js +0 -41
- package/dist/server/types.d.ts +0 -4
- package/dist/server/types.js +0 -1
package/dist/server/bootstrap.js
CHANGED
|
@@ -6,19 +6,104 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
7
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
8
|
import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
-
import { registerCompletions } from '../completions.js';
|
|
10
9
|
import { DEFAULT_LOG_LEVEL, parseEnvInt, REQUIRED_MCP_PROTOCOL_VERSION, } from '../lib/constants.js';
|
|
11
10
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
12
11
|
import { createInMemoryResourceStore } from '../lib/resource-store.js';
|
|
12
|
+
import { isRecord } from '../lib/utils.js';
|
|
13
|
+
import { registerCompletions } from '../completions.js';
|
|
13
14
|
import { pkgInfo } from '../pkg-info.js';
|
|
14
15
|
import { registerGetHelpPrompt } from '../prompts.js';
|
|
15
16
|
import { registerInstructionResource, registerMetricsResource, registerResultResources, registerToolCatalogResource, registerToolInfoResource, registerWorkflowGuideResource, } from '../resources.js';
|
|
16
17
|
import { buildServerInstructions } from '../resources/generated-instructions.js';
|
|
17
18
|
import { registerAllTools } from '../tools.js';
|
|
18
19
|
import { withDefaultIcons } from '../tools/shared.js';
|
|
19
|
-
import { buildServerCapabilities, supportsTaskToolRequests, } from './capabilities.js';
|
|
20
|
-
import { createLoggingState } from './logging.js';
|
|
21
20
|
import { RootsManager } from './roots-manager.js';
|
|
21
|
+
let cachedTaskToolSupport;
|
|
22
|
+
function detectTaskToolSupport() {
|
|
23
|
+
if (cachedTaskToolSupport !== undefined) {
|
|
24
|
+
return cachedTaskToolSupport;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
// Instantiate a minimal, unconnected probe server to duck-type check for
|
|
28
|
+
// task tool support. The probe has no transport or active connections, so
|
|
29
|
+
// close() only releases in-memory state; fire-and-forget is safe here.
|
|
30
|
+
const probe = new McpServer({
|
|
31
|
+
name: 'filesystem-mcp-capability-probe',
|
|
32
|
+
version: '0.0.0',
|
|
33
|
+
}, { capabilities: { tools: {} } });
|
|
34
|
+
cachedTaskToolSupport =
|
|
35
|
+
typeof probe.experimental.tasks.registerToolTask === 'function';
|
|
36
|
+
probe.close().catch(() => { });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
cachedTaskToolSupport = false;
|
|
40
|
+
}
|
|
41
|
+
return cachedTaskToolSupport;
|
|
42
|
+
}
|
|
43
|
+
export function buildServerCapabilities(options = {}) {
|
|
44
|
+
const capabilities = {
|
|
45
|
+
logging: {},
|
|
46
|
+
resources: {},
|
|
47
|
+
tools: {},
|
|
48
|
+
prompts: options.enablePromptListChanged ? { listChanged: true } : {},
|
|
49
|
+
completions: {},
|
|
50
|
+
};
|
|
51
|
+
if (options.enableTaskToolRequests) {
|
|
52
|
+
// NOTE: enabling task tool requests requires the caller to configure
|
|
53
|
+
// an InMemoryTaskStore and InMemoryTaskMessageQueue on the McpServer.
|
|
54
|
+
// InMemoryTaskStore accumulates completed task records with no TTL eviction —
|
|
55
|
+
// suitable for short-lived stdio sessions. Long-running HTTP servers should
|
|
56
|
+
// replace it with a TTL-evicting store to avoid unbounded memory growth.
|
|
57
|
+
capabilities.tasks = {
|
|
58
|
+
list: {},
|
|
59
|
+
cancel: {},
|
|
60
|
+
requests: { tools: { call: {} } },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return capabilities;
|
|
64
|
+
}
|
|
65
|
+
export function supportsTaskToolRequests() {
|
|
66
|
+
return detectTaskToolSupport();
|
|
67
|
+
}
|
|
68
|
+
const MCP_LOGGER_NAME = 'filesystem-mcp';
|
|
69
|
+
const LOG_LEVEL_ORDER = {
|
|
70
|
+
debug: 0,
|
|
71
|
+
info: 1,
|
|
72
|
+
notice: 2,
|
|
73
|
+
warning: 3,
|
|
74
|
+
error: 4,
|
|
75
|
+
critical: 5,
|
|
76
|
+
alert: 6,
|
|
77
|
+
emergency: 7,
|
|
78
|
+
};
|
|
79
|
+
export function createLoggingState(minimumLevel = 'debug') {
|
|
80
|
+
return { minimumLevel };
|
|
81
|
+
}
|
|
82
|
+
function canSendMcpLogs(server) {
|
|
83
|
+
const capabilities = server.server.getClientCapabilities();
|
|
84
|
+
if (!isRecord(capabilities))
|
|
85
|
+
return false;
|
|
86
|
+
if (!('logging' in capabilities))
|
|
87
|
+
return false;
|
|
88
|
+
return !!capabilities['logging'];
|
|
89
|
+
}
|
|
90
|
+
export function logToMcp(server, level, data, minLevel = 'debug') {
|
|
91
|
+
if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[minLevel]) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!server || !canSendMcpLogs(server)) {
|
|
95
|
+
console.error(data);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const params = {
|
|
99
|
+
level,
|
|
100
|
+
logger: MCP_LOGGER_NAME,
|
|
101
|
+
data,
|
|
102
|
+
};
|
|
103
|
+
void server.sendLoggingMessage(params).catch((error) => {
|
|
104
|
+
console.error(`Failed to send MCP log: ${level} | ${data}`, formatUnknownErrorMessage(error));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
22
107
|
const { version: SERVER_VERSION, description: SERVER_DESCRIPTION, homepage: SERVER_HOMEPAGE, } = pkgInfo;
|
|
23
108
|
const rootsManagers = new WeakMap();
|
|
24
109
|
function getRootsManager(server) {
|
|
@@ -242,9 +327,11 @@ export async function startHttpServer(port, options) {
|
|
|
242
327
|
if (typeof authHeader === 'string' &&
|
|
243
328
|
authHeader.startsWith(bearerPrefix)) {
|
|
244
329
|
const userKey = authHeader.slice(bearerPrefix.length);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
330
|
+
if (userKey.length <= 4096) {
|
|
331
|
+
const expectedHash = createHash('sha256').update(apiKey).digest();
|
|
332
|
+
const actualHash = createHash('sha256').update(userKey).digest();
|
|
333
|
+
authorized = timingSafeEqual(expectedHash, actualHash);
|
|
334
|
+
}
|
|
248
335
|
}
|
|
249
336
|
if (!authorized) {
|
|
250
337
|
res.writeHead(401, {
|
|
@@ -283,7 +370,7 @@ export async function startHttpServer(port, options) {
|
|
|
283
370
|
}
|
|
284
371
|
const body = await readRequestBody(req);
|
|
285
372
|
if (isInitializeRequest(body)) {
|
|
286
|
-
const maxSessions =
|
|
373
|
+
const maxSessions = parseEnvInt('FILESYSTEM_MCP_MAX_HTTP_SESSIONS', 100, 1, 10_000);
|
|
287
374
|
if (sessions.size >= maxSessions) {
|
|
288
375
|
sendJsonRpcError(res, 503, -32000, 'Too many sessions');
|
|
289
376
|
return;
|
|
@@ -316,8 +403,15 @@ export async function startHttpServer(port, options) {
|
|
|
316
403
|
}
|
|
317
404
|
}
|
|
318
405
|
else {
|
|
319
|
-
res.writeHead(405, {
|
|
320
|
-
|
|
406
|
+
res.writeHead(405, {
|
|
407
|
+
Allow: 'GET, POST, DELETE',
|
|
408
|
+
'Content-Type': 'application/json',
|
|
409
|
+
});
|
|
410
|
+
res.end(JSON.stringify({
|
|
411
|
+
jsonrpc: '2.0',
|
|
412
|
+
error: { code: -32000, message: 'Method Not Allowed' },
|
|
413
|
+
id: null,
|
|
414
|
+
}));
|
|
321
415
|
}
|
|
322
416
|
}
|
|
323
417
|
catch (error) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { type LoggingState } from './
|
|
3
|
-
import type { ServerOptions } from './
|
|
2
|
+
import { type LoggingState } from './bootstrap.js';
|
|
3
|
+
import type { ServerOptions } from './bootstrap.js';
|
|
4
4
|
export declare class RootsManager {
|
|
5
5
|
private rootsUpdateTimeout;
|
|
6
6
|
private rootDirectories;
|
|
@@ -3,9 +3,9 @@ import { InitializedNotificationSchema, RootsListChangedNotificationSchema, } fr
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
5
5
|
import { assertNotAborted, createTimedAbortSignal, withAbort, } from '../lib/fs-helpers.js';
|
|
6
|
-
import { getAllowedDirectories, getValidRootDirectories, isPathWithinDirectories, normalizePath, setAllowedDirectoriesResolved, } from '../lib/
|
|
7
|
-
import { isRecord } from '../lib/
|
|
8
|
-
import { logToMcp } from './
|
|
6
|
+
import { getAllowedDirectories, getValidRootDirectories, isPathWithinDirectories, normalizePath, setAllowedDirectoriesResolved, } from '../lib/paths.js';
|
|
7
|
+
import { isRecord } from '../lib/utils.js';
|
|
8
|
+
import { logToMcp } from './bootstrap.js';
|
|
9
9
|
const ROOTS_TIMEOUT_MS = 5000;
|
|
10
10
|
const ROOTS_DEBOUNCE_MS = 100;
|
|
11
11
|
function normalizeCLIDirectories(dirs) {
|
|
@@ -4,7 +4,7 @@ import { applyPatch } from 'diff';
|
|
|
4
4
|
import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
5
5
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
6
6
|
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
7
|
-
import { validateExistingPath } from '../lib/
|
|
7
|
+
import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
|
|
8
8
|
import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
|
|
9
9
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
10
10
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
@@ -12,8 +12,8 @@ export const APPLY_PATCH_TOOL = {
|
|
|
12
12
|
name: 'apply_patch',
|
|
13
13
|
title: 'Apply Patch',
|
|
14
14
|
description: 'Apply a unified diff patch to a file. ' +
|
|
15
|
-
'
|
|
16
|
-
'On failure, regenerate
|
|
15
|
+
'Workflow: `diff_files` \u2192 `apply_patch(dryRun:true)` \u2192 `apply_patch`. ' +
|
|
16
|
+
'On failure, regenerate the patch from current file content.',
|
|
17
17
|
inputSchema: ApplyPatchInputSchema,
|
|
18
18
|
outputSchema: ApplyPatchOutputSchema,
|
|
19
19
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
@@ -36,6 +36,7 @@ function assertPatchHasHunks(patch) {
|
|
|
36
36
|
async function handleApplyPatch(args, signal) {
|
|
37
37
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
38
38
|
const validPath = await validateExistingPath(args.path, signal);
|
|
39
|
+
assertAllowedFileAccess(args.path, validPath);
|
|
39
40
|
const stats = await withAbort(fs.stat(validPath), signal);
|
|
40
41
|
assertPatchTargetSizeWithinLimit(validPath, stats.size, maxFileSize);
|
|
41
42
|
const content = await fs.readFile(validPath, { encoding: 'utf-8', signal });
|
|
@@ -48,6 +49,9 @@ async function handleApplyPatch(args, signal) {
|
|
|
48
49
|
if (patched === false) {
|
|
49
50
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch application failed. The file content may have changed or patch context is insufficient. Generate a fresh patch via diff_files against the current file, then retry. If differences are minor, enable fuzzy matching with the fuzzFactor parameter.');
|
|
50
51
|
}
|
|
52
|
+
if (patched === content) {
|
|
53
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch had no effect — the file content is unchanged after applying. The patch may not match the current file content. Generate a fresh patch via diff_files and retry.');
|
|
54
|
+
}
|
|
51
55
|
if (args.dryRun) {
|
|
52
56
|
return buildToolResponse('Dry run successful. Patch can be applied.', {
|
|
53
57
|
ok: true,
|
|
@@ -4,12 +4,13 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { createReadStream } from 'node:fs';
|
|
5
5
|
import { PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
6
6
|
import { ErrorCode } from '../lib/errors.js';
|
|
7
|
-
import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations/
|
|
8
|
-
import { globEntries } from '../lib/file-operations/
|
|
7
|
+
import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations/core.js';
|
|
8
|
+
import { globEntries } from '../lib/file-operations/traversal.js';
|
|
9
9
|
import { assertNotAborted, withAbort } from '../lib/fs-helpers.js';
|
|
10
|
-
import { validateExistingPath } from '../lib/
|
|
10
|
+
import { validateExistingPath } from '../lib/paths.js';
|
|
11
|
+
import { reportPeriodicProgress } from '../lib/utils.js';
|
|
11
12
|
import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas.js';
|
|
12
|
-
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
|
+
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
14
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
14
15
|
const WINDOWS_PATH_SEPARATOR = /\\/gu;
|
|
15
16
|
export const CALCULATE_HASH_TOOL = {
|
|
@@ -54,13 +55,6 @@ function updateCompositeHash(hasher, pathLengthBytes, relativePath, fileHash) {
|
|
|
54
55
|
hasher.update(relativePathBytes);
|
|
55
56
|
hasher.update(fileHash);
|
|
56
57
|
}
|
|
57
|
-
function reportHashProgress(onProgress, current, force = false) {
|
|
58
|
-
if (!onProgress || current === 0)
|
|
59
|
-
return;
|
|
60
|
-
if (!force && current % 25 !== 0)
|
|
61
|
-
return;
|
|
62
|
-
onProgress({ current });
|
|
63
|
-
}
|
|
64
58
|
async function hashDirectory(dirPath, options = {}) {
|
|
65
59
|
const { signal, onProgress } = options;
|
|
66
60
|
const gitignoreMatcher = await loadRootGitignore(dirPath, signal);
|
|
@@ -102,9 +96,12 @@ async function hashDirectory(dirPath, options = {}) {
|
|
|
102
96
|
}));
|
|
103
97
|
entries.push(...batchResults);
|
|
104
98
|
filesHashed += batchResults.length;
|
|
105
|
-
|
|
99
|
+
reportPeriodicProgress(onProgress, filesHashed, { throttleModulo: 25 });
|
|
106
100
|
}
|
|
107
|
-
|
|
101
|
+
reportPeriodicProgress(onProgress, filesHashed, {
|
|
102
|
+
throttleModulo: 25,
|
|
103
|
+
force: true,
|
|
104
|
+
});
|
|
108
105
|
assertNotAborted(signal);
|
|
109
106
|
// Sort by path with byte-wise semantics for deterministic ordering.
|
|
110
107
|
entries.sort(comparePaths);
|
|
@@ -141,7 +138,10 @@ async function handleCalculateHash(args, signal, onProgress) {
|
|
|
141
138
|
else {
|
|
142
139
|
// Hash single file
|
|
143
140
|
const hash = await hashFile(validPath, 'hex', signal);
|
|
144
|
-
|
|
141
|
+
reportPeriodicProgress(onProgress, 1, {
|
|
142
|
+
throttleModulo: 25,
|
|
143
|
+
force: true,
|
|
144
|
+
});
|
|
145
145
|
return buildToolResponse(hash, {
|
|
146
146
|
ok: true,
|
|
147
147
|
path: validPath,
|
|
@@ -171,7 +171,7 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
171
171
|
const result = await handleCalculateHash(args, signal, progressWithMessage);
|
|
172
172
|
const sc = result.structuredContent;
|
|
173
173
|
const totalFiles = sc.ok ? (sc.fileCount ?? 1) : 1;
|
|
174
|
-
const finalCurrent =
|
|
174
|
+
const finalCurrent = resolveFinalProgressCurrent(progress, totalFiles + 1);
|
|
175
175
|
let suffix;
|
|
176
176
|
if (!sc.ok) {
|
|
177
177
|
suffix = 'failed';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
3
3
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
4
|
-
import { validatePathForWrite } from '../lib/
|
|
4
|
+
import { validatePathForWrite } from '../lib/paths.js';
|
|
5
5
|
import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema, } from '../schemas.js';
|
|
6
6
|
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, IDEMPOTENT_WRITE_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { ErrorCode, isNodeError } from '../lib/errors.js';
|
|
3
|
+
import { ErrorCode, isNodeError, McpError } from '../lib/errors.js';
|
|
4
4
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
|
-
import { validatePathForWrite } from '../lib/
|
|
5
|
+
import { isAllowedDirectoryRoot, validatePathForWrite } from '../lib/paths.js';
|
|
6
6
|
import { DeleteFileInputSchema, DeleteFileOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
@@ -14,12 +14,15 @@ export const DELETE_FILE_TOOL = {
|
|
|
14
14
|
outputSchema: DeleteFileOutputSchema,
|
|
15
15
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
16
16
|
gotchas: [
|
|
17
|
-
'
|
|
18
|
-
'Non-empty
|
|
17
|
+
'No undo — deletion is permanent.',
|
|
18
|
+
'Non-empty directories require `recursive=true`.',
|
|
19
19
|
],
|
|
20
20
|
};
|
|
21
21
|
async function handleDeleteFile(args, signal) {
|
|
22
22
|
const validPath = await validatePathForWrite(args.path, signal);
|
|
23
|
+
if (isAllowedDirectoryRoot(validPath)) {
|
|
24
|
+
throw new McpError(ErrorCode.E_ACCESS_DENIED, `Deleting a workspace root directory is not allowed: ${args.path}`);
|
|
25
|
+
}
|
|
23
26
|
let stats;
|
|
24
27
|
try {
|
|
25
28
|
stats = await withAbort(fs.lstat(validPath), signal);
|
package/dist/tools/diff-files.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createTwoFilesPatch } from 'diff';
|
|
|
4
4
|
import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
5
5
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
6
6
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
7
|
-
import { validateExistingPath } from '../lib/
|
|
7
|
+
import { validateExistingPath } from '../lib/paths.js';
|
|
8
8
|
import { DiffFilesInputSchema, DiffFilesOutputSchema } from '../schemas.js';
|
|
9
9
|
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
10
10
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
@@ -13,7 +13,7 @@ export const DIFF_FILES_TOOL = {
|
|
|
13
13
|
title: 'Diff Files',
|
|
14
14
|
description: 'Generate a unified diff between two files. ' +
|
|
15
15
|
'Output feeds directly into `apply_patch`. ' +
|
|
16
|
-
'
|
|
16
|
+
'`isIdentical=true` means files match \u2014 no patch needed.',
|
|
17
17
|
inputSchema: DiffFilesInputSchema,
|
|
18
18
|
outputSchema: DiffFilesOutputSchema,
|
|
19
19
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import RE2 from 're2';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
5
|
+
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
6
|
+
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
7
|
+
import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
|
|
7
8
|
import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
|
|
8
9
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
9
10
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
10
11
|
export const EDIT_FILE_TOOL = {
|
|
11
12
|
name: 'edit',
|
|
12
13
|
title: 'Edit File',
|
|
13
|
-
description: '
|
|
14
|
-
'
|
|
15
|
-
'`
|
|
16
|
-
'Use `dryRun: true` to validate edits before writing.',
|
|
14
|
+
description: 'Apply sequential literal string replacements to a file (first occurrence per edit). ' +
|
|
15
|
+
'`oldText` must match exactly \u2014 include 3\u20135 lines of context for unique targeting. ' +
|
|
16
|
+
'Use `dryRun:true` to preview.',
|
|
17
17
|
inputSchema: EditFileInputSchema,
|
|
18
18
|
outputSchema: EditFileOutputSchema,
|
|
19
19
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
20
|
-
nuances: [
|
|
21
|
-
|
|
22
|
-
],
|
|
23
|
-
gotchas: [
|
|
24
|
-
'`oldText` must match exactly; unmatched items are reported in `unmatchedEdits`.',
|
|
25
|
-
],
|
|
20
|
+
nuances: ['Each edit applies to the output of the previous edit.'],
|
|
21
|
+
gotchas: ['Unmatched `oldText` entries listed in `unmatchedEdits`.'],
|
|
26
22
|
};
|
|
27
23
|
function escapeRegExp(string) {
|
|
28
24
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -88,6 +84,11 @@ function applyEdits(content, edits, ignoreWhitespace) {
|
|
|
88
84
|
}
|
|
89
85
|
export async function handleEditFile(args, signal) {
|
|
90
86
|
const validPath = await validateExistingPath(args.path, signal);
|
|
87
|
+
assertAllowedFileAccess(args.path, validPath);
|
|
88
|
+
const stats = await withAbort(fs.stat(validPath), signal);
|
|
89
|
+
if (stats.size > MAX_TEXT_FILE_SIZE) {
|
|
90
|
+
throw new McpError(ErrorCode.E_TOO_LARGE, `File too large for edit: ${args.path} (${stats.size} bytes > ${MAX_TEXT_FILE_SIZE} bytes)`, args.path, { size: stats.size, maxFileSize: MAX_TEXT_FILE_SIZE });
|
|
91
|
+
}
|
|
91
92
|
const content = await fs.readFile(validPath, { encoding: 'utf-8', signal });
|
|
92
93
|
const { content: newContent, appliedEdits, unmatchedEdits, lineRange, } = applyEdits(content, args.edits, args.ignoreWhitespace);
|
|
93
94
|
const structured = {
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
-
import { formatOperationSummary, joinLines } from '../config.js';
|
|
3
2
|
import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
|
|
4
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
|
-
import { listDirectory } from '../lib/file-operations/
|
|
4
|
+
import { listDirectory } from '../lib/file-operations/metadata.js';
|
|
5
|
+
import { formatOperationSummary, joinLines } from '../config.js';
|
|
6
6
|
import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, decodeOffsetCursor, encodeOffsetCursor, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
export const LIST_DIRECTORY_TOOL = {
|
|
10
10
|
name: 'ls',
|
|
11
11
|
title: 'List Directory',
|
|
12
|
-
description: 'List
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'Use includeIgnored=true to include ignored directories like node_modules. ' +
|
|
16
|
-
'For recursive searches, use find instead.',
|
|
12
|
+
description: 'List immediate directory contents (non-recursive): name, path, type, size, modified date. ' +
|
|
13
|
+
'Omit path for workspace root. `includeIgnored=true` for node_modules etc. ' +
|
|
14
|
+
'For recursive search, use `find`.',
|
|
17
15
|
inputSchema: ListDirectoryInputSchema,
|
|
18
16
|
outputSchema: ListDirectoryOutputSchema,
|
|
19
17
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
package/dist/tools/move-file.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { ErrorCode, formatUnknownErrorMessage, isNodeError, McpError, } from '../lib/errors.js';
|
|
4
4
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
|
-
import { validateExistingPath, validatePathForWrite, } from '../lib/
|
|
5
|
+
import { assertAllowedFileAccess, validateExistingPath, validatePathForWrite, } from '../lib/paths.js';
|
|
6
6
|
import { MoveFileInputSchema, MoveFileOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
@@ -48,6 +48,7 @@ export async function handleMoveFile(args, signal) {
|
|
|
48
48
|
let validSource;
|
|
49
49
|
try {
|
|
50
50
|
validSource = await validateExistingPath(src, signal);
|
|
51
|
+
assertAllowedFileAccess(src, validSource);
|
|
51
52
|
}
|
|
52
53
|
catch (error) {
|
|
53
54
|
failed.push({
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import { DEFAULT_READ_MANY_MAX_TOTAL_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/constants.js';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
|
-
import { readMultipleFiles } from '../lib/file-operations/
|
|
4
|
+
import { readMultipleFiles } from '../lib/file-operations/metadata.js';
|
|
5
5
|
import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
|
|
6
|
-
import { buildBatchPathContext, buildResourceLink, buildToolErrorResponse, buildToolResponse,
|
|
6
|
+
import { buildBatchCompletionSuffix, buildBatchPathContext, buildResourceLink, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
export const READ_MULTIPLE_FILES_TOOL = {
|
|
9
9
|
name: 'read_many',
|
|
10
10
|
title: 'Read Multiple Files',
|
|
11
|
-
description: 'Read multiple text files in
|
|
12
|
-
'
|
|
13
|
-
'For single file, use read for simpler output.',
|
|
11
|
+
description: 'Read multiple text files in one request with contents and metadata. ' +
|
|
12
|
+
'For a single file, use `read`.',
|
|
14
13
|
inputSchema: ReadMultipleFilesInputSchema,
|
|
15
14
|
outputSchema: ReadMultipleFilesOutputSchema,
|
|
16
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
@@ -20,23 +19,6 @@ export const READ_MULTIPLE_FILES_TOOL = {
|
|
|
20
19
|
'Per-file `truncationReason` can be `head`, `range`, or `externalized`.',
|
|
21
20
|
],
|
|
22
21
|
};
|
|
23
|
-
function buildReadManyCompletionSuffix(summary) {
|
|
24
|
-
const total = summary?.total ?? 0;
|
|
25
|
-
const failed = summary?.failed ?? 0;
|
|
26
|
-
const succeeded = summary?.succeeded ?? 0;
|
|
27
|
-
if (failed) {
|
|
28
|
-
return `${succeeded}/${total} read, ${failed} failed`;
|
|
29
|
-
}
|
|
30
|
-
const label = total === 1 ? 'file' : 'files';
|
|
31
|
-
return `${total} ${label} read`;
|
|
32
|
-
}
|
|
33
|
-
function createReadManyProgressCallbacks(extra, context, totalPaths) {
|
|
34
|
-
const progress = createToolProgressSession(extra, `🕮 read_many: ${context}`);
|
|
35
|
-
const onReadComplete = () => {
|
|
36
|
-
progress.increment((current) => `🕮 read_many: ${context} [${current}/${totalPaths} read]`);
|
|
37
|
-
};
|
|
38
|
-
return { progress, onReadComplete };
|
|
39
|
-
}
|
|
40
22
|
function toStructuredReadManyResult(result) {
|
|
41
23
|
const structured = {
|
|
42
24
|
path: result.path,
|
|
@@ -152,13 +134,18 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
152
134
|
context: { path: primaryPath },
|
|
153
135
|
run: async (signal) => {
|
|
154
136
|
const context = buildBatchPathContext(args.paths, 'files');
|
|
155
|
-
const { progress,
|
|
137
|
+
const { progress, onItemComplete } = createBatchProgressCallbacks(extra, {
|
|
138
|
+
toolLabel: '🕮 read_many',
|
|
139
|
+
context,
|
|
140
|
+
totalItems: args.paths.length,
|
|
141
|
+
itemVerb: 'read',
|
|
142
|
+
});
|
|
156
143
|
try {
|
|
157
|
-
const result = await handleReadMultipleFiles(args, signal, options.resourceStore,
|
|
144
|
+
const result = await handleReadMultipleFiles(args, signal, options.resourceStore, onItemComplete);
|
|
158
145
|
const sc = result.structuredContent;
|
|
159
|
-
const suffix =
|
|
146
|
+
const suffix = buildBatchCompletionSuffix(sc.summary, 'files read', 'file read');
|
|
160
147
|
const total = sc.summary?.total ?? 0;
|
|
161
|
-
const finalCurrent =
|
|
148
|
+
const finalCurrent = resolveFinalProgressCurrent(progress, total);
|
|
162
149
|
progress.complete(`🕮 read_many: ${context} • ${suffix}`, finalCurrent);
|
|
163
150
|
return result;
|
|
164
151
|
}
|
package/dist/tools/read.js
CHANGED
|
@@ -8,9 +8,9 @@ import { registerToolTaskIfAvailable } from './task-support.js';
|
|
|
8
8
|
export const READ_FILE_TOOL = {
|
|
9
9
|
name: 'read',
|
|
10
10
|
title: 'Read File',
|
|
11
|
-
description: 'Read
|
|
12
|
-
'Use head
|
|
13
|
-
'For multiple files, use read_many
|
|
11
|
+
description: 'Read text file contents. ' +
|
|
12
|
+
'Use `head` to preview first N lines of large files. ' +
|
|
13
|
+
'For multiple files, use `read_many`.',
|
|
14
14
|
inputSchema: ReadFileInputSchema,
|
|
15
15
|
outputSchema: ReadFileOutputSchema,
|
|
16
16
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
@@ -5,27 +5,26 @@ import RE2 from 're2';
|
|
|
5
5
|
import safeRegex from 'safe-regex2';
|
|
6
6
|
import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
|
|
7
7
|
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
8
|
-
import { globEntries } from '../lib/file-operations/
|
|
8
|
+
import { globEntries } from '../lib/file-operations/traversal.js';
|
|
9
9
|
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
10
|
-
import { validateExistingPath, validatePathForWrite
|
|
10
|
+
import { validateExistingPath, validatePathForWrite } from '../lib/paths.js';
|
|
11
|
+
import { reportPeriodicProgress } from '../lib/utils.js';
|
|
11
12
|
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
|
|
12
|
-
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
|
+
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolveFinalProgressCurrent, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
14
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
14
15
|
export const SEARCH_AND_REPLACE_TOOL = {
|
|
15
16
|
name: 'search_and_replace',
|
|
16
17
|
title: 'Search and Replace',
|
|
17
|
-
description: '
|
|
18
|
-
'Replaces ALL occurrences
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'Returns a unified diff of changes in `dryRun` mode. ' +
|
|
22
|
-
'Literal mode (default) matches exact text; `isRegex: true` enables RE2 regex with capture groups ($1, $2).',
|
|
18
|
+
description: 'Bulk search-and-replace across files matching a glob. ' +
|
|
19
|
+
'Replaces ALL occurrences per file (unlike `edit`: first only). ' +
|
|
20
|
+
'Always `dryRun:true` first \u2014 returns a unified diff. ' +
|
|
21
|
+
'Literal matching by default; `isRegex:true` enables RE2 with capture groups ($1, $2).',
|
|
23
22
|
inputSchema: SearchAndReplaceInputSchema,
|
|
24
23
|
outputSchema: SearchAndReplaceOutputSchema,
|
|
25
24
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
26
25
|
taskSupport: 'optional',
|
|
27
26
|
gotchas: [
|
|
28
|
-
'
|
|
27
|
+
'Replaces ALL occurrences — not just the first. Use `edit` for single replacements.',
|
|
29
28
|
],
|
|
30
29
|
nuances: [
|
|
31
30
|
'Changed-file sample and failure sample are capped/truncated in output.',
|
|
@@ -204,13 +203,6 @@ function createReplacementMatcher(args) {
|
|
|
204
203
|
}
|
|
205
204
|
return createLiteralReplacementMatcher(args.searchPattern);
|
|
206
205
|
}
|
|
207
|
-
function reportReplaceProgress(onProgress, current, force = false) {
|
|
208
|
-
if (current === 0)
|
|
209
|
-
return;
|
|
210
|
-
if (!force && current % 25 !== 0)
|
|
211
|
-
return;
|
|
212
|
-
onProgress({ current });
|
|
213
|
-
}
|
|
214
206
|
export async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
215
207
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
216
208
|
const root = await resolveSearchRoot(args.path, signal);
|
|
@@ -233,14 +225,19 @@ export async function handleSearchAndReplace(args, signal, onProgress = () => {
|
|
|
233
225
|
concurrency: REPLACE_CONCURRENCY,
|
|
234
226
|
onEntry: () => {
|
|
235
227
|
summary.processedFiles++;
|
|
236
|
-
|
|
228
|
+
reportPeriodicProgress(onProgress, summary.processedFiles, {
|
|
229
|
+
throttleModulo: 25,
|
|
230
|
+
});
|
|
237
231
|
},
|
|
238
232
|
runEntry: async (entryPath) => processEntry(entryPath, {
|
|
239
233
|
dryRun: args.dryRun,
|
|
240
234
|
returnDiff: args.returnDiff ?? false,
|
|
241
235
|
}, args.replacement, matcher, maxFileSize, signal, summary),
|
|
242
236
|
});
|
|
243
|
-
|
|
237
|
+
reportPeriodicProgress(onProgress, summary.processedFiles, {
|
|
238
|
+
throttleModulo: 25,
|
|
239
|
+
force: true,
|
|
240
|
+
});
|
|
244
241
|
const failureSuffix = summary.failedFiles > 0 ? ` (${summary.failedFiles} failed)` : '';
|
|
245
242
|
return buildToolResponse(`Found ${summary.totalMatches} matches in ${summary.filesChanged} files${failureSuffix}.${args.dryRun ? ' (Dry run)' : ''}`, {
|
|
246
243
|
ok: true,
|
|
@@ -279,7 +276,7 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
279
276
|
try {
|
|
280
277
|
const result = await handleSearchAndReplace(args, signal, progressWithMessage);
|
|
281
278
|
const sc = result.structuredContent;
|
|
282
|
-
const finalCurrent =
|
|
279
|
+
const finalCurrent = resolveFinalProgressCurrent(progress, (sc.processedFiles ?? 0) + 1);
|
|
283
280
|
const matchWord = (sc.matches ?? 0) === 1 ? 'match' : 'matches';
|
|
284
281
|
const fileWord = (sc.filesChanged ?? 0) === 1 ? 'file' : 'files';
|
|
285
282
|
let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
|
package/dist/tools/roots.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { joinLines } from '../config.js';
|
|
2
1
|
import { ErrorCode } from '../lib/errors.js';
|
|
3
|
-
import { getAllowedDirectories } from '../lib/
|
|
2
|
+
import { getAllowedDirectories } from '../lib/paths.js';
|
|
3
|
+
import { joinLines } from '../config.js';
|
|
4
4
|
import { ListAllowedDirectoriesInputSchema, ListAllowedDirectoriesOutputSchema, } from '../schemas.js';
|
|
5
5
|
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
6
6
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
7
7
|
export const LIST_ALLOWED_DIRECTORIES_TOOL = {
|
|
8
8
|
name: 'roots',
|
|
9
9
|
title: 'Workspace Roots',
|
|
10
|
-
description: 'List
|
|
11
|
-
'Call this first to see available directories. ' +
|
|
12
|
-
'All other tools only work within these directories.',
|
|
10
|
+
description: 'List allowed workspace roots. Call first \u2014 all other tools are scoped to these directories.',
|
|
13
11
|
inputSchema: ListAllowedDirectoriesInputSchema,
|
|
14
12
|
outputSchema: ListAllowedDirectoriesOutputSchema,
|
|
15
13
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
16
|
-
nuances: ['
|
|
14
|
+
nuances: ['Returns absolute paths of all allowed directories.'],
|
|
17
15
|
};
|
|
18
16
|
function buildTextRoots(dirs) {
|
|
19
17
|
if (dirs.length === 0) {
|