From 2ee395b0885b2c4114b12187360928fe5b20b571 Mon Sep 17 00:00:00 2001 From: Ryan Lopopolo Date: Sat, 24 Jan 2026 08:12:24 -0800 Subject: [PATCH 1/2] test(filesystem): add directory_tree MCP SDK regression coverage --- src/filesystem/README.md | 49 ++++++++++++++++ .../__tests__/directory-tree.mcp-sdk.test.ts | 57 +++++++++++++++++++ .../__tests__/structured-content.test.ts | 4 +- 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/filesystem/__tests__/directory-tree.mcp-sdk.test.ts diff --git a/src/filesystem/README.md b/src/filesystem/README.md index e9ddc2b1e2..3d07c4731d 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -209,6 +209,55 @@ Add this to your `claude_desktop_config.json`: Note: you can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server. +## Troubleshooting + +### MCP error -32602: "Invalid structured content for tool directory_tree" + +If you see an error like: + +``` +MCP error -32602: Output validation error +Invalid structured content for tool directory_tree +Expected string, received array at path: ["content"] +``` + +it usually means the server returned `structuredContent.content` as an array even +though the tool's `outputSchema` declares `content` as a string. + +You can reproduce a correct end-to-end `directory_tree` call with the MCP SDK using +the following script: + +```ts +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-tree-')); +const root = await fs.realpath(tmp); +await fs.writeFile(path.join(root, 'a.txt'), 'hello'); + +const transport = new StdioClientTransport({ + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', root], +}); + +const client = new Client({ name: 'debug-client', version: '1.0.0' }, { capabilities: {} }); +await client.connect(transport); + +const result = await client.callTool({ + name: 'directory_tree', + arguments: { path: root }, +}); + +console.log(result.structuredContent); +await client.close(); +``` + +If this script still throws `-32602`, make sure you are running a recent +`@modelcontextprotocol/server-filesystem` build and not a cached older version. + ### Docker Note: all directories must be mounted to `/projects` by default. diff --git a/src/filesystem/__tests__/directory-tree.mcp-sdk.test.ts b/src/filesystem/__tests__/directory-tree.mcp-sdk.test.ts new file mode 100644 index 0000000000..0cbe48c65c --- /dev/null +++ b/src/filesystem/__tests__/directory-tree.mcp-sdk.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as os from 'os'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + +describe('directory_tree MCP SDK regression', () => { + let client: Client; + let transport: StdioClientTransport; + let testDir: string; + + beforeEach(async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-tree-')); + testDir = await fs.realpath(tmp); + + await fs.writeFile(path.join(testDir, 'root.txt'), 'root'); + await fs.mkdir(path.join(testDir, 'nested')); + await fs.writeFile(path.join(testDir, 'nested', 'child.txt'), 'child'); + + const serverPath = path.resolve(__dirname, '../dist/index.js'); + transport = new StdioClientTransport({ + command: 'node', + args: [serverPath, testDir], + }); + + client = new Client( + { name: 'directory-tree-regression-test', version: '1.0.0' }, + { capabilities: {} }, + ); + + await client.connect(transport); + }); + + afterEach(async () => { + await client?.close(); + await fs.rm(testDir, { recursive: true, force: true }); + }); + + it('returns structuredContent.content as a string (not an array) when called via MCP SDK', async () => { + const result = await client.callTool({ + name: 'directory_tree', + arguments: { path: testDir }, + }); + + // Regression test for issues where structuredContent was returned as an array, + // which causes MCP SDK validation to throw -32602 (invalid structured content). + const structured = result.structuredContent as { content: unknown }; + expect(structured).toBeDefined(); + expect(typeof structured.content).toBe('string'); + expect(Array.isArray(structured.content)).toBe(false); + + const parsed = JSON.parse(structured.content as string); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.length).toBeGreaterThan(0); + }); +}); diff --git a/src/filesystem/__tests__/structured-content.test.ts b/src/filesystem/__tests__/structured-content.test.ts index 4b8f92b0a3..52a3186d67 100644 --- a/src/filesystem/__tests__/structured-content.test.ts +++ b/src/filesystem/__tests__/structured-content.test.ts @@ -21,7 +21,9 @@ describe('structuredContent schema compliance', () => { beforeEach(async () => { // Create a temp directory for testing - testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-test-')); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-test-')); + // macOS temp dirs may be symlinked (/var -> /private/var); resolve for server allowlist checks. + testDir = await fs.realpath(tmpDir); // Create test files await fs.writeFile(path.join(testDir, 'test.txt'), 'test content'); From 7a3588a9285962e5c21847998bdccdc79e34c0e6 Mon Sep 17 00:00:00 2001 From: Ryan Lopopolo Date: Sat, 24 Jan 2026 08:14:09 -0800 Subject: [PATCH 2/2] docs(filesystem): drop troubleshooting note from README --- src/filesystem/README.md | 49 ---------------------------------------- 1 file changed, 49 deletions(-) diff --git a/src/filesystem/README.md b/src/filesystem/README.md index 3d07c4731d..e9ddc2b1e2 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -209,55 +209,6 @@ Add this to your `claude_desktop_config.json`: Note: you can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server. -## Troubleshooting - -### MCP error -32602: "Invalid structured content for tool directory_tree" - -If you see an error like: - -``` -MCP error -32602: Output validation error -Invalid structured content for tool directory_tree -Expected string, received array at path: ["content"] -``` - -it usually means the server returned `structuredContent.content` as an array even -though the tool's `outputSchema` declares `content` as a string. - -You can reproduce a correct end-to-end `directory_tree` call with the MCP SDK using -the following script: - -```ts -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; -import fs from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; - -const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-tree-')); -const root = await fs.realpath(tmp); -await fs.writeFile(path.join(root, 'a.txt'), 'hello'); - -const transport = new StdioClientTransport({ - command: 'npx', - args: ['-y', '@modelcontextprotocol/server-filesystem', root], -}); - -const client = new Client({ name: 'debug-client', version: '1.0.0' }, { capabilities: {} }); -await client.connect(transport); - -const result = await client.callTool({ - name: 'directory_tree', - arguments: { path: root }, -}); - -console.log(result.structuredContent); -await client.close(); -``` - -If this script still throws `-32602`, make sure you are running a recent -`@modelcontextprotocol/server-filesystem` build and not a cached older version. - ### Docker Note: all directories must be mounted to `/projects` by default.