2139 字
约 7 分钟
2
Spring AI MCP 实战:同时实现 Server 与 Client

Spring AI MCP 实战:同时实现 Server 与 Client

Function Call 解决了一个问题:

模型怎么调用工具。

但如果每个工具、每个 Agent、每个平台都有自己的一套接入协议,很快就会变成大量适配代码。

MCP(Model Context Protocol)解决的是:

给 AI 应用提供统一的工具接入协议。

可以把它理解成 AI 世界里的 USB:

Claude
Codex
Cursor
Agent
     │
     │ MCP
     ↓
┌───────────────┐
│   MCP Server  │
├───────────────┤
│ 岗位查询       │
│ 浏览器操作     │
│ 文件系统       │
│ 数据库         │
│ 第三方服务     │
└───────────────┘

只要双方遵循 MCP,客户端就不需要针对每个工具重新开发适配。


1. MCP 和 Function Call 是什么关系?

这两个概念很容易混。

Function Call 更偏向:

LLM
 ↓
我要调用 searchJob
 ↓
arguments
 ↓
应用执行

MCP 更偏向:

这些 searchJob、openBrowser、publishArticle
到底从哪里来?
怎么发现?
怎么调用?
怎么跨进程通信?

所以可以简单记:

Function Call
解决“模型怎么使用工具”

MCP
解决“工具怎么标准化提供给模型”

最终两者通常结合使用:

User
 ↓
LLM
 ↓
Function Call
 ↓
MCP Client
 ↓
MCP Server
 ↓
真实工具

2. Spring AI 实现 MCP Server

一个 Spring Boot 应用想把自己的能力开放出去,本质上需要三步。

第一步,引入 MCP Server Starter。

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

第二步,把 Java 方法声明成 Tool。

@Tool(
    description = "根据用户求职条件查询匹配的岗位"
)
public List<Job> searchJobs(JobSearchRequest request) {
    return jobService.search(request);
}

参数也要写清楚:

public class JobSearchRequest {

    @ToolParam(
        description = "工作地点,如武汉、北京、上海",
        required = false
    )
    private String location;

    @ToolParam(
        description = "招聘类型,如秋招、春招、实习",
        required = false
    )
    private String recruitmentType;
}

第三步,把这些工具注册给 MCP Server。

@Bean
ToolCallbackProvider jobTools(JobMcpService service) {
    return MethodToolCallbackProvider.builder()
            .toolObjects(service)
            .build();
}

整体过程就是:

Java Method
     ↓
   @Tool
     ↓
ToolCallback
     ↓
MCP Server
     ↓
外部 AI Client

3. @Tool 描述仍然非常重要

MCP 并没有改变 Tool Calling 的核心逻辑。

模型最终看到的依然是:

tool name
description
input schema

例如用户说:

看看武汉有没有央企秋招岗位

模型需要根据工具 Schema 自动得到:

{
  "location": "武汉",
  "companyType": "央国企",
  "recruitmentType": "秋招"
}

所以:

MCP 标准化了工具接入,但真正决定模型会不会正确使用工具的,仍然是 Tool Description 和 Schema。


4. 多模块项目怎么管理几十个 Tool?

生产系统不会只有两个 Tool。

可能是:

job-module
    searchJob
    recommendJob

crawler-module
    crawlUrl
    extractJob

browser-plugin
    openPage
    click
    screenshot

task-module
    createTask
    scheduleTask

一种比较好的方式是:

每个模块
   ↓
自己定义 @Tool
   ↓
自己注册 ToolCallbackProvider
   ↓
Spring 容器自动收集
   ↓
统一暴露给 MCP Server

于是新增工具时:

新增模块 / 方法
↓
注册 Bean
↓
自动进入 MCP

而不是维护一个巨大的:

registerToolA();
registerToolB();
registerToolC();
...

这背后的思想是:

工具属于业务模块,MCP Server 只负责统一暴露,不应该知道所有工具的实现细节。


5. 一个应用可以同时是 Server 和 Client

这个地方非常值得沉淀。

MCP Server 表示:

把我的能力给别人用。

MCP Client 表示:

我要使用别人的能力。

一个应用完全可以同时拥有两个身份。

                外部 AI Client
                      ↓
                 MCP Server
                      ↓
               ┌──────────┐
               │ 求职系统  │
               └──────────┘
                      │
                 MCP Client
                      ↓
          ┌───────────┴──────────┐
          ↓                      ↓
   Browser MCP            WeChat MCP

例如求职系统自己拥有:

岗位数据库
岗位推荐
岗位搜索

所以作为 MCP Server 对外提供。

但微信公众号发布、浏览器自动化等能力可能来自外部 MCP Server。

此时求职系统又作为 MCP Client 去调用。

于是形成:

内部能力 → 对外输出
外部能力 → 对内引入

这就是 MCP 很有价值的地方。


6. MCP Client 可以扩展 Agent 能力

假设系统原本只能:

搜索岗位
推荐岗位

后来接入 Browser MCP:

导航网页
点击
填写表单
截图

又接入公众号 MCP:

创建文章
发布文章
保存草稿

Agent 就从:

Job Agent

逐渐变成:

Job Agent
+
Browser
+
Publishing
+
Search
+
External Services

而业务 Agent 不需要自己重新实现这些能力。

因此可以把 MCP 理解成:

Agent 的能力插件协议。


7. 一个很典型的工具编排

例如自动生成求职公众号文章:

定时任务
   ↓
查询当天岗位
   ↓
生成 Markdown
   ↓
交给 LLM
   ↓
LLM 决定调用 publishArticle
   ↓
MCP Client
   ↓
公众号 MCP Server
   ↓
保存公众号草稿

这里同时使用了:

本地业务能力
+
LLM
+
外部 MCP Tool

这已经不只是一次 Tool Call,而是在做:

跨系统工具编排。


8. MCP 的生产难点:连接和会话

Demo 往往到:

Client 成功连上 Server

就结束了。

但真正上线之后马上会遇到:

服务重启
网络断开
Client 重连
Session 丢失
初始化状态丢失

MCP 建立连接时通常需要经历:

initialize
      ↓
协议协商
      ↓
notifications/initialized
      ↓
Session Ready

所以即使工具调用本身可以设计得比较无状态,连接初始化过程仍然存在生命周期状态

服务一旦重启:

内存 Session
→ 消失

客户端可能已经认为自己初始化完成,但服务器端已经什么都不知道了。

于是工具调用可能失败。


9. 会话恢复怎么做?

一种生产思路是:

initialize
↓
保存初始化信息

initialized
↓
保存确认信息

持久化:

sessionId
userId
initializePayload
initializedPayload

服务重启以后:

Client 重连
      ↓
发现当前 Session 未初始化
      ↓
数据库查询历史握手数据
      ↓
恢复 / 重放初始化过程
      ↓
继续 Tool Call

本质上是在做:

Session Recovery。

所以 MCP 真正进入生产环境以后,关注点会从:

“工具能不能调用?”

升级成:

“工具系统能不能长期稳定运行?”

10. Session ID 也需要设计

还有一个问题:

服务重启
↓
重新建立连接
↓
生成新的 sessionId

旧 Session 就找不到了。

因此生产设计不能永远只依赖:

sessionId

还可能需要:

userId
clientId
deviceId
connectionId

作为恢复依据。

本质和前面的 ChatMemory 很像:

ID 决定了状态属于谁。

如果未来存在:

同一用户
+
多个设备
+
多个 MCP Client

就必须设计更细的连接身份体系,而不能只按 userId 恢复最近一次 Session。


11. 最终架构

整套架构可以压成这张图:

                Claude / Codex / Cursor
                         │
                         │ MCP
                         ↓
                   MCP Server
                         │
                Spring AI Tools
                         │
          ┌──────────────┼──────────────┐
          ↓              ↓              ↓
       Job Tool      Browser Tool    Task Tool
                         │
                         │
                    AI Application
                         │
                    MCP Client
                         │
          ┌──────────────┼──────────────┐
          ↓              ↓              ↓
      Browser MCP    Publish MCP    Other MCP

所以这个应用既是:

能力提供者
MCP Server

也是:

能力消费者
MCP Client

12. 这篇真正值得沉淀什么?

可以记住这一条链路:

@Tool
 ↓
ToolCallback
 ↓
MCP Server
 ↓
标准协议
 ↓
MCP Client
 ↓
Agent

以及几个核心概念:

MCP Server       对外提供工具
MCP Client       接入外部工具
Tool             真正的业务能力
ToolCallback     Spring AI 工具抽象
Module Registry  模块化工具注册
Session Recovery 生产级连接恢复

最重要的一句话是:

Function Call 让模型学会“调用工具”,MCP 则把工具变成可以跨应用、跨语言、跨进程复用的标准能力。

把前面几篇继续串起来,现在 Spring AI Agent 的主干已经很完整了:

Model Routing
→ 用哪个模型

ChatMemory
→ 模型记住什么

Structured Output
→ 模型怎么返回

Function Call
→ 模型怎么使用工具

MCP
→ 工具从哪里来,以及怎么跨系统复用
Spring AI MCP 实战:同时实现 Server 与 Client
http://www.clxhxhhr.top/posts/659/
作者
clxstart
发布于
2026-09-17
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。