MCP 클라이언트 애노테이션
MCP 클라이언트 애노테이션 (MCP Client Annotations)
MCP Client Annotations는 Java 애노테이션을 사용해 MCP 클라이언트 핸들러를 선언적으로 구현하는 방법을 제공해요. 서버 알림과 클라이언트 측 연산 처리를 간소화하죠. 이 글에서는 @McpLogging, @McpSampling, @McpElicitation, @McpProgress, @McpToolListChanged, @McpResourceListChanged, @McpPromptListChanged 애노테이션을 하나씩 살펴볼게요.
출처: 문서
본문
MCP Client Annotations는 Java 애노테이션을 사용해 MCP 클라이언트 핸들러를 구현하는 선언적 방법을 제공해요. 이 애노테이션들은 서버 알림과 클라이언트 측 연산의 처리를 간소화해요.
| __ | 모든 MCP 클라이언트 애노테이션은 반드시 clients 매개변수를 포함해서 핸들러를 특정 MCP 클라이언트 연결과 연결해야 해요. clients는 애플리케이션 프로퍼티에 구성된 연결 이름과 일치해야 해요. |
|---|
Client Annotations
@McpLogging
@McpLogging 애노테이션은 MCP 서버의 로깅 메시지 알림을 처리해요.
Basic Usage
@Component
public class LoggingHandler {
@McpLogging(clients = "my-mcp-server")
public void handleLoggingMessage(LoggingMessageNotification notification) {
System.out.println("Received log: " + notification.level() +
" - " + notification.data());
}
}
With Individual Parameters
@McpLogging(clients = "my-mcp-server")
public void handleLoggingWithParams(LoggingLevel level, String logger, String data) {
System.out.println(String.format("[%s] %s: %s", level, logger, data));
}
@McpSampling
@McpSampling 애노테이션은 LLM 완성을 위해 MCP 서버로부터의 샘플링 요청을 처리해요.
Synchronous Implementation
@Component
public class SamplingHandler {
@McpSampling(clients = "llm-server")
public CreateMessageResult handleSamplingRequest(CreateMessageRequest request) {
// Process the request and generate a response
String response = generateLLMResponse(request);
return CreateMessageResult.builder(Role.ASSISTANT, response, "gpt-4")
.build();
}
}
Asynchronous Implementation
@Component
public class AsyncSamplingHandler {
@McpSampling(clients = "llm-server")
public Mono<CreateMessageResult> handleAsyncSampling(CreateMessageRequest request) {
return Mono.fromCallable(() -> {
String response = generateLLMResponse(request);
return CreateMessageResult.builder(Role.ASSISTANT, response, "gpt-4")
.build();
}).subscribeOn(Schedulers.boundedElastic());
}
}
@McpElicitation
@McpElicitation 애노테이션은 사용자로부터 추가 정보를 수집하기 위한 elicitation 요청을 처리해요.
Basic Usage
@Component
public class ElicitationHandler {
@McpElicitation(clients = "interactive-server")
public ElicitResult handleElicitationRequest(ElicitRequest request) {
// Present the request to the user and gather input
Map<String, Object> userData = presentFormToUser(request.requestedSchema());
if (userData != null) {
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
} else {
return new ElicitResult(ElicitResult.Action.DECLINE, null);
}
}
}
With User Interaction
@McpElicitation(clients = "interactive-server")
public ElicitResult handleInteractiveElicitation(ElicitRequest request) {
Map<String, Object> schema = request.requestedSchema();
Map<String, Object> userData = new HashMap<>();
// Check what information is being requested
if (schema != null && schema.containsKey("properties")) {
Map<String, Object> properties = (Map<String, Object>) schema.get("properties");
// Gather user input based on schema
if (properties.containsKey("name")) {
userData.put("name", promptUser("Enter your name:"));
}
if (properties.containsKey("email")) {
userData.put("email", promptUser("Enter your email:"));
}
if (properties.containsKey("preferences")) {
userData.put("preferences", gatherPreferences());
}
}
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
}
Async Elicitation
@McpElicitation(clients = "interactive-server")
public Mono<ElicitResult> handleAsyncElicitation(ElicitRequest request) {
return Mono.fromCallable(() -> {
// Async user interaction
Map<String, Object> userData = asyncGatherUserInput(request);
return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
}).timeout(Duration.ofSeconds(30))
.onErrorReturn(new ElicitResult(ElicitResult.Action.CANCEL, null));
}
@McpProgress
@McpProgress 애노테이션은 장기 실행 연산의 진행 상황 알림을 처리해요.
Basic Usage
@Component
public class ProgressHandler {
@McpProgress(clients = "my-mcp-server")
public void handleProgressNotification(ProgressNotification notification) {
double percentage = notification.progress() * 100;
System.out.println(String.format("Progress: %.2f%% - %s",
percentage, notification.message()));
}
}
With Individual Parameters
@McpProgress(clients = "my-mcp-server")
public void handleProgressWithDetails(
String progressToken,
double progress,
Double total,
String message) {
if (total != null) {
System.out.println(String.format("[%s] %.0f/%.0f - %s",
progressToken, progress, total, message));
} else {
System.out.println(String.format("[%s] %.2f%% - %s",
progressToken, progress * 100, message));
}
// Update UI progress bar
updateProgressBar(progressToken, progress);
}
Client-Specific Progress
@McpProgress(clients = "long-running-server")
public void handleLongRunningProgress(ProgressNotification notification) {
// Track progress for specific server
progressTracker.update("long-running-server", notification);
// Send notifications if needed
if (notification.progress() >= 1.0) {
notifyCompletion(notification.progressToken());
}
}
@McpToolListChanged
@McpToolListChanged 애노테이션은 서버의 툴 목록이 변경될 때의 알림을 처리해요.
Basic Usage
@Component
public class ToolListChangedHandler {
@McpToolListChanged(clients = "tool-server")
public void handleToolListChanged(List<McpSchema.Tool> updatedTools) {
System.out.println("Tool list updated: " + updatedTools.size() + " tools available");
// Update local tool registry
toolRegistry.updateTools(updatedTools);
// Log new tools
for (McpSchema.Tool tool : updatedTools) {
System.out.println(" - " + tool.name() + ": " + tool.description());
}
}
}
Async Handling
@McpToolListChanged(clients = "tool-server")
public Mono<Void> handleAsyncToolListChanged(List<McpSchema.Tool> updatedTools) {
return Mono.fromRunnable(() -> {
// Process tool list update asynchronously
processToolListUpdate(updatedTools);
// Notify interested components
eventBus.publish(new ToolListUpdatedEvent(updatedTools));
}).then();
}
Client-Specific Tool Updates
@McpToolListChanged(clients = "dynamic-server")
public void handleDynamicServerToolUpdate(List<McpSchema.Tool> updatedTools) {
// Handle tools from a specific server that frequently changes its tools
dynamicToolManager.updateServerTools("dynamic-server", updatedTools);
// Re-evaluate tool availability
reevaluateToolCapabilities();
}
@McpResourceListChanged
@McpResourceListChanged 애노테이션은 서버의 리소스 목록이 변경될 때의 알림을 처리해요.
Basic Usage
@Component
public class ResourceListChangedHandler {
@McpResourceListChanged(clients = "resource-server")
public void handleResourceListChanged(List<McpSchema.Resource> updatedResources) {
System.out.println("Resources updated: " + updatedResources.size());
// Update resource cache
resourceCache.clear();
for (McpSchema.Resource resource : updatedResources) {
resourceCache.register(resource);
}
}
}
With Resource Analysis
@McpResourceListChanged(clients = "resource-server")
public void analyzeResourceChanges(List<McpSchema.Resource> updatedResources) {
// Analyze what changed
Set<String> newUris = updatedResources.stream()
.map(McpSchema.Resource::uri)
.collect(Collectors.toSet());
Set<String> removedUris = previousUris.stream()
.filter(uri -> !newUris.contains(uri))
.collect(Collectors.toSet());
if (!removedUris.isEmpty()) {
handleRemovedResources(removedUris);
}
// Update tracking
previousUris = newUris;
}
@McpPromptListChanged
@McpPromptListChanged 애노테이션은 서버의 프롬프트 목록이 변경될 때의 알림을 처리해요.
Basic Usage
@Component
public class PromptListChangedHandler {
@McpPromptListChanged(clients = "prompt-server")
public void handlePromptListChanged(List<McpSchema.Prompt> updatedPrompts) {
System.out.println("Prompts updated: " + updatedPrompts.size());
// Update prompt catalog
promptCatalog.updatePrompts(updatedPrompts);
// Refresh UI if needed
if (uiController != null) {
uiController.refreshPromptList(updatedPrompts);
}
}
}
Async Processing
@McpPromptListChanged(clients = "prompt-server")
public Mono<Void> handleAsyncPromptUpdate(List<McpSchema.Prompt> updatedPrompts) {
return Flux.fromIterable(updatedPrompts)
.flatMap(prompt -> validatePrompt(prompt))
.collectList()
.doOnNext(validPrompts -> {
promptRepository.saveAll(validPrompts);
})
.then();
}
Spring Boot Integration
Spring Boot 자동 설정을 사용하면 클라이언트 핸들러가 자동으로 감지되고 등록돼요:
@SpringBootApplication
public class McpClientApplication {
public static void main(String[] args) {
SpringApplication.run(McpClientApplication.class, args);
}
}
@Component
public class MyClientHandlers {
@McpLogging(clients = "my-server")
public void handleLogs(LoggingMessageNotification notification) {
// Handle logs
}
@McpSampling(clients = "my-server")
public CreateMessageResult handleSampling(CreateMessageRequest request) {
// Handle sampling
}
@McpProgress(clients = "my-server")
public void handleProgress(ProgressNotification notification) {
// Handle progress
}
자동 설정은 다음을 수행해요:
-
MCP 클라이언트 애노테이션이 있는 빈 스캔
-
적절한 스펙 생성
-
MCP 클라이언트에 등록
-
동기 및 비동기 구현 모두 지원
-
클라이언트별 핸들러로 여러 클라이언트 처리
Configuration Properties
클라이언트 애노테이션 스캐너와 클라이언트 연결을 구성해요:
spring:
ai:
mcp:
client:
type: SYNC # or ASYNC
annotation-scanner:
enabled: true
# Configure client connections - the connection names become clients values
sse:
connections:
my-server: # This becomes the clients
url: http://localhost:8080
tool-server: # Another clients
url: http://localhost:8081
stdio:
connections:
local-server: # This becomes the clients
command: /path/to/mcp-server
args:
- --mode=production
| __ | 애노테이션의 clients 매개변수는 구성에 정의된 연결 이름과 일치해야 해요. 위 예시에서 유효한 clients 값은 "my-server", "tool-server", "local-server"가 돼요. |
|---|
Usage with MCP Client
애노테이션이 달린 핸들러는 MCP 클라이언트와 자동으로 통합돼요:
@Autowired
private List<McpSyncClient> mcpClients;
// The clients will automatically use your annotated handlers based on clients
// No manual registration needed - handlers are matched to clients by name
각 MCP 클라이언트 연결에 대해, 일치하는 clients가 있는 핸들러가 자동으로 등록되고 해당 이벤트가 발생할 때 호출돼요.