Grovarc 개발기 #06 — Next.js 16 업그레이드 + MCP 서버 구현

Phase 5(프론트엔드)를 마무리하면서 Playwright E2E 테스트를 붙였고, 버전을 최신으로 맞추면서 Next.js 14→16 업그레이드를 진행했다. 그리고 Phase 6에서 드디어 MCP 서버를 구현했다. Cursor뿐만 아니라 Claude Code, Codex CLI, Gemini CLI까지 연동 가능한 범용 서버다.


목표

  • Playwright E2E 테스트로 Phase 5 마무리
  • Next.js를 최신 버전(16)으로 업그레이드
  • TypeScript MCP 서버 구현 — 4개 AI CLI 공통 연동

핵심 구현 포인트

1) Playwright E2E 테스트 3파일

인증(auth), 작업 로그(worklog), 회고(retrospective) 3개 파일로 총 13개 케이스를 작성했다. 실서버가 필요한 테스트라 CI에서는 repo variable RUN_E2E=true일 때만 실행하도록 조건부 처리했다.

// playwright.config.ts - 서버 없을 때 CI 스킵 패턴
jobs:
  web-e2e:
    if: ${{ vars.RUN_E2E == 'true' }}

2) Next.js 14 → 16 업그레이드

처음엔 “Next.js 최신이 15겠지” 했는데 이미 16.2.2였다. 바이브코딩의 함정. 주요 breaking change 3가지를 순서대로 밟았다.

① ESLint next/typescript 설정 없음 eslint-config-next@14에는 next/typescript가 없다. 16으로 올리니 해결.

middleware.tsproxy.ts 리네임 Next.js 16에서 파일 컨벤션이 바뀌었다. 그것도 모자라 함수명도 middlewareproxy로 바꿔야 했다.

// before (Next.js 14)
export function middleware(request: NextRequest) { ... }

// after (Next.js 16)
export function proxy(request: NextRequest) { ... }

useSearchParams() Suspense 필수 14에서도 요구하긴 했는데, 16에서 빌드 자체가 실패하게 됐다. LoginForm으로 분리해서 <Suspense>로 감쌌다.

export default function LoginPage() {
  return (
    <Suspense fallback={null}>
      <LoginForm />  {/* useSearchParams() 여기서만 */}
    </Suspense>
  );
}

3) Docker 빌드 오류 삼연속

빌드 오류가 세 번 연속으로 나왔다.

  1. NEXT_PUBLIC_API_URL 미설정 → rewrites() 조건부 처리 + Dockerfile ARG 추가
  2. public/ 디렉토리 없음 → .gitkeep으로 해결
  3. addgroup 명령어 없음 → oven/bun:1-slim이 Debian 기반이라 groupadd/useradd로 교체

4) MCP 서버 — stdio transport로 4개 CLI 동시 지원

MCP 서버의 핵심 설계 결정은 stdio transport 사용이다. Cursor, Claude Code, Codex CLI, Gemini CLI 모두 MCP 표준 프로토콜을 따르기 때문에, 하나의 서버 바이너리로 모든 클라이언트를 지원할 수 있다.

// src/index.ts
const server = new McpServer({ name: "grovarc", version: "0.1.0" });

for (const tool of tools) {
  server.tool(tool.name, tool.description, tool.inputSchema.shape, tool.handler);
}

const transport = new StdioServerTransport();
await server.connect(transport);

6개 Tool을 구현했다: get_work_logs, get_work_log, get_retrospectives, get_retrospective, get_coaching_result, get_dashboard_stats.

각 클라이언트별 설정은 경로만 다를 뿐 JSON 구조가 동일하다:

{
  "mcpServers": {
    "grovarc": {
      "command": "node",
      "args": ["/path/to/apps/mcp/dist/index.js"],
      "env": { "GROVARC_API_URL": "...", "GROVARC_API_TOKEN": "..." }
    }
  }
}

트러블슈팅 / 고민 포인트

oven/bun:1-slim — Alpine이 아니다

Dockerfile을 작성할 때 addgroup/adduser를 썼는데 command not found가 났다. oven/bun:1-slim은 Alpine이 아니라 Debian 기반이라 Alpine 명령어가 없다. groupadd/useradd로 교체해서 해결.

MCP 서버가 4개 클라이언트를 어떻게 동시 지원하는가

처음엔 “각 클라이언트마다 다른 transport가 필요하지 않을까” 걱정했는데, 모두 MCP 표준 프로토콜(JSON-RPC over stdio)을 따르기 때문에 같은 서버 바이너리로 커버된다. 클라이언트 입장에서 설정 파일 위치와 포맷이 살짝 다를 뿐이다.


결과

  • Phase 5 완료: E2E 13개 케이스, Next.js 16 + React 19 업그레이드
  • Phase 6 완료: MCP 서버 (6 Tools, vitest 9개 pass), 4개 AI CLI 연동 가이드
  • Docker 빌드 3개 오류 모두 수정
  • Phase 2~6 연속 완료

다음 개선 아이디어

  • Phase 7: PRD 공개, 프로젝트 루트 README, 블로그 시리즈 집필
  • Phase 8: QA & 런칭 (성능 테스트, EKS 배포, 커뮤니티 공유)