Java SDK로 MCP 서버 만들기
Java SDK로 MCP 서버 만들기
MCP 서버는 MCP 아키텍처에서 클라이언트에게 도구와 리소스, 역량을 제공하는 핵심 구성 요소예요. Java SDK의 서버는 프로토콜의 서버 쪽을 구현해서, 클라이언트가 발견하고 실행할 수 있는 도구를 노출하고 URI 기반으로 리소스를 관리하는 일을 담당해요.
개요
MCP 서버가 하는 일을 정리하면 이렇습니다:
- 클라이언트가 발견하고 실행할 수 있는 도구 노출
- URI 기반 접근 패턴과 리소스 템플릿으로 리소스 관리
- 프롬프트 템플릿 제공 및 프롬프트 요청 처리
- 클라이언트와의 역량 협상 지원
- 인자 자동완성 제안(completions) 제공
- 서버 쪽 프로토콜 연산 구현
- 동시 클라이언트 연결 관리
- 구조화된 로깅과 알림 제공
팁 — 코어
io.modelcontextprotocol.sdk:mcp모듈은 외부 웹 프레임워크 없이도 STDIO, SSE, Streamable HTTP 서버 트랜스포트 구현을 제공해요. Spring 전용 트랜스포트 구현(mcp-spring-webflux,mcp-spring-webmvc)은 이제 Spring AI 2.0+(grouporg.springframework.ai)에 있고, 이 SDK에서는 더 이상 배포되지 않아요. Spring 기반 서버 설정은 MCP Server Boot Starter 문서를 참고하세요.
서버는 동기/비동기 두 가지 API를 모두 지원해서 서로 다른 애플리케이션 컨텍스트에 유연하게 통합할 수 있어요.
동기 API:
// Create a server with custom configuration
McpSyncServer syncServer = McpServer.sync(transportProvider)
.serverInfo("my-server", "1.0.0")
.capabilities(ServerCapabilities.builder()
.resources(false, true) // Resource support: subscribe=false, listChanged=true
.tools(true) // Enable tool support with list changes
.prompts(true) // Enable prompt support with list changes
.completions() // Enable completions support
.logging() // Enable logging support
.build())
.build();
// Register tools, resources, and prompts
syncServer.addTool(syncToolSpecification);
syncServer.addResource(syncResourceSpecification);
syncServer.addPrompt(syncPromptSpecification);
// Close the server when done
syncServer.close();
비동기 API:
// Create an async server with custom configuration
McpAsyncServer asyncServer = McpServer.async(transportProvider)
.serverInfo("my-server", "1.0.0")
.capabilities(ServerCapabilities.builder()
.resources(false, true) // Resource support: subscribe=false, listChanged=true
.tools(true) // Enable tool support with list changes
.prompts(true) // Enable prompt support with list changes
.completions() // Enable completions support
.logging() // Enable logging support
.build())
.build();
// Register tools, resources, and prompts
asyncServer.addTool(asyncToolSpecification)
.doOnSuccess(v -> logger.info("Tool registered"))
.subscribe();
asyncServer.addResource(asyncResourceSpecification)
.doOnSuccess(v -> logger.info("Resource registered"))
.subscribe();
asyncServer.addPrompt(asyncPromptSpecification)
.doOnSuccess(v -> logger.info("Prompt registered"))
.subscribe();
// Close the server when done
asyncServer.close()
.doOnSuccess(v -> logger.info("Server closed"))
.subscribe();
서버 유형
트랜스포트 요구사항에 따라 서버를 만드는 여러 패턴을 지원해요:
// Single-session server with SSE transport provider
McpSyncServer server = McpServer.sync(sseTransportProvider).build();
// Streamable HTTP server
McpSyncServer server = McpServer.sync(streamableTransportProvider).build();
// Stateless server (no session management)
McpSyncServer server = McpServer.sync(statelessTransport).build();
서버 트랜스포트 프로바이더
MCP SDK의 트랜스포트 레이어는 클라이언트와 서버 사이의 통신을 담당해요. 다양한 통신 프로토콜과 패턴을 지원하는 구현들이 내장되어 있습니다.
STDIO
stdin/stdout을 이용한 프로세스 기반 트랜스포트를 만듭니다:
StdioServerTransportProvider transportProvider =
new StdioServerTransportProvider(McpJsonDefaults.getMapper());
표준 입출력 스트림을 통한 양방향 JSON-RPC 메시지 처리를 제공하며, 비블로킹 메시지 처리와 직렬화/역직렬화, 우아한 종료를 지원해요.
주요 특징:
- stdin/stdout을 통한 양방향 통신
- 프로세스 기반 통합 지원
- 간단한 설정과 구성
- 가벼운 구현
Streamable HTTP
Streamable HTTP 서블릿/WebFlux/WebMvc 세 가지 구현이 각각 있어요.
Servlet 기반(코어 mcp 모듈에 포함):
HttpServletStreamableServerTransportProvider transportProvider =
HttpServletStreamableServerTransportProvider.builder()
.jsonMapper(jsonMapper)
.mcpEndpoint("/mcp")
.build();
Spring Web 애플리케이션에서 쓰려면 Servlet 빈으로 등록하면 돼요:
@Configuration
@EnableWebMvc
public class McpServerConfig implements WebMvcConfigurer {
@Bean
public HttpServletStreamableServerTransportProvider transportProvider(McpJsonMapper jsonMapper) {
return HttpServletStreamableServerTransportProvider.builder()
.jsonMapper(jsonMapper)
.mcpEndpoint("/mcp")
.build();
}
@Bean
public ServletRegistrationBean<?> mcpServlet(
HttpServletStreamableServerTransportProvider transportProvider) {
return new ServletRegistrationBean<>(transportProvider);
}
}
주요 특징: 효율적인 양방향 HTTP 통신, 여러 클라이언트 연결을 위한 세션 관리, 설정 가능한 keep-alive 간격, 보안 검증, 우아한 종료.
WebFlux 기반(mcp-spring-webflux 의존성 필요):
@Configuration
class McpConfig {
@Bean
WebFluxStreamableServerTransportProvider transportProvider(McpJsonMapper jsonMapper) {
return WebFluxStreamableServerTransportProvider.builder()
.jsonMapper(jsonMapper)
.messageEndpoint("/mcp")
.build();
}
@Bean
RouterFunction<?> mcpRouterFunction(
WebFluxStreamableServerTransportProvider transportProvider) {
return transportProvider.getRouterFunction();
}
}
주요 특징: WebFlux를 이용한 리액티브 HTTP 스트리밍, 동시 클라이언트 연결, 설정 가능한 keep-alive 간격, 보안 검증.
WebMvc 기반(mcp-spring-webmvc 의존성 필요):
@Configuration
@EnableWebMvc
class McpConfig {
@Bean
WebMvcStreamableServerTransportProvider transportProvider(McpJsonMapper jsonMapper) {
return WebMvcStreamableServerTransportProvider.builder()
.jsonMapper(jsonMapper)
.mcpEndpoint("/mcp")
.build();
}
@Bean
RouterFunction<ServerResponse> mcpRouterFunction(
WebMvcStreamableServerTransportProvider transportProvider) {
return transportProvider.getRouterFunction();
}
}
SSE HTTP (Legacy)
SSE 서버 트랜스포트도 Servlet/WebFlux/WebMvc 세 가지가 있어요.
Servlet 기반(코어 mcp 모듈에 포함, 어떤 Servlet 컨테이너에서도 사용 가능):
@Configuration
@EnableWebMvc
public class McpServerConfig implements WebMvcConfigurer {
@Bean
public HttpServletSseServerTransportProvider servletSseServerTransportProvider() {
return HttpServletSseServerTransportProvider.builder()
.messageEndpoint("/mcp/message")
.build();
}
@Bean
public ServletRegistrationBean<?> customServletBean(
HttpServletSseServerTransportProvider transportProvider) {
return new ServletRegistrationBean<>(transportProvider);
}
}
전통적인 Servlet API를 사용해 MCP HTTP with SSE 트랜스포트를 구현하며, Servlet 6.0 async 지원을 통한 비동기 메시지 처리와 두 종류의 엔드포인트(서버→클라이언트 이벤트용 /sse, 클라이언트→서버 요청용 message 엔드포인트)를 제공해요.
WebFlux 기반:
@Configuration
class McpConfig {
@Bean
WebFluxSseServerTransportProvider webFluxSseServerTransportProvider(ObjectMapper mapper) {
return new WebFluxSseServerTransportProvider(mapper, "/mcp/message");
}
@Bean
RouterFunction<?> mcpRouterFunction(WebFluxSseServerTransportProvider transportProvider) {
return transportProvider.getRouterFunction();
}
}
WebMvc 기반:
@Configuration
@EnableWebMvc
class McpConfig {
@Bean
WebMvcSseServerTransportProvider webMvcSseServerTransportProvider(ObjectMapper mapper) {
return new WebMvcSseServerTransportProvider(mapper, "/mcp/message");
}
@Bean
RouterFunction<ServerResponse> mcpRouterFunction(
WebMvcSseServerTransportProvider transportProvider) {
return transportProvider.getRouterFunction();
}
}
서버 역량 (Capabilities)
서버는 다양한 역량으로 구성할 수 있어요:
var capabilities = ServerCapabilities.builder()
.resources(true, true) // Resource support: subscribe=true, listChanged=true
.tools(true) // Tool support with list changes notifications
.prompts(true) // Prompt support with list changes notifications
.completions() // Enable completions support
.logging() // Enable logging support
.build();
도구 명세 (Tool Specification)
MCP는 서버가 언어 모델에 의해 호출될 수 있는 도구를 노출하도록 허용해요. Java SDK는 핸들러 함수와 함께 도구 명세를 구현할 수 있게 해주며, 도구 덕분에 AI 모델이 계산을 수행하고 외부 API에 접근하고 데이터베이스를 조회하며 파일을 조작할 수 있어요.
권장하는 방식은 빌더 패턴을 쓰고 CallToolRequest를 핸들러 인자로 받는 거예요.
동기:
// Sync tool specification using builder
var syncToolSpecification = SyncToolSpecification.builder()
.tool(Tool.builder("calculator", schema)
.description("Basic calculator")
.build())
.callHandler((exchange, request) -> {
// Access arguments via request.arguments()
String operation = (String) request.arguments().get("operation");
int a = (int) request.arguments().get("a");
int b = (int) request.arguments().get("b");
// Tool implementation
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent("Result: " + result)))
.build();
})
.build();
비동기:
// Async tool specification using builder
var asyncToolSpecification = AsyncToolSpecification.builder()
.tool(Tool.builder("calculator", schema)
.description("Basic calculator")
.build())
.callHandler((exchange, request) -> {
// Access arguments via request.arguments()
String operation = (String) request.arguments().get("operation");
int a = (int) request.arguments().get("a");
int b = (int) request.arguments().get("b");
// Tool implementation
return Mono.just(CallToolResult.builder()
.content(List.of(new McpSchema.TextContent("Result: " + result)))
.build());
})
.build();
도구 명세는 name, description, inputSchema를 가진 Tool 정의와 도구의 로직을 구현하는 call handler로 구성돼요.
로깅 지원
서버는 심각도가 다른 로그 메시지를 클라이언트로 보낼 수 있는 구조화된 로깅 능력을 제공해요. 로그 알림은 도구/리소스/프롬프트 호출처럼 기존 클라이언트 세션 안에서만 보낼 수 있어요.
var tool = AsyncToolSpecification.builder()
.tool(Tool.builder("logging-test", emptyJsonSchema).description("Test logging notifications").build())
.callHandler((exchange, request) ->
exchange.loggingNotification( // Use the exchange to send log messages
McpSchema.LoggingMessageNotification.builder(McpSchema.LoggingLevel.DEBUG, "Debug message")
.logger("test-logger")
.build())
.then(Mono.just(CallToolResult.builder()
.content(List.of(new McpSchema.TextContent("Logging test completed")))
.build())))
.build();
var mcpServer = McpServer.async(mcpServerTransportProvider)
.serverInfo("test-server", "1.0.0")
.capabilities(
ServerCapabilities.builder()
.logging() // Enable logging support
.tools(true)
.build())
.tools(tool)
.build();
클라이언트 쪽에서는 logging consumer를 등록해 서버의 로그 메시지를 받아볼 수 있어요:
var mcpClient = McpClient.sync(transport)
.loggingConsumer(notification -> {
System.out.println("Received log message: " + notification.data());
})
.build();
mcpClient.initialize();
mcpClient.setLoggingLevel(McpSchema.LoggingLevel.INFO);
클라이언트는 mcpClient.setLoggingLevel(level) 요청으로 받는 최소 로그 레벨을 제어할 수 있어요. 설정된 레벨보다 낮은 메시지는 걸러집니다. 지원되는 로그 레벨(심각도 증가 순): DEBUG (0), INFO (1), NOTICE (2), WARNING (3), ERROR (4), CRITICAL (5), ALERT (6), EMERGENCY (7)
에러 처리
SDK는 McpError 클래스를 통해 프로토콜 호환성, 트랜스포트 통신, JSON-RPC 메시징, 도구 실행, 리소스 관리, 프롬프트 처리, 타임아웃, 연결 문제까지 포괄적으로 처리해요. 동기/비동기 연산 어디서든 일관되고 안정적인 에러 관리를 보장합니다.
도구 구현에서의 에러 처리
에러의 두 계층
MCP는 도구 실행에서 두 종류의 에러를 구분해요.
1. 도구 레벨 에러 (LLM이 복구 가능)
검증 실패, 누락된 인자, LLM이 보고 재시도할 수 있는 도메인 에러에는 CallToolResult를 isError(true)로 설정해 반환해요.
// Example: Domain validation failure (e.g., invalid email format)
if (!emailAddress.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
return CallToolResult.builder()
.content(List.of(new McpSchema.TextContent("Invalid argument: 'email' must be a valid email address.")))
.isError(true)
.build();
}
LLM은 이 응답을 일반적인 도구 응답의 일부로 받아서 이후 상호작용에서 스스로 수정할 수 있어요.
2. 프로토콜 레벨 에러 (복구 불가)
도구 핸들러에서 잡지 못한 예외는 JSON-RPC 에러 응답으로 매핑돼요. 진짜로 예상치 못한 실패(예: DB 타임아웃 같은 인프라 에러)에만 사용하고, 입력 검증에는 쓰지 마세요.
// This propagates as a JSON-RPC error — use sparingly
throw new McpError(McpSchema.ErrorCodes.INTERNAL_ERROR, "Unexpected failure");
결정 가이드
| 상황 | 접근 |
|---|---|
| 도메인 검증 실패 | CallToolResult를 isError=true로 |
| 인프라 / 예상치 못한 에러 | McpError를 던지거나 그대로 전파 |
| 경고와 함께 부분 성공 | text에 경고를 담은 CallToolResult |
더 알아보기 (Learn more)
- Java SDK 퀵스타트 — 의존성 설치하기
- MCP 서버 부트 스타터 문서 — Spring 기반 서버 구성