@j0hanz/filesystem-mcp 1.17.0 → 1.18.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.
@@ -1,4 +1,5 @@
1
1
  import { InMemoryTaskStore, } from '@modelcontextprotocol/server';
2
+ import { CANCELLED_RESULT_TTL_MS } from '../lib/constants.js';
2
3
  import { ErrorCode } from '../lib/errors.js';
3
4
  const DEFAULT_CANCELLED_STATUS_MESSAGE = 'Client cancelled task execution.';
4
5
  function getTaskKey(taskId, sessionId) {
@@ -18,7 +19,16 @@ function buildCancelledTaskResult(statusMessage) {
18
19
  }
19
20
  export class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
20
21
  cancelledResults = new Map();
22
+ evictExpired() {
23
+ const now = Date.now();
24
+ for (const [key, entry] of this.cancelledResults) {
25
+ if (now - entry.createdAt > CANCELLED_RESULT_TTL_MS) {
26
+ this.cancelledResults.delete(key);
27
+ }
28
+ }
29
+ }
21
30
  async getTaskResult(taskId, sessionId) {
31
+ this.evictExpired();
22
32
  try {
23
33
  return await super.getTaskResult(taskId, sessionId);
24
34
  }
@@ -30,9 +40,9 @@ export class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
30
40
  const key = getTaskKey(taskId, sessionId);
31
41
  const existing = this.cancelledResults.get(key);
32
42
  if (existing)
33
- return existing;
43
+ return existing.result;
34
44
  const result = buildCancelledTaskResult(task.statusMessage);
35
- this.cancelledResults.set(key, result);
45
+ this.cancelledResults.set(key, { result, createdAt: Date.now() });
36
46
  return result;
37
47
  }
38
48
  }
@@ -48,15 +58,23 @@ export class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
48
58
  if (task?.status !== 'cancelled') {
49
59
  throw error;
50
60
  }
51
- this.cancelledResults.set(getTaskKey(taskId, sessionId), this.cancelledResults.get(getTaskKey(taskId, sessionId)) ?? result);
61
+ const key = getTaskKey(taskId, sessionId);
62
+ const existing = this.cancelledResults.get(key);
63
+ this.cancelledResults.set(key, {
64
+ result: existing?.result ?? result,
65
+ createdAt: existing?.createdAt ?? Date.now(),
66
+ });
52
67
  }
53
68
  }
54
69
  async updateTaskStatus(taskId, status, statusMessage, sessionId) {
55
70
  await super.updateTaskStatus(taskId, status, statusMessage, sessionId);
56
71
  const key = getTaskKey(taskId, sessionId);
57
72
  if (status === 'cancelled') {
58
- this.cancelledResults.set(key, this.cancelledResults.get(key) ??
59
- buildCancelledTaskResult(statusMessage));
73
+ const existing = this.cancelledResults.get(key);
74
+ this.cancelledResults.set(key, {
75
+ result: existing?.result ?? buildCancelledTaskResult(statusMessage),
76
+ createdAt: existing?.createdAt ?? Date.now(),
77
+ });
60
78
  return;
61
79
  }
62
80
  if (status === 'completed' || status === 'failed') {
@@ -1,10 +1,10 @@
1
- import { readFile, stat } from 'node:fs/promises';
1
+ import { stat } from 'node:fs/promises';
2
2
  import { basename, resolve } from 'node:path';
3
3
  import { applyPatch, parsePatch } from 'diff';
4
4
  import { withAbort } from '../lib/abort.js';
5
5
  import { MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY } from '../lib/constants.js';
6
6
  import { ErrorCode, McpError } from '../lib/errors.js';
7
- import { atomicWriteFile, processInParallel } from '../lib/fs-helpers.js';
7
+ import { atomicWriteFile, processInParallel, readFileWithStats, } from '../lib/fs-helpers.js';
8
8
  import { Logger } from '../lib/logger.js';
9
9
  import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
10
10
  import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
@@ -62,7 +62,12 @@ async function applyDiff(filePath, diff, options, signal) {
62
62
  assertAllowedFileAccess(filePath, validPath);
63
63
  const stats = await withAbort(stat(validPath), signal);
64
64
  assertPatchTargetSizeWithinLimit(validPath, stats.size, MAX_TEXT_FILE_SIZE);
65
- const content = await readFile(validPath, { encoding: 'utf-8', signal });
65
+ const { content } = await readFileWithStats(filePath, validPath, stats, {
66
+ encoding: 'utf-8',
67
+ maxSize: MAX_TEXT_FILE_SIZE,
68
+ skipBinary: true,
69
+ ...(signal ? { signal } : {}),
70
+ });
66
71
  const patched = applyPatch(content, diff, {
67
72
  fuzzFactor: options.fuzzFactor,
68
73
  autoConvertLineEndings: options.autoConvertLineEndings,
@@ -208,21 +213,23 @@ export function registerApplyPatchTool(server, options = {}) {
208
213
  registerStandardTool(server, APPLY_PATCH_TOOL, handler, options, {
209
214
  progressMessage: (args) => {
210
215
  const name = basename(args.path);
211
- return args.dryRun ? `🛠 patch: ${name} [dry run]` : `🛠 patch: ${name}`;
216
+ return args.dryRun
217
+ ? `${APPLY_PATCH_TOOL.title}: ${name} [dry run]`
218
+ : `${APPLY_PATCH_TOOL.title}: ${name}`;
212
219
  },
213
220
  completionMessage: (args, result) => {
214
221
  const name = basename(args.path);
215
222
  if (result.isError)
216
- return `🛠 patch: ${name} • failed`;
223
+ return `${APPLY_PATCH_TOOL.title}: ${name} • ${result.errorCode}`;
217
224
  const sc = result.structuredContent;
218
225
  if (!sc.ok)
219
- return `🛠 patch: ${name} • failed`;
226
+ return `${APPLY_PATCH_TOOL.title}: ${name} • failed`;
220
227
  const added = sc.linesAdded ?? 0;
221
228
  const removed = sc.linesRemoved ?? 0;
222
229
  const dry = args.dryRun ? 'dry run ' : '';
223
230
  if (added > 0 || removed > 0)
224
- return `🛠 patch: ${name} • ${dry} +${added} -${removed}`;
225
- return `🛠 patch: ${name} • ${dry}no changes`;
231
+ return `${APPLY_PATCH_TOOL.title}: ${name} • ${dry} +${added} -${removed}`;
232
+ return `${APPLY_PATCH_TOOL.title}: ${name} • ${dry}no changes`;
226
233
  },
227
234
  });
228
235
  }
@@ -5,7 +5,7 @@ import { basename, relative, win32 } from 'node:path';
5
5
  import { pipeline } from 'node:stream/promises';
6
6
  import { assertNotAborted, withAbort } from '../lib/abort.js';
7
7
  import { PARALLEL_CONCURRENCY } from '../lib/constants.js';
8
- import { ErrorCode } from '../lib/errors.js';
8
+ import { classifyError, ErrorCode } from '../lib/errors.js';
9
9
  import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations/core.js';
10
10
  import { globEntries } from '../lib/file-operations/traversal.js';
11
11
  import { validateExistingPath } from '../lib/paths.js';
@@ -153,12 +153,12 @@ export function registerCalculateHashTool(server, options = {}) {
153
153
  context: { path: args.path },
154
154
  run: async (signal) => {
155
155
  const baseName = basename(args.path);
156
- const progress = createToolProgressSession(ctx, `🕮 hash: ${baseName}`);
156
+ const progress = createToolProgressSession(ctx, `${CALCULATE_HASH_TOOL.title}: ${baseName}`);
157
157
  const progressWithMessage = ({ current, total, }) => {
158
158
  progress.update({
159
159
  current,
160
160
  ...(total !== undefined ? { total } : {}),
161
- message: `🕮 hash: ${baseName} [${current} files]`,
161
+ message: `${CALCULATE_HASH_TOOL.title}: ${baseName} [${current} files]`,
162
162
  });
163
163
  };
164
164
  try {
@@ -173,11 +173,11 @@ export function registerCalculateHashTool(server, options = {}) {
173
173
  else {
174
174
  suffix = `${(sc.hash ?? '').slice(0, 8)}…`;
175
175
  }
176
- progress.complete(`🕮 hash: ${baseName} • ${suffix}`, finalCurrent);
176
+ progress.complete(`${CALCULATE_HASH_TOOL.title}: ${baseName} • ${suffix}`, finalCurrent);
177
177
  return result;
178
178
  }
179
179
  catch (error) {
180
- progress.fail(`🕮 hash: ${baseName} • failed`);
180
+ progress.fail(`${CALCULATE_HASH_TOOL.title}: ${baseName} • ${classifyError(error)}`);
181
181
  throw error;
182
182
  }
183
183
  },
@@ -48,22 +48,21 @@ export function registerCreateDirectoryTool(server, options = {}) {
48
48
  registerStandardTool(server, CREATE_DIRECTORY_TOOL, handler, options, {
49
49
  progressMessage: (args) => {
50
50
  if (args.path && !args.paths?.length) {
51
- return `🛠 mkdir: ${basename(args.path)}`;
51
+ return `${CREATE_DIRECTORY_TOOL.title}: ${basename(args.path)}`;
52
52
  }
53
53
  const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
54
- return `🛠 mkdir: ${count} directories`;
54
+ return `${CREATE_DIRECTORY_TOOL.title}: ${count} directories`;
55
55
  },
56
56
  completionMessage: (args, result) => {
57
57
  if (args.path && !args.paths?.length) {
58
58
  const name = basename(args.path);
59
59
  if (result.isError)
60
- return `🛠 mkdir: ${name} • failed`;
61
- return `🛠 mkdir: ${name}`;
60
+ return `${CREATE_DIRECTORY_TOOL.title}: ${name} • ${result.errorCode}`;
62
61
  }
63
62
  const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
64
63
  if (result.isError)
65
- return `🛠 mkdir: ${count} directories • failed`;
66
- return `🛠 mkdir: ${count} directories`;
64
+ return `${CREATE_DIRECTORY_TOOL.title}: ${count} directories • ${result.errorCode}`;
65
+ return `${CREATE_DIRECTORY_TOOL.title}: ${count} directories`;
67
66
  },
68
67
  });
69
68
  }
@@ -90,12 +90,12 @@ export function registerDeleteFileTool(server, options = {}) {
90
90
  },
91
91
  });
92
92
  registerStandardTool(server, DELETE_FILE_TOOL, handler, options, {
93
- progressMessage: (args) => `🛠 rm: ${basename(args.path)}`,
93
+ progressMessage: (args) => `${DELETE_FILE_TOOL.title}: ${basename(args.path)}`,
94
94
  completionMessage: (args, result) => {
95
95
  const name = basename(args.path);
96
96
  if (result.isError)
97
- return `🛠 rm: ${name} • failed`;
98
- return `🛠 rm: ${name}`;
97
+ return `${DELETE_FILE_TOOL.title}: ${name} • ${result.errorCode}`;
98
+ return `${DELETE_FILE_TOOL.title}: ${name}`;
99
99
  },
100
100
  });
101
101
  }
@@ -116,21 +116,21 @@ export function registerDiffFilesTool(server, options = {}) {
116
116
  progressMessage: (args) => {
117
117
  const n1 = basename(args.original);
118
118
  const n2 = basename(args.modified);
119
- return `🕮 diff: ${n1} ⟷ ${n2}`;
119
+ return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2}`;
120
120
  },
121
121
  completionMessage: (args, result) => {
122
122
  const n1 = basename(args.original);
123
123
  const n2 = basename(args.modified);
124
124
  if (result.isError)
125
- return `🕮 diff: ${n1} ⟷ ${n2} • failed`;
125
+ return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2} • ${result.errorCode}`;
126
126
  const sc = result.structuredContent;
127
127
  if (sc.isIdentical)
128
- return `🕮 diff: ${n1} ⟷ ${n2} • identical`;
128
+ return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2} • identical`;
129
129
  const added = sc.linesAdded ?? 0;
130
130
  const removed = sc.linesRemoved ?? 0;
131
131
  if (added > 0 || removed > 0)
132
- return `🕮 diff: ${n1} ⟷ ${n2} • +${added} -${removed}`;
133
- return `🕮 diff: ${n1} ⟷ ${n2}`;
132
+ return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2} • +${added} -${removed}`;
133
+ return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2}`;
134
134
  },
135
135
  });
136
136
  }
@@ -1,11 +1,11 @@
1
- import { readFile, stat } from 'node:fs/promises';
1
+ import { stat } from 'node:fs/promises';
2
2
  import { basename } from 'node:path';
3
3
  import { createTwoFilesPatch, diffLines } from 'diff';
4
4
  import RE2 from 're2';
5
5
  import { withAbort } from '../lib/abort.js';
6
6
  import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
7
7
  import { ErrorCode, McpError } from '../lib/errors.js';
8
- import { atomicWriteFile } from '../lib/fs-helpers.js';
8
+ import { atomicWriteFile, readFileWithStats } from '../lib/fs-helpers.js';
9
9
  import { Logger } from '../lib/logger.js';
10
10
  import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
11
11
  import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
@@ -167,28 +167,33 @@ async function loadEditableFile(requestedPath, signal) {
167
167
  if (stats.size > MAX_TEXT_FILE_SIZE) {
168
168
  throw new McpError(ErrorCode.TOO_LARGE, `File too large for edit (${stats.size} bytes > ${MAX_TEXT_FILE_SIZE} bytes)`, requestedPath, { size: stats.size, maxFileSize: MAX_TEXT_FILE_SIZE });
169
169
  }
170
- const content = await readFile(validPath, { encoding: 'utf-8', signal });
170
+ const { content } = await readFileWithStats(requestedPath, validPath, stats, {
171
+ encoding: 'utf-8',
172
+ maxSize: MAX_TEXT_FILE_SIZE,
173
+ skipBinary: true,
174
+ ...(signal ? { signal } : {}),
175
+ });
171
176
  return { validPath, content };
172
177
  }
173
178
  function buildEditProgressMessage(args) {
174
179
  const name = basename(args.path);
175
180
  const tag = args.dryRun ? ' [dry run]' : '';
176
- return `🛠 edit: ${name}${tag}`;
181
+ return `${EDIT_FILE_TOOL.title}: ${name}${tag}`;
177
182
  }
178
183
  function buildEditCompletionMessage(args, result) {
179
184
  const name = basename(args.path);
180
185
  if (result.isError)
181
- return `🛠 edit: ${name} • failed`;
186
+ return `${EDIT_FILE_TOOL.title}: ${name} • ${result.errorCode}`;
182
187
  const { structuredContent } = result;
183
188
  if (!structuredContent.ok)
184
- return `🛠 edit: ${name} • failed`;
189
+ return `${EDIT_FILE_TOOL.title}: ${name} • failed`;
185
190
  const applied = structuredContent.appliedEdits ?? 0;
186
191
  if (applied === 0)
187
- return `🛠 edit: ${name} • no changes`;
192
+ return `${EDIT_FILE_TOOL.title}: ${name} • no changes`;
188
193
  const added = structuredContent.linesAdded ?? 0;
189
194
  const removed = structuredContent.linesRemoved ?? 0;
190
195
  const dry = args.dryRun ? 'dry run ' : '';
191
- return `🛠 edit: ${name} • ${dry} +${added} -${removed}`;
196
+ return `${EDIT_FILE_TOOL.title}: ${name} • ${dry} +${added} -${removed}`;
192
197
  }
193
198
  async function applyEdits(content, edits, ignoreWhitespace) {
194
199
  let newContent = content;
@@ -205,14 +205,14 @@ export function registerListDirectoryTool(server, options = {}) {
205
205
  onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_DIRECTORY, args.path ?? '.'),
206
206
  });
207
207
  registerStandardTool(server, LIST_DIRECTORY_TOOL, handler, options, {
208
- progressMessage: (args) => `≣ ls: ${args.path ? basename(args.path) : '.'}`,
208
+ progressMessage: (args) => `${LIST_DIRECTORY_TOOL.title}: ${args.path ? basename(args.path) : '.'}`,
209
209
  completionMessage: (args, result) => {
210
210
  const base = args.path ? basename(args.path) : '.';
211
211
  if (result.isError)
212
- return `≣ ls: ${base} • failed`;
212
+ return `${LIST_DIRECTORY_TOOL.title}: ${base} • ${result.errorCode}`;
213
213
  const sc = result.structuredContent;
214
214
  const count = sc.totalEntries ?? 0;
215
- return `≣ ls: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
215
+ return `${LIST_DIRECTORY_TOOL.title}: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
216
216
  },
217
217
  });
218
218
  }
@@ -157,23 +157,22 @@ export function registerMoveFileTool(server, options = {}) {
157
157
  progressMessage: (args) => {
158
158
  const dest = basename(args.destination);
159
159
  if (args.source && !args.sources?.length) {
160
- return `🛠 mv: ${basename(args.source)} → ${dest}`;
160
+ return `${MOVE_FILE_TOOL.title}: ${basename(args.source)} → ${dest}`;
161
161
  }
162
162
  const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0);
163
- return `🛠 mv: ${count} items → ${dest}`;
163
+ return `${MOVE_FILE_TOOL.title}: ${count} items → ${dest}`;
164
164
  },
165
165
  completionMessage: (args, result) => {
166
166
  const dest = basename(args.destination);
167
167
  if (args.source && !args.sources?.length) {
168
168
  const src = basename(args.source);
169
169
  if (result.isError)
170
- return `🛠 mv: ${src} → ${dest} • failed`;
171
- return `🛠 mv: ${src} → ${dest}`;
170
+ return `${MOVE_FILE_TOOL.title}: ${src} → ${dest} • ${result.errorCode}`;
172
171
  }
173
172
  const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0);
174
173
  if (result.isError)
175
- return `🛠 mv: ${count} items → ${dest} • failed`;
176
- return `🛠 mv: ${count} items → ${dest}`;
174
+ return `${MOVE_FILE_TOOL.title}: ${count} items → ${dest} • ${result.errorCode}`;
175
+ return `${MOVE_FILE_TOOL.title}: ${count} items → ${dest}`;
177
176
  },
178
177
  });
179
178
  }
@@ -1,13 +1,12 @@
1
1
  import { basename } from 'node:path';
2
2
  import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
3
- import { ErrorCode } from '../lib/errors.js';
3
+ import { classifyError, ErrorCode } from '../lib/errors.js';
4
4
  import { readMultipleFiles } from '../lib/file-operations/metadata.js';
5
5
  import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
6
6
  import { FILE_READ_ICONS } from './icons.js';
7
- import { buildBatchCompletionSuffix, buildBatchPathContext, buildResourceLink, buildStructuredError, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, } from './shared.js';
8
- import { registerStandardTool } from './task-support.js';
7
+ import { buildBatchPathContext, buildResourceLink, buildStructuredError, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, } from './shared.js';
8
+ import { registerStandardTool, reportTaskStatus } from './task-support.js';
9
9
  const READ_MANY_TOOL_NAME = 'read_many';
10
- const READ_MANY_TOOL_LABEL = '🕮 read_many';
11
10
  const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
12
11
  export const READ_MANY_TOOL = {
13
12
  name: READ_MANY_TOOL_NAME,
@@ -20,6 +19,7 @@ export const READ_MANY_TOOL = {
20
19
  icons: FILE_READ_ICONS,
21
20
  taskSupport: 'optional',
22
21
  };
22
+ const READ_MANY_TOOL_LABEL = READ_MANY_TOOL.title;
23
23
  function buildReadManyResourceName(filePath) {
24
24
  return `read:${basename(filePath)}`;
25
25
  }
@@ -173,23 +173,30 @@ export function registerReadMultipleFilesTool(server, options = {}) {
173
173
  context: { path: primaryPath },
174
174
  run: async (signal) => {
175
175
  const context = buildBatchPathContext(args.paths, 'files');
176
- const { progress, onItemComplete } = createBatchProgressCallbacks(ctx, {
176
+ const { progress, onItemComplete: rawOnItemComplete } = createBatchProgressCallbacks(ctx, {
177
177
  toolLabel: READ_MANY_TOOL_LABEL,
178
178
  context,
179
179
  totalItems: args.paths.length,
180
180
  itemVerb: 'read',
181
181
  });
182
+ let itemsDone = 0;
183
+ const onItemComplete = () => {
184
+ rawOnItemComplete();
185
+ itemsDone++;
186
+ void reportTaskStatus(`${READ_MANY_TOOL_LABEL}: ${context} [${itemsDone}/${args.paths.length} read]`);
187
+ };
182
188
  try {
183
189
  const result = await handleReadMultipleFiles(args, signal, options.resourceStore, onItemComplete);
184
190
  const sc = result.structuredContent;
185
- const suffix = buildBatchCompletionSuffix(sc.summary, 'files read', 'file read');
186
191
  const total = sc.summary?.total ?? 0;
192
+ const failed = sc.summary?.failed ?? 0;
193
+ const suffix = failed ? `${failed} failed` : 'done';
187
194
  const finalCurrent = resolveFinalProgressCurrent(progress, total);
188
195
  progress.complete(`${READ_MANY_TOOL_LABEL}: ${context} • ${suffix}`, finalCurrent);
189
196
  return result;
190
197
  }
191
198
  catch (error) {
192
- progress.fail(`${READ_MANY_TOOL_LABEL}: ${context} • failed`);
199
+ progress.fail(`${READ_MANY_TOOL_LABEL}: ${context} • ${classifyError(error)}`);
193
200
  throw error;
194
201
  }
195
202
  },
@@ -1,8 +1,7 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { basename } from 'node:path';
3
2
  import { DEFAULT_SEARCH_TIMEOUT_MS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
4
3
  import { ErrorCode } from '../lib/errors.js';
5
- import { readFile } from '../lib/fs-helpers.js';
4
+ import { calculateFileContentHash, readFile } from '../lib/fs-helpers.js';
6
5
  import { ReadFileInputSchema, ReadFileOutputSchema } from '../schemas.js';
7
6
  import { FILE_READ_ICONS } from './icons.js';
8
7
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, } from './shared.js';
@@ -23,7 +22,7 @@ export const READ_FILE_TOOL = {
23
22
  taskSupport: 'forbidden',
24
23
  };
25
24
  const READ_TOOL_NAME = 'read';
26
- const READ_TOOL_LABEL = '🕮 read';
25
+ const READ_TOOL_LABEL = READ_FILE_TOOL.title;
27
26
  const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
28
27
  function buildReadResourceName(filePath) {
29
28
  return `read:${basename(filePath)}`;
@@ -115,7 +114,7 @@ function buildReadProgressMessage(args) {
115
114
  function buildReadCompletionMessage(args, result) {
116
115
  const name = basename(args.path);
117
116
  if (result.isError)
118
- return `${READ_TOOL_LABEL}: ${name} • failed`;
117
+ return `${READ_TOOL_LABEL}: ${name} • ${result.errorCode}`;
119
118
  const structured = result.structuredContent;
120
119
  const lines = structured.linesRead ?? structured.totalLines;
121
120
  if (structured.startLine !== undefined) {
@@ -144,9 +143,7 @@ async function handleReadFile(args, signal, resourceStore) {
144
143
  const result = await readFile(args.path, options);
145
144
  const structured = toStructuredReadFileResult(args, result);
146
145
  if (args.includeHash) {
147
- structured.contentHash = createHash('sha256')
148
- .update(result.content, 'utf-8')
149
- .digest('hex');
146
+ structured.contentHash = await calculateFileContentHash(result.path, signal);
150
147
  }
151
148
  const externalizedResponse = maybeBuildExternalizedReadResponse(args.path, result.content, structured, resourceStore);
152
149
  if (externalizedResponse) {
@@ -4,7 +4,7 @@ import { basename, relative } from 'node:path';
4
4
  import { createTwoFilesPatch } from 'diff';
5
5
  import RE2 from 're2';
6
6
  import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
7
- import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
7
+ import { classifyError, ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
8
8
  import { globEntries } from '../lib/file-operations/traversal.js';
9
9
  import { atomicWriteFile } from '../lib/fs-helpers.js';
10
10
  import { Logger } from '../lib/logger.js';
@@ -340,12 +340,12 @@ export function registerSearchAndReplaceTool(server, options = {}) {
340
340
  const dryLabel = args.dryRun ? ' [dry run]' : '';
341
341
  const truncatedPattern = truncateProgressPattern(args.searchPattern);
342
342
  const context = `"${truncatedPattern}" in ${args.filePattern}${dryLabel}`;
343
- const progress = createToolProgressSession(ctx, `🛠 replace: ${context}`);
343
+ const progress = createToolProgressSession(ctx, `${SEARCH_AND_REPLACE_TOOL.title}: ${context}`);
344
344
  const progressWithMessage = ({ current, total, }) => {
345
345
  progress.update({
346
346
  current,
347
347
  ...(total !== undefined ? { total } : {}),
348
- message: `🛠 replace: ${truncatedPattern} [${current} files]`,
348
+ message: `${SEARCH_AND_REPLACE_TOOL.title}: ${truncatedPattern} [${current} files]`,
349
349
  });
350
350
  };
351
351
  try {
@@ -357,14 +357,14 @@ export function registerSearchAndReplaceTool(server, options = {}) {
357
357
  let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
358
358
  if (sc.failedFiles)
359
359
  endSuffix += `, ${sc.failedFiles} failed`;
360
- progress.complete(`🛠 replace: ${context} • ${endSuffix}`, finalCurrent);
360
+ progress.complete(`${SEARCH_AND_REPLACE_TOOL.title}: ${context} • ${endSuffix}`, finalCurrent);
361
361
  if (!args.dryRun) {
362
362
  void ctx.log?.('info', `search_and_replace: ${String(sc.matches ?? 0)} matches in ${String(sc.filesChanged ?? 0)} files`);
363
363
  }
364
364
  return result;
365
365
  }
366
366
  catch (error) {
367
- progress.fail(`🛠 replace: ${context} • failed`);
367
+ progress.fail(`${SEARCH_AND_REPLACE_TOOL.title}: ${context} • ${classifyError(error)}`);
368
368
  throw error;
369
369
  }
370
370
  },
@@ -41,13 +41,13 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
41
41
  onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN),
42
42
  });
43
43
  registerStandardTool(server, LIST_ALLOWED_DIRECTORIES_TOOL, handler, options, {
44
- progressMessage: () => '≣ roots',
44
+ progressMessage: () => LIST_ALLOWED_DIRECTORIES_TOOL.title,
45
45
  completionMessage: (_args, result) => {
46
46
  if (result.isError)
47
- return `≣ roots failed`;
47
+ return `${LIST_ALLOWED_DIRECTORIES_TOOL.title}${result.errorCode}`;
48
48
  const sc = result.structuredContent;
49
49
  const count = sc.directories?.length ?? 0;
50
- return `≣ roots • ${count} ${count === 1 ? 'root' : 'roots'}`;
50
+ return `${LIST_ALLOWED_DIRECTORIES_TOOL.title} • ${count} ${count === 1 ? 'root' : 'roots'}`;
51
51
  },
52
52
  });
53
53
  }
@@ -1,13 +1,13 @@
1
1
  import { relative } from 'node:path';
2
2
  import RE2 from 're2';
3
3
  import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
4
- import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
4
+ import { classifyError, ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
5
5
  import { searchContent, } from '../lib/file-operations/search.js';
6
6
  import { formatOperationSummary } from '../config.js';
7
7
  import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
8
8
  import { SEARCH_ICONS } from './icons.js';
9
9
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, } from './shared.js';
10
- import { registerStandardTool } from './task-support.js';
10
+ import { registerStandardTool, reportTaskStatus } from './task-support.js';
11
11
  /**
12
12
  * Configuration constants for the Search Content tool.
13
13
  */
@@ -268,7 +268,7 @@ export function registerSearchContentTool(server, options = {}) {
268
268
  context: { path: args.path ?? '.' },
269
269
  run: async (signal) => {
270
270
  const { pattern, filePattern: scope } = args;
271
- const progressLabel = `🔎︎ grep: ${truncateProgressPattern(pattern)}`;
271
+ const progressLabel = `${SEARCH_CONTENT_TOOL.title}: ${truncateProgressPattern(pattern)}`;
272
272
  const progress = createToolProgressSession(ctx, progressLabel);
273
273
  const progressWithMessage = ({ current, total, }) => {
274
274
  progress.update({
@@ -276,6 +276,7 @@ export function registerSearchContentTool(server, options = {}) {
276
276
  ...(total !== undefined ? { total } : {}),
277
277
  message: `${progressLabel} [${current} files]`,
278
278
  });
279
+ void reportTaskStatus(`${progressLabel} ${current} files`);
279
280
  };
280
281
  try {
281
282
  const result = await handleSearchContent(args, signal, options.resourceStore, progressWithMessage);
@@ -287,7 +288,7 @@ export function registerSearchContentTool(server, options = {}) {
287
288
  return result;
288
289
  }
289
290
  catch (error) {
290
- progress.fail(`${progressLabel} • failed`);
291
+ progress.fail(`${progressLabel} • ${classifyError(error)}`);
291
292
  throw error;
292
293
  }
293
294
  },
@@ -1,6 +1,6 @@
1
1
  import { basename, relative } from 'node:path';
2
2
  import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/constants.js';
3
- import { ErrorCode } from '../lib/errors.js';
3
+ import { classifyError, ErrorCode } from '../lib/errors.js';
4
4
  import { searchFiles } from '../lib/file-operations/search.js';
5
5
  import { formatOperationSummary, joinLines } from '../config.js';
6
6
  import { SearchFilesInputSchema, SearchFilesOutputSchema } from '../schemas.js';
@@ -126,7 +126,7 @@ export function registerSearchFilesTool(server, options = {}) {
126
126
  let progressCursor = 0;
127
127
  notifyProgress(ctx, {
128
128
  current: 0,
129
- message: `🔎︎ find: ${truncatedPattern}`,
129
+ message: `${SEARCH_FILES_TOOL.title}: ${truncatedPattern}`,
130
130
  });
131
131
  const baseReporter = createProgressReporter(ctx);
132
132
  const progressWithMessage = ({ current, total, }) => {
@@ -135,7 +135,7 @@ export function registerSearchFilesTool(server, options = {}) {
135
135
  baseReporter({
136
136
  current,
137
137
  ...(total !== undefined ? { total } : {}),
138
- message: `🔎︎ find: ${truncatedPattern} [${current} files]`,
138
+ message: `${SEARCH_FILES_TOOL.title}: ${truncatedPattern} [${current} files]`,
139
139
  });
140
140
  };
141
141
  try {
@@ -162,7 +162,7 @@ export function registerSearchFilesTool(server, options = {}) {
162
162
  notifyProgress(ctx, {
163
163
  current: finalCurrent,
164
164
  total: finalCurrent,
165
- message: `🔎︎ find: ${context} • ${suffix}`,
165
+ message: `${SEARCH_FILES_TOOL.title}: ${context} • ${suffix}`,
166
166
  });
167
167
  return result;
168
168
  }
@@ -171,7 +171,7 @@ export function registerSearchFilesTool(server, options = {}) {
171
171
  notifyProgress(ctx, {
172
172
  current: finalCurrent,
173
173
  total: finalCurrent,
174
- message: `🔎︎ find: ${context} • failed`,
174
+ message: `${SEARCH_FILES_TOOL.title}: ${context} • ${classifyError(error)}`,
175
175
  });
176
176
  throw error;
177
177
  }
@@ -1,4 +1,4 @@
1
- import type { ContentBlock, Icon, LoggingLevel, ProgressNotificationParams, ServerContext } from '@modelcontextprotocol/server';
1
+ import type { ContentBlock, Icon, LoggingLevel, Notification, RequestMeta, ServerContext } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { ErrorCode } from '../lib/errors.js';
4
4
  import type { ResourceStore } from '../lib/resource-store.js';
@@ -56,25 +56,27 @@ export type ToolResponse<T> = ReturnType<typeof buildToolResponse<T>> & {
56
56
  interface ToolErrorResponse extends Record<string, unknown> {
57
57
  content: ContentBlock[];
58
58
  isError: true;
59
- errorCode?: ErrorCode;
59
+ errorCode: ErrorCode;
60
60
  }
61
61
  export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
62
62
  export declare function withValidatedArgs<Args, Result>(schema: z.ZodType<Args>, handler: (args: Args, ctx: ToolContext) => Promise<ToolResult<Result>>): (args: unknown, ctx: ToolContext | ServerContext) => Promise<ToolResult<Result>>;
63
- type ProgressToken = string | number;
63
+ /**
64
+ * App-level tracing metadata passed through {@linkcode RequestMeta}.
65
+ * These fields are preserved by the SDK's loose `RequestMeta` type.
66
+ */
67
+ interface TracingMeta {
68
+ traceparent?: string | undefined;
69
+ tracestate?: string | undefined;
70
+ baggage?: string | undefined;
71
+ }
64
72
  export interface ToolContext {
65
73
  signal?: AbortSignal;
66
- _meta?: {
67
- progressToken?: ProgressToken | undefined;
68
- traceparent?: string | undefined;
69
- tracestate?: string | undefined;
70
- baggage?: string | undefined;
71
- } | undefined;
72
- sendNotification?: (notification: {
73
- method: 'notifications/progress';
74
- params: ProgressNotificationParams;
75
- }) => Promise<void>;
74
+ sessionId?: string;
75
+ _meta?: (RequestMeta & TracingMeta) | undefined;
76
+ sendNotification?: (notification: Notification) => Promise<void>;
76
77
  log?: (level: LoggingLevel, data: unknown, logger?: string) => Promise<void>;
77
78
  }
79
+ export declare function toToolContext(ctx?: ToolContext | ServerContext): ToolContext;
78
80
  export interface IconInfo {
79
81
  src: string;
80
82
  mimeType: string;
@@ -168,8 +170,3 @@ export declare function encodeOffsetCursor(offset: number): string;
168
170
  export declare function decodeOffsetCursor(cursor: string): number;
169
171
  export declare function buildBatchPathContext(paths: readonly string[], unitLabel?: string): string;
170
172
  export declare function truncateProgressPattern(pattern: string, maxLength?: number): string;
171
- export declare function buildBatchCompletionSuffix(summary: {
172
- total?: number;
173
- failed?: number;
174
- succeeded?: number;
175
- } | undefined, successWord: string, singularWord?: string): string;