@j0hanz/filesystem-mcp 1.11.0 → 1.12.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/dist/completions.js +25 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/file-operations/metadata.js +71 -142
- package/dist/lib/file-operations/search.js +119 -19
- package/dist/lib/file-operations/traversal.js +45 -102
- package/dist/lib/fs-helpers.js +175 -158
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.js +58 -0
- package/dist/resources/generated-instructions.js +11 -3
- package/dist/resources/tool-catalog.js +49 -3
- package/dist/resources/tool-info.d.ts +1 -0
- package/dist/resources/tool-info.js +139 -8
- package/dist/resources.js +2 -0
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.js +10 -4
- package/dist/server/bootstrap.js +41 -5
- package/dist/tools/apply-patch.js +30 -33
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/contract.d.ts +1 -1
- package/dist/tools/create-directory.js +3 -3
- package/dist/tools/delete-file.js +2 -2
- package/dist/tools/diff-files.js +32 -20
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +172 -104
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/move-file.js +3 -3
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +123 -99
- package/dist/tools/read.js +104 -76
- package/dist/tools/replace-in-files.d.ts +5 -2
- package/dist/tools/replace-in-files.js +142 -76
- package/dist/tools/roots.js +1 -1
- package/dist/tools/search-content.js +249 -218
- package/dist/tools/shared.js +39 -7
- package/dist/tools/stat-many.js +12 -19
- package/dist/tools/stat.js +1 -1
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +140 -101
- package/dist/tools/write-file.js +4 -4
- package/dist/tools.js +2 -2
- package/package.json +1 -1
|
@@ -1,11 +1,22 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
1
2
|
import { channel } from 'node:diagnostics_channel';
|
|
3
|
+
import { performance } from 'node:perf_hooks';
|
|
4
|
+
import { format } from 'node:util';
|
|
2
5
|
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
-
import { DEFAULT_TASK_TTL_MS } from '../lib/constants.js';
|
|
6
|
+
import { DEFAULT_TASK_TTL_MS, MAX_CONCURRENT_TASKS, MAX_TASK_TTL_MS, } from '../lib/constants.js';
|
|
4
7
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
5
8
|
import { isRecord } from '../lib/utils.js';
|
|
6
9
|
import { buildToolErrorResponse, maybeStripStructuredContentFromResult, withDefaultIcons, } from './shared.js';
|
|
10
|
+
export const taskContext = new AsyncLocalStorage();
|
|
11
|
+
const TASK_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:tasks');
|
|
12
|
+
function publishTaskDiagnostics(event) {
|
|
13
|
+
if (TASK_DIAGNOSTICS_CHANNEL.hasSubscribers) {
|
|
14
|
+
TASK_DIAGNOSTICS_CHANNEL.publish(event);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// --- Type Guards & Helpers ---
|
|
7
18
|
function isExperimentalTaskRegistration(value) {
|
|
8
|
-
if (!value
|
|
19
|
+
if (!isRecord(value))
|
|
9
20
|
return false;
|
|
10
21
|
const { registerToolTask } = value;
|
|
11
22
|
return (registerToolTask === undefined || typeof registerToolTask === 'function');
|
|
@@ -13,7 +24,7 @@ function isExperimentalTaskRegistration(value) {
|
|
|
13
24
|
function getExperimentalTaskRegistration(server) {
|
|
14
25
|
const serverWithExperimental = server;
|
|
15
26
|
const { experimental } = serverWithExperimental;
|
|
16
|
-
if (!experimental
|
|
27
|
+
if (!isRecord(experimental))
|
|
17
28
|
return undefined;
|
|
18
29
|
const { tasks } = experimental;
|
|
19
30
|
if (!isExperimentalTaskRegistration(tasks))
|
|
@@ -24,49 +35,34 @@ function hasTaskToolCapability(server) {
|
|
|
24
35
|
const serverRecord = server;
|
|
25
36
|
const { server: serverRuntime } = serverRecord;
|
|
26
37
|
if (!isRecord(serverRuntime))
|
|
27
|
-
return true;
|
|
38
|
+
return true; // Assume capability if runtime structure is opaque
|
|
28
39
|
const { getCapabilities } = serverRuntime;
|
|
29
|
-
if (typeof getCapabilities !== 'function')
|
|
30
|
-
// Fallback for tests or custom wrappers that provide only registerTool/experimental.
|
|
40
|
+
if (typeof getCapabilities !== 'function')
|
|
31
41
|
return true;
|
|
32
|
-
}
|
|
33
42
|
const capabilities = getCapabilities.call(serverRuntime);
|
|
34
43
|
if (!isRecord(capabilities))
|
|
35
44
|
return false;
|
|
36
45
|
const { tasks } = capabilities;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return false;
|
|
42
|
-
const { tools } = requests;
|
|
43
|
-
if (!isRecord(tools))
|
|
44
|
-
return false;
|
|
45
|
-
const { call } = tools;
|
|
46
|
-
return isRecord(call);
|
|
46
|
+
return (isRecord(tasks) &&
|
|
47
|
+
isRecord(tasks.requests) &&
|
|
48
|
+
isRecord(tasks.requests.tools) &&
|
|
49
|
+
isRecord(tasks.requests.tools.call));
|
|
47
50
|
}
|
|
48
51
|
const TASK_STATUS_NOTIFICATION_METHOD = 'notifications/tasks/status';
|
|
49
|
-
const TASK_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:tasks');
|
|
50
|
-
function publishTaskDiagnostics(event) {
|
|
51
|
-
if (!TASK_DIAGNOSTICS_CHANNEL.hasSubscribers)
|
|
52
|
-
return;
|
|
53
|
-
TASK_DIAGNOSTICS_CHANNEL.publish(event);
|
|
54
|
-
}
|
|
55
52
|
function isRequestTaskStore(value) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
typeof value
|
|
60
|
-
typeof value
|
|
61
|
-
typeof value['getTaskResult'] === 'function');
|
|
53
|
+
return (isRecord(value) &&
|
|
54
|
+
typeof value.createTask === 'function' &&
|
|
55
|
+
typeof value.getTask === 'function' &&
|
|
56
|
+
typeof value.storeTaskResult === 'function' &&
|
|
57
|
+
typeof value.getTaskResult === 'function');
|
|
62
58
|
}
|
|
63
59
|
function isCreateTaskExtra(value) {
|
|
64
|
-
return isRecord(value) && isRequestTaskStore(value
|
|
60
|
+
return isRecord(value) && isRequestTaskStore(value.taskStore);
|
|
65
61
|
}
|
|
66
62
|
function isTaskExtra(value) {
|
|
67
63
|
return (isCreateTaskExtra(value) &&
|
|
68
|
-
typeof value
|
|
69
|
-
value
|
|
64
|
+
typeof value.taskId === 'string' &&
|
|
65
|
+
value.taskId.length > 0);
|
|
70
66
|
}
|
|
71
67
|
function asCreateTaskExtra(value) {
|
|
72
68
|
if (!isCreateTaskExtra(value)) {
|
|
@@ -91,39 +87,34 @@ function isTaskStatus(value) {
|
|
|
91
87
|
return (typeof value === 'string' &&
|
|
92
88
|
TASK_STATUSES.has(value));
|
|
93
89
|
}
|
|
94
|
-
function parseTaskStatus(value) {
|
|
95
|
-
return isTaskStatus(value) ? value : undefined;
|
|
96
|
-
}
|
|
97
90
|
function normalizeGetTaskResult(value) {
|
|
98
|
-
if (!isRecord(value) || typeof value
|
|
91
|
+
if (!isRecord(value) || typeof value.taskId !== 'string') {
|
|
99
92
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Invalid task object.');
|
|
100
93
|
}
|
|
101
|
-
const status =
|
|
94
|
+
const status = isTaskStatus(value.status) ? value.status : undefined;
|
|
102
95
|
if (!status) {
|
|
103
96
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Invalid task status.');
|
|
104
97
|
}
|
|
105
|
-
const createdAt = typeof value
|
|
106
|
-
? value
|
|
98
|
+
const createdAt = typeof value.createdAt === 'string'
|
|
99
|
+
? value.createdAt
|
|
107
100
|
: new Date().toISOString();
|
|
108
|
-
const lastUpdatedAt = typeof value
|
|
109
|
-
|
|
110
|
-
: createdAt;
|
|
111
|
-
const ttl = typeof value['ttl'] === 'number' ? value['ttl'] : null;
|
|
101
|
+
const lastUpdatedAt = typeof value.lastUpdatedAt === 'string' ? value.lastUpdatedAt : createdAt;
|
|
102
|
+
const ttl = typeof value.ttl === 'number' ? value.ttl : null;
|
|
112
103
|
const normalized = {
|
|
113
|
-
taskId: value
|
|
104
|
+
taskId: value.taskId,
|
|
114
105
|
status,
|
|
115
106
|
ttl,
|
|
116
107
|
createdAt,
|
|
117
108
|
lastUpdatedAt,
|
|
118
109
|
};
|
|
119
|
-
if (typeof value
|
|
120
|
-
normalized.pollInterval = value
|
|
110
|
+
if (typeof value.pollInterval === 'number') {
|
|
111
|
+
normalized.pollInterval = value.pollInterval;
|
|
121
112
|
}
|
|
122
|
-
if (typeof value
|
|
123
|
-
normalized.statusMessage = value
|
|
113
|
+
if (typeof value.statusMessage === 'string') {
|
|
114
|
+
normalized.statusMessage = value.statusMessage;
|
|
124
115
|
}
|
|
125
|
-
if (isRecord(value
|
|
126
|
-
normalized._meta = value
|
|
116
|
+
if (isRecord(value._meta)) {
|
|
117
|
+
normalized._meta = value._meta;
|
|
127
118
|
}
|
|
128
119
|
return normalized;
|
|
129
120
|
}
|
|
@@ -134,17 +125,15 @@ function normalizeCallToolResult(value) {
|
|
|
134
125
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Stored task result is not a valid tool result.');
|
|
135
126
|
}
|
|
136
127
|
function getToolResultErrorCode(result) {
|
|
137
|
-
if (!isRecord(result) || result
|
|
128
|
+
if (!isRecord(result) || result.isError !== true)
|
|
138
129
|
return undefined;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return result['errorCode'];
|
|
142
|
-
// Fallback to regex parsing of the human-readable error message for older error results that lack a structured errorCode property.
|
|
130
|
+
if (typeof result.errorCode === 'string')
|
|
131
|
+
return result.errorCode;
|
|
143
132
|
const { content } = result;
|
|
144
133
|
if (!Array.isArray(content) || content.length === 0)
|
|
145
134
|
return undefined;
|
|
146
135
|
const first = content[0];
|
|
147
|
-
if (!isRecord(first) || first
|
|
136
|
+
if (!isRecord(first) || first.type !== 'text')
|
|
148
137
|
return undefined;
|
|
149
138
|
const { text } = first;
|
|
150
139
|
if (typeof text !== 'string')
|
|
@@ -155,6 +144,25 @@ function getToolResultErrorCode(result) {
|
|
|
155
144
|
function isCancelledToolResult(result) {
|
|
156
145
|
return getToolResultErrorCode(result) === ErrorCode.E_CANCELLED;
|
|
157
146
|
}
|
|
147
|
+
function resolveTaskResultStatuses(result) {
|
|
148
|
+
if (isCancelledToolResult(result)) {
|
|
149
|
+
return { storedStatus: 'failed', reportedStatus: 'cancelled' };
|
|
150
|
+
}
|
|
151
|
+
if (isRecord(result) && result.isError === true) {
|
|
152
|
+
return { storedStatus: 'failed', reportedStatus: 'failed' };
|
|
153
|
+
}
|
|
154
|
+
return { storedStatus: 'completed', reportedStatus: 'completed' };
|
|
155
|
+
}
|
|
156
|
+
function attachRelatedTaskMeta(result, taskId) {
|
|
157
|
+
const existingMeta = isRecord(result._meta) ? result._meta : {};
|
|
158
|
+
return {
|
|
159
|
+
...result,
|
|
160
|
+
_meta: {
|
|
161
|
+
...existingMeta,
|
|
162
|
+
'io.modelcontextprotocol/related-task': { taskId },
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
158
166
|
async function projectCancelledTaskStatus(taskStore, task) {
|
|
159
167
|
if (task.status !== 'failed')
|
|
160
168
|
return task;
|
|
@@ -199,14 +207,14 @@ async function notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName) {
|
|
|
199
207
|
phase: 'task_status_notified',
|
|
200
208
|
taskId,
|
|
201
209
|
status: normalized.status,
|
|
202
|
-
...(toolName
|
|
210
|
+
...(toolName ? { toolName } : {}),
|
|
203
211
|
});
|
|
204
212
|
}
|
|
205
213
|
catch {
|
|
206
214
|
publishTaskDiagnostics({
|
|
207
215
|
phase: 'task_status_notify_failed',
|
|
208
216
|
taskId,
|
|
209
|
-
...(toolName
|
|
217
|
+
...(toolName ? { toolName } : {}),
|
|
210
218
|
});
|
|
211
219
|
// Never fail task execution because status notifications are optional.
|
|
212
220
|
}
|
|
@@ -226,7 +234,7 @@ function getTaskId(extra) {
|
|
|
226
234
|
function isErrorResult(result) {
|
|
227
235
|
return 'isError' in result && result.isError;
|
|
228
236
|
}
|
|
229
|
-
// Strips structuredContent from a tool result if present, without modifying the original object.
|
|
237
|
+
// Strips structuredContent from a tool result if present, without modifying the original object.
|
|
230
238
|
function withoutStructuredContent(result) {
|
|
231
239
|
if (!Object.hasOwn(result, 'structuredContent'))
|
|
232
240
|
return result;
|
|
@@ -251,61 +259,89 @@ async function isTaskAlreadyTerminal(taskStore, taskId) {
|
|
|
251
259
|
return false;
|
|
252
260
|
}
|
|
253
261
|
}
|
|
254
|
-
async function
|
|
262
|
+
async function countActiveTasks(taskStore) {
|
|
263
|
+
if (typeof taskStore.listTasks !== 'function')
|
|
264
|
+
return 0;
|
|
265
|
+
const listed = await taskStore.listTasks();
|
|
266
|
+
const tasks = Array.isArray(listed.tasks) ? listed.tasks : [];
|
|
267
|
+
let active = 0;
|
|
268
|
+
for (const task of tasks) {
|
|
269
|
+
if (!isRecord(task) || typeof task.status !== 'string')
|
|
270
|
+
continue;
|
|
271
|
+
if (task.status !== 'completed' &&
|
|
272
|
+
task.status !== 'failed' &&
|
|
273
|
+
task.status !== 'cancelled') {
|
|
274
|
+
active += 1;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return active;
|
|
278
|
+
}
|
|
279
|
+
function resolveRequestedTaskTtl(requestedTtl) {
|
|
280
|
+
if (requestedTtl == null)
|
|
281
|
+
return DEFAULT_TASK_TTL_MS;
|
|
282
|
+
if (!Number.isFinite(requestedTtl)) {
|
|
283
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Task ttl must be a finite number of milliseconds.');
|
|
284
|
+
}
|
|
285
|
+
const normalized = Math.trunc(requestedTtl);
|
|
286
|
+
if (normalized <= 0) {
|
|
287
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Task ttl must be greater than zero.');
|
|
288
|
+
}
|
|
289
|
+
return Math.min(normalized, MAX_TASK_TTL_MS);
|
|
290
|
+
}
|
|
291
|
+
async function safelyStoreTaskResult(taskStore, taskId, storedStatus, result) {
|
|
255
292
|
try {
|
|
256
|
-
await taskStore.storeTaskResult(taskId,
|
|
293
|
+
await taskStore.storeTaskResult(taskId, storedStatus, result);
|
|
257
294
|
}
|
|
258
295
|
catch (error) {
|
|
296
|
+
// If task was already cancelled/failed by another process, ignore the write error
|
|
259
297
|
if (await isTaskAlreadyTerminal(taskStore, taskId))
|
|
260
298
|
return;
|
|
261
299
|
throw error;
|
|
262
300
|
}
|
|
263
301
|
}
|
|
264
302
|
async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName) {
|
|
303
|
+
const start = performance.now();
|
|
304
|
+
let taskStatuses;
|
|
305
|
+
let result;
|
|
265
306
|
try {
|
|
266
|
-
const rawResult = await run(args, extra);
|
|
267
|
-
|
|
268
|
-
|
|
307
|
+
const rawResult = await taskContext.run({ taskId, toolName, startTime: start }, () => run(args, extra));
|
|
308
|
+
taskStatuses = resolveTaskResultStatuses(rawResult);
|
|
309
|
+
result = isErrorResult(rawResult)
|
|
269
310
|
? withoutStructuredContent(rawResult)
|
|
270
311
|
: maybeStripStructuredContentFromResult(rawResult);
|
|
271
|
-
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
taskStatuses = { storedStatus: 'failed', reportedStatus: 'failed' };
|
|
315
|
+
result = maybeStripStructuredContentFromResult(buildToolErrorResponse(error, ErrorCode.E_UNKNOWN));
|
|
316
|
+
}
|
|
317
|
+
const durationMs = performance.now() - start;
|
|
318
|
+
try {
|
|
319
|
+
await safelyStoreTaskResult(taskStore, taskId, taskStatuses.storedStatus, result);
|
|
272
320
|
publishTaskDiagnostics({
|
|
273
321
|
phase: 'task_result_stored',
|
|
274
322
|
taskId,
|
|
275
|
-
status,
|
|
276
|
-
|
|
323
|
+
status: taskStatuses.reportedStatus,
|
|
324
|
+
toolName,
|
|
325
|
+
durationMs,
|
|
277
326
|
});
|
|
278
327
|
await notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName);
|
|
279
328
|
}
|
|
280
|
-
catch (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
329
|
+
catch (innerError) {
|
|
330
|
+
console.error(format('Failed to store task result for task %s:', taskId), innerError);
|
|
331
|
+
const syntheticTask = {
|
|
332
|
+
taskId,
|
|
333
|
+
status: taskStatuses.reportedStatus === 'cancelled' ? 'cancelled' : 'failed',
|
|
334
|
+
ttl: null,
|
|
335
|
+
createdAt: new Date().toISOString(),
|
|
336
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
337
|
+
statusMessage: 'Internal system error while storing result',
|
|
338
|
+
};
|
|
339
|
+
const { sendNotification } = extra;
|
|
340
|
+
if (typeof sendNotification === 'function') {
|
|
341
|
+
void sendNotification({
|
|
342
|
+
method: TASK_STATUS_NOTIFICATION_METHOD,
|
|
343
|
+
params: buildTaskStatusNotificationParams(syntheticTask),
|
|
289
344
|
});
|
|
290
|
-
await notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName);
|
|
291
|
-
}
|
|
292
|
-
catch (innerError) {
|
|
293
|
-
console.error(`Failed to store task failure result for task ${taskId}:`, innerError);
|
|
294
|
-
// If storing the failure result also fails, there's not much we can do. The task will remain in 'working' status until it expires, which is not ideal but at least prevents clients from receiving incorrect results or hanging indefinitely waiting for a result that will never arrive. We log the error to aid debugging, and we attempt to notify the client of the failure if possible, but we don't want to throw further errors that could crash the server or cause cascading failures.
|
|
295
|
-
const syntheticTask = {
|
|
296
|
-
taskId,
|
|
297
|
-
status: 'failed',
|
|
298
|
-
ttl: null,
|
|
299
|
-
createdAt: new Date().toISOString(),
|
|
300
|
-
lastUpdatedAt: new Date().toISOString(),
|
|
301
|
-
};
|
|
302
|
-
const { sendNotification } = extra;
|
|
303
|
-
if (typeof sendNotification === 'function') {
|
|
304
|
-
void sendNotification({
|
|
305
|
-
method: TASK_STATUS_NOTIFICATION_METHOD,
|
|
306
|
-
params: buildTaskStatusNotificationParams(syntheticTask),
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
345
|
}
|
|
310
346
|
}
|
|
311
347
|
}
|
|
@@ -324,7 +360,9 @@ function tryRegisterToolTask(server, toolName, toolDef, taskHandler, iconInfo) {
|
|
|
324
360
|
const existingExecution = def.execution ?? {};
|
|
325
361
|
const taskSupport = def.taskSupport ??
|
|
326
362
|
existingExecution.taskSupport ??
|
|
327
|
-
'
|
|
363
|
+
'forbidden';
|
|
364
|
+
if (taskSupport === 'forbidden')
|
|
365
|
+
return false;
|
|
328
366
|
tasks.registerToolTask(toolName, withDefaultIcons({ ...toolDef, execution: { ...existingExecution, taskSupport } }, iconInfo), taskHandler);
|
|
329
367
|
return true;
|
|
330
368
|
}
|
|
@@ -343,16 +381,17 @@ export function createToolTaskHandler(run, options) {
|
|
|
343
381
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Client not initialized; wait for notifications/initialized');
|
|
344
382
|
}
|
|
345
383
|
const taskStore = getTaskStore(extra);
|
|
384
|
+
if ((await countActiveTasks(taskStore)) >= MAX_CONCURRENT_TASKS) {
|
|
385
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `Too many active tasks. Limit: ${String(MAX_CONCURRENT_TASKS)}.`);
|
|
386
|
+
}
|
|
346
387
|
const task = await taskStore.createTask({
|
|
347
|
-
ttl: extra.taskRequestedTtl
|
|
388
|
+
ttl: resolveRequestedTaskTtl(extra.taskRequestedTtl),
|
|
348
389
|
});
|
|
349
390
|
publishTaskDiagnostics({
|
|
350
391
|
phase: 'task_created',
|
|
351
392
|
taskId: task.taskId,
|
|
352
393
|
status: task.status,
|
|
353
|
-
...(options?.toolName
|
|
354
|
-
? { toolName: options.toolName }
|
|
355
|
-
: {}),
|
|
394
|
+
...(options?.toolName ? { toolName: options.toolName } : {}),
|
|
356
395
|
});
|
|
357
396
|
const taskExtra = {
|
|
358
397
|
...extra,
|
|
@@ -375,7 +414,7 @@ export function createToolTaskHandler(run, options) {
|
|
|
375
414
|
const taskStore = getTaskStore(extra);
|
|
376
415
|
const taskId = getTaskId(extra);
|
|
377
416
|
const result = await taskStore.getTaskResult(taskId);
|
|
378
|
-
return normalizeCallToolResult(result);
|
|
417
|
+
return attachRelatedTaskMeta(normalizeCallToolResult(result), taskId);
|
|
379
418
|
});
|
|
380
419
|
return {
|
|
381
420
|
createTask,
|
package/dist/tools/write-file.js
CHANGED
|
@@ -17,18 +17,18 @@ export const WRITE_FILE_TOOL = {
|
|
|
17
17
|
gotchas: [
|
|
18
18
|
'`write` replaces ALL existing content — use `edit` for partial updates.',
|
|
19
19
|
],
|
|
20
|
-
taskSupport: '
|
|
20
|
+
taskSupport: 'forbidden',
|
|
21
21
|
};
|
|
22
22
|
async function handleWriteFile(args, signal) {
|
|
23
23
|
const validPath = await validatePathForWrite(args.path, signal);
|
|
24
24
|
// Ensure parent directory exists
|
|
25
25
|
await withAbort(fs.mkdir(path.dirname(validPath), { recursive: true }), signal);
|
|
26
26
|
await atomicWriteFile(validPath, args.content, { encoding: 'utf-8', signal });
|
|
27
|
-
const
|
|
27
|
+
const bytesWritten = Buffer.byteLength(args.content, 'utf-8');
|
|
28
28
|
return buildToolResponse(`Successfully wrote to file: ${args.path}`, {
|
|
29
29
|
ok: true,
|
|
30
30
|
path: validPath,
|
|
31
|
-
bytesWritten
|
|
31
|
+
bytesWritten,
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
export function registerWriteFileTool(server, options = {}) {
|
|
@@ -50,7 +50,7 @@ export function registerWriteFileTool(server, options = {}) {
|
|
|
50
50
|
const sc = result.structuredContent;
|
|
51
51
|
if (!sc.ok)
|
|
52
52
|
return `🛠 write: ${name} • failed`;
|
|
53
|
-
return `🛠 write: ${name} • ${formatBytes(sc.bytesWritten ?? 0)}
|
|
53
|
+
return `🛠 write: ${name} • ${formatBytes(sc.bytesWritten ?? 0)}`;
|
|
54
54
|
},
|
|
55
55
|
});
|
|
56
56
|
const validatedHandler = withValidatedArgs(WriteFileInputSchema, wrappedHandler);
|
package/dist/tools.js
CHANGED
|
@@ -7,7 +7,7 @@ import { DIFF_FILES_TOOL, registerDiffFilesTool } from './tools/diff-files.js';
|
|
|
7
7
|
import { EDIT_FILE_TOOL, registerEditFileTool } from './tools/edit-file.js';
|
|
8
8
|
import { LIST_DIRECTORY_TOOL, registerListDirectoryTool, } from './tools/list-directory.js';
|
|
9
9
|
import { MOVE_FILE_TOOL, registerMoveFileTool } from './tools/move-file.js';
|
|
10
|
-
import {
|
|
10
|
+
import { READ_MANY_TOOL, registerReadMultipleFilesTool, } from './tools/read-multiple.js';
|
|
11
11
|
import { READ_FILE_TOOL, registerReadFileTool } from './tools/read.js';
|
|
12
12
|
import { registerSearchAndReplaceTool, SEARCH_AND_REPLACE_TOOL, } from './tools/replace-in-files.js';
|
|
13
13
|
import { LIST_ALLOWED_DIRECTORIES_TOOL, registerListAllowedDirectoriesTool, } from './tools/roots.js';
|
|
@@ -28,7 +28,7 @@ const TOOL_ENTRIES = [
|
|
|
28
28
|
{ contract: TREE_TOOL, register: registerTreeTool },
|
|
29
29
|
{ contract: READ_FILE_TOOL, register: registerReadFileTool },
|
|
30
30
|
{
|
|
31
|
-
contract:
|
|
31
|
+
contract: READ_MANY_TOOL,
|
|
32
32
|
register: registerReadMultipleFilesTool,
|
|
33
33
|
},
|
|
34
34
|
{ contract: GET_FILE_INFO_TOOL, register: registerGetFileInfoTool },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/filesystem-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"mcpName": "io.github.j0hanz/filesystem-mcp",
|
|
5
5
|
"description": "A local filesystem MCP server that lets LLMs and AI agents read, write, search, diff, patch, and manage files safely and efficiently. Built for reliable, structured, and controlled filesystem interaction.",
|
|
6
6
|
"type": "module",
|