# mcp-tool-development > vibe-commander의 MCP 서버 도구를 구현합니다. MCP tool 정의, 핸들러 작성, MCP SDK 사용, 도구 스키마 설계 요청 시 사용합니다. - Author: yachaboom - Repository: viilab/vibe-commander - Version: 20260207190114 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-07 - Source: https://github.com/viilab/vibe-commander - Web: https://mule.run/skillshub/@@viilab/vibe-commander~mcp-tool-development:20260207190114 --- --- name: mcp-tool-development description: vibe-commander의 MCP 서버 도구를 구현합니다. MCP tool 정의, 핸들러 작성, MCP SDK 사용, 도구 스키마 설계 요청 시 사용합니다. metadata: version: 1.0.0 category: adapter tags: [mcp, server, tools, sdk, protocol] --- # MCP Tool Development Guide vibe-commander는 `@modelcontextprotocol/sdk` 1.26.0을 사용하여 MCP 서버를 구현합니다. 에이전트(Claude Code, Gemini CLI 등)가 도구를 직접 호출할 수 있도록 합니다. ## Instructions ### Step 1: 도구 목록 확인 vibe-commander는 4개의 MCP 도구를 제공합니다. | 도구 | 기능 | 필수 파라미터 | |------|------|-------------| | `set_current_unit` | 커맨드 파일 현재 작업 섹션 자동 구성 | `unitId` | | `get_unit_context` | 유닛 컨텍스트 JSON 반환 (읽기 전용) | `unitId` | | `list_available_units` | 착수 가능한 미완료 유닛 목록 | (없음) | | `update_commit_sha` | 커밋 SHA 업데이트 | `sha` | ### Step 2: MCP Server 기본 구조 ```typescript import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; const server = new Server( { name: 'vibe-commander', version: '1.0.0' }, { capabilities: { tools: {} } } ); // 도구 목록 핸들러 server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'set_current_unit', description: '유닛 ID로 커맨드 파일의 현재 작업 섹션을 자동 구성', inputSchema: { type: 'object', properties: { unitId: { type: 'string', description: '유닛 ID' }, type: { type: 'string', description: '유닛 유형 (자동 매칭 시 생략)' }, }, required: ['unitId'], }, }, // ... 나머지 도구 ], })); ``` ### Step 3: 도구 핸들러 구현 ```typescript server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'set_current_unit': return handleSetUnit(args); case 'get_unit_context': return handleGetContext(args); case 'list_available_units': return handleListUnits(args); case 'update_commit_sha': return handleUpdateCommit(args); default: return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: { code: 'UNKNOWN_TOOL', message: `알 수 없는 도구: ${name}` } }) }], isError: true, }; } }); ``` ### Step 4: 서버 시작 ```typescript const transport = new StdioServerTransport(); await server.connect(transport); ``` ## Rules ### 도구 핸들러와 Core 분리 MCP 핸들러는 Adapter(Layer 3)입니다. 비즈니스 로직은 Core/Resolver에 위임합니다. **Do ✅**: ```typescript // MCP 핸들러: 입력 검증 → Core 호출 → 결과 포맷팅 async function handleSetUnit(args: unknown): Promise { // 1. 입력 검증 const parsed = SetUnitInputSchema.safeParse(args); if (!parsed.success) { return errorResult('INVALID_INPUT', '잘못된 입력', parsed.error.format()); } // 2. Core/Resolver 호출 (비즈니스 로직 위임) const config = loadConfig(projectRoot); const result = await setUnit(parsed.data.unitId, config); // 3. MCP 결과 포맷으로 변환 return { content: [{ type: 'text', text: JSON.stringify(result) }], isError: !result.success, }; } ``` **Don't ❌**: ```typescript // MCP 핸들러에 파싱 로직 직접 구현 async function handleSetUnit(args: unknown) { const content = readFileSync(`vibe/unit-plans/${args.unitId}.md`, 'utf-8'); const title = content.match(/^# (.+)/m)?.[1]; // ❌ Core 로직이 Adapter에 // ... } ``` ### MCP 응답 표준 모든 MCP 도구 응답은 `ToolResult` JSON을 text content로 반환합니다. **Do ✅**: ```typescript // 성공 응답 return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: { unitId, title, depsFound, docsFound, commitsFound } }) }], isError: false, }; // 실패 응답 return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: { code: 'PLAN_NOT_FOUND', message: '계획서를 찾을 수 없음' } }) }], isError: true, }; ``` **Don't ❌**: ```typescript // 비구조화된 응답 — 에이전트가 파싱 불가 return { content: [{ type: 'text', text: '계획서가 없습니다' }], isError: true }; ``` ### 입력 검증은 Zod로 **Do ✅**: ```typescript const SetUnitInputSchema = z.object({ unitId: z.string().min(1, '유닛 ID 필수'), type: z.string().optional(), }); const parsed = SetUnitInputSchema.safeParse(args); if (!parsed.success) { return errorResult('INVALID_INPUT', parsed.error.message); } ``` **Don't ❌**: ```typescript // 수동 타입 체크 — 누락 가능 if (typeof args.unitId !== 'string') { /* ... */ } ``` ### description 작성 가이드 MCP 도구의 `description`은 에이전트가 도구를 선택하는 기준입니다. **Do ✅**: ```typescript { name: 'set_current_unit', description: '유닛 ID로 커맨드 파일의 현재 작업 섹션을 자동 구성. 의존성 문서, 커밋 SHA, 특별 요청사항을 자동 수집하여 삽입.', } ``` **Don't ❌**: ```typescript { name: 'set_current_unit', description: '유닛을 설정합니다.', // ❌ 너무 모호 } ``` ## Examples ### Example 1: 새 MCP 도구 추가 User says: "validate 도구를 MCP에 추가해줘" Actions: 1. `ListToolsRequestSchema` 핸들러에 도구 정의 추가 2. `CallToolRequestSchema` 핸들러에 case 추가 3. Core에 검증 로직이 있는지 확인 → 없으면 Core에 먼저 구현 4. 핸들러에서 입력 검증 → Core 호출 → 결과 포맷팅 Result: 에이전트가 `validate` 도구를 호출 가능 ### Example 2: 기존 도구에 파라미터 추가 User says: "list_available_units에 phase 필터 추가해줘" Actions: 1. `inputSchema`에 `phase` 프로퍼티 추가 (optional) 2. Zod 입력 스키마 업데이트 3. Core 함수에 `phase` 파라미터 전달 4. 도구 `description` 업데이트 (phase 필터링 가능하다고 명시) ## Troubleshooting ### Error: MCP 서버가 시작되지 않음 Cause: stdin/stdout 트랜스포트 초기화 실패 Solution: `console.log` 대신 `console.error`로 디버깅 (stdout은 MCP 프로토콜용) ### Error: 도구 호출 시 "Unknown tool" 반환 Cause: `ListToolsRequestSchema` 핸들러에 도구를 등록했지만 `CallToolRequestSchema` 핸들러에 case 미추가 Solution: 두 핸들러를 동기화하여 모든 도구가 양쪽에 존재하는지 확인 ### Error: 에이전트가 도구를 선택하지 않음 Cause: description이 너무 모호하거나 트리거 키워드 부족 Solution: description에 구체적인 사용 시나리오와 키워드 포함