MCP 서버 어노테이션
MCP 서버 어노테이션 (MCP Server Annotations)
MCP 서버 어노테이션은 Java 어노테이션을 사용해 MCP 서버 기능을 선언적으로 구현할 수 있는 방법을 제공해요. 도구, 리소스, 프롬프트, 완성(completion) 핸들러를 만드는 일을 단순화해 주는데, 서버 타입별 메서드 필터링과 비동기 지원까지 잘 정리돼 있어요. 이 글에서 하나씩 살펴볼게요.
출처: 문서
본문
MCP 서버 어노테이션 (MCP Server Annotations)
MCP Server Annotations는 Java 어노테이션을 사용해 MCP 서버 기능을 선언적으로 구현하는 방법을 제공해요. 이 어노테이션들은 도구, 리소스, 프롬프트, 완성 핸들러의 생성을 단순화해요.
서버 어노테이션 (Server Annotations)
@McpTool
@McpTool 어노테이션은 메서드를 자동 JSON 스키마 생성이 포함된 MCP 도구 구현으로 표시해요.
기본 사용법 (Basic Usage)
@Component
public class CalculatorTools {
@McpTool(name = "add", description = "Add two numbers together")
public int add(
@McpToolParam(description = "First number", required = true) int a,
@McpToolParam(description = "Second number", required = true) int b) {
return a + b;
}
}
어노테이션 속성 (Annotation Attributes)
@McpTool 어노테이션은 다음 속성을 지원해요:
| Attribute | Default | Description |
|---|---|---|
name |
메서드 이름 | 도구 식별자. 제공하지 않으면 메서드 이름으로 기본값이 정해져요. |
description |
메서드 이름 | 도구에 대한 사람이 읽을 수 있는 설명. |
title |
"" |
UI와 최종 사용자 맥락을 위한 것 — 사람이 읽기 쉽게 최적화됨. 제공하지 않으면 표시에 name이 사용돼요. (우선순위: annotations.title > title > name) |
generateOutputSchema |
false |
true면 비-원시 반환 타입에 대해 JSON 출력 스키마를 자동 생성해요. |
annotations |
@McpAnnotations |
클라이언트를 위한 추가 힌트 (아래 도구 어노테이션 참고). |
metaProvider |
DefaultMetaProvider.class |
도구 선언의 _meta 필드 데이터를 공급하는 MetaProvider를 구현한 클래스. |
도구 어노테이션 (힌트) (Tool Annotations)
@McpTool(name = "calculate-area",
description = "Calculate the area of a rectangle",
title = "Rectangle Area Calculator",
generateOutputSchema = true,
annotations = @McpTool.McpAnnotations(
title = "Rectangle Area Calculator",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true
))
public AreaResult calculateRectangleArea(
@McpToolParam(description = "Width", required = true) double width,
@McpToolParam(description = "Height", required = true) double height) {
return new AreaResult(width * height, "square units");
}
중첩된 McpAnnotations 어노테이션은 클라이언트 힌트를 제공해요:
| Hint | Default | Description |
|---|---|---|
title |
"" |
도구에 대한 사람이 읽을 수 있는 제목. |
readOnlyHint |
false |
true면 도구가 주변 환경을 수정하지 않아요. |
destructiveHint |
true |
true면 도구가 파괴적인 업데이트를 수행할 수 있어요 (readOnlyHint == false일 때만 의미 있음). |
idempotentHint |
false |
true면 같은 인자로 호출해도 추가 효과가 없어요 (readOnlyHint == false일 때만 의미 있음). |
openWorldHint |
true |
true면 도구가 외부 엔티티와 상호작용할 수 있어요 (예: 웹 검색). false면 도메인이 닫혀 있어요. |
요청 컨텍스트와 함께 (With Request Context)
도구는 고급 작업을 위해 요청 컨텍스트에 접근할 수 있어요:
@McpTool(name = "process-data", description = "Process data with request context")
public String processData(
McpSyncRequestContext context,
@McpToolParam(description = "Data to process", required = true) String data) {
// Send logging notification
context.info("Processing data: " + data);
// Send progress notification (using convenient method)
context.progress(p -> p.progress(0.5).total(1.0).message("Processing..."));
// Ping the client
context.ping();
return "Processed: " + data.toUpperCase();
}
동적 스키마 지원 (Dynamic Schema Support)
도구는 런타임 스키마 처리를 위해 CallToolRequest를 받을 수 있어요:
@McpTool(name = "flexible-tool", description = "Process dynamic schema")
public CallToolResult processDynamic(CallToolRequest request) {
Map<String, Object> args = request.arguments();
// Process based on runtime schema
String result = "Processed " + args.size() + " arguments dynamically";
return CallToolResult.builder()
.addTextContent(result)
.build();
}
진행 추적 (Progress Tracking)
도구는 장기 실행 작업 추적을 위해 진행 토큰을 받을 수 있어요:
@McpTool(name = "long-task", description = "Long-running task with progress")
public String performLongTask(
McpSyncRequestContext context,
@McpToolParam(description = "Task name", required = true) String taskName) {
// Access progress token from context
String progressToken = context.request().progressToken();
if (progressToken != null) {
context.progress(p -> p.progress(0.0).total(1.0).message("Starting task"));
// Perform work...
context.progress(p -> p.progress(1.0).total(1.0).message("Task completed"));
}
return "Task " + taskName + " completed";
}
예외 처리 (Exception Handling)
@McpTool 메서드가 던지는 예외는 일반 Spring AI @Tool 메서드와 동일한 계약을 따르요.
RuntimeException은 에러CallToolResult로 변환되어 모델에 전달되고, 모델이 이에 대해 추론하거나 재시도할 수 있어요.- 선언된 체크 예외나
Error는 그대로 올라가 도구 호출을 실패시키고, 모델에는 전달되지 않아요.
즉, 실패가 모델에 도달하지 않게 하고 싶다면 도구 메서드 시그니처에 체크 예외를 선언하고 던지면 돼요.
@McpTool(name = "read-file", description = "Read a file from disk")
public String readFile(@McpToolParam(description = "Path", required = true) String path) throws IOException {
// An IOException bubbles up and fails the tool call instead of being
// sent back to the model as an error result.
return Files.readString(Path.of(path));
}
반면 RuntimeException은 에러 결과로 모델에 보고되어 복구할 수 있게 해 줘요.
@McpTool(name = "divide", description = "Divide two numbers")
public double divide(int a, int b) {
if (b == 0) {
// Conveyed to the model as an error result.
throw new IllegalArgumentException("Cannot divide by zero");
}
return (double) a / b;
}
참고: 프로토콜 수준의
McpError예외(예: URL elicitation과 기타 MCP 프로토콜 흐름에 사용되는 것들)는 에러CallToolResult로 변환되는 대신 항상 MCP 프로토콜 계층으로 전파돼요. 이는McpError를 직접 던지든 반응형 반환 타입(Mono나Flux)으로 내보내든 동일하게 적용돼요.
@McpResource
@McpResource 어노테이션은 URI 템플릿을 통해 리소스에 접근을 제공해요.
어노테이션 속성 (Annotation Attributes)
| Attribute | Default | Description |
|---|---|---|
uri |
"" |
리소스의 URI (또는 URI 템플릿). 템플릿 변수에는 {varName}을 사용해요. |
name |
"" |
프로그래밍 식별자. title이 없을 때 표시 이름으로도 사용돼요. |
title |
"" |
표시 목적의 선택적 사람이 읽을 수 있는 이름. |
description |
"" |
리소스가 무엇을 나타내는지에 대한 설명. |
mimeType |
"text/plain" |
리소스 콘텐츠의 MIME 타입. |
metaProvider |
DefaultMetaProvider.class |
_meta 필드 데이터를 공급하는 MetaProvider를 구현한 클래스. |
annotations |
@McpAnnotations(…) |
audience, priority, last-modified 메타데이터를 위한 클라이언트 어노테이션. |
리소스용 중첩 McpAnnotations는 다음을 지원해요:
| Attribute | Default | Description |
|---|---|---|
audience |
{Role.USER} |
의도된 소비자를 설명해요 (Role.USER, Role.ASSISTANT, 또는 둘 다). |
priority |
0.5 |
0.0(가장 낮음)부터 1.0(가장 높음)까지의 중요도. 1.0은 사실상 필수임을 의미해요. |
lastModified |
"" |
리소스가 마지막으로 수정된 ISO 8601 날짜-시간. |
기본 사용법 (Basic Usage)
@Component
public class ResourceProvider {
@McpResource(
uri = "config://{key}",
name = "Configuration",
title = "App Configuration",
description = "Provides configuration data")
public String getConfig(String key) {
return configData.get(key);
}
}
ReadResourceResult와 함께 (With ReadResourceResult)
@McpResource(
uri = "user-profile://{username}",
name = "User Profile",
description = "Provides user profile information")
public ReadResourceResult getUserProfile(String username) {
String profileData = loadUserProfile(username);
return ReadResourceResult.builder(List.of(
TextResourceContents.builder(
"user-profile://" + username,
profileData).mimeType("application/json").build()
)).build();
}
요청 컨텍스트와 함께 (With Request Context)
@McpResource(
uri = "data://{id}",
name = "Data Resource",
description = "Resource with request context")
public ReadResourceResult getData(
McpSyncRequestContext context,
String id) {
// Send logging notification using convenient method
context.info("Accessing resource: " + id);
// Ping the client
context.ping();
String data = fetchData(id);
return ReadResourceResult.builder(List.of(
TextResourceContents.builder("data://" + id, data).mimeType("text/plain").build()
)).build();
}
@McpPrompt
@McpPrompt 어노테이션은 AI 상호작용을 위한 프롬프트 메시지를 생성해요.
어노테이션 속성 (Annotation Attributes)
| Attribute | Default | Description |
|---|---|---|
name |
"" |
프롬프트의 고유 식별자. |
title |
"" |
표시 목적의 선택적 사람이 읽을 수 있는 이름. |
description |
"" |
선택적 사람이 읽을 수 있는 설명. |
metaProvider |
DefaultMetaProvider.class |
_meta 필드 데이터를 공급하는 MetaProvider를 구현한 클래스. |
기본 사용법 (Basic Usage)
@Component
public class PromptProvider {
@McpPrompt(
name = "greeting",
description = "Generate a greeting message")
public GetPromptResult greeting(
@McpArg(name = "name", description = "User's name", required = true)
String name) {
String message = "Hello, " + name + "! How can I help you today?";
return GetPromptResult.builder(List.of(new PromptMessage(Role.ASSISTANT, TextContent.builder(message).build())))
.description("Greeting")
.build();
}
}
선택 인자와 함께 (With Optional Arguments)
@McpPrompt(
name = "personalized-message",
description = "Generate a personalized message")
public GetPromptResult personalizedMessage(
@McpArg(name = "name", required = true) String name,
@McpArg(name = "age", required = false) Integer age,
@McpArg(name = "interests", required = false) String interests) {
StringBuilder message = new StringBuilder();
message.append("Hello, ").append(name).append("!\n\n");
if (age != null) {
message.append("At ").append(age).append(" years old, ");
// Add age-specific content
}
if (interests != null && !interests.isEmpty()) {
message.append("Your interest in ").append(interests);
// Add interest-specific content
}
return GetPromptResult.builder(List.of(new PromptMessage(Role.ASSISTANT, TextContent.builder(message.toString()).build())))
.description("Personalized Message")
.build();
}
@McpComplete
@McpComplete 어노테이션은 프롬프트와 리소스 URI 템플릿에 대한 자동 완성 기능을 제공해요.
prompt 또는 uri 속성 중 하나만 사용하세요 — 둘을 동시에 쓰지 마세요:
prompt— 이름이 지정된 프롬프트의 인자를 완성해요.uri— 이름이 지정된 리소스 URI의 URI 템플릿 표현식을 완성해요.
프롬프트 인자 완성 (Prompt Argument Completion)
@Component
public class CompletionProvider {
@McpComplete(prompt = "city-search")
public List<String> completeCityName(String prefix) {
return cities.stream()
.filter(city -> city.toLowerCase().startsWith(prefix.toLowerCase()))
.limit(10)
.toList();
}
}
리소스 URI 완성 (Resource URI Completion)
@McpComplete(uri = "config://{key}")
public List<String> completeConfigKey(String prefix) {
return configKeys.stream()
.filter(key -> key.startsWith(prefix))
.limit(10)
.toList();
}
CompleteRequest.CompleteArgument와 함께 (With CompleteRequest.CompleteArgument)
@McpComplete(prompt = "travel-planner")
public List<String> completeTravelDestination(CompleteRequest.CompleteArgument argument) {
String prefix = argument.value().toLowerCase();
String argumentName = argument.name();
// Different completions based on argument name
if ("city".equals(argumentName)) {
return completeCities(prefix);
} else if ("country".equals(argumentName)) {
return completeCountries(prefix);
}
return List.of();
}
CompleteResult와 함께 (With CompleteResult)
@McpComplete(prompt = "code-completion")
public CompleteResult completeCode(String prefix) {
List<String> completions = generateCodeCompletions(prefix);
return new CompleteResult(
new CompleteResult.CompleteCompletion(
completions,
completions.size(), // total
hasMoreCompletions // hasMore flag
)
);
}
상태 없는(Stateless) vs 상태 유지(Stateful) 구현
통합 요청 컨텍스트 (권장) (Unified Request Context)
상태 유지와 상태 없는 작업 모두에서 동작하는 통합 인터페이스를 위해 McpSyncRequestContext나 McpAsyncRequestContext를 사용하세요:
public record UserInfo(String name, String email, int age) {}
@McpTool(name = "unified-tool", description = "Tool with unified request context")
public String unifiedTool(
McpSyncRequestContext context,
@McpToolParam(description = "Input", required = true) String input) {
// Access request and metadata
String progressToken = context.request().progressToken();
// Logging with convenient methods
context.info("Processing: " + input);
// Progress notifications (Note client should set a progress token
// with its request to be able to receive progress updates)
context.progress(50); // Simple percentage
// Ping client
context.ping();
// Check capabilities before using
if (context.elicitEnabled()) {
// Request user input (only in stateful mode)
StructuredElicitResult<UserInfo> elicitResult = context.elicit(UserInfo.class);
if (elicitResult.action() == ElicitResult.Action.ACCEPT) {
// Use elicited data
}
}
if (context.sampleEnabled()) {
// Request LLM sampling (only in stateful mode)
CreateMessageResult samplingResult = context.sample("Generate response");
// Use sampling result
}
// Access root directories (only in stateful mode)
if (context.rootsEnabled()) {
ListRootsResult roots = context.roots();
roots.roots().forEach(root -> context.info("Root: " + root.uri()));
}
return "Processed with unified context";
}
간단한 작업 (컨텍스트 없음) (Simple Operations)
간단한 작업은 컨텍스트 파라미터를 완전히 생략할 수 있어요:
@McpTool(name = "simple-add", description = "Simple addition")
public int simpleAdd(
@McpToolParam(description = "First number", required = true) int a,
@McpToolParam(description = "Second number", required = true) int b) {
return a + b;
}
가벼운 상태 없음 (McpTransportContext 포함) (Lightweight Stateless)
최소한의 전송 컨텍스트가 필요한 상태 없는 작업의 경우:
@McpTool(name = "stateless-tool", description = "Stateless with transport context")
public String statelessTool(
McpTransportContext context,
@McpToolParam(description = "Input", required = true) String input) {
// Access transport-level context only
// No bidirectional operations (roots, elicitation, sampling)
return "Processed: " + input;
}
참고: 상태 없는 서버는 양방향 작업을 지원하지 않아요. 따라서 상태 없는 모드에서
McpSyncRequestContext나McpAsyncRequestContext를 사용하는 메서드는 무시돼요.
서버 타입별 메서드 필터링 (Method Filtering by Server Type)
MCP annotations 프레임워크는 서버 타입과 메서드 특성에 따라 어노테이션된 메서드를 자동으로 필터링해요. 이를 통해 각 서버 구성에 적절한 메서드만 등록되도록 보장해요. 필터링된 각 메서드에 대해서는 디버깅에 도움이 되도록 경고가 로깅돼요.
동기 vs 비동기 필터링 (Synchronous vs Asynchronous Filtering)
동기 서버 (Synchronous Servers)
동기 서버(spring.ai.mcp.server.type=SYNC로 구성)는 다음 작업을 하는 동기 provider를 사용해요:
-
수락 비-반응형 반환 타입의 메서드:
- 원시 타입 (
int,double,boolean) - 객체 타입 (
String,Integer, 커스텀 POJO) - MCP 타입 (
CallToolResult,ReadResourceResult,GetPromptResult,CompleteResult) - 컬렉션 (
List<String>,Map<String, Object>)
- 원시 타입 (
-
필터링 반응형 반환 타입의 메서드:
Mono<T>Flux<T>Publisher<T>
@Component
public class SyncTools {
@McpTool(name = "sync-tool", description = "Synchronous tool")
public String syncTool(String input) {
// This method WILL be registered on sync servers
return "Processed: " + input;
}
@McpTool(name = "async-tool", description = "Async tool")
public Mono<String> asyncTool(String input) {
// This method will be FILTERED OUT on sync servers
// A warning will be logged
return Mono.just("Processed: " + input);
}
}
비동기 서버 (Asynchronous Servers)
비동기 서버(spring.ai.mcp.server.type=ASYNC로 구성)는 다음 작업을 하는 비동기 provider를 사용해요:
-
수락 반응형 반환 타입의 메서드:
Mono<T>(단일 결과용)Flux<T>(스트리밍 결과용)Publisher<T>(일반 반응형 타입)
-
필터링 비-반응형 반환 타입의 메서드:
- 원시 타입
- 객체 타입
- 컬렉션
- MCP 결과 타입
@Component
public class AsyncTools {
@McpTool(name = "async-tool", description = "Async tool")
public Mono<String> asyncTool(String input) {
// This method WILL be registered on async servers
return Mono.just("Processed: " + input);
}
@McpTool(name = "sync-tool", description = "Sync tool")
public String syncTool(String input) {
// This method will be FILTERED OUT on async servers
// A warning will be logged
return "Processed: " + input;
}
}
상태 유지 vs 상태 없음 필터링 (Stateful vs Stateless Filtering)
상태 유지 서버 (Stateful Servers)
상태 유지 서버는 양방향 통신을 지원하고 다음을 가진 메서드를 수락해요:
-
양방향 컨텍스트 파라미터:
McpSyncRequestContext(동기 작업용)McpAsyncRequestContext(비동기 작업용)McpSyncServerExchange(레거시, 동기 작업용)McpAsyncServerExchange(레거시, 비동기 작업용)
-
양방향 작업 지원:
roots()- 루트 디렉터리 접근elicit()- 사용자 입력 요청sample()- LLM 샘플링 요청
@Component
public class StatefulTools {
@McpTool(name = "interactive-tool", description = "Tool with bidirectional operations")
public String interactiveTool(
McpSyncRequestContext context,
@McpToolParam(description = "Input", required = true) String input) {
// This method WILL be registered on stateful servers
// Can use elicitation, sampling, roots
if (context.sampleEnabled()) {
var samplingResult = context.sample("Generate response");
// Process sampling result...
}
return "Processed with context";
}
}
상태 없는 서버 (Stateless Servers)
상태 없는 서버는 단순한 요청-응답 패턴에 최적화되어 있고:
-
필터링 양방향 컨텍스트 파라미터를 가진 메서드:
McpSyncRequestContext를 가진 메서드는 건너뜀McpAsyncRequestContext를 가진 메서드는 건너뜀McpSyncServerExchange를 가진 메서드는 건너뜀McpAsyncServerExchange를 가진 메서드는 건너뜀- 필터링된 각 메서드에 대해 경고가 로깅됨
-
수락 다음을 가진 메서드:
McpTransportContext(가벼운 상태 없는 컨텍스트)- 컨텍스트 파라미터가 전혀 없음
- 일반
@McpToolParam파라미터만
-
양방향 작업을 지원하지 않음:
roots()- 사용 불가elicit()- 사용 불가sample()- 사용 불가
@Component
public class StatelessTools {
@McpTool(name = "simple-tool", description = "Simple stateless tool")
public String simpleTool(@McpToolParam(description = "Input") String input) {
// This method WILL be registered on stateless servers
return "Processed: " + input;
}
@McpTool(name = "context-tool", description = "Tool with transport context")
public String contextTool(
McpTransportContext context,
@McpToolParam(description = "Input") String input) {
// This method WILL be registered on stateless servers
return "Processed: " + input;
}
@McpTool(name = "bidirectional-tool", description = "Tool with bidirectional context")
public String bidirectionalTool(
McpSyncRequestContext context,
@McpToolParam(description = "Input") String input) {
// This method will be FILTERED OUT on stateless servers
// A warning will be logged
return "Processed with sampling";
}
}
필터링 요약 (Filtering Summary)
| 서버 타입 (Server Type) | 수락되는 메서드 (Accepted Methods) | 필터링되는 메서드 (Filtered Methods) |
|---|---|---|
| Sync Stateful | 비-반응형 반환 + 양방향 컨텍스트 | 반응형 반환 (Mono/Flux) |
| Async Stateful | 반응형 반환 (Mono/Flux) + 양방향 컨텍스트 | 비-반응형 반환 |
| Sync Stateless | 비-반응형 반환 + 양방향 컨텍스트 없음 | 반응형 반환 OR 양방향 컨텍스트 파라미터 |
| Async Stateless | 반응형 반환 (Mono/Flux) + 양방향 컨텍스트 없음 | 비-반응형 반환 OR 양방향 컨텍스트 파라미터 |
참고: 메서드 필터링 모범 사례:
- 메서드를 서버 타입에 맞추세요 - 동기 서버에는 동기 메서드, 비동기 서버에는 비동기 메서드를 사용하세요.
- 상태 유지와 상태 없는 구현을 분리해서 명확성을 위해 다른 클래스에 두세요.
- 시작 시 로그를 확인해서 필터링된 메서드 경고를 살펴보세요.
- 올바른 컨텍스트를 사용하세요 - 상태 유지에는
McpSyncRequestContext/McpAsyncRequestContext, 상태 없음에는McpTransportContext.- 두 모드를 모두 테스트하세요 - 상태 유지와 상태 없는 배포를 모두 지원한다면.
비동기 지원 (Async Support)
모든 서버 어노테이션은 Reactor를 사용한 비동기 구현을 지원해요:
@Component
public class AsyncTools {
@McpTool(name = "async-fetch", description = "Fetch data asynchronously")
public Mono<String> asyncFetch(
@McpToolParam(description = "URL", required = true) String url) {
return Mono.fromCallable(() -> {
// Simulate async operation
return fetchFromUrl(url);
}).subscribeOn(Schedulers.boundedElastic());
}
@McpResource(uri = "async-data://{id}", name = "Async Data")
public Mono<ReadResourceResult> asyncResource(String id) {
return Mono.fromCallable(() -> {
String data = loadData(id);
return ReadResourceResult.builder(List.of(
TextResourceContents.builder("async-data://" + id, data).mimeType("text/plain").build()
)).build();
}).delayElements(Duration.ofMillis(100));
}
}
Spring Boot 통합 (Spring Boot Integration)
Spring Boot 자동 설정을 사용하면 어노테이션된 빈이 자동으로 감지되고 등록돼요:
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}
@Component
public class MyMcpTools {
// Your @McpTool annotated methods
}
@Component
public class MyMcpResources {
// Your @McpResource annotated methods
}
자동 설정은 다음을 수행해요:
- MCP 어노테이션이 있는 빈을 스캔.
- 적절한 명세를 생성.
- MCP 서버에 등록.
- 구성에 따라 동기·비동기 구현을 모두 처리.
구성 프로퍼티 (Configuration Properties)
서버 어노테이션 스캐너를 구성해요:
spring:
ai:
mcp:
server:
type: SYNC # or ASYNC
annotation-scanner:
enabled: true