
Spring AI, JSON-RPC 및 SSE (Server-Sent Events)를 사용한 MCP 서버 구축
요약
Spring AI를 사용하여 JSON-RPC와 SSE 전송 방식을 지원하는 MCP 서버를 구축하는 방법을 설명합니다. MCP 프로토콜의 개념과 함께 비동기 통신을 위한 SSE의 역할 및 아키텍처를 다룹니다.
핵심 포인트
- MCP는 AI 에이전트에게 도구를 노출하는 표준화된 메커니즘임
- JSON-RPC를 통해 경량화된 구조적 요청-응답 처리를 수행함
- SSE를 활용하여 서버에서 클라이언트로의 실시간 스트리밍을 구현함
- Spring AI를 이용한 MCP 서버 아키텍처 구축 가이드 제공
서론 (Introduction):
현대의 LLM (Large Language Model) 기반 애플리케이션은 데이터베이스, API, 클라우드 플랫폼 및 엔터프라이즈 서비스와 같은 실제 시스템과 상호작용하기 위해 외부 도구가 필요합니다. MCP (Model Context Protocol)는 AI 에이전트에게 도구를 노출하기 위한 표준화된 메커니즘을 제공합니다.
이 글에서는 SSE (Server-Sent Events) 전송 지원을 갖춘 Spring AI를 사용하여 MCP 서버를 구축할 것입니다. 또한 JSON-RPC와 SSE (Server-Sent Events)가 어떻게 함께 작동하여 AI 에이전트와 도구 간의 비동기 통신을 가능하게 하는지 이해할 것입니다.
MCP란 무엇인가? (What is MCP?)
MCP (Model Context Protocol)는 도구, 리소스 및 기능을 LLM 기반 애플리케이션에 표준화된 방식으로 노출하도록 설계된 프로토콜입니다.
MCP 서버는 도구 제공자 (Tool Provider) 역할을 하고, MCP 클라이언트는 소비자 (Consumer) 역할을 합니다.
이 프로토콜은 다음을 가능하게 합니다:
- 도구 발견 (Tool discovery)
- 도구 호출 (Tool invocation)
- 비동기 통신 (Asynchronous communication)
- 스트리밍 응답 (Streaming responses)
- 구조화된 요청-응답 처리 (Structured request-response handling)
왜 JSON-RPC인가? (Why JSON-RPC)
MCP (Model Context Protocol)는 통신 프로토콜로 JSON-RPC를 사용합니다.
JSON-RPC는 다음을 제공합니다:
- 경량 원격 프로시저 호출 (Lightweight remote procedure calls)
- 구조화된 요청 ID (Structured request IDs)
- 표준화된 에러 처리 (Standardized error Handling)
- 프로토콜의 단순성 (Protocol simplicity)
- 언어 중립성 (Language neutrality)
요청 예시:
{
"jsonrpc":"2.0",
"id":"101",
"method":"tools/call",
"params":{
"name":"getWeather",
"arguments":{
"city":"Atlanta"
}
}
}
왜 SSE 전송인가? (Why SSE Transport?)
전통적인 HTTP 요청-응답 통신은 오래 지속되는 AI 워크플로우에는 불충분합니다.
SSE (Server-sent Events)는 다음을 가능하게 합니다:
- 지속적인 서버-클라이언트 스트리밍 (Persistent server-to-client streaming)
- 비동기 이벤트 발행 (Asynchronous event publishing)
- 점진적 응답 전달 (Incremental response delivery)
- 실시간 알림 (Real-time notifications)
MCP 아키텍처에서:
- HTTP POST는 JSON-RPC 요청을 제출하는 데 사용됩니다.
- SSE는 응답과 알림을 비동기적으로 스트리밍하는 데 사용됩니다.
Spring AI MCP 서버 아키텍처 (Spring AI MCP Server Architecture) :
⚠️ [IMG:N] 형식 토큰은 이미지 placeholder 입니다. 번역하지 말고 원래 위치에 그대로 유지하세요.
Spring AI MCP 서버 아키텍처 (Spring AI MCP Server Architecture) :
MCP 서버는 다음을 포함합니다:
- Tool Registry (도구 등록소)
- JSON-RPC Dispatcher (JSON-RPC 디스패처)
- Tool Execution Layer (도구 실행 계층)
- SSE Publisher (SSE 발행자)
Spring AI를 사용한 MCP 서버 구현 (Implementing MCP Server Using Spring AI)
폴더 구조
아래 의존성을 pom.xml에 포함하세요.
Spring AI MCP 서버는 내부적으로 어떻게 시작될까요? (What happens internally when Spring AI MCP Server Starts?)
다음 의존성을 고려해 보세요:
org.springframework.ai
spring-ai-starter-mcp-server-webmvc
언뜻 보기에는 간단한 스타터 의존성처럼 보이지만, 내부적으로 Spring Boot는 애플리케이션을 MCP를 준수하는 서버로 변환하기 위해 여러 단계를 수행합니다.
1단계: Spring Boot 시작 (Step 1: Spring Boot Starts)
애플리케이션이 시작될 때: SpringApplication.run(Application.class, args);
Spring Boot는 부트스트랩 프로세스를 시작합니다.
부트스트랩 과정 동안 다음을 스캔합니다:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
모든 스타터 의존성 내부에서.
2단계: MCP 자동 설정 발견 (Step 2: MCP Auto Configuration is Discovered)
The MCP 스타터는 자동 구성 클래스를 제공합니다.
개념적으로: Spring-ai-starter-mcp-server-webmvc는 McpWebMvcServerTransportAutoConfiguration입니다.
Spring Boot는 이 구성을 자동으로 애플리케이션 Context로 가져옵니다.
이 단계에서 Spring은 다음을 위해 필요한 인프라 빈(bean)을 생성합니다:
- MCP 프로토콜
- JSON-RPC 처리
- SSE 전송
- 도구 검색
- 도구 실행
아직 애플리케이션 코드는 실행되지 않았습니다.
3단계: 도구 정의 (Step 3: Defining Tools)
Spring AI MCP 서버는 다음을 사용하여 도구를 노출합니다:
@Tool
@ToolParam
4단계: 도구 등록 (Step 4: Registering Tool)
이 부분이 가장 중요합니다.
Spring AI는 JSON-RPC 요청에서 메서드를 직접 호출하지 않습니다. 대신 발견된 각 도구를 ToolCallback으로 래핑(wrap)합니다.
도구는 MethodToolCallbackProvider를 통해 등록됩니다.
이 도구들은 MCP 도구 레지스트리(Registry)에서 사용할 수 있게 됩니다.
5단계: SSE 엔드포인트 생성 (Step 5: SSE Endpoint is Created)
MCP 자동 설정(auto-configuration)은 SSE 전송(Transport)을 위한 인프라도 생성합니다.
개념적으로는 다음과 같습니다: GET /sse. 이 엔드포인트는 장기 연결(long-lived connections)을 유지합니다.
클라이언트: GET /sse. 연결이 열린 상태로 유지됩니다. Spring은 내부적으로 연결된 클라이언트를 위해 SseEmitter 객체를 생성합니다.
예시: 연결된 클라이언트
클라이언트 A -> SseEmitter
클라이언트 B -> SseEmitter
클라이언트 C -> SseEmitter
이러한 에미터(emitters)들은 이벤트가 발행되어야 할 때마다 유지되고 재사용됩니다.
6단계: JSON-RPC 엔드포인트 생성 (Step 6: JSON-RPC Endpoint is Created)
MCP 스타터(starter)는 또한 POST /mcp/message를 노출합니다. 이 엔드포인트는 JSON-RPC 메시지를 수락합니다.
예시:
{
"jsonrpc":"2.0",
"id":3,
"method":"tools/call",
"params":{
"name":"calculate_discount",
"arguments":{
"originalPrice":100,
"discountPercentage":20
}
}
}
7단계: 요청 도착 (Step 7: Request Arrives)
클라이언트가 POST /mcp/message를 보냅니다. Spring MVC는 요청을 MCP 컨트롤러(Controller)로 디스패치(dispatch)합니다.
개념적으로: DispatcherServlet -> MCP Controller.
MCP 컨트롤러는 method = tools/call, toolName = calculateDiscount, 그리고 arguments = {...}를 파싱(Parse)합니다.
Controller가 Tool Registry를 조회합니다.
개념적으로: ToolCallback callback = registry.find("calculateDiscount");
결과: calculateDiscountCallback.
콜백(callback)은 내부 메서드를 실행합니다.
개념적으로: callback.call(argument)가 내부적으로 calculateDiscount 메서드를 호출하며 Tool 실행이 발생합니다.
단계 8: JSON-RPC 응답 생성
프레임워크가 다음을 구축합니다:
{ "jsonrpc":"2.0", "id":3, "result":{"content":[{"type":"text","text":""Original Price: $100.00, Discount: 20.0%, you Save: $20.00, Final Price: $80.00""}]} }.
참고: id = 3이 유지됩니다. 이 ID는 요청-응답 상관관계(request-response correlation)를 위해 매우 중요합니다.
단계 9: SSE 발행 (SSE Publication)
프레임워크는 원래의 HTTP 요청을 통해 응답을 직접 반환하는 대신, 활성화된 SSE 채널을 통해 응답을 발행합니다.
개념적으로:
SseEmitter.send(responseMessage);
결과:
{ "jsonrpc":"2.0", "id":3, "result":{"content":[{"type":"text","text":""Original Price: $100.00, Discount: 20.0%, you Save: $20.00, Final Price: $80.00""}]} }가 연결된 클라이언트(Client)로 스트리밍됩니다.
단계 10: 클라이언트 이벤트 수신
MCP 클라이언트의 SSE 리스너 스레드(Listener Thread)가 다음을 수신합니다: { "jsonrpc":"2.0", "id":3, "result":{"content":[{"type":"text","text":""Original Price: $100.00, Discount: 20.0%, you Save: $20.00, Final Price: $80.00""}]} }
리스너는
추출합니다: id=3
조회합니다: ConcurrentHashMap< Integer, CompletableFuture> pendingRequest
찾습니다: future = pendingRequest.remove(3)
그 다음: future.complete(response)
대기 중이던 호출자 스레드(caller thread)가 깨어납니다.
실행:
HttpMcpServerApplication.java를 실행하여 Spring Boot 애플리케이션을 부트스트랩(bootstrap)합니다. 이 애플리케이션은 MCP 서버를 호스팅하며 8090 포트에서 대기합니다.
Spring Boot 부트스트랩(bootstrap) 과정 동안, MCP 자동 설정(auto-configuration)은 도구 정의(tool definitions)를 스캔하고, 툴 콜백(tool callbacks)을 생성한 다음, 이를 MCP 서버의 도구 레지스트리(tool registry)에 등록합니다.
MCP 자동 설정은 또한 SSE 전송(SSE Transport)을 위한 인프라를 만듭니다. GET */sse * 엔드포인트는 장기 연결(long-lived connections)을 유지합니다.
MCP 클라이언트가 /sse 엔드포인트에 연결하면, MCP 서버는 서버 전송 이벤트(Server-Sent Events, SSE) 스트림을 설정하고 위 그림에서 볼 수 있듯이 고유 세션 식별자(unique session identifier)를 포함하는 엔드포인트 이벤트를 반환합니다.
클라이언트는 엔드포인트 URL에서 sessionId를 추출하여 다음 모든 HTTP POST 요청에 포함시킵니다. /mcp/message 엔드포인트로 보내는 요청에는 다음이 포함됩니다:
- Initialize
- notifications/initialized
- tools/list
- tools/call
sessionId는 클라이언트 세션을 고유하게 식별하며, MCP 서버가 동일한 클라이언트에 속하는 요청(requests), 응답(responses), 비동기 이벤트(asynchronous events)를 상관관계 분석(correlate)할 수 있도록 합니다.
이 메커니즘은 SSE 전송을 사용할 때 MCP 클라이언트와 MCP 서버 간의 상태 저장 및 비동기 통신 기반을 형성합니다.
MCP 프로토콜 생명 주기(protocol lifecycle)에 따르면, 예상되는 생명 주기는 다음과 같습니다:
- /sse로 연결
- sessionId를 포함하는 엔드포인트 수신
- initialize 전송
- initialize 응답 수신
- notification/initialized 전송
- tools/list 전송 (선택적이지만 일반적임)
- tools/calls 전송
initialize의 목적은 클라이언트(Client)와 서버(Server) 간의 기능(Capabilities) 및 프로토콜 버전(Protocol versions)을 협상하는 것입니다. notification/initialization 메시지는 클라이언트가 초기화(Initialization)를 완료했으며 정상적인 동작을 수행할 준비가 되었음을 서버에 알립니다.
Initialize:
MCP 클라이언트는 JSON-RPC initialize 메시지를 포함하는 HTTP POST 요청을 보냅니다.
요청을 처리하면, MCP 서버는 구축된 /sse 이벤트 스트림(Event stream)을 통해 해당하는 JSON-RPC 응답을 비동기적으로 게시(Publish)합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기







