geul.io MCP 서버 만들다 겪은 시행착오
geul의 MCP를 제품별 예외 없이 정리하고 싶었다. 서버는 표준 연결과 글 권한을 제공하고, Codex나 Claude Code는 각자의 연결 방식을 맡게 하는 것이 목표였다. 그런데 OAuth 로그인에 성공한 뒤에도 Codex에 도구가 나타나지 않았다.
처음 막힌 곳은 인증용 공개 메타데이터 조회였다. 같은 URL이 Node.js에서는 200, 로컬 Workers에서는 403을 반환했다. 정확한 외부 차단 원인은 확인하지 못했지만, 조회만 Node.js로 중계하자 로그인은 통과했다. 인증 검증은 기존 라이브러리가 계속 담당했다.
문제는 그다음이었다. 로그인 성공을 연결 완료로 판단했지만 실제 도구 호출은 여전히 실패했다. 로그를 확인하니 Codex는 MCP 2025-06-18을 요청하고, geul은 2026-07-28만 허용하고 있었다.
제품별 우회를 없애려다가 이전 표준까지 거절한 것이다. 공식 SDK의 설정을 legacy: "stateless"로 바꿔 이전 표준 요청도 처리하게 했다. 별도 클라이언트 코드를 만들 필요는 없었다. 표준 준수와 최신 버전 강제는 구분해야 했다.
서버를 수정하고 Codex를 재시작한 뒤, 등록된 도구로 글 조회와 작성에 성공했다. 저장된 본문을 다시 읽어 확인하는 것으로 검증을 마쳤다. 별도 요청으로 서버를 시험하는 것과 사용자의 클라이언트에서 실제 도구를 호출하는 것도 서로 다른 확인이었다.
임시 중계의 관리 방식도 바뀌었다. 처음에는 Codex 전용 코드라 커밋에서 제외하려 했다. 하지만 다음에도 테스트해야 한다면 재현 가능한 개발 환경이 필요했다. 그래서 pnpm dev는 기본 Workers 환경으로, pnpm dev:mcp는 허용한 공개 문서만 중계하는 환경으로 분리했다. 제품 코드에서 클라이언트 이름을 없애고, 개발 중계는 빌드에서 제외했다. 다만 로컬 중계가 운영의 외부 통신 문제까지 해결한 것은 아니다.
이번에 얻은 기준은 세 가지다.
로그인, 연결, 도구 실행, 저장 확인을 각각 검증한다.
호환성은 공식 SDK에 맡기고 제품별 예외를 줄인다.
반복해서 필요한 임시 해결책은 명시적인 개발 모드로 관리한다.
코드를 덜어내는 것만으로 단순해지지는 않았다. 각 부분이 맡을 책임을 분명히 하고, 사용자가 실제로 수행할 작업까지 확인해야 비로소 연결이 끝났다.
#dev