@j0hanz/filesystem-mcp 1.11.1 → 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.
@@ -1,10 +1,11 @@
1
1
  import * as path from 'node:path';
2
- import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
3
- import { ErrorCode } from '../lib/errors.js';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { DEFAULT_EXCLUDE_PATTERNS, MAX_LIST_ENTRIES, } from '../lib/constants.js';
4
+ import { ErrorCode, McpError } from '../lib/errors.js';
4
5
  import { listDirectory } from '../lib/file-operations/metadata.js';
5
6
  import { formatOperationSummary, joinLines } from '../config.js';
6
7
  import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
7
- import { buildToolErrorResponse, buildToolResponse, decodeOffsetCursor, encodeOffsetCursor, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
+ import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
9
  import { registerToolTaskIfAvailable } from './task-support.js';
9
10
  export const LIST_DIRECTORY_TOOL = {
10
11
  name: 'ls',
@@ -18,6 +19,74 @@ export const LIST_DIRECTORY_TOOL = {
18
19
  taskSupport: 'optional',
19
20
  nuances: ['`pattern` enables filtered recursive traversal up to `maxDepth`.'],
20
21
  };
22
+ const LIST_CURSOR_TTL_MS = parseInt(process.env['FS_CONTEXT_LIST_CURSOR_TTL_MS'] ?? '', 10) ||
23
+ 5 * 60 * 1000;
24
+ const listSnapshots = new Map();
25
+ const listSnapshotTimers = new Map();
26
+ function buildListFingerprint(args) {
27
+ return JSON.stringify({
28
+ path: resolvePathOrRoot(args.path),
29
+ includeHidden: args.includeHidden,
30
+ includeIgnored: args.includeIgnored,
31
+ maxDepth: args.maxDepth,
32
+ sortBy: args.sortBy,
33
+ pattern: args.pattern,
34
+ includeSymlinkTargets: args.includeSymlinkTargets,
35
+ });
36
+ }
37
+ function deleteListSnapshot(snapshotId) {
38
+ listSnapshots.delete(snapshotId);
39
+ const timer = listSnapshotTimers.get(snapshotId);
40
+ if (timer) {
41
+ clearTimeout(timer);
42
+ listSnapshotTimers.delete(snapshotId);
43
+ }
44
+ }
45
+ function storeListSnapshot(snapshot) {
46
+ const snapshotId = randomUUID();
47
+ listSnapshots.set(snapshotId, snapshot);
48
+ const timer = setTimeout(() => {
49
+ deleteListSnapshot(snapshotId);
50
+ }, LIST_CURSOR_TTL_MS);
51
+ if (typeof timer.unref === 'function') {
52
+ timer.unref();
53
+ }
54
+ listSnapshotTimers.set(snapshotId, timer);
55
+ return snapshotId;
56
+ }
57
+ function encodeListCursor(payload) {
58
+ return Buffer.from(JSON.stringify(payload)).toString('base64url');
59
+ }
60
+ function decodeListCursor(cursor) {
61
+ try {
62
+ const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
63
+ if (typeof parsed === 'object' &&
64
+ parsed !== null &&
65
+ typeof parsed.snapshotId === 'string' &&
66
+ typeof parsed.offset === 'number') {
67
+ const payload = parsed;
68
+ if (payload.snapshotId.length > 0 &&
69
+ Number.isInteger(payload.offset) &&
70
+ payload.offset >= 0) {
71
+ return payload;
72
+ }
73
+ }
74
+ }
75
+ catch {
76
+ // fall through to throw
77
+ }
78
+ throw new McpError(ErrorCode.E_INVALID_INPUT, 'Invalid cursor: the cursor value is malformed or expired.');
79
+ }
80
+ function resolveNextListCursor(snapshotId, offset, pageSize, totalEntries) {
81
+ if (!snapshotId)
82
+ return undefined;
83
+ const nextOffset = offset + pageSize;
84
+ if (nextOffset >= totalEntries) {
85
+ deleteListSnapshot(snapshotId);
86
+ return undefined;
87
+ }
88
+ return encodeListCursor({ snapshotId, offset: nextOffset });
89
+ }
21
90
  function buildListTextResult(result, nextCursor) {
22
91
  const { entries, summary, path } = result;
23
92
  if (entries.length === 0) {
@@ -82,7 +151,6 @@ function buildStructuredListResult(result, nextCursor) {
82
151
  }
83
152
  async function handleListDirectory(args, signal) {
84
153
  const dirPath = resolvePathOrRoot(args.path);
85
- const cursorOffset = args.cursor !== undefined ? decodeOffsetCursor(args.cursor) : 0;
86
154
  const pageSize = args.maxEntries;
87
155
  const options = {
88
156
  includeHidden: args.includeHidden,
@@ -90,15 +158,42 @@ async function handleListDirectory(args, signal) {
90
158
  sortBy: args.sortBy,
91
159
  includeSymlinkTargets: args.includeSymlinkTargets,
92
160
  ...(args.maxDepth !== undefined ? { maxDepth: args.maxDepth } : {}),
93
- maxEntries: cursorOffset + pageSize,
161
+ maxEntries: MAX_LIST_ENTRIES,
94
162
  ...(args.pattern !== undefined ? { pattern: args.pattern } : {}),
95
163
  ...(signal ? { signal } : {}),
96
164
  };
97
- const result = await listDirectory(dirPath, options);
98
- const displayEntries = cursorOffset > 0 ? result.entries.slice(cursorOffset) : result.entries;
99
- const nextCursor = result.summary.truncated && displayEntries.length > 0
100
- ? encodeOffsetCursor(cursorOffset + displayEntries.length)
101
- : undefined;
165
+ const fingerprint = buildListFingerprint(args);
166
+ let result;
167
+ let cursorOffset = 0;
168
+ let snapshotId;
169
+ if (args.cursor) {
170
+ const cursor = decodeListCursor(args.cursor);
171
+ const snapshot = listSnapshots.get(cursor.snapshotId);
172
+ if (snapshot?.fingerprint !== fingerprint) {
173
+ throw new McpError(ErrorCode.E_INVALID_INPUT, 'Invalid cursor: the cursor value is malformed or expired.');
174
+ }
175
+ const { offset, snapshotId: storedSnapshotId } = cursor;
176
+ cursorOffset = offset;
177
+ snapshotId = storedSnapshotId;
178
+ result = {
179
+ path: snapshot.path,
180
+ entries: snapshot.entries,
181
+ summary: snapshot.summary,
182
+ };
183
+ }
184
+ else {
185
+ result = await listDirectory(dirPath, options);
186
+ }
187
+ const displayEntries = result.entries.slice(cursorOffset, cursorOffset + pageSize);
188
+ if (!args.cursor && displayEntries.length < result.entries.length) {
189
+ snapshotId = storeListSnapshot({
190
+ path: result.path,
191
+ entries: result.entries,
192
+ summary: result.summary,
193
+ fingerprint,
194
+ });
195
+ }
196
+ const nextCursor = resolveNextListCursor(snapshotId, cursorOffset, displayEntries.length, result.entries.length);
102
197
  const displayResult = { ...result, entries: displayEntries };
103
198
  return buildToolResponse(buildListTextResult(displayResult, nextCursor), buildStructuredListResult(displayResult, nextCursor));
104
199
  }
@@ -1,4 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
- export declare const READ_MULTIPLE_FILES_TOOL: ToolContract;
3
+ export declare const READ_MANY_TOOL: ToolContract;
4
4
  export declare function registerReadMultipleFilesTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -5,8 +5,11 @@ import { readMultipleFiles } from '../lib/file-operations/metadata.js';
5
5
  import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
6
6
  import { buildBatchCompletionSuffix, buildBatchPathContext, buildResourceLink, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';
8
- export const READ_MULTIPLE_FILES_TOOL = {
9
- name: 'read_many',
8
+ const READ_MANY_TOOL_NAME = 'read_many';
9
+ const READ_MANY_TOOL_LABEL = '🕮 read_many';
10
+ const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
11
+ export const READ_MANY_TOOL = {
12
+ name: READ_MANY_TOOL_NAME,
10
13
  title: 'Read Multiple Files',
11
14
  description: 'Read multiple text files in one request with contents and metadata. ' +
12
15
  'For a single file, use `read`.',
@@ -19,26 +22,42 @@ export const READ_MULTIPLE_FILES_TOOL = {
19
22
  'Per-file `truncationReason` can be `head`, `tail`, `range`, or `externalized`.',
20
23
  ],
21
24
  };
25
+ function buildReadManyResourceName(filePath) {
26
+ return `read:${path.basename(filePath)}`;
27
+ }
22
28
  function toStructuredReadManyResult(result) {
23
- return {
29
+ const structuredResult = {
24
30
  path: result.path,
25
- ...(result.content !== undefined ? { content: result.content } : {}),
26
- ...(result.truncated ? { truncated: result.truncated } : {}),
27
- ...(result.resourceUri ? { resourceUri: result.resourceUri } : {}),
28
- ...(result.head !== undefined ? { head: result.head } : {}),
29
- ...(result.tail !== undefined ? { tail: result.tail } : {}),
30
- ...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
31
- ...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
32
- ...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
33
- ...(result.totalLines !== undefined
34
- ? { totalLines: result.totalLines }
35
- : {}),
36
- ...(result.linesRead !== undefined ? { linesRead: result.linesRead } : {}),
37
- ...(result.truncationReason
38
- ? { truncationReason: result.truncationReason }
39
- : {}),
40
- ...(result.error ? { error: result.error } : {}),
41
31
  };
32
+ if (result.content !== undefined)
33
+ structuredResult.content = result.content;
34
+ if (result.truncated)
35
+ structuredResult.truncated = true;
36
+ if (result.resourceUri)
37
+ structuredResult.resourceUri = result.resourceUri;
38
+ if (result.head !== undefined)
39
+ structuredResult.head = result.head;
40
+ if (result.tail !== undefined)
41
+ structuredResult.tail = result.tail;
42
+ if (result.startLine !== undefined) {
43
+ structuredResult.startLine = result.startLine;
44
+ }
45
+ if (result.endLine !== undefined)
46
+ structuredResult.endLine = result.endLine;
47
+ if (result.hasMoreLines)
48
+ structuredResult.hasMoreLines = true;
49
+ if (result.totalLines !== undefined) {
50
+ structuredResult.totalLines = result.totalLines;
51
+ }
52
+ if (result.linesRead !== undefined) {
53
+ structuredResult.linesRead = result.linesRead;
54
+ }
55
+ if (result.truncationReason) {
56
+ structuredResult.truncationReason = result.truncationReason;
57
+ }
58
+ if (result.error)
59
+ structuredResult.error = result.error;
60
+ return structuredResult;
42
61
  }
43
62
  function resolveReadManyTruncationReason(result) {
44
63
  if (!result.truncated)
@@ -60,7 +79,7 @@ function maybeExternalizeReadManyResult(result, resourceStore) {
60
79
  if (!result.content) {
61
80
  return baseResult;
62
81
  }
63
- const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: `read:${path.basename(result.path)}`, mimeType: 'text/plain' });
82
+ const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: buildReadManyResourceName(result.path), mimeType: 'text/plain' });
64
83
  if (!externalized) {
65
84
  return baseResult;
66
85
  }
@@ -72,66 +91,83 @@ function maybeExternalizeReadManyResult(result, resourceStore) {
72
91
  truncationReason: 'externalized',
73
92
  };
74
93
  }
75
- function buildReadManyResourceLinks(results) {
76
- return results.flatMap((result) => {
77
- if (!result.resourceUri)
78
- return [];
79
- return [
80
- buildResourceLink({
81
- uri: result.resourceUri,
82
- name: `read:${path.basename(result.path)}`,
83
- description: 'Full file contents',
84
- }),
85
- ];
86
- });
94
+ function buildReadManyTextSection(result) {
95
+ const header = `=== ${result.path} ===`;
96
+ if (result.error) {
97
+ return `${header}\nError: ${result.error}`;
98
+ }
99
+ return `${header}\n${result.content ?? ''}`;
87
100
  }
88
- function buildReadManyTextResult(results) {
89
- return results
90
- .map((result) => {
91
- const header = `=== ${result.path} ===`;
92
- if (result.error) {
93
- return `${header}\nError: ${result.error}`;
101
+ function buildReadMultipleOptions(args, signal, onReadComplete) {
102
+ const options = {};
103
+ if (signal)
104
+ options.signal = signal;
105
+ if (args.head !== undefined)
106
+ options.head = args.head;
107
+ if (args.tail !== undefined)
108
+ options.tail = args.tail;
109
+ if (args.startLine !== undefined)
110
+ options.startLine = args.startLine;
111
+ if (args.endLine !== undefined)
112
+ options.endLine = args.endLine;
113
+ if (onReadComplete)
114
+ options.onReadComplete = onReadComplete;
115
+ return options;
116
+ }
117
+ function buildReadManyResponsePayload(results, resourceStore) {
118
+ const resourceLinks = [];
119
+ const structuredResults = [];
120
+ const textSections = [];
121
+ let succeeded = 0;
122
+ for (const result of results) {
123
+ const mappedResult = maybeExternalizeReadManyResult(result, resourceStore);
124
+ structuredResults.push(toStructuredReadManyResult(mappedResult));
125
+ textSections.push(buildReadManyTextSection(mappedResult));
126
+ if (mappedResult.resourceUri) {
127
+ resourceLinks.push(buildResourceLink({
128
+ uri: mappedResult.resourceUri,
129
+ name: buildReadManyResourceName(mappedResult.path),
130
+ description: FULL_FILE_CONTENTS_DESCRIPTION,
131
+ }));
94
132
  }
95
- return `${header}\n${result.content ?? ''}`;
96
- })
97
- .join('\n\n');
133
+ if (mappedResult.error === undefined)
134
+ succeeded += 1;
135
+ }
136
+ const total = structuredResults.length;
137
+ return {
138
+ resourceLinks,
139
+ structuredResults,
140
+ summary: {
141
+ total,
142
+ succeeded,
143
+ failed: total - succeeded,
144
+ },
145
+ text: textSections.join('\n\n'),
146
+ };
98
147
  }
99
148
  async function handleReadMultipleFiles(args, signal, resourceStore, onReadComplete) {
100
- const options = {
101
- ...(signal ? { signal } : {}),
102
- ...(args.head !== undefined ? { head: args.head } : {}),
103
- ...(args.tail !== undefined ? { tail: args.tail } : {}),
104
- ...(args.startLine !== undefined ? { startLine: args.startLine } : {}),
105
- ...(args.endLine !== undefined ? { endLine: args.endLine } : {}),
106
- ...(onReadComplete ? { onReadComplete } : {}),
107
- };
149
+ const options = buildReadMultipleOptions(args, signal, onReadComplete);
108
150
  const results = await readMultipleFiles(args.paths, options);
109
- const mappedResults = results.map((result) => maybeExternalizeReadManyResult(result, resourceStore));
110
- const succeeded = mappedResults.filter((r) => r.error === undefined).length;
111
- const failed = mappedResults.length - succeeded;
151
+ const payload = buildReadManyResponsePayload(results, resourceStore);
112
152
  const structured = {
113
153
  ok: true,
114
- results: mappedResults.map((result) => toStructuredReadManyResult(result)),
115
- summary: {
116
- total: mappedResults.length,
117
- succeeded,
118
- failed,
119
- },
154
+ results: payload.structuredResults,
155
+ summary: payload.summary,
120
156
  };
121
- return buildToolResponse(buildReadManyTextResult(mappedResults), structured, buildReadManyResourceLinks(mappedResults));
157
+ return buildToolResponse(payload.text, structured, payload.resourceLinks);
122
158
  }
123
159
  export function registerReadMultipleFilesTool(server, options = {}) {
124
160
  const handler = (args, extra) => {
125
161
  const primaryPath = args.paths[0] ?? '';
126
162
  return executeToolWithDiagnostics({
127
- toolName: 'read_many',
163
+ toolName: READ_MANY_TOOL_NAME,
128
164
  extra,
129
165
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
130
166
  context: { path: primaryPath },
131
167
  run: async (signal) => {
132
168
  const context = buildBatchPathContext(args.paths, 'files');
133
169
  const { progress, onItemComplete } = createBatchProgressCallbacks(extra, {
134
- toolLabel: '🕮 read_many',
170
+ toolLabel: READ_MANY_TOOL_LABEL,
135
171
  context,
136
172
  totalItems: args.paths.length,
137
173
  itemVerb: 'read',
@@ -142,11 +178,11 @@ export function registerReadMultipleFilesTool(server, options = {}) {
142
178
  const suffix = buildBatchCompletionSuffix(sc.summary, 'files read', 'file read');
143
179
  const total = sc.summary?.total ?? 0;
144
180
  const finalCurrent = resolveFinalProgressCurrent(progress, total);
145
- progress.complete(`🕮 read_many: ${context} • ${suffix}`, finalCurrent);
181
+ progress.complete(`${READ_MANY_TOOL_LABEL}: ${context} • ${suffix}`, finalCurrent);
146
182
  return result;
147
183
  }
148
184
  catch (error) {
149
- progress.fail(`🕮 read_many: ${context} • failed`);
185
+ progress.fail(`${READ_MANY_TOOL_LABEL}: ${context} • failed`);
150
186
  throw error;
151
187
  }
152
188
  },
@@ -157,7 +193,7 @@ export function registerReadMultipleFilesTool(server, options = {}) {
157
193
  guard: options.isInitialized,
158
194
  });
159
195
  const validatedHandler = withValidatedArgs(ReadMultipleFilesInputSchema, wrappedHandler);
160
- if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
196
+ if (registerToolTaskIfAvailable(server, READ_MANY_TOOL_NAME, READ_MANY_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
161
197
  return;
162
- server.registerTool('read_many', withDefaultIcons({ ...READ_MULTIPLE_FILES_TOOL }, options.iconInfo), validatedHandler);
198
+ server.registerTool(READ_MANY_TOOL_NAME, withDefaultIcons({ ...READ_MANY_TOOL }, options.iconInfo), validatedHandler);
163
199
  }
@@ -20,51 +20,61 @@ export const READ_FILE_TOOL = {
20
20
  ],
21
21
  taskSupport: 'forbidden',
22
22
  };
23
- async function handleReadFile(args, signal, resourceStore) {
23
+ const READ_TOOL_NAME = 'read';
24
+ const READ_TOOL_LABEL = '🕮 read';
25
+ const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
26
+ function buildReadResourceName(filePath) {
27
+ return `read:${path.basename(filePath)}`;
28
+ }
29
+ function buildReadOptions(args, signal) {
24
30
  const options = {
25
31
  encoding: 'utf-8',
26
32
  maxSize: MAX_TEXT_FILE_SIZE,
27
33
  skipBinary: true,
28
34
  };
29
- if (args.head !== undefined) {
35
+ if (signal)
36
+ options.signal = signal;
37
+ if (args.head !== undefined)
30
38
  options.head = args.head;
31
- }
32
- if (args.tail !== undefined) {
39
+ if (args.tail !== undefined)
33
40
  options.tail = args.tail;
34
- }
35
- if (args.startLine !== undefined) {
41
+ if (args.startLine !== undefined)
36
42
  options.startLine = args.startLine;
37
- }
38
- if (args.endLine !== undefined) {
43
+ if (args.endLine !== undefined)
39
44
  options.endLine = args.endLine;
40
- }
41
- if (signal) {
42
- options.signal = signal;
43
- }
44
- const result = await readFile(args.path, options);
45
+ return options;
46
+ }
47
+ function toStructuredReadFileResult(args, result) {
45
48
  const structured = {
46
49
  ok: true,
47
50
  path: args.path,
48
51
  content: result.content,
49
- ...(result.truncated ? { truncated: result.truncated } : {}),
50
- ...(result.totalLines !== undefined
51
- ? { totalLines: result.totalLines }
52
- : {}),
53
- ...(result.head !== undefined ? { head: result.head } : {}),
54
- ...(result.tail !== undefined ? { tail: result.tail } : {}),
55
- ...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
56
- ...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
57
- ...(result.linesRead !== undefined ? { linesRead: result.linesRead } : {}),
58
- ...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
59
52
  };
60
- const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: `read:${path.basename(args.path)}`, mimeType: 'text/plain' });
61
- if (args.includeHash) {
62
- structured.contentHash = createHash('sha256')
63
- .update(result.content, 'utf-8')
64
- .digest('hex');
65
- }
53
+ if (result.truncated)
54
+ structured.truncated = true;
55
+ if (result.totalLines !== undefined)
56
+ structured.totalLines = result.totalLines;
57
+ if (result.head !== undefined)
58
+ structured.head = result.head;
59
+ if (result.tail !== undefined)
60
+ structured.tail = result.tail;
61
+ if (result.startLine !== undefined)
62
+ structured.startLine = result.startLine;
63
+ if (result.endLine !== undefined)
64
+ structured.endLine = result.endLine;
65
+ if (result.linesRead !== undefined)
66
+ structured.linesRead = result.linesRead;
67
+ if (result.hasMoreLines)
68
+ structured.hasMoreLines = true;
69
+ return structured;
70
+ }
71
+ function maybeBuildExternalizedReadResponse(filePath, content, structured, resourceStore) {
72
+ const externalized = maybeExternalizeTextContent(resourceStore, content, {
73
+ name: buildReadResourceName(filePath),
74
+ mimeType: 'text/plain',
75
+ });
66
76
  if (!externalized) {
67
- return buildToolResponse(result.content, structured);
77
+ return undefined;
68
78
  }
69
79
  const { entry, preview } = externalized;
70
80
  const structuredWithResource = {
@@ -74,7 +84,7 @@ async function handleReadFile(args, signal, resourceStore) {
74
84
  resourceUri: entry.uri,
75
85
  };
76
86
  const text = [
77
- `Output too large to inline (${result.content.length} chars).`,
87
+ `Output too large to inline (${content.length} chars).`,
78
88
  'Preview:',
79
89
  preview,
80
90
  ].join('\n');
@@ -83,14 +93,70 @@ async function handleReadFile(args, signal, resourceStore) {
83
93
  uri: entry.uri,
84
94
  name: entry.name,
85
95
  mimeType: entry.mimeType,
86
- description: 'Full file contents',
96
+ description: FULL_FILE_CONTENTS_DESCRIPTION,
87
97
  expiresAt: entry.expiresAt,
88
98
  }),
89
99
  ]);
90
100
  }
101
+ function buildReadProgressMessage(args) {
102
+ const name = path.basename(args.path);
103
+ if (args.startLine !== undefined) {
104
+ const end = args.endLine ?? '…';
105
+ return `${READ_TOOL_LABEL}: ${name} [lines ${args.startLine}–${end}]`;
106
+ }
107
+ if (args.head !== undefined)
108
+ return `${READ_TOOL_LABEL}: ${name} [head ${args.head}]`;
109
+ if (args.tail !== undefined)
110
+ return `${READ_TOOL_LABEL}: ${name} [tail ${args.tail}]`;
111
+ return `${READ_TOOL_LABEL}: ${name}`;
112
+ }
113
+ function buildReadCompletionMessage(args, result) {
114
+ const name = path.basename(args.path);
115
+ if (result.isError)
116
+ return `${READ_TOOL_LABEL}: ${name} • failed`;
117
+ const structured = result.structuredContent;
118
+ if (!structured.ok)
119
+ return `${READ_TOOL_LABEL}: ${name} • failed`;
120
+ const lines = structured.linesRead ?? structured.totalLines;
121
+ if (structured.startLine !== undefined) {
122
+ const end = structured.linesRead
123
+ ? structured.startLine + structured.linesRead - 1
124
+ : (structured.endLine ?? '…');
125
+ return `${READ_TOOL_LABEL}: ${name} • lines ${structured.startLine}–${end}`;
126
+ }
127
+ if (structured.head !== undefined) {
128
+ return structured.hasMoreLines
129
+ ? `${READ_TOOL_LABEL}: ${name} • first ${String(lines ?? structured.head)} lines`
130
+ : `${READ_TOOL_LABEL}: ${name} • ${String(lines ?? structured.head)} lines`;
131
+ }
132
+ if (structured.tail !== undefined) {
133
+ return structured.hasMoreLines
134
+ ? `${READ_TOOL_LABEL}: ${name} • last ${String(lines ?? structured.tail)} lines`
135
+ : `${READ_TOOL_LABEL}: ${name} • ${String(lines ?? structured.tail)} lines`;
136
+ }
137
+ if (structured.truncated) {
138
+ return `${READ_TOOL_LABEL}: ${name} • truncated [${String(lines)} lines]`;
139
+ }
140
+ return `${READ_TOOL_LABEL}: ${name} • ${String(lines)} lines`;
141
+ }
142
+ async function handleReadFile(args, signal, resourceStore) {
143
+ const options = buildReadOptions(args, signal);
144
+ const result = await readFile(args.path, options);
145
+ const structured = toStructuredReadFileResult(args, result);
146
+ if (args.includeHash) {
147
+ structured.contentHash = createHash('sha256')
148
+ .update(result.content, 'utf-8')
149
+ .digest('hex');
150
+ }
151
+ const externalizedResponse = maybeBuildExternalizedReadResponse(args.path, result.content, structured, resourceStore);
152
+ if (externalizedResponse) {
153
+ return externalizedResponse;
154
+ }
155
+ return buildToolResponse(result.content, structured);
156
+ }
91
157
  export function registerReadFileTool(server, options = {}) {
92
158
  const handler = (args, extra) => executeToolWithDiagnostics({
93
- toolName: 'read',
159
+ toolName: READ_TOOL_NAME,
94
160
  extra,
95
161
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
96
162
  context: { path: args.path },
@@ -99,49 +165,11 @@ export function registerReadFileTool(server, options = {}) {
99
165
  });
100
166
  const wrappedHandler = wrapToolHandler(handler, {
101
167
  guard: options.isInitialized,
102
- progressMessage: (args) => {
103
- const name = path.basename(args.path);
104
- if (args.startLine !== undefined) {
105
- const end = args.endLine ?? '…';
106
- return `🕮 read: ${name} [lines ${args.startLine}–${end}]`;
107
- }
108
- if (args.head !== undefined)
109
- return `🕮 read: ${name} [head ${args.head}]`;
110
- if (args.tail !== undefined)
111
- return `🕮 read: ${name} [tail ${args.tail}]`;
112
- return `🕮 read: ${name}`;
113
- },
114
- completionMessage: (args, result) => {
115
- const name = path.basename(args.path);
116
- if (result.isError)
117
- return `🕮 read: ${name} • failed`;
118
- const sc = result.structuredContent;
119
- if (!sc.ok)
120
- return `🕮 read: ${name} • failed`;
121
- const lines = sc.linesRead ?? sc.totalLines;
122
- if (sc.startLine !== undefined) {
123
- const end = sc.linesRead
124
- ? sc.startLine + sc.linesRead - 1
125
- : (sc.endLine ?? '…');
126
- return `🕮 read: ${name} • lines ${sc.startLine}–${end}`;
127
- }
128
- if (sc.head !== undefined) {
129
- return sc.hasMoreLines
130
- ? `🕮 read: ${name} • first ${lines ?? sc.head} lines`
131
- : `🕮 read: ${name} • ${lines ?? sc.head} lines`;
132
- }
133
- if (sc.tail !== undefined) {
134
- return sc.hasMoreLines
135
- ? `🕮 read: ${name} • last ${lines ?? sc.tail} lines`
136
- : `🕮 read: ${name} • ${lines ?? sc.tail} lines`;
137
- }
138
- if (sc.truncated)
139
- return `🕮 read: ${name} • truncated [${String(lines)} lines]`;
140
- return `🕮 read: ${name} • ${String(lines)} lines`;
141
- },
168
+ progressMessage: buildReadProgressMessage,
169
+ completionMessage: buildReadCompletionMessage,
142
170
  });
143
171
  const validatedHandler = withValidatedArgs(ReadFileInputSchema, wrappedHandler);
144
- if (registerToolTaskIfAvailable(server, 'read', READ_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
172
+ if (registerToolTaskIfAvailable(server, READ_TOOL_NAME, READ_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
145
173
  return;
146
- server.registerTool('read', withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), validatedHandler);
174
+ server.registerTool(READ_TOOL_NAME, withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), validatedHandler);
147
175
  }