ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Java MCP协议实现博客自动发帖:从零搭建可编程发布管道

基于Java MCP协议实现博客自动发帖:从零搭建可编程发布管道 前阵子我一直在折腾博客内容的发布流程Markdown 写完之后要复制到后台、传封面图、改摘要、选分类、点发布一步都不能漏漏一次就后悔一次。后来我干脆用 Java 写了一个 MCP 服务把“发帖”这件事从手动复制粘贴变成了一个可编程、可调度的标准接口。这个 JavaMCP 自动发帖项目跑通之后我的工作流变成了这样本地写完 Markdown保存文件定时任务把文件交给 MCP ServerServer 负责解析、转换、调用平台 API 发布全程不用打开浏览器后台也不会再出现“发完才发现分类选错”这种低级问题。这篇文章不是介绍一个现成的开源工具而是把我从零搭建这个项目时的设计思路、核心实现、踩坑过程和运行经验完整拆一遍。适合下面几类人看想给自己博客做自动化发布管道的开发者团队里需要把内容发布能力以标准接口暴露给 AI 助手或调度系统的后端工程师以及刚接触 MCP 协议、想找一个真实落地场景入门的 Java 程序员。1. 项目到底在解决什么问题1.1 JavaMCP 和自动发帖是怎么结合起来的MCP 的全称是 Model Context Protocol它的定位可以理解成“AI 工具界的 USB-C 接口”。以前一个 AI 客户端想调用外部能力每个平台都要写一套私有协议工具方也要为每个客户端做适配。MCP 把“工具发现、参数描述、调用、返回结果”这些过程标准化了AI 助手、命令行客户端、定时任务都可以通过同一套协议去调用你注册好的工具。JavaMCP 指的就是用 Java 生态去实现 MCP 的 Server 或 Client。放在自动发帖这个场景里我做的事情其实很简单把平台的发布接口封装成 MCP Server 上的一个 Tool比如叫publish_post。调用方传给我文章标题、Markdown 正文、分类 ID、是否立即发布我在 Server 端做校验、转换、调用平台 API再把发布结果返回给调用方。这个做法的好处是调用方不需要关心平台 API 的认证方式、请求格式和错误码。我今天对接的是某个博客平台明天换成企业的 Wiki 系统后面对接公司的 CMS调用方的代码几乎不用改因为它们只认 MCP 协议里的publish_post这个工具。等于说我把“发帖”这个动作从一段不可复制的线下操作变成了一条可以被任何懂 MCP 的客户端调用的标准服务。1.2 它适合什么场景不适合什么场景我实测下来JavaMCP 自动发帖最适合的是下面这几类场景。个人博客和知识库的自动化发布是最直接的。内容以 Markdown 文件形式存在 Git 仓库里提交代码后触发发布任务MCP Server 把 Markdown 解析成目标平台需要的格式并推送出去整个过程可以做到和 Git 提交记录一一对应。团队内容中台是价值比较高的场景。编辑在文档系统里写稿内容通过 MCP Server 分发给公司内部 CMS、对外官网、微信公众号后台等多个渠道。因为每个渠道都是一个独立的 Adapter新增渠道不会影响已有逻辑。AI 辅助写作的场景也值得提一句。现在很多人用 LLM 写初稿但写完还得手动复制到发布后台。如果 MCP Client 集成了publish_draft这个工具AI 就可以在生成完内容之后直接帮你创建草稿人工审核后再点发布体验会顺很多。但也别指望拿它去做灰色地带的批量注册、刷帖这类事情。任何自动化发布都要以目标平台的官方 API 和规则为前提我做的所有设计也都是围绕“合规、可控、有审计”这三个词展开的。想清楚边界再动手这个方向才走得远。1.3 为什么值得自己动手做一遍市面上有不少现成的发布工具但大部分是“单机脚本 平台 API”的组合脚本挂在服务器上改个配置都要动代码更别提把发布能力开放给 AI 客户端了。MCP 的价值在于它把工具和调用方彻底解耦了再加上 Java 生态里的 Spring、Quartz、Micrometer 这些基础设施可以很自然地接上配置中心、监控告警和定时调度。自己动手做一遍的好处不是省下买工具的钱而是你能完全掌控发布链路里的每一个环节幂等怎么做、重试怎么退避、格式怎么兜底、失败怎么告警。这些细节恰恰是现成工具最容易黑盒化的地方。我后面会详细讲我在这些细节上的取舍。2. 先把整体架构想清楚2.1 为什么不用普通 REST 接口非要上 MCP这是我一开始最纠结的问题。单纯实现“输入标题和正文输出发布结果”一个 Spring Boot 项目写个PostMapping(/publish)就够了何必绕一层 MCP后来我列出了两种方案在真实需求下的差异才坚定选了 MCP。直接写 REST 接口的问题不是不能用而是“接口签名”没有统一标准。调用方要自己查文档才知道参数叫什么、类型是什么、可选字段有哪些AI 客户端更是无法自动发现你这个接口。今天给博客写一个/publish明天给 Wiki 写一个/wiki/create每个接口都要单独对接工具数量一多就变成维护灾难。MCP 协议把工具描述、参数 Schema、返回值 Schema 全部标准化了。MCP Client 可以通过tools/list拿到 Server 上所有工具的信息包括字段含义、是否必填、字段描述然后动态构造调用请求。这个体验非常像前端框架拿到 OpenAPI 文档后自动生成类型定义但它比 OpenAPI 更轻量而且是专门为 AI 调用场景设计的。还有一个实际原因是调度系统集成方便。我的定时任务框架不想依赖某个具体的发布 SDK它只需要管 MCP Client 的连接和调用即可。换平台、换适配器调度代码一行不动。这种“面向协议编程”带来的收益在项目跑到第二个月、开始接第三个平台的时候会特别明显。2.2 核心模块怎么划分我最终把项目分成了六个模块每个模块职责单一互相之间通过接口隔离。传输层负责处理 MCP 的底层通信。我常用的有两种stdio 和 streamable HTTP。stdio 适合本地命令行调用启动 MCP Server 进程后通过标准输入输出收发 JSON-RPC 消息HTTP 适合部署成独立服务让远程的 MCP Client 或调度系统连接。协议层处理 MCP 协议的消息编解码、会话初始化和能力协商。这块用官方 SDK 就好不建议自己重新实现 JSON-RPC 2.0 那套东西协议细节很容易写错。工具注册中心是核心枢纽负责把publish_post、save_draft、get_post_status这些工具注册到 MCP Server 上同时维护参数描述和执行函数之间的映射关系。内容解析模块负责把 Markdown 转成目标平台能接受的内容。它要做的不只是字符串替换还要处理 Front Matter、图片路径、内部链接、代码块语法兼容等问题我后面会专门展开。平台适配层是变化最频繁的模块。我用一个接口抽象了“发布、存草稿、查状态”三个动作每个平台一个实现类。这样 BlogAdapter、CMSAdapter、WikiAdapter 之间互不干扰新平台接入只是多写一个类的事。调度与监控模块负责定时任务触发、重试策略、日志记录和告警推送。它和协议层完全解耦只通过 MCP Client 去调用工具。这六个模块的运行关系大概是这样调度任务到点后通过 MCP Client 调用publish_post工具请求经过协议层进入工具注册中心注册中心交给内容解析模块完成格式处理再交给平台适配层执行真正的发布最后把结果原路返回。任何一个环节失败都会被监控模块捕获并触发重试或告警。2.3 同步发布还是异步队列选型要看场景发帖子在技术上是一个典型的“副作用操作”执行成功之后会产生一条外部可见的新记录。这种操作最忌讳的是调用方发起了请求但不知道结果。我一开始图省事工具内部直接同步调用平台 API简单场景没问题但一旦平台 API 响应慢MCP 调用就会长时间卡住超时设置不好还会导致重复发布。后来我做了个区分如果是单篇文章发布走同步模式发布接口必须在 30 秒内返回结果适合人工手动调用或 AI 交互场景如果是一次要发布几十篇文章走异步队列模式工具只负责把发帖任务写入数据库的 outbox 表并立即返回“已受理”后台 Worker 扫描 outbox逐条调用平台适配器完成发布。这两个模式的差异我整理成了表格。对比项同步模式异步队列模式返回速度直到平台返回才返回写入队列后立即返回可靠性依赖网络和超时配置落库后可重试可靠性高适用场景单篇发布、AI 交互确认批量发布、定时排期实现复杂度低中需要任务表和 Worker重复风险超时重试容易重复靠任务状态机控制我自己的最终方案是两者共存工具入参里加一个publish_mode字段默认async手动调试时改成sync。这样可以兼顾交互体验和批量稳定。3. 核心实现与配置要点3.1 搭一个 MCP Server 最小骨架我用的是 Java MCP SDK项目基础是 Spring Boot。依赖管理里加上对应的 starter版本号以你拉取时的最新稳定版为准。dependency groupIdio.modelcontextprotocol/groupId artifactIdmcp-spring-boot-starter/artifactId version0.8.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency配置类里声明了一个McpServerBean把工具类注册进去。Configuration public class McpServerConfig { private final PostTools postTools; public McpServerConfig(PostTools postTools) { this.postTools postTools; } Bean public McpServer mcpServer() { return McpServer.sync(streamableHttpTransport()) .tools(postTools) .capabilities(McpSchema.ServerCapabilities.builder() .tools(true) .build()) .build(); } Bean public HttpTransport streamableHttpTransport() { return new HttpServletStreamableHttpTransport(); } }工具类本身是一个 Spring 组件方法上用Tool注解标明名称和描述。这里有一个容易忽略的点工具描述要写清楚因为 MCP Client 拿到描述后会直接把它作为模型理解工具的依据描述写得模糊AI 调用方就很容易构造出不合法的参数。Component public class PostTools { private final PostPlatformAdapter platformAdapter; private final IdempotencyKeyStore idempotencyKeyStore; public PostTools(PostPlatformAdapter platformAdapter, IdempotencyKeyStore idempotencyKeyStore) { this.platformAdapter platformAdapter; this.idempotencyKeyStore idempotencyKeyStore; } Tool(name publish_post, description 发布一篇文章到指定平台。需要提供标题、Markdown 正文和目标平台名称可以只存草稿不立即发布。) public PublishResult publishPost( ToolParam(description 文章标题长度不超过 200 个字符) String title, ToolParam(description Markdown 格式的文章正文) String content, ToolParam(description 目标平台标识例如 blog 或 cms) String platform, ToolParam(description 是否立即发布默认 false 表示保存为草稿, required false) Boolean published) { boolean needPublish published null ? Boolean.FALSE : published; String idempotencyKey UUID.randomUUID().toString(); try { String normalizedContent MarkdownUtils.normalizeContent(content); PostRequest request new PostRequest(title, normalizedContent, platform, needPublish); return platformAdapter.publish(idempotencyKey, request); } catch (ContentNotValidException e) { return PublishResult.failure(内容校验失败: e.getMessage()); } } }这段代码是简化后的版本但它包含了一个很重要的设计工具方法不要直接把外部输入交给平台 Adapter而是先做参数校验和内容规范化。很多自动发帖事故源头都是“调用方传了什么我就发布什么”没有给平台侧做最后一道防线。3.2 参数设计要站在调用方的角度publish_post的参数表面上就是标题、正文、分类那几个字段但实际设计时我花了不少心思。标题长度、正文字数、必填字段这些校验一定要放在 MCP Server 内。AI 调用方可能在生成内容时产生幻觉把时间写进标题里或者漏传某个必填参数。与其让平台 API 返回一个晦涩的错误码不如在进入平台之前的工具方法里就返回清晰的错误信息。分类和时间参数我做了动态化。分类字段用categoryId接收但允许调用方传入分类路径字符串例如技术/后端Server 端负责把路径解析成目标平台的分类 ID。发布时间接受 ISO 8601 格式支持“不传时间就立即发布传了未来时间就定时发布”的逻辑。这样设计的好处是让调用方可以传人类习惯的值而不是必须先去查平台分类 ID。返回结构我统一成了三个字段success、postUrl、message。不管底层平台返回的数据结构多复杂最终都收敛成这三个字段。MCP Client 可以直接根据success判断结果AI 调用方也可以把postUrl直接呈现给用户。这个收敛逻辑很笨但非常实用。3.3 幂等设计是自动发帖的生命线自动发帖最怕的不是发不出去而是发了两遍。网络超时、MCP 调用重试、调度任务重复执行任何一个环节抖动都可能导致重复请求。我在这个项目里做了三层幂等保护。第一层是 MCP 层。入参里允许调用方传idempotency_key不传的话 Server 端自动生成一个。这个 Key 保存到 Redis 和本地数据库有效期内同一个 Key 只允许成功一次。第二次进来直接返回第一次的结果。第二层是平台 Adapter 层。有些平台 API 本身支持客户端提供的幂等键有些不支持。对于不支持的平台我在调用前会先查“按标题内容哈希”的判断如果已经存在同标题同内容的文章默认跳过发布返回已有文章链接。第三层是任务表状态机。异步队列里的每条任务都有INIT → PROCESSING → SUCCESS/FAILED状态Worker 处理前要乐观锁更新状态确保同一时间只有一个 Worker 能拿到这条任务。这三层下来我实际运行了一个季度没有出现过一次重复发布。要说哪个最管用反而是第二层内容哈希因为它是最终的兜底就算前两层都出问题内容级去重依然能拦住。3.4 平台适配层的接口怎么抽象平台适配是我最早写的模块也是后来改动最多的模块。抽象不好每接一个平台就要动一遍核心逻辑。public interface PostPlatformAdapter { String platformName(); PublishResult publish(String idempotencyKey, PostRequest request); PublishResult saveDraft(String idempotencyKey, PostRequest request); PostStatus queryStatus(String platformPostId); }平台名用字符串标识blog、cms、wiki各来一个实现。Spring 里把实现类都注入到一个MapString, PostPlatformAdapter工具方法里根据入参platform字段直接取对应实现。适配器内部只负责做两件事拼装平台特定的请求结构解析平台特定的返回结果。把这两个步骤封装好核心工具层就完全不需要知道平台差异。新平台接入时我只需要新写一个 Adapter核心层和 MCP 层一行都不用改。这套抽象跑久了还有一个额外好处测试变得非常好写。平台 Adapter 是接口测试环境里可以用 Mock 实现代替真实平台MCP 工具层的单元测试完全不需要依赖外部服务。3.5 配置清单建议我把运行所需的配置项整理成一份清单按这个清单配置基本不会漏。配置项示例值说明mcp.server.namejava-mcp-post-serverMCP Server 名称platform.blog.endpointhttps://api.example.com/v1博客平台 API 地址platform.blog.token${BLOG_TOKEN}通过环境变量注入不要写死platform.default-category-id10默认分类 IDcontent.default-authoradmin默认作者schedule.cron0 30 9 * * MON定时发帖表达式示例为每周一 9:30retry.max-attempts3最大重试次数retry.backoff-base-seconds5指数退避基础时间mq.redis-key-prefixmcp:idempotency幂等键缓存前缀敏感配置我全部通过环境变量注入项目里只保留 placeholder。这个习惯帮我避免了至少一次“代码推上去发现 Token 泄露”的事故。4. 从零跑通整个自动发帖流程4.1 先用 MCP Inspector 调试工具我建议任何刚开始接触 MCP 的人都先花半小时熟悉 MCP Inspector它相当于 MCP 开发里的 Postman。通过它可以查看 Server 暴露了哪些工具、工具的参数 Schema 是什么样的还可以直接发起一次调用并查看原始请求响应。启动 Server 后用命令行工具连接mcp-inspector --server-url http://localhost:8080/mcp连接成功后Inspector 会执行一次tools/list然后在界面上列出publish_post、save_draft这些工具。点进publish_post能看到每个字段的类型和描述这个视图对接口交付给团队同事时特别有用。我建议在正式集成之前先用 Inspector 把每个工具的正常路径和异常路径都测一遍。我这里说的异常路径包括传空标题、传非法分类、传格式错误的 Markdown。提前把这些场景跑通后面接 AI 客户端时会省很多事。4.2 Markdown 内容规范化怎么做自动发帖里的 Markdown 规范化不是做个简单的字符串替换就完事。我实际处理过的问题就有不少。首先是 Front Matter 解析。我的文章在本地仓库里带 YAML 头里面写着标题、日期、标签、封面图字段。MCP Server 收到文件后先解析 Front Matter把它转成工具入参再决定哪些字段需要保留到正文里。为了兼容不同写法我写了一个轻量解析器支持---分隔和json分隔两种格式。然后是图片路径处理。本地文章的图片路径往往是相对路径比如images/foo.png直接发到线上肯定挂。我的处理流程是扫描 Markdown 中的图片语法匹配相对路径把图片对象存储上传到对象存储然后把 Markdown 里的路径替换成 CDN 绝对地址。这一步放到 MCP 工具内部的好处是调用方不用自己管图片上传。还有 HTML 与 Markdown 混排的兼容问题。有些平台对 Markdown 里的原始 HTML 支持不好尤其是表格和自定义容器。我平时写文章又喜欢混排 HTML所以规范化模块里加了一个选项html_strategykeep或html_strategystrip。默认用keep如果发现目标平台渲染有问题再改成strip把自定义 HTML 块转成纯文本或代码块。4.3 接入定时调度让文章到点自动发对自动发帖来说定时调度是“最后一公里”。我用 Spring 自带的Scheduled就能满足需求没有引额外的分布式调度中间件。Component public class PublishScheduler { private final McpClient mcpClient; Scheduled(cron 0 30 9 * * MON, zone Asia/Shanghai) public void publishWeeklyPost() { PublishRequest request loadLatestMarkdownFromRepo(); CallToolResult result mcpClient.callTool(publish_post, request.asJsonMap()); if (!result.isSuccess()) { alertService.send(文章发布失败, result.getMessage()); } } }这里的McpClient指向的其实就是一个运行中的 MCP Server 地址。调度任务本身不关心发布逻辑是怎么实现的它只需要把文件路径作为参数传给publish_post工具。这个设计让“内容生成”和“内容发布”彻底解耦我甚至可以临时把调度任务指向部署在另一台机器上的发布服务。定时任务里我最想提醒的是时区问题。cron表达式的zone必须显式配置否则会跟着服务器默认时区走。我曾经因为没配zone一次计划 9 点发布的任务在下午 5 点才执行整个人差点原地崩溃。4.4 日志、重试和告警要一次性做好发布是有副作用的操作日志和告警必须比普通接口更完整。我给每次调用生成了一个publish_trace_id从 MCP 请求进来开始贯穿到平台 API 调用结束。日志里必然包含这个 Trace ID方便用日志查询工具快速串联整条链路。重试策略我使用的是指数退避加抖动。第一次重试等待 5 秒第二次 10 秒第三次 20 秒每次加一个 0 到 2 秒的随机抖动防止多个任务同时重试时造成流量尖峰。重试范围只覆盖网络异常和平台返回 5xx 的场景业务异常和参数校验错误不重试因为重试多少次都不会成功只会浪费时间。告警我接的是 Webhook。凡是三条重试都失败的任务会有两条告警路径一条发给内容负责人告诉他“文章没有发出去”另一条发给值班研发告诉他“MCP Server 调用平台 API 连续失败”。这里我特意区分了告警人和告警内容避免把研发告警直接发给编辑造成不必要的打扰。5. 我踩过的坑和排查思路5.1 MCP Client 一直连不上 Server有段时间我把 MCP Server 部署成 HTTP 模式本地用stdio模式调试得好好的但远程 Client 总是报连接失败。后来排查发现是 Server 端没有正确配置 CORS 和 Session 管理MCP 协议在 HTTP 模式下会在初始化阶段做一次能力协商如果会话没有正确建立后面所有tools/call请求都会被拒绝。如果你的客户端报“MCP session not initialized”之类的错误先不要怀疑参数写法先把关注点放在 Server 端会话存储上。Spring Boot 集成下要注意servletsession 的开启情况并把日志级别调到 DEBUG 看握手阶段交换的消息。5.2 平台认证总是莫名其妙失败认证失败是自动发帖里最让人恼火的问题。有一类坑特别隐蔽平台 API 返回的 Token 过期时间比文档写的早或者在“调用前验证 Token 有效”和“真正调用时验证”之间 Token 恰好过期。我在工具方法里加了“平台时间偏移量”的计算因为客户端时间如果和平台服务器时间偏差较大基于时间签名的认证很容易失败。另外多平台适配器的 Token 刷新逻辑不能复用同一个定时器。不同平台的有效期差异很大我把 Token 的管理下沉到每个 Adapter 内部并且加了一个“请求前尝试刷新”的钩子Token 剩余有效期小于 10 分钟就先刷新再发请求。5.3 发布后格式错乱代码块全被拆散有一次发出去的博客页面代码块里的缩进全丢了前端展示完全没法看。排查过程发现问题不是 Markdown 转 HTML 的锅而是平台 API 接受 JSON 格式的正文时对\r\n和\n的处理不一致。我在规范化模块里加了统一换行处理所有换行统一成\n代码块内部保留原始缩进但代码块外的多余空行压缩为一个。这个方法看起来很简单但它解决了我们平台上“单行代码显示成一行长文本”的经典问题。如果你发布的正文里有大量代码发布前一定先跑一次离线渲染测试。5.4 重复发帖问题我最早在同步模式下遇到过重复发帖现象是平台上有两篇一模一样的文章创建时间差几秒。原因就是 MCP Client 端超时了调用方以为请求失败自动重试了一次而第一次请求其实还在平台侧处理中。解决方式就是我前面说的三层幂等。这里想补充一个细节幂等键不要只在内存里放要用 Redis 或数据库因为进程一重启内存里的幂等记录就没了。我试用内存方案的那一周恰好赶上服务重启就出现了漏网之鱼。5.5 长连接资源泄漏MCP Server 跑了一段时间之后我发现连接数一直在涨GC 压力也变大。定位发现是 HTTP transport 模式下客户端没有正确关闭会话Server 端也没有做空闲连接回收。后来我在 Server 端配置了空闲会话超时时间并在 Client 增加了一套连接过期自动重建机制问题才彻底消失。这属于运行期才会暴露的问题本地测试很难发现。我建议所有做 MCP Server 的人在上线前一定要压测连接反复创建和销毁的场景。6. 运行一段时间后的经验总结项目跑到现在我个人最大的体会是自动发帖这件事难点从来不是“调个 API”而是如何设计一个能长期稳定运行的发布管道。API 调用只是最后一步前面那些内容解析、幂等控制、平台适配、失败告警才是真正让人省心的地方。如果你也打算做一个 JavaMCP 自动发帖的项目我会给出这几个很实际的经验。第一先小范围跑通再上量。我最初接了一个测试站点跑了两个星期才接正式站。自动发布一旦出问题影响的不只是接口报错还有线上内容的一致性和用户访问体验。第二发布审核不能省。即使流程全自动我也坚持保留一个“草稿状态”的出口。AI 生成的内容或者定时任务自动抓取的内容先落到草稿箱人工审核通过后由点击或 API 触发正式发布。这个折中方案既保留了自动化效率又给风险上了一道保险。第三可以用内容哈希做最后的兜底。无论前面的幂等设计有多好最终防线永远是“上一遍的内容不重复发”。把标题加正文的哈希存起来每次发布前查一遍这个小功能几乎零成本但在关键时刻能拦住最糟糕的结果。这个项目后续还可以继续扩展。比如接入更多平台 Adapter把发布能力开放给团队内部的知识库又比如做一个 MCP Client 网关让公司里的 AI 助手统一走这里创建草稿。方向很多但底层的骨架已经稳定住了后面都是增量工作。最后再说一个我长期保留的习惯发布是一件有副作用的事宁可少发一次、慢一点也绝不要在一个未经验证的流程上批量执行。自动化是为了节省精力不是为了放大风险。
返回列表