@j0hanz/filesystem-mcp 1.9.1 → 1.10.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.
Files changed (45) hide show
  1. package/README.md +10 -10
  2. package/dist/completions.js +140 -115
  3. package/dist/lib/constants.d.ts +1 -0
  4. package/dist/lib/constants.js +2 -0
  5. package/dist/lib/file-operations/metadata.d.ts +5 -1
  6. package/dist/lib/file-operations/metadata.js +14 -1
  7. package/dist/lib/file-operations/search.d.ts +2 -0
  8. package/dist/lib/file-operations/search.js +59 -27
  9. package/dist/lib/fs-helpers.d.ts +3 -1
  10. package/dist/lib/fs-helpers.js +63 -0
  11. package/dist/lib/paths.js +79 -53
  12. package/dist/lib/resource-store.d.ts +2 -0
  13. package/dist/lib/resource-store.js +58 -17
  14. package/dist/prompts.d.ts +2 -0
  15. package/dist/prompts.js +51 -0
  16. package/dist/resources/generated-instructions.js +36 -9
  17. package/dist/resources/tool-catalog.js +30 -7
  18. package/dist/resources/tool-info.d.ts +4 -0
  19. package/dist/resources/tool-info.js +21 -3
  20. package/dist/resources/workflows.js +17 -5
  21. package/dist/schemas.d.ts +36 -1
  22. package/dist/schemas.js +73 -3
  23. package/dist/server/bootstrap.js +85 -65
  24. package/dist/tools/apply-patch.js +135 -31
  25. package/dist/tools/calculate-hash.js +13 -8
  26. package/dist/tools/create-directory.js +14 -3
  27. package/dist/tools/delete-file.js +1 -0
  28. package/dist/tools/diff-files.js +26 -8
  29. package/dist/tools/edit-file.js +11 -8
  30. package/dist/tools/list-directory.js +1 -6
  31. package/dist/tools/move-file.js +39 -7
  32. package/dist/tools/read-multiple.js +9 -1
  33. package/dist/tools/read.js +38 -6
  34. package/dist/tools/replace-in-files.js +72 -25
  35. package/dist/tools/roots.js +1 -0
  36. package/dist/tools/search-content.js +76 -48
  37. package/dist/tools/search-files.js +6 -7
  38. package/dist/tools/shared.d.ts +2 -1
  39. package/dist/tools/shared.js +36 -20
  40. package/dist/tools/stat-many.js +1 -1
  41. package/dist/tools/stat.js +4 -0
  42. package/dist/tools/task-support.js +4 -12
  43. package/dist/tools/tree.js +4 -0
  44. package/dist/tools/write-file.js +4 -2
  45. package/package.json +17 -8
package/dist/schemas.js CHANGED
@@ -39,6 +39,7 @@ const TreeEntrySchema = z.lazy(() => z.strictObject({
39
39
  name: z.string().describe('Name'),
40
40
  type: FileTypeSchema.describe('Type'),
41
41
  relativePath: z.string().describe('Relative path'),
42
+ size: z.number().optional().describe('File size bytes (when includeSizes)'),
42
43
  children: z.array(TreeEntrySchema).optional().describe('Children'),
43
44
  }));
44
45
  const ErrorSchema = z.strictObject({
@@ -57,6 +58,12 @@ const HeadLinesSchema = z
57
58
  .max(100000, 'Max: 100,000')
58
59
  .optional()
59
60
  .describe('Read first N lines');
61
+ const TailLinesSchema = z
62
+ .int({ error: 'Must be integer' })
63
+ .min(1, 'Min: 1')
64
+ .max(100000, 'Max: 100,000')
65
+ .optional()
66
+ .describe('Read last N lines');
60
67
  const LineNumberSchema = z.int({ error: 'Must be integer' }).min(1, 'Min: 1');
61
68
  function addReadRangeIssue(ctx, path, message) {
62
69
  ctx.addIssue({
@@ -67,11 +74,15 @@ function addReadRangeIssue(ctx, path, message) {
67
74
  }
68
75
  const validateReadRange = (value, ctx) => {
69
76
  const hasHead = value.head !== undefined;
77
+ const hasTail = value.tail !== undefined;
70
78
  const hasStart = value.startLine !== undefined;
71
79
  const hasEnd = value.endLine !== undefined;
72
80
  if (hasHead && (hasStart || hasEnd)) {
73
81
  addReadRangeIssue(ctx, 'head', "Cannot use 'head' with 'startLine'/'endLine'");
74
82
  }
83
+ if (hasTail && (hasHead || hasStart || hasEnd)) {
84
+ addReadRangeIssue(ctx, 'tail', "Cannot use 'tail' with 'head'/'startLine'/'endLine'");
85
+ }
75
86
  if (hasEnd && !hasStart) {
76
87
  addReadRangeIssue(ctx, 'endLine', "'endLine' requires 'startLine'");
77
88
  }
@@ -84,6 +95,7 @@ const validateReadRange = (value, ctx) => {
84
95
  function createReadRangeInputFields(descriptions) {
85
96
  return {
86
97
  head: HeadLinesSchema.describe(descriptions.head),
98
+ tail: TailLinesSchema.describe(descriptions.tail),
87
99
  startLine: LineNumberSchema.optional().describe(descriptions.startLine),
88
100
  endLine: LineNumberSchema.optional().describe(descriptions.endLine),
89
101
  };
@@ -193,6 +205,7 @@ export const TreeInputSchema = z.strictObject({
193
205
  .describe(`Max entries. Default: ${DEFAULT_TREE_ENTRIES}`),
194
206
  includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
195
207
  includeIgnored: defaultFalseBoolean('Include ignored items. Disables .gitignore.'),
208
+ includeSizes: defaultFalseBoolean('Include file sizes in tree entries'),
196
209
  });
197
210
  export const SearchContentInputSchema = z.strictObject({
198
211
  path: OptionalPathSchema.describe(DESC_PATH_ROOT),
@@ -227,15 +240,18 @@ export const SearchContentInputSchema = z.strictObject({
227
240
  .describe('Glob for candidate files (e.g. "**/*.ts")'),
228
241
  includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
229
242
  includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, etc).'),
243
+ multiline: defaultFalseBoolean('Multi-line mode. ^ and $ match line boundaries when isRegex=true.'),
230
244
  });
231
245
  export const ReadFileInputSchema = z
232
246
  .strictObject({
233
247
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
234
248
  ...createReadRangeInputFields({
235
249
  head: 'Read first N lines (preview)',
250
+ tail: 'Read last N lines',
236
251
  startLine: 'Start line (1-based, inclusive)',
237
252
  endLine: 'End line (1-based, inclusive). Requires startLine.',
238
253
  }),
254
+ includeHash: defaultFalseBoolean('Include SHA-256 hash of full file content'),
239
255
  })
240
256
  .superRefine(validateReadRange);
241
257
  export const ReadMultipleFilesInputSchema = z
@@ -247,6 +263,7 @@ export const ReadMultipleFilesInputSchema = z
247
263
  .describe('Files to read. e.g. ["src/index.ts"]'),
248
264
  ...createReadRangeInputFields({
249
265
  head: 'Read first N lines of each file',
266
+ tail: 'Read last N lines of each file',
250
267
  startLine: 'Start line (1-based, inclusive) per file',
251
268
  endLine: 'End line (1-based, inclusive) per file. Requires startLine.',
252
269
  }),
@@ -336,6 +353,10 @@ export const SearchContentOutputSchema = SearchSummarySchema.extend({
336
353
  .array(z.strictObject({
337
354
  file: z.string().describe('Relative path'),
338
355
  line: z.number(),
356
+ column: z
357
+ .number()
358
+ .optional()
359
+ .describe('Column of first match (0-based)'),
339
360
  content: z.string(),
340
361
  matchCount: z.number(),
341
362
  contextBefore: z.array(z.string()).optional(),
@@ -370,8 +391,12 @@ const ReadResultSchema = z.strictObject({
370
391
  truncated: z.boolean().optional().describe('Truncated?'),
371
392
  resourceUri: z.string().optional().describe('Full content URI'),
372
393
  totalLines: z.number().optional().describe('Total lines'),
373
- readMode: z.enum(['full', 'head', 'range']).optional().describe('Mode'),
394
+ readMode: z
395
+ .enum(['full', 'head', 'tail', 'range'])
396
+ .optional()
397
+ .describe('Mode'),
374
398
  head: z.number().optional().describe('Head lines'),
399
+ tail: z.number().optional().describe('Tail lines'),
375
400
  startLine: z.number().optional().describe('Start line'),
376
401
  endLine: z.number().optional().describe('End line'),
377
402
  linesRead: z.number().optional().describe('Lines read'),
@@ -380,12 +405,13 @@ const ReadResultSchema = z.strictObject({
380
405
  export const ReadFileOutputSchema = ReadResultSchema.extend({
381
406
  ok: z.boolean(),
382
407
  path: z.string().optional(),
408
+ contentHash: z.string().optional().describe('SHA-256 of full file content'),
383
409
  error: ErrorSchema.optional(),
384
410
  });
385
411
  const ReadMultipleFileResultSchema = ReadResultSchema.extend({
386
412
  path: z.string().describe('File path'),
387
413
  truncationReason: z
388
- .enum(['head', 'range', 'externalized'])
414
+ .enum(['head', 'tail', 'range', 'externalized'])
389
415
  .optional()
390
416
  .describe('Why content was truncated'),
391
417
  maxTotalSize: z.number().optional().describe('Max total size budget'),
@@ -470,6 +496,7 @@ export const EditFileOutputSchema = z.strictObject({
470
496
  .array(z.string())
471
497
  .optional()
472
498
  .describe('Edits that could not be applied'),
499
+ diff: z.string().optional().describe('Unified diff of changes (dryRun)'),
473
500
  error: ErrorSchema.optional(),
474
501
  });
475
502
  export const MoveFileInputSchema = z
@@ -544,6 +571,9 @@ export const DiffFilesOutputSchema = z.strictObject({
544
571
  ok: z.boolean(),
545
572
  diff: z.string().optional().describe('Unified diff content'),
546
573
  isIdentical: z.boolean().optional().describe('True if files are identical'),
574
+ linesAdded: z.number().optional().describe('Lines added'),
575
+ linesRemoved: z.number().optional().describe('Lines removed'),
576
+ hunksCount: z.number().optional().describe('Number of diff hunks'),
547
577
  truncated: z.boolean().optional().describe('Diff content truncated?'),
548
578
  resourceUri: z.string().optional().describe('Full diff content URI'),
549
579
  error: ErrorSchema.optional(),
@@ -552,6 +582,10 @@ export const ApplyPatchInputSchema = z.strictObject({
552
582
  path: RequiredPathSchema.describe('Path to file to patch'),
553
583
  patch: z
554
584
  .string()
585
+ .min(1, 'Patch content required')
586
+ .refine((val) => /@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/u.test(val), {
587
+ error: 'Patch must include hunk headers (e.g., @@ -1,2 +1,2 @@)',
588
+ })
555
589
  .describe('Unified diff with @@ hunk headers. Generate with `diff_files`.'),
556
590
  fuzzFactor: z
557
591
  .int({ error: 'Must be integer' })
@@ -574,6 +608,20 @@ export const ApplyPatchOutputSchema = z.strictObject({
574
608
  ok: z.boolean(),
575
609
  path: z.string().optional(),
576
610
  applied: z.boolean().optional(),
611
+ hunksApplied: z.number().optional().describe('Hunks applied'),
612
+ linesAdded: z.number().optional().describe('Lines added'),
613
+ linesRemoved: z.number().optional().describe('Lines removed'),
614
+ results: z
615
+ .array(z.strictObject({
616
+ path: z.string().describe('File path'),
617
+ applied: z.boolean().describe('Patch applied successfully'),
618
+ hunksApplied: z.number().optional().describe('Hunks applied'),
619
+ linesAdded: z.number().optional().describe('Lines added'),
620
+ linesRemoved: z.number().optional().describe('Lines removed'),
621
+ error: z.string().optional().describe('Error message'),
622
+ }))
623
+ .optional()
624
+ .describe('Per-file results for multi-file patches'),
577
625
  error: ErrorSchema.optional(),
578
626
  });
579
627
  export const SearchAndReplaceInputSchema = z.strictObject({
@@ -582,16 +630,24 @@ export const SearchAndReplaceInputSchema = z.strictObject({
582
630
  .string()
583
631
  .min(1, 'Pattern required')
584
632
  .max(1000, 'Max 1000 chars')
633
+ .optional()
634
+ .default('**/*')
585
635
  .refine((val) => isSafeGlobPattern(val), {
586
636
  error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
587
637
  })
588
- .describe('Glob pattern (e.g. "**/*.ts")'),
638
+ .describe('Glob to filter files. Default: **/*'),
589
639
  searchPattern: z
590
640
  .string()
591
641
  .min(1, 'Search pattern required')
642
+ .max(1000, 'Max 1000 chars')
592
643
  .describe('Text to search for. Literal by default; RE2 regex when `isRegex=true`.'),
593
644
  replacement: z.string().describe('Replacement text'),
594
645
  isRegex: defaultFalseBoolean('Treat searchPattern as RE2 regex. Supports capture groups ($1, $2) in replacement.'),
646
+ caseSensitive: z
647
+ .boolean()
648
+ .optional()
649
+ .default(true)
650
+ .describe('Case-sensitive matching. Default: true.'),
595
651
  dryRun: defaultFalseBoolean('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
596
652
  includeHidden: z
597
653
  .boolean()
@@ -605,6 +661,12 @@ export const SearchAndReplaceInputSchema = z.strictObject({
605
661
  .boolean()
606
662
  .optional()
607
663
  .describe('Return unified diff of changes even if dryRun is false. Default: false.'),
664
+ maxFiles: z
665
+ .int({ error: 'Must be integer' })
666
+ .min(1, 'Min: 1')
667
+ .max(10000, 'Max: 10,000')
668
+ .optional()
669
+ .describe('Max files to process before stopping'),
608
670
  });
609
671
  export const SearchAndReplaceOutputSchema = z.strictObject({
610
672
  ok: z.boolean(),
@@ -631,6 +693,14 @@ export const SearchAndReplaceOutputSchema = z.strictObject({
631
693
  .optional()
632
694
  .describe('Changed file list truncated'),
633
695
  diff: z.string().optional().describe('Unified diff of changes (dryRun only)'),
696
+ diffTruncated: z
697
+ .boolean()
698
+ .optional()
699
+ .describe('Diff was truncated to fit size limit'),
700
+ stoppedReason: z
701
+ .enum(['maxFiles'])
702
+ .optional()
703
+ .describe('Why processing stopped early'),
634
704
  dryRun: z.boolean().optional(),
635
705
  error: ErrorSchema.optional(),
636
706
  });
@@ -13,7 +13,7 @@ import { createInMemoryResourceStore } from '../lib/resource-store.js';
13
13
  import { isRecord } from '../lib/utils.js';
14
14
  import { registerCompletions } from '../completions.js';
15
15
  import { pkgInfo } from '../pkg-info.js';
16
- import { registerGetHelpPrompt } from '../prompts.js';
16
+ import { registerAnalyzePathPrompt, registerCompareFilesPrompt, registerGetHelpPrompt, } from '../prompts.js';
17
17
  import { registerInstructionResource, registerMetricsResource, registerResultResources, registerToolCatalogResource, registerToolInfoResource, registerWorkflowGuideResource, } from '../resources.js';
18
18
  import { buildServerInstructions } from '../resources/generated-instructions.js';
19
19
  import { registerAllTools } from '../tools.js';
@@ -52,9 +52,7 @@ export function buildServerCapabilities(options = {}) {
52
52
  if (options.enableTaskToolRequests) {
53
53
  // NOTE: enabling task tool requests requires the caller to configure
54
54
  // an InMemoryTaskStore and InMemoryTaskMessageQueue on the McpServer.
55
- // InMemoryTaskStore accumulates completed task records with no TTL eviction —
56
- // suitable for short-lived stdio sessions. Long-running HTTP servers should
57
- // replace it with a TTL-evicting store to avoid unbounded memory growth.
55
+ // InMemoryTaskStore auto-evicts tasks after TTL via setTimeout.
58
56
  capabilities.tasks = {
59
57
  list: {},
60
58
  cancel: {},
@@ -145,7 +143,7 @@ export async function createServer(options = {}) {
145
143
  }),
146
144
  };
147
145
  if (taskToolSupport) {
148
- // Enabling task tool support requires configuring a task store and message queue on the server config. We use in-memory implementations which are suitable for short-lived stdio sessions. Long-running HTTP servers should replace these with TTL-evicting implementations to avoid unbounded memory growth.
146
+ // 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.
149
147
  serverConfig.taskStore = new InMemoryTaskStore();
150
148
  serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
151
149
  }
@@ -174,6 +172,8 @@ export async function createServer(options = {}) {
174
172
  registerWorkflowGuideResource(server, localIcon);
175
173
  registerToolInfoResource(server, localIcon);
176
174
  registerGetHelpPrompt(server, serverInstructions, localIcon);
175
+ registerCompareFilesPrompt(server, localIcon);
176
+ registerAnalyzePathPrompt(server, localIcon);
177
177
  registerResultResources(server, resourceStore, localIcon);
178
178
  registerMetricsResource(server, localIcon);
179
179
  registerCompletions(server, serverInstructions);
@@ -250,7 +250,11 @@ async function createHttpSession(options, sessions) {
250
250
  const transport = new StreamableHTTPServerTransport({
251
251
  sessionIdGenerator: () => randomUUID(),
252
252
  onsessioninitialized: (sessionId) => {
253
- sessions.set(sessionId, { server: mcpServer, rootsManager, transport });
253
+ sessions.set(sessionId, {
254
+ server: mcpServer,
255
+ rootsManager,
256
+ transport,
257
+ });
254
258
  rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
255
259
  },
256
260
  });
@@ -276,11 +280,57 @@ function sendJsonRpcError(res, status, code, message) {
276
280
  }));
277
281
  }
278
282
  const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/u;
283
+ const MAX_SESSION_ID_LENGTH = 256;
284
+ const MAX_BEARER_TOKEN_LENGTH = 4096;
285
+ const JSON_RPC_SERVER_ERROR = -32000;
286
+ const JSON_RPC_INVALID_REQUEST = -32600;
287
+ const JSON_RPC_PARSE_ERROR = -32700;
288
+ const JSON_RPC_INTERNAL_ERROR = -32603;
279
289
  function isAllowedOrigin(origin) {
280
290
  if (origin === undefined)
281
291
  return true; // Non-browser clients omit Origin.
282
292
  return LOCALHOST_ORIGIN_RE.test(origin);
283
293
  }
294
+ function getSessionId(req) {
295
+ const rawSessionId = req.headers['mcp-session-id'];
296
+ return typeof rawSessionId === 'string' &&
297
+ rawSessionId.length <= MAX_SESSION_ID_LENGTH
298
+ ? rawSessionId
299
+ : undefined;
300
+ }
301
+ function isAuthorizedBearer(apiKey, authHeader) {
302
+ const bearerPrefix = 'Bearer ';
303
+ if (typeof authHeader !== 'string' || !authHeader.startsWith(bearerPrefix)) {
304
+ return false;
305
+ }
306
+ const userKey = authHeader.slice(bearerPrefix.length);
307
+ if (userKey.length > MAX_BEARER_TOKEN_LENGTH) {
308
+ return false;
309
+ }
310
+ const expectedHash = createHash('sha256').update(apiKey).digest();
311
+ const actualHash = createHash('sha256').update(userKey).digest();
312
+ return timingSafeEqual(expectedHash, actualHash);
313
+ }
314
+ function writeUnauthorizedResponse(res) {
315
+ res.writeHead(401, {
316
+ 'Content-Type': 'application/json',
317
+ 'WWW-Authenticate': 'Bearer',
318
+ });
319
+ res.end(JSON.stringify({
320
+ jsonrpc: '2.0',
321
+ error: { code: JSON_RPC_SERVER_ERROR, message: 'Unauthorized' },
322
+ id: null,
323
+ }));
324
+ }
325
+ function ensureAuthorizedRequest(req, res) {
326
+ const apiKey = process.env['FILESYSTEM_MCP_API_KEY'];
327
+ if (!apiKey)
328
+ return true;
329
+ if (isAuthorizedBearer(apiKey, req.headers['authorization']))
330
+ return true;
331
+ writeUnauthorizedResponse(res);
332
+ return false;
333
+ }
284
334
  function discardRequestBody(req) {
285
335
  req.on('error', () => {
286
336
  // Best effort drain to avoid corrupting keep-alive pipelines.
@@ -290,6 +340,14 @@ function discardRequestBody(req) {
290
340
  async function handleSessionTransportRequest(session, req, res, body) {
291
341
  await withAllowedDirectoriesState(session.rootsManager.getAllowedDirectoriesState(), () => session.transport.handleRequest(req, res, body));
292
342
  }
343
+ function getSessionOrRespondNotFound(sessions, sessionId, res) {
344
+ const session = sessions.get(sessionId);
345
+ if (!session) {
346
+ sendJsonRpcError(res, 404, JSON_RPC_SERVER_ERROR, 'Session not found');
347
+ return undefined;
348
+ }
349
+ return session;
350
+ }
293
351
  function isLoopbackHttpHost(host) {
294
352
  const normalized = host.trim().toLowerCase();
295
353
  return (normalized === '127.0.0.1' ||
@@ -310,92 +368,49 @@ export async function startHttpServer(port, options) {
310
368
  assertHttpBindingSecurity(httpHost);
311
369
  async function handleMcpRequest(req, res) {
312
370
  const { method } = req;
313
- const MAX_SESSION_ID_LENGTH = 256;
314
- const rawSessionId = req.headers['mcp-session-id'];
315
- const sessionId = typeof rawSessionId === 'string' &&
316
- rawSessionId.length <= MAX_SESSION_ID_LENGTH
317
- ? rawSessionId
318
- : undefined;
371
+ const sessionId = getSessionId(req);
319
372
  const { origin } = req.headers;
320
373
  if (!isAllowedOrigin(origin)) {
321
- sendJsonRpcError(res, 403, -32000, 'Forbidden: disallowed origin');
374
+ sendJsonRpcError(res, 403, JSON_RPC_SERVER_ERROR, 'Forbidden: disallowed origin');
322
375
  return;
323
376
  }
324
- const apiKey = process.env['FILESYSTEM_MCP_API_KEY'];
325
- if (apiKey) {
326
- const authHeader = req.headers['authorization'];
327
- const bearerPrefix = 'Bearer ';
328
- let authorized = false;
329
- if (typeof authHeader === 'string' &&
330
- authHeader.startsWith(bearerPrefix)) {
331
- const userKey = authHeader.slice(bearerPrefix.length);
332
- if (userKey.length <= 4096) {
333
- const expectedHash = createHash('sha256').update(apiKey).digest();
334
- const actualHash = createHash('sha256').update(userKey).digest();
335
- authorized = timingSafeEqual(expectedHash, actualHash);
336
- }
337
- }
338
- if (!authorized) {
339
- res.writeHead(401, {
340
- 'Content-Type': 'application/json',
341
- 'WWW-Authenticate': 'Bearer',
342
- });
343
- res.end(JSON.stringify({
344
- jsonrpc: '2.0',
345
- error: { code: -32000, message: 'Unauthorized' },
346
- id: null,
347
- }));
348
- return;
349
- }
350
- }
377
+ if (!ensureAuthorizedRequest(req, res))
378
+ return;
351
379
  try {
352
380
  if (method === 'POST') {
353
381
  if (sessionId) {
354
- if (!sessions.has(sessionId)) {
355
- sendJsonRpcError(res, 404, -32000, 'Session not found');
382
+ const session = getSessionOrRespondNotFound(sessions, sessionId, res);
383
+ if (!session) {
356
384
  discardRequestBody(req);
357
385
  return;
358
386
  }
359
387
  const body = await readRequestBody(req);
360
- const session = sessions.get(sessionId);
361
- if (session) {
362
- await handleSessionTransportRequest(session, req, res, body);
363
- }
364
- else {
365
- sendJsonRpcError(res, 404, -32000, 'Session not found');
366
- }
388
+ await handleSessionTransportRequest(session, req, res, body);
367
389
  return;
368
390
  }
369
391
  const body = await readRequestBody(req);
370
392
  if (isInitializeRequest(body)) {
371
393
  const maxSessions = parseEnvInt('FILESYSTEM_MCP_MAX_HTTP_SESSIONS', 100, 1, 10_000);
372
394
  if (sessions.size >= maxSessions) {
373
- sendJsonRpcError(res, 503, -32000, 'Too many sessions');
395
+ sendJsonRpcError(res, 503, JSON_RPC_SERVER_ERROR, 'Too many sessions');
374
396
  return;
375
397
  }
376
398
  const session = await createHttpSession(options, sessions);
377
399
  await handleSessionTransportRequest(session, req, res, body);
378
400
  return;
379
401
  }
380
- sendJsonRpcError(res, 400, -32000, 'Bad Request: No valid session ID provided');
402
+ sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: No valid session ID provided');
381
403
  discardRequestBody(req);
382
404
  }
383
405
  else if (method === 'GET' || method === 'DELETE') {
384
406
  if (!sessionId) {
385
- sendJsonRpcError(res, 400, -32000, 'Bad Request: Missing session ID');
407
+ sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: Missing session ID');
386
408
  return;
387
409
  }
388
- if (!sessions.has(sessionId)) {
389
- sendJsonRpcError(res, 404, -32000, 'Session not found');
410
+ const session = getSessionOrRespondNotFound(sessions, sessionId, res);
411
+ if (!session)
390
412
  return;
391
- }
392
- const session = sessions.get(sessionId);
393
- if (session) {
394
- await handleSessionTransportRequest(session, req, res);
395
- }
396
- else {
397
- sendJsonRpcError(res, 404, -32000, 'Session not found');
398
- }
413
+ await handleSessionTransportRequest(session, req, res);
399
414
  }
400
415
  else {
401
416
  res.writeHead(405, {
@@ -404,21 +419,26 @@ export async function startHttpServer(port, options) {
404
419
  });
405
420
  res.end(JSON.stringify({
406
421
  jsonrpc: '2.0',
407
- error: { code: -32000, message: 'Method Not Allowed' },
422
+ error: {
423
+ code: JSON_RPC_SERVER_ERROR,
424
+ message: 'Method Not Allowed',
425
+ },
408
426
  id: null,
409
427
  }));
410
428
  }
411
429
  }
412
430
  catch (error) {
413
431
  if (error instanceof RequestBodyError && !res.headersSent) {
414
- const rpcCode = error.statusCode === 413 ? -32600 : -32700;
432
+ const rpcCode = error.statusCode === 413
433
+ ? JSON_RPC_INVALID_REQUEST
434
+ : JSON_RPC_PARSE_ERROR;
415
435
  res.setHeader('Connection', 'close');
416
436
  sendJsonRpcError(res, error.statusCode, rpcCode, error.message);
417
437
  return;
418
438
  }
419
439
  console.error('[HTTP] Error handling request:', formatUnknownErrorMessage(error));
420
440
  if (!res.headersSent) {
421
- sendJsonRpcError(res, 500, -32603, 'Internal Server Error');
441
+ sendJsonRpcError(res, 500, JSON_RPC_INTERNAL_ERROR, 'Internal Server Error');
422
442
  }
423
443
  }
424
444
  }