@j0hanz/filesystem-mcp 1.1.2 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +514 -188
- package/dist/cli.js +29 -12
- package/dist/completions.js +50 -24
- package/dist/config.d.ts +4 -2
- package/dist/config.js +2 -1
- package/dist/index.js +14 -12
- package/dist/instructions.md +109 -97
- package/dist/lib/constants.js +25 -14
- package/dist/lib/errors.js +15 -8
- package/dist/lib/file-operations/common.d.ts +4 -0
- package/dist/lib/file-operations/common.js +9 -0
- package/dist/lib/file-operations/file-info.js +22 -10
- package/dist/lib/file-operations/gitignore.js +14 -11
- package/dist/lib/file-operations/glob-engine.d.ts +1 -0
- package/dist/lib/file-operations/glob-engine.js +46 -33
- package/dist/lib/file-operations/list-directory.js +31 -35
- package/dist/lib/file-operations/read-multiple-files.js +70 -62
- package/dist/lib/file-operations/search-content.js +83 -64
- package/dist/lib/file-operations/search-files.js +32 -30
- package/dist/lib/file-operations/search-worker.js +22 -12
- package/dist/lib/file-operations/tree.js +43 -34
- package/dist/lib/fs-helpers.js +61 -124
- package/dist/lib/observability.js +29 -28
- package/dist/lib/path-format.d.ts +1 -0
- package/dist/lib/path-format.js +7 -0
- package/dist/lib/path-policy.js +22 -20
- package/dist/lib/path-validation.js +13 -7
- package/dist/lib/resource-store.d.ts +2 -0
- package/dist/lib/resource-store.js +26 -5
- package/dist/lib/type-guards.d.ts +1 -0
- package/dist/lib/type-guards.js +3 -0
- package/dist/prompts.d.ts +1 -5
- package/dist/prompts.js +9 -16
- package/dist/resources.d.ts +1 -5
- package/dist/resources.js +12 -26
- package/dist/schemas.d.ts +232 -30
- package/dist/schemas.js +52 -90
- package/dist/server.js +96 -44
- package/dist/tools/apply-patch.js +23 -22
- package/dist/tools/calculate-hash.js +41 -43
- package/dist/tools/create-directory.js +17 -19
- package/dist/tools/delete-file.js +35 -37
- package/dist/tools/diff-files.js +15 -19
- package/dist/tools/edit-file.js +15 -18
- package/dist/tools/list-directory.js +24 -23
- package/dist/tools/move-file.js +17 -19
- package/dist/tools/read-multiple.js +55 -66
- package/dist/tools/read.js +26 -30
- package/dist/tools/replace-in-files.js +27 -33
- package/dist/tools/roots.js +8 -8
- package/dist/tools/search-content.js +73 -72
- package/dist/tools/search-files.js +44 -50
- package/dist/tools/shared.d.ts +44 -6
- package/dist/tools/shared.js +86 -64
- package/dist/tools/stat-many.js +44 -66
- package/dist/tools/stat.js +10 -37
- package/dist/tools/task-support.d.ts +9 -1
- package/dist/tools/task-support.js +86 -81
- package/dist/tools/tree.js +12 -28
- package/dist/tools/write-file.js +17 -19
- package/dist/tools.js +23 -18
- package/package.json +6 -7
|
@@ -3,23 +3,18 @@ import { formatOperationSummary, joinLines } from '../config.js';
|
|
|
3
3
|
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/constants.js';
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { searchFiles } from '../lib/file-operations/search-files.js';
|
|
6
|
-
import { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
7
|
-
import { withToolDiagnostics } from '../lib/observability.js';
|
|
8
6
|
import { SearchFilesInputSchema, SearchFilesOutputSchema } from '../schemas.js';
|
|
9
|
-
import { buildToolErrorResponse, buildToolResponse, createProgressReporter,
|
|
10
|
-
import {
|
|
7
|
+
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
11
9
|
const SEARCH_FILES_TOOL = {
|
|
12
10
|
title: 'Find Files',
|
|
13
11
|
description: 'Find files by glob pattern (e.g., **/*.ts). ' +
|
|
14
12
|
'Returns a list of matching files with metadata. ' +
|
|
15
|
-
'For text search inside files, use grep.'
|
|
13
|
+
'For text search inside files, use grep. ' +
|
|
14
|
+
'To bulk-edit the matched files, pass the same glob pattern to search_and_replace.',
|
|
16
15
|
inputSchema: SearchFilesInputSchema,
|
|
17
16
|
outputSchema: SearchFilesOutputSchema,
|
|
18
|
-
annotations:
|
|
19
|
-
readOnlyHint: true,
|
|
20
|
-
idempotentHint: true,
|
|
21
|
-
openWorldHint: false,
|
|
22
|
-
},
|
|
17
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
23
18
|
};
|
|
24
19
|
async function handleSearchFiles(args, signal, onProgress) {
|
|
25
20
|
const basePath = resolvePathOrRoot(args.path);
|
|
@@ -30,27 +25,30 @@ async function handleSearchFiles(args, signal, onProgress) {
|
|
|
30
25
|
sortBy: args.sortBy,
|
|
31
26
|
respectGitignore: !args.includeIgnored,
|
|
32
27
|
...(args.maxDepth !== undefined ? { maxDepth: args.maxDepth } : {}),
|
|
33
|
-
...(args.maxFilesScanned !== undefined
|
|
34
|
-
? { maxFilesScanned: args.maxFilesScanned }
|
|
35
|
-
: {}),
|
|
36
28
|
...(onProgress ? { onProgress } : {}),
|
|
37
29
|
...(signal ? { signal } : {}),
|
|
38
30
|
};
|
|
39
31
|
const result = await searchFiles(basePath, args.pattern, excludePatterns, searchOptions);
|
|
40
|
-
const relativeResults =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
const relativeResults = [];
|
|
33
|
+
for (const entry of result.results) {
|
|
34
|
+
relativeResults.push({
|
|
35
|
+
path: path.relative(result.basePath, entry.path),
|
|
36
|
+
size: entry.size,
|
|
37
|
+
modified: entry.modified?.toISOString(),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
45
40
|
const structured = {
|
|
46
41
|
ok: true,
|
|
47
42
|
root: basePath,
|
|
48
43
|
pattern: args.pattern,
|
|
49
44
|
results: relativeResults,
|
|
50
45
|
totalMatches: result.summary.matched,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
...(result.summary.truncated
|
|
47
|
+
? { truncated: result.summary.truncated }
|
|
48
|
+
: {}),
|
|
49
|
+
...(result.summary.skippedInaccessible
|
|
50
|
+
? { skippedInaccessible: result.summary.skippedInaccessible }
|
|
51
|
+
: {}),
|
|
54
52
|
...(result.summary.stoppedReason
|
|
55
53
|
? { stoppedReason: result.summary.stoppedReason }
|
|
56
54
|
: {}),
|
|
@@ -71,51 +69,47 @@ async function handleSearchFiles(args, signal, onProgress) {
|
|
|
71
69
|
truncated: result.summary.truncated,
|
|
72
70
|
...(truncatedReason ? { truncatedReason } : {}),
|
|
73
71
|
};
|
|
74
|
-
const textLines =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
const textLines = [];
|
|
73
|
+
if (relativeResults.length === 0) {
|
|
74
|
+
textLines.push('No matches');
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
textLines.push(`Found ${relativeResults.length}:`);
|
|
78
|
+
for (const entry of relativeResults) {
|
|
79
|
+
textLines.push(` ${entry.path}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
80
82
|
const text = joinLines(textLines) + formatOperationSummary(summaryOptions);
|
|
81
83
|
return buildToolResponse(text, structured);
|
|
82
84
|
}
|
|
83
85
|
export function registerSearchFilesTool(server, options = {}) {
|
|
84
|
-
const handler = (args, extra) =>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
87
|
+
toolName: 'find',
|
|
88
|
+
extra,
|
|
89
|
+
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
90
|
+
context: { path: args.path ?? '.' },
|
|
91
|
+
run: async (signal) => {
|
|
92
|
+
notifyProgress(extra, {
|
|
93
|
+
current: 0,
|
|
94
|
+
message: `🔎︎ find: ${args.pattern}`,
|
|
95
|
+
});
|
|
91
96
|
const result = await handleSearchFiles(args, signal, createProgressReporter(extra));
|
|
92
97
|
const sc = result.structuredContent;
|
|
93
|
-
const suffix = sc.ok && sc.totalMatches
|
|
94
|
-
? String(sc.totalMatches)
|
|
95
|
-
: 'No matches';
|
|
98
|
+
const suffix = sc.ok && sc.totalMatches ? String(sc.totalMatches) : 'No matches';
|
|
96
99
|
const finalCurrent = (sc.filesScanned ?? 0) + 1;
|
|
97
100
|
notifyProgress(extra, {
|
|
98
101
|
current: finalCurrent,
|
|
99
102
|
message: `🔎︎ find: ${args.pattern} ➟ ${suffix}`,
|
|
100
103
|
});
|
|
101
104
|
return result;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
}, (error) => buildToolErrorResponse(error, ErrorCode.E_INVALID_PATTERN, args.path)), { path: args.path ?? '.' });
|
|
105
|
+
},
|
|
106
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_INVALID_PATTERN, args.path),
|
|
107
|
+
});
|
|
107
108
|
const { isInitialized } = options;
|
|
108
109
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
109
110
|
guard: isInitialized,
|
|
110
111
|
});
|
|
111
|
-
|
|
112
|
-
const tasks = getExperimentalTaskRegistration(server);
|
|
113
|
-
if (tasks?.registerToolTask) {
|
|
114
|
-
tasks.registerToolTask('find', withDefaultIcons({
|
|
115
|
-
...SEARCH_FILES_TOOL,
|
|
116
|
-
execution: { taskSupport: 'optional' },
|
|
117
|
-
}, options.iconInfo), createToolTaskHandler(wrappedHandler, taskOptions));
|
|
112
|
+
if (registerToolTaskIfAvailable(server, 'find', SEARCH_FILES_TOOL, wrappedHandler, options.iconInfo, isInitialized))
|
|
118
113
|
return;
|
|
119
|
-
}
|
|
120
114
|
server.registerTool('find', withDefaultIcons({ ...SEARCH_FILES_TOOL }, options.iconInfo), wrappedHandler);
|
|
121
115
|
}
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
1
|
import type { ContentBlock, Icon, ProgressNotificationParams } from '@modelcontextprotocol/sdk/types.js';
|
|
3
2
|
import type { z } from 'zod';
|
|
3
|
+
import type { FileInfo } from '../config.js';
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import type { ResourceStore } from '../lib/resource-store.js';
|
|
6
6
|
import type { ToolErrorResponseSchema } from '../schemas.js';
|
|
7
|
+
export declare const READ_ONLY_TOOL_ANNOTATIONS: {
|
|
8
|
+
readonly readOnlyHint: true;
|
|
9
|
+
readonly idempotentHint: true;
|
|
10
|
+
readonly openWorldHint: false;
|
|
11
|
+
};
|
|
12
|
+
export declare const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS: {
|
|
13
|
+
readonly readOnlyHint: false;
|
|
14
|
+
readonly destructiveHint: true;
|
|
15
|
+
readonly openWorldHint: false;
|
|
16
|
+
};
|
|
17
|
+
export declare const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS: {
|
|
18
|
+
readonly readOnlyHint: false;
|
|
19
|
+
readonly idempotentHint: true;
|
|
20
|
+
readonly openWorldHint: false;
|
|
21
|
+
};
|
|
7
22
|
type ResourceEntry = ReturnType<ResourceStore['putText']>;
|
|
8
23
|
export declare function maybeExternalizeTextContent(resourceStore: ResourceStore | undefined, content: string, params: {
|
|
9
24
|
name: string;
|
|
@@ -18,7 +33,7 @@ export declare function buildResourceLink(params: {
|
|
|
18
33
|
mimeType?: string;
|
|
19
34
|
description?: string;
|
|
20
35
|
}): ContentBlock;
|
|
21
|
-
export declare function buildToolResponse<T>(text: string, structuredContent: T, extraContent?: ContentBlock[]
|
|
36
|
+
export declare function buildToolResponse<T>(text: string, structuredContent: T, extraContent?: ContentBlock[]): {
|
|
22
37
|
content: ContentBlock[];
|
|
23
38
|
structuredContent: T;
|
|
24
39
|
};
|
|
@@ -54,14 +69,37 @@ export interface ToolRegistrationOptions {
|
|
|
54
69
|
serverIcon?: string;
|
|
55
70
|
iconInfo?: IconInfo;
|
|
56
71
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
interface FileInfoPayload {
|
|
73
|
+
name: string;
|
|
74
|
+
path: string;
|
|
75
|
+
type: FileInfo['type'];
|
|
76
|
+
size: number;
|
|
77
|
+
tokenEstimate?: number;
|
|
78
|
+
created: string;
|
|
79
|
+
modified: string;
|
|
80
|
+
accessed: string;
|
|
81
|
+
permissions: string;
|
|
82
|
+
isHidden: boolean;
|
|
83
|
+
mimeType?: string;
|
|
84
|
+
symlinkTarget?: string;
|
|
85
|
+
}
|
|
86
|
+
export declare function buildFileInfoPayload(info: FileInfo): FileInfoPayload;
|
|
87
|
+
interface ToolExecutionOptions<T> {
|
|
88
|
+
toolName: string;
|
|
89
|
+
extra: ToolExtra;
|
|
90
|
+
run: (signal: AbortSignal | undefined) => ToolResponse<T> | Promise<ToolResponse<T>>;
|
|
91
|
+
onError: (error: unknown) => ToolResult<T>;
|
|
92
|
+
context?: Record<string, unknown>;
|
|
93
|
+
timedSignal?: {
|
|
94
|
+
timeoutMs?: number;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export declare function executeToolWithDiagnostics<T>(options: ToolExecutionOptions<T>): Promise<ToolResult<T>>;
|
|
61
98
|
export declare function buildToolErrorResponse(error: unknown, defaultCode: ErrorCode, path?: string): ToolErrorResponse;
|
|
62
99
|
export declare function createProgressReporter(extra: ToolExtra): (progress: {
|
|
63
100
|
total?: number;
|
|
64
101
|
current: number;
|
|
102
|
+
message?: string;
|
|
65
103
|
}) => void;
|
|
66
104
|
export declare function notifyProgress(extra: ToolExtra, progress: {
|
|
67
105
|
current: number;
|
package/dist/tools/shared.js
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
-
import { inspect } from 'node:util';
|
|
2
1
|
import { createDetailedError, ErrorCode, formatDetailedError, getSuggestion, McpError, } from '../lib/errors.js';
|
|
2
|
+
import { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
3
|
+
import { withToolDiagnostics } from '../lib/observability.js';
|
|
3
4
|
import { getAllowedDirectories } from '../lib/path-validation.js';
|
|
4
5
|
const MAX_INLINE_CONTENT_CHARS = 20_000;
|
|
5
6
|
const MAX_INLINE_PREVIEW_CHARS = 4_000;
|
|
7
|
+
const PROGRESS_RATE_LIMIT_MS = 50;
|
|
8
|
+
export const READ_ONLY_TOOL_ANNOTATIONS = {
|
|
9
|
+
readOnlyHint: true,
|
|
10
|
+
idempotentHint: true,
|
|
11
|
+
openWorldHint: false,
|
|
12
|
+
};
|
|
13
|
+
export const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS = {
|
|
14
|
+
readOnlyHint: false,
|
|
15
|
+
destructiveHint: true,
|
|
16
|
+
openWorldHint: false,
|
|
17
|
+
};
|
|
18
|
+
export const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS = {
|
|
19
|
+
readOnlyHint: false,
|
|
20
|
+
idempotentHint: true,
|
|
21
|
+
openWorldHint: false,
|
|
22
|
+
};
|
|
6
23
|
function buildTextPreview(text) {
|
|
7
24
|
if (text.length <= MAX_INLINE_PREVIEW_CHARS)
|
|
8
25
|
return text;
|
|
@@ -32,42 +49,9 @@ export function buildResourceLink(params) {
|
|
|
32
49
|
...(params.mimeType ? { mimeType: params.mimeType } : {}),
|
|
33
50
|
};
|
|
34
51
|
}
|
|
35
|
-
function buildContentBlock(text, structuredContent, extraContent = []
|
|
36
|
-
let json;
|
|
37
|
-
try {
|
|
38
|
-
json = JSON.stringify(structuredContent);
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
const preview = inspect(structuredContent, {
|
|
42
|
-
depth: 4,
|
|
43
|
-
colors: false,
|
|
44
|
-
compact: 3,
|
|
45
|
-
breakLength: 80,
|
|
46
|
-
});
|
|
47
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
48
|
-
json = JSON.stringify({
|
|
49
|
-
ok: false,
|
|
50
|
-
error: `Failed to serialize structuredContent: ${errorMessage}`,
|
|
51
|
-
preview,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
const externalized = maybeExternalizeTextContent(resourceStore, json, {
|
|
55
|
-
name: 'tool:structuredContent',
|
|
56
|
-
mimeType: 'application/json',
|
|
57
|
-
});
|
|
58
|
-
const jsonContent = externalized
|
|
59
|
-
? [
|
|
60
|
-
{ type: 'text', text: externalized.preview },
|
|
61
|
-
buildResourceLink({
|
|
62
|
-
uri: externalized.entry.uri,
|
|
63
|
-
name: externalized.entry.name,
|
|
64
|
-
mimeType: externalized.entry.mimeType,
|
|
65
|
-
description: 'Full structuredContent JSON',
|
|
66
|
-
}),
|
|
67
|
-
]
|
|
68
|
-
: [{ type: 'text', text: json }];
|
|
52
|
+
function buildContentBlock(text, structuredContent, extraContent = []) {
|
|
69
53
|
return {
|
|
70
|
-
content: [{ type: 'text', text }, ...extraContent
|
|
54
|
+
content: [{ type: 'text', text }, ...extraContent],
|
|
71
55
|
structuredContent,
|
|
72
56
|
};
|
|
73
57
|
}
|
|
@@ -79,8 +63,8 @@ function resolveDetailedError(error, defaultCode, path) {
|
|
|
79
63
|
}
|
|
80
64
|
return detailed;
|
|
81
65
|
}
|
|
82
|
-
export function buildToolResponse(text, structuredContent, extraContent = []
|
|
83
|
-
return buildContentBlock(text, structuredContent, extraContent
|
|
66
|
+
export function buildToolResponse(text, structuredContent, extraContent = []) {
|
|
67
|
+
return buildContentBlock(text, structuredContent, extraContent);
|
|
84
68
|
}
|
|
85
69
|
function canSendProgress(extra) {
|
|
86
70
|
return (extra._meta?.progressToken !== undefined &&
|
|
@@ -103,25 +87,28 @@ export function withDefaultIcons(tool, iconInfo) {
|
|
|
103
87
|
],
|
|
104
88
|
};
|
|
105
89
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
90
|
+
export function buildFileInfoPayload(info) {
|
|
91
|
+
return {
|
|
92
|
+
name: info.name,
|
|
93
|
+
path: info.path,
|
|
94
|
+
type: info.type,
|
|
95
|
+
size: info.size,
|
|
96
|
+
...(info.tokenEstimate !== undefined
|
|
97
|
+
? { tokenEstimate: info.tokenEstimate }
|
|
98
|
+
: {}),
|
|
99
|
+
created: info.created.toISOString(),
|
|
100
|
+
modified: info.modified.toISOString(),
|
|
101
|
+
accessed: info.accessed.toISOString(),
|
|
102
|
+
permissions: info.permissions,
|
|
103
|
+
isHidden: info.isHidden,
|
|
104
|
+
...(info.mimeType !== undefined ? { mimeType: info.mimeType } : {}),
|
|
105
|
+
...(info.symlinkTarget !== undefined
|
|
106
|
+
? { symlinkTarget: info.symlinkTarget }
|
|
107
|
+
: {}),
|
|
108
|
+
};
|
|
122
109
|
}
|
|
123
110
|
const NOT_INITIALIZED_ERROR = new McpError(ErrorCode.E_INVALID_INPUT, 'Client not initialized; wait for notifications/initialized');
|
|
124
|
-
|
|
111
|
+
async function withToolErrorHandling(run, onError) {
|
|
125
112
|
try {
|
|
126
113
|
return await run();
|
|
127
114
|
}
|
|
@@ -129,6 +116,24 @@ export async function withToolErrorHandling(run, onError) {
|
|
|
129
116
|
return onError(error);
|
|
130
117
|
}
|
|
131
118
|
}
|
|
119
|
+
function getToolSignal(extraSignal, timedSignal) {
|
|
120
|
+
if (!timedSignal) {
|
|
121
|
+
return { signal: extraSignal, cleanup: () => { } };
|
|
122
|
+
}
|
|
123
|
+
const { signal, cleanup } = createTimedAbortSignal(extraSignal, timedSignal.timeoutMs);
|
|
124
|
+
return { signal, cleanup };
|
|
125
|
+
}
|
|
126
|
+
export async function executeToolWithDiagnostics(options) {
|
|
127
|
+
return withToolDiagnostics(options.toolName, () => withToolErrorHandling(async () => {
|
|
128
|
+
const { signal, cleanup } = getToolSignal(options.extra.signal, options.timedSignal);
|
|
129
|
+
try {
|
|
130
|
+
return await options.run(signal);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
cleanup();
|
|
134
|
+
}
|
|
135
|
+
}, options.onError), options.context);
|
|
136
|
+
}
|
|
132
137
|
export function buildToolErrorResponse(error, defaultCode, path) {
|
|
133
138
|
const detailed = resolveDetailedError(error, defaultCode, path);
|
|
134
139
|
const text = formatDetailedError(detailed);
|
|
@@ -163,8 +168,9 @@ async function sendProgressNotification(extra, params) {
|
|
|
163
168
|
params,
|
|
164
169
|
});
|
|
165
170
|
}
|
|
166
|
-
catch {
|
|
171
|
+
catch (error) {
|
|
167
172
|
// Ignore progress notification failures to avoid breaking tool execution.
|
|
173
|
+
console.error('Failed to send progress notification:', error);
|
|
168
174
|
}
|
|
169
175
|
}
|
|
170
176
|
export function createProgressReporter(extra) {
|
|
@@ -172,12 +178,25 @@ export function createProgressReporter(extra) {
|
|
|
172
178
|
return () => { };
|
|
173
179
|
}
|
|
174
180
|
const token = extra._meta.progressToken;
|
|
181
|
+
// State for monotonic enforcement and rate-limiting.
|
|
182
|
+
let lastProgress = -1;
|
|
183
|
+
let lastSentMs = 0;
|
|
175
184
|
return (progress) => {
|
|
176
|
-
const { current, total } = progress;
|
|
185
|
+
const { current, total, message } = progress;
|
|
186
|
+
// Enforce monotonic progress to prevent client confusion. Client behavior on
|
|
187
|
+
if (current <= lastProgress)
|
|
188
|
+
return;
|
|
189
|
+
// Enforce rate-limiting to prevent client flooding. Progress updates that are
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
if (now - lastSentMs < PROGRESS_RATE_LIMIT_MS)
|
|
192
|
+
return;
|
|
193
|
+
lastProgress = current;
|
|
194
|
+
lastSentMs = now;
|
|
177
195
|
void sendProgressNotification(extra, {
|
|
178
196
|
progressToken: token,
|
|
179
|
-
total,
|
|
180
197
|
progress: current,
|
|
198
|
+
...(total !== undefined ? { total } : {}),
|
|
199
|
+
...(message !== undefined ? { message } : {}),
|
|
181
200
|
});
|
|
182
201
|
};
|
|
183
202
|
}
|
|
@@ -194,7 +213,7 @@ export function notifyProgress(extra, progress) {
|
|
|
194
213
|
}
|
|
195
214
|
async function withProgress(message, extra, run, getCompletionMessage) {
|
|
196
215
|
if (!canSendProgress(extra)) {
|
|
197
|
-
return
|
|
216
|
+
return run();
|
|
198
217
|
}
|
|
199
218
|
const token = extra._meta.progressToken;
|
|
200
219
|
const total = 1;
|
|
@@ -216,11 +235,10 @@ async function withProgress(message, extra, run, getCompletionMessage) {
|
|
|
216
235
|
return result;
|
|
217
236
|
}
|
|
218
237
|
catch (error) {
|
|
219
|
-
|
|
238
|
+
void sendProgressNotification(extra, {
|
|
220
239
|
progressToken: token,
|
|
221
240
|
progress: total,
|
|
222
241
|
total,
|
|
223
|
-
message,
|
|
224
242
|
});
|
|
225
243
|
throw error;
|
|
226
244
|
}
|
|
@@ -237,9 +255,9 @@ export function wrapToolHandler(handler, options) {
|
|
|
237
255
|
const completionFn = completionMessage
|
|
238
256
|
? (result) => completionMessage(args, result)
|
|
239
257
|
: undefined;
|
|
240
|
-
return
|
|
258
|
+
return withProgress(message, resolvedExtra, () => handler(args, resolvedExtra), completionFn);
|
|
241
259
|
}
|
|
242
|
-
return
|
|
260
|
+
return handler(args, resolvedExtra);
|
|
243
261
|
};
|
|
244
262
|
}
|
|
245
263
|
export function resolvePathOrRoot(pathValue) {
|
|
@@ -252,5 +270,9 @@ export function resolvePathOrRoot(pathValue) {
|
|
|
252
270
|
if (roots.length > 1) {
|
|
253
271
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Multiple workspace roots configured. Provide an explicit path to disambiguate.');
|
|
254
272
|
}
|
|
255
|
-
|
|
273
|
+
const root = roots[0];
|
|
274
|
+
if (!root) {
|
|
275
|
+
throw new McpError(ErrorCode.E_ACCESS_DENIED, 'Workspace root is unexpectedly undefined');
|
|
276
|
+
}
|
|
277
|
+
return root;
|
|
256
278
|
}
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -1,104 +1,82 @@
|
|
|
1
|
-
import { formatBytes } from '../config.js';
|
|
1
|
+
import { formatBytes, joinLines } from '../config.js';
|
|
2
2
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { getMultipleFileInfo } from '../lib/file-operations/file-info.js';
|
|
5
|
-
import { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
6
|
-
import { withToolDiagnostics } from '../lib/observability.js';
|
|
7
5
|
import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
|
|
8
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
9
|
-
import {
|
|
6
|
+
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
10
8
|
const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
11
9
|
title: 'Get Multiple File Info',
|
|
12
10
|
description: 'Get metadata for multiple files or directories in one request.',
|
|
13
11
|
inputSchema: GetMultipleFileInfoInputSchema,
|
|
14
12
|
outputSchema: GetMultipleFileInfoOutputSchema,
|
|
15
|
-
annotations:
|
|
16
|
-
readOnlyHint: true,
|
|
17
|
-
idempotentHint: true,
|
|
18
|
-
openWorldHint: false,
|
|
19
|
-
},
|
|
13
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
20
14
|
};
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
name
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
permissions: info.permissions,
|
|
34
|
-
isHidden: info.isHidden,
|
|
35
|
-
...(info.mimeType !== undefined ? { mimeType: info.mimeType } : {}),
|
|
36
|
-
...(info.symlinkTarget !== undefined
|
|
37
|
-
? { symlinkTarget: info.symlinkTarget }
|
|
38
|
-
: {}),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function formatFileInfoSummary(pathValue, info) {
|
|
42
|
-
return `${pathValue} (${info.type}, ${formatBytes(info.size)})`;
|
|
15
|
+
function formatFileInfoDetail(info) {
|
|
16
|
+
const lines = [
|
|
17
|
+
`${info.name} (${info.type})`,
|
|
18
|
+
` Path: ${info.path}`,
|
|
19
|
+
` Size: ${formatBytes(info.size)}`,
|
|
20
|
+
` Modified: ${info.modified.toISOString()}`,
|
|
21
|
+
];
|
|
22
|
+
if (info.mimeType)
|
|
23
|
+
lines.push(` Type: ${info.mimeType}`);
|
|
24
|
+
if (info.symlinkTarget)
|
|
25
|
+
lines.push(` Target: ${info.symlinkTarget}`);
|
|
26
|
+
return joinLines(lines);
|
|
43
27
|
}
|
|
44
28
|
async function handleGetMultipleFileInfo(args, signal) {
|
|
45
29
|
const result = await getMultipleFileInfo(args.paths, {
|
|
46
30
|
includeMimeType: true,
|
|
47
31
|
...(signal ? { signal } : {}),
|
|
48
32
|
});
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
33
|
+
const structuredResults = [];
|
|
34
|
+
const textBlocks = [];
|
|
35
|
+
for (const entry of result.results) {
|
|
36
|
+
structuredResults.push({
|
|
52
37
|
path: entry.path,
|
|
53
38
|
info: entry.info ? buildFileInfoPayload(entry.info) : undefined,
|
|
54
39
|
error: entry.error,
|
|
55
|
-
})
|
|
40
|
+
});
|
|
41
|
+
if (entry.error) {
|
|
42
|
+
textBlocks.push(`${entry.path}: ${entry.error}`);
|
|
43
|
+
}
|
|
44
|
+
else if (entry.info) {
|
|
45
|
+
textBlocks.push(formatFileInfoDetail(entry.info));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
textBlocks.push(entry.path);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const structured = {
|
|
52
|
+
ok: true,
|
|
53
|
+
results: structuredResults,
|
|
56
54
|
summary: {
|
|
57
55
|
total: result.summary.total,
|
|
58
56
|
succeeded: result.summary.succeeded,
|
|
59
57
|
failed: result.summary.failed,
|
|
60
58
|
},
|
|
61
59
|
};
|
|
62
|
-
const text =
|
|
63
|
-
.map((entry) => {
|
|
64
|
-
if (entry.error) {
|
|
65
|
-
return `${entry.path}: ${entry.error}`;
|
|
66
|
-
}
|
|
67
|
-
if (entry.info) {
|
|
68
|
-
return formatFileInfoSummary(entry.path, entry.info);
|
|
69
|
-
}
|
|
70
|
-
return entry.path;
|
|
71
|
-
})
|
|
72
|
-
.join('\n');
|
|
60
|
+
const text = textBlocks.join('\n\n');
|
|
73
61
|
return buildToolResponse(text, structured);
|
|
74
62
|
}
|
|
75
63
|
export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
76
64
|
const handler = (args, extra) => {
|
|
77
65
|
const primaryPath = args.paths[0] ?? '';
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}, (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, primaryPath)), { path: primaryPath });
|
|
66
|
+
return executeToolWithDiagnostics({
|
|
67
|
+
toolName: 'stat_many',
|
|
68
|
+
extra,
|
|
69
|
+
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
70
|
+
context: { path: primaryPath },
|
|
71
|
+
run: (signal) => handleGetMultipleFileInfo(args, signal),
|
|
72
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, primaryPath),
|
|
73
|
+
});
|
|
87
74
|
};
|
|
88
75
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
89
76
|
guard: options.isInitialized,
|
|
90
77
|
progressMessage: (args) => `🕮 stat_many: ${args.paths.length} paths`,
|
|
91
78
|
});
|
|
92
|
-
|
|
93
|
-
? { guard: options.isInitialized }
|
|
94
|
-
: undefined;
|
|
95
|
-
const tasks = getExperimentalTaskRegistration(server);
|
|
96
|
-
if (tasks?.registerToolTask) {
|
|
97
|
-
tasks.registerToolTask('stat_many', withDefaultIcons({
|
|
98
|
-
...GET_MULTIPLE_FILE_INFO_TOOL,
|
|
99
|
-
execution: { taskSupport: 'optional' },
|
|
100
|
-
}, options.iconInfo), createToolTaskHandler(wrappedHandler, taskOptions));
|
|
79
|
+
if (registerToolTaskIfAvailable(server, 'stat_many', GET_MULTIPLE_FILE_INFO_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
101
80
|
return;
|
|
102
|
-
}
|
|
103
81
|
server.registerTool('stat_many', withDefaultIcons({ ...GET_MULTIPLE_FILE_INFO_TOOL }, options.iconInfo), wrappedHandler);
|
|
104
82
|
}
|