**摘要:**本文围绕MCP开发实战展开,详细介绍了 Git 信息查询 MCP 服务的完整开发流程,包括服务端与客户端的具体实现。


MCP 开发实战 - 信息查询服务
MCP 服务端开发
可以使用 JGit 或直接调用系统 Git 命令来构建 Git 信息查询服务。
1)首先在项目根目录下新建 module,名称为 git-query-mcp-server:

2)引入必要依赖,包括 Lombok、hutool、Spring AI MCP 服务端依赖,以及 Git 操作依赖:
有 Stdio、WebMVC SSE 和 WebFlux SSE 三种服务端依赖可以选择,开发时只需要填写不同的配置,开发流程都是一样的。此处我们选择引入 WebMVC:
XML
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.10.0.202406071900-r</version>
</dependency>
引入这个依赖后,会自动注册 SSE 端点,供客户端调用,包括消息和 SSE 传输端点。
3)在 resources 目录下编写服务端配置文件,两套配置方案分别实现 stdio 和 SSE 模式的传输。
stdio 配置文件 application-stdio.yml(需关闭 web 支持):
bash
spring:
ai:
mcp:
server:
name: git-query-mcp-server
version: 0.0.1
type: SYNC
# stdio
stdio: true
# stdio
main:
web-application-type: none
banner-mode: off
SSE 配置文件 application-sse.yml(需关闭 stdio 模式):
bash
spring:
ai:
mcp:
server:
name: git-query-mcp-server
version: 0.0.1
type: SYNC
# sse
stdio: false
然后编写主配置文件 application.yml,可以灵活指定激活哪套配置:
bash
spring:
application:
name: git-query-mcp-server
profiles:
active: stdio
server:
port: 8128
4)编写 Git 查询服务类,在 tools 包下新建 GitQueryTool,使用 @Tool 注解标注方法,作为 MCP 服务提供的工具。
java
@Service
public class GitQueryTool {
// 项目Git仓库根路径(可配置化)
private static final String REPO_PATH = System.getProperty("user.dir");
@Tool(description = "查询Git仓库当前状态")
public String getGitStatus() {
try {
return GitUtil.getGitStatus(REPO_PATH);
} catch (Exception e) {
return "查询Git状态失败:" + e.getMessage();
}
}
@Tool(description = "查询最近N条提交记录")
public String getRecentCommits(@ToolParam(description = "查询的提交条数") int count) {
try {
return GitUtil.getRecentCommits(REPO_PATH, count);
} catch (Exception e) {
return "查询提交记录失败:" + e.getMessage();
}
}
@Tool(description = "查询指定文件的变更历史")
public String getFileHistory(@ToolParam(description = "文件相对路径") String filePath) {
try {
return GitUtil.getFileHistory(REPO_PATH, filePath);
} catch (Exception e) {
return "查询文件历史失败:" + e.getMessage();
}
}
@Tool(description = "查询当前Git分支信息")
public String getBranchInfo() {
try {
return GitUtil.getBranchInfo(REPO_PATH);
} catch (Exception e) {
return "查询分支信息失败:" + e.getMessage();
}
}
}
java
public class GitUtil {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 获取 Git 状态
*/
public static String getGitStatus(String repoPath) throws IOException, GitAPIException {
try (Repository repository = openRepository(repoPath)) {
try (Git git = new Git(repository)) {
Status status = git.status().call();
StringBuilder sb = new StringBuilder();
sb.append("=== Git 工作区状态 ===\n");
sb.append("分支: ").append(repository.getBranch()).append("\n\n");
Set<String> added = status.getAdded();
Set<String> changed = status.getChanged();
Set<String> modified = status.getModified();
Set<String> removed = status.getRemoved();
Set<String> untracked = status.getUntracked();
Set<String> conflicting = status.getConflicting();
if (!added.isEmpty()) {
sb.append("【已暂存的新文件】(").append(added.size()).append(")\n");
added.forEach(f -> sb.append(" + ").append(f).append("\n"));
sb.append("\n");
}
if (!changed.isEmpty()) {
sb.append("【已暂存的修改】(").append(changed.size()).append(")\n");
changed.forEach(f -> sb.append(" ~ ").append(f).append("\n"));
sb.append("\n");
}
if (!modified.isEmpty()) {
sb.append("【未暂存的修改】(").append(modified.size()).append(")\n");
modified.forEach(f -> sb.append(" M ").append(f).append("\n"));
sb.append("\n");
}
if (!removed.isEmpty()) {
sb.append("【已删除的文件】(").append(removed.size()).append(")\n");
removed.forEach(f -> sb.append(" - ").append(f).append("\n"));
sb.append("\n");
}
if (!untracked.isEmpty()) {
sb.append("【未跟踪的文件】(").append(untracked.size()).append(")\n");
untracked.stream().limit(20).forEach(f -> sb.append(" ? ").append(f).append("\n"));
if (untracked.size() > 20) {
sb.append(" ... 还有 ").append(untracked.size() - 20).append(" 个文件\n");
}
sb.append("\n");
}
if (!conflicting.isEmpty()) {
sb.append("【冲突文件】(").append(conflicting.size()).append(")\n");
conflicting.forEach(f -> sb.append(" ! ").append(f).append("\n"));
sb.append("\n");
}
if (added.isEmpty() && changed.isEmpty() && modified.isEmpty()
&& removed.isEmpty() && untracked.isEmpty() && conflicting.isEmpty()) {
sb.append("工作区干净,没有变更\n");
}
return sb.toString();
}
}
}
/**
* 获取最近 N 条提交记录
*/
public static String getRecentCommits(String repoPath, int count) throws IOException, GitAPIException {
try (Repository repository = openRepository(repoPath)) {
try (Git git = new Git(repository)) {
Iterable<RevCommit> commits = git.log().setMaxCount(count).call();
StringBuilder sb = new StringBuilder();
sb.append("=== 最近 ").append(count).append(" 条提交记录 ===\n\n");
int index = 1;
for (RevCommit commit : commits) {
sb.append("[").append(index++).append("] ")
.append(commit.getName().substring(0, 8)).append("\n");
sb.append(" 作者: ").append(commit.getAuthorIdent().getName())
.append(" <").append(commit.getAuthorIdent().getEmailAddress()).append(">\n");
sb.append(" 日期: ").append(DATE_FORMAT.format(commit.getAuthorIdent().getWhen())).append("\n");
sb.append(" 提交: ").append(commit.getShortMessage()).append("\n");
sb.append("\n");
}
return sb.toString();
}
}
}
/**
* 获取文件变更历史
*/
public static String getFileHistory(String repoPath, String filePath) throws IOException, GitAPIException {
try (Repository repository = openRepository(repoPath)) {
try (Git git = new Git(repository)) {
Iterable<RevCommit> commits = git.log()
.addPath(filePath)
.setMaxCount(20)
.call();
StringBuilder sb = new StringBuilder();
sb.append("=== 文件历史: ").append(filePath).append(" ===\n\n");
int index = 1;
for (RevCommit commit : commits) {
sb.append("[").append(index++).append("] ")
.append(commit.getName().substring(0, 8)).append("\n");
sb.append(" 作者: ").append(commit.getAuthorIdent().getName()).append("\n");
sb.append(" 日期: ").append(DATE_FORMAT.format(commit.getAuthorIdent().getWhen())).append("\n");
sb.append(" 提交: ").append(commit.getShortMessage()).append("\n");
sb.append("\n");
}
if (index == 1) {
sb.append("未找到该文件的提交历史\n");
}
return sb.toString();
}
}
}
/**
* 获取分支信息
*/
public static String getBranchInfo(String repoPath) throws IOException, GitAPIException {
try (Repository repository = openRepository(repoPath)) {
try (Git git = new Git(repository)) {
StringBuilder sb = new StringBuilder();
sb.append("=== Git 分支信息 ===\n\n");
// 当前分支
String currentBranch = repository.getBranch();
sb.append("当前分支: ").append(currentBranch).append("\n\n");
// 所有本地分支
List<Ref> localBranches = git.branchList().call();
sb.append("本地分支 (").append(localBranches.size()).append("):\n");
for (Ref branch : localBranches) {
String name = branch.getName().replace("refs/heads/", "");
if (name.equals(currentBranch)) {
sb.append(" * ").append(name).append(" (当前)\n");
} else {
sb.append(" ").append(name).append("\n");
}
}
// 远程分支
try {
List<Ref> remoteBranches = git.branchList().setListMode(org.eclipse.jgit.api.ListBranchCommand.ListMode.REMOTE).call();
sb.append("\n远程分支 (").append(remoteBranches.size()).append("):\n");
for (Ref branch : remoteBranches) {
sb.append(" ").append(branch.getName()).append("\n");
}
} catch (Exception e) {
sb.append("\n远程分支: 获取失败 - ").append(e.getMessage()).append("\n");
}
return sb.toString();
}
}
}
/**
* 打开 Git 仓库
*/
public static Repository openRepository(String repoPath) throws IOException {
File gitDir = Paths.get(repoPath, ".git").toFile();
if (!gitDir.exists()) {
throw new IOException("Git 仓库不存在: " + repoPath);
}
FileRepositoryBuilder builder = new FileRepositoryBuilder();
return builder.setGitDir(gitDir)
.readEnvironment()
.findGitDir()
.build();
}
}
编写对应的单元测试类,验证工具是否可用:
java
@SpringBootTest
class GitQueryToolTest {
@Resource
private GitQueryTool gitQueryTool;
@Test
void queryGitInfo() {
// 测试查询Git状态
String status = gitQueryTool.getGitStatus();
Assertions.assertNotNull(status);
// 测试查询最近5条提交
String commits = gitQueryTool.getRecentCommits(5);
Assertions.assertNotNull(commits);
}
}
5)在主类中通过定义 ToolCallbackProvider Bean 来注册工具:
java
@SpringBootApplication
public class GitQueryMcpServerApplication {
public static void main(String[] args) {
SpringApplication.run(GitQueryMcpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider gitQueryTools(GitQueryTool gitQueryTool) {
return MethodToolCallbackProvider.builder()
.toolObjects(gitQueryTool)
.build();
}
}
6)至此开发完成,最后使用 Maven Package 命令打包,会在 target 目录下生成可执行的 JAR 包,供客户端调用。
客户端开发
接下来直接在根项目中开发客户端,调用刚才创建的 Git 查询服务。
1)先引入必要的 MCP 客户端依赖:
XML
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
实际开发中,可按需添加 WebFlux 支持,需与服务端模式匹配。
2)测试 stdio 传输方式。在 mcp-servers.json 配置文件中新增 MCP Server 配置,通过 java 命令执行打包好的 jar 包:
XML
{
"mcpServers": {
"git-query-mcp-server": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-Dspring.main.web-application-type=none",
"-Dlogging.pattern.console=",
"-jar",
"git-query-mcp-server/target/git-query-mcp-server-0.0.1-SNAPSHOT.jar"
],
"env": {}
}
}
}
3)测试运行,编写单元测试代码:
java
@Test
void doChatWithGitMcp() {
// 测试Git查询 MCP
String message = "帮我查询一下项目最近的5次提交记录";
String answer = loveApp.doChatWithMcp(message, chatId);
Assertions.assertNotNull(answer);
}
运行后可看到 MCP 服务提供的工具被成功加载,输出 Git 相关信息。
4)测试 SSE 连接方式,首先修改 MCP 服务端的配置文件,激活 SSE 的配置:
bash
spring:
application:
name: git-query-mcp-server
profiles:
active: sse
server:
port: 8128
以 Debug 模式启动 MCP 服务。
然后修改客户端的配置文件,添加 SSE 配置,注释原有的 stdio 配置避免端口冲突:
bash
spring:
ai:
mcp:
client:
sse:
connections:
server1:
url: http://localhost:8128
# stdio:
# servers-configuration: classpath:mcp-servers.json
测试运行,SSE 模式下可更方便地调试 MCP 服务。
MCP 开发最佳实践
在学会如何开发 MCP 服务端和客户端后,我们来学习一些 MCP 开发的最佳实践。
1)慎用 MCP:MCP 不是银弹,其本质就是工具调用,只不过统一了标准、更容易共享而已。如果我们自己开发一些不需要共享的工具,完全没必要使用 MCP,可以节约开发和部署成本。我个人的建议是能不用就不用,先开发工具调用,之后需要提供 MCP 服务时再将工具调用转换成 MCP 服务即可。
2)传输模式选择:Stdio 模式作为客户端子进程运行,无需网络传输,因此安全性和性能都更高,更适合小型项目;SSE 模式适合作为独立服务部署,可以被多客户端共享调用,更适合模块化的中大型项目团队。
3)明确服务:设计 MCP 服务时,要合理划分工具和资源,并且利用 @Tool、@ToolParam 注解尽可能清楚地描述工具的作用,便于 AI 理解和选择调用。
4)注意容错:和工具开发一样,要注意 MCP 服务的容错性和健壮性,捕获并处理所有可能的异常,并且返回友好的错误信息,便于客户端处理。
5)性能优化:MCP 服务端要防止单次执行时间过长,可以采用异步模式来处理耗时操作,或者设置超时时间。客户端也要合理设置超时时间,防止因为 MCP 调用时间过长而导致 AI 应用阻塞。
6)跨平台兼容性:开发 MCP 服务时,应该考虑在 Windows、Linux 和 macOS 等不同操作系统上的兼容性。特别是使用 stdio 传输模式时,注意路径分隔符差异、进程启动方式和环境变量设置。比如客户端在 Windows 系统中使用命令时需要额外添加 .cmd 后缀。
MCP 部署方案
由于 MCP 的传输方式分为 stdio(本地)和 SSE(远程),因此 MCP 的部署也可以对应分为 本地部署 和 远程部署,部署过程和部署一个后端项目的流程基本一致。
本地部署
适用于 stdio 传输方式。其流程与我们开发 MCP 的流程一致,只需将 MCP Server 的代码打包,然后上传到 MCP Client 可访问到的路径下,通过编写对应的 MCP 配置即可启动。
举个例子,我们的后端项目放到了服务器 A 上,如果这个项目需要调用 java 开发的 MCP Server,就要把 MCP Server 的可执行 jar 包也放到服务器 A 上。
这种方式简单直接,适合小项目,但缺点也很明显:每个 MCP 服务都要单独部署(放到服务器上),如果 MCP 服务数量较多,操作起来会非常繁琐。这时你不禁会想:我为什么不直接在后端项目中开发工具调用,非要新搞个项目开发 MCP 呢?
远程部署
适用于 SSE 传输方式。远程部署 MCP 服务的流程跟部署一个后端 web 项目是一样的,都需要在服务器上部署服务(比如 jar 包)并运行
除了部署到自己的服务器之外,由于 MCP 服务一般都是职责单一的小型项目,很适合部署到 Serverless 平台上。使用 Serverless 平台,开发者只需关注业务代码的编写,无需管理服务器等基础设施,系统会根据实际使用量自动扩容并按使用付费,从而显著降低运维成本和开发复杂度。

百炼提供了详细的 使用和部署 MCP 服务指南,可以将自己的 MCP 服务部署到阿里云函数计算平台,实现 Serverless 部署。
友情提示,如果是学习使用,建议及时删除 MCP 服务哦,会自动关联删除函数计算资源。💡
提交至平台
还可以将 MCP 服务提交到各种第三方 MCP 服务市场,类似于将应用发布到应用商店,让其他人也能使用你的 MCP 服务。
这种做法的好处可以参考开源模式,直白来说,至少能提升技术影响力、收获一波流量,这也是大公司会迅速在 MCP 服务市场布局的原因。

恭喜你学习完成!❀