diff --git a/start-mcp-java-hello-world-sse/README.md b/start-mcp-java-hello-world-sse/README.md new file mode 100644 index 0000000..c0d80eb --- /dev/null +++ b/start-mcp-java-hello-world-sse/README.md @@ -0,0 +1,106 @@ + +> 注:当前项目为 Serverless Devs 应用,由于应用中会存在需要初始化才可运行的变量(例如应用部署地区、函数名等等),所以**不推荐**直接 Clone 本仓库到本地进行部署或直接复制 s.yaml 使用,**强烈推荐**通过 `s init ${模版名称}` 的方法或应用中心进行初始化,详情可参考[部署 & 体验](#部署--体验) 。 + +# start-mcp-java-hello-world-sse 帮助文档 + + + +基于 Java 的 FC MCP SSE Server 案例 + + + + +## 资源准备 + +使用该项目,您需要有开通以下服务并拥有对应权限: + + + + + +| 服务/业务 | 权限 | 相关文档 | +| --- | --- | --- | +| 函数计算 | AliyunFCFullAccess | [帮助文档](https://help.aliyun.com/product/2508973.html) [计费文档](https://help.aliyun.com/document_detail/2512928.html) | + + + + + + + + + + + + + + + +## 部署 & 体验 + + + +- :fire: 通过 [云原生应用开发平台 CAP](https://cap.console.aliyun.com/template-detail?template=start-mcp-java-hello-world) ,[![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](https://cap.console.aliyun.com/template-detail?template=start-mcp-java-hello-world) 该应用。 + + + + + + + +## 案例介绍 + + + +这是一个部署到 FC 的 MCP Server 的 Java hello world 样例。您可以通过这个模版初始化一个简单的、开箱即用的、可进行二次开发的 MCP Server。 + +此样例包含一个名为 `helloWorld` 的 Tool,您可基于此样例 Tool 进行二次开发。 + + + + + + + + + +## 使用流程 + + + +部署完成拿到 URL 后,准备好支持 SSE 或 STDIO 的 MCP Client,通过 Transport 进行连接。 + + + +## 二次开发指南 + + + +此样例包含一个名为 `helloWorld` 的 Tool,您可基于此样例 Tool 进行二次开发。 +```java +@Service +public class McpService { + public McpService() {} + /** + * A sample tool. Return string 'Hello World!' + * @return String + */ + @Tool(description = "Return string 'Hello World!'") + public String helloWorld() { + return "Hello World!"; + } + + public static void main(String[] args) { + McpService client = new McpService(); + System.out.println(client.helloWorld()); + } +} +``` + + + + + + + + diff --git a/start-mcp-java-hello-world-sse/publish.yaml b/start-mcp-java-hello-world-sse/publish.yaml new file mode 100644 index 0000000..1d4a674 --- /dev/null +++ b/start-mcp-java-hello-world-sse/publish.yaml @@ -0,0 +1,42 @@ +Edition: 3.0.0 +Type: Project +Name: fcai-start-mcp-java-hello-world-sse +Version: dev +Provider: + - 阿里云 # 取值内容参考:https://api.devsapp.cn/v3/common/args.html +Description: 基于 Java 的 FC MCP SSE Server 案例 +HomePage: https://github.com/devsapp/fcai-mcp-servers/tree/main/start-mcp-java-hello-world-sse +Tags: #标签详情 + - MCP + - Develop +Category: MCP服务 # 取值内容参考:https://api.devsapp.cn/v3/common/args.html +Service: # 使用的服务 + 函数计算: # 取值内容参考:https://api.devsapp.cn/v3/common/args.html + Authorities: #权限描述 + - AliyunFCFullAccess # 所需要的权限,例如AliyunFCFullAccess +Organization: 阿里云函数计算(FC) # 所属组织 +Effective: Public # 是否公开,取值:Public,Private,Organization +Parameters: + type: object + additionalProperties: false # 不允许增加其他属性 + required: # 必填项 + - region + properties: + region: + title: 地域 + type: string + default: cn-hangzhou + description: 创建应用所在的地区 + required: true + enum: + - cn-beijing + - cn-hangzhou + - cn-shanghai + - cn-shenzhen + - ap-southeast-1 + functionName: + title: 函数名 + type: string + pattern: "^[a-zA-Z_][a-zA-Z0-9-_]{0,127}$" + default: start-mcp-server-java-${default-suffix} + description: 函数名,支持字母、数字、下划线、连字符,不能以数字或连字符开头,长度为1~64个字符。 diff --git a/start-mcp-java-hello-world-sse/src/.signore b/start-mcp-java-hello-world-sse/src/.signore new file mode 100644 index 0000000..75adc01 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/.signore @@ -0,0 +1,3 @@ +./code/node_modules/ +./code/dist +./code/target/ diff --git a/start-mcp-java-hello-world-sse/src/build.yaml b/start-mcp-java-hello-world-sse/src/build.yaml new file mode 100644 index 0000000..b0e39a8 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/build.yaml @@ -0,0 +1,8 @@ +start-mcp-java-hello-world: + default: + rootPath: ./code + languages: + - java21 + steps: + - run: chmod +x ./mvnw + - run: ./mvnw clean install -DskipTests \ No newline at end of file diff --git a/start-mcp-java-hello-world-sse/src/code/.mvn/wrapper/maven-wrapper.properties b/start-mcp-java-hello-world-sse/src/code/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/.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/start-mcp-java-hello-world-sse/src/code/README.md b/start-mcp-java-hello-world-sse/src/code/README.md new file mode 100644 index 0000000..11af7b8 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/README.md @@ -0,0 +1,233 @@ +# Spring AI MCP Weather Server Sample with WebMVC Starter + +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 National Weather Service API. + +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 +- Two weather-related tools: + - Get weather forecast by location (latitude/longitude) + - Get weather alerts by US state + +## 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-weather-starter-webmvc-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-weather-starter-webmvc-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 Forecast Tool +- Name: `getWeatherForecastByLocation` +- Description: Get weather forecast for a specific latitude/longitude +- Parameters: + - `latitude`: double - Latitude coordinate + - `longitude`: double - Longitude coordinate + +### Weather Alerts Tool +- Name: `getAlerts` +- Description: Get weather alerts for a US state +- Parameters: + - `state`: String - Two-letter US state code (e.g., CA, NY) + +## 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 tools using the `@Tool` annotation: + +```java +@Service +public class WeatherService { + @Tool(description = "Get weather forecast for a specific latitude/longitude") + public String getWeatherForecastByLocation(double latitude, double longitude) { + // Implementation using weather.gov API + } + + @Tool(description = "Get weather alerts for a US state. Input is Two-letter US state code (e.g., CA, NY)") + public String getAlerts(String state) { + // Implementation using weather.gov API + } +} +``` + +## 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-weather-starter-webmvc-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 +- [ClientSse.java](src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java): SSE transport connection + +For a better development experience, consider using the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html). These starters enable auto-configuration of multiple STDIO and/or SSE connections to MCP servers. See the [starter-default-client](../../client-starter/starter-default-client) project for examples. + +### Boot Starter Clients + +Let's use the [starter-default-client](../../client-starter/starter-default-client) client to connect to our weather `starter-webmvc-server`. + +Follow the `starter-default-client` readme instruction to build a `mcp-starter-default-client-0.0.1-SNAPSHOT.jar` client application. + +#### STDIO Transport + +1. Create a `mcp-servers-config.json` configuration file with this content: + +```json +{ + "mcpServers": { + "weather-starter-webmvc-server": { + "command": "java", + "args": [ + "-Dspring.ai.mcp.server.stdio=true", + "-Dspring.main.web-application-type=none", + "-Dlogging.pattern.console=", + "-jar", + "/absolute/path/to/mcp-weather-starter-webmvc-server-0.0.1-SNAPSHOT.jar" + ] + } + } +} +``` + +2. Run the client using the configuration file: + +```bash +java -Dspring.ai.mcp.client.stdio.servers-configuration=file:mcp-servers-config.json \ + -Dai.user.input='What is the weather in NY?' \ + -Dlogging.pattern.console= \ + -jar mcp-starter-default-client-0.0.1-SNAPSHOT.jar +``` + +#### SSE (WebMVC) Transport + +1. Start the `mcp-weather-starter-webmvc-server`: + +```bash +java -jar mcp-weather-starter-webmvc-server-0.0.1-SNAPSHOT.jar +``` + +starts the MCP server on port 8080. + +2. In another console start the client configured with SSE transport: + +```bash +java -Dspring.ai.mcp.client.sse.connections.weather-server.url=http://localhost:8080 \ + -Dlogging.pattern.console= \ + -Dai.user.input='What is the weather in NY?' \ + -jar mcp-starter-default-client-0.0.1-SNAPSHOT.jar +``` + +## Additional Resources + +* [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/start-mcp-java-hello-world-sse/src/code/bootstrap b/start-mcp-java-hello-world-sse/src/code/bootstrap new file mode 100755 index 0000000..f24e828 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/bootstrap @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +java -jar target/mcp-starter-webmvc-server-0.0.1-SNAPSHOT.jar \ No newline at end of file diff --git a/start-mcp-java-hello-world-sse/src/code/mvnw b/start-mcp-java-hello-world-sse/src/code/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/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/start-mcp-java-hello-world-sse/src/code/mvnw.cmd b/start-mcp-java-hello-world-sse/src/code/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/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/start-mcp-java-hello-world-sse/src/code/pom.xml b/start-mcp-java-hello-world-sse/src/code/pom.xml new file mode 100644 index 0000000..d8482af --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.6 + + + + com.example + + mcp-starter-webmvc-server + 0.0.1-SNAPSHOT + + Spring AI MCP Sample + Sample Spring Boot application demonstrating MCP client and server usage + + + + + org.springframework.ai + spring-ai-bom + 1.0.0-SNAPSHOT + pom + import + + + + + + + org.springframework.ai + spring-ai-starter-mcp-server-webmvc + + + + + + + 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 + + + + central-portal-snapshots + Central Portal Snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + diff --git a/start-mcp-java-hello-world-sse/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java b/start-mcp-java-hello-world-sse/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java new file mode 100644 index 0000000..be7cff6 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java @@ -0,0 +1,23 @@ +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) { + // Start the Spring Boot application + // and the MCP server + System.out.println("MCP server started..."); + SpringApplication.run(McpServerApplication.class, args); + } + + @Bean + public ToolCallbackProvider tools(McpService mcpService) { + return MethodToolCallbackProvider.builder().toolObjects(mcpService).build(); + } +} diff --git a/start-mcp-java-hello-world-sse/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpService.java b/start-mcp-java-hello-world-sse/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpService.java new file mode 100644 index 0000000..e605f89 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpService.java @@ -0,0 +1,21 @@ +package org.springframework.ai.mcp.sample.server; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.stereotype.Service; + +@Service +public class McpService { + public McpService() {} + /** + * A sample tool. Return string 'Hello World!' + * @return String + */ + @Tool(description = "Return string 'Hello World!'") + public String helloWorld() { + return "Hello World!"; + } + + public static void main(String[] args) { + McpService client = new McpService(); + System.out.println(client.helloWorld()); + } +} \ No newline at end of file diff --git a/start-mcp-java-hello-world-sse/src/code/src/main/resources/application.properties b/start-mcp-java-hello-world-sse/src/code/src/main/resources/application.properties new file mode 100644 index 0000000..e1ab7a4 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/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=my-weather-server +spring.ai.mcp.server.version=0.0.1 + +logging.file.name=./model-context-protocol/weather/starter-webmvc-server/target/starter-webmvc-server.log diff --git a/start-mcp-java-hello-world-sse/src/code/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java b/start-mcp-java-hello-world-sse/src/code/src/test/java/org/springframework/ai/mcp/sample/client/ClientSse.java new file mode 100644 index 0000000..3846cc5 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/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/start-mcp-java-hello-world-sse/src/code/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java b/start-mcp-java-hello-world-sse/src/code/src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java new file mode 100644 index 0000000..4343ab0 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/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/weather/starter-webmvc-server/target/mcp-weather-starter-webmvc-server-0.0.1-SNAPSHOT.jar") + .build(); + + var transport = new StdioClientTransport(stdioParams); + + new SampleClient(transport).run(); + } + +} diff --git a/start-mcp-java-hello-world-sse/src/code/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java b/start-mcp-java-hello-world-sse/src/code/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java new file mode 100644 index 0000000..11afef5 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/code/src/test/java/org/springframework/ai/mcp/sample/client/SampleClient.java @@ -0,0 +1,64 @@ +/* +* 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.CallToolRequest; +import io.modelcontextprotocol.spec.McpSchema.CallToolResult; +import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; + +/** + * @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).build(); + + client.initialize(); + + client.ping(); + + // List and demonstrate tools + ListToolsResult toolsList = client.listTools(); + System.out.println("Available Tools = " + toolsList); + toolsList.tools().stream().forEach(tool -> { + System.out.println("Tool: " + tool.name() + ", description: " + tool.description() + ", schema: " + tool.inputSchema()); + }); + + CallToolResult weatherForcastResult = client.callTool(new CallToolRequest("getWeatherForecastByLocation", + Map.of("latitude", "47.6062", "longitude", "-122.3321"))); + System.out.println("Weather Forcast: " + weatherForcastResult); + + CallToolResult alertResult = client.callTool(new CallToolRequest("getAlerts", Map.of("state", "NY"))); + System.out.println("Alert Response = " + alertResult); + + client.closeGracefully(); + + } + +} diff --git a/start-mcp-java-hello-world-sse/src/readme.md b/start-mcp-java-hello-world-sse/src/readme.md new file mode 100644 index 0000000..78f564f --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/readme.md @@ -0,0 +1,106 @@ + +> 注:当前项目为 Serverless Devs 应用,由于应用中会存在需要初始化才可运行的变量(例如应用部署地区、函数名等等),所以**不推荐**直接 Clone 本仓库到本地进行部署或直接复制 s.yaml 使用,**强烈推荐**通过 `s init ${模版名称}` 的方法或应用中心进行初始化,详情可参考[部署 & 体验](#部署--体验) 。 + +# start-mcp-java-hello-world 帮助文档 + + + +基于 Java 的 FC MCP STDIO Server 案例 + + + + +## 资源准备 + +使用该项目,您需要有开通以下服务并拥有对应权限: + + + + + +| 服务/业务 | 权限 | 相关文档 | +| --- | --- | --- | +| 函数计算 | AliyunFCFullAccess | [帮助文档](https://help.aliyun.com/product/2508973.html) [计费文档](https://help.aliyun.com/document_detail/2512928.html) | + + + + + + + + + + + + + + + +## 部署 & 体验 + + + +- :fire: 通过 [云原生应用开发平台 CAP](https://cap.console.aliyun.com/template-detail?template=start-mcp-java-hello-world) ,[![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](https://cap.console.aliyun.com/template-detail?template=start-mcp-java-hello-world) 该应用。 + + + + + + + +## 案例介绍 + + + +这是一个部署到 FC 的 MCP Server 的 Java hello world 样例。您可以通过这个模版初始化一个简单的、开箱即用的、可进行二次开发的 MCP Server。 + +此样例包含一个名为 `helloWorld` 的 Tool,您可基于此样例 Tool 进行二次开发。 + + + + + + + + + +## 使用流程 + + + +部署完成拿到 URL 后,准备好支持 SSE 或 STDIO 的 MCP Client,通过 Transport 进行连接。 + + + +## 二次开发指南 + + + +此样例包含一个名为 `helloWorld` 的 Tool,您可基于此样例 Tool 进行二次开发。 +```java +@Service +public class McpService { + public McpService() {} + /** + * A sample tool. Return string 'Hello World!' + * @return String + */ + @Tool(description = "Return string 'Hello World!'") + public String helloWorld() { + return "Hello World!"; + } + + public static void main(String[] args) { + McpService client = new McpService(); + System.out.println(client.helloWorld()); + } +} +``` + + + + + + + + diff --git a/start-mcp-java-hello-world-sse/src/s.yaml b/start-mcp-java-hello-world-sse/src/s.yaml new file mode 100644 index 0000000..f80e0c8 --- /dev/null +++ b/start-mcp-java-hello-world-sse/src/s.yaml @@ -0,0 +1,17 @@ +edition: 3.0.0 +name: start-mcp-server-nodejs +access: '{{access}}' +vars: + region: '{{region}}' +resources: + start-mcp-java-hello-world: + component: fcai-mcp-server + props: + region: ${vars.region} + description: mcp server deployed by devs + transport: sse + runtime: java21 + startCommand: ./bootstrap + instanceQuota: 1 + cpu: 1 + memorySize: 2048 \ No newline at end of file diff --git a/start-mcp-java-hello-world-sse/version.md b/start-mcp-java-hello-world-sse/version.md new file mode 100644 index 0000000..4ee89d2 --- /dev/null +++ b/start-mcp-java-hello-world-sse/version.md @@ -0,0 +1,2 @@ +- 初始化项目 +- 测试项目模板 \ No newline at end of file diff --git a/start-mcp-java-hello-world/README.md b/start-mcp-java-hello-world/README.md new file mode 100644 index 0000000..78f564f --- /dev/null +++ b/start-mcp-java-hello-world/README.md @@ -0,0 +1,106 @@ + +> 注:当前项目为 Serverless Devs 应用,由于应用中会存在需要初始化才可运行的变量(例如应用部署地区、函数名等等),所以**不推荐**直接 Clone 本仓库到本地进行部署或直接复制 s.yaml 使用,**强烈推荐**通过 `s init ${模版名称}` 的方法或应用中心进行初始化,详情可参考[部署 & 体验](#部署--体验) 。 + +# start-mcp-java-hello-world 帮助文档 + + + +基于 Java 的 FC MCP STDIO Server 案例 + + + + +## 资源准备 + +使用该项目,您需要有开通以下服务并拥有对应权限: + + + + + +| 服务/业务 | 权限 | 相关文档 | +| --- | --- | --- | +| 函数计算 | AliyunFCFullAccess | [帮助文档](https://help.aliyun.com/product/2508973.html) [计费文档](https://help.aliyun.com/document_detail/2512928.html) | + + + + + + + + + + + + + + + +## 部署 & 体验 + + + +- :fire: 通过 [云原生应用开发平台 CAP](https://cap.console.aliyun.com/template-detail?template=start-mcp-java-hello-world) ,[![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](https://cap.console.aliyun.com/template-detail?template=start-mcp-java-hello-world) 该应用。 + + + + + + + +## 案例介绍 + + + +这是一个部署到 FC 的 MCP Server 的 Java hello world 样例。您可以通过这个模版初始化一个简单的、开箱即用的、可进行二次开发的 MCP Server。 + +此样例包含一个名为 `helloWorld` 的 Tool,您可基于此样例 Tool 进行二次开发。 + + + + + + + + + +## 使用流程 + + + +部署完成拿到 URL 后,准备好支持 SSE 或 STDIO 的 MCP Client,通过 Transport 进行连接。 + + + +## 二次开发指南 + + + +此样例包含一个名为 `helloWorld` 的 Tool,您可基于此样例 Tool 进行二次开发。 +```java +@Service +public class McpService { + public McpService() {} + /** + * A sample tool. Return string 'Hello World!' + * @return String + */ + @Tool(description = "Return string 'Hello World!'") + public String helloWorld() { + return "Hello World!"; + } + + public static void main(String[] args) { + McpService client = new McpService(); + System.out.println(client.helloWorld()); + } +} +``` + + + + + + + + diff --git a/start-mcp-java-hello-world/publish.yaml b/start-mcp-java-hello-world/publish.yaml new file mode 100644 index 0000000..159d667 --- /dev/null +++ b/start-mcp-java-hello-world/publish.yaml @@ -0,0 +1,42 @@ +Edition: 3.0.0 +Type: Project +Name: fcai-start-mcp-java-hello-world +Version: dev +Provider: + - 阿里云 # 取值内容参考:https://api.devsapp.cn/v3/common/args.html +Description: 基于 Java 的 FC MCP Server 案例 +HomePage: https://github.com/devsapp/fcai-mcp-servers/tree/main/start-mcp-java-hello-world +Tags: #标签详情 + - MCP + - Develop +Category: MCP服务 # 取值内容参考:https://api.devsapp.cn/v3/common/args.html +Service: # 使用的服务 + 函数计算: # 取值内容参考:https://api.devsapp.cn/v3/common/args.html + Authorities: #权限描述 + - AliyunFCFullAccess # 所需要的权限,例如AliyunFCFullAccess +Organization: 阿里云函数计算(FC) # 所属组织 +Effective: Public # 是否公开,取值:Public,Private,Organization +Parameters: + type: object + additionalProperties: false # 不允许增加其他属性 + required: # 必填项 + - region + properties: + region: + title: 地域 + type: string + default: cn-hangzhou + description: 创建应用所在的地区 + required: true + enum: + - cn-beijing + - cn-hangzhou + - cn-shanghai + - cn-shenzhen + - ap-southeast-1 + functionName: + title: 函数名 + type: string + pattern: "^[a-zA-Z_][a-zA-Z0-9-_]{0,127}$" + default: start-mcp-server-java-${default-suffix} + description: 函数名,支持字母、数字、下划线、连字符,不能以数字或连字符开头,长度为1~64个字符。 diff --git a/start-mcp-java-hello-world/src/.signore b/start-mcp-java-hello-world/src/.signore new file mode 100644 index 0000000..75adc01 --- /dev/null +++ b/start-mcp-java-hello-world/src/.signore @@ -0,0 +1,3 @@ +./code/node_modules/ +./code/dist +./code/target/ diff --git a/start-mcp-java-hello-world/src/build.yaml b/start-mcp-java-hello-world/src/build.yaml new file mode 100644 index 0000000..dd45392 --- /dev/null +++ b/start-mcp-java-hello-world/src/build.yaml @@ -0,0 +1,8 @@ +start-mcp-java-hello-world: + default: + rootPath: ./code + languages: + - java21 + steps: + - run: chmod +x ./mvnw + - run: ./mvnw clean install -DskipTests \ No newline at end of file diff --git a/start-mcp-java-hello-world/src/code/.mvn/wrapper/maven-wrapper.properties b/start-mcp-java-hello-world/src/code/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d00ff33 --- /dev/null +++ b/start-mcp-java-hello-world/src/code/.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 \ No newline at end of file diff --git a/start-mcp-java-hello-world/src/code/README.md b/start-mcp-java-hello-world/src/code/README.md new file mode 100644 index 0000000..27bebbd --- /dev/null +++ b/start-mcp-java-hello-world/src/code/README.md @@ -0,0 +1,237 @@ +# Spring AI MCP Weather STDIO Server + +A Spring Boot starter project demonstrating how to build a Model Context Protocol (MCP) server that provides weather-related tools using the National Weather Service (weather.gov) API. This project showcases the Spring AI MCP Server Boot Starter capabilities with STDIO transport implementation. + +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. + +## Prerequisites + +- Java 17 or later +- Maven 3.6 or later +- Understanding of Spring Boot and Spring AI concepts +- (Optional) Claude Desktop for AI assistant integration + +## About Spring AI MCP Server Boot Starter + +The `spring-ai-mcp-server-spring-boot-starter` provides: +- Automatic configuration of MCP server components +- Support for both synchronous and asynchronous operation modes +- STDIO transport layer implementation +- Flexible tool registration through Spring beans +- Change notification capabilities + +## Project Structure + +``` +src/ +├── main/ +│ ├── java/ +│ │ └── org/springframework/ai/mcp/sample/server/ +│ │ ├── McpServerApplication.java # Main application class with tool registration +│ │ └── WeatherService.java # Weather service implementation with MCP tools +│ └── resources/ +│ └── application.properties # Server and transport configuration +└── test/ + └── java/ + └── org/springframework/ai/mcp/sample/client/ + └── ClientStdio.java # Test client implementation +``` + +## Building and Running + +The server uses STDIO transport mode and is typically started automatically by the client. To build the server jar: + +```bash +./mvnw clean install -DskipTests +``` + +## Tool Implementation + +The project demonstrates how to implement and register MCP tools using Spring's dependency injection and auto-configuration: + +```java +@Service +public class WeatherService { + @Tool(description = "Get weather forecast for a specific latitude/longitude") + public String getWeatherForecastByLocation( + double latitude, // Latitude coordinate + double longitude // Longitude coordinate + ) { + // Implementation + } + + @Tool(description = "Get weather alerts for a US state") + public String getAlerts( + String state // Two-letter US state code (e.g., CA, NY) + ) { + // Implementation + } +} + +@SpringBootApplication +public class McpServerApplication { + @Bean + public List weatherTools(WeatherService weatherService) { + return ToolCallbacks.from(weatherService); + } +} +``` + +The auto-configuration automatically registers these tools with the MCP server. You can have multiple beans producing lists of ToolCallbacks, and the auto-configuration will merge them. + +## Available Tools + +### 1. Weather Forecast Tool +```java +@Tool(description = "Get weather forecast for a specific latitude/longitude") +public String getWeatherForecastByLocation( + double latitude, // Latitude coordinate + double longitude // Longitude coordinate +) { + // Returns detailed forecast including: + // - Temperature and unit + // - Wind speed and direction + // - Detailed forecast description +} + +// Example usage: +CallToolResult forecast = client.callTool( + new CallToolRequest("getWeatherForecastByLocation", + Map.of( + "latitude", 47.6062, // Seattle coordinates + "longitude", -122.3321 + ) + ) +); +``` + +### 2. Weather Alerts Tool +```java +@Tool(description = "Get weather alerts for a US state") +public String getAlerts( + String state // Two-letter US state code (e.g., CA, NY) +) { + // Returns active alerts including: + // - Event type + // - Affected area + // - Severity + // - Description + // - Safety instructions +} + +// Example usage: +CallToolResult alerts = client.callTool( + new CallToolRequest("getAlerts", + Map.of("state", "NY") + ) +); +``` + +## Client Integration + +### Java Client Example + +#### Create MCP Client Manually + +```java +// Create server parameters +ServerParameters stdioParams = ServerParameters.builder("java") + .args("-Dspring.ai.mcp.server.transport=STDIO", + "-Dspring.main.web-application-type=none", + "-Dlogging.pattern.console=", + "-jar", + "target/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar") + .build(); + +// Initialize transport and client +var transport = new StdioClientTransport(stdioParams); +var client = McpClient.sync(transport).build(); +``` + +The [ClientStdio.java](src/test/java/org/springframework/ai/mcp/sample/client/ClientStdio.java) demonstrates how to implement an MCP client manually. + +For a better development experience, consider using the [MCP Client Boot Starters](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html). These starters enable auto-configuration of multiple STDIO and/or SSE connections to MCP servers. See the [starter-default-client](../../client-starter/starter-default-client) and [starter-webflux-client](../../client-starter/starter-webflux-client) projects for examples. + +#### Use MCP Client Boot Starter + +Use the [starter-default-client](../../client-starter/starter-default-client) to connect to the weather `starter-stdio-server`: + +1. Follow the `starter-default-client` readme instructions to build a `mcp-starter-default-client-0.0.1-SNAPSHOT.jar` client application. + +2. Run the client using the configuration file: + +```bash +java -Dspring.ai.mcp.client.stdio.connections.server1.command=java \ + -Dspring.ai.mcp.client.stdio.connections.server1.args=-jar,/Users/christiantzolov/Dev/projects/spring-ai-examples/model-context-protocol/weather/starter-stdio-server/target/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar \ + -Dai.user.input='What is the weather in NY?' \ + -Dlogging.pattern.console= \ + -jar mcp-starter-default-client-0.0.1-SNAPSHOT.jar +``` + +### Claude Desktop Integration + +To integrate with Claude Desktop, add the following configuration to your Claude Desktop settings: + +```json +{ + "mcpServers": { + "spring-ai-mcp-weather": { + "command": "java", + "args": [ + "-Dspring.ai.mcp.server.stdio=true", + "-Dspring.main.web-application-type=none", + "-Dlogging.pattern.console=", + "-jar", + "/absolute/path/to/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar" + ] + } + } +} +``` + +Replace `/absolute/path/to/` with the actual path to your built jar file. + +## Configuration + +### Application Properties + +All properties are prefixed with `spring.ai.mcp.server`: + +```properties +# Required STDIO Configuration +spring.main.web-application-type=none +spring.main.banner-mode=off +logging.pattern.console= + +# Server Configuration +spring.ai.mcp.server.enabled=true +spring.ai.mcp.server.name=my-weather-server +spring.ai.mcp.server.version=0.0.1 +# SYNC or ASYNC +spring.ai.mcp.server.type=SYNC +spring.ai.mcp.server.resource-change-notification=true +spring.ai.mcp.server.tool-change-notification=true +spring.ai.mcp.server.prompt-change-notification=true + +# Optional file logging +logging.file.name=mcp-weather-stdio-server.log +``` + +### Key Configuration Notes + +1. **STDIO Mode Requirements** + - Disable web application type (`spring.main.web-application-type=none`) + - Disable Spring banner (`spring.main.banner-mode=off`) + - Clear console logging pattern (`logging.pattern.console=`) + +2. **Server Type** + - `SYNC` (default): Uses `McpSyncServer` for straightforward request-response patterns + - `ASYNC`: Uses `McpAsyncServer` for non-blocking operations with Project Reactor support + +## 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/start-mcp-java-hello-world/src/code/bootstrap b/start-mcp-java-hello-world/src/code/bootstrap new file mode 100644 index 0000000..f18ba64 --- /dev/null +++ b/start-mcp-java-hello-world/src/code/bootstrap @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +java -Dspring.ai.mcp.server.stdio=true -Dspring.main.web-application-type=none -Dlogging.pattern.console= -jar target/mcp-stdio-server-0.0.1-SNAPSHOT.jar \ No newline at end of file diff --git a/start-mcp-java-hello-world/src/code/bootstrap_sse b/start-mcp-java-hello-world/src/code/bootstrap_sse new file mode 100644 index 0000000..f79cf5c --- /dev/null +++ b/start-mcp-java-hello-world/src/code/bootstrap_sse @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +java -jar target/mcp-stdio-server-0.0.1-SNAPSHOT.jar \ No newline at end of file diff --git a/start-mcp-java-hello-world/src/code/mvnw b/start-mcp-java-hello-world/src/code/mvnw new file mode 100644 index 0000000..19529dd --- /dev/null +++ b/start-mcp-java-hello-world/src/code/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/start-mcp-java-hello-world/src/code/mvnw.cmd b/start-mcp-java-hello-world/src/code/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/start-mcp-java-hello-world/src/code/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/start-mcp-java-hello-world/src/code/pom.xml b/start-mcp-java-hello-world/src/code/pom.xml new file mode 100644 index 0000000..648c849 --- /dev/null +++ b/start-mcp-java-hello-world/src/code/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.6 + + + + com.example + + mcp-stdio-server + 0.0.1-SNAPSHOT + + Spring AI MCP STDIO server + Sample Spring Boot application demonstrating MCP stdio server usage + + + + + + org.springframework.ai + spring-ai-bom + 1.0.0-SNAPSHOT + pom + import + + + + + + + org.springframework.ai + spring-ai-starter-mcp-server + + + + org.springframework + spring-web + + + + + + + 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/start-mcp-java-hello-world/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java b/start-mcp-java-hello-world/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java new file mode 100644 index 0000000..be7cff6 --- /dev/null +++ b/start-mcp-java-hello-world/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpServerApplication.java @@ -0,0 +1,23 @@ +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) { + // Start the Spring Boot application + // and the MCP server + System.out.println("MCP server started..."); + SpringApplication.run(McpServerApplication.class, args); + } + + @Bean + public ToolCallbackProvider tools(McpService mcpService) { + return MethodToolCallbackProvider.builder().toolObjects(mcpService).build(); + } +} diff --git a/start-mcp-java-hello-world/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpService.java b/start-mcp-java-hello-world/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpService.java new file mode 100644 index 0000000..e605f89 --- /dev/null +++ b/start-mcp-java-hello-world/src/code/src/main/java/org/springframework/ai/mcp/sample/server/McpService.java @@ -0,0 +1,21 @@ +package org.springframework.ai.mcp.sample.server; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.stereotype.Service; + +@Service +public class McpService { + public McpService() {} + /** + * A sample tool. Return string 'Hello World!' + * @return String + */ + @Tool(description = "Return string 'Hello World!'") + public String helloWorld() { + return "Hello World!"; + } + + public static void main(String[] args) { + McpService client = new McpService(); + System.out.println(client.helloWorld()); + } +} \ No newline at end of file diff --git a/start-mcp-java-hello-world/src/code/src/main/resources/application.properties b/start-mcp-java-hello-world/src/code/src/main/resources/application.properties new file mode 100644 index 0000000..44e5b03 --- /dev/null +++ b/start-mcp-java-hello-world/src/code/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.name=my-weather-server +spring.ai.mcp.server.version=0.0.1 +server.port=8080 + +logging.file.name=./model-context-protocol/weather/starter-stdio-server/target/mcp-weather-stdio-server.log + diff --git a/start-mcp-java-hello-world/src/readme.md b/start-mcp-java-hello-world/src/readme.md new file mode 100644 index 0000000..78f564f --- /dev/null +++ b/start-mcp-java-hello-world/src/readme.md @@ -0,0 +1,106 @@ + +> 注:当前项目为 Serverless Devs 应用,由于应用中会存在需要初始化才可运行的变量(例如应用部署地区、函数名等等),所以**不推荐**直接 Clone 本仓库到本地进行部署或直接复制 s.yaml 使用,**强烈推荐**通过 `s init ${模版名称}` 的方法或应用中心进行初始化,详情可参考[部署 & 体验](#部署--体验) 。 + +# start-mcp-java-hello-world 帮助文档 + + + +基于 Java 的 FC MCP STDIO Server 案例 + + + + +## 资源准备 + +使用该项目,您需要有开通以下服务并拥有对应权限: + + + + + +| 服务/业务 | 权限 | 相关文档 | +| --- | --- | --- | +| 函数计算 | AliyunFCFullAccess | [帮助文档](https://help.aliyun.com/product/2508973.html) [计费文档](https://help.aliyun.com/document_detail/2512928.html) | + + + + + + + + + + + + + + + +## 部署 & 体验 + + + +- :fire: 通过 [云原生应用开发平台 CAP](https://cap.console.aliyun.com/template-detail?template=start-mcp-java-hello-world) ,[![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](https://cap.console.aliyun.com/template-detail?template=start-mcp-java-hello-world) 该应用。 + + + + + + + +## 案例介绍 + + + +这是一个部署到 FC 的 MCP Server 的 Java hello world 样例。您可以通过这个模版初始化一个简单的、开箱即用的、可进行二次开发的 MCP Server。 + +此样例包含一个名为 `helloWorld` 的 Tool,您可基于此样例 Tool 进行二次开发。 + + + + + + + + + +## 使用流程 + + + +部署完成拿到 URL 后,准备好支持 SSE 或 STDIO 的 MCP Client,通过 Transport 进行连接。 + + + +## 二次开发指南 + + + +此样例包含一个名为 `helloWorld` 的 Tool,您可基于此样例 Tool 进行二次开发。 +```java +@Service +public class McpService { + public McpService() {} + /** + * A sample tool. Return string 'Hello World!' + * @return String + */ + @Tool(description = "Return string 'Hello World!'") + public String helloWorld() { + return "Hello World!"; + } + + public static void main(String[] args) { + McpService client = new McpService(); + System.out.println(client.helloWorld()); + } +} +``` + + + + + + + + diff --git a/start-mcp-java-hello-world/src/s.yaml b/start-mcp-java-hello-world/src/s.yaml new file mode 100644 index 0000000..81ce6d3 --- /dev/null +++ b/start-mcp-java-hello-world/src/s.yaml @@ -0,0 +1,17 @@ +edition: 3.0.0 +name: start-mcp-server-nodejs +access: '{{access}}' +vars: + region: '{{region}}' +resources: + start-mcp-java-hello-world: + component: fcai-mcp-server + props: + region: ${vars.region} + description: mcp server deployed by devs + transport: stdio + runtime: java21 + startCommand: java -Dspring.ai.mcp.server.stdio=true -Dspring.main.web-application-type=none -Dlogging.pattern.console= -jar ./target/mcp-stdio-server-0.0.1-SNAPSHOT.jar + instanceQuota: 1 + cpu: 1 + memorySize: 2048 \ No newline at end of file diff --git a/start-mcp-java-hello-world/version.md b/start-mcp-java-hello-world/version.md new file mode 100644 index 0000000..4ee89d2 --- /dev/null +++ b/start-mcp-java-hello-world/version.md @@ -0,0 +1,2 @@ +- 初始化项目 +- 测试项目模板 \ No newline at end of file