@j0hanz/filesystem-mcp 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/completions.js +25 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/file-operations/metadata.js +71 -142
- package/dist/lib/file-operations/search.js +119 -19
- package/dist/lib/file-operations/traversal.js +45 -102
- package/dist/lib/fs-helpers.js +175 -158
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.js +58 -0
- package/dist/resources/generated-instructions.js +11 -3
- package/dist/resources/tool-catalog.js +49 -3
- package/dist/resources/tool-info.d.ts +1 -0
- package/dist/resources/tool-info.js +139 -8
- package/dist/resources.js +2 -0
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.js +10 -4
- package/dist/server/bootstrap.js +41 -5
- package/dist/tools/apply-patch.js +30 -33
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/contract.d.ts +1 -1
- package/dist/tools/create-directory.js +3 -3
- package/dist/tools/delete-file.js +2 -2
- package/dist/tools/diff-files.js +32 -20
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +172 -104
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/move-file.js +3 -3
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +123 -99
- package/dist/tools/read.js +104 -76
- package/dist/tools/replace-in-files.d.ts +5 -2
- package/dist/tools/replace-in-files.js +142 -76
- package/dist/tools/roots.js +1 -1
- package/dist/tools/search-content.js +249 -218
- package/dist/tools/shared.js +39 -7
- package/dist/tools/stat-many.js +12 -19
- package/dist/tools/stat.js +1 -1
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +140 -101
- package/dist/tools/write-file.js +4 -4
- package/dist/tools.js +2 -2
- package/package.json +1 -1
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { DEFAULT_SEARCH_CONTENT_RESULTS, MAX_SEARCH_RESULTS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
|
|
2
3
|
import { ALL_TOOLS } from '../tools.js';
|
|
4
|
+
function getTaskSupportLabel(taskSupport) {
|
|
5
|
+
switch (taskSupport) {
|
|
6
|
+
case 'optional':
|
|
7
|
+
return '[Task: Optional]';
|
|
8
|
+
case 'required':
|
|
9
|
+
return '[Task: Required]';
|
|
10
|
+
default:
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
3
14
|
function toEntry(contract) {
|
|
4
15
|
const annotations = [];
|
|
5
16
|
if (contract.annotations?.destructiveHint)
|
|
@@ -8,11 +19,12 @@ function toEntry(contract) {
|
|
|
8
19
|
annotations.push('[Idempotent]');
|
|
9
20
|
if (contract.annotations?.readOnlyHint)
|
|
10
21
|
annotations.push('[Read-Only]');
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
annotations.push(
|
|
22
|
+
const taskLabel = getTaskSupportLabel(contract.taskSupport);
|
|
23
|
+
if (taskLabel)
|
|
24
|
+
annotations.push(taskLabel);
|
|
14
25
|
return {
|
|
15
26
|
name: contract.name,
|
|
27
|
+
title: contract.title,
|
|
16
28
|
description: contract.description,
|
|
17
29
|
...(annotations.length > 0 ? { annotations } : {}),
|
|
18
30
|
...(contract.nuances && contract.nuances.length > 0
|
|
@@ -43,6 +55,11 @@ export function getTaskCapableToolNames() {
|
|
|
43
55
|
contract.taskSupport === 'required')
|
|
44
56
|
.map((contract) => contract.name);
|
|
45
57
|
}
|
|
58
|
+
export function getTaskToolNamesBySupport(taskSupport) {
|
|
59
|
+
return getSortedToolContracts()
|
|
60
|
+
.filter((contract) => contract.taskSupport === taskSupport)
|
|
61
|
+
.map((contract) => contract.name);
|
|
62
|
+
}
|
|
46
63
|
export function buildCoreContextPack() {
|
|
47
64
|
const rows = getSortedToolContracts().map((contract) => {
|
|
48
65
|
const e = ENTRIES[contract.name];
|
|
@@ -61,25 +78,139 @@ export function getSharedConstraints() {
|
|
|
61
78
|
'If response includes `resourceUri`, call `resources/read` immediately — cached results expire on restart.',
|
|
62
79
|
];
|
|
63
80
|
}
|
|
81
|
+
function formatTaskSupportLabel(taskSupport) {
|
|
82
|
+
switch (taskSupport) {
|
|
83
|
+
case 'optional':
|
|
84
|
+
return 'optional';
|
|
85
|
+
case 'required':
|
|
86
|
+
return 'required';
|
|
87
|
+
default:
|
|
88
|
+
return 'forbidden';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function formatAnnotationValue(value) {
|
|
92
|
+
return value ? 'true' : 'false';
|
|
93
|
+
}
|
|
94
|
+
function toJsonSchemaObject(schema, io = 'output') {
|
|
95
|
+
return z.toJSONSchema(schema, { io });
|
|
96
|
+
}
|
|
97
|
+
function summarizeSchemaType(schema) {
|
|
98
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
99
|
+
return `enum(${schema.enum.map((value) => JSON.stringify(value)).join(', ')})`;
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
|
|
102
|
+
return schema.anyOf.map(summarizeSchemaType).join(' | ');
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
|
|
105
|
+
return schema.oneOf.map(summarizeSchemaType).join(' | ');
|
|
106
|
+
}
|
|
107
|
+
if (schema.type === 'array') {
|
|
108
|
+
const itemType = schema.items
|
|
109
|
+
? summarizeSchemaType(schema.items)
|
|
110
|
+
: 'unknown';
|
|
111
|
+
return `array<${itemType}>`;
|
|
112
|
+
}
|
|
113
|
+
if (typeof schema.type === 'string' && schema.type.length > 0) {
|
|
114
|
+
return schema.type;
|
|
115
|
+
}
|
|
116
|
+
return 'unknown';
|
|
117
|
+
}
|
|
118
|
+
function buildSchemaFieldLines(label, schema) {
|
|
119
|
+
if (!schema) {
|
|
120
|
+
return [`<${label}>`, '- none', `</${label}>`];
|
|
121
|
+
}
|
|
122
|
+
const io = label === 'input_fields' ? 'input' : 'output';
|
|
123
|
+
const jsonSchema = toJsonSchemaObject(schema, io);
|
|
124
|
+
const properties = jsonSchema.properties ?? {};
|
|
125
|
+
const required = new Set(jsonSchema.required ?? []);
|
|
126
|
+
const fieldNames = Object.keys(properties);
|
|
127
|
+
const lines = [`<${label}>`];
|
|
128
|
+
if (typeof jsonSchema.description === 'string' &&
|
|
129
|
+
jsonSchema.description.length > 0) {
|
|
130
|
+
lines.push(`- Schema constraints: ${jsonSchema.description}`);
|
|
131
|
+
}
|
|
132
|
+
if (fieldNames.length === 0) {
|
|
133
|
+
lines.push('- object with no fields', `</${label}>`);
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
lines.push(...fieldNames.map((fieldName) => {
|
|
137
|
+
const fieldSchema = properties[fieldName] ?? {};
|
|
138
|
+
const type = summarizeSchemaType(fieldSchema);
|
|
139
|
+
const requiredLabel = required.has(fieldName) ? 'required' : 'optional';
|
|
140
|
+
const description = typeof fieldSchema.description === 'string' &&
|
|
141
|
+
fieldSchema.description.length > 0
|
|
142
|
+
? fieldSchema.description
|
|
143
|
+
: 'No description.';
|
|
144
|
+
return `- ${fieldName} (${type}, ${requiredLabel}): ${description}`;
|
|
145
|
+
}), `</${label}>`);
|
|
146
|
+
return lines;
|
|
147
|
+
}
|
|
148
|
+
function buildProtocolNotes(contract) {
|
|
149
|
+
const notes = [
|
|
150
|
+
'- Protocol failures use JSON-RPC `error`; execution failures use tool result `isError: true`.',
|
|
151
|
+
];
|
|
152
|
+
if (contract.outputSchema) {
|
|
153
|
+
notes.push('- Successful responses include `structuredContent` that must match the declared output schema.');
|
|
154
|
+
}
|
|
155
|
+
if (contract.taskSupport === 'optional') {
|
|
156
|
+
notes.push('- Supports inline execution by default and task mode when durable polling or deferred results are needed.');
|
|
157
|
+
}
|
|
158
|
+
if (contract.taskSupport === 'required') {
|
|
159
|
+
notes.push('- Must run in task mode; callers should poll `tasks/get` and fetch the payload via `tasks/result`.');
|
|
160
|
+
}
|
|
161
|
+
if (contract.taskSupport === 'forbidden') {
|
|
162
|
+
notes.push('- Runs inline only; task augmentation is not supported for this tool.');
|
|
163
|
+
}
|
|
164
|
+
return notes;
|
|
165
|
+
}
|
|
64
166
|
export function buildToolInfo(name) {
|
|
167
|
+
const contract = CONTRACTS_BY_NAME.get(name);
|
|
65
168
|
const entry = ENTRIES[name];
|
|
66
|
-
if (!entry)
|
|
169
|
+
if (!entry || !contract)
|
|
67
170
|
return undefined;
|
|
68
|
-
const lines = [
|
|
171
|
+
const lines = [
|
|
172
|
+
`<tool_info name="${entry.name}">`,
|
|
173
|
+
`## ${entry.name}`,
|
|
174
|
+
'',
|
|
175
|
+
`Title: ${entry.title}`,
|
|
176
|
+
`Description: ${entry.description}`,
|
|
177
|
+
'',
|
|
178
|
+
'<execution>',
|
|
179
|
+
`- taskSupport: ${formatTaskSupportLabel(contract.taskSupport)}`,
|
|
180
|
+
'</execution>',
|
|
181
|
+
'',
|
|
182
|
+
'<annotations>',
|
|
183
|
+
`- readOnlyHint: ${formatAnnotationValue(contract.annotations?.readOnlyHint)}`,
|
|
184
|
+
`- idempotentHint: ${formatAnnotationValue(contract.annotations?.idempotentHint)}`,
|
|
185
|
+
`- destructiveHint: ${formatAnnotationValue(contract.annotations?.destructiveHint)}`,
|
|
186
|
+
`- openWorldHint: ${formatAnnotationValue(contract.annotations?.openWorldHint)}`,
|
|
187
|
+
'</annotations>',
|
|
188
|
+
'',
|
|
189
|
+
...buildSchemaFieldLines('input_fields', contract.inputSchema),
|
|
190
|
+
'',
|
|
191
|
+
...buildSchemaFieldLines('output_fields', contract.outputSchema),
|
|
192
|
+
'',
|
|
193
|
+
'<protocol_notes>',
|
|
194
|
+
...buildProtocolNotes(contract),
|
|
195
|
+
'</protocol_notes>',
|
|
196
|
+
];
|
|
69
197
|
if (entry.annotations && entry.annotations.length > 0) {
|
|
70
|
-
lines.push('',
|
|
198
|
+
lines.push('', `<quick_hints>${entry.annotations.join(' ')}</quick_hints>`);
|
|
71
199
|
}
|
|
72
200
|
if (entry.nuances && entry.nuances.length > 0) {
|
|
73
|
-
lines.push('', '
|
|
201
|
+
lines.push('', '<nuances>');
|
|
74
202
|
for (const nuance of entry.nuances) {
|
|
75
203
|
lines.push(`- ${nuance}`);
|
|
76
204
|
}
|
|
205
|
+
lines.push('</nuances>');
|
|
77
206
|
}
|
|
78
207
|
if (entry.gotchas && entry.gotchas.length > 0) {
|
|
79
|
-
lines.push('', '
|
|
208
|
+
lines.push('', '<gotchas>');
|
|
80
209
|
for (const gotcha of entry.gotchas) {
|
|
81
210
|
lines.push(`- ${gotcha}`);
|
|
82
211
|
}
|
|
212
|
+
lines.push('</gotchas>');
|
|
83
213
|
}
|
|
214
|
+
lines.push('</tool_info>');
|
|
84
215
|
return lines.join('\n');
|
|
85
216
|
}
|
package/dist/resources.js
CHANGED
|
@@ -13,6 +13,8 @@ const TOOL_INFO_TEMPLATE = new ResourceTemplate('internal://tool-info/{name}', {
|
|
|
13
13
|
resources: getToolContracts().map((contract) => ({
|
|
14
14
|
uri: `internal://tool-info/${contract.name}`,
|
|
15
15
|
name: contract.name,
|
|
16
|
+
title: contract.title,
|
|
17
|
+
description: contract.description,
|
|
16
18
|
mimeType: 'text/markdown',
|
|
17
19
|
})),
|
|
18
20
|
}),
|
package/dist/schemas.d.ts
CHANGED
|
@@ -556,6 +556,8 @@ export declare const EditFileOutputSchema: z.ZodObject<{
|
|
|
556
556
|
ok: z.ZodBoolean;
|
|
557
557
|
path: z.ZodOptional<z.ZodString>;
|
|
558
558
|
appliedEdits: z.ZodOptional<z.ZodNumber>;
|
|
559
|
+
linesAdded: z.ZodOptional<z.ZodNumber>;
|
|
560
|
+
linesRemoved: z.ZodOptional<z.ZodNumber>;
|
|
559
561
|
lineRange: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
|
|
560
562
|
unmatchedEdits: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
561
563
|
diff: z.ZodOptional<z.ZodString>;
|
package/dist/schemas.js
CHANGED
|
@@ -253,7 +253,8 @@ export const ReadFileInputSchema = z
|
|
|
253
253
|
}),
|
|
254
254
|
includeHash: defaultFalseBoolean('Include SHA-256 hash of full file content'),
|
|
255
255
|
})
|
|
256
|
-
.superRefine(validateReadRange)
|
|
256
|
+
.superRefine(validateReadRange)
|
|
257
|
+
.describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'. 'endLine' requires 'startLine'.");
|
|
257
258
|
export const ReadMultipleFilesInputSchema = z
|
|
258
259
|
.strictObject({
|
|
259
260
|
paths: z
|
|
@@ -268,7 +269,8 @@ export const ReadMultipleFilesInputSchema = z
|
|
|
268
269
|
endLine: 'End line (1-based, inclusive) per file. Requires startLine.',
|
|
269
270
|
}),
|
|
270
271
|
})
|
|
271
|
-
.superRefine(validateReadRange)
|
|
272
|
+
.superRefine(validateReadRange)
|
|
273
|
+
.describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'. 'endLine' requires 'startLine'.");
|
|
272
274
|
export const GetFileInfoInputSchema = z.strictObject({
|
|
273
275
|
path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
|
|
274
276
|
});
|
|
@@ -451,7 +453,8 @@ export const CreateDirectoryInputSchema = z
|
|
|
451
453
|
.refine((data) => data.path !== undefined || data.paths !== undefined, {
|
|
452
454
|
error: "Either 'path' or 'paths' must be provided",
|
|
453
455
|
path: ['path'],
|
|
454
|
-
})
|
|
456
|
+
})
|
|
457
|
+
.describe("Provide either 'path' or 'paths'.");
|
|
455
458
|
export const CreateDirectoryOutputSchema = z.strictObject({
|
|
456
459
|
ok: z.boolean(),
|
|
457
460
|
path: z.string().optional(),
|
|
@@ -488,6 +491,8 @@ export const EditFileOutputSchema = z.strictObject({
|
|
|
488
491
|
ok: z.boolean(),
|
|
489
492
|
path: z.string().optional(),
|
|
490
493
|
appliedEdits: z.number().optional(),
|
|
494
|
+
linesAdded: z.number().optional().describe('Lines added'),
|
|
495
|
+
linesRemoved: z.number().optional().describe('Lines removed'),
|
|
491
496
|
lineRange: z
|
|
492
497
|
.tuple([z.number(), z.number()])
|
|
493
498
|
.optional()
|
|
@@ -508,7 +513,8 @@ export const MoveFileInputSchema = z
|
|
|
508
513
|
.refine((data) => (data.source ?? data.sources) !== undefined, {
|
|
509
514
|
error: "Either 'source' or 'sources' must be provided",
|
|
510
515
|
path: ['source'],
|
|
511
|
-
})
|
|
516
|
+
})
|
|
517
|
+
.describe("Provide either 'source' or 'sources'.");
|
|
512
518
|
export const MoveFileOutputSchema = z.strictObject({
|
|
513
519
|
ok: z.boolean(),
|
|
514
520
|
source: z.string().optional(),
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -5,7 +5,7 @@ import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextproto
|
|
|
5
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
7
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
|
-
import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import { isInitializeRequest, LATEST_PROTOCOL_VERSION, SetLevelRequestSchema, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import { DEFAULT_LOG_LEVEL, parseEnvInt } from '../lib/constants.js';
|
|
10
10
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
11
11
|
import { withAllowedDirectoriesState } from '../lib/paths.js';
|
|
@@ -13,7 +13,7 @@ import { createInMemoryResourceStore } from '../lib/resource-store.js';
|
|
|
13
13
|
import { isRecord } from '../lib/utils.js';
|
|
14
14
|
import { registerCompletions } from '../completions.js';
|
|
15
15
|
import { pkgInfo } from '../pkg-info.js';
|
|
16
|
-
import { registerAnalyzePathPrompt, registerCompareFilesPrompt, registerGetHelpPrompt, } from '../prompts.js';
|
|
16
|
+
import { registerAnalyzePathPrompt, registerCompareFilesPrompt, registerGetHelpPrompt, registerGetToolHelpPrompt, } from '../prompts.js';
|
|
17
17
|
import { registerInstructionResource, registerMetricsResource, registerResultResources, registerToolCatalogResource, registerToolInfoResource, registerWorkflowGuideResource, } from '../resources.js';
|
|
18
18
|
import { buildServerInstructions } from '../resources/generated-instructions.js';
|
|
19
19
|
import { registerAllTools } from '../tools.js';
|
|
@@ -174,6 +174,7 @@ export async function createServer(options = {}) {
|
|
|
174
174
|
registerGetHelpPrompt(server, serverInstructions, localIcon);
|
|
175
175
|
registerCompareFilesPrompt(server, localIcon);
|
|
176
176
|
registerAnalyzePathPrompt(server, localIcon);
|
|
177
|
+
registerGetToolHelpPrompt(server, localIcon);
|
|
177
178
|
registerResultResources(server, resourceStore, localIcon);
|
|
178
179
|
registerMetricsResource(server, localIcon);
|
|
179
180
|
registerCompletions(server, serverInstructions);
|
|
@@ -242,7 +243,7 @@ async function readRequestBody(req) {
|
|
|
242
243
|
req.on('error', reject);
|
|
243
244
|
});
|
|
244
245
|
}
|
|
245
|
-
async function createHttpSession(options, sessions) {
|
|
246
|
+
async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
|
|
246
247
|
const mcpServer = await createServer(options);
|
|
247
248
|
const rootsManager = getRootsManager(mcpServer);
|
|
248
249
|
rootsManager.registerHandlers(mcpServer);
|
|
@@ -254,6 +255,7 @@ async function createHttpSession(options, sessions) {
|
|
|
254
255
|
server: mcpServer,
|
|
255
256
|
rootsManager,
|
|
256
257
|
transport,
|
|
258
|
+
negotiatedProtocolVersion,
|
|
257
259
|
});
|
|
258
260
|
rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
|
|
259
261
|
},
|
|
@@ -269,7 +271,12 @@ async function createHttpSession(options, sessions) {
|
|
|
269
271
|
});
|
|
270
272
|
};
|
|
271
273
|
await mcpServer.connect(transport);
|
|
272
|
-
return {
|
|
274
|
+
return {
|
|
275
|
+
server: mcpServer,
|
|
276
|
+
rootsManager,
|
|
277
|
+
transport,
|
|
278
|
+
negotiatedProtocolVersion,
|
|
279
|
+
};
|
|
273
280
|
}
|
|
274
281
|
function sendJsonRpcError(res, status, code, message) {
|
|
275
282
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
@@ -298,6 +305,29 @@ function getSessionId(req) {
|
|
|
298
305
|
? rawSessionId
|
|
299
306
|
: undefined;
|
|
300
307
|
}
|
|
308
|
+
function getProtocolVersionHeader(req) {
|
|
309
|
+
const rawProtocolVersion = req.headers['mcp-protocol-version'];
|
|
310
|
+
return typeof rawProtocolVersion === 'string'
|
|
311
|
+
? rawProtocolVersion
|
|
312
|
+
: undefined;
|
|
313
|
+
}
|
|
314
|
+
function resolveNegotiatedProtocolVersion(requestedVersion) {
|
|
315
|
+
return SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
|
|
316
|
+
? requestedVersion
|
|
317
|
+
: LATEST_PROTOCOL_VERSION;
|
|
318
|
+
}
|
|
319
|
+
function ensureSessionProtocolVersion(req, res, session) {
|
|
320
|
+
const protocolVersion = getProtocolVersionHeader(req);
|
|
321
|
+
if (!protocolVersion) {
|
|
322
|
+
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: Missing MCP-Protocol-Version header');
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
if (protocolVersion !== session.negotiatedProtocolVersion) {
|
|
326
|
+
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, `Bad Request: MCP-Protocol-Version must match negotiated version ${session.negotiatedProtocolVersion}`);
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
301
331
|
function isAuthorizedBearer(apiKey, authHeader) {
|
|
302
332
|
const bearerPrefix = 'Bearer ';
|
|
303
333
|
if (typeof authHeader !== 'string' || !authHeader.startsWith(bearerPrefix)) {
|
|
@@ -384,6 +414,10 @@ export async function startHttpServer(port, options) {
|
|
|
384
414
|
discardRequestBody(req);
|
|
385
415
|
return;
|
|
386
416
|
}
|
|
417
|
+
if (!ensureSessionProtocolVersion(req, res, session)) {
|
|
418
|
+
discardRequestBody(req);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
387
421
|
const body = await readRequestBody(req);
|
|
388
422
|
await handleSessionTransportRequest(session, req, res, body);
|
|
389
423
|
return;
|
|
@@ -395,7 +429,7 @@ export async function startHttpServer(port, options) {
|
|
|
395
429
|
sendJsonRpcError(res, 503, JSON_RPC_SERVER_ERROR, 'Too many sessions');
|
|
396
430
|
return;
|
|
397
431
|
}
|
|
398
|
-
const session = await createHttpSession(options, sessions);
|
|
432
|
+
const session = await createHttpSession(options, sessions, resolveNegotiatedProtocolVersion(body.params.protocolVersion));
|
|
399
433
|
await handleSessionTransportRequest(session, req, res, body);
|
|
400
434
|
return;
|
|
401
435
|
}
|
|
@@ -410,6 +444,8 @@ export async function startHttpServer(port, options) {
|
|
|
410
444
|
const session = getSessionOrRespondNotFound(sessions, sessionId, res);
|
|
411
445
|
if (!session)
|
|
412
446
|
return;
|
|
447
|
+
if (!ensureSessionProtocolVersion(req, res, session))
|
|
448
|
+
return;
|
|
413
449
|
await handleSessionTransportRequest(session, req, res);
|
|
414
450
|
}
|
|
415
451
|
else {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
1
|
import * as path from 'node:path';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
3
|
import { applyPatch, parsePatch } from 'diff';
|
|
4
4
|
import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
5
5
|
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
@@ -22,22 +22,13 @@ export const APPLY_PATCH_TOOL = {
|
|
|
22
22
|
'Multi-file patches use `path` as base directory; per-file results in `results[]`.',
|
|
23
23
|
],
|
|
24
24
|
gotchas: ['Patch must include valid hunk headers; use `dryRun=true` first.'],
|
|
25
|
-
taskSupport: '
|
|
25
|
+
taskSupport: 'forbidden',
|
|
26
26
|
};
|
|
27
27
|
function assertPatchTargetSizeWithinLimit(filePath, size, maxFileSize) {
|
|
28
28
|
if (size <= maxFileSize)
|
|
29
29
|
return;
|
|
30
30
|
throw new McpError(ErrorCode.E_TOO_LARGE, `File too large for patch: ${filePath} (${size} bytes > ${maxFileSize} bytes).`, filePath, { size, maxFileSize });
|
|
31
31
|
}
|
|
32
|
-
function assertPatchHasHunks(patch) {
|
|
33
|
-
if (!patch.trim()) {
|
|
34
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch content is empty.');
|
|
35
|
-
}
|
|
36
|
-
const hasHunk = /@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/u.test(patch);
|
|
37
|
-
if (!hasHunk) {
|
|
38
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch must include unified hunk headers (e.g., @@ -1,2 +1,2 @@).');
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
32
|
function countStructuredPatchStats(diff) {
|
|
42
33
|
let linesAdded = 0;
|
|
43
34
|
let linesRemoved = 0;
|
|
@@ -63,12 +54,12 @@ function extractPatchTargetPath(diff) {
|
|
|
63
54
|
return stripGitPrefix(diff.oldFileName);
|
|
64
55
|
return undefined;
|
|
65
56
|
}
|
|
66
|
-
async function
|
|
57
|
+
async function applyDiff(filePath, diff, options, signal) {
|
|
67
58
|
const validPath = await validateExistingPath(filePath, signal);
|
|
68
59
|
assertAllowedFileAccess(filePath, validPath);
|
|
69
|
-
const stats = await withAbort(
|
|
60
|
+
const stats = await withAbort(stat(validPath), signal);
|
|
70
61
|
assertPatchTargetSizeWithinLimit(validPath, stats.size, MAX_TEXT_FILE_SIZE);
|
|
71
|
-
const content = await
|
|
62
|
+
const content = await readFile(validPath, { encoding: 'utf-8', signal });
|
|
72
63
|
const patched = applyPatch(content, diff, {
|
|
73
64
|
fuzzFactor: options.fuzzFactor,
|
|
74
65
|
autoConvertLineEndings: options.autoConvertLineEndings,
|
|
@@ -89,32 +80,31 @@ async function applyPatchToFile(filePath, diff, options, signal) {
|
|
|
89
80
|
}
|
|
90
81
|
return { path: validPath, applied: true, ...patchStats };
|
|
91
82
|
}
|
|
92
|
-
async function
|
|
83
|
+
async function processMultiFilePatch(basePath, parsed, options, signal) {
|
|
93
84
|
const validBase = await validateExistingPath(basePath, signal);
|
|
94
|
-
const
|
|
95
|
-
for (const diff of parsed) {
|
|
85
|
+
const promises = parsed.map(async (diff) => {
|
|
96
86
|
const fileName = extractPatchTargetPath(diff);
|
|
97
87
|
if (!fileName) {
|
|
98
|
-
|
|
88
|
+
return {
|
|
99
89
|
path: '<unknown>',
|
|
100
90
|
applied: false,
|
|
101
91
|
error: 'Missing file name in patch header',
|
|
102
|
-
}
|
|
103
|
-
continue;
|
|
92
|
+
};
|
|
104
93
|
}
|
|
105
94
|
const filePath = path.resolve(validBase, fileName);
|
|
106
95
|
try {
|
|
107
|
-
const result = await
|
|
108
|
-
|
|
96
|
+
const result = await applyDiff(filePath, diff, options, signal);
|
|
97
|
+
return { ...result, path: fileName };
|
|
109
98
|
}
|
|
110
99
|
catch (error) {
|
|
111
|
-
|
|
100
|
+
return {
|
|
112
101
|
path: fileName,
|
|
113
102
|
applied: false,
|
|
114
103
|
error: formatUnknownErrorMessage(error),
|
|
115
|
-
}
|
|
104
|
+
};
|
|
116
105
|
}
|
|
117
|
-
}
|
|
106
|
+
});
|
|
107
|
+
const results = await Promise.all(promises);
|
|
118
108
|
const totals = results.reduce((acc, r) => {
|
|
119
109
|
if (r.applied) {
|
|
120
110
|
acc.applied++;
|
|
@@ -137,24 +127,28 @@ async function handleMultiFilePatch(basePath, parsed, options, signal) {
|
|
|
137
127
|
});
|
|
138
128
|
}
|
|
139
129
|
async function handleApplyPatch(args, signal) {
|
|
140
|
-
|
|
130
|
+
if (!args.patch.trim()) {
|
|
131
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch content is empty.');
|
|
132
|
+
}
|
|
141
133
|
const fuzzFactor = args.fuzzFactor ?? 0;
|
|
142
134
|
const parsed = parsePatch(args.patch);
|
|
135
|
+
const hasHunks = parsed.some((p) => p.hunks.length > 0);
|
|
136
|
+
if (!hasHunks) {
|
|
137
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch must include unified hunk headers (e.g., @@ -1,2 +1,2 @@).');
|
|
138
|
+
}
|
|
143
139
|
const options = {
|
|
144
140
|
dryRun: args.dryRun,
|
|
145
141
|
fuzzFactor,
|
|
146
142
|
autoConvertLineEndings: args.autoConvertLineEndings,
|
|
147
143
|
};
|
|
148
|
-
// Multi-file patch: best-effort per file
|
|
149
144
|
if (parsed.length > 1) {
|
|
150
|
-
return
|
|
145
|
+
return processMultiFilePatch(args.path, parsed, options, signal);
|
|
151
146
|
}
|
|
152
|
-
// Single-file patch: delegate to shared helper, then assert success
|
|
153
147
|
const diff = parsed[0];
|
|
154
148
|
if (!diff) {
|
|
155
149
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'No patch content found.');
|
|
156
150
|
}
|
|
157
|
-
const result = await
|
|
151
|
+
const result = await applyDiff(args.path, diff, options, signal);
|
|
158
152
|
if (!result.applied) {
|
|
159
153
|
throw new McpError(ErrorCode.E_INVALID_INPUT, result.error === 'Patch had no effect'
|
|
160
154
|
? 'Patch had no effect \u2014 the file content is unchanged after applying. The patch may not match the current file content. Generate a fresh patch via diff_files and retry.'
|
|
@@ -194,9 +188,12 @@ export function registerApplyPatchTool(server, options = {}) {
|
|
|
194
188
|
const sc = result.structuredContent;
|
|
195
189
|
if (!sc.ok)
|
|
196
190
|
return `🛠 patch: ${name} • failed`;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
191
|
+
const added = sc.linesAdded ?? 0;
|
|
192
|
+
const removed = sc.linesRemoved ?? 0;
|
|
193
|
+
const dry = args.dryRun ? 'dry run ' : '';
|
|
194
|
+
if (added > 0 || removed > 0)
|
|
195
|
+
return `🛠 patch: ${name} • ${dry} +${added} -${removed}`;
|
|
196
|
+
return `🛠 patch: ${name} • ${dry}no changes`;
|
|
200
197
|
},
|
|
201
198
|
});
|
|
202
199
|
const validatedHandler = withValidatedArgs(ApplyPatchInputSchema, wrappedHandler);
|
|
@@ -2,6 +2,7 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { createReadStream } from 'node:fs';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
6
|
import { PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
6
7
|
import { ErrorCode } from '../lib/errors.js';
|
|
7
8
|
import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations/core.js';
|
|
@@ -26,15 +27,9 @@ export const CALCULATE_HASH_TOOL = {
|
|
|
26
27
|
taskSupport: 'optional',
|
|
27
28
|
};
|
|
28
29
|
async function hashFile(filePath, encoding, signal) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for await (const chunk of stream) {
|
|
33
|
-
hashOp.update(chunk);
|
|
34
|
-
assertNotAborted(signal);
|
|
35
|
-
}
|
|
36
|
-
assertNotAborted(signal);
|
|
37
|
-
return encoding === undefined ? hashOp.digest() : hashOp.digest(encoding);
|
|
30
|
+
const hasher = createHash('sha256');
|
|
31
|
+
await pipeline(createReadStream(filePath, { signal, highWaterMark: 64 * 1024 }), hasher, { signal });
|
|
32
|
+
return encoding ? hasher.digest(encoding) : hasher.digest();
|
|
38
33
|
}
|
|
39
34
|
function toStableRelativePath(root, entryPath) {
|
|
40
35
|
const relativePath = path.relative(root, entryPath);
|
|
@@ -84,25 +79,27 @@ async function hashDirectory(dirPath, options = {}) {
|
|
|
84
79
|
});
|
|
85
80
|
}
|
|
86
81
|
assertNotAborted(signal);
|
|
87
|
-
// Phase 2: hash files concurrently with bounded pool.
|
|
88
82
|
const concurrency = Math.min(PARALLEL_CONCURRENCY, 8);
|
|
89
83
|
const entries = [];
|
|
90
84
|
let filesHashed = 0;
|
|
91
85
|
const totalFiles = filteredPaths.length;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
86
|
+
const taskQueue = filteredPaths;
|
|
87
|
+
const worker = async () => {
|
|
88
|
+
while (taskQueue.length > 0) {
|
|
89
|
+
const task = taskQueue.pop();
|
|
90
|
+
if (!task)
|
|
91
|
+
break;
|
|
92
|
+
assertNotAborted(signal);
|
|
93
|
+
const fileHash = await hashFile(task.filePath, undefined, signal);
|
|
94
|
+
entries.push({ path: task.relativePath, hash: fileHash });
|
|
95
|
+
filesHashed++;
|
|
96
|
+
reportPeriodicProgress(onProgress, filesHashed, {
|
|
97
|
+
throttleModulo: 25,
|
|
98
|
+
total: totalFiles,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
106
103
|
reportPeriodicProgress(onProgress, filesHashed, {
|
|
107
104
|
throttleModulo: 25,
|
|
108
105
|
total: totalFiles,
|
package/dist/tools/contract.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const CREATE_DIRECTORY_TOOL = {
|
|
|
14
14
|
outputSchema: CreateDirectoryOutputSchema,
|
|
15
15
|
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
16
16
|
nuances: ['Succeeds silently if the directory already exists (idempotent).'],
|
|
17
|
-
taskSupport: '
|
|
17
|
+
taskSupport: 'forbidden',
|
|
18
18
|
};
|
|
19
19
|
export async function handleCreateDirectory(args, signal) {
|
|
20
20
|
const allPaths = [];
|
|
@@ -55,12 +55,12 @@ export function registerCreateDirectoryTool(server, options = {}) {
|
|
|
55
55
|
const name = path.basename(args.path);
|
|
56
56
|
if (result.isError)
|
|
57
57
|
return `🛠 mkdir: ${name} • failed`;
|
|
58
|
-
return `🛠 mkdir: ${name}
|
|
58
|
+
return `🛠 mkdir: ${name}`;
|
|
59
59
|
}
|
|
60
60
|
const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
|
|
61
61
|
if (result.isError)
|
|
62
62
|
return `🛠 mkdir: ${count} directories • failed`;
|
|
63
|
-
return `🛠 mkdir: ${count} directories
|
|
63
|
+
return `🛠 mkdir: ${count} directories`;
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
66
|
const validatedHandler = withValidatedArgs(CreateDirectoryInputSchema, wrappedHandler);
|
|
@@ -17,7 +17,7 @@ export const DELETE_FILE_TOOL = {
|
|
|
17
17
|
'No undo — deletion is permanent.',
|
|
18
18
|
'Non-empty directories require `recursive=true`.',
|
|
19
19
|
],
|
|
20
|
-
taskSupport: '
|
|
20
|
+
taskSupport: 'forbidden',
|
|
21
21
|
};
|
|
22
22
|
async function handleDeleteFile(args, signal) {
|
|
23
23
|
const validPath = await validatePathForWrite(args.path, signal);
|
|
@@ -90,7 +90,7 @@ export function registerDeleteFileTool(server, options = {}) {
|
|
|
90
90
|
const name = path.basename(args.path);
|
|
91
91
|
if (result.isError)
|
|
92
92
|
return `🛠 rm: ${name} • failed`;
|
|
93
|
-
return `🛠 rm: ${name}
|
|
93
|
+
return `🛠 rm: ${name}`;
|
|
94
94
|
},
|
|
95
95
|
});
|
|
96
96
|
const validatedHandler = withValidatedArgs(DeleteFileInputSchema, wrappedHandler);
|