@j0hanz/filesystem-mcp 1.2.1 → 1.2.3
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 +11 -0
- package/dist/cli.js +14 -8
- package/dist/completions.js +15 -6
- package/dist/lib/file-operations/glob-engine.js +3 -9
- package/dist/lib/file-operations/read-multiple-files.js +4 -23
- package/dist/lib/file-operations/search-content.js +11 -18
- package/dist/lib/observability.js +4 -4
- package/dist/lib/path-validation.js +22 -9
- package/dist/lib/resource-store.js +1 -1
- package/dist/pkg-info.d.ts +6 -0
- package/dist/pkg-info.js +9 -0
- package/dist/schemas.d.ts +28 -28
- package/dist/schemas.js +26 -26
- package/dist/server/bootstrap.d.ts +4 -0
- package/dist/server/bootstrap.js +117 -0
- package/dist/server/capabilities.d.ts +10 -0
- package/dist/server/capabilities.js +40 -0
- package/dist/server/logging.d.ts +7 -0
- package/dist/server/logging.js +41 -0
- package/dist/server/roots-manager.d.ts +19 -0
- package/dist/server/roots-manager.js +173 -0
- package/dist/server/types.d.ts +4 -0
- package/dist/server/types.js +1 -0
- package/dist/server.d.ts +2 -8
- package/dist/server.js +1 -346
- package/dist/tools/apply-patch.js +17 -3
- package/dist/tools/calculate-hash.js +48 -13
- package/dist/tools/create-directory.js +13 -3
- package/dist/tools/delete-file.js +9 -2
- package/dist/tools/diff-files.js +16 -2
- package/dist/tools/edit-file.js +8 -7
- package/dist/tools/list-directory.js +13 -2
- package/dist/tools/move-file.js +11 -3
- package/dist/tools/read-multiple.js +21 -3
- package/dist/tools/read.js +16 -2
- package/dist/tools/replace-in-files.js +44 -11
- package/dist/tools/roots.js +12 -2
- package/dist/tools/search-content.js +62 -29
- package/dist/tools/search-files.js +60 -13
- package/dist/tools/shared.d.ts +5 -1
- package/dist/tools/shared.js +55 -8
- package/dist/tools/stat-many.js +22 -3
- package/dist/tools/stat.js +12 -2
- package/dist/tools/task-support.js +60 -13
- package/dist/tools/tree.js +15 -2
- package/dist/tools/write-file.js +13 -3
- package/package.json +4 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
3
3
|
import { isRecord } from '../lib/type-guards.js';
|
|
4
|
-
import { buildToolErrorResponse, withDefaultIcons } from './shared.js';
|
|
4
|
+
import { buildToolErrorResponse, maybeStripStructuredContentFromResult, withDefaultIcons, } from './shared.js';
|
|
5
5
|
function isExperimentalTaskRegistration(value) {
|
|
6
6
|
if (!value || typeof value !== 'object')
|
|
7
7
|
return false;
|
|
@@ -18,6 +18,29 @@ function getExperimentalTaskRegistration(server) {
|
|
|
18
18
|
return undefined;
|
|
19
19
|
return tasks;
|
|
20
20
|
}
|
|
21
|
+
function hasTaskToolCapability(server) {
|
|
22
|
+
const maybeServer = server;
|
|
23
|
+
const serverRuntime = maybeServer.server;
|
|
24
|
+
const capabilityGetter = serverRuntime?.getCapabilities;
|
|
25
|
+
if (typeof capabilityGetter !== 'function') {
|
|
26
|
+
// Fallback for tests or custom wrappers that provide only registerTool/experimental.
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const capabilities = capabilityGetter.call(serverRuntime);
|
|
30
|
+
if (!isRecord(capabilities))
|
|
31
|
+
return false;
|
|
32
|
+
const { tasks } = capabilities;
|
|
33
|
+
if (!isRecord(tasks))
|
|
34
|
+
return false;
|
|
35
|
+
const { requests } = tasks;
|
|
36
|
+
if (!isRecord(requests))
|
|
37
|
+
return false;
|
|
38
|
+
const { tools } = requests;
|
|
39
|
+
if (!isRecord(tools))
|
|
40
|
+
return false;
|
|
41
|
+
const { call } = tools;
|
|
42
|
+
return isRecord(call);
|
|
43
|
+
}
|
|
21
44
|
const RELATED_TASK_META_KEY = 'io.modelcontextprotocol/related-task';
|
|
22
45
|
const TASK_STATUS_NOTIFICATION_METHOD = 'notifications/tasks/status';
|
|
23
46
|
function isRequestTaskStore(value) {
|
|
@@ -101,6 +124,35 @@ function normalizeCallToolResult(value) {
|
|
|
101
124
|
return parsed.data;
|
|
102
125
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Stored task result is not a valid tool result.');
|
|
103
126
|
}
|
|
127
|
+
function getToolResultErrorCode(result) {
|
|
128
|
+
if (!isRecord(result) || result['isError'] !== true)
|
|
129
|
+
return undefined;
|
|
130
|
+
const structured = result['structuredContent'];
|
|
131
|
+
if (!isRecord(structured))
|
|
132
|
+
return undefined;
|
|
133
|
+
const { error } = structured;
|
|
134
|
+
if (!isRecord(error))
|
|
135
|
+
return undefined;
|
|
136
|
+
const { code } = error;
|
|
137
|
+
return typeof code === 'string' ? code : undefined;
|
|
138
|
+
}
|
|
139
|
+
function isCancelledToolResult(result) {
|
|
140
|
+
return getToolResultErrorCode(result) === ErrorCode.E_CANCELLED;
|
|
141
|
+
}
|
|
142
|
+
async function projectCancelledTaskStatus(taskStore, task) {
|
|
143
|
+
if (task.status !== 'failed')
|
|
144
|
+
return task;
|
|
145
|
+
try {
|
|
146
|
+
const result = await taskStore.getTaskResult(task.taskId);
|
|
147
|
+
if (isCancelledToolResult(result)) {
|
|
148
|
+
return { ...task, status: 'cancelled' };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Best effort only: task result may not be available yet.
|
|
153
|
+
}
|
|
154
|
+
return task;
|
|
155
|
+
}
|
|
104
156
|
function withRelatedTaskMeta(result, taskId) {
|
|
105
157
|
const existingMeta = isRecord(result['_meta']) ? result['_meta'] : {};
|
|
106
158
|
return {
|
|
@@ -129,7 +181,7 @@ async function notifyTaskStatusIfPossible(extra, taskStore, taskId) {
|
|
|
129
181
|
const notify = sendNotification;
|
|
130
182
|
try {
|
|
131
183
|
const task = await taskStore.getTask(taskId);
|
|
132
|
-
const normalized = normalizeGetTaskResult(task);
|
|
184
|
+
const normalized = await projectCancelledTaskStatus(taskStore, normalizeGetTaskResult(task));
|
|
133
185
|
await notify({
|
|
134
186
|
method: TASK_STATUS_NOTIFICATION_METHOD,
|
|
135
187
|
params: buildTaskStatusNotificationParams(normalized),
|
|
@@ -159,12 +211,6 @@ const TERMINAL_TASK_STATUSES = new Set([
|
|
|
159
211
|
'failed',
|
|
160
212
|
'cancelled',
|
|
161
213
|
]);
|
|
162
|
-
function isTerminalTaskStoreError(error) {
|
|
163
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
-
const normalized = message.toLowerCase();
|
|
165
|
-
return (normalized.includes('terminal status') ||
|
|
166
|
-
normalized.includes('task not found'));
|
|
167
|
-
}
|
|
168
214
|
async function isTaskAlreadyTerminal(taskStore, taskId) {
|
|
169
215
|
try {
|
|
170
216
|
const task = await taskStore.getTask(taskId);
|
|
@@ -183,21 +229,20 @@ async function tryStoreTaskResult(taskStore, taskId, status, result) {
|
|
|
183
229
|
await taskStore.storeTaskResult(taskId, status, resultWithTaskMeta);
|
|
184
230
|
}
|
|
185
231
|
catch (error) {
|
|
186
|
-
if (
|
|
187
|
-
(await isTaskAlreadyTerminal(taskStore, taskId)))
|
|
232
|
+
if (await isTaskAlreadyTerminal(taskStore, taskId))
|
|
188
233
|
return;
|
|
189
234
|
throw error;
|
|
190
235
|
}
|
|
191
236
|
}
|
|
192
237
|
async function runTaskInBackground(run, args, extra, taskStore, taskId) {
|
|
193
238
|
try {
|
|
194
|
-
const result = await run(args, extra);
|
|
239
|
+
const result = maybeStripStructuredContentFromResult(await run(args, extra));
|
|
195
240
|
const status = isErrorResult(result) ? 'failed' : 'completed';
|
|
196
241
|
await tryStoreTaskResult(taskStore, taskId, status, result);
|
|
197
242
|
await notifyTaskStatusIfPossible(extra, taskStore, taskId);
|
|
198
243
|
}
|
|
199
244
|
catch (error) {
|
|
200
|
-
const fallback = buildToolErrorResponse(error, ErrorCode.E_UNKNOWN);
|
|
245
|
+
const fallback = maybeStripStructuredContentFromResult(buildToolErrorResponse(error, ErrorCode.E_UNKNOWN));
|
|
201
246
|
try {
|
|
202
247
|
await tryStoreTaskResult(taskStore, taskId, 'failed', fallback);
|
|
203
248
|
await notifyTaskStatusIfPossible(extra, taskStore, taskId);
|
|
@@ -213,6 +258,8 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId) {
|
|
|
213
258
|
* `server.registerTool`.
|
|
214
259
|
*/
|
|
215
260
|
export function tryRegisterToolTask(server, toolName, toolDef, taskHandler, iconInfo) {
|
|
261
|
+
if (!hasTaskToolCapability(server))
|
|
262
|
+
return false;
|
|
216
263
|
const tasks = getExperimentalTaskRegistration(server);
|
|
217
264
|
if (!tasks?.registerToolTask)
|
|
218
265
|
return false;
|
|
@@ -248,7 +295,7 @@ export function createToolTaskHandler(run, options) {
|
|
|
248
295
|
const taskStore = getTaskStore(extra);
|
|
249
296
|
const taskId = getTaskId(extra);
|
|
250
297
|
const task = await taskStore.getTask(taskId);
|
|
251
|
-
return normalizeGetTaskResult(task);
|
|
298
|
+
return projectCancelledTaskStatus(taskStore, normalizeGetTaskResult(task));
|
|
252
299
|
});
|
|
253
300
|
const getTaskResult = (async (argsOrExtra, maybeExtra) => {
|
|
254
301
|
const extra = asTaskRequestExtra(maybeExtra ?? argsOrExtra);
|
package/dist/tools/tree.js
CHANGED
|
@@ -3,7 +3,7 @@ import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { formatTreeAscii, treeDirectory } from '../lib/file-operations/tree.js';
|
|
5
5
|
import { TreeInputSchema, TreeOutputSchema } from '../schemas.js';
|
|
6
|
-
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
const TREE_TOOL = {
|
|
9
9
|
title: 'Tree',
|
|
@@ -47,7 +47,8 @@ export function registerTreeTool(server, options = {}) {
|
|
|
47
47
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, targetPath),
|
|
48
48
|
});
|
|
49
49
|
};
|
|
50
|
-
const
|
|
50
|
+
const validatedHandler = withValidatedArgs(TreeInputSchema, handler);
|
|
51
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
51
52
|
guard: options.isInitialized,
|
|
52
53
|
progressMessage: (args) => {
|
|
53
54
|
if (args.path) {
|
|
@@ -55,6 +56,18 @@ export function registerTreeTool(server, options = {}) {
|
|
|
55
56
|
}
|
|
56
57
|
return '≣ tree';
|
|
57
58
|
},
|
|
59
|
+
completionMessage: (args, result) => {
|
|
60
|
+
const base = args.path ? path.basename(args.path) : '.';
|
|
61
|
+
if (result.isError)
|
|
62
|
+
return `≣ tree: ${base} • failed`;
|
|
63
|
+
const sc = result.structuredContent;
|
|
64
|
+
if (!sc.ok)
|
|
65
|
+
return `≣ tree: ${base} • failed`;
|
|
66
|
+
const count = sc.totalEntries ?? 0;
|
|
67
|
+
if (sc.truncated)
|
|
68
|
+
return `≣ tree: ${base} • ${count} entries [truncated]`;
|
|
69
|
+
return `≣ tree: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
|
|
70
|
+
},
|
|
58
71
|
});
|
|
59
72
|
if (registerToolTaskIfAvailable(server, 'tree', TREE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
60
73
|
return;
|
package/dist/tools/write-file.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ErrorCode } from '../lib/errors.js';
|
|
|
4
4
|
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
5
5
|
import { validatePathForWrite } from '../lib/path-validation.js';
|
|
6
6
|
import { WriteFileInputSchema, WriteFileOutputSchema } from '../schemas.js';
|
|
7
|
-
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
const WRITE_FILE_TOOL = {
|
|
10
10
|
title: 'Write File',
|
|
@@ -34,9 +34,19 @@ export function registerWriteFileTool(server, options = {}) {
|
|
|
34
34
|
run: (signal) => handleWriteFile(args, signal),
|
|
35
35
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
36
36
|
});
|
|
37
|
-
const
|
|
37
|
+
const validatedHandler = withValidatedArgs(WriteFileInputSchema, handler);
|
|
38
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
38
39
|
guard: options.isInitialized,
|
|
39
|
-
progressMessage: (args) => `🛠 write: ${path.basename(args.path)}`,
|
|
40
|
+
progressMessage: (args) => `🛠 write: ${path.basename(args.path)} [${args.content.length} chars]`,
|
|
41
|
+
completionMessage: (args, result) => {
|
|
42
|
+
const name = path.basename(args.path);
|
|
43
|
+
if (result.isError)
|
|
44
|
+
return `🛠 write: ${name} • failed`;
|
|
45
|
+
const sc = result.structuredContent;
|
|
46
|
+
if (!sc.ok)
|
|
47
|
+
return `🛠 write: ${name} • failed`;
|
|
48
|
+
return `🛠 write: ${name} • ${sc.bytesWritten ?? 0} bytes`;
|
|
49
|
+
},
|
|
40
50
|
});
|
|
41
51
|
if (registerToolTaskIfAvailable(server, 'write', WRITE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
42
52
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/filesystem-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"mcpName": "io.github.j0hanz/filesystem-mcp",
|
|
5
5
|
"description": "MCP Server that enables LLMs to interact with the local filesystem.",
|
|
6
6
|
"type": "module",
|
|
@@ -31,12 +31,14 @@
|
|
|
31
31
|
"start": "node dist/index.js",
|
|
32
32
|
"format": "prettier --write .",
|
|
33
33
|
"type-check": "node scripts/tasks.mjs type-check",
|
|
34
|
+
"type-check:src": "node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"type-check:tests": "node node_modules/typescript/bin/tsc -p tsconfig.test.json --noEmit",
|
|
34
36
|
"type-check:diagnostics": "tsc --noEmit --extendedDiagnostics",
|
|
35
37
|
"type-check:trace": "node -e \"require('fs').rmSync('.ts-trace',{recursive:true,force:true})\" && tsc --noEmit --generateTrace .ts-trace",
|
|
36
38
|
"lint": "eslint .",
|
|
37
39
|
"lint:fix": "eslint . --fix",
|
|
38
40
|
"test": "node scripts/tasks.mjs test",
|
|
39
|
-
"test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts",
|
|
41
|
+
"test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts node-tests/**/*.test.ts",
|
|
40
42
|
"test:coverage": "node scripts/tasks.mjs test --coverage",
|
|
41
43
|
"knip": "knip",
|
|
42
44
|
"knip:fix": "knip --fix",
|