@j0hanz/filesystem-mcp 1.14.0 → 1.14.1
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/README.md +47 -47
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -13
- package/dist/config.js +12 -12
- package/dist/index.js +1 -1
- package/dist/lib/abort.d.ts +7 -0
- package/dist/lib/abort.js +81 -0
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +8 -2
- package/dist/lib/errors.d.ts +7 -3
- package/dist/lib/errors.js +59 -39
- package/dist/lib/file-operations/core.d.ts +3 -3
- package/dist/lib/file-operations/core.js +23 -20
- package/dist/lib/file-operations/metadata.d.ts +2 -2
- package/dist/lib/file-operations/metadata.js +60 -19
- package/dist/lib/file-operations/search.js +83 -84
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +20 -98
- package/dist/lib/globs.js +1 -1
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.js +91 -0
- package/dist/lib/observability.d.ts +7 -0
- package/dist/lib/observability.js +19 -9
- package/dist/lib/paths.js +55 -55
- package/dist/lib/resource-store.js +4 -4
- package/dist/lib/utils.d.ts +0 -12
- package/dist/lib/utils.js +0 -13
- package/dist/resources/generated-instructions.js +40 -31
- package/dist/resources/tool-catalog.js +32 -26
- package/dist/resources/tool-info.js +34 -29
- package/dist/resources/workflows.js +39 -18
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +66 -66
- package/dist/schemas.js +21 -44
- package/dist/server/bootstrap.d.ts +12 -11
- package/dist/server/bootstrap.js +95 -86
- package/dist/server/roots-manager.d.ts +5 -2
- package/dist/server/roots-manager.js +8 -6
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +26 -18
- package/dist/tools/calculate-hash.js +11 -22
- package/dist/tools/create-directory.js +10 -8
- package/dist/tools/delete-file.js +17 -15
- package/dist/tools/diff-files.js +15 -15
- package/dist/tools/edit-file.js +7 -4
- package/dist/tools/list-directory.js +6 -6
- package/dist/tools/move-file.js +98 -81
- package/dist/tools/read-multiple.js +4 -4
- package/dist/tools/read.js +5 -5
- package/dist/tools/replace-in-files.js +20 -23
- package/dist/tools/roots.js +1 -1
- package/dist/tools/search-content.js +32 -41
- package/dist/tools/search-files.js +54 -41
- package/dist/tools/shared.d.ts +3 -0
- package/dist/tools/shared.js +70 -28
- package/dist/tools/stat-many.js +11 -7
- package/dist/tools/stat.js +4 -4
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +3 -3
- package/dist/tools/write-file.js +10 -7
- package/package.json +9 -9
package/dist/schemas.js
CHANGED
|
@@ -20,6 +20,13 @@ const PathSchemaBase = z
|
|
|
20
20
|
.max(MAX_PATH_LENGTH, `Path too long (max ${MAX_PATH_LENGTH} chars)`);
|
|
21
21
|
const OptionalPathSchema = PathSchemaBase.optional();
|
|
22
22
|
const RequiredPathSchema = PathSchemaBase.min(1, 'Path required');
|
|
23
|
+
const SafeGlobPatternSchema = z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1, 'Pattern required')
|
|
26
|
+
.max(1000, 'Max 1000 chars')
|
|
27
|
+
.refine((val) => isSafeGlobPattern(val), {
|
|
28
|
+
error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
|
|
29
|
+
});
|
|
23
30
|
const FileTypeSchema = z.enum(['file', 'directory', 'symlink', 'other']);
|
|
24
31
|
const ListDirectorySortSchema = z.enum(['name', 'size', 'modified', 'type']);
|
|
25
32
|
const SearchFilesSortSchema = z.enum(['name', 'size', 'modified', 'path']);
|
|
@@ -37,7 +44,7 @@ const TreeEntrySchema = z.lazy(() => z.strictObject({
|
|
|
37
44
|
children: z.array(TreeEntrySchema).optional().describe('Children'),
|
|
38
45
|
}));
|
|
39
46
|
const ErrorSchema = z.strictObject({
|
|
40
|
-
code: z.enum(ErrorCode).describe('Error code (e.g.
|
|
47
|
+
code: z.enum(ErrorCode).describe('Error code (e.g. NOT_FOUND)'),
|
|
41
48
|
message: z.string().describe('Human-readable message'),
|
|
42
49
|
path: z.string().optional().describe('Relevant path'),
|
|
43
50
|
suggestion: z.string().optional().describe('Fix suggestion'),
|
|
@@ -126,15 +133,7 @@ export const ListDirectoryInputSchema = z.strictObject({
|
|
|
126
133
|
sortBy: ListDirectorySortSchema.optional()
|
|
127
134
|
.default('name')
|
|
128
135
|
.describe('Sort field (name, size, modified, type)'),
|
|
129
|
-
pattern:
|
|
130
|
-
.string()
|
|
131
|
-
.min(1, 'Pattern required')
|
|
132
|
-
.max(1000, 'Max 1000 chars')
|
|
133
|
-
.refine((val) => isSafeGlobPattern(val), {
|
|
134
|
-
error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
|
|
135
|
-
})
|
|
136
|
-
.optional()
|
|
137
|
-
.describe('Optional glob pattern filter (e.g. "**/*.ts")'),
|
|
136
|
+
pattern: SafeGlobPatternSchema.optional().describe('Optional glob pattern filter (e.g. "**/*.ts")'),
|
|
138
137
|
includeSymlinkTargets: defaultFalseBoolean('Resolve and include symlink targets in results'),
|
|
139
138
|
cursor: z
|
|
140
139
|
.string()
|
|
@@ -146,14 +145,7 @@ export const ListAllowedDirectoriesInputSchema = z
|
|
|
146
145
|
.describe('No input parameters.');
|
|
147
146
|
export const SearchFilesInputSchema = z.strictObject({
|
|
148
147
|
path: OptionalPathSchema.describe(DESC_PATH_ROOT),
|
|
149
|
-
pattern:
|
|
150
|
-
.string()
|
|
151
|
-
.min(1, 'Pattern required')
|
|
152
|
-
.max(1000, 'Max 1000 chars')
|
|
153
|
-
.refine((val) => isSafeGlobPattern(val), {
|
|
154
|
-
error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
|
|
155
|
-
})
|
|
156
|
-
.describe('Glob pattern (e.g. "**/*.ts", "src/*.js")'),
|
|
148
|
+
pattern: SafeGlobPatternSchema.describe('Glob pattern (e.g. "**/*.ts", "src/*.js")'),
|
|
157
149
|
maxResults: z
|
|
158
150
|
.int({ error: 'Must be integer' })
|
|
159
151
|
.min(1, 'Min: 1')
|
|
@@ -221,11 +213,7 @@ export const SearchContentInputSchema = z.strictObject({
|
|
|
221
213
|
.optional()
|
|
222
214
|
.default(DEFAULT_SEARCH_CONTENT_RESULTS)
|
|
223
215
|
.describe(`Maximum match rows to return. Default: ${DEFAULT_SEARCH_CONTENT_RESULTS}`),
|
|
224
|
-
filePattern:
|
|
225
|
-
.string()
|
|
226
|
-
.min(1, 'Pattern required')
|
|
227
|
-
.max(1000, 'Max 1000 chars')
|
|
228
|
-
.optional()
|
|
216
|
+
filePattern: SafeGlobPatternSchema.optional()
|
|
229
217
|
.default('**/*')
|
|
230
218
|
.describe('Glob for candidate files (e.g. "**/*.ts")'),
|
|
231
219
|
includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
|
|
@@ -449,6 +437,8 @@ export const EditFileInputSchema = z.strictObject({
|
|
|
449
437
|
.array(z.strictObject({
|
|
450
438
|
oldText: z
|
|
451
439
|
.string()
|
|
440
|
+
.min(1, 'oldText required')
|
|
441
|
+
.max(102400, 'Max 100KB')
|
|
452
442
|
.describe('Exact literal string to replace (character-for-character). Include 3–5 lines of context for unique targeting.'),
|
|
453
443
|
newText: z
|
|
454
444
|
.string()
|
|
@@ -621,15 +611,8 @@ export const ApplyPatchOutputSchema = z.strictObject({
|
|
|
621
611
|
});
|
|
622
612
|
export const SearchAndReplaceInputSchema = z.strictObject({
|
|
623
613
|
path: OptionalPathSchema.describe(DESC_PATH_ROOT),
|
|
624
|
-
filePattern:
|
|
625
|
-
.string()
|
|
626
|
-
.min(1, 'Pattern required')
|
|
627
|
-
.max(1000, 'Max 1000 chars')
|
|
628
|
-
.optional()
|
|
614
|
+
filePattern: SafeGlobPatternSchema.optional()
|
|
629
615
|
.default('**/*')
|
|
630
|
-
.refine((val) => isSafeGlobPattern(val), {
|
|
631
|
-
error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
|
|
632
|
-
})
|
|
633
616
|
.describe('Glob to filter files. Default: **/*'),
|
|
634
617
|
searchPattern: z
|
|
635
618
|
.string()
|
|
@@ -644,18 +627,9 @@ export const SearchAndReplaceInputSchema = z.strictObject({
|
|
|
644
627
|
.default(true)
|
|
645
628
|
.describe('Case-sensitive matching. Default: true.'),
|
|
646
629
|
dryRun: defaultFalseBoolean('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
|
|
647
|
-
includeHidden:
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
.describe('Include hidden files/directories (starting with .). Default: false.'),
|
|
651
|
-
includeIgnored: z
|
|
652
|
-
.boolean()
|
|
653
|
-
.optional()
|
|
654
|
-
.describe('Include .gitignore-ignored files (node_modules, dist). Default: false.'),
|
|
655
|
-
returnDiff: z
|
|
656
|
-
.boolean()
|
|
657
|
-
.optional()
|
|
658
|
-
.describe('Return unified diff of changes even if dryRun is false. Default: false.'),
|
|
630
|
+
includeHidden: defaultFalseBoolean('Include hidden files/directories (starting with .). Default: false.'),
|
|
631
|
+
includeIgnored: defaultFalseBoolean('Include .gitignore-ignored files (node_modules, dist). Default: false.'),
|
|
632
|
+
returnDiff: defaultFalseBoolean('Return unified diff of changes even if dryRun is false. Default: false.'),
|
|
659
633
|
maxFiles: z
|
|
660
634
|
.int({ error: 'Must be integer' })
|
|
661
635
|
.min(1, 'Min: 1')
|
|
@@ -687,7 +661,10 @@ export const SearchAndReplaceOutputSchema = z.strictObject({
|
|
|
687
661
|
.boolean()
|
|
688
662
|
.optional()
|
|
689
663
|
.describe('Changed file list truncated'),
|
|
690
|
-
diff: z
|
|
664
|
+
diff: z
|
|
665
|
+
.string()
|
|
666
|
+
.optional()
|
|
667
|
+
.describe('Unified diff of changes when `dryRun` or `returnDiff` is enabled'),
|
|
691
668
|
diffTruncated: z
|
|
692
669
|
.boolean()
|
|
693
670
|
.optional()
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import * as http from 'node:http';
|
|
2
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
-
import type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
import { type Server } from 'node:http';
|
|
3
|
+
import { type LoggingState } from '../lib/logger.js';
|
|
4
|
+
import { type ServerOptions } from './roots-manager.js';
|
|
5
|
+
export declare const activeServers: Map<string, {
|
|
6
|
+
server: McpServer;
|
|
7
|
+
loggingState: LoggingState;
|
|
8
|
+
}>;
|
|
9
|
+
export declare let stdioServer: {
|
|
10
|
+
server: McpServer;
|
|
11
|
+
loggingState: LoggingState;
|
|
12
|
+
} | undefined;
|
|
12
13
|
export declare function createServer(options?: ServerOptions): Promise<McpServer>;
|
|
13
14
|
export declare function startServer(server: McpServer): Promise<void>;
|
|
14
|
-
export declare function startHttpServer(port: number, options: ServerOptions): Promise<
|
|
15
|
+
export declare function startHttpServer(port: number, options: ServerOptions): Promise<Server>;
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as http from 'node:http';
|
|
3
|
-
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
4
|
-
import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
|
|
1
|
+
import { InMemoryTaskMessageQueue } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
|
|
5
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
4
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
5
|
import { isInitializeRequest, LATEST_PROTOCOL_VERSION, SetLevelRequestSchema, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
7
|
+
import { channel } from 'node:diagnostics_channel';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { createServer as createHttpServer, } from 'node:http';
|
|
9
10
|
import { DEFAULT_LOG_LEVEL, parseEnvInt } from '../lib/constants.js';
|
|
10
11
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
12
|
+
import { createLoggingState, Logger, logToMcp, SessionContext, } from '../lib/logger.js';
|
|
11
13
|
import { withAllowedDirectoriesState } from '../lib/paths.js';
|
|
12
14
|
import { createInMemoryResourceStore } from '../lib/resource-store.js';
|
|
13
|
-
import { isRecord } from '../lib/utils.js';
|
|
14
15
|
import { registerCompletions } from '../completions.js';
|
|
15
16
|
import { pkgInfo } from '../pkg-info.js';
|
|
16
17
|
import { registerAnalyzePathPrompt, registerCompareFilesPrompt, registerGetHelpPrompt, registerGetToolHelpPrompt, } from '../prompts.js';
|
|
@@ -19,6 +20,7 @@ import { buildServerInstructions } from '../resources/generated-instructions.js'
|
|
|
19
20
|
import { registerAllTools } from '../tools.js';
|
|
20
21
|
import { withDefaultIcons } from '../tools/shared.js';
|
|
21
22
|
import { RootsManager } from './roots-manager.js';
|
|
23
|
+
import { createTaskStore } from './task-store.js';
|
|
22
24
|
let cachedTaskToolSupport;
|
|
23
25
|
function detectTaskToolSupport() {
|
|
24
26
|
if (cachedTaskToolSupport !== undefined) {
|
|
@@ -48,6 +50,7 @@ function buildServerCapabilities(options = {}) {
|
|
|
48
50
|
tools: {},
|
|
49
51
|
prompts: options.enablePromptListChanged ? { listChanged: true } : {},
|
|
50
52
|
completions: {},
|
|
53
|
+
extensions: {},
|
|
51
54
|
};
|
|
52
55
|
if (options.enableTaskToolRequests) {
|
|
53
56
|
// NOTE: enabling task tool requests requires the caller to configure
|
|
@@ -64,45 +67,30 @@ function buildServerCapabilities(options = {}) {
|
|
|
64
67
|
function supportsTaskToolRequests() {
|
|
65
68
|
return detectTaskToolSupport();
|
|
66
69
|
}
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
alert: 6,
|
|
76
|
-
emergency: 7,
|
|
77
|
-
};
|
|
78
|
-
function createLoggingState(minimumLevel = 'debug') {
|
|
79
|
-
return { minimumLevel };
|
|
80
|
-
}
|
|
81
|
-
function canSendMcpLogs(server) {
|
|
82
|
-
const capabilities = server.server.getClientCapabilities();
|
|
83
|
-
if (!isRecord(capabilities))
|
|
84
|
-
return false;
|
|
85
|
-
if (!('logging' in capabilities))
|
|
86
|
-
return false;
|
|
87
|
-
return !!capabilities['logging'];
|
|
70
|
+
// Global map of all active servers by sessionId for routing logs
|
|
71
|
+
export const activeServers = new Map();
|
|
72
|
+
// For stdio (single session without a specific ID)
|
|
73
|
+
export let stdioServer;
|
|
74
|
+
function stringifyData(data) {
|
|
75
|
+
if (!data)
|
|
76
|
+
return '';
|
|
77
|
+
return ` ${typeof data === 'string' ? data : JSON.stringify(data)}`;
|
|
88
78
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
79
|
+
channel('filesystem-mcp:log').subscribe((message) => {
|
|
80
|
+
const event = message;
|
|
81
|
+
const target = event.sessionId
|
|
82
|
+
? activeServers.get(event.sessionId)
|
|
83
|
+
: stdioServer;
|
|
84
|
+
const dataStr = stringifyData(event.data);
|
|
85
|
+
if (target) {
|
|
86
|
+
logToMcp(target.server, event.level, `${event.message}${dataStr}`, target.loggingState.minimumLevel);
|
|
92
87
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
else {
|
|
89
|
+
// Fallback if no server
|
|
90
|
+
const fullMsg = `${event.message}${dataStr}`;
|
|
91
|
+
console.error(`[${event.level.toUpperCase()}] ${fullMsg}`);
|
|
96
92
|
}
|
|
97
|
-
|
|
98
|
-
level,
|
|
99
|
-
logger: MCP_LOGGER_NAME,
|
|
100
|
-
data,
|
|
101
|
-
};
|
|
102
|
-
void server.sendLoggingMessage(params).catch((error) => {
|
|
103
|
-
console.error(`Failed to send MCP log: ${level} | ${data}`, formatUnknownErrorMessage(error));
|
|
104
|
-
});
|
|
105
|
-
}
|
|
93
|
+
});
|
|
106
94
|
const { version: SERVER_VERSION, description: SERVER_DESCRIPTION, homepage: SERVER_HOMEPAGE, } = pkgInfo;
|
|
107
95
|
const rootsManagers = new WeakMap();
|
|
108
96
|
function getRootsManager(server) {
|
|
@@ -119,7 +107,7 @@ async function getLocalIconInfo() {
|
|
|
119
107
|
for (const candidate of candidates) {
|
|
120
108
|
try {
|
|
121
109
|
const iconPath = new URL(candidate, import.meta.url);
|
|
122
|
-
const buffer = await
|
|
110
|
+
const buffer = await readFile(iconPath);
|
|
123
111
|
return {
|
|
124
112
|
src: `data:${mime};base64,${buffer.toString('base64')}`,
|
|
125
113
|
mimeType: mime,
|
|
@@ -144,7 +132,7 @@ export async function createServer(options = {}) {
|
|
|
144
132
|
};
|
|
145
133
|
if (taskToolSupport) {
|
|
146
134
|
// Enabling task tool support requires configuring a task store and message queue on the server config. We use in-memory implementations from the SDK which auto-evict tasks after their TTL expires (via setTimeout). Suitable for both stdio and HTTP sessions.
|
|
147
|
-
serverConfig.taskStore =
|
|
135
|
+
serverConfig.taskStore = createTaskStore();
|
|
148
136
|
serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
|
|
149
137
|
}
|
|
150
138
|
if (serverInstructions) {
|
|
@@ -163,10 +151,15 @@ export async function createServer(options = {}) {
|
|
|
163
151
|
const loggingState = createLoggingState(DEFAULT_LOG_LEVEL);
|
|
164
152
|
const rootsManager = new RootsManager(options, loggingState);
|
|
165
153
|
rootsManagers.set(server, rootsManager);
|
|
154
|
+
// 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.
|
|
155
|
+
// Wait, in stdio there's only one server. In HTTP there are multiple.
|
|
166
156
|
server.server.setRequestHandler(SetLevelRequestSchema, (req) => {
|
|
167
157
|
loggingState.minimumLevel = req.params.level;
|
|
158
|
+
Logger.notice(`Log level set to ${req.params.level}`);
|
|
168
159
|
return {};
|
|
169
160
|
});
|
|
161
|
+
// Track stdio server by default, or it will be overwritten per HTTP session later
|
|
162
|
+
stdioServer ??= { server, loggingState };
|
|
170
163
|
registerInstructionResource(server, serverInstructions, localIcon);
|
|
171
164
|
registerToolCatalogResource(server, localIcon);
|
|
172
165
|
registerWorkflowGuideResource(server, localIcon);
|
|
@@ -257,6 +250,10 @@ async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
|
|
|
257
250
|
transport,
|
|
258
251
|
negotiatedProtocolVersion,
|
|
259
252
|
});
|
|
253
|
+
activeServers.set(sessionId, {
|
|
254
|
+
server: mcpServer,
|
|
255
|
+
loggingState: rootsManager['loggingState'],
|
|
256
|
+
});
|
|
260
257
|
rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
|
|
261
258
|
},
|
|
262
259
|
});
|
|
@@ -264,10 +261,11 @@ async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
|
|
|
264
261
|
const { sessionId } = transport;
|
|
265
262
|
if (sessionId) {
|
|
266
263
|
sessions.delete(sessionId);
|
|
264
|
+
activeServers.delete(sessionId);
|
|
267
265
|
}
|
|
268
266
|
rootsManager.destroy();
|
|
269
267
|
mcpServer.close().catch((err) => {
|
|
270
|
-
|
|
268
|
+
Logger.error('[HTTP] Error closing MCP server:', formatUnknownErrorMessage(err));
|
|
271
269
|
});
|
|
272
270
|
};
|
|
273
271
|
await mcpServer.connect(transport);
|
|
@@ -368,7 +366,12 @@ function discardRequestBody(req) {
|
|
|
368
366
|
req.resume();
|
|
369
367
|
}
|
|
370
368
|
async function handleSessionTransportRequest(session, req, res, body) {
|
|
371
|
-
|
|
369
|
+
const store = session.transport.sessionId
|
|
370
|
+
? { sessionId: session.transport.sessionId }
|
|
371
|
+
: {};
|
|
372
|
+
await SessionContext.run(store, async () => {
|
|
373
|
+
await withAllowedDirectoriesState(session.rootsManager.getAllowedDirectoriesState(), () => session.transport.handleRequest(req, res, body));
|
|
374
|
+
});
|
|
372
375
|
}
|
|
373
376
|
function getSessionOrRespondNotFound(sessions, sessionId, res) {
|
|
374
377
|
const session = sessions.get(sessionId);
|
|
@@ -396,6 +399,47 @@ export async function startHttpServer(port, options) {
|
|
|
396
399
|
const sessions = new Map();
|
|
397
400
|
const httpHost = process.env['FILESYSTEM_MCP_HTTP_HOST'] ?? '127.0.0.1';
|
|
398
401
|
assertHttpBindingSecurity(httpHost);
|
|
402
|
+
async function handlePostRequest(req, res, sessionId) {
|
|
403
|
+
if (sessionId) {
|
|
404
|
+
const session = getSessionOrRespondNotFound(sessions, sessionId, res);
|
|
405
|
+
if (!session) {
|
|
406
|
+
discardRequestBody(req);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!ensureSessionProtocolVersion(req, res, session)) {
|
|
410
|
+
discardRequestBody(req);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const body = await readRequestBody(req);
|
|
414
|
+
await handleSessionTransportRequest(session, req, res, body);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const body = await readRequestBody(req);
|
|
418
|
+
if (isInitializeRequest(body)) {
|
|
419
|
+
const maxSessions = parseEnvInt('FILESYSTEM_MCP_MAX_HTTP_SESSIONS', 100, 1, 10_000);
|
|
420
|
+
if (sessions.size >= maxSessions) {
|
|
421
|
+
sendJsonRpcError(res, 503, JSON_RPC_SERVER_ERROR, 'Too many sessions');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const session = await createHttpSession(options, sessions, resolveNegotiatedProtocolVersion(body.params.protocolVersion));
|
|
425
|
+
await handleSessionTransportRequest(session, req, res, body);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: No valid session ID provided');
|
|
429
|
+
discardRequestBody(req);
|
|
430
|
+
}
|
|
431
|
+
async function handleGetDeleteRequest(req, res, sessionId) {
|
|
432
|
+
if (!sessionId) {
|
|
433
|
+
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: Missing session ID');
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const session = getSessionOrRespondNotFound(sessions, sessionId, res);
|
|
437
|
+
if (!session)
|
|
438
|
+
return;
|
|
439
|
+
if (!ensureSessionProtocolVersion(req, res, session))
|
|
440
|
+
return;
|
|
441
|
+
await handleSessionTransportRequest(session, req, res);
|
|
442
|
+
}
|
|
399
443
|
async function handleMcpRequest(req, res) {
|
|
400
444
|
const { method } = req;
|
|
401
445
|
const sessionId = getSessionId(req);
|
|
@@ -408,45 +452,10 @@ export async function startHttpServer(port, options) {
|
|
|
408
452
|
return;
|
|
409
453
|
try {
|
|
410
454
|
if (method === 'POST') {
|
|
411
|
-
|
|
412
|
-
const session = getSessionOrRespondNotFound(sessions, sessionId, res);
|
|
413
|
-
if (!session) {
|
|
414
|
-
discardRequestBody(req);
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
if (!ensureSessionProtocolVersion(req, res, session)) {
|
|
418
|
-
discardRequestBody(req);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
const body = await readRequestBody(req);
|
|
422
|
-
await handleSessionTransportRequest(session, req, res, body);
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
const body = await readRequestBody(req);
|
|
426
|
-
if (isInitializeRequest(body)) {
|
|
427
|
-
const maxSessions = parseEnvInt('FILESYSTEM_MCP_MAX_HTTP_SESSIONS', 100, 1, 10_000);
|
|
428
|
-
if (sessions.size >= maxSessions) {
|
|
429
|
-
sendJsonRpcError(res, 503, JSON_RPC_SERVER_ERROR, 'Too many sessions');
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
const session = await createHttpSession(options, sessions, resolveNegotiatedProtocolVersion(body.params.protocolVersion));
|
|
433
|
-
await handleSessionTransportRequest(session, req, res, body);
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: No valid session ID provided');
|
|
437
|
-
discardRequestBody(req);
|
|
455
|
+
await handlePostRequest(req, res, sessionId);
|
|
438
456
|
}
|
|
439
457
|
else if (method === 'GET' || method === 'DELETE') {
|
|
440
|
-
|
|
441
|
-
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: Missing session ID');
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
const session = getSessionOrRespondNotFound(sessions, sessionId, res);
|
|
445
|
-
if (!session)
|
|
446
|
-
return;
|
|
447
|
-
if (!ensureSessionProtocolVersion(req, res, session))
|
|
448
|
-
return;
|
|
449
|
-
await handleSessionTransportRequest(session, req, res);
|
|
458
|
+
await handleGetDeleteRequest(req, res, sessionId);
|
|
450
459
|
}
|
|
451
460
|
else {
|
|
452
461
|
res.writeHead(405, {
|
|
@@ -472,17 +481,17 @@ export async function startHttpServer(port, options) {
|
|
|
472
481
|
sendJsonRpcError(res, error.statusCode, rpcCode, error.message);
|
|
473
482
|
return;
|
|
474
483
|
}
|
|
475
|
-
|
|
484
|
+
Logger.error('[HTTP] Error handling request:', formatUnknownErrorMessage(error));
|
|
476
485
|
if (!res.headersSent) {
|
|
477
486
|
sendJsonRpcError(res, 500, JSON_RPC_INTERNAL_ERROR, 'Internal Server Error');
|
|
478
487
|
}
|
|
479
488
|
}
|
|
480
489
|
}
|
|
481
|
-
const httpServer =
|
|
490
|
+
const httpServer = createHttpServer((req, res) => {
|
|
482
491
|
const urlPath = (req.url ?? '/').split('?')[0];
|
|
483
492
|
if (urlPath === '/mcp') {
|
|
484
493
|
handleMcpRequest(req, res).catch((err) => {
|
|
485
|
-
|
|
494
|
+
Logger.error('[HTTP] Unhandled error in request handler:', formatUnknownErrorMessage(err));
|
|
486
495
|
});
|
|
487
496
|
}
|
|
488
497
|
else {
|
|
@@ -493,7 +502,7 @@ export async function startHttpServer(port, options) {
|
|
|
493
502
|
return new Promise((resolve, reject) => {
|
|
494
503
|
httpServer.once('error', reject);
|
|
495
504
|
httpServer.listen(port, httpHost, () => {
|
|
496
|
-
|
|
505
|
+
Logger.info(`MCP HTTP server listening on ${httpHost}:${port}`);
|
|
497
506
|
resolve(httpServer);
|
|
498
507
|
});
|
|
499
508
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type LoggingState } from '../lib/logger.js';
|
|
2
3
|
import { type AllowedDirectoriesState } from '../lib/paths.js';
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
export interface ServerOptions {
|
|
5
|
+
allowCwd?: boolean;
|
|
6
|
+
cliAllowedDirs?: string[];
|
|
7
|
+
}
|
|
5
8
|
export declare class RootsManager {
|
|
6
9
|
private _debouncedUpdate;
|
|
7
10
|
private rootDirectories;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
1
|
import { InitializedNotificationSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { assertNotAborted, createTimedAbortSignal, withAbort, } from '../lib/abort.js';
|
|
4
5
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
5
|
-
import {
|
|
6
|
+
import { Logger, logToMcp } from '../lib/logger.js';
|
|
6
7
|
import { getValidRootDirectories, isPathWithinDirectories, normalizePath, resolveAllowedDirectoriesState, setAllowedDirectoriesStateResolved, } from '../lib/paths.js';
|
|
7
8
|
import { debounce, isRecord } from '../lib/utils.js';
|
|
8
|
-
import { logToMcp } from './bootstrap.js';
|
|
9
9
|
const ROOTS_TIMEOUT_MS = 5000;
|
|
10
10
|
const ROOTS_DEBOUNCE_MS = 100;
|
|
11
11
|
function normalizeCLIDirectories(dirs) {
|
|
@@ -61,7 +61,7 @@ async function isRootWithinBaseline(normalizedRoot, baseline, signal) {
|
|
|
61
61
|
}
|
|
62
62
|
try {
|
|
63
63
|
assertNotAborted(signal);
|
|
64
|
-
const realPath = await withAbort(
|
|
64
|
+
const realPath = await withAbort(realpath(normalizedRoot), signal);
|
|
65
65
|
const normalizedReal = normalizePath(realPath);
|
|
66
66
|
return isPathWithinDirectories(normalizedReal, baseline);
|
|
67
67
|
}
|
|
@@ -88,9 +88,10 @@ export class RootsManager {
|
|
|
88
88
|
expanded: [],
|
|
89
89
|
};
|
|
90
90
|
clientInitialized = false;
|
|
91
|
-
//
|
|
91
|
+
// Guard concurrent root refreshes; if one is already running we queue one
|
|
92
|
+
// replay so the last-known state still gets applied after completion.
|
|
92
93
|
updatingRoots = false;
|
|
93
|
-
//
|
|
94
|
+
// Tracks whether a roots change arrived while the previous refresh ran.
|
|
94
95
|
pendingRootsUpdate = false;
|
|
95
96
|
options;
|
|
96
97
|
loggingState;
|
|
@@ -187,6 +188,7 @@ export class RootsManager {
|
|
|
187
188
|
}
|
|
188
189
|
finally {
|
|
189
190
|
await this.recomputeAllowedDirectories();
|
|
191
|
+
Logger.info(`Roots updated: ${this.rootDirectories.length} root(s), ${this.allowedDirectoriesState.expanded.length} allowed dir(s)`);
|
|
190
192
|
this.updatingRoots = false;
|
|
191
193
|
// If a change arrived while we were running, apply it now.
|
|
192
194
|
if (this.pendingRootsUpdate) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
|
|
2
|
+
import type { Result } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
export declare class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
|
|
4
|
+
private readonly cancelledResults;
|
|
5
|
+
getTaskResult(taskId: string, sessionId?: string): Promise<Result>;
|
|
6
|
+
storeTaskResult(taskId: string, status: 'completed' | 'failed', result: Result, sessionId?: string): Promise<void>;
|
|
7
|
+
updateTaskStatus(taskId: string, status: 'working' | 'input_required' | 'completed' | 'failed' | 'cancelled', statusMessage?: string, sessionId?: string): Promise<void>;
|
|
8
|
+
cleanup(): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function createTaskStore(): ResultAwareInMemoryTaskStore;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
|
|
2
|
+
import { ErrorCode } from '../lib/errors.js';
|
|
3
|
+
const DEFAULT_CANCELLED_STATUS_MESSAGE = 'Client cancelled task execution.';
|
|
4
|
+
function getTaskKey(taskId, sessionId) {
|
|
5
|
+
return `${sessionId ?? ''}:${taskId}`;
|
|
6
|
+
}
|
|
7
|
+
function buildCancelledTaskResult(statusMessage) {
|
|
8
|
+
return {
|
|
9
|
+
content: [
|
|
10
|
+
{
|
|
11
|
+
type: 'text',
|
|
12
|
+
text: `Error [${ErrorCode.CANCELLED}]: ${statusMessage ?? DEFAULT_CANCELLED_STATUS_MESSAGE}`,
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
isError: true,
|
|
16
|
+
errorCode: ErrorCode.CANCELLED,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
|
|
20
|
+
cancelledResults = new Map();
|
|
21
|
+
async getTaskResult(taskId, sessionId) {
|
|
22
|
+
try {
|
|
23
|
+
return await super.getTaskResult(taskId, sessionId);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const task = await super.getTask(taskId, sessionId);
|
|
27
|
+
if (task?.status !== 'cancelled') {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
const key = getTaskKey(taskId, sessionId);
|
|
31
|
+
const existing = this.cancelledResults.get(key);
|
|
32
|
+
if (existing)
|
|
33
|
+
return existing;
|
|
34
|
+
const result = buildCancelledTaskResult(task.statusMessage);
|
|
35
|
+
this.cancelledResults.set(key, result);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async storeTaskResult(taskId, status, result, sessionId) {
|
|
40
|
+
try {
|
|
41
|
+
await super.storeTaskResult(taskId, status, result, sessionId);
|
|
42
|
+
if (status !== 'failed') {
|
|
43
|
+
this.cancelledResults.delete(getTaskKey(taskId, sessionId));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const task = await super.getTask(taskId, sessionId);
|
|
48
|
+
if (task?.status !== 'cancelled') {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
this.cancelledResults.set(getTaskKey(taskId, sessionId), this.cancelledResults.get(getTaskKey(taskId, sessionId)) ?? result);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async updateTaskStatus(taskId, status, statusMessage, sessionId) {
|
|
55
|
+
await super.updateTaskStatus(taskId, status, statusMessage, sessionId);
|
|
56
|
+
const key = getTaskKey(taskId, sessionId);
|
|
57
|
+
if (status === 'cancelled') {
|
|
58
|
+
this.cancelledResults.set(key, this.cancelledResults.get(key) ??
|
|
59
|
+
buildCancelledTaskResult(statusMessage));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (status === 'completed' || status === 'failed') {
|
|
63
|
+
this.cancelledResults.delete(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
cleanup() {
|
|
67
|
+
this.cancelledResults.clear();
|
|
68
|
+
super.cleanup();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function createTaskStore() {
|
|
72
|
+
return new ResultAwareInMemoryTaskStore();
|
|
73
|
+
}
|