@j0hanz/filesystem-mcp 1.2.3 → 1.2.4

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.
package/dist/cli.d.ts CHANGED
@@ -5,4 +5,5 @@ export declare class CliExitError extends Error {
5
5
  export declare function parseArgs(): Promise<{
6
6
  allowedDirs: string[];
7
7
  allowCwd: boolean;
8
+ port: number | undefined;
8
9
  }>;
package/dist/cli.js CHANGED
@@ -123,6 +123,7 @@ function createCliProgram(output) {
123
123
  .description('MCP filesystem server. Positional directories define allowed access roots.')
124
124
  .argument('[allowedDirs...]', 'Directories the MCP server can access on disk', parseAllowedDirArgument)
125
125
  .option('--allow_cwd, --allow-cwd', 'Allow the current working directory as an additional root')
126
+ .option('--port <number>', 'Enable HTTP transport on the given port (MCP Streamable HTTP with SSE)')
126
127
  .helpOption('-h, --help', 'Display command help')
127
128
  .version(SERVER_VERSION, '-v, --version', 'Display server version')
128
129
  .addHelpText('after', `
@@ -130,6 +131,7 @@ Examples:
130
131
  $ filesystem-mcp /path/to/allowed/dir
131
132
  $ filesystem-mcp --allow-cwd
132
133
  $ filesystem-mcp /project/src /project/tests --allow-cwd
134
+ $ filesystem-mcp --port 3000 /path/to/allowed/dir
133
135
  `);
134
136
  cli.allowUnknownOption(false);
135
137
  cli.allowExcessArguments(false);
@@ -171,6 +173,15 @@ function deduplicateAllowedDirectories(dirs) {
171
173
  }
172
174
  return deduplicated;
173
175
  }
176
+ function parsePortOption(raw) {
177
+ if (raw === undefined)
178
+ return undefined;
179
+ const n = Number(raw);
180
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
181
+ throw new CliExitError(`Error: --port must be an integer between 1 and 65535`, 1);
182
+ }
183
+ return n;
184
+ }
174
185
  export async function parseArgs() {
175
186
  const output = [];
176
187
  const cli = createCliProgram(output);
@@ -185,6 +196,7 @@ export async function parseArgs() {
185
196
  }
186
197
  const options = cli.opts();
187
198
  const allowCwd = options.allowCwd === true;
199
+ const port = parsePortOption(options.port);
188
200
  const positionals = getParsedAllowedDirs(cli);
189
201
  let allowedDirs;
190
202
  try {
@@ -195,5 +207,5 @@ export async function parseArgs() {
195
207
  throw new CliExitError(normalizeCliExitMessage(error), 1);
196
208
  }
197
209
  const deduplicatedDirs = deduplicateAllowedDirectories(allowedDirs);
198
- return { allowedDirs: deduplicatedDirs, allowCwd };
210
+ return { allowedDirs: deduplicatedDirs, allowCwd, port };
199
211
  }
package/dist/index.js CHANGED
@@ -5,9 +5,10 @@ import { DEFAULT_SEARCH_TIMEOUT_MS } from './lib/constants.js';
5
5
  import { formatUnknownErrorMessage } from './lib/errors.js';
6
6
  import { createTimedAbortSignal } from './lib/fs-helpers.js';
7
7
  import { setAllowedDirectoriesResolved } from './lib/path-validation.js';
8
- import { createServer, startServer } from './server.js';
8
+ import { createServer, startHttpServer, startServer } from './server.js';
9
9
  const SHUTDOWN_TIMEOUT_MS = 5000;
10
10
  let activeServer;
11
+ let activeHttpServer;
11
12
  let shutdownStarted = false;
12
13
  function isStdinEvent(event) {
13
14
  return event === 'end' || event === 'close';
@@ -31,6 +32,14 @@ async function shutdown(reason, exitCode = 0) {
31
32
  }, SHUTDOWN_TIMEOUT_MS);
32
33
  timer.unref();
33
34
  try {
35
+ if (activeHttpServer) {
36
+ const server = activeHttpServer;
37
+ await new Promise((resolve) => {
38
+ server.close(() => {
39
+ resolve();
40
+ });
41
+ });
42
+ }
34
43
  if (activeServer) {
35
44
  await activeServer.close();
36
45
  }
@@ -48,9 +57,10 @@ async function shutdown(reason, exitCode = 0) {
48
57
  async function main() {
49
58
  let allowedDirs;
50
59
  let allowCwd;
60
+ let port;
51
61
  try {
52
62
  const parsed = await parseArgs();
53
- ({ allowedDirs, allowCwd } = parsed);
63
+ ({ allowedDirs, allowCwd, port } = parsed);
54
64
  }
55
65
  catch (error) {
56
66
  if (error instanceof CliExitError) {
@@ -78,12 +88,20 @@ async function main() {
78
88
  else {
79
89
  console.error(`No directories specified via CLI. Will use MCP Roots${allowCwd ? ' or current working directory' : ''}.`);
80
90
  }
81
- const server = await createServer({
82
- allowCwd,
83
- cliAllowedDirs: allowedDirs,
84
- });
85
- activeServer = server;
86
- await startServer(server);
91
+ if (port !== undefined) {
92
+ activeHttpServer = await startHttpServer(port, {
93
+ allowCwd,
94
+ cliAllowedDirs: allowedDirs,
95
+ });
96
+ }
97
+ else {
98
+ const server = await createServer({
99
+ allowCwd,
100
+ cliAllowedDirs: allowedDirs,
101
+ });
102
+ activeServer = server;
103
+ await startServer(server);
104
+ }
87
105
  }
88
106
  registerShutdownTrigger('SIGTERM');
89
107
  registerShutdownTrigger('SIGINT');
@@ -1,4 +1,6 @@
1
+ import * as http from 'node:http';
1
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import type { ServerOptions } from './types.js';
3
4
  export declare function createServer(options?: ServerOptions): Promise<McpServer>;
4
5
  export declare function startServer(server: McpServer): Promise<void>;
6
+ export declare function startHttpServer(port: number, options: ServerOptions): Promise<http.Server>;
@@ -1,10 +1,13 @@
1
1
  import * as fs from 'node:fs/promises';
2
+ import * as http from 'node:http';
2
3
  import * as path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
3
5
  import { fileURLToPath } from 'node:url';
4
6
  import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
5
7
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
9
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
10
+ import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
11
  import { registerCompletions } from '../completions.js';
9
12
  import { formatUnknownErrorMessage } from '../lib/errors.js';
10
13
  import { createInMemoryResourceStore } from '../lib/resource-store.js';
@@ -115,3 +118,183 @@ export async function startServer(server) {
115
118
  };
116
119
  rootsManager.logMissingDirectoriesIfNeeded(server);
117
120
  }
121
+ async function readRequestBody(req) {
122
+ return new Promise((resolve, reject) => {
123
+ const chunks = [];
124
+ req.on('data', (chunk) => {
125
+ chunks.push(chunk);
126
+ });
127
+ req.on('end', () => {
128
+ const raw = Buffer.concat(chunks).toString('utf-8');
129
+ if (!raw) {
130
+ resolve(undefined);
131
+ return;
132
+ }
133
+ try {
134
+ resolve(JSON.parse(raw));
135
+ }
136
+ catch {
137
+ resolve(undefined);
138
+ }
139
+ });
140
+ req.on('error', reject);
141
+ });
142
+ }
143
+ async function createHttpSession(options, sessions) {
144
+ const mcpServer = await createServer(options);
145
+ const rootsManager = getRootsManager(mcpServer);
146
+ rootsManager.registerHandlers(mcpServer);
147
+ await rootsManager.recomputeAllowedDirectories();
148
+ const transport = new StreamableHTTPServerTransport({
149
+ sessionIdGenerator: () => randomUUID(),
150
+ onsessioninitialized: (sessionId) => {
151
+ sessions.set(sessionId, { server: mcpServer, transport });
152
+ rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
153
+ },
154
+ onsessionclosed: (sessionId) => {
155
+ sessions.delete(sessionId);
156
+ },
157
+ });
158
+ transport.onclose = () => {
159
+ const { sessionId } = transport;
160
+ if (sessionId) {
161
+ sessions.delete(sessionId);
162
+ }
163
+ rootsManager.destroy();
164
+ mcpServer.close().catch((err) => {
165
+ console.error('[HTTP] Error closing MCP server:', formatUnknownErrorMessage(err));
166
+ });
167
+ };
168
+ await mcpServer.connect(transport);
169
+ return { server: mcpServer, transport };
170
+ }
171
+ export async function startHttpServer(port, options) {
172
+ const sessions = new Map();
173
+ async function handleMcpRequest(req, res) {
174
+ const { method } = req;
175
+ const sessionId = req.headers['mcp-session-id'];
176
+ try {
177
+ if (method === 'POST') {
178
+ const body = await readRequestBody(req);
179
+ if (sessionId && sessions.has(sessionId)) {
180
+ const session = sessions.get(sessionId);
181
+ if (session) {
182
+ await session.transport.handleRequest(req, res, body);
183
+ }
184
+ else {
185
+ res.writeHead(400, { 'Content-Type': 'application/json' });
186
+ res.end(JSON.stringify({
187
+ jsonrpc: '2.0',
188
+ error: {
189
+ code: -32000,
190
+ message: 'Bad Request: Session not found',
191
+ },
192
+ id: null,
193
+ }));
194
+ }
195
+ }
196
+ else if (!sessionId && isInitializeRequest(body)) {
197
+ const { transport } = await createHttpSession(options, sessions);
198
+ await transport.handleRequest(req, res, body);
199
+ }
200
+ else {
201
+ res.writeHead(400, { 'Content-Type': 'application/json' });
202
+ res.end(JSON.stringify({
203
+ jsonrpc: '2.0',
204
+ error: {
205
+ code: -32000,
206
+ message: 'Bad Request: No valid session ID provided',
207
+ },
208
+ id: null,
209
+ }));
210
+ }
211
+ }
212
+ else if (method === 'GET') {
213
+ if (!sessionId || !sessions.has(sessionId)) {
214
+ res.writeHead(400, { 'Content-Type': 'application/json' });
215
+ res.end(JSON.stringify({
216
+ jsonrpc: '2.0',
217
+ error: {
218
+ code: -32000,
219
+ message: 'Bad Request: Invalid or missing session ID',
220
+ },
221
+ id: null,
222
+ }));
223
+ return;
224
+ }
225
+ const session = sessions.get(sessionId);
226
+ if (session) {
227
+ await session.transport.handleRequest(req, res);
228
+ }
229
+ else {
230
+ res.writeHead(400, { 'Content-Type': 'application/json' });
231
+ res.end(JSON.stringify({
232
+ jsonrpc: '2.0',
233
+ error: { code: -32000, message: 'Bad Request: Session not found' },
234
+ id: null,
235
+ }));
236
+ }
237
+ }
238
+ else if (method === 'DELETE') {
239
+ if (!sessionId || !sessions.has(sessionId)) {
240
+ res.writeHead(400, { 'Content-Type': 'application/json' });
241
+ res.end(JSON.stringify({
242
+ jsonrpc: '2.0',
243
+ error: {
244
+ code: -32000,
245
+ message: 'Bad Request: Invalid or missing session ID',
246
+ },
247
+ id: null,
248
+ }));
249
+ return;
250
+ }
251
+ const session = sessions.get(sessionId);
252
+ if (session) {
253
+ await session.transport.handleRequest(req, res);
254
+ }
255
+ else {
256
+ res.writeHead(400, { 'Content-Type': 'application/json' });
257
+ res.end(JSON.stringify({
258
+ jsonrpc: '2.0',
259
+ error: { code: -32000, message: 'Bad Request: Session not found' },
260
+ id: null,
261
+ }));
262
+ }
263
+ }
264
+ else {
265
+ res.writeHead(405, { Allow: 'GET, POST, DELETE' });
266
+ res.end('Method Not Allowed');
267
+ }
268
+ }
269
+ catch (error) {
270
+ console.error('[HTTP] Error handling request:', formatUnknownErrorMessage(error));
271
+ if (!res.headersSent) {
272
+ res.writeHead(500, { 'Content-Type': 'application/json' });
273
+ res.end(JSON.stringify({
274
+ jsonrpc: '2.0',
275
+ error: { code: -32603, message: 'Internal Server Error' },
276
+ id: null,
277
+ }));
278
+ }
279
+ }
280
+ }
281
+ const httpServer = http.createServer((req, res) => {
282
+ const urlPath = (req.url ?? '/').split('?')[0];
283
+ if (urlPath === '/mcp') {
284
+ handleMcpRequest(req, res).catch((err) => {
285
+ console.error('[HTTP] Unhandled error in request handler:', formatUnknownErrorMessage(err));
286
+ });
287
+ }
288
+ else {
289
+ res.writeHead(404);
290
+ res.end('Not Found');
291
+ }
292
+ });
293
+ return new Promise((resolve, reject) => {
294
+ httpServer.once('error', reject);
295
+ httpServer.listen(port, () => {
296
+ console.error(`MCP HTTP server listening on port ${port}`);
297
+ resolve(httpServer);
298
+ });
299
+ });
300
+ }
package/dist/server.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { createServer, startServer } from './server/bootstrap.js';
1
+ export { createServer, startHttpServer, startServer } from './server/bootstrap.js';
2
2
  export type { ServerOptions } from './server/types.js';
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- export { createServer, startServer } from './server/bootstrap.js';
1
+ export { createServer, startHttpServer, startServer } from './server/bootstrap.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "mcpName": "io.github.j0hanz/filesystem-mcp",
5
5
  "description": "MCP Server that enables LLMs to interact with the local filesystem.",
6
6
  "type": "module",