feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* MCP 配置加载和验证测试
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
loadMCPConfig,
|
||||
validateMCPConfig,
|
||||
normalizeMCPConfig,
|
||||
getEnabledServers,
|
||||
isToolEnabled,
|
||||
resolveEnvVariables,
|
||||
resolveEnvInObject,
|
||||
} from '../../../src/mcp/config.js';
|
||||
import type { MCPConfig } from '../../../src/mcp/types.js';
|
||||
|
||||
// Mock fs 模块
|
||||
vi.mock('fs');
|
||||
vi.mock('path', async () => {
|
||||
const actual = await vi.importActual('path');
|
||||
return {
|
||||
...actual,
|
||||
extname: (actual as typeof import('path')).extname,
|
||||
};
|
||||
});
|
||||
|
||||
describe('MCP Config', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
// 默认不存在配置文件
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('resolveEnvVariables', () => {
|
||||
it('应该解析环境变量引用', () => {
|
||||
process.env.TEST_VAR = 'test_value';
|
||||
const result = resolveEnvVariables('{env:TEST_VAR}');
|
||||
expect(result).toBe('test_value');
|
||||
delete process.env.TEST_VAR;
|
||||
});
|
||||
|
||||
it('应该处理多个环境变量', () => {
|
||||
process.env.VAR1 = 'value1';
|
||||
process.env.VAR2 = 'value2';
|
||||
const result = resolveEnvVariables('{env:VAR1}/{env:VAR2}');
|
||||
expect(result).toBe('value1/value2');
|
||||
delete process.env.VAR1;
|
||||
delete process.env.VAR2;
|
||||
});
|
||||
|
||||
it('应该对未设置的变量返回空字符串', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const result = resolveEnvVariables('{env:NONEXISTENT_VAR}');
|
||||
expect(result).toBe('');
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('应该保留普通字符串不变', () => {
|
||||
const result = resolveEnvVariables('plain string');
|
||||
expect(result).toBe('plain string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveEnvInObject', () => {
|
||||
it('应该递归解析对象中的环境变量', () => {
|
||||
process.env.TEST_KEY = 'secret';
|
||||
const obj = {
|
||||
key: '{env:TEST_KEY}',
|
||||
nested: {
|
||||
value: '{env:TEST_KEY}',
|
||||
},
|
||||
array: ['{env:TEST_KEY}'],
|
||||
};
|
||||
const result = resolveEnvInObject(obj);
|
||||
expect(result).toEqual({
|
||||
key: 'secret',
|
||||
nested: {
|
||||
value: 'secret',
|
||||
},
|
||||
array: ['secret'],
|
||||
});
|
||||
delete process.env.TEST_KEY;
|
||||
});
|
||||
|
||||
it('应该保留非字符串值不变', () => {
|
||||
const obj = {
|
||||
number: 123,
|
||||
boolean: true,
|
||||
null: null,
|
||||
};
|
||||
const result = resolveEnvInObject(obj);
|
||||
expect(result).toEqual(obj);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateMCPConfig', () => {
|
||||
it('应该接受空配置', () => {
|
||||
const errors = validateMCPConfig({});
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('应该接受有效的本地服务器配置', () => {
|
||||
const config: MCPConfig = {
|
||||
mcp: {
|
||||
filesystem: {
|
||||
type: 'local',
|
||||
command: ['npx', '-y', '@anthropic-ai/mcp-server-filesystem'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const errors = validateMCPConfig(config);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('应该接受有效的远程服务器配置', () => {
|
||||
const config: MCPConfig = {
|
||||
mcp: {
|
||||
remote: {
|
||||
type: 'remote',
|
||||
url: 'https://mcp.example.com',
|
||||
},
|
||||
},
|
||||
};
|
||||
const errors = validateMCPConfig(config);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('应该拒绝无效的服务器名称', () => {
|
||||
const config: MCPConfig = {
|
||||
mcp: {
|
||||
'123invalid': {
|
||||
type: 'local',
|
||||
command: ['echo'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const errors = validateMCPConfig(config);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0]).toContain('服务器名称');
|
||||
});
|
||||
|
||||
it('应该拒绝缺少 command 的本地服务器', () => {
|
||||
const config: MCPConfig = {
|
||||
mcp: {
|
||||
server: {
|
||||
type: 'local',
|
||||
command: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const errors = validateMCPConfig(config);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0]).toContain('command');
|
||||
});
|
||||
|
||||
it('应该拒绝无效的 URL', () => {
|
||||
const config: MCPConfig = {
|
||||
mcp: {
|
||||
remote: {
|
||||
type: 'remote',
|
||||
url: 'invalid-url',
|
||||
},
|
||||
},
|
||||
};
|
||||
const errors = validateMCPConfig(config);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0]).toContain('url');
|
||||
});
|
||||
|
||||
it('应该验证 tools 配置', () => {
|
||||
const config: MCPConfig = {
|
||||
mcp: {},
|
||||
tools: {
|
||||
'valid-tool': true,
|
||||
'invalid-tool': 'not-boolean' as unknown as boolean,
|
||||
},
|
||||
};
|
||||
const errors = validateMCPConfig(config);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0]).toContain('布尔值');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeMCPConfig', () => {
|
||||
it('应该添加默认值', () => {
|
||||
const config: MCPConfig = {
|
||||
mcp: {
|
||||
server: {
|
||||
type: 'local',
|
||||
command: ['echo'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const normalized = normalizeMCPConfig(config);
|
||||
expect(normalized.mcp?.server.enabled).toBe(true);
|
||||
expect(normalized.mcp?.server.timeout).toBe(30000);
|
||||
});
|
||||
|
||||
it('应该保留显式设置的值', () => {
|
||||
const config: MCPConfig = {
|
||||
mcp: {
|
||||
server: {
|
||||
type: 'local',
|
||||
command: ['echo'],
|
||||
enabled: false,
|
||||
timeout: 5000,
|
||||
},
|
||||
},
|
||||
};
|
||||
const normalized = normalizeMCPConfig(config);
|
||||
expect(normalized.mcp?.server.enabled).toBe(false);
|
||||
expect(normalized.mcp?.server.timeout).toBe(5000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEnabledServers', () => {
|
||||
it('应该返回启用的服务器', () => {
|
||||
const config: MCPConfig = {
|
||||
mcp: {
|
||||
enabled: {
|
||||
type: 'local',
|
||||
command: ['echo'],
|
||||
enabled: true,
|
||||
},
|
||||
disabled: {
|
||||
type: 'local',
|
||||
command: ['echo'],
|
||||
enabled: false,
|
||||
},
|
||||
default: {
|
||||
type: 'local',
|
||||
command: ['echo'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const servers = getEnabledServers(config);
|
||||
expect(servers).toHaveLength(2);
|
||||
expect(servers.map((s) => s.name)).toContain('enabled');
|
||||
expect(servers.map((s) => s.name)).toContain('default');
|
||||
expect(servers.map((s) => s.name)).not.toContain('disabled');
|
||||
});
|
||||
|
||||
it('应该处理空配置', () => {
|
||||
const servers = getEnabledServers({});
|
||||
expect(servers).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isToolEnabled', () => {
|
||||
it('没有配置时默认启用', () => {
|
||||
expect(isToolEnabled('any-tool')).toBe(true);
|
||||
expect(isToolEnabled('any-tool', undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该精确匹配工具名', () => {
|
||||
const config = {
|
||||
'server-tool': false,
|
||||
'server-other': true,
|
||||
};
|
||||
expect(isToolEnabled('server-tool', config)).toBe(false);
|
||||
expect(isToolEnabled('server-other', config)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该支持通配符匹配', () => {
|
||||
const config = {
|
||||
'server-*': false,
|
||||
};
|
||||
expect(isToolEnabled('server-tool1', config)).toBe(false);
|
||||
expect(isToolEnabled('server-tool2', config)).toBe(false);
|
||||
expect(isToolEnabled('other-tool', config)).toBe(true);
|
||||
});
|
||||
|
||||
it('精确匹配应优先于通配符', () => {
|
||||
const config = {
|
||||
'server-*': false,
|
||||
'server-special': true,
|
||||
};
|
||||
expect(isToolEnabled('server-special', config)).toBe(true);
|
||||
expect(isToolEnabled('server-other', config)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadMCPConfig', () => {
|
||||
it('没有配置文件时返回空配置', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
const config = loadMCPConfig('/test/dir');
|
||||
expect(config).toEqual({});
|
||||
});
|
||||
|
||||
it('应该加载 JSON 配置文件', () => {
|
||||
const testConfig = {
|
||||
mcp: {
|
||||
server: {
|
||||
type: 'local',
|
||||
command: ['echo'],
|
||||
},
|
||||
},
|
||||
};
|
||||
vi.mocked(fs.existsSync).mockImplementation((filePath) => {
|
||||
return String(filePath).endsWith('config.json');
|
||||
});
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(testConfig));
|
||||
|
||||
const config = loadMCPConfig('/test/dir');
|
||||
expect(config.mcp).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该合并用户级和项目级配置', () => {
|
||||
const userConfig = {
|
||||
mcp: {
|
||||
userServer: {
|
||||
type: 'local',
|
||||
command: ['user-cmd'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const projectConfig = {
|
||||
mcp: {
|
||||
projectServer: {
|
||||
type: 'local',
|
||||
command: ['project-cmd'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let callCount = 0;
|
||||
vi.mocked(fs.existsSync).mockImplementation((filePath) => {
|
||||
return (
|
||||
String(filePath).includes('.ai-assist') &&
|
||||
String(filePath).endsWith('config.json')
|
||||
);
|
||||
});
|
||||
vi.mocked(fs.readFileSync).mockImplementation(() => {
|
||||
callCount++;
|
||||
// 第一次调用返回用户配置,第二次返回项目配置
|
||||
return JSON.stringify(callCount === 1 ? userConfig : projectConfig);
|
||||
});
|
||||
|
||||
const config = loadMCPConfig('/test/dir');
|
||||
expect(config.mcp?.userServer).toBeDefined();
|
||||
expect(config.mcp?.projectServer).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* MCP Manager 测试
|
||||
* 测试不需要实际连接的功能
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { MCPManager, getMCPManager, resetMCPManager } from '../../../src/mcp/manager.js';
|
||||
import type { MCPConfig } from '../../../src/mcp/types.js';
|
||||
|
||||
describe('MCPManager', () => {
|
||||
afterEach(async () => {
|
||||
resetMCPManager();
|
||||
});
|
||||
|
||||
describe('基础状态', () => {
|
||||
it('初始状态应该是未初始化', () => {
|
||||
const manager = new MCPManager();
|
||||
expect(manager.isInitialized()).toBe(false);
|
||||
});
|
||||
|
||||
it('初始时没有工具', () => {
|
||||
const manager = new MCPManager();
|
||||
expect(manager.getTools()).toEqual([]);
|
||||
});
|
||||
|
||||
it('初始时没有服务器状态', () => {
|
||||
const manager = new MCPManager();
|
||||
expect(manager.getServerStatuses()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('配置处理', () => {
|
||||
it('空配置不应该连接任何服务器', async () => {
|
||||
const manager = new MCPManager();
|
||||
await manager.initialize({});
|
||||
expect(manager.isInitialized()).toBe(true);
|
||||
expect(manager.getTools()).toEqual([]);
|
||||
});
|
||||
|
||||
it('禁用的服务器应该显示在状态中', async () => {
|
||||
const manager = new MCPManager();
|
||||
const config: MCPConfig = {
|
||||
mcp: {
|
||||
disabled: {
|
||||
type: 'local',
|
||||
command: ['echo'],
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
await manager.initialize(config);
|
||||
const statuses = manager.getServerStatuses();
|
||||
expect(statuses).toHaveLength(1);
|
||||
expect(statuses[0].name).toBe('disabled');
|
||||
expect(statuses[0].status).toBe('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('工具名解析', () => {
|
||||
it('无效的工具名格式应返回错误', async () => {
|
||||
const manager = new MCPManager();
|
||||
await manager.initialize({});
|
||||
|
||||
const result = await manager.callTool('invalidname', {});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.isError).toBe(true);
|
||||
expect((result.content[0] as { text: string }).text).toContain('Invalid tool name');
|
||||
});
|
||||
|
||||
it('服务器未连接时应返回错误', async () => {
|
||||
const manager = new MCPManager();
|
||||
await manager.initialize({});
|
||||
|
||||
const result = await manager.callTool('server-tool', {});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.isError).toBe(true);
|
||||
expect((result.content[0] as { text: string }).text).toContain('not connected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reconnect', () => {
|
||||
it('服务器不存在时应抛出错误', async () => {
|
||||
const manager = new MCPManager();
|
||||
await manager.initialize({ mcp: {} });
|
||||
|
||||
await expect(manager.reconnect('nonexistent')).rejects.toThrow('Server not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setServerEnabled', () => {
|
||||
it('服务器不存在时应抛出错误', async () => {
|
||||
const manager = new MCPManager();
|
||||
await manager.initialize({ mcp: {} });
|
||||
|
||||
await expect(manager.setServerEnabled('nonexistent', true)).rejects.toThrow(
|
||||
'Server not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shutdown', () => {
|
||||
it('空初始化后可以安全关闭', async () => {
|
||||
const manager = new MCPManager();
|
||||
await manager.initialize({});
|
||||
await manager.shutdown();
|
||||
expect(manager.isInitialized()).toBe(false);
|
||||
});
|
||||
|
||||
it('未初始化时可以安全关闭', async () => {
|
||||
const manager = new MCPManager();
|
||||
await manager.shutdown();
|
||||
expect(manager.isInitialized()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('事件发射', () => {
|
||||
it('应该是 EventEmitter 实例', () => {
|
||||
const manager = new MCPManager();
|
||||
expect(typeof manager.on).toBe('function');
|
||||
expect(typeof manager.emit).toBe('function');
|
||||
});
|
||||
|
||||
it('应该支持事件监听', () => {
|
||||
const manager = new MCPManager();
|
||||
const callback = vi.fn();
|
||||
manager.on('tools:changed', callback);
|
||||
manager.emit('tools:changed');
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMCPManager / resetMCPManager', () => {
|
||||
afterEach(() => {
|
||||
resetMCPManager();
|
||||
});
|
||||
|
||||
it('应该返回单例实例', () => {
|
||||
const manager1 = getMCPManager();
|
||||
const manager2 = getMCPManager();
|
||||
expect(manager1).toBe(manager2);
|
||||
});
|
||||
|
||||
it('重置后应该返回新实例', () => {
|
||||
const manager1 = getMCPManager();
|
||||
resetMCPManager();
|
||||
const manager2 = getMCPManager();
|
||||
expect(manager1).not.toBe(manager2);
|
||||
});
|
||||
|
||||
it('重置的实例应该是未初始化状态', () => {
|
||||
const manager1 = getMCPManager();
|
||||
resetMCPManager();
|
||||
const manager2 = getMCPManager();
|
||||
expect(manager2.isInitialized()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user