前言
首先介绍下MCP是什么?
MCP是由开发了 Claude 模型的 Anthropic 公司2024年11月提出并开源的一项开放标准,全称:Model Context Protocol,它是一个开放协议,它使 LLM 应用与外部数据源和工具之间的无缝集成成为可能。无论你是构建 AI 驱动的 IDE、改善chat 交互,还是构建自定义的 AI 工作流,MCP 提供了一种标准化的方式,将 LLM 与它们所需的上下文连接起来。
大白话
如果不使用 MCP 是什么样的?
就像自己(LLM大模型)学做菜,首先需要学会如何使用刀、锅、锅铲、炉灶,甚至需要自己生火,每一个步骤都需要从头摸索工具,无法专注烹饪本身。
使用 MCP
MCP 协议相当于给大模型配了一个服务员(MCP Client),当食客(LLM大模型)需要吃什么菜时,可以直接根据菜单上的菜品告诉服务员(MCP Client),知道菜品后,服务员(MCP Client)根据菜品是哪个菜系找不同菜系的厨师(MCP Server),厨师(MCP Server)接到炒菜的任务就使用冰箱、食材、锅、锅铲等(工具)完成菜品制作任务,并将菜品(结果)精准端给服务员(MCP Client),让大模型无需直接操作工具就能完成复杂任务。
初衷
最近接触到 MCP 协议,我觉得它在未来AI实际应用中潜力巨大,很可能成为行业趋势。不过,我留意到mcp.so网站上,大部分 MCP Server 是用 Python 编写的,用 Java 开发的极为少见。就连 Spring AI,也是在 2025 年 2 月才开始支持并封装 MCP 协议的大部分逻辑。所以,我希望有更多从事 Java 开发的人员能够关注这项技术,将其广泛运用到实际项目里 。
正文
流程图
环境准备
Jenkins(需启用「远程访问API」权限)
JDK 17
SpringBoot 3.3.6
IDEA
Maven 3
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.0</version> </dependency> <dependency> <groupId>com.google.inject</groupId> <artifactId>guice</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>io.github.cdancy</groupId> <artifactId>jenkins-rest</artifactId> <version>1.0.2</version> <exclusions> <exclusion> <artifactId>guice</artifactId> <groupId>com.google.inject</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.36</version> <scope>provided</scope> </dependency>
核心代码
JenkinsMcpServerConfig.java
MCP Server必须的配置类
package com.agua.ai.mcp.server.config; import com.agua.ai.mcp.server.service.JenkinsApiService; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JenkinsMcpServerConfig { @Bean public ToolCallbackProvider jenkinsTools(JenkinsApiService jenkinsApiService) { return MethodToolCallbackProvider.builder().toolObjects(jenkinsApiService).build(); } }
JenkinsProperties.java
定义Jenkins的配置
package com.agua.ai.mcp.server.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties("jenkins") public class JenkinsProperties { /** * 服务URI */ private String serverUri; /** * 用户名 */ private String username; /** * 密码/token */ private String password; }
JenkinsTemplate.java
对com.cdancy.jenkins(封装了Jenkins Rest API的工具类)已经集成的方法进行再次封装,方便调用
package com.agua.ai.mcp.server.util; import com.cdancy.jenkins.rest.JenkinsClient; import com.cdancy.jenkins.rest.domain.common.IntegerResponse; import com.cdancy.jenkins.rest.domain.common.RequestStatus; import com.cdancy.jenkins.rest.domain.job.BuildInfo; import com.cdancy.jenkins.rest.domain.job.JobInfo; import com.cdancy.jenkins.rest.domain.job.JobList; import com.cdancy.jenkins.rest.domain.job.ProgressiveText; import com.cdancy.jenkins.rest.features.JobsApi; import com.agua.ai.mcp.server.properties.JenkinsProperties; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; /** * Jenkins 模板类,用于封装 Jenkins API 的调用 */ @Component public class JenkinsTemplate { private final JenkinsClient jenkinsClient; private JobsApi jobsApi; @Autowired public JenkinsTemplate(JenkinsProperties jenkinsProperties) { this.jenkinsClient = JenkinsClient.builder() .endPoint(jenkinsProperties.getServerUri()) .credentials(jenkinsProperties.getUsername() + ":" + jenkinsProperties.getPassword()) .build(); } @PostConstruct public void init() { this.jobsApi = jenkinsClient.api().jobsApi(); } /** * 获取任务列表 * * @param optionalFolderPath 可选的文件夹路径 * @return 任务列表 */ public JobList getJobList(String optionalFolderPath) { return jobsApi.jobList(optionalFolderPath); } /** * 获取任务信息 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @return 任务信息 */ public JobInfo getJobInfo(String optionalFolderPath, String jobName) { return jobsApi.jobInfo(optionalFolderPath, jobName); } /** * 使用 XML 文件创建任务 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @param configXML 任务的配置 XML * @return 请求状态 */ public RequestStatus createJob(String optionalFolderPath, String jobName, String configXML) { return jobsApi.create(optionalFolderPath, jobName, configXML); } /** * 删除任务 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @return 请求状态 */ public RequestStatus deleteJob(String optionalFolderPath, String jobName) { return jobsApi.delete(optionalFolderPath, jobName); } /** * 启用任务 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @return 是否成功 */ public boolean enableJob(String optionalFolderPath, String jobName) { return jobsApi.enable(optionalFolderPath, jobName); } /** * 禁用任务 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @return 是否成功 */ public boolean disableJob(String optionalFolderPath, String jobName) { return jobsApi.disable(optionalFolderPath, jobName); } /** * 获取任务配置文件内容 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @return 配置文件内容 */ public String getJobConfig(String optionalFolderPath, String jobName) { return jobsApi.config(optionalFolderPath, jobName); } /** * 更新任务配置文件内容 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @param configXML 新的配置 XML * @return 是否成功 */ public boolean updateJobConfig(String optionalFolderPath, String jobName, String configXML) { return jobsApi.config(optionalFolderPath, jobName, configXML); } /** * 构建任务 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @return 构建响应 */ public IntegerResponse buildJob(String optionalFolderPath, String jobName) { return jobsApi.build(optionalFolderPath, jobName); } /** * 构建带参数的任务 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @param properties 参数列表 * @return 构建响应 */ public IntegerResponse buildJobWithParams(String optionalFolderPath, String jobName, Map<String, List<String>> properties) { return jobsApi.buildWithParameters(optionalFolderPath, jobName, properties); } /** * 获取任务上次构建序号 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @return 构建序号 */ public Integer getLastBuildNumber(String optionalFolderPath, String jobName) { return jobsApi.lastBuildNumber(optionalFolderPath, jobName); } /** * 获取任务上次构建时间戳 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @return 时间戳 */ public String getLastBuildTimestamp(String optionalFolderPath, String jobName) { return jobsApi.lastBuildTimestamp(optionalFolderPath, jobName); } /** * 获取构建信息 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @param buildNumber 构建编号 * @return 构建信息 */ public BuildInfo getBuildInfo(String optionalFolderPath, String jobName, int buildNumber) { return jobsApi.buildInfo(optionalFolderPath, jobName, buildNumber); } /** * 获取构建控制台输出内容 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @param buildNumber 构建编号 * @param start 开始位置 * @return 控制台输出内容 */ public ProgressiveText getBuildLog(String optionalFolderPath, String jobName, int buildNumber, int start) { return jobsApi.progressiveText(optionalFolderPath, jobName, buildNumber, start); } /** * 重命名任务 * * @param optionalFolderPath 可选的文件夹路径 * @param currentJobName 当前任务名称 * @param newJobName 新任务名称 * @return 是否成功 */ public boolean renameJob(String optionalFolderPath, String currentJobName, String newJobName) { return jobsApi.rename(optionalFolderPath, currentJobName, newJobName); } /** * 停止任务 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @param buildNumber 构建编号 * @return 是否成功 */ public RequestStatus killJob(String optionalFolderPath, String jobName, int buildNumber) { return jobsApi.kill(optionalFolderPath, jobName, buildNumber); } /** * 查看执行日志 * * @param optionalFolderPath 可选的文件夹路径 * @param jobName 任务名称 * @param start 开始位置 * @return 是否成功 */ public ProgressiveText progressiveTextJob(String optionalFolderPath, String jobName, int start) { return jobsApi.progressiveText(optionalFolderPath, jobName, start); } }
JenkinsApiService.java
直接暴露给LLM大模型的可调用的工具的Service
package com.agua.ai.mcp.server.service; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.TypeReference; import com.google.gson.ExclusionStrategy; import com.google.gson.FieldAttributes; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.agua.ai.mcp.server.util.JenkinsTemplate; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Service; @Service @AllArgsConstructor public class JenkinsApiService { private final JenkinsTemplate jenkinsTemplate; @Tool(description = "获取任务列表") public String getJobList(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.getJobList(optionalFolderPath)); } @Tool(description = "获取任务信息") public String getJobInfo(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.getJobInfo(optionalFolderPath, jobName)); } @Tool(description = "使用 XML 文件创建任务") public String createJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName, @ToolParam(description = "任务的配置 XML") String configXML) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.createJob(optionalFolderPath, jobName, configXML)); } @Tool(description = "删除任务") public String deleteJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.deleteJob(optionalFolderPath, jobName)); } @Tool(description = "启用任务") public String enableJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.enableJob(optionalFolderPath, jobName)); } @Tool(description = "禁用任务") public String disableJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.disableJob(optionalFolderPath, jobName)); } @Tool(description = "获取任务配置文件内容") public String getJobConfig(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.getJobConfig(optionalFolderPath, jobName)); } @Tool(description = "更新任务配置文件内容") public String updateJobConfig(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName, @ToolParam(description = "新的配置 XML") String configXML) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.updateJobConfig(optionalFolderPath, jobName, configXML)); } @Tool(description = "构建任务") public String buildJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.buildJob(optionalFolderPath, jobName)); } @Tool(description = "构建带参数的任务") public String buildJobWithParams(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName, @Schema(description = "参数列表(格式:Map<String, List<String>>)") String properties) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.buildJobWithParams(optionalFolderPath, jobName, JSON.parseObject(properties, new TypeReference<>() {}))); } @Tool(description = "获取任务上次构建序号") public String getLastBuildNumber(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.getLastBuildNumber(optionalFolderPath, jobName)); } @Tool(description = "获取任务上次构建时间戳") public String getLastBuildTimestamp(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.getLastBuildTimestamp(optionalFolderPath, jobName)); } @Tool(description = "获取构建信息") public String getBuildInfo(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName, @ToolParam(description = "构建编号(必须是整数)") String buildNumber, @ToolParam(description = "是否返回变更历史(boolean类型)") String changeSetFlag) { Gson gson = new GsonBuilder().setExclusionStrategies(new ExclusionStrategy() { @Override public boolean shouldSkipField(FieldAttributes f) { return Boolean.parseBoolean(changeSetFlag) && "changeSet".equals(f.getName()); } @Override public boolean shouldSkipClass(Class<?> clazz) { return false; } }).create(); return gson.toJson(jenkinsTemplate.getBuildInfo(optionalFolderPath, jobName, Integer.parseInt(buildNumber))); } @Tool(description = "获取构建控制台输出内容") public String getBuildLog(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName, @ToolParam(description = "构建编号(必须是整数)") String buildNumber, @ToolParam(description = "开始位置(必须是整数)") String start) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.getBuildLog(optionalFolderPath, jobName, Integer.parseInt(buildNumber), Integer.parseInt(start))); } @Tool(description = "重命名任务") public String renameJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "当前任务名称") String currentJobName, @ToolParam(description = "新任务名称") String newJobName) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.renameJob(optionalFolderPath, currentJobName, newJobName)); } @Tool(description = "停止任务(必须二次确认)") public String killJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName, @ToolParam(description = "构建编号(必须是整数)") String buildNumber) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.killJob(optionalFolderPath, jobName, Integer.parseInt(buildNumber))); } @Tool(description = "查看执行日志") public String progressiveTextJob(@ToolParam(description = "可选的文件夹路径") String optionalFolderPath, @ToolParam(description = "任务名称") String jobName, @ToolParam(description = "开始位置(必须是整数)") String start) { Gson gson = new GsonBuilder().create(); return gson.toJson(jenkinsTemplate.progressiveTextJob(optionalFolderPath, jobName, Integer.parseInt(start))); } }
application.yml
spring: ai: mcp: server: stdio: true name: jenkins-api version: 0.0.1 type: SYNC main: web-application-type: none banner-mode: off jenkins: # jenkins的访问url server-uri: ${JENKINS_API_SERVER_URI} username: ${JENKINS_API_USERNAME} password: ${JENKINS_API_TOKEN} logging: level: root: INFO
使用配置
如果是用 Cursor 作为客户端,那么可以通过一下方式启动 MCP Server ,本地 MCP Server 服务请将{你的路径}
替换成实际的 jar 包存放路径
command方式
java -Dspring.ai.mcp.server.transport=STDIO -Dspring.main.web-application-type=none -jar {你的路径}\mcp-jenkins-server-0.0.1-SNAPSHOT.jar
mcp.json配置
{ "mcpServers": { "jenkins-mcp": { "command": "java", "args": [ "-jar", "{你的路径}\\mcp-jenkins-server-0.0.1-SNAPSHOT.jar" ], "env": { "JENKINS_API_SERVER_URI": "jenkins-uri", "JENKINS_API_USERNAME": "username", "JENKINS_API_TOKEN": "password/token" } } } }
最终演示效果
用户提问
请部署v1.2.3版本到测试环境
MCP Client解析后调用
{ "tool": "buildJobWithParams", "params": { "optionalFolderPath": "", "jobName": "qa-system", "properties": {"version": ["v1.2.3"], "env": ["test"]} } }
Jenkins MCP Server执行结果
{ "value": "12345", "errors": [] }
总结
目前大模型的优势就是它能够一定程度地理解用户所说的内容,并转换成调用工具所需的请求参数,减少人工解析的工作量并且降低人工适配的成本。现在 MCP 还处于初期发展阶段,因此需要广大开发者的支持,才能支撑起庞大 AI 应用生态构建。