@j0hanz/filesystem-mcp 2.1.0 → 2.1.2

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 (41) hide show
  1. package/README.md +27 -86
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +6 -31
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/fmt.d.ts +1 -1
  6. package/dist/core/fmt.d.ts.map +1 -1
  7. package/dist/core/fmt.js +0 -5
  8. package/dist/core/fmt.js.map +1 -1
  9. package/dist/core/input-required.d.ts +0 -9
  10. package/dist/core/input-required.d.ts.map +1 -1
  11. package/dist/core/input-required.js +4 -26
  12. package/dist/core/input-required.js.map +1 -1
  13. package/dist/core/primitives.d.ts +0 -1
  14. package/dist/core/primitives.d.ts.map +1 -1
  15. package/dist/core/primitives.js +0 -3
  16. package/dist/core/primitives.js.map +1 -1
  17. package/dist/core/schema.d.ts +0 -1
  18. package/dist/core/schema.d.ts.map +1 -1
  19. package/dist/core/schema.js +1 -1
  20. package/dist/core/schema.js.map +1 -1
  21. package/dist/prompts.d.ts.map +1 -1
  22. package/dist/prompts.js +5 -11
  23. package/dist/prompts.js.map +1 -1
  24. package/dist/tools/define.d.ts.map +1 -1
  25. package/dist/tools/define.js +4 -5
  26. package/dist/tools/define.js.map +1 -1
  27. package/dist/tools/progress.d.ts +2 -7
  28. package/dist/tools/progress.d.ts.map +1 -1
  29. package/dist/tools/progress.js +10 -26
  30. package/dist/tools/progress.js.map +1 -1
  31. package/dist/transport/http.d.ts.map +1 -1
  32. package/dist/transport/http.js +12 -12
  33. package/dist/transport/http.js.map +1 -1
  34. package/dist/transport/shared.d.ts +0 -5
  35. package/dist/transport/shared.d.ts.map +1 -1
  36. package/dist/transport/shared.js +4 -0
  37. package/dist/transport/shared.js.map +1 -1
  38. package/dist/transport/stdio.d.ts.map +1 -1
  39. package/dist/transport/stdio.js +54 -8
  40. package/dist/transport/stdio.js.map +1 -1
  41. package/package.json +2 -3
@@ -1 +1 @@
1
- {"version":3,"file":"progress.js","sourceRoot":"","sources":["../../src/tools/progress.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Notification, ProgressNotificationParams } from '@modelcontextprotocol/server';\n\nimport { formatUnknownErrorMessage } from '../core/errors.js';\nimport { Logger } from '../core/observability.js';\n\nfunction reportDetachedError(toolName: string, context: string, error: unknown): void {\n const message = formatUnknownErrorMessage(error);\n Logger.emit('warning', `${toolName}: ${context} failed: ${message}`);\n}\n\nexport type ProgressEvent =\n | { kind: 'tick'; current: number; total?: number; message: string }\n | { kind: 'complete'; current: number; total?: number; message: string }\n | {\n kind: 'fail';\n current: number;\n total?: number;\n message: string;\n error: unknown;\n };\n\nexport interface ProgressSink {\n readonly name: string;\n readonly emit: (event: ProgressEvent) => Promise<void> | void;\n}\n\ninterface ProgressSessionOptions {\n label: string;\n total?: number;\n sinks: ProgressSink[];\n /** Override the rate limit window. Default: 50ms. */\n rateLimitMs?: number;\n}\n\nconst DEFAULT_RATE_LIMIT_MS = 50;\n\nexport class ProgressSession {\n readonly #label: string;\n readonly #total: number | undefined;\n readonly #sinks: ProgressSink[];\n readonly #rateLimitMs: number;\n readonly #startTime: number;\n\n #cursor = 0;\n #lastSentMs = 0;\n #done = false;\n\n constructor(opts: ProgressSessionOptions) {\n this.#label = opts.label;\n this.#total = opts.total;\n this.#sinks = opts.sinks;\n this.#rateLimitMs = opts.rateLimitMs ?? DEFAULT_RATE_LIMIT_MS;\n\n const now = Date.now();\n this.#startTime = now;\n this.#lastSentMs = now - this.#rateLimitMs;\n\n // Synthetic start tick — preserves today's \"fire 0/total at session creation\" wire behavior.\n this.#dispatch({\n kind: 'tick',\n current: 0,\n ...(this.#total !== undefined ? { total: this.#total } : {}),\n message: this.#label,\n });\n }\n\n set(input: { current: number; total?: number; message?: string }): void {\n if (this.#done) return;\n // The spec requires `progress` to increase on every notification, and the\n // constructor already dispatched current: 0. A repeated or backward tick\n // would put a duplicate value on the wire — drop it.\n if (input.current <= this.#cursor) return;\n this.#cursor = input.current;\n const total = input.total ?? this.#total;\n this.#dispatch({\n kind: 'tick',\n current: this.#cursor,\n ...(total !== undefined ? { total } : {}),\n message: input.message ?? this.#label,\n });\n }\n\n complete(message: string): void {\n if (this.#done) return;\n this.#done = true;\n this.#dispatch({\n kind: 'complete',\n current: this.#cursor,\n ...(this.#total !== undefined ? { total: this.#total } : {}),\n message,\n });\n }\n\n fail(error: unknown, message?: string): void {\n if (this.#done) return;\n this.#done = true;\n this.#dispatch({\n kind: 'fail',\n current: this.#cursor,\n ...(this.#total !== undefined ? { total: this.#total } : {}),\n message: message ?? this.#label,\n error,\n });\n }\n\n #dispatch(event: ProgressEvent): void {\n if (this.#shouldRateLimit(event)) {\n return;\n }\n\n this.#lastSentMs = Date.now();\n\n for (const sink of this.#sinks) {\n this.#emitGuarded(sink, event);\n }\n }\n\n #shouldRateLimit(event: ProgressEvent): boolean {\n if (event.kind !== 'tick') {\n return false;\n }\n\n // Widen the window after 5s of execution; a rateLimitMs of 0 (tests) stays 0\n // only inside the first 5s, which every test run fits in.\n const now = Date.now();\n const effectiveRateLimit =\n now - this.#startTime > 5000 ? Math.max(this.#rateLimitMs, 250) : this.#rateLimitMs;\n\n const elapsed = now - this.#lastSentMs;\n return elapsed < effectiveRateLimit;\n }\n\n #emitGuarded(sink: ProgressSink, event: ProgressEvent): void {\n try {\n const result = sink.emit(event);\n if (result instanceof Promise) {\n result.catch((err: unknown) => {\n Logger.warn('ProgressSink emit failed', {\n sink: sink.name,\n eventKind: event.kind,\n err,\n });\n });\n }\n } catch (err) {\n Logger.warn('ProgressSink emit failed', {\n sink: sink.name,\n eventKind: event.kind,\n err,\n });\n }\n }\n}\n\nexport class McpProgressSink implements ProgressSink {\n readonly name = 'mcp';\n private readonly toolName: string;\n private readonly token: string | number;\n private readonly notify: (n: Notification) => Promise<void>;\n readonly #pending = new Set<Promise<void>>();\n #lastProgress = -1;\n\n constructor(\n toolName: string,\n token: string | number,\n notify: (n: Notification) => Promise<void>,\n ) {\n this.toolName = toolName;\n this.token = token;\n this.notify = notify;\n }\n\n emit(event: ProgressEvent): void {\n let current = event.current;\n let total = event.total;\n if (event.kind === 'complete') {\n current = total ?? current;\n total = current;\n }\n if (event.kind !== 'tick') {\n // A terminal frame carries the outcome message, so it must reach the\n // wire. It repeats the cursor whenever the session has no `total` (there\n // is nothing higher to report), and the spec requires progress to\n // increase per token — so advance past the last value instead of dropping\n // the frame.\n current = Math.max(current, this.#lastProgress + 1);\n if (total !== undefined && total < current) total = current;\n }\n // Spec: progress must increase on every notification for the same token.\n if (current <= this.#lastProgress) return;\n this.#lastProgress = current;\n const notificationParams: ProgressNotificationParams = {\n progressToken: this.token,\n progress: current,\n ...(total !== undefined ? { total } : {}),\n message: event.message,\n };\n const promise = this.notify({\n method: 'notifications/progress',\n params: notificationParams,\n })\n .catch((error: unknown) => {\n reportDetachedError(this.toolName, 'progressNotification', error);\n })\n .finally(() => {\n this.#pending.delete(promise);\n });\n this.#pending.add(promise);\n }\n\n async flush(): Promise<void> {\n if (this.#pending.size > 0) {\n await Promise.allSettled([...this.#pending]);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"progress.js","sourceRoot":"","sources":["../../src/tools/progress.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAElD,SAAS,mBAAmB,CAAC,QAAgB,EAAE,OAAe,EAAE,KAAc;IAC5E,MAAM,OAAO,GAAG,yBAAyB,CAAC,KAAK,CAAC,CAAC;IACjD,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,QAAQ,KAAK,OAAO,YAAY,OAAO,EAAE,CAAC,CAAC;AACvE,CAAC;AAqBD,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAEjC,MAAM,OAAO,eAAe;IACjB,MAAM,CAAS;IACf,MAAM,CAAqB;IAC3B,KAAK,CAA8B;IACnC,YAAY,CAAS;IACrB,UAAU,CAAS;IAE5B,OAAO,GAAG,CAAC,CAAC;IACZ,WAAW,GAAG,CAAC,CAAC;IAChB,KAAK,GAAG,KAAK,CAAC;IAEd,YAAY,IAA4B;QACtC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,WAAW,IAAI,qBAAqB,CAAC;QAE9D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;QACtB,IAAI,CAAC,WAAW,GAAG,GAAG,GAAG,IAAI,CAAC,YAAY,CAAC;QAE3C,6FAA6F;QAC7F,IAAI,CAAC,SAAS,CAAC;YACb,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,CAAC;YACV,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5D,OAAO,EAAE,IAAI,CAAC,MAAM;SACrB,CAAC,CAAC;IACL,CAAC;IAED,GAAG,CAAC,KAA4D;QAC9D,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,0EAA0E;QAC1E,yEAAyE;QACzE,qDAAqD;QACrD,IAAI,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QAC1C,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC;QACzC,IAAI,CAAC,SAAS,CAAC;YACb,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM;SACtC,CAAC,CAAC;IACL,CAAC;IAED,QAAQ,CAAC,OAAe;QACtB,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,SAAS,CAAC;YACb,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5D,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC,KAAc,EAAE,OAAgB;QACnC,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,SAAS,CAAC;YACb,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5D,OAAO,EAAE,OAAO,IAAI,IAAI,CAAC,MAAM;YAC/B,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAED,SAAS,CAAC,KAAoB;QAC5B,IAAI,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO;QACT,CAAC;QAED,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAE9B,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QACxB,IAAI,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,mEAAmE;YACnE,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IAED,gBAAgB,CAAC,KAAoB;QACnC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,OAAO,KAAK,CAAC;QACf,CAAC;QAED,6EAA6E;QAC7E,0DAA0D;QAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,kBAAkB,GACtB,GAAG,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC;QAEtF,MAAM,OAAO,GAAG,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC;QACvC,OAAO,OAAO,GAAG,kBAAkB,CAAC;IACtC,CAAC;CACF;AAED,MAAM,OAAO,eAAe;IACT,QAAQ,CAAS;IACjB,KAAK,CAAkB;IACvB,MAAM,CAAqC;IACnD,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAC;IAC7C,aAAa,GAAG,CAAC,CAAC,CAAC;IAEnB,YACE,QAAgB,EAChB,KAAsB,EACtB,MAA0C;QAE1C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,IAAI,CAAC,KAAoB;QACvB,IAAI,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC5B,IAAI,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxB,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC9B,OAAO,GAAG,KAAK,IAAI,OAAO,CAAC;YAC3B,KAAK,GAAG,OAAO,CAAC;QAClB,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,qEAAqE;YACrE,yEAAyE;YACzE,kEAAkE;YAClE,0EAA0E;YAC1E,aAAa;YACb,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC;YACpD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,GAAG,OAAO;gBAAE,KAAK,GAAG,OAAO,CAAC;QAC9D,CAAC;QACD,yEAAyE;QACzE,IAAI,OAAO,IAAI,IAAI,CAAC,aAAa;YAAE,OAAO;QAC1C,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC;QAC7B,MAAM,kBAAkB,GAA+B;YACrD,aAAa,EAAE,IAAI,CAAC,KAAK;YACzB,QAAQ,EAAE,OAAO;YACjB,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,OAAO,EAAE,KAAK,CAAC,OAAO;SACvB,CAAC;QACF,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC;YAC1B,MAAM,EAAE,wBAAwB;YAChC,MAAM,EAAE,kBAAkB;SAC3B,CAAC;aACC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACxB,mBAAmB,CAAC,IAAI,CAAC,QAAQ,EAAE,sBAAsB,EAAE,KAAK,CAAC,CAAC;QACpE,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC,CAAC,CAAC;QACL,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;CACF","sourcesContent":["import type { Notification, ProgressNotificationParams } from '@modelcontextprotocol/server';\n\nimport { formatUnknownErrorMessage } from '../core/errors.js';\nimport { Logger } from '../core/observability.js';\n\nfunction reportDetachedError(toolName: string, context: string, error: unknown): void {\n const message = formatUnknownErrorMessage(error);\n Logger.emit('warning', `${toolName}: ${context} failed: ${message}`);\n}\n\nexport type ProgressEvent =\n | { kind: 'tick'; current: number; total?: number; message: string }\n | { kind: 'complete'; current: number; total?: number; message: string }\n | {\n kind: 'fail';\n current: number;\n total?: number;\n message: string;\n error: unknown;\n };\n\ninterface ProgressSessionOptions {\n label: string;\n total?: number;\n sink?: McpProgressSink;\n /** Override the rate limit window. Default: 50ms. */\n rateLimitMs?: number;\n}\n\nconst DEFAULT_RATE_LIMIT_MS = 50;\n\nexport class ProgressSession {\n readonly #label: string;\n readonly #total: number | undefined;\n readonly #sink: McpProgressSink | undefined;\n readonly #rateLimitMs: number;\n readonly #startTime: number;\n\n #cursor = 0;\n #lastSentMs = 0;\n #done = false;\n\n constructor(opts: ProgressSessionOptions) {\n this.#label = opts.label;\n this.#total = opts.total;\n this.#sink = opts.sink;\n this.#rateLimitMs = opts.rateLimitMs ?? DEFAULT_RATE_LIMIT_MS;\n\n const now = Date.now();\n this.#startTime = now;\n this.#lastSentMs = now - this.#rateLimitMs;\n\n // Synthetic start tick — preserves today's \"fire 0/total at session creation\" wire behavior.\n this.#dispatch({\n kind: 'tick',\n current: 0,\n ...(this.#total !== undefined ? { total: this.#total } : {}),\n message: this.#label,\n });\n }\n\n set(input: { current: number; total?: number; message?: string }): void {\n if (this.#done) return;\n // The spec requires `progress` to increase on every notification, and the\n // constructor already dispatched current: 0. A repeated or backward tick\n // would put a duplicate value on the wire — drop it.\n if (input.current <= this.#cursor) return;\n this.#cursor = input.current;\n const total = input.total ?? this.#total;\n this.#dispatch({\n kind: 'tick',\n current: this.#cursor,\n ...(total !== undefined ? { total } : {}),\n message: input.message ?? this.#label,\n });\n }\n\n complete(message: string): void {\n if (this.#done) return;\n this.#done = true;\n this.#dispatch({\n kind: 'complete',\n current: this.#cursor,\n ...(this.#total !== undefined ? { total: this.#total } : {}),\n message,\n });\n }\n\n fail(error: unknown, message?: string): void {\n if (this.#done) return;\n this.#done = true;\n this.#dispatch({\n kind: 'fail',\n current: this.#cursor,\n ...(this.#total !== undefined ? { total: this.#total } : {}),\n message: message ?? this.#label,\n error,\n });\n }\n\n #dispatch(event: ProgressEvent): void {\n if (this.#shouldRateLimit(event)) {\n return;\n }\n\n this.#lastSentMs = Date.now();\n\n if (!this.#sink) return;\n try {\n this.#sink.emit(event);\n } catch (err) {\n // A sink failure must never fail the tool call it is reporting on.\n Logger.warn('progress sink emit failed', { eventKind: event.kind, err });\n }\n }\n\n #shouldRateLimit(event: ProgressEvent): boolean {\n if (event.kind !== 'tick') {\n return false;\n }\n\n // Widen the window after 5s of execution; a rateLimitMs of 0 (tests) stays 0\n // only inside the first 5s, which every test run fits in.\n const now = Date.now();\n const effectiveRateLimit =\n now - this.#startTime > 5000 ? Math.max(this.#rateLimitMs, 250) : this.#rateLimitMs;\n\n const elapsed = now - this.#lastSentMs;\n return elapsed < effectiveRateLimit;\n }\n}\n\nexport class McpProgressSink {\n private readonly toolName: string;\n private readonly token: string | number;\n private readonly notify: (n: Notification) => Promise<void>;\n readonly #pending = new Set<Promise<void>>();\n #lastProgress = -1;\n\n constructor(\n toolName: string,\n token: string | number,\n notify: (n: Notification) => Promise<void>,\n ) {\n this.toolName = toolName;\n this.token = token;\n this.notify = notify;\n }\n\n emit(event: ProgressEvent): void {\n let current = event.current;\n let total = event.total;\n if (event.kind === 'complete') {\n current = total ?? current;\n total = current;\n }\n if (event.kind !== 'tick') {\n // A terminal frame carries the outcome message, so it must reach the\n // wire. It repeats the cursor whenever the session has no `total` (there\n // is nothing higher to report), and the spec requires progress to\n // increase per token — so advance past the last value instead of dropping\n // the frame.\n current = Math.max(current, this.#lastProgress + 1);\n if (total !== undefined && total < current) total = current;\n }\n // Spec: progress must increase on every notification for the same token.\n if (current <= this.#lastProgress) return;\n this.#lastProgress = current;\n const notificationParams: ProgressNotificationParams = {\n progressToken: this.token,\n progress: current,\n ...(total !== undefined ? { total } : {}),\n message: event.message,\n };\n const promise = this.notify({\n method: 'notifications/progress',\n params: notificationParams,\n })\n .catch((error: unknown) => {\n reportDetachedError(this.toolName, 'progressNotification', error);\n })\n .finally(() => {\n this.#pending.delete(promise);\n });\n this.#pending.add(promise);\n }\n\n async flush(): Promise<void> {\n if (this.#pending.size > 0) {\n await Promise.allSettled([...this.#pending]);\n }\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transport/http.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAUxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAuBrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAyOjD,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,aAAa,EACtB,MAAM,GAAE,aAAkB,GACzB,OAAO,CAAC,MAAM,CAAC,CAgIjB"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transport/http.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AASxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAuBrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwPjD,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,aAAa,EACtB,MAAM,GAAE,aAAkB,GACzB,OAAO,CAAC,MAAM,CAAC,CAuHjB"}
@@ -2,10 +2,9 @@
2
2
  // per-request server factory, and listen-filter watcher gating on POST /mcp.
3
3
  import { createMcpExpressApp } from '@modelcontextprotocol/express';
4
4
  import { toNodeHandler } from '@modelcontextprotocol/node';
5
- import { createMcpHandler, DEFAULT_REQUEST_TIMEOUT_MSEC, ProtocolErrorCode, } from '@modelcontextprotocol/server';
5
+ import { createMcpHandler, DEFAULT_REQUEST_TIMEOUT_MSEC, isJsonContentType, ProtocolErrorCode, } from '@modelcontextprotocol/server';
6
6
  import { createServer as createHttpServer } from 'node:http';
7
7
  import { formatUnknownErrorMessage } from '../core/errors.js';
8
- import { assertFleetRequestStateKey } from '../core/input-required.js';
9
8
  import { Logger } from '../core/observability.js';
10
9
  import { PageSnapshotStore } from '../core/page-store.js';
11
10
  import { PathGuard } from '../core/path.js';
@@ -23,6 +22,7 @@ const MAX_REQUEST_BODY_BYTES = parseEnvInt('FS_MAX_REQUEST_BYTES', 4 * MIB, 1024
23
22
  * 405 envelope identical to the one the handler would have produced.
24
23
  */
25
24
  const SDK_METHOD_NOT_ALLOWED_CODE = -32000;
25
+ const SDK_UNSUPPORTED_MEDIA_TYPE_CODE = -32000;
26
26
  /** Body-parser rejections and anything else that reaches the end of the chain. */
27
27
  function errorHandlerMiddleware(err, _req, res, next) {
28
28
  if (res.headersSent) {
@@ -115,7 +115,16 @@ function setupExpressApp(httpHost, apiKey, allowedHosts, modernNodeHandler, shar
115
115
  };
116
116
  app.post('/mcp', (req, res, next) => {
117
117
  void (async () => {
118
+ if (!isJsonContentType(req.headers['content-type'])) {
119
+ sendJsonRpcError(res, 415, SDK_UNSUPPORTED_MEDIA_TYPE_CODE, 'Unsupported Media Type: Content-Type must be application/json');
120
+ return;
121
+ }
118
122
  const parsedBody = req.body;
123
+ if (parsedBody === undefined) {
124
+ // Undefined tells the Node adapter to read the raw stream without our parser's limit.
125
+ sendJsonRpcError(res, 400, ProtocolErrorCode.ParseError, 'Invalid JSON in request body');
126
+ return;
127
+ }
119
128
  // Only a structurally valid listen gets watchers, mirroring the stdio
120
129
  // gate. A malformed one cannot succeed downstream, so attaching handles
121
130
  // for it only gives the response-close release something to undo — and
@@ -183,16 +192,8 @@ function setupExpressApp(httpHost, apiKey, allowedHosts, modernNodeHandler, shar
183
192
  }
184
193
  export async function startHttpServer(port, options, config = {}) {
185
194
  const httpHost = config.httpHost ?? '127.0.0.1';
186
- const { apiKey, eventBus } = config;
187
- const fleet = config.deploymentMode === 'fleet';
195
+ const { apiKey } = config;
188
196
  assertHttpBindingPolicy(httpHost, apiKey);
189
- if (fleet && !apiKey) {
190
- throw new Error('Fleet deployment mode requires an API key.');
191
- }
192
- if (fleet && !eventBus) {
193
- throw new Error('Fleet deployment mode requires a shared event bus.');
194
- }
195
- assertFleetRequestStateKey(fleet);
196
197
  const allowedHosts = resolveAllowedHosts(httpHost, process.env['FS_ALLOWED_HOSTS']);
197
198
  assertHttpHostPolicy(httpHost, allowedHosts, parseTrueEnvFlag(process.env['FS_ALLOW_UNRESTRICTED_HOSTS'], 'FS_ALLOW_UNRESTRICTED_HOSTS'));
198
199
  const sharedRegistry = createWatcherRegistry();
@@ -219,7 +220,6 @@ export async function startHttpServer(port, options, config = {}) {
219
220
  await sharedPathGuard.recomputeAllowedDirectories();
220
221
  const modernHandler = createMcpHandler(makeHttpModernFactory(options, () => modernHandler.notify, sharedRegistry, sharedPathGuard, sharedStore, sharedPageStore, apiKey), {
221
222
  legacy: 'reject',
222
- ...(eventBus ? { bus: eventBus } : {}),
223
223
  onerror: (error) => {
224
224
  Logger.error('[HTTP] modern leg error:', formatUnknownErrorMessage(error));
225
225
  },
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/transport/http.ts"],"names":[],"mappings":"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","sourcesContent":["// HTTP hosting: express app assembly, security middleware wiring, the modern\n// per-request server factory, and listen-filter watcher gating on POST /mcp.\nimport { createMcpExpressApp } from '@modelcontextprotocol/express';\nimport { toNodeHandler } from '@modelcontextprotocol/node';\nimport type {\n McpHttpHandler,\n McpServerFactory,\n ServerNotifier,\n} from '@modelcontextprotocol/server';\nimport {\n createMcpHandler,\n DEFAULT_REQUEST_TIMEOUT_MSEC,\n ProtocolErrorCode,\n} from '@modelcontextprotocol/server';\n\nimport type { Server } from 'node:http';\nimport { createServer as createHttpServer } from 'node:http';\n\nimport type { Express, NextFunction, Request, Response } from 'express';\n\nimport { formatUnknownErrorMessage } from '../core/errors.js';\nimport { assertFleetRequestStateKey } from '../core/input-required.js';\nimport { Logger } from '../core/observability.js';\nimport { PageSnapshotStore } from '../core/page-store.js';\nimport { PathGuard } from '../core/path.js';\nimport type { ServerOptions } from '../core/path.js';\nimport { parseTrueEnvFlag } from '../core/primitives.js';\nimport { ResourceStore } from '../core/store.js';\nimport { MIB, parseEnvInt } from '../core/util.js';\nimport {\n createWatcherRegistry,\n MAX_WATCHERS,\n type WatcherRegistry,\n} from '../core/watcher-registry.js';\nimport {\n assertHttpBindingPolicy,\n assertHttpHostPolicy,\n bearerAuthMiddleware,\n computeAllowedOriginHostnames,\n corsOriginMiddleware,\n corsPreflightHandler,\n createRateLimiter,\n protectedResourceUrl,\n resolveAllowedHosts,\n resolveTrustProxySetting,\n sendJsonRpcError,\n} from '../http-policy.js';\nimport { createServer } from '../server.js';\nimport type { RuntimeConfig } from './shared.js';\nimport {\n isStructurallyValidListen,\n jsonRpcRequestId,\n listenSubscriptionUris,\n prepareListenWatchers,\n} from './shared.js';\n\nconst MAX_REQUEST_BODY_BYTES = parseEnvInt('FS_MAX_REQUEST_BYTES', 4 * MIB, 1024, 256 * MIB);\n\n/**\n * The code the SDK's own non-POST rejection carries. Not a `ProtocolErrorCode`\n * member — the enum has no `-32000` — so it is pinned here to keep the local\n * 405 envelope identical to the one the handler would have produced.\n */\nconst SDK_METHOD_NOT_ALLOWED_CODE = -32000;\n\n/** Body-parser rejections and anything else that reaches the end of the chain. */\nfunction errorHandlerMiddleware(\n err: Error & { status?: number },\n _req: Request,\n res: Response,\n next: NextFunction,\n): void {\n if (res.headersSent) {\n next(err);\n return;\n }\n if (err.status === 413) {\n sendJsonRpcError(res, 413, ProtocolErrorCode.InvalidRequest, 'Request body too large');\n return;\n }\n if (err.status === 400) {\n sendJsonRpcError(res, 400, ProtocolErrorCode.ParseError, 'Invalid JSON in request body');\n return;\n }\n Logger.error('[HTTP] Unhandled middleware error:', formatUnknownErrorMessage(err));\n sendJsonRpcError(res, 500, ProtocolErrorCode.InternalError, 'Internal Server Error');\n}\n\nfunction makeHttpModernFactory(\n options: ServerOptions,\n getNotifier: () => ServerNotifier,\n sharedRegistry: WatcherRegistry,\n sharedPathGuard: PathGuard,\n sharedStore: ResourceStore,\n sharedPageStore: PageSnapshotStore,\n apiKey: string | undefined,\n): McpServerFactory {\n return async ({ era }) => {\n const notifier = getNotifier();\n const c = await createServer(options, {\n watcherRegistry: sharedRegistry,\n notifier,\n pathGuard: sharedPathGuard,\n resourceStore: sharedStore,\n pageStore: sharedPageStore,\n era,\n ...(apiKey !== undefined ? { apiKey } : {}),\n });\n const previousOnClose = c.mcp.server.onclose;\n c.mcp.server.onclose = () => {\n previousOnClose?.();\n c.disposeRuntimeState();\n };\n return c.mcp;\n };\n}\n\nfunction setupExpressApp(\n httpHost: string,\n apiKey: string | undefined,\n allowedHosts: string[],\n modernNodeHandler: (req: Request, res: Response, parsedBody?: unknown) => Promise<void>,\n sharedPathGuard: PathGuard,\n sharedRegistry: WatcherRegistry,\n notifier: ServerNotifier,\n): Express {\n const allowedOriginHostnames = computeAllowedOriginHostnames(process.env['FS_ALLOWED_ORIGINS']);\n // Read once here and passed down, like every other env-derived policy input:\n // http-policy holds no state and reads no env of its own.\n const publicUrl = process.env['FS_PUBLIC_URL'];\n\n const app = createMcpExpressApp({\n host: httpHost,\n jsonLimit: `${MAX_REQUEST_BODY_BYTES}b`,\n ...(allowedHosts.length > 0 ? { allowedHosts: [...allowedHosts] } : {}),\n ...(allowedOriginHostnames.length > 0 ? { allowedOrigins: [...allowedOriginHostnames] } : {}),\n });\n\n const trustProxy = resolveTrustProxySetting(process.env['FS_TRUST_PROXY']);\n if (trustProxy !== undefined) {\n app.set('trust proxy', trustProxy);\n }\n\n if (allowedHosts.length === 0) {\n Logger.warn(\n '[HTTP] FS_ALLOW_UNRESTRICTED_HOSTS is set: binding globally without Host validation.',\n );\n }\n\n app.options('/mcp', corsPreflightHandler(allowedOriginHostnames));\n app.use('/mcp', corsOriginMiddleware(allowedOriginHostnames));\n\n // Unconditional: the spec's rate-limit MUST is not scoped to authenticated\n // binds. A keyless bind is loopback-only, so the cap is looser, not absent.\n const rpm = parseEnvInt('FS_RATE_LIMIT_RPM', apiKey ? 120 : 6_000, 1, 100_000);\n app.use('/mcp', createRateLimiter(rpm));\n\n if (apiKey && allowedHosts.length > 0) {\n const metadataHandler = (req: Request, res: Response): void => {\n const resource = protectedResourceUrl(req, true, publicUrl);\n if (!resource) {\n // Unreachable for the unrestricted case (the gate above excludes it),\n // but defend a FS_PUBLIC_URL parse failure with a bodyless\n // 404 — a generic OAuth client cannot parse a JSON-RPC error envelope.\n res.status(404).end();\n return;\n }\n res.header('Access-Control-Allow-Origin', '*');\n res.status(200).json({\n resource: resource.href,\n bearer_methods_supported: ['header'],\n resource_name: 'filesystem-mcp',\n });\n };\n app.get(\n ['/.well-known/oauth-protected-resource', '/.well-known/oauth-protected-resource/mcp'],\n metadataHandler,\n );\n }\n\n app.get('/healthz', (_req: Request, res: Response) => {\n res.status(200).json({\n status: 'ok',\n uptime: process.uptime(),\n });\n });\n\n app.use('/mcp', bearerAuthMiddleware(apiKey, allowedHosts.length > 0, publicUrl));\n\n const resourceUpdateSink = (uri: string): void => {\n notifier.resourceUpdated(uri);\n };\n\n app.post('/mcp', (req: Request, res: Response, next: NextFunction) => {\n void (async () => {\n const parsedBody = req.body as unknown;\n // Only a structurally valid listen gets watchers, mirroring the stdio\n // gate. A malformed one cannot succeed downstream, so attaching handles\n // for it only gives the response-close release something to undo — and\n // everything that is not a listen takes no watchers either way.\n if (!isStructurallyValidListen(parsedBody)) {\n await modernNodeHandler(req, res, parsedBody);\n return;\n }\n\n // Reject an over-cap listen before the ack so the client does not believe\n // every requested URI is watched when the watcher budget is exhausted.\n // The per-URI `capped` failure below would also reject, but only after\n // creating and tearing down every watcher up to the cap.\n const requestedUris = listenSubscriptionUris(parsedBody);\n // Only URIs without a live watcher consume a slot; a re-listen to an\n // already-watched URI just retains another lease.\n const newUris = requestedUris.filter((uri) => !sharedRegistry.hasWatcher(uri));\n const available = MAX_WATCHERS - sharedRegistry.size();\n if (newUris.length > available) {\n sendJsonRpcError(\n res,\n 400,\n ProtocolErrorCode.InvalidParams,\n `subscriptions/listen names ${newUris.length} not-yet-watched URIs but only ${available} watcher slots remain (cap ${MAX_WATCHERS}). Reduce the resourceSubscriptions list.`,\n jsonRpcRequestId(parsedBody),\n );\n return;\n }\n\n const prepared = await prepareListenWatchers(\n parsedBody,\n sharedPathGuard,\n sharedRegistry,\n resourceUpdateSink,\n );\n if (!prepared.ok) {\n sendJsonRpcError(\n res,\n 400,\n ProtocolErrorCode.InvalidParams,\n prepared.message,\n jsonRpcRequestId(parsedBody),\n );\n return;\n }\n\n let released = false;\n const release = (): void => {\n if (released) return;\n released = true;\n for (const uri of prepared.acquiredUris) sharedRegistry.release(uri);\n };\n if (prepared.acquiredUris.length > 0) {\n // The attach loop awaits per-URI path validation, so the client can be\n // gone by the time it returns. `close` fires once: a listener attached\n // after it would never run and every lease this batch took would leak\n // until process shutdown.\n if (res.closed || res.destroyed || res.writableEnded) release();\n else res.once('close', release);\n }\n\n try {\n await modernNodeHandler(req, res, parsedBody);\n } catch (error) {\n release();\n throw error;\n }\n })().catch(next);\n });\n\n // Answer non-POST here rather than handing it to `modernNodeHandler`. The\n // SDK's own 405 is byte-identical to this one, but it omits the `Allow`\n // header RFC 9110 §15.5.6 requires and routes every routine GET probe through\n // the handler's `onerror` — which this server logs at error level.\n app.all('/mcp', (_req: Request, res: Response) => {\n sendJsonRpcError(res, 405, SDK_METHOD_NOT_ALLOWED_CODE, 'Method not allowed.', null, {\n Allow: 'POST, OPTIONS',\n });\n });\n\n app.use(errorHandlerMiddleware);\n\n return app;\n}\n\nexport async function startHttpServer(\n port: number,\n options: ServerOptions,\n config: RuntimeConfig = {},\n): Promise<Server> {\n const httpHost = config.httpHost ?? '127.0.0.1';\n const { apiKey, eventBus } = config;\n const fleet = config.deploymentMode === 'fleet';\n assertHttpBindingPolicy(httpHost, apiKey);\n if (fleet && !apiKey) {\n throw new Error('Fleet deployment mode requires an API key.');\n }\n if (fleet && !eventBus) {\n throw new Error('Fleet deployment mode requires a shared event bus.');\n }\n assertFleetRequestStateKey(fleet);\n const allowedHosts = resolveAllowedHosts(httpHost, process.env['FS_ALLOWED_HOSTS']);\n assertHttpHostPolicy(\n httpHost,\n allowedHosts,\n parseTrueEnvFlag(process.env['FS_ALLOW_UNRESTRICTED_HOSTS'], 'FS_ALLOW_UNRESTRICTED_HOSTS'),\n );\n\n const sharedRegistry = createWatcherRegistry();\n // One store for the whole endpoint, same lifetime and same one-credential\n // trust argument as the shared guard below: a result a tool externalized in\n // one POST must survive to the follow-up resources/read, and the modern leg\n // builds a fresh McpServer per request so a per-request store would discard\n // it immediately.\n // Fires only from a tool call, which is long after `modernHandler` below is\n // constructed, so reading it from the closure is safe (same as `getNotifier`).\n const sharedStore = new ResourceStore(() => {\n modernHandler.notify.resourcesChanged();\n });\n const sharedPageStore = new PageSnapshotStore();\n // One guard for the whole endpoint. The modern leg builds a fresh McpServer\n // per request, so a per-instance guard would discard every accepted access\n // grant the moment the request ended — re-prompting on each subsequent call\n // and leaving the listen-watcher path validating against a stale allowed set.\n // Grant scope is therefore the endpoint, not the connection: with API_KEY set\n // every caller presents the same key (one auth context by construction), and\n // without it the bind is loopback-only. Split into per-auth-context guards if\n // this ever serves more than one credential.\n const sharedPathGuard = new PathGuard(options, true);\n await sharedPathGuard.recomputeAllowedDirectories();\n\n const modernHandler: McpHttpHandler = createMcpHandler(\n makeHttpModernFactory(\n options,\n () => modernHandler.notify,\n sharedRegistry,\n sharedPathGuard,\n sharedStore,\n sharedPageStore,\n apiKey,\n ),\n {\n legacy: 'reject',\n ...(eventBus ? { bus: eventBus } : {}),\n onerror: (error: Error) => {\n Logger.error('[HTTP] modern leg error:', formatUnknownErrorMessage(error));\n },\n },\n );\n const modernNodeHandler = toNodeHandler(modernHandler);\n\n const app = setupExpressApp(\n httpHost,\n apiKey,\n allowedHosts,\n modernNodeHandler,\n sharedPathGuard,\n sharedRegistry,\n modernHandler.notify,\n );\n\n const httpServer = createHttpServer(app);\n // Must exceed the idle timeout of any proxy in front of this server, or the\n // proxy reuses connections the server already closed (intermittent 502s).\n const keepAliveMs = parseEnvInt('FS_KEEPALIVE_TIMEOUT_MS', 5_000, 1_000, 600_000);\n httpServer.keepAliveTimeout = keepAliveMs;\n httpServer.headersTimeout = keepAliveMs + 5_000;\n httpServer.requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC;\n\n const onHttpServerError = (error: Error): void => {\n Logger.error('[HTTP] runtime server error', {\n host: httpHost,\n port,\n error: formatUnknownErrorMessage(error),\n });\n };\n httpServer.on('error', onHttpServerError);\n\n const originalClose = httpServer.close.bind(httpServer);\n httpServer.close = function (callback?: (error?: Error) => void) {\n sharedRegistry.destroy();\n sharedPageStore.clear();\n modernHandler\n .close()\n .then(() => {\n originalClose(callback);\n })\n .catch((err: unknown) => {\n Logger.error(\n '[HTTP] Error closing handler during HTTP server close:',\n formatUnknownErrorMessage(err),\n );\n originalClose(callback);\n });\n return httpServer;\n };\n\n return new Promise((resolve, reject) => {\n const onError = (err: Error) => {\n sharedRegistry.destroy();\n sharedPageStore.clear();\n modernHandler.close().catch((closeErr: unknown) => {\n Logger.error(\n '[HTTP] Error closing handler on startup failure:',\n formatUnknownErrorMessage(closeErr),\n );\n });\n reject(err);\n };\n httpServer.once('error', onError);\n\n httpServer.listen(port, httpHost, () => {\n httpServer.removeListener('error', onError);\n Logger.info(`[HTTP] Server listening on http://${httpHost}:${port}`);\n resolve(httpServer);\n });\n });\n}\n"]}
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/transport/http.ts"],"names":[],"mappings":"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","sourcesContent":["// HTTP hosting: express app assembly, security middleware wiring, the modern\n// per-request server factory, and listen-filter watcher gating on POST /mcp.\nimport { createMcpExpressApp } from '@modelcontextprotocol/express';\nimport { toNodeHandler } from '@modelcontextprotocol/node';\nimport type {\n McpHttpHandler,\n McpServerFactory,\n ServerNotifier,\n} from '@modelcontextprotocol/server';\nimport {\n createMcpHandler,\n DEFAULT_REQUEST_TIMEOUT_MSEC,\n isJsonContentType,\n ProtocolErrorCode,\n} from '@modelcontextprotocol/server';\n\nimport type { Server } from 'node:http';\nimport { createServer as createHttpServer } from 'node:http';\n\nimport type { Express, NextFunction, Request, Response } from 'express';\n\nimport { formatUnknownErrorMessage } from '../core/errors.js';\nimport { Logger } from '../core/observability.js';\nimport { PageSnapshotStore } from '../core/page-store.js';\nimport { PathGuard } from '../core/path.js';\nimport type { ServerOptions } from '../core/path.js';\nimport { parseTrueEnvFlag } from '../core/primitives.js';\nimport { ResourceStore } from '../core/store.js';\nimport { MIB, parseEnvInt } from '../core/util.js';\nimport {\n createWatcherRegistry,\n MAX_WATCHERS,\n type WatcherRegistry,\n} from '../core/watcher-registry.js';\nimport {\n assertHttpBindingPolicy,\n assertHttpHostPolicy,\n bearerAuthMiddleware,\n computeAllowedOriginHostnames,\n corsOriginMiddleware,\n corsPreflightHandler,\n createRateLimiter,\n protectedResourceUrl,\n resolveAllowedHosts,\n resolveTrustProxySetting,\n sendJsonRpcError,\n} from '../http-policy.js';\nimport { createServer } from '../server.js';\nimport type { RuntimeConfig } from './shared.js';\nimport {\n isStructurallyValidListen,\n jsonRpcRequestId,\n listenSubscriptionUris,\n prepareListenWatchers,\n} from './shared.js';\n\nconst MAX_REQUEST_BODY_BYTES = parseEnvInt('FS_MAX_REQUEST_BYTES', 4 * MIB, 1024, 256 * MIB);\n\n/**\n * The code the SDK's own non-POST rejection carries. Not a `ProtocolErrorCode`\n * member — the enum has no `-32000` — so it is pinned here to keep the local\n * 405 envelope identical to the one the handler would have produced.\n */\nconst SDK_METHOD_NOT_ALLOWED_CODE = -32000;\nconst SDK_UNSUPPORTED_MEDIA_TYPE_CODE = -32000;\n\n/** Body-parser rejections and anything else that reaches the end of the chain. */\nfunction errorHandlerMiddleware(\n err: Error & { status?: number },\n _req: Request,\n res: Response,\n next: NextFunction,\n): void {\n if (res.headersSent) {\n next(err);\n return;\n }\n if (err.status === 413) {\n sendJsonRpcError(res, 413, ProtocolErrorCode.InvalidRequest, 'Request body too large');\n return;\n }\n if (err.status === 400) {\n sendJsonRpcError(res, 400, ProtocolErrorCode.ParseError, 'Invalid JSON in request body');\n return;\n }\n Logger.error('[HTTP] Unhandled middleware error:', formatUnknownErrorMessage(err));\n sendJsonRpcError(res, 500, ProtocolErrorCode.InternalError, 'Internal Server Error');\n}\n\nfunction makeHttpModernFactory(\n options: ServerOptions,\n getNotifier: () => ServerNotifier,\n sharedRegistry: WatcherRegistry,\n sharedPathGuard: PathGuard,\n sharedStore: ResourceStore,\n sharedPageStore: PageSnapshotStore,\n apiKey: string | undefined,\n): McpServerFactory {\n return async ({ era }) => {\n const notifier = getNotifier();\n const c = await createServer(options, {\n watcherRegistry: sharedRegistry,\n notifier,\n pathGuard: sharedPathGuard,\n resourceStore: sharedStore,\n pageStore: sharedPageStore,\n era,\n ...(apiKey !== undefined ? { apiKey } : {}),\n });\n const previousOnClose = c.mcp.server.onclose;\n c.mcp.server.onclose = () => {\n previousOnClose?.();\n c.disposeRuntimeState();\n };\n return c.mcp;\n };\n}\n\nfunction setupExpressApp(\n httpHost: string,\n apiKey: string | undefined,\n allowedHosts: string[],\n modernNodeHandler: (req: Request, res: Response, parsedBody?: unknown) => Promise<void>,\n sharedPathGuard: PathGuard,\n sharedRegistry: WatcherRegistry,\n notifier: ServerNotifier,\n): Express {\n const allowedOriginHostnames = computeAllowedOriginHostnames(process.env['FS_ALLOWED_ORIGINS']);\n // Read once here and passed down, like every other env-derived policy input:\n // http-policy holds no state and reads no env of its own.\n const publicUrl = process.env['FS_PUBLIC_URL'];\n\n const app = createMcpExpressApp({\n host: httpHost,\n jsonLimit: `${MAX_REQUEST_BODY_BYTES}b`,\n ...(allowedHosts.length > 0 ? { allowedHosts: [...allowedHosts] } : {}),\n ...(allowedOriginHostnames.length > 0 ? { allowedOrigins: [...allowedOriginHostnames] } : {}),\n });\n\n const trustProxy = resolveTrustProxySetting(process.env['FS_TRUST_PROXY']);\n if (trustProxy !== undefined) {\n app.set('trust proxy', trustProxy);\n }\n\n if (allowedHosts.length === 0) {\n Logger.warn(\n '[HTTP] FS_ALLOW_UNRESTRICTED_HOSTS is set: binding globally without Host validation.',\n );\n }\n\n app.options('/mcp', corsPreflightHandler(allowedOriginHostnames));\n app.use('/mcp', corsOriginMiddleware(allowedOriginHostnames));\n\n // Unconditional: the spec's rate-limit MUST is not scoped to authenticated\n // binds. A keyless bind is loopback-only, so the cap is looser, not absent.\n const rpm = parseEnvInt('FS_RATE_LIMIT_RPM', apiKey ? 120 : 6_000, 1, 100_000);\n app.use('/mcp', createRateLimiter(rpm));\n\n if (apiKey && allowedHosts.length > 0) {\n const metadataHandler = (req: Request, res: Response): void => {\n const resource = protectedResourceUrl(req, true, publicUrl);\n if (!resource) {\n // Unreachable for the unrestricted case (the gate above excludes it),\n // but defend a FS_PUBLIC_URL parse failure with a bodyless\n // 404 — a generic OAuth client cannot parse a JSON-RPC error envelope.\n res.status(404).end();\n return;\n }\n res.header('Access-Control-Allow-Origin', '*');\n res.status(200).json({\n resource: resource.href,\n bearer_methods_supported: ['header'],\n resource_name: 'filesystem-mcp',\n });\n };\n app.get(\n ['/.well-known/oauth-protected-resource', '/.well-known/oauth-protected-resource/mcp'],\n metadataHandler,\n );\n }\n\n app.get('/healthz', (_req: Request, res: Response) => {\n res.status(200).json({\n status: 'ok',\n uptime: process.uptime(),\n });\n });\n\n app.use('/mcp', bearerAuthMiddleware(apiKey, allowedHosts.length > 0, publicUrl));\n\n const resourceUpdateSink = (uri: string): void => {\n notifier.resourceUpdated(uri);\n };\n\n app.post('/mcp', (req: Request, res: Response, next: NextFunction) => {\n void (async () => {\n if (!isJsonContentType(req.headers['content-type'])) {\n sendJsonRpcError(\n res,\n 415,\n SDK_UNSUPPORTED_MEDIA_TYPE_CODE,\n 'Unsupported Media Type: Content-Type must be application/json',\n );\n return;\n }\n const parsedBody = req.body as unknown;\n if (parsedBody === undefined) {\n // Undefined tells the Node adapter to read the raw stream without our parser's limit.\n sendJsonRpcError(res, 400, ProtocolErrorCode.ParseError, 'Invalid JSON in request body');\n return;\n }\n // Only a structurally valid listen gets watchers, mirroring the stdio\n // gate. A malformed one cannot succeed downstream, so attaching handles\n // for it only gives the response-close release something to undo — and\n // everything that is not a listen takes no watchers either way.\n if (!isStructurallyValidListen(parsedBody)) {\n await modernNodeHandler(req, res, parsedBody);\n return;\n }\n\n // Reject an over-cap listen before the ack so the client does not believe\n // every requested URI is watched when the watcher budget is exhausted.\n // The per-URI `capped` failure below would also reject, but only after\n // creating and tearing down every watcher up to the cap.\n const requestedUris = listenSubscriptionUris(parsedBody);\n // Only URIs without a live watcher consume a slot; a re-listen to an\n // already-watched URI just retains another lease.\n const newUris = requestedUris.filter((uri) => !sharedRegistry.hasWatcher(uri));\n const available = MAX_WATCHERS - sharedRegistry.size();\n if (newUris.length > available) {\n sendJsonRpcError(\n res,\n 400,\n ProtocolErrorCode.InvalidParams,\n `subscriptions/listen names ${newUris.length} not-yet-watched URIs but only ${available} watcher slots remain (cap ${MAX_WATCHERS}). Reduce the resourceSubscriptions list.`,\n jsonRpcRequestId(parsedBody),\n );\n return;\n }\n\n const prepared = await prepareListenWatchers(\n parsedBody,\n sharedPathGuard,\n sharedRegistry,\n resourceUpdateSink,\n );\n if (!prepared.ok) {\n sendJsonRpcError(\n res,\n 400,\n ProtocolErrorCode.InvalidParams,\n prepared.message,\n jsonRpcRequestId(parsedBody),\n );\n return;\n }\n\n let released = false;\n const release = (): void => {\n if (released) return;\n released = true;\n for (const uri of prepared.acquiredUris) sharedRegistry.release(uri);\n };\n if (prepared.acquiredUris.length > 0) {\n // The attach loop awaits per-URI path validation, so the client can be\n // gone by the time it returns. `close` fires once: a listener attached\n // after it would never run and every lease this batch took would leak\n // until process shutdown.\n if (res.closed || res.destroyed || res.writableEnded) release();\n else res.once('close', release);\n }\n\n try {\n await modernNodeHandler(req, res, parsedBody);\n } catch (error) {\n release();\n throw error;\n }\n })().catch(next);\n });\n\n // Answer non-POST here rather than handing it to `modernNodeHandler`. The\n // SDK's own 405 is byte-identical to this one, but it omits the `Allow`\n // header RFC 9110 §15.5.6 requires and routes every routine GET probe through\n // the handler's `onerror` — which this server logs at error level.\n app.all('/mcp', (_req: Request, res: Response) => {\n sendJsonRpcError(res, 405, SDK_METHOD_NOT_ALLOWED_CODE, 'Method not allowed.', null, {\n Allow: 'POST, OPTIONS',\n });\n });\n\n app.use(errorHandlerMiddleware);\n\n return app;\n}\n\nexport async function startHttpServer(\n port: number,\n options: ServerOptions,\n config: RuntimeConfig = {},\n): Promise<Server> {\n const httpHost = config.httpHost ?? '127.0.0.1';\n const { apiKey } = config;\n assertHttpBindingPolicy(httpHost, apiKey);\n const allowedHosts = resolveAllowedHosts(httpHost, process.env['FS_ALLOWED_HOSTS']);\n assertHttpHostPolicy(\n httpHost,\n allowedHosts,\n parseTrueEnvFlag(process.env['FS_ALLOW_UNRESTRICTED_HOSTS'], 'FS_ALLOW_UNRESTRICTED_HOSTS'),\n );\n\n const sharedRegistry = createWatcherRegistry();\n // One store for the whole endpoint, same lifetime and same one-credential\n // trust argument as the shared guard below: a result a tool externalized in\n // one POST must survive to the follow-up resources/read, and the modern leg\n // builds a fresh McpServer per request so a per-request store would discard\n // it immediately.\n // Fires only from a tool call, which is long after `modernHandler` below is\n // constructed, so reading it from the closure is safe (same as `getNotifier`).\n const sharedStore = new ResourceStore(() => {\n modernHandler.notify.resourcesChanged();\n });\n const sharedPageStore = new PageSnapshotStore();\n // One guard for the whole endpoint. The modern leg builds a fresh McpServer\n // per request, so a per-instance guard would discard every accepted access\n // grant the moment the request ended — re-prompting on each subsequent call\n // and leaving the listen-watcher path validating against a stale allowed set.\n // Grant scope is therefore the endpoint, not the connection: with API_KEY set\n // every caller presents the same key (one auth context by construction), and\n // without it the bind is loopback-only. Split into per-auth-context guards if\n // this ever serves more than one credential.\n const sharedPathGuard = new PathGuard(options, true);\n await sharedPathGuard.recomputeAllowedDirectories();\n\n const modernHandler: McpHttpHandler = createMcpHandler(\n makeHttpModernFactory(\n options,\n () => modernHandler.notify,\n sharedRegistry,\n sharedPathGuard,\n sharedStore,\n sharedPageStore,\n apiKey,\n ),\n {\n legacy: 'reject',\n onerror: (error: Error) => {\n Logger.error('[HTTP] modern leg error:', formatUnknownErrorMessage(error));\n },\n },\n );\n const modernNodeHandler = toNodeHandler(modernHandler);\n\n const app = setupExpressApp(\n httpHost,\n apiKey,\n allowedHosts,\n modernNodeHandler,\n sharedPathGuard,\n sharedRegistry,\n modernHandler.notify,\n );\n\n const httpServer = createHttpServer(app);\n // Must exceed the idle timeout of any proxy in front of this server, or the\n // proxy reuses connections the server already closed (intermittent 502s).\n const keepAliveMs = parseEnvInt('FS_KEEPALIVE_TIMEOUT_MS', 5_000, 1_000, 600_000);\n httpServer.keepAliveTimeout = keepAliveMs;\n httpServer.headersTimeout = keepAliveMs + 5_000;\n httpServer.requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC;\n\n const onHttpServerError = (error: Error): void => {\n Logger.error('[HTTP] runtime server error', {\n host: httpHost,\n port,\n error: formatUnknownErrorMessage(error),\n });\n };\n httpServer.on('error', onHttpServerError);\n\n const originalClose = httpServer.close.bind(httpServer);\n httpServer.close = function (callback?: (error?: Error) => void) {\n sharedRegistry.destroy();\n sharedPageStore.clear();\n modernHandler\n .close()\n .then(() => {\n originalClose(callback);\n })\n .catch((err: unknown) => {\n Logger.error(\n '[HTTP] Error closing handler during HTTP server close:',\n formatUnknownErrorMessage(err),\n );\n originalClose(callback);\n });\n return httpServer;\n };\n\n return new Promise((resolve, reject) => {\n const onError = (err: Error) => {\n sharedRegistry.destroy();\n sharedPageStore.clear();\n modernHandler.close().catch((closeErr: unknown) => {\n Logger.error(\n '[HTTP] Error closing handler on startup failure:',\n formatUnknownErrorMessage(closeErr),\n );\n });\n reject(err);\n };\n httpServer.once('error', onError);\n\n httpServer.listen(port, httpHost, () => {\n httpServer.removeListener('error', onError);\n Logger.info(`[HTTP] Server listening on http://${httpHost}:${port}`);\n resolve(httpServer);\n });\n });\n}\n"]}
@@ -1,4 +1,3 @@
1
- import type { ServerEventBus } from '@modelcontextprotocol/server';
2
1
  import type { PathGuard } from '../core/path.js';
3
2
  import { type WatcherRegistry } from '../core/watcher-registry.js';
4
3
  /**
@@ -12,10 +11,6 @@ export interface RuntimeConfig {
12
11
  httpHost?: string;
13
12
  /** `--api-key` or `API_KEY`. Unset means open access (loopback dev mode). */
14
13
  apiKey?: string;
15
- /** Shared change-event bus for multi-instance HTTP deployments. Caller-owned. */
16
- eventBus?: ServerEventBus;
17
- /** Explicit topology; fleet mode requires shared state and event delivery. */
18
- deploymentMode?: 'single' | 'fleet';
19
14
  }
20
15
  /**
21
16
  * True when `message` really is a `subscriptions/listen` request, not merely a
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/transport/shared.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAInE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,6BAA6B,CAAC;AAErC;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,8EAA8E;IAC9E,cAAc,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAGnE;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5E;AAED,wFAAwF;AACxF,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,EAAE,CAYpE;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAoBrD;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,UAAU,EAAE,OAAO,EACnB,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CAY5B"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/transport/shared.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,6BAA6B,CAAC;AAErC;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAGnE;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5E;AAED,wFAAwF;AACxF,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,EAAE,CAYpE;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAoBrD;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,UAAU,EAAE,OAAO,EACnB,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CAY5B"}
@@ -1,3 +1,7 @@
1
+ // Pieces both transport legs share: the runtime config contract and the
2
+ // `subscriptions/listen` watcher-preparation ladder. stdio and HTTP gate the
3
+ // same message shape; only where the lease is released differs (connection
4
+ // close vs response close).
1
5
  import { specTypeSchemas } from '@modelcontextprotocol/server';
2
6
  import { formatUnknownErrorMessage } from '../core/errors.js';
3
7
  import { MAX_WATCHERS, } from '../core/watcher-registry.js';
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/transport/shared.ts"],"names":[],"mappings":"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","sourcesContent":["// Pieces both transport legs share: the runtime config contract and the\n// `subscriptions/listen` watcher-preparation ladder. stdio and HTTP gate the\n// same message shape; only where the lease is released differs (connection\n// close vs response close).\nimport type { ServerEventBus } from '@modelcontextprotocol/server';\nimport { specTypeSchemas } from '@modelcontextprotocol/server';\n\nimport { formatUnknownErrorMessage } from '../core/errors.js';\nimport type { PathGuard } from '../core/path.js';\nimport {\n MAX_WATCHERS,\n type WatcherAttachResult,\n type WatcherRegistry,\n} from '../core/watcher-registry.js';\n\n/**\n * Runtime inputs the CLI resolves once (flag, else the operator's env var) and\n * hands the transport. Separate from `ServerOptions` because that object is\n * `PathGuard`'s constructor argument: the filesystem guard has no use for a\n * bind address and no business holding a bearer secret.\n */\nexport interface RuntimeConfig {\n /** `--http-host` or `HTTP_HOST`. The HTTP bind defaults to loopback without it. */\n httpHost?: string;\n /** `--api-key` or `API_KEY`. Unset means open access (loopback dev mode). */\n apiKey?: string;\n /** Shared change-event bus for multi-instance HTTP deployments. Caller-owned. */\n eventBus?: ServerEventBus;\n /** Explicit topology; fleet mode requires shared state and event delivery. */\n deploymentMode?: 'single' | 'fleet';\n}\n\n/**\n * True when `message` really is a `subscriptions/listen` request, not merely a\n * body carrying that method string. Both legs gate watcher attachment on this:\n * a malformed listen cannot succeed downstream, so creating and tearing down\n * `fs.watch` handles for it is pure waste — and on HTTP the teardown depended on\n * the response-close listener firing. Non-listen bodies fail it too, which is\n * the same answer `listenSubscriptionUris` gives them.\n */\nexport function isStructurallyValidListen(message: unknown): boolean {\n const result = specTypeSchemas.SubscriptionsListenRequest['~standard'].validate(message);\n return !('issues' in result);\n}\n\n/** The request id of a parsed JSON-RPC body, for error-envelope echo. */\nexport function jsonRpcRequestId(parsedBody: unknown): string | number | null {\n const id = (parsedBody as { id?: unknown } | null | undefined)?.id;\n return typeof id === 'string' || typeof id === 'number' ? id : null;\n}\n\n/** The `resourceSubscriptions` URIs of a `subscriptions/listen` body, de-duplicated. */\nexport function listenSubscriptionUris(parsedBody: unknown): string[] {\n if (typeof parsedBody !== 'object' || parsedBody === null) return [];\n const body = parsedBody as {\n method?: unknown;\n params?: { notifications?: { resourceSubscriptions?: unknown } };\n };\n if (body.method !== 'subscriptions/listen') return [];\n const uris = body.params?.notifications?.resourceSubscriptions;\n if (!Array.isArray(uris)) return [];\n // De-duplicate: one attach must yield one ref-count, or the release below\n // decrements further than it incremented and tears down a live watcher.\n return [...new Set(uris.filter((uri): uri is string => typeof uri === 'string'))];\n}\n\nexport type ListenPreparation =\n | { readonly ok: true; readonly acquiredUris: string[] }\n | { readonly ok: false; readonly message: string };\n\nconst WATCHER_FAILURE_REASONS = {\n 'bad-uri': 'unsupported resource URI',\n capped: `watcher limit ${MAX_WATCHERS} reached`,\n 'attach-failed': 'filesystem watcher could not be created',\n stale: 'subscription was cancelled during setup',\n} as const;\n\nfunction watcherFailureMessage(\n uri: string,\n result: Exclude<WatcherAttachResult, { ok: true }>,\n): string {\n const why =\n result.reason === 'invalid-path'\n ? formatUnknownErrorMessage(result.error)\n : WATCHER_FAILURE_REASONS[result.reason];\n return `Cannot subscribe to ${uri}: ${why}`;\n}\n\n/**\n * Prepare every filesystem watcher named by a `subscriptions/listen` filter.\n * The batch is all-or-nothing: a failed URI releases each prior lease.\n */\nexport async function prepareListenWatchers(\n parsedBody: unknown,\n pathGuard: PathGuard,\n registry: WatcherRegistry,\n notify: (uri: string) => void,\n): Promise<ListenPreparation> {\n const uris = listenSubscriptionUris(parsedBody);\n const acquired: string[] = [];\n for (const uri of uris) {\n const result = await registry.acquire(pathGuard, uri, notify);\n if (!result.ok) {\n for (const prior of acquired) registry.release(prior);\n return { ok: false, message: watcherFailureMessage(uri, result) };\n }\n acquired.push(uri);\n }\n return { ok: true, acquiredUris: acquired };\n}\n"]}
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/transport/shared.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,6EAA6E;AAC7E,2EAA2E;AAC3E,4BAA4B;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,OAAO,EACL,YAAY,GAGb,MAAM,6BAA6B,CAAC;AAerC;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAgB;IACxD,MAAM,MAAM,GAAG,eAAe,CAAC,0BAA0B,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACzF,OAAO,CAAC,CAAC,QAAQ,IAAI,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,gBAAgB,CAAC,UAAmB;IAClD,MAAM,EAAE,GAAI,UAAkD,EAAE,EAAE,CAAC;IACnE,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,sBAAsB,CAAC,UAAmB;IACxD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACrE,MAAM,IAAI,GAAG,UAGZ,CAAC;IACF,IAAI,IAAI,CAAC,MAAM,KAAK,sBAAsB;QAAE,OAAO,EAAE,CAAC;IACtD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,qBAAqB,CAAC;IAC/D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACpC,0EAA0E;IAC1E,wEAAwE;IACxE,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAiB,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAMD,MAAM,uBAAuB,GAAG;IAC9B,SAAS,EAAE,0BAA0B;IACrC,MAAM,EAAE,iBAAiB,YAAY,UAAU;IAC/C,eAAe,EAAE,yCAAyC;IAC1D,KAAK,EAAE,yCAAyC;CACxC,CAAC;AAEX,SAAS,qBAAqB,CAC5B,GAAW,EACX,MAAkD;IAElD,MAAM,GAAG,GACP,MAAM,CAAC,MAAM,KAAK,cAAc;QAC9B,CAAC,CAAC,yBAAyB,CAAC,MAAM,CAAC,KAAK,CAAC;QACzC,CAAC,CAAC,uBAAuB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC7C,OAAO,uBAAuB,GAAG,KAAK,GAAG,EAAE,CAAC;AAC9C,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,UAAmB,EACnB,SAAoB,EACpB,QAAyB,EACzB,MAA6B;IAE7B,MAAM,IAAI,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAC9D,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,KAAK,MAAM,KAAK,IAAI,QAAQ;gBAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACtD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC;QACpE,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;AAC9C,CAAC","sourcesContent":["// Pieces both transport legs share: the runtime config contract and the\n// `subscriptions/listen` watcher-preparation ladder. stdio and HTTP gate the\n// same message shape; only where the lease is released differs (connection\n// close vs response close).\nimport { specTypeSchemas } from '@modelcontextprotocol/server';\n\nimport { formatUnknownErrorMessage } from '../core/errors.js';\nimport type { PathGuard } from '../core/path.js';\nimport {\n MAX_WATCHERS,\n type WatcherAttachResult,\n type WatcherRegistry,\n} from '../core/watcher-registry.js';\n\n/**\n * Runtime inputs the CLI resolves once (flag, else the operator's env var) and\n * hands the transport. Separate from `ServerOptions` because that object is\n * `PathGuard`'s constructor argument: the filesystem guard has no use for a\n * bind address and no business holding a bearer secret.\n */\nexport interface RuntimeConfig {\n /** `--http-host` or `HTTP_HOST`. The HTTP bind defaults to loopback without it. */\n httpHost?: string;\n /** `--api-key` or `API_KEY`. Unset means open access (loopback dev mode). */\n apiKey?: string;\n}\n\n/**\n * True when `message` really is a `subscriptions/listen` request, not merely a\n * body carrying that method string. Both legs gate watcher attachment on this:\n * a malformed listen cannot succeed downstream, so creating and tearing down\n * `fs.watch` handles for it is pure waste — and on HTTP the teardown depended on\n * the response-close listener firing. Non-listen bodies fail it too, which is\n * the same answer `listenSubscriptionUris` gives them.\n */\nexport function isStructurallyValidListen(message: unknown): boolean {\n const result = specTypeSchemas.SubscriptionsListenRequest['~standard'].validate(message);\n return !('issues' in result);\n}\n\n/** The request id of a parsed JSON-RPC body, for error-envelope echo. */\nexport function jsonRpcRequestId(parsedBody: unknown): string | number | null {\n const id = (parsedBody as { id?: unknown } | null | undefined)?.id;\n return typeof id === 'string' || typeof id === 'number' ? id : null;\n}\n\n/** The `resourceSubscriptions` URIs of a `subscriptions/listen` body, de-duplicated. */\nexport function listenSubscriptionUris(parsedBody: unknown): string[] {\n if (typeof parsedBody !== 'object' || parsedBody === null) return [];\n const body = parsedBody as {\n method?: unknown;\n params?: { notifications?: { resourceSubscriptions?: unknown } };\n };\n if (body.method !== 'subscriptions/listen') return [];\n const uris = body.params?.notifications?.resourceSubscriptions;\n if (!Array.isArray(uris)) return [];\n // De-duplicate: one attach must yield one ref-count, or the release below\n // decrements further than it incremented and tears down a live watcher.\n return [...new Set(uris.filter((uri): uri is string => typeof uri === 'string'))];\n}\n\nexport type ListenPreparation =\n | { readonly ok: true; readonly acquiredUris: string[] }\n | { readonly ok: false; readonly message: string };\n\nconst WATCHER_FAILURE_REASONS = {\n 'bad-uri': 'unsupported resource URI',\n capped: `watcher limit ${MAX_WATCHERS} reached`,\n 'attach-failed': 'filesystem watcher could not be created',\n stale: 'subscription was cancelled during setup',\n} as const;\n\nfunction watcherFailureMessage(\n uri: string,\n result: Exclude<WatcherAttachResult, { ok: true }>,\n): string {\n const why =\n result.reason === 'invalid-path'\n ? formatUnknownErrorMessage(result.error)\n : WATCHER_FAILURE_REASONS[result.reason];\n return `Cannot subscribe to ${uri}: ${why}`;\n}\n\n/**\n * Prepare every filesystem watcher named by a `subscriptions/listen` filter.\n * The batch is all-or-nothing: a failed URI releases each prior lease.\n */\nexport async function prepareListenWatchers(\n parsedBody: unknown,\n pathGuard: PathGuard,\n registry: WatcherRegistry,\n notify: (uri: string) => void,\n): Promise<ListenPreparation> {\n const uris = listenSubscriptionUris(parsedBody);\n const acquired: string[] = [];\n for (const uri of uris) {\n const result = await registry.acquire(pathGuard, uri, notify);\n if (!result.ok) {\n for (const prior of acquired) registry.release(prior);\n return { ok: false, message: watcherFailureMessage(uri, result) };\n }\n acquired.push(uri);\n }\n return { ok: true, acquiredUris: acquired };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/transport/stdio.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,oCAAoC,CAAC;AAM5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAE5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAgCjD;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,uBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqCvF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,GAAE,aAAkB,GAAG,iBAAiB,CA+LjG"}
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/transport/stdio.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,oCAAoC,CAAC;AAM5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAE5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAgCjD;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,uBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqCvF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,GAAE,aAAkB,GAAG,iBAAiB,CA0OjG"}
@@ -91,6 +91,7 @@ export async function seedRootsFromClient(ctx) {
91
91
  * rejection, graceful completion, or connection close.
92
92
  */
93
93
  export function startServer(options, config = {}) {
94
+ let closed = false;
94
95
  let activeCtx;
95
96
  const pathGuard = new PathGuard(options, true);
96
97
  let pathGuardReady;
@@ -109,6 +110,14 @@ export function startServer(options, config = {}) {
109
110
  era,
110
111
  ...(config.apiKey !== undefined ? { apiKey: config.apiKey } : {}),
111
112
  });
113
+ if (closed) {
114
+ // The connection went away while this instance was being built.
115
+ // `cleanupConnection` already ran and its `closed` guard makes every later
116
+ // call a no-op, so publishing `c` would strand it: nothing would dispose
117
+ // it. Return it unactivated — the wire is gone, so nothing serves it.
118
+ c.disposeRuntimeState();
119
+ return c.mcp;
120
+ }
112
121
  activeCtx = c;
113
122
  if (era === 'legacy') {
114
123
  // Fires when the client's `notifications/initialized` lands. Safe to own:
@@ -152,6 +161,39 @@ export function startServer(options, config = {}) {
152
161
  for (const uri of state.acquiredUris)
153
162
  registry.release(uri);
154
163
  };
164
+ const cleanupConnection = () => {
165
+ if (closed)
166
+ return;
167
+ closed = true;
168
+ for (const state of listens.values())
169
+ state.cancelled = true;
170
+ listens.clear();
171
+ registry.destroy();
172
+ const ctx = activeCtx;
173
+ activeCtx = undefined;
174
+ // Both steps below are guarded separately, and for the same reason: this
175
+ // also runs from `wire.onclose`, where a throw is an uncaught exception on
176
+ // the stdin close event rather than a rejected close(). One shared catch
177
+ // would let a failed disposal skip the unref and strand the process.
178
+ try {
179
+ ctx?.disposeRuntimeState();
180
+ }
181
+ catch {
182
+ /* idempotent — disposeRuntimeState guards cleanedUp */
183
+ }
184
+ try {
185
+ // The SDK's transport close pauses stdin only when no other 'data'
186
+ // listener is left, so a fatal read error (a ReadBuffer overflow) tears
187
+ // the connection down but leaves the handle referenced and this process
188
+ // alive with nothing left to serve. Unref rather than pause: a listener
189
+ // the SDK still owns keeps receiving data, the loop just stops being held
190
+ // open for it.
191
+ process.stdin.unref();
192
+ }
193
+ catch {
194
+ /* a file-backed stdin is an `fs.ReadStream`, which has no `unref` */
195
+ }
196
+ };
155
197
  const send = wire.send.bind(wire);
156
198
  wire.send = async (message) => {
157
199
  if ('id' in message && ('result' in message || 'error' in message)) {
@@ -168,6 +210,15 @@ export function startServer(options, config = {}) {
168
210
  Logger.error('[Stdio] serve error:', formatUnknownErrorMessage(error));
169
211
  },
170
212
  });
213
+ const onclose = wire.onclose;
214
+ wire.onclose = () => {
215
+ try {
216
+ cleanupConnection();
217
+ }
218
+ finally {
219
+ onclose?.();
220
+ }
221
+ };
171
222
  // Wrap the callback `serveStdio` just installed. Listen requests are admitted
172
223
  // in order so two requests naming the same URI cannot race the registry's
173
224
  // ref-count. Cancellation is serialized with pending admission by request ID:
@@ -262,17 +313,12 @@ export function startServer(options, config = {}) {
262
313
  };
263
314
  return {
264
315
  close: async () => {
265
- for (const id of [...listens.keys()])
266
- releaseListen(id);
267
- registry.destroy();
268
316
  try {
269
- activeCtx?.disposeRuntimeState();
317
+ cleanupConnection();
270
318
  }
271
- catch {
272
- /* idempotent — disposeRuntimeState guards cleanedUp */
319
+ finally {
320
+ await handle.close();
273
321
  }
274
- activeCtx = undefined;
275
- await handle.close();
276
322
  },
277
323
  };
278
324
  }
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../src/transport/stdio.ts"],"names":[],"mappings":"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","sourcesContent":["// stdio hosting: the pinned-instance factory, legacy roots seeding, and the\n// gated transport that attaches listen-filter watchers before the SDK sees the\n// message.\nimport type { McpServerFactory } from '@modelcontextprotocol/server';\nimport { JSONRPC_VERSION, ProtocolErrorCode } from '@modelcontextprotocol/server';\nimport {\n serveStdio,\n type StdioServerHandle,\n StdioServerTransport,\n} from '@modelcontextprotocol/server/stdio';\n\nimport { fileURLToPath } from 'node:url';\n\nimport { formatUnknownErrorMessage } from '../core/errors.js';\nimport { Logger } from '../core/observability.js';\nimport type { ServerOptions } from '../core/path.js';\nimport { PathGuard } from '../core/path.js';\nimport { createWatcherRegistry } from '../core/watcher-registry.js';\nimport type { FilesystemServerContext } from '../server.js';\nimport { createServer } from '../server.js';\nimport type { RuntimeConfig } from './shared.js';\nimport {\n isStructurallyValidListen,\n jsonRpcRequestId,\n listenSubscriptionUris,\n prepareListenWatchers,\n} from './shared.js';\n\ninterface StdioListenState {\n acquiredUris: string[];\n cancelled: boolean;\n delivered: boolean;\n}\n\n// Read through a function, not `state.cancelled` directly: a cancellation lands\n// between the checks, and control-flow narrowing from the first one would\n// otherwise make the second read a compile-time constant.\nfunction isListenCancelled(state: StdioListenState): boolean {\n return state.cancelled;\n}\n\nfunction cancelledRequestId(message: unknown): string | number | null {\n if (typeof message !== 'object' || message === null) return null;\n const notification = message as {\n method?: unknown;\n params?: { requestId?: unknown };\n };\n if (notification.method !== 'notifications/cancelled') return null;\n const requestId = notification.params?.requestId;\n return typeof requestId === 'string' || typeof requestId === 'number' ? requestId : null;\n}\n\n/**\n * Seed the guard's allowed roots from the client's declared workspace roots.\n * Legacy-era only: push-style `roots/list` is deprecated (SEP-2577) and throws\n * on a 2026-07-28 connection, where clients pass paths as tool arguments and\n * the access-grant round-trip covers out-of-root paths instead. Every root\n * still passes `applyGrant`'s boundary and unsafe-path guards, so a client\n * cannot root-declare its way into $HOME or past FS_ROOT_BOUNDARY — a refused\n * root is skipped and its paths fail closed at validateAccess like any other.\n */\nexport async function seedRootsFromClient(ctx: FilesystemServerContext): Promise<number> {\n let roots: readonly { uri: string }[];\n try {\n // eslint-disable-next-line @typescript-eslint/no-deprecated -- legacy-era-only; the modern era never reaches the hooks that call this.\n ({ roots } = await ctx.mcp.server.listRoots());\n } catch (err: unknown) {\n // Client without the roots capability (strict-capabilities throw), or the\n // request failed — either way there is nothing to seed.\n Logger.debug('[Stdio] roots/list unavailable', { error: formatUnknownErrorMessage(err) });\n return 0;\n }\n let granted = 0;\n for (const root of roots) {\n let dir: string;\n try {\n dir = fileURLToPath(root.uri);\n } catch {\n continue; // not a file:// root\n }\n try {\n if (await ctx.pathGuard.applyGrant(dir)) granted += 1;\n else Logger.debug('[Stdio] client root refused by grant policy', { dir });\n } catch (err: unknown) {\n Logger.debug('[Stdio] client root grant failed', {\n dir,\n error: formatUnknownErrorMessage(err),\n });\n }\n }\n // No list-changed notification follows a grant. Allowed roots were once listed\n // as concrete resources, and are not any more: the file template's `list()`\n // returns nothing and the other two lists never read the roots at all (see\n // resources.ts). Notifying bought the client a re-fetch of an unchanged list.\n if (granted > 0) {\n Logger.info(`[Stdio] allowed ${granted} client-declared workspace root(s)`);\n }\n return granted;\n}\n\n/**\n * Serve filesystem-mcp over stdio using modern protocol revision 2026-07-28.\n *\n * The SDK's `StdioListenRouter` acknowledges `subscriptions/listen` and routes\n * the pinned instance's outbound change notifications onto the matching\n * streams, but it exposes no listen-filter hook, so nothing attaches the\n * filesystem watcher that would produce those notifications. `serveStdio`'s\n * `transport` option is the seam: it installs its own `onmessage` synchronously\n * and only then starts the wire, so wrapping that callback afterwards gives the\n * same view of the inbound `resourceSubscriptions` filter the HTTP leg reads off\n * `req.body`. The watcher's notify sink calls `sendResourceUpdated` on the\n * pinned instance, which the router then delivers to the listening stream.\n *\n * Watcher leases are tracked by listen request ID and released on cancellation,\n * rejection, graceful completion, or connection close.\n */\nexport function startServer(options: ServerOptions, config: RuntimeConfig = {}): StdioServerHandle {\n let activeCtx: FilesystemServerContext | undefined;\n const pathGuard = new PathGuard(options, true);\n let pathGuardReady: Promise<void> | undefined;\n const ensurePathGuard = (): Promise<void> => {\n pathGuardReady ??= pathGuard.recomputeAllowedDirectories();\n return pathGuardReady;\n };\n // Shared with the resource contract so a `resources/subscribe` and a\n // `subscriptions/listen` naming the same URI reuse one watcher.\n const registry = createWatcherRegistry();\n const factory: McpServerFactory = async ({ era }) => {\n await ensurePathGuard();\n const c = await createServer(options, {\n watcherRegistry: registry,\n pathGuard,\n era,\n ...(config.apiKey !== undefined ? { apiKey: config.apiKey } : {}),\n });\n activeCtx = c;\n if (era === 'legacy') {\n // Fires when the client's `notifications/initialized` lands. Safe to own:\n // the SDK's only touchpoint is its own initialized handler reading it.\n c.mcp.server.oninitialized = () => {\n void seedRootsFromClient(c);\n };\n // Re-list and grant anything new. Grants are session-additive (R8): a\n // root the client withdrew is not revoked mid-session.\n c.mcp.server.setNotificationHandler('notifications/roots/list_changed', () => {\n void seedRootsFromClient(c);\n });\n }\n return c.mcp;\n };\n\n // One sink for the whole connection. `addCallback` de-duplicates by function\n // identity, so re-using this closure makes a repeat `subscriptions/listen` on\n // an already-watched URI a no-op; a fresh closure per listen would instead\n // stack a callback every time, double-notifying and growing an\n // `activeCallbacks` set that MAX_WATCHERS does not bound (it caps watchers,\n // not callbacks). Reads `activeCtx` when it fires rather than capturing it, so\n // it never pins a disposed instance.\n const sink = (uri: string): void => {\n // A failed notify means the connection went away; nothing to recover.\n void activeCtx?.mcp.server.sendResourceUpdated({ uri }).catch((err: unknown) => {\n Logger.debug('[Stdio] resource update not delivered', {\n uri,\n error: formatUnknownErrorMessage(err),\n });\n });\n };\n\n const wire = new StdioServerTransport();\n const listens = new Map<string | number, StdioListenState>();\n\n // Deleting the entry is what makes this idempotent: a second call for the\n // same id finds nothing to release.\n const releaseListen = (id: string | number): void => {\n const state = listens.get(id);\n if (!state) return;\n listens.delete(id);\n for (const uri of state.acquiredUris) registry.release(uri);\n };\n\n const send = wire.send.bind(wire);\n wire.send = async (message) => {\n if ('id' in message && ('result' in message || 'error' in message)) {\n const id = message.id;\n if (id !== undefined) releaseListen(id);\n }\n await send(message);\n };\n\n const handle = serveStdio(factory, {\n legacy: 'serve',\n transport: wire,\n onerror: (error: unknown) => {\n Logger.error('[Stdio] serve error:', formatUnknownErrorMessage(error));\n },\n });\n\n // Wrap the callback `serveStdio` just installed. Listen requests are admitted\n // in order so two requests naming the same URI cannot race the registry's\n // ref-count. Cancellation is serialized with pending admission by request ID:\n // a pending request is suppressed and releases after preparation, while an\n // active request releases before the SDK closes its stream.\n //\n // serveStdio installs its onmessage synchronously and only then starts the\n // wire; this wrapper is only correct because of that ordering. Assert it\n // rather than degrade to a silent no-op if a future SDK release changes it.\n const deliver = wire.onmessage;\n if (!deliver) {\n throw new Error(\n 'serveStdio did not install a synchronous onmessage handler; the subscriptions/listen watcher gate cannot attach. This is an SDK contract change, not a configuration error.',\n );\n }\n let gated = Promise.resolve();\n wire.onmessage = (message) => {\n const cancelId = cancelledRequestId(message);\n if (cancelId !== null) {\n const state = listens.get(cancelId);\n if (!state) {\n deliver(message);\n return;\n }\n state.cancelled = true;\n if (state.delivered) {\n releaseListen(cancelId);\n deliver(message);\n }\n return;\n }\n\n const subscriptionUris = listenSubscriptionUris(message);\n if (subscriptionUris.length === 0) {\n deliver(message);\n return;\n }\n const id = jsonRpcRequestId(message);\n if (id === null || listens.has(id)) {\n deliver(message);\n return;\n }\n const state: StdioListenState = {\n acquiredUris: [],\n cancelled: false,\n delivered: false,\n };\n listens.set(id, state);\n gated = gated\n .then(async () => {\n if (!isStructurallyValidListen(message)) {\n listens.delete(id);\n deliver(message);\n return;\n }\n if (isListenCancelled(state)) {\n listens.delete(id);\n return;\n }\n await ensurePathGuard();\n const prepared = await prepareListenWatchers(message, pathGuard, registry, sink);\n if (!prepared.ok) {\n listens.delete(id);\n await wire.send({\n jsonrpc: JSONRPC_VERSION,\n id,\n error: { code: ProtocolErrorCode.InvalidParams, message: prepared.message },\n });\n return;\n }\n state.acquiredUris = prepared.acquiredUris;\n if (isListenCancelled(state)) {\n releaseListen(id);\n return;\n }\n state.delivered = true;\n deliver(message);\n })\n .catch(async (error: unknown) => {\n releaseListen(id);\n const failure =\n error instanceof Error ? error : new Error(formatUnknownErrorMessage(error));\n wire.onerror?.(failure);\n // The gate swallowed the message, so nothing downstream will answer it.\n // Without this the client waits out its whole request timeout.\n await wire\n .send({\n jsonrpc: JSONRPC_VERSION,\n id,\n error: { code: ProtocolErrorCode.InternalError, message: failure.message },\n })\n .catch(() => {\n /* the wire is gone; onerror above already reported it */\n });\n });\n };\n\n return {\n close: async () => {\n for (const id of [...listens.keys()]) releaseListen(id);\n registry.destroy();\n try {\n activeCtx?.disposeRuntimeState();\n } catch {\n /* idempotent — disposeRuntimeState guards cleanedUp */\n }\n activeCtx = undefined;\n await handle.close();\n },\n };\n}\n"]}
1
+ {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../src/transport/stdio.ts"],"names":[],"mappings":"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","sourcesContent":["// stdio hosting: the pinned-instance factory, legacy roots seeding, and the\n// gated transport that attaches listen-filter watchers before the SDK sees the\n// message.\nimport type { McpServerFactory } from '@modelcontextprotocol/server';\nimport { JSONRPC_VERSION, ProtocolErrorCode } from '@modelcontextprotocol/server';\nimport {\n serveStdio,\n type StdioServerHandle,\n StdioServerTransport,\n} from '@modelcontextprotocol/server/stdio';\n\nimport { fileURLToPath } from 'node:url';\n\nimport { formatUnknownErrorMessage } from '../core/errors.js';\nimport { Logger } from '../core/observability.js';\nimport type { ServerOptions } from '../core/path.js';\nimport { PathGuard } from '../core/path.js';\nimport { createWatcherRegistry } from '../core/watcher-registry.js';\nimport type { FilesystemServerContext } from '../server.js';\nimport { createServer } from '../server.js';\nimport type { RuntimeConfig } from './shared.js';\nimport {\n isStructurallyValidListen,\n jsonRpcRequestId,\n listenSubscriptionUris,\n prepareListenWatchers,\n} from './shared.js';\n\ninterface StdioListenState {\n acquiredUris: string[];\n cancelled: boolean;\n delivered: boolean;\n}\n\n// Read through a function, not `state.cancelled` directly: a cancellation lands\n// between the checks, and control-flow narrowing from the first one would\n// otherwise make the second read a compile-time constant.\nfunction isListenCancelled(state: StdioListenState): boolean {\n return state.cancelled;\n}\n\nfunction cancelledRequestId(message: unknown): string | number | null {\n if (typeof message !== 'object' || message === null) return null;\n const notification = message as {\n method?: unknown;\n params?: { requestId?: unknown };\n };\n if (notification.method !== 'notifications/cancelled') return null;\n const requestId = notification.params?.requestId;\n return typeof requestId === 'string' || typeof requestId === 'number' ? requestId : null;\n}\n\n/**\n * Seed the guard's allowed roots from the client's declared workspace roots.\n * Legacy-era only: push-style `roots/list` is deprecated (SEP-2577) and throws\n * on a 2026-07-28 connection, where clients pass paths as tool arguments and\n * the access-grant round-trip covers out-of-root paths instead. Every root\n * still passes `applyGrant`'s boundary and unsafe-path guards, so a client\n * cannot root-declare its way into $HOME or past FS_ROOT_BOUNDARY — a refused\n * root is skipped and its paths fail closed at validateAccess like any other.\n */\nexport async function seedRootsFromClient(ctx: FilesystemServerContext): Promise<number> {\n let roots: readonly { uri: string }[];\n try {\n // eslint-disable-next-line @typescript-eslint/no-deprecated -- legacy-era-only; the modern era never reaches the hooks that call this.\n ({ roots } = await ctx.mcp.server.listRoots());\n } catch (err: unknown) {\n // Client without the roots capability (strict-capabilities throw), or the\n // request failed — either way there is nothing to seed.\n Logger.debug('[Stdio] roots/list unavailable', { error: formatUnknownErrorMessage(err) });\n return 0;\n }\n let granted = 0;\n for (const root of roots) {\n let dir: string;\n try {\n dir = fileURLToPath(root.uri);\n } catch {\n continue; // not a file:// root\n }\n try {\n if (await ctx.pathGuard.applyGrant(dir)) granted += 1;\n else Logger.debug('[Stdio] client root refused by grant policy', { dir });\n } catch (err: unknown) {\n Logger.debug('[Stdio] client root grant failed', {\n dir,\n error: formatUnknownErrorMessage(err),\n });\n }\n }\n // No list-changed notification follows a grant. Allowed roots were once listed\n // as concrete resources, and are not any more: the file template's `list()`\n // returns nothing and the other two lists never read the roots at all (see\n // resources.ts). Notifying bought the client a re-fetch of an unchanged list.\n if (granted > 0) {\n Logger.info(`[Stdio] allowed ${granted} client-declared workspace root(s)`);\n }\n return granted;\n}\n\n/**\n * Serve filesystem-mcp over stdio using modern protocol revision 2026-07-28.\n *\n * The SDK's `StdioListenRouter` acknowledges `subscriptions/listen` and routes\n * the pinned instance's outbound change notifications onto the matching\n * streams, but it exposes no listen-filter hook, so nothing attaches the\n * filesystem watcher that would produce those notifications. `serveStdio`'s\n * `transport` option is the seam: it installs its own `onmessage` synchronously\n * and only then starts the wire, so wrapping that callback afterwards gives the\n * same view of the inbound `resourceSubscriptions` filter the HTTP leg reads off\n * `req.body`. The watcher's notify sink calls `sendResourceUpdated` on the\n * pinned instance, which the router then delivers to the listening stream.\n *\n * Watcher leases are tracked by listen request ID and released on cancellation,\n * rejection, graceful completion, or connection close.\n */\nexport function startServer(options: ServerOptions, config: RuntimeConfig = {}): StdioServerHandle {\n let closed = false;\n let activeCtx: FilesystemServerContext | undefined;\n const pathGuard = new PathGuard(options, true);\n let pathGuardReady: Promise<void> | undefined;\n const ensurePathGuard = (): Promise<void> => {\n pathGuardReady ??= pathGuard.recomputeAllowedDirectories();\n return pathGuardReady;\n };\n // Shared with the resource contract so a `resources/subscribe` and a\n // `subscriptions/listen` naming the same URI reuse one watcher.\n const registry = createWatcherRegistry();\n const factory: McpServerFactory = async ({ era }) => {\n await ensurePathGuard();\n const c = await createServer(options, {\n watcherRegistry: registry,\n pathGuard,\n era,\n ...(config.apiKey !== undefined ? { apiKey: config.apiKey } : {}),\n });\n if (closed) {\n // The connection went away while this instance was being built.\n // `cleanupConnection` already ran and its `closed` guard makes every later\n // call a no-op, so publishing `c` would strand it: nothing would dispose\n // it. Return it unactivated — the wire is gone, so nothing serves it.\n c.disposeRuntimeState();\n return c.mcp;\n }\n activeCtx = c;\n if (era === 'legacy') {\n // Fires when the client's `notifications/initialized` lands. Safe to own:\n // the SDK's only touchpoint is its own initialized handler reading it.\n c.mcp.server.oninitialized = () => {\n void seedRootsFromClient(c);\n };\n // Re-list and grant anything new. Grants are session-additive (R8): a\n // root the client withdrew is not revoked mid-session.\n c.mcp.server.setNotificationHandler('notifications/roots/list_changed', () => {\n void seedRootsFromClient(c);\n });\n }\n return c.mcp;\n };\n\n // One sink for the whole connection. `addCallback` de-duplicates by function\n // identity, so re-using this closure makes a repeat `subscriptions/listen` on\n // an already-watched URI a no-op; a fresh closure per listen would instead\n // stack a callback every time, double-notifying and growing an\n // `activeCallbacks` set that MAX_WATCHERS does not bound (it caps watchers,\n // not callbacks). Reads `activeCtx` when it fires rather than capturing it, so\n // it never pins a disposed instance.\n const sink = (uri: string): void => {\n // A failed notify means the connection went away; nothing to recover.\n void activeCtx?.mcp.server.sendResourceUpdated({ uri }).catch((err: unknown) => {\n Logger.debug('[Stdio] resource update not delivered', {\n uri,\n error: formatUnknownErrorMessage(err),\n });\n });\n };\n\n const wire = new StdioServerTransport();\n const listens = new Map<string | number, StdioListenState>();\n\n // Deleting the entry is what makes this idempotent: a second call for the\n // same id finds nothing to release.\n const releaseListen = (id: string | number): void => {\n const state = listens.get(id);\n if (!state) return;\n listens.delete(id);\n for (const uri of state.acquiredUris) registry.release(uri);\n };\n\n const cleanupConnection = (): void => {\n if (closed) return;\n closed = true;\n for (const state of listens.values()) state.cancelled = true;\n listens.clear();\n registry.destroy();\n const ctx = activeCtx;\n activeCtx = undefined;\n // Both steps below are guarded separately, and for the same reason: this\n // also runs from `wire.onclose`, where a throw is an uncaught exception on\n // the stdin close event rather than a rejected close(). One shared catch\n // would let a failed disposal skip the unref and strand the process.\n try {\n ctx?.disposeRuntimeState();\n } catch {\n /* idempotent — disposeRuntimeState guards cleanedUp */\n }\n try {\n // The SDK's transport close pauses stdin only when no other 'data'\n // listener is left, so a fatal read error (a ReadBuffer overflow) tears\n // the connection down but leaves the handle referenced and this process\n // alive with nothing left to serve. Unref rather than pause: a listener\n // the SDK still owns keeps receiving data, the loop just stops being held\n // open for it.\n process.stdin.unref();\n } catch {\n /* a file-backed stdin is an `fs.ReadStream`, which has no `unref` */\n }\n };\n\n const send = wire.send.bind(wire);\n wire.send = async (message) => {\n if ('id' in message && ('result' in message || 'error' in message)) {\n const id = message.id;\n if (id !== undefined) releaseListen(id);\n }\n await send(message);\n };\n\n const handle = serveStdio(factory, {\n legacy: 'serve',\n transport: wire,\n onerror: (error: unknown) => {\n Logger.error('[Stdio] serve error:', formatUnknownErrorMessage(error));\n },\n });\n const onclose = wire.onclose;\n wire.onclose = () => {\n try {\n cleanupConnection();\n } finally {\n onclose?.();\n }\n };\n\n // Wrap the callback `serveStdio` just installed. Listen requests are admitted\n // in order so two requests naming the same URI cannot race the registry's\n // ref-count. Cancellation is serialized with pending admission by request ID:\n // a pending request is suppressed and releases after preparation, while an\n // active request releases before the SDK closes its stream.\n //\n // serveStdio installs its onmessage synchronously and only then starts the\n // wire; this wrapper is only correct because of that ordering. Assert it\n // rather than degrade to a silent no-op if a future SDK release changes it.\n const deliver = wire.onmessage;\n if (!deliver) {\n throw new Error(\n 'serveStdio did not install a synchronous onmessage handler; the subscriptions/listen watcher gate cannot attach. This is an SDK contract change, not a configuration error.',\n );\n }\n let gated = Promise.resolve();\n wire.onmessage = (message) => {\n const cancelId = cancelledRequestId(message);\n if (cancelId !== null) {\n const state = listens.get(cancelId);\n if (!state) {\n deliver(message);\n return;\n }\n state.cancelled = true;\n if (state.delivered) {\n releaseListen(cancelId);\n deliver(message);\n }\n return;\n }\n\n const subscriptionUris = listenSubscriptionUris(message);\n if (subscriptionUris.length === 0) {\n deliver(message);\n return;\n }\n const id = jsonRpcRequestId(message);\n if (id === null || listens.has(id)) {\n deliver(message);\n return;\n }\n const state: StdioListenState = {\n acquiredUris: [],\n cancelled: false,\n delivered: false,\n };\n listens.set(id, state);\n gated = gated\n .then(async () => {\n if (!isStructurallyValidListen(message)) {\n listens.delete(id);\n deliver(message);\n return;\n }\n if (isListenCancelled(state)) {\n listens.delete(id);\n return;\n }\n await ensurePathGuard();\n const prepared = await prepareListenWatchers(message, pathGuard, registry, sink);\n if (!prepared.ok) {\n listens.delete(id);\n await wire.send({\n jsonrpc: JSONRPC_VERSION,\n id,\n error: { code: ProtocolErrorCode.InvalidParams, message: prepared.message },\n });\n return;\n }\n state.acquiredUris = prepared.acquiredUris;\n if (isListenCancelled(state)) {\n releaseListen(id);\n return;\n }\n state.delivered = true;\n deliver(message);\n })\n .catch(async (error: unknown) => {\n releaseListen(id);\n const failure =\n error instanceof Error ? error : new Error(formatUnknownErrorMessage(error));\n wire.onerror?.(failure);\n // The gate swallowed the message, so nothing downstream will answer it.\n // Without this the client waits out its whole request timeout.\n await wire\n .send({\n jsonrpc: JSONRPC_VERSION,\n id,\n error: { code: ProtocolErrorCode.InternalError, message: failure.message },\n })\n .catch(() => {\n /* the wire is gone; onerror above already reported it */\n });\n });\n };\n\n return {\n close: async () => {\n try {\n cleanupConnection();\n } finally {\n await handle.close();\n }\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "mcpName": "io.github.j0hanz/filesystem-mcp",
5
5
  "description": "Secure filesystem MCP server for reading, writing, searching, diffing, and patching files.",
6
6
  "author": "j0hanz",
@@ -40,8 +40,7 @@
40
40
  "test": "node --test --import tsx \"__tests__/**/*.test.ts\"",
41
41
  "test:inspector": "node --test --import tsx \"__tests__/inspector-*.test.ts\"",
42
42
  "inspector": "npm run build && npx -y @modelcontextprotocol/inspector node dist/index.js",
43
- "tasks": "node scripts/tasks.mjs",
44
- "qa": "npm run build && node scripts/qa.mjs"
43
+ "tasks": "node scripts/tasks.mjs"
45
44
  },
46
45
  "keywords": [
47
46
  "mcp",