@j0hanz/filesystem-mcp 1.6.0 → 1.6.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 CHANGED
@@ -565,6 +565,7 @@ Set `FS_CONTEXT_STRIP_STRUCTURED=1` to strip `structuredContent` from tool resul
565
565
  - **Input limits**: Paths are bounded to 4,096 characters; patterns to 1,000 characters.
566
566
  - **Atomic writes**: File writes use an atomic write-then-rename strategy to prevent partial writes.
567
567
  - **Docker**: The container runs as a non-root user (`mcp`).
568
+ - **HTTP host binding**: The HTTP transport binds to `127.0.0.1` by default. Setting `FILESYSTEM_MCP_HTTP_HOST=0.0.0.0` binds to all network interfaces and exposes the server externally — only do this behind a trusted reverse proxy with `FILESYSTEM_MCP_AUTH_TOKEN` configured.
568
569
 
569
570
  > [!IMPORTANT]
570
571
  > All diagnostic output goes to `stderr`. Tool handlers must never write to `stdout`, as doing so would corrupt the stdio transport.
@@ -623,6 +624,21 @@ The [Glama](https://glama.ai/mcp/servers/j0hanz/filesystem-mcp) listing requires
623
624
  docker build -t filesystem-mcp .
624
625
  ```
625
626
 
627
+ ## HTTP Conformance Notes
628
+
629
+ For Streamable HTTP clients, this server enforces the following behavior:
630
+
631
+ - Session-bound requests must include `MCP-Protocol-Version: 2025-11-25`; missing or unsupported values return `400`.
632
+ - Requests with invalid or expired `mcp-session-id` return `404`.
633
+ - Initialize requests continue to be accepted without a session ID.
634
+
635
+ ## Backlog Hardening (Planned)
636
+
637
+ The following hardening items are intentionally tracked as follow-up work:
638
+
639
+ - Add a TTL-evicting task store for long-lived HTTP deployments to bound memory usage.
640
+ - Add an optional per-session roots isolation mode for multi-tenant HTTP deployments.
641
+
626
642
  ## Troubleshooting
627
643
 
628
644
  **No directories configured**
@@ -6,8 +6,18 @@ import { getAllowedDirectories, isPathWithinDirectories, normalizePath, } from '
6
6
  import { isRecord } from './lib/type-guards.js';
7
7
  const MAX_COMPLETION_ITEMS = 100;
8
8
  const COMPLETION_RATE_LIMIT_MS = 100;
9
- const completionLastCallMs = new Map();
10
- const completionLastResult = new Map();
9
+ // WeakMap keyed by McpServer instance so that each HTTP session gets isolated
10
+ // rate-limit state. In stdio mode there is a single server; in HTTP mode every
11
+ // session creates its own McpServer, so cross-session cache pollution is avoided.
12
+ const completionState = new WeakMap();
13
+ function getCompletionState(server) {
14
+ let state = completionState.get(server);
15
+ if (state === undefined) {
16
+ state = { lastCallMs: new Map(), lastResult: new Map() };
17
+ completionState.set(server, state);
18
+ }
19
+ return state;
20
+ }
11
21
  function extractTopicCompletions(instructions) {
12
22
  const headers = [];
13
23
  for (const line of instructions.split('\n')) {
@@ -393,9 +403,10 @@ export function registerCompletions(server, instructions = '') {
393
403
  return { completion: { values: [], total: 0, hasMore: false } };
394
404
  }
395
405
  const now = Date.now();
396
- const lastCallMs = completionLastCallMs.get(argName) ?? 0;
406
+ const sessionState = getCompletionState(server);
407
+ const lastCallMs = sessionState.lastCallMs.get(argName) ?? 0;
397
408
  if (now - lastCallMs < COMPLETION_RATE_LIMIT_MS) {
398
- const lastResult = completionLastResult.get(argName);
409
+ const lastResult = sessionState.lastResult.get(argName);
399
410
  if (lastResult) {
400
411
  return {
401
412
  completion: {
@@ -407,14 +418,14 @@ export function registerCompletions(server, instructions = '') {
407
418
  }
408
419
  return { completion: { values: [], total: 0, hasMore: false } };
409
420
  }
410
- completionLastCallMs.set(argName, now);
421
+ sessionState.lastCallMs.set(argName, now);
411
422
  const contextArguments = extractContextArguments(params.context);
412
423
  const { value } = argument;
413
424
  const completions = await getPathCompletions(value, {
414
425
  argumentName: argName,
415
426
  ...(contextArguments ? { contextArguments } : {}),
416
427
  });
417
- completionLastResult.set(argName, completions);
428
+ sessionState.lastResult.set(argName, completions);
418
429
  return {
419
430
  completion: {
420
431
  values: completions.values,
@@ -1,5 +1,6 @@
1
1
  export declare function parseTrueEnvFlag(value: string | undefined): boolean;
2
2
  export declare const DEFAULT_LOG_LEVEL: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
3
+ export declare const REQUIRED_MCP_PROTOCOL_VERSION = "2025-11-25";
3
4
  export declare const PARALLEL_CONCURRENCY: number;
4
5
  export declare const MAX_SEARCHABLE_FILE_SIZE: number;
5
6
  export declare const MAX_TEXT_FILE_SIZE: number;
@@ -69,7 +69,8 @@ function parseEnvLogLevel(envVar, defaultValue) {
69
69
  console.error(`[WARNING] Invalid ${envVar} value: ${value} (must be ${VALID_LOG_LEVELS.join('|')}). Using default: ${defaultValue}`);
70
70
  return defaultValue;
71
71
  }
72
- export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'debug');
72
+ export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'info');
73
+ export const REQUIRED_MCP_PROTOCOL_VERSION = '2025-11-25';
73
74
  // Auto-tuned parallelism based on CPU cores (no env override)
74
75
  const BYTES_PER_PARALLEL_TASK = 64 * MIB;
75
76
  const BYTES_PER_SEARCH_WORKER = 128 * MIB;
@@ -108,7 +108,13 @@ function normalizeAllowedDirectories(dirs) {
108
108
  // Preserve first-seen order while deduping.
109
109
  return dedupePreserveOrder(normalized);
110
110
  }
111
- // Cached module state (configured roots).
111
+ // Process-global singleton state for allowed directory roots.
112
+ //
113
+ // These are set once at startup (via setAllowedDirectoriesResolved) and
114
+ // mutated only through setAllowedDirectoriesState. In stdio mode there is a
115
+ // single MCP session per process, so this is safe. In HTTP mode all HTTP
116
+ // sessions within the same process share one policy — multi-tenant isolation
117
+ // (different roots per session) requires separate server processes.
112
118
  let allowedDirectoriesExpanded = [];
113
119
  let allowedDirectoriesPrimary = [];
114
120
  function setAllowedDirectoriesState(primary, expanded) {
@@ -2,3 +2,4 @@ import type { ToolContract } from '../tools/contract.js';
2
2
  export declare function getToolContracts(): ToolContract[];
3
3
  export declare function buildCoreContextPack(): string;
4
4
  export declare function getSharedConstraints(): string[];
5
+ export declare function buildToolInfo(name: string): string | undefined;
@@ -43,3 +43,25 @@ export function getSharedConstraints() {
43
43
  'If a response includes `resourceUri`, call `resources/read` immediately — results expire on process restart.',
44
44
  ];
45
45
  }
46
+ export function buildToolInfo(name) {
47
+ const entry = ENTRIES[name];
48
+ if (!entry)
49
+ return undefined;
50
+ const lines = [`## ${entry.name}`, '', entry.description];
51
+ if (entry.annotations && entry.annotations.length > 0) {
52
+ lines.push('', `**Annotations:** ${entry.annotations.join(', ')}`);
53
+ }
54
+ if (entry.nuances && entry.nuances.length > 0) {
55
+ lines.push('', '**Nuances:**');
56
+ for (const nuance of entry.nuances) {
57
+ lines.push(`- ${nuance}`);
58
+ }
59
+ }
60
+ if (entry.gotchas && entry.gotchas.length > 0) {
61
+ lines.push('', '**Gotchas:**');
62
+ for (const gotcha of entry.gotchas) {
63
+ lines.push(`- ${gotcha}`);
64
+ }
65
+ }
66
+ return lines.join('\n');
67
+ }
@@ -5,4 +5,5 @@ export declare function registerInstructionResource(server: McpServer, instructi
5
5
  export declare function registerToolCatalogResource(server: McpServer, iconInfo?: IconInfo): void;
6
6
  export declare function registerWorkflowGuideResource(server: McpServer, iconInfo?: IconInfo): void;
7
7
  export declare function registerResultResources(server: McpServer, store: ResourceStore, iconInfo?: IconInfo): void;
8
+ export declare function registerToolInfoResource(server: McpServer, iconInfo?: IconInfo): void;
8
9
  export declare function registerMetricsResource(server: McpServer, iconInfo?: IconInfo): void;
package/dist/resources.js CHANGED
@@ -2,11 +2,23 @@ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { ErrorCode, McpError } from './lib/errors.js';
3
3
  import { globalMetrics } from './lib/observability.js';
4
4
  import { buildToolCatalog } from './resources/tool-catalog.js';
5
+ import { buildToolInfo, getToolContracts } from './resources/tool-info.js';
5
6
  import { buildWorkflowGuide } from './resources/workflows.js';
6
7
  import { withDefaultIcons } from './tools/shared.js';
7
8
  const RESULT_TEMPLATE = new ResourceTemplate('filesystem-mcp://result/{id}', {
8
9
  list: undefined,
9
10
  });
11
+ const TOOL_INFO_TEMPLATE = new ResourceTemplate('internal://tool-info/{name}', {
12
+ list: () => ({
13
+ resources: getToolContracts().map((contract) => ({
14
+ uri: `internal://tool-info/${contract.name}`,
15
+ name: contract.name,
16
+ mimeType: 'text/markdown',
17
+ })),
18
+ }),
19
+ });
20
+ const TOOL_INFO_RESOURCE_NAME = 'filesystem-mcp-tool-info';
21
+ const TOOL_INFO_RESOURCE_DESCRIPTION = 'Per-tool contract details, nuances, and gotchas. Read internal://tool-info/{name} with a tool name such as "read", "ls", or "grep".';
10
22
  const INSTRUCTIONS_RESOURCE_NAME = 'filesystem-mcp-instructions';
11
23
  const INSTRUCTIONS_RESOURCE_URI = 'internal://instructions';
12
24
  const INSTRUCTIONS_RESOURCE_DESCRIPTION = 'Guidance for using the filesystem-mcp MCP tools effectively.';
@@ -104,6 +116,35 @@ export function registerResultResources(server, store, iconInfo) {
104
116
  };
105
117
  });
106
118
  }
119
+ export function registerToolInfoResource(server, iconInfo) {
120
+ server.registerResource(TOOL_INFO_RESOURCE_NAME, TOOL_INFO_TEMPLATE, withDefaultIcons({
121
+ title: 'Tool Info',
122
+ description: TOOL_INFO_RESOURCE_DESCRIPTION,
123
+ mimeType: 'text/markdown',
124
+ annotations: {
125
+ audience: ['assistant'],
126
+ priority: 0.65,
127
+ },
128
+ }, iconInfo), (uri, variables) => {
129
+ const { name } = variables;
130
+ if (typeof name !== 'string' || name.length === 0) {
131
+ throw new McpError(ErrorCode.E_INVALID_INPUT, 'Tool name is required');
132
+ }
133
+ const content = buildToolInfo(name);
134
+ if (content === undefined) {
135
+ throw new McpError(ErrorCode.E_INVALID_INPUT, `Tool not found: ${name}`);
136
+ }
137
+ return {
138
+ contents: [
139
+ {
140
+ uri: uri.href,
141
+ mimeType: 'text/markdown',
142
+ text: content,
143
+ },
144
+ ],
145
+ };
146
+ });
147
+ }
107
148
  export function registerMetricsResource(server, iconInfo) {
108
149
  server.registerResource(METRICS_RESOURCE_NAME, METRICS_RESOURCE_URI, withDefaultIcons({
109
150
  title: 'Tool Metrics',
@@ -7,12 +7,12 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
7
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
8
8
  import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
9
9
  import { registerCompletions } from '../completions.js';
10
- import { DEFAULT_LOG_LEVEL } from '../lib/constants.js';
10
+ import { DEFAULT_LOG_LEVEL, REQUIRED_MCP_PROTOCOL_VERSION, } from '../lib/constants.js';
11
11
  import { formatUnknownErrorMessage } from '../lib/errors.js';
12
12
  import { createInMemoryResourceStore } from '../lib/resource-store.js';
13
13
  import { pkgInfo } from '../pkg-info.js';
14
14
  import { registerGetHelpPrompt } from '../prompts.js';
15
- import { registerInstructionResource, registerMetricsResource, registerResultResources, registerToolCatalogResource, registerWorkflowGuideResource, } from '../resources.js';
15
+ import { registerInstructionResource, registerMetricsResource, registerResultResources, registerToolCatalogResource, registerToolInfoResource, registerWorkflowGuideResource, } from '../resources.js';
16
16
  import { buildServerInstructions } from '../resources/generated-instructions.js';
17
17
  import { registerAllTools } from '../tools.js';
18
18
  import { withDefaultIcons } from '../tools/shared.js';
@@ -59,6 +59,9 @@ export async function createServer(options = {}) {
59
59
  }),
60
60
  };
61
61
  if (taskToolSupport) {
62
+ // Note: InMemoryTaskStore has no TTL — tasks accumulate for the process
63
+ // lifetime. In HTTP mode this may grow unboundedly for long-lived servers.
64
+ // Use a custom TaskStore with eviction for production HTTP deployments.
62
65
  serverConfig.taskStore = new InMemoryTaskStore();
63
66
  serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
64
67
  }
@@ -85,6 +88,7 @@ export async function createServer(options = {}) {
85
88
  registerInstructionResource(server, serverInstructions, localIcon);
86
89
  registerToolCatalogResource(server, localIcon);
87
90
  registerWorkflowGuideResource(server, localIcon);
91
+ registerToolInfoResource(server, localIcon);
88
92
  registerGetHelpPrompt(server, serverInstructions, localIcon);
89
93
  registerResultResources(server, resourceStore, localIcon);
90
94
  registerMetricsResource(server, localIcon);
@@ -198,11 +202,40 @@ function isAllowedOrigin(origin) {
198
202
  return true; // Non-browser clients omit Origin.
199
203
  return LOCALHOST_ORIGIN_RE.test(origin);
200
204
  }
205
+ function getProtocolVersionHeader(req) {
206
+ const rawProtocolVersion = req.headers['mcp-protocol-version'];
207
+ if (typeof rawProtocolVersion === 'string') {
208
+ return rawProtocolVersion;
209
+ }
210
+ if (Array.isArray(rawProtocolVersion)) {
211
+ return rawProtocolVersion.find((value) => value === REQUIRED_MCP_PROTOCOL_VERSION);
212
+ }
213
+ return undefined;
214
+ }
215
+ function ensureProtocolVersionHeader(req, res) {
216
+ const protocolVersion = getProtocolVersionHeader(req);
217
+ if (protocolVersion === REQUIRED_MCP_PROTOCOL_VERSION) {
218
+ return true;
219
+ }
220
+ sendJsonRpcError(res, 400, -32000, 'Bad Request: MCP-Protocol-Version header missing or unsupported');
221
+ return false;
222
+ }
223
+ function discardRequestBody(req) {
224
+ req.on('error', () => {
225
+ // Best effort drain to avoid corrupting keep-alive pipelines.
226
+ });
227
+ req.resume();
228
+ }
201
229
  export async function startHttpServer(port, options) {
202
230
  const sessions = new Map();
203
231
  async function handleMcpRequest(req, res) {
204
232
  const { method } = req;
205
- const sessionId = req.headers['mcp-session-id'];
233
+ const MAX_SESSION_ID_LENGTH = 256;
234
+ const rawSessionId = req.headers['mcp-session-id'];
235
+ const sessionId = typeof rawSessionId === 'string' &&
236
+ rawSessionId.length <= MAX_SESSION_ID_LENGTH
237
+ ? rawSessionId
238
+ : undefined;
206
239
  const { origin } = req.headers;
207
240
  if (!isAllowedOrigin(origin)) {
208
241
  sendJsonRpcError(res, 403, -32000, 'Forbidden: disallowed origin');
@@ -235,33 +268,59 @@ export async function startHttpServer(port, options) {
235
268
  }
236
269
  try {
237
270
  if (method === 'POST') {
238
- const body = await readRequestBody(req);
239
- if (sessionId && sessions.has(sessionId)) {
271
+ if (sessionId) {
272
+ if (!sessions.has(sessionId)) {
273
+ sendJsonRpcError(res, 404, -32000, 'Session not found');
274
+ discardRequestBody(req);
275
+ return;
276
+ }
277
+ if (!ensureProtocolVersionHeader(req, res)) {
278
+ discardRequestBody(req);
279
+ return;
280
+ }
281
+ const body = await readRequestBody(req);
240
282
  const session = sessions.get(sessionId);
241
283
  if (session) {
242
284
  await session.transport.handleRequest(req, res, body);
243
285
  }
286
+ else {
287
+ sendJsonRpcError(res, 404, -32000, 'Session not found');
288
+ }
289
+ return;
244
290
  }
245
- else if (!sessionId && isInitializeRequest(body)) {
291
+ const body = await readRequestBody(req);
292
+ if (isInitializeRequest(body)) {
293
+ const maxSessions = parseInt(process.env['FILESYSTEM_MCP_MAX_HTTP_SESSIONS'] ?? '', 10) || 100;
294
+ if (sessions.size >= maxSessions) {
295
+ sendJsonRpcError(res, 503, -32000, 'Too many sessions');
296
+ return;
297
+ }
246
298
  const { transport } = await createHttpSession(options, sessions);
247
299
  await transport.handleRequest(req, res, body);
300
+ return;
248
301
  }
249
- else if (sessionId) {
250
- sendJsonRpcError(res, 400, -32000, 'Bad Request: Session not found');
251
- }
252
- else {
253
- sendJsonRpcError(res, 400, -32000, 'Bad Request: No valid session ID provided');
254
- }
302
+ sendJsonRpcError(res, 400, -32000, 'Bad Request: No valid session ID provided');
303
+ discardRequestBody(req);
255
304
  }
256
305
  else if (method === 'GET' || method === 'DELETE') {
257
- if (!sessionId || !sessions.has(sessionId)) {
258
- sendJsonRpcError(res, 400, -32000, 'Bad Request: Invalid or missing session ID');
306
+ if (!sessionId) {
307
+ sendJsonRpcError(res, 400, -32000, 'Bad Request: Missing session ID');
308
+ return;
309
+ }
310
+ if (!sessions.has(sessionId)) {
311
+ sendJsonRpcError(res, 404, -32000, 'Session not found');
312
+ return;
313
+ }
314
+ if (!ensureProtocolVersionHeader(req, res)) {
259
315
  return;
260
316
  }
261
317
  const session = sessions.get(sessionId);
262
318
  if (session) {
263
319
  await session.transport.handleRequest(req, res);
264
320
  }
321
+ else {
322
+ sendJsonRpcError(res, 404, -32000, 'Session not found');
323
+ }
265
324
  }
266
325
  else {
267
326
  res.writeHead(405, { Allow: 'GET, POST, DELETE' });
@@ -27,6 +27,11 @@ export function buildServerCapabilities(options = {}) {
27
27
  completions: {},
28
28
  };
29
29
  if (options.enableTaskToolRequests) {
30
+ // NOTE: enabling task tool requests requires the caller to configure
31
+ // an InMemoryTaskStore and InMemoryTaskMessageQueue on the McpServer.
32
+ // InMemoryTaskStore accumulates completed task records with no TTL eviction —
33
+ // suitable for short-lived stdio sessions. Long-running HTTP servers should
34
+ // replace it with a TTL-evicting store to avoid unbounded memory growth.
30
35
  capabilities.tasks = {
31
36
  list: {},
32
37
  cancel: {},
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
+ import RE2 from 're2';
3
4
  import { ErrorCode } from '../lib/errors.js';
4
5
  import { atomicWriteFile } from '../lib/fs-helpers.js';
5
6
  import { validateExistingPath } from '../lib/path-validation.js';
@@ -35,7 +36,7 @@ function applyEdits(content, edits, ignoreWhitespace) {
35
36
  for (const edit of edits) {
36
37
  if (ignoreWhitespace) {
37
38
  const pattern = escapeRegExp(edit.oldText).replace(/\s+/g, '\\s+');
38
- const regex = new RegExp(pattern);
39
+ const regex = new RE2(pattern);
39
40
  const match = regex.exec(newContent);
40
41
  if (!match) {
41
42
  unmatchedEdits.push(edit.oldText);
@@ -17,9 +17,10 @@ export const LIST_DIRECTORY_TOOL = {
17
17
  inputSchema: ListDirectoryInputSchema,
18
18
  outputSchema: ListDirectoryOutputSchema,
19
19
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
20
+ taskSupport: 'optional',
20
21
  nuances: ['`pattern` enables filtered recursive traversal up to `maxDepth`.'],
21
22
  };
22
- function buildListTextResult(result) {
23
+ function buildListTextResult(result, nextCursor) {
23
24
  const { entries, summary, path } = result;
24
25
  if (entries.length === 0) {
25
26
  if (!summary.entriesScanned || summary.entriesScanned === 0) {
@@ -45,7 +46,11 @@ function buildListTextResult(result) {
45
46
  truncated: summary.truncated,
46
47
  ...(truncatedReason ? { truncatedReason } : {}),
47
48
  };
48
- return joinLines(lines) + formatOperationSummary(summaryOptions);
49
+ let text = joinLines(lines) + formatOperationSummary(summaryOptions);
50
+ if (nextCursor) {
51
+ text += `\n[Next page available. Use cursor: "${nextCursor}"]`;
52
+ }
53
+ return text;
49
54
  }
50
55
  function buildStructuredListEntry(entry) {
51
56
  return {
@@ -115,7 +120,7 @@ async function handleListDirectory(args, signal) {
115
120
  ? encodeCursor(cursorOffset + displayEntries.length)
116
121
  : undefined;
117
122
  const displayResult = { ...result, entries: displayEntries };
118
- return buildToolResponse(buildListTextResult(displayResult), buildStructuredListResult(displayResult, nextCursor));
123
+ return buildToolResponse(buildListTextResult(displayResult, nextCursor), buildStructuredListResult(displayResult, nextCursor));
119
124
  }
120
125
  export function registerListDirectoryTool(server, options = {}) {
121
126
  const handler = (args, extra) => executeToolWithDiagnostics({
@@ -224,6 +224,11 @@ export function registerSearchContentTool(server, options = {}) {
224
224
  });
225
225
  };
226
226
  try {
227
+ if (signal) {
228
+ signal.addEventListener('abort', () => {
229
+ console.error('searchContent signal aborted!');
230
+ });
231
+ }
227
232
  const result = await handleSearchContent(args, signal, options.resourceStore, progressWithMessage);
228
233
  const sc = result.structuredContent;
229
234
  const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
@@ -113,7 +113,10 @@ async function handleSearchFiles(args, signal, onProgress) {
113
113
  textLines.push(` ${entry.path}`);
114
114
  }
115
115
  }
116
- const text = joinLines(textLines) + formatOperationSummary(summaryOptions);
116
+ let text = joinLines(textLines) + formatOperationSummary(summaryOptions);
117
+ if (nextCursor) {
118
+ text += `\n[Next page available. Use cursor: "${nextCursor}"]`;
119
+ }
117
120
  return buildToolResponse(text, structured);
118
121
  }
119
122
  export function registerSearchFilesTool(server, options = {}) {
@@ -3,7 +3,6 @@ import { z } from 'zod';
3
3
  import type { FileInfo } from '../config.js';
4
4
  import { ErrorCode } from '../lib/errors.js';
5
5
  import type { ResourceStore } from '../lib/resource-store.js';
6
- import type { ToolErrorResponseSchema } from '../schemas.js';
7
6
  export { type ToolContract } from './contract.js';
8
7
  export declare const READ_ONLY_TOOL_ANNOTATIONS: {
9
8
  readonly readOnlyHint: true;
@@ -42,11 +41,11 @@ export declare function buildToolResponse<T>(text: string, structuredContent: T,
42
41
  content: ContentBlock[];
43
42
  structuredContent: T;
44
43
  };
45
- export type ToolResponse<T> = ReturnType<typeof buildToolResponse<T>> & Record<string, unknown>;
46
- type ToolErrorStructuredContent = z.infer<typeof ToolErrorResponseSchema>;
44
+ export type ToolResponse<T> = ReturnType<typeof buildToolResponse<T>> & {
45
+ isError?: never;
46
+ } & Record<string, unknown>;
47
47
  interface ToolErrorResponse extends Record<string, unknown> {
48
48
  content: ContentBlock[];
49
- structuredContent: ToolErrorStructuredContent;
50
49
  isError: true;
51
50
  }
52
51
  export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
@@ -195,23 +195,8 @@ export async function executeToolWithDiagnostics(options) {
195
195
  export function buildToolErrorResponse(error, defaultCode, path) {
196
196
  const detailed = resolveDetailedError(error, defaultCode, path);
197
197
  const text = formatDetailedError(detailed);
198
- const errorContent = {
199
- code: detailed.code,
200
- message: detailed.message,
201
- };
202
- if (detailed.path !== undefined) {
203
- errorContent.path = detailed.path;
204
- }
205
- if (detailed.suggestion !== undefined) {
206
- errorContent.suggestion = detailed.suggestion;
207
- }
208
- const structuredContent = {
209
- ok: false,
210
- error: errorContent,
211
- };
212
198
  return {
213
199
  content: [{ type: 'text', text }],
214
- structuredContent,
215
200
  isError: true,
216
201
  };
217
202
  }
@@ -134,14 +134,17 @@ function normalizeCallToolResult(value) {
134
134
  function getToolResultErrorCode(result) {
135
135
  if (!isRecord(result) || result['isError'] !== true)
136
136
  return undefined;
137
- const structured = result['structuredContent'];
138
- if (!isRecord(structured))
137
+ const { content } = result;
138
+ if (!Array.isArray(content) || content.length === 0)
139
139
  return undefined;
140
- const { error } = structured;
141
- if (!isRecord(error))
140
+ const first = content[0];
141
+ if (!isRecord(first) || first['type'] !== 'text')
142
142
  return undefined;
143
- const { code } = error;
144
- return typeof code === 'string' ? code : undefined;
143
+ const { text } = first;
144
+ if (typeof text !== 'string')
145
+ return undefined;
146
+ const match = /^Error \[([A-Z0-9_]+)\]:/.exec(text);
147
+ return match ? match[1] : undefined;
145
148
  }
146
149
  function isCancelledToolResult(result) {
147
150
  return getToolResultErrorCode(result) === ErrorCode.E_CANCELLED;
@@ -222,7 +225,7 @@ function getTaskId(extra) {
222
225
  return extra.taskId;
223
226
  }
224
227
  function isErrorResult(result) {
225
- return 'isError' in result && result.isError === true;
228
+ return 'isError' in result && result.isError;
226
229
  }
227
230
  // Strips structuredContent from a tool result if present, without modifying the original object. This is used when storing error results as 'completed' to prevent client-side output schema validation errors, while still allowing the human-readable error message in content[0].text to be returned to clients.
228
231
  function withoutStructuredContent(result) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
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",