@j0hanz/filesystem-mcp 1.11.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,66 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
1
53
  import * as fsp from 'node:fs/promises';
2
54
  import * as path from 'node:path';
3
55
  import { isUtf8 } from 'node:buffer';
4
56
  import { randomUUID } from 'node:crypto';
5
- import { Writable } from 'node:stream';
6
- import { pipeline } from 'node:stream/promises';
7
57
  import { BINARY_CHECK_BUFFER_SIZE, KNOWN_BINARY_EXTENSIONS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from './constants.js';
8
58
  import { ErrorCode, McpError, normalizeUnknownError } from './errors.js';
9
59
  import { assertAllowedFileAccess, validateExistingPath } from './paths.js';
10
60
  function createAbortError(message = 'Operation aborted') {
11
61
  return new DOMException(message, 'AbortError');
12
62
  }
63
+ const READ_ONLY_FILE_FLAG = 'r';
13
64
  const SHARED_NOOP_SIGNAL = new AbortController().signal;
14
65
  function normalizeAbortReason(reason, message) {
15
66
  if (reason instanceof Error)
@@ -55,39 +106,22 @@ function getAbortError(signal, message) {
55
106
  export function withAbort(promise, signal) {
56
107
  if (!signal)
57
108
  return promise;
58
- assertNotAborted(signal);
109
+ signal.throwIfAborted();
59
110
  return new Promise((resolve, reject) => {
60
- let settled = false;
61
- const finish = (run) => {
62
- if (settled)
63
- return;
64
- settled = true;
65
- signal.removeEventListener('abort', onAbort);
66
- run();
67
- };
68
111
  const onAbort = () => {
69
- finish(() => {
70
- reject(getAbortError(signal));
71
- });
112
+ reject(getAbortError(signal));
72
113
  };
73
- signal.addEventListener('abort', onAbort, { once: true });
74
- try {
75
- signal.throwIfAborted();
76
- }
77
- catch {
114
+ if (signal.aborted) {
78
115
  onAbort();
79
116
  return;
80
117
  }
81
- promise
82
- .then((value) => {
83
- finish(() => {
84
- resolve(value);
85
- });
86
- })
87
- .catch((error) => {
88
- finish(() => {
89
- reject(normalizeUnknownError(error));
90
- });
118
+ signal.addEventListener('abort', onAbort, { once: true });
119
+ promise.then((value) => {
120
+ signal.removeEventListener('abort', onAbort);
121
+ resolve(value);
122
+ }, (error) => {
123
+ signal.removeEventListener('abort', onAbort);
124
+ reject(normalizeUnknownError(error));
91
125
  });
92
126
  });
93
127
  }
@@ -102,12 +136,12 @@ export function createTimedAbortSignal(baseSignal, timeoutMs) {
102
136
  };
103
137
  }
104
138
  if (baseSignal) {
105
- return createForwardedSignal(baseSignal);
139
+ return { signal: baseSignal, cleanup: () => { } };
106
140
  }
107
141
  if (timeoutSignal) {
108
142
  return { signal: timeoutSignal, cleanup: () => { } };
109
143
  }
110
- return createNoopSignal();
144
+ return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
111
145
  }
112
146
  export async function withTimedAbortSignal(baseSignal, timeoutMs, run) {
113
147
  const { signal, cleanup } = createTimedAbortSignal(baseSignal, timeoutMs);
@@ -118,12 +152,6 @@ export async function withTimedAbortSignal(baseSignal, timeoutMs, run) {
118
152
  cleanup();
119
153
  }
120
154
  }
121
- function createNoopSignal() {
122
- return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
123
- }
124
- function createForwardedSignal(baseSignal) {
125
- return { signal: baseSignal, cleanup: () => { } };
126
- }
127
155
  function createParallelAbortError() {
128
156
  return createAbortError();
129
157
  }
@@ -193,20 +221,8 @@ function hasKnownBinaryExtension(filePath) {
193
221
  const ext = path.extname(filePath).toLowerCase();
194
222
  return KNOWN_BINARY_EXTENSIONS.has(ext);
195
223
  }
196
- async function withFileHandle(filePath, fn, existingHandle, signal) {
197
- if (existingHandle) {
198
- return fn(existingHandle);
199
- }
200
- const effectivePath = await validateExistingPath(filePath, signal);
201
- const handle = await withAbort(fsp.open(effectivePath, 'r'), signal);
202
- try {
203
- return await fn(handle);
204
- }
205
- finally {
206
- await handle.close().catch((error) => {
207
- console.error('Failed to close file handle:', error);
208
- });
209
- }
224
+ async function openReadableFileHandle(filePath, signal) {
225
+ return withAbort(fsp.open(filePath, READ_ONLY_FILE_FLAG), signal);
210
226
  }
211
227
  async function readProbe(handle, signal) {
212
228
  const buffer = Buffer.alloc(BINARY_CHECK_BUFFER_SIZE);
@@ -222,13 +238,29 @@ function hasUtf16Bom(slice) {
222
238
  (slice[0] === 0xfe && slice[1] === 0xff)));
223
239
  }
224
240
  export async function isProbablyBinary(filePath, existingHandle, signal) {
225
- if (hasKnownBinaryExtension(filePath)) {
226
- return true;
227
- }
228
- return withFileHandle(filePath, async (handle) => {
241
+ const env_1 = { stack: [], error: void 0, hasError: false };
242
+ try {
243
+ if (hasKnownBinaryExtension(filePath)) {
244
+ return true;
245
+ }
246
+ if (existingHandle) {
247
+ const slice = await readProbe(existingHandle, signal);
248
+ return isBinarySlice(slice);
249
+ }
250
+ const effectivePath = await validateExistingPath(filePath, signal);
251
+ const handle = __addDisposableResource(env_1, await openReadableFileHandle(effectivePath, signal), true);
229
252
  const slice = await readProbe(handle, signal);
230
253
  return isBinarySlice(slice);
231
- }, existingHandle, signal);
254
+ }
255
+ catch (e_1) {
256
+ env_1.error = e_1;
257
+ env_1.hasError = true;
258
+ }
259
+ finally {
260
+ const result_1 = __disposeResources(env_1);
261
+ if (result_1)
262
+ await result_1;
263
+ }
232
264
  }
233
265
  function isBinarySlice(slice) {
234
266
  if (slice.length === 0)
@@ -316,42 +348,35 @@ const STREAM_CHUNK_SIZE = 64 * 1024;
316
348
  function createTooLargeError(bytesRead, maxSize, requestedPath) {
317
349
  return new McpError(ErrorCode.E_TOO_LARGE, `File exceeds maximum size (${bytesRead} > ${maxSize}): ${requestedPath}`, requestedPath, { size: bytesRead, maxSize });
318
350
  }
319
- class BufferCollector extends Writable {
320
- #chunks = [];
321
- #totalSize = 0;
322
- #maxSize;
323
- #requestedPath;
324
- constructor(maxSize, requestedPath) {
325
- super({ autoDestroy: true });
326
- this.#maxSize = maxSize;
327
- this.#requestedPath = requestedPath;
328
- }
329
- _write(chunk, _encoding, callback) {
330
- const buffer = Buffer.isBuffer(chunk)
331
- ? chunk
332
- : Buffer.from(chunk, _encoding);
333
- this.#totalSize += buffer.length;
334
- if (this.#totalSize > this.#maxSize) {
335
- callback(createTooLargeError(this.#totalSize, this.#maxSize, this.#requestedPath));
336
- return;
337
- }
338
- this.#chunks.push(buffer);
339
- callback();
340
- }
341
- getResult() {
342
- return Buffer.concat(this.#chunks, this.#totalSize);
343
- }
344
- }
345
351
  async function readFileBufferWithLimit(handle, maxSize, requestedPath, signal) {
346
352
  const stream = handle.createReadStream({
347
353
  start: 0,
348
354
  highWaterMark: STREAM_CHUNK_SIZE,
349
355
  autoClose: false,
350
356
  emitClose: false,
357
+ signal,
351
358
  });
352
- const collector = new BufferCollector(maxSize, requestedPath);
353
- await pipeline(stream, collector, { signal });
354
- return collector.getResult();
359
+ const chunks = [];
360
+ let totalSize = 0;
361
+ try {
362
+ for await (const chunk of stream) {
363
+ const buffer = Buffer.isBuffer(chunk)
364
+ ? chunk
365
+ : Buffer.from(chunk);
366
+ totalSize += buffer.length;
367
+ if (totalSize > maxSize) {
368
+ stream.destroy();
369
+ throw createTooLargeError(totalSize, maxSize, requestedPath);
370
+ }
371
+ chunks.push(buffer);
372
+ }
373
+ }
374
+ finally {
375
+ if (!stream.destroyed) {
376
+ stream.destroy();
377
+ }
378
+ }
379
+ return Buffer.concat(chunks, totalSize);
355
380
  }
356
381
  async function headFile(handle, numLines, encoding = 'utf-8', maxBytesRead, signal) {
357
382
  assertNotAborted(signal);
@@ -499,15 +524,24 @@ async function assertNotBinary(validPath, filePath, normalized) {
499
524
  return;
500
525
  throw new McpError(ErrorCode.E_INVALID_INPUT, `Binary file detected: ${filePath}. Refusing to read as text.`, filePath);
501
526
  }
502
- function requireHead(normalized, filePath) {
503
- if (normalized.head === undefined) {
504
- throw new McpError(ErrorCode.E_INVALID_INPUT, 'Missing head option', filePath);
527
+ function assertSizeWithinLimit(size, maxSize, filePath) {
528
+ if (size <= maxSize)
529
+ return;
530
+ throw new McpError(ErrorCode.E_TOO_LARGE, `File too large: ${size} bytes (max: ${maxSize} bytes). Use head parameter to preview the first N lines.`, filePath, { size, maxSize });
531
+ }
532
+ function requireReadOption(normalized, key, filePath) {
533
+ const value = normalized[key];
534
+ if (value !== undefined) {
535
+ return value;
505
536
  }
506
- return normalized.head;
537
+ throw new McpError(ErrorCode.E_INVALID_INPUT, `Missing ${key} option`, filePath);
507
538
  }
508
- function buildHeadResult(validPath, content, truncated, head, linesRead, hasMoreLines) {
539
+ async function executeHeadRead(context) {
540
+ const head = requireReadOption(context.normalized, 'head', context.filePath);
541
+ const readOptions = buildReadContentOptions(context.normalized);
542
+ const { content, truncated, linesRead, hasMoreLines } = await readHeadContent(context.handle, head, readOptions);
509
543
  return {
510
- path: validPath,
544
+ path: context.validPath,
511
545
  content,
512
546
  truncated,
513
547
  readMode: 'head',
@@ -516,24 +550,27 @@ function buildHeadResult(validPath, content, truncated, head, linesRead, hasMore
516
550
  hasMoreLines,
517
551
  };
518
552
  }
519
- function buildRangeResult(validPath, content, truncated, startLine, endLine, linesRead, hasMoreLines) {
520
- const result = {
521
- path: validPath,
553
+ async function executeRangeRead(context) {
554
+ const startLine = requireReadOption(context.normalized, 'startLine', context.filePath);
555
+ const { endLine } = context.normalized;
556
+ const readOptions = buildReadContentOptions(context.normalized);
557
+ const { content, truncated, linesRead, hasMoreLines } = await readRangeContent(context.handle, startLine, endLine, readOptions);
558
+ return {
559
+ path: context.validPath,
522
560
  content,
523
561
  truncated,
524
562
  readMode: 'range',
525
563
  startLine,
564
+ ...(endLine !== undefined ? { endLine } : {}),
526
565
  linesRead,
527
566
  hasMoreLines,
528
567
  };
529
- if (endLine !== undefined) {
530
- result.endLine = endLine;
531
- }
532
- return result;
533
568
  }
534
- function buildFullResult(validPath, content, totalLines) {
569
+ async function executeFullRead(context) {
570
+ assertSizeWithinLimit(context.stats.size, context.normalized.maxSize, context.filePath);
571
+ const { content, totalLines } = await readFullContent(context.handle, context.normalized.encoding, context.normalized.maxSize, context.filePath, context.normalized.signal);
535
572
  return {
536
- path: validPath,
573
+ path: context.validPath,
537
574
  content,
538
575
  truncated: false,
539
576
  totalLines,
@@ -542,9 +579,12 @@ function buildFullResult(validPath, content, totalLines) {
542
579
  hasMoreLines: false,
543
580
  };
544
581
  }
545
- function buildTailResult(validPath, content, truncated, tail, linesRead, hasMoreLines) {
582
+ async function executeTailRead(context) {
583
+ const tail = requireReadOption(context.normalized, 'tail', context.filePath);
584
+ const readOptions = buildReadContentOptions(context.normalized);
585
+ const { content, truncated, linesRead, hasMoreLines } = await readTailContent(context.handle, tail, readOptions);
546
586
  return {
547
- path: validPath,
587
+ path: context.validPath,
548
588
  content,
549
589
  truncated,
550
590
  readMode: 'tail',
@@ -553,51 +593,15 @@ function buildTailResult(validPath, content, truncated, tail, linesRead, hasMore
553
593
  hasMoreLines,
554
594
  };
555
595
  }
556
- function assertSizeWithinLimit(size, maxSize, filePath) {
557
- if (size <= maxSize)
558
- return;
559
- throw new McpError(ErrorCode.E_TOO_LARGE, `File too large: ${size} bytes (max: ${maxSize} bytes). Use head parameter to preview the first N lines.`, filePath, { size, maxSize });
560
- }
561
- async function readHeadResult(handle, validPath, filePath, normalized) {
562
- const head = requireHead(normalized, filePath);
563
- const readOptions = buildReadContentOptions(normalized);
564
- const { content, truncated, linesRead, hasMoreLines } = await readHeadContent(handle, head, readOptions);
565
- return buildHeadResult(validPath, content, truncated, head, linesRead, hasMoreLines);
566
- }
567
- async function readRangeResult(handle, validPath, filePath, normalized) {
568
- const { startLine, endLine } = normalized;
569
- if (startLine === undefined) {
570
- throw new McpError(ErrorCode.E_INVALID_INPUT, 'Missing startLine option', filePath);
571
- }
572
- const readOptions = buildReadContentOptions(normalized);
573
- const { content, truncated, linesRead, hasMoreLines } = await readRangeContent(handle, startLine, endLine, readOptions);
574
- return buildRangeResult(validPath, content, truncated, startLine, endLine, linesRead, hasMoreLines);
575
- }
576
- async function readFullResult(handle, validPath, filePath, stats, normalized) {
577
- assertSizeWithinLimit(stats.size, normalized.maxSize, filePath);
578
- const { content, totalLines } = await readFullContent(handle, normalized.encoding, normalized.maxSize, filePath, normalized.signal);
579
- return buildFullResult(validPath, content, totalLines);
580
- }
581
- async function readTailResult(handle, validPath, filePath, normalized) {
582
- if (normalized.tail === undefined) {
583
- throw new McpError(ErrorCode.E_INVALID_INPUT, 'Missing tail option', filePath);
584
- }
585
- const readOptions = buildReadContentOptions(normalized);
586
- const { content, truncated, linesRead, hasMoreLines } = await readTailContent(handle, normalized.tail, readOptions);
587
- return buildTailResult(validPath, content, truncated, normalized.tail, linesRead, hasMoreLines);
588
- }
589
- async function readByMode(handle, validPath, filePath, stats, normalized) {
590
- const mode = resolveReadMode(normalized);
591
- if (mode === 'head') {
592
- return readHeadResult(handle, validPath, filePath, normalized);
593
- }
594
- if (mode === 'tail') {
595
- return readTailResult(handle, validPath, filePath, normalized);
596
- }
597
- if (mode === 'range') {
598
- return readRangeResult(handle, validPath, filePath, normalized);
599
- }
600
- return readFullResult(handle, validPath, filePath, stats, normalized);
596
+ const READ_MODE_HANDLERS = {
597
+ head: executeHeadRead,
598
+ range: executeRangeRead,
599
+ full: executeFullRead,
600
+ tail: executeTailRead,
601
+ };
602
+ async function readByMode(context) {
603
+ const mode = resolveReadMode(context.normalized);
604
+ return READ_MODE_HANDLERS[mode](context);
601
605
  }
602
606
  function assertFileStats(filePath, stats) {
603
607
  if (!stats.isFile()) {
@@ -605,19 +609,32 @@ function assertFileStats(filePath, stats) {
605
609
  }
606
610
  }
607
611
  async function readFileWithStatsInternal(filePath, validPath, stats, normalized) {
608
- assertNotAborted(normalized.signal);
609
- assertAllowedFileAccess(filePath, validPath);
610
- assertFileStats(filePath, stats);
611
- if (normalized.skipBinary) {
612
- await assertNotBinary(validPath, filePath, normalized);
613
- }
614
- assertNotAborted(normalized.signal);
615
- const handle = await withAbort(fsp.open(validPath, 'r'), normalized.signal);
612
+ const env_2 = { stack: [], error: void 0, hasError: false };
616
613
  try {
617
- return await readByMode(handle, validPath, filePath, stats, normalized);
614
+ assertNotAborted(normalized.signal);
615
+ assertAllowedFileAccess(filePath, validPath);
616
+ assertFileStats(filePath, stats);
617
+ if (normalized.skipBinary) {
618
+ await assertNotBinary(validPath, filePath, normalized);
619
+ }
620
+ assertNotAborted(normalized.signal);
621
+ const handle = __addDisposableResource(env_2, await openReadableFileHandle(validPath, normalized.signal), true);
622
+ return await readByMode({
623
+ handle,
624
+ validPath,
625
+ filePath,
626
+ stats,
627
+ normalized,
628
+ });
629
+ }
630
+ catch (e_2) {
631
+ env_2.error = e_2;
632
+ env_2.hasError = true;
618
633
  }
619
634
  finally {
620
- await handle.close();
635
+ const result_2 = __disposeResources(env_2);
636
+ if (result_2)
637
+ await result_2;
621
638
  }
622
639
  }
623
640
  export async function readFileWithStats(filePath, validPath, stats, options = {}) {
@@ -636,7 +653,7 @@ export async function atomicWriteFile(filePath, content, options = {}) {
636
653
  const tempPath = `${filePath}.${randomUUID()}.tmp`;
637
654
  try {
638
655
  assertNotAborted(signal);
639
- await withAbort(fsp.writeFile(tempPath, content, { encoding, signal }), signal);
656
+ await fsp.writeFile(tempPath, content, { encoding, signal });
640
657
  await withAbort(fsp.rename(tempPath, filePath), signal);
641
658
  }
642
659
  catch (error) {
@@ -91,8 +91,8 @@ function formatTaskSupportLabel(taskSupport) {
91
91
  function formatAnnotationValue(value) {
92
92
  return value ? 'true' : 'false';
93
93
  }
94
- function toJsonSchemaObject(schema) {
95
- return z.toJSONSchema(schema);
94
+ function toJsonSchemaObject(schema, io = 'output') {
95
+ return z.toJSONSchema(schema, { io });
96
96
  }
97
97
  function summarizeSchemaType(schema) {
98
98
  if (Array.isArray(schema.enum) && schema.enum.length > 0) {
@@ -119,27 +119,31 @@ function buildSchemaFieldLines(label, schema) {
119
119
  if (!schema) {
120
120
  return [`<${label}>`, '- none', `</${label}>`];
121
121
  }
122
- const jsonSchema = toJsonSchemaObject(schema);
122
+ const io = label === 'input_fields' ? 'input' : 'output';
123
+ const jsonSchema = toJsonSchemaObject(schema, io);
123
124
  const properties = jsonSchema.properties ?? {};
124
125
  const required = new Set(jsonSchema.required ?? []);
125
126
  const fieldNames = Object.keys(properties);
126
- if (fieldNames.length === 0) {
127
- return [`<${label}>`, '- object with no fields', `</${label}>`];
127
+ const lines = [`<${label}>`];
128
+ if (typeof jsonSchema.description === 'string' &&
129
+ jsonSchema.description.length > 0) {
130
+ lines.push(`- Schema constraints: ${jsonSchema.description}`);
128
131
  }
129
- return [
130
- `<${label}>`,
131
- ...fieldNames.map((fieldName) => {
132
- const fieldSchema = properties[fieldName] ?? {};
133
- const type = summarizeSchemaType(fieldSchema);
134
- const requiredLabel = required.has(fieldName) ? 'required' : 'optional';
135
- const description = typeof fieldSchema.description === 'string' &&
136
- fieldSchema.description.length > 0
137
- ? fieldSchema.description
138
- : 'No description.';
139
- return `- ${fieldName} (${type}, ${requiredLabel}): ${description}`;
140
- }),
141
- `</${label}>`,
142
- ];
132
+ if (fieldNames.length === 0) {
133
+ lines.push('- object with no fields', `</${label}>`);
134
+ return lines;
135
+ }
136
+ lines.push(...fieldNames.map((fieldName) => {
137
+ const fieldSchema = properties[fieldName] ?? {};
138
+ const type = summarizeSchemaType(fieldSchema);
139
+ const requiredLabel = required.has(fieldName) ? 'required' : 'optional';
140
+ const description = typeof fieldSchema.description === 'string' &&
141
+ fieldSchema.description.length > 0
142
+ ? fieldSchema.description
143
+ : 'No description.';
144
+ return `- ${fieldName} (${type}, ${requiredLabel}): ${description}`;
145
+ }), `</${label}>`);
146
+ return lines;
143
147
  }
144
148
  function buildProtocolNotes(contract) {
145
149
  const notes = [
package/dist/schemas.d.ts CHANGED
@@ -63,8 +63,8 @@ export declare const SearchFilesInputSchema: z.ZodObject<{
63
63
  sortBy: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
64
64
  name: "name";
65
65
  size: "size";
66
- modified: "modified";
67
66
  path: "path";
67
+ modified: "modified";
68
68
  }>>>;
69
69
  maxDepth: z.ZodOptional<z.ZodInt>;
70
70
  cursor: z.ZodOptional<z.ZodString>;
package/dist/schemas.js CHANGED
@@ -253,7 +253,8 @@ export const ReadFileInputSchema = z
253
253
  }),
254
254
  includeHash: defaultFalseBoolean('Include SHA-256 hash of full file content'),
255
255
  })
256
- .superRefine(validateReadRange);
256
+ .superRefine(validateReadRange)
257
+ .describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'. 'endLine' requires 'startLine'.");
257
258
  export const ReadMultipleFilesInputSchema = z
258
259
  .strictObject({
259
260
  paths: z
@@ -268,7 +269,8 @@ export const ReadMultipleFilesInputSchema = z
268
269
  endLine: 'End line (1-based, inclusive) per file. Requires startLine.',
269
270
  }),
270
271
  })
271
- .superRefine(validateReadRange);
272
+ .superRefine(validateReadRange)
273
+ .describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'. 'endLine' requires 'startLine'.");
272
274
  export const GetFileInfoInputSchema = z.strictObject({
273
275
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
274
276
  });
@@ -451,7 +453,8 @@ export const CreateDirectoryInputSchema = z
451
453
  .refine((data) => data.path !== undefined || data.paths !== undefined, {
452
454
  error: "Either 'path' or 'paths' must be provided",
453
455
  path: ['path'],
454
- });
456
+ })
457
+ .describe("Provide either 'path' or 'paths'.");
455
458
  export const CreateDirectoryOutputSchema = z.strictObject({
456
459
  ok: z.boolean(),
457
460
  path: z.string().optional(),
@@ -510,7 +513,8 @@ export const MoveFileInputSchema = z
510
513
  .refine((data) => (data.source ?? data.sources) !== undefined, {
511
514
  error: "Either 'source' or 'sources' must be provided",
512
515
  path: ['source'],
513
- });
516
+ })
517
+ .describe("Provide either 'source' or 'sources'.");
514
518
  export const MoveFileOutputSchema = z.strictObject({
515
519
  ok: z.boolean(),
516
520
  source: z.string().optional(),
@@ -5,7 +5,7 @@ import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextproto
5
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
7
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
8
- import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
+ import { isInitializeRequest, LATEST_PROTOCOL_VERSION, SetLevelRequestSchema, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
9
9
  import { DEFAULT_LOG_LEVEL, parseEnvInt } from '../lib/constants.js';
10
10
  import { formatUnknownErrorMessage } from '../lib/errors.js';
11
11
  import { withAllowedDirectoriesState } from '../lib/paths.js';
@@ -243,7 +243,7 @@ async function readRequestBody(req) {
243
243
  req.on('error', reject);
244
244
  });
245
245
  }
246
- async function createHttpSession(options, sessions) {
246
+ async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
247
247
  const mcpServer = await createServer(options);
248
248
  const rootsManager = getRootsManager(mcpServer);
249
249
  rootsManager.registerHandlers(mcpServer);
@@ -255,6 +255,7 @@ async function createHttpSession(options, sessions) {
255
255
  server: mcpServer,
256
256
  rootsManager,
257
257
  transport,
258
+ negotiatedProtocolVersion,
258
259
  });
259
260
  rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
260
261
  },
@@ -270,7 +271,12 @@ async function createHttpSession(options, sessions) {
270
271
  });
271
272
  };
272
273
  await mcpServer.connect(transport);
273
- return { server: mcpServer, rootsManager, transport };
274
+ return {
275
+ server: mcpServer,
276
+ rootsManager,
277
+ transport,
278
+ negotiatedProtocolVersion,
279
+ };
274
280
  }
275
281
  function sendJsonRpcError(res, status, code, message) {
276
282
  res.writeHead(status, { 'Content-Type': 'application/json' });
@@ -299,6 +305,29 @@ function getSessionId(req) {
299
305
  ? rawSessionId
300
306
  : undefined;
301
307
  }
308
+ function getProtocolVersionHeader(req) {
309
+ const rawProtocolVersion = req.headers['mcp-protocol-version'];
310
+ return typeof rawProtocolVersion === 'string'
311
+ ? rawProtocolVersion
312
+ : undefined;
313
+ }
314
+ function resolveNegotiatedProtocolVersion(requestedVersion) {
315
+ return SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
316
+ ? requestedVersion
317
+ : LATEST_PROTOCOL_VERSION;
318
+ }
319
+ function ensureSessionProtocolVersion(req, res, session) {
320
+ const protocolVersion = getProtocolVersionHeader(req);
321
+ if (!protocolVersion) {
322
+ sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: Missing MCP-Protocol-Version header');
323
+ return false;
324
+ }
325
+ if (protocolVersion !== session.negotiatedProtocolVersion) {
326
+ sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, `Bad Request: MCP-Protocol-Version must match negotiated version ${session.negotiatedProtocolVersion}`);
327
+ return false;
328
+ }
329
+ return true;
330
+ }
302
331
  function isAuthorizedBearer(apiKey, authHeader) {
303
332
  const bearerPrefix = 'Bearer ';
304
333
  if (typeof authHeader !== 'string' || !authHeader.startsWith(bearerPrefix)) {
@@ -385,6 +414,10 @@ export async function startHttpServer(port, options) {
385
414
  discardRequestBody(req);
386
415
  return;
387
416
  }
417
+ if (!ensureSessionProtocolVersion(req, res, session)) {
418
+ discardRequestBody(req);
419
+ return;
420
+ }
388
421
  const body = await readRequestBody(req);
389
422
  await handleSessionTransportRequest(session, req, res, body);
390
423
  return;
@@ -396,7 +429,7 @@ export async function startHttpServer(port, options) {
396
429
  sendJsonRpcError(res, 503, JSON_RPC_SERVER_ERROR, 'Too many sessions');
397
430
  return;
398
431
  }
399
- const session = await createHttpSession(options, sessions);
432
+ const session = await createHttpSession(options, sessions, resolveNegotiatedProtocolVersion(body.params.protocolVersion));
400
433
  await handleSessionTransportRequest(session, req, res, body);
401
434
  return;
402
435
  }
@@ -411,6 +444,8 @@ export async function startHttpServer(port, options) {
411
444
  const session = getSessionOrRespondNotFound(sessions, sessionId, res);
412
445
  if (!session)
413
446
  return;
447
+ if (!ensureSessionProtocolVersion(req, res, session))
448
+ return;
414
449
  await handleSessionTransportRequest(session, req, res);
415
450
  }
416
451
  else {