@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 +1 -0
- package/dist/cli.js +13 -1
- package/dist/index.js +26 -8
- package/dist/server/bootstrap.d.ts +2 -0
- package/dist/server/bootstrap.js +184 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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>;
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -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 {
|
|
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';
|