Files
qiming/qimingclaw/crates/agent-gui-server/tests/mcp/integration.test.ts

149 lines
4.7 KiB
TypeScript

/**
* MCP Integration Test
*
* Tests the full MCP server lifecycle:
* 1. Start server on a random port
* 2. Connect MCP client via Streamable HTTP
* 3. List tools (should return 14)
* 4. Verify tool names
* 5. Stop server
*
* Note: gui_screenshot and gui_execute_task require actual desktop access,
* so we only verify tool listing and schema in this test.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { createGuiAgentServer, type GuiAgentServer } from '../../src/mcp/server.js';
import type { GuiAgentConfig } from '../../src/config.js';
const TEST_PORT = 60099; // Use a high port unlikely to conflict
function createTestConfig(): GuiAgentConfig {
return {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
apiKey: 'test-integration-key',
port: TEST_PORT,
maxSteps: 10,
stepDelayMs: 100,
stuckThreshold: 3,
jpegQuality: 75,
displayIndex: 0,
transport: 'http',
};
}
const EXPECTED_ATOMIC_TOOLS = [
'gui_screenshot',
'gui_click',
'gui_double_click',
'gui_move_mouse',
'gui_drag',
'gui_scroll',
'gui_type',
'gui_press_key',
'gui_hotkey',
'gui_cursor_position',
'gui_list_displays',
'gui_find_image',
'gui_wait_for_image',
'gui_analyze_screen',
];
const EXPECTED_TASK_TOOLS = [
'gui_execute_task',
];
describe('MCP Integration', () => {
let server: GuiAgentServer;
let client: Client;
let transport: StreamableHTTPClientTransport;
beforeAll(async () => {
// Start server
server = createGuiAgentServer(createTestConfig());
await server.start();
// Connect client
transport = new StreamableHTTPClientTransport(
new URL(`http://127.0.0.1:${TEST_PORT}/mcp`),
);
client = new Client({ name: 'test-client', version: '1.0.0' });
await client.connect(transport);
}, 10000);
afterAll(async () => {
try { await client?.close(); } catch { /* ignore */ }
try { await server?.stop(); } catch { /* ignore */ }
}, 10000);
it('should list all 15 tools', async () => {
const result = await client.listTools();
expect(result.tools).toHaveLength(15);
});
it('should include all atomic tools', async () => {
const result = await client.listTools();
const toolNames = result.tools.map(t => t.name);
for (const name of EXPECTED_ATOMIC_TOOLS) {
expect(toolNames).toContain(name);
}
});
it('should include gui_execute_task tool', async () => {
const result = await client.listTools();
const toolNames = result.tools.map(t => t.name);
for (const name of EXPECTED_TASK_TOOLS) {
expect(toolNames).toContain(name);
}
});
it('should have correct schema for gui_execute_task', async () => {
const result = await client.listTools();
const taskTool = result.tools.find(t => t.name === 'gui_execute_task');
expect(taskTool).toBeDefined();
expect(taskTool!.inputSchema).toBeDefined();
expect(taskTool!.inputSchema.properties).toHaveProperty('task');
expect(taskTool!.inputSchema.required).toContain('task');
});
it('should have correct schema for gui_click', async () => {
const result = await client.listTools();
const clickTool = result.tools.find(t => t.name === 'gui_click');
expect(clickTool).toBeDefined();
expect(clickTool!.inputSchema.properties).toHaveProperty('x');
expect(clickTool!.inputSchema.properties).toHaveProperty('y');
expect(clickTool!.inputSchema.required).toContain('x');
expect(clickTool!.inputSchema.required).toContain('y');
});
it('should list resources', async () => {
const result = await client.listResources();
expect(result.resources.length).toBeGreaterThanOrEqual(3);
const uris = result.resources.map(r => r.uri);
expect(uris).toContain('gui://status');
expect(uris).toContain('gui://permissions');
expect(uris).toContain('gui://audit-log');
});
it('should read gui://status resource', async () => {
const result = await client.readResource({ uri: 'gui://status' });
expect(result.contents).toHaveLength(1);
expect(result.contents[0].mimeType).toBe('application/json');
const status = JSON.parse(result.contents[0].text as string);
expect(status).toHaveProperty('platform');
expect(status).toHaveProperty('model');
expect(status.model).toBe('claude-sonnet-4-20250514');
});
it('should read gui://audit-log resource', async () => {
const result = await client.readResource({ uri: 'gui://audit-log' });
expect(result.contents).toHaveLength(1);
const log = JSON.parse(result.contents[0].text as string);
expect(Array.isArray(log)).toBe(true);
});
});