@j0hanz/filesystem-mcp 1.17.1 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +3 -1
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +1 -1
- package/dist/prompts.js +20 -0
- package/dist/resources.js +4 -5
- package/dist/server/bootstrap.js +11 -37
- package/dist/server/event-store.d.ts +18 -0
- package/dist/server/event-store.js +71 -0
- package/dist/server/task-store.d.ts +1 -0
- package/dist/server/task-store.js +23 -5
- package/dist/tools/apply-patch.js +7 -5
- package/dist/tools/calculate-hash.js +5 -5
- package/dist/tools/create-directory.js +5 -6
- package/dist/tools/delete-file.js +3 -3
- package/dist/tools/diff-files.js +5 -5
- package/dist/tools/edit-file.js +5 -5
- package/dist/tools/list-directory.js +3 -3
- package/dist/tools/move-file.js +5 -6
- package/dist/tools/read-multiple.js +14 -7
- package/dist/tools/read.js +2 -2
- package/dist/tools/replace-in-files.js +5 -5
- package/dist/tools/roots.js +3 -3
- package/dist/tools/search-content.js +5 -4
- package/dist/tools/search-files.js +5 -5
- package/dist/tools/shared.d.ts +15 -18
- package/dist/tools/shared.js +5 -17
- package/dist/tools/stat-many.js +7 -6
- package/dist/tools/stat.js +4 -4
- package/dist/tools/task-support.d.ts +5 -0
- package/dist/tools/task-support.js +57 -107
- package/dist/tools/tree.js +5 -5
- package/dist/tools/write-file.js +3 -3
- package/package.json +1 -1
package/dist/lib/constants.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ export declare const MAX_CONCURRENT_TASKS: number;
|
|
|
7
7
|
export declare const TASK_CANCEL_POLL_MS = 2000;
|
|
8
8
|
export declare function getInitHandshakeTimeoutMs(): number;
|
|
9
9
|
export declare const INIT_TIMEOUT_CLOSE: boolean;
|
|
10
|
-
export declare const TASK_POLL_INTERVAL_MS =
|
|
10
|
+
export declare const TASK_POLL_INTERVAL_MS = 500;
|
|
11
|
+
/** How long cancelled-task results are retained before lazy eviction. */
|
|
12
|
+
export declare const CANCELLED_RESULT_TTL_MS: number;
|
|
11
13
|
export declare const PARALLEL_CONCURRENCY: number;
|
|
12
14
|
export declare const MAX_SEARCHABLE_FILE_SIZE: number;
|
|
13
15
|
export declare const MAX_TEXT_FILE_SIZE: number;
|
package/dist/lib/constants.js
CHANGED
|
@@ -80,7 +80,9 @@ export function getInitHandshakeTimeoutMs() {
|
|
|
80
80
|
return parseEnvInt('FS_INIT_HANDSHAKE_TIMEOUT_MS', 30_000, 1_000, 300_000);
|
|
81
81
|
}
|
|
82
82
|
export const INIT_TIMEOUT_CLOSE = parseTrueEnvFlag(process.env['FS_INIT_TIMEOUT_CLOSE']);
|
|
83
|
-
export const TASK_POLL_INTERVAL_MS =
|
|
83
|
+
export const TASK_POLL_INTERVAL_MS = 500;
|
|
84
|
+
/** How long cancelled-task results are retained before lazy eviction. */
|
|
85
|
+
export const CANCELLED_RESULT_TTL_MS = 2 * 60 * 1_000; // 2 minutes
|
|
84
86
|
// Auto-tuned parallelism based on CPU cores (no env override)
|
|
85
87
|
const BYTES_PER_PARALLEL_TASK = 64 * MIB;
|
|
86
88
|
const BYTES_PER_SEARCH_WORKER = 128 * MIB;
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export declare class McpError extends Error {
|
|
|
22
22
|
static accessDenied(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
|
|
23
23
|
static timeout(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
|
|
24
24
|
}
|
|
25
|
+
export declare function classifyError(error: unknown): ErrorCode;
|
|
25
26
|
export declare function createDetailedError(error: unknown, path?: string, additionalDetails?: Record<string, unknown>): DetailedError;
|
|
26
27
|
export declare function formatDetailedError(error: DetailedError): string;
|
|
27
28
|
export declare function getSuggestion(code: ErrorCode): string | undefined;
|
package/dist/lib/errors.js
CHANGED
|
@@ -236,7 +236,7 @@ function classifyMessageError(error) {
|
|
|
236
236
|
}
|
|
237
237
|
return undefined;
|
|
238
238
|
}
|
|
239
|
-
function classifyError(error) {
|
|
239
|
+
export function classifyError(error) {
|
|
240
240
|
let timeoutCode;
|
|
241
241
|
let fallbackCode;
|
|
242
242
|
const terminalCode = walkErrorChain(error, (candidate) => {
|
package/dist/prompts.js
CHANGED
|
@@ -57,6 +57,10 @@ export function registerGetHelpPrompt(server, instructions, iconInfo) {
|
|
|
57
57
|
content: {
|
|
58
58
|
type: 'text',
|
|
59
59
|
text,
|
|
60
|
+
annotations: {
|
|
61
|
+
audience: ['assistant'],
|
|
62
|
+
priority: 1,
|
|
63
|
+
},
|
|
60
64
|
},
|
|
61
65
|
},
|
|
62
66
|
],
|
|
@@ -81,6 +85,10 @@ export function registerCompareFilesPrompt(server, iconInfo) {
|
|
|
81
85
|
content: {
|
|
82
86
|
type: 'text',
|
|
83
87
|
text: `Compare files and explain differences.\n\n1. Call \`diff_files\` with:\n - original: ${original}\n - modified: ${modified}\n2. Summarize: additions, deletions, and semantic changes.\n3. Flag any potential issues (conflicts, regressions, breaking changes).`,
|
|
88
|
+
annotations: {
|
|
89
|
+
audience: ['assistant'],
|
|
90
|
+
priority: 1,
|
|
91
|
+
},
|
|
84
92
|
},
|
|
85
93
|
},
|
|
86
94
|
],
|
|
@@ -103,6 +111,10 @@ export function registerAnalyzePathPrompt(server, iconInfo) {
|
|
|
103
111
|
content: {
|
|
104
112
|
type: 'text',
|
|
105
113
|
text: `Analyze the path: ${targetPath}\n\n1. Call \`stat\` to determine if it is a file or directory.\n2. If file: call \`read\` with \`includeHash: true\` and summarize contents.\n3. If directory: call \`tree\` (maxDepth: 3) and \`ls\` to summarize structure.\n4. Report: type, size, permissions, key observations.`,
|
|
114
|
+
annotations: {
|
|
115
|
+
audience: ['assistant'],
|
|
116
|
+
priority: 1,
|
|
117
|
+
},
|
|
106
118
|
},
|
|
107
119
|
},
|
|
108
120
|
],
|
|
@@ -138,6 +150,10 @@ export function registerGetToolHelpPrompt(server, iconInfo) {
|
|
|
138
150
|
type: 'text',
|
|
139
151
|
text: `Use the embedded contract for \`${toolName}\` as the authoritative reference. ` +
|
|
140
152
|
'Summarize when to use it, its key constraints, and the safest next action.',
|
|
153
|
+
annotations: {
|
|
154
|
+
audience: ['assistant'],
|
|
155
|
+
priority: 1,
|
|
156
|
+
},
|
|
141
157
|
},
|
|
142
158
|
},
|
|
143
159
|
{
|
|
@@ -149,6 +165,10 @@ export function registerGetToolHelpPrompt(server, iconInfo) {
|
|
|
149
165
|
mimeType: 'text/markdown',
|
|
150
166
|
text: toolInfo,
|
|
151
167
|
},
|
|
168
|
+
annotations: {
|
|
169
|
+
audience: ['assistant'],
|
|
170
|
+
priority: 1,
|
|
171
|
+
},
|
|
152
172
|
},
|
|
153
173
|
},
|
|
154
174
|
],
|
package/dist/resources.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { ResourceTemplate, } from '@modelcontextprotocol/server';
|
|
2
|
-
import { ErrorCode, McpError } from './lib/errors.js';
|
|
1
|
+
import { ProtocolError, ProtocolErrorCode, ResourceTemplate, } from '@modelcontextprotocol/server';
|
|
3
2
|
import { globalMetrics } from './lib/observability.js';
|
|
4
3
|
import { buildToolCatalog } from './resources/tool-catalog.js';
|
|
5
4
|
import { buildToolInfo, getToolContracts } from './resources/tool-info.js';
|
|
@@ -104,7 +103,7 @@ export function registerResultResources(server, store, iconInfo) {
|
|
|
104
103
|
}, iconInfo), (uri, variables) => {
|
|
105
104
|
const { id } = variables;
|
|
106
105
|
if (typeof id !== 'string' || id.length === 0) {
|
|
107
|
-
throw new
|
|
106
|
+
throw new ProtocolError(ProtocolErrorCode.ResourceNotFound, 'Cached result expired. Re-run the tool to regenerate.');
|
|
108
107
|
}
|
|
109
108
|
const entry = store.getText(uri.toString());
|
|
110
109
|
return {
|
|
@@ -130,11 +129,11 @@ export function registerToolInfoResource(server, iconInfo) {
|
|
|
130
129
|
}, iconInfo), (uri, variables) => {
|
|
131
130
|
const { name } = variables;
|
|
132
131
|
if (typeof name !== 'string' || name.length === 0) {
|
|
133
|
-
throw new
|
|
132
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Tool name is required');
|
|
134
133
|
}
|
|
135
134
|
const content = buildToolInfo(name);
|
|
136
135
|
if (content === undefined) {
|
|
137
|
-
throw new
|
|
136
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool not found: ${name}`);
|
|
138
137
|
}
|
|
139
138
|
return {
|
|
140
139
|
contents: [
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
|
2
|
-
import { InMemoryTaskMessageQueue, isInitializeRequest,
|
|
2
|
+
import { InMemoryTaskMessageQueue, isInitializeRequest, localhostAllowedHostnames, McpServer, ProtocolErrorCode, StdioServerTransport, validateHostHeader, } from '@modelcontextprotocol/server';
|
|
3
3
|
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
4
4
|
import { channel } from 'node:diagnostics_channel';
|
|
5
5
|
import { readFile } from 'node:fs/promises';
|
|
@@ -17,6 +17,7 @@ import { registerInstructionResource, registerMetricsResource, registerResultRes
|
|
|
17
17
|
import { buildServerInstructions } from '../resources/generated-instructions.js';
|
|
18
18
|
import { registerAllTools } from '../tools.js';
|
|
19
19
|
import { withDefaultIcons } from '../tools/shared.js';
|
|
20
|
+
import { InMemoryEventStore } from './event-store.js';
|
|
20
21
|
import { RootsManager } from './roots-manager.js';
|
|
21
22
|
import { createTaskStore } from './task-store.js';
|
|
22
23
|
function buildServerCapabilities(options = {}) {
|
|
@@ -221,7 +222,7 @@ async function readRequestBody(req) {
|
|
|
221
222
|
req.on('error', reject);
|
|
222
223
|
});
|
|
223
224
|
}
|
|
224
|
-
async function createHttpSession(options, sessions,
|
|
225
|
+
async function createHttpSession(options, sessions, eventStore) {
|
|
225
226
|
const mcpServer = await createServer(options);
|
|
226
227
|
const rootsManager = getRootsManager(mcpServer);
|
|
227
228
|
rootsManager.registerHandlers(mcpServer);
|
|
@@ -235,6 +236,7 @@ async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
|
|
|
235
236
|
if (sessionId) {
|
|
236
237
|
sessions.delete(sessionId);
|
|
237
238
|
activeServers.delete(sessionId);
|
|
239
|
+
eventStore.delete(sessionId);
|
|
238
240
|
}
|
|
239
241
|
rootsManager.destroy();
|
|
240
242
|
};
|
|
@@ -244,12 +246,12 @@ async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
|
|
|
244
246
|
};
|
|
245
247
|
const transport = new NodeStreamableHTTPServerTransport({
|
|
246
248
|
sessionIdGenerator: () => randomUUID(),
|
|
249
|
+
eventStore,
|
|
247
250
|
onsessioninitialized: (sessionId) => {
|
|
248
251
|
sessions.set(sessionId, {
|
|
249
252
|
server: mcpServer,
|
|
250
253
|
rootsManager,
|
|
251
254
|
transport,
|
|
252
|
-
negotiatedProtocolVersion,
|
|
253
255
|
createdAt: Date.now(),
|
|
254
256
|
cleanup,
|
|
255
257
|
close,
|
|
@@ -267,7 +269,6 @@ async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
|
|
|
267
269
|
server: mcpServer,
|
|
268
270
|
rootsManager,
|
|
269
271
|
transport,
|
|
270
|
-
negotiatedProtocolVersion,
|
|
271
272
|
createdAt: Date.now(),
|
|
272
273
|
cleanup,
|
|
273
274
|
close,
|
|
@@ -285,9 +286,9 @@ const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?
|
|
|
285
286
|
const MAX_SESSION_ID_LENGTH = 256;
|
|
286
287
|
const MAX_BEARER_TOKEN_LENGTH = 4096;
|
|
287
288
|
const JSON_RPC_SERVER_ERROR = -32000;
|
|
288
|
-
const JSON_RPC_INVALID_REQUEST =
|
|
289
|
-
const JSON_RPC_PARSE_ERROR =
|
|
290
|
-
const JSON_RPC_INTERNAL_ERROR =
|
|
289
|
+
const JSON_RPC_INVALID_REQUEST = ProtocolErrorCode.InvalidRequest;
|
|
290
|
+
const JSON_RPC_PARSE_ERROR = ProtocolErrorCode.ParseError;
|
|
291
|
+
const JSON_RPC_INTERNAL_ERROR = ProtocolErrorCode.InternalError;
|
|
291
292
|
function isAllowedOrigin(origin) {
|
|
292
293
|
if (origin === undefined)
|
|
293
294
|
return true; // Non-browser clients omit Origin.
|
|
@@ -318,29 +319,6 @@ function getSessionId(req) {
|
|
|
318
319
|
? rawSessionId
|
|
319
320
|
: undefined;
|
|
320
321
|
}
|
|
321
|
-
function getProtocolVersionHeader(req) {
|
|
322
|
-
const rawProtocolVersion = req.headers['mcp-protocol-version'];
|
|
323
|
-
return typeof rawProtocolVersion === 'string'
|
|
324
|
-
? rawProtocolVersion
|
|
325
|
-
: undefined;
|
|
326
|
-
}
|
|
327
|
-
function resolveNegotiatedProtocolVersion(requestedVersion) {
|
|
328
|
-
return SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
|
|
329
|
-
? requestedVersion
|
|
330
|
-
: LATEST_PROTOCOL_VERSION;
|
|
331
|
-
}
|
|
332
|
-
function ensureSessionProtocolVersion(req, res, session) {
|
|
333
|
-
const protocolVersion = getProtocolVersionHeader(req);
|
|
334
|
-
if (!protocolVersion) {
|
|
335
|
-
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: Missing MCP-Protocol-Version header');
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
if (protocolVersion !== session.negotiatedProtocolVersion) {
|
|
339
|
-
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, `Bad Request: MCP-Protocol-Version must match negotiated version ${session.negotiatedProtocolVersion}`);
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
344
322
|
function isAuthorizedBearer(apiKey, authHeader) {
|
|
345
323
|
const bearerPrefix = 'Bearer ';
|
|
346
324
|
if (typeof authHeader !== 'string' || !authHeader.startsWith(bearerPrefix)) {
|
|
@@ -460,6 +438,7 @@ function handleHttpRequestError(error, res) {
|
|
|
460
438
|
}
|
|
461
439
|
export async function startHttpServer(port, options) {
|
|
462
440
|
const sessions = new Map();
|
|
441
|
+
const eventStore = new InMemoryEventStore();
|
|
463
442
|
const httpHost = process.env['FILESYSTEM_MCP_HTTP_HOST'] ?? '127.0.0.1';
|
|
464
443
|
assertHttpBindingSecurity(httpHost);
|
|
465
444
|
let closingSessions;
|
|
@@ -469,6 +448,7 @@ export async function startHttpServer(port, options) {
|
|
|
469
448
|
closingSessions = (async () => {
|
|
470
449
|
const activeSessions = [...sessions.values()];
|
|
471
450
|
sessions.clear();
|
|
451
|
+
eventStore.clear();
|
|
472
452
|
await Promise.allSettled(activeSessions.map((session) => session.close()));
|
|
473
453
|
})();
|
|
474
454
|
await closingSessions;
|
|
@@ -480,10 +460,6 @@ export async function startHttpServer(port, options) {
|
|
|
480
460
|
discardRequestBody(req);
|
|
481
461
|
return;
|
|
482
462
|
}
|
|
483
|
-
if (!ensureSessionProtocolVersion(req, res, session)) {
|
|
484
|
-
discardRequestBody(req);
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
463
|
const body = await readRequestBody(req);
|
|
488
464
|
await handleSessionTransportRequest(session, req, res, body);
|
|
489
465
|
return;
|
|
@@ -495,7 +471,7 @@ export async function startHttpServer(port, options) {
|
|
|
495
471
|
sendJsonRpcError(res, 503, JSON_RPC_SERVER_ERROR, 'Too many sessions');
|
|
496
472
|
return;
|
|
497
473
|
}
|
|
498
|
-
const session = await createHttpSession(options, sessions,
|
|
474
|
+
const session = await createHttpSession(options, sessions, eventStore);
|
|
499
475
|
await handleSessionTransportRequest(session, req, res, body);
|
|
500
476
|
return;
|
|
501
477
|
}
|
|
@@ -510,8 +486,6 @@ export async function startHttpServer(port, options) {
|
|
|
510
486
|
const session = getSessionOrRespondNotFound(sessions, sessionId, res);
|
|
511
487
|
if (!session)
|
|
512
488
|
return;
|
|
513
|
-
if (!ensureSessionProtocolVersion(req, res, session))
|
|
514
|
-
return;
|
|
515
489
|
await handleSessionTransportRequest(session, req, res);
|
|
516
490
|
}
|
|
517
491
|
async function dispatchMcpMethod(method, req, res, sessionId) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { JSONRPCMessage } from '@modelcontextprotocol/server';
|
|
2
|
+
export declare class InMemoryEventStore {
|
|
3
|
+
private streams;
|
|
4
|
+
private eventIdToStreamId;
|
|
5
|
+
storeEvent(streamId: string, message: JSONRPCMessage): Promise<string>;
|
|
6
|
+
getStreamIdForEventId(eventId: string): Promise<string | undefined>;
|
|
7
|
+
replayEventsAfter(lastEventId: string, callbacks: {
|
|
8
|
+
send: (eventId: string, message: JSONRPCMessage) => Promise<void>;
|
|
9
|
+
}): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Cleans up all events for a given streamId.
|
|
12
|
+
*/
|
|
13
|
+
delete(streamId: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Cleans up all streams.
|
|
16
|
+
*/
|
|
17
|
+
clear(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
const MAX_EVENTS_PER_STREAM = 1000;
|
|
3
|
+
export class InMemoryEventStore {
|
|
4
|
+
// Map of streamId -> StoredEvent[]
|
|
5
|
+
streams = new Map();
|
|
6
|
+
// Map of eventId -> streamId for fast lookup
|
|
7
|
+
eventIdToStreamId = new Map();
|
|
8
|
+
storeEvent(streamId, message) {
|
|
9
|
+
const eventId = randomUUID();
|
|
10
|
+
let stream = this.streams.get(streamId);
|
|
11
|
+
if (!stream) {
|
|
12
|
+
stream = [];
|
|
13
|
+
this.streams.set(streamId, stream);
|
|
14
|
+
}
|
|
15
|
+
// Add new event
|
|
16
|
+
stream.push({ id: eventId, message });
|
|
17
|
+
this.eventIdToStreamId.set(eventId, streamId);
|
|
18
|
+
// Enforce limits
|
|
19
|
+
if (stream.length > MAX_EVENTS_PER_STREAM) {
|
|
20
|
+
const removed = stream.shift();
|
|
21
|
+
if (removed) {
|
|
22
|
+
this.eventIdToStreamId.delete(removed.id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return Promise.resolve(eventId);
|
|
26
|
+
}
|
|
27
|
+
getStreamIdForEventId(eventId) {
|
|
28
|
+
return Promise.resolve(this.eventIdToStreamId.get(eventId));
|
|
29
|
+
}
|
|
30
|
+
async replayEventsAfter(lastEventId, callbacks) {
|
|
31
|
+
const streamId = this.eventIdToStreamId.get(lastEventId);
|
|
32
|
+
if (!streamId) {
|
|
33
|
+
throw new Error(`Event ID ${lastEventId} not found or expired`);
|
|
34
|
+
}
|
|
35
|
+
const stream = this.streams.get(streamId);
|
|
36
|
+
if (!stream) {
|
|
37
|
+
throw new Error(`Stream ${streamId} not found`);
|
|
38
|
+
}
|
|
39
|
+
const eventIndex = stream.findIndex((e) => e.id === lastEventId);
|
|
40
|
+
if (eventIndex === -1) {
|
|
41
|
+
throw new Error(`Event ID ${lastEventId} not found in stream ${streamId}`);
|
|
42
|
+
}
|
|
43
|
+
// Replay all events after the found index
|
|
44
|
+
for (let i = eventIndex + 1; i < stream.length; i++) {
|
|
45
|
+
const event = stream[i];
|
|
46
|
+
if (event) {
|
|
47
|
+
await callbacks.send(event.id, event.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return streamId;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Cleans up all events for a given streamId.
|
|
54
|
+
*/
|
|
55
|
+
delete(streamId) {
|
|
56
|
+
const stream = this.streams.get(streamId);
|
|
57
|
+
if (stream) {
|
|
58
|
+
for (const event of stream) {
|
|
59
|
+
this.eventIdToStreamId.delete(event.id);
|
|
60
|
+
}
|
|
61
|
+
this.streams.delete(streamId);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Cleans up all streams.
|
|
66
|
+
*/
|
|
67
|
+
clear() {
|
|
68
|
+
this.streams.clear();
|
|
69
|
+
this.eventIdToStreamId.clear();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { InMemoryTaskStore, type Result, type Task } from '@modelcontextprotocol/server';
|
|
2
2
|
export declare class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
|
|
3
3
|
private readonly cancelledResults;
|
|
4
|
+
private evictExpired;
|
|
4
5
|
getTaskResult(taskId: string, sessionId?: string): Promise<Result>;
|
|
5
6
|
storeTaskResult(taskId: string, status: 'completed' | 'failed', result: Result, sessionId?: string): Promise<void>;
|
|
6
7
|
updateTaskStatus(taskId: string, status: Task['status'], statusMessage?: string, sessionId?: string): Promise<void>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InMemoryTaskStore, } from '@modelcontextprotocol/server';
|
|
2
|
+
import { CANCELLED_RESULT_TTL_MS } from '../lib/constants.js';
|
|
2
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
3
4
|
const DEFAULT_CANCELLED_STATUS_MESSAGE = 'Client cancelled task execution.';
|
|
4
5
|
function getTaskKey(taskId, sessionId) {
|
|
@@ -18,7 +19,16 @@ function buildCancelledTaskResult(statusMessage) {
|
|
|
18
19
|
}
|
|
19
20
|
export class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
|
|
20
21
|
cancelledResults = new Map();
|
|
22
|
+
evictExpired() {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
for (const [key, entry] of this.cancelledResults) {
|
|
25
|
+
if (now - entry.createdAt > CANCELLED_RESULT_TTL_MS) {
|
|
26
|
+
this.cancelledResults.delete(key);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
21
30
|
async getTaskResult(taskId, sessionId) {
|
|
31
|
+
this.evictExpired();
|
|
22
32
|
try {
|
|
23
33
|
return await super.getTaskResult(taskId, sessionId);
|
|
24
34
|
}
|
|
@@ -30,9 +40,9 @@ export class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
|
|
|
30
40
|
const key = getTaskKey(taskId, sessionId);
|
|
31
41
|
const existing = this.cancelledResults.get(key);
|
|
32
42
|
if (existing)
|
|
33
|
-
return existing;
|
|
43
|
+
return existing.result;
|
|
34
44
|
const result = buildCancelledTaskResult(task.statusMessage);
|
|
35
|
-
this.cancelledResults.set(key, result);
|
|
45
|
+
this.cancelledResults.set(key, { result, createdAt: Date.now() });
|
|
36
46
|
return result;
|
|
37
47
|
}
|
|
38
48
|
}
|
|
@@ -48,15 +58,23 @@ export class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
|
|
|
48
58
|
if (task?.status !== 'cancelled') {
|
|
49
59
|
throw error;
|
|
50
60
|
}
|
|
51
|
-
|
|
61
|
+
const key = getTaskKey(taskId, sessionId);
|
|
62
|
+
const existing = this.cancelledResults.get(key);
|
|
63
|
+
this.cancelledResults.set(key, {
|
|
64
|
+
result: existing?.result ?? result,
|
|
65
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
66
|
+
});
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
69
|
async updateTaskStatus(taskId, status, statusMessage, sessionId) {
|
|
55
70
|
await super.updateTaskStatus(taskId, status, statusMessage, sessionId);
|
|
56
71
|
const key = getTaskKey(taskId, sessionId);
|
|
57
72
|
if (status === 'cancelled') {
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
const existing = this.cancelledResults.get(key);
|
|
74
|
+
this.cancelledResults.set(key, {
|
|
75
|
+
result: existing?.result ?? buildCancelledTaskResult(statusMessage),
|
|
76
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
77
|
+
});
|
|
60
78
|
return;
|
|
61
79
|
}
|
|
62
80
|
if (status === 'completed' || status === 'failed') {
|
|
@@ -213,21 +213,23 @@ export function registerApplyPatchTool(server, options = {}) {
|
|
|
213
213
|
registerStandardTool(server, APPLY_PATCH_TOOL, handler, options, {
|
|
214
214
|
progressMessage: (args) => {
|
|
215
215
|
const name = basename(args.path);
|
|
216
|
-
return args.dryRun
|
|
216
|
+
return args.dryRun
|
|
217
|
+
? `${APPLY_PATCH_TOOL.title}: ${name} [dry run]`
|
|
218
|
+
: `${APPLY_PATCH_TOOL.title}: ${name}`;
|
|
217
219
|
},
|
|
218
220
|
completionMessage: (args, result) => {
|
|
219
221
|
const name = basename(args.path);
|
|
220
222
|
if (result.isError)
|
|
221
|
-
return
|
|
223
|
+
return `${APPLY_PATCH_TOOL.title}: ${name} • ${result.errorCode}`;
|
|
222
224
|
const sc = result.structuredContent;
|
|
223
225
|
if (!sc.ok)
|
|
224
|
-
return
|
|
226
|
+
return `${APPLY_PATCH_TOOL.title}: ${name} • failed`;
|
|
225
227
|
const added = sc.linesAdded ?? 0;
|
|
226
228
|
const removed = sc.linesRemoved ?? 0;
|
|
227
229
|
const dry = args.dryRun ? 'dry run ' : '';
|
|
228
230
|
if (added > 0 || removed > 0)
|
|
229
|
-
return
|
|
230
|
-
return
|
|
231
|
+
return `${APPLY_PATCH_TOOL.title}: ${name} • ${dry} +${added} -${removed}`;
|
|
232
|
+
return `${APPLY_PATCH_TOOL.title}: ${name} • ${dry}no changes`;
|
|
231
233
|
},
|
|
232
234
|
});
|
|
233
235
|
}
|
|
@@ -5,7 +5,7 @@ import { basename, relative, win32 } from 'node:path';
|
|
|
5
5
|
import { pipeline } from 'node:stream/promises';
|
|
6
6
|
import { assertNotAborted, withAbort } from '../lib/abort.js';
|
|
7
7
|
import { PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
8
|
-
import { ErrorCode } from '../lib/errors.js';
|
|
8
|
+
import { classifyError, ErrorCode } from '../lib/errors.js';
|
|
9
9
|
import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations/core.js';
|
|
10
10
|
import { globEntries } from '../lib/file-operations/traversal.js';
|
|
11
11
|
import { validateExistingPath } from '../lib/paths.js';
|
|
@@ -153,12 +153,12 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
153
153
|
context: { path: args.path },
|
|
154
154
|
run: async (signal) => {
|
|
155
155
|
const baseName = basename(args.path);
|
|
156
|
-
const progress = createToolProgressSession(ctx,
|
|
156
|
+
const progress = createToolProgressSession(ctx, `${CALCULATE_HASH_TOOL.title}: ${baseName}`);
|
|
157
157
|
const progressWithMessage = ({ current, total, }) => {
|
|
158
158
|
progress.update({
|
|
159
159
|
current,
|
|
160
160
|
...(total !== undefined ? { total } : {}),
|
|
161
|
-
message:
|
|
161
|
+
message: `${CALCULATE_HASH_TOOL.title}: ${baseName} [${current} files]`,
|
|
162
162
|
});
|
|
163
163
|
};
|
|
164
164
|
try {
|
|
@@ -173,11 +173,11 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
173
173
|
else {
|
|
174
174
|
suffix = `${(sc.hash ?? '').slice(0, 8)}…`;
|
|
175
175
|
}
|
|
176
|
-
progress.complete(
|
|
176
|
+
progress.complete(`${CALCULATE_HASH_TOOL.title}: ${baseName} • ${suffix}`, finalCurrent);
|
|
177
177
|
return result;
|
|
178
178
|
}
|
|
179
179
|
catch (error) {
|
|
180
|
-
progress.fail(
|
|
180
|
+
progress.fail(`${CALCULATE_HASH_TOOL.title}: ${baseName} • ${classifyError(error)}`);
|
|
181
181
|
throw error;
|
|
182
182
|
}
|
|
183
183
|
},
|
|
@@ -48,22 +48,21 @@ export function registerCreateDirectoryTool(server, options = {}) {
|
|
|
48
48
|
registerStandardTool(server, CREATE_DIRECTORY_TOOL, handler, options, {
|
|
49
49
|
progressMessage: (args) => {
|
|
50
50
|
if (args.path && !args.paths?.length) {
|
|
51
|
-
return
|
|
51
|
+
return `${CREATE_DIRECTORY_TOOL.title}: ${basename(args.path)}`;
|
|
52
52
|
}
|
|
53
53
|
const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
|
|
54
|
-
return
|
|
54
|
+
return `${CREATE_DIRECTORY_TOOL.title}: ${count} directories`;
|
|
55
55
|
},
|
|
56
56
|
completionMessage: (args, result) => {
|
|
57
57
|
if (args.path && !args.paths?.length) {
|
|
58
58
|
const name = basename(args.path);
|
|
59
59
|
if (result.isError)
|
|
60
|
-
return
|
|
61
|
-
return `🛠 mkdir: ${name}`;
|
|
60
|
+
return `${CREATE_DIRECTORY_TOOL.title}: ${name} • ${result.errorCode}`;
|
|
62
61
|
}
|
|
63
62
|
const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
|
|
64
63
|
if (result.isError)
|
|
65
|
-
return
|
|
66
|
-
return
|
|
64
|
+
return `${CREATE_DIRECTORY_TOOL.title}: ${count} directories • ${result.errorCode}`;
|
|
65
|
+
return `${CREATE_DIRECTORY_TOOL.title}: ${count} directories`;
|
|
67
66
|
},
|
|
68
67
|
});
|
|
69
68
|
}
|
|
@@ -90,12 +90,12 @@ export function registerDeleteFileTool(server, options = {}) {
|
|
|
90
90
|
},
|
|
91
91
|
});
|
|
92
92
|
registerStandardTool(server, DELETE_FILE_TOOL, handler, options, {
|
|
93
|
-
progressMessage: (args) =>
|
|
93
|
+
progressMessage: (args) => `${DELETE_FILE_TOOL.title}: ${basename(args.path)}`,
|
|
94
94
|
completionMessage: (args, result) => {
|
|
95
95
|
const name = basename(args.path);
|
|
96
96
|
if (result.isError)
|
|
97
|
-
return
|
|
98
|
-
return
|
|
97
|
+
return `${DELETE_FILE_TOOL.title}: ${name} • ${result.errorCode}`;
|
|
98
|
+
return `${DELETE_FILE_TOOL.title}: ${name}`;
|
|
99
99
|
},
|
|
100
100
|
});
|
|
101
101
|
}
|
package/dist/tools/diff-files.js
CHANGED
|
@@ -116,21 +116,21 @@ export function registerDiffFilesTool(server, options = {}) {
|
|
|
116
116
|
progressMessage: (args) => {
|
|
117
117
|
const n1 = basename(args.original);
|
|
118
118
|
const n2 = basename(args.modified);
|
|
119
|
-
return
|
|
119
|
+
return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2}`;
|
|
120
120
|
},
|
|
121
121
|
completionMessage: (args, result) => {
|
|
122
122
|
const n1 = basename(args.original);
|
|
123
123
|
const n2 = basename(args.modified);
|
|
124
124
|
if (result.isError)
|
|
125
|
-
return
|
|
125
|
+
return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2} • ${result.errorCode}`;
|
|
126
126
|
const sc = result.structuredContent;
|
|
127
127
|
if (sc.isIdentical)
|
|
128
|
-
return
|
|
128
|
+
return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2} • identical`;
|
|
129
129
|
const added = sc.linesAdded ?? 0;
|
|
130
130
|
const removed = sc.linesRemoved ?? 0;
|
|
131
131
|
if (added > 0 || removed > 0)
|
|
132
|
-
return
|
|
133
|
-
return
|
|
132
|
+
return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2} • +${added} -${removed}`;
|
|
133
|
+
return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2}`;
|
|
134
134
|
},
|
|
135
135
|
});
|
|
136
136
|
}
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -178,22 +178,22 @@ async function loadEditableFile(requestedPath, signal) {
|
|
|
178
178
|
function buildEditProgressMessage(args) {
|
|
179
179
|
const name = basename(args.path);
|
|
180
180
|
const tag = args.dryRun ? ' [dry run]' : '';
|
|
181
|
-
return
|
|
181
|
+
return `${EDIT_FILE_TOOL.title}: ${name}${tag}`;
|
|
182
182
|
}
|
|
183
183
|
function buildEditCompletionMessage(args, result) {
|
|
184
184
|
const name = basename(args.path);
|
|
185
185
|
if (result.isError)
|
|
186
|
-
return
|
|
186
|
+
return `${EDIT_FILE_TOOL.title}: ${name} • ${result.errorCode}`;
|
|
187
187
|
const { structuredContent } = result;
|
|
188
188
|
if (!structuredContent.ok)
|
|
189
|
-
return
|
|
189
|
+
return `${EDIT_FILE_TOOL.title}: ${name} • failed`;
|
|
190
190
|
const applied = structuredContent.appliedEdits ?? 0;
|
|
191
191
|
if (applied === 0)
|
|
192
|
-
return
|
|
192
|
+
return `${EDIT_FILE_TOOL.title}: ${name} • no changes`;
|
|
193
193
|
const added = structuredContent.linesAdded ?? 0;
|
|
194
194
|
const removed = structuredContent.linesRemoved ?? 0;
|
|
195
195
|
const dry = args.dryRun ? 'dry run ' : '';
|
|
196
|
-
return
|
|
196
|
+
return `${EDIT_FILE_TOOL.title}: ${name} • ${dry} +${added} -${removed}`;
|
|
197
197
|
}
|
|
198
198
|
async function applyEdits(content, edits, ignoreWhitespace) {
|
|
199
199
|
let newContent = content;
|
|
@@ -205,14 +205,14 @@ export function registerListDirectoryTool(server, options = {}) {
|
|
|
205
205
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_DIRECTORY, args.path ?? '.'),
|
|
206
206
|
});
|
|
207
207
|
registerStandardTool(server, LIST_DIRECTORY_TOOL, handler, options, {
|
|
208
|
-
progressMessage: (args) =>
|
|
208
|
+
progressMessage: (args) => `${LIST_DIRECTORY_TOOL.title}: ${args.path ? basename(args.path) : '.'}`,
|
|
209
209
|
completionMessage: (args, result) => {
|
|
210
210
|
const base = args.path ? basename(args.path) : '.';
|
|
211
211
|
if (result.isError)
|
|
212
|
-
return
|
|
212
|
+
return `${LIST_DIRECTORY_TOOL.title}: ${base} • ${result.errorCode}`;
|
|
213
213
|
const sc = result.structuredContent;
|
|
214
214
|
const count = sc.totalEntries ?? 0;
|
|
215
|
-
return
|
|
215
|
+
return `${LIST_DIRECTORY_TOOL.title}: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
|
|
216
216
|
},
|
|
217
217
|
});
|
|
218
218
|
}
|
package/dist/tools/move-file.js
CHANGED
|
@@ -157,23 +157,22 @@ export function registerMoveFileTool(server, options = {}) {
|
|
|
157
157
|
progressMessage: (args) => {
|
|
158
158
|
const dest = basename(args.destination);
|
|
159
159
|
if (args.source && !args.sources?.length) {
|
|
160
|
-
return
|
|
160
|
+
return `${MOVE_FILE_TOOL.title}: ${basename(args.source)} → ${dest}`;
|
|
161
161
|
}
|
|
162
162
|
const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0);
|
|
163
|
-
return
|
|
163
|
+
return `${MOVE_FILE_TOOL.title}: ${count} items → ${dest}`;
|
|
164
164
|
},
|
|
165
165
|
completionMessage: (args, result) => {
|
|
166
166
|
const dest = basename(args.destination);
|
|
167
167
|
if (args.source && !args.sources?.length) {
|
|
168
168
|
const src = basename(args.source);
|
|
169
169
|
if (result.isError)
|
|
170
|
-
return
|
|
171
|
-
return `🛠 mv: ${src} → ${dest}`;
|
|
170
|
+
return `${MOVE_FILE_TOOL.title}: ${src} → ${dest} • ${result.errorCode}`;
|
|
172
171
|
}
|
|
173
172
|
const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0);
|
|
174
173
|
if (result.isError)
|
|
175
|
-
return
|
|
176
|
-
return
|
|
174
|
+
return `${MOVE_FILE_TOOL.title}: ${count} items → ${dest} • ${result.errorCode}`;
|
|
175
|
+
return `${MOVE_FILE_TOOL.title}: ${count} items → ${dest}`;
|
|
177
176
|
},
|
|
178
177
|
});
|
|
179
178
|
}
|