@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.
- 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/resources/tool-info.js +23 -19
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +8 -4
- package/dist/server/bootstrap.js +39 -4
- package/dist/tools/apply-patch.js +24 -30
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/diff-files.js +26 -18
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +163 -112
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +101 -65
- package/dist/tools/read.js +103 -75
- package/dist/tools/replace-in-files.js +85 -38
- package/dist/tools/search-content.js +245 -216
- package/dist/tools/shared.js +35 -4
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +137 -100
- package/dist/tools.js +2 -2
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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,
|
|
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:
|
|
161
|
+
maxEntries: MAX_LIST_ENTRIES,
|
|
94
162
|
...(args.pattern !== undefined ? { pattern: args.pattern } : {}),
|
|
95
163
|
...(signal ? { signal } : {}),
|
|
96
164
|
};
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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:
|
|
115
|
-
summary:
|
|
116
|
-
total: mappedResults.length,
|
|
117
|
-
succeeded,
|
|
118
|
-
failed,
|
|
119
|
-
},
|
|
154
|
+
results: payload.structuredResults,
|
|
155
|
+
summary: payload.summary,
|
|
120
156
|
};
|
|
121
|
-
return buildToolResponse(
|
|
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:
|
|
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:
|
|
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(
|
|
181
|
+
progress.complete(`${READ_MANY_TOOL_LABEL}: ${context} • ${suffix}`, finalCurrent);
|
|
146
182
|
return result;
|
|
147
183
|
}
|
|
148
184
|
catch (error) {
|
|
149
|
-
progress.fail(
|
|
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,
|
|
196
|
+
if (registerToolTaskIfAvailable(server, READ_MANY_TOOL_NAME, READ_MANY_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|
|
161
197
|
return;
|
|
162
|
-
server.registerTool(
|
|
198
|
+
server.registerTool(READ_MANY_TOOL_NAME, withDefaultIcons({ ...READ_MANY_TOOL }, options.iconInfo), validatedHandler);
|
|
163
199
|
}
|
package/dist/tools/read.js
CHANGED
|
@@ -20,51 +20,61 @@ export const READ_FILE_TOOL = {
|
|
|
20
20
|
],
|
|
21
21
|
taskSupport: 'forbidden',
|
|
22
22
|
};
|
|
23
|
-
|
|
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 (
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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 (${
|
|
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:
|
|
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:
|
|
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:
|
|
103
|
-
|
|
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,
|
|
172
|
+
if (registerToolTaskIfAvailable(server, READ_TOOL_NAME, READ_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|
|
145
173
|
return;
|
|
146
|
-
server.registerTool(
|
|
174
|
+
server.registerTool(READ_TOOL_NAME, withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), validatedHandler);
|
|
147
175
|
}
|