From 49e5b901dc80a156222796607b536b6ce9cc91ce Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Wed, 27 Aug 2025 10:02:33 +0200 Subject: [PATCH] refactor(mcp-annotations): migrate client/server samples to annotation-based auto-registration - Removed explicit dependency on from client and server modules; now rely on auto-registration via Spring AI MCP core. - Deleted manual customizer/configuration classes (, ) from client. - Refactored handler/provider classes: renamed and moved to reflect annotation-based usage. - Simplified application classes to remove explicit bean registration for MCP handlers/providers. - Updated documentation to describe the new annotation-driven approach, project structure, and removed boilerplate. - Adjusted properties and configuration for clarity and to match the new structure. - Ensure the proper clients paramter is set to all client mcp annotations, - Update READMEs This refactor streamlines the MCP annotation samples, leveraging automatic handler and tool registration for reduced boilerplate and improved maintainability. Signed-off-by: Christian Tzolov --- .../mcp-annotations/README.md | 177 ++++--------- .../mcp-annotations-client/pom.xml | 17 -- .../samples/client/McpClientApplication.java | 58 +---- ...rs.java => McpClientHandlerProviders.java} | 25 +- .../AnnotationSyncClientCustomizer.java | 78 ------ .../src/main/resources/application.properties | 10 +- .../mcp-annotations-server/README.md | 246 ++++++++++++------ .../mcp-annotations-server/pom.xml | 5 - .../sample/server/McpServerApplication.java | 39 +-- .../CompletionProvider.java} | 6 +- .../PromptProvider.java} | 4 +- .../SpringAiToolProvider.java | 2 +- .../ToolProvider.java} | 8 +- .../ToolProvider2.java} | 6 +- .../UserProfileResourceProvider.java} | 11 +- .../src/main/resources/application.properties | 7 +- .../sampling/annotations/README.md | 218 +++++++--------- .../mcp-sampling-client-annotations/pom.xml | 7 - .../mcp/samples/client/AppConfiguration.java | 70 ----- .../samples/client/McpClientApplication.java | 9 + .../mcp/samples/client/McpClientHandlers.java | 6 +- .../AnnotationSyncClientCustomizer.java | 78 ------ .../mcp-sampling-server-annotations/pom.xml | 7 - .../sample/server/McpServerApplication.java | 12 - .../ai/mcp/sample/server/WeatherService.java | 6 +- 25 files changed, 389 insertions(+), 723 deletions(-) rename model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/{McpClientHandlers.java => McpClientHandlerProviders.java} (72%) delete mode 100644 model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/customizers/AnnotationSyncClientCustomizer.java rename model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/{provider/McpCompletionProvider.java => providers/CompletionProvider.java} (97%) rename model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/{provider/McpPromptProvider.java => providers/PromptProvider.java} (98%) rename model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/{provider => providers}/SpringAiToolProvider.java (98%) rename model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/{provider/McpToolProvider.java => providers/ToolProvider.java} (89%) rename model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/{provider/McpToolProvider2.java => providers/ToolProvider2.java} (93%) rename model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/{provider/McpUserProfileResourceProvider.java => providers/UserProfileResourceProvider.java} (94%) delete mode 100644 model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/AppConfiguration.java delete mode 100644 model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/customizers/AnnotationSyncClientCustomizer.java diff --git a/model-context-protocol/mcp-annotations/README.md b/model-context-protocol/mcp-annotations/README.md index 1d886ff8..288dc756 100644 --- a/model-context-protocol/mcp-annotations/README.md +++ b/model-context-protocol/mcp-annotations/README.md @@ -1,43 +1,25 @@ # Spring AI MCP Annotations Examples -This directory contains comprehensive examples demonstrating the Model Context Protocol (MCP) using Spring AI's annotation-based approach. These examples showcase the full spectrum of MCP capabilities including tools, resources, prompts, completions, and client-side handlers using declarative annotations. +This directory contains examples demonstrating the Model Context Protocol (MCP) using Spring AI's annotation-based approach. These examples showcase MCP capabilities including tools, resources, prompts, completions, and client-side handlers using declarative annotations. ## Overview The MCP Annotations examples demonstrate: -- **Complete MCP Feature Set**: Tools, resources, prompts, completions, and client handlers - **Annotation-Driven Development**: Simplified MCP development using `@McpTool`, `@McpResource`, `@McpPrompt`, `@McpComplete`, and client annotations - **Declarative Configuration**: Automatic registration and configuration through annotations -- **Multiple Data Sources**: Weather APIs, user profiles, and completion databases - **Client-Server Communication**: Full bidirectional MCP communication patterns -- **Advanced MCP Features**: Progress tracking, logging, sampling, and elicitation - -## What Makes This Special? - -This is the most comprehensive MCP annotation example, showcasing: - -### Server-Side Capabilities -- **Multiple Tool Providers**: Weather data from different APIs (Open-Meteo and Weather.gov) -- **Rich Resource System**: User profiles with various access patterns and URI templates -- **Dynamic Prompts**: Contextual prompt generation with parameter validation -- **Smart Completions**: Auto-completion for usernames, countries, and other data +- **Multiple MCP Features**: Tools, resources, prompts, completions, and client handlers - **Mixed Annotation Styles**: Both MCP annotations and Spring AI `@Tool` annotations -### Client-Side Features -- **Comprehensive Handlers**: Logging, progress, sampling, and elicitation support -- **Annotation-Based Configuration**: Automatic handler registration -- **Custom Client Customizers**: Advanced client configuration patterns - ## Projects ### 1. mcp-annotations-server -A comprehensive MCP server implementation showcasing all major MCP features: +A comprehensive MCP server implementation showcasing all major MCP features using annotations. #### Tools -- **Weather Tools**: Temperature data from Open-Meteo API using `@McpTool` -- **Weather Forecast**: Detailed forecasts from Weather.gov API using Spring AI `@Tool` -- **Weather Alerts**: State-based weather alerts using Spring AI `@Tool` +- **Weather Tool**: Temperature data from Open-Meteo API using `@McpTool` +- **Additional Tools**: Weather forecast and alerts from Weather.gov API using Spring AI `@Tool` #### Resources - **User Profiles**: Complete user information with multiple access patterns @@ -46,20 +28,23 @@ A comprehensive MCP server implementation showcasing all major MCP features: - **User Connections**: Social connections and relationships - **User Notifications**: Dynamic notification generation - **User Avatars**: Binary data handling with custom MIME types +- **User Location**: Simple location information #### Prompts - **Greeting Prompts**: Simple parameterized greetings - **Personalized Messages**: Complex prompts with multiple parameters and logic - **Conversation Starters**: Multi-message conversation flows - **Dynamic Content**: Map-based argument handling +- **Single Message**: Single message responses +- **String List**: List-based responses #### Completions - **Username Completion**: Auto-complete for user status URIs +- **Name Completion**: Auto-complete for personalized message prompts - **Country Completion**: Travel-related country name completion -- **Context-Aware**: Different completion strategies based on context ### 2. mcp-annotations-client -A comprehensive MCP client implementation demonstrating all client-side MCP capabilities: +A simple MCP client implementation demonstrating client-side MCP capabilities using annotations. #### Handler Types - **Progress Handlers**: Track long-running operations with `@McpProgress` @@ -74,30 +59,6 @@ A comprehensive MCP client implementation demonstrating all client-side MCP capa ```java @SpringBootApplication public class McpServerApplication { - // Automatic tool registration from multiple providers - @Bean - public List toolSpecs(McpToolProvider toolProvider, McpToolProvider2 toolProvider2) { - return SyncMcpAnnotationProvider.createSyncToolSpecifications(List.of(toolProvider, toolProvider2)); - } - - // Resource specifications from annotation providers - @Bean - public List resourceSpecs(McpUserProfileResourceProvider userProfileResourceProvider) { - return SyncMcpAnnotationProvider.createSyncResourceSpecifications(List.of(userProfileResourceProvider)); - } - - // Prompt specifications - @Bean - public List promptSpecs(McpPromptProvider promptProvider) { - return SyncMcpAnnotationProvider.createSyncPromptSpecifications(List.of(promptProvider)); - } - - // Completion specifications - @Bean - public List completionSpecs(McpCompletionProvider autocompleteProvider) { - return SyncMcpAnnotationProvider.createSyncCompleteSpecifications(List.of(autocompleteProvider)); - } - // Traditional Spring AI tool integration @Bean public ToolCallbackProvider weatherTools(SpringAiToolProvider weatherService) { @@ -106,41 +67,40 @@ public class McpServerApplication { } ``` +The server uses automatic annotation scanning to register MCP providers. All `@McpTool`, `@McpResource`, `@McpPrompt`, and `@McpComplete` annotated methods are automatically discovered and registered. + ### Client Architecture ```java @SpringBootApplication public class McpClientApplication { - // Automatic handler registration - @Bean - List loggingSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProvider.createSyncLoggingSpecifications(List.of(clientMcpHandlers)); - } - - @Bean - List samplingSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProvider.createSyncSamplingSpecifications(List.of(clientMcpHandlers)); - } - - // Custom client configuration @Bean - McpSyncClientCustomizer annotationMcpSyncClientCustomizer( - List loggingSpecs, - List samplingSpecs, - List elicitationSpecs, - List progressSpecs) { - return new AnnotationSyncClientCustomizer(samplingSpecs, loggingSpecs, elicitationSpecs, progressSpecs); + public CommandLineRunner predefinedQuestions(List mcpClients) { + return args -> { + for (McpSyncClient mcpClient : mcpClients) { + // Call tools and interact with MCP servers + CallToolRequest toolRequest = CallToolRequest.builder() + .name("tool1") + .arguments(Map.of("input", "test input")) + .progressToken("test-progress-token") + .build(); + + CallToolResult response = mcpClient.callTool(toolRequest); + } + }; } } ``` +Client handlers are automatically registered through annotation scanning. + ## Key Implementation Examples ### Tool Implementation ```java @Service -public class McpToolProvider { +public class ToolProvider { @McpTool(description = "Get the temperature (in celsius) for a specific location") public WeatherResponse getTemperature( @McpToolParam(description = "The location latitude") double latitude, @@ -160,7 +120,7 @@ public class McpToolProvider { ```java @Service -public class McpUserProfileResourceProvider { +public class UserProfileResourceProvider { @McpResource(uri = "user-profile://{username}", name = "User Profile", description = "Provides user profile information for a specific user") @@ -187,7 +147,7 @@ public class McpUserProfileResourceProvider { ```java @Service -public class McpPromptProvider { +public class PromptProvider { @McpPrompt(name = "personalized-message", description = "Generates a personalized message based on user information") public GetPromptResult personalizedMessage( @@ -225,7 +185,7 @@ public class McpPromptProvider { ```java @Service -public class McpCompletionProvider { +public class CompletionProvider { @McpComplete(uri = "user-status://{username}") public List completeUsername(String usernamePrefix) { String prefix = usernamePrefix.toLowerCase(); @@ -241,7 +201,7 @@ public class McpCompletionProvider { .toList(); } - @McpComplete(prompt = "travel-planner") + @McpComplete(prompt = "personalized-message") public CompleteResult completeCountryName(CompleteRequest request) { String prefix = request.argument().value().toLowerCase(); String firstLetter = prefix.substring(0, 1); @@ -260,20 +220,20 @@ public class McpCompletionProvider { ```java @Service -public class McpClientHandlers { - @McpProgress(clientId = "server1") +public class McpClientHandlerProviders { + @McpProgress(clients = "server1") public void progressHandler(ProgressNotification progressNotification) { logger.info("MCP PROGRESS: [{}] progress: {} total: {} message: {}", progressNotification.progressToken(), progressNotification.progress(), progressNotification.total(), progressNotification.message()); } - @McpLogging + @McpLogging(clients = "server1") public void loggingHandler(LoggingMessageNotification loggingMessage) { logger.info("MCP LOGGING: [{}] {}", loggingMessage.level(), loggingMessage.data()); } - @McpSampling + @McpSampling(clients = "server1") public CreateMessageResult samplingHandler(CreateMessageRequest llmRequest) { String userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text(); String modelHint = llmRequest.modelPreferences().hints().get(0).name(); @@ -283,7 +243,7 @@ public class McpClientHandlers { .build(); } - @McpElicitation + @McpElicitation(clients = "server1") public ElicitResult elicitationHandler(McpSchema.ElicitRequest request) { logger.info("MCP ELICITATION: {}", request); return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message())); @@ -293,16 +253,6 @@ public class McpClientHandlers { ## Key Dependencies -Both projects use the Spring AI MCP Annotations library: - -```xml - - org.springaicommunity - spring-ai-mcp-annotations - 0.2.0-SNAPSHOT - -``` - ### Server Dependencies ```xml @@ -317,14 +267,6 @@ Both projects use the Spring AI MCP Annotations library: org.springframework.ai spring-ai-starter-mcp-client - - org.springframework.ai - spring-ai-starter-model-openai - - - org.springframework.ai - spring-ai-starter-model-anthropic - ``` ## Running the Examples @@ -333,8 +275,6 @@ Both projects use the Spring AI MCP Annotations library: - Java 17 or later - Maven 3.6+ -- OpenAI API key (for client) -- Anthropic API key (for client) ### Step 1: Start the MCP Annotations Server @@ -346,14 +286,7 @@ java -jar target/mcp-annotations-server-0.0.1-SNAPSHOT.jar The server will start on `http://localhost:8080` with SSE transport enabled. -### Step 2: Set Environment Variables (for client) - -```bash -export OPENAI_API_KEY=your-openai-key -export ANTHROPIC_API_KEY=your-anthropic-key -``` - -### Step 3: Run the MCP Annotations Client +### Step 2: Run the MCP Annotations Client ```bash cd mcp-annotations-client @@ -365,8 +298,6 @@ java -jar target/mcp-annotations-client-0.0.1-SNAPSHOT.jar ### Tools - `getTemperature`: Get temperature data using Open-Meteo API -- `getWeatherForecastByLocation`: Detailed weather forecast from Weather.gov -- `getAlerts`: Weather alerts for US states ### Resources - `user-profile://{username}`: Complete user profile information @@ -387,8 +318,8 @@ java -jar target/mcp-annotations-client-0.0.1-SNAPSHOT.jar ### Completions - Username completion for `user-status://` URIs +- Name completion for `personalized-message` prompts - Country name completion for travel prompts -- Context-aware completion strategies ## Configuration @@ -403,7 +334,7 @@ spring.ai.mcp.server.version=0.0.1 spring.main.banner-mode=off # Logging configuration -logging.file.name=./model-context-protocol/weather/starter-webmvc-server/target/starter-webmvc-server.log +logging.file.name=./model-context-protocol/mcp-annotations/mcp-annotations-server/target/mcp-annotations-server.log # Uncomment for STDIO transport # spring.ai.mcp.server.stdio=true @@ -420,10 +351,6 @@ logging.file.name=./model-context-protocol/weather/starter-webmvc-server/target/ spring.application.name=mcp spring.main.web-application-type=none -# API keys for LLM providers -spring.ai.openai.api-key=${OPENAI_API_KEY} -spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY} - # MCP client connection spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080 @@ -475,17 +402,13 @@ Resources can specify custom MIME types for different content types, including b ### Server Exchange Integration Methods can access `McpSyncServerExchange` for advanced server operations like logging notifications and progress tracking. -## Comparison with Other Examples +## Sample Data -| Feature | Basic Examples | Sampling Examples | **Annotations Examples** | -|---------|---------------|-------------------|---------------------------| -| **Tools** | ✓ Basic | ✓ With Sampling | ✓ **Comprehensive** | -| **Resources** | ✗ | ✗ | ✓ **Full Featured** | -| **Prompts** | ✗ | ✗ | ✓ **Dynamic & Flexible** | -| **Completions** | ✗ | ✗ | ✓ **Context-Aware** | -| **Client Handlers** | ✗ | ✓ Sampling Only | ✓ **All Types** | -| **Annotation Style** | Manual Config | Manual Config | ✓ **Declarative** | -| **Mixed Approaches** | ✗ | ✗ | ✓ **MCP + Spring AI** | +The server includes sample user profiles for testing: +- **john**: John Smith (New York, age 32) +- **jane**: Jane Doe (London, age 28) +- **bob**: Bob Johnson (Tokyo, age 45) +- **alice**: Alice Brown (Sydney, age 36) ## Related Projects @@ -497,8 +420,8 @@ Methods can access `McpSyncServerExchange` for advanced server operations like l ## Additional Resources * [Spring AI Documentation](https://docs.spring.io/spring-ai/reference/) -* [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) -* [MCP Client Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) +* [Spring AI MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) +* [Spring AI MCP Client Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) +* [Spring AI Annotations](https://docs.spring.io/spring-ai/reference/1.1-SNAPSHOT/api/mcp/mcp-annotations-overview.html) +* [Java MCP Annotations](https://github.com/spring-ai-community/mcp-annotations) * [Model Context Protocol Specification](https://modelcontextprotocol.github.io/specification/) -* [Spring Boot Documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/) -* [Spring AI MCP Annotations](https://github.com/spring-projects-experimental/spring-ai-mcp-annotations) diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-client/pom.xml b/model-context-protocol/mcp-annotations/mcp-annotations-client/pom.xml index fae0b858..abcec9d7 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-client/pom.xml +++ b/model-context-protocol/mcp-annotations/mcp-annotations-client/pom.xml @@ -34,27 +34,10 @@ - - org.springaicommunity - spring-ai-mcp-annotations - 0.2.0-SNAPSHOT - - org.springframework.ai spring-ai-starter-mcp-client - - - org.springframework.ai - spring-ai-starter-model-openai - - - - org.springframework.ai - spring-ai-starter-model-anthropic - - diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java b/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java index 0c80594c..a83373be 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java +++ b/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java @@ -18,14 +18,6 @@ import java.util.List; import java.util.Map; -import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification; -import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification; -import org.springaicommunity.mcp.method.progress.SyncProgressSpecification; -import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification; -import org.springaicommunity.mcp.spring.SyncMcpAnnotationProviders; -import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; -import org.springframework.ai.mcp.samples.client.customizers.AnnotationSyncClientCustomizer; -import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -43,49 +35,25 @@ public static void main(String[] args) { } @Bean - public CommandLineRunner predefinedQuestions(OpenAiChatModel openAiChatModel, + public CommandLineRunner predefinedQuestions( List mcpClients) { return args -> { - McpSyncClient mcpClient = mcpClients.get(0); - // Call a tool that sends progress notifications - CallToolRequest toolRequest = CallToolRequest.builder() - .name("tool1") - .arguments(Map.of("input", "test input")) - .progressToken("test-progress-token") - .build(); + for (McpSyncClient mcpClient : mcpClients) { + System.out.println(">>> MCP Client: " + mcpClient.getClientInfo()); - CallToolResult response = mcpClient.callTool(toolRequest); + // Call a tool that sends progress notifications + CallToolRequest toolRequest = CallToolRequest.builder() + .name("tool1") + .arguments(Map.of("input", "test input")) + .progressToken("test-progress-token") + .build(); - System.out.println("Tool response: " + response); - }; - } - - @Bean - List loggingSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.loggingSpecifications(List.of(clientMcpHandlers)); - } - - @Bean - List samplingSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.samplingSpecifications(List.of(clientMcpHandlers)); - } + CallToolResult response = mcpClient.callTool(toolRequest); - @Bean - List elicitationSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.elicitationSpecifications(List.of(clientMcpHandlers)); - } - - @Bean - List progressSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.progressSpecifications(List.of(clientMcpHandlers)); - } - - @Bean - McpSyncClientCustomizer annotationMcpSyncClientCustomizer(List loggingSpecs, - List samplingSpecs, List elicitationSpecs, - List progressSpecs) { - return new AnnotationSyncClientCustomizer(samplingSpecs, loggingSpecs, elicitationSpecs, progressSpecs); + System.out.println("Tool response: " + response); + } + }; } } \ No newline at end of file diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientHandlers.java b/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientHandlerProviders.java similarity index 72% rename from model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientHandlers.java rename to model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientHandlerProviders.java index fc7b1e35..c1f87275 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientHandlers.java +++ b/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientHandlerProviders.java @@ -18,23 +18,36 @@ import io.modelcontextprotocol.spec.McpSchema.ProgressNotification; @Service -public class McpClientHandlers { +public class McpClientHandlerProviders { - private static final Logger logger = LoggerFactory.getLogger(McpClientHandlers.class); + private static final Logger logger = LoggerFactory.getLogger(McpClientHandlerProviders.class); - @McpProgress(clientId = "server1") + /** + * Handles progress notifications for the client identified by {@code clientId = "server1"}. + *
+ * The {@code clientId} is configured via application properties, for example: + *
    + *
  • {@code spring.ai.mcp.client.sse.connections.server1.url=...}
  • + *
  • {@code spring.ai.mcp.client.streamable-http.connections.server1.url=...}
  • + *
+ * + * The handler is assigned only to the client with ID "server1". + * + * @param progressNotification the progress notification received from the server + */ + @McpProgress(clients = "server1") public void progressHandler(ProgressNotification progressNotification) { logger.info("MCP PROGRESS: [{}] progress: {} total: {} message: {}", progressNotification.progressToken(), progressNotification.progress(), progressNotification.total(), progressNotification.message()); } - @McpLogging + @McpLogging(clients = "server1") public void loggingHandler(LoggingMessageNotification loggingMessage) { logger.info("MCP LOGGING: [{}] {}", loggingMessage.level(), loggingMessage.data()); } - @McpSampling + @McpSampling(clients = "server1") public CreateMessageResult samplingHandler(CreateMessageRequest llmRequest) { logger.info("MCP SAMPLING: {}", llmRequest); @@ -46,7 +59,7 @@ public CreateMessageResult samplingHandler(CreateMessageRequest llmRequest) { .build(); } - @McpElicitation + @McpElicitation(clients = "server1") public ElicitResult elicitationHandler(McpSchema.ElicitRequest request) { logger.info("MCP ELICITATION: {}", request); return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message())); diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/customizers/AnnotationSyncClientCustomizer.java b/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/customizers/AnnotationSyncClientCustomizer.java deleted file mode 100644 index 9c87bb73..00000000 --- a/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/java/org/springframework/ai/mcp/samples/client/customizers/AnnotationSyncClientCustomizer.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2025-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.ai.mcp.samples.client.customizers; - -import java.util.List; - -import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification; -import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification; -import org.springaicommunity.mcp.method.progress.SyncProgressSpecification; -import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification; -import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; - -import io.modelcontextprotocol.client.McpClient.SyncSpec; - -/** - * NOTE: This class temporarily here. It will be moved to the Spring-AI project - */ -public class AnnotationSyncClientCustomizer implements McpSyncClientCustomizer { - - private final List syncSamplingSpecifications; - private final List syncLoggingSpecifications; - private final List syncElicitationSpecifications; - private final List syncProgressSpecifications; - - public AnnotationSyncClientCustomizer(List syncSamplingSpecifications, - List syncLoggingSpecifications, - List syncElicitationSpecifications, - List syncProgressSpecifications) { - - this.syncSamplingSpecifications = syncSamplingSpecifications; - this.syncLoggingSpecifications = syncLoggingSpecifications; - this.syncElicitationSpecifications = syncElicitationSpecifications; - this.syncProgressSpecifications = syncProgressSpecifications; - } - - @Override - public void customize(String name, SyncSpec clientSpec) { - - this.syncSamplingSpecifications.forEach(samplingSpec -> { - if (samplingSpec.clientId().isEmpty() || samplingSpec.clientId().equals(name)) { - clientSpec.sampling(samplingSpec.samplingHandler()); - } - }); - - this.syncLoggingSpecifications.forEach(loggingSpec -> { - if (loggingSpec.clientId().isEmpty() || loggingSpec.clientId().equals(name)) { - clientSpec.loggingConsumer(loggingSpec.loggingHandler()); - } - }); - - this.syncElicitationSpecifications.forEach(elicitationSpec -> { - if (elicitationSpec.clientId().isEmpty() || elicitationSpec.clientId().equals(name)) { - clientSpec.elicitation(elicitationSpec.elicitationHandler()); - } - }); - - this.syncProgressSpecifications.forEach(progressSpec -> { - if (progressSpec.clientId().isEmpty() || progressSpec.clientId().equals(name)) { - clientSpec.progressConsumer(progressSpec.progressHandler()); - } - }); - } - -} diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/resources/application.properties b/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/resources/application.properties index e83ae6ac..5241196d 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/resources/application.properties +++ b/model-context-protocol/mcp-annotations/mcp-annotations-client/src/main/resources/application.properties @@ -1,17 +1,15 @@ spring.application.name=mcp spring.main.web-application-type=none -# Disable the chat client auto-configuration because we are using multiple chat models -spring.ai.chat.client.enabled=false -spring.ai.openai.api-key=${OPENAI_API_KEY} -spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY} +# spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080 +spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080 +# spring.ai.mcp.client.streamable-http.connections.server2.url=http://localhost:8081 -spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080 spring.ai.mcp.client.request-timeout=5m logging.level.io.modelcontextprotocol.client=WARN logging.level.io.modelcontextprotocol.spec=WARN -spring.ai.mcp.client.toolcallback.enabled=false \ No newline at end of file +# spring.ai.mcp.client.toolcallback.enabled=false \ No newline at end of file diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-server/README.md b/model-context-protocol/mcp-annotations/mcp-annotations-server/README.md index 8ecbbe2e..4de52ce9 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-server/README.md +++ b/model-context-protocol/mcp-annotations/mcp-annotations-server/README.md @@ -52,11 +52,6 @@ This sample demonstrates: The project requires the Spring AI MCP Server WebMVC Boot Starter and MCP Annotations: ```xml - - org.springaicommunity - spring-ai-mcp-annotations - 0.2.0-SNAPSHOT - org.springframework.ai spring-ai-starter-mcp-server-webmvc @@ -65,8 +60,8 @@ The project requires the Spring AI MCP Server WebMVC Boot Starter and MCP Annota These dependencies provide: - HTTP-based transport using Spring MVC (`WebMvcSseServerTransport`) -- Auto-configured SSE endpoints -- Optional STDIO transport +- Auto-configured SSE, Streamable-HTTP or Stateless endpoints, configured by the `spring.ai.mcp.server.protocol=...` property and defaulting to `SSE`. +- Optional STDIO transport, if the `spring.ai.mcp.server.stdio=true` is set - Annotation-based method handling for MCP operations ## Building the Project @@ -80,9 +75,12 @@ Build the project using Maven: The server supports two transport modes: -### WebMVC SSE Mode (Default) +### WebMVC SSE/Streamable-HTTP/Stateless Mode + +Mode depends on the `spring.ai.mcp.server.protocol=...` setting. + ```bash -java -jar target/mcp-annotations-server-0.0.1-SNAPSHOT.jar +java -Dspring.ai.mcp.server.protocol=STREAMABLE -jar target/mcp-annotations-server-0.0.1-SNAPSHOT.jar ``` ### STDIO Mode @@ -99,6 +97,9 @@ Configure the server through `application.properties`: # Server identification spring.ai.mcp.server.name=my-weather-server spring.ai.mcp.server.version=0.0.1 +spring.ai.mcp.server.protocol=STREAMABLE +# spring.ai.mcp.server.protocol=STATELESS + # Transport configuration (uncomment to enable STDIO) # spring.ai.mcp.server.stdio=true @@ -109,7 +110,7 @@ spring.main.banner-mode=off # logging.pattern.console= # Log file location -logging.file.name=./model-context-protocol/weather/starter-webmvc-server/target/starter-webmvc-server.log +logging.file.name=./model-context-protocol/mcp-annotations/mcp-annotations-server/target/server.log ``` ## Server Implementation @@ -123,30 +124,11 @@ public class McpServerApplication { SpringApplication.run(McpServerApplication.class, args); } + // (Optional) not MCP annotation. Just demostrates how to use the @Tool along with the @McpTool to provision MCP Server Tools. @Bean public ToolCallbackProvider weatherTools(SpringAiToolProvider weatherService) { return MethodToolCallbackProvider.builder().toolObjects(weatherService).build(); } - - @Bean - public List resourceSpecs(UserProfileResourceProvider userProfileResourceProvider) { - return SyncMcpAnnotationProvider.createSyncResourceSpecifications(List.of(userProfileResourceProvider)); - } - - @Bean - public List promptSpecs(PromptProvider promptProvider) { - return SyncMcpAnnotationProvider.createSyncPromptSpecifications(List.of(promptProvider)); - } - - @Bean - public List completionSpecs(AutocompleteProvider autocompleteProvider) { - return SyncMcpAnnotationProvider.createSyncCompleteSpecifications(List.of(autocompleteProvider)); - } - - @Bean - public List toolSpecs(McpToolProvider toolProvider) { - return SyncMcpAnnotationProvider.createSyncToolSpecifications(List.of(toolProvider)); - } } ``` @@ -183,7 +165,8 @@ Uses MCP-specific `@McpTool` annotation for temperature retrieval: @Service public class McpToolProvider { @McpTool(description = "Get the temperature (in celsius) for a specific location") - public WeatherResponse getTemperature( + public WeatherResponse getTemperature(McpSyncServerExchange exchange, + @McpProgressToken String progressToken @McpToolParam(description = "The location latitude") double latitude, @McpToolParam(description = "The location longitude") double longitude, @McpToolParam(description = "The city name") String city) { @@ -364,45 +347,171 @@ public class AutocompleteProvider { You can connect to the server using either STDIO or SSE transport: -### Manual Clients +### Boot Starter Clients -#### WebMVC SSE Client +For a better development experience, consider using the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/1.1-SNAPSHOT/api/mcp/mcp-client-boot-starter-docs.html) and related [MCP Client Annotations](https://docs.spring.io/spring-ai/reference/1.1-SNAPSHOT/api/mcp/mcp-annotations-client.html) support. -For servers using SSE transport: +The MCP Client Boot Starter provides: +- Auto-configuration for MCP client connections +- Declarative annotation-based handlers for server notifications +- Support for multiple transport protocols (SSE, Streamable-HTTP, STDIO) +- Automatic registration of client handlers -```java -var transport = HttpClientSseClientTransport.builder("http://localhost:8080").build(); -var client = McpClient.sync(transport).build(); +#### Client Dependencies + +Add the MCP Client Boot Starter to your project: + +```xml + + org.springframework.ai + spring-ai-starter-mcp-client + ``` -#### STDIO Client +#### Client Annotations + +The client supports several annotations for handling server notifications: -For servers using STDIO transport: +- `@McpLogging` - Handle logging message notifications from MCP servers +- `@McpProgress` - Handle progress notifications for long-running operations +- `@McpSampling` - Handle sampling requests from MCP servers for LLM completions +- `@McpElicitation` - Handle elicitation requests to gather additional information from users +- `@McpToolListChanged` - Handle notifications when the server's tool list changes +- `@McpResourceListChanged` - Handle notifications when the server's resource list changes +- `@McpPromptListChanged` - Handle notifications when the server's prompt list changes + +**Important**: All MCP client annotations **MUST** include a `clients` parameter to associate the handler with a specific MCP client connection. + +#### Example Client Implementation ```java -var stdioParams = ServerParameters.builder("java") - .args("-Dspring.ai.mcp.server.stdio=true", - "-Dspring.main.web-application-type=none", - "-Dspring.main.banner-mode=off", - "-Dlogging.pattern.console=", - "-jar", - "target/mcp-annotations-server-0.0.1-SNAPSHOT.jar") - .build(); - -var transport = new StdioClientTransport(stdioParams); -var client = McpClient.sync(transport).build(); +@SpringBootApplication +public class McpClientApplication { + public static void main(String[] args) { + SpringApplication.run(McpClientApplication.class, args).close(); + } + + @Bean + public CommandLineRunner predefinedQuestions(List mcpClients) { + return args -> { + for (McpSyncClient mcpClient : mcpClients) { + System.out.println(">>> MCP Client: " + mcpClient.getClientInfo()); + + // Call a tool that sends progress notifications + CallToolRequest toolRequest = CallToolRequest.builder() + .name("tool1") + .arguments(Map.of("input", "test input")) + .progressToken("test-progress-token") + .build(); + + CallToolResult response = mcpClient.callTool(toolRequest); + + System.out.println("Tool response: " + response); + } + }; + } +} ``` -The sample project includes example client implementations: -- [SampleClient.java](src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java): Manual MCP client implementation -- [ClientStdio.java](src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java): STDIO transport connection -- [ClientSse.java](src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java): SSE transport connection +#### Client Handler Providers -### Boot Starter Clients +```java +@Service +public class McpClientHandlerProviders { -For a better development experience, consider using the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html). These starters enable auto-configuration of multiple STDIO and/or SSE connections to MCP servers. + private static final Logger logger = LoggerFactory.getLogger(McpClientHandlerProviders.class); -#### STDIO Transport + @McpProgress(clients = "server1") + public void progressHandler(ProgressNotification progressNotification) { + logger.info("MCP PROGRESS: [{}] progress: {} total: {} message: {}", + progressNotification.progressToken(), progressNotification.progress(), + progressNotification.total(), progressNotification.message()); + } + + @McpLogging(clients = "server1") + public void loggingHandler(LoggingMessageNotification loggingMessage) { + logger.info("MCP LOGGING: [{}] {}", loggingMessage.level(), loggingMessage.data()); + } + + @McpSampling(clients = "server1") + public CreateMessageResult samplingHandler(CreateMessageRequest llmRequest) { + logger.info("MCP SAMPLING: {}", llmRequest); + String userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text(); + String modelHint = llmRequest.modelPreferences().hints().get(0).name(); + + return CreateMessageResult.builder() + .content(new McpSchema.TextContent("Response " + userPrompt + " with model hint " + modelHint)) + .build(); + } + + @McpElicitation(clients = "server1") + public ElicitResult elicitationHandler(McpSchema.ElicitRequest request) { + logger.info("MCP ELICITATION: {}", request); + return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message())); + } +} +``` + +#### Client Configuration + +Configure client connections in `application.properties`: + +```properties +spring.application.name=mcp +spring.main.web-application-type=none + +# Streamable-HTTP transport configuration +spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080 + +# SSE transport configuration (alternative) +# spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080 + +# Global client settings +spring.ai.mcp.client.request-timeout=5m + +# Logging configuration +logging.level.io.modelcontextprotocol.client=WARN +logging.level.io.modelcontextprotocol.spec=WARN + +# Optional: Disable tool callback if not needed +# spring.ai.mcp.client.toolcallback.enabled=false +``` + +#### Running the Client Examples + +##### Streamable-HTTP (HttpClient) Transport + +1. Start the MCP annotations server: + +```bash +java -Dspring.ai.mcp.server.protocol=STREAMABLE -jar mcp-annotations-server-0.0.1-SNAPSHOT.jar +``` + +2. In another console, start the client configured with Streamable-HTTP transport: + +```bash +java -Dspring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080 \ + -jar mcp-annotations-client-0.0.1-SNAPSHOT.jar +``` + +##### SSE Transport + +1. Start the MCP annotations server with SSE protocol: + +```bash +java -Dspring.ai.mcp.server.protocol=SSE -jar mcp-annotations-server-0.0.1-SNAPSHOT.jar +``` + +2. Start the client configured with SSE transport: + +```bash +java -Dspring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080 \ + -jar mcp-annotations-client-0.0.1-SNAPSHOT.jar +``` + +**NOTE:** the `clients="server1"` parameter of the `@McpLogging`, `@McpSampling`, `@McpProgress` and `@McpElicitate` annotations corresponds to the conneciton name you put in your `spring.ai.mcp.client.streamable-http.connections.server1.url=` or `spring.ai.mcp.client.sse.connections.server1.url=` configuraitons. + +##### STDIO Transport 1. Create a `mcp-servers-config.json` configuration file: @@ -427,27 +536,16 @@ For a better development experience, consider using the [MCP Client Boot Starter ```bash java -Dspring.ai.mcp.client.stdio.servers-configuration=file:mcp-servers-config.json \ - -Dai.user.input='What is the weather in NY?' \ -Dlogging.pattern.console= \ - -jar mcp-starter-default-client-0.0.1-SNAPSHOT.jar + -jar mcp-annotations-client-0.0.1-SNAPSHOT.jar ``` -#### SSE (WebMVC) Transport - -1. Start the MCP annotations server: +The client will automatically: +- Connect to the configured MCP servers +- Register annotated handlers based on the `clients` parameter +- Handle server notifications and requests through the annotated methods +- Provide logging and progress updates as configured -```bash -java -jar mcp-annotations-server-0.0.1-SNAPSHOT.jar -``` - -2. In another console, start the client configured with SSE transport: - -```bash -java -Dspring.ai.mcp.client.sse.connections.annotations-server.url=http://localhost:8080 \ - -Dlogging.pattern.console= \ - -Dai.user.input='What is the weather in NY?' \ - -jar mcp-starter-default-client-0.0.1-SNAPSHOT.jar -``` ## Additional Resources diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-server/pom.xml b/model-context-protocol/mcp-annotations/mcp-annotations-server/pom.xml index bae38e1a..3c22fe5a 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-server/pom.xml +++ b/model-context-protocol/mcp-annotations/mcp-annotations-server/pom.xml @@ -31,11 +31,6 @@ - - org.springaicommunity - spring-ai-mcp-annotations - 0.2.0-SNAPSHOT - org.springframework.ai spring-ai-starter-mcp-server-webmvc diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java index 538513cd..aec26aea 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java +++ b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java @@ -1,25 +1,12 @@ package org.springframework.ai.mcp.sample.server; -import java.util.List; - -import org.springaicommunity.mcp.spring.SyncMcpAnnotationProviders; -import org.springframework.ai.mcp.sample.server.provider.McpCompletionProvider; -import org.springframework.ai.mcp.sample.server.provider.McpPromptProvider; -import org.springframework.ai.mcp.sample.server.provider.McpToolProvider; -import org.springframework.ai.mcp.sample.server.provider.McpToolProvider2; -import org.springframework.ai.mcp.sample.server.provider.McpUserProfileResourceProvider; -import org.springframework.ai.mcp.sample.server.provider.SpringAiToolProvider; +import org.springframework.ai.mcp.sample.server.providers.SpringAiToolProvider; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; -import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification; -import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification; -import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification; -import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification; - @SpringBootApplication public class McpServerApplication { @@ -27,30 +14,10 @@ public static void main(String[] args) { SpringApplication.run(McpServerApplication.class, args); } + // Note: this is not MCP Annotations related, but demonstrates how to register a SpringAI tool + // callback provider as MCP tools along with the @McpTool such @Bean public ToolCallbackProvider weatherTools(SpringAiToolProvider weatherService) { return MethodToolCallbackProvider.builder().toolObjects(weatherService).build(); } - - @Bean - public List resourceSpecs(McpUserProfileResourceProvider userProfileResourceProvider) { - return SyncMcpAnnotationProviders.resourceSpecifications(List.of(userProfileResourceProvider)); - } - - @Bean - public List promptSpecs(McpPromptProvider promptProvider) { - return SyncMcpAnnotationProviders.promptSpecifications(List.of(promptProvider)); - } - - @Bean - public List completionSpecs(McpCompletionProvider autocompleteProvider) { - return SyncMcpAnnotationProviders.completeSpecifications(List.of(autocompleteProvider)); - } - - @Bean - public List toolSpecs(McpToolProvider toolProvider, McpToolProvider2 toolProvider2) { - var toolSpecs = SyncMcpAnnotationProviders.toolSpecifications(List.of(toolProvider, toolProvider2)); - return toolSpecs; - } - } diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpCompletionProvider.java b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/CompletionProvider.java similarity index 97% rename from model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpCompletionProvider.java rename to model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/CompletionProvider.java index dfb5cf7a..8793eb65 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpCompletionProvider.java +++ b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/CompletionProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.ai.mcp.sample.server.provider; +package org.springframework.ai.mcp.sample.server.providers; import java.util.HashMap; import java.util.List; @@ -30,13 +30,13 @@ * @author Christian Tzolov */ @Service -public class McpCompletionProvider { +public class CompletionProvider { private final Map> countryDatabase = new HashMap<>(); private final Map> usernameDatabase = new HashMap<>(); - public McpCompletionProvider() { + public CompletionProvider() { usernameDatabase.put("a", List.of("alex123", "admin", "alice_wonder", "andrew99")); usernameDatabase.put("b", List.of("bob_builder", "blue_sky", "batman", "butterfly")); diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpPromptProvider.java b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/PromptProvider.java similarity index 98% rename from model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpPromptProvider.java rename to model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/PromptProvider.java index 0aec8eb2..4b818a3c 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpPromptProvider.java +++ b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/PromptProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.ai.mcp.sample.server.provider; +package org.springframework.ai.mcp.sample.server.providers; import java.util.List; import java.util.Map; @@ -35,7 +35,7 @@ * @author Christian Tzolov */ @Service -public class McpPromptProvider { +public class PromptProvider { /** * A simple greeting prompt that takes a name parameter. * @param name The name to greet diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/SpringAiToolProvider.java b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/SpringAiToolProvider.java similarity index 98% rename from model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/SpringAiToolProvider.java rename to model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/SpringAiToolProvider.java index 1b5ee65a..e9a16292 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/SpringAiToolProvider.java +++ b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/SpringAiToolProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.ai.mcp.sample.server.provider; +package org.springframework.ai.mcp.sample.server.providers; import java.util.List; import java.util.Map; diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpToolProvider.java b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/ToolProvider.java similarity index 89% rename from model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpToolProvider.java rename to model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/ToolProvider.java index 7fd928b9..2eb8af12 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpToolProvider.java +++ b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/ToolProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.ai.mcp.sample.server.provider; +package org.springframework.ai.mcp.sample.server.providers; import java.time.LocalDateTime; @@ -28,13 +28,13 @@ * @author Christian Tzolov */ @Service -public class McpToolProvider { +public class ToolProvider { - private static final Logger logger = org.slf4j.LoggerFactory.getLogger(McpToolProvider.class); + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(ToolProvider.class); private final RestClient restClient; - public McpToolProvider() { + public ToolProvider() { this.restClient = RestClient.create(); } diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpToolProvider2.java b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/ToolProvider2.java similarity index 93% rename from model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpToolProvider2.java rename to model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/ToolProvider2.java index 0460ff6d..947de771 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpToolProvider2.java +++ b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/ToolProvider2.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.ai.mcp.sample.server.provider; +package org.springframework.ai.mcp.sample.server.providers; import java.util.List; import java.util.Map; @@ -36,10 +36,10 @@ * @author Christian Tzolov */ @Service -public class McpToolProvider2 { +public class ToolProvider2 { @McpTool(description = "Test tool", name = "tool1") - public String toolWithSamplingAndElicitation(McpSyncServerExchange exchange, @McpToolParam String input, + public String toolLggingSamplingElicitationProgress(McpSyncServerExchange exchange, @McpToolParam String input, @McpProgressToken String progressToken) { exchange.loggingNotification(LoggingMessageNotification.builder().data("Tool1 Started!").build()); diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpUserProfileResourceProvider.java b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/UserProfileResourceProvider.java similarity index 94% rename from model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpUserProfileResourceProvider.java rename to model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/UserProfileResourceProvider.java index 547524c6..4ee6ed3c 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/provider/McpUserProfileResourceProvider.java +++ b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/java/org/springframework/ai/mcp/sample/server/providers/UserProfileResourceProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.ai.mcp.sample.server.provider; +package org.springframework.ai.mcp.sample.server.providers; import java.util.HashMap; import java.util.List; @@ -32,11 +32,11 @@ * @author Christian Tzolov */ @Service -public class McpUserProfileResourceProvider { +public class UserProfileResourceProvider { private final Map> userProfiles = new HashMap<>(); - public McpUserProfileResourceProvider() { + public UserProfileResourceProvider() { // Initialize with some sample data Map johnProfile = new HashMap<>(); johnProfile.put("name", "John Smith"); @@ -106,7 +106,7 @@ public ReadResourceResult getUserAttribute(String username, String attribute) { /** * Resource method that takes an exchange and URI variables. */ - @McpResource(uri = "user-profile-exchange://{username}", name = "User Profile with Exchange", description = "Provides user profile information with server exchange context") + // @McpResource(uri = "user-profile-exchange://{username}", name = "User Profile with Exchange", description = "Provides user profile information with server exchange context") public ReadResourceResult getProfileWithExchange(McpSyncServerExchange exchange, String username) { String profileInfo = formatProfileInfo(userProfiles.getOrDefault(username.toLowerCase(), new HashMap<>())); @@ -130,8 +130,7 @@ public List getUserConnections(String username) { * URI variable parameters. */ @McpResource(uri = "user-notifications://{username}", name = "User Notifications", description = "Provides notifications for a specific user") - public List getUserNotifications(McpSyncServerExchange exchange, ReadResourceRequest request, - String username) { + public List getUserNotifications(ReadResourceRequest request, String username) { // Generate notifications based on username String notifications = generateNotifications(username); diff --git a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/resources/application.properties b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/resources/application.properties index 36eba258..6a4cbd47 100644 --- a/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/resources/application.properties +++ b/model-context-protocol/mcp-annotations/mcp-annotations-server/src/main/resources/application.properties @@ -10,7 +10,10 @@ spring.main.banner-mode=off spring.ai.mcp.server.name=my-weather-server spring.ai.mcp.server.version=0.0.1 -# spring.ai.mcp.server.protocol=STREAMABLE +spring.ai.mcp.server.protocol=STREAMABLE # spring.ai.mcp.server.protocol=STATELESS -logging.file.name=./model-context-protocol/weather/starter-webmvc-server/target/starter-webmvc-server.log +logging.file.name=./model-context-protocol/mcp-annotations/mcp-annotations-server/target/server.log + + +# server.port=8081 \ No newline at end of file diff --git a/model-context-protocol/sampling/annotations/README.md b/model-context-protocol/sampling/annotations/README.md index 4def6825..b81d58e6 100644 --- a/model-context-protocol/sampling/annotations/README.md +++ b/model-context-protocol/sampling/annotations/README.md @@ -2,6 +2,24 @@ This directory contains annotation-based examples demonstrating the Model Context Protocol (MCP) Sampling capability in Spring AI. These examples showcase the same MCP Sampling functionality as the main sampling examples but use Spring AI's annotation-based approach for simplified development. +--- + +## Project Structure + +``` +annotations/ +├── mcp-sampling-client-annotations/ # Sample MCP client (annotation-based) +├── mcp-sampling-server-annotations/ # Sample MCP server (annotation-based) +└── README.md +``` + +| Sub-Project | Type | Description | +|------------------------------------|--------|-----------------------------------------------------------------------------| +| mcp-sampling-server-annotations | Server | Annotation-based MCP server exposing weather+poem tool via LLMs | +| mcp-sampling-client-annotations | Client | Annotation-based MCP client aggregating creative responses from all LLMs | + +--- + ## Overview The annotation-based MCP Sampling examples demonstrate: @@ -18,176 +36,99 @@ The annotation-based approach provides several advantages over the traditional i ### Server-Side Benefits - **`@McpTool`**: Automatic tool registration without manual `ToolCallbackProvider` configuration -- **`SyncMcpAnnotationProvider`**: Automatic tool specification generation +- **Automatic Tool Specification Generation**: No need for explicit bean registration - **Simplified Dependency Injection**: Direct access to `McpSyncServerExchange` as method parameter ### Client-Side Benefits - **`@McpSampling`**: Declarative sampling request handling - **`@McpLogging`**: Built-in logging support for MCP operations - **`@McpProgress`**: Progress notification handling -- **`AnnotationSyncClientCustomizer`**: Automatic registration of annotation-based handlers +- **Automatic Handler Registration**: Annotation-based handlers are auto-registered + +--- -## Projects +## Sub-Projects ### 1. mcp-sampling-server-annotations + An MCP server implementation using Spring AI's annotation-based approach that: - Provides weather information via the `@McpTool` annotation - Uses MCP Sampling to generate creative content from multiple LLM providers - Demonstrates simplified server-side MCP implementation -### 2. mcp-sampling-client-annotations -An MCP client implementation using annotation-based handlers that: -- Handles sampling requests using `@McpSampling` -- Routes requests to different LLM providers based on model hints -- Provides logging and progress tracking capabilities - -## How It Works - -### Annotation-Based Server Implementation - -The server uses the `@McpTool` annotation for automatic tool registration: +#### Key Implementation ```java @Service public class WeatherService { - + // ... see source for details ... @McpTool(description = "Get the temperature (in celsius) for a specific location") public String getTemperature2(McpSyncServerExchange exchange, @McpToolParam(description = "The location latitude") double latitude, @McpToolParam(description = "The location longitude") double longitude) { - - // Retrieve weather data - WeatherResponse weatherResponse = restClient - .get() - .uri("https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=temperature_2m", - latitude, longitude) - .retrieve() - .body(WeatherResponse.class); - - // Use MCP Sampling with model preferences - if (exchange.getClientCapabilities().sampling() != null) { - var messageRequestBuilder = McpSchema.CreateMessageRequest.builder() - .systemPrompt("You are a poet!") - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Please write a poem about this weather forecast...")))); - - // Request poem from OpenAI - var openAiRequest = messageRequestBuilder - .modelPreferences(ModelPreferences.builder().addHint("openai").build()) - .build(); - CreateMessageResult openAiResponse = exchange.createMessage(openAiRequest); - - // Request poem from Anthropic - var anthropicRequest = messageRequestBuilder - .modelPreferences(ModelPreferences.builder().addHint("anthropic").build()) - .build(); - CreateMessageResult anthropicResponse = exchange.createMessage(anthropicRequest); - } - - return combinedResponse; + // Fetch weather, request poems from OpenAI/Anthropic, return combined result } } ``` -Tool registration is handled automatically: +The server is a standard Spring Boot application: ```java @SpringBootApplication public class McpServerApplication { - @Bean - public List toolSpecs(WeatherService weatherService) { - return SyncMcpAnnotationProvider.createSyncToolSpecifications(List.of(weatherService)); + public static void main(String[] args) { + SpringApplication.run(McpServerApplication.class, args); } } ``` -### Annotation-Based Client Implementation +### 2. mcp-sampling-client-annotations -The client uses annotation-based handlers for MCP operations: +An MCP client implementation using annotation-based handlers that: +- Handles sampling requests using `@McpSampling` +- Routes requests to different LLM providers based on model hints +- Provides logging and progress tracking capabilities -```java -@Service -public class ClientMcpHandlers { - - @Autowired - Map chatClients; - - @McpSampling - public CreateMessageResult samplingHandler(CreateMessageRequest llmRequest) { - var userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text(); - String modelHint = llmRequest.modelPreferences().hints().get(0).name(); - - // Find the appropriate chat client based on the model hint - ChatClient hintedChatClient = chatClients.entrySet().stream() - .filter(e -> e.getKey().contains(modelHint)) - .findFirst() - .orElseThrow().getValue(); - - // Generate response using the selected model - String response = hintedChatClient.prompt() - .system(llmRequest.systemPrompt()) - .user(userPrompt) - .call() - .content(); - - return CreateMessageResult.builder() - .content(new McpSchema.TextContent(response)) - .build(); - } +#### Key Implementation - @McpLogging - public void loggingHandler(LoggingMessageNotification loggingMessage) { - logger.info("MCP LOGGING: [{}] {}", loggingMessage.level(), loggingMessage.data()); +```java +@SpringBootApplication +public class McpClientApplication { + public static void main(String[] args) { + SpringApplication.run(McpClientApplication.class, args).close(); } - - @McpProgress(clientId = "server1") - public void progressHandler(ProgressNotification progressNotification) { - logger.info("MCP PROGRESS: [{}] progress: {} total: {} message: {}", - progressNotification.progressToken(), progressNotification.progress(), - progressNotification.total(), progressNotification.message()); + @Bean + public CommandLineRunner predefinedQuestions(OpenAiChatModel openAiChatModel, List mcpClients) { + return args -> { + var mcpToolProvider = new SyncMcpToolCallbackProvider(mcpClients); + ChatClient chatClient = ChatClient.builder(openAiChatModel) + .defaultToolCallbacks(mcpToolProvider) + .build(); + String userQuestion = """ + What is the weather in Amsterdam right now? + Please incorporate all creative responses from all LLM providers. + After the other providers add a poem that synthesizes the poems from all the other providers. + """; + System.out.println("> USER: " + userQuestion); + System.out.println("> ASSISTANT: " + chatClient.prompt(userQuestion).call().content()); + }; } } ``` -Handler registration is managed through configuration: - -```java -@Configuration -public class AppConfiguration { +--- - @Bean - List samplingSpecs(ClientMcpHandlers clientMcpHandlers) { - return SyncMcpAnnotationProvider.createSyncSamplingSpecifications(List.of(clientMcpHandlers)); - } +## How It Works - @Bean - List loggingSpecs(ClientMcpHandlers clientMcpHandlers) { - return SyncMcpAnnotationProvider.createSyncLoggingSpecifications(List.of(clientMcpHandlers)); - } +- The server exposes a tool for weather queries and, if sampling is requested, generates poems using both OpenAI and Anthropic models. +- The client sends a prompt, receives creative responses from all LLM providers, and prints the result. +- Logging notifications are sent for sampling start and finish. +- The Open-Meteo API is used for real-time weather data. - @Bean - McpSyncClientCustomizer annotationMcpSyncClientCustomizer( - List loggingSpecs, - List samplingSpecs, - List elicitationSpecs, - List progressSpecs) { - return new AnnotationSyncClientCustomizer(samplingSpecs, loggingSpecs, elicitationSpecs, progressSpecs); - } -} -``` +--- ## Key Dependencies -Both projects use the Spring AI MCP Annotations library: - -```xml - - org.springaicommunity - spring-ai-mcp-annotations - 0.2.0-SNAPSHOT - -``` - ### Server Dependencies ```xml @@ -212,6 +153,8 @@ Both projects use the Spring AI MCP Annotations library: ``` +--- + ## Running the Examples ### Prerequisites @@ -246,6 +189,8 @@ cd mcp-sampling-client-annotations java -jar target/mcp-sampling-client-annotations-0.0.1-SNAPSHOT.jar ``` +--- + ## Configuration ### Stateless-HTTP Transport Configuration @@ -308,6 +253,8 @@ logging.level.io.modelcontextprotocol.client=WARN logging.level.io.modelcontextprotocol.spec=WARN ``` +--- + ## Sample Output When you run the annotation-based client, you'll see output similar to: @@ -342,6 +289,8 @@ Weather Data: } ``` +--- + ## Comparison with Traditional Implementation | Aspect | Traditional Approach | Annotation-Based Approach | @@ -353,6 +302,8 @@ Weather Data: | **Type Safety** | Manual parameter handling | Automatic parameter injection | | **Debugging** | Complex stack traces | Clearer annotation-based flow | +--- + ## Advantages of Annotation-Based Approach 1. **Reduced Boilerplate**: Less configuration code required @@ -362,6 +313,8 @@ Weather Data: 5. **Easier Testing**: Simplified unit testing of individual handlers 6. **Maintainability**: Cleaner separation of concerns +--- + ## Related Projects - **[Main Sampling Examples](../)**: Traditional implementation using manual configuration @@ -370,11 +323,24 @@ Weather Data: - **[SQLite Server](../../sqlite)**: Database access MCP server - **[Filesystem Server](../../filesystem)**: File system operations MCP server +--- + ## Additional Resources * [Spring AI Documentation](https://docs.spring.io/spring-ai/reference/) -* [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) -* [MCP Client Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) +* [Spring AI MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) +* [Spring AI MCP Client Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) +* [Spring AI Annotations](https://docs.spring.io/spring-ai/reference/1.1-SNAPSHOT/api/mcp/mcp-annotations-overview.html) +* [Java MCP Annotations](https://github.com/spring-ai-community/mcp-annotations) * [Model Context Protocol Specification](https://modelcontextprotocol.github.io/specification/) -* [Spring Boot Documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/) -* [Spring AI MCP Annotations](https://github.com/spring-projects-experimental/spring-ai-mcp-annotations) + +--- + +## Troubleshooting & FAQ + +- **Q: I get a connection refused error when running the client.** + - A: Make sure the server is running and accessible at the configured URL. +- **Q: I get authentication errors from OpenAI or Anthropic.** + - A: Ensure your API keys are set correctly in your environment or application.properties. +- **Q: How do I switch to STDIO transport?** + - A: Uncomment the relevant properties in `application.properties` as shown above. diff --git a/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/pom.xml b/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/pom.xml index e3797508..f30f5326 100644 --- a/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/pom.xml +++ b/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/pom.xml @@ -18,7 +18,6 @@ 17 1.1.0-SNAPSHOT - 0.2.0-SNAPSHOT @@ -35,12 +34,6 @@ - - org.springaicommunity - spring-ai-mcp-annotations - ${mcp-annotations.version} - - org.springframework.ai spring-ai-starter-mcp-client diff --git a/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/AppConfiguration.java b/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/AppConfiguration.java deleted file mode 100644 index 0c8ca242..00000000 --- a/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/AppConfiguration.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2025-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.ai.mcp.samples.client; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification; -import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification; -import org.springaicommunity.mcp.method.progress.SyncProgressSpecification; -import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification; -import org.springaicommunity.mcp.spring.SyncMcpAnnotationProviders; -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; -import org.springframework.ai.mcp.samples.client.customizers.AnnotationSyncClientCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class AppConfiguration { - - @Bean - List loggingSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.loggingSpecifications(List.of(clientMcpHandlers)); - } - - @Bean - List samplingSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.samplingSpecifications(List.of(clientMcpHandlers)); - } - - @Bean - List elicitationSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.elicitationSpecifications(List.of(clientMcpHandlers)); - } - - @Bean - List progressSpecs(McpClientHandlers clientMcpHandlers) { - return SyncMcpAnnotationProviders.progressSpecifications(List.of(clientMcpHandlers)); - } - - @Bean - McpSyncClientCustomizer annotationMcpSyncClientCustomizer(List loggingSpecs, - List samplingSpecs, List elicitationSpecs, - List progressSpecs) { - return new AnnotationSyncClientCustomizer(samplingSpecs, loggingSpecs, elicitationSpecs, progressSpecs); - } - - @Bean - public Map chatClients(List chatModels) { - return chatModels.stream().collect(Collectors.toMap(model -> model.getClass().getSimpleName().toLowerCase(), - model -> ChatClient.builder(model).build())); - - } -} \ No newline at end of file diff --git a/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java b/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java index 2934a673..9fecbd76 100644 --- a/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java +++ b/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java @@ -16,8 +16,11 @@ package org.springframework.ai.mcp.samples.client; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.boot.CommandLineRunner; @@ -54,4 +57,10 @@ public CommandLineRunner predefinedQuestions(OpenAiChatModel openAiChatModel, System.out.println("> ASSISTANT: " + chatClient.prompt(userQuestion).call().content()); }; } + + @Bean + public Map chatClients(List chatModels) { + return chatModels.stream().collect(Collectors.toMap(model -> model.getClass().getSimpleName().toLowerCase(), + model -> ChatClient.builder(model).build())); + } } \ No newline at end of file diff --git a/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/McpClientHandlers.java b/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/McpClientHandlers.java index 0977b68e..06a64a68 100644 --- a/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/McpClientHandlers.java +++ b/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/McpClientHandlers.java @@ -25,19 +25,19 @@ public class McpClientHandlers { @Autowired Map chatClients; - @McpProgress(clientId = "server1") + @McpProgress(clients = "server1") public void progressHandler(ProgressNotification progressNotification) { logger.info("MCP PROGRESS: [{}] progress: {} total: {} message: {}", progressNotification.progressToken(), progressNotification.progress(), progressNotification.total(), progressNotification.message()); } - @McpLogging + @McpLogging(clients = "server1") public void loggingHandler(LoggingMessageNotification loggingMessage) { logger.info("MCP LOGGING: [{}] {}", loggingMessage.level(), loggingMessage.data()); } - @McpSampling + @McpSampling(clients = "server1") public CreateMessageResult samplingHandler(CreateMessageRequest llmRequest) { logger.info("MCP SAMPLING: {}", llmRequest); diff --git a/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/customizers/AnnotationSyncClientCustomizer.java b/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/customizers/AnnotationSyncClientCustomizer.java deleted file mode 100644 index 9c87bb73..00000000 --- a/model-context-protocol/sampling/annotations/mcp-sampling-client-annotations/src/main/java/org/springframework/ai/mcp/samples/client/customizers/AnnotationSyncClientCustomizer.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2025-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.ai.mcp.samples.client.customizers; - -import java.util.List; - -import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification; -import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification; -import org.springaicommunity.mcp.method.progress.SyncProgressSpecification; -import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification; -import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; - -import io.modelcontextprotocol.client.McpClient.SyncSpec; - -/** - * NOTE: This class temporarily here. It will be moved to the Spring-AI project - */ -public class AnnotationSyncClientCustomizer implements McpSyncClientCustomizer { - - private final List syncSamplingSpecifications; - private final List syncLoggingSpecifications; - private final List syncElicitationSpecifications; - private final List syncProgressSpecifications; - - public AnnotationSyncClientCustomizer(List syncSamplingSpecifications, - List syncLoggingSpecifications, - List syncElicitationSpecifications, - List syncProgressSpecifications) { - - this.syncSamplingSpecifications = syncSamplingSpecifications; - this.syncLoggingSpecifications = syncLoggingSpecifications; - this.syncElicitationSpecifications = syncElicitationSpecifications; - this.syncProgressSpecifications = syncProgressSpecifications; - } - - @Override - public void customize(String name, SyncSpec clientSpec) { - - this.syncSamplingSpecifications.forEach(samplingSpec -> { - if (samplingSpec.clientId().isEmpty() || samplingSpec.clientId().equals(name)) { - clientSpec.sampling(samplingSpec.samplingHandler()); - } - }); - - this.syncLoggingSpecifications.forEach(loggingSpec -> { - if (loggingSpec.clientId().isEmpty() || loggingSpec.clientId().equals(name)) { - clientSpec.loggingConsumer(loggingSpec.loggingHandler()); - } - }); - - this.syncElicitationSpecifications.forEach(elicitationSpec -> { - if (elicitationSpec.clientId().isEmpty() || elicitationSpec.clientId().equals(name)) { - clientSpec.elicitation(elicitationSpec.elicitationHandler()); - } - }); - - this.syncProgressSpecifications.forEach(progressSpec -> { - if (progressSpec.clientId().isEmpty() || progressSpec.clientId().equals(name)) { - clientSpec.progressConsumer(progressSpec.progressHandler()); - } - }); - } - -} diff --git a/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/pom.xml b/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/pom.xml index b02c613b..04ff2e07 100644 --- a/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/pom.xml +++ b/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/pom.xml @@ -21,7 +21,6 @@ 17 1.1.0-SNAPSHOT - 0.2.0-SNAPSHOT @@ -37,12 +36,6 @@ - - org.springaicommunity - spring-ai-mcp-annotations - ${mcp-annotations.version} - - org.springframework.ai spring-ai-starter-mcp-server-webmvc diff --git a/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java b/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java index fbef4f19..b707c076 100644 --- a/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java +++ b/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java @@ -1,13 +1,7 @@ package org.springframework.ai.mcp.sample.server; -import java.util.List; - -import org.springaicommunity.mcp.spring.SyncMcpAnnotationProviders; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; - -import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification; @SpringBootApplication public class McpServerApplication { @@ -15,10 +9,4 @@ public class McpServerApplication { public static void main(String[] args) { SpringApplication.run(McpServerApplication.class, args); } - - @Bean - public List toolSpecs(WeatherService weatherService) { - return SyncMcpAnnotationProviders.toolSpecifications(List.of(weatherService)); - } - } diff --git a/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java b/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java index b2c2427d..be8c53fe 100644 --- a/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java +++ b/model-context-protocol/sampling/annotations/mcp-sampling-server-annotations/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java @@ -40,11 +40,7 @@ public class WeatherService { private static final Logger logger = org.slf4j.LoggerFactory.getLogger(WeatherService.class); - private final RestClient restClient; - - public WeatherService() { - this.restClient = RestClient.create(); - } + private final RestClient restClient = RestClient.create(); /** * The response format from the Open-Meteo API