@j0hanz/filesystem-mcp 1.16.2 → 1.16.3

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.
Files changed (69) hide show
  1. package/README.md +18 -14
  2. package/dist/assets/logo.svg +26 -11
  3. package/dist/cli.js +1 -1
  4. package/dist/completions.d.ts +1 -1
  5. package/dist/completions.js +1 -2
  6. package/dist/lib/constants.d.ts +2 -0
  7. package/dist/lib/constants.js +2 -3
  8. package/dist/lib/fs-helpers.d.ts +1 -1
  9. package/dist/lib/fs-helpers.js +0 -1
  10. package/dist/lib/logger.d.ts +3 -4
  11. package/dist/lib/logger.js +1 -1
  12. package/dist/lib/paths.d.ts +1 -1
  13. package/dist/prompts.d.ts +1 -1
  14. package/dist/prompts.js +11 -11
  15. package/dist/resources/workflows.js +12 -0
  16. package/dist/resources.d.ts +1 -1
  17. package/dist/resources.js +1 -1
  18. package/dist/schemas.d.ts +4 -4
  19. package/dist/server/bootstrap.d.ts +1 -10
  20. package/dist/server/bootstrap.js +165 -95
  21. package/dist/server/roots-manager.d.ts +3 -2
  22. package/dist/server/roots-manager.js +30 -4
  23. package/dist/server/task-store.d.ts +2 -3
  24. package/dist/server/task-store.js +1 -1
  25. package/dist/tools/apply-patch.d.ts +1 -1
  26. package/dist/tools/apply-patch.js +16 -12
  27. package/dist/tools/calculate-hash.d.ts +1 -1
  28. package/dist/tools/calculate-hash.js +8 -12
  29. package/dist/tools/contract.d.ts +5 -0
  30. package/dist/tools/create-directory.d.ts +1 -1
  31. package/dist/tools/create-directory.js +7 -10
  32. package/dist/tools/delete-file.d.ts +1 -1
  33. package/dist/tools/delete-file.js +12 -11
  34. package/dist/tools/diff-files.d.ts +1 -1
  35. package/dist/tools/diff-files.js +8 -11
  36. package/dist/tools/edit-file.d.ts +1 -1
  37. package/dist/tools/edit-file.js +12 -11
  38. package/dist/tools/icons.d.ts +15 -0
  39. package/dist/tools/icons.js +24 -0
  40. package/dist/tools/list-directory.d.ts +1 -1
  41. package/dist/tools/list-directory.js +7 -10
  42. package/dist/tools/move-file.d.ts +1 -1
  43. package/dist/tools/move-file.js +12 -11
  44. package/dist/tools/read-multiple.d.ts +1 -1
  45. package/dist/tools/read-multiple.js +8 -12
  46. package/dist/tools/read.d.ts +1 -1
  47. package/dist/tools/read.js +7 -10
  48. package/dist/tools/replace-in-files.d.ts +1 -1
  49. package/dist/tools/replace-in-files.js +11 -13
  50. package/dist/tools/roots.d.ts +1 -1
  51. package/dist/tools/roots.js +7 -10
  52. package/dist/tools/search-content.d.ts +1 -1
  53. package/dist/tools/search-content.js +8 -13
  54. package/dist/tools/search-files.d.ts +1 -1
  55. package/dist/tools/search-files.js +11 -16
  56. package/dist/tools/shared.d.ts +15 -12
  57. package/dist/tools/shared.js +84 -56
  58. package/dist/tools/stat-many.d.ts +1 -1
  59. package/dist/tools/stat-many.js +8 -12
  60. package/dist/tools/stat.d.ts +1 -1
  61. package/dist/tools/stat.js +7 -10
  62. package/dist/tools/task-support.d.ts +14 -12
  63. package/dist/tools/task-support.js +98 -92
  64. package/dist/tools/tree.d.ts +1 -1
  65. package/dist/tools/tree.js +11 -15
  66. package/dist/tools/write-file.d.ts +1 -1
  67. package/dist/tools/write-file.js +12 -11
  68. package/dist/tools.d.ts +1 -1
  69. package/package.json +6 -4
@@ -1,13 +1,10 @@
1
- import { InMemoryTaskMessageQueue } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
- import { isInitializeRequest, LATEST_PROTOCOL_VERSION, SetLevelRequestSchema, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
1
+ import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
2
+ import { InMemoryTaskMessageQueue, isInitializeRequest, LATEST_PROTOCOL_VERSION, localhostAllowedHostnames, McpServer, StdioServerTransport, SUPPORTED_PROTOCOL_VERSIONS, validateHostHeader, } from '@modelcontextprotocol/server';
6
3
  import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
7
4
  import { channel } from 'node:diagnostics_channel';
8
5
  import { readFile } from 'node:fs/promises';
9
6
  import { createServer as createHttpServer, } from 'node:http';
10
- import { DEFAULT_LOG_LEVEL, parseEnvInt } from '../lib/constants.js';
7
+ import { DEFAULT_LOG_LEVEL, INIT_HANDSHAKE_TIMEOUT_MS, INIT_TIMEOUT_CLOSE, parseEnvInt, } from '../lib/constants.js';
11
8
  import { formatUnknownErrorMessage } from '../lib/errors.js';
12
9
  import { createLoggingState, Logger, logToMcp, SessionContext, } from '../lib/logger.js';
13
10
  import { withAllowedDirectoriesState } from '../lib/paths.js';
@@ -21,28 +18,6 @@ import { registerAllTools } from '../tools.js';
21
18
  import { withDefaultIcons } from '../tools/shared.js';
22
19
  import { RootsManager } from './roots-manager.js';
23
20
  import { createTaskStore } from './task-store.js';
24
- let cachedTaskToolSupport;
25
- function detectTaskToolSupport() {
26
- if (cachedTaskToolSupport !== undefined) {
27
- return cachedTaskToolSupport;
28
- }
29
- try {
30
- // Instantiate a minimal, unconnected probe server to duck-type check for
31
- // task tool support. The probe has no transport or active connections, so
32
- // close() only releases in-memory state; fire-and-forget is safe here.
33
- const probe = new McpServer({
34
- name: 'filesystem-mcp-capability-probe',
35
- version: '0.0.0',
36
- }, { capabilities: { tools: {} } });
37
- cachedTaskToolSupport =
38
- typeof probe.experimental.tasks.registerToolTask === 'function';
39
- probe.close().catch(() => { });
40
- }
41
- catch {
42
- cachedTaskToolSupport = false;
43
- }
44
- return cachedTaskToolSupport;
45
- }
46
21
  function buildServerCapabilities(options = {}) {
47
22
  const capabilities = {
48
23
  logging: {},
@@ -64,13 +39,10 @@ function buildServerCapabilities(options = {}) {
64
39
  }
65
40
  return capabilities;
66
41
  }
67
- function supportsTaskToolRequests() {
68
- return detectTaskToolSupport();
69
- }
70
42
  // Global map of all active servers by sessionId for routing logs
71
- export const activeServers = new Map();
43
+ const activeServers = new Map();
72
44
  // For stdio (single session without a specific ID)
73
- export let stdioServer;
45
+ let stdioServer;
74
46
  function stringifyData(data) {
75
47
  if (!data)
76
48
  return '';
@@ -123,18 +95,18 @@ export async function createServer(options = {}) {
123
95
  const resourceStore = createInMemoryResourceStore();
124
96
  const serverInstructions = buildServerInstructions();
125
97
  const localIcon = await getLocalIconInfo();
126
- const taskToolSupport = supportsTaskToolRequests();
127
- const serverConfig = {
128
- capabilities: buildServerCapabilities({
129
- enablePromptListChanged: false,
130
- enableTaskToolRequests: taskToolSupport,
131
- }),
132
- };
133
- if (taskToolSupport) {
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.
135
- serverConfig.taskStore = createTaskStore();
136
- serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
98
+ const capabilities = buildServerCapabilities({
99
+ enablePromptListChanged: false,
100
+ enableTaskToolRequests: true,
101
+ });
102
+ if (capabilities.tasks) {
103
+ capabilities.tasks = {
104
+ ...capabilities.tasks,
105
+ taskStore: createTaskStore(),
106
+ taskMessageQueue: new InMemoryTaskMessageQueue(),
107
+ };
137
108
  }
109
+ const serverConfig = { capabilities };
138
110
  if (serverInstructions) {
139
111
  serverConfig.instructions =
140
112
  'filesystem-mcp: Secure local filesystem MCP server. ' +
@@ -153,7 +125,7 @@ export async function createServer(options = {}) {
153
125
  rootsManagers.set(server, rootsManager);
154
126
  // 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
127
  // Wait, in stdio there's only one server. In HTTP there are multiple.
156
- server.server.setRequestHandler(SetLevelRequestSchema, (req) => {
128
+ server.server.setRequestHandler('logging/setLevel', (req) => {
157
129
  loggingState.minimumLevel = req.params.level;
158
130
  Logger.notice(`Log level set to ${req.params.level}`);
159
131
  return {};
@@ -181,7 +153,11 @@ export async function createServer(options = {}) {
181
153
  export async function startServer(server) {
182
154
  const transport = new StdioServerTransport();
183
155
  const rootsManager = getRootsManager(server);
184
- rootsManager.registerHandlers(server);
156
+ rootsManager.registerHandlers(server, INIT_TIMEOUT_CLOSE
157
+ ? () => {
158
+ void server.close();
159
+ }
160
+ : undefined);
185
161
  await rootsManager.recomputeAllowedDirectories();
186
162
  await server.connect(transport);
187
163
  const sdkOnClose = transport.onclose;
@@ -241,7 +217,23 @@ async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
241
217
  const rootsManager = getRootsManager(mcpServer);
242
218
  rootsManager.registerHandlers(mcpServer);
243
219
  await rootsManager.recomputeAllowedDirectories();
244
- const transport = new StreamableHTTPServerTransport({
220
+ let cleanedUp = false;
221
+ const cleanup = () => {
222
+ if (cleanedUp)
223
+ return;
224
+ cleanedUp = true;
225
+ const { sessionId } = transport;
226
+ if (sessionId) {
227
+ sessions.delete(sessionId);
228
+ activeServers.delete(sessionId);
229
+ }
230
+ rootsManager.destroy();
231
+ };
232
+ const close = async () => {
233
+ cleanup();
234
+ await mcpServer.close();
235
+ };
236
+ const transport = new NodeStreamableHTTPServerTransport({
245
237
  sessionIdGenerator: () => randomUUID(),
246
238
  onsessioninitialized: (sessionId) => {
247
239
  sessions.set(sessionId, {
@@ -249,6 +241,9 @@ async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
249
241
  rootsManager,
250
242
  transport,
251
243
  negotiatedProtocolVersion,
244
+ createdAt: Date.now(),
245
+ cleanup,
246
+ close,
252
247
  });
253
248
  activeServers.set(sessionId, {
254
249
  server: mcpServer,
@@ -257,23 +252,16 @@ async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
257
252
  rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
258
253
  },
259
254
  });
260
- transport.onclose = () => {
261
- const { sessionId } = transport;
262
- if (sessionId) {
263
- sessions.delete(sessionId);
264
- activeServers.delete(sessionId);
265
- }
266
- rootsManager.destroy();
267
- mcpServer.close().catch((err) => {
268
- Logger.error('[HTTP] Error closing MCP server:', formatUnknownErrorMessage(err));
269
- });
270
- };
255
+ transport.onclose = cleanup;
271
256
  await mcpServer.connect(transport);
272
257
  return {
273
258
  server: mcpServer,
274
259
  rootsManager,
275
260
  transport,
276
261
  negotiatedProtocolVersion,
262
+ createdAt: Date.now(),
263
+ cleanup,
264
+ close,
277
265
  };
278
266
  }
279
267
  function sendJsonRpcError(res, status, code, message) {
@@ -296,6 +284,24 @@ function isAllowedOrigin(origin) {
296
284
  return true; // Non-browser clients omit Origin.
297
285
  return LOCALHOST_ORIGIN_RE.test(origin);
298
286
  }
287
+ function normalizeAllowedHostname(host) {
288
+ const trimmed = host.trim().toLowerCase();
289
+ if (trimmed === '::1')
290
+ return '[::1]';
291
+ return trimmed;
292
+ }
293
+ function getAllowedHostnames(httpHost) {
294
+ if (isLoopbackHttpHost(httpHost)) {
295
+ return localhostAllowedHostnames().map(normalizeAllowedHostname);
296
+ }
297
+ const normalizedHost = normalizeAllowedHostname(httpHost);
298
+ if (normalizedHost === '0.0.0.0' ||
299
+ normalizedHost === '::' ||
300
+ normalizedHost === '[::]') {
301
+ return undefined;
302
+ }
303
+ return [normalizedHost];
304
+ }
299
305
  function getSessionId(req) {
300
306
  const rawSessionId = req.headers['mcp-session-id'];
301
307
  return typeof rawSessionId === 'string' &&
@@ -359,6 +365,26 @@ function ensureAuthorizedRequest(req, res) {
359
365
  writeUnauthorizedResponse(res);
360
366
  return false;
361
367
  }
368
+ function ensureAllowedOrigin(req, res) {
369
+ const { origin } = req.headers;
370
+ if (isAllowedOrigin(origin)) {
371
+ return true;
372
+ }
373
+ sendJsonRpcError(res, 403, JSON_RPC_SERVER_ERROR, 'Forbidden: disallowed origin');
374
+ return false;
375
+ }
376
+ function ensureAllowedHostHeader(req, res, httpHost) {
377
+ const allowedHostnames = getAllowedHostnames(httpHost);
378
+ if (!allowedHostnames || allowedHostnames.length === 0) {
379
+ return true;
380
+ }
381
+ const hostHeader = typeof req.headers['host'] === 'string' ? req.headers['host'] : undefined;
382
+ const result = validateHostHeader(hostHeader, allowedHostnames);
383
+ if (result.ok)
384
+ return true;
385
+ sendJsonRpcError(res, 403, JSON_RPC_SERVER_ERROR, `Forbidden: ${result.message}`);
386
+ return false;
387
+ }
362
388
  function discardRequestBody(req) {
363
389
  req.on('error', () => {
364
390
  // Best effort drain to avoid corrupting keep-alive pipelines.
@@ -395,10 +421,49 @@ function assertHttpBindingSecurity(host) {
395
421
  return;
396
422
  throw new Error(`Refusing to bind HTTP server to non-loopback host '${host}' without FILESYSTEM_MCP_API_KEY.`);
397
423
  }
424
+ function writeMethodNotAllowedResponse(res) {
425
+ res.writeHead(405, {
426
+ Allow: 'GET, POST, DELETE',
427
+ 'Content-Type': 'application/json',
428
+ });
429
+ res.end(JSON.stringify({
430
+ jsonrpc: '2.0',
431
+ error: {
432
+ code: JSON_RPC_SERVER_ERROR,
433
+ message: 'Method Not Allowed',
434
+ },
435
+ id: null,
436
+ }));
437
+ }
438
+ function handleHttpRequestError(error, res) {
439
+ if (error instanceof RequestBodyError && !res.headersSent) {
440
+ const rpcCode = error.statusCode === 413
441
+ ? JSON_RPC_INVALID_REQUEST
442
+ : JSON_RPC_PARSE_ERROR;
443
+ res.setHeader('Connection', 'close');
444
+ sendJsonRpcError(res, error.statusCode, rpcCode, error.message);
445
+ return;
446
+ }
447
+ Logger.error('[HTTP] Error handling request:', formatUnknownErrorMessage(error));
448
+ if (!res.headersSent) {
449
+ sendJsonRpcError(res, 500, JSON_RPC_INTERNAL_ERROR, 'Internal Server Error');
450
+ }
451
+ }
398
452
  export async function startHttpServer(port, options) {
399
453
  const sessions = new Map();
400
454
  const httpHost = process.env['FILESYSTEM_MCP_HTTP_HOST'] ?? '127.0.0.1';
401
455
  assertHttpBindingSecurity(httpHost);
456
+ let closingSessions;
457
+ async function closeAllSessions() {
458
+ if (closingSessions)
459
+ return closingSessions;
460
+ closingSessions = (async () => {
461
+ const activeSessions = [...sessions.values()];
462
+ sessions.clear();
463
+ await Promise.allSettled(activeSessions.map((session) => session.close()));
464
+ })();
465
+ await closingSessions;
466
+ }
402
467
  async function handlePostRequest(req, res, sessionId) {
403
468
  if (sessionId) {
404
469
  const session = getSessionOrRespondNotFound(sessions, sessionId, res);
@@ -440,53 +505,48 @@ export async function startHttpServer(port, options) {
440
505
  return;
441
506
  await handleSessionTransportRequest(session, req, res);
442
507
  }
508
+ async function dispatchMcpMethod(method, req, res, sessionId) {
509
+ switch (method) {
510
+ case 'POST':
511
+ await handlePostRequest(req, res, sessionId);
512
+ return;
513
+ case 'GET':
514
+ case 'DELETE':
515
+ await handleGetDeleteRequest(req, res, sessionId);
516
+ return;
517
+ default:
518
+ writeMethodNotAllowedResponse(res);
519
+ }
520
+ }
443
521
  async function handleMcpRequest(req, res) {
444
- const { method } = req;
445
522
  const sessionId = getSessionId(req);
446
- const { origin } = req.headers;
447
- if (!isAllowedOrigin(origin)) {
448
- sendJsonRpcError(res, 403, JSON_RPC_SERVER_ERROR, 'Forbidden: disallowed origin');
523
+ if (!ensureAllowedOrigin(req, res))
524
+ return;
525
+ if (!ensureAllowedHostHeader(req, res, httpHost))
449
526
  return;
450
- }
451
527
  if (!ensureAuthorizedRequest(req, res))
452
528
  return;
453
529
  try {
454
- if (method === 'POST') {
455
- await handlePostRequest(req, res, sessionId);
456
- }
457
- else if (method === 'GET' || method === 'DELETE') {
458
- await handleGetDeleteRequest(req, res, sessionId);
459
- }
460
- else {
461
- res.writeHead(405, {
462
- Allow: 'GET, POST, DELETE',
463
- 'Content-Type': 'application/json',
464
- });
465
- res.end(JSON.stringify({
466
- jsonrpc: '2.0',
467
- error: {
468
- code: JSON_RPC_SERVER_ERROR,
469
- message: 'Method Not Allowed',
470
- },
471
- id: null,
472
- }));
473
- }
530
+ await dispatchMcpMethod(req.method, req, res, sessionId);
474
531
  }
475
532
  catch (error) {
476
- if (error instanceof RequestBodyError && !res.headersSent) {
477
- const rpcCode = error.statusCode === 413
478
- ? JSON_RPC_INVALID_REQUEST
479
- : JSON_RPC_PARSE_ERROR;
480
- res.setHeader('Connection', 'close');
481
- sendJsonRpcError(res, error.statusCode, rpcCode, error.message);
482
- return;
483
- }
484
- Logger.error('[HTTP] Error handling request:', formatUnknownErrorMessage(error));
485
- if (!res.headersSent) {
486
- sendJsonRpcError(res, 500, JSON_RPC_INTERNAL_ERROR, 'Internal Server Error');
487
- }
533
+ handleHttpRequestError(error, res);
488
534
  }
489
535
  }
536
+ const SWEEP_INTERVAL_MS = INIT_HANDSHAKE_TIMEOUT_MS * 2;
537
+ const sweepTimer = setInterval(() => {
538
+ const now = Date.now();
539
+ for (const [sessionId, session] of sessions) {
540
+ if (!session.rootsManager.isInitialized() &&
541
+ now - session.createdAt > INIT_HANDSHAKE_TIMEOUT_MS) {
542
+ Logger.warn(`[HTTP] Evicting stale session ${sessionId}: client never sent notifications/initialized`);
543
+ session.server.close().catch((err) => {
544
+ Logger.error(`[HTTP] Error closing stale session ${sessionId}:`, formatUnknownErrorMessage(err));
545
+ });
546
+ }
547
+ }
548
+ }, SWEEP_INTERVAL_MS);
549
+ sweepTimer.unref();
490
550
  const httpServer = createHttpServer((req, res) => {
491
551
  const urlPath = (req.url ?? '/').split('?')[0];
492
552
  if (urlPath === '/mcp') {
@@ -499,6 +559,16 @@ export async function startHttpServer(port, options) {
499
559
  res.end('Not Found');
500
560
  }
501
561
  });
562
+ httpServer.once('close', () => {
563
+ clearInterval(sweepTimer);
564
+ });
565
+ const originalClose = httpServer.close.bind(httpServer);
566
+ httpServer.close = ((callback) => {
567
+ void closeAllSessions().catch((error) => {
568
+ Logger.error('[HTTP] Error closing sessions before server shutdown:', formatUnknownErrorMessage(error));
569
+ });
570
+ return originalClose(callback);
571
+ });
502
572
  return new Promise((resolve, reject) => {
503
573
  httpServer.once('error', reject);
504
574
  httpServer.listen(port, httpHost, () => {
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { type McpServer } from '@modelcontextprotocol/server';
2
2
  import { type LoggingState } from '../lib/logger.js';
3
3
  import { type AllowedDirectoriesState } from '../lib/paths.js';
4
4
  export interface ServerOptions {
@@ -10,6 +10,7 @@ export declare class RootsManager {
10
10
  private rootDirectories;
11
11
  private allowedDirectoriesState;
12
12
  private clientInitialized;
13
+ private initTimer;
13
14
  private updatingRoots;
14
15
  private pendingRootsUpdate;
15
16
  private readonly options;
@@ -19,7 +20,7 @@ export declare class RootsManager {
19
20
  destroy(): void;
20
21
  getAllowedDirectoriesState(): AllowedDirectoriesState;
21
22
  logMissingDirectoriesIfNeeded(server: McpServer): void;
22
- registerHandlers(server: McpServer): void;
23
+ registerHandlers(server: McpServer, onInitTimeout?: () => void): void;
23
24
  recomputeAllowedDirectories(): Promise<void>;
24
25
  private scheduleRootsUpdate;
25
26
  private logMissingDirectories;
@@ -1,13 +1,16 @@
1
- import { InitializedNotificationSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
1
+ import {} from '@modelcontextprotocol/server';
2
+ import { channel } from 'node:diagnostics_channel';
2
3
  import { realpath } from 'node:fs/promises';
3
4
  import { z } from 'zod';
4
5
  import { assertNotAborted, createTimedAbortSignal, withAbort, } from '../lib/abort.js';
6
+ import { INIT_HANDSHAKE_TIMEOUT_MS } from '../lib/constants.js';
5
7
  import { formatUnknownErrorMessage } from '../lib/errors.js';
6
8
  import { Logger, logToMcp } from '../lib/logger.js';
7
9
  import { getValidRootDirectories, isPathWithinDirectories, normalizePath, resolveAllowedDirectoriesState, setAllowedDirectoriesStateResolved, } from '../lib/paths.js';
8
10
  import { debounce, isRecord } from '../lib/utils.js';
9
11
  const ROOTS_TIMEOUT_MS = 5000;
10
12
  const ROOTS_DEBOUNCE_MS = 100;
13
+ const LIFECYCLE_CHANNEL = channel('filesystem-mcp:lifecycle');
11
14
  function normalizeCLIDirectories(dirs) {
12
15
  const normalized = [];
13
16
  for (const dir of dirs) {
@@ -88,6 +91,7 @@ export class RootsManager {
88
91
  expanded: [],
89
92
  };
90
93
  clientInitialized = false;
94
+ initTimer;
91
95
  // Guard concurrent root refreshes; if one is already running we queue one
92
96
  // replay so the last-known state still gets applied after completion.
93
97
  updatingRoots = false;
@@ -103,6 +107,10 @@ export class RootsManager {
103
107
  return this.clientInitialized;
104
108
  }
105
109
  destroy() {
110
+ if (this.initTimer) {
111
+ clearTimeout(this.initTimer);
112
+ this.initTimer = undefined;
113
+ }
106
114
  if (this._debouncedUpdate) {
107
115
  this._debouncedUpdate.cancel();
108
116
  this._debouncedUpdate = undefined;
@@ -119,16 +127,34 @@ export class RootsManager {
119
127
  this.logMissingDirectories(server);
120
128
  }
121
129
  }
122
- registerHandlers(server) {
123
- server.server.setNotificationHandler(InitializedNotificationSchema, async () => {
130
+ registerHandlers(server, onInitTimeout) {
131
+ server.server.setNotificationHandler('notifications/initialized', async () => {
132
+ if (this.initTimer) {
133
+ clearTimeout(this.initTimer);
134
+ this.initTimer = undefined;
135
+ }
124
136
  this.clientInitialized = true;
125
137
  await this.updateRootsFromClient(server);
126
138
  });
127
- server.server.setNotificationHandler(RootsListChangedNotificationSchema, () => {
139
+ server.server.setNotificationHandler('notifications/roots/list_changed', () => {
128
140
  if (!this.clientInitialized)
129
141
  return;
130
142
  this.scheduleRootsUpdate(server);
131
143
  });
144
+ this.initTimer = setTimeout(() => {
145
+ if (!this.clientInitialized) {
146
+ if (LIFECYCLE_CHANNEL.hasSubscribers) {
147
+ LIFECYCLE_CHANNEL.publish({
148
+ phase: 'init_timeout',
149
+ timeoutMs: INIT_HANDSHAKE_TIMEOUT_MS,
150
+ });
151
+ }
152
+ logToMcp(server, 'warning', `Client did not send notifications/initialized within ${String(INIT_HANDSHAKE_TIMEOUT_MS)}ms`, this.loggingState.minimumLevel);
153
+ onInitTimeout?.();
154
+ }
155
+ this.initTimer = undefined;
156
+ }, INIT_HANDSHAKE_TIMEOUT_MS);
157
+ this.initTimer.unref();
132
158
  }
133
159
  async recomputeAllowedDirectories() {
134
160
  const cliAllowedDirs = normalizeCLIDirectories(this.options.cliAllowedDirs ?? []);
@@ -1,10 +1,9 @@
1
- import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
2
- import type { Result } from '@modelcontextprotocol/sdk/types.js';
1
+ import { InMemoryTaskStore, type Result, type Task } from '@modelcontextprotocol/server';
3
2
  export declare class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
4
3
  private readonly cancelledResults;
5
4
  getTaskResult(taskId: string, sessionId?: string): Promise<Result>;
6
5
  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>;
6
+ updateTaskStatus(taskId: string, status: Task['status'], statusMessage?: string, sessionId?: string): Promise<void>;
8
7
  cleanup(): void;
9
8
  }
10
9
  export declare function createTaskStore(): ResultAwareInMemoryTaskStore;
@@ -1,4 +1,4 @@
1
- import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
1
+ import { InMemoryTaskStore, } from '@modelcontextprotocol/server';
2
2
  import { ErrorCode } from '../lib/errors.js';
3
3
  const DEFAULT_CANCELLED_STATUS_MESSAGE = 'Client cancelled task execution.';
4
4
  function getTaskKey(taskId, sessionId) {
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import type { McpServer } from '@modelcontextprotocol/server';
2
2
  import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
3
  export declare const APPLY_PATCH_TOOL: ToolContract;
4
4
  export declare function registerApplyPatchTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -8,8 +8,9 @@ import { atomicWriteFile, processInParallel } from '../lib/fs-helpers.js';
8
8
  import { Logger } from '../lib/logger.js';
9
9
  import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
10
10
  import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
11
- import { buildStructuredError, buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
12
- import { registerToolTaskIfAvailable } from './task-support.js';
11
+ import { FILE_EDIT_ICONS } from './icons.js';
12
+ import { buildStructuredError, buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, } from './shared.js';
13
+ import { registerStandardTool } from './task-support.js';
13
14
  export const APPLY_PATCH_TOOL = {
14
15
  name: 'apply_patch',
15
16
  title: 'Apply Patch',
@@ -20,10 +21,11 @@ export const APPLY_PATCH_TOOL = {
20
21
  inputSchema: ApplyPatchInputSchema,
21
22
  outputSchema: ApplyPatchOutputSchema,
22
23
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
24
+ icons: FILE_EDIT_ICONS,
23
25
  nuances: [
24
26
  'Multi-file patches use `path` as base directory; per-file results in `results[]`.',
25
27
  ],
26
- taskSupport: 'forbidden',
28
+ taskSupport: 'optional',
27
29
  };
28
30
  function assertPatchTargetSizeWithinLimit(filePath, size, maxFileSize) {
29
31
  if (size <= maxFileSize)
@@ -187,17 +189,23 @@ async function handleApplyPatch(args, signal) {
187
189
  });
188
190
  }
189
191
  export function registerApplyPatchTool(server, options = {}) {
190
- const handler = (args, extra) => executeToolWithDiagnostics({
192
+ const handler = (args, ctx) => executeToolWithDiagnostics({
191
193
  toolName: 'apply_patch',
192
- extra,
194
+ ctx,
193
195
  outputSchema: ApplyPatchOutputSchema,
194
196
  timedSignal: {},
195
197
  context: { path: args.path },
196
- run: (signal) => handleApplyPatch(args, signal),
198
+ run: async (signal) => {
199
+ const result = await handleApplyPatch(args, signal);
200
+ if (!args.dryRun) {
201
+ const sc = result.structuredContent;
202
+ void ctx.log?.('info', `patch: ${args.path} (+${String(sc.linesAdded ?? 0)}/-${String(sc.linesRemoved ?? 0)})`);
203
+ }
204
+ return result;
205
+ },
197
206
  onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
198
207
  });
199
- const wrappedHandler = wrapToolHandler(handler, {
200
- guard: options.isInitialized,
208
+ registerStandardTool(server, APPLY_PATCH_TOOL, handler, options, {
201
209
  progressMessage: (args) => {
202
210
  const name = basename(args.path);
203
211
  return args.dryRun ? `🛠 patch: ${name} [dry run]` : `🛠 patch: ${name}`;
@@ -217,8 +225,4 @@ export function registerApplyPatchTool(server, options = {}) {
217
225
  return `🛠 patch: ${name} • ${dry}no changes`;
218
226
  },
219
227
  });
220
- const validatedHandler = withValidatedArgs(ApplyPatchInputSchema, wrappedHandler);
221
- if (registerToolTaskIfAvailable(server, 'apply_patch', APPLY_PATCH_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
222
- return;
223
- server.registerTool('apply_patch', withDefaultIcons({ ...APPLY_PATCH_TOOL }, options.iconInfo), validatedHandler);
224
228
  }
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import type { McpServer } from '@modelcontextprotocol/server';
2
2
  import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
3
  export declare const CALCULATE_HASH_TOOL: ToolContract;
4
4
  export declare function registerCalculateHashTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -10,8 +10,9 @@ import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations
10
10
  import { globEntries } from '../lib/file-operations/traversal.js';
11
11
  import { validateExistingPath } from '../lib/paths.js';
12
12
  import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas.js';
13
- import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
14
- import { registerToolTaskIfAvailable } from './task-support.js';
13
+ import { FILE_READ_ICONS } from './icons.js';
14
+ import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, } from './shared.js';
15
+ import { registerStandardTool } from './task-support.js';
15
16
  const WINDOWS_PATH_SEPARATOR = /\\/gu;
16
17
  export const CALCULATE_HASH_TOOL = {
17
18
  name: 'calculate_hash',
@@ -20,6 +21,7 @@ export const CALCULATE_HASH_TOOL = {
20
21
  inputSchema: CalculateHashInputSchema,
21
22
  outputSchema: CalculateHashOutputSchema,
22
23
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
24
+ icons: FILE_READ_ICONS,
23
25
  nuances: [
24
26
  'Directory hashing respects root `.gitignore` and sorts paths for stable output.',
25
27
  ],
@@ -143,15 +145,15 @@ async function handleCalculateHash(args, signal, onProgress) {
143
145
  }
144
146
  }
145
147
  export function registerCalculateHashTool(server, options = {}) {
146
- const handler = (args, extra) => executeToolWithDiagnostics({
148
+ const handler = (args, ctx) => executeToolWithDiagnostics({
147
149
  toolName: 'calculate_hash',
148
- extra,
150
+ ctx,
149
151
  outputSchema: CalculateHashOutputSchema,
150
152
  timedSignal: {},
151
153
  context: { path: args.path },
152
154
  run: async (signal) => {
153
155
  const baseName = basename(args.path);
154
- const progress = createToolProgressSession(extra, `🕮 hash: ${baseName}`);
156
+ const progress = createToolProgressSession(ctx, `🕮 hash: ${baseName}`);
155
157
  const progressWithMessage = ({ current, total, }) => {
156
158
  progress.update({
157
159
  current,
@@ -181,11 +183,5 @@ export function registerCalculateHashTool(server, options = {}) {
181
183
  },
182
184
  onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
183
185
  });
184
- const wrappedHandler = wrapToolHandler(handler, {
185
- guard: options.isInitialized,
186
- });
187
- const validatedHandler = withValidatedArgs(CalculateHashInputSchema, wrappedHandler);
188
- if (registerToolTaskIfAvailable(server, 'calculate_hash', CALCULATE_HASH_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
189
- return;
190
- server.registerTool('calculate_hash', withDefaultIcons({ ...CALCULATE_HASH_TOOL }, options.iconInfo), validatedHandler);
186
+ registerStandardTool(server, CALCULATE_HASH_TOOL, handler, options);
191
187
  }
@@ -1,3 +1,4 @@
1
+ import type { Icon } from '@modelcontextprotocol/server';
1
2
  import type { ZodType } from 'zod';
2
3
  export interface ToolContract {
3
4
  /**
@@ -38,6 +39,10 @@ export interface ToolContract {
38
39
  * Common pitfalls or warnings for documentation.
39
40
  */
40
41
  gotchas?: string[];
42
+ /**
43
+ * Optional icons for display in user interfaces.
44
+ */
45
+ icons?: Icon[];
41
46
  /**
42
47
  * Task support level for the tool. Defaults to 'forbidden'.
43
48
  */
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import type { McpServer } from '@modelcontextprotocol/server';
2
2
  import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
3
  export declare const CREATE_DIRECTORY_TOOL: ToolContract;
4
4
  export declare function registerCreateDirectoryTool(server: McpServer, options?: ToolRegistrationOptions): void;