4beaf088d0
实现 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 格式
- 通配符模式工具启用/禁用
290 lines
8.4 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|