🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

MCP 서버 구현 WebMVC 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2026. 2. 1. · 8 Views

MCP 서버 구현 WebMVC 완벽 가이드

Spring WebMVC를 활용하여 Model Context Protocol 서버를 구현하는 방법을 단계별로 알아봅니다. AI 에이전트와 통신하는 MCP 서버의 엔드포인트 구성부터 도구 등록, 에러 핸들링까지 실무에 필요한 핵심 내용을 다룹니다.


목차

  1. WebMVC_기반_MCP_서버_설정
  2. 엔드포인트_구성
  3. 요청_응답_핸들링
  4. 도구_등록_및_관리
  5. 에러_핸들링
  6. 실전_MCP_서버_구현

1. WebMVC 기반 MCP 서버 설정

김개발 씨는 최근 회사에서 AI 에이전트 프로젝트를 맡게 되었습니다. 팀장님이 말씀하셨습니다.

"Claude 같은 AI가 우리 시스템의 기능을 직접 호출할 수 있도록 MCP 서버를 구축해줘." MCP라는 단어를 처음 들은 김개발 씨는 어디서부터 시작해야 할지 막막했습니다.

**MCP(Model Context Protocol)**는 AI 모델이 외부 도구나 리소스와 상호작용할 수 있게 해주는 표준 프로토콜입니다. 마치 식당에서 웨이터가 손님과 주방 사이를 연결해주는 것처럼, MCP 서버는 AI 에이전트와 여러분의 백엔드 시스템 사이를 연결합니다.

Spring WebMVC 기반으로 MCP 서버를 구축하면 기존 Spring 생태계의 장점을 그대로 활용할 수 있습니다.

다음 코드를 살펴봅시다.

// build.gradle - MCP 서버 의존성 설정
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'io.modelcontextprotocol:mcp-spring-webmvc:0.9.0'
    implementation 'com.fasterxml.jackson.core:jackson-databind'
}

// McpServerConfig.java - MCP 서버 기본 설정
@Configuration
@EnableWebMvc
public class McpServerConfig {

    @Bean
    public McpServer mcpServer(ToolRegistry toolRegistry) {
        return McpServer.builder()
            .serverInfo("my-mcp-server", "1.0.0")
            .toolRegistry(toolRegistry)
            .build();
    }
}

김개발 씨는 입사 2년 차 백엔드 개발자입니다. 그동안 REST API는 수없이 만들어봤지만, AI 에이전트와 통신하는 서버는 처음입니다.

팀장님의 요청을 받고 일단 검색부터 시작했습니다. "MCP가 뭐지?" 검색 결과를 훑어보던 김개발 씨의 눈이 반짝였습니다.

Model Context Protocol. Anthropic에서 만든 오픈 프로토콜로, AI 모델이 외부 도구를 호출할 수 있게 해주는 표준이었습니다.

그렇다면 MCP 서버란 정확히 무엇일까요? 쉽게 비유하자면, MCP 서버는 마치 통역사와 같습니다.

외국인 관광객이 현지 상점에서 물건을 사고 싶을 때, 통역사가 양쪽의 언어를 번역해주죠. MCP 서버도 마찬가지입니다.

AI 에이전트의 요청을 받아서 여러분의 시스템이 이해할 수 있는 형태로 변환하고, 결과를 다시 AI가 이해할 수 있는 형태로 돌려줍니다. MCP가 없던 시절에는 어땠을까요?

개발자들은 AI 에이전트마다 다른 통신 방식을 구현해야 했습니다. ChatGPT 플러그인 방식, Claude 방식, 자체 구현 방식 등 각각 다른 규격을 맞춰야 했죠.

프로젝트가 커질수록 유지보수는 악몽이 되었습니다. 바로 이런 문제를 해결하기 위해 MCP가 등장했습니다.

표준 프로토콜을 사용하면 한 번의 구현으로 여러 AI 에이전트와 통신할 수 있습니다. Spring WebMVC와 결합하면 기존에 익숙한 방식으로 개발할 수 있다는 장점도 있습니다.

위의 코드를 살펴보겠습니다. 먼저 build.gradle에서 필요한 의존성을 추가합니다.

mcp-spring-webmvc가 핵심 라이브러리입니다. 설정 클래스에서는 @Configuration 어노테이션으로 Spring 설정임을 명시하고, **McpServer.builder()**를 통해 서버 인스턴스를 생성합니다.

serverInfo에는 서버 이름과 버전을 지정합니다. AI 에이전트가 어떤 서버와 통신하는지 식별하는 데 사용됩니다.

toolRegistry는 이 서버가 제공하는 도구들을 등록하는 곳입니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 고객 서비스 챗봇을 개발한다고 가정해봅시다. AI가 "주문 조회", "배송 추적", "환불 요청" 같은 기능을 직접 호출할 수 있도록 MCP 서버를 구축합니다.

한 번 구축해두면 Claude든 GPT든 어떤 AI 에이전트와도 연동할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가봅시다.

기본 설정을 마친 김개발 씨는 이제 엔드포인트를 구성할 차례입니다. "생각보다 어렵지 않네!" 자신감이 붙기 시작했습니다.

실전 팁

💡 - MCP 라이브러리 버전은 공식 문서에서 최신 버전을 확인하세요

  • serverInfo의 버전은 시맨틱 버저닝을 따르는 것이 좋습니다

2. 엔드포인트 구성

기본 설정을 마친 김개발 씨 앞에 새로운 과제가 놓였습니다. 선배 박시니어 씨가 물었습니다.

"MCP 서버가 어떤 URL로 요청을 받을 건데?" 김개발 씨는 잠시 생각에 잠겼습니다. REST API처럼 여러 엔드포인트가 필요한 걸까, 아니면 다른 방식일까?

MCP 서버의 엔드포인트는 AI 에이전트가 요청을 보내는 진입점입니다. 마치 회사의 대표 전화번호처럼, 하나의 엔드포인트로 모든 요청을 받고 내부에서 적절한 처리기로 라우팅합니다.

Spring WebMVC에서는 @RestController@PostMapping을 활용하여 MCP 엔드포인트를 구성합니다.

다음 코드를 살펴봅시다.

@RestController
@RequestMapping("/mcp")
public class McpEndpointController {

    private final McpServer mcpServer;

    public McpEndpointController(McpServer mcpServer) {
        this.mcpServer = mcpServer;
    }

    // MCP 메시지를 받는 단일 엔드포인트
    @PostMapping("/message")
    public ResponseEntity<McpResponse> handleMessage(
            @RequestBody McpRequest request) {
        McpResponse response = mcpServer.processRequest(request);
        return ResponseEntity.ok(response);
    }

    // 서버 정보 조회 엔드포인트
    @GetMapping("/info")
    public ResponseEntity<ServerInfo> getServerInfo() {
        return ResponseEntity.ok(mcpServer.getServerInfo());
    }
}

박시니어 씨의 질문에 김개발 씨는 REST API를 떠올렸습니다. "주문 조회는 /orders, 배송 추적은 /tracking...

이렇게 여러 개 만들어야 하나요?" 박시니어 씨가 고개를 저었습니다. "MCP는 조금 달라.

하나의 문으로 들어와서 내부에서 분기하는 방식이야." 그렇다면 MCP 엔드포인트는 어떻게 동작할까요? 쉽게 비유하자면, MCP 엔드포인트는 백화점의 안내 데스크와 같습니다.

손님이 무엇을 원하든 일단 안내 데스크로 옵니다. "화장품 코너 어디예요?", "식당가는 몇 층이에요?", "주차 정산은 어디서 해요?" 안내 데스크 직원이 요청을 듣고 적절한 곳으로 안내해주죠.

MCP 서버도 마찬가지입니다. /mcp/message라는 단일 엔드포인트로 모든 요청이 들어옵니다.

요청 내용을 분석해서 "이건 도구 호출이네", "이건 리소스 조회네" 하고 내부에서 적절한 처리기로 연결합니다. 왜 이런 방식을 선택했을까요?

여러 엔드포인트를 관리하는 것보다 단일 엔드포인트가 AI 에이전트 입장에서 훨씬 단순합니다. AI는 복잡한 라우팅 로직을 신경 쓸 필요 없이 한 곳으로만 요청을 보내면 됩니다.

프로토콜의 일관성도 유지됩니다. 위의 코드를 자세히 살펴보겠습니다.

**@RequestMapping("/mcp")**로 기본 경로를 설정합니다. 모든 MCP 관련 요청은 /mcp 아래로 들어옵니다.

핵심은 handleMessage 메서드입니다. **@PostMapping("/message")**로 POST 요청을 받고, 요청 본문을 McpRequest 객체로 자동 변환합니다.

**mcpServer.processRequest(request)**가 실제 처리를 담당합니다. 요청 내용에 따라 도구를 호출하거나, 리소스를 조회하거나, 필요한 작업을 수행합니다.

결과는 McpResponse 형태로 반환됩니다. /info 엔드포인트는 부가적인 것입니다.

AI 에이전트가 이 서버가 어떤 서버인지, 어떤 기능을 제공하는지 확인할 때 사용합니다. 실무에서 주의할 점이 있습니다.

MCP 엔드포인트는 인증과 권한 검사가 필수입니다. 아무나 호출할 수 있으면 보안 문제가 생기겠죠.

Spring Security를 연동하여 API 키 검증이나 OAuth 인증을 추가하는 것이 좋습니다. 김개발 씨가 물었습니다.

"그럼 AI가 보내는 요청은 어떻게 생겼어요?" 박시니어 씨가 웃으며 대답했습니다. "다음으로 요청과 응답 구조를 살펴보자."

실전 팁

💡 - 엔드포인트 경로는 /mcp 같이 명확하게 구분하세요

  • 프로덕션에서는 반드시 인증 로직을 추가하세요

3. 요청 응답 핸들링

엔드포인트를 만들었으니 이제 실제로 요청이 어떻게 생겼는지 알아야 합니다. 김개발 씨가 Postman으로 테스트 요청을 보내보았지만 400 에러만 돌아왔습니다.

"요청 형식이 뭔가 잘못된 것 같은데..." MCP 프로토콜의 요청과 응답 구조를 제대로 이해해야 할 때입니다.

MCP의 요청과 응답은 JSON-RPC 2.0 형식을 따릅니다. 마치 편지의 봉투와 내용물처럼, 메타 정보를 담은 껍데기와 실제 데이터를 담은 본문으로 구성됩니다.

Spring에서는 이를 객체로 매핑하여 타입 안전하게 처리할 수 있습니다.

다음 코드를 살펴봅시다.

// MCP 요청 객체 정의
public record McpRequest(
    String jsonrpc,      // 항상 "2.0"
    String method,       // 호출할 메서드명 (예: "tools/call")
    @JsonInclude(NON_NULL) Object params,  // 메서드 파라미터
    String id            // 요청 식별자
) {}

// MCP 응답 객체 정의
public record McpResponse(
    String jsonrpc,
    String id,
    @JsonInclude(NON_NULL) Object result,
    @JsonInclude(NON_NULL) McpError error
) {
    // 성공 응답 생성 팩토리 메서드
    public static McpResponse success(String id, Object result) {
        return new McpResponse("2.0", id, result, null);
    }

    // 에러 응답 생성 팩토리 메서드
    public static McpResponse error(String id, McpError error) {
        return new McpResponse("2.0", id, null, error);
    }
}

김개발 씨는 400 에러 메시지를 자세히 읽어보았습니다. "Invalid JSON-RPC format".

JSON-RPC라는 단어가 눈에 들어왔습니다. "JSON-RPC가 뭐예요?" 김개발 씨가 박시니어 씨에게 물었습니다.

박시니어 씨가 설명을 시작했습니다. "JSON-RPC는 JSON을 사용한 원격 프로시저 호출 프로토콜이야.

REST가 리소스 중심이라면, JSON-RPC는 동작 중심이지." 좀 더 쉽게 비유해보겠습니다. REST API가 도서관에서 책을 찾는 것이라면, JSON-RPC는 사서에게 부탁하는 것과 같습니다.

REST에서는 "3번 서가의 5번째 책을 주세요"라고 위치를 지정합니다. JSON-RPC에서는 "해리포터 첫 번째 책 찾아주세요"라고 원하는 동작을 말합니다.

MCP 요청의 구조를 살펴보겠습니다. jsonrpc 필드는 항상 "2.0"입니다.

프로토콜 버전을 명시합니다. method 필드가 핵심입니다.

"tools/call"이면 도구 호출, "tools/list"면 도구 목록 조회입니다. params에는 메서드에 필요한 파라미터가 들어갑니다.

id는 요청을 식별하는 값으로, 응답과 매칭할 때 사용합니다. 응답 구조도 비슷합니다.

성공하면 result에 결과가 담기고, 실패하면 error에 에러 정보가 담깁니다. 둘 중 하나만 존재합니다.

@JsonInclude(NON_NULL) 어노테이션으로 null인 필드는 JSON에서 제외합니다. Java의 record를 사용한 점도 주목할 만합니다.

record는 불변 데이터 클래스를 간결하게 정의하는 Java 14+ 기능입니다. getter, equals, hashCode, toString이 자동 생성됩니다.

요청과 응답처럼 데이터를 담기만 하는 객체에 딱 맞습니다. 팩토리 메서드 패턴도 활용했습니다.

**McpResponse.success()**와 **McpResponse.error()**를 통해 응답 생성을 표준화했습니다. 개발자가 실수로 result와 error를 동시에 설정하는 일을 방지할 수 있습니다.

김개발 씨가 테스트 요청을 수정했습니다. jsonrpc, method, id를 제대로 넣으니 드디어 200 응답이 돌아왔습니다.

"이제 요청 형식은 알겠어요. 그런데 도구는 어떻게 등록하는 거예요?"

실전 팁

💡 - JSON-RPC의 id는 문자열 또는 숫자 모두 가능합니다

  • 응답 생성 시 팩토리 메서드를 활용하면 실수를 줄일 수 있습니다

4. 도구 등록 및 관리

요청과 응답 구조를 이해한 김개발 씨는 이제 핵심 기능에 도전합니다. AI 에이전트가 실제로 호출할 수 있는 도구를 만들어야 합니다.

"주문 조회 기능을 도구로 등록하려면 어떻게 해야 하지?" 선배의 코드를 살펴보니 @Tool 어노테이션이 눈에 들어왔습니다.

MCP의 **도구(Tool)**는 AI 에이전트가 호출할 수 있는 기능 단위입니다. 마치 스위스 아미 나이프의 각 도구처럼, 칼, 가위, 드라이버가 각각 다른 기능을 수행하듯 MCP 도구도 각자의 역할이 있습니다.

Spring에서는 어노테이션 기반으로 도구를 선언하고 자동으로 등록할 수 있습니다.

다음 코드를 살펴봅시다.

@Service
public class OrderTools {

    private final OrderRepository orderRepository;

    // 도구 정의 - AI가 호출할 수 있는 기능
    @Tool(name = "getOrderStatus",
          description = "주문 번호로 주문 상태를 조회합니다")
    public ToolResult getOrderStatus(
            @ToolParam(description = "조회할 주문 번호")
            String orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));

        return ToolResult.builder()
            .content(Map.of(
                "orderId", order.getId(),
                "status", order.getStatus(),
                "updatedAt", order.getUpdatedAt()
            ))
            .build();
    }

    @Tool(name = "cancelOrder",
          description = "주문을 취소합니다. 배송 전 상태만 가능합니다")
    public ToolResult cancelOrder(
            @ToolParam(description = "취소할 주문 번호") String orderId,
            @ToolParam(description = "취소 사유") String reason) {
        // 주문 취소 로직
        orderRepository.cancel(orderId, reason);
        return ToolResult.success("주문이 취소되었습니다");
    }
}

김개발 씨는 선배의 코드를 보며 감탄했습니다. "어노테이션 하나로 도구가 등록되네요!" 박시니어 씨가 설명을 이었습니다.

"그래, Spring의 장점이 여기서 빛을 발하지. 복잡한 설정 없이 어노테이션만으로 도구를 선언할 수 있어." 도구 등록의 핵심은 무엇일까요?

@Tool 어노테이션의 description이 가장 중요합니다. AI 에이전트는 이 설명을 읽고 어떤 상황에서 이 도구를 사용할지 판단합니다.

"주문 번호로 주문 상태를 조회합니다"라는 설명을 보고, 사용자가 "내 주문 어디까지 왔어?"라고 물으면 이 도구를 호출하는 것입니다. 비유하자면, 도구의 description은 제품 설명서와 같습니다.

설명서가 부실하면 제품을 제대로 사용할 수 없듯이, description이 부실하면 AI도 도구를 제대로 활용할 수 없습니다. "주문 조회"보다 "주문 번호로 주문 상태를 조회합니다"가 훨씬 명확하죠.

@ToolParam도 마찬가지입니다. 파라미터에 붙는 이 어노테이션은 각 입력값이 무엇인지 설명합니다.

AI가 사용자의 자연어를 분석해서 적절한 파라미터를 추출하는 데 이 정보를 활용합니다. 코드를 더 자세히 살펴보겠습니다.

getOrderStatus 도구는 주문 ID를 받아서 상태를 반환합니다. 내부적으로는 일반적인 Spring 서비스 로직과 동일합니다.

Repository에서 데이터를 조회하고, 결과를 ToolResult로 감싸서 반환합니다. ToolResult는 도구 실행 결과를 담는 표준 형식입니다.

content에 실제 데이터를 Map이나 객체 형태로 담습니다. 성공/실패 여부, 에러 메시지 등의 메타 정보도 포함할 수 있습니다.

AI 에이전트는 이 결과를 받아서 사용자에게 자연스러운 언어로 전달합니다. cancelOrder처럼 부수 효과가 있는 도구도 만들 수 있습니다.

단, 주의가 필요합니다. AI가 실수로 호출할 수 있으니, description에 "배송 전 상태만 가능합니다"처럼 제약 조건을 명시하는 것이 좋습니다.

김개발 씨가 질문했습니다. "도구에서 에러가 나면 어떻게 되나요?" 박시니어 씨가 대답했습니다.

"좋은 질문이야. 에러 핸들링을 제대로 해야 AI가 사용자에게 적절한 안내를 할 수 있거든."

실전 팁

💡 - description은 AI가 이해할 수 있도록 명확하고 구체적으로 작성하세요

  • 부수 효과가 있는 도구는 description에 주의사항을 명시하세요

5. 에러 핸들링

김개발 씨가 만든 주문 조회 도구를 테스트하던 중 문제가 생겼습니다. 존재하지 않는 주문 번호를 입력하자 500 에러가 발생한 것입니다.

AI 에이전트는 "서버 오류가 발생했습니다"라는 무미건조한 메시지만 사용자에게 전달했습니다. "이래선 안 되겠는데..."

MCP 서버의 에러 핸들링은 AI 에이전트가 사용자에게 적절한 피드백을 줄 수 있게 하는 핵심 요소입니다. 마치 고객 서비스 직원이 "죄송합니다, 해당 상품은 품절입니다"라고 구체적으로 안내하는 것처럼, MCP 에러도 명확한 코드와 메시지를 담아야 합니다.

다음 코드를 살펴봅시다.

// MCP 에러 코드 상수 정의
public class McpErrorCode {
    public static final int PARSE_ERROR = -32700;
    public static final int INVALID_REQUEST = -32600;
    public static final int METHOD_NOT_FOUND = -32601;
    public static final int INVALID_PARAMS = -32602;
    public static final int INTERNAL_ERROR = -32603;
    // 커스텀 에러 코드 (애플리케이션 정의)
    public static final int RESOURCE_NOT_FOUND = -32000;
    public static final int PERMISSION_DENIED = -32001;
}

// 전역 예외 핸들러
@RestControllerAdvice
public class McpExceptionHandler {

    @ExceptionHandler(OrderNotFoundException.class)
    public ResponseEntity<McpResponse> handleOrderNotFound(
            OrderNotFoundException ex, WebRequest request) {
        McpError error = new McpError(
            McpErrorCode.RESOURCE_NOT_FOUND,
            "주문을 찾을 수 없습니다: " + ex.getOrderId(),
            Map.of("orderId", ex.getOrderId())
        );
        String requestId = extractRequestId(request);
        return ResponseEntity.ok(McpResponse.error(requestId, error));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<McpResponse> handleGeneral(Exception ex) {
        McpError error = new McpError(
            McpErrorCode.INTERNAL_ERROR,
            "서버 내부 오류가 발생했습니다"
        );
        return ResponseEntity.ok(McpResponse.error(null, error));
    }
}

500 에러를 본 김개발 씨는 당황했습니다. 분명 예외 처리를 했는데 왜 이런 결과가 나온 걸까요?

박시니어 씨가 코드를 살펴보며 말했습니다. "예외는 던졌는데, MCP 형식으로 변환하는 과정이 빠졌네.

MCP는 HTTP 상태 코드가 아니라 자체 에러 형식을 사용해." MCP 에러의 핵심은 무엇일까요? JSON-RPC 2.0 스펙에 따르면, 에러는 code, message, data 세 가지 필드로 구성됩니다.

code는 숫자로 된 에러 유형, message는 사람이 읽을 수 있는 설명, data는 추가 정보입니다. 비유하자면, 이것은 병원의 진단서와 같습니다.

"아프다"고만 하면 치료할 수 없습니다. "왼쪽 무릎 인대 손상, 2주 휴식 필요"처럼 구체적인 진단이 있어야 적절한 조치를 취할 수 있죠.

MCP 에러도 마찬가지입니다. 표준 에러 코드를 살펴보겠습니다.

-32700부터 -32603까지는 JSON-RPC 표준 에러입니다. 파싱 에러, 잘못된 요청, 메서드 없음 등 프로토콜 수준의 문제를 나타냅니다.

-32000부터는 애플리케이션이 정의하는 커스텀 에러입니다. 주문 없음, 권한 없음 같은 비즈니스 로직 에러를 여기에 정의합니다.

@RestControllerAdvice를 활용한 전역 예외 핸들러가 핵심입니다. Spring의 예외 처리 메커니즘을 그대로 활용하면서, 예외를 MCP 형식으로 변환합니다.

OrderNotFoundException이 발생하면 RESOURCE_NOT_FOUND 코드와 함께 구체적인 메시지를 반환합니다. 한 가지 주의할 점이 있습니다.

MCP에서는 에러가 발생해도 HTTP 상태 코드는 200을 반환합니다. 에러 정보는 응답 본문의 error 필드에 담깁니다.

HTTP 4xx, 5xx를 반환하면 AI 에이전트가 제대로 처리하지 못할 수 있습니다. 김개발 씨가 에러 핸들러를 추가하고 다시 테스트했습니다.

이번에는 AI가 "해당 주문 번호를 찾을 수 없습니다. 주문 번호를 다시 확인해주세요"라고 친절하게 안내했습니다.

"이제야 제대로 된 것 같아요!"

실전 팁

💡 - MCP 에러는 HTTP 200과 함께 응답 본문에 담아야 합니다

  • 커스텀 에러 코드는 -32000 이하의 숫자를 사용하세요

6. 실전 MCP 서버 구현

개별 개념들을 익힌 김개발 씨는 이제 전체를 조립할 차례입니다. 팀장님이 말씀하셨습니다.

"테스트 환경에 배포해서 실제로 AI 에이전트와 연동해봐." 지금까지 배운 모든 것을 하나로 엮어 완전한 MCP 서버를 구현해야 합니다.

실전 MCP 서버는 설정, 엔드포인트, 도구, 에러 핸들링이 유기적으로 결합된 형태입니다. 마치 오케스트라의 여러 악기가 조화롭게 연주하듯, 각 구성 요소가 협력하여 AI 에이전트의 요청을 처리합니다.

여기에 로깅, 인증, 검증 같은 프로덕션 필수 요소도 추가해야 합니다.

다음 코드를 살펴봅시다.

@SpringBootApplication
public class McpServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(McpServerApplication.class, args);
    }
}

// 완전한 MCP 서버 설정
@Configuration
public class McpConfiguration {

    @Bean
    public McpServer mcpServer(List<Object> toolBeans) {
        ToolRegistry registry = new ToolRegistry();
        // @Tool 어노테이션이 붙은 메서드 자동 스캔
        toolBeans.forEach(registry::registerToolsFrom);

        return McpServer.builder()
            .serverInfo("order-service-mcp", "1.0.0")
            .toolRegistry(registry)
            .capabilities(Capabilities.builder()
                .tools(true)
                .resources(false)
                .build())
            .build();
    }

    @Bean
    public List<Object> toolBeans(OrderTools orderTools,
                                   ShippingTools shippingTools) {
        return List.of(orderTools, shippingTools);
    }
}

// 인증 필터 추가
@Component
public class McpAuthFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain) {
        String apiKey = request.getHeader("X-MCP-API-Key");
        if (!isValidApiKey(apiKey)) {
            response.setStatus(401);
            return;
        }
        chain.doFilter(request, response);
    }
}

김개발 씨는 지금까지 만든 코드 조각들을 하나로 모았습니다. 설정 클래스, 엔드포인트 컨트롤러, 도구 클래스, 에러 핸들러.

퍼즐 조각을 맞추듯 하나씩 연결해나갔습니다. 박시니어 씨가 코드를 리뷰하며 조언했습니다.

"구조는 좋아. 그런데 프로덕션에 배포하려면 몇 가지 더 필요해." 실전 배포에 필요한 것은 무엇일까요?

첫째, 인증입니다. MCP 엔드포인트를 아무나 호출할 수 있으면 안 됩니다.

API 키 검증이나 OAuth 토큰 검증을 추가해야 합니다. 위 코드의 McpAuthFilter가 이 역할을 합니다.

둘째, 도구 자동 등록입니다. 도구 클래스가 늘어날 때마다 수동으로 등록하면 실수하기 쉽습니다.

@Tool 어노테이션이 붙은 메서드를 자동으로 스캔하여 등록하는 방식이 유지보수에 유리합니다. 셋째, capabilities 선언입니다.

이 서버가 어떤 기능을 지원하는지 AI 에이전트에게 알려줍니다. **tools(true)**는 도구 호출 지원, **resources(false)**는 리소스 조회 미지원을 의미합니다.

AI 에이전트는 이 정보를 보고 무엇을 요청할 수 있는지 판단합니다. 전체 흐름을 정리해보겠습니다.

AI 에이전트가 /mcp/message로 요청을 보냅니다. McpAuthFilter가 API 키를 검증합니다.

통과하면 McpEndpointController가 요청을 받아 McpServer에 전달합니다. McpServer는 요청의 method를 분석하여 적절한 도구를 찾고 실행합니다.

결과는 ToolResult로 반환되고, 최종적으로 McpResponse 형태로 AI에게 돌아갑니다. 에러가 발생하면 어떻게 될까요?

도구 실행 중 예외가 발생하면 McpExceptionHandler가 잡아서 MCP 에러 형식으로 변환합니다. AI 에이전트는 이 에러 정보를 받아 사용자에게 적절한 안내를 합니다.

김개발 씨가 테스트 환경에 배포를 완료했습니다. Claude 에이전트와 연동 테스트를 진행했습니다.

"내 주문 상태 알려줘"라는 사용자 요청에 AI가 MCP 서버의 getOrderStatus 도구를 호출하고, 결과를 자연스러운 한국어로 전달했습니다. 팀장님이 웃으며 말했습니다.

"잘했어. 이제 프로덕션 배포 준비하자." 김개발 씨는 뿌듯했습니다.

처음엔 막막했던 MCP 서버 구현이 이제는 손에 잡히는 것 같았습니다. 앞으로 더 많은 도구를 추가하고, 더 복잡한 AI 연동 시나리오를 다뤄볼 수 있을 것 같습니다.

실전 팁

💡 - 프로덕션에서는 반드시 인증과 rate limiting을 적용하세요

  • 도구가 많아지면 도메인별로 클래스를 분리하여 관리하세요
  • 로깅을 추가하여 AI의 도구 호출 패턴을 분석하면 서비스 개선에 도움이 됩니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Spring#MCP#WebMVC#AI통신#ToolRegistry#Spring,AI,MCP

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.