@j0hanz/filesystem-mcp 1.13.2 → 1.14.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 +162 -145
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -14
- package/dist/config.js +12 -12
- package/dist/index.js +1 -1
- package/dist/lib/abort.d.ts +7 -0
- package/dist/lib/abort.js +81 -0
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +8 -2
- package/dist/lib/errors.d.ts +7 -3
- package/dist/lib/errors.js +64 -41
- package/dist/lib/file-operations/core.d.ts +3 -3
- package/dist/lib/file-operations/core.js +23 -20
- package/dist/lib/file-operations/metadata.d.ts +2 -2
- package/dist/lib/file-operations/metadata.js +69 -22
- package/dist/lib/file-operations/search.d.ts +0 -1
- package/dist/lib/file-operations/search.js +87 -95
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +29 -108
- package/dist/lib/globs.d.ts +2 -0
- package/dist/lib/globs.js +19 -0
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.js +91 -0
- package/dist/lib/observability.d.ts +7 -0
- package/dist/lib/observability.js +19 -9
- package/dist/lib/paths.js +55 -55
- package/dist/lib/resource-store.js +4 -4
- package/dist/lib/utils.d.ts +0 -12
- package/dist/lib/utils.js +0 -13
- package/dist/lib/zod-codecs.d.ts +2 -0
- package/dist/lib/zod-codecs.js +18 -0
- package/dist/pkg-info.d.ts +1 -0
- package/dist/pkg-info.js +2 -2
- package/dist/prompts.js +3 -3
- package/dist/resources/generated-instructions.js +41 -41
- package/dist/resources/tool-catalog.js +33 -58
- package/dist/resources/tool-info.d.ts +0 -1
- package/dist/resources/tool-info.js +44 -67
- package/dist/resources/workflows.js +47 -19
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +185 -465
- package/dist/schemas.js +174 -206
- package/dist/server/bootstrap.d.ts +12 -11
- package/dist/server/bootstrap.js +95 -86
- package/dist/server/roots-manager.d.ts +5 -2
- package/dist/server/roots-manager.js +9 -7
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +39 -20
- package/dist/tools/calculate-hash.js +14 -27
- package/dist/tools/create-directory.js +11 -9
- package/dist/tools/delete-file.js +19 -19
- package/dist/tools/diff-files.js +16 -18
- package/dist/tools/edit-file.js +11 -5
- package/dist/tools/list-directory.js +16 -21
- package/dist/tools/move-file.js +105 -100
- package/dist/tools/read-multiple.js +15 -10
- package/dist/tools/read.js +6 -7
- package/dist/tools/replace-in-files.js +76 -115
- package/dist/tools/roots.js +3 -7
- package/dist/tools/search-content.js +158 -203
- package/dist/tools/search-files.js +59 -50
- package/dist/tools/shared.d.ts +10 -0
- package/dist/tools/shared.js +105 -36
- package/dist/tools/stat-many.js +15 -9
- package/dist/tools/stat.js +6 -6
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +4 -4
- package/dist/tools/write-file.js +11 -12
- package/package.json +10 -9
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import * as http from 'node:http';
|
|
2
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
-
import type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
import { type Server } from 'node:http';
|
|
3
|
+
import { type LoggingState } from '../lib/logger.js';
|
|
4
|
+
import { type ServerOptions } from './roots-manager.js';
|
|
5
|
+
export declare const activeServers: Map<string, {
|
|
6
|
+
server: McpServer;
|
|
7
|
+
loggingState: LoggingState;
|
|
8
|
+
}>;
|
|
9
|
+
export declare let stdioServer: {
|
|
10
|
+
server: McpServer;
|
|
11
|
+
loggingState: LoggingState;
|
|
12
|
+
} | undefined;
|
|
12
13
|
export declare function createServer(options?: ServerOptions): Promise<McpServer>;
|
|
13
14
|
export declare function startServer(server: McpServer): Promise<void>;
|
|
14
|
-
export declare function startHttpServer(port: number, options: ServerOptions): Promise<
|
|
15
|
+
export declare function startHttpServer(port: number, options: ServerOptions): Promise<Server>;
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as http from 'node:http';
|
|
3
|
-
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
4
|
-
import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
|
|
1
|
+
import { InMemoryTaskMessageQueue } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
|
|
5
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
4
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
5
|
import { isInitializeRequest, LATEST_PROTOCOL_VERSION, SetLevelRequestSchema, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
7
|
+
import { channel } from 'node:diagnostics_channel';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { createServer as createHttpServer, } from 'node:http';
|
|
9
10
|
import { DEFAULT_LOG_LEVEL, parseEnvInt } from '../lib/constants.js';
|
|
10
11
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
12
|
+
import { createLoggingState, Logger, logToMcp, SessionContext, } from '../lib/logger.js';
|
|
11
13
|
import { withAllowedDirectoriesState } from '../lib/paths.js';
|
|
12
14
|
import { createInMemoryResourceStore } from '../lib/resource-store.js';
|
|
13
|
-
import { isRecord } from '../lib/utils.js';
|
|
14
15
|
import { registerCompletions } from '../completions.js';
|
|
15
16
|
import { pkgInfo } from '../pkg-info.js';
|
|
16
17
|
import { registerAnalyzePathPrompt, registerCompareFilesPrompt, registerGetHelpPrompt, registerGetToolHelpPrompt, } from '../prompts.js';
|
|
@@ -19,6 +20,7 @@ import { buildServerInstructions } from '../resources/generated-instructions.js'
|
|
|
19
20
|
import { registerAllTools } from '../tools.js';
|
|
20
21
|
import { withDefaultIcons } from '../tools/shared.js';
|
|
21
22
|
import { RootsManager } from './roots-manager.js';
|
|
23
|
+
import { createTaskStore } from './task-store.js';
|
|
22
24
|
let cachedTaskToolSupport;
|
|
23
25
|
function detectTaskToolSupport() {
|
|
24
26
|
if (cachedTaskToolSupport !== undefined) {
|
|
@@ -48,6 +50,7 @@ function buildServerCapabilities(options = {}) {
|
|
|
48
50
|
tools: {},
|
|
49
51
|
prompts: options.enablePromptListChanged ? { listChanged: true } : {},
|
|
50
52
|
completions: {},
|
|
53
|
+
extensions: {},
|
|
51
54
|
};
|
|
52
55
|
if (options.enableTaskToolRequests) {
|
|
53
56
|
// NOTE: enabling task tool requests requires the caller to configure
|
|
@@ -64,45 +67,30 @@ function buildServerCapabilities(options = {}) {
|
|
|
64
67
|
function supportsTaskToolRequests() {
|
|
65
68
|
return detectTaskToolSupport();
|
|
66
69
|
}
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
alert: 6,
|
|
76
|
-
emergency: 7,
|
|
77
|
-
};
|
|
78
|
-
function createLoggingState(minimumLevel = 'debug') {
|
|
79
|
-
return { minimumLevel };
|
|
80
|
-
}
|
|
81
|
-
function canSendMcpLogs(server) {
|
|
82
|
-
const capabilities = server.server.getClientCapabilities();
|
|
83
|
-
if (!isRecord(capabilities))
|
|
84
|
-
return false;
|
|
85
|
-
if (!('logging' in capabilities))
|
|
86
|
-
return false;
|
|
87
|
-
return !!capabilities['logging'];
|
|
70
|
+
// Global map of all active servers by sessionId for routing logs
|
|
71
|
+
export const activeServers = new Map();
|
|
72
|
+
// For stdio (single session without a specific ID)
|
|
73
|
+
export let stdioServer;
|
|
74
|
+
function stringifyData(data) {
|
|
75
|
+
if (!data)
|
|
76
|
+
return '';
|
|
77
|
+
return ` ${typeof data === 'string' ? data : JSON.stringify(data)}`;
|
|
88
78
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
79
|
+
channel('filesystem-mcp:log').subscribe((message) => {
|
|
80
|
+
const event = message;
|
|
81
|
+
const target = event.sessionId
|
|
82
|
+
? activeServers.get(event.sessionId)
|
|
83
|
+
: stdioServer;
|
|
84
|
+
const dataStr = stringifyData(event.data);
|
|
85
|
+
if (target) {
|
|
86
|
+
logToMcp(target.server, event.level, `${event.message}${dataStr}`, target.loggingState.minimumLevel);
|
|
92
87
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
else {
|
|
89
|
+
// Fallback if no server
|
|
90
|
+
const fullMsg = `${event.message}${dataStr}`;
|
|
91
|
+
console.error(`[${event.level.toUpperCase()}] ${fullMsg}`);
|
|
96
92
|
}
|
|
97
|
-
|
|
98
|
-
level,
|
|
99
|
-
logger: MCP_LOGGER_NAME,
|
|
100
|
-
data,
|
|
101
|
-
};
|
|
102
|
-
void server.sendLoggingMessage(params).catch((error) => {
|
|
103
|
-
console.error(`Failed to send MCP log: ${level} | ${data}`, formatUnknownErrorMessage(error));
|
|
104
|
-
});
|
|
105
|
-
}
|
|
93
|
+
});
|
|
106
94
|
const { version: SERVER_VERSION, description: SERVER_DESCRIPTION, homepage: SERVER_HOMEPAGE, } = pkgInfo;
|
|
107
95
|
const rootsManagers = new WeakMap();
|
|
108
96
|
function getRootsManager(server) {
|
|
@@ -119,7 +107,7 @@ async function getLocalIconInfo() {
|
|
|
119
107
|
for (const candidate of candidates) {
|
|
120
108
|
try {
|
|
121
109
|
const iconPath = new URL(candidate, import.meta.url);
|
|
122
|
-
const buffer = await
|
|
110
|
+
const buffer = await readFile(iconPath);
|
|
123
111
|
return {
|
|
124
112
|
src: `data:${mime};base64,${buffer.toString('base64')}`,
|
|
125
113
|
mimeType: mime,
|
|
@@ -144,7 +132,7 @@ export async function createServer(options = {}) {
|
|
|
144
132
|
};
|
|
145
133
|
if (taskToolSupport) {
|
|
146
134
|
// Enabling task tool support requires configuring a task store and message queue on the server config. We use in-memory implementations from the SDK which auto-evict tasks after their TTL expires (via setTimeout). Suitable for both stdio and HTTP sessions.
|
|
147
|
-
serverConfig.taskStore =
|
|
135
|
+
serverConfig.taskStore = createTaskStore();
|
|
148
136
|
serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
|
|
149
137
|
}
|
|
150
138
|
if (serverInstructions) {
|
|
@@ -163,10 +151,15 @@ export async function createServer(options = {}) {
|
|
|
163
151
|
const loggingState = createLoggingState(DEFAULT_LOG_LEVEL);
|
|
164
152
|
const rootsManager = new RootsManager(options, loggingState);
|
|
165
153
|
rootsManagers.set(server, rootsManager);
|
|
154
|
+
// Subscribe to Logger channel if not already done, but we need to route based on session or fallback to this server if it's stdio.
|
|
155
|
+
// Wait, in stdio there's only one server. In HTTP there are multiple.
|
|
166
156
|
server.server.setRequestHandler(SetLevelRequestSchema, (req) => {
|
|
167
157
|
loggingState.minimumLevel = req.params.level;
|
|
158
|
+
Logger.notice(`Log level set to ${req.params.level}`);
|
|
168
159
|
return {};
|
|
169
160
|
});
|
|
161
|
+
// Track stdio server by default, or it will be overwritten per HTTP session later
|
|
162
|
+
stdioServer ??= { server, loggingState };
|
|
170
163
|
registerInstructionResource(server, serverInstructions, localIcon);
|
|
171
164
|
registerToolCatalogResource(server, localIcon);
|
|
172
165
|
registerWorkflowGuideResource(server, localIcon);
|
|
@@ -257,6 +250,10 @@ async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
|
|
|
257
250
|
transport,
|
|
258
251
|
negotiatedProtocolVersion,
|
|
259
252
|
});
|
|
253
|
+
activeServers.set(sessionId, {
|
|
254
|
+
server: mcpServer,
|
|
255
|
+
loggingState: rootsManager['loggingState'],
|
|
256
|
+
});
|
|
260
257
|
rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
|
|
261
258
|
},
|
|
262
259
|
});
|
|
@@ -264,10 +261,11 @@ async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
|
|
|
264
261
|
const { sessionId } = transport;
|
|
265
262
|
if (sessionId) {
|
|
266
263
|
sessions.delete(sessionId);
|
|
264
|
+
activeServers.delete(sessionId);
|
|
267
265
|
}
|
|
268
266
|
rootsManager.destroy();
|
|
269
267
|
mcpServer.close().catch((err) => {
|
|
270
|
-
|
|
268
|
+
Logger.error('[HTTP] Error closing MCP server:', formatUnknownErrorMessage(err));
|
|
271
269
|
});
|
|
272
270
|
};
|
|
273
271
|
await mcpServer.connect(transport);
|
|
@@ -368,7 +366,12 @@ function discardRequestBody(req) {
|
|
|
368
366
|
req.resume();
|
|
369
367
|
}
|
|
370
368
|
async function handleSessionTransportRequest(session, req, res, body) {
|
|
371
|
-
|
|
369
|
+
const store = session.transport.sessionId
|
|
370
|
+
? { sessionId: session.transport.sessionId }
|
|
371
|
+
: {};
|
|
372
|
+
await SessionContext.run(store, async () => {
|
|
373
|
+
await withAllowedDirectoriesState(session.rootsManager.getAllowedDirectoriesState(), () => session.transport.handleRequest(req, res, body));
|
|
374
|
+
});
|
|
372
375
|
}
|
|
373
376
|
function getSessionOrRespondNotFound(sessions, sessionId, res) {
|
|
374
377
|
const session = sessions.get(sessionId);
|
|
@@ -396,6 +399,47 @@ export async function startHttpServer(port, options) {
|
|
|
396
399
|
const sessions = new Map();
|
|
397
400
|
const httpHost = process.env['FILESYSTEM_MCP_HTTP_HOST'] ?? '127.0.0.1';
|
|
398
401
|
assertHttpBindingSecurity(httpHost);
|
|
402
|
+
async function handlePostRequest(req, res, sessionId) {
|
|
403
|
+
if (sessionId) {
|
|
404
|
+
const session = getSessionOrRespondNotFound(sessions, sessionId, res);
|
|
405
|
+
if (!session) {
|
|
406
|
+
discardRequestBody(req);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!ensureSessionProtocolVersion(req, res, session)) {
|
|
410
|
+
discardRequestBody(req);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const body = await readRequestBody(req);
|
|
414
|
+
await handleSessionTransportRequest(session, req, res, body);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const body = await readRequestBody(req);
|
|
418
|
+
if (isInitializeRequest(body)) {
|
|
419
|
+
const maxSessions = parseEnvInt('FILESYSTEM_MCP_MAX_HTTP_SESSIONS', 100, 1, 10_000);
|
|
420
|
+
if (sessions.size >= maxSessions) {
|
|
421
|
+
sendJsonRpcError(res, 503, JSON_RPC_SERVER_ERROR, 'Too many sessions');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const session = await createHttpSession(options, sessions, resolveNegotiatedProtocolVersion(body.params.protocolVersion));
|
|
425
|
+
await handleSessionTransportRequest(session, req, res, body);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: No valid session ID provided');
|
|
429
|
+
discardRequestBody(req);
|
|
430
|
+
}
|
|
431
|
+
async function handleGetDeleteRequest(req, res, sessionId) {
|
|
432
|
+
if (!sessionId) {
|
|
433
|
+
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: Missing session ID');
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const session = getSessionOrRespondNotFound(sessions, sessionId, res);
|
|
437
|
+
if (!session)
|
|
438
|
+
return;
|
|
439
|
+
if (!ensureSessionProtocolVersion(req, res, session))
|
|
440
|
+
return;
|
|
441
|
+
await handleSessionTransportRequest(session, req, res);
|
|
442
|
+
}
|
|
399
443
|
async function handleMcpRequest(req, res) {
|
|
400
444
|
const { method } = req;
|
|
401
445
|
const sessionId = getSessionId(req);
|
|
@@ -408,45 +452,10 @@ export async function startHttpServer(port, options) {
|
|
|
408
452
|
return;
|
|
409
453
|
try {
|
|
410
454
|
if (method === 'POST') {
|
|
411
|
-
|
|
412
|
-
const session = getSessionOrRespondNotFound(sessions, sessionId, res);
|
|
413
|
-
if (!session) {
|
|
414
|
-
discardRequestBody(req);
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
if (!ensureSessionProtocolVersion(req, res, session)) {
|
|
418
|
-
discardRequestBody(req);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
const body = await readRequestBody(req);
|
|
422
|
-
await handleSessionTransportRequest(session, req, res, body);
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
const body = await readRequestBody(req);
|
|
426
|
-
if (isInitializeRequest(body)) {
|
|
427
|
-
const maxSessions = parseEnvInt('FILESYSTEM_MCP_MAX_HTTP_SESSIONS', 100, 1, 10_000);
|
|
428
|
-
if (sessions.size >= maxSessions) {
|
|
429
|
-
sendJsonRpcError(res, 503, JSON_RPC_SERVER_ERROR, 'Too many sessions');
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
const session = await createHttpSession(options, sessions, resolveNegotiatedProtocolVersion(body.params.protocolVersion));
|
|
433
|
-
await handleSessionTransportRequest(session, req, res, body);
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: No valid session ID provided');
|
|
437
|
-
discardRequestBody(req);
|
|
455
|
+
await handlePostRequest(req, res, sessionId);
|
|
438
456
|
}
|
|
439
457
|
else if (method === 'GET' || method === 'DELETE') {
|
|
440
|
-
|
|
441
|
-
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: Missing session ID');
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
const session = getSessionOrRespondNotFound(sessions, sessionId, res);
|
|
445
|
-
if (!session)
|
|
446
|
-
return;
|
|
447
|
-
if (!ensureSessionProtocolVersion(req, res, session))
|
|
448
|
-
return;
|
|
449
|
-
await handleSessionTransportRequest(session, req, res);
|
|
458
|
+
await handleGetDeleteRequest(req, res, sessionId);
|
|
450
459
|
}
|
|
451
460
|
else {
|
|
452
461
|
res.writeHead(405, {
|
|
@@ -472,17 +481,17 @@ export async function startHttpServer(port, options) {
|
|
|
472
481
|
sendJsonRpcError(res, error.statusCode, rpcCode, error.message);
|
|
473
482
|
return;
|
|
474
483
|
}
|
|
475
|
-
|
|
484
|
+
Logger.error('[HTTP] Error handling request:', formatUnknownErrorMessage(error));
|
|
476
485
|
if (!res.headersSent) {
|
|
477
486
|
sendJsonRpcError(res, 500, JSON_RPC_INTERNAL_ERROR, 'Internal Server Error');
|
|
478
487
|
}
|
|
479
488
|
}
|
|
480
489
|
}
|
|
481
|
-
const httpServer =
|
|
490
|
+
const httpServer = createHttpServer((req, res) => {
|
|
482
491
|
const urlPath = (req.url ?? '/').split('?')[0];
|
|
483
492
|
if (urlPath === '/mcp') {
|
|
484
493
|
handleMcpRequest(req, res).catch((err) => {
|
|
485
|
-
|
|
494
|
+
Logger.error('[HTTP] Unhandled error in request handler:', formatUnknownErrorMessage(err));
|
|
486
495
|
});
|
|
487
496
|
}
|
|
488
497
|
else {
|
|
@@ -493,7 +502,7 @@ export async function startHttpServer(port, options) {
|
|
|
493
502
|
return new Promise((resolve, reject) => {
|
|
494
503
|
httpServer.once('error', reject);
|
|
495
504
|
httpServer.listen(port, httpHost, () => {
|
|
496
|
-
|
|
505
|
+
Logger.info(`MCP HTTP server listening on ${httpHost}:${port}`);
|
|
497
506
|
resolve(httpServer);
|
|
498
507
|
});
|
|
499
508
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type LoggingState } from '../lib/logger.js';
|
|
2
3
|
import { type AllowedDirectoriesState } from '../lib/paths.js';
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
export interface ServerOptions {
|
|
5
|
+
allowCwd?: boolean;
|
|
6
|
+
cliAllowedDirs?: string[];
|
|
7
|
+
}
|
|
5
8
|
export declare class RootsManager {
|
|
6
9
|
private _debouncedUpdate;
|
|
7
10
|
private rootDirectories;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
1
|
import { InitializedNotificationSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { assertNotAborted, createTimedAbortSignal, withAbort, } from '../lib/abort.js';
|
|
4
5
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
5
|
-
import {
|
|
6
|
+
import { Logger, logToMcp } from '../lib/logger.js';
|
|
6
7
|
import { getValidRootDirectories, isPathWithinDirectories, normalizePath, resolveAllowedDirectoriesState, setAllowedDirectoriesStateResolved, } from '../lib/paths.js';
|
|
7
8
|
import { debounce, isRecord } from '../lib/utils.js';
|
|
8
|
-
import { logToMcp } from './bootstrap.js';
|
|
9
9
|
const ROOTS_TIMEOUT_MS = 5000;
|
|
10
10
|
const ROOTS_DEBOUNCE_MS = 100;
|
|
11
11
|
function normalizeCLIDirectories(dirs) {
|
|
@@ -22,7 +22,7 @@ const RootSchema = z.strictObject({
|
|
|
22
22
|
uri: z.string(),
|
|
23
23
|
name: z.string().optional(),
|
|
24
24
|
});
|
|
25
|
-
const RootsResponseSchema = z.
|
|
25
|
+
const RootsResponseSchema = z.strictObject({
|
|
26
26
|
roots: z.array(RootSchema).optional(),
|
|
27
27
|
});
|
|
28
28
|
function isRoot(value) {
|
|
@@ -61,7 +61,7 @@ async function isRootWithinBaseline(normalizedRoot, baseline, signal) {
|
|
|
61
61
|
}
|
|
62
62
|
try {
|
|
63
63
|
assertNotAborted(signal);
|
|
64
|
-
const realPath = await withAbort(
|
|
64
|
+
const realPath = await withAbort(realpath(normalizedRoot), signal);
|
|
65
65
|
const normalizedReal = normalizePath(realPath);
|
|
66
66
|
return isPathWithinDirectories(normalizedReal, baseline);
|
|
67
67
|
}
|
|
@@ -88,9 +88,10 @@ export class RootsManager {
|
|
|
88
88
|
expanded: [],
|
|
89
89
|
};
|
|
90
90
|
clientInitialized = false;
|
|
91
|
-
//
|
|
91
|
+
// Guard concurrent root refreshes; if one is already running we queue one
|
|
92
|
+
// replay so the last-known state still gets applied after completion.
|
|
92
93
|
updatingRoots = false;
|
|
93
|
-
//
|
|
94
|
+
// Tracks whether a roots change arrived while the previous refresh ran.
|
|
94
95
|
pendingRootsUpdate = false;
|
|
95
96
|
options;
|
|
96
97
|
loggingState;
|
|
@@ -187,6 +188,7 @@ export class RootsManager {
|
|
|
187
188
|
}
|
|
188
189
|
finally {
|
|
189
190
|
await this.recomputeAllowedDirectories();
|
|
191
|
+
Logger.info(`Roots updated: ${this.rootDirectories.length} root(s), ${this.allowedDirectoriesState.expanded.length} allowed dir(s)`);
|
|
190
192
|
this.updatingRoots = false;
|
|
191
193
|
// If a change arrived while we were running, apply it now.
|
|
192
194
|
if (this.pendingRootsUpdate) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
|
|
2
|
+
import type { Result } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
export declare class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
|
|
4
|
+
private readonly cancelledResults;
|
|
5
|
+
getTaskResult(taskId: string, sessionId?: string): Promise<Result>;
|
|
6
|
+
storeTaskResult(taskId: string, status: 'completed' | 'failed', result: Result, sessionId?: string): Promise<void>;
|
|
7
|
+
updateTaskStatus(taskId: string, status: 'working' | 'input_required' | 'completed' | 'failed' | 'cancelled', statusMessage?: string, sessionId?: string): Promise<void>;
|
|
8
|
+
cleanup(): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function createTaskStore(): ResultAwareInMemoryTaskStore;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
|
|
2
|
+
import { ErrorCode } from '../lib/errors.js';
|
|
3
|
+
const DEFAULT_CANCELLED_STATUS_MESSAGE = 'Client cancelled task execution.';
|
|
4
|
+
function getTaskKey(taskId, sessionId) {
|
|
5
|
+
return `${sessionId ?? ''}:${taskId}`;
|
|
6
|
+
}
|
|
7
|
+
function buildCancelledTaskResult(statusMessage) {
|
|
8
|
+
return {
|
|
9
|
+
content: [
|
|
10
|
+
{
|
|
11
|
+
type: 'text',
|
|
12
|
+
text: `Error [${ErrorCode.CANCELLED}]: ${statusMessage ?? DEFAULT_CANCELLED_STATUS_MESSAGE}`,
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
isError: true,
|
|
16
|
+
errorCode: ErrorCode.CANCELLED,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
|
|
20
|
+
cancelledResults = new Map();
|
|
21
|
+
async getTaskResult(taskId, sessionId) {
|
|
22
|
+
try {
|
|
23
|
+
return await super.getTaskResult(taskId, sessionId);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const task = await super.getTask(taskId, sessionId);
|
|
27
|
+
if (task?.status !== 'cancelled') {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
const key = getTaskKey(taskId, sessionId);
|
|
31
|
+
const existing = this.cancelledResults.get(key);
|
|
32
|
+
if (existing)
|
|
33
|
+
return existing;
|
|
34
|
+
const result = buildCancelledTaskResult(task.statusMessage);
|
|
35
|
+
this.cancelledResults.set(key, result);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async storeTaskResult(taskId, status, result, sessionId) {
|
|
40
|
+
try {
|
|
41
|
+
await super.storeTaskResult(taskId, status, result, sessionId);
|
|
42
|
+
if (status !== 'failed') {
|
|
43
|
+
this.cancelledResults.delete(getTaskKey(taskId, sessionId));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const task = await super.getTask(taskId, sessionId);
|
|
48
|
+
if (task?.status !== 'cancelled') {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
this.cancelledResults.set(getTaskKey(taskId, sessionId), this.cancelledResults.get(getTaskKey(taskId, sessionId)) ?? result);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async updateTaskStatus(taskId, status, statusMessage, sessionId) {
|
|
55
|
+
await super.updateTaskStatus(taskId, status, statusMessage, sessionId);
|
|
56
|
+
const key = getTaskKey(taskId, sessionId);
|
|
57
|
+
if (status === 'cancelled') {
|
|
58
|
+
this.cancelledResults.set(key, this.cancelledResults.get(key) ??
|
|
59
|
+
buildCancelledTaskResult(statusMessage));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (status === 'completed' || status === 'failed') {
|
|
63
|
+
this.cancelledResults.delete(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
cleanup() {
|
|
67
|
+
this.cancelledResults.clear();
|
|
68
|
+
super.cleanup();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function createTaskStore() {
|
|
72
|
+
return new ResultAwareInMemoryTaskStore();
|
|
73
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import * as path from 'node:path';
|
|
2
1
|
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, resolve } from 'node:path';
|
|
3
3
|
import { applyPatch, parsePatch } from 'diff';
|
|
4
|
+
import { withAbort } from '../lib/abort.js';
|
|
4
5
|
import { MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
5
|
-
import { ErrorCode,
|
|
6
|
-
import { atomicWriteFile, processInParallel
|
|
6
|
+
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
7
|
+
import { atomicWriteFile, processInParallel } from '../lib/fs-helpers.js';
|
|
8
|
+
import { Logger } from '../lib/logger.js';
|
|
7
9
|
import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
|
|
8
10
|
import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
|
|
9
|
-
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
11
|
+
import { buildStructuredError, buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
10
12
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
11
13
|
export const APPLY_PATCH_TOOL = {
|
|
12
14
|
name: 'apply_patch',
|
|
@@ -21,13 +23,12 @@ export const APPLY_PATCH_TOOL = {
|
|
|
21
23
|
nuances: [
|
|
22
24
|
'Multi-file patches use `path` as base directory; per-file results in `results[]`.',
|
|
23
25
|
],
|
|
24
|
-
gotchas: ['Patch must include valid hunk headers; use `dryRun=true` first.'],
|
|
25
26
|
taskSupport: 'forbidden',
|
|
26
27
|
};
|
|
27
28
|
function assertPatchTargetSizeWithinLimit(filePath, size, maxFileSize) {
|
|
28
29
|
if (size <= maxFileSize)
|
|
29
30
|
return;
|
|
30
|
-
throw new McpError(ErrorCode.
|
|
31
|
+
throw new McpError(ErrorCode.TOO_LARGE, `File too large for patch (${size} bytes > ${maxFileSize} bytes).`, filePath, { size, maxFileSize });
|
|
31
32
|
}
|
|
32
33
|
function countStructuredPatchStats(diff) {
|
|
33
34
|
let linesAdded = 0;
|
|
@@ -68,11 +69,15 @@ async function applyDiff(filePath, diff, options, signal) {
|
|
|
68
69
|
return {
|
|
69
70
|
path: validPath,
|
|
70
71
|
applied: false,
|
|
71
|
-
error: 'Patch application failed',
|
|
72
|
+
error: buildStructuredError(new McpError(ErrorCode.INVALID_INPUT, 'Patch application failed', validPath), ErrorCode.INVALID_INPUT, validPath),
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
75
|
if (patched === content) {
|
|
75
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
path: validPath,
|
|
78
|
+
applied: false,
|
|
79
|
+
error: buildStructuredError(new McpError(ErrorCode.INVALID_INPUT, 'Patch had no effect', validPath), ErrorCode.INVALID_INPUT, validPath),
|
|
80
|
+
};
|
|
76
81
|
}
|
|
77
82
|
const patchStats = countStructuredPatchStats(diff);
|
|
78
83
|
if (!options.dryRun) {
|
|
@@ -88,10 +93,10 @@ async function processMultiFilePatch(basePath, parsed, options, signal) {
|
|
|
88
93
|
return () => Promise.resolve({
|
|
89
94
|
path: '<unknown>',
|
|
90
95
|
applied: false,
|
|
91
|
-
error: 'Missing file name in patch header',
|
|
96
|
+
error: buildStructuredError(new McpError(ErrorCode.INVALID_INPUT, 'Missing file name in patch header'), ErrorCode.INVALID_INPUT),
|
|
92
97
|
});
|
|
93
98
|
}
|
|
94
|
-
const filePath =
|
|
99
|
+
const filePath = resolve(validBase, fileName);
|
|
95
100
|
return async () => {
|
|
96
101
|
try {
|
|
97
102
|
const result = await applyDiff(filePath, diff, options, signal);
|
|
@@ -101,7 +106,7 @@ async function processMultiFilePatch(basePath, parsed, options, signal) {
|
|
|
101
106
|
return {
|
|
102
107
|
path: fileName,
|
|
103
108
|
applied: false,
|
|
104
|
-
error:
|
|
109
|
+
error: buildStructuredError(error, ErrorCode.INVALID_INPUT, filePath),
|
|
105
110
|
};
|
|
106
111
|
}
|
|
107
112
|
};
|
|
@@ -117,7 +122,17 @@ async function processMultiFilePatch(basePath, parsed, options, signal) {
|
|
|
117
122
|
return acc;
|
|
118
123
|
}, { applied: 0, hunks: 0, added: 0, removed: 0 });
|
|
119
124
|
const label = options.dryRun ? ' (dry run)' : '';
|
|
125
|
+
if (totals.applied === 0) {
|
|
126
|
+
const failedPaths = results
|
|
127
|
+
.filter((r) => !r.applied)
|
|
128
|
+
.map((r) => r.path)
|
|
129
|
+
.join(', ');
|
|
130
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `All ${parsed.length} patches failed${label}. Files: ${failedPaths}. Regenerate via diff_files.`);
|
|
131
|
+
}
|
|
120
132
|
const text = `Applied ${totals.applied}/${parsed.length} file patches${label}`;
|
|
133
|
+
if (!options.dryRun) {
|
|
134
|
+
Logger.info(`apply_patch: ${basePath} (${totals.applied} file(s), +${totals.added}/-${totals.removed})`);
|
|
135
|
+
}
|
|
121
136
|
return buildToolResponse(text, {
|
|
122
137
|
ok: totals.applied === parsed.length,
|
|
123
138
|
path: basePath,
|
|
@@ -130,13 +145,13 @@ async function processMultiFilePatch(basePath, parsed, options, signal) {
|
|
|
130
145
|
}
|
|
131
146
|
async function handleApplyPatch(args, signal) {
|
|
132
147
|
if (!args.patch.trim()) {
|
|
133
|
-
throw new McpError(ErrorCode.
|
|
148
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Patch content is empty.');
|
|
134
149
|
}
|
|
135
150
|
const fuzzFactor = args.fuzzFactor ?? 0;
|
|
136
151
|
const parsed = parsePatch(args.patch);
|
|
137
152
|
const hasHunks = parsed.some((p) => p.hunks.length > 0);
|
|
138
153
|
if (!hasHunks) {
|
|
139
|
-
throw new McpError(ErrorCode.
|
|
154
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Patch must include unified hunk headers (@@ -n,n +n,n @@).');
|
|
140
155
|
}
|
|
141
156
|
const options = {
|
|
142
157
|
dryRun: args.dryRun,
|
|
@@ -148,17 +163,20 @@ async function handleApplyPatch(args, signal) {
|
|
|
148
163
|
}
|
|
149
164
|
const diff = parsed[0];
|
|
150
165
|
if (!diff) {
|
|
151
|
-
throw new McpError(ErrorCode.
|
|
166
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'No patch content found.');
|
|
152
167
|
}
|
|
153
168
|
const result = await applyDiff(args.path, diff, options, signal);
|
|
154
169
|
if (!result.applied) {
|
|
155
|
-
throw new McpError(ErrorCode.
|
|
156
|
-
? 'Patch had no effect
|
|
157
|
-
: 'Patch
|
|
170
|
+
throw new McpError(ErrorCode.INVALID_INPUT, result.error?.message === 'Patch had no effect'
|
|
171
|
+
? 'Patch had no effect. Content unchanged. Regenerate via diff_files.'
|
|
172
|
+
: 'Patch failed. Content may have changed. Regenerate via diff_files. For minor diffs, use fuzzFactor.');
|
|
158
173
|
}
|
|
159
174
|
const text = args.dryRun
|
|
160
175
|
? 'Dry run successful. Patch can be applied.'
|
|
161
176
|
: `Successfully patched ${args.path}`;
|
|
177
|
+
if (!args.dryRun) {
|
|
178
|
+
Logger.info(`apply_patch: ${args.path} (+${result.linesAdded ?? 0}/-${result.linesRemoved ?? 0})`);
|
|
179
|
+
}
|
|
162
180
|
return buildToolResponse(text, {
|
|
163
181
|
ok: true,
|
|
164
182
|
path: result.path,
|
|
@@ -172,19 +190,20 @@ export function registerApplyPatchTool(server, options = {}) {
|
|
|
172
190
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
173
191
|
toolName: 'apply_patch',
|
|
174
192
|
extra,
|
|
193
|
+
outputSchema: ApplyPatchOutputSchema,
|
|
175
194
|
timedSignal: {},
|
|
176
195
|
context: { path: args.path },
|
|
177
196
|
run: (signal) => handleApplyPatch(args, signal),
|
|
178
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
197
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
|
|
179
198
|
});
|
|
180
199
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
181
200
|
guard: options.isInitialized,
|
|
182
201
|
progressMessage: (args) => {
|
|
183
|
-
const name =
|
|
202
|
+
const name = basename(args.path);
|
|
184
203
|
return args.dryRun ? `🛠 patch: ${name} [dry run]` : `🛠 patch: ${name}`;
|
|
185
204
|
},
|
|
186
205
|
completionMessage: (args, result) => {
|
|
187
|
-
const name =
|
|
206
|
+
const name = basename(args.path);
|
|
188
207
|
if (result.isError)
|
|
189
208
|
return `🛠 patch: ${name} • failed`;
|
|
190
209
|
const sc = result.structuredContent;
|