目 录CONTENT

文章目录

让大模型自己查天气:Spring AI + DeepSeek 的 MCP 服务端/客户端实战

成培培
2026-10-30 / 0 评论 / 0 点赞 / 5 阅读 / 0 字

从 0 搭建一个可运行的 MCP Demo,理解 Tool、Streamable HTTP 与自动工具调用。


一、大模型会聊天,但它不知道今天几度

大模型很擅长写代码、总结文档、陪人聊天,但它有几个天然的限制:

  • 知识有截止时间,不知道“现在”的天气;
  • 不能直接访问你的数据库、内部系统和业务 API;
  • 即使接入了工具,每换一个模型、每接一个系统,往往都要重新写一套适配代码。

Function Calling 解决了“模型可以调用函数”的问题,但没有解决“工具如何被标准化地发现、描述和调用”的问题。于是,MCP(Model Context Protocol,模型上下文协议) 出现了。

MCP 由 Anthropic 在 2024 年底开源,目标是做 AI 应用与外部能力之间的标准接口。一个很形象的比喻是:

MCP 是 AI 世界的 USB-C。
工具提供方按 MCP 协议实现一次,任何支持 MCP 的 AI 客户端都可以即插即用。

适合读者:有 Java / Spring Boot 基础,想快速理解 MCP,并落地一个可运行 Demo 的后端开发者。

本文不走纯理论路线,而是直接从一个真实可运行的项目出发:

  • 用 Spring AI 写一个 MCP 服务端,对外提供 getWeather 天气查询工具;
  • 用 Spring AI MCP Client + DeepSeek 写一个 AI 客户端;
  • 用户在网页上问“今天武汉天气怎么样”,DeepSeek 自动调用 MCP 工具,拿到结果后再用自然语言回答。

服务端的天气数据是写死的武汉市模拟数据,重点是把 MCP 的完整链路跑通。等链路通了,把模拟数据换成真实天气 API 只是替换一个方法的事。


二、MCP 的核心概念

MCP 采用典型的 Client/Server 架构,但和普通 HTTP 接口不同,它把“能力”抽象成了几种标准原语。

2.1 三个角色

角色 职责
Host(宿主) 用户直接使用的 AI 应用,例如 IDE、聊天客户端、桌面助手
Client(客户端) Host 内部与某个 MCP Server 保持连接的组件,负责协议通信
Server(服务端) 对外暴露工具、资源、提示词等能力的一方

一个 Host 可以同时连接多个 MCP Server,例如一个查天气、一个查数据库、一个操作 Git。对用户来说,它们就像同一套工具集。

2.2 三类核心能力

原语 作用 典型例子
Tools(工具) 让模型主动调用的函数 查天气、发邮件、创建工单
Resources(资源) 提供给模型读取的上下文数据 文件、数据库记录、日志
Prompts(提示词) 服务端预置的提示词模板 代码审查模板、周报模板

本文的 Demo 只用到 Tools,这也是目前落地最广泛的一类能力。

2.3 传输方式

MCP 支持多种传输方式,常见的有:

  • stdio:客户端把 MCP Server 作为子进程启动,通过标准输入输出通信,适合本地工具;
  • Streamable HTTP:通过 HTTP 传输,支持流式响应,适合远程服务;
  • SSE:早期基于 Server-Sent Events 的方案,新版本中已逐步被 Streamable HTTP 取代。

本文使用 Streamable HTTP,服务端端点为 /mcp,客户端通过 http://localhost:8080/mcp 连接。相比 stdio,它更接近真实生产环境:服务可以独立部署、独立扩缩容,也方便加鉴权、限流和监控。

2.4 MCP 和 Function Calling 的关系

两者不是替代关系,而是分工不同:

  • Function Calling 解决“模型如何表达我要调用某个函数”;
  • MCP 解决“这个函数/工具从哪里来、如何描述、如何被标准化调用”。

可以把 MCP 理解为工具层的“服务发现 + 协议规范”。模型仍然通过 Function Calling 发起调用,只是工具的定义和实际执行由 MCP 统一管理。


三、项目目标与整体架构

最终项目包含两个独立的 Spring Boot 应用:

spring-ai-mcp/
├── mcp-server/     # MCP 服务端,端口 8080,提供 getWeather 工具
└── mcp-client/     # MCP 客户端 + AI 聊天页面,端口 8081,调用 DeepSeek

完整调用链路如下:

sequenceDiagram
    autonumber
    participant U as 用户/浏览器
    participant C as MCP 客户端<br/>Spring Boot :8081
    participant L as DeepSeek API
    participant S as MCP 服务端<br/>Spring Boot :8080

    U->>C: POST /api/chat "今天武汉天气怎么样?"
    C->>L: chat/completions(messages + tools)
    L-->>C: tool_calls: getWeather(city=武汉, date=...)
    C->>S: MCP tools/call
    S-->>C: 武汉模拟天气
    C->>L: 工具结果 + 继续对话
    L-->>C: 自然语言天气总结
    C-->>U: {conversationId, answer}

这条链路里有三个关键点:

  1. 工具定义由 MCP 服务端提供,客户端启动时自动发现,不需要手写工具 Schema;
  2. 是否调用工具由大模型决定,客户端只负责把工具交给模型;
  3. 工具执行发生在 MCP 服务端,客户端通过 MCP 协议发起 tools/call。

四、技术栈与工程结构

4.1 版本选择

组件 版本
JDK 17
Spring Boot 3.5.15
Spring AI 1.1.8
MCP Java SDK 0.18.3(由 Spring AI Starter 传递)
大模型 DeepSeek deepseek-chat
构建工具 Maven 3.6+

选择 Spring AI 1.1.8 的原因很简单:它是 1.x 系列最新的稳定版本,Starter 与 Spring Boot 3.5.x 对齐,Spring AI 也提供了 DeepSeek 的 Starter,不需要自己拼 OpenAI 兼容层的配置。

4.2 父 POM

在父工程里统一管理版本,避免两个模块各自引版本导致冲突:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.15</version>
    <relativePath/>
</parent>

<properties>
    <java.version>17</java.version>
    <spring-ai.version>1.1.8</spring-ai.version>
</properties>

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

有了 BOM,子模块引入 Spring AI 依赖时就不需要再写版本号。


五、MCP 服务端:把普通方法变成 MCP 工具

5.1 引入依赖

mcp-server/pom.xml:

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

这个 Starter 基于 Spring MVC 提供 MCP Server 能力,内置了 Streamable HTTP 传输支持。如果需要响应式编程,可以换成 spring-ai-starter-mcp-server-webflux。

5.2 用 @Tool 定义天气工具

核心类 WeatherService 就是一个普通的 Spring @Service,不需要继承任何 MCP 接口:

@Service
public class WeatherService {

    private static final Logger log = LoggerFactory.getLogger(WeatherService.class);

    @Tool(
        name = "getWeather",
        description = """
            查询指定城市在指定日期的天气情况。
            参数 city 为城市中文名称(例如:武汉),参数 date 为日期,
            格式必须是 yyyy-MM-dd(例如:2026-10-30)。
            当前 Demo 仅内置了武汉市的模拟天气数据,查询其他城市会返回不支持提示。
            """
    )
    public String getWeather(
            @ToolParam(description = "城市名称,例如:武汉") String city,
            @ToolParam(description = "查询日期,格式 yyyy-MM-dd,例如:2026-10-30") String date) {

        log.info("【MCP 工具被调用】getWeather(city={}, date={})", city, date);

        LocalDate targetDate;
        try {
            targetDate = LocalDate.parse(date);
        } catch (DateTimeParseException | NullPointerException e) {
            return "查询失败:日期格式不正确,请使用 yyyy-MM-dd 格式,例如 2026-10-30。";
        }

        if (city == null || city.isBlank()) {
            city = "武汉";
        }
        String normalizedCity = city.trim();
        if (!normalizedCity.contains("武汉")
                && !"wuhan".equalsIgnoreCase(normalizedCity)) {
            return "抱歉,当前 Demo 仅内置了武汉市(Wuhan)的模拟天气数据,暂不支持查询「"
                    + normalizedCity + "」。";
        }

        // 写死的武汉模拟天气,真实项目里替换成调用气象 API 即可
        return """
            城市:武汉市
            日期:%s
            天气:晴间多云
            气温:22℃ ~ 28℃
            体感温度:26℃
            湿度:65%%
            风向风力:东南风 2 级
            空气质量:良(AQI 72)
            紫外线:中等
            穿衣建议:短袖或薄外套,注意早晚温差
            数据说明:以上为 MCP 服务端内置的模拟数据,仅用于演示,不是真实天气。
            """.formatted(targetDate);
    }
}

这段代码有几个容易被忽略的细节:

  • 工具描述就是提示词。@Tool 的 description 和 @ToolParam 的 description 会进入模型的上下文,直接影响模型是否调用、参数填得对不对。日期格式、默认城市、支持范围最好都写清楚。
  • 返回纯文本而不是 JSON。对模型来说,结构清晰的自然语言往往比嵌套 JSON 更容易理解和总结。
  • 参数校验放在服务端。模型可能传错日期、传空城市,工具自身要做兜底,不能假设调用方一定正确。
  • 日志要打出来。调试 MCP 时,最直观的信号就是服务端有没有打印“工具被调用”。

5.3 注册 ToolCallbackProvider

只写 @Tool 还不够,需要把带有工具方法的 Bean 注册成 ToolCallbackProvider:

@Configuration
public class McpToolConfig {

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

Spring AI 的 MCP Server 自动配置会收集容器里所有的 ToolCallbackProvider,把它们暴露给 MCP 客户端。也就是说:

你只需要关心“工具体”怎么写,协议层、注册表、tools/list 的响应都由 Spring AI 处理。

5.4 服务端配置

mcp-server/src/main/resources/application.yml:

server:
  port: 8080

spring:
  application:
    name: weather-mcp-server
  ai:
    mcp:
      server:
        name: weather-mcp-server
        version: 1.0.0
        protocol: STREAMABLE
        instructions: "武汉天气查询 MCP 服务:提供 getWeather 工具,参数为 city 和 date。"
        capabilities:
          tool: true
          resource: false
          prompt: false
        streamable-http:
          mcp-endpoint: /mcp

关键配置:

  • protocol: STREAMABLE:使用 Streamable HTTP;
  • mcp-endpoint: /mcp:MCP 端点路径;
  • capabilities.tool: true:开启工具能力;
  • instructions:会随初始化响应返回给客户端,相当于服务端的“使用说明”。

启动后看到下面两行日志,说明工具注册成功:

Registered tools: 1
Started McpServerApplication in 4.163 seconds

六、MCP 客户端:让 DeepSeek 自动调用工具

6.1 引入依赖

mcp-client/pom.xml 需要三块能力:

<!-- Web 页面和 REST 接口 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- MCP 客户端,负责连接 MCP Server、发现工具 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>

<!-- DeepSeek 聊天模型 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>

6.2 配置 DeepSeek 和 MCP 连接

mcp-client/src/main/resources/application.yml:

server:
  port: 8081
  error:
    include-message: always

spring:
  application:
    name: weather-mcp-client

  ai:
    deepseek:
      # 把这里换成你自己的 Key,或者设置环境变量 DEEPSEEK_API_KEY
      api-key: "${DEEPSEEK_API_KEY:sk-请填写你的DeepSeekAPIKey}"
      base-url: "${DEEPSEEK_BASE_URL:https://api.deepseek.com}"
      chat:
        options:
          model: "${DEEPSEEK_MODEL:deepseek-chat}"
          temperature: 0.7

    mcp:
      client:
        name: weather-mcp-client
        version: 1.0.0
        type: SYNC
        request-timeout: 30s
        streamable-http:
          connections:
            weather-server:
              url: "${MCP_SERVER_URL:http://localhost:8080}"
              endpoint: /mcp

这段配置里有两个关键点:

  • type: SYNC:使用同步 MCP 客户端,工具调用会阻塞等待结果,代码更直观;
  • streamable-http.connections.weather-server:声明一个名为 weather-server 的连接,客户端启动时会自动连接并拉取工具列表。

Spring AI 会自动创建 SyncMcpToolCallbackProvider,它实现了 ToolCallbackProvider,里面封装了从服务端发现的所有工具。我们只需要把它注入 ChatClient。

6.3 组装 ChatClient

@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder,
                                 ToolCallbackProvider mcpToolCallbackProvider,
                                 ChatMemory chatMemory) {
        return builder
                .defaultToolCallbacks(mcpToolCallbackProvider)
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }
}

这三行代码分别解决三个问题:

  • defaultToolCallbacks(...):把 MCP 发现的工具交给模型,模型才能发起 getWeather 调用;
  • MessageChatMemoryAdvisor:按会话保存多轮对话,用户问完“今天”再问“明天”时模型能理解上下文;
  • ChatClient:Spring AI 的统一聊天入口,屏蔽不同模型厂商的差异。

再补一个启动日志,方便确认工具发现是否成功:

@Bean
public ApplicationRunner mcpToolLogger(ToolCallbackProvider provider) {
    return args -> {
        ToolCallback[] callbacks = provider.getToolCallbacks();
        log.info("MCP 客户端已连接,发现 {} 个工具:", callbacks.length);
        Arrays.stream(callbacks).forEach(callback -> log.info("  - {}:{}",
                callback.getToolDefinition().name(),
                callback.getToolDefinition().description()));
    };
}

启动后应该看到:

MCP 客户端已连接,发现 1 个工具:
  - getWeather:查询指定城市在指定日期的天气情况。...

如果这里是 0 个工具,后面的对话一定不会调用天气能力,应该先排查 MCP 连接。

6.4 对话接口与系统提示词

/api/chat 接口的核心逻辑很短:

@PostMapping("/chat")
public ChatResponse chat(@RequestBody ChatRequest request) {
    String conversationId = (request.conversationId() == null || request.conversationId().isBlank())
            ? UUID.randomUUID().toString()
            : request.conversationId();

    String answer = chatClient.prompt()
            .system(systemPrompt())
            .user(request.message())
            .advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, conversationId))
            .call()
            .content();

    return new ChatResponse(conversationId, answer);
}

系统提示词是“模型会不会乖乖调用工具”的关键。这里每次请求都动态生成,把真实的今天、明天、后天注入进去:

private String systemPrompt() {
    LocalDate today = LocalDate.now();
    String week = today.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.CHINA);

    return """
        你是“天气小助手”,一个基于 Spring AI + MCP 的中文天气助手。
        当前日期:%s(%s)。

        回答规则:
        1. 当用户询问天气时,必须调用 getWeather 工具获取数据,禁止凭空编造天气。
        2. 调用 getWeather 时:
           - city:城市中文名;如果用户没有指定城市,默认使用“武汉”;
           - date:yyyy-MM-dd 格式的日期。
        3. 相对日期的换算基准:今天 = %s,明天 = %s,后天 = %s。
        4. 拿到工具结果后,用简洁自然的中文总结天气,并注明“这是演示用的模拟数据”。
        5. 如果工具提示不支持该城市,请如实告诉用户。
        6. 如果用户只是打招呼或询问与天气无关的问题,可以正常聊天,不必调用工具。
        """.formatted(today, week, today, today.plusDays(1), today.plusDays(2));
}

为什么要把日期写进提示词?

因为“今天”“明天”是相对日期,模型自己算容易错,尤其跨月、跨年时。把当前日期明确告诉它,再给出换算基准,能显著降低传错 date 的概率。

6.5 内置聊天页面

客户端还带了一个纯静态的 index.html,访问 http://localhost:8081 就能聊天,不需要额外启动前端工程。

页面会:

  • 调用 /api/tools 展示当前 MCP 工具;
  • 调用 /api/chat 发送消息;
  • 用 crypto.randomUUID() 生成 conversationId;
  • 支持回车发送、Shift + Enter 换行、清空会话。

对于 Demo 来说,这比单独写一个 React 工程更省事,也方便分享给不懂前端的人直接体验。


七、跑起来:从 Key 到第一条天气回答

7.1 配置 DeepSeek Key

推荐用环境变量,避免把 Key 提交到 Git:

export DEEPSEEK_API_KEY=sk-你的真实Key

Windows PowerShell:

$env:DEEPSEEK_API_KEY="sk-你的真实Key"

也可以直接修改 mcp-client/src/main/resources/application.yml 里的 api-key。项目里预留的占位符是:

api-key: "${DEEPSEEK_API_KEY:sk-请填写你的DeepSeekAPIKey}"

7.2 编译

mvn clean package -DskipTests

会得到两个可执行 Jar:

mcp-server/target/mcp-server-1.0.0.jar
mcp-client/target/mcp-client-1.0.0.jar

7.3 先启动服务端

java -jar mcp-server/target/mcp-server-1.0.0.jar

服务端监听 8080,MCP 端点:

http://localhost:8080/mcp

7.4 再启动客户端

java -jar mcp-client/target/mcp-client-1.0.0.jar

客户端监听 8081。启动日志里确认工具发现成功:

MCP 客户端已连接,发现 1 个工具:
  - getWeather:查询指定城市在指定日期的天气情况。...
Tomcat started on port 8081 (http)

7.5 提问

打开浏览器访问:

http://localhost:8081

输入:

今天武汉天气怎么样?

此时服务端控制台会打印:

【MCP 工具被调用】getWeather(city=武汉, date=2026-10-30)

页面最终返回类似:

武汉今天(2026-10-30)晴间多云,气温 22℃~28℃,体感 26℃,湿度 65%,东南风 2 级,空气质量良(AQI 72)。这是演示用的模拟数据。

也可以继续问“明天呢?”“后天会下雨吗?”,客户端会把相对日期换算成具体日期,再调用工具。


八、这条链路在协议层到底发生了什么

从代码看,好像只是 chatClient.prompt().call() 一行,但底层发生了很多事。

8.1 启动阶段:工具发现

MCP 客户端启动时,会向服务端发起初始化握手,然后调用 tools/list。服务端返回的工具定义大致如下:

{
  "name": "getWeather",
  "description": "查询指定城市在指定日期的天气情况。...",
  "inputSchema": {
    "type": "object",
    "properties": {
      "city": { "type": "string", "description": "城市名称,例如:武汉" },
      "date": { "type": "string", "description": "查询日期,格式 yyyy-MM-dd" }
    },
    "required": ["city", "date"]
  }
}

Spring AI 把这些定义转换成模型能理解的 Function Schema,交给 DeepSeek。

8.2 对话阶段:模型决定调用

用户提问后,客户端把“系统提示词 + 用户消息 + 工具定义”一起发给 DeepSeek。DeepSeek 判断需要查天气,返回类似:

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_xxx",
      "type": "function",
      "function": {
        "name": "getWeather",
        "arguments": "{\"city\":\"武汉\",\"date\":\"2026-10-30\"}"
      }
    }
  ]
}

注意:模型只负责“决定调用什么、参数是什么”,并不执行工具。真正执行的是 MCP 客户端。

8.3 工具执行:MCP tools/call

客户端收到 tool_calls 后,通过 MCP 协议向服务端发起调用:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "getWeather",
    "arguments": {
      "city": "武汉",
      "date": "2026-10-30"
    }
  }
}

服务端执行 WeatherService.getWeather,返回:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "城市:武汉市\n日期:2026-10-30\n天气:晴间多云\n..."
      }
    ],
    "isError": false
  }
}

8.4 最终回答:工具结果回传模型

客户端把工具结果作为一条 tool 消息追加到对话里,再次请求 DeepSeek。DeepSeek 这时已经拿到了真实数据,只需要把它总结成自然语言。

这就是为什么用户感觉“模型自己查了天气”,但实际上模型只做了两件事:决定调用工具 和 总结工具结果。


九、踩坑记录:那些文档里不一定写的细节

9.1 客户端启动时报连接失败

MCP 客户端默认 initialized: true,启动阶段就会连接服务端。如果先启动客户端、后启动服务端,客户端会初始化失败。

解决方式:先启动 mcp-server,再启动 mcp-client。如果顺序反了,重启客户端。

9.2 没有配置 Key,服务能启动但对话报错

spring.ai.deepseek.api-key 只要非空,Spring AI 就允许启动。占位符 sk-请填写你的DeepSeekAPIKey 也是非空字符串,所以服务能正常启动,但调用 DeepSeek 时会返回 401。

Demo 里的 ApiExceptionHandler 会把底层异常包装成 HTTP 502,并在 error 字段里带上原因,方便前端展示。排查时先看这个字段。

9.3 模型不调用工具,直接编天气

优先检查三件事:

  1. 客户端启动日志里是否发现 getWeather;
  2. /api/tools 是否返回工具;
  3. 工具描述和参数描述是否足够清楚。

如果工具描述写得太模糊,模型可能“觉得”自己知道答案,就不调用工具。系统提示词里明确写“天气问题必须调用工具”也有帮助。

9.4 “今天”被模型算错

大模型对相对日期不敏感,尤其是跨月、跨年。不要让它自己算,直接把当前日期和“今天/明天/后天”的换算基准写进系统提示词。

9.5 工具返回 JSON,模型反而总结得不好

工具结果不是越结构化越好。对 LLM 来说,一段字段清晰的纯文本通常比深层嵌套 JSON 更好读。这个 Demo 故意返回“城市/日期/天气/气温”这样的文本块,模型很容易提取。

9.6 Maven 下载依赖很慢

Spring AI 的依赖树不小,国内访问 Maven Central 可能很慢。可以在 ~/.m2/settings.xml 配置阿里云镜像:

<mirror>
    <id>aliyun</id>
    <mirrorOf>central</mirrorOf>
    <url>https://maven.aliyun.com/repository/public</url>
</mirror>

如果某个 jar 下载损坏,删掉本地仓库对应目录后执行 mvn -U clean package -DskipTests 重试。

9.7 用 Mock 测试时,注意请求体编码

如果你像本文一样用 Python/Node 写一个假的 OpenAI 接口来验证链路,要注意:JDK HttpClient 在请求体长度未知时会使用 Transfer-Encoding: chunked。很多简易 Mock 只读 Content-Length,会解析出空 body,导致模型一直返回 tool_calls,客户端无限调用工具。

这不是 Spring AI 的问题,而是 Mock 没正确处理 chunked 编码。给 Mock 加上 chunked 解码即可。


十、从 Demo 到生产:还可以做什么

这个 Demo 只实现了最小闭环,真实项目里还可以继续扩展:

10.1 接入真实天气 API

替换 WeatherService.getWeather 的实现,调用和风天气、OpenWeatherMap、高德天气等 HTTP API。MCP 工具签名不变,客户端完全不需要改。

10.2 增加更多工具

例如:

  • getAirQuality(city, date):空气质量;
  • getWeatherForecast(city, days):未来几天预报;
  • getTravelAdvice(city, date):出行建议;
  • sendWeatherAlert(...):极端天气提醒。

每个工具仍然是一个 @Tool 方法,注册到 ToolCallbackProvider 即可。

10.3 使用 Resources 和 Prompts

除了 Tools,MCP 还支持 Resources 和 Prompts。例如把“天气查询规范”做成 Prompt 模板,把“城市列表”做成 Resource,让客户端按需读取。

10.4 会话持久化

Demo 使用 InMemoryChatMemoryRepository,重启后会话丢失。生产环境可以换成 Redis、JDBC 等持久化实现。

10.5 鉴权、限流与可观测性

Streamable HTTP 服务部署到公网时,至少要考虑:

  • MCP 端点鉴权(API Key / OAuth / 网关);
  • 工具调用限流,避免被刷;
  • 超时与重试;
  • 工具调用日志、耗时、成功率指标;
  • 敏感工具的人工确认(Human-in-the-loop)。

10.6 多 MCP Server 聚合

一个客户端可以配置多个 connections,同时连接天气、数据库、Git 等多个 MCP Server。Spring AI 会把所有工具聚合到同一个 ToolCallbackProvider 中,模型根据描述自行选择。


十一、总结

MCP 的价值,不是让大模型“多了一个函数”,而是把 AI 与外部能力之间的连接方式标准化:

  • 工具提供方只需实现一次 MCP Server;
  • 任何支持 MCP 的客户端都能发现并调用;
  • 模型、工具、宿主应用可以各自演进,互不绑死。

Spring AI 则把 Java 接入 MCP 的门槛降到了几个注解和几行配置:

  • 服务端:@Tool + MethodToolCallbackProvider + spring-ai-starter-mcp-server-webmvc;
  • 客户端:spring-ai-starter-mcp-client + ChatClient.defaultToolCallbacks(...);
  • 大模型:换成 DeepSeek,只需要一个 Starter 和一行 api-key。

如果你正在做 Java 侧的 AI 应用,不妨从这个天气 Demo 开始:先让链路跑通,再逐步把模拟数据换成真实业务能力。等工具越来越多,你会越来越体会到“协议标准化”带来的价值。


参考资料


附:项目结构速查

spring-ai-mcp/
├── pom.xml
├── mcp-server/
│   ├── pom.xml
│   └── src/main/
│       ├── java/com/example/mcp/server/
│       │   ├── McpServerApplication.java
│       │   ├── config/McpToolConfig.java
│       │   └── weather/WeatherService.java
│       └── resources/application.yml
└── mcp-client/
    ├── pom.xml
    └── src/main/
        ├── java/com/example/mcp/client/
        │   ├── McpClientApplication.java
        │   ├── config/ChatClientConfig.java
        │   └── chat/ChatController.java
        └── resources/
            ├── application.yml
            └── static/index.html

启动顺序再强调一次:

# 1. 配置 Key
export DEEPSEEK_API_KEY=sk-你的真实Key

# 2. 编译
mvn clean package -DskipTests

# 3. 启动 MCP 服务端(8080)
java -jar mcp-server/target/mcp-server-1.0.0.jar

# 4. 启动 MCP 客户端(8081)
java -jar mcp-client/target/mcp-client-1.0.0.jar

然后打开 http://localhost:8081,问一句:

今天武汉天气怎么样?
0

评论区