Merge branch 'main' into sep-1330-enums

This commit is contained in:
Cliff Hall
2025-12-11 17:52:10 -05:00
committed by GitHub
12 changed files with 455 additions and 1810 deletions

View File

@@ -27,16 +27,16 @@
"start:streamableHttp": "node dist/streamableHttp.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
"@modelcontextprotocol/sdk": "^1.24.0",
"cors": "^2.8.5",
"express": "^4.22.0",
"express": "^5.2.1",
"jszip": "^3.10.1",
"zod": "^3.25.0",
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/express": "^5.0.6",
"shx": "^0.3.4",
"typescript": "^5.6.2"
}

View File

@@ -0,0 +1,158 @@
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';
import { spawn } from 'child_process';
/**
* Integration tests to verify that tool handlers return structuredContent
* that matches the declared outputSchema.
*
* These tests address issues #3110, #3106, #3093 where tools were returning
* structuredContent: { content: [contentBlock] } (array) instead of
* structuredContent: { content: string } as declared in outputSchema.
*/
describe('structuredContent schema compliance', () => {
let client: Client;
let transport: StdioClientTransport;
let testDir: string;
beforeEach(async () => {
// Create a temp directory for testing
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-test-'));
// Create test files
await fs.writeFile(path.join(testDir, 'test.txt'), 'test content');
await fs.mkdir(path.join(testDir, 'subdir'));
await fs.writeFile(path.join(testDir, 'subdir', 'nested.txt'), 'nested content');
// Start the MCP server
const serverPath = path.resolve(__dirname, '../dist/index.js');
transport = new StdioClientTransport({
command: 'node',
args: [serverPath, testDir],
});
client = new Client({
name: 'test-client',
version: '1.0.0',
}, {
capabilities: {}
});
await client.connect(transport);
});
afterEach(async () => {
await client?.close();
await fs.rm(testDir, { recursive: true, force: true });
});
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 }
});
// The result should have structuredContent
expect(result.structuredContent).toBeDefined();
// structuredContent.content should be a string (matching outputSchema: { content: z.string() })
const structuredContent = result.structuredContent as { content: unknown };
expect(typeof structuredContent.content).toBe('string');
// It should NOT be an array
expect(Array.isArray(structuredContent.content)).toBe(false);
// The content should be valid JSON representing the tree
const treeData = JSON.parse(structuredContent.content as string);
expect(Array.isArray(treeData)).toBe(true);
});
});
describe('list_directory_with_sizes', () => {
it('should return structuredContent.content as a string, not an array', async () => {
const result = await client.callTool({
name: 'list_directory_with_sizes',
arguments: { path: testDir }
});
// The result should have structuredContent
expect(result.structuredContent).toBeDefined();
// structuredContent.content should be a string (matching outputSchema: { content: z.string() })
const structuredContent = result.structuredContent as { content: unknown };
expect(typeof structuredContent.content).toBe('string');
// It should NOT be an array
expect(Array.isArray(structuredContent.content)).toBe(false);
// The content should contain directory listing info
expect(structuredContent.content).toContain('[FILE]');
});
});
describe('move_file', () => {
it('should return structuredContent.content as a string, not an array', 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
}
});
// The result should have structuredContent
expect(result.structuredContent).toBeDefined();
// structuredContent.content should be a string (matching outputSchema: { content: z.string() })
const structuredContent = result.structuredContent as { content: unknown };
expect(typeof structuredContent.content).toBe('string');
// It should NOT be an array
expect(Array.isArray(structuredContent.content)).toBe(false);
// The content should contain success message
expect(structuredContent.content).toContain('Successfully moved');
});
});
describe('list_directory (control - already working)', () => {
it('should return structuredContent.content as a string', async () => {
const result = await client.callTool({
name: 'list_directory',
arguments: { path: testDir }
});
expect(result.structuredContent).toBeDefined();
const structuredContent = result.structuredContent as { content: unknown };
expect(typeof structuredContent.content).toBe('string');
expect(Array.isArray(structuredContent.content)).toBe(false);
});
});
describe('search_files (control - already working)', () => {
it('should return structuredContent.content as a string', async () => {
const result = await client.callTool({
name: 'search_files',
arguments: {
path: testDir,
pattern: '*.txt'
}
});
expect(result.structuredContent).toBeDefined();
const structuredContent = result.structuredContent as { content: unknown };
expect(typeof structuredContent.content).toBe('string');
expect(Array.isArray(structuredContent.content)).toBe(false);
});
});
});

View File

@@ -500,7 +500,7 @@ server.registerTool(
const contentBlock = { type: "text" as const, text };
return {
content: [contentBlock],
structuredContent: { content: [contentBlock] }
structuredContent: { content: text }
};
}
);
@@ -570,7 +570,7 @@ server.registerTool(
const contentBlock = { type: "text" as const, text };
return {
content: [contentBlock],
structuredContent: { content: [contentBlock] }
structuredContent: { content: text }
};
}
);
@@ -599,7 +599,7 @@ server.registerTool(
const contentBlock = { type: "text" as const, text };
return {
content: [contentBlock],
structuredContent: { content: [contentBlock] }
structuredContent: { content: text }
};
}
);

View File

@@ -25,7 +25,7 @@
"test": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
"@modelcontextprotocol/sdk": "^1.24.0",
"diff": "^5.1.0",
"glob": "^10.5.0",
"minimatch": "^10.0.1",

View File

@@ -143,13 +143,15 @@ def test_git_diff_staged_empty(test_repository):
assert result == ""
def test_git_diff(test_repository):
# Get the default branch name (could be "main" or "master")
default_branch = test_repository.active_branch.name
test_repository.git.checkout("-b", "feature-diff")
file_path = Path(test_repository.working_dir) / "test.txt"
file_path.write_text("feature changes")
test_repository.index.add(["test.txt"])
test_repository.index.commit("feature commit")
result = git_diff(test_repository, "master")
result = git_diff(test_repository, default_branch)
assert "test.txt" in result
assert "feature changes" in result

View File

@@ -25,7 +25,7 @@
"test": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0"
"@modelcontextprotocol/sdk": "^1.24.0"
},
"devDependencies": {
"@types/node": "^22",

View File

@@ -25,7 +25,7 @@
"test": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
"@modelcontextprotocol/sdk": "^1.24.0",
"chalk": "^5.3.0",
"yargs": "^17.7.2"
},