@j0hanz/filesystem-mcp 1.2.2 → 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.
Files changed (48) hide show
  1. package/README.md +11 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +15 -5
  4. package/dist/completions.js +15 -6
  5. package/dist/index.js +26 -8
  6. package/dist/lib/file-operations/glob-engine.js +3 -9
  7. package/dist/lib/file-operations/read-multiple-files.js +4 -23
  8. package/dist/lib/file-operations/search-content.js +11 -18
  9. package/dist/lib/observability.js +4 -4
  10. package/dist/lib/path-validation.js +22 -9
  11. package/dist/pkg-info.d.ts +6 -0
  12. package/dist/pkg-info.js +9 -0
  13. package/dist/schemas.d.ts +28 -28
  14. package/dist/schemas.js +26 -26
  15. package/dist/server/bootstrap.d.ts +6 -0
  16. package/dist/server/bootstrap.js +300 -0
  17. package/dist/server/capabilities.d.ts +10 -0
  18. package/dist/server/capabilities.js +40 -0
  19. package/dist/server/logging.d.ts +7 -0
  20. package/dist/server/logging.js +41 -0
  21. package/dist/server/roots-manager.d.ts +19 -0
  22. package/dist/server/roots-manager.js +173 -0
  23. package/dist/server/types.d.ts +4 -0
  24. package/dist/server/types.js +1 -0
  25. package/dist/server.d.ts +2 -8
  26. package/dist/server.js +1 -317
  27. package/dist/tools/apply-patch.js +3 -2
  28. package/dist/tools/calculate-hash.js +3 -2
  29. package/dist/tools/create-directory.js +3 -2
  30. package/dist/tools/delete-file.js +9 -2
  31. package/dist/tools/diff-files.js +3 -2
  32. package/dist/tools/edit-file.js +5 -4
  33. package/dist/tools/list-directory.js +13 -2
  34. package/dist/tools/move-file.js +11 -3
  35. package/dist/tools/read-multiple.js +6 -5
  36. package/dist/tools/read.js +3 -2
  37. package/dist/tools/replace-in-files.js +3 -2
  38. package/dist/tools/roots.js +12 -2
  39. package/dist/tools/search-content.js +10 -15
  40. package/dist/tools/search-files.js +13 -9
  41. package/dist/tools/shared.d.ts +3 -1
  42. package/dist/tools/shared.js +19 -1
  43. package/dist/tools/stat-many.js +6 -5
  44. package/dist/tools/stat.js +4 -3
  45. package/dist/tools/task-support.js +57 -10
  46. package/dist/tools/tree.js +15 -2
  47. package/dist/tools/write-file.js +3 -2
  48. package/package.json +1 -1
@@ -0,0 +1,300 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as http from 'node:http';
3
+ import * as path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
10
+ import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
11
+ import { registerCompletions } from '../completions.js';
12
+ import { formatUnknownErrorMessage } from '../lib/errors.js';
13
+ import { createInMemoryResourceStore } from '../lib/resource-store.js';
14
+ import { pkgInfo } from '../pkg-info.js';
15
+ import { registerGetHelpPrompt } from '../prompts.js';
16
+ import { registerInstructionResource, registerResultResources, } from '../resources.js';
17
+ import { registerAllTools } from '../tools.js';
18
+ import { withDefaultIcons } from '../tools/shared.js';
19
+ import { buildServerCapabilities, supportsTaskToolRequests, } from './capabilities.js';
20
+ import { createLoggingState } from './logging.js';
21
+ import { RootsManager } from './roots-manager.js';
22
+ const { version: SERVER_VERSION, description: SERVER_DESCRIPTION, homepage: SERVER_HOMEPAGE, } = pkgInfo;
23
+ const rootsManagers = new WeakMap();
24
+ function getRootsManager(server) {
25
+ const manager = rootsManagers.get(server);
26
+ if (!manager) {
27
+ throw new Error('Roots manager not initialized for server instance');
28
+ }
29
+ return manager;
30
+ }
31
+ async function loadServerInstructions() {
32
+ const defaultInstructions = `
33
+ Filesystem MCP Instructions
34
+ (Detailed instructions failed to load - check logs)
35
+ `;
36
+ try {
37
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
38
+ return await fs.readFile(path.join(currentDir, '../instructions.md'), 'utf-8');
39
+ }
40
+ catch (error) {
41
+ console.error('[WARNING] Failed to load instructions.md:', formatUnknownErrorMessage(error));
42
+ return defaultInstructions;
43
+ }
44
+ }
45
+ async function getLocalIconInfo() {
46
+ const name = 'logo.svg';
47
+ const mime = 'image/svg+xml';
48
+ const candidates = [`../assets/${name}`, `../../assets/${name}`];
49
+ for (const candidate of candidates) {
50
+ try {
51
+ const iconPath = new URL(candidate, import.meta.url);
52
+ const buffer = await fs.readFile(iconPath);
53
+ return {
54
+ src: `data:${mime};base64,${buffer.toString('base64')}`,
55
+ mimeType: mime,
56
+ };
57
+ }
58
+ catch {
59
+ // Try next candidate.
60
+ }
61
+ }
62
+ return undefined;
63
+ }
64
+ export async function createServer(options = {}) {
65
+ const resourceStore = createInMemoryResourceStore();
66
+ const serverInstructions = await loadServerInstructions();
67
+ const localIcon = await getLocalIconInfo();
68
+ const taskToolSupport = supportsTaskToolRequests();
69
+ const serverConfig = {
70
+ capabilities: buildServerCapabilities({
71
+ enablePromptListChanged: false,
72
+ enableTaskToolRequests: taskToolSupport,
73
+ }),
74
+ };
75
+ if (taskToolSupport) {
76
+ serverConfig.taskStore = new InMemoryTaskStore();
77
+ serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
78
+ }
79
+ if (serverInstructions) {
80
+ serverConfig.instructions = serverInstructions;
81
+ }
82
+ const server = new McpServer(withDefaultIcons({
83
+ name: 'filesystem-mcp',
84
+ title: 'Filesystem MCP',
85
+ version: SERVER_VERSION,
86
+ ...(SERVER_DESCRIPTION ? { description: SERVER_DESCRIPTION } : {}),
87
+ ...(SERVER_HOMEPAGE ? { websiteUrl: SERVER_HOMEPAGE } : {}),
88
+ }, localIcon), serverConfig);
89
+ const loggingState = createLoggingState('debug');
90
+ const rootsManager = new RootsManager(options, loggingState);
91
+ rootsManagers.set(server, rootsManager);
92
+ server.server.setRequestHandler(SetLevelRequestSchema, (req) => {
93
+ loggingState.minimumLevel = req.params.level;
94
+ return {};
95
+ });
96
+ registerInstructionResource(server, serverInstructions, localIcon);
97
+ registerGetHelpPrompt(server, serverInstructions, localIcon);
98
+ registerResultResources(server, resourceStore, localIcon);
99
+ registerCompletions(server);
100
+ registerAllTools(server, {
101
+ resourceStore,
102
+ isInitialized: () => rootsManager.isInitialized(),
103
+ ...(localIcon ? { iconInfo: localIcon } : {}),
104
+ });
105
+ return server;
106
+ }
107
+ export async function startServer(server) {
108
+ const transport = new StdioServerTransport();
109
+ const rootsManager = getRootsManager(server);
110
+ rootsManager.registerHandlers(server);
111
+ await rootsManager.recomputeAllowedDirectories();
112
+ await server.connect(transport);
113
+ const transportAny = transport;
114
+ const sdkOnClose = transportAny.onclose;
115
+ transportAny.onclose = () => {
116
+ rootsManager.destroy();
117
+ sdkOnClose?.();
118
+ };
119
+ rootsManager.logMissingDirectoriesIfNeeded(server);
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
+ }
@@ -0,0 +1,10 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export interface CapabilityOptions {
3
+ enablePromptListChanged?: boolean;
4
+ enableTaskToolRequests?: boolean;
5
+ }
6
+ type ServerCapabilities = NonNullable<ConstructorParameters<typeof McpServer>[1]>['capabilities'];
7
+ type NonOptionalServerCapabilities = NonNullable<ServerCapabilities>;
8
+ export declare function buildServerCapabilities(options?: CapabilityOptions): NonOptionalServerCapabilities;
9
+ export declare function supportsTaskToolRequests(): boolean;
10
+ export {};
@@ -0,0 +1,40 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ let cachedTaskToolSupport;
3
+ function detectTaskToolSupport() {
4
+ if (cachedTaskToolSupport !== undefined) {
5
+ return cachedTaskToolSupport;
6
+ }
7
+ try {
8
+ const probe = new McpServer({
9
+ name: 'filesystem-mcp-capability-probe',
10
+ version: '0.0.0',
11
+ }, { capabilities: { tools: {} } });
12
+ cachedTaskToolSupport =
13
+ typeof probe.experimental.tasks.registerToolTask === 'function';
14
+ void probe.close().catch(() => { });
15
+ }
16
+ catch {
17
+ cachedTaskToolSupport = false;
18
+ }
19
+ return cachedTaskToolSupport;
20
+ }
21
+ export function buildServerCapabilities(options = {}) {
22
+ const capabilities = {
23
+ logging: {},
24
+ resources: {},
25
+ tools: {},
26
+ prompts: options.enablePromptListChanged ? { listChanged: true } : {},
27
+ completions: {},
28
+ };
29
+ if (options.enableTaskToolRequests) {
30
+ capabilities.tasks = {
31
+ list: {},
32
+ cancel: {},
33
+ requests: { tools: { call: {} } },
34
+ };
35
+ }
36
+ return capabilities;
37
+ }
38
+ export function supportsTaskToolRequests() {
39
+ return detectTaskToolSupport();
40
+ }
@@ -0,0 +1,7 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { LoggingLevel } from '@modelcontextprotocol/sdk/types.js';
3
+ export interface LoggingState {
4
+ minimumLevel: LoggingLevel;
5
+ }
6
+ export declare function createLoggingState(minimumLevel?: LoggingLevel): LoggingState;
7
+ export declare function logToMcp(server: McpServer | undefined, level: LoggingLevel, data: string, minLevel?: LoggingLevel): void;
@@ -0,0 +1,41 @@
1
+ import { formatUnknownErrorMessage } from '../lib/errors.js';
2
+ import { isRecord } from '../lib/type-guards.js';
3
+ const MCP_LOGGER_NAME = 'filesystem-mcp';
4
+ const LOG_LEVEL_ORDER = {
5
+ debug: 0,
6
+ info: 1,
7
+ notice: 2,
8
+ warning: 3,
9
+ error: 4,
10
+ critical: 5,
11
+ alert: 6,
12
+ emergency: 7,
13
+ };
14
+ export function createLoggingState(minimumLevel = 'debug') {
15
+ return { minimumLevel };
16
+ }
17
+ function canSendMcpLogs(server) {
18
+ const capabilities = server.server.getClientCapabilities();
19
+ if (!isRecord(capabilities))
20
+ return false;
21
+ if (!('logging' in capabilities))
22
+ return false;
23
+ return capabilities.logging !== null;
24
+ }
25
+ export function logToMcp(server, level, data, minLevel = 'debug') {
26
+ if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[minLevel]) {
27
+ return;
28
+ }
29
+ if (!server || !canSendMcpLogs(server)) {
30
+ console.error(data);
31
+ return;
32
+ }
33
+ const params = {
34
+ level,
35
+ logger: MCP_LOGGER_NAME,
36
+ data,
37
+ };
38
+ void server.sendLoggingMessage(params).catch((error) => {
39
+ console.error(`Failed to send MCP log: ${level} | ${data}`, formatUnknownErrorMessage(error));
40
+ });
41
+ }
@@ -0,0 +1,19 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type LoggingState } from './logging.js';
3
+ import type { ServerOptions } from './types.js';
4
+ export declare class RootsManager {
5
+ private rootsUpdateTimeout;
6
+ private rootDirectories;
7
+ private clientInitialized;
8
+ private readonly options;
9
+ readonly loggingState: LoggingState;
10
+ constructor(options: ServerOptions, loggingState: LoggingState);
11
+ isInitialized(): boolean;
12
+ destroy(): void;
13
+ logMissingDirectoriesIfNeeded(server: McpServer): void;
14
+ registerHandlers(server: McpServer): void;
15
+ recomputeAllowedDirectories(): Promise<void>;
16
+ private scheduleRootsUpdate;
17
+ private logMissingDirectories;
18
+ private updateRootsFromClient;
19
+ }
@@ -0,0 +1,173 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { InitializedNotificationSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
3
+ import { z } from 'zod';
4
+ import { formatUnknownErrorMessage } from '../lib/errors.js';
5
+ import { assertNotAborted, createTimedAbortSignal, withAbort, } from '../lib/fs-helpers.js';
6
+ import { getAllowedDirectories, getValidRootDirectories, isPathWithinDirectories, normalizePath, setAllowedDirectoriesResolved, } from '../lib/path-validation.js';
7
+ import { isRecord } from '../lib/type-guards.js';
8
+ import { logToMcp } from './logging.js';
9
+ const ROOTS_TIMEOUT_MS = 5000;
10
+ const ROOTS_DEBOUNCE_MS = 100;
11
+ function normalizeCLIDirectories(dirs) {
12
+ const normalized = [];
13
+ for (const dir of dirs) {
14
+ const trimmed = dir.trim();
15
+ if (trimmed.length === 0)
16
+ continue;
17
+ normalized.push(normalizePath(trimmed));
18
+ }
19
+ return normalized;
20
+ }
21
+ const RootSchema = z.strictObject({
22
+ uri: z.string(),
23
+ name: z.string().optional(),
24
+ });
25
+ const RootsResponseSchema = z.object({
26
+ roots: z.array(RootSchema).optional(),
27
+ });
28
+ function isRoot(value) {
29
+ return isRecord(value) && typeof value['uri'] === 'string';
30
+ }
31
+ function normalizeRoot(root) {
32
+ return root.name ? { uri: root.uri, name: root.name } : { uri: root.uri };
33
+ }
34
+ function extractRoots(value) {
35
+ const parsed = RootsResponseSchema.safeParse(value);
36
+ if (!parsed.success || !parsed.data.roots) {
37
+ return [];
38
+ }
39
+ const roots = [];
40
+ for (const root of parsed.data.roots) {
41
+ if (isRoot(root)) {
42
+ roots.push(normalizeRoot(root));
43
+ }
44
+ }
45
+ return roots;
46
+ }
47
+ async function resolveRootDirectories(roots) {
48
+ if (roots.length === 0)
49
+ return [];
50
+ const { signal, cleanup } = createTimedAbortSignal(undefined, ROOTS_TIMEOUT_MS);
51
+ try {
52
+ return await getValidRootDirectories(roots, signal);
53
+ }
54
+ finally {
55
+ cleanup();
56
+ }
57
+ }
58
+ async function isRootWithinBaseline(normalizedRoot, baseline, signal) {
59
+ if (!isPathWithinDirectories(normalizedRoot, baseline)) {
60
+ return false;
61
+ }
62
+ try {
63
+ assertNotAborted(signal);
64
+ const realPath = await withAbort(fs.realpath(normalizedRoot), signal);
65
+ const normalizedReal = normalizePath(realPath);
66
+ return isPathWithinDirectories(normalizedReal, baseline);
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
72
+ async function filterRootsWithinBaseline(roots, baseline, signal) {
73
+ const normalizedBaseline = normalizeCLIDirectories(baseline);
74
+ const normalizedRoots = roots.map(normalizePath);
75
+ if (normalizedRoots.length === 0)
76
+ return [];
77
+ const results = await Promise.allSettled(normalizedRoots.map((normalizedRoot) => isRootWithinBaseline(normalizedRoot, normalizedBaseline, signal)));
78
+ return normalizedRoots.filter((_, i) => {
79
+ const result = results[i];
80
+ return result?.status === 'fulfilled' && result.value;
81
+ });
82
+ }
83
+ export class RootsManager {
84
+ rootsUpdateTimeout;
85
+ rootDirectories = [];
86
+ clientInitialized = false;
87
+ options;
88
+ loggingState;
89
+ constructor(options, loggingState) {
90
+ this.options = options;
91
+ this.loggingState = loggingState;
92
+ }
93
+ isInitialized() {
94
+ return this.clientInitialized;
95
+ }
96
+ destroy() {
97
+ if (this.rootsUpdateTimeout) {
98
+ clearTimeout(this.rootsUpdateTimeout);
99
+ this.rootsUpdateTimeout = undefined;
100
+ }
101
+ }
102
+ logMissingDirectoriesIfNeeded(server) {
103
+ if (getAllowedDirectories().length === 0) {
104
+ this.logMissingDirectories(server);
105
+ }
106
+ }
107
+ registerHandlers(server) {
108
+ server.server.setNotificationHandler(InitializedNotificationSchema, async () => {
109
+ this.clientInitialized = true;
110
+ await this.updateRootsFromClient(server);
111
+ });
112
+ server.server.setNotificationHandler(RootsListChangedNotificationSchema, () => {
113
+ if (!this.clientInitialized)
114
+ return;
115
+ this.scheduleRootsUpdate(server);
116
+ });
117
+ }
118
+ async recomputeAllowedDirectories() {
119
+ const cliAllowedDirs = normalizeCLIDirectories(this.options.cliAllowedDirs ?? []);
120
+ const allowCwd = this.options.allowCwd === true;
121
+ const allowCwdDirs = allowCwd ? [normalizePath(process.cwd())] : [];
122
+ const baseline = [...cliAllowedDirs, ...allowCwdDirs];
123
+ const { signal, cleanup } = createTimedAbortSignal(undefined, ROOTS_TIMEOUT_MS);
124
+ try {
125
+ const rootsToInclude = baseline.length > 0
126
+ ? await filterRootsWithinBaseline(this.rootDirectories, baseline, signal)
127
+ : this.rootDirectories;
128
+ const combined = [...baseline, ...rootsToInclude];
129
+ await setAllowedDirectoriesResolved(combined, signal);
130
+ }
131
+ finally {
132
+ cleanup();
133
+ }
134
+ }
135
+ scheduleRootsUpdate(server) {
136
+ if (this.rootsUpdateTimeout) {
137
+ this.rootsUpdateTimeout.refresh();
138
+ return;
139
+ }
140
+ this.rootsUpdateTimeout = setTimeout(() => {
141
+ this.rootsUpdateTimeout = undefined;
142
+ void this.updateRootsFromClient(server);
143
+ }, ROOTS_DEBOUNCE_MS);
144
+ this.rootsUpdateTimeout.unref();
145
+ }
146
+ logMissingDirectories(server) {
147
+ if (this.options.allowCwd) {
148
+ logToMcp(server, 'notice', 'No allowed directories specified. Using the current working directory as an allowed directory.', this.loggingState.minimumLevel);
149
+ return;
150
+ }
151
+ logToMcp(server, 'warning', 'No allowed directories specified. Please provide directories as command-line arguments or enable --allow-cwd to use the current working directory.', this.loggingState.minimumLevel);
152
+ }
153
+ async updateRootsFromClient(server) {
154
+ try {
155
+ const clientCapabilities = server.server.getClientCapabilities();
156
+ if (!clientCapabilities?.roots) {
157
+ this.rootDirectories = [];
158
+ return;
159
+ }
160
+ const rootsResult = await server.server.listRoots(undefined, {
161
+ timeout: ROOTS_TIMEOUT_MS,
162
+ });
163
+ const roots = extractRoots(rootsResult);
164
+ this.rootDirectories = await resolveRootDirectories(roots);
165
+ }
166
+ catch (error) {
167
+ logToMcp(server, 'debug', `[DEBUG] MCP Roots protocol unavailable or failed: ${formatUnknownErrorMessage(error)}`, this.loggingState.minimumLevel);
168
+ }
169
+ finally {
170
+ await this.recomputeAllowedDirectories();
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,4 @@
1
+ export interface ServerOptions {
2
+ allowCwd?: boolean;
3
+ cliAllowedDirs?: string[];
4
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/server.d.ts CHANGED
@@ -1,8 +1,2 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- interface ServerOptions {
3
- allowCwd?: boolean;
4
- cliAllowedDirs?: string[];
5
- }
6
- export declare function createServer(options?: ServerOptions): Promise<McpServer>;
7
- export declare function startServer(server: McpServer): Promise<void>;
8
- export {};
1
+ export { createServer, startHttpServer, startServer } from './server/bootstrap.js';
2
+ export type { ServerOptions } from './server/types.js';