@j0hanz/filesystem-mcp 1.1.2 → 1.2.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 +514 -188
- package/dist/cli.js +29 -12
- package/dist/completions.js +50 -24
- package/dist/config.d.ts +4 -2
- package/dist/config.js +2 -1
- package/dist/index.js +14 -12
- package/dist/instructions.md +109 -97
- package/dist/lib/constants.js +25 -14
- package/dist/lib/errors.js +15 -8
- package/dist/lib/file-operations/common.d.ts +4 -0
- package/dist/lib/file-operations/common.js +9 -0
- package/dist/lib/file-operations/file-info.js +22 -10
- package/dist/lib/file-operations/gitignore.js +14 -11
- package/dist/lib/file-operations/glob-engine.d.ts +1 -0
- package/dist/lib/file-operations/glob-engine.js +46 -33
- package/dist/lib/file-operations/list-directory.js +31 -35
- package/dist/lib/file-operations/read-multiple-files.js +70 -62
- package/dist/lib/file-operations/search-content.js +83 -64
- package/dist/lib/file-operations/search-files.js +32 -30
- package/dist/lib/file-operations/search-worker.js +22 -12
- package/dist/lib/file-operations/tree.js +43 -34
- package/dist/lib/fs-helpers.js +61 -124
- package/dist/lib/observability.js +29 -28
- package/dist/lib/path-format.d.ts +1 -0
- package/dist/lib/path-format.js +7 -0
- package/dist/lib/path-policy.js +22 -20
- package/dist/lib/path-validation.js +13 -7
- package/dist/lib/resource-store.d.ts +2 -0
- package/dist/lib/resource-store.js +26 -5
- package/dist/lib/type-guards.d.ts +1 -0
- package/dist/lib/type-guards.js +3 -0
- package/dist/prompts.d.ts +1 -5
- package/dist/prompts.js +9 -16
- package/dist/resources.d.ts +1 -5
- package/dist/resources.js +12 -26
- package/dist/schemas.d.ts +232 -30
- package/dist/schemas.js +52 -90
- package/dist/server.js +96 -44
- package/dist/tools/apply-patch.js +23 -22
- package/dist/tools/calculate-hash.js +41 -43
- package/dist/tools/create-directory.js +17 -19
- package/dist/tools/delete-file.js +35 -37
- package/dist/tools/diff-files.js +15 -19
- package/dist/tools/edit-file.js +15 -18
- package/dist/tools/list-directory.js +24 -23
- package/dist/tools/move-file.js +17 -19
- package/dist/tools/read-multiple.js +55 -66
- package/dist/tools/read.js +26 -30
- package/dist/tools/replace-in-files.js +27 -33
- package/dist/tools/roots.js +8 -8
- package/dist/tools/search-content.js +73 -72
- package/dist/tools/search-files.js +44 -50
- package/dist/tools/shared.d.ts +44 -6
- package/dist/tools/shared.js +86 -64
- package/dist/tools/stat-many.js +44 -66
- package/dist/tools/stat.js +10 -37
- package/dist/tools/task-support.d.ts +9 -1
- package/dist/tools/task-support.js +86 -81
- package/dist/tools/tree.js +12 -28
- package/dist/tools/write-file.js +17 -19
- package/dist/tools.js +23 -18
- package/package.json +6 -7
package/dist/tools/stat.js
CHANGED
|
@@ -3,41 +3,15 @@ import { formatBytes, joinLines } from '../config.js';
|
|
|
3
3
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { getFileInfo } from '../lib/file-operations/file-info.js';
|
|
6
|
-
import { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
7
|
-
import { withToolDiagnostics } from '../lib/observability.js';
|
|
8
6
|
import { GetFileInfoInputSchema, GetFileInfoOutputSchema } from '../schemas.js';
|
|
9
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
7
|
+
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
10
8
|
const GET_FILE_INFO_TOOL = {
|
|
11
9
|
title: 'Get File Info',
|
|
12
10
|
description: 'Get metadata (size, modified time, permissions, mime type) for a file or directory.',
|
|
13
11
|
inputSchema: GetFileInfoInputSchema,
|
|
14
12
|
outputSchema: GetFileInfoOutputSchema,
|
|
15
|
-
annotations:
|
|
16
|
-
readOnlyHint: true,
|
|
17
|
-
idempotentHint: true,
|
|
18
|
-
openWorldHint: false,
|
|
19
|
-
},
|
|
13
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
20
14
|
};
|
|
21
|
-
function buildFileInfoPayload(info) {
|
|
22
|
-
return {
|
|
23
|
-
name: info.name,
|
|
24
|
-
path: info.path,
|
|
25
|
-
type: info.type,
|
|
26
|
-
size: info.size,
|
|
27
|
-
...(info.tokenEstimate !== undefined
|
|
28
|
-
? { tokenEstimate: info.tokenEstimate }
|
|
29
|
-
: {}),
|
|
30
|
-
created: info.created.toISOString(),
|
|
31
|
-
modified: info.modified.toISOString(),
|
|
32
|
-
accessed: info.accessed.toISOString(),
|
|
33
|
-
permissions: info.permissions,
|
|
34
|
-
isHidden: info.isHidden,
|
|
35
|
-
...(info.mimeType !== undefined ? { mimeType: info.mimeType } : {}),
|
|
36
|
-
...(info.symlinkTarget !== undefined
|
|
37
|
-
? { symlinkTarget: info.symlinkTarget }
|
|
38
|
-
: {}),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
15
|
function formatFileInfoDetails(info) {
|
|
42
16
|
const lines = [
|
|
43
17
|
`${info.name} (${info.type})`,
|
|
@@ -63,15 +37,14 @@ async function handleGetFileInfo(args, signal) {
|
|
|
63
37
|
return buildToolResponse(formatFileInfoDetails(info), structured);
|
|
64
38
|
}
|
|
65
39
|
export function registerGetFileInfoTool(server, options = {}) {
|
|
66
|
-
const handler = (args, extra) =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}, (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, args.path)), { path: args.path });
|
|
40
|
+
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
41
|
+
toolName: 'stat',
|
|
42
|
+
extra,
|
|
43
|
+
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
44
|
+
context: { path: args.path },
|
|
45
|
+
run: (signal) => handleGetFileInfo(args, signal),
|
|
46
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, args.path),
|
|
47
|
+
});
|
|
75
48
|
server.registerTool('stat', withDefaultIcons({ ...GET_FILE_INFO_TOOL }, options.iconInfo), wrapToolHandler(handler, {
|
|
76
49
|
guard: options.isInitialized,
|
|
77
50
|
progressMessage: (args) => `🕮 stat: ${path.basename(args.path)}`,
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import type { ToolTaskHandler } from '@modelcontextprotocol/sdk/experimental/tasks/interfaces.js';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import type { AnySchema, SchemaOutput, ShapeOutput, ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
|
|
3
4
|
import type { RequestTaskStore } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
4
|
-
import type { ToolExtra, ToolResult } from './shared.js';
|
|
5
|
+
import type { IconInfo, ToolExtra, ToolResult } from './shared.js';
|
|
5
6
|
type TaskToolExtra = ToolExtra & {
|
|
6
7
|
taskId?: string;
|
|
7
8
|
taskStore?: RequestTaskStore;
|
|
8
9
|
taskRequestedTtl?: number | null;
|
|
9
10
|
};
|
|
10
11
|
type ToolArgs<Args extends ZodRawShapeCompat | AnySchema | undefined> = Args extends ZodRawShapeCompat ? ShapeOutput<Args> : Args extends AnySchema ? SchemaOutput<Args> : undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Registers a tool preferring task-capable registration when available, and
|
|
14
|
+
* returns `true`. Returns `false` so the caller can fall through to standard
|
|
15
|
+
* `server.registerTool`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function tryRegisterToolTask<Args extends ZodRawShapeCompat | AnySchema | undefined>(server: McpServer, toolName: string, toolDef: object, taskHandler: ToolTaskHandler<Args>, iconInfo: IconInfo | undefined): boolean;
|
|
18
|
+
export declare function registerToolTaskIfAvailable<Args extends ZodRawShapeCompat | AnySchema | undefined, Result>(server: McpServer, toolName: string, toolDef: object, run: (args: ToolArgs<Args>, extra: TaskToolExtra) => Promise<ToolResult<Result>>, iconInfo: IconInfo | undefined, guard?: () => boolean): boolean;
|
|
11
19
|
export declare function createToolTaskHandler<Result>(run: (args: undefined, extra: TaskToolExtra) => Promise<ToolResult<Result>>, options?: {
|
|
12
20
|
guard?: () => boolean;
|
|
13
21
|
}): ToolTaskHandler;
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
3
|
-
import {
|
|
3
|
+
import { isRecord } from '../lib/type-guards.js';
|
|
4
|
+
import { buildToolErrorResponse, withDefaultIcons } from './shared.js';
|
|
5
|
+
function isExperimentalTaskRegistration(value) {
|
|
6
|
+
if (!value || typeof value !== 'object')
|
|
7
|
+
return false;
|
|
8
|
+
const { registerToolTask } = value;
|
|
9
|
+
return (registerToolTask === undefined || typeof registerToolTask === 'function');
|
|
10
|
+
}
|
|
11
|
+
function getExperimentalTaskRegistration(server) {
|
|
12
|
+
const serverWithExperimental = server;
|
|
13
|
+
const { experimental } = serverWithExperimental;
|
|
14
|
+
if (!experimental || typeof experimental !== 'object')
|
|
15
|
+
return undefined;
|
|
16
|
+
const { tasks } = experimental;
|
|
17
|
+
if (!isExperimentalTaskRegistration(tasks))
|
|
18
|
+
return undefined;
|
|
19
|
+
return tasks;
|
|
20
|
+
}
|
|
4
21
|
const RELATED_TASK_META_KEY = 'io.modelcontextprotocol/related-task';
|
|
5
22
|
const TASK_STATUS_NOTIFICATION_METHOD = 'notifications/tasks/status';
|
|
6
|
-
function isRecord(value) {
|
|
7
|
-
return value !== null && typeof value === 'object';
|
|
8
|
-
}
|
|
9
23
|
function isRequestTaskStore(value) {
|
|
10
24
|
if (!isRecord(value))
|
|
11
25
|
return false;
|
|
@@ -41,26 +55,24 @@ const TASK_STATUSES = new Set([
|
|
|
41
55
|
'failed',
|
|
42
56
|
'cancelled',
|
|
43
57
|
]);
|
|
58
|
+
function isTaskStatus(value) {
|
|
59
|
+
return (typeof value === 'string' &&
|
|
60
|
+
TASK_STATUSES.has(value));
|
|
61
|
+
}
|
|
44
62
|
function parseTaskStatus(value) {
|
|
45
|
-
|
|
46
|
-
value === 'input_required' ||
|
|
47
|
-
value === 'completed' ||
|
|
48
|
-
value === 'failed' ||
|
|
49
|
-
value === 'cancelled') {
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
63
|
+
return isTaskStatus(value) ? value : undefined;
|
|
53
64
|
}
|
|
54
65
|
function normalizeGetTaskResult(value) {
|
|
55
66
|
if (!isRecord(value) || typeof value['taskId'] !== 'string') {
|
|
56
67
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Invalid task object.');
|
|
57
68
|
}
|
|
58
69
|
const status = parseTaskStatus(value['status']);
|
|
59
|
-
if (!status
|
|
70
|
+
if (!status) {
|
|
60
71
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Invalid task status.');
|
|
61
72
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
73
|
+
const createdAt = typeof value['createdAt'] === 'string'
|
|
74
|
+
? value['createdAt']
|
|
75
|
+
: new Date().toISOString();
|
|
64
76
|
const lastUpdatedAt = typeof value['lastUpdatedAt'] === 'string'
|
|
65
77
|
? value['lastUpdatedAt']
|
|
66
78
|
: createdAt;
|
|
@@ -90,51 +102,35 @@ function normalizeCallToolResult(value) {
|
|
|
90
102
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Stored task result is not a valid tool result.');
|
|
91
103
|
}
|
|
92
104
|
function withRelatedTaskMeta(result, taskId) {
|
|
93
|
-
if (!isRecord(result)) {
|
|
94
|
-
return {
|
|
95
|
-
_meta: {
|
|
96
|
-
[RELATED_TASK_META_KEY]: { taskId },
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
105
|
const existingMeta = isRecord(result['_meta']) ? result['_meta'] : {};
|
|
101
106
|
return {
|
|
102
107
|
...result,
|
|
103
|
-
_meta: {
|
|
104
|
-
...existingMeta,
|
|
105
|
-
[RELATED_TASK_META_KEY]: { taskId },
|
|
106
|
-
},
|
|
108
|
+
_meta: { ...existingMeta, [RELATED_TASK_META_KEY]: { taskId } },
|
|
107
109
|
};
|
|
108
110
|
}
|
|
109
|
-
function getTaskStatusNotificationSender(extra) {
|
|
110
|
-
const candidate = extra.sendNotification;
|
|
111
|
-
return typeof candidate === 'function'
|
|
112
|
-
? candidate
|
|
113
|
-
: undefined;
|
|
114
|
-
}
|
|
115
111
|
function buildTaskStatusNotificationParams(task) {
|
|
116
|
-
|
|
112
|
+
const params = {
|
|
117
113
|
taskId: task.taskId,
|
|
118
114
|
status: task.status,
|
|
119
115
|
ttl: task.ttl,
|
|
120
116
|
createdAt: task.createdAt,
|
|
121
117
|
lastUpdatedAt: task.lastUpdatedAt,
|
|
122
|
-
...(task.pollInterval !== undefined
|
|
123
|
-
? { pollInterval: task.pollInterval }
|
|
124
|
-
: {}),
|
|
125
|
-
...(task.statusMessage !== undefined
|
|
126
|
-
? { statusMessage: task.statusMessage }
|
|
127
|
-
: {}),
|
|
128
118
|
};
|
|
119
|
+
if (task.pollInterval !== undefined)
|
|
120
|
+
params.pollInterval = task.pollInterval;
|
|
121
|
+
if (task.statusMessage !== undefined)
|
|
122
|
+
params.statusMessage = task.statusMessage;
|
|
123
|
+
return params;
|
|
129
124
|
}
|
|
130
125
|
async function notifyTaskStatusIfPossible(extra, taskStore, taskId) {
|
|
131
|
-
const sendNotification =
|
|
132
|
-
if (
|
|
126
|
+
const { sendNotification } = extra;
|
|
127
|
+
if (typeof sendNotification !== 'function')
|
|
133
128
|
return;
|
|
129
|
+
const notify = sendNotification;
|
|
134
130
|
try {
|
|
135
131
|
const task = await taskStore.getTask(taskId);
|
|
136
132
|
const normalized = normalizeGetTaskResult(task);
|
|
137
|
-
await
|
|
133
|
+
await notify({
|
|
138
134
|
method: TASK_STATUS_NOTIFICATION_METHOD,
|
|
139
135
|
params: buildTaskStatusNotificationParams(normalized),
|
|
140
136
|
});
|
|
@@ -158,49 +154,75 @@ function getTaskId(extra) {
|
|
|
158
154
|
function isErrorResult(result) {
|
|
159
155
|
return 'isError' in result && result.isError === true;
|
|
160
156
|
}
|
|
161
|
-
const TERMINAL_TASK_STATUSES = new Set([
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return typeof status === 'string' ? status : undefined;
|
|
167
|
-
}
|
|
168
|
-
function isTerminalTaskStatus(status) {
|
|
169
|
-
if (!status)
|
|
170
|
-
return false;
|
|
171
|
-
return TERMINAL_TASK_STATUSES.has(status);
|
|
172
|
-
}
|
|
157
|
+
const TERMINAL_TASK_STATUSES = new Set([
|
|
158
|
+
'completed',
|
|
159
|
+
'failed',
|
|
160
|
+
'cancelled',
|
|
161
|
+
]);
|
|
173
162
|
function isTerminalTaskStoreError(error) {
|
|
174
163
|
const message = error instanceof Error ? error.message : String(error);
|
|
175
164
|
const normalized = message.toLowerCase();
|
|
176
165
|
return (normalized.includes('terminal status') ||
|
|
177
166
|
normalized.includes('task not found'));
|
|
178
167
|
}
|
|
179
|
-
async function
|
|
168
|
+
async function isTaskAlreadyTerminal(taskStore, taskId) {
|
|
180
169
|
try {
|
|
181
170
|
const task = await taskStore.getTask(taskId);
|
|
182
|
-
|
|
171
|
+
if (!isRecord(task))
|
|
172
|
+
return false;
|
|
173
|
+
const { status } = task;
|
|
174
|
+
return typeof status === 'string' && TERMINAL_TASK_STATUSES.has(status);
|
|
183
175
|
}
|
|
184
176
|
catch {
|
|
185
|
-
return
|
|
177
|
+
return false;
|
|
186
178
|
}
|
|
187
179
|
}
|
|
188
180
|
async function tryStoreTaskResult(taskStore, taskId, status, result) {
|
|
189
181
|
const resultWithTaskMeta = withRelatedTaskMeta(result, taskId);
|
|
190
|
-
const beforeStatus = await getCurrentTaskStatus(taskStore, taskId);
|
|
191
|
-
if (isTerminalTaskStatus(beforeStatus))
|
|
192
|
-
return;
|
|
193
182
|
try {
|
|
194
183
|
await taskStore.storeTaskResult(taskId, status, resultWithTaskMeta);
|
|
195
184
|
}
|
|
196
185
|
catch (error) {
|
|
197
|
-
|
|
198
|
-
|
|
186
|
+
if (isTerminalTaskStoreError(error) ||
|
|
187
|
+
(await isTaskAlreadyTerminal(taskStore, taskId)))
|
|
199
188
|
return;
|
|
200
|
-
}
|
|
201
189
|
throw error;
|
|
202
190
|
}
|
|
203
191
|
}
|
|
192
|
+
async function runTaskInBackground(run, args, extra, taskStore, taskId) {
|
|
193
|
+
try {
|
|
194
|
+
const result = await run(args, extra);
|
|
195
|
+
const status = isErrorResult(result) ? 'failed' : 'completed';
|
|
196
|
+
await tryStoreTaskResult(taskStore, taskId, status, result);
|
|
197
|
+
await notifyTaskStatusIfPossible(extra, taskStore, taskId);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const fallback = buildToolErrorResponse(error, ErrorCode.E_UNKNOWN);
|
|
201
|
+
try {
|
|
202
|
+
await tryStoreTaskResult(taskStore, taskId, 'failed', fallback);
|
|
203
|
+
await notifyTaskStatusIfPossible(extra, taskStore, taskId);
|
|
204
|
+
}
|
|
205
|
+
catch (innerError) {
|
|
206
|
+
console.error(`Failed to store task failure result for task ${taskId}:`, innerError);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Registers a tool preferring task-capable registration when available, and
|
|
212
|
+
* returns `true`. Returns `false` so the caller can fall through to standard
|
|
213
|
+
* `server.registerTool`.
|
|
214
|
+
*/
|
|
215
|
+
export function tryRegisterToolTask(server, toolName, toolDef, taskHandler, iconInfo) {
|
|
216
|
+
const tasks = getExperimentalTaskRegistration(server);
|
|
217
|
+
if (!tasks?.registerToolTask)
|
|
218
|
+
return false;
|
|
219
|
+
tasks.registerToolTask(toolName, withDefaultIcons({ ...toolDef, execution: { taskSupport: 'optional' } }, iconInfo), taskHandler);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
export function registerToolTaskIfAvailable(server, toolName, toolDef, run, iconInfo, guard) {
|
|
223
|
+
const taskOptions = guard ? { guard } : undefined;
|
|
224
|
+
return tryRegisterToolTask(server, toolName, toolDef, createToolTaskHandler(run, taskOptions), iconInfo);
|
|
225
|
+
}
|
|
204
226
|
export function createToolTaskHandler(run, options) {
|
|
205
227
|
const createTask = (async (argsOrExtra, maybeExtra) => {
|
|
206
228
|
const extra = asCreateTaskExtra(maybeExtra ?? argsOrExtra);
|
|
@@ -218,24 +240,7 @@ export function createToolTaskHandler(run, options) {
|
|
|
218
240
|
taskId: task.taskId,
|
|
219
241
|
};
|
|
220
242
|
void notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId);
|
|
221
|
-
void (
|
|
222
|
-
try {
|
|
223
|
-
const result = await run(args, taskExtra);
|
|
224
|
-
const status = isErrorResult(result) ? 'failed' : 'completed';
|
|
225
|
-
await tryStoreTaskResult(taskStore, task.taskId, status, result);
|
|
226
|
-
await notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId);
|
|
227
|
-
}
|
|
228
|
-
catch (error) {
|
|
229
|
-
const fallback = buildToolErrorResponse(error, ErrorCode.E_UNKNOWN);
|
|
230
|
-
try {
|
|
231
|
-
await tryStoreTaskResult(taskStore, task.taskId, 'failed', fallback);
|
|
232
|
-
await notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId);
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
// Swallow to avoid unhandled rejections from background task writes.
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
})();
|
|
243
|
+
void runTaskInBackground(run, args, taskExtra, taskStore, task.taskId);
|
|
239
244
|
return { task };
|
|
240
245
|
});
|
|
241
246
|
const getTask = (async (argsOrExtra, maybeExtra) => {
|
package/dist/tools/tree.js
CHANGED
|
@@ -2,11 +2,9 @@ import * as path from 'node:path';
|
|
|
2
2
|
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
|
-
import { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
6
|
-
import { withToolDiagnostics } from '../lib/observability.js';
|
|
7
5
|
import { TreeInputSchema, TreeOutputSchema } from '../schemas.js';
|
|
8
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
9
|
-
import {
|
|
6
|
+
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
10
8
|
const TREE_TOOL = {
|
|
11
9
|
title: 'Tree',
|
|
12
10
|
description: 'Render a directory tree (bounded recursion). ' +
|
|
@@ -14,11 +12,7 @@ const TREE_TOOL = {
|
|
|
14
12
|
'Note: maxDepth=0 returns only the root node with empty children array.',
|
|
15
13
|
inputSchema: TreeInputSchema,
|
|
16
14
|
outputSchema: TreeOutputSchema,
|
|
17
|
-
annotations:
|
|
18
|
-
readOnlyHint: true,
|
|
19
|
-
idempotentHint: true,
|
|
20
|
-
openWorldHint: false,
|
|
21
|
-
},
|
|
15
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
22
16
|
};
|
|
23
17
|
async function handleTree(args, signal) {
|
|
24
18
|
const basePath = resolvePathOrRoot(args.path);
|
|
@@ -44,15 +38,14 @@ async function handleTree(args, signal) {
|
|
|
44
38
|
export function registerTreeTool(server, options = {}) {
|
|
45
39
|
const handler = (args, extra) => {
|
|
46
40
|
const targetPath = args.path ?? '.';
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}, (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, targetPath)), { path: targetPath });
|
|
41
|
+
return executeToolWithDiagnostics({
|
|
42
|
+
toolName: 'tree',
|
|
43
|
+
extra,
|
|
44
|
+
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
45
|
+
context: { path: targetPath },
|
|
46
|
+
run: (signal) => handleTree(args, signal),
|
|
47
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, targetPath),
|
|
48
|
+
});
|
|
56
49
|
};
|
|
57
50
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
58
51
|
guard: options.isInitialized,
|
|
@@ -63,16 +56,7 @@ export function registerTreeTool(server, options = {}) {
|
|
|
63
56
|
return '≣ tree';
|
|
64
57
|
},
|
|
65
58
|
});
|
|
66
|
-
|
|
67
|
-
? { guard: options.isInitialized }
|
|
68
|
-
: undefined;
|
|
69
|
-
const tasks = getExperimentalTaskRegistration(server);
|
|
70
|
-
if (tasks?.registerToolTask) {
|
|
71
|
-
tasks.registerToolTask('tree', withDefaultIcons({
|
|
72
|
-
...TREE_TOOL,
|
|
73
|
-
execution: { taskSupport: 'optional' },
|
|
74
|
-
}, options.iconInfo), createToolTaskHandler(wrappedHandler, taskOptions));
|
|
59
|
+
if (registerToolTaskIfAvailable(server, 'tree', TREE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
75
60
|
return;
|
|
76
|
-
}
|
|
77
61
|
server.registerTool('tree', withDefaultIcons({ ...TREE_TOOL }, options.iconInfo), wrappedHandler);
|
|
78
62
|
}
|
package/dist/tools/write-file.js
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
|
-
import { atomicWriteFile,
|
|
5
|
-
import { withToolDiagnostics } from '../lib/observability.js';
|
|
4
|
+
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
6
5
|
import { validatePathForWrite } from '../lib/path-validation.js';
|
|
7
6
|
import { WriteFileInputSchema, WriteFileOutputSchema } from '../schemas.js';
|
|
8
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
7
|
+
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
const WRITE_FILE_TOOL = {
|
|
10
10
|
title: 'Write File',
|
|
11
11
|
description: 'Write content to a file. Creates the file if it does not exist.',
|
|
12
12
|
inputSchema: WriteFileInputSchema,
|
|
13
13
|
outputSchema: WriteFileOutputSchema,
|
|
14
|
-
annotations:
|
|
15
|
-
readOnlyHint: false,
|
|
16
|
-
destructiveHint: true,
|
|
17
|
-
openWorldHint: false,
|
|
18
|
-
},
|
|
14
|
+
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
19
15
|
};
|
|
20
16
|
async function handleWriteFile(args, signal) {
|
|
21
17
|
const validPath = await validatePathForWrite(args.path, signal);
|
|
@@ -30,17 +26,19 @@ async function handleWriteFile(args, signal) {
|
|
|
30
26
|
});
|
|
31
27
|
}
|
|
32
28
|
export function registerWriteFileTool(server, options = {}) {
|
|
33
|
-
const handler = (args, extra) =>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
server.registerTool('write', withDefaultIcons({ ...WRITE_FILE_TOOL }, options.iconInfo), wrapToolHandler(handler, {
|
|
29
|
+
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
30
|
+
toolName: 'write',
|
|
31
|
+
extra,
|
|
32
|
+
timedSignal: {},
|
|
33
|
+
context: { path: args.path },
|
|
34
|
+
run: (signal) => handleWriteFile(args, signal),
|
|
35
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
36
|
+
});
|
|
37
|
+
const wrappedHandler = wrapToolHandler(handler, {
|
|
43
38
|
guard: options.isInitialized,
|
|
44
39
|
progressMessage: (args) => `🛠 write: ${path.basename(args.path)}`,
|
|
45
|
-
})
|
|
40
|
+
});
|
|
41
|
+
if (registerToolTaskIfAvailable(server, 'write', WRITE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
42
|
+
return;
|
|
43
|
+
server.registerTool('write', withDefaultIcons({ ...WRITE_FILE_TOOL }, options.iconInfo), wrappedHandler);
|
|
46
44
|
}
|
package/dist/tools.js
CHANGED
|
@@ -17,23 +17,28 @@ import { registerGetFileInfoTool } from './tools/stat.js';
|
|
|
17
17
|
import { registerTreeTool } from './tools/tree.js';
|
|
18
18
|
import { registerWriteFileTool } from './tools/write-file.js';
|
|
19
19
|
export { buildToolErrorResponse, buildToolResponse } from './tools/shared.js';
|
|
20
|
+
const TOOL_REGISTRARS = [
|
|
21
|
+
registerListAllowedDirectoriesTool,
|
|
22
|
+
registerListDirectoryTool,
|
|
23
|
+
registerSearchFilesTool,
|
|
24
|
+
registerTreeTool,
|
|
25
|
+
registerReadFileTool,
|
|
26
|
+
registerReadMultipleFilesTool,
|
|
27
|
+
registerGetFileInfoTool,
|
|
28
|
+
registerGetMultipleFileInfoTool,
|
|
29
|
+
registerSearchContentTool,
|
|
30
|
+
registerCreateDirectoryTool,
|
|
31
|
+
registerWriteFileTool,
|
|
32
|
+
registerEditFileTool,
|
|
33
|
+
registerMoveFileTool,
|
|
34
|
+
registerDeleteFileTool,
|
|
35
|
+
registerCalculateHashTool,
|
|
36
|
+
registerDiffFilesTool,
|
|
37
|
+
registerApplyPatchTool,
|
|
38
|
+
registerSearchAndReplaceTool,
|
|
39
|
+
];
|
|
20
40
|
export function registerAllTools(server, options = {}) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
registerTreeTool(server, options);
|
|
25
|
-
registerReadFileTool(server, options);
|
|
26
|
-
registerReadMultipleFilesTool(server, options);
|
|
27
|
-
registerGetFileInfoTool(server, options);
|
|
28
|
-
registerGetMultipleFileInfoTool(server, options);
|
|
29
|
-
registerSearchContentTool(server, options);
|
|
30
|
-
registerCreateDirectoryTool(server, options);
|
|
31
|
-
registerWriteFileTool(server, options);
|
|
32
|
-
registerEditFileTool(server, options);
|
|
33
|
-
registerMoveFileTool(server, options);
|
|
34
|
-
registerDeleteFileTool(server, options);
|
|
35
|
-
registerCalculateHashTool(server, options);
|
|
36
|
-
registerDiffFilesTool(server, options);
|
|
37
|
-
registerApplyPatchTool(server, options);
|
|
38
|
-
registerSearchAndReplaceTool(server, options);
|
|
41
|
+
for (const registerTool of TOOL_REGISTRARS) {
|
|
42
|
+
registerTool(server, options);
|
|
43
|
+
}
|
|
39
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/filesystem-mcp",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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",
|
|
@@ -71,21 +71,20 @@
|
|
|
71
71
|
"zod": "^4.3.6"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
|
-
"@eslint/js": "^
|
|
74
|
+
"@eslint/js": "^10.0.1",
|
|
75
|
+
"eslint": "^10.0.0",
|
|
75
76
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
76
77
|
"@types/node": "^24",
|
|
77
|
-
"eslint": "^9.39.2",
|
|
78
78
|
"eslint-config-prettier": "^10.1.8",
|
|
79
79
|
"eslint-plugin-de-morgan": "^2.0.0",
|
|
80
80
|
"eslint-plugin-depend": "^1.4.0",
|
|
81
|
-
"eslint-plugin-
|
|
82
|
-
"eslint-plugin-unused-imports": "^4.3.0",
|
|
81
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
83
82
|
"jscpd": "^4.0.8",
|
|
84
|
-
"knip": "^5.
|
|
83
|
+
"knip": "^5.84.1",
|
|
85
84
|
"prettier": "^3.8.1",
|
|
86
85
|
"tsx": "^4.21.0",
|
|
87
86
|
"typescript": "^5.9.3",
|
|
88
|
-
"typescript-eslint": "^8.
|
|
87
|
+
"typescript-eslint": "^8.56.0"
|
|
89
88
|
},
|
|
90
89
|
"engines": {
|
|
91
90
|
"node": ">=24"
|