402 lines
13 KiB
TypeScript
402 lines
13 KiB
TypeScript
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* Multi-Agent Orchestrator MCP Server
|
||
|
|
*
|
||
|
|
* 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 { exec } from "child_process";
|
||
|
|
import { promisify } from "util";
|
||
|
|
import { platform } from "os";
|
||
|
|
import { AGENT_CONFIGS } from "./agents/prompts.js";
|
||
|
|
import { AgentType, ParallelResult } from "./agents/types.js";
|
||
|
|
import { logger } from "./utils/logger.js";
|
||
|
|
|
||
|
|
const execAsync = promisify(exec);
|
||
|
|
|
||
|
|
// 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: {},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Cursor Agent CLI를 통해 에이전트 호출
|
||
|
|
* Cursor Team Plan 사용 - API 키 불필요!
|
||
|
|
*
|
||
|
|
* 크로스 플랫폼 지원:
|
||
|
|
* - Windows: cmd /c "echo. | agent ..." (stdin 닫기 위해)
|
||
|
|
* - Mac/Linux: echo "" | agent ... (bash 사용)
|
||
|
|
*/
|
||
|
|
async function callAgentCLI(
|
||
|
|
agentType: AgentType,
|
||
|
|
task: string,
|
||
|
|
context?: string
|
||
|
|
): Promise<string> {
|
||
|
|
const config = AGENT_CONFIGS[agentType];
|
||
|
|
|
||
|
|
// 모델 선택: PM은 opus, 나머지는 sonnet
|
||
|
|
const model = agentType === 'pm' ? 'opus-4.5' : 'sonnet-4.5';
|
||
|
|
|
||
|
|
logger.info(`Calling ${agentType} agent via CLI`, { model, task: task.substring(0, 100) });
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 프롬프트 구성
|
||
|
|
const systemPrompt = config.systemPrompt
|
||
|
|
.replace(/\r?\n/g, ' ') // 줄바꿈을 공백으로
|
||
|
|
.replace(/"/g, '\\"'); // 쌍따옴표 이스케이프
|
||
|
|
|
||
|
|
const userMessage = context
|
||
|
|
? `${task} (Background info: ${context})`
|
||
|
|
: task;
|
||
|
|
|
||
|
|
// 전체 프롬프트 (시스템 + 유저)
|
||
|
|
const fullPrompt = `SYSTEM INSTRUCTIONS: ${systemPrompt} --- TASK REQUEST: ${userMessage}`
|
||
|
|
.replace(/\[/g, '(') // 대괄호를 괄호로 변환 (쉘 호환)
|
||
|
|
.replace(/\]/g, ')')
|
||
|
|
.replace(/"/g, '\\"'); // 쌍따옴표 이스케이프
|
||
|
|
|
||
|
|
let cmd: string;
|
||
|
|
let shell: string;
|
||
|
|
|
||
|
|
if (isWindows) {
|
||
|
|
// Windows: CMD를 통해 echo로 빈 입력 파이프
|
||
|
|
cmd = `cmd /c "echo. | agent -p \\"${fullPrompt}\\" --model ${model} --output-format text"`;
|
||
|
|
shell = 'cmd.exe';
|
||
|
|
} else {
|
||
|
|
// Mac/Linux: Bash를 통해 빈 문자열 파이프
|
||
|
|
// 참고: Mac에서는 agent CLI가 ~/.cursor-agent/bin/agent 경로에 있을 수 있음
|
||
|
|
cmd = `echo "" | agent -p "${fullPrompt}" --model ${model} --output-format text`;
|
||
|
|
shell = '/bin/bash';
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.debug(`Executing on ${isWindows ? 'Windows' : 'Mac/Linux'}: agent -p "..." --model ${model}`);
|
||
|
|
|
||
|
|
const { stdout, stderr } = await execAsync(cmd, {
|
||
|
|
cwd: process.cwd(),
|
||
|
|
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
||
|
|
timeout: 300000, // 5분 타임아웃
|
||
|
|
shell,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (stderr && !stderr.includes('warning')) {
|
||
|
|
logger.warn(`${agentType} agent stderr`, { stderr });
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info(`${agentType} agent completed via CLI`);
|
||
|
|
return stdout.trim();
|
||
|
|
} catch (error) {
|
||
|
|
logger.error(`${agentType} agent CLI error`, error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 도구 목록 핸들러
|
||
|
|
*/
|
||
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||
|
|
return {
|
||
|
|
tools: [
|
||
|
|
{
|
||
|
|
name: "ask_backend_agent",
|
||
|
|
description:
|
||
|
|
"백엔드 전문가에게 질문하거나 작업을 요청합니다. " +
|
||
|
|
"API 설계, 서비스 로직, 라우팅, 미들웨어 관련 작업에 사용하세요. " +
|
||
|
|
"담당 폴더: backend-node/src/ (Cursor Team Plan 사용, sonnet-4.5 모델)",
|
||
|
|
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 Team Plan 사용, sonnet-4.5 모델)",
|
||
|
|
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 Team Plan 사용, sonnet-4.5 모델)",
|
||
|
|
inputSchema: {
|
||
|
|
type: "object" as const,
|
||
|
|
properties: {
|
||
|
|
task: {
|
||
|
|
type: "string",
|
||
|
|
description: "프론트엔드 에이전트에게 요청할 작업 내용",
|
||
|
|
},
|
||
|
|
context: {
|
||
|
|
type: "string",
|
||
|
|
description: "작업에 필요한 배경 정보 (선택사항)",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
required: ["task"],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "parallel_ask",
|
||
|
|
description:
|
||
|
|
"여러 전문가에게 동시에 질문합니다 (진짜 병렬 실행!). " +
|
||
|
|
"정보 수집 단계에서 모든 영역의 현황을 빠르게 파악할 때 유용합니다. " +
|
||
|
|
"모든 에이전트가 동시에 실행되어 시간 절약! (Cursor Team Plan 사용)",
|
||
|
|
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 (TRUE PARALLEL!)`);
|
||
|
|
|
||
|
|
// 진짜 병렬 실행! 모든 에이전트가 동시에 작업
|
||
|
|
const results: ParallelResult[] = await Promise.all(
|
||
|
|
requests.map(async (req) => {
|
||
|
|
try {
|
||
|
|
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)",
|
||
|
|
apiKey: "NOT REQUIRED! Using Cursor subscription",
|
||
|
|
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,
|
||
|
|
separate_api_key: false,
|
||
|
|
real_multi_session: 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 (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);
|
||
|
|
});
|