Spring AI MCP 博客推送开发手记:从零搭建到踩坑全记录
- 一、环境准备——先别急着写代码
- 二、MCP服务器开发——这个才是重头戏
- 项目结构
- pom.xml里有个插件必须加
- 启动类怎么写
- 日志配置——这个坑了我好久
- 实现MCP工具——参数必须用基本类型
- 构建项目
- 三、MCP客户端集成——配置比代码重要
- 配置文件怎么写
- 客户端配置
- 测试工具调用
- 四、REST API封装——给应用加个接口
- 五、Docker容器化部署——最后一步
- push.sh脚本
- 基础设施配置
- 应用服务配置
- 六、踩坑记录——这12个坑你可能也会遇到
- 七、完整部署流程
- 首次部署
- 更新部署
- 验证接口
Spring AI MCP 博客推送开发手记:从零搭建到踩坑全记录
❝
这个项目让我折腾了一周。写篇文章记录下,顺便帮后来人避避坑。
事情是这样的,前段时间我想做一个自动发博客的工具——让AI直接帮我生成文章,然后自动发布到博客园。
听起来挺简单的对吧?结果这一折腾就是好几天。
我把自己踩过的12个坑整理了一下,希望你别再踩了。
一、环境准备——先别急着写代码
JDK版本问题
第一个坑就给我整懵了。
项目跑不起来,报了一堆乱七八糟的错。后来发现是因为我电脑里装了多个JDK版本,代码编译用一个版本,运行又用另一个版本,直接干蒙了。
后来老老实实用了Azul Zulu JDK 17.0.16,统一了编译和运行环境。
对了,macOS上的路径是:
/Users/xxx/Library/Java/JavaVirtualMachines/azul-17.0.16/Contents/Home
Maven配置
Spring Boot版本用的是3.4.3,Spring AI用的是1.0.0-M6。这个倒是没踩什么坑,就是提醒下别搞错版本。
阿里云API Key
这个我差点忘了——欠费的话API会直接报错。记得去阿里云百炼控制台充点钱,不然到时文章发不出去,你都不知道咋回事。对了还有模型,有的模型不支持工具调用,这里用的qwen-plus没问题。
二、MCP服务器开发——这个才是重头戏
项目结构
先说说我是怎么组织代码的:
mcp-server-csdn/
├── src/main/java/mcp/server/blog/
│ ├── McpServerApplication.java # 启动类
│ ├── domain/service/
│ │ └── CnblogsMcpService.java # 工具实现
│ ├── infrastructure/gateway/
│ │ ├── CnblogsApiService.java # API 接口
│ │ └── dto/
│ │ ├── BlogPostRequest.java # 请求 DTO
│ │ └── BlogPostResponse.java # 响应 DTO
│ └── type/
│ └── ApiProperties.java # 配置属性
├── src/main/resources/
│ ├── application.yml # 应用配置
│ └── logback-spring.xml # 日志配置
└── pom.xml # Maven 配置
说实话,结构这块没什么特别的,就是按常规Spring Boot项目的方式来。主要是为了后面好扩展。
pom.xml里有个插件必须加
这点我栽过一次。
打包成JAR后运行不起来,报错是”没有主清单属性”。查了半天,发现是pom.xml里少加了spring-boot-maven-plugin插件。
xml
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>server.blog.McpServerApplication</mainClass>
<layout>JAR</layout>
</configuration>
</plugin>
</plugins>
</build>
如果你也遇到同样的报错,先检查这个配置有没有加。
启动类怎么写
启动类里需要注册ToolCallbackProvider:
java
@SpringBootApplication
publicclassMcpServerApplication{
publicstaticvoidmain(String[] args){
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider getTools(CnblogsMcpService cnblogsMcpService){
return MethodToolCallbackProvider.builder()
.toolObjects(cnblogsMcpService)
.build();
}
}
日志配置——这个坑了我好久
为什么日志配置重要?因为MCP用的是Stdio模式,所有日志输出到stdout的话,会污染MCP协议的通信。
我当时各种调试都正常,但就是连不上MCP服务器。后来才反应过来——日志在往stdout写,把MCP的响应给搞乱了。
所以logback-spring.xml里只能输出到文件:
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appendername="FILE"class="ch.qos.logback.core.FileAppender">
<file>data/log/mcp-server-blog.log</file>
<append>true</append>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<rootlevel="INFO">
<appender-refref="FILE"/><!-- 只输出到文件 -->
</root>
</configuration>
实现MCP工具——参数必须用基本类型
这个也是血的教训。
我一开始写的方法参数是DTO对象,结果AI根本调不通。查文档、翻源码,最后发现必须用基本类型加上@ToolParam注解。
java
@Tool(description = "发布文章到博客园,需要提供文章标题、文章正文内容(支持Markdown格式)和可选的发布日期")
public String saveArticle(
@ToolParam(description = "文章标题") String title,
@ToolParam(description = "文章正文内容,支持Markdown格式") String postBody,
@ToolParam(description = "发布日期,ISO 8601格式,可选,默认当前时间") String datePublished) throws IOException {
// 构建请求
BlogPostRequest postRequest = new BlogPostRequest();
postRequest.setTitle(title);
postRequest.setPostBody(postBody);
postRequest.setIsMarkdown(true);
postRequest.setIsPublished(true);
// 同步执行HTTP请求
String cookie = apiProperties.getCookie();
String xsrfToken = extractXsrfToken(cookie);
try {
Call<BlogPostResponse> call = blogService.createPost(postRequest, cookie, xsrfToken);
Response<BlogPostResponse> response = call.execute(); // 同步执行
if (response.isSuccessful() && response.body() != null) {
log.info("文章发布成功,标题:{}", title);
return String.format("文章发布成功!标题: %s", title);
} else {
String errorMsg = response.errorBody().string();
log.error("文章发布失败:{}", errorMsg);
return"文章发布失败:" + errorMsg;
}
} catch (IOException e) {
log.error("文章发布异常", e);
return"文章发布异常:" + e.getMessage();
}
}
另外注意下,返回值要是同步执行的,不能返回Call异步对象,否则AI拿不到结果。
构建项目
bash
cd mcp-server-csdn
mvn clean package -DskipTests
三、MCP客户端集成——配置比代码重要
配置文件怎么写
mcp-servers-config.json里配置MCP服务器。注意这个JSON文件不支持注释,我之前顺手加了//注释,直接报错解析失败。
json
{
"mcpServers": {
"mcp-server-git": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-jar",
"/mcp/jar/mcp-git-server.jar"
],
"env": {
"GITEE_ACCESS_TOKEN": "你的Token"
}
},
"mcp-server-blog": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-jar",
"/mcp/jar/mcp-server-csdn-1.0.0.jar"
],
"env": {
"BLOG_API_CATEGORIES": "Java场景面试宝典",
"BLOG_API_COOKIE": "你的Cookie"
}
}
}
}
客户端配置
application-dev.yml里配置客户端连接:
yaml
server:
port:8090
spring:
ai:
mcp:
client:
request-timeout:360s
stdio:
servers-configuration:classpath:/config/mcp-servers-config.json
openai:
base-url:https://dashscope.aliyuncs.com/compatible-mode
api-key:sk-你的API-KEY
embedding-model:text-embedding-v3
测试工具调用
写了个简单的测试类验证功能:
java
@Test
publicvoidtest_article(){
String userInput = """
我需要你帮我生成一篇文章...
将以上内容发布文章到Blog
""";
var chatClient = chatClientBuilder
.defaultTools(tools) // 启用 MCP 工具
.defaultOptions(OpenAiChatOptions.builder()
.model("qwen-plus")
.build())
.build();
System.out.println(chatClient.prompt(userInput).call().content());
}
看到发成功了

四、REST API封装——给应用加个接口
为了方便调用,我给MCP功能包了一层REST API:
java
@Slf4j
@RestController
@RequestMapping("/api/v1/mcp")
publicclassMcpArticleController{
privatefinal ChatClient.Builder chatClientBuilder;
privatefinal ToolCallbackProvider tools;
publicMcpArticleController(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools){
this.chatClientBuilder = chatClientBuilder;
this.tools = tools;
}
@PostMapping("/article/generate")
public String generateAndPublishArticle(@RequestBody ArticleRequest request){
String userInput = String.format("""
我需要你帮我生成一篇文章,要求如下;
1. 场景为互联网大厂java求职者面试
2. 提问的技术栈如下;
%s
生成100字的文章,将以上内容发布文章到Blog
""", request.getTechStack());
var chatClient = chatClientBuilder
.defaultTools(tools)
.defaultOptions(OpenAiChatOptions.builder()
.model(request.getModel())
.build())
.build();
log.info(">>> QUESTION: {}", userInput);
String result = chatClient.prompt(userInput).call().content();
log.info(">>> ASSISTANT: {}", result);
return result;
}
@Data
publicstaticclassArticleRequest{
private String techStack;
private String model = "qwen-plus";
}
}
调用示例:
bash
curl -X POST http://localhost:8090/api/v1/mcp/article/generate \
-H "Content-Type: application/json" \
-d '{
"techStack": "Java SE, Spring Boot, JVM",
"model": "qwen-plus"
}'
五、Docker容器化部署——最后一步
部署主要分三块:
1. push.sh – 构建和推送脚本
登录阿里云镜像仓库 → Maven 构建项目 → Docker 构建镜像 → 推送到阿里云 → 登出
代码更新后要发布新版本时执行这个脚本。
2. docker-compose-environment-aliyun.yml – 基础设施配置
启动项目依赖的基础设施:Redis(端口16379)、PostgreSQL+pgvector(端口15432)等。
首次部署或需要重启基础设施时用这个。
3. docker-compose-app-v1.0.yml – 应用服务配置
启动AI MCP应用本身,需要连接PostgreSQL和Redis,挂载MCP配置文件和JAR包。
push.sh脚本
bash
#!/bin/bash
# 指定 Java 17 环境
export JAVA_HOME=/Users/xxx/Library/Java/JavaVirtualMachines/azul-17.0.16/Contents/Home
export PATH=$JAVA_HOME/bin:$PATH
set -e
# 配置变量
ALIYUN_REGISTRY="crpi-xxx.cn-guangzhou.personal.cr.aliyuncs.com"
NAMESPACE="yanglingcong"
IMAGE_NAME="ai-mcp-knowledge-app"
IMAGE_TAG="1.1"
# 登录阿里云镜像仓库
echo"Logging into Aliyun Docker Registry..."
docker login --username="用户名" --password="密码"$ALIYUN_REGISTRY
# 构建项目
echo"Building the Docker image..."
cd ..
mvn clean package -DskipTests
cd ai-mcp-knowledge-app
# 构建 Docker 镜像
docker build -t ${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG} .
# 标记镜像
echo"Tagging the Docker image..."
docker tag ${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}${ALIYUN_REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}
# 推送镜像
echo"Pushing the Docker image to Aliyun..."
docker push ${ALIYUN_REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}
echo"Docker image pushed successfully!"
echo"检出地址:docker pull ${ALIYUN_REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}"
# 登出
docker logout$ALIYUN_REGISTRY
执行完推送成功:


基础设施配置
yaml
version:'3'
services:
redis:
image:xxxx/redis:6.2
container_name:redis
restart:always
ports:
-16379:6379
networks:
-my-network
vector_db:
image:xxxx/pgvector:v0.5.0
container_name:vector_db
restart:always
environment:
-POSTGRES_USER=postgres
-POSTGRES_PASSWORD=postgres
-POSTGRES_DB=ai-rag-knowledge
ports:
-'15432:5432'
networks:
-my-network
networks:
my-network:
driver:bridge
应用服务配置
yaml
version:'3.8'
services:
ai-mcp-knowledge-app:
image:crpi-xxx.cn-guangzhou.personal.cr.aliyuncs.com/yanglingcong/ai-mcp-knowledge-app:1.1
container_name:ai-mcp-knowledge-app
restart:always
ports:
-"8090:8090"
volumes:
-./log:/data/log
-/本地路径/config:/mcp/config
-/本地路径/jar:/mcp/jar
environment:
-TZ=PRC
-SERVER_PORT=8090
-SPRING_DATASOURCE_URL=jdbc:postgresql://vector_db:5432/ai-rag-knowledge
-SPRING_AI_OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode
-SPRING_AI_OPENAI_API_KEY=sk-你的API-KEY
-SPRING_AI_MCP_CLIENT_STDIO_SERVERS_CONFIGURATION=file:/mcp/config/mcp-servers-config.json
networks:
-my-network
networks:
my-network:
driver:bridge
启动成功后发起请求试试:


六、踩坑记录——这12个坑你可能也会遇到
坑1:JAR包无法执行
❝
报错:没有主清单属性 解决:pom.xml里加上spring-boot-maven-plugin插件
坑2:Maven构建失败(JDK版本不匹配)
❝
报错:Fatal error compiling: 无效的标记: –release 解决:在push.sh里显式设置JAVA_HOME
坑3:MCP连接超时
❝
报错:TimeoutException: Did not observe any item or terminal signal within 360000ms 原因:JAR包问题、JDK版本问题、日志污染Stdio通道 解决:禁止日志输出到stdout
坑4:JSON解析失败
❝
报错:JsonParseException: Unexpected character (‘-‘ (code 45)) 原因:Spring Boot启动日志输出到stdout,污染了MCP协议 解决:
1.application.yml设置
banner-mode: "off"2.logback-spring.xml移除STDOUT appender
3.启动参数添加
-Dlogging.pattern.console=
坑5:工具未被调用
❝
现象:AI没有调用MCP工具 原因:Tool方法使用了复杂对象参数(DTO) 解决:拆解为基本类型 +
@ToolParam注解
坑6:工具返回结果异常
❝
现象:工具被调用但实际没有执行成功 原因:返回了
Call<T>异步对象 解决:同步执行并返回String
坑7:Docker镜像拉取失败
❝
报错:403 Forbidden或pull access denied 原因:镜像加速器失效、硬编码了阿里云公共库地址 解决:配置有效的镜像加速器,或使用官方镜像源
FROM openjdk:17-jdk-slim
坑8:Maven资源过滤损坏二进制文件
❝
报错:MalformedInputException: Input length = 1 原因:
pom.xml中配置了<filtering>true</filtering>,导致JAR包等二进制文件被错误处理 解决:分离资源配置,文本文件和二进制文件分开处理
坑9:Docker容器内找不到配置文件
❝
报错:FileNotFoundException: /mcp/config/mcp-servers-config.json 解决:创建目录并复制配置文件
bash
mkdir -p mcp/config mcp/jar
cp src/main/resources/config/mcp-servers-config.json mcp/config/
坑10:Docker容器内找不到Node.js
❝
报错:Cannot run program “npx”: error=2, No such file or directory 原因:MCP配置中包含了需要npx的服务器,但容器镜像只有Java 解决:修改mcp-servers-config.json,移除需要Node.js的服务器
坑11:Web接口无法访问
❝
现象:容器运行正常,但curl返回Connection reset by peer 原因:application-dev.yml配置了
web-application-type: none,禁用了Web服务器 解决:移除该配置或设置为servlet
坑12:阿里云API欠费
❝
报错:Arrearage: Access denied 解决:登录阿里云百炼控制台充值
七、完整部署流程
首次部署
bash
# 1. 启动基础设施
docker-compose -f docker-compose-environment-aliyun.yml up -d
# 2. 构建并推送镜像
cd ai-mcp-knowledge-app
bash push.sh
# 3. 启动应用
docker-compose -f docker-compose-app-v1.0.yml up -d
# 4. 查看日志
docker logs -f ai-mcp-knowledge-app
更新部署
bash
# 1. 修改代码后重新构建推送
bash push.sh
# 2. 重启应用
docker-compose -f docker-compose-app-v1.0.yml down
docker-compose -f docker-compose-app-v1.0.yml up -d
验证接口
bash
curl -X POST http://localhost:8090/api/v1/mcp/article/generate \
-H "Content-Type: application/json" \
-d '{"techStack": "Java SE, Spring Boot", "model": "qwen-plus"}'
夜雨聆风