MCP发帖服务实战:从Spring Boot工具封装到AI Agent调用全记录
2026/9/15 2:36:01 网站建设 项目流程

运营那边每天最机械却离不开的动作,就是把AI生成的稿子复制到CMS里,填好标题,点发布。这一过程被同事吐槽为“人工RPA”。当MCP开始被拉出来讨论的时候,我第一反应很直接:这不就是把“系统操作交给AI”的标准管道么。于是内部立项时,我随手把这个试验项目起名为MCP发帖功能测试_20231015,后面的日期串是我们的版本快照命名习惯,不是发布日期。目标很朴素:把现有的发帖接口通过MCP包成一个AI能直接调用的工具,然后在真实客户端里端到端跑通一次。

这篇文章就是把整个从设计、实现、测试到踩坑的过程记录下来。它不像官方文档那样面面俱到,更偏向我们实操时的判断:为什么选择Spring Boot做底座、发帖功能为什么映射成“工具”而不是教条的“资源”、stdio和HTTP模式怎么选、测试时参数为什么会报错、以及agent skill和MCP到底怎么分工。正在折腾MCP服务接入的人,应该能从里面抄到不少直接能用的东西。

1. 发帖能力该不该走MCP:我的判断依据

1.1 在有普通API的情况下为什么要多套一层

先说一个常见质疑:发帖系统本身已经有HTTP接口,前端调用、后端调用都很成熟,为什么还要用MCP再包一层?

这个质疑放在“人直接调用”的场景里完全成立。但如果调用方是AI Agent,情况就不一样了。大模型本身不会主动去翻你的接口文档,它只能根据上下文里的工具描述来决定调用什么、传什么参数。过去我做Function Calling,得针对每个模型写一套工具注册逻辑,OpenAI一套、Claude一套,项目里还跑过几套国产模型,每个平台的参数格式和调用机制都有一点差异,维护起来相当头疼。

MCP相当于把“工具说明、参数Schema、调用入口、结果返回”这几件事统一成一个标准协议。你写一次服务端,只要是支持MCP的客户端都能直接认出来。Claude Code能用,Codex能用,Cursor能用,自研的智能体也能用。发帖这种高频操作,包装成MCP工具,远比在每个Agent平台上各写一遍适配器划算。

我在立项文档里写的那句话后来被同事反复引用:MCP解决的不是“调用问题”,而是“发现和接入成本问题”。普通API是给人看的,MCP是给模型看的。它把API的调用方式翻译成了模型最容易理解的“动作描述”。

1.2 MCP能帮“发帖”解决的三类真实问题

第一个是客户端多样性。同一个发帖服务,可能今天在Claude Code里用,明天在Codex里用,后天写进我们自己的对话机器人。没有MCP,每个端都要单独做工具注册;有MCP,服务端和客户端互相遵循一份协议,接入成本从“天”降到“小时”。

第二个是工具描述标准化。发帖动作需要哪些参数、哪些必填、哪些可选、返回值长什么样、出错时怎么告诉模型,这些在MCP的Tool定义里都能写清楚。模型不用猜,前端也不用写大段FAQ。

第三个是权限和审计的集中化。发帖是写操作,不能给模型一把梭的权限。走MCP之后,所有从AI侧来的发帖请求都会经过同一个工具入口,身份校验、IP限制、操作日志、配额控制都可以收敛在这一层。比每个Agent自己实现一套安全机制要靠谱。

这三个点落实到一个实际项目里,就是我下面要展开的服务设计与实现。如果你只是想在本地快速验证一下MCP到底行不行,也可以直接跳到最后两节,那部分的故障排查经验会更接近实际踩坑现场。

2. 设计发帖MCP服务时最关键的几个决定

2.1 选用Java与Spring Boot作为服务底座的原因

市面上MCP服务端有很多轻量选择,Python有官方SDK,Node也有。我们最后选了Java + Spring Boot,不是因为Java在MCP生态里有压倒性优势,而是基于三个现实条件:

一是团队主栈就是Java,发帖平台的核心服务是Spring Boot写的。把MCP服务直接放进主服务工程,或者单独起一个Spring Boot子服务,维护成本最低。团队不需要为了一个试验项目再引入一套Python技术栈。

二是Spring Boot在依赖注入、配置管理、线程池、监控这些方面已经有成熟方案,MCP服务端虽然只是薄薄一层,但接上真实的发帖平台之后,连接池、超时、异步补偿这些能力都是刚需。用Spring Boot,很多现成组件直接复用。

三是官方Java SDK已经比较稳定。社区里也有spring-ai-alibaba这样的整合项目,可以直接把REST接口声明成MCP工具,省掉手写协议解析的活。我这次是先手写了一个相对朴素的MCP Server来跑测试,主要想先把原理吃透,避免框架替我遮住太多细节。

如果手头只有一个凌晨需要跑通的场景,直接用spring-ai-alibaba或官方SDK里的Spring Boot Starter都行。但如果想理解MCP的调用链路,我建议还是先手写一遍,至少搞清楚三个问题:工具声明的Schema是怎么传给模型的、模型返回的调用请求长什么样、工具执行结果又是怎么传回去的。

2.2 “发帖”映射到Tools而非Resources或Prompts

MCP协议里有三个核心概念:Tools、Resources、Prompts。我第一次接触时,第一反应是发帖应该用Resources,因为帖子和文章属于“内容资源”。仔细看完规范之后,这个判断是错的。

Resources在MCP里更接近“只读知识源”,比如把某个系统的接口文档提供给模型,或者把当前用户信息给模型做上下文填充。发帖是个明显的写操作,有副作用,会修改平台状态,应该映射成Tools。Tools是让模型根据语义描述自主决定调用的动作,参数由模型生成,服务端执行后把结果返回给模型。

另外Prompts也不是发帖该用的东西。Prompts偏向模版化的提示词,比如“帮我生成一篇周报”这种流程引导,它不直接执行业务动作。如果我希望只暴露一个“从草稿箱发布文章”的操作,正确的做法就是声明一个名为create_postpublish_article的Tool,把参数表定义清楚,剩下的交给模型去理解。

这里有个容易忽略的细节:MCP里每个Tool的descriptioninputSchema会直接决定模型能不能正确调用。如果描述写得含糊,模型可能宁可让用户自己操作也不调用,或者把参数传得乱七八糟。所以描述要写“动作 + 对象 + 准备条件”三层,例如:

发布一篇新文章到内容平台。调用前需要拿到合法标题和正文内容;调用成功后返回文章ID和发布状态。如果题目或内容为空,不要调用此工具。

2.3 传输方式选择:stdio还是HTTP

MCP的传输方式,我这次实测下来主要分两类:stdio和HTTP(现阶段新版实际上推荐Streamable HTTP,老的SSE传输逐渐边缘化)。

stdio模式适合本地开发:客户端(比如Claude Code)直接拉起MCP服务进程,通过标准输入输出通信。配置最简单,不需要起端口,不需要考虑跨域和鉴权。缺点是服务生命周期跟着客户端走,客户端退出服务就停了,只能在单机使用。

HTTP模式适合部署成独立服务:客户端通过URL访问MCP服务,服务可以多客户端共用,也方便做权限控制、负载均衡和集中监控。缺点是配置和部署更复杂,需要处理鉴权、HTTPS、允许的来源等问题。

我们第一轮测试用的是stdio,因为目标非常单纯:验证MCP链路能不能通、模型会不会正确调用工具、发帖返回结果能不能正确解析。第二轮才把同一套服务打成独立Java服务,用HTTP模式接入Codex和自研客户端。这样分阶段走,能快速定位问题到底出在协议层、工具描述层还是真实业务层。

3. 搭建可复用的MCP发帖服务

3.1 最小Java MCP服务端工程

我用的MCP Java SDK是io.modelcontextprotocol.sdk:mcp,在Maven里加上依赖就能开写。下面是最小工程的核心结构:

<!-- pom.xml 片段,版本号以你当前实际可用版本为准 --> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp</artifactId> <version>0.10.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>

如果是纯stdio模式,甚至不需要Spring Boot的Web依赖。我保留Web依赖是因为后续要切HTTP模式,干脆在第一步就放进去了。

服务端主类长这样:

@SpringBootApplication public class PostMcpServerApplication { public static void main(String[] args) { SpringApplication.run(PostMcpServerApplication.class, args); } @Bean public ToolCallbackRegistry toolCallbackRegistry(PostService postService) { ToolCallback toolCallback = new ToolCallback() { @Override public String getToolDefinition() { // 返回JSON格式的工具定义,包含name、description、inputSchema return "{\"name\":\"create_post\",\"description\":\"...\",\"inputSchema\":{...}}"; } @Override public JsonNode getToolParameters() { return objectMapper.readTree(getToolDefinition()).get("inputSchema"); } }; return new ToolCallbackRegistry(...); } }

这段代码我故意写成了偏“原理展示”的形式,而不是完整可运行的业务代码。真正的生产项目里,你可以直接用官方SDK提供的McpServerFeatures.SyncToolSpecification,会省很多事:

McpServerFeatures.SyncToolSpecification createPostTool = new McpServerFeatures.SyncToolSpecification( "create_post", "发布一篇新文章到内容平台;调用前需要拿到合法标题和正文内容。", postInputSchema(), request -> { Map<String, Object> args = request.arguments(); String title = String.valueOf(args.get("title")); String content = String.valueOf(args.get("content")); boolean publishNow = Boolean.parseBoolean( String.valueOf(args.getOrDefault("publishNow", "true")) ); // 这里调用真实的发帖平台服务 PostResult result = postService.publish(title, content, publishNow); return new McpSchema.CallToolResult( List.of(new McpSchema.TextContent( "postId=" + result.id() + ", status=" + result.status() )), false ); } );

按照这个方式,每新增一个发帖相关的操作就增加一个Tool:比如create_postupdate_postdelete_postget_post_status。每个Tool都要有独立的Schema,不要偷懒共用。

3.2 工具输入输出Schema怎么设计

工具Schema直接决定模型理解参数的程度。create_post的 inputSchema 我最终定成:

{ "type": "object", "properties": { "title": { "type": "string", "description": "文章标题,长度不能超过100个字符" }, "content": { "type": "string", "description": "文章正文,支持Markdown格式" }, "category": { "type": "string", "description": "文章分类,可选值:tech, life, product, other", "enum": ["tech", "life", "product", "other"] }, "publishNow": { "type": "boolean", "description": "是否立即发布,false表示保存为草稿", "default": true } }, "required": ["title", "content"] }

几个容易被忽视的点:

  • description写清楚长度限制和枚举值。模型看到“长度不能超过100个字符”,生成参数时就会自动裁剪;不写,你就要自己在服务端兜底。
  • enum字段能极大降低模型传参出错的概率。写死可选值,模型一般不会胡来。
  • default字段不是所有客户端都严格遵守,所以服务端依然要做默认值处理。我在上面代码里用getOrDefault就是这个原因。
  • 返回值不要返回整个复杂的对象,而是返回一段简洁的文本。模型更容易理解,也方便后续追问。我返回的postId=123, status=published就够用了。

3.3 客户端侧的接入与验证方法

服务端写完,客户端接入才是重头。我用Claude Code做第一轮测试,在项目根目录维护了一个MCP配置文件,内容大致如下:

{ "mcpServers": { "post-mcp": { "command": "java", "args": ["-jar", "target/post-mcp-server.jar"], "env": { "CMS_BASE_URL": "http://127.0.0.1:8080", "CMS_API_TOKEN": "test-token-123" } } } }

注意,env里的CMS_API_TOKEN不要硬编码在配置里,更不要提交到代码仓库。正式环境建议从环境变量读取,或者在客户端侧使用密钥管理服务。

配置完成后,执行类似下面的命令来验证:

claude -p "请调用发帖工具,发布一篇文章:标题是《MCP测试文章》,正文是'这是一次MCP联调测试'。"

在终端里能看到模型自主完成了“识别工具 → 生成参数 → 调用工具 → 返回结果”的完整链路。如果配置有问题,通常会先报No MCP servers foundTool not found这一类错误。这些我在下一节细说。

4. 一次完整发帖测试的实证过程

4.1 测试链路与测试场景

这次测试,我按真实使用链路拆成了三段:

  • 启动层:MCP服务能不能被客户端正确拉起、配置路径有没有问题。
  • 协议层:工具定义能不能被客户端正确发现,模型能不能看到这个工具。
  • 业务层:模型生成的参数是否正确、服务端执行业务是否成功、结果是否返回给模型。

测试场景我列了一张表,按优先级从小到大:

场景操作预期结果
发现工具客户端列出可用工具出现 create_post,且描述了标题、正文、分类等参数
正常发帖让模型发布一篇完整文章返回 postId,平台出现新文章
缺失必填参数只给一段正文,不给标题模型在调用前要求补充,或服务端返回参数校验错误
草稿模式指定 publishNow=false文章保存为草稿,不对外发布
幂等重试同样的指令连发两次两次调用返回不同的postId,符合预期;但如果是重放同一请求,要考虑幂等键

第一轮测试最顺利的是“发现工具”。Claude Code启动后加载MCP服务,很快就列出了create_post和另外两个辅助工具。我心里踏实了不少,至少协议链路是通的。

到了“正常发帖”就翻车了。

4.2 第一次翻车:模型传的字段名对不上

我让模型发一篇标题为“MCP测试”的文章,结果服务端报错:field 'title' is missing。打开日志我发现,模型确实调用工具了,但传入的参数是:

{ "subject": "MCP测试", "body": "这是一次MCP联调测试" }

它把title传成了subject,把content传成了body。这两个字段名它从哪来的?大概率是从我的业务背景描述里抓的关键词,或者是它自己对“发文章”这件事的默认理解。

这个坑很典型:工具Schema的字段名和描述如果不和模型语言习惯对齐,模型就会按自己的一套来。我当时给工具起名create_post,却把标题字段命名成title,这本身没错,但我在加载MCP之前给对话的上下文里频繁提到了“文章的subject和body”,模型就产生了混淆。

修复方法有两个方向:一是把上下文里那些不准确的词改掉,保持口径一致;二是在Schema的description里加一句“必填参数标题对应字段名为title,正文对应content”。我两个都做了。而且从那以后,给MCP工具写Schema时,凡是和模型高频打交道的字段,我都会在描述里明确写出字段名,避免产生二义性。

4.3 第二次翻车:无权限发帖

链路跑通之后,我换了更贴近现实的场景:让模型从草稿箱里找一篇昨天写的稿子并发布。这个场景本意是测试Resources和Tools的配合,结果直接暴露了权限问题。

因为MCP服务端用的CMS_API_TOKEN是测试号,只有查看草稿的权限,没有发布权限。模型成功调用了get_draft,接着自信地调用publish_post,然后服务端返回403 Forbidden

这个错误本身不复杂,但它提醒了我在真实项目里必须懂得局部放权。发帖权限很敏感,建议不要把一个拥有全部CMS权限的token明文传给MCP服务。正确做法是:MCP服务端只申请调用发帖接口的最小权限,没有列表权限和删除权限。同时加一层调用者身份,避免任何一个拿到客户端的人都能让AI发任意内容。

4.4 如何判定测试真正通过

发帖测试不是“调用返回成功”就算完。我定了三条硬标准:

  • 内容平台里能查到这篇文章,状态和返回结果一致。
  • 从模型发起请求到最终平台落库,全程有操作日志,且能关联到模型对话ID和工具调用ID。
  • 非法参数和越权请求会被服务端拦截,并且返回给模型的错误信息是“可理解”的,而不是裸抛一个空指针异常。

按这个标准测完,我才敢说MCP发帖这个功能基本可用。否则只能叫“链路通”,不能叫“功能过”。

5. 从这次测试里沉淀下来的经验

5.1 Agent Skill、MCP和普通函数调用到底怎样分工

准备这个项目之前,我也被“agent skill和MCP有哪些区别”这个话题绕了很久。我现在的理解很收敛:

  • MCP是给Agent提供外部操作通道的协议。它让Agent能调用一个真实世界的动作或数据接口,是“接线管道”。
  • Agent Skill是一种提示词和工作流的封装。它教Agent“遇到什么情况,按什么步骤处理”,本质是方法论和模板,不是系统连接能力。
  • 普通函数调用是硬编码在单个代码库里的内部方法。它不跨系统,只在本进程里执行。

放到“发帖”场景:如果你需要让Agent执行真正的发帖动作,最好用MCP,因为MCP和平台服务是解耦的;如果你想告诉Agent“发帖之前要先去查一下这篇内容合规不合规,不合规就退回编辑”,这是一个Skill,用来编排流程,但它最终落地的发帖操作还是得调用工具。

MCP强调的是连接,Skill强调的是决策路径。两者不冲突,甚至可以嵌套使用:Skill里编排流程,涉及真实动作时就调用MCP暴露出来的Tool。

5.2 写操作类MCP工具必须考虑的安全与回归问题

发帖是典型的写操作,而且是对外可见的写操作。这类工具和只读工具的安全模型完全不同。

我这次踩了两个坑,一个是权限过大,一个是幂等缺失。权限过大的问题前面提到了。幂等缺失是这样的:客户端在高延迟场景下会自动重试,如果我第一次调用实际上已经成功发帖,但返回结果因为网络原因丢失,客户端就会再发一次,结果一篇文章发了两遍。真实场景里这种情况非常危险。

解决思路是加一个幂等键参数。在工具体系里加一个idempotencyKey字段,客户端每次会话生成一个唯一值,服务端记录这个键,如果重复收到相同键的请求就直接返回上一次的结果。这样能避免重复发帖。虽然在实际工作中,不是所有AI客户端都会配合传这个参数,但服务端至少要有这种兜底设计。

还要考虑的是错误信息过滤。MCP服务端返回给模型的错误信息不能直接透传平台内部堆栈,不然很容易把数据库结构、内部服务名这些敏感信息暴露给模型,模型再转述给用户,就变成信息泄露。我后来在服务端把所有异常都包装成统一风格:调用发帖服务失败,错误码:1001,具体错误详情只打到日志里。

5.3 从发帖测试延伸到后续还能扩展的方向

这次测试用的服务虽然叫“发帖MCP”,但底层模式完全可以复用。后面我们又接了三个类似能力:批量导入文章、生成文章摘要、定时发布。每一个都按同一个套路来做:

  • 先定义工具名和清晰的中文描述。
  • 再设计入参Schema,枚举值和默认值尽量写细。
  • 然后接业务服务和权限校验。
  • 最后用真实客户端跑一遍端到端测试。

关于Spring Boot这些技术选型,我也建议维护一套内部基础镜像或脚手架,把日志、鉴权、幂等、重试这些横切能力都沉淀进去。不然新接一个工具,每个人都要把安全设计和日志排查重新写一遍。

另一个值得关注的方向是MCP的HTTP模式。本地stdio跑通之后,我们很快就把同一套发帖服务部署成了远程HTTP服务,这样各个开发机和CI环境都能通过统一地址访问。远程模式下的鉴权、限流、调用审计,比本地stdio复杂一些,但只要第一步服务端和客户端都跑通了,后面只是部署问题,难度不大。

这次测试最大的价值,不是证明“MCP能发帖”,而是让我摸清了MCP从服务端到客户端、从协议到业务的一套完整落地路径。下次不管接什么系统,心里都有底了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询