@j0hanz/filesystem-mcp 1.19.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/README.md +333 -838
  2. package/dist/cli-help.d.ts +3 -0
  3. package/dist/cli-help.d.ts.map +1 -0
  4. package/dist/cli-help.js +169 -0
  5. package/dist/cli-help.js.map +1 -0
  6. package/dist/cli.d.ts +28 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +168 -123
  9. package/dist/cli.js.map +1 -0
  10. package/dist/core/concurrency.d.ts +70 -0
  11. package/dist/core/concurrency.d.ts.map +1 -0
  12. package/dist/core/concurrency.js +173 -0
  13. package/dist/core/concurrency.js.map +1 -0
  14. package/dist/core/config.d.ts +19 -0
  15. package/dist/core/config.d.ts.map +1 -0
  16. package/dist/core/config.js +11 -0
  17. package/dist/core/config.js.map +1 -0
  18. package/dist/core/cursor.d.ts +23 -0
  19. package/dist/core/cursor.d.ts.map +1 -0
  20. package/dist/core/cursor.js +62 -0
  21. package/dist/core/cursor.js.map +1 -0
  22. package/dist/core/errors.d.ts +96 -0
  23. package/dist/core/errors.d.ts.map +1 -0
  24. package/dist/core/errors.js +356 -0
  25. package/dist/core/errors.js.map +1 -0
  26. package/dist/core/file-uri.d.ts +24 -0
  27. package/dist/core/file-uri.d.ts.map +1 -0
  28. package/dist/core/file-uri.js +52 -0
  29. package/dist/core/file-uri.js.map +1 -0
  30. package/dist/core/fmt.d.ts +38 -0
  31. package/dist/core/fmt.d.ts.map +1 -0
  32. package/dist/core/fmt.js +109 -0
  33. package/dist/core/fmt.js.map +1 -0
  34. package/dist/core/fs.d.ts +108 -0
  35. package/dist/core/fs.d.ts.map +1 -0
  36. package/dist/core/fs.js +243 -0
  37. package/dist/core/fs.js.map +1 -0
  38. package/dist/core/glob.d.ts +41 -0
  39. package/dist/core/glob.d.ts.map +1 -0
  40. package/dist/core/glob.js +399 -0
  41. package/dist/core/glob.js.map +1 -0
  42. package/dist/core/input-required.d.ts +147 -0
  43. package/dist/core/input-required.d.ts.map +1 -0
  44. package/dist/core/input-required.js +261 -0
  45. package/dist/core/input-required.js.map +1 -0
  46. package/dist/core/mime.d.ts +15 -0
  47. package/dist/core/mime.d.ts.map +1 -0
  48. package/dist/core/mime.js +188 -0
  49. package/dist/core/mime.js.map +1 -0
  50. package/dist/core/observability.d.ts +10 -0
  51. package/dist/core/observability.d.ts.map +1 -0
  52. package/dist/core/observability.js +76 -0
  53. package/dist/core/observability.js.map +1 -0
  54. package/dist/core/page-store.d.ts +41 -0
  55. package/dist/core/page-store.d.ts.map +1 -0
  56. package/dist/core/page-store.js +74 -0
  57. package/dist/core/page-store.js.map +1 -0
  58. package/dist/core/path-completer.d.ts +10 -0
  59. package/dist/core/path-completer.d.ts.map +1 -0
  60. package/dist/core/path-completer.js +227 -0
  61. package/dist/core/path-completer.js.map +1 -0
  62. package/dist/core/path-discovery.d.ts +8 -0
  63. package/dist/core/path-discovery.d.ts.map +1 -0
  64. package/dist/core/path-discovery.js +105 -0
  65. package/dist/core/path-discovery.js.map +1 -0
  66. package/dist/core/path-utils.d.ts +9 -0
  67. package/dist/core/path-utils.d.ts.map +1 -0
  68. package/dist/core/path-utils.js +121 -0
  69. package/dist/core/path-utils.js.map +1 -0
  70. package/dist/core/path.d.ts +148 -0
  71. package/dist/core/path.d.ts.map +1 -0
  72. package/dist/core/path.js +674 -0
  73. package/dist/core/path.js.map +1 -0
  74. package/dist/core/primitives.d.ts +23 -0
  75. package/dist/core/primitives.d.ts.map +1 -0
  76. package/dist/core/primitives.js +54 -0
  77. package/dist/core/primitives.js.map +1 -0
  78. package/dist/core/read.d.ts +74 -0
  79. package/dist/core/read.d.ts.map +1 -0
  80. package/dist/core/read.js +562 -0
  81. package/dist/core/read.js.map +1 -0
  82. package/dist/core/schema.d.ts +139 -0
  83. package/dist/core/schema.d.ts.map +1 -0
  84. package/dist/core/schema.js +345 -0
  85. package/dist/core/schema.js.map +1 -0
  86. package/dist/core/search.d.ts +100 -0
  87. package/dist/core/search.d.ts.map +1 -0
  88. package/dist/core/search.js +248 -0
  89. package/dist/core/search.js.map +1 -0
  90. package/dist/core/sensitive.d.ts +6 -0
  91. package/dist/core/sensitive.d.ts.map +1 -0
  92. package/dist/core/sensitive.js +142 -0
  93. package/dist/core/sensitive.js.map +1 -0
  94. package/dist/core/store.d.ts +57 -0
  95. package/dist/core/store.d.ts.map +1 -0
  96. package/dist/core/store.js +162 -0
  97. package/dist/core/store.js.map +1 -0
  98. package/dist/core/util.d.ts +26 -0
  99. package/dist/core/util.d.ts.map +1 -0
  100. package/dist/core/util.js +66 -0
  101. package/dist/core/util.js.map +1 -0
  102. package/dist/core/watcher-registry.d.ts +68 -0
  103. package/dist/core/watcher-registry.d.ts.map +1 -0
  104. package/dist/core/watcher-registry.js +312 -0
  105. package/dist/core/watcher-registry.js.map +1 -0
  106. package/dist/http-policy.d.ts +121 -0
  107. package/dist/http-policy.d.ts.map +1 -0
  108. package/dist/http-policy.js +340 -0
  109. package/dist/http-policy.js.map +1 -0
  110. package/dist/index.d.ts +1 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +77 -41
  113. package/dist/index.js.map +1 -0
  114. package/dist/instructions.d.ts +12 -0
  115. package/dist/instructions.d.ts.map +1 -0
  116. package/dist/instructions.js +71 -0
  117. package/dist/instructions.js.map +1 -0
  118. package/dist/prompts.d.ts +8 -6
  119. package/dist/prompts.d.ts.map +1 -0
  120. package/dist/prompts.js +74 -168
  121. package/dist/prompts.js.map +1 -0
  122. package/dist/resources.d.ts +73 -9
  123. package/dist/resources.d.ts.map +1 -0
  124. package/dist/resources.js +392 -163
  125. package/dist/resources.js.map +1 -0
  126. package/dist/server.d.ts +62 -1
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +139 -1
  129. package/dist/server.js.map +1 -0
  130. package/dist/tools/batch.d.ts +89 -0
  131. package/dist/tools/batch.d.ts.map +1 -0
  132. package/dist/tools/batch.js +157 -0
  133. package/dist/tools/batch.js.map +1 -0
  134. package/dist/tools/create.d.ts +2 -0
  135. package/dist/tools/create.d.ts.map +1 -0
  136. package/dist/tools/create.js +142 -0
  137. package/dist/tools/create.js.map +1 -0
  138. package/dist/tools/define.d.ts +110 -0
  139. package/dist/tools/define.d.ts.map +1 -0
  140. package/dist/tools/define.js +394 -0
  141. package/dist/tools/define.js.map +1 -0
  142. package/dist/tools/delete-file.d.ts +2 -4
  143. package/dist/tools/delete-file.d.ts.map +1 -0
  144. package/dist/tools/delete-file.js +330 -87
  145. package/dist/tools/delete-file.js.map +1 -0
  146. package/dist/tools/diff.d.ts +2 -0
  147. package/dist/tools/diff.d.ts.map +1 -0
  148. package/dist/tools/diff.js +94 -0
  149. package/dist/tools/diff.js.map +1 -0
  150. package/dist/tools/edit.d.ts +2 -0
  151. package/dist/tools/edit.d.ts.map +1 -0
  152. package/dist/tools/edit.js +442 -0
  153. package/dist/tools/edit.js.map +1 -0
  154. package/dist/tools/index.d.ts +26 -0
  155. package/dist/tools/index.d.ts.map +1 -0
  156. package/dist/tools/index.js +52 -0
  157. package/dist/tools/index.js.map +1 -0
  158. package/dist/tools/list.d.ts +2 -0
  159. package/dist/tools/list.d.ts.map +1 -0
  160. package/dist/tools/list.js +291 -0
  161. package/dist/tools/list.js.map +1 -0
  162. package/dist/tools/move.d.ts +2 -0
  163. package/dist/tools/move.d.ts.map +1 -0
  164. package/dist/tools/move.js +321 -0
  165. package/dist/tools/move.js.map +1 -0
  166. package/dist/tools/patch.d.ts +2 -0
  167. package/dist/tools/patch.d.ts.map +1 -0
  168. package/dist/tools/patch.js +155 -0
  169. package/dist/tools/patch.js.map +1 -0
  170. package/dist/tools/progress.d.ts +52 -0
  171. package/dist/tools/progress.d.ts.map +1 -0
  172. package/dist/tools/progress.js +172 -0
  173. package/dist/tools/progress.js.map +1 -0
  174. package/dist/tools/read.d.ts +2 -4
  175. package/dist/tools/read.d.ts.map +1 -0
  176. package/dist/tools/read.js +398 -152
  177. package/dist/tools/read.js.map +1 -0
  178. package/dist/tools/replace-in-files.d.ts +2 -4
  179. package/dist/tools/replace-in-files.d.ts.map +1 -0
  180. package/dist/tools/replace-in-files.js +435 -264
  181. package/dist/tools/replace-in-files.js.map +1 -0
  182. package/dist/tools/roots.d.ts +2 -4
  183. package/dist/tools/roots.d.ts.map +1 -0
  184. package/dist/tools/roots.js +29 -52
  185. package/dist/tools/roots.js.map +1 -0
  186. package/dist/tools/search-content.d.ts +2 -4
  187. package/dist/tools/search-content.d.ts.map +1 -0
  188. package/dist/tools/search-content.js +262 -228
  189. package/dist/tools/search-content.js.map +1 -0
  190. package/dist/tools/search-files.d.ts +2 -4
  191. package/dist/tools/search-files.d.ts.map +1 -0
  192. package/dist/tools/search-files.js +162 -161
  193. package/dist/tools/search-files.js.map +1 -0
  194. package/dist/tools/stat.d.ts +2 -4
  195. package/dist/tools/stat.d.ts.map +1 -0
  196. package/dist/tools/stat.js +202 -62
  197. package/dist/tools/stat.js.map +1 -0
  198. package/dist/transport/http.d.ts +5 -0
  199. package/dist/transport/http.d.ts.map +1 -0
  200. package/dist/transport/http.js +276 -0
  201. package/dist/transport/http.js.map +1 -0
  202. package/dist/transport/shared.d.ts +45 -0
  203. package/dist/transport/shared.d.ts.map +1 -0
  204. package/dist/transport/shared.js +65 -0
  205. package/dist/transport/shared.js.map +1 -0
  206. package/dist/transport/stdio.d.ts +32 -0
  207. package/dist/transport/stdio.d.ts.map +1 -0
  208. package/dist/transport/stdio.js +279 -0
  209. package/dist/transport/stdio.js.map +1 -0
  210. package/dist/transport.d.ts +5 -0
  211. package/dist/transport.d.ts.map +1 -0
  212. package/dist/transport.js +4 -0
  213. package/dist/transport.js.map +1 -0
  214. package/package.json +37 -46
  215. package/dist/assets/logo.svg +0 -35
  216. package/dist/completions.d.ts +0 -2
  217. package/dist/completions.js +0 -572
  218. package/dist/config.d.ts +0 -119
  219. package/dist/config.js +0 -31
  220. package/dist/lib/abort.d.ts +0 -7
  221. package/dist/lib/abort.js +0 -81
  222. package/dist/lib/constants.d.ts +0 -42
  223. package/dist/lib/constants.js +0 -313
  224. package/dist/lib/errors.d.ts +0 -28
  225. package/dist/lib/errors.js +0 -301
  226. package/dist/lib/file-operations/core.d.ts +0 -53
  227. package/dist/lib/file-operations/core.js +0 -144
  228. package/dist/lib/file-operations/metadata.d.ts +0 -77
  229. package/dist/lib/file-operations/metadata.js +0 -878
  230. package/dist/lib/file-operations/search.d.ts +0 -46
  231. package/dist/lib/file-operations/search.js +0 -1194
  232. package/dist/lib/file-operations/traversal.d.ts +0 -40
  233. package/dist/lib/file-operations/traversal.js +0 -310
  234. package/dist/lib/fs-helpers.d.ts +0 -46
  235. package/dist/lib/fs-helpers.js +0 -606
  236. package/dist/lib/globs.d.ts +0 -2
  237. package/dist/lib/globs.js +0 -19
  238. package/dist/lib/logger.d.ts +0 -27
  239. package/dist/lib/logger.js +0 -91
  240. package/dist/lib/observability.d.ts +0 -33
  241. package/dist/lib/observability.js +0 -373
  242. package/dist/lib/paths.d.ts +0 -35
  243. package/dist/lib/paths.js +0 -634
  244. package/dist/lib/resource-store.d.ts +0 -28
  245. package/dist/lib/resource-store.js +0 -195
  246. package/dist/lib/utils.d.ts +0 -7
  247. package/dist/lib/utils.js +0 -37
  248. package/dist/lib/zod-codecs.d.ts +0 -2
  249. package/dist/lib/zod-codecs.js +0 -18
  250. package/dist/pkg-info.d.ts +0 -7
  251. package/dist/pkg-info.js +0 -9
  252. package/dist/resources/generated-instructions.d.ts +0 -1
  253. package/dist/resources/generated-instructions.js +0 -105
  254. package/dist/resources/tool-catalog.d.ts +0 -2
  255. package/dist/resources/tool-catalog.js +0 -78
  256. package/dist/resources/tool-info.d.ts +0 -9
  257. package/dist/resources/tool-info.js +0 -206
  258. package/dist/resources/workflows.d.ts +0 -1
  259. package/dist/resources/workflows.js +0 -72
  260. package/dist/schemas.d.ts +0 -505
  261. package/dist/schemas.js +0 -676
  262. package/dist/server/bootstrap.d.ts +0 -6
  263. package/dist/server/bootstrap.js +0 -585
  264. package/dist/server/event-store.d.ts +0 -18
  265. package/dist/server/event-store.js +0 -71
  266. package/dist/server/roots-manager.d.ts +0 -28
  267. package/dist/server/roots-manager.js +0 -227
  268. package/dist/server/task-store.d.ts +0 -10
  269. package/dist/server/task-store.js +0 -91
  270. package/dist/tools/apply-patch.d.ts +0 -4
  271. package/dist/tools/apply-patch.js +0 -235
  272. package/dist/tools/calculate-hash.d.ts +0 -4
  273. package/dist/tools/calculate-hash.js +0 -187
  274. package/dist/tools/contract.d.ts +0 -50
  275. package/dist/tools/contract.js +0 -1
  276. package/dist/tools/create-directory.d.ts +0 -4
  277. package/dist/tools/create-directory.js +0 -68
  278. package/dist/tools/diff-files.d.ts +0 -4
  279. package/dist/tools/diff-files.js +0 -136
  280. package/dist/tools/edit-file.d.ts +0 -4
  281. package/dist/tools/edit-file.js +0 -256
  282. package/dist/tools/icons.d.ts +0 -15
  283. package/dist/tools/icons.js +0 -24
  284. package/dist/tools/list-directory.d.ts +0 -4
  285. package/dist/tools/list-directory.js +0 -218
  286. package/dist/tools/move-file.d.ts +0 -4
  287. package/dist/tools/move-file.js +0 -178
  288. package/dist/tools/read-multiple.d.ts +0 -4
  289. package/dist/tools/read-multiple.js +0 -207
  290. package/dist/tools/shared.d.ts +0 -172
  291. package/dist/tools/shared.js +0 -599
  292. package/dist/tools/stat-many.d.ts +0 -4
  293. package/dist/tools/stat-many.js +0 -104
  294. package/dist/tools/task-support.d.ts +0 -29
  295. package/dist/tools/task-support.js +0 -473
  296. package/dist/tools/tree.d.ts +0 -4
  297. package/dist/tools/tree.js +0 -102
  298. package/dist/tools/write-file.d.ts +0 -4
  299. package/dist/tools/write-file.js +0 -60
  300. package/dist/tools.d.ts +0 -5
  301. package/dist/tools.js +0 -54
@@ -1,585 +0,0 @@
1
- import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
2
- import { InMemoryTaskMessageQueue, isInitializeRequest, localhostAllowedHostnames, McpServer, ProtocolErrorCode, StdioServerTransport, validateHostHeader, } from '@modelcontextprotocol/server';
3
- import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
4
- import { channel } from 'node:diagnostics_channel';
5
- import { readFile } from 'node:fs/promises';
6
- import { createServer as createHttpServer, } from 'node:http';
7
- import { inspect } from 'node:util';
8
- import { DEFAULT_LOG_LEVEL, getInitHandshakeTimeoutMs, INIT_TIMEOUT_CLOSE, parseEnvInt, } from '../lib/constants.js';
9
- import { formatUnknownErrorMessage } from '../lib/errors.js';
10
- import { createLoggingState, Logger, logToMcp, SessionContext, } from '../lib/logger.js';
11
- import { withAllowedDirectoriesState } from '../lib/paths.js';
12
- import { createInMemoryResourceStore } from '../lib/resource-store.js';
13
- import { registerCompletions } from '../completions.js';
14
- import { pkgInfo } from '../pkg-info.js';
15
- import { registerAnalyzePathPrompt, registerCompareFilesPrompt, registerGetHelpPrompt, registerGetToolHelpPrompt, } from '../prompts.js';
16
- import { registerInstructionResource, registerMetricsResource, registerResultResources, registerToolCatalogResource, registerToolInfoResource, registerWorkflowGuideResource, } from '../resources.js';
17
- import { buildServerInstructions } from '../resources/generated-instructions.js';
18
- import { registerAllTools } from '../tools.js';
19
- import { withDefaultIcons } from '../tools/shared.js';
20
- import { InMemoryEventStore } from './event-store.js';
21
- import { RootsManager } from './roots-manager.js';
22
- import { createTaskStore } from './task-store.js';
23
- function buildServerCapabilities(options = {}) {
24
- const capabilities = {
25
- logging: {},
26
- resources: { listChanged: true },
27
- tools: {},
28
- prompts: options.enablePromptListChanged ? { listChanged: true } : {},
29
- completions: {},
30
- extensions: {},
31
- };
32
- if (options.enableTaskToolRequests) {
33
- // NOTE: enabling task tool requests requires the caller to configure
34
- // an InMemoryTaskStore and InMemoryTaskMessageQueue on the McpServer.
35
- // InMemoryTaskStore auto-evicts tasks after TTL via setTimeout.
36
- capabilities.tasks = {
37
- list: {},
38
- cancel: {},
39
- requests: { tools: { call: {} } },
40
- };
41
- }
42
- return capabilities;
43
- }
44
- // Global map of all active servers by sessionId for routing logs
45
- const activeServers = new Map();
46
- // For stdio (single session without a specific ID)
47
- let stdioServer;
48
- function stringifyData(data) {
49
- if (data === undefined)
50
- return '';
51
- if (typeof data === 'string')
52
- return ` ${data}`;
53
- if (data === null ||
54
- typeof data === 'number' ||
55
- typeof data === 'boolean' ||
56
- typeof data === 'bigint') {
57
- return ` ${String(data)}`;
58
- }
59
- return ` ${inspect(data, { depth: 4, colors: false, compact: 3 })}`;
60
- }
61
- channel('filesystem-mcp:log').subscribe((message) => {
62
- const event = message;
63
- const target = event.sessionId
64
- ? activeServers.get(event.sessionId)
65
- : stdioServer;
66
- const dataStr = stringifyData(event.data);
67
- if (target) {
68
- logToMcp(target.server, event.level, `${event.message}${dataStr}`, target.loggingState.minimumLevel);
69
- }
70
- else {
71
- // Fallback if no server
72
- const fullMsg = `${event.message}${dataStr}`;
73
- console.error(`[${event.level.toUpperCase()}] ${fullMsg}`);
74
- }
75
- });
76
- const { version: SERVER_VERSION, description: SERVER_DESCRIPTION, homepage: SERVER_HOMEPAGE, } = pkgInfo;
77
- const rootsManagers = new WeakMap();
78
- function getRootsManager(server) {
79
- const manager = rootsManagers.get(server);
80
- if (!manager) {
81
- throw new Error('Roots manager not initialized for server instance');
82
- }
83
- return manager;
84
- }
85
- async function getLocalIconInfo() {
86
- const name = 'logo.svg';
87
- const mime = 'image/svg+xml';
88
- const candidates = [`../assets/${name}`, `../../assets/${name}`];
89
- for (const candidate of candidates) {
90
- try {
91
- const iconPath = new URL(candidate, import.meta.url);
92
- const buffer = await readFile(iconPath);
93
- return {
94
- src: `data:${mime};base64,${buffer.toString('base64')}`,
95
- mimeType: mime,
96
- };
97
- }
98
- catch {
99
- // Try next candidate.
100
- }
101
- }
102
- return undefined;
103
- }
104
- export async function createServer(options = {}) {
105
- const resourceStore = createInMemoryResourceStore();
106
- const serverInstructions = buildServerInstructions();
107
- const localIcon = await getLocalIconInfo();
108
- const capabilities = buildServerCapabilities({
109
- enablePromptListChanged: false,
110
- enableTaskToolRequests: true,
111
- });
112
- if (capabilities.tasks) {
113
- capabilities.tasks = {
114
- ...capabilities.tasks,
115
- taskStore: createTaskStore(),
116
- taskMessageQueue: new InMemoryTaskMessageQueue(),
117
- };
118
- }
119
- const serverConfig = { capabilities };
120
- if (serverInstructions) {
121
- serverConfig.instructions =
122
- 'filesystem-mcp: Secure local filesystem MCP server. ' +
123
- 'Start with: roots -> ls/find -> stat -> read. Never guess paths. ' +
124
- 'For full guidance, read internal://instructions or run the get-help prompt.';
125
- }
126
- const server = new McpServer(withDefaultIcons({
127
- name: 'filesystem-mcp',
128
- title: 'Filesystem MCP',
129
- version: SERVER_VERSION,
130
- ...(SERVER_DESCRIPTION ? { description: SERVER_DESCRIPTION } : {}),
131
- ...(SERVER_HOMEPAGE ? { websiteUrl: SERVER_HOMEPAGE } : {}),
132
- }, localIcon), serverConfig);
133
- const loggingState = createLoggingState(DEFAULT_LOG_LEVEL);
134
- const rootsManager = new RootsManager(options, loggingState);
135
- rootsManagers.set(server, rootsManager);
136
- // Subscribe to Logger channel if not already done, but we need to route based on session or fallback to this server if it's stdio.
137
- // Wait, in stdio there's only one server. In HTTP there are multiple.
138
- server.server.setRequestHandler('logging/setLevel', (req) => {
139
- loggingState.minimumLevel = req.params.level;
140
- Logger.notice(`Log level set to ${req.params.level}`);
141
- return {};
142
- });
143
- // Track stdio server by default, or it will be overwritten per HTTP session later
144
- stdioServer ??= { server, loggingState };
145
- registerInstructionResource(server, serverInstructions, localIcon);
146
- registerToolCatalogResource(server, localIcon);
147
- registerWorkflowGuideResource(server, localIcon);
148
- registerToolInfoResource(server, localIcon);
149
- registerGetHelpPrompt(server, serverInstructions, localIcon);
150
- registerCompareFilesPrompt(server, localIcon);
151
- registerAnalyzePathPrompt(server, localIcon);
152
- registerGetToolHelpPrompt(server, localIcon);
153
- registerResultResources(server, resourceStore, localIcon);
154
- registerMetricsResource(server, localIcon);
155
- registerCompletions(server, serverInstructions);
156
- registerAllTools(server, {
157
- resourceStore,
158
- isInitialized: () => rootsManager.isInitialized(),
159
- ...(localIcon ? { iconInfo: localIcon } : {}),
160
- });
161
- return server;
162
- }
163
- export async function startServer(server) {
164
- const transport = new StdioServerTransport();
165
- const rootsManager = getRootsManager(server);
166
- rootsManager.registerHandlers(server, INIT_TIMEOUT_CLOSE
167
- ? () => {
168
- void server.close();
169
- }
170
- : undefined);
171
- await rootsManager.recomputeAllowedDirectories();
172
- await server.connect(transport);
173
- const sdkOnClose = transport.onclose;
174
- transport.onclose = () => {
175
- rootsManager.destroy();
176
- sdkOnClose?.();
177
- };
178
- rootsManager.logMissingDirectoriesIfNeeded(server);
179
- }
180
- const MAX_REQUEST_BODY_BYTES = parseEnvInt('FS_CONTEXT_MAX_REQUEST_BYTES', 4 * 1024 * 1024, 1024, 256 * 1024 * 1024);
181
- class RequestBodyError extends Error {
182
- statusCode;
183
- constructor(message, statusCode) {
184
- super(message);
185
- this.statusCode = statusCode;
186
- this.name = 'RequestBodyError';
187
- }
188
- }
189
- async function readRequestBody(req) {
190
- return new Promise((resolve, reject) => {
191
- const chunks = [];
192
- let totalBytes = 0;
193
- let tooBig = false;
194
- req.on('data', (chunk) => {
195
- totalBytes += chunk.length;
196
- if (totalBytes > MAX_REQUEST_BODY_BYTES) {
197
- if (!tooBig) {
198
- tooBig = true;
199
- chunks.length = 0; // free accumulated memory
200
- req.pause(); // stop emitting data events; TCP window fills naturally
201
- reject(new RequestBodyError('Request body too large', 413));
202
- }
203
- return;
204
- }
205
- chunks.push(chunk);
206
- });
207
- req.on('end', () => {
208
- if (tooBig)
209
- return; // already rejected in 'data' handler
210
- const raw = Buffer.concat(chunks).toString('utf-8');
211
- if (!raw) {
212
- resolve(undefined);
213
- return;
214
- }
215
- try {
216
- resolve(JSON.parse(raw));
217
- }
218
- catch {
219
- reject(new RequestBodyError('Invalid JSON in request body', 400));
220
- }
221
- });
222
- req.on('error', reject);
223
- });
224
- }
225
- async function createHttpSession(options, sessions, eventStore) {
226
- const mcpServer = await createServer(options);
227
- const rootsManager = getRootsManager(mcpServer);
228
- rootsManager.registerHandlers(mcpServer);
229
- await rootsManager.recomputeAllowedDirectories();
230
- let cleanedUp = false;
231
- const cleanup = () => {
232
- if (cleanedUp)
233
- return;
234
- cleanedUp = true;
235
- const { sessionId } = transport;
236
- if (sessionId) {
237
- sessions.delete(sessionId);
238
- activeServers.delete(sessionId);
239
- eventStore.delete(sessionId);
240
- }
241
- rootsManager.destroy();
242
- };
243
- const close = async () => {
244
- cleanup();
245
- await mcpServer.close();
246
- };
247
- const transport = new NodeStreamableHTTPServerTransport({
248
- sessionIdGenerator: () => randomUUID(),
249
- eventStore,
250
- onsessioninitialized: (sessionId) => {
251
- sessions.set(sessionId, {
252
- server: mcpServer,
253
- rootsManager,
254
- transport,
255
- createdAt: Date.now(),
256
- cleanup,
257
- close,
258
- });
259
- activeServers.set(sessionId, {
260
- server: mcpServer,
261
- loggingState: rootsManager['loggingState'],
262
- });
263
- rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
264
- },
265
- });
266
- transport.onclose = cleanup;
267
- await mcpServer.connect(transport);
268
- return {
269
- server: mcpServer,
270
- rootsManager,
271
- transport,
272
- createdAt: Date.now(),
273
- cleanup,
274
- close,
275
- };
276
- }
277
- function sendJsonRpcError(res, status, code, message) {
278
- res.writeHead(status, { 'Content-Type': 'application/json' });
279
- res.end(JSON.stringify({
280
- jsonrpc: '2.0',
281
- error: { code, message },
282
- id: null,
283
- }));
284
- }
285
- const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/u;
286
- const MAX_SESSION_ID_LENGTH = 256;
287
- const MAX_BEARER_TOKEN_LENGTH = 4096;
288
- const JSON_RPC_SERVER_ERROR = -32000;
289
- const JSON_RPC_INVALID_REQUEST = ProtocolErrorCode.InvalidRequest;
290
- const JSON_RPC_PARSE_ERROR = ProtocolErrorCode.ParseError;
291
- const JSON_RPC_INTERNAL_ERROR = ProtocolErrorCode.InternalError;
292
- function isAllowedOrigin(origin) {
293
- if (origin === undefined)
294
- return true; // Non-browser clients omit Origin.
295
- return LOCALHOST_ORIGIN_RE.test(origin);
296
- }
297
- const EXPOSED_HEADERS = [
298
- 'WWW-Authenticate',
299
- 'Mcp-Session-Id',
300
- 'Mcp-Protocol-Version',
301
- ].join(', ');
302
- function setCorsHeaders(req, res) {
303
- const { origin } = req.headers;
304
- if (origin === undefined)
305
- return;
306
- res.setHeader('Access-Control-Allow-Origin', origin);
307
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
308
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id, Last-Event-ID');
309
- res.setHeader('Access-Control-Expose-Headers', EXPOSED_HEADERS);
310
- res.setHeader('Access-Control-Max-Age', '86400');
311
- }
312
- function normalizeAllowedHostname(host) {
313
- const trimmed = host.trim().toLowerCase();
314
- if (trimmed === '::1')
315
- return '[::1]';
316
- return trimmed;
317
- }
318
- function getAllowedHostnames(httpHost) {
319
- if (isLoopbackHttpHost(httpHost)) {
320
- return localhostAllowedHostnames().map(normalizeAllowedHostname);
321
- }
322
- const normalizedHost = normalizeAllowedHostname(httpHost);
323
- if (normalizedHost === '0.0.0.0' ||
324
- normalizedHost === '::' ||
325
- normalizedHost === '[::]') {
326
- return undefined;
327
- }
328
- return [normalizedHost];
329
- }
330
- function getSessionId(req) {
331
- const rawSessionId = req.headers['mcp-session-id'];
332
- return typeof rawSessionId === 'string' &&
333
- rawSessionId.length <= MAX_SESSION_ID_LENGTH
334
- ? rawSessionId
335
- : undefined;
336
- }
337
- function isAuthorizedBearer(apiKey, authHeader) {
338
- const bearerPrefix = 'Bearer ';
339
- if (typeof authHeader !== 'string' || !authHeader.startsWith(bearerPrefix)) {
340
- return false;
341
- }
342
- const userKey = authHeader.slice(bearerPrefix.length);
343
- if (userKey.length > MAX_BEARER_TOKEN_LENGTH) {
344
- return false;
345
- }
346
- const expectedHash = createHash('sha256').update(apiKey).digest();
347
- const actualHash = createHash('sha256').update(userKey).digest();
348
- return timingSafeEqual(expectedHash, actualHash);
349
- }
350
- function writeUnauthorizedResponse(res) {
351
- res.writeHead(401, {
352
- 'Content-Type': 'application/json',
353
- 'WWW-Authenticate': 'Bearer',
354
- });
355
- res.end(JSON.stringify({
356
- jsonrpc: '2.0',
357
- error: { code: JSON_RPC_SERVER_ERROR, message: 'Unauthorized' },
358
- id: null,
359
- }));
360
- }
361
- function ensureAuthorizedRequest(req, res) {
362
- const apiKey = process.env['FILESYSTEM_MCP_API_KEY'];
363
- if (!apiKey)
364
- return true;
365
- if (isAuthorizedBearer(apiKey, req.headers['authorization']))
366
- return true;
367
- writeUnauthorizedResponse(res);
368
- return false;
369
- }
370
- function ensureAllowedOrigin(req, res) {
371
- const { origin } = req.headers;
372
- if (isAllowedOrigin(origin)) {
373
- return true;
374
- }
375
- sendJsonRpcError(res, 403, JSON_RPC_SERVER_ERROR, 'Forbidden: disallowed origin');
376
- return false;
377
- }
378
- function ensureAllowedHostHeader(req, res, httpHost) {
379
- const allowedHostnames = getAllowedHostnames(httpHost);
380
- if (!allowedHostnames || allowedHostnames.length === 0) {
381
- return true;
382
- }
383
- const hostHeader = typeof req.headers['host'] === 'string' ? req.headers['host'] : undefined;
384
- const result = validateHostHeader(hostHeader, allowedHostnames);
385
- if (result.ok)
386
- return true;
387
- sendJsonRpcError(res, 403, JSON_RPC_SERVER_ERROR, `Forbidden: ${result.message}`);
388
- return false;
389
- }
390
- function discardRequestBody(req) {
391
- req.on('error', () => {
392
- // Best effort drain to avoid corrupting keep-alive pipelines.
393
- });
394
- req.resume();
395
- }
396
- async function handleSessionTransportRequest(session, req, res, body) {
397
- const store = session.transport.sessionId
398
- ? { sessionId: session.transport.sessionId }
399
- : {};
400
- await SessionContext.run(store, async () => {
401
- await withAllowedDirectoriesState(session.rootsManager.getAllowedDirectoriesState(), () => session.transport.handleRequest(req, res, body));
402
- });
403
- }
404
- function getSessionOrRespondNotFound(sessions, sessionId, res) {
405
- const session = sessions.get(sessionId);
406
- if (!session) {
407
- sendJsonRpcError(res, 404, JSON_RPC_SERVER_ERROR, 'Session not found');
408
- return undefined;
409
- }
410
- return session;
411
- }
412
- function isLoopbackHttpHost(host) {
413
- const normalized = host.trim().toLowerCase();
414
- return (normalized === '127.0.0.1' ||
415
- normalized === 'localhost' ||
416
- normalized === '::1' ||
417
- normalized === '[::1]');
418
- }
419
- function assertHttpBindingSecurity(host) {
420
- if (isLoopbackHttpHost(host))
421
- return;
422
- if (process.env['FILESYSTEM_MCP_API_KEY'])
423
- return;
424
- throw new Error(`Refusing to bind HTTP server to non-loopback host '${host}' without FILESYSTEM_MCP_API_KEY.`);
425
- }
426
- function writeMethodNotAllowedResponse(res) {
427
- res.writeHead(405, {
428
- Allow: 'GET, POST, DELETE, OPTIONS',
429
- 'Content-Type': 'application/json',
430
- });
431
- res.end(JSON.stringify({
432
- jsonrpc: '2.0',
433
- error: {
434
- code: JSON_RPC_SERVER_ERROR,
435
- message: 'Method Not Allowed',
436
- },
437
- id: null,
438
- }));
439
- }
440
- function handleHttpRequestError(error, res) {
441
- if (error instanceof RequestBodyError && !res.headersSent) {
442
- const rpcCode = error.statusCode === 413
443
- ? JSON_RPC_INVALID_REQUEST
444
- : JSON_RPC_PARSE_ERROR;
445
- res.setHeader('Connection', 'close');
446
- sendJsonRpcError(res, error.statusCode, rpcCode, error.message);
447
- return;
448
- }
449
- Logger.error('[HTTP] Error handling request:', formatUnknownErrorMessage(error));
450
- if (!res.headersSent) {
451
- sendJsonRpcError(res, 500, JSON_RPC_INTERNAL_ERROR, 'Internal Server Error');
452
- }
453
- }
454
- export async function startHttpServer(port, options) {
455
- const sessions = new Map();
456
- const eventStore = new InMemoryEventStore();
457
- const httpHost = process.env['FILESYSTEM_MCP_HTTP_HOST'] ?? '127.0.0.1';
458
- assertHttpBindingSecurity(httpHost);
459
- let closingSessions;
460
- async function closeAllSessions() {
461
- if (closingSessions)
462
- return closingSessions;
463
- closingSessions = (async () => {
464
- const activeSessions = [...sessions.values()];
465
- sessions.clear();
466
- eventStore.clear();
467
- await Promise.allSettled(activeSessions.map((session) => session.close()));
468
- })();
469
- await closingSessions;
470
- }
471
- async function handlePostRequest(req, res, sessionId) {
472
- if (sessionId) {
473
- const session = getSessionOrRespondNotFound(sessions, sessionId, res);
474
- if (!session) {
475
- discardRequestBody(req);
476
- return;
477
- }
478
- const body = await readRequestBody(req);
479
- await handleSessionTransportRequest(session, req, res, body);
480
- return;
481
- }
482
- const body = await readRequestBody(req);
483
- if (isInitializeRequest(body)) {
484
- const maxSessions = parseEnvInt('FILESYSTEM_MCP_MAX_HTTP_SESSIONS', 100, 1, 10_000);
485
- if (sessions.size >= maxSessions) {
486
- sendJsonRpcError(res, 503, JSON_RPC_SERVER_ERROR, 'Too many sessions');
487
- return;
488
- }
489
- const session = await createHttpSession(options, sessions, eventStore);
490
- await handleSessionTransportRequest(session, req, res, body);
491
- return;
492
- }
493
- sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: No valid session ID provided');
494
- discardRequestBody(req);
495
- }
496
- async function handleGetDeleteRequest(req, res, sessionId) {
497
- if (!sessionId) {
498
- sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: Missing session ID');
499
- return;
500
- }
501
- const session = getSessionOrRespondNotFound(sessions, sessionId, res);
502
- if (!session)
503
- return;
504
- await handleSessionTransportRequest(session, req, res);
505
- }
506
- async function dispatchMcpMethod(method, req, res, sessionId) {
507
- switch (method) {
508
- case 'OPTIONS':
509
- res.writeHead(204);
510
- res.end();
511
- return;
512
- case 'POST':
513
- await handlePostRequest(req, res, sessionId);
514
- return;
515
- case 'GET':
516
- case 'DELETE':
517
- await handleGetDeleteRequest(req, res, sessionId);
518
- return;
519
- default:
520
- writeMethodNotAllowedResponse(res);
521
- }
522
- }
523
- async function handleMcpRequest(req, res) {
524
- const sessionId = getSessionId(req);
525
- if (!ensureAllowedOrigin(req, res))
526
- return;
527
- setCorsHeaders(req, res);
528
- if (!ensureAllowedHostHeader(req, res, httpHost))
529
- return;
530
- if (!ensureAuthorizedRequest(req, res))
531
- return;
532
- try {
533
- await dispatchMcpMethod(req.method, req, res, sessionId);
534
- }
535
- catch (error) {
536
- handleHttpRequestError(error, res);
537
- }
538
- }
539
- const initHandshakeTimeoutMs = getInitHandshakeTimeoutMs();
540
- const SWEEP_INTERVAL_MS = initHandshakeTimeoutMs * 2;
541
- const sweepTimer = setInterval(() => {
542
- const now = Date.now();
543
- for (const [sessionId, session] of sessions) {
544
- if (!session.rootsManager.isInitialized() &&
545
- now - session.createdAt > initHandshakeTimeoutMs) {
546
- Logger.warn(`[HTTP] Evicting stale session ${sessionId}: client never sent notifications/initialized`);
547
- session.close().catch((err) => {
548
- Logger.error(`[HTTP] Error closing stale session ${sessionId}:`, formatUnknownErrorMessage(err));
549
- // Ensure event store is cleaned even if session.close() fails
550
- eventStore.delete(sessionId);
551
- });
552
- }
553
- }
554
- }, SWEEP_INTERVAL_MS);
555
- sweepTimer.unref();
556
- const httpServer = createHttpServer((req, res) => {
557
- const urlPath = (req.url ?? '/').split('?')[0];
558
- if (urlPath === '/mcp') {
559
- handleMcpRequest(req, res).catch((err) => {
560
- Logger.error('[HTTP] Unhandled error in request handler:', formatUnknownErrorMessage(err));
561
- });
562
- }
563
- else {
564
- res.writeHead(404);
565
- res.end('Not Found');
566
- }
567
- });
568
- httpServer.once('close', () => {
569
- clearInterval(sweepTimer);
570
- });
571
- const originalClose = httpServer.close.bind(httpServer);
572
- httpServer.close = ((callback) => {
573
- void closeAllSessions().catch((error) => {
574
- Logger.error('[HTTP] Error closing sessions before server shutdown:', formatUnknownErrorMessage(error));
575
- });
576
- return originalClose(callback);
577
- });
578
- return new Promise((resolve, reject) => {
579
- httpServer.once('error', reject);
580
- httpServer.listen(port, httpHost, () => {
581
- Logger.info(`MCP HTTP server listening on ${httpHost}:${port}`);
582
- resolve(httpServer);
583
- });
584
- });
585
- }
@@ -1,18 +0,0 @@
1
- import type { JSONRPCMessage } from '@modelcontextprotocol/server';
2
- export declare class InMemoryEventStore {
3
- private streams;
4
- private eventIdToStreamId;
5
- storeEvent(streamId: string, message: JSONRPCMessage): Promise<string>;
6
- getStreamIdForEventId(eventId: string): Promise<string | undefined>;
7
- replayEventsAfter(lastEventId: string, callbacks: {
8
- send: (eventId: string, message: JSONRPCMessage) => Promise<void>;
9
- }): Promise<string>;
10
- /**
11
- * Cleans up all events for a given streamId.
12
- */
13
- delete(streamId: string): void;
14
- /**
15
- * Cleans up all streams.
16
- */
17
- clear(): void;
18
- }
@@ -1,71 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- const MAX_EVENTS_PER_STREAM = 1000;
3
- export class InMemoryEventStore {
4
- // Map of streamId -> StoredEvent[]
5
- streams = new Map();
6
- // Map of eventId -> streamId for fast lookup
7
- eventIdToStreamId = new Map();
8
- storeEvent(streamId, message) {
9
- const eventId = randomUUID();
10
- let stream = this.streams.get(streamId);
11
- if (!stream) {
12
- stream = [];
13
- this.streams.set(streamId, stream);
14
- }
15
- // Add new event
16
- stream.push({ id: eventId, message });
17
- this.eventIdToStreamId.set(eventId, streamId);
18
- // Enforce limits
19
- if (stream.length > MAX_EVENTS_PER_STREAM) {
20
- const removed = stream.shift();
21
- if (removed) {
22
- this.eventIdToStreamId.delete(removed.id);
23
- }
24
- }
25
- return Promise.resolve(eventId);
26
- }
27
- getStreamIdForEventId(eventId) {
28
- return Promise.resolve(this.eventIdToStreamId.get(eventId));
29
- }
30
- async replayEventsAfter(lastEventId, callbacks) {
31
- const streamId = this.eventIdToStreamId.get(lastEventId);
32
- if (!streamId) {
33
- throw new Error(`Event ID ${lastEventId} not found or expired`);
34
- }
35
- const stream = this.streams.get(streamId);
36
- if (!stream) {
37
- throw new Error(`Stream ${streamId} not found`);
38
- }
39
- const eventIndex = stream.findIndex((e) => e.id === lastEventId);
40
- if (eventIndex === -1) {
41
- throw new Error(`Event ID ${lastEventId} not found in stream ${streamId}`);
42
- }
43
- // Replay all events after the found index
44
- for (let i = eventIndex + 1; i < stream.length; i++) {
45
- const event = stream[i];
46
- if (event) {
47
- await callbacks.send(event.id, event.message);
48
- }
49
- }
50
- return streamId;
51
- }
52
- /**
53
- * Cleans up all events for a given streamId.
54
- */
55
- delete(streamId) {
56
- const stream = this.streams.get(streamId);
57
- if (stream) {
58
- for (const event of stream) {
59
- this.eventIdToStreamId.delete(event.id);
60
- }
61
- this.streams.delete(streamId);
62
- }
63
- }
64
- /**
65
- * Cleans up all streams.
66
- */
67
- clear() {
68
- this.streams.clear();
69
- this.eventIdToStreamId.clear();
70
- }
71
- }