1. 为什么 Java 开发者需要一个 CSDN 发帖 MCP
先说清楚这个东西是什么。MCP 全称 Model Context Protocol,你可以把它理解成一套“AI 和外部系统之间的插座标准”。以前你想让大模型帮你发一篇文章到 CSDN,得自己写一堆胶水代码,把模型的输出解析出来、拼成 HTTP 请求、处理 Cookie、再手动触发。MCP 出现之后,这件事变成了:你写一个符合协议的工具服务,模型自己决定什么时候调用、传什么参数,调用完把结果拿回去继续推理。
那为什么是 Java?因为大量后端同学的主力技术栈就是 Java,尤其是 Spring 生态。你不可能为了玩一个 MCP 就把整个工程换成 Python。Spring AI 从 1.0.0-M6 开始对 MCP 的支持已经比较完整了,spring-ai-mcp-server-spring-boot-starter这个 starter 能让你用几个注解就把一个普通 Spring Bean 变成模型可调用的工具。这篇要做的,就是基于 Spring AI 构建一个 CSDN 发帖 MCP Server,走 Stdio 通信模式,本地跑通,然后集成到 Spring AI 的客户端里验证一次真实发帖。
适合谁看?有 Java 基础、用过 Spring Boot、想搞清楚 MCP 到底怎么落地的人。如果你只是想调个 API 发文章,那用 Postman 就够了,不需要 MCP。MCP 的价值在于“让模型自主编排工具”,比如你后面可以做一个 Agent,让它先查资料、再写草稿、再调发帖工具,全程不用你插手。
Stdio 模式又是怎么回事?MCP 有两种常见通信方式:SSE(走 HTTP,适合远程服务)和 Stdio(走标准输入输出,适合本地进程)。Stdio 的好处是简单、无网络依赖、启动快,客户端直接java -jar拉起你的进程,通过 stdin/stdout 交换 JSON-RPC 消息。缺点是一个进程只能服务一个客户端。对于本地开发和个人使用,Stdio 是最省事的选择。
我试过把整个链路拆成“先写一个能跑的 HTTP 调用,再把它包装成 MCP 工具”,这样排错会容易很多。因为 MCP 层出问题时你很难判断是协议问题还是业务问题,先把业务跑通,再套协议壳,是更稳的路径。下面就从环境准备开始,一步步来。
2. 环境准备与 CSDN 接口抓包:Cookie 和 saveArticle 怎么拿
开发环境这块不复杂,但版本要对齐,否则 Spring AI 的 starter 会拉不起来。JDK 17 是硬性要求,Spring Boot 3.4.3,Spring AI 用 1.0.0-M6。Maven 3.6 以上。这些版本组合我实测能跑通,别自己乱升,M6 和后面的 RC 版本 API 有差异。
CSDN 账号方面,你需要一个已实名认证、开通了博客的账号。没认证的话发帖接口会返回权限错误。这一步没什么技术含量,但绕不过去。
关键在抓包。打开浏览器,登录 CSDN,进入创作中心,用 Markdown 编辑器随便写一篇测试文章,按 F12 打开开发者工具,切到 Network 面板,勾选 Preserve log,然后点“发布”或“保存草稿”。你会看到一个请求:
POST https://bizapi.csdn.net/blog-console-api/v3/mdeditor/saveArticle这个就是我们要的接口。点开它,看 Request Headers,重点抓两样东西:Cookie 和那几个x-ca-*签名头。Cookie 是身份凭证,x-ca-key、x-ca-nonce、x-ca-signature是 CSDN 的网关签名。这里有个坑:x-ca-nonce和x-ca-signature是每次请求动态生成的,理论上你复用抓到的旧值也能用一段时间,但不保证长期有效。如果后面发帖返回签名错误,就得重新抓一次。
把整个请求右键 Copy as cURL,导入到 Apifox 或 Postman 里,你能更清楚地看到请求体结构。请求体是 JSON,字段包括title、markdowncontent、content(HTML)、tags、categories、readType、type、pubStatus等。readType控制可见性,private是仅自己可见,public是公开。pubStatus设成draft就是存草稿,设成publish才是正式发布。建议第一次测试用private+draft,避免误发。
这里要提醒一句:Cookie 属于敏感信息,别硬编码进代码提交到 Git。用环境变量注入,后面配置文件里我会写成${CSDN_API_COOKIE}的形式。
抓包完成后,你手上应该有三样东西:完整的接口 URL、一份可用的 Cookie、一份请求体字段清单。有了这些,就可以开始写 Java 代码了。下一节先把 Maven 依赖和配置文件搭好。
3. 可复制的 Spring AI MCP Server 配置:pom.xml 与 application.yml
这一节给你能直接抄的配置。先看pom.xml的关键部分。父工程用 Spring Boot 3.4.3,属性里声明 Spring AI 版本 1.0.0-M6。
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-ai.version>1.0.0-M6</spring-ai.version> <spring-boot.version>3.4.3</spring-boot.version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.3</version> <relativePath/> </parent>依赖里最核心的是 MCP Server starter,它负责把 Stdio 通信、JSON-RPC 解析、工具注册这些脏活全包了:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId> </dependency>HTTP 客户端我用 Retrofit,比 RestTemplate 写起来干净,接口式声明很适合这种固定 API:
<dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>retrofit</artifactId> <version>2.9.0</version> </dependency> <dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>converter-jackson</artifactId> <version>2.9.0</version> </dependency>Markdown 转 HTML 用 Flexmark,因为 CSDN 的content字段要的是 HTML,而模型生成的一般是 Markdown:
<dependency> <groupId>com.vladsch.flexmark</groupId> <artifactId>flexmark-all</artifactId> <version>0.64.8</version> </dependency>别忘了dependencyManagement里导入 Spring AI BOM,否则 starter 版本对不上:
<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>然后是application.yml。Stdio 模式最关键的两行是web-application-type: none和banner-mode: off。前者告诉 Spring 不要启动内嵌 Tomcat,后者避免 banner 输出污染 stdout——记住,Stdio 模式下 stdout 是协议通道,任何多余输出都会破坏 JSON-RPC 消息。
server: servlet: encoding: charset: UTF-8 force: true enabled: true spring: application: name: mcp-server-csdn ai: mcp: server: name: ${spring.application.name} version: 1.0.0 main: banner-mode: off web-application-type: none csdn: api: categories: ${CSDN_API_CATEGORIES} cookie: ${CSDN_API_COOKIE} logging: pattern: console: file: name: data/log/${spring.application.name}.log注意日志配置:我把 console 的 pattern 留空了,因为 Stdio 模式下控制台输出会干扰协议。日志全部写到文件里,排查问题时去看data/log/mcp-server-csdn.log。这个细节很多人会踩坑,启动后客户端一直报解析错误,八成就是有日志打到了 stdout。
配置类CSDNApiProperties用@ConfigurationProperties(prefix = "csdn.api")把这两个值读进来:
@ConfigurationProperties(prefix = "csdn.api") @Component public class CSDNApiProperties { private String cookie; private String categories; // getter/setter 省略 }到这里,配置骨架就搭好了。下一节写业务代码:Retrofit 接口、DTO、Markdown 转换、以及最关键的@Tool注解方法。
4. 工具注册与发帖调用:从 Retrofit 接口到 @Tool 方法
先定义 Retrofit 接口。请求头这块要完整模拟浏览器,尤其是x-ca-*那几个签名头,缺一个都可能被网关拒掉:
public interface ICSDNService { @Headers({ "accept: */*", "content-type: application/json", "origin: https://editor.csdn.net", "referer: https://editor.csdn.net/", "user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", "x-ca-key: 203803574", "x-ca-nonce: a70ca99e-8bfa-46d1-8d12-363c72707ebe", "x-ca-signature: NGLzlIyvH7BuQgGJrgfGOzao0SVpzdTs4aTcw3hio6Y=", "x-ca-signature-headers: x-ca-key,x-ca-nonce" }) @POST("/blog-console-api/v3/mdeditor/saveArticle") Call<ArticleResponseDTO> saveArticle( @Body ArticleRequestDTO request, @Header("Cookie") String cookieValue ); }请求 DTO 字段比较多,但大部分可以给默认值。核心是title、markdowncontent、content、tags、categories、readType、pubStatus:
@Data public class ArticleRequestDTO { private String title; private String markdowncontent; private String content; private String readType = "private"; private String level = "0"; private String tags; private Integer status = 0; private String categories = "测试"; private String type = "original"; private Boolean authorized_status = true; private String Description; private String source = "pc_mdeditor"; private String pubStatus = "draft"; private Integer is_new = 1; // 其余字段给默认值即可 }Markdown 转 HTML 的工具类,静态初始化解析器和渲染器,避免每次调用都重建:
public class MarkdownConverter { private static final Parser parser; private static final HtmlRenderer renderer; static { MutableDataSet options = new MutableDataSet(); parser = Parser.builder(options).build(); renderer = HtmlRenderer.builder(options).build(); } public static String convertToHtml(String markdown) { if (markdown == null || markdown.trim().isEmpty()) { return ""; } return renderer.render(parser.parse(markdown)); } }现在到最关键的一步:用@Tool注解把方法暴露给模型。Spring AI 的MethodToolCallbackProvider会扫描带@Tool的 Bean 方法,自动生成 JSON Schema 描述,模型看到的就是这些描述。
@Slf4j @Service public class CSDNArticleService { @Resource private ICSDNPort port; @Tool(description = "发布文章到CSDN,需要提供标题、Markdown内容、标签和简述") public ArticleFunctionResponse saveArticle(ArticleFunctionRequest request) throws IOException { log.info("CSDN发帖,标题:{} 标签:{}", request.getTitle(), request.getTags()); return port.writeArticle(request); } }请求参数类ArticleFunctionRequest用 Jackson 注解描述每个字段,这些描述会变成模型看到的参数说明:
@Data @JsonInclude(JsonInclude.Include.NON_NULL) public class ArticleFunctionRequest { @JsonProperty(required = true, value = "title") @JsonPropertyDescription("文章标题") private String title; @JsonProperty(required = true, value = "markdowncontent") @JsonPropertyDescription("文章内容,Markdown格式") private String markdowncontent; @JsonProperty(required = true, value = "tags") @JsonPropertyDescription("文章标签,英文逗号隔开") private String tags; @JsonProperty(required = true, value = "Description") @JsonPropertyDescription("文章简述") private String Description; @JsonProperty(required = false, value = "readType") @JsonPropertyDescription("可见性:private 或 public,默认 private") private String readType = "private"; public String getContent() { return MarkdownConverter.convertToHtml(markdowncontent); } }主启动类里注册 Retrofit Bean 和 ToolCallbackProvider:
@SpringBootApplication public class McpServerApplication { public static void main(String[] args) { SpringApplication.run(McpServerApplication.class, args); } @Bean public ICSDNService csdnService() { Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://bizapi.csdn.net/") .addConverterFactory(JacksonConverterFactory.create()) .build(); return retrofit.create(ICSDNService.class); } @Bean public ToolCallbackProvider csdnTools(CSDNArticleService service) { return MethodToolCallbackProvider.builder() .toolObjects(service) .build(); } }这里有个容易忽略的点:MethodToolCallbackProvider注册的是整个对象,它会扫描对象里所有@Tool方法。如果你一个 Service 里有多个工具方法,都会被注册进去。工具名默认是方法名,所以saveArticle就是模型看到的工具名。
代码写完后,mvn clean package打包,得到mcp-server-csdn-app.jar。注意spring-boot-maven-plugin的mainClass要指向McpServerApplication,否则打出来的 jar 没有主清单,java -jar会报 no main manifest attribute。
5. 本地 Stdio 启动与发帖验证:mcp-servers-config.json 配置
打包完成后,先别急着集成到客户端,单独测一下 jar 能不能起来。直接命令行跑:
java -Dspring.ai.mcp.server.stdio=true \ -Dfile.encoding=UTF-8 \ -jar target/mcp-server-csdn-app.jar如果进程挂住不动、没有报错退出,说明 Stdio 服务正常启动了,它在等 stdin 输入。按 Ctrl+C 退出即可。如果看到 Spring banner 或者 Tomcat 启动日志,说明web-application-type: none没生效,回去检查 yml。
接下来配置客户端。Spring AI 的 MCP 客户端通过一个 JSON 文件描述要启动哪些 Server,文件名通常叫mcp-servers-config.json:
{ "mcpServers": { "mcp-server-csdn": { "command": "java", "args": [ "-Dspring.ai.mcp.server.stdio=true", "-Dfile.encoding=UTF-8", "-Dconsole.encoding=UTF-8", "-Dstdin.encoding=UTF-8", "-Dstdout.encoding=UTF-8", "-Dstderr.encoding=UTF-8", "-jar", "D:\\code\\JAVA\\mcp-server-csdn\\stdio\\mcp-server-csdn-app.jar" ], "env": { "CSDN_API_CATEGORIES": "Java", "CSDN_API_COOKIE": "你的真实Cookie" } } } }这里三件套必须齐全:command是启动命令,args是参数,env是环境变量。Base URL 在代码里写死了https://bizapi.csdn.net/,Key 就是 Cookie,Model ID 在 MCP 场景下对应的是工具名saveArticle。这三个概念在 MCP 里和普通 API 调用不太一样,但对应关系要清楚。
编码参数那一堆-Dxxx.encoding=UTF-8不是凑数的。Windows 默认 GBK,中文标题和内容传过去会乱码,加上这些参数强制 UTF-8。Linux/Mac 上可以省略,但加上无害。
配置好后,在 Spring AI 客户端里问一句“有哪些工具可以使用”,正常会返回类似:
您可以使用以下工具: 1. functions.saveArticle: 用于将文章发布到CSDN 2. multi_tool_use.parallel: 用于同时运行多个工具看到saveArticle就说明工具注册成功了。然后让它发一篇测试文章,比如“帮我发一篇标题为‘MCP测试’的草稿到CSDN,内容是 hello world,标签 Java”。模型会调用saveArticle,传入参数,你的 Server 收到请求后走 Retrofit 发到 CSDN,返回文章 URL 和 ID。
验证成功的标志:日志文件里出现请求CSDN发帖的 req/res 记录,且 response 的code是 200,data.url有值。去 CSDN 创作中心的草稿箱,能看到那篇文章。第一次建议用readType=private+pubStatus=draft,确认无误后再改成公开。
6. 常见报错排查:401、local proxy failed 与 reading choices
这一节列几个真实会撞上的错误,以及怎么定位。
401 Unauthorized 或 code 非 200:九成是 Cookie 失效。CSDN 的 Cookie 有效期不长,隔天可能就过期。重新抓一次,更新env里的CSDN_API_COOKIE。另外检查 Cookie 有没有被 shell 转义,里面有分号和空格,JSON 里要完整保留。
local proxy failed / connection refused:客户端报这个通常是 Server 进程没起来。先手动java -jar跑一遍,看有没有异常堆栈。常见原因是 jar 路径写错、JDK 版本不对(低于 17 会报 UnsupportedClassVersionError)、或者web-application-type没设成 none 导致端口冲突。
reading choices 相关解析错误:这个报错一般出现在客户端侧,意思是它从 stdout 读到的不是合法 JSON-RPC 消息。根因是 Server 往 stdout 打了非协议内容。检查三处:banner-mode: off有没有生效、日志有没有配到 console、有没有System.out.println残留。我踩过的坑就是在测试类里留了个System.out.println,打包后忘了删,客户端一直解析失败。
OAuth / 签名错误:如果返回x-ca-signature相关错误,说明 CSDN 的网关签名校验没过。x-ca-nonce和x-ca-signature是抓包时的快照,可能已过期。重新抓一次请求,把这两个头更新到@Headers里。长期方案是研究签名算法动态生成,但个人使用重新抓包更省事。
中文乱码:标题或内容变成问号。确认-Dfile.encoding=UTF-8等参数都加上了,且application.yml里server.servlet.encoding.force: true。另外 Flexmark 转换时如果源字符串编码不对,也会乱码,确保读入的 Markdown 是 UTF-8。
工具没被识别:客户端问“有哪些工具”时看不到saveArticle。检查@Tool注解有没有加、ToolCallbackProviderBean 有没有注册、MethodToolCallbackProvider.builder().toolObjects()传的对象对不对。还有一个隐蔽问题:如果方法抛异常且没被捕获,工具注册阶段可能静默失败,加日志确认。
排查顺序建议:先手动跑 jar 确认进程正常,再用一个最简单的 HTTP 测试类直接调 Retrofit 接口确认业务通,最后才走 MCP 客户端。分层定位比一上来就调 MCP 快得多。
7. 接入 TaoToken 与后续扩展
工具跑通之后,如果你想让模型侧更稳定地调用,可以把模型接入层换成 TaoToken。它的 API 地址是https://taotoken.net/api,兼容 OpenAI 风格的接口,Spring AI 里配置base-url和api-key就能用。模型对话调试可以去 模型对话 页面直接试,确认工具调用返回正常。
如果你打算长期做编码类 Agent,比如让模型自动写文章、自动发帖、自动回评论,那用 Coding Plan 会更划算,额度按编码场景优化过。API Key 在 API Keys 页面生成,接入细节看 接入文档。控制台在 Console。
扩展方向有几个。一是把saveArticle拆成saveDraft和publishArticle两个工具,让模型自己决定存草稿还是直接发。二是加一个queryArticle工具,支持按标题查已发文章。三是把 Cookie 换成动态刷新,避免频繁抓包。四是把 Stdio 换成 SSE 模式,这样多个客户端能共享一个 Server 实例。
最后留个实用技巧:把mcp-servers-config.json里的 jar 路径和 Cookie 用环境变量管理,别写死在文件里。团队协作时,每个人本地配自己的 Cookie,配置文件可以提交到 Git,敏感信息走.env或系统环境变量。这样既方便又安全。