mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-02 03:00:15 -04:00
Merge branch 'main' into sep-1330-enums
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
158
src/filesystem/__tests__/structured-content.test.ts
Normal file
158
src/filesystem/__tests__/structured-content.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"test": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0"
|
||||
"@modelcontextprotocol/sdk": "^1.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user