import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'; import { fileURLToPath } from 'node:url'; function parseJsonToolResult(result: unknown) { const response = result as { content?: Array<{ text?: string }>; isError?: boolean; }; const text = response.content?.[0]?.text; if (response.isError) { throw new Error(text ?? 'Tool returned an unspecified error'); } return JSON.parse(text ?? 'null'); } async function writeTeamConfig(claudeDir: string, teamName: string) { const teamDir = path.join(claudeDir, 'teams', teamName); await mkdir(teamDir, { recursive: true }); await writeFile( path.join(teamDir, 'config.json'), JSON.stringify( { name: teamName, members: [ { name: 'team-lead', agentType: 'team-lead' }, { name: 'alice', agentType: 'teammate', role: 'developer' }, ], }, null, 2 ), 'utf8' ); } class McpStdIoClient { private readonly child: ChildProcessWithoutNullStreams; private stdoutBuffer = ''; constructor(serverPath: string, cwd: string) { this.child = spawn('node', [serverPath], { cwd, stdio: ['pipe', 'pipe', 'pipe'], }); this.child.stdout.setEncoding('utf8'); this.child.stdout.on('data', (chunk: string) => { this.stdoutBuffer += chunk; }); } async initialize() { const response = await this.request(1, 'initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'vitest-e2e', version: '1.0.0' }, }); this.notify('notifications/initialized'); return response; } async listTools() { return this.request(2, 'tools/list', {}); } async callTool(name: string, args: Record, id = 3) { return this.request(id, 'tools/call', { name, arguments: args }); } async close() { this.child.kill('SIGTERM'); await new Promise((resolve) => { this.child.once('exit', () => resolve()); setTimeout(() => resolve(), 1000).unref(); }); } private notify(method: string, params?: Record) { this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method, ...(params ? { params } : {}) })}\n`); } private async request(id: number, method: string, params: Record) { this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`); return this.readMessage(id); } private async readMessage(expectedId: number) { const deadline = Date.now() + 15000; while (Date.now() < deadline) { const newlineIndex = this.stdoutBuffer.indexOf('\n'); if (newlineIndex !== -1) { const line = this.stdoutBuffer.slice(0, newlineIndex).trim(); this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1); if (!line) { continue; } const parsed = JSON.parse(line) as { id?: number }; if (parsed.id === expectedId) { return parsed; } } await new Promise((resolve) => setTimeout(resolve, 20)); } throw new Error(`Timed out waiting for MCP response ${expectedId}`); } } describe('agent-teams-mcp stdio e2e', () => { const serverPath = fileURLToPath(new URL('../dist/index.js', import.meta.url)); const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url)); let claudeDir: string; beforeEach(async () => { claudeDir = await mkdtemp(path.join(os.tmpdir(), 'agent-teams-mcp-e2e-')); }); afterEach(async () => { await rm(claudeDir, { recursive: true, force: true }); }); it('boots over stdio, lists task tools, and executes task lifecycle calls', async () => { await writeTeamConfig(claudeDir, 'e2e-team'); const client = new McpStdIoClient(serverPath, workspaceRoot); try { const init = await client.initialize(); expect(init).toHaveProperty('result'); const tools = (await client.listTools()) as { result?: { tools?: Array<{ name: string }> }; }; const toolNames = (tools.result?.tools ?? []).map((tool) => tool.name); expect(toolNames).toContain('task_create'); expect(toolNames).toContain('task_start'); expect(toolNames).toContain('review_approve'); const createResult = await client.callTool( 'task_create', { claudeDir, teamName: 'e2e-team', subject: 'Smoke task', owner: 'alice', }, 3 ); const createdTask = parseJsonToolResult((createResult as { result: unknown }).result); expect(createdTask.subject).toBe('Smoke task'); expect(createdTask.owner).toBe('alice'); expect(typeof createdTask.id).toBe('string'); const startResult = await client.callTool( 'task_start', { claudeDir, teamName: 'e2e-team', taskId: createdTask.id, actor: 'alice', }, 4 ); const startedTask = parseJsonToolResult((startResult as { result: unknown }).result); expect(startedTask.status).toBe('in_progress'); expect(startedTask.id).toBe(createdTask.id); } finally { await client.close(); } }); });