java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > java mcp服务发布maven 中央仓库

使用 Java 开发 MCP 服务并发布到 Maven 中央仓库实践指南

作者:wb04307201

本文介绍了使用Java开发MCP服务并发布到Maven中央仓库的流程,重点讲解了如何通过JBang和stdio将MCP服务集成到大模型中,以及相关最佳实践和常见问题解决方案,感兴趣的朋友跟随小编一起看看吧

什么是 MCP

Model Context Protocol (MCP) 是一个开放协议,用于标准化大语言模型与外部数据源和工具之间的交互。它允许开发者将应用程序、数据源或 AI 功能无缝集成到任何使用 MCP 的 LLM 客户端中。

MCP 的核心优势

MCP 通信模式

MCP 支持多种通信方式:

本文将以中国天气查询服务为例,详细介绍如何使用 Java 开发 MCP 服务,并发布到 Maven 中央仓库,最终通过 JBang 和 stdio 方式集成到大模型工具中。

项目架构与技术选型

技术栈

项目结构

cn-weather-mcp/
├── src/main/java/cn/wubo/cn/weather/mcp/
│   ├── WeatherApplication.java    # 主启动类
│   └── WeatherService.java        # 天气服务实现
├── src/main/resources/
│   └── application.yml            # 配置文件
├── pom.xml                        # Maven 配置
└── .github/workflows/
    └── publish.yml                # CI/CD 发布流程

开发环境准备

必需软件

gpg --version

GPG 密钥生成

发布到 Maven 中央仓库需要 GPG 签名:

# 生成 GPG 密钥
gpg --full-generate-key
# 查看生成的密钥
gpg --list-keys
# 将公钥上传到密钥服务器
gpg --keyserver keyserver.ubuntu.com --send-keys YOUR_KEY_ID

创建 Spring Boot 项目

1. 创建 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.13</version>
        <relativePath/>
    </parent>
    
    <groupId>io.github.your-username</groupId>
    <artifactId>cn-weather-mcp</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>cn-weather-mcp</name>
    <description>中国天气 MCP 服务 - 提供中国城市天气查询服务</description>
    
    <!-- 项目元信息 -->
    <url>https://github.com/your-username/cn-weather-mcp</url>
    
    <developers>
        <developer>
            <id>your-id</id>
            <name>Your Name</name>
            <email>your-email@example.com</email>
        </developer>
    </developers>
    
    <licenses>
        <license>
            <name>The Apache Software License, Version 2.0</name>
            <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
        </license>
    </licenses>
    
    <scm>
        <connection>scm:git:https://github.com/your-username/cn-weather-mcp.git</connection>
        <developerConnection>scm:git:git@github.com:your-username/cn-weather-mcp.git</developerConnection>
        <url>https://github.com/your-username/cn-weather-mcp</url>
    </scm>
    
    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.1.3</spring-ai.version>
    </properties>
    
    <!-- 依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <dependencies>
        <!-- Spring AI MCP Server -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server</artifactId>
        </dependency>
        
        <!-- Spring Web (用于 HTTP 请求) -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <!-- Spring Boot Maven 插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            
            <!-- 源码插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>3.3.0</version>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <goals>
                            <goal>jar-no-fork</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            
            <!-- Javadoc 插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
                <version>3.6.0</version>
                <executions>
                    <execution>
                        <id>attach-javadocs</id>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            
            <!-- GPG 签名插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-gpg-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <id>sign-artifacts</id>
                        <phase>verify</phase>
                        <goals>
                            <goal>sign</goal>
                        </goals>
                        <configuration>
                            <gpgArguments>
                                <arg>--pinentry-mode</arg>
                                <arg>loopback</arg>
                            </gpgArguments>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            
            <!-- Central Publishing 插件 -->
            <plugin>
                <groupId>org.sonatype.central</groupId>
                <artifactId>central-publishing-maven-plugin</artifactId>
                <version>0.9.0</version>
                <extensions>true</extensions>
                <configuration>
                    <publishingServerId>central</publishingServerId>
                    <autoPublish>true</autoPublish>
                    <waitUntil>published</waitUntil>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

2. 创建配置文件 application.yml

spring:
  main:
    web-application-type: none  # 非 Web 应用
    banner-mode: off            # 关闭启动横幅
  ai:
    mcp:
      server:
        name: cn-weather-mcp    # MCP 服务名称
        version: 1.0.0          # MCP 服务版本
logging:
  file:
    name: ./mcp/cn-weather-mcp.log  # 日志文件路径

实现 MCP 服务核心逻辑

1. 创建服务类 WeatherService

这是 MCP 服务的核心,包含所有工具方法的实现:

package cn.wubo.cn.weather.mcp;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class WeatherService {
    private static final String BASE_URL = "http://t.weather.itboy.net/api/weather/city/";
    private final RestClient restClient;
    private final ObjectMapper objectMapper;
    private final Map<String, CityCodeInfo> cityCodeMap;
    public WeatherService() {
        this.restClient = RestClient.builder()
                .baseUrl(BASE_URL)
                .build();
        this.objectMapper = new ObjectMapper();
        this.cityCodeMap = loadCityCodes();
    }
    // 加载城市代码数据
    private static Map<String, CityCodeInfo> loadCityCodes() {
        List<CityCodeInfo> cityCodes = Arrays.asList(
            new CityCodeInfo(1, "北京", "北京", "101010100"),
            new CityCodeInfo(23, "天津市", "天津", "101030100"),
            new CityCodeInfo(36, "上海", "上海", "101020100"),
            // ... 更多城市数据
        );
        return cityCodes.stream()
            .collect(Collectors.toMap(CityCodeInfo::cityCode, info -> info));
    }
    // 数据记录类
    @JsonIgnoreProperties(ignoreUnknown = true)
    public record WeatherResponse(
            @JsonProperty("status") Integer status,
            @JsonProperty("message") String message,
            @JsonProperty("cityInfo") CityInfo cityInfo,
            @JsonProperty("data") WeatherDataWrapper data
    ) {}
    @JsonIgnoreProperties(ignoreUnknown = true)
    public record CityInfo(
            @JsonProperty("city") String city,
            @JsonProperty("citykey") String cityKey,
            @JsonProperty("parent") String parent,
            @JsonProperty("updateTime") String updateTime
    ) {}
    @JsonIgnoreProperties(ignoreUnknown = true)
    public record WeatherDataWrapper(
            @JsonProperty("shidu") String humidity,
            @JsonProperty("pm25") Double pm25,
            @JsonProperty("pm10") Double pm10,
            @JsonProperty("quality") String quality,
            @JsonProperty("wendu") String temperature,
            @JsonProperty("ganmao") String healthTip,
            @JsonProperty("forecast") List<Forecast> forecast
    ) {}
    @JsonIgnoreProperties(ignoreUnknown = true)
    public record Forecast(
            @JsonProperty("date") String date,
            @JsonProperty("high") String highTemp,
            @JsonProperty("low") String lowTemp,
            @JsonProperty("ymd") String ymd,
            @JsonProperty("week") String week,
            @JsonProperty("fx") String windDirection,
            @JsonProperty("fl") String windLevel,
            @JsonProperty("type") String weatherType,
            @JsonProperty("notice") String notice
    ) {}
    public record CityCodeInfo(
        Integer id,
        String province,
        String city,
        String cityCode
    ) {}
    /**
     * 工具方法 1:获取当前天气
     */
    @Tool(description = "Get current weather for a Chinese city. Input is city code (e.g., 101010100 for Beijing)")
    public String getCurrentWeather(
        @ToolParam(description = "City code (e.g., 101010100 for Beijing, 101020100 for Shanghai)") 
        String cityCode
    ) {
        ResponseEntity<byte[]> responseEntity = restClient.get()
                .uri("{cityCode}", cityCode)
                .retrieve()
                .toEntity(byte[].class);
        byte[] body = responseEntity.getBody();
        if (body == null) {
            throw new RuntimeException("Empty response from weather API");
        }
        String response = new String(body, StandardCharsets.UTF_8);
        WeatherResponse weatherResponse;
        try {
            weatherResponse = objectMapper.readValue(response, WeatherResponse.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to parse weather data: " + e.getMessage());
        }
        if (200 != weatherResponse.status()) {
            throw new RuntimeException("Weather API returned error: " + weatherResponse.message());
        }
        WeatherDataWrapper data = weatherResponse.data();
        CityInfo cityInfo = weatherResponse.cityInfo();
        return String.format("""
                        城市:%s (%s)
                        温度:%s°C
                        湿度:%s
                        空气质量:%s (PM2.5: %s)
                        风向:%s %s
                        温馨提示:%s
                        更新时间:%s
                        """,
                cityInfo.city(),
                cityInfo.parent(),
                data.temperature(),
                data.humidity(),
                data.quality(),
                data.pm25(),
                data.forecast().get(0).windDirection(),
                data.forecast().get(0).windLevel(),
                data.healthTip(),
                cityInfo.updateTime());
    }
    /**
     * 工具方法 2:搜索城市代码
     */
    @Tool(description = "Search Chinese city codes by city name. Returns list of cities with their codes for weather queries")
    public String searchCityCode(
        @ToolParam(description = "City name to search (e.g., '北京', '上海', '广州')") 
        String cityName
    ) {
        if (cityName == null || cityName.trim().isEmpty()) {
            return "请输入要查询的城市名称";
        }
        String searchName = cityName.trim();
        List<CityCodeInfo> results = cityCodeMap.values().stream()
            .filter(info -> info.city().contains(searchName) ||
                    info.province().contains(searchName))
            .limit(20)
            .toList();
        if (results.isEmpty()) {
            return String.format("未找到包含 '%s' 的城市,请检查输入后重试", searchName);
        }
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("找到 %d 个匹配的城市:\n\n", results.size()));
        sb.append(String.format("%-4s %-10s %-10s %-12s%n", "编号", "省份", "城市", "城市编码"));
        sb.append("-".repeat(40)).append("\n");
        for (CityCodeInfo info : results) {
            sb.append(String.format("%-4d %-10s %-10s %-12s%n",
                info.id(), info.province(), info.city(), info.cityCode()));
        }
        sb.append("\n提示:使用城市编码可以查询具体天气");
        return sb.toString();
    }
}

关键注解说明

配置 MCP Server

创建启动类 WeatherApplication

package cn.wubo.cn.weather.mcp;
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 WeatherApplication {
    public static void main(String[] args) {
        SpringApplication.run(WeatherApplication.class, args);
    }
    @Bean
    public ToolCallbackProvider weatherTools(WeatherService weatherService) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(weatherService)
                .build();
    }
}

配置说明

发布到 Maven 中央仓库

1. 在 Sonatype Central 创建账户

访问 Sonatype Central 注册账户并完成验证。

2. 配置认证信息

本地测试(~/.m2/settings.xml)

<settings>
  <servers>
    <server>
      <id>central</id>
      <username>your-username</username>
      <password>your-token</password>
    </server>
  </servers>
</settings>

GitHub Secrets 配置

在 GitHub 仓库设置中添加以下 Secrets:

3. 创建 GitHub Actions 发布流程

创建 .github/workflows/publish.yml

name: Publish to Sonatype Central
on:
  release:
    types: [created]
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven
      - name: Create Maven settings.xml
        run: |
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml << EOF
          <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
            <servers>
              <server>
                <id>central</id>
                <username>\${CENTRAL_USERNAME}</username>
                <password>\${CENTRAL_TOKEN}</password>
              </server>
            </servers>
          </settings>
          EOF
        env:
          CENTRAL_USERNAME: \${{ secrets.CENTRAL_USERNAME }}
          CENTRAL_TOKEN: \${{ secrets.CENTRAL_TOKEN }}
      - name: Import GPG Key
        uses: crazy-max/ghaction-import-gpg@v6
        with:
          gpg_private_key: \${{ secrets.GPG_PRIVATE_KEY }}
          passphrase: \${{ secrets.GPG_PASSPHRASE }}
          git_user_signingkey: false
          git_commit_gpgsign: false
      - name: Extract version from tag
        id: version
        run: echo "VERSION=\${GITHUB_REF#refs/tags/v}" >> \$GITHUB_OUTPUT
      - name: Set Version
        run: mvn versions:set -DnewVersion=\${{ steps.version.outputs.VERSION }} -DgenerateBackupPoms=false -B
      - name: Publish to Central
        run: mvn -B clean deploy
        env:
          GPG_PASSPHRASE: \${{ secrets.GPG_PASSPHRASE }}

4. 发布流程

提交代码到 Git

git add .
git commit -m "Initial release"
git push origin main

创建 Git Release

# 打标签
git tag v1.0.0
git push origin v1.0.0
# 或在 GitHub UI 创建 Release

5. 验证发布

发布成功后,可以在以下地址查看:

使用 JBang 通过 stdio 集成到大模型

什么是 JBang

JBang 是一个允许你无需安装 JDK 或配置项目即可运行 Java 代码的工具。它非常适合快速原型开发和脚本编写。

1. 安装 JBang

Windows (PowerShell)

iex "& { $(iwr https://ps.jbang.dev) } app setup"

Linux / macOS

curl -Ls https://sh.jbang.dev | bash -s - app setup

2. 验证安装

jbang --version

3. MCP Client 配置

以大模型工具的 MCP 配置为例,创建配置文件:

Claude Desktop 配置

编辑 claude_desktop_config.json

{
  "mcpServers": {
    "cn-weather-mcp": {
      "command": "jbang",
      "args": [
        "io.github.wb04307201:cn-weather-mcp:1.0.0"
      ]
    }
  }
}

配置说明

4. stdio 工作原理

┌─────────────┐         ┌──────────────┐         ┌─────────────┐
│  LLM Client │◄───────►│  JBang       │◄───────►│  MCP Server │
│  (Claude)   │  JSON   │  (Runner)    │  stdio  │  (Your Jar) │
└─────────────┘  RPC    └──────────────┘  IO     └─────────────┘

通信流程

  1. LLM Client 发送 JSON-RPC 请求到 JBang
  2. JBang 启动 Java 进程,通过 stdin/stdout 与 MCP Server 通信
  3. MCP Server 处理请求并返回结果
  4. 结果沿原路返回给 LLM Client

5. 测试 MCP 服务

本地测试

# 直接使用 JBang 运行
jbang io.github.wb04307201:cn-weather-mcp:1.0.0

在 IDE 中测试

运行 WeatherApplication.main() 方法,然后通过 MCP 客户端工具连接。

6. 在大模型中使用

配置完成后,在大模型对话中可以直接使用:

示例对话

用户:北京今天天气怎么样?

助手:[调用 MCP 工具 searchCityCode("北京")]
      [获取城市代码 101010100]
      [调用 MCP 工具 getCurrentWeather("101010100")]
      
      北京今天的天气情况如下:
      - 温度:25°C
      - 湿度:60%
      - 空气质量:良 (PM2.5: 35)
      - 风向:东南风 2 级
      - 温馨提示:天气舒适,适合户外活动

完整示例代码清单

项目关键文件

  1. pom.xml - Maven 配置(见前文)
  2. application.yml - 应用配置(见前文)
  3. WeatherApplication.java - 启动类(见前文)
  4. WeatherService.java - 服务实现(见前文)
  5. publish.yml - GitHub Actions(见前文)

运行命令

# 编译项目
mvn clean package
# 本地测试
mvn spring-boot:run
# 发布到 Maven Central
mvn clean deploy
# 使用 JBang 运行
jbang io.github.wb04307201:cn-weather-mcp:1.0.0

常见问题与解决方案

Q1: GPG 签名失败

错误信息gpg: signing failed: No secret key

解决方案

# 确认 GPG 密钥存在
gpg --list-secret-keys
# 重新生成密钥
gpg --full-generate-key

Q2: Maven 部署被拒绝

原因:缺少必要的元数据或签名

解决方案

Q3: JBang 无法下载 JAR

错误信息Failed to resolve artifact

解决方案

Q4: MCP 工具无法被识别

原因@Tool 注解未正确配置

解决方案

最佳实践建议

1. 代码规范

2. 安全性

3. 性能优化

4. 可维护性

总结

本文详细介绍了如何使用 Java 开发 MCP 服务并发布到 Maven 中央仓库的完整流程:

核心步骤回顾

  1. 环境准备:安装 JDK、Maven、GPG
  2. 项目搭建:创建 Spring Boot 项目,配置 Maven POM
  3. 服务开发:实现 @Tool 注解的业务方法
  4. MCP 配置:配置 ToolCallbackProvider Bean
  5. 发布准备:配置 GPG 签名和 Sonatype Central
  6. 自动化发布:使用 GitHub Actions 自动发布
  7. 集成使用:通过 JBang 和 stdio 集成到大模型

技术优势

应用场景

后续学习资源

通过本文的学习,你已经掌握了从零开始开发、发布和使用 MCP 服务的完整技能树。现在就开始创建你自己的 MCP 服务,让大模型能够调用你的代码吧!

项目源码参考:https://github.com/wb04307201/cn-weather-mcp

到此这篇关于使用 Java 开发 MCP 服务并发布到 Maven 中央仓库完整指南的文章就介绍到这了,更多相关java mcp服务发布maven 中央仓库内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文