乐于分享
好东西不私藏

Spring AI MCP 博客推送开发手记:从零搭建到踩坑全记录

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.classargs);
    }

@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());
}

看到发成功了

image-20260423223607372

四、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

执行完推送成功:

image-20260501232925331.png
image-20260501232948198.png

基础设施配置

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

启动成功后发起请求试试:

image-20260501232714199.png
image-20260501232653428.png

六、踩坑记录——这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. 1.application.yml设置banner-mode: "off"

  2. 2.logback-spring.xml移除STDOUT appender

  3. 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"}'