MCP server 개발기

MCP server 개발기

들어가며

AI 챗봇 기능을 추가하는 프로젝트가 시작되면서, 저는 그중에서도 “LLM이 시스템 데이터를 조회할 수 있게 만드는” 부분을 맡게 됐습니다. LLM이 사용자 질문에 답하려면 결국 우리 시스템이 갖고 있는 실시간 데이터를 봐야 하는데, 이걸 어떤 방식으로 연결할지가 제 몫이었습니다.

MCP(Model Context Protocol)라는 개념 자체는 이미 알고 있었습니다. LLM에 “도구(Tool)”를 붙여서 외부 시스템과 상호작용하게 만드는 표준 프로토콜이라는 것도, 최근 여러 AI 서비스들이 이걸 채택하고 있다는 것도 뉴스나 문서로 접해본 적은 있었죠. 하지만 그건 “알고 있다”였지 “만들어봤다”는 아니었습니다. 개념을 설명할 수 있는 것과, 실제로 Spring Boot 프로젝트 하나를 MCP 서버로 세워서 운영 가능한 수준까지 만드는 것 사이에는 생각보다 큰 간극이 있었습니다.

이 글은 그 간극을 메워가면서 배운 것들, 그리고 그 과정에서 Spring AI가 실제로 얼마나 많은 걸 대신 해주는지를 정리한 기록입니다.

1. 문제 — 개념은 아는데, 어디서부터 만들어야 할지 모르겠다

1-1. 이 프로젝트에서 제가 풀어야 했던 문제

우리 팀의 AI 챗봇 아키텍처는 이렇게 정리돼 있었습니다.

UI → AI 서버 → LLM
        ↓ (MCP)
     MCP 서버
        ↓
MSA로 각각의 데이터를 수집하고 있는 서비스들

사용자가 “지금 데이터 수집이 안 되는 장비가 있어?”라고 물으면, AI 서버가 LLM을 통해 “이 질문에 답하려면 서버 목록을 조회하는 도구가 필요하다”고 판단하고, 그 도구 호출을 MCP로 제가 만든 MCP 서버에 넘깁니다. MCP 서버는 그 요청을 실제 서버 관리 서비스의 REST API 호출로 바꿔서 실행하고, 결과를 LLM이 이해하기 좋은 형태로 정리해서 돌려줍니다.

제가 맡은 게 바로 이 MCP 서버였습니다. 즉 “기존 사내 시스템의 기능을, AI가 쓸 수 있는 형태로 새로 노출하는 서버”를 처음부터 만드는 일이었습니다.

1-2. 개발해야 하는 것들

MCP 스펙 문서를 읽어보면 initialize, tools/list, tools/call 같은 JSON-RPC 메시지들이 오가는 프로토콜이라는 건 이해가 됩니다. 그런데 실제로 개발을 시작하려니 구체적인 질문들이 쏟아졌습니다.

  • 이 프로토콜을 Spring Boot 컨트롤러로 직접 구현해야 하나? JSON-RPC 라우팅, 세션 관리(Mcp-Session-Id), Streamable HTTP 트랜스포트까지 다 손으로 짜야 한다면 배보다 배꼽이 더 큰 일이었습니다.
  • “도구(Tool)”는 코드로 어떻게 표현해야 하나? 그냥 메서드 하나가 도구 하나인가, 아니면 별도의 인터페이스를 구현해야 하나?
  • 도구의 입력/출력 스펙(JSON Schema)은 누가 만드나? MCP 클라이언트(LLM 쪽)는 도구를 호출하기 전에 이 도구가 어떤 파라미터를 받는지 알아야 하는데, 이걸 매번 손으로 스키마를 작성해야 한다면 도구를 하나 추가할 때마다 엄청난 보일러플레이트가 생길 것 같았습니다.
  • 우리 서비스는 이미 서버 관리, 장비 관리 등 여러 백엔드 서비스가 있고, 각각 자기만의 REST API 컨벤션과 클라이언트 라이브러리를 갖고 있습니다. 이걸 MCP 도구로 감쌀 때, 이 프로젝트만의 구조를 어떻게 잡아야 나중에 다른 팀원들이 다른 서비스를 추가할 때도 헷갈리지 않을까?

정리하면, “MCP가 뭔지”는 문제가 아니었습니다. “Spring Boot 생태계 안에서 이걸 어떻게 구조를 잡고 짜야 하는지”가 진짜 문제였습니다.

2. 해결방안 — Spring AI의 MCP Server 지원

이 문제를 Spring AI(Spring 진영의 공식 AI 통합 프로젝트)라는 프레임워크를 사용해서 해결하기로 했습니다. Spring AI는 MCP 서버 구현을 위한 스타터를 제공하는데, 크게 두 가지를 대신해줍니다.

  • 프로토콜 레벨 처리: initialize, tools/list, tools/call 같은 MCP 프로토콜 메시지 라우팅과 세션 관리를 프레임워크가 맡아줍니다.
  • 애노테이션 기반 도구 등록: 개발자는 그냥 평범한 메서드에 “이게 하나의 도구다”라는 애노테이션만 붙이면, 나머지(스키마 생성, 등록, 호출 연결)는 프레임워크가 자동으로 처리합니다.

왜 이걸 선택했는지는 명확했습니다.

  • 기존 스택과의 자연스러운 통합: 우리 백엔드는 전부 Spring Boot + Gradle이고, 각 서비스의 클라이언트 라이브러리도 Spring 생태계 안에서 동작합니다. MCP 서버도 같은 스택으로 가면 팀원들이 진입장벽 없이 바로 이해할 수 있습니다.
  • 애노테이션 기반이라 도구 추가 비용이 낮음: 새 도구 하나를 추가하는 게 “메서드 하나 작성 + 애노테이션 몇 개”로 끝난다면, 앞으로 다른 팀원들이 다른 도메인의 도구를 이어서 추가할 때도 똑같은 패턴만 반복하면 됩니다.
  • 프로토콜 세부사항에서 자유로움: JSON-RPC 메시지 포맷, 세션 관리, 에러 응답 포맷 같은 건 스펙을 따라 정확히 구현해야 클라이언트(LLM 쪽 MCP 클라이언트)와 제대로 통신되는데, 이런 저수준 디테일을 프레임워크가 대신 처리해준다는 게 결정적이었습니다.

결과적으로 build.gradle에 의존성 하나, application.yml에 설정 몇 줄이 이 프로젝트의 MCP 서버 골격 전부였습니다.

spring:
  ai:
    mcp:
      server:
        name: my-mcp-server
        protocol: STREAMABLE
        type: SYNC
        annotation-scanner:
          enabled: true

3. 구현 사례 — Spring AI가 실제로 대신 해주는 것들

여기서부터는 실제로 코드를 짜면서 “아, 이래서 프레임워크를 쓰는구나”를 느꼈던 지점들을 정리했습니다.

3-1. MCP Inspector로 직접 확인해보기

이론적으로 프로토콜이 자동 처리된다고 해도, 실제로 눈으로 확인은 해봐야 안심이 됩니다. 이때 사용할 수 있는것이 MCP Inspector입니다. Anthropic이 공식으로 배포하는 웹 기반 MCP 서버 테스트/디버깅 도구로, 별도 설치 없이 아래 한 줄이면 바로 실행됩니다.

npx @modelcontextprotocol/inspector

위 코드로 실행하면 웹 UI를 통해 개발 중인 tool을 테스트 및 조회할 수 있습니다. Servers 화면에서 Add Servers를 누르고, Transport Type을 Streamable HTTP로 선택한 뒤 개발 중인 서버의 MCP 엔드포인트 주소(예: http://localhost:8088/mcp)를 입력하면 연결됩니다. 연결한 후 Tools 탭에 제가 애노테이션으로 등록해둔 도구 목록이 이름, 설명, 입력 스키마까지 그대로 나타나고, 도구를 하나 골라 파라미터를 채운 뒤 직접 호출해볼 수 있습니다.

이 화면에서 직접 눌러보면서, 프로토콜 레벨에서는 정확히 이런 흐름이 오간다는 걸 확인할 수 있었습니다.

  1. initialize 요청을 보내면 서버가 세션을 만들고 Mcp-Session-Id를 응답 헤더에 실어 돌려줍니다.
  2. 이후 요청은 이 세션 ID를 헤더에 실어 보내면 되고,
  3. tools/list를 호출하면 제가 애노테이션으로 등록해둔 도구 목록이 이름, 설명, 입력 스키마와 함께 자동으로 반환됩니다.
  4. tools/call로 실제 도구를 호출하면, 제가 짠 자바 메서드가 실행되고 반환값이 MCP 응답 포맷(content, isError 등)으로 자동 래핑됩니다.

제가 짠 코드에는 이런 프로토콜 처리 로직이 단 한 줄도 없습니다. 이 부분을 직접 구현했다면 프로젝트 초반 몇 주는 순수하게 “MCP 스펙 재구현”에만 썼을 것 같습니다.

테스트하면서 실제로 헷갈렸던 부분도 하나 있었습니다. 배열(List) 타입 파라미터를 받는 도구를 호출할 때, Inspector 입력창에 값을 그냥 하나 써넣었더니 “integer 발견, array 예상”이라는 스키마 검증 에러가 났습니다. 서버가 내려준 스키마 자체는 array로 정상이었는데, 입력창에 ["value1", "value2"]처럼 JSON 배열로 넣어야 한다는 걸 모르고 있었던 겁니다. 이런 건 문서만 봐서는 알기 어렵고, 직접 도구를 하나하나 호출해보면서야 알게 되는 디테일이었습니다.

3-2. @McpTool / @McpToolParam — 메서드 하나가 도구 하나가 되는 구조

실제로 만든 도구 중 하나를 단순화해서 보면 이렇습니다.

@McpTool(
        name = "findServers",
        description = "서버 목록을 조회합니다. "
                + "이름 부분 문자열로 필터링할 수 있습니다.",
        annotations = @McpTool.McpAnnotations(
                title = "서버 목록 조회",
                readOnlyHint = true,
                destructiveHint = false,
                idempotentHint = true,
                openWorldHint = false
        )
)
public List<GatewaySummary> findGateways(
        @McpToolParam(description = "서버 이름 필터용 문자열", required = false)
        String nameFilter
) {
    // 내부적으로는 서버 관리 서비스의 기존 API 클라이언트를 그대로 호출
}

이 코드가 실행되면 프레임워크가 자동으로 다음을 처리합니다.

  • 메서드 시그니처(String nameFilter)를 분석해서 JSON Schema({"type": "string"})를 자동 생성
  • description은 LLM이 “이 도구를 언제 써야 하는지” 판단하는 근거가 됩니다. 즉 이 설명을 얼마나 구체적으로 쓰느냐가 실제로 LLM이 도구를 잘 골라 쓰는지에 직결됩니다.
  • McpAnnotations의 readOnlyHint, destructiveHint, idempotentHint, openWorldHint는 MCP 스펙에 정의된 “이 도구가 시스템에 어떤 영향을 주는지”에 대한 메타데이터입니다. 저희는 전부 조회 전용 도구라 readOnlyHint=true, destructiveHint=false로 통일했는데, 이런 힌트들은 클라이언트가 “이 도구는 안전하게 반복 호출해도 되는가”를 판단하는 데 쓰입니다.
  • 리턴 타입도 별도 변환 코드 없이 JSON으로 직렬화돼 응답에 담깁니다.

즉 제가 실제로 짠 코드는 “기존 API 클라이언트를 호출해서 결과를 정리하는 평범한 자바 메서드” 가 전부입니다. MCP스러운 부분은 애노테이션 몇 줄뿐이고, 나머지는 원래 Spring 개발자가 익숙한 방식 그대로였습니다.

3-3. 여러 백엔드를 하나의 일관된 구조로 감싸기

사내에는 이미 서버 관리, 장비 관리, 데이터 수집, 알림/모니터링을 각각 담당하는 여러 백엔드 서비스가 있고, 저마다 자기만의 클라이언트 라이브러리를 갖고 있습니다. 이 프로젝트에서는 이걸 그대로 재사용하면서, 도메인별로 폴더를 나눠 도구를 구성했습니다.

tools/
├── server/    ServerTools
├── device/     DeviceModelTools
├── collect/    DataCollectionTools
└── alert/      AlarmTools, ConditionTools

이 구조를 잡아둔 이유는 실용성 때문입니다. 다른 팀원이 나중에 새 도메인의 도구를 추가할 때, “이 폴더 밑에 비슷한 패턴으로 클래스 하나 더 만들면 된다”는 게 바로 보이도록 하고 싶었습니다. Spring AI 쪽은 애노테이션이 붙은 빈(Bean)을 자동으로 스캔해서 도구로 등록해주기 때문에, 이 구조를 얼마나 깔끔하게 짜느냐가 곧 “다음 사람이 얼마나 쉽게 확장할 수 있는가”로 직결됐습니다.

4. 도입 결과 및 향후 과제

결과

  • 서버 관리, 장비 관리, 데이터 수집, 알림/모니터링 4개 도메인에 여러 MCP 도구를 구축했습니다.
  • 각 도구는 MCP Inspector와 curl을 통한 실제 프로토콜 핸드셰이크(initialize → tools/list → tools/call)로 정상 동작을 확인했습니다.
  • 원래 기획서에 있던 핵심 도메인 연동은 클러스터 인프라 관련 항목을 제외하고 모두 마무리한 상태입니다.

상용화를 위해 개선하면 좋을 점

  • 인증: 지금은 MCP 엔드포인트에 별도 인증이 없습니다. 사내망 안에서만 AI 서버가 호출한다는 전제로 우선 개발했지만, 실제 운영 배포 전에는 반드시 붙여야 하는 부분입니다.
  • 확장: 지금 구축한 구조(도메인별 폴더, 클라이언트 라이브러리 재사용 패턴)를 다른 팀원들에게 공유해서, 남은 도메인들을 이어서 확장해나갈 계획입니다.

5. Spring AI, 이럴 때 쓰면 좋습니다 (장단점 정리)

Spring AI로 MCP 서버를 만들어보려는 분들을 위해, 직접 써보면서 느낀 장단점을 정리해봅니다.

장점

  • 보일러플레이트가 거의 없습니다. JSON-RPC 라우팅, 세션 관리, Streamable HTTP 트랜스포트, JSON Schema 생성까지 전부 프레임워크가 처리해줍니다. 개발자는 도구가 될 메서드와 애노테이션만 신경 쓰면 됩니다.
  • 기존 Spring Boot 자산을 그대로 재사용할 수 있습니다. DI, 설정 관리, 이미 만들어둔 클라이언트 라이브러리, 로깅 등 익숙한 방식이 그대로 이어집니다. 새로운 프레임워크를 처음부터 배울 필요가 없습니다.
  • 확장이 쉽습니다. 도구를 하나 늘리는 게 “메서드 하나 + 애노테이션”이라, 여러 명이 나눠서 각자 도메인을 맡아 병렬로 개발하기에도 좋은 구조입니다.

단점

  • 버전이 빠르게 올라갑니다. 그만큼 API가 바뀔 여지도 있다는 뜻이라, 최신 버전을 바로 프로덕션에 쓸 때는 마이너 업그레이드 하나에도 주의가 필요합니다.
  • 프레임워크가 감춰주는 만큼, 저수준을 직접 제어하고 싶을 땐 답답할 수 있습니다. 표준적인 흐름을 벗어난 특수한 요구사항(예: 커스텀 트랜스포트, 프로토콜 확장)이 있다면 오히려 프레임워크의 추상화를 걷어내는 게 더 까다로울 수 있습니다.

결론적으로, “Spring Boot 기반 서비스에서 빠르게 MCP 서버를 세우고 싶다”는 목적이라면 충분하고 진입장벽이 낮으며, 직관적이라 매우 편리하다고 생각합니다.

6. 마치며

이번 프로젝트를 하면서 느낀 건, MCP 서버 개발에서 진짜 어려운 부분은 프로토콜 자체가 아니라는 점이었습니다. 프로토콜은 Spring AI가 대부분 대신 처리해줬습니다. 정작 시간을 많이 쓴 건 “LLM이 이 도구를 잘 골라 쓰려면 이름과 설명을 어떻게 지어야 하는가”, “여러 백엔드 시스템을 어떤 기준으로 나눠서 도구로 노출해야 다른 사람도 헷갈리지 않는가” 같은, 프레임워크가 대신해줄 수 없는 설계 판단들이었습니다.

MCP라는 개념을 알고 있는 것과, 그걸 실제 서비스 구조로 옮기는 건 확실히 다른 경험이었습니다. 이제는 우리 시스템에 새로운 기능이 추가될 때마다 “이걸 AI가 쓸 수 있게 하려면 어떻게 도구로 노출할까”를 자연스럽게 고민하게 됩니다. AI가 사내 시스템에 안전하고 일관된 방식으로 연결되도록 만드는 이런 추상화 계층이, 앞으로 AI 기능이 늘어날수록 더 중요해질 거라고 생각합니다.

sauce0127

Site footer