一条龙开发指南:MCP AI Agent 理论+项目实战开发你的MCP Server

前言

halo,我是不易, 继ChatGPT发布已经过去了快三年了, 随着 AI 人工智能的不断发展给我们开发人员甚至UI设计人员带来了巨大的变化.

24年横空出世Claude 3.5 模型给我震撼到了, 力压 ChatGPT

我的小程序以及UI设计差不多都是它完成, 这个团队的强大让我不得不去关注.

24年10月2日,Anthropic 发布 MCP 协议,旨在为 AI 模型提供一个开放、通用且有共识的协议标准。

它允许开发者将 AI 模型连接到各种数据源和工具,从而实现更强大的 AI 应用。

你可以把 MCP 想象成 AI 世界的“USB-C 接口”——就像 USB-C 让各种设备轻松互联,MCP 也为 AI 模型连接不同数据源和工具提供了标准化方式。

这又是AI 再次迎来新突破, 新的概念, 新的协议, 新的生态.

那时 MCP 还没现在这么火,也没有这么多 MCP Server, 大部分的MCP Server 都是基于Nodejs 开发, 没有Java 的生态, 随着 Spring 的生态支持,Java 也迎来了“春天”——只需简单配置,就能实现自己的 MCP Server。

对于刚接触的朋友来说,MCP 和 MCP Server 可能有些陌生。别担心,接下来我们会一步步探索它们的玩法:从认识概念,到简单操作,再到实战开发。流程图如下:

⚠️ 本文纯手敲, 涵盖了理论知识+实战知识, 如果您觉得帮助到了您,请给我点个👍
如果本文有错别字,或者有不对的地方请指出谢谢啦!
🛫 本期内容涉及较多, 本次的理论知识较多,如果你已经掌握了理论那么可以直接前往项目实战开发
MCP Server 开发技术选型:
Java + Spring Ai 来完成我们的腾讯云开发者社区 MCP Server
另外接入支付宝 MCP 来完成支付场景, 带着大家结合起来情景演绎, AI 助手让AI调用多个MCP Server
比如有人找我写论文我 AI 生成完毕需要让他交钱我们就可以生成链接让他去支付
最后会统一将这些 MCP 串联起来形成 MCP Agent 变成一个 AI 小助手
如下图的结合

什么是 MCP? 为什么一定要用 MCP?

  • MCP 是一个标准协议,就像给 AI 大模型装了一个 “万能接口”,让 AI 模型能够与不同的数据源和工具进行无缝交互。它就像 USB-C 接口一样,提供了一种标准化的方法,将 AI 模型连接到各种数据源和工具。
  • MCP 旨在替换碎片化的 Agent 代码集成,从而使 AI 系统更可靠,更有效。通过建立通用标准,服务商可以基于协议来推出它们自己服务的 AI 能力,从而支持开发者更快的构建更强大的 AI 应用。开发者也不需要重复造轮子,通过开源项目可以建立强大的 AI Agent 生态。
  • MCP 可以在不同的应用 / 服务之间保持上下文,增强整体自主执行任务的能力。

下面图展现了 MCP 在实际应用中的整体架构和数据流转过程:

  1. MCP Client(如 Claude、IDE、工具等):这是你本地的“指挥中心”,负责发起各种 AI 能力请求。无论你用的是 AI 助手、开发工具还是自动化脚本,只要支持 MCP 协议,都可以作为 MCP Client。
  2. MCP 协议:它就像一根“万能数据线”,把 MCP Client 和不同的 MCP Server 连接起来。所有的请求和响应都通过 MCP 协议进行标准化传递,保证了兼容性和可扩展性。
  3. MCP Server A/B/C:每个 MCP Server 都可以对接不同的数据源或服务。例如:
    • MCP Server A 连接本地数据源 A(如本地数据库、文件等)
    • MCP Server B 连接本地数据源 B
    • MCP Server C 通过 Web API 连接远程服务 C(如云端 API、第三方平台等)
  4. 数据流转:当 MCP Client 发起请求时,会根据需求选择合适的 MCP Server。Server 再去访问对应的数据源或服务,获取结果后通过 MCP 协议返回给 Client。
  5. 本地与远程资源整合:无论数据源在本地还是远程,MCP 都能实现统一调用和管理。比如你既可以让 AI 读取本地文件,也能让它调用云端服务,全部通过同一套 MCP 协议完成。

这下懂了吧, 是不是觉得很强大, 赋予一个 “USB” 就可以访问你的数据库, 并且进行让他 CRUD、操作本地文件等等.

MCP Stdio 协议 & JSON-RPC 传输格式

在前面的架构图中,我们已经看到 MCP 如何像"USB-C"一样,把本地和远程的各种数据源、工具统一接入到 AI 系统中。这种统一接入极大提升了开发效率和系统扩展性。

这就很像 function call 让模型可以按需调用预设函数,自动获取数据或执行等等操作, 确实带来了便利,但也引入了新的问题:平台依赖强、API 不兼容,每次更换模型或平台都要重写集成代码,既费时又容易出错,还存在安全和交互上的局限。

MCP 正是为了解决这些"碎片化对接"的难题而生。它最大的特点就是"开箱即用"——你无需编写任何代码,只需通过简单的配置,就能直接接入和调用各种工具和服务。

接下来,我们重点介绍 MCP 的接入协议

MCP 支持很多协议: Stdio/SSE/Str/eamable HTTP/同进程调用, 其余的本文内容较多将另外单开一个专题详细讲解

我这里就重点介绍 MCP 协议之一:Stdio

标准输入/输出(Stdio)

Stdio 协议通过标准输入和输出流实现通信,非常适合本地集成和命令行工具开发。常见的使用场景包括:

  • 构建命令行工具
  • 实现本地系统集成
  • 简单的进程间通信
  • 使用 shell 脚本自动化

传输格式与配置示例

MCP 的配置也非常直观。你只需在 mcpServers 中定义好需要接入的 MCP Server(比如 mcp-gitee、支付宝mcp、mysql mcp 等),然后通过 MCP 客户端统一调用即可

例如下面的配置 JSON-RPC 传输格式 :

{
  "mcpServers": {
    "gitee": {
      "command": "npx",
      "args": [
        "-y",
        "@gitee/mcp-gitee@latest"
      ],
      "env": {
        "GITEE_API_BASE": "https://gitee.com/api/v5",
        "GITEE_ACCESS_TOKEN": "<your personal access token>"
      }
    }
  }
}

在任何支持MCP协议的客户端使用, 比如 Claude Desktop 、腾讯云代码助手、Cursor 等等

通过这种方式,无论你需要接入什么工具或服务,只需简单配置即可,无需关心底层实现细节,极大提升了开发效率和系统可维护性。

实操 MCP

那么接下来我们来看看如何使用, 以 腾讯云代码助手为例(非常好用强烈推荐), 接入 mcp-gitee 实现创建仓库、查询仓库、提交代码、推送代码等操作

前往 GITEE 仓库搜索 mcp-gitee 地址: https://gitee.com/oschina/mcp-gitee

往下滑可以看到 npx 启动方式 这个方式就需要你安装 Node.js 还有一种就是可执行文件启动这个就是要你自己去源码构建了, 我这边就直接使用 Node.js 方式集成, 如果你还没安装 Node.js 那么打开腾讯云 ai 对话问它如何安装即可非常全: https://copilot.tencent.com/chat/

{
  "mcpServers": {
    "gitee": {
      "command": "npx",
      "args": [
        "-y",
        "@gitee/mcp-gitee@latest"
      ],
      "env": {
        "GITEE_API_BASE": "https://gitee.com/api/v5",
        "GITEE_ACCESS_TOKEN": "<your personal access token>"
      }
    }
  }
}

继续往下滑可以看到这个 mcp server 支持的功能

有很多功能工具, 也有环境变量、也有命令行配置信息

我就列举几个教大家如何简单使用, 其他的同学们感兴趣就多玩玩

可用工具

服务器提供了各种与 Gitee 交互的工具:

工具类别描述
list_user_repos仓库列出用户授权的仓库
get_file_content仓库获取仓库中文件的内容
create_user_repo仓库创建用户仓库
create_org_repo仓库创建组织仓库
create_enter_repo仓库创建企业仓库
fork_repository仓库Fork 仓库
create_release仓库为仓库创建发行版
list_releases仓库列出仓库发行版
search_open_source_repositories仓库搜索开源仓库
list_repo_pullsPull Request列出仓库中的拉取请求
merge_pullPull Request合并拉取请求
create_pullPull Request创建拉取请求
update_pullPull Request更新拉取请求
get_pull_detailPull Request获取拉取请求的详细信息
comment_pullPull Request评论拉取请求
list_pull_commentsPull Request列出拉取请求的所有评论
create_issueIssue创建 Issue
update_issueIssue更新 Issue
get_repo_issue_detailIssue获取仓库 Issue 的详细信息
list_repo_issuesIssue列出仓库 Issue
comment_issueIssue评论 Issue
list_issue_commentsIssue列出 Issue 的评论
get_user_info用户获取当前认证用户信息
search_users用户搜索用户
list_user_notifications通知列出用户通知

获取 MCP-GITEE 配置信息

在上面可以看到 mcp server 配置当中需要填入 GITEE_ACCESS_TOKEN 点击下面链接前往获取

填写好你的令牌名称、过期时间

⚠️ 令牌可千万别泄漏了, 别人是可以拿这个访问可以访问的操作权限.

使用 MCP-GITEE

前往下载腾讯云代码助手 https://copilot.tencent.com/#install

我就使用 Cursor 来下载腾讯云代码助手进行使用, 我这里已经下载过了, 同学们没下载的速度下载.

在侧边栏点击助手图标就可以使用了, 点击到 Craft 模式它支持 MCP 服务的使用

点击小花花图标, 可以看到 MCP 市场 我们搜索 MCP-GITEE 搜索不到, 因为还没收录进来

我们就手动添加即可, 复制上面的 mcp-gitee server json

点击已经安装, 配置 MCP Server , 会弹出编辑 mcp 配置文件

复制进去之后,在配置好 gitee 的令牌 token 即可看到旁边的 gitee 显示出这么多的工具

⚠️ 注意: 复制上面的npx命令,如果你是windows用户,需要做如下的修改:command修改为cmd ,下面的args数组增加 “/c"和 “npx”,注意顺序不要乱undefined"服务”: {
“command”: “cmd”,
“args”:
“/c”,
“npx”,
“-y”,
“@modelcontextprotocol/xxxxxxxxxxx”
undefined }

尝鲜 MCP Server 接入客户端使用

继续点击旁边的播放图标, 让 CodeBuddy 进行测试它是否可以正常使用

那么可以看到已经是输出了, 并且调用了 查询工具 去查询 gitee 上面叫 ruanyf 的技术博主, 接下来我们来搜索自己

可以看到也是可以正常的搜索出来的, 看到返回的信息没什么问题都是正确的

那么继续完成前面说的的任务吧

  • 获取当前登录用户的信息, 说实话很快嗷, 眼睛没眨眼就输出出来了, 腾讯云助手这么快吗

  • 创建用户仓库
  • 帮我创建一个仓库名称为: mcp-study 为公开仓库, 默认生成readme.md 里面的内容是 从零玩转MCP

不得不夸一下腾讯云部署的模型了,这么快的吗? 我用别的模型都要等 30 秒

可以看到调用了三个 mco 工具 , 但是仓库不默认给我公开, 可能是安全原因吧, 下面也提示我要我主动去设置一下.

1、创建仓库

2、获取仓库文件修改仓库文件为 我们指定的内容

3、创建了 issue

来一一认证

可以看到成功创建仓库 并且创建了 readme 文件 内容也更新了 还创建了 issue 来提醒你干什么操作了, 可以看到 MCP Server 的强大、便捷了, 只需要配置一下 3 秒就完成

拥有 MCP 协议大模型可以做任何事情, 操作系统文件也不例外, 来我们继续玩玩

操作电脑文件 MCP Server

打开腾讯云开发者社区 MCP 广场 我们找找操作系统文件的 MCP Server

https://cloud.tencent.com/developer/mcp

直接 ctrl + f 搜索即可 可以看到有一个点进去

可以看到支持这些功能操作, 具体的我们看都不用看 都理解了 它是干嘛的,直接配置, 下滑找到 npx

CodeBuddy 集成文件系统 MCP Server

  "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/yangbuyi/Desktop",
        "/Users/yangbuyi/Desktop"
      ]
    },

⚠️ 注意: 复制上面的npx命令,如果你是windows用户,需要做如下的修改:command修改为cmd ,下面的args数组增加 “/c"和 “npx”,注意顺序不要乱undefined"服务”: { “command”: “cmd”,
“args”:
“/c”,
“npx”,
“-y”,
“@modelcontextprotocol/xxxxxxxxxxx”
undefined }

和前面一样点击播放图标让 CodeBuddy 进行测试这个 MCP Server

可以看到上面列举了这么多功能操作, 我这边就简单演示几个感兴趣的同学可以多研究玩玩

MCP Server - 操作电脑文件

  • 帮我在桌面创建 从零玩转 MCP 的文件 .txt 格式 内容帮我填入 腾讯云开发者社区杨不易呀

默认像是这种存在操作系统相关的会进行询问一下,你可以点击‘每次询问’ 换成自动, 这里呢我们直接修改即可

然后继续询问已经创建成功了, 是否应用这个操作? 我们直接接受即可

可以看到桌面已经创建好了,并且内容也写入进来了

到这里是不是已经充分认识到 MCP Server 的作用了和强大了呢?

接下来我带着大家手把手使用 Java 语言制作自己的 MCP Server

项目实战 - 自定义 MCP Server

在上面我们已经简单玩了下 MCP Server , 接下来我带着大家动手做一个 MCP Server

我们要做的是腾讯云文章发布 MCP Server , 这个功能的目的是指可以将文章发布各个平台, 但是目前只是集成了腾讯云开发者社区后续会慢慢的集成其他平台

其实这个功能因为之前就想做 无奈 JS 逆向技术有限, 无法突破旧版本的文章发布里面的 content 加密 (有大佬感兴趣可以去试试看,提醒一下应该有 2 层加密其中有 base64), 然而腾讯云发布了新版的文章编辑器嘿嘿我进去一看没加密, 至此腾讯云文章发布 MCP Server 诞生了, 也是一个很好的锻炼开发 MCP Server 的机会(官方看到了不会把加密加上了吧) , 那么开始吧, 先看看流程图, 在来效果演示

效果演示

我们先来看一下效果图

可以看到生成了文章 并且发布到腾讯云开发者社区了 点进去看,可以看到已经发布成功

我还提前发布了一点, 测试文章发布稳定状态

开发前置工作

前置工作

  • 需要有腾讯云开发者社区帐号 - 后续需要拿 Cookie
  • 开发者工具去获取请求接口以及参数

接下来我们先来获取接口: https://cloud.tencent.com/developer/article/write-new

随便输入一点东西, 达到能够发布文章的校验, 然后打开 F12

分别勾选这两个 开启录制和保留请求接口日志

点击发送, 观察接口 addArticle 然后对接口进行右击获取请求 CUrl 格式

点击 以 cUrl 格式复制, 然后顺便打开你的 api 调试工具 这里我使用 apifox

点击导入, 复制进去即可, 就可以看到接口请求信息了

此时直接请求是不能请求的, 没有 cookie 也就是权限认证, 我们手动的去复制即可

回到刚刚的接口, 复制下面的 Cookie , 然后到 api 调试工具新增 请求头 key 为 cookie 参数就是这个复制过来

填写好后我们请求一下, 查看是否请求成功

可以看到返回了一个文章的 ID 和状态码 为零的 表示成功了, 我们看看社区创作中心看看是否存在

可以看到, 成功请求成功了, 那么就先删除这几个测试文章, 接下来创建项目开发服务

实战 MCP Server

创建 tencent-add-article-mcp-server 项目

然后接着一直下一步就行, 无需配置任何东西

使用CodeBuddy开发发布文章功能

前面我们已经将腾讯云社区发布文章到API复制到apifox, 接下来我带着大家用腾讯云发布的产品

CodeBuddy 来为我编写发布文章接口, 我这里录制了一个演示视频让 CodeBuddy 帮我写

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下面是生成出来的代码, 不想用代码伙伴的话(强烈建议用一下很舒服)

发布文章代码结构

出入参实体类

package com.yby6.mcp.server.tencent.api.dto;

import lombok.Data;

import java.util.List;
import java.util.Map;

/**
 * 腾讯云开发者社区发布文章请求DTO
 *
 * 该DTO类用于封装发布文章到腾讯云开发者社区时所需的所有参数。
 * 包含了文章的基本信息、分类信息、标签信息、展示设置等多个维度的数据。
 * 使用Lombok的@Data注解自动生成getter、setter等方法。
 *
 * 主要功能:
 * 1. 封装文章发布所需的所有参数
 * 2. 支持Markdown到ProseMirror格式的自动转换
 * 3. 提供文章分类、标签、专栏等管理功能
 *
 * @author yby6
 * @version 1.0.0
 */
@Data
public class AddArticleRequest {
    /**
     * 文章标题
     * 必填字段,用于显示文章的主标题
     */
    private String title;

    /**
     * 文章内容(JSON格式)
     * 包含富文本编辑器的完整内容结构
     * 使用ProseMirror格式存储,支持复杂的富文本编辑功能
     */
    private String content;

    /**
     * 文章纯文本内容
     * 用于SEO和摘要展示
     * 当content为空时,会自动将plain转换为ProseMirror格式
     */
    private String plain;

    /**
     * 文章来源类型
     * 1: 原创
     * 用于标识文章的创作类型
     */
    private Integer sourceType;

    /**
     * 文章分类ID列表
     * 用于文章分类管理
     * 支持多分类,每个分类对应一个ID
     */
    private List<Integer> classifyIds;

    /**
     * 文章标签ID列表
     * 用于文章标签管理
     * 支持多标签,每个标签对应一个ID
     */
    private List<Integer> tagIds;

    /**
     * 长尾标签列表
     * 用于SEO优化
     * 支持自定义长尾关键词,提升文章搜索可见性
     */
    private List<String> longtailTag;

    /**
     * 专栏ID列表
     * 用于文章专栏管理
     * 支持将文章添加到多个专栏中
     */
    private List<Integer> columnIds;

    /**
     * 是否开启评论
     * 1: 开启
     * 0: 关闭
     * 控制文章是否允许读者评论
     */
    private Integer openComment;

    /**
     * 是否关闭文本链接
     * 1: 关闭
     * 0: 开启
     * 控制文章中的文本链接是否可点击
     */
    private Integer closeTextLink;

    /**
     * 用户摘要
     * 用于文章预览展示
     * 提供文章内容的简短描述
     */
    private String userSummary;

    /**
     * 文章封面图片URL
     * 用于文章列表和详情页的展示
     * 支持自定义封面图片
     */
    private String pic;

    /**
     * 来源详情
     * 用于记录文章来源的详细信息
     * 包含来源类型、URL等额外信息
     */
    private Map<String, Object> sourceDetail;

    /**
     * 专区名称
     * 用于指定文章所属的专区
     * 支持文章分类到特定专区
     */
    private String zoneName;

    /**
     * 草稿ID
     * 如果是编辑已有草稿,需要提供此ID
     * 用于支持文章的草稿编辑功能
     */
    private Long draftId;
    
    /**
     * 获取ProseMirror格式的内容
     * <p>
     * 该方法用于获取文章内容的ProseMirror格式。
     * 如果content为空,则自动将plain转换为ProseMirror格式。
     * 确保文章内容始终以正确的格式提供给编辑器。
     *
     * @return ProseMirror格式的内容
     */
    public String getContent() {
        return "";
    }
    
}
package com.yby6.mcp.server.tencent.api.dto;

import lombok.Data;

/**
 * 腾讯云开发者社区发布文章响应DTO
 *
 * 该DTO类用于封装发布文章到腾讯云开发者社区后的返回结果。
 * 包含了文章发布的状态信息和文章的唯一标识符。
 * 使用Lombok的@Data注解自动生成getter、setter等方法。
 *
 * 主要功能:
 * 1. 提供文章发布结果的反馈
 * 2. 返回新创建文章的ID
 * 3. 指示发布操作的成功或失败状态
 *
 * @author yby6
 * @version 1.0.0
 */
@Data
public class AddArticleResponse {
    /**
     * 文章ID
     * 发布成功后返回的文章唯一标识
     * 用于后续对文章进行编辑、删除等操作
     * 在发布失败时可能为null
     */
    private Long articleId;

    /**
     * 发布状态
     * 0: 成功
     * 非0: 失败
     * 用于快速判断文章发布是否成功
     * 具体的错误码需要参考腾讯云开发者社区的API文档
     */
    private Integer status;
}

Retrofit2 RestFul 接口 api

package com.yby6.mcp.server.tencent.api;

import com.yby6.mcp.server.tencent.api.dto.AddArticleRequest;
import com.yby6.mcp.server.tencent.api.dto.AddArticleResponse;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.POST;

/**
 * 腾讯云开发者社区服务接口
 * <p>
 * 该接口定义了与腾讯云开发者社区API交互的方法。
 * 使用Retrofit框架实现HTTP请求,支持文章的发布等操作。
 * 所有请求都需要包含必要的认证信息和请求头。
 *
 * @author yby6
 * @version 1.0.0
 */
public interface ITencentService {
    
    /**
     * 发布文章到腾讯云开发者社区
     * <p>
     * 该方法通过HTTP POST请求将文章发布到腾讯云开发者社区。
     * 请求包含完整的HTTP头信息,模拟浏览器行为,确保请求能够被正确处理。
     * <p>
     * 请求头说明:
     * - accept: 指定接受的响应类型
     * - content-type: 指定请求体类型为JSON
     * - origin/referer: 指定请求来源
     * - user-agent: 指定客户端信息
     * - 其他安全相关头部
     *
     * @param cookie  用户认证Cookie,用于身份验证
     * @param request 文章发布请求,包含文章内容、标题等信息
     * @return 包含发布结果的响应对象
     */
    @POST("https://cloud.tencent.com/developer/api/article/addArticle")
    @Headers({
            "accept: application/json, text/plain, */*",
            "accept-language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
            "content-type: application/json",
            "origin: https://cloud.tencent.com",
            "priority: u=1, i",
            "referer: https://cloud.tencent.com/developer/article/write-new?draftId=213590",
            "sec-ch-ua: \"Google Chrome\";v=\"135\", \"Not-A.Brand\";v=\"8\", \"Chromium\";v=\"135\"",
            "sec-ch-ua-mobile: ?0",
            "sec-ch-ua-platform: \"Windows\"",
            "sec-fetch-dest: empty",
            "sec-fetch-mode: cors",
            "sec-fetch-site: same-origin",
            "sec-gpc: 1",
            "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
    })
    Call<AddArticleResponse> addArticle(
            @Header("Cookie") String cookie,
            @Body AddArticleRequest request
    );
}

Retrofit2 配置

    /**
     * 配置腾讯云开发者社区API服务
     *
     * @return ITencentService 腾讯云开发者社区API服务接口
     */
    @Bean
    public ITencentService tencentService() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://cloud.tencent.com")
                .addConverterFactory(JacksonConverterFactory.create())
                .build();

        return retrofit.create(ITencentService.class);
    }

单元测试

package com.yby6.mcp.server.tencent.test;

import com.yby6.mcp.server.tencent.api.ITencentService;
import com.yby6.mcp.server.tencent.api.dto.AddArticleRequest;
import com.yby6.mcp.server.tencent.api.dto.AddArticleResponse;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import retrofit2.Call;
import retrofit2.Response;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;

/**
 * 腾讯云开发者社区API测试类
 */
@Slf4j
@SpringBootTest
public class APITest {

    @Autowired
    private ITencentService tencentService;

    /**
     * 测试添加文章接口
     */
    @Test
    public void testAddArticle() {
        // 构建请求参数
        AddArticleRequest request = AddArticleRequest.builder()
                .title("测试文章标题")
                .content("{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"attrs\":{\"id\":\"test-id\"},\"content\":[{\"type\":\"text\",\"text\":\"测试内容\"}]}]}")
                .plain("测试内容")
                .sourceType(1)
                .classifyIds(Arrays.asList(2))
                .tagIds(Arrays.asList(18126))
                .longtailTag(Arrays.asList("mcp"))
                .columnIds(Arrays.asList(101806))
                .openComment(1)
                .closeTextLink(0)
                .userSummary("测试摘要")
                .pic("")
                .sourceDetail(new HashMap<>())
                .zoneName("")
                .draftId(0L)
                .build();

        // 这里需要替换为实际的Cookie
        String cookie = "qcommunity_identify_id=V6pvjxmW1JPuCEMovMUTz; qcloud_uid=VaIEoNdiKsUA; web_uid=ae70b69c-f87e-41c8-adfc-06efafb5678d; loginType=wx; lastLoginIdentity=51605f0971933755e7f8a53c030a98bc; qcommunity_session=8f9a94888dd80c7cba9f04103107b4821b78caacdbdc8a2ef693787a26905024; hy_user=UxVHgDrfj6lr9DCS; hy_token=v7nHUfmj1QeqIBGE0PdzJE3F2MFlLqxXGRlSMnkuFp6vk8Ndj5K0IapEp/t/E5rt; hy_source=web; language=zh; _ga=GA1.2.273799001.1746384291; qcstats_seo_keywords=%E5%93%81%E7%89%8C%E8%AF%8D-%E5%93%81%E7%89%8C%E8%AF%8D-%E8%85%BE%E8%AE%AF%E4%BA%91; _gcl_au=1.1.1073535231.1746384292; qcloud_from=qcloud.outside.yuque-1746547699994; trafficParams=***%24%3Btimestamp%3D1746547700395%3Bfrom_type%3Dserver%3Btrack%3D557760a9-1a5b-40a5-a2fb-7be7d1fe46d0%3B%24***; ewpUid=a3134f73-11f8-48ef-a865-7a91c897b948; qcmainCSRFToken=YGV3LRdgdrsF; qcloud_visitId=94096bdf035e23553f3d171dfa6cd886; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%22100005325524%22%2C%22first_id%22%3A%221969c9d82841f57-06185ef48d4f27-1a525636-2104200-1969c9d82852679%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTk2OWM5ZDgyODQxZjU3LTA2MTg1ZWY0OGQ0ZjI3LTFhNTI1NjM2LTIxMDQyMDAtMTk2OWM5ZDgyODUyNjc5IiwiJGlkZW50aXR5X2xvZ2luX2lkIjoiMTAwMDA1MzI1NTI0In0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%24identity_login_id%22%2C%22value%22%3A%22100005325524%22%7D%2C%22%24device_id%22%3A%221969c9d82841f57-06185ef48d4f27-1a525636-2104200-1969c9d82852679%22%7D; _gat=1; uin=o100005325524; nick=1692700664; intl=1";

        try {
            // 调用API
            Call<AddArticleResponse> call = tencentService.addArticle(cookie, request);
            Response<AddArticleResponse> response = call.execute();

            if (response.isSuccessful()) {
                AddArticleResponse result = response.body();
                log.info("添加文章成功,文章ID: {}, 状态: {}", result.getArticleId(), result.getStatus());
            } else {
                log.error("添加文章失败,错误码: {}, 错误信息: {}", response.code(), response.errorBody().string());
            }
        } catch (IOException e) {
            log.error("调用添加文章接口异常", e);
        }
    }
}

启动 APITest

测试成功发送文章

到此我们的功能就暂时开发完毕, 接下来我们开发我们的 MCP Server

使用 SpringAi

功能我们开发完毕了, 需要将它升级为 MCP Server 我们得引入 SpringAi 框架快速搭建 MCP Server, 去官方网站找找如何接入

点击前往官方文档: https://spring.io/projects/spring-ai#learn

可以看到现在官方给出了一个快照版本,和一个里程碑预发布版本,我们就使用相对稳定的里程碑版本 1.0.0-M6点击旁边的 Reference Doc. 前往版本文档

点击旁边的 Model Context Protocol (MCP) 再点击 MCP Server Boot Starter

来我们看文档来一步步操作,英文不好的自己翻译一下哈,比如我就不好

ok,官方给定了一个 maven 依赖,我们只需要集成就拥有了以下的操作,非常方便还得是“春天”

Spring AI MCP(模型上下文协议)服务器启动启动器提供了在 Spring Boot 应用程序中设置 MCP 服务器的自动配置功能。它支持 MCP 服务器功能与 Spring Boot 的自动配置系统无缝集成。

MCP 服务器启动器提供:

  • MCP 服务器组件的自动配置
  • 支持同步和异步操作模式
  • 多种传输层选项
  • 灵活的工具、资源和提示规范
  • 更改通知功能

当然了, 官方给了相关的示例案例,我们可以先去项目代码, 把相关的配置复制过来我们自己运行运行,在回过头来看各个参数的意思,这样子学习就更加的高效,有时候你光看都没有概念的.

这里我们选择带有 STDIO 传输的 Spring AI MCP 服务器启动器。

可以看到官方的案例,代码结构,就一个启动器一个 service 这个 service 就是 tool mcp 工具

我们把这些代码复制到我们的工程当中

复制 pom.xml 相关依赖配置

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <spring-ai.version>1.0.0-M6</spring-ai.version>
        <spring-boot.version>3.4.5</spring-boot.version>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <file.encoding>UTF-8</file.encoding>
    </properties>


    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.28</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

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

        <dependency>
            <groupId>com.squareup.retrofit2</groupId>
            <artifactId>adapter-rxjava2</artifactId>
            <version>2.9.0</version>
        </dependency>

        <dependency>
            <groupId>com.vladsch.flexmark</groupId>
            <artifactId>flexmark-all</artifactId>
            <version>0.64.8</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- CommonMark 核心库 -->
        <dependency>
            <groupId>org.commonmark</groupId>
            <artifactId>commonmark</artifactId>
            <version>0.21.0</version>
        </dependency>

        <!-- Jackson 用于 JSON 处理 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.2</version>
        </dependency>

    </dependencies>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

复制 WatherService

将案例当中的 mcp server 代码复制到我们工程当中

/**
 * WeatherService
 *
 * @author yangs
 * @date 2025/05/09
 */
@Service
public class WeatherService {
    @Tool(description = "Get weather forecast for a specific latitude/longitude")
    public String getWeatherForecastByLocation(
        double latitude,   // Latitude coordinate
        double longitude   // Longitude coordinate
    ) {
        // Implementation
        return "";
    }

    @Tool(description = "Get weather alerts for a US state")
    public String getAlerts(
        String state  // Two-letter US state code (e.g., CA, NY)
    ) {
        return "";
    }
}

可以看到 WatherService 的代码当中有一个 @Tool 标记, 这样做就是让大模型知道要调用的工具是哪个

我们点 Tool 注解进去观看源码, 拥有一下参数:

  1. 工具的名称。如果没有提供,将使用方法名称。
  2. 工具的描述。如果没有提供,将使用方法名称。
  3. 工具结果是应该直接返回还是传递回模型。
  4. 是否将结果转换为 String 的类

以上参数我们一般情况下只需要关心 name、description 这两个参数, 大模型根据这两个来判断调用

yml 程序配置

我们是 stdio 模式必须设置为 true、设置项目名称、版本、启动 banner 也要禁止开启

新增我们的项目配置信息

spring:
  application:
    name: tencent-send-article-mcp-server
  main:
    web-application-type: none
    banner-mode: off
  ai:
    mcp:
      server:
        stdio: true
        name: tencent-send-article-mcp-mcpService
        version: 1.0.0

注册 MCP 工具

自动配置会自动将工具回调注册为 MCP 工具。您可以有多个 bean 生成 ToolCallbacks。自动配置会将它们合并。

在启动程序下面添加工具回调注册为 MCP 工具,后续我们改成放在一个配置文件夹里面先暂时这样先把功能运行起来

把我们的 mcp weatherService 服务注册进来

    @Bean
    public ToolCallbackProvider weatherServiceTools(WeatherService weatherService) {
        return MethodToolCallbackProvider
                .builder()
                .toolObjects(weatherService)
                .build();
    }

ok ,恭喜你成功创建了自己的 MCP Server

没错就是这么的 easy 简单、和切菜一样, 那么接下来我们创建一个 MCP Client 客户端进行使用这个 MCP Server 官方也提供的单元测试( 你也可以用支持 MCP Client 的插件、软件 比如 CodeBuudy)

测试 McpServer

我们使用 stdio 传输,MCP 服务器由客户端自动启动

前提我们得要将我们的 MCP Server 先构建一下打个 jar 包让它去运行

可以用命令去运行打包

./mvnw clean install -DskipTests

或者 idea 去点两下看你自己

可以看到已经打包成功了, 我们就复制这个地址就行,也可以在 target 目录下面复制路径都可以的

然后我们修改 mcp client 代码让它去调用我们的 mcp server

如下图进行将我们的 jar 地址复制进去,这个就是上面我们操作 MCP 的 JSON 格式一样的

⚠️ 注意: 复制上面的npx命令,如果你是windows用户,需要做如下的修改:command修改为cmd ,下面的args数组增加 “/c"和 “npx”,注意顺序不要乱"服务”:
{ “command”: “cmd”,
“args”:
“/c”,
“npx”,
“-y”,
“@modelcontextprotocol/xxxxxxxxxxx”
}

可以看到我们成功调用了 MCP Server

打印了我们这个 MCP Server 可用的工具, 也分别调用了两个工具. 那么到这里你就会 mcp server 开发了对的没有错就是这么简单, 那么接下来我们升级前面写好的文章发布接口为 mcp tool

注意如果你报错了,如下:

这就是它内部操作日志的时候出问题了,得要配置一下, 官方案例也配置日志了的别掉以轻心

spring:
  application:
    name: tencent-send-article-mcp-server

  ai:
    mcp:
      server:
        name: ${spring.application.name}
        version: 1.0.0

  main:
    banner-mode: off
    web-application-type: none


logging:
  pattern:
    console:
  file:
    name: data/log/${spring.application.name}.log

server:
  servlet:
    encoding:
      charset: UTF-8
      force: true
      enabled: true

升级发布文章接口为 MCP Tool

在前面呢我们已经实现了文章发布的功能, 我们现在需要改成 MCP Tool 应该如何更改?

同学们🤔片刻, 前面我们写过了的哦

ok, 想必有同学已经知道了, 那么下面的图片代码我们就很熟悉了, 是前面写过的, 可以看到单元测试代码是发布文章的功能, 我们是不是可以直接 cv 到 tool 里面? 然后定义出入参 那么大模型就可以根据我们的出入参传递数据给到我们的 Tool

新增文章发布 MCPTool

在 mcpService 目录下创建 TencentArticleToolService

新增 saveArticle 方法

package com.yby6.mcp.server.tencent.mcpService.tools;


import com.yby6.mcp.server.tencent.api.ITencentService;
import com.yby6.mcp.server.tencent.api.dto.AddArticleRequest;
import com.yby6.mcp.server.tencent.api.dto.AddArticleResponse;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import retrofit2.Call;
import retrofit2.Response;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;

/**
 * 腾讯云开发者社区文章服务
 * <p>
 * 该服务类负责处理与腾讯云开发者社区文章相关的业务逻辑,
 * 包括文章的发布、更新等操作。作为领域服务层的一部分,
 * 它通过端口适配器模式与基础设施层进行交互。
 *
 * @author yby6
 * @version 1.0.0
 */
@Slf4j
@Service
public class TencentArticleToolsService {

    @Resource
    private ITencentService tencentService;

    public void saveArticle(String val) {
        AddArticleRequest request = AddArticleRequest.builder()
                .title("测试文章标题")
                .content("{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"attrs\":{\"id\":\"test-id\"},\"content\":[{\"type\":\"text\",\"text\":\"测试内容\"}]}]}")
                .plain("测试内容")
                .sourceType(1)
                .classifyIds(Arrays.asList(2))
                .tagIds(Arrays.asList(18126))
                .longtailTag(Arrays.asList("mcp"))
                .columnIds(Arrays.asList(101806))
                .openComment(1)
                .closeTextLink(0)
                .userSummary("测试摘要")
                .pic("")
                .sourceDetail(new HashMap<>())
                .zoneName("")
                .draftId(0L)
                .build();

        // 这里需要替换为实际的Cookie
        String cookie = "qcommunity_identify_id=V6pvjxmW1JPuCEMovMUTz; qcloud_uid=VaIEoNdiKsUA; web_uid=ae70b69c-f87e-41c8-adfc-06efafb5678d; loginType=wx; lastLoginIdentity=51";

        try {
            // 调用API
            Call<AddArticleResponse> call = tencentService.addArticle(cookie, request);
            Response<AddArticleResponse> response = call.execute();

            if (response.isSuccessful()) {
                AddArticleResponse result = response.body();
                log.info("添加文章成功,文章ID: {}, 状态: {}", result.getArticleId(), result.getStatus());
            } else {
                log.error("添加文章失败,错误码: {}, 错误信息: {}", response.code(), response.errorBody().string());
            }
        } catch (IOException e) {
            log.error("调用添加文章接口异常", e);
        }

    }
}

在前面我们已经学过 成为 Tool 就需要标记为 Tool 所以我们在需要变成 MCP Tool 的方法上面进行标记一下

@Tool(description = "发布文章到腾讯云开发者社区")

感兴趣的同学,这个时候可以继续测试一下, 通过 Client 进行调用这个 Tool, 我这里就不演示测试来, 跟着上面单元测试来操作即可

发布文章的出入参

观察我们的请求参数不难发现, 我们需要大模型传递的参数也就

title、content、plain、userSummary

  1. title 文章的标题
  2. content 文章内容
  3. plain 文章纯文本内容可以是 markdown
  4. userSummary文章摘要

其中 content 里面的文本和我们平常见到的不一样不是 markdown 也不是富文本的感觉, 这时候就又需要问到 CodeBuddy 这是什么结构的数据

可以看到 CodeBuddy 说这是 ProseMirror , 那么我们就需要将文本转换为这个格式, 我们一般写文章都是用 markdown 格式来进行创作, 那么我们就要让大模型给我们传递 markdown 格式的文本, 接着将这个文本转换为 ProseMirror 否则将会发送失败, 转换的工具我们继续让 Code Buddy 来进行编写

CoodeBuud 编写需要的转换工具

直接打开 CoodeBuddy 让它来编写我们需要的工具即可

帮我编写一个工具类,将markdown格式转换为ProseMirror格式

ok, 我这里呢直接生成完毕了, 代码已经贴入下面的链接当中, 直接点击链接下载, 因为这个类有点大

📎MarkdownToProseMirrorConverter.java

CodeBuddy 也帮我编写了单元测试, 可以去简单测试一下, 我已经测试过了, 都是 ok 的

package com.yby6.mcp.server.tencent;

import com.yby6.mcp.server.tencent.mcpService.utils.MarkdownToProseMirrorConverter;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * Markdown转ProseMirror格式转换器测试类
 * <p>
 * 该测试类用于验证MarkdownToProseMirrorConverter的功能是否正常。
 * 包含了多种Markdown语法的测试用例,确保转换结果符合ProseMirror格式要求。
 *
 * @author yby6
 * @version 1.0.0
 */
@SpringBootTest
public class MarkdownConverterTest {

    @Autowired
    private MarkdownToProseMirrorConverter converter;

    /**
     * 测试基本的Markdown转换功能
     * <p>
     * 验证各种Markdown语法元素是否能正确转换为ProseMirror格式
     */
    @Test
    public void testMarkdownConversion() {
        // 准备测试用的Markdown文本,包含多种Markdown语法
        String markdown = "# 标题1\n\n" +
                "这是一个**粗体**和*斜体*的测试。\n\n" +
                "## 标题2\n\n" +
                "- 无序列表项1\n" +
                "- 无序列表项2\n\n" +
                "1. 有序列表项1\n" +
                "2. 有序列表项2\n\n" +
                "> 这是一个引用\n\n" +
                "```java\n" +
                "// 这是一段Java代码\n" +
                "public class Test {\n" +
                "    public static void main(String[] args) {\n" +
                "        System.out.println(\"Hello, World!\");\n" +
                "    }\n" +
                "}\n" +
                "```\n\n" +
                "[这是一个链接](https://www.example.com)\n\n" +
                "---\n\n" +
                "这是最后一段。";

        // 执行转换
        String proseMirror = converter.convert(markdown);

        // 验证结果
        assertNotNull(proseMirror, "转换结果不应为null");
        assertTrue(proseMirror.contains("\"type\":\"doc\""), "转换结果应包含文档类型");
        assertTrue(proseMirror.contains("\"type\":\"heading\""), "转换结果应包含标题");
        assertTrue(proseMirror.contains("\"type\":\"paragraph\""), "转换结果应包含段落");
        assertTrue(proseMirror.contains("\"type\":\"bold\""), "转换结果应包含粗体");
        assertTrue(proseMirror.contains("\"type\":\"italic\""), "转换结果应包含斜体");
        assertTrue(proseMirror.contains("\"type\":\"bullet_list\""), "转换结果应包含无序列表");
        assertTrue(proseMirror.contains("\"type\":\"ordered_list\""), "转换结果应包含有序列表");
        assertTrue(proseMirror.contains("\"type\":\"blockquote\""), "转换结果应包含引用");
        assertTrue(proseMirror.contains("\"type\":\"code_block\""), "转换结果应包含代码块");
        assertTrue(proseMirror.contains("\"type\":\"link\""), "转换结果应包含链接");
        assertTrue(proseMirror.contains("\"type\":\"horizontal_rule\""), "转换结果应包含分隔线");

        // 输出转换结果,方便查看
        System.out.println("Markdown转换为ProseMirror格式的结果:");
        System.out.println(proseMirror);
    }

    /**
     * 测试空Markdown内容的处理
     * <p>
     * 验证转换器对空内容的处理是否正确
     */
    @Test
    public void testEmptyMarkdown() {
        String proseMirror = converter.convert("");
        assertNotNull(proseMirror, "空Markdown转换结果不应为null");
        assertTrue(proseMirror.contains("\"type\":\"doc\""), "空Markdown转换结果应包含文档类型");
        assertTrue(proseMirror.contains("\"type\":\"paragraph\""), "空Markdown转换结果应包含空段落");
    }

    /**
     * 测试复杂嵌套结构的Markdown转换
     * <p>
     * 验证转换器对嵌套结构的处理是否正确
     */
    @Test
    public void testComplexMarkdown() {
        // 准备测试用的复杂Markdown文本,包含嵌套结构
        String markdown = "# 复杂结构测试\n\n" +
                "- 列表项1\n" +
                "  - 嵌套列表项1.1\n" +
                "  - 嵌套列表项1.2\n" +
                "    - 深度嵌套列表项1.2.1\n" +
                "- 列表项2\n\n" +
                "> 引用1\n" +
                "> \n" +
                "> > 嵌套引用\n" +
                "> \n" +
                "> 引用2\n\n" +
                "```\n" +
                "代码块中的**Markdown语法**不应被解析\n" +
                "```\n";

        // 执行转换
        String proseMirror = converter.convert(markdown);

        // 验证结果
        assertNotNull(proseMirror, "复杂Markdown转换结果不应为null");
        assertTrue(proseMirror.contains("\"type\":\"doc\""), "复杂Markdown转换结果应包含文档类型");
        assertTrue(proseMirror.contains("\"type\":\"bullet_list\""), "复杂Markdown转换结果应包含无序列表");
        assertTrue(proseMirror.contains("\"type\":\"blockquote\""), "复杂Markdown转换结果应包含引用");
        assertTrue(proseMirror.contains("\"type\":\"code_block\""), "复杂Markdown转换结果应包含代码块");

        // 输出转换结果,方便查看
        System.out.println("复杂Markdown转换为ProseMirror格式的结果:");
        System.out.println(proseMirror);
    }
}

改造 AddArticleRequest 新增转换方法

然后我们修改 AddArticleRequest 文章请求类里面新增一个获取 content 的方法

    /**
     * 获取ProseMirror格式的内容
     * <p>
     * 该方法用于获取文章内容的ProseMirror格式。
     * 如果content为空,则自动将plain转换为ProseMirror格式。
     * 确保文章内容始终以正确的格式提供给编辑器。
     *
     * @return ProseMirror格式的内容
     */
    public String getContent() {
        MarkdownToProseMirrorConverter converter = new MarkdownToProseMirrorConverter();
        return converter.convert(plain);
    }

那么根据上面分析编写我们的 Tool 需要接收的参数

编写 saveArticleTool 的出入参

创建 ArticleFunctionRequest 用于接收大模型参数

plain 可以直接是 markdown 无需大模型传递

package com.yby6.mcp.server.tencent.mcpService.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.Data;

/**
 * 文章功能请求模型
 * <p>
 * 该模型类用于封装文章发布功能所需的请求参数。
 * 使用Jackson注解进行JSON序列化配置,确保必填字段的验证。
 * 使用Lombok的@Data注解自动生成getter、setter等方法。
 * <p>
 * 主要功能:
 * 1. 定义文章发布所需的基本信息
 * 2. 提供JSON序列化配置
 * 3. 支持必填字段验证
 *
 * @author Yang Shuai
 * @version 1.0.0
 * @since 2025/05/10
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ArticleFunctionRequest {

    /**
     * 文章标题
     *
     * 必填字段,用于显示文章的主标题。
     * 在JSON序列化时使用"title"作为字段名。
     */
    @JsonProperty(required = true, value = "title")
    @JsonPropertyDescription("文章标题")
    private String title;

    /**
     * 文章内容
     *
     * 必填字段,使用Markdown格式存储文章内容。
     * 在JSON序列化时使用"markdowncontent"作为字段名。
     * 支持富文本格式,包括标题、列表、代码块等。
     */
    @JsonProperty(required = true, value = "markdowncontent")
    @JsonPropertyDescription("文章内容")
    private String markdowncontent;

    /**
     * 文章摘要
     *
     * 必填字段,用于提供文章内容的简短描述。
     * 在JSON序列化时使用"userSummary"作为字段名。
     * 通常用于文章列表展示和SEO优化。
     */
    @JsonProperty(required = true, value = "userSummary")
    @JsonPropertyDescription("文章摘要")
    private String userSummary;
}

创建 ArticleFunctionResponse 用于返回大模型结果

里面可以添加任意数据, 这里是会返回给大模型, 可以在大模型调用的时候看到返回 JSON

package com.yby6.mcp.server.tencent.mcpService.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.Data;

/**
 * 文章功能响应模型
 * <p>
 * 该模型类用于封装文章发布功能的结果响应。
 * 使用Jackson注解进行JSON序列化配置,确保必填字段的验证。
 * 使用Lombok的@Data注解自动生成getter、setter等方法。
 * <p>
 * 主要功能:
 * 1. 提供文章发布结果的反馈信息
 * 2. 包含文章的唯一标识和访问链接
 * 3. 指示发布操作的成功或失败状态
 *
 * @author Yang Shuai
 * @version 1.0.0
 * @since 2025/05/10
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ArticleFunctionResponse {
    
    /**
     * 文章ID
     * <p>
     * 必填字段,发布成功后返回的文章唯一标识。
     * 在JSON序列化时使用"articleId"作为字段名。
     * 用于后续对文章进行编辑、删除等操作。
     * 在发布失败时可能为null。
     */
    @JsonProperty(required = true, value = "articleId")
    @JsonPropertyDescription("articleId")
    private Long articleId;
    
    /**
     * 发布状态
     * <p>
     * 必填字段,用于指示文章发布的结果。
     * 在JSON序列化时使用"status"作为字段名。
     * 状态码说明:
     * - 0: 发布成功
     * - 非0: 发布失败,具体错误码需要参考API文档
     */
    @JsonProperty(required = true, value = "status")
    @JsonPropertyDescription("status")
    private Integer status;
    
    /**
     * 文章链接
     * <p>
     * 必填字段,发布成功后返回的文章访问URL。
     * 在JSON序列化时使用"url"作为字段名。
     * 用于直接访问已发布的文章。
     * 在发布失败时可能为null。
     */
    @JsonProperty(required = true, value = "url")
    @JsonPropertyDescription("url")
    private String url;
    
    /**
     * 工具版权信息
     */
    @JsonProperty(required = true, value = "copyright")
    @JsonPropertyDescription("copyright")
    private String copyright;
}

那么到这里就差不多完成了, 接下来只需要对代码进行微调

可以看到我们的 addArticle 请求工具需要传递 Cookie 这里我们不可能钉死的, 每个授权登录都有时间效益的, 所以我们需要改成可配置, 外界传递, 也是为了别的用户填入 cookie 就可以直接用

新增配置属性文件

package com.yby6.mcp.server.tencent.mcpService.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 腾讯云API配置属性类
 * <p>
 * 该配置类用于管理腾讯云开发者社区API的配置属性。
 * 使用Spring Boot的配置属性机制,从配置文件中读取相关配置。
 * 配置前缀为"tencent.api"。
 * <p>
 * 主要功能:
 * 1. 管理API认证信息(Cookie)
 * 2. 管理文章分类信息
 * 3. 提供配置属性的访问方法
 *
 * @author yby6
 * @version 1.0.0
 */
@ConfigurationProperties(prefix = "tencent.api")
@Component
public class TencentApiProperties {

    /**
     * 认证Cookie
     * <p>
     * 用于腾讯云开发者社区API的身份验证。
     * 在配置文件中通过tencent.api.cookie属性设置。
     * 该值在应用启动时会被验证,不能为空。
     */
    private String cookie;

    /**
     * 获取认证Cookie
     *
     * @return 认证Cookie字符串
     */
    public String getCookie() {
        return cookie;
    }

    /**
     * 设置认证Cookie
     *
     * @param cookie 认证Cookie字符串
     */
    public void setCookie(String cookie) {
        this.cookie = cookie;
    }
}

修改 mcp 发布文章方法里面使用配置文件传递进来的 Cookie

public class TencentArticleToolService implements ToolServiceMarker {
    
    @Resource
    private ITencentService iTencentService;
    
    @Resource
    private TencentApiProperties tencentApiProperties;

    Call<AddArticleResponse> call = iTencentService.addArticle(tencentApiProperties.getCookie(), addArticleRequest);
  

那么到这里我们就已经完成了对 MCP Tool 的升级, 完整的 TencentArticleToolService 如下

package com.yby6.mcp.server.tencent.mcpService.tools;

import com.alibaba.fastjson.JSON;
import com.yby6.mcp.server.tencent.api.ITencentService;
import com.yby6.mcp.server.tencent.api.dto.AddArticleRequest;
import com.yby6.mcp.server.tencent.api.dto.AddArticleResponse;
import com.yby6.mcp.server.tencent.mcpService.config.properties.TencentApiProperties;
import com.yby6.mcp.server.tencent.mcpService.model.ArticleFunctionRequest;
import com.yby6.mcp.server.tencent.mcpService.model.ArticleFunctionResponse;
import com.yby6.mcp.server.tencent.mcpService.ToolServiceMarker;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import retrofit2.Call;
import retrofit2.Response;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;

/**
 * 腾讯云开发者社区文章服务
 * <p>
 * 该服务类负责处理与腾讯云开发者社区文章相关的业务逻辑,
 * 包括文章的发布、更新等操作。作为领域服务层的一部分,
 * 它通过端口适配器模式与基础设施层进行交互。
 *
 * @author yby6
 * @version 1.0.0
 */
@Slf4j
@Service
public class TencentArticleToolService {

    @Resource
    private ITencentService iTencentService;

    @Resource
    private TencentApiProperties tencentApiProperties;

    /**
     * 发布文章到腾讯云开发者社区
     * <p>
     * 该方法是一个MCP工具方法,用于将文章发布到腾讯云开发者社区。
     * 主要功能:
     * 1. 接收文章发布请求
     * 2. 记录请求参数日志
     * 3. 通过端口适配器调用实际的文章发布服务
     * 4. 处理异常情况并返回响应
     *
     * @param request 文章发布请求,包含文章标题、内容等信息
     * @return 文章发布响应,包含发布结果信息
     * @throws IOException 当发布过程中发生IO异常时抛出
     */
    @Tool(description = "发布文章到腾讯云开发者社区")
    public ArticleFunctionResponse saveArticle(ArticleFunctionRequest request) {
        try {
            log.info("腾讯云开发者社区发帖参数:{}", JSON.toJSONString(request));
            log.info("接收到的参数: {}", request.toString());

            AddArticleRequest addArticleRequest = new AddArticleRequest();
            addArticleRequest.setTitle(request.getTitle());
            addArticleRequest.setPlain(request.getMarkdowncontent());
            addArticleRequest.setContent(addArticleRequest.getContent());
            addArticleRequest.setSourceType(1);  // 设置为原创
            addArticleRequest.setClassifyIds(List.of(2));  // 设置文章分类
            addArticleRequest.setTagIds(List.of(18126));  // 设置文章标签
            addArticleRequest.setLongtailTag(List.of("mcp"));  // 设置长尾标签
            addArticleRequest.setColumnIds(List.of(101806));  // 设置专栏ID
            addArticleRequest.setOpenComment(1);  // 开启评论
            addArticleRequest.setCloseTextLink(0);  // 允许文本链接
            addArticleRequest.setUserSummary(request.getUserSummary());
            addArticleRequest.setPic("");  // 设置封面图片
            addArticleRequest.setSourceDetail(new HashMap<>());  // 设置来源详情
            addArticleRequest.setZoneName("");  // 设置专区名称

            // 执行API调用
            Call<AddArticleResponse> call = iTencentService.addArticle(tencentApiProperties.getCookie(), addArticleRequest);
            Response<AddArticleResponse> response = call.execute();

            // 记录请求和响应日志
            log.info("\n\n请求腾讯云开发者社区发布文章\n req:{} \nres:{}", JSON.toJSONString(addArticleRequest), JSON.toJSONString(response));

            // 构建返回对象
            ArticleFunctionResponse articleFunctionResponse = new ArticleFunctionResponse();
            articleFunctionResponse.setStatus(-1);
            articleFunctionResponse.setArticleId(null);
            articleFunctionResponse.setUrl(null);
            articleFunctionResponse.setCopyright(getCopyright());

            if (response.isSuccessful()) {
                log.info("腾讯云开发者社区发布文章成功: {}", JSON.toJSONString(response.body()));

                // 处理成功响应
                AddArticleResponse articleResponseDTO = response.body();
                if (null == articleResponseDTO) return null;

                articleFunctionResponse.setStatus(articleResponseDTO.getStatus());
                articleFunctionResponse.setArticleId(articleResponseDTO.getArticleId());
                articleFunctionResponse.setUrl("https://cloud.tencent.com/developer/article/" + articleResponseDTO.getArticleId());

                return articleFunctionResponse;
            }
            log.error("腾讯云开发者社区发布文章失败: {}", JSON.toJSONString(response));
            return articleFunctionResponse;
        } catch (Exception e) {
            log.error("腾讯云开发者社区发帖失败 ", e);
        }
        return null;
    }
}

CodeBuddy 测试 Tencent MCP Server

接下来我们就需要完整的测试一下, 和之前一样我们需要打 jar 包然后客户端进行调用, 这次我就使用 CodeBuddy 进行对我们的 MCP Server 接入调用

打新的 Jar 包

打开老朋友 CodeBuddy

点击 MCP 功能, 点击 ➕ 可以看到我们前面已经使用过的配置, 往里面继续添加即可

和我们前面编写 Client 一样 command 为 java jdk 地址 我这里本地是 jdk 1.8 所以这样做直接指定使用 jdk17 版本

里面的 args 参数也是一样的 定义了编码是 utf-8, ok 可以看到侧边栏的 tencent-send-article-mcp-server 已经成功点亮我们点击播放键看看效果

  "tencent-send-article-mcp-server": {
    "command": "你的JDK17以上: /graalvm-jdk-17.0.11/Contents/Home/bin/java",
    "args": [
      "-Dfile.encoding=UTF-8",
      "-jar",
      "你的jar包地址: tencent-send-article-mcp-server/1.0.0/tencent-send-article-mcp-server-1.0.0.jar"
    ]
  }

测试效果

可以看到我们调用失败了,

也是返回了错误原因, 说我们没有配置 cookie 参数那么我们就在 json rpc 配置里面加上, 在代码里面我们不是写了一个配置属性 cookie 外界传递

我们加上继续测试

下面改成你的 cookie

"--tencent.api.cookie=qcommunity_identify_id=V6pvjxmW1JPuCEMovMUTz; qcloud_uid=VaIEoNdiKsUA; web_uid=ae70b69c-f87e-41c"

点击播放键继续测试

可以看到文章已经帮我发布成功了, 我们去看看是否存在

完美, 已经成功的发布成功

最后

本期结束咱们下次再见👋~

🌊关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗

参考文献

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值