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 0000000..c1dd12f Binary files /dev/null and b/model-context-protocol/sampling/mcp-sampling-client/.mvn/wrapper/maven-wrapper.jar differ 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..7f7576e --- /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-mcp-client-spring-boot-starter + + + + org.springframework.ai + spring-ai-openai-spring-boot-starter + + + + org.springframework.ai + spring-ai-anthropic-spring-boot-starter + + + + + + + + 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..c8854fb --- /dev/null +++ b/model-context-protocol/sampling/mcp-sampling-client/src/main/java/org/springframework/ai/mcp/samples/client/McpClientApplication.java @@ -0,0 +1,100 @@ +/* + * 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.beans.factory.ObjectProvider; +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, ObjectProvider> mcpClientsProvider) { + + return args -> { + + var mcpToolProvider = new SyncMcpToolCallbackProvider( + mcpClientsProvider.stream().flatMap(List::stream).toList()); + + ChatClient chatClient = ChatClient.builder(openAiChatModel).defaultTools(mcpToolProvider).build(); + + String userQuestion = """ + What is the wather 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, spec) -> { + spec.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..5d6ea5a --- /dev/null +++ b/model-context-protocol/sampling/mcp-sampling-client/src/main/resources/application.properties @@ -0,0 +1,13 @@ +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 diff --git a/model-context-protocol/sampling/mcp-weather-webmvc-server/.mvn/wrapper/maven-wrapper.properties b/model-context-protocol/sampling/mcp-weather-webmvc-server/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# 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/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/sampling/mcp-weather-webmvc-server/mvnw b/model-context-protocol/sampling/mcp-weather-webmvc-server/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/mvnw @@ -0,0 +1,259 @@ +#!/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/sampling/mcp-weather-webmvc-server/mvnw.cmd b/model-context-protocol/sampling/mcp-weather-webmvc-server/mvnw.cmd new file mode 100644 index 0000000..b150b91 --- /dev/null +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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/sampling/mcp-weather-webmvc-server/pom.xml b/model-context-protocol/sampling/mcp-weather-webmvc-server/pom.xml new file mode 100644 index 0000000..d0eab1c --- /dev/null +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.6 + + + + com.example + + mcp-sampling-weather-server + 0.0.1-SNAPSHOT + + Spring AI MCP Sampling - Weather Server + Sample Spring Boot application demonstrating MCP client and server sampling usage + + + + + org.springframework.ai + spring-ai-bom + 1.0.0-SNAPSHOT + pom + import + + + + + + + org.springframework.ai + spring-ai-mcp-server-webmvc-spring-boot-starter + + + + + + + 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/milestone + + 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-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/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java new file mode 100644 index 0000000..3846cc5 --- /dev/null +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java @@ -0,0 +1,31 @@ +/* +* 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.HttpClientSseClientTransport; + + +/** + * @author Christian Tzolov + */ +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/sampling/mcp-weather-webmvc-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 new file mode 100644 index 0000000..325448e --- /dev/null +++ b/model-context-protocol/sampling/mcp-weather-webmvc-server/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java @@ -0,0 +1,49 @@ +/* +* 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 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) { + + 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/sampling/mcp-weather-webmvc-server/target/mcp-sampling-weather-server-0.0.1-SNAPSHOT.jar") + .build(); + + 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/pom.xml b/pom.xml index 9931258..0783ec2 100644 --- a/pom.xml +++ b/pom.xml @@ -45,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