From 04565eb4807bf93629f9d74a4748e34414aab92d Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 6 Feb 2026 00:03:56 +0900 Subject: [PATCH 1/5] feat: Add Multi-Agent Orchestrator MCP Server using Cursor Agent CLI --- .cursor/mcp.json | 8 + docs/multi-agent-system-plan.md | 989 ++++++++++++ mcp-agent-orchestrator/README.md | 189 +++ mcp-agent-orchestrator/package-lock.json | 1444 ++++++++++++++++++ mcp-agent-orchestrator/package.json | 29 + mcp-agent-orchestrator/src/agents/index.ts | 6 + mcp-agent-orchestrator/src/agents/prompts.ts | 261 ++++ mcp-agent-orchestrator/src/agents/types.ts | 63 + mcp-agent-orchestrator/src/index.ts | 401 +++++ mcp-agent-orchestrator/src/utils/logger.ts | 55 + mcp-agent-orchestrator/tsconfig.json | 19 + 11 files changed, 3464 insertions(+) create mode 100644 .cursor/mcp.json create mode 100644 docs/multi-agent-system-plan.md create mode 100644 mcp-agent-orchestrator/README.md create mode 100644 mcp-agent-orchestrator/package-lock.json create mode 100644 mcp-agent-orchestrator/package.json create mode 100644 mcp-agent-orchestrator/src/agents/index.ts create mode 100644 mcp-agent-orchestrator/src/agents/prompts.ts create mode 100644 mcp-agent-orchestrator/src/agents/types.ts create mode 100644 mcp-agent-orchestrator/src/index.ts create mode 100644 mcp-agent-orchestrator/src/utils/logger.ts create mode 100644 mcp-agent-orchestrator/tsconfig.json diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 00000000..48855331 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "agent-orchestrator": { + "command": "node", + "args": ["C:/Users/defaultuser0/ERP-node/mcp-agent-orchestrator/build/index.js"] + } + } +} diff --git a/docs/multi-agent-system-plan.md b/docs/multi-agent-system-plan.md new file mode 100644 index 00000000..46a1df3c --- /dev/null +++ b/docs/multi-agent-system-plan.md @@ -0,0 +1,989 @@ +# Multi-Agent 협업 시스템 설계서 + +> Cursor 에이전트 간 협업을 통한 효율적인 개발 시스템 + +## 목차 + +1. [개요](#개요) +2. [아키텍처](#아키텍처) +3. [에이전트 역할 정의](#에이전트-역할-정의) +4. [통신 프로토콜](#통신-프로토콜) +5. [워크플로우](#워크플로우) +6. [프롬프트 템플릿](#프롬프트-템플릿) +7. [MCP 서버 구현](#mcp-서버-구현) +8. [비용 분석](#비용-분석) +9. [한계점 및 해결방안](#한계점-및-해결방안) + +--- + +## 개요 + +### 문제점: 단일 에이전트의 한계 + +``` +단일 에이전트 문제: +┌─────────────────────────────────────────┐ +│ • 컨텍스트 폭발 (50k+ 토큰 → 까먹음) │ +│ • 전문성 분산 (모든 영역 얕게 앎) │ +│ • 재작업 빈번 (실수, 누락) │ +│ • 검증 부재 (크로스체크 없음) │ +└─────────────────────────────────────────┘ +``` + +### 해결책: Multi-Agent 협업 + +``` +멀티 에이전트 장점: +┌─────────────────────────────────────────┐ +│ • 컨텍스트 분리 (각자 작은 컨텍스트) │ +│ • 전문성 집중 (영역별 깊은 이해) │ +│ • 크로스체크 (서로 검증) │ +│ • 병렬 처리 (동시 작업) │ +└─────────────────────────────────────────┘ +``` + +### 모델 티어링 전략 + +| 에이전트 | 모델 | 역할 | 비용 | +|----------|------|------|------| +| Agent A (PM) | Claude Opus 4.5 | 분석, 계획, 조율 | 높음 | +| Agent B (Backend) | Claude Sonnet | 백엔드 구현 | 낮음 | +| Agent C (DB) | Claude Sonnet | DB/쿼리 담당 | 낮음 | +| Agent D (Frontend) | Claude Sonnet | 프론트 구현 | 낮음 | + +**예상 비용 절감: 50-60%** + +--- + +## 아키텍처 + +### 전체 구조 + +``` + ┌─────────────┐ + │ USER │ + └──────┬──────┘ + │ + ▼ + ┌───────────────────────┐ + │ Agent A (PM) │ + │ Claude Opus 4.5 │ + │ │ + │ • 사용자 의도 파악 │ + │ • 작업 분배 │ + │ • 결과 통합 │ + │ • 품질 검증 │ + └───────────┬───────────┘ + │ + ┌─────────────────┼─────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ Agent B │ │ Agent C │ │ Agent D │ + │ (Backend) │ │ (Database) │ │ (Frontend) │ + │ Sonnet │ │ Sonnet │ │ Sonnet │ + │ │ │ │ │ │ + │ • API 설계/구현 │ │ • 스키마 설계 │ │ • 컴포넌트 구현 │ + │ • 서비스 로직 │ │ • 쿼리 작성 │ │ • 페이지 구현 │ + │ • 라우팅 │ │ • 마이그레이션 │ │ • 스타일링 │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └─────────────────┴─────────────────┘ + │ + ▼ + ┌───────────────────────┐ + │ MCP Orchestrator │ + │ │ + │ • 메시지 라우팅 │ + │ • 병렬 실행 │ + │ • 결과 수집 │ + └───────────────────────┘ +``` + +### 폴더별 담당 영역 + +| 에이전트 | 담당 폴더 | 파일 유형 | +|----------|-----------|-----------| +| Agent B (Backend) | `backend-node/src/` | `.ts`, `.js` | +| Agent C (DB) | `src/com/pms/mapper/`, `db/` | `.xml`, `.sql` | +| Agent D (Frontend) | `frontend/` | `.tsx`, `.ts`, `.css` | +| Agent A (PM) | 전체 조율 | 모든 파일 (읽기 위주) | + +--- + +## 에이전트 역할 정의 + +### Agent A (PM) - 프로젝트 매니저 + +```yaml +역할: 전체 조율 및 사용자 인터페이스 +모델: Claude Opus 4.5 + +핵심 책임: + 의도 파악: + - 사용자 요청 분석 + - 모호한 요청 명확화 + - 숨겨진 요구사항 발굴 + + 작업 분배: + - 작업을 세부 태스크로 분해 + - 적절한 에이전트에게 할당 + - 우선순위 및 의존성 결정 + + 품질 관리: + - 결과물 검증 + - 일관성 체크 + - 충돌 해결 + + 통합: + - 개별 결과물 취합 + - 최종 결과 생성 + - 사용자에게 보고 + +하지 않는 것: + - 직접 코드 구현 (전문가에게 위임) + - 특정 영역 깊이 분석 (전문가에게 요청) +``` + +### Agent B (Backend) - 백엔드 전문가 + +```yaml +역할: API 및 서버 로직 담당 +모델: Claude Sonnet + +담당 영역: + 폴더: + - backend-node/src/controllers/ + - backend-node/src/services/ + - backend-node/src/routes/ + - backend-node/src/middleware/ + - backend-node/src/utils/ + + 작업: + - REST API 엔드포인트 설계/구현 + - 비즈니스 로직 구현 + - 미들웨어 작성 + - 에러 핸들링 + - 인증/인가 로직 + +담당 아닌 것: + - frontend/ 폴더 (Agent D 담당) + - SQL 쿼리 직접 작성 (Agent C에게 요청) + - DB 스키마 변경 (Agent C 담당) + +협업 필요 시: + - DB 쿼리 필요 → Agent C에게 요청 + - 프론트 연동 문제 → Agent D와 협의 +``` + +### Agent C (Database) - DB 전문가 + +```yaml +역할: 데이터베이스 및 쿼리 담당 +모델: Claude Sonnet + +담당 영역: + 폴더: + - src/com/pms/mapper/ + - db/ + - backend-node/src/database/ + + 작업: + - 테이블 스키마 설계 + - MyBatis 매퍼 XML 작성 + - SQL 쿼리 최적화 + - 인덱스 설계 + - 마이그레이션 스크립트 + +담당 아닌 것: + - API 로직 (Agent B 담당) + - 프론트엔드 (Agent D 담당) + - 비즈니스 로직 판단 (Agent A에게 확인) + +협업 필요 시: + - API에서 필요한 데이터 구조 → Agent B와 협의 + - 쿼리 결과 사용법 → Agent B에게 전달 +``` + +### Agent D (Frontend) - 프론트엔드 전문가 + +```yaml +역할: UI/UX 및 클라이언트 로직 담당 +모델: Claude Sonnet + +담당 영역: + 폴더: + - frontend/components/ + - frontend/pages/ + - frontend/lib/ + - frontend/hooks/ + - frontend/styles/ + + 작업: + - React 컴포넌트 구현 + - 페이지 레이아웃 + - 상태 관리 + - API 연동 (호출) + - 스타일링 + +담당 아닌 것: + - API 구현 (Agent B 담당) + - DB 쿼리 (Agent C 담당) + - API 스펙 결정 (Agent A/B와 협의) + +협업 필요 시: + - API 엔드포인트 필요 → Agent B에게 요청 + - 데이터 구조 확인 → Agent C에게 문의 +``` + +--- + +## 통신 프로토콜 + +### 메시지 포맷 + +```typescript +// 요청 메시지 +interface TaskRequest { + id: string; // 고유 ID (예: "task-001") + from: 'A' | 'B' | 'C' | 'D'; // 발신자 + to: 'A' | 'B' | 'C' | 'D'; // 수신자 + type: 'info_request' | 'work_request' | 'question'; + priority: 'high' | 'medium' | 'low'; + content: { + task: string; // 작업 내용 + context?: string; // 배경 정보 + expected_output?: string; // 기대 결과 + depends_on?: string[]; // 선행 작업 ID + }; + timestamp: string; +} + +// 응답 메시지 +interface TaskResponse { + id: string; // 요청 ID와 매칭 + from: 'A' | 'B' | 'C' | 'D'; + to: 'A' | 'B' | 'C' | 'D'; + status: 'success' | 'partial' | 'failed' | 'need_clarification'; + confidence: 'high' | 'medium' | 'low'; + + result?: { + summary: string; // 한 줄 요약 + details: string; // 상세 내용 + files_affected?: string[]; // 영향받는 파일 + code_changes?: CodeChange[]; // 코드 변경사항 + }; + + // 메타 정보 + scope_violations?: string[]; // 스코프 벗어난 요청 + dependencies?: string[]; // 필요한 선행 작업 + side_effects?: string[]; // 부작용 + alternatives?: string[]; // 대안 + + // 추가 요청 + questions?: string[]; // 명확화 필요 + needs_from_others?: { + agent: 'A' | 'B' | 'C' | 'D'; + request: string; + }[]; + + timestamp: string; +} + +// 코드 변경 +interface CodeChange { + file: string; + action: 'create' | 'modify' | 'delete'; + content?: string; // 전체 코드 또는 diff + line_start?: number; + line_end?: number; +} +``` + +### 상태 코드 정의 + +| 상태 | 의미 | 후속 조치 | +|------|------|-----------| +| `success` | 완전히 완료 | 결과 사용 가능 | +| `partial` | 부분 완료 | 추가 작업 필요 | +| `failed` | 실패 | 에러 확인 후 재시도 | +| `need_clarification` | 명확화 필요 | 질문에 답변 후 재요청 | + +### 확신도 정의 + +| 확신도 | 의미 | 권장 조치 | +|--------|------|-----------| +| `high` | 확실함 | 바로 적용 가능 | +| `medium` | 대체로 맞음 | 검토 후 적용 | +| `low` | 추측임 | 반드시 검증 필요 | + +--- + +## 워크플로우 + +### Phase 1: 정보 수집 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Phase 1: 정보 수집 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 1. User → Agent A: "주문 관리 기능 만들어줘" │ +│ │ +│ 2. Agent A 분석: │ +│ - 기능 범위 파악 │ +│ - 필요한 정보 식별 │ +│ - 정보 수집 요청 생성 │ +│ │ +│ 3. Agent A → B, C, D (병렬): │ +│ - B에게: "현재 order 관련 API 구조 분석해줘" │ +│ - C에게: "orders 테이블 스키마 알려줘" │ +│ - D에게: "주문 관련 컴포넌트 현황 알려줘" │ +│ │ +│ 4. B, C, D → Agent A (응답): │ +│ - B: API 현황 보고 │ +│ - C: 스키마 정보 보고 │ +│ - D: 컴포넌트 현황 보고 │ +│ │ +│ 5. Agent A: 정보 취합 및 계획 수립 │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Phase 2: 작업 분배 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Phase 2: 작업 분배 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 1. Agent A: 종합 계획 수립 │ +│ ┌─────────────────────────────────────────┐ │ +│ │ 분석 결과: │ │ +│ │ - API에 pagination 추가 필요 │ │ +│ │ - DB는 현재 구조 유지 │ │ +│ │ - 프론트 무한스크롤 → 페이지네이션 │ │ +│ │ │ │ +│ │ 작업 순서: │ │ +│ │ 1. C: 페이징 쿼리 준비 │ │ +│ │ 2. B: API 수정 (C 결과 의존) │ │ +│ │ 3. D: 프론트 수정 (B 결과 의존) │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ 2. Agent A → B, C, D: 작업 할당 │ +│ - C에게: "cursor 기반 페이징 쿼리 작성" │ +│ - B에게: "GET /api/orders에 pagination 추가" (C 대기) │ +│ - D에게: "Pagination 컴포넌트 적용" (B 대기) │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Phase 3: 실행 및 통합 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Phase 3: 실행 및 통합 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 1. 순차/병렬 실행: │ +│ - C: 쿼리 작성 → 완료 보고 │ +│ - B: API 수정 (C 완료 후) → 완료 보고 │ +│ - D: 프론트 수정 (B 완료 후) → 완료 보고 │ +│ │ +│ 2. Agent A: 결과 검증 │ +│ - 일관성 체크 │ +│ - 누락 확인 │ +│ - 충돌 해결 │ +│ │ +│ 3. Agent A → User: 최종 보고 │ +│ ┌─────────────────────────────────────────┐ │ +│ │ 완료된 작업: │ │ +│ │ ✅ orders.xml - 페이징 쿼리 추가 │ │ +│ │ ✅ OrderController.ts - pagination 적용 │ │ +│ │ ✅ OrderListPage.tsx - UI 수정 │ │ +│ │ │ │ +│ │ 테스트 필요: │ │ +│ │ - GET /api/orders?page=1&limit=10 │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 프롬프트 템플릿 + +### Agent A (PM) 시스템 프롬프트 + +```markdown +# 역할 +너는 PM(Project Manager) 에이전트야. +사용자 요청을 분석하고, 전문가 에이전트들(Backend, DB, Frontend)에게 +작업을 분배하고, 결과를 통합해서 최종 결과물을 만들어. + +# 사용 가능한 도구 +- ask_backend_agent: 백엔드 전문가에게 질문/작업 요청 +- ask_db_agent: DB 전문가에게 질문/작업 요청 +- ask_frontend_agent: 프론트 전문가에게 질문/작업 요청 +- parallel_ask: 여러 전문가에게 동시에 요청 + +# 작업 프로세스 + +## Phase 1: 분석 +1. 사용자 요청 분석 +2. 필요한 정보 식별 +3. 정보 수집 요청 (parallel_ask 활용) + +## Phase 2: 계획 +1. 수집된 정보 분석 +2. 작업 분해 및 의존성 파악 +3. 우선순위 결정 +4. 작업 분배 계획 수립 + +## Phase 3: 실행 +1. 의존성 순서대로 작업 요청 +2. 결과 검증 +3. 필요시 재요청 + +## Phase 4: 통합 +1. 모든 결과 취합 +2. 일관성 검증 +3. 사용자에게 보고 + +# 작업 분배 기준 +- Backend Agent: API, 서비스 로직, 라우팅 (backend-node/) +- DB Agent: 스키마, 쿼리, 마이그레이션 (mapper/, db/) +- Frontend Agent: 컴포넌트, 페이지, 스타일 (frontend/) + +# 판단 기준 +- 불확실하면 사용자에게 물어봐 +- 에이전트 결과가 이상하면 재요청 +- 영향 범위 크면 사용자 확인 +- 충돌 시 더 안전한 방향 선택 + +# 응답 형식 +작업 분배 시: +```json +{ + "phase": "info_gathering | work_distribution | integration", + "reasoning": "왜 이렇게 분배하는지", + "tasks": [ + { + "agent": "backend | db | frontend", + "priority": 1, + "task": "구체적인 작업 내용", + "depends_on": [], + "expected_output": "기대 결과" + } + ] +} +``` + +최종 보고 시: +```json +{ + "summary": "한 줄 요약", + "completed_tasks": ["완료된 작업들"], + "files_changed": ["변경된 파일들"], + "next_steps": ["다음 단계 (있다면)"], + "test_instructions": ["테스트 방법"] +} +``` +``` + +### Agent B (Backend) 시스템 프롬프트 + +```markdown +# 역할 +너는 Backend 전문가 에이전트야. +backend-node/ 폴더의 API, 서비스, 라우팅을 담당해. + +# 담당 영역 (이것만!) +- backend-node/src/controllers/ +- backend-node/src/services/ +- backend-node/src/routes/ +- backend-node/src/middleware/ +- backend-node/src/utils/ + +# 담당 아닌 것 (절대 건들지 마) +- frontend/ → Frontend Agent 담당 +- src/com/pms/mapper/ → DB Agent 담당 +- SQL 쿼리 직접 작성 → DB Agent에게 요청 + +# 코드 작성 규칙 +1. TypeScript 사용 +2. 에러 핸들링 필수 +3. 주석은 한글로 +4. 기존 코드 스타일 따르기 +5. ... 생략 없이 완전한 코드 + +# 응답 형식 +```json +{ + "status": "success | partial | failed | need_clarification", + "confidence": "high | medium | low", + "result": { + "summary": "한 줄 요약", + "details": "상세 설명", + "files_affected": ["파일 경로들"], + "code_changes": [ + { + "file": "경로", + "action": "create | modify | delete", + "content": "전체 코드" + } + ] + }, + "needs_from_others": [ + {"agent": "db", "request": "필요한 것"} + ], + "side_effects": ["영향받는 것들"], + "questions": ["명확하지 않은 것들"] +} +``` + +# 협업 규칙 +1. 내 영역 아니면 즉시 보고 (scope_violation) +2. 확실하지 않으면 confidence: "low" +3. 다른 에이전트 필요하면 needs_from_others에 명시 +4. 부작용 있으면 반드시 보고 +``` + +### Agent C (Database) 시스템 프롬프트 + +```markdown +# 역할 +너는 Database 전문가 에이전트야. +DB 스키마, 쿼리, 마이그레이션을 담당해. + +# 담당 영역 (이것만!) +- src/com/pms/mapper/ (MyBatis XML) +- db/ (스키마, 마이그레이션) +- backend-node/src/database/ + +# 담당 아닌 것 (절대 건들지 마) +- API 로직 → Backend Agent 담당 +- 프론트엔드 → Frontend Agent 담당 +- 비즈니스 로직 판단 → PM에게 확인 + +# 코드 작성 규칙 +1. PostgreSQL 문법 사용 +2. 파라미터 바인딩 (#{}) 필수 - SQL 인젝션 방지 +3. 인덱스 고려 +4. 성능 최적화 (EXPLAIN 결과 고려) + +# MyBatis 매퍼 규칙 +```xml + +WHERE id = #{id} + + + + AND name LIKE '%' || #{name} || '%' + + + +LIMIT #{limit} OFFSET #{offset} +``` + +# 응답 형식 +```json +{ + "status": "success | partial | failed | need_clarification", + "confidence": "high | medium | low", + "result": { + "summary": "한 줄 요약", + "details": "상세 설명", + "schema_info": { + "tables": ["관련 테이블"], + "columns": ["주요 컬럼"], + "indexes": ["인덱스"] + }, + "code_changes": [ + { + "file": "경로", + "action": "create | modify", + "content": "쿼리/스키마" + } + ] + }, + "performance_notes": ["성능 관련 참고사항"], + "questions": ["명확하지 않은 것들"] +} +``` +``` + +### Agent D (Frontend) 시스템 프롬프트 + +```markdown +# 역할 +너는 Frontend 전문가 에이전트야. +React/Next.js 기반 UI 구현을 담당해. + +# 담당 영역 (이것만!) +- frontend/components/ +- frontend/pages/ (또는 app/) +- frontend/lib/ +- frontend/hooks/ +- frontend/styles/ + +# 담당 아닌 것 (절대 건들지 마) +- backend-node/ → Backend Agent 담당 +- DB 관련 → DB Agent 담당 +- API 스펙 결정 → PM/Backend와 협의 + +# 코드 작성 규칙 +1. TypeScript 사용 +2. React 함수형 컴포넌트 +3. 커스텀 훅 활용 +4. 주석은 한글로 +5. Tailwind CSS 또는 기존 스타일 시스템 따르기 + +# API 호출 규칙 +- 절대 fetch 직접 사용 금지 +- lib/api/ 클라이언트 사용 +- 에러 핸들링 필수 + +# 응답 형식 +```json +{ + "status": "success | partial | failed | need_clarification", + "confidence": "high | medium | low", + "result": { + "summary": "한 줄 요약", + "details": "상세 설명", + "components_affected": ["컴포넌트 목록"], + "code_changes": [ + { + "file": "경로", + "action": "create | modify", + "content": "전체 코드" + } + ] + }, + "needs_from_others": [ + {"agent": "backend", "request": "필요한 API"} + ], + "ui_notes": ["UX 관련 참고사항"], + "questions": ["명확하지 않은 것들"] +} +``` +``` + +--- + +## MCP 서버 구현 + +### 프로젝트 구조 + +``` +mcp-agent-orchestrator/ +├── package.json +├── tsconfig.json +├── src/ +│ ├── index.ts # 메인 서버 +│ ├── agents/ +│ │ ├── types.ts # 타입 정의 +│ │ ├── pm.ts # PM 에이전트 프롬프트 +│ │ ├── backend.ts # Backend 에이전트 프롬프트 +│ │ ├── database.ts # DB 에이전트 프롬프트 +│ │ └── frontend.ts # Frontend 에이전트 프롬프트 +│ └── utils/ +│ └── logger.ts # 로깅 +└── build/ + └── index.js # 컴파일된 파일 +``` + +### 핵심 코드 + +```typescript +// src/index.ts +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import Anthropic from "@anthropic-ai/sdk"; +import { PM_PROMPT, BACKEND_PROMPT, DB_PROMPT, FRONTEND_PROMPT } from "./agents"; + +const server = new Server({ + name: "agent-orchestrator", + version: "1.0.0", +}); + +const anthropic = new Anthropic(); + +// 에이전트별 설정 +const AGENT_CONFIG = { + pm: { model: "claude-opus-4-5-20250214", prompt: PM_PROMPT }, + backend: { model: "claude-sonnet-4-20250514", prompt: BACKEND_PROMPT }, + db: { model: "claude-sonnet-4-20250514", prompt: DB_PROMPT }, + frontend: { model: "claude-sonnet-4-20250514", prompt: FRONTEND_PROMPT }, +}; + +// 도구 목록 +server.setRequestHandler("tools/list", async () => ({ + tools: [ + { + name: "ask_backend_agent", + description: "백엔드 전문가에게 질문하거나 작업 요청", + inputSchema: { + type: "object", + properties: { + task: { type: "string", description: "작업 내용" }, + context: { type: "string", description: "배경 정보 (선택)" }, + }, + required: ["task"], + }, + }, + { + name: "ask_db_agent", + description: "DB 전문가에게 질문하거나 작업 요청", + inputSchema: { + type: "object", + properties: { + task: { type: "string", description: "작업 내용" }, + context: { type: "string", description: "배경 정보 (선택)" }, + }, + required: ["task"], + }, + }, + { + name: "ask_frontend_agent", + description: "프론트엔드 전문가에게 질문하거나 작업 요청", + inputSchema: { + type: "object", + properties: { + task: { type: "string", description: "작업 내용" }, + context: { type: "string", description: "배경 정보 (선택)" }, + }, + required: ["task"], + }, + }, + { + name: "parallel_ask", + description: "여러 전문가에게 동시에 질문 (병렬 실행)", + inputSchema: { + type: "object", + properties: { + requests: { + type: "array", + items: { + type: "object", + properties: { + agent: { + type: "string", + enum: ["backend", "db", "frontend"] + }, + task: { type: "string" }, + context: { type: "string" }, + }, + required: ["agent", "task"], + }, + }, + }, + required: ["requests"], + }, + }, + ], +})); + +// 도구 실행 +server.setRequestHandler("tools/call", async (request) => { + const { name, arguments: args } = request.params; + + const callAgent = async (agentType: string, task: string, context?: string) => { + const config = AGENT_CONFIG[agentType]; + const response = await anthropic.messages.create({ + model: config.model, + max_tokens: 8192, + system: config.prompt, + messages: [ + { + role: "user", + content: context ? `${task}\n\n배경 정보:\n${context}` : task, + }, + ], + }); + return response.content[0].text; + }; + + switch (name) { + case "ask_backend_agent": + return { + content: [ + { type: "text", text: await callAgent("backend", args.task, args.context) }, + ], + }; + + case "ask_db_agent": + return { + content: [ + { type: "text", text: await callAgent("db", args.task, args.context) }, + ], + }; + + case "ask_frontend_agent": + return { + content: [ + { type: "text", text: await callAgent("frontend", args.task, args.context) }, + ], + }; + + case "parallel_ask": + const results = await Promise.all( + args.requests.map(async (req) => ({ + agent: req.agent, + result: await callAgent(req.agent, req.task, req.context), + })) + ); + return { + content: [ + { type: "text", text: JSON.stringify(results, null, 2) }, + ], + }; + + default: + throw new Error(`Unknown tool: ${name}`); + } +}); + +// 서버 시작 +const transport = new StdioServerTransport(); +await server.connect(transport); +``` + +### Cursor 설정 + +```json +// .cursor/mcp.json +{ + "mcpServers": { + "agent-orchestrator": { + "command": "node", + "args": ["C:/Users/defaultuser0/mcp-agent-orchestrator/build/index.js"], + "env": { + "ANTHROPIC_API_KEY": "your-api-key-here" + } + } + } +} +``` + +--- + +## 비용 분석 + +### 토큰 사용량 비교 + +| 시나리오 | 단일 에이전트 | 멀티 에이전트 | 절감 | +|----------|--------------|--------------|------| +| 기능 1개 추가 | 100,000 토큰 | 60,000 토큰 | 40% | +| 시스템 리팩토링 | 300,000 토큰 | 150,000 토큰 | 50% | +| 새 모듈 개발 | 500,000 토큰 | 200,000 토큰 | 60% | + +### 비용 계산 (예시) + +``` +단일 에이전트 (전부 Opus): +- 300,000 토큰 × $15/M = $4.50 + +멀티 에이전트 (Opus PM + Sonnet Workers): +- PM (Opus): 50,000 토큰 × $15/M = $0.75 +- Workers (Sonnet): 100,000 토큰 × $3/M = $0.30 +- 총: $1.05 + +절감: $4.50 - $1.05 = $3.45 (76% 절감!) +``` + +### ROI 분석 + +``` +초기 투자: +- MCP 서버 개발: 4-6시간 +- 프롬프트 튜닝: 2-4시간 +- 테스트: 2시간 +- 총: 8-12시간 + +회수: +- 대규모 작업당 $3-5 절감 +- 재작업 시간 70% 감소 +- 품질 30% 향상 + +손익분기점: 대규모 작업 3-5회 +``` + +--- + +## 한계점 및 해결방안 + +### 현재 한계 + +| 한계 | 설명 | 해결방안 | +|------|------|----------| +| 완전 자동화 불가 | Cursor 에이전트 간 직접 통신 없음 | MCP 서버로 우회 | +| 파일 읽기 제한 | 각 에이전트가 모든 파일 접근 어려움 | 컨텍스트에 필요한 정보 전달 | +| 실시간 동기화 | 변경사항 즉시 반영 어려움 | 명시적 갱신 요청 | +| 에러 복구 | 자동 롤백 메커니즘 없음 | 수동 복구 또는 git 활용 | + +### 향후 개선 방향 + +1. **파일 시스템 연동** + - MCP 서버에 파일 읽기/쓰기 도구 추가 + - 에이전트가 직접 코드 확인 가능 + +2. **결과 자동 적용** + - 코드 변경사항 자동 적용 + - git 커밋 자동화 + +3. **피드백 루프** + - 테스트 자동 실행 + - 실패 시 자동 재시도 + +4. **히스토리 관리** + - 대화 이력 저장 + - 컨텍스트 캐싱 + +--- + +## 체크리스트 + +### 구현 전 준비 + +- [ ] Node.js 18+ 설치 +- [ ] Anthropic API 키 발급 +- [ ] 프로젝트 폴더 생성 + +### MCP 서버 구현 + +- [ ] package.json 설정 +- [ ] TypeScript 설정 +- [ ] 기본 서버 구조 +- [ ] 도구 정의 (4개) +- [ ] 에이전트 프롬프트 작성 +- [ ] 빌드 및 테스트 + +### Cursor 연동 + +- [ ] mcp.json 설정 +- [ ] Cursor 재시작 +- [ ] 도구 호출 테스트 +- [ ] 실제 작업 테스트 + +### 튜닝 + +- [ ] 프롬프트 개선 +- [ ] 에러 핸들링 강화 +- [ ] 로깅 추가 +- [ ] 성능 최적화 + +--- + +## 참고 자료 + +- [MCP SDK 문서](https://modelcontextprotocol.io/) +- [Anthropic API 문서](https://docs.anthropic.com/) +- [CrewAI](https://github.com/joaomdmoura/crewAI) - 멀티에이전트 프레임워크 참고 +- [AutoGen](https://github.com/microsoft/autogen) - Microsoft 멀티에이전트 참고 + +--- + +*작성일: 2026-02-05* +*버전: 1.0* diff --git a/mcp-agent-orchestrator/README.md b/mcp-agent-orchestrator/README.md new file mode 100644 index 00000000..ce9aac42 --- /dev/null +++ b/mcp-agent-orchestrator/README.md @@ -0,0 +1,189 @@ +# Multi-Agent Orchestrator MCP Server v2.0 + +Cursor Agent CLI를 활용한 멀티에이전트 시스템입니다. +**Cursor Team Plan만으로 동작** - 외부 API 키 불필요! + +## 아키텍처 + +``` +┌─────────────────────────────────────────┐ +│ Cursor IDE (PM Agent) │ +│ Claude Opus 4.5 │ +└────────────────────┬────────────────────┘ + │ MCP Tools + ┌────────────────┼────────────────┐ + ▼ ▼ ▼ +┌────────┐ ┌────────┐ ┌────────┐ +│Backend │ │ DB │ │Frontend│ +│ Agent │ │ Agent │ │ Agent │ +│ via CLI│ │ via CLI│ │ via CLI│ +│Sonnet │ │Sonnet │ │Sonnet │ +└────────┘ └────────┘ └────────┘ + ↑ ↑ ↑ + └──────────────┴───────────────┘ + Cursor Agent CLI + (Team Plan 크레딧 사용) +``` + +## 특징 + +- **API 키 불필요**: Cursor Team Plan 크레딧만 사용 +- **크로스 플랫폼**: Windows, Mac, Linux 지원 +- **진짜 병렬 실행**: `parallel_ask`로 동시 작업 +- **모델 티어링**: PM=Opus, Sub-agents=Sonnet + +## 사전 요구사항 + +1. **Cursor Team/Pro Plan** 구독 +2. **Cursor Agent CLI** 설치 및 로그인 + ```bash + # 설치 후 로그인 확인 + agent status + ``` + +## 설치 + +```bash +cd mcp-agent-orchestrator +npm install +npm run build +``` + +## Cursor 설정 + +### Windows + +`.cursor/mcp.json`: +```json +{ + "mcpServers": { + "agent-orchestrator": { + "command": "node", + "args": ["C:/Users/YOUR_USERNAME/ERP-node/mcp-agent-orchestrator/build/index.js"] + } + } +} +``` + +### Mac + +`.cursor/mcp.json`: +```json +{ + "mcpServers": { + "agent-orchestrator": { + "command": "node", + "args": ["/Users/YOUR_USERNAME/ERP-node/mcp-agent-orchestrator/build/index.js"] + } + } +} +``` + +**주의**: Mac에서 agent CLI가 PATH에 있어야 합니다. +```bash +# agent CLI 위치 확인 +which agent +# 보통: ~/.cursor-agent/bin/agent 또는 /usr/local/bin/agent + +# PATH에 없으면 추가 (.zshrc 또는 .bashrc) +export PATH="$HOME/.cursor-agent/bin:$PATH" +``` + +## 사용 가능한 도구 + +### ask_backend_agent +백엔드 전문가에게 질문/작업 요청 +- API 설계, 서비스 로직, 라우팅 +- 담당 폴더: `backend-node/src/` + +### ask_db_agent +DB 전문가에게 질문/작업 요청 +- 스키마, 쿼리, MyBatis 매퍼 +- 담당 폴더: `src/com/pms/mapper/`, `db/` + +### ask_frontend_agent +프론트엔드 전문가에게 질문/작업 요청 +- React 컴포넌트, 페이지, 스타일 +- 담당 폴더: `frontend/` + +### parallel_ask +여러 전문가에게 동시에 질문 (진짜 병렬 실행!) +- 정보 수집 단계에서 유용 + +### get_agent_info +에이전트 시스템 정보 확인 + +## 워크플로우 예시 + +### 1단계: 정보 수집 (병렬) +``` +parallel_ask([ + { agent: "backend", task: "현재 order 관련 API 구조 분석" }, + { agent: "db", task: "orders 테이블 스키마 분석" }, + { agent: "frontend", task: "주문 관련 컴포넌트 현황 분석" } +]) +``` + +### 2단계: 개별 작업 (순차) +``` +ask_db_agent("cursor 기반 페이징 쿼리 작성") +ask_backend_agent("GET /api/orders에 pagination 추가") +ask_frontend_agent("Pagination 컴포넌트 적용") +``` + +## 모델 설정 + +| Agent | Model | 역할 | +|-------|-------|------| +| PM (Cursor IDE) | Opus 4.5 | 전체 조율, 사용자 대화 | +| Backend | Sonnet 4.5 | API, 서비스 로직 | +| DB | Sonnet 4.5 | 스키마, 쿼리 | +| Frontend | Sonnet 4.5 | 컴포넌트, UI | + +**비용 최적화**: PM만 Opus, 나머지는 Sonnet 사용 + +## 환경 변수 + +- `LOG_LEVEL`: 로그 레벨 (debug, info, warn, error) + +## 트러블슈팅 + +### Windows: agent 명령어가 안 됨 +```powershell +# PowerShell 실행 정책 확인 +Get-ExecutionPolicy -List + +# 필요시 변경 +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +### Mac: agent 명령어를 찾을 수 없음 +```bash +# agent CLI 위치 확인 +ls -la ~/.cursor-agent/bin/ + +# PATH 추가 +echo 'export PATH="$HOME/.cursor-agent/bin:$PATH"' >> ~/.zshrc +source ~/.zshrc +``` + +### 응답이 오래 걸림 +- 정상입니다! 각 에이전트 호출에 15-30초 소요 +- `parallel_ask`로 병렬 처리하면 시간 절약 + +## 개발 + +```bash +# 개발 모드 (watch) +npm run dev + +# 빌드 +npm run build + +# 테스트 실행 +npm start +``` + +## 라이선스 + +MIT diff --git a/mcp-agent-orchestrator/package-lock.json b/mcp-agent-orchestrator/package-lock.json new file mode 100644 index 00000000..79594e1f --- /dev/null +++ b/mcp-agent-orchestrator/package-lock.json @@ -0,0 +1,1444 @@ +{ + "name": "mcp-agent-orchestrator", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-agent-orchestrator", + "version": "1.0.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", + "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "20.19.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.31.tgz", + "integrity": "sha512-5jsi0wpncvTD33Sh1UCgacK37FFwDn+EG7wCmEvs62fCvBL+n8/76cAYDok21NF6+jaVWIqKwCZyX7Vbu8eB3A==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/mcp-agent-orchestrator/package.json b/mcp-agent-orchestrator/package.json new file mode 100644 index 00000000..bc8b5b7c --- /dev/null +++ b/mcp-agent-orchestrator/package.json @@ -0,0 +1,29 @@ +{ + "name": "mcp-agent-orchestrator", + "version": "2.0.0", + "description": "Multi-Agent Orchestrator MCP Server using Cursor Agent CLI (Team Plan)", + "type": "module", + "main": "build/index.js", + "scripts": { + "build": "tsc", + "start": "node build/index.js", + "dev": "tsc --watch" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "keywords": [ + "cursor", + "mcp", + "multi-agent", + "ai", + "orchestrator" + ] +} diff --git a/mcp-agent-orchestrator/src/agents/index.ts b/mcp-agent-orchestrator/src/agents/index.ts new file mode 100644 index 00000000..7aebaa5c --- /dev/null +++ b/mcp-agent-orchestrator/src/agents/index.ts @@ -0,0 +1,6 @@ +/** + * 에이전트 모듈 내보내기 + */ + +export * from "./types.js"; +export * from "./prompts.js"; diff --git a/mcp-agent-orchestrator/src/agents/prompts.ts b/mcp-agent-orchestrator/src/agents/prompts.ts new file mode 100644 index 00000000..339ae84d --- /dev/null +++ b/mcp-agent-orchestrator/src/agents/prompts.ts @@ -0,0 +1,261 @@ +/** + * Agent System Prompts (English to avoid CMD encoding issues) + * Agents will still respond in Korean based on user preferences + */ + +export const PM_PROMPT = `# Role +You are a PM (Project Manager) agent. +Analyze user requests, distribute tasks to specialist agents (Backend, DB, Frontend), +and integrate results to create the final deliverable. + +# Available Tools +- ask_backend_agent: Ask/request tasks from backend expert +- ask_db_agent: Ask/request tasks from DB expert +- ask_frontend_agent: Ask/request tasks from frontend expert +- parallel_ask: Request from multiple experts simultaneously + +# Work Process + +## Phase 1: Analysis +1. Analyze user request +2. Identify required information +3. Request info gathering (use parallel_ask) + +## Phase 2: Planning +1. Analyze gathered information +2. Break down tasks and identify dependencies +3. Determine priorities +4. Create work distribution plan + +## Phase 3: Execution +1. Request tasks in dependency order +2. Verify results +3. Re-request if needed + +## Phase 4: Integration +1. Collect all results +2. Verify consistency +3. Report to user + +# Task Distribution Criteria +- Backend Agent: API, service logic, routing (backend-node/) +- DB Agent: Schema, queries, migrations (mapper/, db/) +- Frontend Agent: Components, pages, styles (frontend/) + +# Decision Criteria +- Ask user if uncertain +- Re-request if agent result seems wrong +- Confirm with user if impact is large +- Choose safer direction when conflicts arise + +# Response Format +Use JSON format when distributing tasks: +{ + "phase": "info_gathering | work_distribution | integration", + "reasoning": "why distributing this way", + "tasks": [ + { + "agent": "backend | db | frontend", + "priority": 1, + "task": "specific task content", + "depends_on": [], + "expected_output": "expected result" + } + ] +} + +Final report: +{ + "summary": "one line summary", + "completed_tasks": ["completed tasks"], + "files_changed": ["changed files"], + "next_steps": ["next steps"], + "test_instructions": ["how to test"] +}`; + +export const BACKEND_PROMPT = `# Role +You are a Backend specialist agent. +You handle API, services, and routing in the backend-node/ folder. + +# Your Domain (ONLY these!) +- backend-node/src/controllers/ +- backend-node/src/services/ +- backend-node/src/routes/ +- backend-node/src/middleware/ +- backend-node/src/utils/ + +# NOT Your Domain (NEVER touch) +- frontend/ -> Frontend Agent handles this +- src/com/pms/mapper/ -> DB Agent handles this +- Direct SQL queries -> Request from DB Agent + +# Code Rules +1. Use TypeScript +2. Error handling required +3. Comments in Korean +4. Follow existing code style +5. Complete code, no ... ellipsis + +# Response Format (JSON) +{ + "status": "success | partial | failed | need_clarification", + "confidence": "high | medium | low", + "result": { + "summary": "one line summary", + "details": "detailed explanation", + "files_affected": ["file paths"], + "code_changes": [ + { + "file": "path", + "action": "create | modify | delete", + "content": "complete code" + } + ] + }, + "needs_from_others": [ + {"agent": "db", "request": "what you need"} + ], + "side_effects": ["affected areas"], + "questions": ["unclear points"] +} + +# Collaboration Rules +1. Report immediately if out of scope (scope_violation) +2. Set confidence: "low" if uncertain +3. Specify in needs_from_others if other agents needed +4. Always report side effects`; + +export const DB_PROMPT = `# Role +You are a Database specialist agent. +You handle DB schema, queries, and migrations. + +# Your Domain (ONLY these!) +- src/com/pms/mapper/ (MyBatis XML) +- db/ (schema, migrations) +- backend-node/src/database/ + +# NOT Your Domain (NEVER touch) +- API logic -> Backend Agent handles this +- Frontend -> Frontend Agent handles this +- Business logic decisions -> Confirm with PM + +# Code Rules +1. Use PostgreSQL syntax +2. Parameter binding (#{}) required - prevent SQL injection +3. Consider indexes +4. Consider performance optimization + +# MyBatis Mapper Rules +- Parameter binding: WHERE id = #{id} +- Dynamic queries: ... +- Pagination: LIMIT #{limit} OFFSET #{offset} + +# Response Format (JSON) +{ + "status": "success | partial | failed | need_clarification", + "confidence": "high | medium | low", + "result": { + "summary": "one line summary", + "details": "detailed explanation", + "schema_info": { + "tables": ["related tables"], + "columns": ["main columns"], + "indexes": ["indexes"] + }, + "code_changes": [ + { + "file": "path", + "action": "create | modify", + "content": "query/schema" + } + ] + }, + "performance_notes": ["performance considerations"], + "questions": ["unclear points"] +} + +# Collaboration Rules +1. Report immediately if out of scope +2. Set confidence: "low" if uncertain +3. Always mention performance issues`; + +export const FRONTEND_PROMPT = `# Role +You are a Frontend specialist agent. +You handle React/Next.js UI implementation. + +# Your Domain (ONLY these!) +- frontend/components/ +- frontend/pages/ or frontend/app/ +- frontend/lib/ +- frontend/hooks/ +- frontend/styles/ + +# NOT Your Domain (NEVER touch) +- backend-node/ -> Backend Agent handles this +- DB related -> DB Agent handles this +- API spec decisions -> Discuss with PM/Backend + +# Code Rules +1. Use TypeScript +2. React functional components +3. Use custom hooks +4. Comments in Korean +5. Follow Tailwind CSS or existing style system + +# API Call Rules +- NEVER use fetch directly! +- Use lib/api/ client +- Error handling required + +# Response Format (JSON) +{ + "status": "success | partial | failed | need_clarification", + "confidence": "high | medium | low", + "result": { + "summary": "one line summary", + "details": "detailed explanation", + "components_affected": ["component list"], + "code_changes": [ + { + "file": "path", + "action": "create | modify", + "content": "complete code" + } + ] + }, + "needs_from_others": [ + {"agent": "backend", "request": "needed API"} + ], + "ui_notes": ["UX considerations"], + "questions": ["unclear points"] +} + +# Collaboration Rules +1. Report immediately if out of scope +2. Set confidence: "low" if uncertain +3. Specify in needs_from_others if API needed +4. Suggest UX improvements if any`; + +// 에이전트 설정 맵 +export const AGENT_CONFIGS = { + pm: { + model: 'claude-opus-4-5-20250214', + systemPrompt: PM_PROMPT, + maxTokens: 8192, + }, + backend: { + model: 'claude-sonnet-4-20250514', + systemPrompt: BACKEND_PROMPT, + maxTokens: 8192, + }, + db: { + model: 'claude-sonnet-4-20250514', + systemPrompt: DB_PROMPT, + maxTokens: 8192, + }, + frontend: { + model: 'claude-sonnet-4-20250514', + systemPrompt: FRONTEND_PROMPT, + maxTokens: 8192, + }, +} as const; diff --git a/mcp-agent-orchestrator/src/agents/types.ts b/mcp-agent-orchestrator/src/agents/types.ts new file mode 100644 index 00000000..ad045ba6 --- /dev/null +++ b/mcp-agent-orchestrator/src/agents/types.ts @@ -0,0 +1,63 @@ +/** + * Multi-Agent System 타입 정의 + */ + +// 에이전트 타입 +export type AgentType = 'pm' | 'backend' | 'db' | 'frontend'; + +// 에이전트 설정 +export interface AgentConfig { + model: string; + systemPrompt: string; + maxTokens: number; +} + +// 작업 요청 +export interface TaskRequest { + agent: AgentType; + task: string; + context?: string; +} + +// 작업 응답 상태 +export type ResponseStatus = 'success' | 'partial' | 'failed' | 'need_clarification'; + +// 확신도 +export type ConfidenceLevel = 'high' | 'medium' | 'low'; + +// 코드 변경 +export interface CodeChange { + file: string; + action: 'create' | 'modify' | 'delete'; + content?: string; + lineStart?: number; + lineEnd?: number; +} + +// 에이전트 응답 +export interface AgentResponse { + status: ResponseStatus; + confidence: ConfidenceLevel; + result?: { + summary: string; + details: string; + filesAffected?: string[]; + codeChanges?: CodeChange[]; + }; + scopeViolations?: string[]; + dependencies?: string[]; + sideEffects?: string[]; + alternatives?: string[]; + questions?: string[]; + needsFromOthers?: { + agent: AgentType; + request: string; + }[]; +} + +// 병렬 요청 결과 +export interface ParallelResult { + agent: AgentType; + result: string; + error?: string; +} diff --git a/mcp-agent-orchestrator/src/index.ts b/mcp-agent-orchestrator/src/index.ts new file mode 100644 index 00000000..fb62e7b8 --- /dev/null +++ b/mcp-agent-orchestrator/src/index.ts @@ -0,0 +1,401 @@ +#!/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 { + 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); +}); diff --git a/mcp-agent-orchestrator/src/utils/logger.ts b/mcp-agent-orchestrator/src/utils/logger.ts new file mode 100644 index 00000000..9e74d552 --- /dev/null +++ b/mcp-agent-orchestrator/src/utils/logger.ts @@ -0,0 +1,55 @@ +/** + * 간단한 로깅 유틸리티 + */ + +type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +const LOG_LEVELS: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, +}; + +// 환경변수로 로그 레벨 설정 (기본: info) +const currentLevel = (process.env.LOG_LEVEL as LogLevel) || 'info'; + +function shouldLog(level: LogLevel): boolean { + return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel]; +} + +function formatMessage(level: LogLevel, message: string, data?: unknown): string { + const timestamp = new Date().toISOString(); + const prefix = `[${timestamp}] [${level.toUpperCase()}]`; + + if (data) { + return `${prefix} ${message} ${JSON.stringify(data, null, 2)}`; + } + return `${prefix} ${message}`; +} + +export const logger = { + debug(message: string, data?: unknown): void { + if (shouldLog('debug')) { + console.error(formatMessage('debug', message, data)); + } + }, + + info(message: string, data?: unknown): void { + if (shouldLog('info')) { + console.error(formatMessage('info', message, data)); + } + }, + + warn(message: string, data?: unknown): void { + if (shouldLog('warn')) { + console.error(formatMessage('warn', message, data)); + } + }, + + error(message: string, data?: unknown): void { + if (shouldLog('error')) { + console.error(formatMessage('error', message, data)); + } + }, +}; diff --git a/mcp-agent-orchestrator/tsconfig.json b/mcp-agent-orchestrator/tsconfig.json new file mode 100644 index 00000000..c974e14e --- /dev/null +++ b/mcp-agent-orchestrator/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "build"] +} From 40057c7d3cc8e0b6ceb6e0c2446c42cd263714a9 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 6 Feb 2026 13:33:19 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20macOS=20Agent=20CLI=20=ED=98=B8?= =?UTF-8?q?=ED=99=98=EC=84=B1=20=EC=88=98=EC=A0=95=20(=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EB=AA=85,=20Base64=20=EC=9D=B8=EC=BD=94=EB=94=A9,=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .cursor/mcp.json | 2 +- mcp-agent-orchestrator/package-lock.json | 273 +---------------------- mcp-agent-orchestrator/src/index.ts | 66 +++--- 3 files changed, 39 insertions(+), 302 deletions(-) diff --git a/.cursor/mcp.json b/.cursor/mcp.json index 48855331..7a87d1a0 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "agent-orchestrator": { "command": "node", - "args": ["C:/Users/defaultuser0/ERP-node/mcp-agent-orchestrator/build/index.js"] + "args": ["/Users/gbpark/ERP-node/mcp-agent-orchestrator/build/index.js"] } } } diff --git a/mcp-agent-orchestrator/package-lock.json b/mcp-agent-orchestrator/package-lock.json index 79594e1f..af26ebd8 100644 --- a/mcp-agent-orchestrator/package-lock.json +++ b/mcp-agent-orchestrator/package-lock.json @@ -1,14 +1,13 @@ { "name": "mcp-agent-orchestrator", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcp-agent-orchestrator", - "version": "1.0.0", + "version": "2.0.0", "dependencies": { - "@anthropic-ai/sdk": "^0.39.0", "@modelcontextprotocol/sdk": "^1.0.0" }, "devDependencies": { @@ -19,36 +18,6 @@ "node": ">=18.0.0" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", - "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", - "license": "MIT", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - } - }, - "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, "node_modules/@hono/node-server": { "version": "1.19.9", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", @@ -105,33 +74,12 @@ "version": "20.19.31", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.31.tgz", "integrity": "sha512-5jsi0wpncvTD33Sh1UCgacK37FFwDn+EG7wCmEvs62fCvBL+n8/76cAYDok21NF6+jaVWIqKwCZyX7Vbu8eB3A==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.4" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -145,18 +93,6 @@ "node": ">= 0.6" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -190,12 +126,6 @@ } } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -258,18 +188,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -358,15 +276,6 @@ } } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -435,21 +344,6 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -465,15 +359,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -600,62 +485,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT" - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "license": "MIT", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -744,21 +573,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -801,15 +615,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -953,46 +758,6 @@ "node": ">= 0.6" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1317,12 +1082,6 @@ "node": ">=0.6" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1355,6 +1114,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -1375,31 +1135,6 @@ "node": ">= 0.8" } }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/mcp-agent-orchestrator/src/index.ts b/mcp-agent-orchestrator/src/index.ts index fb62e7b8..e202aa8b 100644 --- a/mcp-agent-orchestrator/src/index.ts +++ b/mcp-agent-orchestrator/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Multi-Agent Orchestrator MCP Server + * Multi-Agent Orchestrator MCP Server v2.0 * * Cursor Agent CLI를 활용한 멀티에이전트 시스템 * - PM (Cursor IDE): 전체 조율 @@ -48,7 +48,7 @@ const server = new Server( * * 크로스 플랫폼 지원: * - Windows: cmd /c "echo. | agent ..." (stdin 닫기 위해) - * - Mac/Linux: echo "" | agent ... (bash 사용) + * - Mac/Linux: ~/.local/bin/agent 사용 */ async function callAgentCLI( agentType: AgentType, @@ -63,46 +63,45 @@ async function callAgentCLI( 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}\n\n배경 정보:\n${context}` : task; - // 전체 프롬프트 (시스템 + 유저) - const fullPrompt = `SYSTEM INSTRUCTIONS: ${systemPrompt} --- TASK REQUEST: ${userMessage}` - .replace(/\[/g, '(') // 대괄호를 괄호로 변환 (쉘 호환) - .replace(/\]/g, ')') - .replace(/"/g, '\\"'); // 쌍따옴표 이스케이프 + // 프롬프트를 임시 파일에 저장하여 쉘 이스케이프 문제 회피 + const fullPrompt = `${config.systemPrompt}\n\n---\n\n${userMessage}`; + + // Base64 인코딩으로 특수문자 문제 해결 + const encodedPrompt = Buffer.from(fullPrompt).toString('base64'); let cmd: string; let shell: string; + const agentPath = isWindows ? 'agent' : `${process.env.HOME}/.local/bin/agent`; if (isWindows) { - // Windows: CMD를 통해 echo로 빈 입력 파이프 - cmd = `cmd /c "echo. | agent -p \\"${fullPrompt}\\" --model ${model} --output-format text"`; - shell = 'cmd.exe'; + // Windows: PowerShell을 통해 Base64 디코딩 후 실행 + cmd = `powershell -Command "$prompt = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${encodedPrompt}')); echo $prompt | ${agentPath} --model ${model} --print"`; + shell = 'powershell.exe'; } else { - // Mac/Linux: Bash를 통해 빈 문자열 파이프 - // 참고: Mac에서는 agent CLI가 ~/.cursor-agent/bin/agent 경로에 있을 수 있음 - cmd = `echo "" | agent -p "${fullPrompt}" --model ${model} --output-format text`; + // Mac/Linux: echo로 base64 디코딩 후 파이프 + cmd = `echo "${encodedPrompt}" | base64 -d | ${agentPath} --model ${model} --print`; shell = '/bin/bash'; } - logger.debug(`Executing on ${isWindows ? 'Windows' : 'Mac/Linux'}: agent -p "..." --model ${model}`); + logger.debug(`Executing: ${agentPath} --model ${model} --print`); const { stdout, stderr } = await execAsync(cmd, { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024, // 10MB buffer timeout: 300000, // 5분 타임아웃 shell, + env: { + ...process.env, + PATH: `${process.env.HOME}/.local/bin:${process.env.PATH}`, + }, }); - if (stderr && !stderr.includes('warning')) { - logger.warn(`${agentType} agent stderr`, { stderr }); + if (stderr && !stderr.includes('warning') && !stderr.includes('info')) { + logger.warn(`${agentType} agent stderr`, { stderr: stderr.substring(0, 500) }); } logger.info(`${agentType} agent completed via CLI`); @@ -124,7 +123,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "백엔드 전문가에게 질문하거나 작업을 요청합니다. " + "API 설계, 서비스 로직, 라우팅, 미들웨어 관련 작업에 사용하세요. " + - "담당 폴더: backend-node/src/ (Cursor Team Plan 사용, sonnet-4.5 모델)", + "담당 폴더: backend-node/src/ (Cursor Agent CLI, sonnet-4.5 모델)", inputSchema: { type: "object" as const, properties: { @@ -145,7 +144,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "DB 전문가에게 질문하거나 작업을 요청합니다. " + "스키마 설계, SQL 쿼리, MyBatis 매퍼, 마이그레이션 관련 작업에 사용하세요. " + - "담당 폴더: src/com/pms/mapper/, db/ (Cursor Team Plan 사용, sonnet-4.5 모델)", + "담당 폴더: src/com/pms/mapper/, db/ (Cursor Agent CLI, sonnet-4.5 모델)", inputSchema: { type: "object" as const, properties: { @@ -166,7 +165,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "프론트엔드 전문가에게 질문하거나 작업을 요청합니다. " + "React 컴포넌트, 페이지, 스타일링, 상태관리 관련 작업에 사용하세요. " + - "담당 폴더: frontend/ (Cursor Team Plan 사용, sonnet-4.5 모델)", + "담당 폴더: frontend/ (Cursor Agent CLI, sonnet-4.5 모델)", inputSchema: { type: "object" as const, properties: { @@ -187,7 +186,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "여러 전문가에게 동시에 질문합니다 (진짜 병렬 실행!). " + "정보 수집 단계에서 모든 영역의 현황을 빠르게 파악할 때 유용합니다. " + - "모든 에이전트가 동시에 실행되어 시간 절약! (Cursor Team Plan 사용)", + "모든 에이전트가 Cursor Agent CLI를 통해 동시에 실행되어 시간 절약!", inputSchema: { type: "object" as const, properties: { @@ -316,7 +315,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { system: "Multi-Agent Orchestrator v2.0", version: "2.0.0", backend: "Cursor Agent CLI (Team Plan)", - apiKey: "NOT REQUIRED! Using Cursor subscription", + cliPath: `${process.env.HOME}/.local/bin/agent`, + apiKey: "NOT REQUIRED! Using Cursor Team Plan credits", agents: { pm: { role: "Project Manager", @@ -325,19 +325,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }, backend: { role: "Backend Specialist", - model: "sonnet-4.5 (via agent CLI)", + model: "sonnet-4.5 (via Agent CLI)", description: "API, 서비스 로직, 라우팅 담당", folder: "backend-node/src/", }, db: { role: "Database Specialist", - model: "sonnet-4.5 (via agent CLI)", + 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)", + model: "sonnet-4.5 (via Agent CLI)", description: "컴포넌트, 페이지, 스타일링 담당", folder: "frontend/", }, @@ -345,8 +345,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { features: { parallel_execution: true, cursor_team_plan: true, + cursor_agent_cli: true, separate_api_key: false, - real_multi_session: true, + cross_platform: true, }, usage: { single_agent: "ask_backend_agent, ask_db_agent, ask_frontend_agent", @@ -387,7 +388,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { */ 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!)"); + 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); From 153ec5b65f132ff7be4348a567706fbba2907384 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 6 Feb 2026 13:40:30 +0900 Subject: [PATCH 3/5] refactor: Update prompts and descriptions for clarity and efficiency in the MCP Agent Orchestrator - Revised agent prompts to enhance clarity and token efficiency. - Added specific instructions regarding task distribution and project rules for Backend, DB, and Frontend agents. - Included warnings for users about the appropriate use of parallel_ask and task handling. - Updated the overall structure and content of prompts to align with project requirements and improve user guidance. --- mcp-agent-orchestrator/src/agents/prompts.ts | 347 +++++++++---------- mcp-agent-orchestrator/src/index.ts | 16 +- 2 files changed, 182 insertions(+), 181 deletions(-) diff --git a/mcp-agent-orchestrator/src/agents/prompts.ts b/mcp-agent-orchestrator/src/agents/prompts.ts index 339ae84d..f248e29d 100644 --- a/mcp-agent-orchestrator/src/agents/prompts.ts +++ b/mcp-agent-orchestrator/src/agents/prompts.ts @@ -1,81 +1,60 @@ /** - * Agent System Prompts (English to avoid CMD encoding issues) - * Agents will still respond in Korean based on user preferences + * Agent System Prompts v2.1 + * All prompts in English for better token efficiency and model performance. + * Agents will respond in Korean based on user preferences. */ export const PM_PROMPT = `# Role -You are a PM (Project Manager) agent. +You are a PM (Project Manager) agent for ERP-node project. Analyze user requests, distribute tasks to specialist agents (Backend, DB, Frontend), and integrate results to create the final deliverable. # Available Tools -- ask_backend_agent: Ask/request tasks from backend expert -- ask_db_agent: Ask/request tasks from DB expert -- ask_frontend_agent: Ask/request tasks from frontend expert -- parallel_ask: Request from multiple experts simultaneously +- ask_backend_agent: Backend expert (API, services, routing) +- ask_db_agent: DB expert (schema, queries, migrations) +- ask_frontend_agent: Frontend expert (components, pages, styles) +- parallel_ask: Multiple experts simultaneously # Work Process +1. Analyze request -> identify scope +2. If cross-domain (FE+BE+DB): use parallel_ask +3. If single domain: use specific agent +4. Integrate results -> report to user -## Phase 1: Analysis -1. Analyze user request -2. Identify required information -3. Request info gathering (use parallel_ask) +# Task Distribution +- Backend Agent: backend-node/src/ (controllers, services, routes) +- DB Agent: db/, mapper/ (schema, migrations, queries) +- Frontend Agent: frontend/ (components, pages, lib) -## Phase 2: Planning -1. Analyze gathered information -2. Break down tasks and identify dependencies -3. Determine priorities -4. Create work distribution plan - -## Phase 3: Execution -1. Request tasks in dependency order -2. Verify results -3. Re-request if needed - -## Phase 4: Integration -1. Collect all results -2. Verify consistency -3. Report to user - -# Task Distribution Criteria -- Backend Agent: API, service logic, routing (backend-node/) -- DB Agent: Schema, queries, migrations (mapper/, db/) -- Frontend Agent: Components, pages, styles (frontend/) - -# Decision Criteria -- Ask user if uncertain -- Re-request if agent result seems wrong -- Confirm with user if impact is large -- Choose safer direction when conflicts arise - -# Response Format -Use JSON format when distributing tasks: -{ - "phase": "info_gathering | work_distribution | integration", - "reasoning": "why distributing this way", - "tasks": [ - { - "agent": "backend | db | frontend", - "priority": 1, - "task": "specific task content", - "depends_on": [], - "expected_output": "expected result" - } - ] -} - -Final report: -{ - "summary": "one line summary", - "completed_tasks": ["completed tasks"], - "files_changed": ["changed files"], - "next_steps": ["next steps"], - "test_instructions": ["how to test"] -}`; +# Response: Always concise! Summarize key findings only.`; export const BACKEND_PROMPT = `# Role -You are a Backend specialist agent. -You handle API, services, and routing in the backend-node/ folder. +You are a Backend specialist for ERP-node project. +Stack: Node.js + Express + TypeScript + PostgreSQL Raw Query. + +# CRITICAL PROJECT RULES + +## 1. Multi-tenancy (ABSOLUTE MUST!) +- ALL queries MUST include company_code filter +- Use req.user!.companyCode from auth middleware +- NEVER trust client-sent company_code +- Super Admin (company_code = "*") sees all data +- Regular users CANNOT see company_code = "*" data + +## 2. Super Admin Visibility +- If req.user.companyCode !== "*", add: WHERE company_code != '*' +- Super admin users must be hidden from regular company users + +## 3. Required Code Pattern +\`\`\`typescript +const companyCode = req.user!.companyCode; +if (companyCode === "*") { + query = "SELECT * FROM table ORDER BY company_code"; +} else { + query = "SELECT * FROM table WHERE company_code = $1 AND company_code != '*'"; + params = [companyCode]; +} +\`\`\` # Your Domain (ONLY these!) - backend-node/src/controllers/ @@ -84,178 +63,196 @@ You handle API, services, and routing in the backend-node/ folder. - backend-node/src/middleware/ - backend-node/src/utils/ -# NOT Your Domain (NEVER touch) -- frontend/ -> Frontend Agent handles this -- src/com/pms/mapper/ -> DB Agent handles this -- Direct SQL queries -> Request from DB Agent +# NOT Your Domain +- frontend/ -> Frontend Agent +- db/migrations/ -> DB Agent +- Direct SQL schema design -> DB Agent # Code Rules -1. Use TypeScript -2. Error handling required +1. TypeScript strict mode +2. Error handling with try/catch 3. Comments in Korean -4. Follow existing code style -5. Complete code, no ... ellipsis +4. Follow existing code patterns +5. Use logger for important operations -# Response Format (JSON) +# Response Format (JSON) - BE CONCISE! { - "status": "success | partial | failed | need_clarification", + "status": "success | partial | failed", "confidence": "high | medium | low", "result": { "summary": "one line summary", - "details": "detailed explanation", - "files_affected": ["file paths"], - "code_changes": [ - { - "file": "path", - "action": "create | modify | delete", - "content": "complete code" - } - ] + "details": "brief explanation", + "files_affected": ["paths"], + "code_changes": [{"file": "path", "action": "create|modify", "content": "code"}] }, - "needs_from_others": [ - {"agent": "db", "request": "what you need"} - ], - "side_effects": ["affected areas"], - "questions": ["unclear points"] + "needs_from_others": [], + "questions": [] } -# Collaboration Rules -1. Report immediately if out of scope (scope_violation) -2. Set confidence: "low" if uncertain -3. Specify in needs_from_others if other agents needed -4. Always report side effects`; +# IMPORTANT: Keep responses SHORT. No unnecessary explanations.`; export const DB_PROMPT = `# Role -You are a Database specialist agent. -You handle DB schema, queries, and migrations. +You are a Database specialist for ERP-node project. +Stack: PostgreSQL + Raw Query (no ORM). Migrations in db/migrations/. + +# CRITICAL PROJECT RULES + +## 1. Multi-tenancy (ABSOLUTE MUST!) +- ALL tables MUST have company_code VARCHAR(20) NOT NULL +- ALL queries MUST filter by company_code +- JOINs MUST include company_code matching condition +- Subqueries MUST include company_code filter +- Aggregates (COUNT, SUM) MUST filter by company_code +- CREATE INDEX on company_code for every table + +## 2. company_code = "*" Meaning +- NOT shared/common data! +- Super admin ONLY data +- Regular companies CANNOT see it: WHERE company_code != '*' + +## 3. Required SQL Patterns +\`\`\`sql +-- Standard query pattern +SELECT * FROM table_name +WHERE company_code = $1 + AND company_code != '*' +ORDER BY created_date DESC; + +-- JOIN pattern (company_code matching required!) +SELECT a.*, b.name +FROM table_a a +LEFT JOIN table_b b ON a.ref_id = b.id + AND a.company_code = b.company_code +WHERE a.company_code = $1; +\`\`\` + +## 4. Migration Rules +- File naming: NNN_description.sql (e.g., 034_add_new_table.sql) +- Always include company_code column +- Always create index on company_code +- Add foreign key to company_info(company_code) when possible # Your Domain (ONLY these!) -- src/com/pms/mapper/ (MyBatis XML) -- db/ (schema, migrations) -- backend-node/src/database/ +- db/migrations/ +- SQL schema design +- Query optimization +- Index strategy -# NOT Your Domain (NEVER touch) -- API logic -> Backend Agent handles this -- Frontend -> Frontend Agent handles this -- Business logic decisions -> Confirm with PM +# NOT Your Domain +- API logic -> Backend Agent +- Frontend -> Frontend Agent +- Business logic decisions -> PM Agent # Code Rules -1. Use PostgreSQL syntax -2. Parameter binding (#{}) required - prevent SQL injection -3. Consider indexes -4. Consider performance optimization +1. PostgreSQL syntax only +2. Parameter binding ($1, $2) - prevent SQL injection +3. Consider indexes for frequently queried columns +4. Use COALESCE for NULL handling +5. Use TIMESTAMPTZ for dates -# MyBatis Mapper Rules -- Parameter binding: WHERE id = #{id} -- Dynamic queries: ... -- Pagination: LIMIT #{limit} OFFSET #{offset} - -# Response Format (JSON) +# Response Format (JSON) - BE CONCISE! { - "status": "success | partial | failed | need_clarification", + "status": "success | partial | failed", "confidence": "high | medium | low", "result": { "summary": "one line summary", - "details": "detailed explanation", - "schema_info": { - "tables": ["related tables"], - "columns": ["main columns"], - "indexes": ["indexes"] - }, - "code_changes": [ - { - "file": "path", - "action": "create | modify", - "content": "query/schema" - } - ] + "details": "brief explanation", + "schema_info": {"tables": [], "columns": [], "indexes": []}, + "code_changes": [{"file": "path", "action": "create|modify", "content": "sql"}] }, - "performance_notes": ["performance considerations"], - "questions": ["unclear points"] + "performance_notes": [], + "questions": [] } -# Collaboration Rules -1. Report immediately if out of scope -2. Set confidence: "low" if uncertain -3. Always mention performance issues`; +# IMPORTANT: Keep responses SHORT. Focus on schema and queries only.`; export const FRONTEND_PROMPT = `# Role -You are a Frontend specialist agent. -You handle React/Next.js UI implementation. +You are a Frontend specialist for ERP-node project. +Stack: Next.js 14 + React + TypeScript + Tailwind CSS + shadcn/ui. + +# CRITICAL PROJECT RULES + +## 1. API Client (ABSOLUTE RULE!) +- NEVER use fetch() directly! +- ALWAYS use lib/api/ clients (Axios-based) +\`\`\`typescript +// FORBIDDEN +const res = await fetch('/api/flow/definitions'); + +// MUST USE +import { getFlowDefinitions } from '@/lib/api/flow'; +const res = await getFlowDefinitions(); +\`\`\` + +## 2. shadcn/ui Style Rules +- Use CSS variables: bg-primary, text-muted-foreground (NOT bg-blue-500) +- No nested boxes: Card inside Card is FORBIDDEN +- Button variants: default, secondary, outline, ghost, destructive +- Responsive: mobile-first approach (sm:, md:, lg:) +- Modal standard: max-w-[95vw] sm:max-w-[500px] + +## 3. Component Rules +- Functional components only +- Korean comments for code documentation +- Custom hooks for reusable logic +- TypeScript strict typing required # Your Domain (ONLY these!) - frontend/components/ -- frontend/pages/ or frontend/app/ +- frontend/app/ or frontend/pages/ - frontend/lib/ - frontend/hooks/ - frontend/styles/ -# NOT Your Domain (NEVER touch) -- backend-node/ -> Backend Agent handles this -- DB related -> DB Agent handles this -- API spec decisions -> Discuss with PM/Backend +# NOT Your Domain +- backend-node/ -> Backend Agent +- DB schema -> DB Agent +- API endpoint decisions -> PM/Backend Agent # Code Rules -1. Use TypeScript -2. React functional components -3. Use custom hooks -4. Comments in Korean -5. Follow Tailwind CSS or existing style system +1. TypeScript strict mode +2. React functional components with hooks +3. Prefer shadcn/ui components +4. Use cn() utility for conditional classes +5. Comments in Korean -# API Call Rules -- NEVER use fetch directly! -- Use lib/api/ client -- Error handling required - -# Response Format (JSON) +# Response Format (JSON) - BE CONCISE! { - "status": "success | partial | failed | need_clarification", + "status": "success | partial | failed", "confidence": "high | medium | low", "result": { "summary": "one line summary", - "details": "detailed explanation", - "components_affected": ["component list"], - "code_changes": [ - { - "file": "path", - "action": "create | modify", - "content": "complete code" - } - ] + "details": "brief explanation", + "components_affected": ["list"], + "code_changes": [{"file": "path", "action": "create|modify", "content": "code"}] }, - "needs_from_others": [ - {"agent": "backend", "request": "needed API"} - ], - "ui_notes": ["UX considerations"], - "questions": ["unclear points"] + "needs_from_others": [], + "ui_notes": [], + "questions": [] } -# Collaboration Rules -1. Report immediately if out of scope -2. Set confidence: "low" if uncertain -3. Specify in needs_from_others if API needed -4. Suggest UX improvements if any`; +# IMPORTANT: Keep responses SHORT. No lengthy analysis unless explicitly asked.`; -// 에이전트 설정 맵 +// Agent configuration map export const AGENT_CONFIGS = { pm: { model: 'claude-opus-4-5-20250214', systemPrompt: PM_PROMPT, - maxTokens: 8192, + maxTokens: 4096, }, backend: { model: 'claude-sonnet-4-20250514', systemPrompt: BACKEND_PROMPT, - maxTokens: 8192, + maxTokens: 4096, }, db: { model: 'claude-sonnet-4-20250514', systemPrompt: DB_PROMPT, - maxTokens: 8192, + maxTokens: 4096, }, frontend: { model: 'claude-sonnet-4-20250514', systemPrompt: FRONTEND_PROMPT, - maxTokens: 8192, + maxTokens: 4096, }, } as const; diff --git a/mcp-agent-orchestrator/src/index.ts b/mcp-agent-orchestrator/src/index.ts index e202aa8b..3634cf70 100644 --- a/mcp-agent-orchestrator/src/index.ts +++ b/mcp-agent-orchestrator/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Multi-Agent Orchestrator MCP Server v2.0 +b * Multi-Agent Orchestrator MCP Server v2.0 * * Cursor Agent CLI를 활용한 멀티에이전트 시스템 * - PM (Cursor IDE): 전체 조율 @@ -123,7 +123,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "백엔드 전문가에게 질문하거나 작업을 요청합니다. " + "API 설계, 서비스 로직, 라우팅, 미들웨어 관련 작업에 사용하세요. " + - "담당 폴더: backend-node/src/ (Cursor Agent CLI, sonnet-4.5 모델)", + "담당 폴더: backend-node/src/ (Cursor Agent CLI, sonnet-4.5 모델)" + + "주의: 단순 파일 읽기/수정은 PM이 직접 처리하세요. 깊은 분석이 필요할 때만 호출!", inputSchema: { type: "object" as const, properties: { @@ -144,7 +145,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "DB 전문가에게 질문하거나 작업을 요청합니다. " + "스키마 설계, SQL 쿼리, MyBatis 매퍼, 마이그레이션 관련 작업에 사용하세요. " + - "담당 폴더: src/com/pms/mapper/, db/ (Cursor Agent CLI, sonnet-4.5 모델)", + "담당 폴더: src/com/pms/mapper/, db/ (Cursor Agent CLI, sonnet-4.5 모델)" + + "주의: 단순 스키마 확인은 PM이 직접 처리하세요. 복잡한 쿼리 설계/최적화 시에만 호출!", inputSchema: { type: "object" as const, properties: { @@ -165,7 +167,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "프론트엔드 전문가에게 질문하거나 작업을 요청합니다. " + "React 컴포넌트, 페이지, 스타일링, 상태관리 관련 작업에 사용하세요. " + - "담당 폴더: frontend/ (Cursor Agent CLI, sonnet-4.5 모델)", + "담당 폴더: frontend/ (Cursor Agent CLI, sonnet-4.5 모델)" + + "주의: 단순 컴포넌트 읽기/수정은 PM이 직접 처리하세요. 구조 분석이 필요할 때만 호출!", inputSchema: { type: "object" as const, properties: { @@ -185,8 +188,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "parallel_ask", description: "여러 전문가에게 동시에 질문합니다 (진짜 병렬 실행!). " + - "정보 수집 단계에서 모든 영역의 현황을 빠르게 파악할 때 유용합니다. " + - "모든 에이전트가 Cursor Agent CLI를 통해 동시에 실행되어 시간 절약!", + "3개 영역(FE+BE+DB) 크로스도메인 분석이 필요할 때만 사용하세요. " + + "주의: 호출 시간이 오래 걸림! 단순 작업은 PM이 직접 처리하는 게 훨씬 빠릅니다. " + + "적합한 경우: 전체 아키텍처 파악, 대규모 리팩토링 계획, 크로스도메인 영향 분석", inputSchema: { type: "object" as const, properties: { From f9803b0e6ce503cad802674d6bd5f50890754cba Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 6 Feb 2026 15:18:27 +0900 Subject: [PATCH 4/5] feat: Enhance ScreenDesigner with alignment and distribution features - Added new alignment, distribution, and size matching functionalities to the ScreenDesigner component. - Implemented keyboard shortcuts for nudging components and toggling labels. - Introduced a modal for displaying keyboard shortcuts to improve user experience. - Updated SlimToolbar to support new alignment and distribution actions based on selected components. - Enhanced zoom control with RAF throttling to prevent flickering during zoom operations. --- frontend/components/screen/ScreenDesigner.tsx | 260 ++++++++++++++++- .../components/screen/ScreenSettingModal.tsx | 54 ++-- .../screen/modals/KeyboardShortcutsModal.tsx | 144 ++++++++++ .../components/screen/toolbar/SlimToolbar.tsx | 121 ++++++++ frontend/components/v2/V2Select.tsx | 8 +- frontend/lib/utils/alignmentUtils.ts | 265 ++++++++++++++++++ 6 files changed, 814 insertions(+), 38 deletions(-) create mode 100644 frontend/components/screen/modals/KeyboardShortcutsModal.tsx create mode 100644 frontend/lib/utils/alignmentUtils.ts diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index df88cb04..429f91f8 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -35,6 +35,17 @@ import { snapSizeToGrid, snapToGrid, } from "@/lib/utils/gridUtils"; +import { + alignComponents, + distributeComponents, + matchComponentSize, + toggleAllLabels, + nudgeComponents, + AlignMode, + DistributeDirection, + MatchSizeMode, +} from "@/lib/utils/alignmentUtils"; +import { KeyboardShortcutsModal } from "./modals/KeyboardShortcutsModal"; // 10px 단위 스냅 함수 const snapTo10px = (value: number): number => { @@ -170,6 +181,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU // 메뉴 할당 모달 상태 const [showMenuAssignmentModal, setShowMenuAssignmentModal] = useState(false); + // 단축키 도움말 모달 상태 + const [showShortcutsModal, setShowShortcutsModal] = useState(false); + // 파일첨부 상세 모달 상태 const [showFileAttachmentModal, setShowFileAttachmentModal] = useState(false); const [selectedFileComponent, setSelectedFileComponent] = useState(null); @@ -360,6 +374,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const [zoomLevel, setZoomLevel] = useState(1); // 1 = 100% const MIN_ZOOM = 0.1; // 10% const MAX_ZOOM = 3; // 300% + const zoomRafRef = useRef(null); // 줌 RAF throttle용 // 전역 파일 상태 변경 시 강제 리렌더링을 위한 상태 const [forceRenderTrigger, setForceRenderTrigger] = useState(0); @@ -1647,7 +1662,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU panState.innerScrollTop, ]); - // 마우스 휠로 줌 제어 + // 마우스 휠로 줌 제어 (RAF throttle 적용으로 깜빡임 방지) useEffect(() => { const handleWheel = (e: WheelEvent) => { // 캔버스 컨테이너 내에서만 동작 @@ -1660,9 +1675,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const delta = e.deltaY; const zoomFactor = 0.001; // 줌 속도 조절 - setZoomLevel((prevZoom) => { - const newZoom = prevZoom - delta * zoomFactor; - return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom)); + // RAF throttle: 프레임당 한 번만 상태 업데이트 + if (zoomRafRef.current !== null) { + cancelAnimationFrame(zoomRafRef.current); + } + zoomRafRef.current = requestAnimationFrame(() => { + setZoomLevel((prevZoom) => { + const newZoom = prevZoom - delta * zoomFactor; + return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom)); + }); + zoomRafRef.current = null; }); } } @@ -1674,6 +1696,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU const containerRef = canvasContainerRef.current; return () => { containerRef?.removeEventListener("wheel", handleWheel); + if (zoomRafRef.current !== null) { + cancelAnimationFrame(zoomRafRef.current); + } }; }, [MIN_ZOOM, MAX_ZOOM]); @@ -1785,6 +1810,103 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU toast.success(`${adjustedComponents.length}개 컴포넌트가 격자에 맞게 재정렬되었습니다.`); }, [layout, screenResolution, saveToHistory]); + // === 정렬/배분/동일크기/라벨토글/Nudge 핸들러 === + + // 컴포넌트 정렬 + const handleGroupAlign = useCallback( + (mode: AlignMode) => { + if (groupState.selectedComponents.length < 2) { + toast.warning("2개 이상의 컴포넌트를 선택해주세요."); + return; + } + saveToHistory(layout); + const newComponents = alignComponents(layout.components, groupState.selectedComponents, mode); + setLayout((prev) => ({ ...prev, components: newComponents })); + + const modeNames: Record = { + left: "좌측", right: "우측", centerX: "가로 중앙", + top: "상단", bottom: "하단", centerY: "세로 중앙", + }; + toast.success(`${modeNames[mode]} 정렬 완료`); + }, + [groupState.selectedComponents, layout, saveToHistory] + ); + + // 컴포넌트 균등 배분 + const handleGroupDistribute = useCallback( + (direction: DistributeDirection) => { + if (groupState.selectedComponents.length < 3) { + toast.warning("3개 이상의 컴포넌트를 선택해주세요."); + return; + } + saveToHistory(layout); + const newComponents = distributeComponents(layout.components, groupState.selectedComponents, direction); + setLayout((prev) => ({ ...prev, components: newComponents })); + toast.success(`${direction === "horizontal" ? "가로" : "세로"} 균등 배분 완료`); + }, + [groupState.selectedComponents, layout, saveToHistory] + ); + + // 동일 크기 맞추기 + const handleMatchSize = useCallback( + (mode: MatchSizeMode) => { + if (groupState.selectedComponents.length < 2) { + toast.warning("2개 이상의 컴포넌트를 선택해주세요."); + return; + } + saveToHistory(layout); + const newComponents = matchComponentSize( + layout.components, + groupState.selectedComponents, + mode, + selectedComponent?.id + ); + setLayout((prev) => ({ ...prev, components: newComponents })); + + const modeNames: Record = { + width: "너비", height: "높이", both: "크기", + }; + toast.success(`${modeNames[mode]} 맞추기 완료`); + }, + [groupState.selectedComponents, layout, selectedComponent?.id, saveToHistory] + ); + + // 라벨 일괄 토글 + const handleToggleAllLabels = useCallback(() => { + saveToHistory(layout); + const newComponents = toggleAllLabels(layout.components); + setLayout((prev) => ({ ...prev, components: newComponents })); + + const hasHidden = layout.components.some( + (c) => c.type === "widget" && (c.style as any)?.labelDisplay === false + ); + toast.success(hasHidden ? "모든 라벨 표시" : "모든 라벨 숨기기"); + }, [layout, saveToHistory]); + + // Nudge (화살표 키 이동) + const handleNudge = useCallback( + (direction: "up" | "down" | "left" | "right", distance: number) => { + const targetIds = + groupState.selectedComponents.length > 0 + ? groupState.selectedComponents + : selectedComponent + ? [selectedComponent.id] + : []; + + if (targetIds.length === 0) return; + + const newComponents = nudgeComponents(layout.components, targetIds, direction, distance); + setLayout((prev) => ({ ...prev, components: newComponents })); + + // 선택된 컴포넌트 업데이트 + if (selectedComponent && targetIds.includes(selectedComponent.id)) { + const updated = newComponents.find((c) => c.id === selectedComponent.id); + if (updated) setSelectedComponent(updated); + } + }, + [groupState.selectedComponents, selectedComponent, layout.components] + ); + // 저장 const handleSave = useCallback(async () => { if (!selectedScreen?.screenId) { @@ -5359,6 +5481,105 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU } return false; } + + // === 9. 화살표 키 Nudge (컴포넌트 미세 이동) === + if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) { + // 입력 필드에서는 무시 + const active = document.activeElement; + if ( + active instanceof HTMLInputElement || + active instanceof HTMLTextAreaElement || + active?.getAttribute("contenteditable") === "true" + ) { + return; + } + + if (selectedComponent || groupState.selectedComponents.length > 0) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + const distance = e.shiftKey ? 10 : 1; // Shift 누르면 10px + const dirMap: Record = { + ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right", + }; + handleNudge(dirMap[e.key], distance); + return false; + } + } + + // === 10. 정렬 단축키 (Alt + 키) - 다중 선택 시 === + if (e.altKey && !e.ctrlKey && !e.metaKey) { + const alignKey = e.key?.toLowerCase(); + const alignMap: Record = { + l: "left", r: "right", c: "centerX", + t: "top", b: "bottom", m: "centerY", + }; + + if (alignMap[alignKey] && groupState.selectedComponents.length >= 2) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleGroupAlign(alignMap[alignKey]); + return false; + } + + // 균등 배분 (Alt+H: 가로, Alt+V: 세로) + if (alignKey === "h" && groupState.selectedComponents.length >= 3) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleGroupDistribute("horizontal"); + return false; + } + if (alignKey === "v" && groupState.selectedComponents.length >= 3) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleGroupDistribute("vertical"); + return false; + } + + // 동일 크기 맞추기 (Alt+W: 너비, Alt+E: 높이) + if (alignKey === "w" && groupState.selectedComponents.length >= 2) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleMatchSize("width"); + return false; + } + if (alignKey === "e" && groupState.selectedComponents.length >= 2) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleMatchSize("height"); + return false; + } + } + + // === 11. 라벨 일괄 토글 (Alt+Shift+L) === + if (e.altKey && e.shiftKey && e.key?.toLowerCase() === "l") { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + handleToggleAllLabels(); + return false; + } + + // === 12. 단축키 도움말 (? 키) === + if (e.key === "?" && !e.ctrlKey && !e.metaKey && !e.altKey) { + // 입력 필드에서는 무시 + const active = document.activeElement; + if ( + active instanceof HTMLInputElement || + active instanceof HTMLTextAreaElement || + active?.getAttribute("contenteditable") === "true" + ) { + return; + } + e.preventDefault(); + setShowShortcutsModal(true); + return false; + } }; // window 레벨에서 캡처 단계에서 가장 먼저 처리 @@ -5376,6 +5597,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU groupState.selectedComponents, layout, selectedScreen, + handleNudge, + handleGroupAlign, + handleGroupDistribute, + handleMatchSize, + handleToggleAllLabels, ]); // 플로우 위젯 높이 자동 업데이트 이벤트 리스너 @@ -5503,6 +5729,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU onOpenMultilangSettings={() => setShowMultilangSettingsModal(true)} isPanelOpen={panelStates.v2?.isOpen || false} onTogglePanel={() => togglePanel("v2")} + selectedCount={groupState.selectedComponents.length} + onAlign={handleGroupAlign} + onDistribute={handleGroupDistribute} + onMatchSize={handleMatchSize} + onToggleLabels={handleToggleAllLabels} + onShowShortcuts={() => setShowShortcutsModal(true)} /> {/* 메인 컨테이너 (패널들 + 캔버스) */}
@@ -6013,8 +6245,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
)} - {/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - 좌우 최소화, 위아래 넉넉한 여유 */} -
+ {/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - GPU 가속 스크롤 적용 */} +
{/* Pan 모드 안내 - 제거됨 */} {/* 줌 레벨 표시 */}
@@ -6123,12 +6359,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
); })()} - {/* 🔥 줌 적용 시 스크롤 영역 확보를 위한 래퍼 - 중앙 정렬로 변경 */} + {/* 줌 적용 시 스크롤 영역 확보를 위한 래퍼 - 중앙 정렬 + contain 최적화 */}
{/* 실제 작업 캔버스 (해상도 크기) - 고정 크기 + 줌 적용 */} @@ -6141,8 +6378,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU maxWidth: `${screenResolution.width}px`, minHeight: `${screenResolution.height}px`, flexShrink: 0, - transform: `scale(${zoomLevel})`, + transform: `scale3d(${zoomLevel}, ${zoomLevel}, 1)`, transformOrigin: "top center", // 중앙 기준으로 스케일 + willChange: "transform", // GPU 가속 레이어 생성 + backfaceVisibility: "hidden" as const, // 리페인트 최적화 }} >
+ {/* 단축키 도움말 모달 */} + setShowShortcutsModal(false)} + />
diff --git a/frontend/components/screen/ScreenSettingModal.tsx b/frontend/components/screen/ScreenSettingModal.tsx index 88ee9ece..fa802893 100644 --- a/frontend/components/screen/ScreenSettingModal.tsx +++ b/frontend/components/screen/ScreenSettingModal.tsx @@ -365,7 +365,7 @@ export function ScreenSettingModal({ return ( <> - + @@ -525,34 +525,30 @@ export function ScreenSettingModal({ - {/* ScreenDesigner 전체 화면 모달 */} - - - 화면 디자이너 -
- { - setShowDesignerModal(false); - // 디자이너에서 저장 후 모달 닫으면 데이터 새로고침 - await loadData(); - // 데이터 로드 완료 후 iframe 갱신 - setIframeKey(prev => prev + 1); - }} - /> -
-
-
+ {/* ScreenDesigner 전체 화면 - Dialog 중첩 시 오버레이 컴포지팅으로 깜빡임 발생하여 직접 렌더링 */} + {/* ScreenDesigner 자체 SlimToolbar에 "목록으로" 닫기 버튼이 있으므로 별도 X 버튼 불필요 */} + {showDesignerModal && ( +
+ { + setShowDesignerModal(false); + await loadData(); + setIframeKey(prev => prev + 1); + }} + /> +
+ )} {/* TableSettingModal */} {tableSettingTarget && ( diff --git a/frontend/components/screen/modals/KeyboardShortcutsModal.tsx b/frontend/components/screen/modals/KeyboardShortcutsModal.tsx new file mode 100644 index 00000000..0f122c53 --- /dev/null +++ b/frontend/components/screen/modals/KeyboardShortcutsModal.tsx @@ -0,0 +1,144 @@ +"use client"; + +import React from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; + +interface ShortcutItem { + keys: string[]; + description: string; +} + +interface ShortcutGroup { + title: string; + shortcuts: ShortcutItem[]; +} + +const shortcutGroups: ShortcutGroup[] = [ + { + title: "기본 조작", + shortcuts: [ + { keys: ["Ctrl", "S"], description: "레이아웃 저장" }, + { keys: ["Ctrl", "Z"], description: "실행취소" }, + { keys: ["Ctrl", "Y"], description: "다시실행" }, + { keys: ["Ctrl", "A"], description: "전체 선택" }, + { keys: ["Delete"], description: "선택 삭제" }, + { keys: ["Esc"], description: "선택 해제" }, + ], + }, + { + title: "복사/붙여넣기", + shortcuts: [ + { keys: ["Ctrl", "C"], description: "컴포넌트 복사" }, + { keys: ["Ctrl", "V"], description: "컴포넌트 붙여넣기" }, + ], + }, + { + title: "그룹 관리", + shortcuts: [ + { keys: ["Ctrl", "G"], description: "그룹 생성" }, + { keys: ["Ctrl", "Shift", "G"], description: "그룹 해제" }, + ], + }, + { + title: "이동 (Nudge)", + shortcuts: [ + { keys: ["Arrow"], description: "1px 이동" }, + { keys: ["Shift", "Arrow"], description: "10px 이동" }, + ], + }, + { + title: "정렬 (다중 선택 시)", + shortcuts: [ + { keys: ["Alt", "L"], description: "좌측 정렬" }, + { keys: ["Alt", "R"], description: "우측 정렬" }, + { keys: ["Alt", "C"], description: "가로 중앙 정렬" }, + { keys: ["Alt", "T"], description: "상단 정렬" }, + { keys: ["Alt", "B"], description: "하단 정렬" }, + { keys: ["Alt", "M"], description: "세로 중앙 정렬" }, + ], + }, + { + title: "배분/크기 (다중 선택 시)", + shortcuts: [ + { keys: ["Alt", "H"], description: "가로 균등 배분" }, + { keys: ["Alt", "V"], description: "세로 균등 배분" }, + { keys: ["Alt", "W"], description: "너비 맞추기" }, + { keys: ["Alt", "E"], description: "높이 맞추기" }, + ], + }, + { + title: "보기/탐색", + shortcuts: [ + { keys: ["Space", "Drag"], description: "캔버스 팬(이동)" }, + { keys: ["Wheel"], description: "줌 인/아웃" }, + { keys: ["P"], description: "패널 열기/닫기" }, + { keys: ["Alt", "Shift", "L"], description: "라벨 일괄 표시/숨기기" }, + { keys: ["?"], description: "단축키 도움말" }, + ], + }, +]; + +interface KeyboardShortcutsModalProps { + isOpen: boolean; + onClose: () => void; +} + +export const KeyboardShortcutsModal: React.FC = ({ + isOpen, + onClose, +}) => { + return ( + + + + + 키보드 단축키 + + + 화면 디자이너에서 사용할 수 있는 단축키 목록입니다. Mac에서는 Ctrl 대신 Cmd를 사용합니다. + + + +
+ {shortcutGroups.map((group) => ( +
+

+ {group.title} +

+
+ {group.shortcuts.map((shortcut, idx) => ( +
+ + {shortcut.description} + +
+ {shortcut.keys.map((key, kidx) => ( + + {kidx > 0 && ( + + + )} + + {key} + + + ))} +
+
+ ))} +
+
+ ))} +
+
+
+ ); +}; diff --git a/frontend/components/screen/toolbar/SlimToolbar.tsx b/frontend/components/screen/toolbar/SlimToolbar.tsx index d71ed93a..2dbd7129 100644 --- a/frontend/components/screen/toolbar/SlimToolbar.tsx +++ b/frontend/components/screen/toolbar/SlimToolbar.tsx @@ -22,6 +22,18 @@ import { Settings2, PanelLeft, PanelLeftClose, + AlignStartVertical, + AlignCenterVertical, + AlignEndVertical, + AlignStartHorizontal, + AlignCenterHorizontal, + AlignEndHorizontal, + AlignHorizontalSpaceAround, + AlignVerticalSpaceAround, + RulerIcon, + Tag, + Keyboard, + Equal, } from "lucide-react"; import { ScreenResolution, SCREEN_RESOLUTIONS } from "@/types/screen"; import { @@ -50,6 +62,10 @@ interface GridSettings { gridOpacity?: number; } +type AlignMode = "left" | "centerX" | "right" | "top" | "centerY" | "bottom"; +type DistributeDirection = "horizontal" | "vertical"; +type MatchSizeMode = "width" | "height" | "both"; + interface SlimToolbarProps { screenName?: string; tableName?: string; @@ -67,6 +83,13 @@ interface SlimToolbarProps { // 패널 토글 기능 isPanelOpen?: boolean; onTogglePanel?: () => void; + // 정렬/배분/크기 기능 + selectedCount?: number; + onAlign?: (mode: AlignMode) => void; + onDistribute?: (direction: DistributeDirection) => void; + onMatchSize?: (mode: MatchSizeMode) => void; + onToggleLabels?: () => void; + onShowShortcuts?: () => void; } export const SlimToolbar: React.FC = ({ @@ -85,6 +108,12 @@ export const SlimToolbar: React.FC = ({ onOpenMultilangSettings, isPanelOpen = false, onTogglePanel, + selectedCount = 0, + onAlign, + onDistribute, + onMatchSize, + onToggleLabels, + onShowShortcuts, }) => { // 사용자 정의 해상도 상태 const [customWidth, setCustomWidth] = useState(""); @@ -325,8 +354,100 @@ export const SlimToolbar: React.FC = ({ )}
+ {/* 중앙: 정렬/배분 도구 (다중 선택 시 표시) */} + {selectedCount >= 2 && (onAlign || onDistribute || onMatchSize) && ( +
+ {/* 정렬 */} + {onAlign && ( + <> + 정렬 + + + +
+ + + + + )} + + {/* 배분 (3개 이상 선택 시) */} + {onDistribute && selectedCount >= 3 && ( + <> +
+ 배분 + + + + )} + + {/* 크기 맞추기 */} + {onMatchSize && ( + <> +
+ 크기 + + + + + )} + +
+ {selectedCount}개 선택 +
+ )} + {/* 우측: 버튼들 */}
+ {/* 라벨 토글 버튼 */} + {onToggleLabels && ( + + )} + + {/* 단축키 도움말 */} + {onShowShortcuts && ( + + )} + {onPreview && (