From 41c921bc33e4ca431c94e222ada32ddd8c176643 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Tue, 18 Mar 2025 12:26:20 +0100 Subject: [PATCH] refactor: update MCP API usage to version 0.8.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove all book-library MCP examples (servlet, webflux, and webmvc implementations) - Update weather example to use MCP version 0.8.0-SNAPSHOT - Refactor transport handling to use transport providers instead of direct transport objects - Update method calls from toSyncToolRegistration to toSyncToolSpecifications - Add central-portal-snapshots repository to pom.xml for dependency resolution - Align with the new MCP client class names Add MCP Sampling capability with weather example Adds MCP Sampling implementation that demonstrates how to delegate LLM requests to multiple providers. - add a weather server that retrieves data and uses MCP Sampling to generate creative content - add a client that routes requests to different LLM providers (OpenAI and Anthropic) based on model hints - add README documentation explaining the MCP Sampling workflow and implementation details The MCP Sampling capability enables applications to leverage multiple LLM providers within a single workflow, allowing for creative content generation, model comparison, and specialized task delegation. refactor: migrate to spring-ai-mcp-client-spring-boot-starter - Replace spring-ai-mcp dependency with spring-ai-mcp-client-spring-boot-starter - Update import statements from org.springframework.ai.mcp.* to io.modelcontextprotocol.client.* - Replace McpFunctionCallback with SyncMcpToolCallbackProvider - Update Spring AI version from 1.0.0-M5 to 1.0.0-SNAPSHOT in multiple projects - Enable tool callback auto-configuration with spring.ai.mcp.client.toolcallback.enabled refactor: update Spring AI artifact IDs to new naming convention - Update all Spring AI dependencies to use the new naming convention: spring-ai-*-spring-boot-starter → spring-ai-starter-* spring-ai-openai-spring-boot-starter → spring-ai-starter-model-openai spring-ai-mcp-client-spring-boot-starter → spring-ai-starter-mcp-client - And similar patterns for other artifacts - Enable debug mode in brave module's application.properties Signed-off-by: Christian Tzolov --- mcp.weather.log | 4 + .../manual-servlet-server/README.md | 222 ------------- .../manual-servlet-server/pom.xml | 76 ----- .../servlet/server/McpServerConfig.java | 147 --------- .../server/McpServletServerApplication.java | 13 - .../sample/servlet/server/OpenLibrary.java | 78 ----- .../src/main/resources/application.properties | 10 - .../sample/servlet/client/ClientStdio.java | 39 --- .../sample/servlet/client/SampleClient.java | 81 ----- .../manual-webflux-server/.flattened-pom.xml | 48 --- .../.mvn/wrapper/maven-wrapper.properties | 19 -- .../manual-webflux-server/README.md | 257 --------------- .../book-library/manual-webflux-server/mvnw | 259 --------------- .../manual-webflux-server/mvnw.cmd | 149 --------- .../manual-webflux-server/pom.xml | 109 ------- .../sample/server/McpServerApplication.java | 13 - .../ai/mcp/sample/server/McpServerConfig.java | 163 ---------- .../ai/mcp/sample/server/OpenLibrary.java | 74 ----- .../src/main/resources/application.properties | 11 - .../ai/mcp/sample/client/ClientStdio.java | 43 --- .../mcp/sample/client/ClientWebFluxSse.java | 32 -- .../ai/mcp/sample/client/SampleClient.java | 80 ----- .../manual-webmvc-server/.flattened-pom.xml | 49 --- .../.mvn/wrapper/maven-wrapper.properties | 19 -- .../manual-webmvc-server/README.md | 257 --------------- .../book-library/manual-webmvc-server/mvnw | 259 --------------- .../manual-webmvc-server/mvnw.cmd | 149 --------- .../book-library/manual-webmvc-server/pom.xml | 114 ------- .../server/McpMvcServerApplication.java | 13 - .../sample/webmvc/server/McpServerConfig.java | 166 ---------- .../mcp/sample/webmvc/server/OpenLibrary.java | 74 ----- .../src/main/resources/application.properties | 10 - .../mcp/sample/webmvc/client/ClientStdio.java | 39 --- .../webmvc/client/ClientWebFluxSse.java | 32 -- .../sample/webmvc/client/HttpClientSse.java | 31 -- .../sample/webmvc/client/SampleClient.java | 80 ----- .../.mvn/wrapper/maven-wrapper.properties | 19 -- .../starter-webflux-server/README.md | 200 ------------ .../book-library/starter-webflux-server/mvnw | 259 --------------- .../starter-webflux-server/mvnw.cmd | 149 --------- .../sample/server/McpServerApplication.java | 117 ------- .../ai/mcp/sample/server/OpenLibrary.java | 82 ----- .../src/main/resources/application.properties | 9 - .../mcp/sample/client/ClientWebFluxSse.java | 33 -- .../ai/mcp/sample/client/SampleClient.java | 80 ----- model-context-protocol/brave/.DS_Store | Bin 0 -> 6148 bytes model-context-protocol/brave/pom.xml | 17 +- .../ai/mcp/samples/brave/Application.java | 31 +- .../src/main/resources/application.properties | 3 + .../main/resources/mcp-servers-config.json | 13 + .../starter-default-client/pom.xml | 6 +- .../src/main/resources/application.properties | 4 +- .../starter-webflux-client/pom.xml | 6 +- .../src/main/resources/application.properties | 5 +- model-context-protocol/filesystem/pom.xml | 9 +- .../mcp/samples/filesystem/Application.java | 31 +- model-context-protocol/sampling/README.md | 190 +++++++++++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 58727 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + .../sampling/mcp-sampling-client/README.md | 221 +++++++++++++ .../sampling/mcp-sampling-client/mvnw | 305 ++++++++++++++++++ .../sampling/mcp-sampling-client/mvnw.cmd | 172 ++++++++++ .../sampling/mcp-sampling-client/pom.xml | 94 ++++++ .../samples/client/McpClientApplication.java | 98 ++++++ .../src/main/resources/application.properties | 16 + .../.mvn/wrapper/maven-wrapper.properties | 0 .../mcp-weather-webmvc-server/README.md | 244 ++++++++++++++ .../mcp-weather-webmvc-server}/mvnw | 0 .../mcp-weather-webmvc-server}/mvnw.cmd | 0 .../mcp-weather-webmvc-server}/pom.xml | 14 +- .../sample/server/McpServerApplication.java | 20 ++ .../ai/mcp/sample/server/WeatherService.java | 117 +++++++ .../src/main/resources/application.properties | 13 + .../ai/mcp/sample/client/ClientSse.java} | 6 +- .../ai/mcp/sample/client/ClientStdio.java | 19 +- .../ai/mcp/sample/client/SampleClient.java | 69 ++++ model-context-protocol/sqlite/chatbot/pom.xml | 9 +- .../ai/mcp/samples/sqlite/Application.java | 25 +- model-context-protocol/sqlite/simple/pom.xml | 9 +- .../ai/mcp/samples/sqlite/Application.java | 26 +- .../weather/manual-webflux-server/pom.xml | 16 +- .../ai/mcp/sample/server/McpServerConfig.java | 24 +- .../ai/mcp/sample/client/SampleClient.java | 6 +- .../weather/starter-stdio-server/pom.xml | 2 +- .../weather/starter-webflux-server/pom.xml | 2 +- .../ai/mcp/sample/client/SampleClient.java | 6 +- .../weather/starter-webmvc-server/pom.xml | 2 +- .../src/main/resources/application.properties | 2 +- .../ai/mcp/sample/client/ClientStdio.java | 2 +- .../ai/mcp/sample/client/SampleClient.java | 6 +- .../web-search/brave-chatbot/pom.xml | 4 +- .../ai/mcp/samples/brave/Application.java | 7 +- .../web-search/brave-starter/README.md | 8 +- .../web-search/brave-starter/pom.xml | 6 +- .../ai/mcp/samples/brave/Application.java | 9 +- pom.xml | 9 +- 96 files changed, 1732 insertions(+), 4340 deletions(-) create mode 100644 mcp.weather.log delete mode 100644 model-context-protocol/book-library/manual-servlet-server/README.md delete mode 100644 model-context-protocol/book-library/manual-servlet-server/pom.xml delete mode 100644 model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/McpServerConfig.java delete mode 100644 model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/McpServletServerApplication.java delete mode 100644 model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/OpenLibrary.java delete mode 100644 model-context-protocol/book-library/manual-servlet-server/src/main/resources/application.properties delete mode 100644 model-context-protocol/book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/ClientStdio.java delete mode 100644 model-context-protocol/book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/SampleClient.java delete mode 100644 model-context-protocol/book-library/manual-webflux-server/.flattened-pom.xml delete mode 100644 model-context-protocol/book-library/manual-webflux-server/.mvn/wrapper/maven-wrapper.properties delete mode 100644 model-context-protocol/book-library/manual-webflux-server/README.md delete mode 100755 model-context-protocol/book-library/manual-webflux-server/mvnw delete mode 100644 model-context-protocol/book-library/manual-webflux-server/mvnw.cmd delete mode 100644 model-context-protocol/book-library/manual-webflux-server/pom.xml delete mode 100644 model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java delete mode 100644 model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerConfig.java delete mode 100644 model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/OpenLibrary.java delete mode 100644 model-context-protocol/book-library/manual-webflux-server/src/main/resources/application.properties delete mode 100644 model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java delete mode 100644 model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientWebFluxSse.java delete mode 100644 model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/.flattened-pom.xml delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/.mvn/wrapper/maven-wrapper.properties delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/README.md delete mode 100755 model-context-protocol/book-library/manual-webmvc-server/mvnw delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/mvnw.cmd delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/pom.xml delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/McpMvcServerApplication.java delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/McpServerConfig.java delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/OpenLibrary.java delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/src/main/resources/application.properties delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/ClientStdio.java delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/ClientWebFluxSse.java delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/HttpClientSse.java delete mode 100644 model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/SampleClient.java delete mode 100644 model-context-protocol/book-library/starter-webflux-server/.mvn/wrapper/maven-wrapper.properties delete mode 100644 model-context-protocol/book-library/starter-webflux-server/README.md delete mode 100755 model-context-protocol/book-library/starter-webflux-server/mvnw delete mode 100644 model-context-protocol/book-library/starter-webflux-server/mvnw.cmd delete mode 100644 model-context-protocol/book-library/starter-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java delete mode 100644 model-context-protocol/book-library/starter-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/OpenLibrary.java delete mode 100644 model-context-protocol/book-library/starter-webflux-server/src/main/resources/application.properties delete mode 100644 model-context-protocol/book-library/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientWebFluxSse.java delete mode 100644 model-context-protocol/book-library/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java create mode 100644 model-context-protocol/brave/.DS_Store create mode 100644 model-context-protocol/brave/src/main/resources/mcp-servers-config.json create mode 100644 model-context-protocol/sampling/README.md create mode 100644 model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.jar create mode 100644 model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.properties create mode 100644 model-context-protocol/sampling/mcp-sampling-client/README.md create mode 100755 model-context-protocol/sampling/mcp-sampling-client/mvnw create mode 100755 model-context-protocol/sampling/mcp-sampling-client/mvnw.cmd create mode 100644 model-context-protocol/sampling/mcp-sampling-client/pom.xml create mode 100644 model-context-protocol/sampling/mcp-sampling-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java create mode 100644 model-context-protocol/sampling/mcp-sampling-client/src/main/resources/application.properties rename model-context-protocol/{book-library/manual-servlet-server => sampling/mcp-weather-webmvc-server}/.mvn/wrapper/maven-wrapper.properties (100%) create mode 100644 model-context-protocol/sampling/mcp-weather-webmvc-server/README.md rename model-context-protocol/{book-library/manual-servlet-server => sampling/mcp-weather-webmvc-server}/mvnw (100%) rename model-context-protocol/{book-library/manual-servlet-server => sampling/mcp-weather-webmvc-server}/mvnw.cmd (100%) rename model-context-protocol/{book-library/starter-webflux-server => sampling/mcp-weather-webmvc-server}/pom.xml (85%) create mode 100644 model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java create mode 100644 model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java create mode 100644 model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/resources/application.properties rename model-context-protocol/{book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/HttpClientSse.java => sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java} (91%) rename model-context-protocol/{book-library/starter-webflux-server => sampling/mcp-weather-webmvc-server}/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java (69%) create mode 100644 model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java diff --git a/mcp.weather.log b/mcp.weather.log new file mode 100644 index 0000000..f4ae0ed --- /dev/null +++ b/mcp.weather.log @@ -0,0 +1,4 @@ +2025-03-17T21:20:53.178+01:00 INFO 52952 --- [main] o.s.a.m.s.server.McpServerApplication : Starting McpServerApplication using Java 17.0.12 with PID 52952 (/Users/christiantzolov/Dev/projects/spring-ai-examples/model-context-protocol/weather/manual-webflux-server/target/classes started by christiantzolov in /Users/christiantzolov/Dev/projects/spring-ai-examples) +2025-03-17T21:20:53.179+01:00 INFO 52952 --- [main] o.s.a.m.s.server.McpServerApplication : No active profile set, falling back to 1 default profile: "default" +2025-03-17T21:20:53.788+01:00 INFO 52952 --- [main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8080 (http) +2025-03-17T21:20:53.797+01:00 INFO 52952 --- [main] o.s.a.m.s.server.McpServerApplication : Started McpServerApplication in 0.782 seconds (process running for 0.909) diff --git a/model-context-protocol/book-library/manual-servlet-server/README.md b/model-context-protocol/book-library/manual-servlet-server/README.md deleted file mode 100644 index 1fadaac..0000000 --- a/model-context-protocol/book-library/manual-servlet-server/README.md +++ /dev/null @@ -1,222 +0,0 @@ -# Spring AI MCP Servlet SSE Server Sample Project - -This sample project demonstrates the usage of the Spring AI Model Context Protocol (MCP) implementation with HttpServletSseServerTransport. It showcases how to create and use MCP servers with Servlet-based SSE transport and various capabilities. - -## Overview - -The sample provides: -- Two transport mode implementations: - - HttpServletSseServerTransport for SSE (Server-Sent Events) - - StdioServerTransport for standard I/O -- Server capabilities: - - Tools support with list changes notifications - - Resources support with list changes notifications (no subscribe support) - - Prompts support with list changes notifications -- Sample implementations: - - Two MCP tools: Weather and Calculator - - One MCP resource: System Information - - One MCP prompt: Greeting - -## Building the Project - -```bash -./mvnw clean install -DskipTests -``` - -## Running the Server - -The server can be started in two transport modes, controlled by the `transport.mode` property: - -### Stdio Mode (Default) - -```bash -java -Dtransport.mode=stdio -jar target/mcp-servlet-server-0.0.1-SNAPSHOT.jar -``` - -The Stdio mode server is automatically started by the client - no explicit server startup is needed. -But you have to build the server jar first: `./mvnw clean install -DskipTests`. - -In Stdio mode the server must not emit any messages/logs to the console (e.g. standard out) but the JSON messages produced by the server. - -### SSE Mode with HttpServletSseServerTransport -```bash -java -Dtransport.mode=sse -jar target/mcp-servlet-server-0.0.1-SNAPSHOT.jar -``` - -The HttpServletSseServerTransport provides two endpoints: -- SSE endpoint at `/sse` - For server-to-client events -- Message endpoint at `/mcp/message` - For client-to-server messages - -## Sample Clients - -The project includes example clients for both transport modes: - -### Stdio Client (ClientStdio.java) -```java -var stdioParams = ServerParameters.builder("java") - .args("-Dtransport.mode=stdio", "-jar", - "target/mcp-servlet-server-0.0.1-SNAPSHOT.jar") - .build(); - -var transport = new StdioClientTransport(stdioParams); -var client = McpClient.using(transport).sync(); -``` - -### SSE Client (HttpClientSse.java) -```java -var transport = new HttpClientSseClientTransport("http://localhost:8080"); -var client = McpClient.using(transport).sync(); -``` - -## Available Tools - -### Weather Tool -- Name: `weather` -- Description: Weather forecast tool by location -- Parameters: - - `city`: String - The city to get weather for -- Example: -```java -CallToolResult response = client.callTool( - new CallToolRequest("weather", Map.of("city", "Sofia")) -); -``` - -### Calculator Tool -- Name: `calculator` -- Description: Performs basic arithmetic operations -- Parameters (JSON Schema): - ```json - { - "operation": { - "type": "string", - "enum": ["add", "subtract", "multiply", "divide"], - "description": "The arithmetic operation to perform" - }, - "a": { - "type": "number", - "description": "First operand" - }, - "b": { - "type": "number", - "description": "Second operand" - } - } - ``` -- Example: -```java -CallToolResult response = client.callTool( - new CallToolRequest("calculator", - Map.of("operation", "multiply", "a", 2.0, "b", 3.0)) -); -``` - -## Available Resources - -### System Information Resource -- URI: `system://info` -- Description: Provides basic system information including Java version, OS, etc. -- MIME Type: application/json -- Returns: JSON object containing: javaVersion, osName, osVersion, osArch, processors, timestamp - -## Available Prompts - -### Greeting Prompt -- Name: `greeting` -- Description: A friendly greeting prompt -- Parameters: - - `name`: String (required) - The name to greet -- Returns: A personalized greeting message from an assistant - -## Client Usage Example - -```java -// Initialize client -client.initialize(); - -// Test connection -client.ping(); - -// List available tools -ListToolsResult tools = client.listTools(); -System.out.println("Available tools: " + tools); - -// Call weather tool -CallToolResult weather = client.callTool( - new CallToolRequest("weather", Map.of("city", "Sofia")) -); -System.out.println("Weather: " + weather); - -// Access system info resource -ReadResourceResult sysInfo = client.readResource( - new ReadResourceRequest("system://info") -); -System.out.println("System Info: " + sysInfo); - -// Use greeting prompt -GetPromptResult greeting = client.getPrompt( - new GetPromptRequest("greeting", Map.of("name", "John")) -); -System.out.println("Greeting: " + greeting); - -// Close client -client.closeGracefully(); -``` - -## Server Configuration Example - -```java -@Configuration -@EnableWebMvc -public class McpServerConfig implements WebMvcConfigurer { - - @Bean - @ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse") - public HttpServletSseServerTransport webSseServerTransport() { - return new HttpServletSseServerTransport(new ObjectMapper(), "/mcp/message"); - } - - @Bean - @ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse") - public ServletRegistrationBean customServletBean(HttpServletSseServerTransport servlet) { - return new ServletRegistrationBean(servlet, "/sse", "/mcp/message"); - } - - @Bean - public McpAsyncServer mcpServer(ServerMcpTransport transport) { - // Configure server capabilities - var capabilities = McpSchema.ServerCapabilities.builder() - .resources(false, true) // Resource support with list changes notifications - .tools(true) // Tool support with list changes notifications - .prompts(true) // Prompt support with list changes notifications - .build(); - - // Create and configure the server - return McpServer.using(transport) - .serverInfo("MCP Demo Server", "1.0.0") - .capabilities(capabilities) - // Add your tools, resources, and prompts here - .async(); - } -} -``` - -## Configuration - -The application can be configured through `application.properties`: - -- `transport.mode`: Transport mode to use (stdio/sse) -- `server.port`: Server port for SSE mode (default: 8080) -- Various logging configurations are available for debugging - -## Implementation Details - -The sample demonstrates: -- Using HttpServletSseServerTransport for SSE-based communication -- Creating an MCP server with custom tools, resources, and prompts -- Configuring different transport modes -- Implementing tool handlers with JSON schema validation -- Resource implementations with dynamic content generation -- Prompt implementations with parameter handling -- Error handling and response formatting -- Synchronous client usage patterns diff --git a/model-context-protocol/book-library/manual-servlet-server/pom.xml b/model-context-protocol/book-library/manual-servlet-server/pom.xml deleted file mode 100644 index 2394605..0000000 --- a/model-context-protocol/book-library/manual-servlet-server/pom.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.3.6 - - - - com.example - mcp-servlet-server - 0.0.1-SNAPSHOT - - Spring AI MCP Servlet Server Sample - Sample application demonstrating MCP Servlet server usage - - - 1.0.0-SNAPSHOT - - - - - - org.springframework.ai - spring-ai-bom - ${sprign-ai.version} - pom - import - - - - - - - org.springframework.ai - spring-ai-mcp - - - - org.springframework.boot - spring-boot-starter-web - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - false - - - - - \ No newline at end of file diff --git a/model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/McpServerConfig.java b/model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/McpServerConfig.java deleted file mode 100644 index 67f6d37..0000000 --- a/model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/McpServerConfig.java +++ /dev/null @@ -1,147 +0,0 @@ -package org.springframework.ai.mcp.sample.servlet.server; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.server.McpServer; -import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptRegistration; -import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceRegistration; -import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration; -import io.modelcontextprotocol.server.McpSyncServer; -import io.modelcontextprotocol.server.transport.HttpServletSseServerTransport; -import io.modelcontextprotocol.server.transport.StdioServerTransport; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.PromptMessage; -import io.modelcontextprotocol.spec.McpSchema.Role; -import io.modelcontextprotocol.spec.McpSchema.TextContent; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.mcp.McpToolUtils; -import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.tool.ToolCallbacks; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.web.servlet.ServletRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -@EnableWebMvc -public class McpServerConfig implements WebMvcConfigurer { - - private static final Logger logger = LoggerFactory.getLogger(McpServerConfig.class); - - @Bean - @ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse") - public HttpServletSseServerTransport webMvcSseServerTransport() { - return new HttpServletSseServerTransport(new ObjectMapper(), "/mcp/message"); - } - - @Bean - @ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse") - public ServletRegistrationBean customServletBean(HttpServletSseServerTransport servlet) { - return new ServletRegistrationBean(servlet); - } - - // STDIO transport - @Bean - @ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "stdio") - public StdioServerTransport stdioServerTransport() { - return new StdioServerTransport(); - } - - public static record ToUpperCaseInput(String input) { - } - - @Bean - public McpSyncServer mcpServer(ServerMcpTransport transport, OpenLibrary openLibrary) { // @formatter:off - - // Configure server capabilities with resource support - var capabilities = McpSchema.ServerCapabilities.builder() - .resources(false, true) // No subscribe support, but list changes notifications - .tools(true) // Tool support with list changes notifications - .prompts(true) // Prompt support with list changes notifications - .logging() // Logging support - .build(); - - // Create the server with both tool and resource capabilities - var server = McpServer.sync(transport) - .serverInfo("MCP Demo Servlet Server", "1.0.0") - .capabilities(capabilities) - .resources(systemInfoResourceRegistration()) - .prompts(greetingPromptRegistration()) - .tools( - McpToolUtils.toSyncToolRegistration( - FunctionToolCallback.builder("toUpperCase", (Function) s -> s.input().toUpperCase()) - .description("To upper case") - .inputType(ToUpperCaseInput.class) - .build())) - .tools(openLibraryToolRegistrations(openLibrary)) - .build(); - return server; // @formatter:on - } // @formatter:on - - public static List openLibraryToolRegistrations(OpenLibrary openLibrary) { - ToolCallback[] tools = ToolCallbacks.from(openLibrary); - return McpToolUtils.toSyncToolRegistration(tools); - } - - private static SyncResourceRegistration systemInfoResourceRegistration() { - - // Create a resource registration for system information - var systemInfoResource = new McpSchema.Resource( // @formatter:off - "system://info", - "System Information", - "Provides basic system information including Java version, OS, etc.", - "application/json", null - ); - - var resourceRegistration = new SyncResourceRegistration(systemInfoResource, (request) -> { - try { - var systemInfo = Map.of( - "javaVersion", System.getProperty("java.version"), - "osName", System.getProperty("os.name"), - "osVersion", System.getProperty("os.version"), - "osArch", System.getProperty("os.arch"), - "processors", Runtime.getRuntime().availableProcessors(), - "timestamp", System.currentTimeMillis()); - - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); - - return new McpSchema.ReadResourceResult( - List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent))); - } - catch (Exception e) { - throw new RuntimeException("Failed to generate system info", e); - } - }); // @formatter:on - - return resourceRegistration; - } - - private static SyncPromptRegistration greetingPromptRegistration() { - - var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt", - List.of(new McpSchema.PromptArgument("name", "The name to greet", true))); - - return new SyncPromptRegistration(prompt, getPromptRequest -> { - - String nameArgument = (String) getPromptRequest.arguments().get("name"); - if (nameArgument == null) { - nameArgument = "friend"; - } - - var userMessage = new PromptMessage(Role.USER, - new TextContent("Hello " + nameArgument + "! How can I assist you today?")); - - return new GetPromptResult("A personalized greeting message", List.of(userMessage)); - }); - } -} diff --git a/model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/McpServletServerApplication.java b/model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/McpServletServerApplication.java deleted file mode 100644 index 146dc7f..0000000 --- a/model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/McpServletServerApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.springframework.ai.mcp.sample.servlet.server; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class McpServletServerApplication { - - public static void main(String[] args) { - SpringApplication.run(McpServletServerApplication.class, args); - } - -} diff --git a/model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/OpenLibrary.java b/model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/OpenLibrary.java deleted file mode 100644 index 47bb548..0000000 --- a/model-context-protocol/book-library/manual-servlet-server/src/main/java/org/springframework/ai/mcp/sample/servlet/server/OpenLibrary.java +++ /dev/null @@ -1,78 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.servlet.server; - -import java.util.List; -import java.util.Map; - -import org.springframework.ai.tool.annotation.Tool; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClient; - -/** - * @author Christian Tzolov - */ -@Service -public class OpenLibrary { - - private RestClient restClient; - - public OpenLibrary(RestClient.Builder restClientBuilder) { - this.restClient = restClientBuilder.baseUrl("https://openlibrary.org").build(); - } - - public record Books(Integer numFound, Integer start, Boolean numFoundExact, List> docs) { - } - - public record Book(List isbn, String title, List authorName) { - } - - @Tool(description = "Search for books by title") - public List getBooks(String title) { - Books books = restClient.get() - .uri(uriBuilder -> uriBuilder.path("/search.json").queryParam("q", title).build()) - .retrieve() - .body(Books.class); - if (books == null) { - return List.of(); - } - return books.docs.stream() - .map(doc -> new Book((List) doc.get("isbn"), (String) doc.get("title"), - (List) doc.get("author_name"))) - .toList(); - } - - @Tool(description = "Search for books by author") - public List getBookTitlesByAuthor(String authorName) { - var books = restClient.get() - .uri(uriBuilder -> uriBuilder.path("/search/authors.json").queryParam("q", authorName).build()) - .retrieve() - .body(Books.class); - if (books == null) { - return List.of(); - } - return books.docs.stream().map(doc -> (String) doc.get("top_work")).toList(); - } - - public static void main(String[] args) { - OpenLibrary openLibrary = new OpenLibrary(RestClient.builder()); - List books = openLibrary.getBooks("Spring Framework"); - System.out.println(books); - List booksByAuthor = openLibrary.getBookTitlesByAuthor("Craig Walls"); - System.out.println(booksByAuthor); - } - -} diff --git a/model-context-protocol/book-library/manual-servlet-server/src/main/resources/application.properties b/model-context-protocol/book-library/manual-servlet-server/src/main/resources/application.properties deleted file mode 100644 index c4bd128..0000000 --- a/model-context-protocol/book-library/manual-servlet-server/src/main/resources/application.properties +++ /dev/null @@ -1,10 +0,0 @@ -spring.main.web-application-type=SERVLET - -# NOTE: You must disable the banner and the console logging -# to allow the STDIO transport to work !!! -spring.main.banner-mode=off -logging.pattern.console= - -transport.mode=sse - -logging.file.name=mcp.servlet-server.log \ No newline at end of file diff --git a/model-context-protocol/book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/ClientStdio.java b/model-context-protocol/book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/ClientStdio.java deleted file mode 100644 index 6c7d857..0000000 --- a/model-context-protocol/book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/ClientStdio.java +++ /dev/null @@ -1,39 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.servlet.client; - -import io.modelcontextprotocol.client.transport.ServerParameters; -import io.modelcontextprotocol.client.transport.StdioClientTransport; - -/** - * With stdio transport, the MCP server is automatically started by the client. But you - * have to build the server jar first:
./mvnw clean install -DskipTests
- */ -public class ClientStdio { - - public static void main(String[] args) { - - var stdioParams = ServerParameters.builder("java") - .args("-Dtransport.mode=stdio", "-Dspring.main.web-application-type=none", "-jar", - "model-context-protocol/book-library/manual-servlet-server/target/mcp-servlet-server-0.0.1-SNAPSHOT.jar") - .build(); - - var transport = new StdioClientTransport(stdioParams); - - new SampleClient(transport).run(); - } - -} diff --git a/model-context-protocol/book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/SampleClient.java b/model-context-protocol/book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/SampleClient.java deleted file mode 100644 index 9d37bd4..0000000 --- a/model-context-protocol/book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/SampleClient.java +++ /dev/null @@ -1,81 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.servlet.client; - -import java.util.Map; - -import io.modelcontextprotocol.client.McpClient; -import io.modelcontextprotocol.spec.ClientMcpTransport; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult; -import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; -import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; -import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; - - -/** - * @author Christian Tzolov - */ - -public class SampleClient { - - private final ClientMcpTransport transport; - - public SampleClient(ClientMcpTransport transport) { - this.transport = transport; - } - - public void run() { - - var client = McpClient.sync(this.transport).build(); - - client.initialize(); - - client.ping(); - - // List and demonstrate tools - ListToolsResult toolsList = client.listTools(); - System.out.println("Available Tools = " + toolsList); - - CallToolResult upperCaseResult = client - .callTool(new CallToolRequest("toUpperCase", Map.of("input", "accountName"))); - System.out.println("Upper case Response = " + upperCaseResult); - - CallToolResult books = client.callTool(new CallToolRequest("getBooks", Map.of("title", "Spring Framework"))); - System.out.println("Books Response = " + books); - - // List and demonstrate resources - var resourcesList = client.listResources(); - System.out.println("\nAvailable Resources = " + resourcesList); - - // Read the system info resource - var systemInfo = client.readResource(new ReadResourceRequest("system://info")); - System.out.println("System Info = " + systemInfo); - - // List and demonstrate prompts - ListPromptsResult promptsList = client.listPrompts(); - System.out.println("\nAvailable Prompts = " + promptsList); - - // Try the greeting prompt - var greetingResponse = client.getPrompt(new GetPromptRequest("greeting", Map.of("name", "Spring"))); - System.out.println("Greeting Response = " + greetingResponse); - - client.closeGracefully(); - - } - -} diff --git a/model-context-protocol/book-library/manual-webflux-server/.flattened-pom.xml b/model-context-protocol/book-library/manual-webflux-server/.flattened-pom.xml deleted file mode 100644 index 74d4c41..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/.flattened-pom.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - 4.0.0 - org.springframework.experimental - spring-ai-mcp-sample - 0.5.0-SNAPSHOT - Spring AI MCP Sample - Sample Spring Boot application demonstrating MCP server usage - - Spring Framework - https://spring.io - - - - Apache 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - Christian Tzolov - - - Dariusz Jędrzejczyk - - - - - org.springframework.experimental - mcp-webflux-sse-transport - 0.5.0-SNAPSHOT - compile - - - org.springframework.experimental - spring-ai-mcp - 0.5.0-SNAPSHOT - compile - - - org.springframework.boot - spring-boot-starter-webflux - 3.4.1 - compile - - - \ No newline at end of file diff --git a/model-context-protocol/book-library/manual-webflux-server/.mvn/wrapper/maven-wrapper.properties b/model-context-protocol/book-library/manual-webflux-server/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index d58dfb7..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,19 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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 -# -# http://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. -wrapperVersion=3.3.2 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/model-context-protocol/book-library/manual-webflux-server/README.md b/model-context-protocol/book-library/manual-webflux-server/README.md deleted file mode 100644 index 619a75f..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/README.md +++ /dev/null @@ -1,257 +0,0 @@ -# Spring AI MCP Sample Project - -This sample project demonstrates the usage of the Spring AI Model Context Protocol (MCP) implementation. It showcases how to create and use MCP servers and clients with different transport modes and capabilities. - -## Overview - -The sample provides: -- Two transport mode implementations: Stdio and SSE (Server-Sent Events) -- Server capabilities: - - Tools support with list changes notifications - - Resources support with list changes notifications (no subscribe support) - - Prompts support with list changes notifications -- Sample implementations: - - Two MCP tools: Weather and Calculator - - One MCP resource: System Information - - One MCP prompt: Greeting - -## Building the Project - -```bash -./mvnw clean package -``` - -## Running the Server - -The server can be started in two transport modes, controlled by the `transport.mode` property: - -### Stdio Mode (Default) - -```bash -java -Dtransport.mode=stdio -jar model-context-protocol/mcp-webflux-server/target/mcp-webflux-server-0.0.1-SNAPSHOT.jar -``` - -The Stdio mode server is automatically started by the client - no explicit server startup is needed. -But you have to build the server jar first: `./mvnw clean install -DskipTests`. - -In Stdio mode the server must not emit any messages/logs to the console (e.g. standard out) but the JSON messages produced by the server. - -### SSE Mode -```bash -java -Dtransport.mode=sse -jar model-context-protocol/mcp-webflux-server/target/mcp-webflux-server-0.0.1-SNAPSHOT.jar -``` - -## Sample Clients - -The project includes example clients for both transport modes: - -### Stdio Client (ClientStdio.java) -```java -var stdioParams = ServerParameters.builder("java") - .args("-Dtransport.mode=stdio", "-jar", - "model-context-protocol/mcp-webflux-server/target/mcp-webflux-server-0.0.1-SNAPSHOT.jar") - .build(); - -var transport = new StdioClientTransport(stdioParams); -var client = McpClient.using(transport).sync(); -``` - -### SSE Client (ClientWebFluxSse.java) -```java -var transport = new SseClientTransport(WebClient.builder().baseUrl("http://localhost:8080")); -var client = McpClient.using(transport).sync(); -``` - -## Available Tools - -### Weather Tool -- Name: `weather` -- Description: Weather forecast tool by location -- Parameters: - - `city`: String - The city to get weather for -- Example: -```java -CallToolResult response = client.callTool( - new CallToolRequest("weather", Map.of("city", "Sofia")) -); -``` - -### Calculator Tool -- Name: `calculator` -- Description: Performs basic arithmetic operations -- Parameters (JSON Schema): - ```json - { - "operation": { - "type": "string", - "enum": ["add", "subtract", "multiply", "divide"], - "description": "The arithmetic operation to perform" - }, - "a": { - "type": "number", - "description": "First operand" - }, - "b": { - "type": "number", - "description": "Second operand" - } - } - ``` -- Example: -```java -CallToolResult response = client.callTool( - new CallToolRequest("calculator", - Map.of("operation", "multiply", "a", 2.0, "b", 3.0)) -); -``` - -## Available Resources - -### System Information Resource -- URI: `system://info` -- Description: Provides basic system information including Java version, OS, etc. -- MIME Type: application/json -- Returns: JSON object containing: javaVersion, osName, osVersion, osArch, processors, timestamp - -## Available Prompts - -### Greeting Prompt -- Name: `greeting` -- Description: A friendly greeting prompt -- Parameters: - - `name`: String (required) - The name to greet -- Returns: A personalized greeting message from an assistant - -## Client Usage Example - -```java -// Initialize client -client.initialize(); - -// Test connection -client.ping(); - -// List available tools -ListToolsResult tools = client.listTools(); -System.out.println("Available tools: " + tools); - -// Call weather tool -CallToolResult weather = client.callTool( - new CallToolRequest("weather", Map.of("city", "Sofia")) -); -System.out.println("Weather: " + weather); - -// Call calculator tool -CallToolResult calc = client.callTool( - new CallToolRequest("calculator", - Map.of("operation", "multiply", "a", 2.0, "b", 3.0)) -); -System.out.println("Calculation: " + calc); - -// Access system info resource -ReadResourceResult sysInfo = client.readResource( - new ReadResourceRequest("system://info") -); -System.out.println("System Info: " + sysInfo); - -// Use greeting prompt -GetPromptResult greeting = client.getPrompt( - new GetPromptRequest("greeting", Map.of("name", "John")) -); -System.out.println("Greeting: " + greeting); - -// Close client -client.closeGracefully(); -``` - -## Server Usage Example - -```java -@Configuration -public class CustomMcpServerConfig { - @Bean - public McpAsyncServer mcpServer(McpTransport transport) { - // Configure server capabilities - var capabilities = McpSchema.ServerCapabilities.builder() - .resources(false, true) // Resource support with list changes notifications - .tools(true) // Tool support with list changes notifications - .prompts(true) // Prompt support with list changes notifications - .build(); - - // Create custom tool - var customTool = new ToolRegistration( - new McpSchema.Tool("custom-tool", "Description", Map.of("param", "String")), - (arguments) -> { - String param = (String) arguments.get("param"); - return new CallToolResult( - List.of(new TextContent("Result: " + param)), - false - ); - } - ); - - // Create custom resource - var customResource = new ResourceRegistration( - new McpSchema.Resource( - "custom://resource", - "Custom Resource", - "Description", - "application/json", - null - ), - (request) -> new ReadResourceResult( - List.of(new TextResourceContents( - request.uri(), - "application/json", - "{\"data\": \"example\"}" - )) - ) - ); - - // Create custom prompt - var customPrompt = new PromptRegistration( - new McpSchema.Prompt( - "custom-prompt", - "Description", - List.of(new McpSchema.PromptArgument("input", "Description", true)) - ), - request -> { - String input = (String) request.arguments().get("input"); - var message = new PromptMessage( - Role.ASSISTANT, - new TextContent("Response to: " + input) - ); - return new GetPromptResult("Result", List.of(message)); - } - ); - - // Create and configure the server - return McpServer.using(transport) - .info("Custom MCP Server", "1.0.0") - .capabilities(capabilities) - .tools(List.of(customTool)) - .resources(Map.of("custom://resource", customResource)) - .prompts(List.of(customPrompt)) - .async(); - } -} -``` - -## Configuration - -The application can be configured through `application.properties`: - -- `transport.mode`: Transport mode to use (stdio/sse) -- `server.port`: Server port for SSE mode (default: 8080) -- Various logging configurations are available for debugging - -## Implementation Details - -The sample demonstrates: -- Creating an MCP server with custom tools, resources, and prompts -- Configuring different transport modes -- Implementing tool handlers with JSON schema validation -- Resource implementations with dynamic content generation -- Prompt implementations with parameter handling -- Error handling and response formatting -- Synchronous client usage patterns diff --git a/model-context-protocol/book-library/manual-webflux-server/mvnw b/model-context-protocol/book-library/manual-webflux-server/mvnw deleted file mode 100755 index 19529dd..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/mvnw +++ /dev/null @@ -1,259 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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 -# -# http://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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/model-context-protocol/book-library/manual-webflux-server/mvnw.cmd b/model-context-protocol/book-library/manual-webflux-server/mvnw.cmd deleted file mode 100644 index b150b91..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/mvnw.cmd +++ /dev/null @@ -1,149 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" -if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" -} -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/model-context-protocol/book-library/manual-webflux-server/pom.xml b/model-context-protocol/book-library/manual-webflux-server/pom.xml deleted file mode 100644 index 9e5ab7f..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/pom.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.3.6 - - - - com.example - - mcp-webflux-server - 0.0.1-SNAPSHOT - - Spring AI MCP StdIO and WebFluxSSE Sample - Sample Spring Boot application demonstrating MCP server usage - - - 0.6.0 - - - - - - org.springframework.experimental - mcp-bom - ${spring-ai-mcp.version} - pom - import - - - - - - - org.springframework.experimental - mcp-webflux-sse-transport - - - - org.springframework.experimental - spring-ai-mcp - - - - org.springframework.boot - spring-boot-starter-webflux - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - repackage - - - - - - io.spring.javaformat - spring-javaformat-maven-plugin - 0.0.43 - - - validate - true - - validate - - - - - - - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - false - - - - - \ No newline at end of file diff --git a/model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java b/model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java deleted file mode 100644 index 586f8d8..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.springframework.ai.mcp.sample.server; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class McpServerApplication { - - public static void main(String[] args) { - SpringApplication.run(McpServerApplication.class, args); - } - -} diff --git a/model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerConfig.java b/model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerConfig.java deleted file mode 100644 index 05e4bfd..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerConfig.java +++ /dev/null @@ -1,163 +0,0 @@ -package org.springframework.ai.mcp.sample.server; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.mcp.server.McpServer; -import org.springframework.ai.mcp.server.McpServerFeatures; -import org.springframework.ai.mcp.server.McpSyncServer; -import org.springframework.ai.mcp.server.transport.StdioServerTransport; -import org.springframework.ai.mcp.server.transport.WebFluxSseServerTransport; -import org.springframework.ai.mcp.spec.McpSchema; -import org.springframework.ai.mcp.spec.McpSchema.GetPromptResult; -import org.springframework.ai.mcp.spec.McpSchema.PromptMessage; -import org.springframework.ai.mcp.spec.McpSchema.Role; -import org.springframework.ai.mcp.spec.McpSchema.TextContent; -import org.springframework.ai.mcp.spec.ServerMcpTransport; -import org.springframework.ai.mcp.spring.ToolHelper; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.server.RouterFunction; - -@Configuration -public class McpServerConfig { - - private static final Logger logger = LoggerFactory.getLogger(McpServerConfig.class); - - // STDIO transport - @Bean - @ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "stdio") - public StdioServerTransport stdioServerTransport() { - return new StdioServerTransport(); - } - - // SSE transport - @Bean - @ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse") - public WebFluxSseServerTransport sseServerTransport() { - return new WebFluxSseServerTransport(new ObjectMapper(), "/mcp/message"); - } - - // Router function for SSE transport used by Spring WebFlux to start an HTTP - // server. - @Bean - @ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse") - public RouterFunction mcpRouterFunction(WebFluxSseServerTransport transport) { - return transport.getRouterFunction(); - } - - public static record ToUpperCaseInput(String input) { - } - - @Bean - public McpSyncServer mcpServer(ServerMcpTransport transport, OpenLibrary openLibrary) { // @formatter:off - - // Configure server capabilities with resource support - var capabilities = McpSchema.ServerCapabilities.builder() - .resources(false, true) // No subscribe support, but list changes notifications - .tools(true) // Tool support with list changes notifications - .prompts(true) // Prompt support with list changes notifications - .logging() // Logging support - .build(); - - // Create the server with both tool and resource capabilities - McpSyncServer server = McpServer.sync(transport) - .serverInfo("MCP Demo WebFlux Server", "1.0.0") - .capabilities(capabilities) - .resources(systemInfoResourceRegistration()) // Resources - .prompts(greetingPromptRegistration()) // Prompts - .tools(openLibraryToolRegistrations(openLibrary)) // Method based tools - .tools(ToolHelper.toSyncToolRegistration( // java.util.Function based tools - FunctionCallback.builder() - .function("toUpperCase", (Function) s -> s.input().toUpperCase()) - .description("To upper case") - .inputType(ToUpperCaseInput.class) - .build())) - .build(); - - return server; // @formatter:on - } // @formatter:on - - public static List openLibraryToolRegistrations(OpenLibrary openLibrary) { - - var books = FunctionCallback.builder() - .method("getBooks", String.class) - .description("Get list of Books by title") - .targetObject(openLibrary) - .build(); - - var bookTitlesByAuthor = FunctionCallback.builder() - .method("getBookTitlesByAuthor", String.class) - .description("Get book titles by author") - .targetObject(openLibrary) - .build(); - - return ToolHelper.toSyncToolRegistration(books, bookTitlesByAuthor); - } - - private static McpServerFeatures.SyncResourceRegistration systemInfoResourceRegistration() { - - // Create a resource registration for system information - var systemInfoResource = new McpSchema.Resource( // @formatter:off - "system://info", - "System Information", - "Provides basic system information including Java version, OS, etc.", - "application/json", null - ); - - var resourceRegistration = new McpServerFeatures.SyncResourceRegistration(systemInfoResource, (request) -> { - try { - var systemInfo = Map.of( - "javaVersion", System.getProperty("java.version"), - "osName", System.getProperty("os.name"), - "osVersion", System.getProperty("os.version"), - "osArch", System.getProperty("os.arch"), - "processors", Runtime.getRuntime().availableProcessors(), - "timestamp", System.currentTimeMillis()); - - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); - - return new McpSchema.ReadResourceResult( - List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent))); - } - catch (Exception e) { - throw new RuntimeException("Failed to generate system info", e); - } - }); // @formatter:on - - return resourceRegistration; - } - - private static McpServerFeatures.SyncPromptRegistration greetingPromptRegistration() { - - var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt", - List.of(new McpSchema.PromptArgument("name", "The name to greet", true))); - - return new McpServerFeatures.SyncPromptRegistration(prompt, getPromptRequest -> { - - String nameArgument = (String) getPromptRequest.arguments().get("name"); - if (nameArgument == null) { - nameArgument = "friend"; - } - - var userMessage = new PromptMessage(Role.USER, - new TextContent("Hello " + nameArgument + "! How can I assist you today?")); - - return new GetPromptResult("A personalized greeting message", List.of(userMessage)); - }); - } - - @Bean - public OpenLibrary openLibrary() { - return new OpenLibrary(RestClient.builder()); - } - -} diff --git a/model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/OpenLibrary.java b/model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/OpenLibrary.java deleted file mode 100644 index 7f96573..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/OpenLibrary.java +++ /dev/null @@ -1,74 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.server; - -import java.util.List; -import java.util.Map; - -import org.springframework.web.client.RestClient; - -/** - * @author Christian Tzolov - */ - -public class OpenLibrary { - - private RestClient restClient; - - public OpenLibrary(RestClient.Builder restClientBuilder) { - this.restClient = restClientBuilder.baseUrl("https://openlibrary.org").build(); - } - - public record Books(Integer numFound, Integer start, Boolean numFoundExact, List> docs) { - } - - public record Book(List isbn, String title, List authorName) { - } - - public List getBooks(String title) { - Books books = restClient.get() - .uri(uriBuilder -> uriBuilder.path("/search.json").queryParam("q", title).build()) - .retrieve() - .body(Books.class); - if (books == null) { - return List.of(); - } - return books.docs.stream() - .map(doc -> new Book((List) doc.get("isbn"), (String) doc.get("title"), - (List) doc.get("author_name"))) - .toList(); - } - - public List getBookTitlesByAuthor(String authorName) { - var books = restClient.get() - .uri(uriBuilder -> uriBuilder.path("/search/authors.json").queryParam("q", authorName).build()) - .retrieve() - .body(Books.class); - if (books == null) { - return List.of(); - } - return books.docs.stream().map(doc -> (String) doc.get("top_work")).toList(); - } - - public static void main(String[] args) { - OpenLibrary openLibrary = new OpenLibrary(RestClient.builder()); - List books = openLibrary.getBooks("Spring Framework"); - System.out.println(books); - List booksByAuthor = openLibrary.getBookTitlesByAuthor("Craig Walls"); - System.out.println(booksByAuthor); - } - -} diff --git a/model-context-protocol/book-library/manual-webflux-server/src/main/resources/application.properties b/model-context-protocol/book-library/manual-webflux-server/src/main/resources/application.properties deleted file mode 100644 index b2b3fcc..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/src/main/resources/application.properties +++ /dev/null @@ -1,11 +0,0 @@ -# spring.main.web-application-type=none - -# NOTE: You must disable the banner and the console logging -# to allow the STDIO transport to work !!! -spring.main.banner-mode=off -logging.pattern.console= - -# transport.mode=stdio -transport.mode=sse - -logging.file.name=mcp.webflux.log \ No newline at end of file diff --git a/model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java b/model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java deleted file mode 100644 index 9476dfd..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java +++ /dev/null @@ -1,43 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.client; - -import java.io.File; - -import org.springframework.ai.mcp.client.transport.ServerParameters; -import org.springframework.ai.mcp.client.transport.StdioClientTransport; - -/** - * With stdio transport, the MCP server is automatically started by the client. But you - * have to build the server jar first:
./mvnw clean install -DskipTests
- */ -public class ClientStdio { - - public static void main(String[] args) { - - System.out.println(new File(".").getAbsolutePath()); - - var stdioParams = ServerParameters.builder("java") - .args("-Dtransport.mode=stdio", "-Dspring.main.web-application-type=none", "-jar", - "model-context-protocol/mcp-webflux-server/target/mcp-webflux-server-0.0.1-SNAPSHOT.jar") - .build(); - - var transport = new StdioClientTransport(stdioParams); - - new SampleClient(transport).run(); - } - -} diff --git a/model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientWebFluxSse.java b/model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientWebFluxSse.java deleted file mode 100644 index de0b211..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientWebFluxSse.java +++ /dev/null @@ -1,32 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.client; - -import org.springframework.ai.mcp.client.transport.WebFluxSseClientTransport; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author Christian Tzolov - */ -public class ClientWebFluxSse { - - public static void main(String[] args) { - var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080")); - - new SampleClient(transport).run(); - } - -} diff --git a/model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java b/model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java deleted file mode 100644 index a2b0eba..0000000 --- a/model-context-protocol/book-library/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java +++ /dev/null @@ -1,80 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.client; - -import java.util.Map; - -import org.springframework.ai.mcp.client.McpClient; -import org.springframework.ai.mcp.spec.ClientMcpTransport; -import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest; -import org.springframework.ai.mcp.spec.McpSchema.CallToolResult; -import org.springframework.ai.mcp.spec.McpSchema.GetPromptRequest; -import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult; -import org.springframework.ai.mcp.spec.McpSchema.ListToolsResult; -import org.springframework.ai.mcp.spec.McpSchema.ReadResourceRequest; - -/** - * @author Christian Tzolov - */ - -public class SampleClient { - - private final ClientMcpTransport transport; - - public SampleClient(ClientMcpTransport transport) { - this.transport = transport; - } - - public void run() { - - var client = McpClient.sync(this.transport).build(); - - client.initialize(); - - client.ping(); - - // List and demonstrate tools - ListToolsResult toolsList = client.listTools(); - System.out.println("Available Tools = " + toolsList); - - CallToolResult upperCaseResult = client - .callTool(new CallToolRequest("toUpperCase", Map.of("input", "accountName"))); - System.out.println("Upper case Response = " + upperCaseResult); - - CallToolResult books = client.callTool(new CallToolRequest("getBooks", Map.of("title", "Spring Framework"))); - System.out.println("Books Response = " + books); - - // List and demonstrate resources - var resourcesList = client.listResources(); - System.out.println("\nAvailable Resources = " + resourcesList); - - // Read the system info resource - var systemInfo = client.readResource(new ReadResourceRequest("system://info")); - System.out.println("System Info = " + systemInfo); - - // List and demonstrate prompts - ListPromptsResult promptsList = client.listPrompts(); - System.out.println("\nAvailable Prompts = " + promptsList); - - // Try the greeting prompt - var greetingResponse = client.getPrompt(new GetPromptRequest("greeting", Map.of("name", "Spring"))); - System.out.println("Greeting Response = " + greetingResponse); - - client.closeGracefully(); - - } - -} diff --git a/model-context-protocol/book-library/manual-webmvc-server/.flattened-pom.xml b/model-context-protocol/book-library/manual-webmvc-server/.flattened-pom.xml deleted file mode 100644 index b7ac98a..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/.flattened-pom.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - 4.0.0 - org.springframework.experimental - spring-ai-mcp-webmvc-sample - 0.5.0-SNAPSHOT - Spring AI MCP Sample - Sample Spring Boot application demonstrating MCP server usage - - Spring Framework - https://spring.io - - - - Apache 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - Christian Tzolov - - - Dariusz Jędrzejczyk - - - - - org.springframework.experimental - mcp-webmvc-sse-transport - 0.5.0-SNAPSHOT - compile - - - org.springframework.experimental - spring-ai-mcp - 0.5.0-SNAPSHOT - compile - - - org.springframework.boot - spring-boot-starter-web - 3.4.1 - compile - - - diff --git a/model-context-protocol/book-library/manual-webmvc-server/.mvn/wrapper/maven-wrapper.properties b/model-context-protocol/book-library/manual-webmvc-server/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index d58dfb7..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,19 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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 -# -# http://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. -wrapperVersion=3.3.2 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/model-context-protocol/book-library/manual-webmvc-server/README.md b/model-context-protocol/book-library/manual-webmvc-server/README.md deleted file mode 100644 index 7b82ab2..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/README.md +++ /dev/null @@ -1,257 +0,0 @@ -# Spring AI MCP WebMVC Server Sample Project - -This sample project demonstrates the usage of the Spring AI Model Context Protocol (MCP) implementation. It showcases how to create and use MCP WebMvc servers and Http Sse clients with different capabilities. - -## Overview - -The sample provides: -- Two transport mode implementations: Stdio and SSE (Server-Sent Events) -- Server capabilities: - - Tools support with list changes notifications - - Resources support with list changes notifications (no subscribe support) - - Prompts support with list changes notifications -- Sample implementations: - - Two MCP tools: Weather and Calculator - - One MCP resource: System Information - - One MCP prompt: Greeting - -## Building the Project - -```bash -./mvnw clean package -``` - -## Running the Server - -The server can be started in two transport modes, controlled by the `transport.mode` property: - -### Stdio Mode (Default) - -```bash -java -Dtransport.mode=stdio -jar target/mcp-webmvc-server-0.0.1-SNAPSHOT.jar -``` - -The Stdio mode server is automatically started by the client - no explicit server startup is needed. -But you have to build the server jar first: `./mvnw clean install -DskipTests`. - -In Stdio mode the server must not emit any messages/logs to the console (e.g. standard out) but the JSON messages produced by the server. - -### SSE Mode -```bash -java -Dtransport.mode=sse -jar target/smcp-webmvc-server-0.0.1-SNAPSHOT.jar -``` - -## Sample Clients - -The project includes example clients for both transport modes: - -### Stdio Client (ClientStdio.java) -```java -var stdioParams = ServerParameters.builder("java") - .args("-Dtransport.mode=stdio", "-jar", - "target/mcp-webmvc-server-0.0.1-SNAPSHOT.jar") - .build(); - -var transport = new StdioClientTransport(stdioParams); -var client = McpClient.using(transport).sync(); -``` - -### SSE Client (ClientSse.java) -```java -var transport = new HttpClientSseClientTransport("http://localhost:8080"); -var client = McpClient.using(transport).sync(); -``` - -## Available Tools - -### Weather Tool -- Name: `weather` -- Description: Weather forecast tool by location -- Parameters: - - `city`: String - The city to get weather for -- Example: -```java -CallToolResult response = client.callTool( - new CallToolRequest("weather", Map.of("city", "Sofia")) -); -``` - -### Calculator Tool -- Name: `calculator` -- Description: Performs basic arithmetic operations -- Parameters (JSON Schema): - ```json - { - "operation": { - "type": "string", - "enum": ["add", "subtract", "multiply", "divide"], - "description": "The arithmetic operation to perform" - }, - "a": { - "type": "number", - "description": "First operand" - }, - "b": { - "type": "number", - "description": "Second operand" - } - } - ``` -- Example: -```java -CallToolResult response = client.callTool( - new CallToolRequest("calculator", - Map.of("operation", "multiply", "a", 2.0, "b", 3.0)) -); -``` - -## Available Resources - -### System Information Resource -- URI: `system://info` -- Description: Provides basic system information including Java version, OS, etc. -- MIME Type: application/json -- Returns: JSON object containing: javaVersion, osName, osVersion, osArch, processors, timestamp - -## Available Prompts - -### Greeting Prompt -- Name: `greeting` -- Description: A friendly greeting prompt -- Parameters: - - `name`: String (required) - The name to greet -- Returns: A personalized greeting message from an assistant - -## Client Usage Example - -```java -// Initialize client -client.initialize(); - -// Test connection -client.ping(); - -// List available tools -ListToolsResult tools = client.listTools(); -System.out.println("Available tools: " + tools); - -// Call weather tool -CallToolResult weather = client.callTool( - new CallToolRequest("weather", Map.of("city", "Sofia")) -); -System.out.println("Weather: " + weather); - -// Call calculator tool -CallToolResult calc = client.callTool( - new CallToolRequest("calculator", - Map.of("operation", "multiply", "a", 2.0, "b", 3.0)) -); -System.out.println("Calculation: " + calc); - -// Access system info resource -ReadResourceResult sysInfo = client.readResource( - new ReadResourceRequest("system://info") -); -System.out.println("System Info: " + sysInfo); - -// Use greeting prompt -GetPromptResult greeting = client.getPrompt( - new GetPromptRequest("greeting", Map.of("name", "John")) -); -System.out.println("Greeting: " + greeting); - -// Close client -client.closeGracefully(); -``` - -## Server Usage Example - -```java -@Configuration -public class CustomMcpServerConfig { - @Bean - public McpAsyncServer mcpServer(McpTransport transport) { - // Configure server capabilities - var capabilities = McpSchema.ServerCapabilities.builder() - .resources(false, true) // Resource support with list changes notifications - .tools(true) // Tool support with list changes notifications - .prompts(true) // Prompt support with list changes notifications - .build(); - - // Create custom tool - var customTool = new ToolRegistration( - new McpSchema.Tool("custom-tool", "Description", Map.of("param", "String")), - (arguments) -> { - String param = (String) arguments.get("param"); - return new CallToolResult( - List.of(new TextContent("Result: " + param)), - false - ); - } - ); - - // Create custom resource - var customResource = new ResourceRegistration( - new McpSchema.Resource( - "custom://resource", - "Custom Resource", - "Description", - "application/json", - null - ), - (request) -> new ReadResourceResult( - List.of(new TextResourceContents( - request.uri(), - "application/json", - "{\"data\": \"example\"}" - )) - ) - ); - - // Create custom prompt - var customPrompt = new PromptRegistration( - new McpSchema.Prompt( - "custom-prompt", - "Description", - List.of(new McpSchema.PromptArgument("input", "Description", true)) - ), - request -> { - String input = (String) request.arguments().get("input"); - var message = new PromptMessage( - Role.ASSISTANT, - new TextContent("Response to: " + input) - ); - return new GetPromptResult("Result", List.of(message)); - } - ); - - // Create and configure the server - return McpServer.using(transport) - .info("Custom MCP Server", "1.0.0") - .capabilities(capabilities) - .tools(List.of(customTool)) - .resources(Map.of("custom://resource", customResource)) - .prompts(List.of(customPrompt)) - .async(); - } -} -``` - -## Configuration - -The application can be configured through `application.properties`: - -- `transport.mode`: Transport mode to use (stdio/sse) -- `server.port`: Server port for SSE mode (default: 8080) -- Various logging configurations are available for debugging - -## Implementation Details - -The sample demonstrates: -- Creating an MCP server with custom tools, resources, and prompts -- Configuring different transport modes -- Implementing tool handlers with JSON schema validation -- Resource implementations with dynamic content generation -- Prompt implementations with parameter handling -- Error handling and response formatting -- Synchronous client usage patterns diff --git a/model-context-protocol/book-library/manual-webmvc-server/mvnw b/model-context-protocol/book-library/manual-webmvc-server/mvnw deleted file mode 100755 index 19529dd..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/mvnw +++ /dev/null @@ -1,259 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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 -# -# http://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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/model-context-protocol/book-library/manual-webmvc-server/mvnw.cmd b/model-context-protocol/book-library/manual-webmvc-server/mvnw.cmd deleted file mode 100644 index b150b91..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/mvnw.cmd +++ /dev/null @@ -1,149 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" -if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" -} -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/model-context-protocol/book-library/manual-webmvc-server/pom.xml b/model-context-protocol/book-library/manual-webmvc-server/pom.xml deleted file mode 100644 index 45c6cc2..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/pom.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.3.6 - - - - com.example - mcp-webmvc-server - 0.0.1-SNAPSHOT - - Spring AI MCP WebMvc Server Sample - Sample Spring Boot application demonstrating MCP WebMvc server usage - - - 0.6.0 - - - - - - org.springframework.experimental - mcp-bom - ${spring-ai-mcp.version} - pom - import - - - - - - - org.springframework.experimental - mcp-webmvc-sse-transport - - - - - org.springframework.experimental - mcp-webflux-sse-transport - - - - org.springframework.experimental - spring-ai-mcp - - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - repackage - - - - - - io.spring.javaformat - spring-javaformat-maven-plugin - 0.0.43 - - - validate - true - - validate - - - - - - - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - false - - - - - \ No newline at end of file diff --git a/model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/McpMvcServerApplication.java b/model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/McpMvcServerApplication.java deleted file mode 100644 index cfff5e0..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/McpMvcServerApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.springframework.ai.mcp.sample.webmvc.server; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class McpMvcServerApplication { - - public static void main(String[] args) { - SpringApplication.run(McpMvcServerApplication.class, args); - } - -} diff --git a/model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/McpServerConfig.java b/model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/McpServerConfig.java deleted file mode 100644 index 19e3282..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/McpServerConfig.java +++ /dev/null @@ -1,166 +0,0 @@ -package org.springframework.ai.mcp.sample.webmvc.server; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.mcp.server.McpServer; -import org.springframework.ai.mcp.server.McpServerFeatures.SyncPromptRegistration; -import org.springframework.ai.mcp.server.McpServerFeatures.SyncResourceRegistration; -import org.springframework.ai.mcp.server.McpServerFeatures.SyncToolRegistration; -import org.springframework.ai.mcp.server.McpSyncServer; -import org.springframework.ai.mcp.server.transport.StdioServerTransport; -import org.springframework.ai.mcp.server.transport.WebMvcSseServerTransport; -import org.springframework.ai.mcp.spec.McpSchema; -import org.springframework.ai.mcp.spec.McpSchema.GetPromptResult; -import org.springframework.ai.mcp.spec.McpSchema.PromptMessage; -import org.springframework.ai.mcp.spec.McpSchema.Role; -import org.springframework.ai.mcp.spec.McpSchema.TextContent; -import org.springframework.ai.mcp.spec.ServerMcpTransport; -import org.springframework.ai.mcp.spring.ToolHelper; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestClient; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.function.RouterFunction; -import org.springframework.web.servlet.function.ServerResponse; - -@Configuration -@EnableWebMvc -public class McpServerConfig implements WebMvcConfigurer { - - private static final Logger logger = LoggerFactory.getLogger(McpServerConfig.class); - - @Bean - @ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse") - public WebMvcSseServerTransport webMvcSseServerTransport() { - return new WebMvcSseServerTransport(new ObjectMapper(), "/mcp/message"); - } - - @Bean - @ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse") - public RouterFunction routerFunction(WebMvcSseServerTransport transport) { - return transport.getRouterFunction(); - } - - // STDIO transport - @Bean - @ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "stdio") - public StdioServerTransport stdioServerTransport() { - return new StdioServerTransport(); - } - - public static record ToUpperCaseInput(String input) { - } - - @Bean - public McpSyncServer mcpServer(ServerMcpTransport transport, OpenLibrary openLibrary) { // @formatter:off - - // Configure server capabilities with resource support - var capabilities = McpSchema.ServerCapabilities.builder() - .resources(false, true) // No subscribe support, but list changes notifications - .tools(true) // Tool support with list changes notifications - .prompts(true) // Prompt support with list changes notifications - .logging() // Logging support - .build(); - - // Create the server with both tool and resource capabilities - var server = McpServer.sync(transport) - .serverInfo("MCP Demo WebMVC Server", "1.0.0") - .capabilities(capabilities) - .resources(systemInfoResourceRegistration()) - .prompts(greetingPromptRegistration()) - .tools( - ToolHelper.toSyncToolRegistration( - FunctionCallback.builder() - .function("toUpperCase", (Function) s -> s.input().toUpperCase()) - .description("To upper case") - .inputType(ToUpperCaseInput.class) - .build())) - .tools(openLibraryToolRegistrations(openLibrary)) - .build(); - return server; // @formatter:on - } // @formatter:on - - public static List openLibraryToolRegistrations(OpenLibrary openLibrary) { - - var books = FunctionCallback.builder() - .method("getBooks", String.class) - .description("Get list of Books by title") - .targetObject(openLibrary) - .build(); - - var bookTitlesByAuthor = FunctionCallback.builder() - .method("getBookTitlesByAuthor", String.class) - .description("Get book titles by author") - .targetObject(openLibrary) - .build(); - - return ToolHelper.toSyncToolRegistration(books, bookTitlesByAuthor); - } - - private static SyncResourceRegistration systemInfoResourceRegistration() { - - // Create a resource registration for system information - var systemInfoResource = new McpSchema.Resource( // @formatter:off - "system://info", - "System Information", - "Provides basic system information including Java version, OS, etc.", - "application/json", null - ); - - var resourceRegistration = new SyncResourceRegistration(systemInfoResource, (request) -> { - try { - var systemInfo = Map.of( - "javaVersion", System.getProperty("java.version"), - "osName", System.getProperty("os.name"), - "osVersion", System.getProperty("os.version"), - "osArch", System.getProperty("os.arch"), - "processors", Runtime.getRuntime().availableProcessors(), - "timestamp", System.currentTimeMillis()); - - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); - - return new McpSchema.ReadResourceResult( - List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent))); - } - catch (Exception e) { - throw new RuntimeException("Failed to generate system info", e); - } - }); // @formatter:on - - return resourceRegistration; - } - - private static SyncPromptRegistration greetingPromptRegistration() { - - var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt", - List.of(new McpSchema.PromptArgument("name", "The name to greet", true))); - - return new SyncPromptRegistration(prompt, getPromptRequest -> { - - String nameArgument = (String) getPromptRequest.arguments().get("name"); - if (nameArgument == null) { - nameArgument = "friend"; - } - - var userMessage = new PromptMessage(Role.USER, - new TextContent("Hello " + nameArgument + "! How can I assist you today?")); - - return new GetPromptResult("A personalized greeting message", List.of(userMessage)); - }); - } - - @Bean - public OpenLibrary openLibrary() { - return new OpenLibrary(RestClient.builder()); - } - -} diff --git a/model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/OpenLibrary.java b/model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/OpenLibrary.java deleted file mode 100644 index 3dfe835..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/webmvc/server/OpenLibrary.java +++ /dev/null @@ -1,74 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.webmvc.server; - -import java.util.List; -import java.util.Map; - -import org.springframework.web.client.RestClient; - -/** - * @author Christian Tzolov - */ - -public class OpenLibrary { - - private RestClient restClient; - - public OpenLibrary(RestClient.Builder restClientBuilder) { - this.restClient = restClientBuilder.baseUrl("https://openlibrary.org").build(); - } - - public record Books(Integer numFound, Integer start, Boolean numFoundExact, List> docs) { - } - - public record Book(List isbn, String title, List authorName) { - } - - public List getBooks(String title) { - Books books = restClient.get() - .uri(uriBuilder -> uriBuilder.path("/search.json").queryParam("q", title).build()) - .retrieve() - .body(Books.class); - if (books == null) { - return List.of(); - } - return books.docs.stream() - .map(doc -> new Book((List) doc.get("isbn"), (String) doc.get("title"), - (List) doc.get("author_name"))) - .toList(); - } - - public List getBookTitlesByAuthor(String authorName) { - var books = restClient.get() - .uri(uriBuilder -> uriBuilder.path("/search/authors.json").queryParam("q", authorName).build()) - .retrieve() - .body(Books.class); - if (books == null) { - return List.of(); - } - return books.docs.stream().map(doc -> (String) doc.get("top_work")).toList(); - } - - public static void main(String[] args) { - OpenLibrary openLibrary = new OpenLibrary(RestClient.builder()); - List books = openLibrary.getBooks("Spring Framework"); - System.out.println(books); - List booksByAuthor = openLibrary.getBookTitlesByAuthor("Craig Walls"); - System.out.println(booksByAuthor); - } - -} diff --git a/model-context-protocol/book-library/manual-webmvc-server/src/main/resources/application.properties b/model-context-protocol/book-library/manual-webmvc-server/src/main/resources/application.properties deleted file mode 100644 index 3d554fd..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/src/main/resources/application.properties +++ /dev/null @@ -1,10 +0,0 @@ -spring.main.web-application-type=SERVLET - -# NOTE: You must disable the banner and the console logging -# to allow the STDIO transport to work !!! -spring.main.banner-mode=off -logging.pattern.console= - -transport.mode=sse - -logging.file.name=mcp.webmvc.log \ No newline at end of file diff --git a/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/ClientStdio.java b/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/ClientStdio.java deleted file mode 100644 index 8d7914e..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/ClientStdio.java +++ /dev/null @@ -1,39 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.webmvc.client; - -import org.springframework.ai.mcp.client.transport.ServerParameters; -import org.springframework.ai.mcp.client.transport.StdioClientTransport; - -/** - * With stdio transport, the MCP server is automatically started by the client. But you - * have to build the server jar first:
./mvnw clean install -DskipTests
- */ -public class ClientStdio { - - public static void main(String[] args) { - - var stdioParams = ServerParameters.builder("java") - .args("-Dtransport.mode=stdio", "-Dspring.main.web-application-type=none", "-jar", - "model-context-protocol/mcp-webmvc-server/target/mcp-webmvc-server-0.0.1-SNAPSHOT.jar") - .build(); - - var transport = new StdioClientTransport(stdioParams); - - new SampleClient(transport).run(); - } - -} diff --git a/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/ClientWebFluxSse.java b/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/ClientWebFluxSse.java deleted file mode 100644 index 60f5609..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/ClientWebFluxSse.java +++ /dev/null @@ -1,32 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.webmvc.client; - -import org.springframework.ai.mcp.client.transport.WebFluxSseClientTransport; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author Christian Tzolov - */ -public class ClientWebFluxSse { - - public static void main(String[] args) { - var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080")); - - new SampleClient(transport).run(); - } - -} diff --git a/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/HttpClientSse.java b/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/HttpClientSse.java deleted file mode 100644 index 27f7964..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/HttpClientSse.java +++ /dev/null @@ -1,31 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.webmvc.client; - -import org.springframework.ai.mcp.client.transport.HttpClientSseClientTransport; - -/** - * @author Christian Tzolov - */ -public class HttpClientSse { - - public static void main(String[] args) { - var transport = new HttpClientSseClientTransport("http://localhost:8080"); - - new SampleClient(transport).run(); - } - -} diff --git a/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/SampleClient.java b/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/SampleClient.java deleted file mode 100644 index e6af73e..0000000 --- a/model-context-protocol/book-library/manual-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/webmvc/client/SampleClient.java +++ /dev/null @@ -1,80 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.webmvc.client; - -import java.util.Map; - -import org.springframework.ai.mcp.client.McpClient; -import org.springframework.ai.mcp.spec.ClientMcpTransport; -import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest; -import org.springframework.ai.mcp.spec.McpSchema.CallToolResult; -import org.springframework.ai.mcp.spec.McpSchema.GetPromptRequest; -import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult; -import org.springframework.ai.mcp.spec.McpSchema.ListToolsResult; -import org.springframework.ai.mcp.spec.McpSchema.ReadResourceRequest; - -/** - * @author Christian Tzolov - */ - -public class SampleClient { - - private final ClientMcpTransport transport; - - public SampleClient(ClientMcpTransport transport) { - this.transport = transport; - } - - public void run() { - - var client = McpClient.sync(this.transport).build(); - - client.initialize(); - - client.ping(); - - // List and demonstrate tools - ListToolsResult toolsList = client.listTools(); - System.out.println("Available Tools = " + toolsList); - - CallToolResult upperCaseResult = client - .callTool(new CallToolRequest("toUpperCase", Map.of("input", "accountName"))); - System.out.println("Upper case Response = " + upperCaseResult); - - CallToolResult books = client.callTool(new CallToolRequest("getBooks", Map.of("title", "Spring Framework"))); - System.out.println("Books Response = " + books); - - // List and demonstrate resources - var resourcesList = client.listResources(); - System.out.println("\nAvailable Resources = " + resourcesList); - - // Read the system info resource - var systemInfo = client.readResource(new ReadResourceRequest("system://info")); - System.out.println("System Info = " + systemInfo); - - // List and demonstrate prompts - ListPromptsResult promptsList = client.listPrompts(); - System.out.println("\nAvailable Prompts = " + promptsList); - - // Try the greeting prompt - var greetingResponse = client.getPrompt(new GetPromptRequest("greeting", Map.of("name", "Spring"))); - System.out.println("Greeting Response = " + greetingResponse); - - client.closeGracefully(); - - } - -} diff --git a/model-context-protocol/book-library/starter-webflux-server/.mvn/wrapper/maven-wrapper.properties b/model-context-protocol/book-library/starter-webflux-server/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index d58dfb7..0000000 --- a/model-context-protocol/book-library/starter-webflux-server/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,19 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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 -# -# http://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. -wrapperVersion=3.3.2 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/model-context-protocol/book-library/starter-webflux-server/README.md b/model-context-protocol/book-library/starter-webflux-server/README.md deleted file mode 100644 index 3354c76..0000000 --- a/model-context-protocol/book-library/starter-webflux-server/README.md +++ /dev/null @@ -1,200 +0,0 @@ -# Spring AI MCP WebFlux Server Starter - -This project demonstrates how to create a Spring AI Model Context Protocol (MCP) server using Spring WebFlux. It showcases how to implement MCP tools, resources, and prompts with Spring Boot's auto-configuration capabilities. - -## Overview - -The project provides: -- Spring Boot starter for MCP server implementation -- WebFlux-based transport mode -- Server capabilities: - - Tools support with OpenLibrary integration - - Resources support with system information - - Prompts support with greeting functionality - -## Building the Project - -```bash -./mvnw clean package -``` - -## Running the Server - -The server uses WebFlux transport mode by default: - -```bash -./mvnw spring-boot:run -``` - -## Running the Client Test - -After starting the server, you can run the WebFlux SSE client test located in `src/test/java/org/springframework/ai/mcp/sample/client/ClientWebFluxSse.java`. This test demonstrates how to connect to the server using Spring WebFlux's SSE transport. - -To run the test: -1. Ensure the server is running (previous step) -2. Open `ClientWebFluxSse.java` in your IDE -3. Run the main method in `ClientWebFluxSse` class - -The test creates a WebFlux SSE client that connects to `http://localhost:8080` and demonstrates the client-server interaction using the Model Context Protocol. - -## Available Tools - -### OpenLibrary Tools -The server integrates with the OpenLibrary API to provide book-related functionality: - -#### Get Books by Title -- Description: Get list of Books by title -- Parameters: - - `title`: String - The book title to search for -- Example: -```java -CallToolResult response = client.callTool( - new CallToolRequest("getBooks", Map.of("title", "Spring Framework")) -); -``` - -#### Get Book Titles by Author -- Description: Get book titles by author -- Parameters: - - `authorName`: String - The author name to search for -- Example: -```java -CallToolResult response = client.callTool( - new CallToolRequest("getBookTitlesByAuthor", Map.of("authorName", "Craig Walls")) -); -``` - -### Text Processing Tool -#### toUpperCase -- Description: Converts input text to uppercase -- Parameters: - - `input`: String - The text to convert -- Example: -```java -CallToolResult response = client.callTool( - new CallToolRequest("toUpperCase", Map.of("input", "hello world")) -); -``` - -## Available Resources - -### System Information Resource -- URI: `system://info` -- Description: Provides basic system information including Java version, OS, etc. -- MIME Type: application/json -- Returns: JSON object containing: - - javaVersion - - osName - - osVersion - - osArch - - processors - - timestamp - -## Available Prompts - -### Greeting Prompt -- Name: `greeting` -- Description: A friendly greeting prompt -- Parameters: - - `name`: String (required) - The name to greet -- Returns: A personalized greeting message - -## Configuration - -The application can be configured through `application.properties`: - -```properties -# Server Configuration -spring.ai.mcp.server.enabled=true -spring.ai.mcp.server.name=my-webflux-server-starter -spring.ai.mcp.server.version=0.0.1 -spring.ai.mcp.server.transport=WEBFLUX - -# Logging Configuration (Required for proper operation) -spring.main.banner-mode=off -logging.pattern.console= -logging.file.name=./target/mcp.webflux-server-starter.log -``` - -## Implementation Details - -### Server Configuration -```java -@SpringBootApplication -public class McpServerApplication { - @Bean - public ToolCallbackProvider openLibraryTools(OpenLibrary openLibrary) { - return MethodToolCallbackProvider.builder().toolObjects(openLibrary).build(); - } - - @Bean - public ToolCallback tools(OpenLibrary openLibrary) { - return FunctionToolCallback - .builder("toUpperCase", (Function) s -> s.input().toUpperCase()) - .description("To upper case") - .inputType(ToUpperCaseInput.class) - .build(); - } -} -``` - -### OpenLibrary Integration -```java -@Service -public class OpenLibrary { - @Tool(description = "Get list of Books by title") - public List getBooks(String title) { - // Implementation - } - - @Tool(description = "Get book titles by author") - public List getBookTitlesByAuthor(String authorName) { - // Implementation - } -} -``` - -### Resource Implementation -```java -@Bean -public List resourceRegistrations() { - var systemInfoResource = new McpSchema.Resource( - "system://info", - "System Information", - "Provides basic system information including Java version, OS, etc.", - "application/json", - null - ); - - // Resource registration implementation -} -``` - -### Prompt Implementation -```java -@Bean -public List promptRegistrations() { - var prompt = new McpSchema.Prompt( - "greeting", - "A friendly greeting prompt", - List.of(new McpSchema.PromptArgument("name", "The name to greet", true)) - ); - - // Prompt registration implementation -} -``` - -## Key Features - -1. **Spring Boot Integration**: Leverages Spring Boot's auto-configuration for easy setup -2. **WebFlux Transport**: Uses reactive programming model for efficient request handling -3. **OpenLibrary Integration**: Demonstrates external API integration -4. **System Resource**: Shows how to expose system information as an MCP resource -5. **Function Tools**: Examples of both service-based and function-based tool implementations -6. **Prompt Support**: Implements basic prompt functionality with parameter handling - -## Notes - -- The server is configured to use file-based logging to ensure proper operation of the transport layer -- Banner mode is disabled to prevent interference with the transport protocol -- The OpenLibrary integration demonstrates how to integrate external REST APIs with MCP tools diff --git a/model-context-protocol/book-library/starter-webflux-server/mvnw b/model-context-protocol/book-library/starter-webflux-server/mvnw deleted file mode 100755 index 19529dd..0000000 --- a/model-context-protocol/book-library/starter-webflux-server/mvnw +++ /dev/null @@ -1,259 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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 -# -# http://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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/model-context-protocol/book-library/starter-webflux-server/mvnw.cmd b/model-context-protocol/book-library/starter-webflux-server/mvnw.cmd deleted file mode 100644 index b150b91..0000000 --- a/model-context-protocol/book-library/starter-webflux-server/mvnw.cmd +++ /dev/null @@ -1,149 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" -if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" -} -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/model-context-protocol/book-library/starter-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java b/model-context-protocol/book-library/starter-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java deleted file mode 100644 index c4bb400..0000000 --- a/model-context-protocol/book-library/starter-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.springframework.ai.mcp.sample.server; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.function.Function; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.server.McpServerFeatures; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.PromptMessage; -import io.modelcontextprotocol.spec.McpSchema.Role; -import io.modelcontextprotocol.spec.McpSchema.TextContent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.tool.ToolCallbackProvider; -import org.springframework.ai.tool.ToolCallbacks; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.ai.tool.method.MethodToolCallbackProvider; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; - -@SpringBootApplication -public class McpServerApplication { - - private static final Logger logger = LoggerFactory.getLogger(McpServerApplication.class); - - public static void main(String[] args) { - SpringApplication.run(McpServerApplication.class, args); - } - - public static record ToUpperCaseInput(String input) { - } - - @Bean - public ToolCallbackProvider openLibraryTools(OpenLibrary openLibrary) { - return MethodToolCallbackProvider.builder().toolObjects(openLibrary).build(); - } - - @Bean - public ToolCallback tools(OpenLibrary openLibrary) { - return FunctionToolCallback - .builder("toUpperCase", (Function) s -> s.input().toUpperCase()) - .description("To upper case") - .inputType(ToUpperCaseInput.class) - .build(); - } - - @Bean - public List resourceRegistrations() { - - // Create a resource registration for system information - var systemInfoResource = new McpSchema.Resource( // @formatter:off - "system://info", - "System Information", - "Provides basic system information including Java version, OS, etc.", - "application/json", null - ); - - var resourceRegistration = new McpServerFeatures.SyncResourceRegistration(systemInfoResource, (request) -> { - try { - var systemInfo = Map.of( - "javaVersion", System.getProperty("java.version"), - "osName", System.getProperty("os.name"), - "osVersion", System.getProperty("os.version"), - "osArch", System.getProperty("os.arch"), - "processors", Runtime.getRuntime().availableProcessors(), - "timestamp", System.currentTimeMillis()); - - String jsonContent = new ObjectMapper().writeValueAsString(systemInfo); - - return new McpSchema.ReadResourceResult( - List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent))); - } - catch (Exception e) { - throw new RuntimeException("Failed to generate system info", e); - } - }); // @formatter:on - - return List.of(resourceRegistration); - } - - @Bean - public List promptRegistrations() { - - var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt", - List.of(new McpSchema.PromptArgument("name", "The name to greet", true))); - - var promptRegistration = new McpServerFeatures.SyncPromptRegistration(prompt, getPromptRequest -> { - - String nameArgument = (String) getPromptRequest.arguments().get("name"); - if (nameArgument == null) { - nameArgument = "friend"; - } - - var userMessage = new PromptMessage(Role.USER, - new TextContent("Hello " + nameArgument + "! How can I assist you today?")); - - return new GetPromptResult("A personalized greeting message", List.of(userMessage)); - }); - - return List.of(promptRegistration); - } - - @Bean - public Consumer> rootsChangeConsumer() { - return roots -> { - logger.info("Registering root resources: {}", roots); - }; - } - -} diff --git a/model-context-protocol/book-library/starter-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/OpenLibrary.java b/model-context-protocol/book-library/starter-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/OpenLibrary.java deleted file mode 100644 index 04d3d93..0000000 --- a/model-context-protocol/book-library/starter-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/OpenLibrary.java +++ /dev/null @@ -1,82 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.server; - -import java.util.List; -import java.util.Map; - -import org.springframework.ai.tool.annotation.Tool; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClient; - -/** - * @author Christian Tzolov - */ -@Service -public class OpenLibrary { - - private RestClient restClient; - - public OpenLibrary() { - this(RestClient.builder()); - } - - public OpenLibrary(RestClient.Builder restClientBuilder) { - this.restClient = restClientBuilder.baseUrl("https://openlibrary.org").build(); - } - - public record Books(Integer numFound, Integer start, Boolean numFoundExact, List> docs) { - } - - public record Book(List isbn, String title, List authorName) { - } - - @Tool(description = "Get list of Books by title") - public List getBooks(String title) { - Books books = restClient.get() - .uri(uriBuilder -> uriBuilder.path("/search.json").queryParam("q", title).build()) - .retrieve() - .body(Books.class); - if (books == null) { - return List.of(); - } - return books.docs.stream() - .map(doc -> new Book((List) doc.get("isbn"), (String) doc.get("title"), - (List) doc.get("author_name"))) - .toList(); - } - - @Tool(description = "Get book titles by author") - public List getBookTitlesByAuthor(String authorName) { - var books = restClient.get() - .uri(uriBuilder -> uriBuilder.path("/search/authors.json").queryParam("q", authorName).build()) - .retrieve() - .body(Books.class); - if (books == null) { - return List.of(); - } - return books.docs.stream().map(doc -> (String) doc.get("top_work")).toList(); - } - - public static void main(String[] args) { - OpenLibrary openLibrary = new OpenLibrary(RestClient.builder()); - List books = openLibrary.getBooks("Spring Framework"); - System.out.println(books); - List booksByAuthor = openLibrary.getBookTitlesByAuthor("Craig Walls"); - System.out.println(booksByAuthor); - } - -} diff --git a/model-context-protocol/book-library/starter-webflux-server/src/main/resources/application.properties b/model-context-protocol/book-library/starter-webflux-server/src/main/resources/application.properties deleted file mode 100644 index 96c96b1..0000000 --- a/model-context-protocol/book-library/starter-webflux-server/src/main/resources/application.properties +++ /dev/null @@ -1,9 +0,0 @@ -# NOTE: You must disable the banner and the console logging -# to allow the STDIO transport to work !!! -spring.main.banner-mode=off -# logging.pattern.console= -logging.file.name=./model-context-protocol/mcp-webflux-server-starter/target/mcp.webflux-server-starter.log - -spring.ai.mcp.server.name=my-webflux-server-starter -spring.ai.mcp.server.version=0.0.1 - diff --git a/model-context-protocol/book-library/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientWebFluxSse.java b/model-context-protocol/book-library/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientWebFluxSse.java deleted file mode 100644 index 46dfbff..0000000 --- a/model-context-protocol/book-library/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientWebFluxSse.java +++ /dev/null @@ -1,33 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.client; - -import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; - -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author Christian Tzolov - */ -public class ClientWebFluxSse { - - public static void main(String[] args) { - var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:8080")); - - new SampleClient(transport).run(); - } - -} diff --git a/model-context-protocol/book-library/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java b/model-context-protocol/book-library/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java deleted file mode 100644 index 9beb9a5..0000000 --- a/model-context-protocol/book-library/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java +++ /dev/null @@ -1,80 +0,0 @@ -/* -* Copyright 2024 - 2024 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.sample.client; - -import java.util.Map; - -import io.modelcontextprotocol.client.McpClient; -import io.modelcontextprotocol.spec.ClientMcpTransport; -import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; -import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult; -import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest; - -/** - * @author Christian Tzolov - */ - -public class SampleClient { - - private final ClientMcpTransport transport; - - public SampleClient(ClientMcpTransport transport) { - this.transport = transport; - } - - public void run() { - - var client = McpClient.sync(this.transport).build(); - - client.initialize(); - - client.ping(); - - // List and demonstrate tools - ListToolsResult toolsList = client.listTools(); - System.out.println("Available Tools = " + toolsList); - - CallToolResult upperCaseResult = client - .callTool(new CallToolRequest("toUpperCase", Map.of("input", "accountName"))); - System.out.println("Upper case Response = " + upperCaseResult); - - CallToolResult books = client.callTool(new CallToolRequest("getBooks", Map.of("title", "Spring Framework"))); - System.out.println("Books Response = " + books); - - // List and demonstrate resources - var resourcesList = client.listResources(); - System.out.println("\nAvailable Resources = " + resourcesList); - - // Read the system info resource - var systemInfo = client.readResource(new ReadResourceRequest("system://info")); - System.out.println("System Info = " + systemInfo); - - // List and demonstrate prompts - ListPromptsResult promptsList = client.listPrompts(); - System.out.println("\nAvailable Prompts = " + promptsList); - - // Try the greeting prompt - var greetingResponse = client.getPrompt(new GetPromptRequest("greeting", Map.of("name", "Spring"))); - System.out.println("Greeting Response = " + greetingResponse); - - client.closeGracefully(); - - } - -} diff --git a/model-context-protocol/brave/.DS_Store b/model-context-protocol/brave/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ee771938787a1bb39cadddab0eb4f7463f2ead2b GIT binary patch literal 6148 zcmeHKI|>3Z5S>vG!N$@uSMUZw^aOhW1;s`bM6I{-TprCgpGH?ZZR8D1UNV`NkXP*N zh=|TFo0-T&L`HB!x!KS)+c)o6FCz+si0p zwti-h{ zq5pp-aYY5Fz+Wk#gGIBL<4IXtJCCzkTi`3W<=o+Bm^%f7mt&xpV=Sy3PdzE}ip{ZK V6Wc(iBkpt{e+En!8Ws4p0uNe26{i3I literal 0 HcmV?d00001 diff --git a/model-context-protocol/brave/pom.xml b/model-context-protocol/brave/pom.xml index 7399b44..a541e2f 100644 --- a/model-context-protocol/brave/pom.xml +++ b/model-context-protocol/brave/pom.xml @@ -1,20 +1,23 @@ - 4.0.0 org.springframework.boot spring-boot-starter-parent 3.3.6 - + com.example ai-mcp-brave 0.0.1-SNAPSHOT Spring AI - Model Context Protocol - Brave Simple AI Application using MCP client to use Brave for Internet search + 17 + 1.0.0-SNAPSHOT @@ -22,7 +25,7 @@ org.springframework.ai spring-ai-bom - 1.0.0-SNAPSHOT + ${spring-ai.version} pom import @@ -37,16 +40,14 @@ org.springframework.ai - spring-ai-openai-spring-boot-starter + spring-ai-starter-model-openai - org.springframework.ai - spring-ai-mcp + spring-ai-starter-mcp-client - @@ -78,4 +79,4 @@ - + \ No newline at end of file diff --git a/model-context-protocol/brave/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java b/model-context-protocol/brave/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java index e263869..0a9044a 100644 --- a/model-context-protocol/brave/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java +++ b/model-context-protocol/brave/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java @@ -1,14 +1,13 @@ package org.springframework.ai.mcp.samples.brave; -import io.modelcontextprotocol.client.McpClient; +import java.util.List; + import io.modelcontextprotocol.client.McpSyncClient; -import io.modelcontextprotocol.client.transport.ServerParameters; -import io.modelcontextprotocol.client.transport.StdioClientTransport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.mcp.SyncMcpToolCallback; +import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -26,16 +25,12 @@ public class Application { @Bean public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, - McpSyncClient mcpSyncClient, ConfigurableApplicationContext context) { + List mcpSyncClients, ConfigurableApplicationContext context) { return args -> { var chatClient = chatClientBuilder - .defaultTools(mcpSyncClient.listTools(null) - .tools() - .stream() - .map(tool -> new SyncMcpToolCallback(mcpSyncClient, tool)) - .toArray(SyncMcpToolCallback[]::new)) + .defaultTools(new SyncMcpToolCallbackProvider(mcpSyncClients)) .build(); String question = "Does Spring AI supports the Model Context Protocol? Please provide some references."; @@ -45,20 +40,4 @@ public class Application { context.close(); }; } - - @Bean - public McpSyncClient mcpClient() { - - // https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search - var stdioParams = ServerParameters.builder("npx") - .args("-y", "@modelcontextprotocol/server-brave-search") - .addEnvVar("BRAVE_API_KEY", System.getenv("BRAVE_API_KEY")) - .build(); - - var mcpClient = McpClient.sync(new StdioClientTransport(stdioParams)).build(); - var init = mcpClient.initialize(); - logger.info("MCP Initialized: {}", init); - return mcpClient; - } - } \ No newline at end of file diff --git a/model-context-protocol/brave/src/main/resources/application.properties b/model-context-protocol/brave/src/main/resources/application.properties index 861eae3..7b98bbc 100644 --- a/model-context-protocol/brave/src/main/resources/application.properties +++ b/model-context-protocol/brave/src/main/resources/application.properties @@ -1,8 +1,11 @@ +debug=true spring.application.name=mcp spring.main.web-application-type=none spring.ai.openai.api-key=${OPENAI_API_KEY} +spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json + # Logging configuration logging.level.root=INFO logging.level.org.springframework.ai.mcp=WARN diff --git a/model-context-protocol/brave/src/main/resources/mcp-servers-config.json b/model-context-protocol/brave/src/main/resources/mcp-servers-config.json new file mode 100644 index 0000000..df98809 --- /dev/null +++ b/model-context-protocol/brave/src/main/resources/mcp-servers-config.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "brave-search": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-brave-search" + ], + "env": { + } + } + } +} \ No newline at end of file diff --git a/model-context-protocol/client-starter/starter-default-client/pom.xml b/model-context-protocol/client-starter/starter-default-client/pom.xml index 4735fb9..40b5ef2 100644 --- a/model-context-protocol/client-starter/starter-default-client/pom.xml +++ b/model-context-protocol/client-starter/starter-default-client/pom.xml @@ -36,17 +36,17 @@ org.springframework.ai - spring-ai-mcp-client-spring-boot-starter + spring-ai-starter-mcp-client org.springframework.ai - spring-ai-anthropic-spring-boot-starter + spring-ai-starter-model-anthropic diff --git a/model-context-protocol/client-starter/starter-default-client/src/main/resources/application.properties b/model-context-protocol/client-starter/starter-default-client/src/main/resources/application.properties index d3b267e..0df91dc 100644 --- a/model-context-protocol/client-starter/starter-default-client/src/main/resources/application.properties +++ b/model-context-protocol/client-starter/starter-default-client/src/main/resources/application.properties @@ -14,4 +14,6 @@ spring.ai.mcp.client.stdio.connections.brave-search.args=-y,@modelcontextprotoco logging.level.io.modelcontextprotocol.client=WARN logging.level.io.modelcontextprotocol.spec=WARN -ai.user.input=What tools are available? \ No newline at end of file +ai.user.input=What tools are available? + +spring.ai.mcp.client.toolcallback.enabled=true \ No newline at end of file diff --git a/model-context-protocol/client-starter/starter-webflux-client/pom.xml b/model-context-protocol/client-starter/starter-webflux-client/pom.xml index e4408f3..9220872 100644 --- a/model-context-protocol/client-starter/starter-webflux-client/pom.xml +++ b/model-context-protocol/client-starter/starter-webflux-client/pom.xml @@ -36,17 +36,17 @@ org.springframework.ai - spring-ai-mcp-client-webflux-spring-boot-starter + spring-ai-starter-mcp-client-webflux org.springframework.ai - spring-ai-anthropic-spring-boot-starter + spring-ai-starter-model-anthropic diff --git a/model-context-protocol/client-starter/starter-webflux-client/src/main/resources/application.properties b/model-context-protocol/client-starter/starter-webflux-client/src/main/resources/application.properties index 007a113..6d2d730 100644 --- a/model-context-protocol/client-starter/starter-webflux-client/src/main/resources/application.properties +++ b/model-context-protocol/client-starter/starter-webflux-client/src/main/resources/application.properties @@ -13,4 +13,7 @@ spring.ai.mcp.client.stdio.connections.brave-search.args=-y,@modelcontextprotoco logging.level.io.modelcontextprotocol.client=WARN logging.level.io.modelcontextprotocol.spec=WARN -ai.user.input=What tools are available? \ No newline at end of file +ai.user.input=What tools are available? + +# Enable the mcp client tool-callback auto-configuration +spring.ai.mcp.client.toolcallback.enabled=true \ No newline at end of file diff --git a/model-context-protocol/filesystem/pom.xml b/model-context-protocol/filesystem/pom.xml index 765ca0e..3ec9025 100644 --- a/model-context-protocol/filesystem/pom.xml +++ b/model-context-protocol/filesystem/pom.xml @@ -22,7 +22,7 @@ org.springframework.ai spring-ai-bom - 1.0.0-M5 + 1.0.0-SNAPSHOT pom import @@ -37,13 +37,12 @@ org.springframework.ai - spring-ai-openai-spring-boot-starter + spring-ai-starter-model-openai - org.springframework.experimental - spring-ai-mcp - 0.6.0 + org.springframework.ai + spring-ai-starter-mcp-client diff --git a/model-context-protocol/filesystem/src/main/java/org/springframework/ai/mcp/samples/filesystem/Application.java b/model-context-protocol/filesystem/src/main/java/org/springframework/ai/mcp/samples/filesystem/Application.java index 099ab0e..391dbce 100644 --- a/model-context-protocol/filesystem/src/main/java/org/springframework/ai/mcp/samples/filesystem/Application.java +++ b/model-context-protocol/filesystem/src/main/java/org/springframework/ai/mcp/samples/filesystem/Application.java @@ -2,14 +2,14 @@ package org.springframework.ai.mcp.samples.filesystem; import java.nio.file.Paths; import java.time.Duration; -import java.util.List; + +import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.client.transport.ServerParameters; +import io.modelcontextprotocol.client.transport.StdioClientTransport; import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.mcp.client.McpClient; -import org.springframework.ai.mcp.client.McpSyncClient; -import org.springframework.ai.mcp.client.transport.ServerParameters; -import org.springframework.ai.mcp.client.transport.StdioClientTransport; -import org.springframework.ai.mcp.spring.McpFunctionCallback; +import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -25,22 +25,22 @@ public class Application { @Bean public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, - List functionCallbacks, ConfigurableApplicationContext context) { + McpSyncClient mcpClient, ConfigurableApplicationContext context) { return args -> { var chatClient = chatClientBuilder - .defaultFunctions(functionCallbacks.toArray(new McpFunctionCallback[0])) + .defaultTools(new SyncMcpToolCallbackProvider(mcpClient)) .build(); System.out.println("Running predefined questions with AI model responses:\n"); // Question 1 - String question1 = "Can you explain the content of the spring-ai-mcp-overview.txt file?"; + String question1 = "Can you explain the content of the target/spring-ai-mcp-overview.txt file?"; System.out.println("QUESTION: " + question1); System.out.println("ASSISTANT: " + chatClient.prompt(question1).call().content()); // Question 2 - String question2 = "Pleses summarize the content of the spring-ai-mcp-overview.txt file and store it a new summary.md as Markdown format?"; + String question2 = "Pleses summarize the content of the target/spring-ai-mcp-overview.txt file and store it a new target/summary.md as Markdown format?"; System.out.println("\nQUESTION: " + question2); System.out.println("ASSISTANT: " + chatClient.prompt(question2).call().content()); @@ -50,17 +50,6 @@ public class Application { }; } - @Bean - public List functionCallbacks(McpSyncClient mcpClient) { - - var callbacks = mcpClient.listTools(null) - .tools() - .stream() - .map(tool -> new McpFunctionCallback(mcpClient, tool)) - .toList(); - return callbacks; - } - @Bean(destroyMethod = "close") public McpSyncClient mcpClient() { diff --git a/model-context-protocol/sampling/README.md b/model-context-protocol/sampling/README.md new file mode 100644 index 0000000..bc54226 --- /dev/null +++ b/model-context-protocol/sampling/README.md @@ -0,0 +1,190 @@ +# Spring AI MCP Sampling Examples + +This directory contains examples demonstrating the Model Context Protocol (MCP) Sampling capability in Spring AI. MCP Sampling allows an MCP server to delegate certain requests to LLM providers, enabling multi-model interactions and creative content generation. + +## Overview + +The MCP Sampling examples showcase: + +- How an MCP server can delegate LLM requests to a client +- How a client can route requests to different LLM providers based on model hints +- Integration with multiple LLM providers (OpenAI and Anthropic) +- Creative content generation using multiple models +- Combining responses from different LLMs into a unified result + +## Projects + +This directory contains two main projects: + +1. **[mcp-weather-webmvc-server](./mcp-weather-webmvc-server)**: An MCP server that provides weather information and uses MCP Sampling to generate creative content +2. **[mcp-sampling-client](./mcp-sampling-client)**: An MCP client that handles sampling requests and routes them to different LLM providers + +## What is MCP Sampling? + +MCP Sampling is a powerful capability of the Model Context Protocol that allows: + +- An MCP server to delegate certain requests to LLM providers +- Specifying model preferences to target specific LLM providers +- Routing requests based on model hints +- Combining responses from multiple LLMs + +This approach enables applications to leverage multiple LLM providers within a single workflow, allowing for creative content generation, model comparison, and specialized task delegation. + +## How It Works + +The MCP Sampling workflow in these examples follows these steps: + +1. **Client Initialization**: + - The client connects to the MCP Weather Server + - It registers a sampling handler that can route requests to different LLM providers + +2. **User Query**: + - The user sends a weather-related query to the client + - The client forwards the query to the MCP Weather Server + +3. **Server Processing**: + - The server retrieves weather data from the Open-Meteo API + - It extracts the `McpSyncServerExchange` from the tool context + - It creates sampling requests with different model preferences: + - One targeting OpenAI with `ModelPreferences.builder().addHint("openai").build()` + - One targeting Anthropic with `ModelPreferences.builder().addHint("anthropic").build()` + +4. **Sampling Delegation**: + - The server sends the sampling requests back to the client + - The client's sampling handler receives the requests + +5. **Model Routing**: + - The client extracts the model hint from each request + - It selects the appropriate LLM provider based on the hint + - It forwards the prompt to the selected LLM + +6. **Response Generation**: + - Each LLM generates a creative response (in this case, a poem about the weather) + - The client returns the responses to the server + +7. **Result Combination**: + - The server combines the responses from different LLMs + - It returns the combined result to the user + +## Server Implementation + +The MCP Weather Server implements the server-side of MCP Sampling: + +```java +public String callMcpSampling(ToolContext toolContext, WeatherResponse weatherResponse) { + String openAiWeatherPoem = ""; + String anthropicWeatherPoem = ""; + + if (toolContext != null && toolContext.getContext().containsKey("exchange")) { + // Spring AI MCP Auto-configuration injects the McpSyncServerExchange into the ToolContext + McpSyncServerExchange exchange = (McpSyncServerExchange) toolContext.getContext().get("exchange"); + 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 (temperature is in Celsius)...")))); + + // Request poem from OpenAI + var openAiLlmMessageRequest = messageRequestBuilder + .modelPreferences(ModelPreferences.builder().addHint("openai").build()) + .build(); + CreateMessageResult openAiLlmResponse = exchange.createMessage(openAiLlmMessageRequest); + openAiWeatherPoem = ((McpSchema.TextContent) openAiLlmResponse.content()).text(); + + // Request poem from Anthropic + var anthropicLlmMessageRequest = messageRequestBuilder + .modelPreferences(ModelPreferences.builder().addHint("anthropic").build()) + .build(); + CreateMessageResult anthropicAiLlmResponse = exchange.createMessage(anthropicLlmMessageRequest); + anthropicWeatherPoem = ((McpSchema.TextContent) anthropicAiLlmResponse.content()).text(); + } + } + + // Combine responses + return "OpenAI poem about the weather: " + openAiWeatherPoem + "\n\n" + + "Anthropic poem about the weather: " + anthropicWeatherPoem + "\n" + + ModelOptionsUtils.toJsonStringPrettyPrinter(weatherResponse); +} +``` + +## Client Implementation + +The MCP Sampling Client implements the client-side handling of sampling requests: + +```java +@Bean +McpSyncClientCustomizer samplingCustomizer(Map chatClients) { + return (name, spec) -> { + spec.sampling(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(); + }); + }; +} +``` + +## Running the Examples + +### Prerequisites + +- Java 17 or later +- Maven 3.6+ +- OpenAI API key +- Anthropic API key + +### Step 1: Start the MCP Weather Server + +```bash +cd mcp-weather-webmvc-server +./mvnw clean install -DskipTests +java -jar target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar +``` + +### Step 2: Set Environment Variables + +```bash +export OPENAI_API_KEY=your-openai-key +export ANTHROPIC_API_KEY=your-anthropic-key +``` + +### Step 3: Run the MCP Sampling Client + +```bash +cd mcp-sampling-client +./mvnw clean install +java -Dai.user.input='What is the weather in Amsterdam right now?' -jar target/mcp-sampling-client-0.0.1-SNAPSHOT.jar +``` + +## Sample Output + +When you run the application, you'll see creative responses from both OpenAI and Anthropic models, each generating a poem about the current weather in Amsterdam. + +## Related Projects + +Spring AI provides several MCP client and server implementations: + +- **[starter-default-client](../client-starter/starter-default-client)**: A default MCP client implementation using Spring Boot +- **[starter-webflux-client](../client-starter/starter-webflux-client)**: An MCP client implementation using Spring WebFlux + +## 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) +* [Model Context Protocol Specification](https://modelcontextprotocol.github.io/specification/) +* [Spring Boot Documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/) diff --git a/model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.jar b/model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c1dd12f17644411d6e840bd5a10c6ecda0175f18 GIT binary patch literal 58727 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%0ANsA|A3-jj|r`KAo%oTajX3>^E` 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 YM{}PByk10KD_nuQ4E7Du?}+~TKh4V)`~Uy| literal 0 HcmV?d00001 diff --git a/model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.properties b/model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..b7cb93e --- /dev/null +++ b/model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/model-context-protocol/sampling/mcp-sampling-client/README.md b/model-context-protocol/sampling/mcp-sampling-client/README.md new file mode 100644 index 0000000..4a054bc --- /dev/null +++ b/model-context-protocol/sampling/mcp-sampling-client/README.md @@ -0,0 +1,221 @@ +# Spring AI MCP Sampling Client + +This project demonstrates how to implement a client for the MCP (Model Context Protocol) Sampling capability using Spring AI. It showcases how to route LLM requests to different providers (OpenAI and Anthropic) based on model preferences. + +## Overview + +The MCP Sampling Client: +- Connects to an MCP server using SSE (Server-Sent Events) transport +- Implements a sampling handler that routes requests to different LLM providers +- Integrates with both OpenAI and Anthropic models +- Demonstrates how to use model hints to select the appropriate LLM +- Combines creative responses from multiple LLMs into a single result + +## MCP Sampling Implementation + +MCP Sampling is a powerful capability that allows an MCP server to delegate certain requests to LLM providers. This client implements the client-side handling of sampling requests: + +1. **Sampling Handler Registration**: The client registers a sampling handler using the `McpSyncClientCustomizer`: + +```java +@Bean +McpSyncClientCustomizer samplingCustomizer(Map chatClients) { + return (name, spec) -> { + spec.sampling(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(); + }); + }; +} +``` + +2. **Model Routing**: The client uses the model hint from the request to select the appropriate LLM provider: + - If the hint is "openai", it routes to the OpenAI model + - If the hint is "anthropic", it routes to the Anthropic model + +3. **Chat Client Management**: The client creates and manages multiple chat clients, one for each LLM provider: + +```java +@Bean +public Map chatClients(List chatModels) { + return chatModels.stream().collect(Collectors.toMap( + model -> model.getClass().getSimpleName().toLowerCase(), + model -> ChatClient.builder(model).build())); +} +``` + +4. **Integration with Spring AI**: The client leverages Spring AI's auto-configuration to set up the necessary components: + +```java +var mcpToolProvider = new SyncMcpToolCallbackProvider( + mcpClientsProvider.stream().flatMap(List::stream).toList()); + +ChatClient chatClient = ChatClient.builder(openAiChatModel) + .defaultTools(mcpToolProvider) + .build(); +``` + +## Dependencies + +The project requires the following Spring AI dependencies: + +```xml + + + org.springframework.ai + spring-ai-mcp-client-spring-boot-starter + + + org.springframework.ai + spring-ai-openai-spring-boot-starter + + + org.springframework.ai + spring-ai-anthropic-spring-boot-starter + + +``` + +## Configuration + +### Application Properties + +The application is configured through `application.properties`: + +```properties +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 + +# API keys for LLM providers +spring.ai.openai.api-key=${OPENAI_API_KEY} +spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY} + +# MCP server connection +spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080 + +# Logging configuration +logging.level.io.modelcontextprotocol.client=WARN +logging.level.io.modelcontextprotocol.spec=WARN +``` + +## How It Works + +The application demonstrates MCP Sampling with a weather-related query: + +1. The client connects to the MCP Weather Server +2. It sends a weather-related question: "What is the weather in Amsterdam right now?" +3. The server retrieves the weather data and sends sampling requests to the client +4. The client routes each request to the appropriate LLM based on model hints +5. Each LLM generates a creative poem about the weather +6. The server combines the responses and returns them to the user + +## Running the Application + +1. First, start the MCP Weather Server: + ```bash + cd ../mcp-weather-webmvc-server + ./mvnw clean install -DskipTests + java -jar target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar + ``` + +2. Set the required environment variables: + ```bash + export OPENAI_API_KEY=your-openai-key + export ANTHROPIC_API_KEY=your-anthropic-key + ``` + +3. Build and run the MCP Sampling Client: + ```bash + cd ../mcp-sampling-client + ./mvnw clean install + java -Dai.user.input='What is the weather in Amsterdam right now?' -jar target/mcp-sampling-client-0.0.1-SNAPSHOT.jar + ``` + +The application will: +1. Connect to the MCP Weather Server +2. Process the weather query +3. Handle sampling requests from the server +4. Route each request to the appropriate LLM +5. Display the combined creative responses + +## Sample Output + +When you run the application, you'll see output similar to: + +``` +> USER: 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 the poems from all the other providers. + +> ASSISTANT: I checked the current weather in Amsterdam for you. Here are the creative responses from different AI providers: + +OpenAI poem about the weather: +# Amsterdam's Embrace + +In Amsterdam, where canals reflect the sky, +A gentle warmth of sixteen degrees goes by. +The autumn air, a crisp and tender touch, +Caresses faces of the Dutch. + +Time stands still at this perfect hour, +As sunshine breaks through with gentle power. +The city breathes with calm delight, +In this moment, everything feels right. + +Anthropic poem about the weather: +## Amsterdam Today + +Sixteen degrees in Amsterdam's embrace, +A perfect autumn day unfolds with grace. +The canals reflect the passing clouds above, +As the city hums with life and love. + +Neither cold nor warm, but just between, +The perfect weather for this Dutch scene. +Time captured in this moment's gentle hold, +As Amsterdam's story continues to unfold. + +My synthesis of these weather poems: +# Amsterdam's Gentle Harmony + +Where canals mirror skies in Dutch design, +Sixteen degrees - a temperature divine. +Neither cold nor warm, but perfectly between, +The autumn air paints Amsterdam's scene. + +Time seems suspended in this golden hour, +As gentle sunshine shows its subtle power. +The city breathes, alive with calm delight, +In this moment, everything feels just right. + +Amsterdam's story, continuing to unfold, +Embraces all within its gentle hold. +A perfect day that poets can't ignore, +In this beautiful city we all adore. + +Current weather data shows the temperature is 16°C in Amsterdam right now. +``` + +## Additional Resources + +* [Spring AI Documentation](https://docs.spring.io/spring-ai/reference/) +* [MCP Client Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) +* [Model Context Protocol Specification](https://modelcontextprotocol.github.io/specification/) +* [Spring Boot Documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/) diff --git a/model-context-protocol/sampling/mcp-sampling-client/mvnw b/model-context-protocol/sampling/mcp-sampling-client/mvnw new file mode 100755 index 0000000..eb65ff2 --- /dev/null +++ b/model-context-protocol/sampling/mcp-sampling-client/mvnw @@ -0,0 +1,305 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ "$MVNW_REPOURL" = true]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/model-context-protocol/sampling/mcp-sampling-client/mvnw.cmd b/model-context-protocol/sampling/mcp-sampling-client/mvnw.cmd new file mode 100755 index 0000000..4f5150a --- /dev/null +++ b/model-context-protocol/sampling/mcp-sampling-client/mvnw.cmd @@ -0,0 +1,172 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar" + ) + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/model-context-protocol/sampling/mcp-sampling-client/pom.xml b/model-context-protocol/sampling/mcp-sampling-client/pom.xml new file mode 100644 index 0000000..d8b498c --- /dev/null +++ b/model-context-protocol/sampling/mcp-sampling-client/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.6 + + + com.example + mcp-sampling-client + 0.0.1-SNAPSHOT + Spring AI - MCP Starter Default Client + Spring AI - MCP Starter Default Client + + + 17 + 1.0.0-SNAPSHOT + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + + + + org.springframework.ai + spring-ai-starter-mcp-client + + + + org.springframework.ai + spring-ai-starter-model-openai + + + + org.springframework.ai + spring-ai-starter-model-anthropic + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + + \ No newline at end of file diff --git a/model-context-protocol/sampling/mcp-sampling-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java b/model-context-protocol/sampling/mcp-sampling-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java new file mode 100644 index 0000000..cca303f --- /dev/null +++ b/model-context-protocol/sampling/mcp-sampling-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java @@ -0,0 +1,98 @@ +/* + * 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 io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; +import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class McpClientApplication { + + public static void main(String[] args) { + SpringApplication.run(McpClientApplication.class, args); + } + + @Bean + public CommandLineRunner predefinedQuestions(OpenAiChatModel openAiChatModel, + ConfigurableApplicationContext context, List mcpClients) { + + return args -> { + + var mcpToolProvider = new SyncMcpToolCallbackProvider(mcpClients); + + ChatClient chatClient = ChatClient.builder(openAiChatModel).defaultTools(mcpToolProvider).build(); + + String userQuestion = """ + What is the weather in Amsterdam right now? + Please incorporate all createive responses from all LLM providers. + After the other providers add a poem that synthesizes the the poems from all the other providers. + """; + + System.out.println("> USER: " + userQuestion); + System.out.println("> ASSISTANT: " + chatClient.prompt(userQuestion).call().content()); + + context.close(); + }; + } + + @Bean + McpSyncClientCustomizer samplingCustomizer(Map chatClients) { + + return (name, mcpClientSpec) -> { + mcpClientSpec.sampling(llmRequest -> { + var userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text(); + String modelHint = llmRequest.modelPreferences().hints().get(0).name(); + + ChatClient hintedChatClient = chatClients.entrySet().stream() + .filter(e -> e.getKey().contains(modelHint)).findFirst() + .orElseThrow().getValue(); + + String response = hintedChatClient.prompt() + .system(llmRequest.systemPrompt()) + .user(userPrompt) + .call() + .content(); + + return CreateMessageResult.builder().content(new McpSchema.TextContent(response)).build(); + }); + System.out.println("Customizing " + name); + }; + } + + @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/mcp-sampling-client/src/main/resources/application.properties b/model-context-protocol/sampling/mcp-sampling-client/src/main/resources/application.properties new file mode 100644 index 0000000..6a2ece5 --- /dev/null +++ b/model-context-protocol/sampling/mcp-sampling-client/src/main/resources/application.properties @@ -0,0 +1,16 @@ +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 + +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 diff --git a/model-context-protocol/book-library/manual-servlet-server/.mvn/wrapper/maven-wrapper.properties b/model-context-protocol/sampling/mcp-weather-webmvc-server/.mvn/wrapper/maven-wrapper.properties similarity index 100% rename from model-context-protocol/book-library/manual-servlet-server/.mvn/wrapper/maven-wrapper.properties rename to model-context-protocol/sampling/mcp-weather-webmvc-server/.mvn/wrapper/maven-wrapper.properties diff --git a/model-context-protocol/sampling/mcp-weather-webmvc-server/README.md b/model-context-protocol/sampling/mcp-weather-webmvc-server/README.md new file mode 100644 index 0000000..a1adc8b --- /dev/null +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/README.md @@ -0,0 +1,244 @@ +# Spring AI MCP Weather Server Sample with WebMVC Starter and Sampling + +This sample project demonstrates how to create an MCP server using the Spring AI MCP Server Boot Starter with WebMVC transport. It implements a weather service that exposes tools for retrieving weather information using the Open-Meteo API and showcases MCP Sampling capabilities. + +For more information, see the [MCP Server Boot Starter](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) reference documentation. + +## Overview + +The sample showcases: +- Integration with `spring-ai-mcp-server-webmvc-spring-boot-starter` +- Support for both SSE (Server-Sent Events) and STDIO transports +- Automatic tool registration using Spring AI's `@Tool` annotation +- MCP Sampling implementation that demonstrates LLM provider routing +- Weather tool that retrieves temperature data and generates creative responses using multiple LLMs + +## MCP Sampling Implementation + +This project demonstrates the MCP Sampling capability, which allows an MCP server to delegate certain requests to LLM providers. The implementation includes: + +1. **Server-side Sampling**: The `WeatherService` class implements a `callMcpSampling` method that: + - Extracts the `McpSyncServerExchange` from the tool context + - Creates two separate message requests with different model preferences: + - One targeting OpenAI models with `ModelPreferences.builder().addHint("openai").build()` + - One targeting Anthropic models with `ModelPreferences.builder().addHint("anthropic").build()` + - Sends both requests to generate creative poems about the weather data + - Combines the responses into a single result + +2. **Client-side Sampling**: The companion client project (`mcp-sampling-client`) implements: + - A `McpSyncClientCustomizer` that handles sampling requests + - Logic to route requests to the appropriate LLM based on model hints + - Integration with both OpenAI and Anthropic models + +This approach demonstrates how MCP can be used to leverage multiple LLM providers within a single application, allowing for creative content generation and model comparison. + +## Dependencies + +The project requires the Spring AI MCP Server WebMVC Boot Starter: + +```xml + + org.springframework.ai + spring-ai-mcp-server-webmvc-spring-boot-starter + +``` + +This starter provides: +- HTTP-based transport using Spring MVC (`WebMvcSseServerTransport`) +- Auto-configured SSE endpoints +- Optional STDIO transport +- Included `spring-boot-starter-web` and `mcp-spring-webmvc` dependencies + +## Building the Project + +Build the project using Maven: +```bash +./mvnw clean install -DskipTests +``` + +## Running the Server + +The server supports two transport modes: + +### WebMVC SSE Mode (Default) +```bash +java -jar target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar +``` + +### STDIO Mode +To enable STDIO transport, set the appropriate properties: +```bash +java -Dspring.ai.mcp.server.stdio=true -Dspring.main.web-application-type=none -jar target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar +``` + +## Configuration + +Configure the server through `application.properties`: + +```properties +# Server identification +spring.ai.mcp.server.name=my-weather-server +spring.ai.mcp.server.version=0.0.1 + +# Server type (SYNC/ASYNC) +spring.ai.mcp.server.type=SYNC + +# Transport configuration +spring.ai.mcp.server.stdio=false +spring.ai.mcp.server.sse-message-endpoint=/mcp/message + +# Change notifications +spring.ai.mcp.server.resource-change-notification=true +spring.ai.mcp.server.tool-change-notification=true +spring.ai.mcp.server.prompt-change-notification=true + +# Logging (required for STDIO transport) +spring.main.banner-mode=off +logging.file.name=./target/starter-webmvc-server.log +``` + +## Available Tools + +### Weather Temperature Tool +- Name: `getTemperature` +- Description: Get the temperature (in celsius) for a specific location +- Parameters: + - `latitude`: double - The location latitude + - `longitude`: double - The location longitude + - `toolContext`: ToolContext - Automatically provided by Spring AI + +This tool not only retrieves the current temperature from the Open-Meteo API but also uses MCP Sampling to generate creative poems about the weather from both OpenAI and Anthropic models. + +## Server Implementation + +The server uses Spring Boot and Spring AI's tool annotations for automatic tool registration: + +```java +@SpringBootApplication +public class McpServerApplication { + public static void main(String[] args) { + SpringApplication.run(McpServerApplication.class, args); + } + + @Bean + public ToolCallbackProvider weatherTools(WeatherService weatherService){ + return MethodToolCallbackProvider.builder().toolObjects(weatherService).build(); + } +} +``` + +The `WeatherService` implements the weather tool using the `@Tool` annotation and includes MCP Sampling functionality: + +```java +@Service +public class WeatherService { + @Tool(description = "Get the temperature (in celsius) for a specific location") + public String getTemperature(double latitude, double longitude, ToolContext toolContext) { + // Retrieve weather data from Open-Meteo API + 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 to generate creative responses + String responseWithPoems = callMcpSampling(toolContext, weatherResponse); + + return responseWithPoems; + } + + public String callMcpSampling(ToolContext toolContext, WeatherResponse weatherResponse) { + // Implementation that calls both OpenAI and Anthropic models + // to generate poems about the weather + } +} +``` + +## MCP Clients + +You can connect to the weather server using either STDIO or SSE transport: + +### Manual Clients + +#### WebMVC SSE Client + +For servers using SSE transport: + +```java +var transport = new HttpClientSseClientTransport("http://localhost:8080"); +var client = McpClient.sync(transport).build(); +``` + +#### STDIO Client + +For servers using STDIO transport: + +```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-sampling-weather-server-0.0.1-SNAPSHOT.jar") + .build(); + +var transport = new StdioClientTransport(stdioParams); +var client = McpClient.sync(transport).build(); +``` + +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 + +### Sampling Client + +The companion project `mcp-sampling-client` demonstrates how to implement a client that handles MCP Sampling requests: + +```java +@Bean +McpSyncClientCustomizer samplingCustomizer(Map chatClients) { + return (name, spec) -> { + spec.sampling(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(); + }); + }; +} +``` + +To run the sampling client: + +1. Start the MCP server +2. Set the required environment variables: + ```bash + export OPENAI_API_KEY=your-openai-key + export ANTHROPIC_API_KEY=your-anthropic-key + ``` +3. Run the client: + ```bash + java -jar target/mcp-sampling-client-0.0.1-SNAPSHOT.jar + ``` + +## 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-server-boot-client-docs.html) +* [Model Context Protocol Specification](https://modelcontextprotocol.github.io/specification/) +* [Spring Boot Auto-configuration](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration) diff --git a/model-context-protocol/book-library/manual-servlet-server/mvnw b/model-context-protocol/sampling/mcp-weather-webmvc-server/mvnw similarity index 100% rename from model-context-protocol/book-library/manual-servlet-server/mvnw rename to model-context-protocol/sampling/mcp-weather-webmvc-server/mvnw diff --git a/model-context-protocol/book-library/manual-servlet-server/mvnw.cmd b/model-context-protocol/sampling/mcp-weather-webmvc-server/mvnw.cmd similarity index 100% rename from model-context-protocol/book-library/manual-servlet-server/mvnw.cmd rename to model-context-protocol/sampling/mcp-weather-webmvc-server/mvnw.cmd diff --git a/model-context-protocol/book-library/starter-webflux-server/pom.xml b/model-context-protocol/sampling/mcp-weather-webmvc-server/pom.xml similarity index 85% rename from model-context-protocol/book-library/starter-webflux-server/pom.xml rename to model-context-protocol/sampling/mcp-weather-webmvc-server/pom.xml index 0dbad54..5c75521 100644 --- a/model-context-protocol/book-library/starter-webflux-server/pom.xml +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/pom.xml @@ -12,22 +12,18 @@ com.example - mcp-webflux-server-starter + mcp-sampling-weather-server 0.0.1-SNAPSHOT - Spring AI MCP StdIO and WebFluxSSE Sample - Sample Spring Boot application demonstrating MCP server usage - - - 1.0.0-SNAPSHOT - + Spring AI MCP Sampling - Weather Server + Sample Spring Boot application demonstrating MCP client and server sampling usage org.springframework.ai spring-ai-bom - ${sprign-ai.version} + 1.0.0-SNAPSHOT pom import @@ -37,7 +33,7 @@ org.springframework.ai - spring-ai-mcp-server-webflux-spring-boot-starter + spring-ai-starter-mcp-server-webmvc diff --git a/model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java new file mode 100644 index 0000000..77606bc --- /dev/null +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java @@ -0,0 +1,20 @@ +package org.springframework.ai.mcp.sample.server; + +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; + +@SpringBootApplication +public class McpServerApplication { + + public static void main(String[] args) { + SpringApplication.run(McpServerApplication.class, args); + } + + @Bean + public ToolCallbackProvider weatherTools(WeatherService weatherService) { + return MethodToolCallbackProvider.builder().toolObjects(weatherService).build(); + } +} diff --git a/model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java new file mode 100644 index 0000000..baac096 --- /dev/null +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server/WeatherService.java @@ -0,0 +1,117 @@ +/* +* 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.sample.server; + +import java.time.LocalDateTime; +import java.util.List; + +import io.modelcontextprotocol.server.McpSyncServerExchange; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; +import io.modelcontextprotocol.spec.McpSchema.ModelPreferences; +import org.slf4j.Logger; + +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +/** + * @author Christian Tzolov + */ +@Service +public class WeatherService { + + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(WeatherService.class); + + private final RestClient restClient; + + public WeatherService() { + this.restClient = RestClient.create(); + } + + /** + * The response format from the Open-Meteo API + */ + public record WeatherResponse(Current current) { + public record Current(LocalDateTime time, int interval, double temperature_2m) { + } + } + + @Tool(description = "Get the temperature (in celsius) for a specific location") + public String getTemperature(@ToolParam(description = "The location latitude") double latitude, + @ToolParam(description = "The location longitude") double longitude, + ToolContext toolContext) { + + 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); + + String responseWithPoems = callMcpSampling(toolContext, weatherResponse); + + return responseWithPoems; + } + + public String callMcpSampling(ToolContext toolContext, WeatherResponse weatherResponse) { + + String openAiWeatherPoem = ""; + String anthropicWeatherPoem = ""; + + if (toolContext != null && toolContext.getContext().containsKey("exchange")) { + + // Spring AI MCP Auto-configuration injects the McpSyncServerExchange into the ToolContext under the key "exchange" + McpSyncServerExchange exchange = (McpSyncServerExchange) toolContext.getContext().get("exchange"); + 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 thius weather forecast (temperature is in Celsious). Use markdown format :\n " + + ModelOptionsUtils.toJsonStringPrettyPrinter(weatherResponse))))); + + var opeAiLlmMessageRequest = messageRequestBuilder + .modelPreferences(ModelPreferences.builder().addHint("openai").build()) + .build(); + CreateMessageResult openAiLlmResponse = exchange.createMessage(opeAiLlmMessageRequest); + + openAiWeatherPoem = ((McpSchema.TextContent) openAiLlmResponse.content()).text(); + + var anthropicLlmMessageRequest = messageRequestBuilder + .modelPreferences(ModelPreferences.builder().addHint("anthropic").build()) + .build(); + CreateMessageResult anthropicAiLlmResponse = exchange.createMessage(anthropicLlmMessageRequest); + + anthropicWeatherPoem = ((McpSchema.TextContent) anthropicAiLlmResponse.content()).text(); + + } + } + + String responseWithPoems = "OpenAI poem about the weather: " + openAiWeatherPoem + "\n\n" + + "Anthropic poem about the weather: " + anthropicWeatherPoem + "\n" + + ModelOptionsUtils.toJsonStringPrettyPrinter(weatherResponse); + + logger.info(anthropicWeatherPoem, responseWithPoems); + + return responseWithPoems; + + } + +} \ No newline at end of file diff --git a/model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/resources/application.properties b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/resources/application.properties new file mode 100644 index 0000000..cc33bc0 --- /dev/null +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/main/resources/application.properties @@ -0,0 +1,13 @@ +# spring.main.web-application-type=none + +# NOTE: You must disable the banner and the console logging +# to allow the STDIO transport to work !!! +spring.main.banner-mode=off +# logging.pattern.console= + +# spring.ai.mcp.server.stdio=false + +spring.ai.mcp.server.name=mcp-sampling-server +spring.ai.mcp.server.version=0.0.1 + +logging.file.name=./model-context-protocol/sampling/mcp-weather-webmvc-server/target/mcp-sampling-server.log diff --git a/model-context-protocol/book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/HttpClientSse.java b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java similarity index 91% rename from model-context-protocol/book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/HttpClientSse.java rename to model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java index 44e841d..3846cc5 100644 --- a/model-context-protocol/book-library/manual-servlet-server/src/test/java/org/springframework/ai/mcp/sample/servlet/client/HttpClientSse.java +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.ai.mcp.sample.servlet.client; +package org.springframework.ai.mcp.sample.client; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; + /** * @author Christian Tzolov */ -public class HttpClientSse { +public class ClientSse { public static void main(String[] args) { var transport = new HttpClientSseClientTransport("http://localhost:8080"); - new SampleClient(transport).run(); } diff --git a/model-context-protocol/book-library/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java similarity index 69% rename from model-context-protocol/book-library/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java rename to model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java index caacea6..325448e 100644 --- a/model-context-protocol/book-library/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java @@ -15,11 +15,14 @@ */ package org.springframework.ai.mcp.sample.client; +import java.io.File; + import io.modelcontextprotocol.client.transport.ServerParameters; import io.modelcontextprotocol.client.transport.StdioClientTransport; /** - * With stdio transport, the MCP server is automatically started by the client. But you + * With stdio transport, the MCP server is automatically started by the client. + * But you * have to build the server jar first: * *
@@ -30,13 +33,17 @@ public class ClientStdio {
 
 	public static void main(String[] args) {
 
+		System.out.println(new File(".").getAbsolutePath());
+		
 		var stdioParams = ServerParameters.builder("java")
-			.args("-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none",
-					"-Dlogging.pattern.console=", "-jar",
-					"model-context-protocol/mcp-webflux-server-starter/target/mcp-webflux-server-starter-0.0.1-SNAPSHOT.jar")
-			.build();
+				.args("-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none",
+						"-Dlogging.pattern.console=", "-jar",
+						"model-context-protocol/sampling/mcp-weather-webmvc-server/target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar")
+				.build();
 
-		new SampleClient(new StdioClientTransport(stdioParams)).run();
+		var transport = new StdioClientTransport(stdioParams);
+
+		new SampleClient(transport).run();
 	}
 
 }
diff --git a/model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java
new file mode 100644
index 0000000..c8fbfe0
--- /dev/null
+++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java
@@ -0,0 +1,69 @@
+/*
+* Copyright 2024 - 2024 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.sample.client;
+
+import java.util.Map;
+
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.spec.McpClientTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
+import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
+import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
+
+import org.springframework.ai.chat.model.ToolContext;
+
+/**
+ * @author Christian Tzolov
+ */
+
+public class SampleClient {
+
+	private final McpClientTransport transport;
+
+	public SampleClient(McpClientTransport transport) {
+		this.transport = transport;
+	}
+
+	public void run() {
+
+		var client = McpClient.sync(this.transport)
+				.sampling(request -> {
+					System.out.println("Received a new message: " + request);
+					return CreateMessageResult.builder()
+							.content(new McpSchema.TextContent("Bla bla bla"))
+							.build();
+				})
+				.build();
+
+		client.initialize();
+
+		client.ping();
+
+		// List and demonstrate tools
+		ListToolsResult toolsList = client.listTools();
+		System.out.println("Available Tools = " + toolsList);
+
+		CallToolResult weatherForcastResult = client.callTool(new CallToolRequest("getTemperature",
+				Map.of("latitude", "47.6062", "longitude", "-122.3321", "toolContext", new ToolContext(Map.of()))));
+		System.out.println("Weather Forcast: " + weatherForcastResult);
+
+		client.closeGracefully();
+
+	}
+
+}
diff --git a/model-context-protocol/sqlite/chatbot/pom.xml b/model-context-protocol/sqlite/chatbot/pom.xml
index b0eb32f..f6ce845 100644
--- a/model-context-protocol/sqlite/chatbot/pom.xml
+++ b/model-context-protocol/sqlite/chatbot/pom.xml
@@ -22,7 +22,7 @@
 			
 				org.springframework.ai
 				spring-ai-bom
-				1.0.0-M5
+				1.0.0-SNAPSHOT
 				pom
 				import
 			
@@ -37,13 +37,12 @@
 
 		
 			org.springframework.ai
-			spring-ai-openai-spring-boot-starter
+			spring-ai-starter-model-openai
 		
 
 		
-			org.springframework.experimental
-			spring-ai-mcp
-			0.6.0
+			org.springframework.ai
+			spring-ai-starter-mcp-client
 		
 
 	
diff --git a/model-context-protocol/sqlite/chatbot/src/main/java/org/springframework/ai/mcp/samples/sqlite/Application.java b/model-context-protocol/sqlite/chatbot/src/main/java/org/springframework/ai/mcp/samples/sqlite/Application.java
index 0993dfb..83d6254 100644
--- a/model-context-protocol/sqlite/chatbot/src/main/java/org/springframework/ai/mcp/samples/sqlite/Application.java
+++ b/model-context-protocol/sqlite/chatbot/src/main/java/org/springframework/ai/mcp/samples/sqlite/Application.java
@@ -5,14 +5,15 @@ import java.time.Duration;
 import java.util.List;
 import java.util.Scanner;
 
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.ServerParameters;
+import io.modelcontextprotocol.client.transport.StdioClientTransport;
+
 import org.springframework.ai.chat.client.ChatClient;
 import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
 import org.springframework.ai.chat.memory.InMemoryChatMemory;
-import org.springframework.ai.mcp.client.McpClient;
-import org.springframework.ai.mcp.client.McpSyncClient;
-import org.springframework.ai.mcp.client.transport.ServerParameters;
-import org.springframework.ai.mcp.client.transport.StdioClientTransport;
-import org.springframework.ai.mcp.spring.McpFunctionCallback;
+import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -28,12 +29,12 @@ public class Application {
 
 	@Bean
 	public CommandLineRunner interactiveChat(ChatClient.Builder chatClientBuilder,
-			List functionCallbacks,
+			List mcpClients,
 			ConfigurableApplicationContext context) {
 		return args -> {
 
 			var chatClient = chatClientBuilder
-					.defaultFunctions(functionCallbacks.toArray(new McpFunctionCallback[0]))
+					.defaultTools(new SyncMcpToolCallbackProvider(mcpClients))
 					.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
 					.build();
 
@@ -61,16 +62,6 @@ public class Application {
 		};
 	}
 
-	@Bean
-	public List functionCallbacks(McpSyncClient mcpClient) {
-
-		var callbacks = mcpClient.listTools(null)
-				.tools()
-				.stream()
-				.map(tool -> new McpFunctionCallback(mcpClient, tool))
-				.toList();
-		return callbacks;
-	}
 
 	@Bean(destroyMethod = "close")
 	public McpSyncClient mcpClient() {
diff --git a/model-context-protocol/sqlite/simple/pom.xml b/model-context-protocol/sqlite/simple/pom.xml
index 0797eb3..dc5ebd9 100644
--- a/model-context-protocol/sqlite/simple/pom.xml
+++ b/model-context-protocol/sqlite/simple/pom.xml
@@ -22,7 +22,7 @@
 			
 				org.springframework.ai
 				spring-ai-bom
-				1.0.0-M5
+				1.0.0-SNAPSHOT
 				pom
 				import
 			
@@ -37,13 +37,12 @@
 
 		
 			org.springframework.ai
-			spring-ai-openai-spring-boot-starter
+			spring-ai-starter-model-openai
 		
 
 		
-			org.springframework.experimental
-			spring-ai-mcp
-			0.6.0
+			org.springframework.ai
+			spring-ai-starter-mcp-client
 		
 
 	
diff --git a/model-context-protocol/sqlite/simple/src/main/java/org/springframework/ai/mcp/samples/sqlite/Application.java b/model-context-protocol/sqlite/simple/src/main/java/org/springframework/ai/mcp/samples/sqlite/Application.java
index 4f09466..4db16d4 100644
--- a/model-context-protocol/sqlite/simple/src/main/java/org/springframework/ai/mcp/samples/sqlite/Application.java
+++ b/model-context-protocol/sqlite/simple/src/main/java/org/springframework/ai/mcp/samples/sqlite/Application.java
@@ -4,12 +4,13 @@ import java.nio.file.Paths;
 import java.time.Duration;
 import java.util.List;
 
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.ServerParameters;
+import io.modelcontextprotocol.client.transport.StdioClientTransport;
+
 import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.ai.mcp.client.McpClient;
-import org.springframework.ai.mcp.client.McpSyncClient;
-import org.springframework.ai.mcp.client.transport.ServerParameters;
-import org.springframework.ai.mcp.client.transport.StdioClientTransport;
-import org.springframework.ai.mcp.spring.McpFunctionCallback;
+import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -25,11 +26,11 @@ public class Application {
 
 	@Bean
 	public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder,
-			List functionCallbacks, ConfigurableApplicationContext context) {
+			List mcpClients, ConfigurableApplicationContext context) {
 
 		return args -> {
 			var chatClient = chatClientBuilder
-					.defaultFunctions(functionCallbacks.toArray(new McpFunctionCallback[0]))
+					.defaultTools(new SyncMcpToolCallbackProvider(mcpClients))
 					.build();
 			System.out.println("Running predefined questions with AI model responses:\n");
 
@@ -59,17 +60,6 @@ public class Application {
 		};
 	}
 
-	@Bean
-	public List functionCallbacks(McpSyncClient mcpClient) {
-
-		var callbacks = mcpClient.listTools(null)
-				.tools()
-				.stream()
-				.map(tool -> new McpFunctionCallback(mcpClient, tool))
-				.toList();
-		return callbacks;
-	}
-
 	@Bean(destroyMethod = "close")
 	public McpSyncClient mcpClient() {
 
diff --git a/model-context-protocol/weather/manual-webflux-server/pom.xml b/model-context-protocol/weather/manual-webflux-server/pom.xml
index 8101dab..5afd7af 100644
--- a/model-context-protocol/weather/manual-webflux-server/pom.xml
+++ b/model-context-protocol/weather/manual-webflux-server/pom.xml
@@ -19,7 +19,7 @@
     Sample Spring Boot application demonstrating MCP client and server usage
 
     
-        0.7.0-SNAPSHOT
+        0.8.0-SNAPSHOT
     
 
     
@@ -47,7 +47,7 @@
             org.springframework.ai
             spring-ai-core
         
-        
+
         
             org.springframework.ai
             spring-ai-mcp
@@ -102,6 +102,18 @@
     
 
     
+        
+            Central Portal Snapshots
+            central-portal-snapshots
+            https://central.sonatype.com/repository/maven-snapshots/
+            
+                false
+            
+            
+                true
+            
+        
+
         
             spring-milestones
             Spring Milestones
diff --git a/model-context-protocol/weather/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerConfig.java b/model-context-protocol/weather/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerConfig.java
index 5177182..14f33c6 100644
--- a/model-context-protocol/weather/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerConfig.java
+++ b/model-context-protocol/weather/manual-webflux-server/src/main/java/org/springframework/ai/mcp/sample/server/McpServerConfig.java
@@ -3,10 +3,10 @@ package org.springframework.ai.mcp.sample.server;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import io.modelcontextprotocol.server.McpServer;
 import io.modelcontextprotocol.server.McpSyncServer;
-import io.modelcontextprotocol.server.transport.StdioServerTransport;
-import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport;
+import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
+import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
 import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
+import io.modelcontextprotocol.spec.McpServerTransportProvider;
 
 import org.springframework.ai.mcp.McpToolUtils;
 import org.springframework.ai.tool.ToolCallbacks;
@@ -21,23 +21,23 @@ public class McpServerConfig {
 	// STDIO transport
 	@Bean
 	@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "stdio")
-	public StdioServerTransport stdioServerTransport() {
-		return new StdioServerTransport();
+	public StdioServerTransportProvider stdioServerTransportProvider() {
+		return new StdioServerTransportProvider();
 	}
 
 	// SSE transport
 	@Bean
 	@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
-	public WebFluxSseServerTransport sseServerTransport() {
-		return new WebFluxSseServerTransport(new ObjectMapper(), "/mcp/message");
+	public WebFluxSseServerTransportProvider sseServerTransportProvider() {
+		return new WebFluxSseServerTransportProvider(new ObjectMapper(), "/mcp/message");
 	}
 
 	// Router function for SSE transport used by Spring WebFlux to start an HTTP
 	// server.
 	@Bean
 	@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "sse")
-	public RouterFunction mcpRouterFunction(WebFluxSseServerTransport transport) {
-		return transport.getRouterFunction();
+	public RouterFunction mcpRouterFunction(WebFluxSseServerTransportProvider transportProvider) {
+		return transportProvider.getRouterFunction();
 	}
 
 	@Bean
@@ -46,7 +46,7 @@ public class McpServerConfig {
 	}
 
 	@Bean
-	public McpSyncServer mcpServer(ServerMcpTransport transport, WeatherApiClient weatherApiClient) { // @formatter:off
+	public McpSyncServer mcpServer(McpServerTransportProvider transportProvider, WeatherApiClient weatherApiClient) { // @formatter:off
 
 		// Configure server capabilities with resource support
 		var capabilities = McpSchema.ServerCapabilities.builder()
@@ -55,10 +55,10 @@ public class McpServerConfig {
 			.build();
 
 		// Create the server with both tool and resource capabilities
-		McpSyncServer server = McpServer.sync(transport)
+		McpSyncServer server = McpServer.sync(transportProvider)
 			.serverInfo("MCP Demo Weather Server", "1.0.0")
 			.capabilities(capabilities)
-			.tools(McpToolUtils.toSyncToolRegistration(ToolCallbacks.from(weatherApiClient))) // Add @Tools
+			.tools(McpToolUtils.toSyncToolSpecifications(ToolCallbacks.from(weatherApiClient))) // Add @Tools
 			.build();
 		
 		return server; // @formatter:on
diff --git a/model-context-protocol/weather/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java b/model-context-protocol/weather/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java
index 518658d..a02400c 100644
--- a/model-context-protocol/weather/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java
+++ b/model-context-protocol/weather/manual-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java
@@ -18,7 +18,7 @@ package org.springframework.ai.mcp.sample.client;
 import java.util.Map;
 
 import io.modelcontextprotocol.client.McpClient;
-import io.modelcontextprotocol.spec.ClientMcpTransport;
+import io.modelcontextprotocol.spec.McpClientTransport;
 import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
 import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
 import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
@@ -29,9 +29,9 @@ import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
 
 public class SampleClient {
 
-	private final ClientMcpTransport transport;
+	private final McpClientTransport transport;
 
-	public SampleClient(ClientMcpTransport transport) {
+	public SampleClient(McpClientTransport transport) {
 		this.transport = transport;
 	}
 
diff --git a/model-context-protocol/weather/starter-stdio-server/pom.xml b/model-context-protocol/weather/starter-stdio-server/pom.xml
index 18ec531..e472a2d 100644
--- a/model-context-protocol/weather/starter-stdio-server/pom.xml
+++ b/model-context-protocol/weather/starter-stdio-server/pom.xml
@@ -34,7 +34,7 @@
     
         
             org.springframework.ai
-            spring-ai-mcp-server-spring-boot-starter
+            spring-ai-starter-mcp-server
         
         
         
diff --git a/model-context-protocol/weather/starter-webflux-server/pom.xml b/model-context-protocol/weather/starter-webflux-server/pom.xml
index 93bae11..78a8fe6 100644
--- a/model-context-protocol/weather/starter-webflux-server/pom.xml
+++ b/model-context-protocol/weather/starter-webflux-server/pom.xml
@@ -33,7 +33,7 @@
     
         
             org.springframework.ai
-            spring-ai-mcp-server-webflux-spring-boot-starter
+            spring-ai-starter-mcp-server-webflux
         
     
 
diff --git a/model-context-protocol/weather/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java b/model-context-protocol/weather/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java
index 518658d..a02400c 100644
--- a/model-context-protocol/weather/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java
+++ b/model-context-protocol/weather/starter-webflux-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java
@@ -18,7 +18,7 @@ package org.springframework.ai.mcp.sample.client;
 import java.util.Map;
 
 import io.modelcontextprotocol.client.McpClient;
-import io.modelcontextprotocol.spec.ClientMcpTransport;
+import io.modelcontextprotocol.spec.McpClientTransport;
 import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
 import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
 import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
@@ -29,9 +29,9 @@ import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
 
 public class SampleClient {
 
-	private final ClientMcpTransport transport;
+	private final McpClientTransport transport;
 
-	public SampleClient(ClientMcpTransport transport) {
+	public SampleClient(McpClientTransport transport) {
 		this.transport = transport;
 	}
 
diff --git a/model-context-protocol/weather/starter-webmvc-server/pom.xml b/model-context-protocol/weather/starter-webmvc-server/pom.xml
index 49ce75e..9eefa09 100644
--- a/model-context-protocol/weather/starter-webmvc-server/pom.xml
+++ b/model-context-protocol/weather/starter-webmvc-server/pom.xml
@@ -33,7 +33,7 @@
     
         
             org.springframework.ai
-            spring-ai-mcp-server-webmvc-spring-boot-starter
+            spring-ai-starter-mcp-server-webmvc
         
     
 
diff --git a/model-context-protocol/weather/starter-webmvc-server/src/main/resources/application.properties b/model-context-protocol/weather/starter-webmvc-server/src/main/resources/application.properties
index 0988617..e1ab7a4 100644
--- a/model-context-protocol/weather/starter-webmvc-server/src/main/resources/application.properties
+++ b/model-context-protocol/weather/starter-webmvc-server/src/main/resources/application.properties
@@ -5,7 +5,7 @@
 spring.main.banner-mode=off
 # logging.pattern.console=
 
-spring.ai.mcp.server.stdio=false
+# spring.ai.mcp.server.stdio=false
 
 spring.ai.mcp.server.name=my-weather-server
 spring.ai.mcp.server.version=0.0.1
diff --git a/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java b/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java
index f820715..4343ab0 100644
--- a/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java
+++ b/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java
@@ -34,7 +34,7 @@ public class ClientStdio {
 	public static void main(String[] args) {
 
 		System.out.println(new File(".").getAbsolutePath());
-
+		
 		var stdioParams = ServerParameters.builder("java")
 				.args("-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none",
 						"-Dlogging.pattern.console=", "-jar",
diff --git a/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java b/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java
index 518658d..a02400c 100644
--- a/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java
+++ b/model-context-protocol/weather/starter-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java
@@ -18,7 +18,7 @@ package org.springframework.ai.mcp.sample.client;
 import java.util.Map;
 
 import io.modelcontextprotocol.client.McpClient;
-import io.modelcontextprotocol.spec.ClientMcpTransport;
+import io.modelcontextprotocol.spec.McpClientTransport;
 import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
 import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
 import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
@@ -29,9 +29,9 @@ import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
 
 public class SampleClient {
 
-	private final ClientMcpTransport transport;
+	private final McpClientTransport transport;
 
-	public SampleClient(ClientMcpTransport transport) {
+	public SampleClient(McpClientTransport transport) {
 		this.transport = transport;
 	}
 
diff --git a/model-context-protocol/web-search/brave-chatbot/pom.xml b/model-context-protocol/web-search/brave-chatbot/pom.xml
index 6adddc9..c41dcad 100644
--- a/model-context-protocol/web-search/brave-chatbot/pom.xml
+++ b/model-context-protocol/web-search/brave-chatbot/pom.xml
@@ -35,12 +35,12 @@
 
 		
 			org.springframework.ai
-			spring-ai-mcp-client-spring-boot-starter
+			spring-ai-starter-mcp-client
 		
 
 		
 			org.springframework.ai
-			spring-ai-anthropic-spring-boot-starter
+			spring-ai-starter-model-anthropic
 		
 
 	
diff --git a/model-context-protocol/web-search/brave-chatbot/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java b/model-context-protocol/web-search/brave-chatbot/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java
index a318f50..ff3f64d 100644
--- a/model-context-protocol/web-search/brave-chatbot/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java
+++ b/model-context-protocol/web-search/brave-chatbot/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java
@@ -3,9 +3,12 @@ package org.springframework.ai.mcp.samples.brave;
 import java.util.List;
 import java.util.Scanner;
 
+import io.modelcontextprotocol.client.McpSyncClient;
+
 import org.springframework.ai.chat.client.ChatClient;
 import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
 import org.springframework.ai.chat.memory.InMemoryChatMemory;
+import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
 import org.springframework.ai.tool.ToolCallback;
 import org.springframework.ai.tool.ToolCallbackProvider;
 import org.springframework.boot.CommandLineRunner;
@@ -21,13 +24,13 @@ public class Application {
 	}
 
 	@Bean
-	public CommandLineRunner chatbot(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools) {
+	public CommandLineRunner chatbot(ChatClient.Builder chatClientBuilder, List mcpSyncClients) {
 
 		return args -> {
 
 			var chatClient = chatClientBuilder
 					.defaultSystem("You are useful assistant and can perform web searches Brave's search API to reply to your questions.")
-					.defaultTools(tools)
+					.defaultTools(new SyncMcpToolCallbackProvider(mcpSyncClients))
 					.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
 					.build();
 
diff --git a/model-context-protocol/web-search/brave-starter/README.md b/model-context-protocol/web-search/brave-starter/README.md
index 19582ae..ca49553 100644
--- a/model-context-protocol/web-search/brave-starter/README.md
+++ b/model-context-protocol/web-search/brave-starter/README.md
@@ -11,12 +11,12 @@ The project uses the following key dependencies:
 ```xml
 
     org.springframework.ai
-    spring-ai-mcp-client-spring-boot-starter
+    spring-ai-starter-mcp-client
 
 
 
     org.springframework.ai
-    spring-ai-openai-spring-boot-starter
+    spring-ai-starter-model-openai
 
 ```
 
@@ -76,12 +76,12 @@ The project uses Spring AI's MCP client Spring Boot starter and OpenAI starter:
 ```xml
 
     org.springframework.ai
-    spring-ai-mcp-client-spring-boot-starter
+    spring-ai-starter-mcp-client
 
 
 
     org.springframework.ai
-    spring-ai-openai-spring-boot-starter
+    spring-ai-starter-model-openai
 
 ```
 
diff --git a/model-context-protocol/web-search/brave-starter/pom.xml b/model-context-protocol/web-search/brave-starter/pom.xml
index 54d1749..f143841 100644
--- a/model-context-protocol/web-search/brave-starter/pom.xml
+++ b/model-context-protocol/web-search/brave-starter/pom.xml
@@ -34,16 +34,16 @@
 	
 		
 			org.springframework.ai
-			spring-ai-mcp-client-spring-boot-starter
+			spring-ai-starter-mcp-client
 		
 
 		
 		
 			org.springframework.ai
-			spring-ai-anthropic-spring-boot-starter
+			spring-ai-starter-model-anthropic
 		
 		
 	
diff --git a/model-context-protocol/web-search/brave-starter/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java b/model-context-protocol/web-search/brave-starter/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java
index 0299055..1923941 100644
--- a/model-context-protocol/web-search/brave-starter/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java
+++ b/model-context-protocol/web-search/brave-starter/src/main/java/org/springframework/ai/mcp/samples/brave/Application.java
@@ -15,7 +15,12 @@
  */
 package org.springframework.ai.mcp.samples.brave;
 
+import java.util.List;
+
+import io.modelcontextprotocol.client.McpSyncClient;
+
 import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
 import org.springframework.ai.tool.ToolCallbackProvider;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.boot.SpringApplication;
@@ -31,13 +36,13 @@ public class Application {
 	}	
 
 	@Bean
-	public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools,
+	public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, List mcpSyncClients,
 			ConfigurableApplicationContext context) {
 
 		return args -> {
 
 			var chatClient = chatClientBuilder
-					.defaultTools(tools)
+					.defaultTools(new SyncMcpToolCallbackProvider(mcpSyncClients))
 					.build();
 
 			String question = "Does Spring AI support the Model Context Protocol? Please provide some references.";
diff --git a/pom.xml b/pom.xml
index 99a57c4..0783ec2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,11 +26,6 @@
 
 
 		
-		model-context-protocol/book-library/manual-servlet-server
-		model-context-protocol/book-library/manual-webflux-server
-		model-context-protocol/book-library/manual-webmvc-server
-		model-context-protocol/book-library/starter-webflux-server
-
 		model-context-protocol/brave
 
 
@@ -50,6 +45,10 @@
 		model-context-protocol/web-search/brave-chatbot
 
 
+		model-context-protocol/sampling/mcp-weather-webmvc-server
+		model-context-protocol/sampling/mcp-sampling-client
+
+
 		agentic-patterns