#!/usr/bin/env node /** b * Multi-Agent Orchestrator MCP Server v2.0 * * Cursor Agent CLI를 활용한 멀티에이전트 시스템 * - PM (Cursor IDE): 전체 조율 * - Sub-agents (agent CLI): 전문가별 작업 수행 * * 모든 AI 호출이 Cursor Team Plan으로 처리됨! * API 키 불필요! */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { spawn } from "child_process"; import { platform } from "os"; import { AGENT_CONFIGS } from "./agents/prompts.js"; import { AgentType, ParallelResult } from "./agents/types.js"; import { logger } from "./utils/logger.js"; // OS 감지 const isWindows = platform() === "win32"; logger.info(`Platform detected: ${platform()} (isWindows: ${isWindows})`); // MCP 서버 생성 const server = new Server( { name: "agent-orchestrator", version: "2.0.0", }, { capabilities: { tools: {}, }, } ); /** * 유틸: ms만큼 대기 */ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Cursor Agent CLI 단일 호출 (내부용) * spawn + stdin 직접 전달 */ function spawnAgentOnce( agentType: AgentType, fullPrompt: string, model: string ): Promise { const agentPath = isWindows ? 'agent' : `${process.env.HOME}/.local/bin/agent`; return new Promise((resolve, reject) => { let stdout = ''; let stderr = ''; let settled = false; const child = spawn(agentPath, ['--model', model, '--print'], { cwd: process.cwd(), env: { ...process.env, PATH: `${process.env.HOME}/.local/bin:${process.env.PATH}`, }, stdio: ['pipe', 'pipe', 'pipe'], }); child.stdout.on('data', (data: Buffer) => { stdout += data.toString(); }); child.stderr.on('data', (data: Buffer) => { stderr += data.toString(); }); child.on('error', (err: Error) => { if (!settled) { settled = true; reject(err); } }); child.on('close', (code: number | null) => { if (settled) return; settled = true; if (stderr) { const significantStderr = stderr .split('\n') .filter((line: string) => line && !line.includes('warning') && !line.includes('info') && !line.includes('debug')) .join('\n'); if (significantStderr) { logger.warn(`${agentType} agent stderr`, { stderr: significantStderr.substring(0, 500) }); } } if (code === 0 || stdout.trim().length > 0) { resolve(stdout.trim()); } else { reject(new Error( `Agent exited with code ${code}. stderr: ${stderr.substring(0, 1000)}` )); } }); // 타임아웃 (5분) const timeout = setTimeout(() => { if (!settled) { settled = true; child.kill('SIGTERM'); reject(new Error(`${agentType} agent timed out after 5 minutes`)); } }, 300000); child.on('close', () => clearTimeout(timeout)); // stdin으로 프롬프트 직접 전달 child.stdin.write(fullPrompt); child.stdin.end(); }); } /** * Cursor Agent CLI를 통해 에이전트 호출 (재시도 포함) * * - 최대 2회 재시도 (총 3회 시도) * - 재시도 간 2초 대기 (Cursor CLI 동시 실행 제한 대응) */ async function callAgentCLI( agentType: AgentType, task: string, context?: string ): Promise { const config = AGENT_CONFIGS[agentType]; const model = agentType === 'pm' ? 'opus-4.5' : 'sonnet-4.5'; const maxRetries = 2; logger.info(`Calling ${agentType} agent via CLI (spawn+retry)`, { model, task: task.substring(0, 100), }); const userMessage = context ? `${task}\n\n배경 정보:\n${context}` : task; const fullPrompt = `${config.systemPrompt}\n\n---\n\n${userMessage}`; let lastError: Error | null = null; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { if (attempt > 0) { const delay = attempt * 2000; // 2초, 4초 logger.info(`${agentType} agent retry ${attempt}/${maxRetries} (waiting ${delay}ms)`); await sleep(delay); } const result = await spawnAgentOnce(agentType, fullPrompt, model); logger.info(`${agentType} agent completed (attempt ${attempt + 1})`); return result; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); logger.warn(`${agentType} agent attempt ${attempt + 1} failed`, { error: lastError.message.substring(0, 200), }); } } // 모든 재시도 실패 logger.error(`${agentType} agent failed after ${maxRetries + 1} attempts`); throw lastError!; } /** * 도구 목록 핸들러 */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "ask_backend_agent", description: "백엔드 전문가에게 질문하거나 작업을 요청합니다. " + "API 설계, 서비스 로직, 라우팅, 미들웨어 관련 작업에 사용하세요. " + "담당 폴더: backend-node/src/ (Cursor Agent CLI, sonnet-4.5 모델)" + "주의: 단순 파일 읽기/수정은 PM이 직접 처리하세요. 깊은 분석이 필요할 때만 호출!", inputSchema: { type: "object" as const, properties: { task: { type: "string", description: "백엔드 에이전트에게 요청할 작업 내용", }, context: { type: "string", description: "작업에 필요한 배경 정보 (선택사항)", }, }, required: ["task"], }, }, { name: "ask_db_agent", description: "DB 전문가에게 질문하거나 작업을 요청합니다. " + "스키마 설계, SQL 쿼리, MyBatis 매퍼, 마이그레이션 관련 작업에 사용하세요. " + "담당 폴더: src/com/pms/mapper/, db/ (Cursor Agent CLI, sonnet-4.5 모델)" + "주의: 단순 스키마 확인은 PM이 직접 처리하세요. 복잡한 쿼리 설계/최적화 시에만 호출!", inputSchema: { type: "object" as const, properties: { task: { type: "string", description: "DB 에이전트에게 요청할 작업 내용", }, context: { type: "string", description: "작업에 필요한 배경 정보 (선택사항)", }, }, required: ["task"], }, }, { name: "ask_frontend_agent", description: "프론트엔드 전문가에게 질문하거나 작업을 요청합니다. " + "React 컴포넌트, 페이지, 스타일링, 상태관리 관련 작업에 사용하세요. " + "담당 폴더: frontend/ (Cursor Agent CLI, sonnet-4.5 모델)" + "주의: 단순 컴포넌트 읽기/수정은 PM이 직접 처리하세요. 구조 분석이 필요할 때만 호출!", inputSchema: { type: "object" as const, properties: { task: { type: "string", description: "프론트엔드 에이전트에게 요청할 작업 내용", }, context: { type: "string", description: "작업에 필요한 배경 정보 (선택사항)", }, }, required: ["task"], }, }, { name: "parallel_ask", description: "여러 전문가에게 동시에 질문합니다 (진짜 병렬 실행!). " + "3개 영역(FE+BE+DB) 크로스도메인 분석이 필요할 때만 사용하세요. " + "주의: 호출 시간이 오래 걸림! 단순 작업은 PM이 직접 처리하는 게 훨씬 빠릅니다. " + "적합한 경우: 전체 아키텍처 파악, 대규모 리팩토링 계획, 크로스도메인 영향 분석", inputSchema: { type: "object" as const, properties: { requests: { type: "array", description: "각 에이전트에게 보낼 요청 목록", items: { type: "object", properties: { agent: { type: "string", enum: ["backend", "db", "frontend"], description: "요청할 에이전트 타입", }, task: { type: "string", description: "해당 에이전트에게 요청할 작업", }, context: { type: "string", description: "배경 정보 (선택사항)", }, }, required: ["agent", "task"], }, }, }, required: ["requests"], }, }, { name: "get_agent_info", description: "에이전트 시스템의 현재 상태와 사용 가능한 에이전트 정보를 확인합니다.", inputSchema: { type: "object" as const, properties: {}, }, }, ], }; }); /** * 도구 호출 핸들러 */ server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger.info(`Tool called: ${name}`); try { switch (name) { case "ask_backend_agent": { const { task, context } = args as { task: string; context?: string }; const result = await callAgentCLI("backend", task, context); return { content: [{ type: "text" as const, text: result }], }; } case "ask_db_agent": { const { task, context } = args as { task: string; context?: string }; const result = await callAgentCLI("db", task, context); return { content: [{ type: "text" as const, text: result }], }; } case "ask_frontend_agent": { const { task, context } = args as { task: string; context?: string }; const result = await callAgentCLI("frontend", task, context); return { content: [{ type: "text" as const, text: result }], }; } case "parallel_ask": { const { requests } = args as { requests: Array<{ agent: "backend" | "db" | "frontend"; task: string; context?: string; }>; }; logger.info(`Parallel ask to ${requests.length} agents (STAGGERED PARALLEL)`); // 시차 병렬 실행: 각 에이전트를 500ms 간격으로 시작 // Cursor Agent CLI 동시 실행 제한 대응 const STAGGER_DELAY = 500; // ms const results: ParallelResult[] = await Promise.all( requests.map(async (req, index) => { try { // 시차 적용 (첫 번째는 즉시, 이후 500ms 간격) if (index > 0) { await sleep(index * STAGGER_DELAY); } const result = await callAgentCLI(req.agent, req.task, req.context); return { agent: req.agent, result }; } catch (error) { return { agent: req.agent, result: "", error: error instanceof Error ? error.message : "Unknown error", }; } }) ); // 결과를 보기 좋게 포맷팅 const formattedResults = results.map((r) => { const header = `\n${"=".repeat(60)}\n## ${r.agent.toUpperCase()} Agent 응답\n${"=".repeat(60)}\n`; if (r.error) { return `${header}❌ 에러: ${r.error}`; } return `${header}${r.result}`; }); return { content: [ { type: "text" as const, text: formattedResults.join("\n"), }, ], }; } case "get_agent_info": { const info = { system: "Multi-Agent Orchestrator v2.0", version: "2.0.0", backend: "Cursor Agent CLI (Team Plan)", cliPath: `${process.env.HOME}/.local/bin/agent`, apiKey: "NOT REQUIRED! Using Cursor Team Plan credits", agents: { pm: { role: "Project Manager", model: "opus-4.5 (Cursor IDE에서 직접)", description: "전체 조율, 사용자 의도 파악, 작업 분배", }, backend: { role: "Backend Specialist", model: "sonnet-4.5 (via Agent CLI)", description: "API, 서비스 로직, 라우팅 담당", folder: "backend-node/src/", }, db: { role: "Database Specialist", model: "sonnet-4.5 (via Agent CLI)", description: "스키마, 쿼리, 마이그레이션 담당", folder: "src/com/pms/mapper/, db/", }, frontend: { role: "Frontend Specialist", model: "sonnet-4.5 (via Agent CLI)", description: "컴포넌트, 페이지, 스타일링 담당", folder: "frontend/", }, }, features: { parallel_execution: true, cursor_team_plan: true, cursor_agent_cli: true, separate_api_key: false, cross_platform: true, }, usage: { single_agent: "ask_backend_agent, ask_db_agent, ask_frontend_agent", parallel: "parallel_ask로 여러 에이전트 동시 호출", workflow: "1. parallel_ask로 정보 수집 → 2. 개별 에이전트로 작업 분배", }, }; return { content: [ { type: "text" as const, text: JSON.stringify(info, null, 2), }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { logger.error(`Tool error: ${name}`, error); return { content: [ { type: "text" as const, text: `❌ 에러 발생: ${error instanceof Error ? error.message : "Unknown error"}`, }, ], isError: true, }; } }); /** * 서버 시작 */ async function main() { logger.info("Starting Multi-Agent Orchestrator MCP Server v2.0..."); logger.info(`Backend: Cursor Agent CLI (${process.env.HOME}/.local/bin/agent)`); logger.info("Credits: Cursor Team Plan - No API Key Required!"); const transport = new StdioServerTransport(); await server.connect(transport); logger.info("MCP Server connected and ready!"); } main().catch((error) => { logger.error("Server failed to start", error); process.exit(1); });