Files
ai-terminal-assistant/tests/unit/mcp/tool-adapter.test.ts
T
kurihada 4beaf088d0 feat: 添加 MCP (Model Context Protocol) 集成支持
实现 MCP 协议集成,允许通过外部服务器扩展工具能力:

核心模块:
- types.ts: MCP 类型定义 (LocalMCPServer, RemoteMCPServer, MCPTool 等)
- config.ts: 配置加载、验证,支持 {env:VAR} 环境变量语法
- transports/stdio.ts: stdio 传输层实现 (JSON-RPC 2.0)
- client.ts: MCP 客户端,处理协议握手和工具调用
- manager.ts: 多服务器生命周期管理
- tool-adapter.ts: 将 MCP 工具转换为内部格式

CLI 命令:
- ai-assist mcp list: 列出服务器状态
- ai-assist mcp tools: 列出可用工具
- ai-assist mcp test <server>: 测试连接

配置支持:
- 用户级 (~/.ai-assist/config.json) 和项目级配置
- JSON/YAML 格式
- 通配符模式工具启用/禁用
2025-12-11 21:18:41 +08:00

290 lines
8.4 KiB
TypeScript

/**
* MCP 工具适配器测试
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MCPToolAdapter, createMCPToolAdapter } from '../../../src/mcp/tool-adapter.js';
import type { MCPTool, MCPManager, MCPToolCallResult } from '../../../src/mcp/types.js';
// 创建 mock MCPManager
function createMockManager(): MCPManager {
return {
callTool: vi.fn(),
getTools: vi.fn().mockReturnValue([]),
getServerStatuses: vi.fn().mockReturnValue([]),
initialize: vi.fn(),
shutdown: vi.fn(),
on: vi.fn(),
emit: vi.fn(),
} as unknown as MCPManager;
}
describe('MCPToolAdapter', () => {
let mockManager: MCPManager;
let adapter: MCPToolAdapter;
beforeEach(() => {
mockManager = createMockManager();
adapter = new MCPToolAdapter(mockManager);
});
describe('adaptToInternalTool', () => {
it('应该将 MCP 工具转换为内部格式', () => {
const mcpTool: MCPTool = {
server: 'filesystem',
name: 'filesystem-read_file',
originalName: 'read_file',
description: 'Read a file from the filesystem',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Path to the file',
},
},
required: ['path'],
},
};
const internalTool = adapter.adaptToInternalTool(mcpTool);
expect(internalTool.name).toBe('filesystem-read_file');
expect(internalTool.description).toBe('Read a file from the filesystem');
expect(internalTool.parameters).toHaveProperty('path');
expect(internalTool.parameters.path.type).toBe('string');
expect(internalTool.parameters.path.required).toBe(true);
expect(internalTool.metadata.category).toBe('agent');
expect(internalTool.metadata.keywords).toContain('mcp');
expect(internalTool.metadata.keywords).toContain('filesystem');
});
it('应该正确转换参数类型', () => {
const mcpTool: MCPTool = {
server: 'test',
name: 'test-tool',
originalName: 'tool',
description: 'Test tool',
inputSchema: {
type: 'object',
properties: {
stringParam: { type: 'string' },
numberParam: { type: 'number' },
integerParam: { type: 'integer' },
booleanParam: { type: 'boolean' },
arrayParam: { type: 'array' },
objectParam: { type: 'object' },
},
},
};
const internalTool = adapter.adaptToInternalTool(mcpTool);
expect(internalTool.parameters.stringParam.type).toBe('string');
expect(internalTool.parameters.numberParam.type).toBe('number');
expect(internalTool.parameters.integerParam.type).toBe('number');
expect(internalTool.parameters.booleanParam.type).toBe('boolean');
expect(internalTool.parameters.arrayParam.type).toBe('array');
expect(internalTool.parameters.objectParam.type).toBe('object');
});
it('应该处理联合类型', () => {
const mcpTool: MCPTool = {
server: 'test',
name: 'test-tool',
originalName: 'tool',
description: 'Test tool',
inputSchema: {
type: 'object',
properties: {
nullable: { type: ['string', 'null'] },
},
},
};
const internalTool = adapter.adaptToInternalTool(mcpTool);
expect(internalTool.parameters.nullable.type).toBe('string');
});
it('应该处理空 inputSchema', () => {
const mcpTool: MCPTool = {
server: 'test',
name: 'test-tool',
originalName: 'tool',
description: 'Test tool',
inputSchema: {},
};
const internalTool = adapter.adaptToInternalTool(mcpTool);
expect(internalTool.parameters).toEqual({});
});
it('execute 应该调用 manager.callTool', async () => {
const mcpTool: MCPTool = {
server: 'test',
name: 'test-tool',
originalName: 'tool',
description: 'Test tool',
inputSchema: {
type: 'object',
properties: {
arg: { type: 'string' },
},
},
};
const mockResult: MCPToolCallResult = {
success: true,
content: [{ type: 'text', text: 'result' }],
isError: false,
};
vi.mocked(mockManager.callTool).mockResolvedValue(mockResult);
const internalTool = adapter.adaptToInternalTool(mcpTool);
const result = await internalTool.execute({ arg: 'value' });
expect(mockManager.callTool).toHaveBeenCalledWith('test-tool', {
arg: 'value',
});
expect(result.success).toBe(true);
expect(result.output).toBe('result');
});
it('execute 应该处理错误结果', async () => {
const mcpTool: MCPTool = {
server: 'test',
name: 'test-tool',
originalName: 'tool',
description: 'Test tool',
inputSchema: {},
};
const mockResult: MCPToolCallResult = {
success: false,
content: [{ type: 'text', text: 'error message' }],
isError: true,
};
vi.mocked(mockManager.callTool).mockResolvedValue(mockResult);
const internalTool = adapter.adaptToInternalTool(mcpTool);
const result = await internalTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toBe('error message');
});
});
describe('adaptTools', () => {
it('应该批量转换工具', () => {
const mcpTools: MCPTool[] = [
{
server: 'server1',
name: 'server1-tool1',
originalName: 'tool1',
description: 'Tool 1',
inputSchema: {},
},
{
server: 'server2',
name: 'server2-tool2',
originalName: 'tool2',
description: 'Tool 2',
inputSchema: {},
},
];
const internalTools = adapter.adaptTools(mcpTools);
expect(internalTools).toHaveLength(2);
expect(internalTools[0].name).toBe('server1-tool1');
expect(internalTools[1].name).toBe('server2-tool2');
});
});
describe('createMCPToolAdapter', () => {
it('应该创建适配器实例', () => {
const adapter = createMCPToolAdapter(mockManager);
expect(adapter).toBeInstanceOf(MCPToolAdapter);
});
});
describe('内容转换', () => {
it('应该处理图片内容', async () => {
const mcpTool: MCPTool = {
server: 'test',
name: 'test-tool',
originalName: 'tool',
description: 'Test tool',
inputSchema: {},
};
const mockResult: MCPToolCallResult = {
success: true,
content: [{ type: 'image', data: 'base64data', mimeType: 'image/png' }],
isError: false,
};
vi.mocked(mockManager.callTool).mockResolvedValue(mockResult);
const internalTool = adapter.adaptToInternalTool(mcpTool);
const result = await internalTool.execute({});
expect(result.output).toBe('[Image: image/png]');
});
it('应该处理资源内容', async () => {
const mcpTool: MCPTool = {
server: 'test',
name: 'test-tool',
originalName: 'tool',
description: 'Test tool',
inputSchema: {},
};
const mockResult: MCPToolCallResult = {
success: true,
content: [
{
type: 'resource',
resource: {
uri: 'file:///test.txt',
text: 'file content',
},
},
],
isError: false,
};
vi.mocked(mockManager.callTool).mockResolvedValue(mockResult);
const internalTool = adapter.adaptToInternalTool(mcpTool);
const result = await internalTool.execute({});
expect(result.output).toBe('file content');
});
it('应该处理混合内容', async () => {
const mcpTool: MCPTool = {
server: 'test',
name: 'test-tool',
originalName: 'tool',
description: 'Test tool',
inputSchema: {},
};
const mockResult: MCPToolCallResult = {
success: true,
content: [
{ type: 'text', text: 'line 1' },
{ type: 'text', text: 'line 2' },
],
isError: false,
};
vi.mocked(mockManager.callTool).mockResolvedValue(mockResult);
const internalTool = adapter.adaptToInternalTool(mcpTool);
const result = await internalTool.execute({});
expect(result.output).toBe('line 1\nline 2');
});
});
});