Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 259 additions & 52 deletions src/filesystem/__tests__/structured-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ 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';
import { spawn } from 'child_process';

/**
* Integration tests to verify that tool handlers return structuredContent
Expand All @@ -13,6 +12,8 @@ import { spawn } from 'child_process';
* These tests address issues #3110, #3106, #3093 where tools were returning
* structuredContent: { content: [contentBlock] } (array) instead of
* structuredContent: { content: string } as declared in outputSchema.
*
* Coverage: all 14 tools registered by the filesystem MCP server.
*/
describe('structuredContent schema compliance', () => {
let client: Client;
Expand Down Expand Up @@ -50,26 +51,172 @@ describe('structuredContent schema compliance', () => {
await fs.rm(testDir, { recursive: true, force: true });
});

describe('directory_tree', () => {
it('should return structuredContent.content as a string, not an array', async () => {
// ── Tools with outputSchema { content: z.string() } ──────────────

describe('read_text_file', () => {
it('should return structuredContent.content as a string', async () => {
const result = await client.callTool({
name: 'directory_tree',
arguments: { path: testDir }
name: 'read_text_file',
arguments: { path: path.join(testDir, 'test.txt') }
});

expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);
expect(sc.content).toContain('test content');
});
});

describe('read_file (deprecated, back-compat)', () => {
it('should return structuredContent.content as a string, matching read_text_file', async () => {
const result = await client.callTool({
name: 'read_file',
arguments: { path: path.join(testDir, 'test.txt') }
});

// The result should have structuredContent
expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);
expect(sc.content).toContain('test content');
});
});

// structuredContent.content should be a string (matching outputSchema: { content: z.string() })
const structuredContent = result.structuredContent as { content: unknown };
expect(typeof structuredContent.content).toBe('string');
describe('read_multiple_files', () => {
it('should return structuredContent.content as a string containing both paths', async () => {
const file1 = path.join(testDir, 'test.txt');
const file2 = path.join(testDir, 'subdir', 'nested.txt');

// It should NOT be an array
expect(Array.isArray(structuredContent.content)).toBe(false);
const result = await client.callTool({
name: 'read_multiple_files',
arguments: { paths: [file1, file2] }
});

// The content should be valid JSON representing the tree
const treeData = JSON.parse(structuredContent.content as string);
expect(Array.isArray(treeData)).toBe(true);
expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);
expect(sc.content).toContain('test.txt');
expect(sc.content).toContain('nested.txt');
});
});

describe('write_file', () => {
it('should return structuredContent.content as a string and persist data', async () => {
const targetPath = path.join(testDir, 'written.txt');

const result = await client.callTool({
name: 'write_file',
arguments: { path: targetPath, content: 'hello world' }
});

expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);
expect(sc.content).toContain('Successfully wrote');

// Verify the file was actually written
const onDisk = await fs.readFile(targetPath, 'utf-8');
expect(onDisk).toBe('hello world');
});
});

describe('edit_file', () => {
it('should return structuredContent.content as a string with diff output', async () => {
const filePath = path.join(testDir, 'editable.txt');
await fs.writeFile(filePath, 'line one\nline two\nline three\n');

const result = await client.callTool({
name: 'edit_file',
arguments: {
path: filePath,
edits: [{ oldText: 'line two', newText: 'LINE TWO REPLACED' }],
dryRun: false
}
});

expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);

// Verify actual file was edited
const onDisk = await fs.readFile(filePath, 'utf-8');
expect(onDisk).toContain('LINE TWO REPLACED');
});

it('should return structuredContent.content as a string in dryRun mode without modifying the file', async () => {
const filePath = path.join(testDir, 'dryrun.txt');
await fs.writeFile(filePath, 'alpha\nbeta\ngamma\n');

const result = await client.callTool({
name: 'edit_file',
arguments: {
path: filePath,
edits: [{ oldText: 'beta', newText: 'BETA' }],
dryRun: true
}
});

expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);

// File should be unchanged after dry run
const onDisk = await fs.readFile(filePath, 'utf-8');
expect(onDisk).toContain('beta');
expect(onDisk).not.toContain('BETA');
});
});

describe('create_directory', () => {
it('should return structuredContent.content as a string and create the directory', async () => {
const dirPath = path.join(testDir, 'newdir');

const result = await client.callTool({
name: 'create_directory',
arguments: { path: dirPath }
});

expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);
expect(sc.content).toContain('Successfully created directory');

// Verify the directory exists
const stat = await fs.stat(dirPath);
expect(stat.isDirectory()).toBe(true);
});

it('should succeed idempotently when called twice on the same path', async () => {
const dirPath = path.join(testDir, 'idem');

await client.callTool({ name: 'create_directory', arguments: { path: dirPath } });
const result = await client.callTool({ name: 'create_directory', arguments: { path: dirPath } });

expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
});
});

describe('list_directory', () => {
it('should return structuredContent.content as a string with [FILE] and [DIR] markers', async () => {
const result = await client.callTool({
name: 'list_directory',
arguments: { path: testDir }
});

expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);
expect(sc.content).toContain('[FILE]');
expect(sc.content).toContain('[DIR]');
});
});

Expand All @@ -80,79 +227,139 @@ describe('structuredContent schema compliance', () => {
arguments: { path: testDir }
});

// The result should have structuredContent
expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);
expect(sc.content).toContain('[FILE]');
});
});

// structuredContent.content should be a string (matching outputSchema: { content: z.string() })
const structuredContent = result.structuredContent as { content: unknown };
expect(typeof structuredContent.content).toBe('string');
describe('directory_tree', () => {
it('should return structuredContent.content as a string, not an array', async () => {
const result = await client.callTool({
name: 'directory_tree',
arguments: { path: testDir }
});

// It should NOT be an array
expect(Array.isArray(structuredContent.content)).toBe(false);
expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);

// The content should contain directory listing info
expect(structuredContent.content).toContain('[FILE]');
// The content should be valid JSON representing the tree
const treeData = JSON.parse(sc.content as string);
expect(Array.isArray(treeData)).toBe(true);
});
});

describe('move_file', () => {
it('should return structuredContent.content as a string, not an array', async () => {
it('should return structuredContent.content as a string and move the file', async () => {
const sourcePath = path.join(testDir, 'test.txt');
const destPath = path.join(testDir, 'moved.txt');

const result = await client.callTool({
name: 'move_file',
arguments: {
source: sourcePath,
destination: destPath
}
arguments: { source: sourcePath, destination: destPath }
});

// The result should have structuredContent
expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);
expect(sc.content).toContain('Successfully moved');

// structuredContent.content should be a string (matching outputSchema: { content: z.string() })
const structuredContent = result.structuredContent as { content: unknown };
expect(typeof structuredContent.content).toBe('string');
// Verify source is gone and destination exists
await expect(fs.access(sourcePath)).rejects.toThrow();
const destContent = await fs.readFile(destPath, 'utf-8');
expect(destContent).toBe('test content');
});
});

// It should NOT be an array
expect(Array.isArray(structuredContent.content)).toBe(false);
describe('search_files', () => {
it('should return structuredContent.content as a string with matching paths', async () => {
const result = await client.callTool({
name: 'search_files',
arguments: { path: testDir, pattern: '*.txt' }
});

// The content should contain success message
expect(structuredContent.content).toContain('Successfully moved');
expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);
expect(sc.content).toContain('test.txt');
});
});

describe('list_directory (control - already working)', () => {
it('should return structuredContent.content as a string', async () => {
describe('get_file_info', () => {
it('should return structuredContent.content as a string with metadata', async () => {
const result = await client.callTool({
name: 'list_directory',
arguments: { path: testDir }
name: 'get_file_info',
arguments: { path: path.join(testDir, 'test.txt') }
});

expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);
expect(sc.content).toContain('size');
});
});

const structuredContent = result.structuredContent as { content: unknown };
expect(typeof structuredContent.content).toBe('string');
expect(Array.isArray(structuredContent.content)).toBe(false);
describe('list_allowed_directories', () => {
it('should return structuredContent.content as a string listing the allowed directories', async () => {
const result = await client.callTool({
name: 'list_allowed_directories',
arguments: {}
});

expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };
expect(typeof sc.content).toBe('string');
expect(Array.isArray(sc.content)).toBe(false);
expect(sc.content).toContain('Allowed directories');
});
});

describe('search_files (control - already working)', () => {
it('should return structuredContent.content as a string', async () => {
// ── Tool with outputSchema { content: z.array(...) } ─────────────

describe('read_media_file', () => {
it('should return structuredContent.content as an array matching its outputSchema', async () => {
// Write a minimal valid PNG (1x1 pixel, transparent)
const pngHeader = Buffer.from([
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, // 8-bit RGB
0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, // IDAT chunk
0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00,
0x00, 0x00, 0x02, 0x00, 0x01, 0xE2, 0x21, 0xBC,
0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, // IEND chunk
0x44, 0xAE, 0x42, 0x60, 0x82,
]);
const imgPath = path.join(testDir, 'pixel.png');
await fs.writeFile(imgPath, pngHeader);

const result = await client.callTool({
name: 'search_files',
arguments: {
path: testDir,
pattern: '*.txt'
}
name: 'read_media_file',
arguments: { path: imgPath }
});

expect(result.structuredContent).toBeDefined();
const sc = result.structuredContent as { content: unknown };

// read_media_file's outputSchema declares content as an array of
// { type, data, mimeType } objects — unlike every other tool which
// declares content as a plain string.
expect(Array.isArray(sc.content)).toBe(true);

const structuredContent = result.structuredContent as { content: unknown };
expect(typeof structuredContent.content).toBe('string');
expect(Array.isArray(structuredContent.content)).toBe(false);
const items = sc.content as Array<{ type: string; data: string; mimeType: string }>;
expect(items.length).toBeGreaterThanOrEqual(1);
expect(items[0].type).toBe('image');
expect(items[0].mimeType).toBe('image/png');
expect(typeof items[0].data).toBe('string'); // base64
// TODO: see WRG case-study F-006 — envelope reconciliation pending in separate PR
// (https://github.com/yakuphanycl/wrg-skills/blob/main/docs/case-studies/filesystem-mcp-2026-04-26.md#F-006)
});
});
});
Loading