Spring AI入门到实战到原理源码-MCP

#MCP

它是通过网络上面的一些通用信息,以及垂直业务领域的一些信息

某个位置同边有什么好吃的,

我自己业务系统当中的一些信息,他肯定是不知道的,我们就需要通过function-Call,或者叫做TOOlS来进行实现,这样呢就可以让大模型,间接的去调用我们系统当中的方法,从而呢去查询第三方的天气接口,或者说昵去查询地图信息,去查询我们自己业务的一些信息,想要基础大模型具备这个功能呢,首先我们得告诉大模型,我们系统当中提供7哪些工具,以及你要调用我这个工具呢,需要提供什么信息给我,就此如说我们需要查询天气,那你要调用我这工具呢,麻烦提供OCatjon位置给我,如果你有多个AI应用,是不是就会出现一些重复的工作量,那我们单个ai应用,是不是就形成了信息的孤岛呢,那外部的这个tool服务呢

MCP 服务 其实就是通过json数据的方式,去告诉每一个ai 应用 ,我的这个mcp服务当中,比如说,有获取指定位是空气质量的工具,有获取某个位置同边信息的工具等等,

跟基础大模型来进行通讯,所以你会发现啊,MCP它其实只运用在我们的应用层,而大模型层,它其实依然是用传统tools方式,来进行实现的,至于你AI应用是调用自己的业务方法,还是去调用外部工具的服务的方法,我不关心,

其实mcp 就是将一个公共的tools ,抽取出来,单独的进行分布式部署的,一个外部工具的服务,用于解决AI 应用的tools 开发的工作量,以及单个ai 应用的信息孤岛问题,那听到这里懂了的话

让大模型间接调用我们接口 。

需要提供什么信息给找

查查询天气 : 我提供了一个获取某个位置天气的工具

mcp 的三种方式

SSE

是SSE首先,建立一个SSE的长链接,那的本质呢,依然是通过HTTP的TCP通信方式,会进行三次握手,那么当长链接建立完之后,MCP服务端就可以不断的向客户端,单向的去发送数据,

而不用每一次通讯呢,都进行三次握手,从而提升了性能,可以主动地去关闭SSE的长链接,那目前,MCP协议已经把这种方式废弃了,主要的原因呢是因为啊,长链按一旦建立完之后,整个通信过程,都需要依赖这个长链接,一旦它出现了一些网络的毛刺,MCP往客户端去发送数据的时候呢,就会丟失,并且MCP Server呢,短暂时间之肉呢,它是感知不到的,那当连接恢复之后呢,之前发送的数据都已经丢失,所以在最新的MCP协议当中呢,它改用streamable,也同样的也会建立一个长链接,只不过这种方式呢,

MCP Server 传输方式选择指南

概述

MCP (Model Context Protocol) 是连接 AI 客户端与外部数据源和工具的标准化协议。在开发 MCP Server 时,选择合适的传输方式至关重要。本文档介绍两种主要的传输方式:StdioStreamable (HTTP),帮助开发者根据应用场景做出正确选择。

MCP 传输方式原理

MCP Server 通过不同的传输机制与客户端进行通信,每种方式都有其特定的实现原理和适用场景。


一、Stdio 传输方式

工作原理

  1. 启动机制:客户端通过命令方式启动 MCP Server 包(可以是 Java、Python 或 Node.js 开发的包)
  2. 通信方式:通过标准输入输出(stdin/stdout)进行数据传输
  3. 进程关系:一个客户端对应一个 MCP Server 进程,一对一绑定
  4. 权限控制:通过环境变量(env)传递用户信息

核心特性

  • 本地运行:绑定在客户端进程中,无需 HTTP 环境
  • 成本较低:适合开发辅助性工具
  • 简单直接:适合桌面应用和个人用户场景
  • 不支持并发:一个客户端只能启动一个 Server 实例
  • 不支持动态切换:无法在运行时切换用户权限
  • 升级复杂:需要客户端重启并重新获取更新包

适用场景

  • ✅ 面向个人用户的桌面应用
  • ✅ 本地辅助工具开发
  • ✅ 单用户场景
  • ✅ 需要读取本地文件等客户端资源

不适用场景

  • ❌ 基于浏览器的 Web 应用
  • ❌ 需要多客户端并发访问的场景
  • ❌ 需要动态权限切换的场景
  • ❌ 需要热更新的生产环境

二、Streamable (HTTP) 传输方式

工作原理

  1. 架构模式:客户端 → Web 服务器 → MCP Server(三层架构)
  2. 通信协议:基于 HTTP 进行通信
  3. 服务模式:MCP Server 面向服务,而非直接面向用户
  4. 认证机制:通过 HTTP 认证(如 JWT)在请求头中传递认证 token

核心特性

  • 支持高并发:可以处理多个客户端同时访问
  • 动态权限控制:通过 HTTP 认证机制实现灵活的权限管理
  • 支持热更新:Server 端更新后只需重新部署,无需重启客户端
  • 面向服务:适合 Web 应用和服务化架构
  • 标准化:基于 HTTP 协议,易于集成和调试

适用场景

  • ✅ 基于浏览器的 Web 应用
  • ✅ 需要多用户并发访问的场景
  • ✅ 需要动态权限切换的企业应用
  • ✅ 需要热更新的生产环境
  • ✅ 微服务架构

三、对比总结

特性 Stdio Streamable (HTTP)
启动方式 客户端命令启动 Web 服务器代理
通信协议 标准输入输出 HTTP
部署模式 本地进程 服务化部署
并发支持 ❌ 不支持 ✅ 支持
权限控制 环境变量 HTTP 认证(JWT 等)
热更新 ❌ 需重启客户端 ✅ 支持
适用场景 桌面应用、单用户 Web 应用、多用户
开发成本 较低 中等
网络依赖 无需网络 需要网络

四、重点总结

1. Stdio 方式的核心要点

  • 一对一绑定:一个客户端对应一个 MCP Server 进程
  • 环境变量传递:用户信息通过启动时的环境变量传递,无法动态切换
  • 本地运行:适合本地文件操作和桌面应用
  • 升级限制:需要客户端配合重启才能更新

2. Streamable 方式的核心要点

  • 三层架构:客户端通过 Web 服务器访问 MCP Server
  • HTTP 认证:支持 JWT 等标准认证机制
  • 服务化设计:面向服务,支持高并发
  • 热更新能力:支持不停机更新

五、注意事项

⚠️ Stdio 方式的注意事项

  1. Web 应用不适用

    • 如果开发基于浏览器的 Web 应用,不要选择 stdio 传输方式
    • 原因:无法支持多客户端并发和动态权限切换
  2. 环境依赖

    • 客户端必须安装对应语言环境(Java/Python/Node.js)
    • 否则无法启动 MCP Server 包
  3. 权限限制

    • 权限信息通过环境变量传递,在启动时确定
    • 无法在运行时动态切换用户权限
    • 多个客户端无法共享同一个 Server 实例
  4. 升级流程

    • MCP Server 包升级后,客户端需要:
      • 重新获取更新后的包
      • 重启客户端程序
    • 升级过程相对复杂

⚠️ Streamable 方式的注意事项

  1. 架构设计

    • 需要设计 Web 服务器层作为中间代理
    • 客户端不直接连接 MCP Server
  2. 认证实现

    • 需要在 HTTP 请求头中实现认证机制
    • 建议使用 JWT 等标准认证方案
  3. 部署要求

    • 需要网络环境支持
    • 需要部署 Web 服务器

六、选择建议

选择 Stdio 的场景

  • ✅ 开发桌面应用或本地工具
  • ✅ 单用户使用场景
  • ✅ 需要访问客户端本地资源(如文件系统)
  • ✅ 对部署复杂度要求较低
  • ✅ 不需要高并发支持

选择 Streamable 的场景

  • ✅ 开发 Web 应用
  • ✅ 需要多用户并发访问
  • ✅ 需要动态权限管理
  • ✅ 需要热更新能力
  • ✅ 服务化架构

七、MCP 协议原理补充

MCP 的核心组件

  1. MCP Host(主机):提供 AI 交互环境的应用程序
  2. MCP Client(客户端):在主机内运行,负责与 MCP Server 通信
  3. MCP Server(服务器):提供工具、资源和提示模板

MCP 的通信流程

  1. 客户端通过传输层(Stdio 或 HTTP)向 Server 发送请求
  2. Server 执行相应操作或检索数据
  3. Server 将结果返回给客户端
  4. 客户端将结果传递给 AI 模型

传输层的作用

传输层是 MCP 协议的基础设施,负责:

  • 建立客户端与服务器之间的通信通道
  • 传递请求和响应数据
  • 处理认证和权限控制
  • 管理连接生命周期

八、开发建议

  1. 明确应用场景:根据目标用户和应用类型选择传输方式
  2. 考虑扩展性:如果未来可能需要支持 Web 访问,优先考虑 Streamable
  3. 评估部署成本:Stdio 部署简单但扩展性差,Streamable 需要更多基础设施
  4. 权限设计:提前规划权限控制方案,避免后期重构
  5. 升级策略:考虑未来更新和维护的便利性

参考资料


最后更新:根据实际开发经验整理

使用

Streamable http目前springai1.0版本不支持,我们先掌握SSE和STDIO ,分别说下STDIO和SSE的方式:

  • STDIO更适合客户端桌面应用和辅助工具
  • SSE更适合web应用、业务有关的公共tools

STDIO

MCP Server

  • 现成共用MCP Server
  • 自定义 MCP Server

MCP Client

通过工具

CherryStudio、Cursor、Claude Desktop、Cline 等等很多,这里不---一演示,不会的话自己找个文章, 具使用都很简单!

mcp 官网

https://mcp.so/

vscode中安装Cline

配置Cline

使用阿里巴巴的千问平台。

以百度地图为例子 。

https://github.com/baidu-maps/mcp

https://mcp.so/server/baidu-map/baidu-maps

百度地图apikey 申请
https://lbs.baidu.com/apiconsole/center

百度地图 MCP 服务器文档

项目地址 : https://github.com/baidu-maps/mcp
官方文档 : lbsyun.baidu.com/faq/api?title=mcpserver/base
许可证: MIT License


🚀 项目介绍

百度地图 MCP 服务器是一个完全符合 MCP 协议规范的开源位置服务(LBS)解决方案,为开发者和 AI 智能体提供全面的地理空间 API 和工具集。作为中国首个支持 Model Context Protocol (MCP) 的地图服务提供商,百度地图 MCP 服务器架起了大语言模型(LLM)、AI 智能体与现实世界位置数据和服务之间的桥梁。

通过百度地图 MCP 服务器,您可以轻松为您的应用程序、LLM 和智能体提供高级地图、地理编码、POI 搜索、路线规划、天气、交通等功能------所有这些都通过标准化、开发者友好的 MCP 接口实现。

核心特性

  • 完整的 MCP 协议支持:与任何符合 MCP 规范的智能体、LLM 或平台无缝集成
  • 丰富的 LBS 能力:地理编码、逆地理编码、POI 搜索、路线规划(驾车、步行、骑行、公交)、天气、IP 定位、实时路况等
  • 跨平台 SDK:官方 Python 和 TypeScript SDK,易于 CLI 和云部署
  • 企业级数据:基于百度地图权威、最新的地理空间数据
  • 高性能与稳定性:推荐使用 SSE(Server-Sent Events)访问,低延迟、高可靠性
  • 开源与可扩展:MIT 许可证,易于定制和扩展

适用场景

  • 旅行助手
  • 物流平台
  • 智慧城市解决方案
  • LLM 驱动的智能体
  • 导航应用
  • 配送服务
  • 需要位置感知的 AI 应用

MCP 服务器架构优势

  • 无缝 AI 集成:允许 LLM 和智能体自然地理解和处理位置数据
  • 上下文理解:为更智能的决策提供丰富的地理空间上下文
  • 标准化接口:遵循 MCP 原则的一致 API 设计,易于集成
  • 可扩展实现:适用于任何规模的项目,从小型应用到企业解决方案

🛠️ 支持的工具和 API

百度地图 MCP 服务器提供以下符合 MCP 规范的 API(工具):

工具名称 描述
map_geocode 将地址转换为地理坐标(地理编码)
map_reverse_geocode 根据坐标获取地址、区域和 POI 信息(逆地理编码)
map_search_places 通过关键字、类型、区域或半径范围内搜索全球 POI
map_place_details 根据唯一 ID 获取 POI 的详细信息
map_directions_matrix 批量路线规划,支持多个起点/终点(驾车、步行、骑行)
map_directions 两点之间的路线规划(驾车、步行、骑行、公交)
map_weather 根据区域或坐标查询实时天气和天气预报
map_ip_location 根据 IP 地址定位城市和坐标
map_road_traffic 查询道路或区域的实时交通状况
map_poi_extract* 从自由文本中提取 POI 信息(需要高级权限)

注意:某些高级功能需要额外权限。详情请参阅授权文档。

所有 API 都遵循 MCP 协议,可以从任何符合 MCP 规范的客户端、LLM 或智能体平台调用。


⚡ 快速开始

1. 获取 API Key

  1. 访问 百度地图开放平台
  2. 注册并创建服务端 API Key (AK)
  3. 重要:确保启用 "MCP (SSE)" 服务以获得最佳性能

2. Python 集成

安装 SDK
bash 复制代码
pip install mcp-server-baidu-maps
作为脚本运行
bash 复制代码
python -m mcp_server_baidu_maps
在 MCP 客户端中配置(如 Claude、Cursor)

在您的 MCP 客户端配置文件中添加:

json 复制代码
{
  "mcpServers": {
    "baidu-maps": {
      "command": "python",
      "args": ["-m", "mcp_server_baidu_maps"],
      "env": {
        "BAIDU_MAPS_API_KEY": "<YOUR_API_KEY>"
      }
    }
  }
}

3. Node.js/TypeScript 集成

安装
bash 复制代码
npm install @baidumap/mcp-server-baidu-map
在 MCP 客户端中配置
json 复制代码
{
  "mcpServers": {
    "baidu-map": {
      "command": "npx",
      "args": [
        "-y",
        "@baidumap/mcp-server-baidu-map"
      ],
      "env": {
        "BAIDU_MAP_API_KEY": "<YOUR_API_KEY>"
      }
    }
  }
}

4. 推荐:使用 SSE 实现低延迟、稳定访问

SSE(Server-Sent Events)是推荐的访问方式,提供更好的性能和稳定性。详情请参阅 SSE 快速开始指南

5. 更多平台集成

  • Claude/Agent/千帆AppBuilder :详细的中文集成指南和高级配置请参阅 README_zh.md

🚀 高级用例

1. 旅行规划助手

使用 map_search_placesmap_directionsmap_weather 构建一个智能体,能够:

  • 规划最优观光路线
  • 检查天气情况
  • 推荐 POI(兴趣点)

示例场景

复制代码
用户:"帮我规划北京三日游,包括天安门、故宫、颐和园"
智能体:
1. 使用 map_search_places 搜索这些景点
2. 使用 map_directions 规划最优路线
3. 使用 map_weather 检查天气
4. 生成完整的旅行计划

2. 批量路线矩阵

使用 map_directions_matrix 计算多条路线和时长,用于:

  • 物流优化
  • 配送路径规划
  • 多起点/多终点的路线分析

应用场景

  • 外卖配送:计算多个餐厅到多个客户的配送时间
  • 物流调度:优化车辆分配和路线规划
  • 网约车:匹配最近的司机和乘客

3. 文本到 POI 提取

使用 map_poi_extract 从用户输入或旅行笔记中提取 POI 信息。

注意:此功能需要高级权限。

示例

复制代码
输入文本:"我昨天去了三里屯的星巴克,然后去了国贸的苹果店"
输出:提取出两个 POI(星巴克、苹果店)及其位置信息

4. 实时交通和天气感知导航

结合 map_road_trafficmap_weather 实现:

  • 动态路线规划
  • 上下文感知的旅行建议
  • 实时路况避让
  • 天气预警

5. 与 Claude、千帆、AppBuilder 集成

无缝连接百度地图 MCP 服务器到 LLM 和智能体框架,实现自然语言地理空间推理。


⛰️ 高级教程


📦 安装和依赖

Python 环境要求

  • Python 3.7+
  • pip

Node.js 环境要求

  • Node.js 14+
  • npm 或 yarn

依赖项

Python

  • mcp-server-baidu-maps - 百度地图 MCP 服务器 Python SDK

Node.js

  • @baidumap/mcp-server-baidu-map - 百度地图 MCP 服务器 TypeScript/JavaScript SDK

🔧 配置说明

环境变量

变量名 说明 必需
BAIDU_MAPS_API_KEY 百度地图 API Key(Python)
BAIDU_MAP_API_KEY 百度地图 API Key(Node.js)

配置示例

Python 配置
bash 复制代码
export BAIDU_MAPS_API_KEY="your_api_key_here"
python -m mcp_server_baidu_maps
Node.js 配置
bash 复制代码
export BAIDU_MAP_API_KEY="your_api_key_here"
npx -y @baidumap/mcp-server-baidu-map

📊 项目统计

  • GitHub Stars: 379 ⭐
  • Forks: 44
  • 贡献者: 6
  • 编程语言 :
    • Python: 55.9%
    • JavaScript: 44.1%
  • 许可证: MIT License

👥 贡献者

感谢以下贡献者:

  • @Pineapple274
  • @yintaizhou
  • @TommyZihao
  • @ligaofeng0901
  • @9a-aaaaaaaa
  • @m1101322632

📄 许可证

本项目采用 MIT License 开源许可证。


🔗 相关链接


💡 使用建议

最佳实践

  1. 使用 SSE 访问方式:推荐使用 Server-Sent Events 方式访问,获得更好的性能和稳定性
  2. API Key 安全:不要在代码中硬编码 API Key,使用环境变量或配置文件
  3. 错误处理:实现适当的错误处理和重试机制
  4. 缓存策略:对于不经常变化的数据(如 POI 信息),考虑实现缓存机制
  5. 请求频率:注意 API 调用频率限制,避免超出配额

性能优化

  • 使用批量 API(如 map_directions_matrix)减少请求次数
  • 实现合理的缓存策略
  • 使用 SSE 方式访问以获得更好的性能
  • 考虑异步处理长时间运行的操作

安全建议

  • 保护 API Key,不要提交到版本控制系统
  • 使用环境变量或密钥管理服务
  • 实现适当的访问控制和权限管理
  • 监控 API 使用情况,防止滥用

🐛 问题反馈

如果遇到问题或有建议,请通过以下方式反馈:

  1. GitHub Issues : https://github.com/baidu-maps/mcp/issues
  2. 官方文档 : 查看 FAQ

📝 更新日志

最新版本: v0.2.4 (2025年8月22日)

查看 GitHub Releases 获取详细的版本更新信息。


最后更新 : 2025年1月
文档版本: 1.0

安装node

Node.js 安装指南(macOS)

目录

  1. 概述
  2. 安装方法
  3. 验证安装
  4. 版本管理
  5. 常见问题
  6. [卸载 Node.js](#卸载 Node.js)
  7. 推荐方案

概述

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,用于构建服务器端和网络应用程序。npm(Node Package Manager)是 Node.js 的包管理器,随 Node.js 一起安装。

系统要求

  • 操作系统:macOS 10.15 或更高版本
  • 架构:支持 Intel 和 Apple Silicon (M1/M2/M3)
  • 磁盘空间:至少 500MB 可用空间

版本说明

  • LTS(Long Term Support):长期支持版本,推荐生产环境使用
  • Current:最新版本,包含最新特性,可能不够稳定
  • 推荐版本:Node.js 20.x LTS 或 Node.js 22.x LTS

安装方法

方法一:使用 Homebrew(推荐)

Homebrew 是 macOS 上最流行的包管理器,安装简单,管理方便。

步骤 1:检查是否已安装 Homebrew
bash 复制代码
which brew

如果输出路径(如 /opt/homebrew/bin/brew),说明已安装。如果未安装,先安装 Homebrew:

bash 复制代码
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
步骤 2:安装 Node.js
bash 复制代码
# 安装最新 LTS 版本
brew install node

# 或者安装特定版本
brew install node@20
步骤 3:验证安装
bash 复制代码
node --version
npm --version
优点

✅ 安装简单,一条命令即可

✅ 自动管理依赖

✅ 易于更新和卸载

✅ 与系统包管理器集成

缺点

❌ 需要先安装 Homebrew

❌ 版本切换不够灵活


方法二:使用 nvm(Node Version Manager,推荐用于多版本管理)

nvm 允许你在同一台机器上安装和管理多个 Node.js 版本,非常适合开发环境。

步骤 1:安装 nvm
bash 复制代码
# 使用 curl 安装
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

# 或者使用 wget
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
步骤 2:配置 Shell

安装脚本会自动将 nvm 配置添加到 ~/.zshrc~/.bash_profile

重新加载配置:

bash 复制代码
# 如果使用 zsh(macOS 默认)
source ~/.zshrc

# 如果使用 bash
source ~/.bash_profile
步骤 3:验证 nvm 安装
bash 复制代码
nvm --version
步骤 4:安装 Node.js
bash 复制代码
# 安装最新 LTS 版本
nvm install --lts

# 安装最新版本
nvm install node

# 安装特定版本
nvm install 20.11.0
nvm install 18.19.0
步骤 5:使用 Node.js 版本
bash 复制代码
# 使用最新 LTS 版本
nvm use --lts

# 使用特定版本
nvm use 20.11.0

# 设置为默认版本
nvm alias default 20.11.0
常用命令
bash 复制代码
# 列出已安装的版本
nvm list

# 列出所有可用版本
nvm list-remote

# 查看当前使用的版本
nvm current

# 卸载指定版本
nvm uninstall 18.19.0
优点

✅ 可以安装和管理多个 Node.js 版本

✅ 版本切换方便

✅ 适合不同项目使用不同版本

✅ 不污染系统环境

缺点

❌ 需要手动配置 Shell

❌ 每次新开终端可能需要切换版本


方法三:使用 fnm(Fast Node Manager)

fnm 是 nvm 的快速替代品,使用 Rust 编写,性能更好。

步骤 1:安装 fnm
bash 复制代码
# 使用 Homebrew 安装
brew install fnm

# 或者使用 curl
curl -fsSL https://fnm.vercel.app/install | bash
步骤 2:配置 Shell
bash 复制代码
# 添加到 ~/.zshrc
echo 'eval "$(fnm env --use-on-cd)"' >> ~/.zshrc

# 重新加载配置
source ~/.zshrc
步骤 3:安装 Node.js
bash 复制代码
# 安装最新 LTS 版本
fnm install --lts

# 安装特定版本
fnm install 20
fnm install 18

# 使用版本
fnm use 20

# 设置为默认版本
fnm default 20
优点

✅ 性能比 nvm 更快

✅ 自动切换版本(根据项目中的 .nvmrc 文件)

✅ 安装简单

缺点

❌ 社区相对较小

❌ 功能不如 nvm 丰富


方法四:官方安装包

直接从 Node.js 官网下载安装包,适合不熟悉命令行的用户。

步骤 1:访问官网

访问 Node.js 官网:https://nodejs.org/

步骤 2:下载安装包
  • 选择 LTS 版本 (推荐)或 Current 版本
  • 下载 macOS 安装包(.pkg 文件)
  • 根据系统架构选择:
    • Intel 芯片:x64 版本
    • Apple Silicon:ARM64 版本
步骤 3:安装
  1. 双击下载的 .pkg 文件
  2. 按照安装向导完成安装
  3. 安装完成后,Node.js 和 npm 会自动添加到系统 PATH
步骤 4:验证安装
bash 复制代码
node --version
npm --version
优点

✅ 图形界面,操作简单

✅ 官方提供,稳定可靠

✅ 适合企业环境

缺点

❌ 无法管理多个版本

❌ 更新需要重新下载安装包

❌ 卸载相对复杂


方法五:使用 MacPorts

如果你使用 MacPorts 作为包管理器:

bash 复制代码
# 安装 Node.js
sudo port install nodejs20

# 或者安装最新版本
sudo port install nodejs

验证安装

基本验证

bash 复制代码
# 查看 Node.js 版本
node --version
# 或
node -v

# 查看 npm 版本
npm --version
# 或
npm -v

# 查看安装路径
which node
which npm

# 查看 Node.js 信息
node -p "process.versions"

运行测试

创建一个测试文件 test.js

javascript 复制代码
console.log('Node.js 安装成功!');
console.log('Node.js 版本:', process.version);
console.log('npm 版本:', require('child_process').execSync('npm -v').toString().trim());

运行测试:

bash 复制代码
node test.js

预期输出

复制代码
Node.js 安装成功!
Node.js 版本: v20.11.0
npm 版本: 10.2.4

版本管理

使用 nvm 管理版本

bash 复制代码
# 安装多个版本
nvm install 18.19.0
nvm install 20.11.0
nvm install 22.0.0

# 列出已安装的版本
nvm list

# 切换版本
nvm use 20.11.0

# 设置默认版本
nvm alias default 20.11.0

# 在项目目录中创建 .nvmrc 文件
echo "20.11.0" > .nvmrc

# 自动使用 .nvmrc 中指定的版本
nvm use

使用 fnm 管理版本

bash 复制代码
# 安装多个版本
fnm install 18
fnm install 20
fnm install 22

# 列出已安装的版本
fnm list

# 切换版本
fnm use 20

# 设置默认版本
fnm default 20

# 在项目目录中创建 .node-version 文件
echo "20.11.0" > .node-version

# 自动使用 .node-version 中指定的版本
fnm use

常见问题

问题 1:命令未找到(command not found)

原因:Node.js 未正确添加到 PATH

解决方法

bash 复制代码
# 检查 Node.js 安装路径
which node

# 如果使用 nvm,确保已加载配置
source ~/.zshrc

# 如果使用 Homebrew,检查 PATH
echo $PATH | grep -o '/opt/homebrew/bin'

问题 2:权限错误(Permission denied)

原因:npm 全局包安装权限问题

解决方法

bash 复制代码
# 方法 1:使用 npm 配置(推荐)
mkdir ~/.npm-global
npm config set prefix '~/.npm-global'
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.zshrc
source ~/.zshrc

# 方法 2:使用 sudo(不推荐)
sudo npm install -g <package>

问题 3:版本冲突

原因:系统中存在多个 Node.js 安装

解决方法

bash 复制代码
# 检查所有 Node.js 安装位置
which -a node

# 使用 nvm 管理,避免冲突
nvm use <version>

# 或者卸载其他安装方式
brew uninstall node  # 如果使用 Homebrew 安装

问题 4:npm 版本过旧

解决方法

bash 复制代码
# 更新 npm 到最新版本
npm install -g npm@latest

# 或者更新到特定版本
npm install -g npm@10.2.4

问题 5:nvm 命令不生效

解决方法

bash 复制代码
# 检查 nvm 是否已加载
type nvm

# 手动加载 nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# 添加到 ~/.zshrc
cat >> ~/.zshrc << 'EOF'
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
EOF

source ~/.zshrc

问题 6:安装速度慢

解决方法

bash 复制代码
# 使用国内镜像(npm)
npm config set registry https://registry.npmmirror.com

# 或者使用淘宝镜像
npm config set registry https://registry.npm.taobao.org

# 恢复官方镜像
npm config set registry https://registry.npmjs.org

# 查看当前镜像
npm config get registry

卸载 Node.js

如果使用 Homebrew 安装

bash 复制代码
# 卸载 Node.js
brew uninstall node

# 清理残留文件
brew cleanup

如果使用 nvm 安装

bash 复制代码
# 卸载特定版本
nvm uninstall 20.11.0

# 卸载所有版本并删除 nvm
rm -rf ~/.nvm
# 然后从 ~/.zshrc 或 ~/.bash_profile 中删除 nvm 相关配置

如果使用官方安装包

bash 复制代码
# 删除 Node.js 可执行文件
sudo rm -rf /usr/local/bin/node
sudo rm -rf /usr/local/bin/npm
sudo rm -rf /usr/local/lib/node_modules

# 删除配置文件
rm -rf ~/.npm
rm -rf ~/.node-gyp

完全清理

bash 复制代码
# 删除所有 Node.js 相关文件
sudo rm -rf /usr/local/bin/node
sudo rm -rf /usr/local/bin/npm
sudo rm -rf /usr/local/lib/node_modules
sudo rm -rf /usr/local/include/node
sudo rm -rf ~/.npm
sudo rm -rf ~/.node-gyp
sudo rm -rf ~/.nvm

推荐方案

场景 1:日常开发(推荐 nvm)

原因

  • 可以管理多个 Node.js 版本
  • 不同项目可以使用不同版本
  • 切换版本方便

安装步骤

bash 复制代码
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.zshrc
nvm install --lts
nvm use --lts
nvm alias default node

场景 2:简单使用(推荐 Homebrew)

原因

  • 安装简单,一条命令
  • 自动管理依赖
  • 适合只需要一个版本的用户

安装步骤

bash 复制代码
brew install node

场景 3:企业环境(推荐官方安装包)

原因

  • 官方提供,稳定可靠
  • 图形界面,操作简单
  • 适合统一部署

安装步骤

  1. 访问 https://nodejs.org/
  2. 下载 LTS 版本安装包
  3. 双击安装

场景 4:性能要求高(推荐 fnm)

原因

  • 性能比 nvm 更快
  • 自动切换版本
  • 适合大型项目

安装步骤

bash 复制代码
brew install fnm
echo 'eval "$(fnm env --use-on-cd)"' >> ~/.zshrc
source ~/.zshrc
fnm install --lts

快速参考

安装命令速查

方法 安装命令 版本管理
Homebrew brew install node
nvm nvm install --lts
fnm fnm install --lts
官方安装包 下载 .pkg 文件

关闭vs code , 再次打开 。

配置百度地图需要查询的权限信息

如果不知道 ,就全部勾上 。

百度上输入ip

在vscode 的mcp 服务中输入查ip

查ip : 36.21.87.91 信息

得出数据

另外一个mcp 服务 ,文件相关的操作。

MCP 文件系统服务器文档

项目地址 : https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
MCP 官方仓库 : https://github.com/modelcontextprotocol/servers
MCP 协议: Model Context Protocol (MCP) 是由 Anthropic 于 2024 年 11 月推出的开放标准


📖 项目介绍

MCP 文件系统服务器是 Model Context Protocol (MCP) 官方提供的参考服务器实现之一,它允许 AI 模型(如大型语言模型 LLM)通过标准化的 MCP 协议安全地访问和操作文件系统资源。

什么是 MCP?

Model Context Protocol (MCP) 是一个开放标准和开源框架,旨在标准化 AI 系统(如 LLM)与外部工具、系统和数据源的集成方式。MCP 提供了通用接口,支持:

  • 读取文件
  • 执行函数
  • 处理上下文提示
  • 访问外部数据源

MCP 已被 OpenAI、Google DeepMind 等主要 AI 提供商采纳,成为 AI 系统与外部世界交互的标准协议。

核心特性

  • 标准化接口:基于 MCP 协议,提供统一的文件系统访问接口
  • 安全访问:通过权限管理和安全机制,确保文件系统操作的安全性
  • 跨平台支持:支持多种操作系统和文件系统
  • 易于集成:可以轻松集成到支持 MCP 的 AI 应用和平台中
  • 开源实现:MIT 许可证,可自由使用和修改

🏗️ 技术原理

架构设计

复制代码
┌─────────────────────────────────────────────────────────┐
│                    MCP Client (AI Model)                 │
│  - 发送文件操作请求                                        │
│  - 接收操作结果                                           │
│  - 处理文件内容                                           │
└──────────────────┬──────────────────────────────────────┘
                   │ MCP Protocol (JSON-RPC)
                   │
        ┌──────────┴──────────┐
        │                     │
   ┌────▼────┐          ┌─────▼─────┐
   │ STDIO   │          │   SSE     │
   │ Server  │          │  Server   │
   └────┬────┘          └─────┬─────┘
        │                     │
        └──────────┬──────────┘
                   │
        ┌──────────▼──────────┐
        │  Filesystem Server   │
        │  - 文件读取           │
        │  - 文件写入           │
        │  - 目录操作           │
        │  - 权限管理           │
        └──────────┬──────────┘
                   │
        ┌──────────▼──────────┐
        │   File System        │
        │   (本地/远程)         │
        └─────────────────────┘

工作原理

1. 协议层

MCP 文件系统服务器基于 JSON-RPC 2.0 协议,通过以下方式与客户端通信:

  • STDIO 传输:通过标准输入/输出进行通信(适合本地进程)
  • SSE 传输:通过 Server-Sent Events 进行通信(适合 Web 应用)
2. 请求处理流程
复制代码
1. AI 模型发送文件操作请求
   ↓
2. MCP 客户端将请求转换为 JSON-RPC 消息
   ↓
3. 通过传输层(STDIO/SSE)发送到服务器
   ↓
4. 文件系统服务器解析请求
   ↓
5. 执行相应的文件系统操作
   ↓
6. 将结果序列化为 JSON-RPC 响应
   ↓
7. 返回给 AI 模型
3. 支持的操作

文件系统服务器通常支持以下操作:

操作类型 功能描述 示例
读取文件 读取文件内容 读取代码文件、配置文件
写入文件 创建或修改文件 保存代码、写入日志
列出目录 获取目录内容 浏览项目结构
创建目录 创建新目录 创建项目文件夹
删除文件/目录 删除文件或目录 清理临时文件
文件信息 获取文件元数据 文件大小、修改时间
搜索文件 按模式搜索文件 查找特定类型的文件
4. 安全机制
  • 路径限制:限制可访问的目录范围
  • 权限检查:验证操作权限
  • 沙箱隔离:在隔离环境中执行操作
  • 操作审计:记录所有文件操作

🚀 使用方式

前置要求

  • Node.js 14+ 或 Python 3.8+(取决于实现语言)
  • 支持 MCP 协议的 AI 客户端(如 Claude Desktop、Cursor 等)
  • 基本的命令行操作知识

安装步骤

方法 1:从 GitHub 克隆(推荐)
bash 复制代码
# 克隆 MCP servers 仓库
git clone https://github.com/modelcontextprotocol/servers.git

# 进入文件系统服务器目录
cd servers/src/filesystem

# 查看 README 了解具体安装步骤
cat README.md
方法 2:使用 npm/pip 安装(如果已发布到包管理器)
bash 复制代码
# Node.js 版本(如果可用)
npm install @modelcontextprotocol/server-filesystem

# Python 版本(如果可用)
pip install mcp-server-filesystem

配置 MCP 客户端

Claude Desktop 配置

编辑 Claude Desktop 的配置文件(位置因操作系统而异):

macOS : ~/Library/Application Support/Claude/claude_desktop_config.json
Windows : %APPDATA%\Claude\claude_desktop_config.json
Linux : ~/.config/Claude/claude_desktop_config.json

json 复制代码
{
  "mcpServers": {
    "filesystem": {
      "command": "node",
      "args": [
        "/path/to/servers/src/filesystem/index.js"
      ],
      "env": {
        "ALLOWED_DIRECTORIES": "/path/to/allowed/directory"
      }
    }
  }
}
Cursor 配置

在 Cursor 的 MCP 配置文件中添加:

json 复制代码
{
  "mcpServers": {
    "filesystem": {
      "command": "python",
      "args": [
        "-m",
        "mcp_server_filesystem"
      ],
      "env": {
        "ALLOWED_DIRECTORIES": "/path/to/allowed/directory"
      }
    }
  }
}

环境变量配置

变量名 说明 必需 默认值
ALLOWED_DIRECTORIES 允许访问的目录列表(逗号分隔)
READ_ONLY 是否只读模式 false
MAX_FILE_SIZE 最大文件大小(字节) 10485760 (10MB)

使用示例

示例 1:读取文件

AI 模型可以通过自然语言请求读取文件:

复制代码
用户:"请读取项目根目录下的 README.md 文件"
AI:调用 filesystem.read_file("README.md")
结果:返回文件内容
示例 2:列出目录
复制代码
用户:"显示 src 目录下的所有文件"
AI:调用 filesystem.list_directory("src")
结果:返回文件列表
示例 3:写入文件
复制代码
用户:"创建一个新的配置文件 config.json"
AI:调用 filesystem.write_file("config.json", "{...}")
结果:文件创建成功
示例 4:搜索文件
复制代码
用户:"查找所有 .java 文件"
AI:调用 filesystem.search_files("*.java")
结果:返回匹配的文件列表

命令行使用

如果服务器支持命令行模式:

bash 复制代码
# 启动服务器(STDIO 模式)
node src/filesystem/index.js

# 或使用 Python
python -m mcp_server_filesystem

💼 业务价值

1. 增强 AI 模型能力

价值:使 AI 模型能够直接访问和操作文件系统,扩展其应用范围。

应用场景

  • 代码助手:AI 可以读取、分析和修改代码文件
  • 文档处理:自动读取和处理文档,生成摘要或翻译
  • 数据分析:读取数据文件,进行数据分析和报告生成
  • 项目管理:浏览项目结构,理解项目架构

示例

复制代码
场景:AI 代码审查助手
- AI 读取代码文件
- 分析代码质量
- 生成审查报告
- 提出改进建议

2. 标准化接口

价值:通过 MCP 提供统一的接口,简化了与文件系统的集成过程。

优势

  • 一致性:所有支持 MCP 的 AI 模型都可以使用相同的接口
  • 可移植性:代码可以在不同的 AI 平台间复用
  • 降低复杂度:无需为每个 AI 模型单独实现文件系统访问逻辑

对比

复制代码
传统方式:
- 每个 AI 平台需要单独实现文件系统访问
- 接口不统一,维护成本高
- 代码难以复用

MCP 方式:
- 统一的 MCP 接口
- 一次实现,多处使用
- 降低开发和维护成本

3. 提高开发效率

价值:开发者无需重复实现文件系统访问逻辑,可以专注于业务功能。

效率提升

  • 减少重复代码:不需要为每个项目重新实现文件操作
  • 快速集成:通过配置即可集成文件系统功能
  • 专注业务:将精力集中在核心业务逻辑上

时间节省

复制代码
传统开发:
- 实现文件读取:2-3 天
- 实现文件写入:2-3 天
- 实现目录操作:1-2 天
- 实现权限管理:2-3 天
总计:7-11 天

使用 MCP:
- 配置和集成:0.5 天
- 测试和调试:0.5 天
总计:1 天

4. 安全性保障

价值:通过 MCP 的权限管理和安全机制,确保文件系统操作的安全性和合规性。

安全特性

  • 路径限制:只能访问指定的目录
  • 权限控制:可以设置只读模式
  • 操作审计:记录所有文件操作
  • 沙箱隔离:在隔离环境中执行操作

合规性

  • 符合企业安全策略
  • 支持审计和监控
  • 防止未授权访问
  • 保护敏感数据

5. 业务场景应用

场景 1:智能代码审查

需求:自动审查代码质量,生成审查报告

实现

  1. AI 读取代码文件
  2. 分析代码结构、风格、潜在问题
  3. 生成详细的审查报告
  4. 提出改进建议

价值

  • 提高代码质量
  • 减少人工审查时间
  • 统一代码标准
场景 2:自动化文档生成

需求:根据代码自动生成 API 文档

实现

  1. AI 读取源代码
  2. 分析函数、类、接口
  3. 生成 Markdown 或 HTML 文档
  4. 保存到指定目录

价值

  • 保持文档与代码同步
  • 减少文档维护成本
  • 提高开发效率
场景 3:智能项目管理

需求:AI 助手帮助理解和管理项目

实现

  1. AI 浏览项目目录结构
  2. 分析项目架构
  3. 回答项目相关问题
  4. 协助项目重构

价值

  • 快速理解大型项目
  • 提高团队协作效率
  • 降低项目维护成本
场景 4:数据分析和报告

需求:读取数据文件,生成分析报告

实现

  1. AI 读取 CSV、JSON 等数据文件
  2. 分析数据内容
  3. 生成可视化报告
  4. 保存报告文件

价值

  • 自动化数据分析
  • 快速生成报告
  • 提高决策效率

6. 成本效益分析

开发成本
项目 传统方式 MCP 方式 节省
开发时间 7-11 天 1 天 85-91%
维护成本 显著降低
集成复杂度 显著降低
运营成本
  • 减少错误:标准化接口减少实现错误
  • 提高可靠性:经过测试的参考实现
  • 降低风险:安全机制保护系统

🔧 高级配置

权限配置

json 复制代码
{
  "mcpServers": {
    "filesystem": {
      "command": "node",
      "args": ["/path/to/filesystem/index.js"],
      "env": {
        "ALLOWED_DIRECTORIES": "/home/user/projects,/home/user/documents",
        "READ_ONLY": "false",
        "MAX_FILE_SIZE": "52428800"
      }
    }
  }
}

多目录配置

json 复制代码
{
  "ALLOWED_DIRECTORIES": "/path/to/dir1,/path/to/dir2,/path/to/dir3"
}

只读模式

json 复制代码
{
  "READ_ONLY": "true"
}

📚 最佳实践

1. 安全配置

  • 限制访问范围:只允许访问必要的目录
  • 使用只读模式:对于不需要修改的场景
  • 设置文件大小限制:防止读取过大文件
  • 定期审计:检查文件操作日志

2. 性能优化

  • 缓存文件信息:减少重复的文件系统调用
  • 批量操作:合并多个文件操作
  • 异步处理:对于大文件使用异步读取

3. 错误处理

  • 优雅降级:文件操作失败时提供友好提示
  • 详细日志:记录操作和错误信息
  • 重试机制:对于临时性错误实现重试

🐛 常见问题

Q1: 如何限制 AI 只能访问特定目录?

A : 在配置中设置 ALLOWED_DIRECTORIES 环境变量,只包含允许的目录路径。

Q2: 可以同时使用多个文件系统服务器吗?

A: 可以,在配置中为每个服务器使用不同的名称即可。

Q3: 如何实现只读模式?

A : 设置环境变量 READ_ONLY=true

Q4: 支持哪些文件系统操作?

A: 通常支持读取、写入、列出目录、创建目录、删除等基本操作,具体取决于实现。

Q5: 如何处理大文件?

A : 可以设置 MAX_FILE_SIZE 限制,或使用流式处理。


🔗 相关资源


📝 总结

MCP 文件系统服务器为 AI 模型提供了标准化、安全、高效的文件系统访问能力。通过使用 MCP 协议,开发者可以:

  1. 快速集成:通过简单配置即可集成文件系统功能
  2. 提高效率:减少重复开发,专注于业务逻辑
  3. 确保安全:通过权限管理和安全机制保护系统
  4. 扩展能力:使 AI 模型能够处理更复杂的任务

无论是构建代码助手、文档生成工具,还是数据分析应用,MCP 文件系统服务器都能为您的 AI 应用提供强大的文件操作能力。


最后更新 : 2025年1月
文档版本 : 1.0
参考来源 : MCP Servers GitHub Repository

MCP使用---springai+公共mcp-server

mcp-servers-config.json

java 复制代码
{
  "mcpServers": {
    "baidu-map": {
      "command": "npx",
      "args": [
        "-y",
        "@baidumap/mcp-server-baidu-map"
      ],
      "env": {
        "BAIDU_MAP_API_KEY": "xxx",
        "NODE_OPTIONS": "--use-openssl-ca",
        "NODE_EXTRA_CA_CERTS": "/opt/homebrew/opt/ca-certificates/share/ca-certificates/cacert.pem"
      }
    }
  }
}

application.yml

java 复制代码
server:
  port: 8080
  servlet:
    encoding:
      charset: UTF-8
      enabled: true
      force: true

spring:
  ai:
    mcp:
      client:
        request-timeout: 60000
        stdio:
          # 1. 单独放在一个json文件(推荐)
          servers-configuration: classpath:/mcp/mcp-servers-config.json
#        sse:
#          connections:
#            UserSSEInfo:
#              url: http://localhost:8088
        # 2. 直接配置全局配置文件中
#          connections:
#            baidu-map:
#              command: cmd
#              args:
#                - /c
#                - npx
#                - -y
#                - @baidumap/mcp-server-baidu-map
#              env:
#                BAIDU_MAP_API_KEY: xxx

# 调试日志
logging:
  level:
    io:
      modelcontextprotocol:
        client: DEBUG
        spec: DEBUG

编写测试类

java 复制代码
package com.xs.ai.controller;


import com.xs.ai.services.ToolsService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.time.LocalDate;


/**
 * 智能航空客服控制器
 *
 * <p>该控制器提供了智能航空客服的核心功能,集成了多种 AI 技术:
 * <ul>
 *   <li><b>RAG(检索增强生成)</b>:基于服务条款文档回答问题</li>
 *   <li><b>工具调用(Function Calling)</b>:AI 可以调用退票、查询航班等业务方法</li>
 *   <li><b>对话记忆(Chat Memory)</b>:保持上下文,支持多轮对话</li>
 *   <li><b>流式响应(SSE)</b>:实时返回 AI 回复,提升用户体验</li>
 * </ul>
 *
 * <p><b>核心功能:</b>
 * <ul>
 *   <li>智能客服对话:自然语言交互,解答航班预订相关问题</li>
 *   <li>知识库问答:基于服务条款文档回答政策性问题(如退票费用、改签规则等)</li>
 *   <li>业务操作:AI 可以调用工具执行退票、查询航班等操作</li>
 *   <li>安全确认:涉及增删改操作时,需要用户确认后才执行</li>
 * </ul>
 *
 * @author wx:程序员徐庶
 * @version 1.0
 * @see com.xs.ai.services.ToolsService 工具服务,定义可被 AI 调用的业务方法
 * @see com.xs.ai.config.RagConfig RAG 配置,向量存储配置
 * @see org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor QuestionAnswerAdvisor,RAG Advisor
 * @since 1.0
 */
@RestController
@CrossOrigin
public class OpenAiController {

    /**
     * ChatClient 实例,配置了 RAG、工具调用、对话记忆等功能
     */
    ChatClient chatClient;

    /**
     * 向量存储,用于 RAG 文档检索
     */
    VectorStore vectorStore;

    /**
     * 构造函数,初始化 ChatClient
     *
     * <p>在 Spring 容器创建该 Bean 时,通过构造函数注入的方式初始化 ChatClient。
     * 配置包括:
     * <ul>
     *   <li>System Prompt:定义 AI 角色和行为规范</li>
     *   <li>Advisors:
     *       <ul>
     *         <li>PromptChatMemoryAdvisor:自动管理对话记忆</li>
     *         <li>SimpleLoggerAdvisor:记录对话日志(DEBUG 级别)</li>
     *       </ul>
     *   </li>
     *   <li>Tools:注册工具服务,AI 可以调用退票、查询航班等方法</li>
     *   <li>ToolCallbacks:工具回调提供者</li>
     * </ul>
     *
     * <p><b>安全机制:</b>
     * System Prompt 中明确要求:在涉及增删改(除了查询)的 function-call 前,
     * 必须等用户回复"确认"后再调用 tool。这确保了关键操作的安全性。
     *
     * @param chatClientBuilder ChatClient 构建器,由 Spring AI 自动配置
     * @param chatMemory 对话记忆,用于保持上下文
     * @param toolsService 工具服务,包含可被 AI 调用的业务方法
     * @param toolCallbackProvider 工具回调提供者
     * @param vectorStore 向量存储,用于 RAG 文档检索
     */
    public OpenAiController(ChatClient.Builder chatClientBuilder,
                            ChatMemory chatMemory,
                            ToolsService toolsService,
                            ToolCallbackProvider toolCallbackProvider,
                            VectorStore vectorStore
                           ) {
        this.chatClient = chatClientBuilder
                .defaultSystem("""
						##角色
                          您是"图灵"航空公司的客户聊天支持代理。请以友好、乐于助人且愉快的方式来回复。
                          您正在通过在线聊天系统与客户互动。
                        ##要求
                          1.在涉及增删改(除了查询)function-call前,必须等用户回复"确认"后再调用tool。
                          2.请讲中文。
                          
                          今天的日期是 {current_date}.
					""")
                .defaultAdvisors(
                        PromptChatMemoryAdvisor.builder(chatMemory).build(),
                        new SimpleLoggerAdvisor())
                .defaultTools(toolsService)
                .defaultToolCallbacks(toolCallbackProvider)
                .build();


        this.vectorStore=vectorStore;
    }

    /**
     * 流式生成 AI 回复
     *
     * <p>该接口提供智能客服的流式对话功能。用户发送消息后,AI 会:
     * <ol>
     *   <li>使用 RAG 从 VectorStore 中检索相关文档片段(topK=5,相似度阈值 0.4)</li>
     *   <li>结合检索到的文档和用户消息,分析用户意图</li>
     *   <li>如果需要,调用相应的工具(如查询航班、退票)</li>
     *   <li>生成自然语言回复</li>
     *   <li>通过 SSE(Server-Sent Events)流式返回回复内容</li>
     * </ol>
     *
     * <p><b>RAG 检索配置:</b>
     * <ul>
     *   <li><b>topK=5</b>:返回相似度最高的 5 个文档片段</li>
     *   <li><b>similarityThreshold=0.4</b>:相似度阈值,低于此值的文档片段会被过滤</li>
     * </ul>
     *
     * <p><b>使用场景:</b>
     * <ul>
     *   <li><b>知识库问答</b>:用户问"退票的费用是多少?"
     *       <ul>
     *         <li>RAG 检索相关文档片段</li>
     *         <li>AI 基于文档内容生成回答</li>
     *       </ul>
     *   </li>
     *   <li><b>业务查询</b>:用户问"查询我的订单101,姓名徐庶"
     *       <ul>
     *         <li>AI 识别需要调用 getBookingDetails 工具</li>
     *         <li>调用工具获取航班信息</li>
     *         <li>生成包含查询结果的回复</li>
     *       </ul>
     *   </li>
     *   <li><b>业务操作</b>:用户说"我要退票,订单号101,姓名徐庶"
     *       <ul>
     *         <li>AI 识别需要调用 cancel 工具</li>
     *         <li>由于是增删改操作,AI 会先询问用户确认</li>
     *         <li>用户回复"确认"后,AI 调用工具执行退票</li>
     *         <li>生成包含退票结果的回复</li>
     *       </ul>
     *   </li>
     * </ul>
     *
     * <p><b>响应格式:</b>
     * <ul>
     *   <li>Content-Type: text/event-stream(SSE 格式)</li>
     *   <li>数据格式:流式文本,实时返回 AI 生成的每个词</li>
     * </ul>
     *
     * @param message 用户输入的消息,默认为"讲个笑话"
     * @return Flux&lt;String&gt; 流式响应,通过 SSE 实时返回 AI 生成的回复
     *
     * @see org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor QuestionAnswerAdvisor,RAG Advisor
     * @see org.springframework.ai.vectorstore.SearchRequest SearchRequest,RAG 检索请求
     * @see com.xs.ai.services.ToolsService 工具服务,定义可被调用的业务方法
     */
    @CrossOrigin
    // http://localhost:8080/ai/generateStreamAsString?message=讲个笑话
    // http://localhost:8080/ai/generateStreamAsString?message=查询我的订单101,姓名徐庶
    // http://localhost:8080/ai/generateStreamAsString?message=我要退票,订单号101,姓名徐庶
    // http://localhost:8080/ai/generateStreamAsString?message=退票的费用是多少?
    // http://localhost:8080/ai/generateStreamAsString?message=从长沙到武汉的骑行路线
    // http://localhost:8080/ai/generateStreamAsString?message=查询北京天气
//    @GetMapping(value = "/ai/generateStreamAsString", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    @GetMapping(value = "/ai/generateStreamAsString")
    public Flux<String> generateStreamAsString(
            @RequestParam(value = "message", defaultValue = "讲个笑话") String message) {

        return chatClient.prompt().user(message)
                .system(p -> p.param("current_date", LocalDate.now()))  // 注入当前日期
                .advisors(
                        // RAG Advisor:从 VectorStore 中检索相关文档片段
                        QuestionAnswerAdvisor.builder(vectorStore)
                                .searchRequest(
                                        SearchRequest.builder()
                                                .topK(5)  // 返回相似度最高的 5 个文档片段
                                                .similarityThreshold(0.4)  // 相似度阈值
                                                .build())
                                .build()
                )
                .stream().content();  // 流式返回内容
    }

}

注意

如果你是mac 电脑 ,但是使用时报如下错误 。

java 复制代码
2026-01-10T23:18:31.183+08:00 ERROR 95171 --- [oundedElastic-2] o.s.ai.chat.model.MessageAggregator      : Aggregation Error

java.lang.IllegalStateException: Stream processing failed
	at org.springframework.ai.chat.client.advisor.api.BaseAdvisor.lambda$adviseStream$2(BaseAdvisor.java:73) ~[spring-ai-client-chat-1.0.0.jar:1.0.0]
	at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:94) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxMap$MapSubscriber.onError(FluxMap.java:134) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.MonoFlatMapMany$FlatMapManyInner.onError(MonoFlatMapMany.java:256) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onError(FluxContextWrite.java:121) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onError(FluxDoFinally.java:119) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onError(FluxPeekFuseable.java:903) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxMap$MapConditionalSubscriber.onError(FluxMap.java:265) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onError(FluxPeekFuseable.java:903) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onError(FluxPeekFuseable.java:903) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onError(FluxPeekFuseable.java:903) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onError(FluxPeekFuseable.java:903) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxHandle$HandleConditionalSubscriber.onError(FluxHandle.java:430) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onError(FluxContextWrite.java:121) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onError(FluxDoFinally.java:119) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.onError(FluxPeekFuseable.java:553) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxPublishOn$PublishOnConditionalSubscriber.doError(FluxPublishOn.java:1112) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxPublishOn$PublishOnConditionalSubscriber.checkTerminated(FluxPublishOn.java:1137) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxPublishOn$PublishOnConditionalSubscriber.runAsync(FluxPublishOn.java:996) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxPublishOn$PublishOnConditionalSubscriber.run(FluxPublishOn.java:1079) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:84) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:37) ~[reactor-core-3.6.5.jar:3.6.5]
	at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264) ~[na:na]
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na]
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na]
	at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]
Caused by: java.lang.IllegalStateException: Error calling tool: [TextContent[audience=null, priority=null, text=Error: request to https://api.map.baidu.com/location/ip?ip=36.21.87.91&coor=bd09ll&ak=DFb9e0a2885ac1f125ff5328233c6688&from=node_mcp failed, reason: unable to get local issuer certificate]]
	at org.springframework.ai.mcp.SyncMcpToolCallback.call(SyncMcpToolCallback.java:118) ~[spring-ai-mcp-1.0.0.jar:1.0.0]
	at org.springframework.ai.mcp.SyncMcpToolCallback.call(SyncMcpToolCallback.java:126) ~[spring-ai-mcp-1.0.0.jar:1.0.0]
	at org.springframework.ai.model.tool.DefaultToolCallingManager.lambda$executeToolCall$5(DefaultToolCallingManager.java:224) ~[spring-ai-model-1.0.0.jar:1.0.0]
	at io.micrometer.observation.Observation.observe(Observation.java:565) ~[micrometer-observation-1.12.5.jar:1.12.5]
	at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCall(DefaultToolCallingManager.java:221) ~[spring-ai-model-1.0.0.jar:1.0.0]
	at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCalls(DefaultToolCallingManager.java:137) ~[spring-ai-model-1.0.0.jar:1.0.0]
	at com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel.lambda$internalStream$7(DashScopeChatModel.java:257) ~[spring-ai-alibaba-core-1.0.0.2.jar:1.0.0.2]
	at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:46) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxSubscribeOn$SubscribeOnSubscriber.run(FluxSubscribeOn.java:194) ~[reactor-core-3.6.5.jar:3.6.5]
	... 8 common frames omitted

2026-01-10T23:18:31.190+08:00 ERROR 95171 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] threw exception

java.lang.IllegalStateException: Error calling tool: [TextContent[audience=null, priority=null, text=Error: request to https://api.map.baidu.com/location/ip?ip=36.21.87.91&coor=bd09ll&ak=DFb9e0a2885ac1f125ff5328233c6688&from=node_mcp failed, reason: unable to get local issuer certificate]]
	at org.springframework.ai.mcp.SyncMcpToolCallback.call(SyncMcpToolCallback.java:118) ~[spring-ai-mcp-1.0.0.jar:1.0.0]
	at org.springframework.ai.mcp.SyncMcpToolCallback.call(SyncMcpToolCallback.java:126) ~[spring-ai-mcp-1.0.0.jar:1.0.0]
	at org.springframework.ai.model.tool.DefaultToolCallingManager.lambda$executeToolCall$5(DefaultToolCallingManager.java:224) ~[spring-ai-model-1.0.0.jar:1.0.0]
	at io.micrometer.observation.Observation.observe(Observation.java:565) ~[micrometer-observation-1.12.5.jar:1.12.5]
	at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCall(DefaultToolCallingManager.java:221) ~[spring-ai-model-1.0.0.jar:1.0.0]
	at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCalls(DefaultToolCallingManager.java:137) ~[spring-ai-model-1.0.0.jar:1.0.0]
	at com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel.lambda$internalStream$7(DashScopeChatModel.java:257) ~[spring-ai-alibaba-core-1.0.0.2.jar:1.0.0.2]
	at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:46) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxSubscribeOn$SubscribeOnSubscriber.run(FluxSubscribeOn.java:194) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:84) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:37) ~[reactor-core-3.6.5.jar:3.6.5]
	at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264) ~[na:na]
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na]
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na]
	at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]

2026-01-10T23:18:31.190+08:00 ERROR 95171 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalStateException: Stream processing failed] with root cause

java.lang.IllegalStateException: Error calling tool: [TextContent[audience=null, priority=null, text=Error: request to https://api.map.baidu.com/location/ip?ip=36.21.87.91&coor=bd09ll&ak=DFb9e0a2885ac1f125ff5328233c6688&from=node_mcp failed, reason: unable to get local issuer certificate]]
	at org.springframework.ai.mcp.SyncMcpToolCallback.call(SyncMcpToolCallback.java:118) ~[spring-ai-mcp-1.0.0.jar:1.0.0]
	at org.springframework.ai.mcp.SyncMcpToolCallback.call(SyncMcpToolCallback.java:126) ~[spring-ai-mcp-1.0.0.jar:1.0.0]
	at org.springframework.ai.model.tool.DefaultToolCallingManager.lambda$executeToolCall$5(DefaultToolCallingManager.java:224) ~[spring-ai-model-1.0.0.jar:1.0.0]
	at io.micrometer.observation.Observation.observe(Observation.java:565) ~[micrometer-observation-1.12.5.jar:1.12.5]
	at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCall(DefaultToolCallingManager.java:221) ~[spring-ai-model-1.0.0.jar:1.0.0]
	at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCalls(DefaultToolCallingManager.java:137) ~[spring-ai-model-1.0.0.jar:1.0.0]
	at com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel.lambda$internalStream$7(DashScopeChatModel.java:257) ~[spring-ai-alibaba-core-1.0.0.2.jar:1.0.0.2]
	at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:46) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.publisher.FluxSubscribeOn$SubscribeOnSubscriber.run(FluxSubscribeOn.java:194) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:84) ~[reactor-core-3.6.5.jar:3.6.5]
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:37) ~[reactor-core-3.6.5.jar:3.6.5]
	at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264) ~[na:na]
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na]
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na]
	at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]

解决办法 :

MCP 证书验证错误解决方案

问题描述

在使用百度地图 MCP 服务器时,遇到以下错误:

复制代码
Error: request to https://api.map.baidu.com/weather/v1/... failed, 
reason: unable to get local issuer certificate

错误原因

这是一个 SSL/TLS 证书验证失败的问题。Node.js 在发起 HTTPS 请求时,无法验证服务器的 SSL 证书,通常是因为:

  1. 缺少 CA 证书:系统缺少证书颁发机构(CA)的根证书
  2. 证书路径配置错误:Node.js 无法找到正确的证书存储位置
  3. 网络环境问题:代理、防火墙或企业网络环境导致证书验证失败
  4. Node.js 版本问题:旧版本的 Node.js 可能存在证书验证问题

解决方案

方案 1:更新 CA 证书(推荐)

macOS 系统
  1. 更新系统证书

    bash 复制代码
    # 更新 Homebrew(如果使用)
    brew update
    
    # 安装/更新 CA 证书
    brew install ca-certificates
  2. 证书文件位置

    Homebrew 安装的 ca-certificates 证书文件位置:

    • Apple Silicon (M1/M2/M3) Mac:

      复制代码
      /opt/homebrew/opt/ca-certificates/share/ca-certificates/cacert.pem
    • Intel Mac:

      复制代码
      /usr/local/opt/ca-certificates/share/ca-certificates/cacert.pem

    验证证书文件是否存在

    bash 复制代码
    # Apple Silicon Mac
    ls -la /opt/homebrew/opt/ca-certificates/share/ca-certificates/cacert.pem
    
    # Intel Mac
    ls -la /usr/local/opt/ca-certificates/share/ca-certificates/cacert.pem
  3. 设置环境变量

    bash 复制代码
    # 在 ~/.zshrc 或 ~/.bash_profile 中添加
    # Apple Silicon Mac
    export NODE_EXTRA_CA_CERTS=/opt/homebrew/opt/ca-certificates/share/ca-certificates/cacert.pem
    
    # Intel Mac
    export NODE_EXTRA_CA_CERTS=/usr/local/opt/ca-certificates/share/ca-certificates/cacert.pem
    
    # 重新加载配置
    source ~/.zshrc  # 或 source ~/.bash_profile

    或者使用 Homebrew 路径变量(推荐):

    bash 复制代码
    # 自动检测 Homebrew 安装路径
    export NODE_EXTRA_CA_CERTS=$(brew --prefix ca-certificates)/share/ca-certificates/cacert.pem

修改 MCP 服务器配置

如果使用配置文件,可以在配置中添加 SSL 选项:

json 复制代码
{
  "mcpServers": {
    "baidu-map": {
      "command": "npx",
      "args": ["-y", "@baidumap/mcp-server-baidu-map"],
      "env": {
        "BAIDU_MAP_API_KEY": "<YOUR_API_KEY>",
        "NODE_OPTIONS": "--use-openssl-ca",
        "NODE_EXTRA_CA_CERTS": "/etc/ssl/certs/ca-certificates.crt"
      }
    }
  }
}

最后在浏览器中输入

结果输出:

Spring AI MCP STDIO 服务器使用指南

项目路径 : 07mcp-stdio-server
核心类 : McpServerApplication.java, OpenMeteoService.java
官方文档 : Spring AI MCP Documentation


📖 目录

  1. [什么是 STDIO 传输方式](#什么是 STDIO 传输方式)
  2. 快速开始
  3. 项目结构
  4. 核心组件详解
  5. 配置说明
  6. 使用方式
  7. 工作原理
  8. 最佳实践
  9. 常见问题

什么是 STDIO 传输方式

STDIO 简介

STDIO(Standard Input/Output) 是标准输入/输出传输方式,是 MCP 协议支持的一种通信方式。

特点

  • 简单直接:通过标准输入(stdin)和标准输出(stdout)进行通信
  • 进程隔离:服务器作为子进程被客户端启动和管理
  • 资源占用小:不需要 Web 服务器,资源占用低
  • 安全性高:进程隔离,安全性好
  • 启动快速:无需等待 Web 服务器启动

适用场景

  • 本地进程间通信
  • 嵌入式工具集成
  • CLI 工具集成
  • 单客户端场景
  • 需要快速启动的场景

与 SSE 的区别

特性 STDIO SSE
通信方式 stdin/stdout HTTP/SSE
进程模型 子进程 独立服务器
网络需求 无需网络 需要网络
多客户端 不支持 支持
资源占用 较高
启动速度 较慢
适用场景 本地工具 Web 应用

快速开始

1. 环境要求

  • Java 17 或更高版本
  • Maven 3.6+
  • Spring Boot 3.x
  • Spring AI 1.0.0+

2. 项目依赖

pom.xml 中已包含以下依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>

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

3. 构建项目

bash 复制代码
cd 07mcp-stdio-server
./mvnw clean install -DskipTests

构建完成后,会在 target 目录生成 JAR 文件:

复制代码
target/07mcp-stdio-server-0.0.1-xs.jar

项目结构

复制代码
07mcp-stdio-server/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/xs/ai/mcp/sse/server/
│   │   │       ├── McpServerApplication.java    # 主应用类
│   │   │       └── OpenMeteoService.java        # 天气服务实现
│   │   └── resources/
│   │       └── application.yml                  # 应用配置
│   └── test/
│       └── java/
│           └── org/springframework/ai/mcp/sample/client/
│               └── ClientStdio.java              # 客户端测试示例
├── pom.xml                                       # Maven 配置
└── README.md                                     # 项目说明

核心组件详解

1. McpServerApplication(主应用类)

位置 : com.xs.ai.mcp.sse.server.McpServerApplication

作用:

  • Spring Boot 应用入口
  • 注册工具服务为 MCP 工具
  • 配置 STDIO 传输方式

关键代码:

java 复制代码
@SpringBootApplication
public class McpServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(McpServerApplication.class, args);
    }

    @Bean
    public ToolCallbackProvider weatherTools(OpenMeteoService openMeteoService) {
        return MethodToolCallbackProvider.builder()
            .toolObjects(openMeteoService)
            .build();
    }
}

关键点:

  • 使用 @SpringBootApplication 注解
  • 通过 @Bean 方法注册 ToolCallbackProvider
  • MethodToolCallbackProvider 自动扫描 @Tool 注解的方法

2. OpenMeteoService(工具服务类)

位置 : com.xs.ai.mcp.sse.server.OpenMeteoService

作用:

  • 实现天气查询工具
  • 通过 @Tool 注解暴露给 MCP 客户端

关键代码:

java 复制代码
@Service
public class OpenMeteoService {

    @Tool(description = "获取指定经纬度的天气预报,根据位置自动推算经纬度")
    public String getWeatherForecastByLocation(
            double latitude,
            double longitude) {
        // 实现天气查询逻辑
        // ...
        return weatherInfo.toString();
    }
}

关键点:

  • 使用 @Service 注解标记为 Spring Bean
  • 使用 @Tool 注解标记工具方法
  • 方法参数会自动映射为工具参数
  • 返回值会自动序列化为 JSON

配置说明

application.yml 配置

yaml 复制代码
spring:
  main:
    web-application-type: none    # 必须禁用 Web 应用
    banner-mode: off              # 必须禁用启动横幅
  ai:
    mcp:
      server:
        name: my-weather-server   # MCP 服务器名称
        version: 0.0.1            # 服务器版本

# 注意:必须禁用控制台日志,避免干扰 STDIO 通信
logging:
  pattern:
    console: ""                   # 禁用控制台日志输出

配置要点

1. 禁用 Web 应用类型
yaml 复制代码
spring.main.web-application-type: none

原因: STDIO 服务器不需要 Web 服务器,必须禁用 Web 应用类型。

2. 禁用启动横幅
yaml 复制代码
spring.main.banner-mode: off

原因: 启动横幅会输出到 stdout,会干扰 STDIO 通信。

3. 禁用控制台日志
yaml 复制代码
logging.pattern.console: ""

原因: 控制台日志会输出到 stdout,会干扰 STDIO 通信。

4. MCP 服务器配置
yaml 复制代码
spring.ai.mcp.server:
  name: my-weather-server
  version: 0.0.1

说明: 配置 MCP 服务器的名称和版本,用于客户端识别。


使用方式

方式一:作为独立服务器运行

1. 构建 JAR 包
bash 复制代码
cd 07mcp-stdio-server
./mvnw clean package -DskipTests
2. 运行服务器
bash 复制代码
java -jar target/07mcp-stdio-server-0.0.1-xs.jar

服务器会启动并等待通过 stdin 接收 JSON-RPC 请求。

方式二:通过 MCP 客户端调用

1. Java 客户端示例

参考 ClientStdio.java 中的示例:

java 复制代码
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.spec.McpSchema.*;

public class ClientStdio {
    public static void main(String[] args) {
        // 1. 配置服务器启动参数
        var stdioParams = ServerParameters.builder("java")
                .args("-jar", "target/07mcp-stdio-server-0.0.1-xs.jar")
                .build();

        // 2. 创建 STDIO 传输层
        var transport = new StdioClientTransport(stdioParams);
        
        // 3. 创建 MCP 客户端
        var client = McpClient.sync(transport).build();

        // 4. 初始化连接
        client.initialize();

        // 5. 列出可用工具
        ListToolsResult toolsList = client.listTools();
        System.out.println("可用工具: " + toolsList);

        // 6. 调用工具
        CallToolResult result = client.callTool(
            new CallToolRequest("getWeatherForecastByLocation",
                Map.of("latitude", 39.9042, "longitude", 116.4074))
        );
        System.out.println("结果: " + result);

        // 7. 关闭连接
        client.closeGracefully();
    }
}
2. 在 Spring Boot 应用中集成

application.yml 中配置:

yaml 复制代码
spring:
  ai:
    mcp:
      client:
        stdio:
          connections:
            weather-server:
              command: java
              args:
                - -jar
                - /path/to/07mcp-stdio-server-0.0.1-xs.jar
3. 在代码中使用
java 复制代码
@Autowired
private ChatClient chatClient;

public String getWeather(double lat, double lon) {
    return chatClient.prompt()
        .user("查询天气,纬度: " + lat + ", 经度: " + lon)
        .call()
        .content();
}

方式三:通过命令行工具调用

1. 使用 npx(Node.js)
bash 复制代码
npx @modelcontextprotocol/cli stdio \
  --command java \
  --args -jar target/07mcp-stdio-server-0.0.1-xs.jar
2. 使用 Python MCP 客户端
python 复制代码
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    server_params = StdioServerParameters(
        command="java",
        args=["-jar", "target/07mcp-stdio-server-0.0.1-xs.jar"]
    )
    
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # 列出工具
            tools = await session.list_tools()
            print(f"可用工具: {tools}")
            
            # 调用工具
            result = await session.call_tool(
                "getWeatherForecastByLocation",
                {"latitude": 39.9042, "longitude": 116.4074}
            )
            print(f"结果: {result}")

工作原理

1. 启动流程

复制代码
客户端启动
    ↓
创建子进程(java -jar server.jar)
    ↓
服务器启动(Spring Boot)
    ↓
注册 ToolCallbackProvider
    ↓
监听 stdin,等待 JSON-RPC 请求

2. 工具发现流程

复制代码
客户端发送 listTools 请求
    ↓
服务器接收 JSON-RPC 请求(stdin)
    ↓
查找所有 ToolCallbackProvider Bean
    ↓
MethodToolCallbackProvider 扫描 @Tool 方法
    ↓
生成工具描述(名称、参数、返回值)
    ↓
返回工具列表(stdout)

3. 工具调用流程

复制代码
客户端发送 callTool 请求
    ↓
服务器接收 JSON-RPC 请求(stdin)
    ↓
解析请求,获取工具名称和参数
    ↓
查找对应的 ToolCallbackProvider
    ↓
MethodToolCallbackProvider 映射参数
    ↓
通过反射调用 Java 方法
    ↓
执行方法,获取返回值
    ↓
序列化返回值为 JSON
    ↓
返回结果(stdout)

4. 通信协议

STDIO 传输使用 JSON-RPC 2.0 协议:

请求格式:

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "getWeatherForecastByLocation",
    "arguments": {
      "latitude": 39.9042,
      "longitude": 116.4074
    }
  }
}

响应格式:

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "当前天气: 温度: 15.0°C..."
      }
    ]
  }
}

最佳实践

1. 工具方法设计

✅ 好的设计
java 复制代码
@Tool(description = "获取指定位置的天气预报")
public String getWeather(double latitude, double longitude) {
    // 单一职责:只做一件事
    // 参数简单:基本类型或简单对象
    // 返回值明确:String 或简单对象
}
❌ 不好的设计
java 复制代码
@Tool(description = "获取天气")
public Map<String, Object> getWeatherAndAirQualityAndTraffic(
    double lat, double lon, boolean includeHistory, 
    String format, int days) {
    // 职责过多:做了多件事
    // 参数复杂:太多参数
    // 返回值复杂:Map 结构不明确
}

2. 错误处理

java 复制代码
@Tool(description = "获取天气")
public String getWeather(double latitude, double longitude) {
    try {
        // 业务逻辑
        return result;
    } catch (Exception e) {
        // 返回友好的错误信息
        return "获取天气信息失败: " + e.getMessage();
    }
}

测试 :

java 复制代码
{
  "mcpServers": {
    "mcp-server-weather":{
      "command":"java",
      "args":[
       "-Dspring.ai.mcp.server.stdio=true",
        "-Dlogging.pattern.console=",
        "-jar",
        "/Users/quyixiao/Desktop//spring-ai-parent/07mcp-stdio-server/target/07mcp-stdio-server-0.0.1-xs.jar"
      ]
    }
  }
}
java 复制代码
package com.xs.ai.controller;


import com.xs.ai.services.ToolsService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.time.LocalDate;


/**
 * 智能航空客服控制器
 *
 * <p>该控制器提供了智能航空客服的核心功能,集成了多种 AI 技术:
 * <ul>
 *   <li><b>RAG(检索增强生成)</b>:基于服务条款文档回答问题</li>
 *   <li><b>工具调用(Function Calling)</b>:AI 可以调用退票、查询航班等业务方法</li>
 *   <li><b>对话记忆(Chat Memory)</b>:保持上下文,支持多轮对话</li>
 *   <li><b>流式响应(SSE)</b>:实时返回 AI 回复,提升用户体验</li>
 * </ul>
 *
 * <p><b>核心功能:</b>
 * <ul>
 *   <li>智能客服对话:自然语言交互,解答航班预订相关问题</li>
 *   <li>知识库问答:基于服务条款文档回答政策性问题(如退票费用、改签规则等)</li>
 *   <li>业务操作:AI 可以调用工具执行退票、查询航班等操作</li>
 *   <li>安全确认:涉及增删改操作时,需要用户确认后才执行</li>
 * </ul>
 *
 * @author wx:程序员徐庶
 * @version 1.0
 * @see com.xs.ai.services.ToolsService 工具服务,定义可被 AI 调用的业务方法
 * @see com.xs.ai.config.RagConfig RAG 配置,向量存储配置
 * @see org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor QuestionAnswerAdvisor,RAG Advisor
 * @since 1.0
 */
@RestController
@CrossOrigin
public class OpenAiController {

    /**
     * ChatClient 实例,配置了 RAG、工具调用、对话记忆等功能
     */
    ChatClient chatClient;

    /**
     * 向量存储,用于 RAG 文档检索
     */
    VectorStore vectorStore;

    /**
     * 构造函数,初始化 ChatClient
     *
     * <p>在 Spring 容器创建该 Bean 时,通过构造函数注入的方式初始化 ChatClient。
     * 配置包括:
     * <ul>
     *   <li>System Prompt:定义 AI 角色和行为规范</li>
     *   <li>Advisors:
     *       <ul>
     *         <li>PromptChatMemoryAdvisor:自动管理对话记忆</li>
     *         <li>SimpleLoggerAdvisor:记录对话日志(DEBUG 级别)</li>
     *       </ul>
     *   </li>
     *   <li>Tools:注册工具服务,AI 可以调用退票、查询航班等方法</li>
     *   <li>ToolCallbacks:工具回调提供者</li>
     * </ul>
     *
     * <p><b>安全机制:</b>
     * System Prompt 中明确要求:在涉及增删改(除了查询)的 function-call 前,
     * 必须等用户回复"确认"后再调用 tool。这确保了关键操作的安全性。
     *
     * @param chatClientBuilder ChatClient 构建器,由 Spring AI 自动配置
     * @param chatMemory 对话记忆,用于保持上下文
     * @param toolsService 工具服务,包含可被 AI 调用的业务方法
     * @param toolCallbackProvider 工具回调提供者
     * @param vectorStore 向量存储,用于 RAG 文档检索
     */
    public OpenAiController(ChatClient.Builder chatClientBuilder,
                            ChatMemory chatMemory,
                            ToolsService toolsService,
                            ToolCallbackProvider toolCallbackProvider,
                            VectorStore vectorStore
                           ) {
        this.chatClient = chatClientBuilder
                .defaultSystem("""
						##角色
                          您是"图灵"航空公司的客户聊天支持代理。请以友好、乐于助人且愉快的方式来回复。
                          您正在通过在线聊天系统与客户互动。
                        ##要求
                          1.在涉及增删改(除了查询)function-call前,必须等用户回复"确认"后再调用tool。
                          2.请讲中文。
                          
                          今天的日期是 {current_date}.
					""")
                .defaultAdvisors(
                        PromptChatMemoryAdvisor.builder(chatMemory).build(),
                        new SimpleLoggerAdvisor())
                .defaultTools(toolsService)
                .defaultToolCallbacks(toolCallbackProvider)
                .build();


        this.vectorStore=vectorStore;
    }

    /**
     * 流式生成 AI 回复
     *
     * <p>该接口提供智能客服的流式对话功能。用户发送消息后,AI 会:
     * <ol>
     *   <li>使用 RAG 从 VectorStore 中检索相关文档片段(topK=5,相似度阈值 0.4)</li>
     *   <li>结合检索到的文档和用户消息,分析用户意图</li>
     *   <li>如果需要,调用相应的工具(如查询航班、退票)</li>
     *   <li>生成自然语言回复</li>
     *   <li>通过 SSE(Server-Sent Events)流式返回回复内容</li>
     * </ol>
     *
     * <p><b>RAG 检索配置:</b>
     * <ul>
     *   <li><b>topK=5</b>:返回相似度最高的 5 个文档片段</li>
     *   <li><b>similarityThreshold=0.4</b>:相似度阈值,低于此值的文档片段会被过滤</li>
     * </ul>
     *
     * <p><b>使用场景:</b>
     * <ul>
     *   <li><b>知识库问答</b>:用户问"退票的费用是多少?"
     *       <ul>
     *         <li>RAG 检索相关文档片段</li>
     *         <li>AI 基于文档内容生成回答</li>
     *       </ul>
     *   </li>
     *   <li><b>业务查询</b>:用户问"查询我的订单101,姓名徐庶"
     *       <ul>
     *         <li>AI 识别需要调用 getBookingDetails 工具</li>
     *         <li>调用工具获取航班信息</li>
     *         <li>生成包含查询结果的回复</li>
     *       </ul>
     *   </li>
     *   <li><b>业务操作</b>:用户说"我要退票,订单号101,姓名徐庶"
     *       <ul>
     *         <li>AI 识别需要调用 cancel 工具</li>
     *         <li>由于是增删改操作,AI 会先询问用户确认</li>
     *         <li>用户回复"确认"后,AI 调用工具执行退票</li>
     *         <li>生成包含退票结果的回复</li>
     *       </ul>
     *   </li>
     * </ul>
     *
     * <p><b>响应格式:</b>
     * <ul>
     *   <li>Content-Type: text/event-stream(SSE 格式)</li>
     *   <li>数据格式:流式文本,实时返回 AI 生成的每个词</li>
     * </ul>
     *
     * @param message 用户输入的消息,默认为"讲个笑话"
     * @return Flux&lt;String&gt; 流式响应,通过 SSE 实时返回 AI 生成的回复
     *
     * @see org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor QuestionAnswerAdvisor,RAG Advisor
     * @see org.springframework.ai.vectorstore.SearchRequest SearchRequest,RAG 检索请求
     * @see com.xs.ai.services.ToolsService 工具服务,定义可被调用的业务方法
     */
    @CrossOrigin
    // http://localhost:8080/ai/generateStreamAsString?message=讲个笑话
    // http://localhost:8080/ai/generateStreamAsString?message=查询我的订单101,姓名徐庶
    // http://localhost:8080/ai/generateStreamAsString?message=我要退票,订单号101,姓名徐庶
    // http://localhost:8080/ai/generateStreamAsString?message=退票的费用是多少?
    // http://localhost:8080/ai/generateStreamAsString?message=从长沙到武汉的骑行路线
    // http://localhost:8080/ai/generateStreamAsString?message=查询北京天气
//    @GetMapping(value = "/ai/generateStreamAsString", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    @GetMapping(value = "/ai/generateStreamAsString")
    public Flux<String> generateStreamAsString(
            @RequestParam(value = "message", defaultValue = "讲个笑话") String message) {

        return chatClient.prompt().user(message)
                .system(p -> p.param("current_date", LocalDate.now()))  // 注入当前日期
                .advisors(
                        // RAG Advisor:从 VectorStore 中检索相关文档片段
                        QuestionAnswerAdvisor.builder(vectorStore)
                                .searchRequest(
                                        SearchRequest.builder()
                                                .topK(5)  // 返回相似度最高的 5 个文档片段
                                                .similarityThreshold(0.4)  // 相似度阈值
                                                .build())
                                .build()
                )
                .stream().content();  // 流式返回内容
    }

}

返回结果中用到了 07mcp-stdio-server-0.0.1-xs.jar 包用到的OpenMeteoService类。

java 复制代码
/*
 * Copyright 2025-2026 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @author brianxiadong
 */
package com.xs.ai.mcp.sse.server;

import java.util.List;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;

/**
 * OpenMeteo 天气服务实现类
 * 
 * 该类提供了基于 OpenMeteo 免费天气 API 的天气查询服务。
 * OpenMeteo 是一个开源的天气 API,提供全球天气预报数据,无需 API 密钥即可使用。
 * 
 * 核心功能:
 * - 根据经纬度获取当前天气信息
 * - 获取未来 7 天的天气预报
 * - 提供详细的天气数据(温度、湿度、风速、降水等)
 * - 将天气代码转换为可读的描述信息
 * 
 * MCP 工具集成:
 * - 使用 @Tool 注解标记的方法会被 MCP Server 自动发现
 * - AI 模型可以通过 MCP 协议调用这些方法
 * - 方法参数和返回值会自动序列化/反序列化为 JSON
 * 
 * 技术实现:
 * - 使用 Spring Web 的 RestClient 进行 HTTP 请求
 * - 使用 Jackson 进行 JSON 序列化/反序列化
 * - 使用 Java Record 定义数据模型,简化代码
 * 
 * 数据来源:
 * - OpenMeteo API: https://api.open-meteo.com/v1
 * - 提供全球范围的天气预报数据
 * - 数据更新频率:每小时
 * - 支持多种天气参数(温度、湿度、风速、降水、天气代码等)
 * 
 * 使用场景:
 * - AI 助手查询天气信息
 * - 智能客服提供天气咨询服务
 * - 旅行规划应用获取目的地天气
 * - 农业应用获取天气数据用于决策
 * 
 * 注意事项:
 * - OpenMeteo API 是免费的,但可能有请求频率限制
 * - 生产环境建议添加缓存机制,减少 API 调用
 * - 可以扩展支持更多天气参数(如紫外线指数、空气质量等)
 * - 错误处理:当 API 请求失败时,会抛出 RestClientException
 * 
 * @author brianxiadong
 * @see org.springframework.ai.tool.annotation.Tool
 * @see org.springframework.web.client.RestClient
 */
@Service
public class OpenMeteoService {

    // OpenMeteo免费天气API基础URL
    private static final String BASE_URL = "https://api.open-meteo.com/v1";

    private final RestClient restClient;

    public OpenMeteoService() {
        this.restClient = RestClient.builder()
                .baseUrl(BASE_URL)
                .defaultHeader("Accept", "application/json")
                .defaultHeader("User-Agent", "OpenMeteoClient/1.0")
                .build();
    }

    // OpenMeteo天气数据模型
    @JsonIgnoreProperties(ignoreUnknown = true)
    public record WeatherData(
            @JsonProperty("latitude") Double latitude,
            @JsonProperty("longitude") Double longitude,
            @JsonProperty("timezone") String timezone,
            @JsonProperty("current") CurrentWeather current,
            @JsonProperty("daily") DailyForecast daily,
            @JsonProperty("current_units") CurrentUnits currentUnits) {

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record CurrentWeather(
                @JsonProperty("time") String time,
                @JsonProperty("temperature_2m") Double temperature,
                @JsonProperty("apparent_temperature") Double feelsLike,
                @JsonProperty("relative_humidity_2m") Integer humidity,
                @JsonProperty("precipitation") Double precipitation,
                @JsonProperty("weather_code") Integer weatherCode,
                @JsonProperty("wind_speed_10m") Double windSpeed,
                @JsonProperty("wind_direction_10m") Integer windDirection) {
        }

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record CurrentUnits(
                @JsonProperty("time") String timeUnit,
                @JsonProperty("temperature_2m") String temperatureUnit,
                @JsonProperty("relative_humidity_2m") String humidityUnit,
                @JsonProperty("wind_speed_10m") String windSpeedUnit) {
        }

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record DailyForecast(
                @JsonProperty("time") List<String> time,
                @JsonProperty("temperature_2m_max") List<Double> tempMax,
                @JsonProperty("temperature_2m_min") List<Double> tempMin,
                @JsonProperty("precipitation_sum") List<Double> precipitationSum,
                @JsonProperty("weather_code") List<Integer> weatherCode,
                @JsonProperty("wind_speed_10m_max") List<Double> windSpeedMax,
                @JsonProperty("wind_direction_10m_dominant") List<Integer> windDirection) {
        }
    }

    /**
     * 获取天气代码对应的描述
     */
    private String getWeatherDescription(int code) {
        return switch (code) {
            case 0 -> "晴朗";
            case 1, 2, 3 -> "多云";
            case 45, 48 -> "雾";
            case 51, 53, 55 -> "毛毛雨";
            case 56, 57 -> "冻雨";
            case 61, 63, 65 -> "雨";
            case 66, 67 -> "冻雨";
            case 71, 73, 75 -> "雪";
            case 77 -> "雪粒";
            case 80, 81, 82 -> "阵雨";
            case 85, 86 -> "阵雪";
            case 95 -> "雷暴";
            case 96, 99 -> "雷暴伴有冰雹";
            default -> "未知天气";
        };
    }

    /**
     * 获取风向描述
     */
    private String getWindDirection(int degrees) {
        if (degrees >= 337.5 || degrees < 22.5)
            return "北风";
        if (degrees >= 22.5 && degrees < 67.5)
            return "东北风";
        if (degrees >= 67.5 && degrees < 112.5)
            return "东风";
        if (degrees >= 112.5 && degrees < 157.5)
            return "东南风";
        if (degrees >= 157.5 && degrees < 202.5)
            return "南风";
        if (degrees >= 202.5 && degrees < 247.5)
            return "西南风";
        if (degrees >= 247.5 && degrees < 292.5)
            return "西风";
        return "西北风";
    }

    /**
     * 获取指定经纬度的天气预报
     * 
     * @param latitude  纬度
     * @param longitude 经度
     * @return 指定位置的天气预报
     * @throws RestClientException 如果请求失败
     */
    @Tool(description = "获取指定经纬度的天气预报,根据位置自动推算经纬度")
    public String getWeatherForecastByLocation(
              double latitude,
              double longitude) {
        // 获取天气数据(当前和未来7天)
        var weatherData = restClient.get()
                .uri("/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m,apparent_temperature,relative_humidity_2m,precipitation,weather_code,wind_speed_10m,wind_direction_10m&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code,wind_speed_10m_max,wind_direction_10m_dominant&timezone=auto&forecast_days=7",
                        latitude, longitude)
                .retrieve()
                .body(WeatherData.class);

        // 拼接天气信息
        StringBuilder weatherInfo = new StringBuilder();

        // 添加当前天气信息
        WeatherData.CurrentWeather current = weatherData.current();
        String temperatureUnit = weatherData.currentUnits() != null ? weatherData.currentUnits().temperatureUnit()
                : "°C";
        String windSpeedUnit = weatherData.currentUnits() != null ? weatherData.currentUnits().windSpeedUnit() : "km/h";
        String humidityUnit = weatherData.currentUnits() != null ? weatherData.currentUnits().humidityUnit() : "%";

        weatherInfo.append(String.format("""
                当前天气:
                温度: %.1f%s (体感温度: %.1f%s)
                天气: %s
                风向: %s (%.1f %s)
                湿度: %d%s
                降水量: %.1f 毫米

                """,
                current.temperature(),
                temperatureUnit,
                current.feelsLike(),
                temperatureUnit,
                getWeatherDescription(current.weatherCode()),
                getWindDirection(current.windDirection()),
                current.windSpeed(),
                windSpeedUnit,
                current.humidity(),
                humidityUnit,
                current.precipitation()));

        // 添加未来天气预报
        weatherInfo.append("未来天气预报:\n");
        WeatherData.DailyForecast daily = weatherData.daily();

        for (int i = 0; i < daily.time().size(); i++) {
            String date = daily.time().get(i);
            double tempMin = daily.tempMin().get(i);
            double tempMax = daily.tempMax().get(i);
            int weatherCode = daily.weatherCode().get(i);
            double windSpeed = daily.windSpeedMax().get(i);
            int windDir = daily.windDirection().get(i);
            double precip = daily.precipitationSum().get(i);

            // 格式化日期
            LocalDate localDate = LocalDate.parse(date);
            String formattedDate = localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd (EEE)"));

            weatherInfo.append(String.format("""
                    %s:
                    温度: %.1f%s ~ %.1f%s
                    天气: %s
                    风向: %s (%.1f %s)
                    降水量: %.1f 毫米

                    """,
                    formattedDate,
                    tempMin, temperatureUnit,
                    tempMax, temperatureUnit,
                    getWeatherDescription(weatherCode),
                    getWindDirection(windDir),
                    windSpeed, windSpeedUnit,
                    precip));
        }

        return weatherInfo.toString();
    }
}

SSE

Spring AI MCP SSE 服务器使用与原理分析

项目 : 08mcp-sse-server
技术栈 : Spring Boot + Spring AI MCP Server + Server-Sent Events (SSE)
作者 : brianxiadong
最后更新: 2025年1月


📋 目录

  1. 项目概述
  2. [SSE 技术原理](#SSE 技术原理)
  3. [MCP SSE 服务器架构](#MCP SSE 服务器架构)
  4. 使用方式
  5. 核心组件分析
  6. [与 STDIO 服务器对比](#与 STDIO 服务器对比)
  7. 最佳实践
  8. 常见问题
  9. 参考文档

项目概述

什么是 MCP SSE 服务器?

MCP SSE 服务器 是基于 Server-Sent Events (SSE) 技术的 Model Context Protocol (MCP) 服务器实现。它允许 AI 模型和智能体通过 HTTP/SSE 协议与 Java 后端服务进行实时通信,调用业务工具和服务。

核心特性

  • 基于 HTTP/SSE 通信:使用标准的 HTTP 协议和 SSE 技术
  • 实时双向通信:支持服务器主动推送数据到客户端
  • Web 标准协议:易于与 Web 应用和微服务集成
  • 流式响应:支持流式数据传输,适合长时间运行的任务
  • 跨网络访问:支持跨网络、跨主机的服务调用
  • Spring Boot 集成:基于 Spring Boot,易于开发和部署

适用场景

  • 🌐 Web 应用集成:前端应用需要调用 AI 工具服务
  • 🏗️ 微服务架构:作为独立的工具服务在微服务架构中运行
  • 📡 实时数据推送:需要实时反馈的工具调用场景
  • 🌍 分布式系统:跨网络、跨主机的工具服务调用
  • 🔄 流式处理:需要流式返回结果的长时间任务

SSE 技术原理

什么是 Server-Sent Events (SSE)?

Server-Sent Events (SSE) 是一种基于 HTTP 的服务器推送技术,允许服务器通过持久的 HTTP 连接主动向客户端推送数据。SSE 是 HTML5 标准的一部分,由 W3C 定义。

SSE 工作原理

1. 连接建立
复制代码
客户端                         服务器
   |                              |
   |---- HTTP GET 请求 ---------->|
   |   Accept: text/event-stream  |
   |                              |
   |<---- 200 OK -----------------|
   |   Content-Type: text/event-stream
   |   Connection: keep-alive
   |   Cache-Control: no-cache
   |
   |<==== 持久连接建立 ===========|
2. 事件格式

SSE 事件使用特定的文本格式:

复制代码
event: tool-result
id: 12345
data: {"result": "success", "data": "..."}

event: tool-progress
id: 12346
data: {"progress": 50, "message": "处理中..."}

事件字段说明

  • event: 事件类型(可选)
  • id: 事件 ID(可选),用于断线重连
  • data: 事件数据(必需),可以是单行或多行
  • retry: 重连间隔(可选),单位毫秒
3. 数据推送流程
复制代码
服务器端:
1. 接收客户端请求
2. 设置响应头(Content-Type: text/event-stream)
3. 保持连接开放
4. 通过连接发送事件数据
5. 客户端接收并处理事件
6. 连接保持开放,持续推送数据

SSE vs WebSocket vs 轮询

特性 SSE WebSocket 轮询
通信方向 单向(服务器→客户端) 双向 双向(通过请求/响应)
协议 HTTP WebSocket HTTP
连接方式 持久 HTTP 连接 独立协议连接 短连接
实现复杂度 简单 中等 简单
浏览器支持 原生支持 原生支持 原生支持
自动重连 支持 需手动实现 需手动实现
适用场景 服务器推送数据 实时双向通信 低频数据更新

SSE 优势

  1. 简单易用:基于标准 HTTP,无需额外协议
  2. 自动重连:浏览器自动处理连接断开和重连
  3. 文本格式:易于调试和查看
  4. HTTP 兼容:可以通过代理和防火墙
  5. 流式传输:支持实时数据流传输

SSE 限制

  1. 单向通信:只能服务器向客户端推送
  2. 文本格式:只支持文本数据(JSON 需要序列化)
  3. 连接数限制:浏览器对同一域名有连接数限制(通常 6 个)

MCP SSE 服务器架构

整体架构图

复制代码
┌─────────────────────────────────────────────────────────┐
│                    MCP Client (AI Model)                 │
│  - 发现工具                                              │
│  - 调用工具                                              │
│  - 处理流式响应                                          │
└──────────────────┬──────────────────────────────────────┘
                   │ HTTP/SSE Protocol
                   │
        ┌──────────┴──────────┐
        │                     │
   ┌────▼────┐          ┌─────▼─────┐
   │ HTTP    │          │   SSE     │
   │ Request │          │  Stream   │
   └────┬────┘          └─────┬─────┘
        │                     │
        └──────────┬──────────┘
                   │
        ┌──────────▼──────────┐
        │  Spring Boot Web    │
        │  Application        │
        │  (Port 8088)        │
        └──────────┬──────────┘
                   │
        ┌──────────▼──────────┐
        │  MCP Server         │
        │  (Spring AI)        │
        └──────────┬──────────┘
                   │
        ┌──────────▼──────────┐
        │ ToolCallbackProvider│
        │  (Bean)            │
        └──────────┬──────────┘
                   │
        ┌──────────▼──────────┐
        │MethodToolCallback   │
        │Provider             │
        └──────────┬──────────┘
                   │
        ┌──────────▼──────────┐
        │  Service Classes    │
        │  (@Tool methods)    │
        └─────────────────────┘

通信流程

1. 初始化阶段
复制代码
客户端                         服务器
   |                              |
   |---- HTTP POST /mcp/init ---->|
   |   { "jsonrpc": "2.0", ... }  |
   |                              |
   |<---- 200 OK -----------------|
   |   { "result": {...} }        |
2. 工具发现阶段
复制代码
客户端                         服务器
   |                              |
   |---- HTTP POST /mcp/tools -->|
   |   { "method": "tools/list" } |
   |                              |
   |<---- SSE Stream -------------|
   |   event: tool-list
   |   data: {"tools": [...]}     |
3. 工具调用阶段
复制代码
客户端                         服务器
   |                              |
   |---- HTTP POST /mcp/call ---->|
   |   { "method": "tools/call",  |
   |     "params": {...} }        |
   |                              |
   |<---- SSE Stream -------------|
   |   event: tool-progress       |
   |   data: {"progress": 30}     |
   |                              |
   |   event: tool-result         |
   |   data: {"result": "..."}   |

核心组件

1. Spring AI MCP Server WebFlux

依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>

功能

  • 自动创建 HTTP 端点接收 MCP 请求
  • 处理 JSON-RPC 协议
  • 管理 SSE 连接和事件推送
  • 集成 Spring WebFlux 响应式编程模型
2. ToolCallbackProvider

接口org.springframework.ai.tool.ToolCallbackProvider

作用

  • 提供工具发现功能(listTools()
  • 提供工具调用功能(call()
  • 管理工具的生命周期
3. MethodToolCallbackProvider

实现类org.springframework.ai.tool.method.MethodToolCallbackProvider

功能

  • 使用反射扫描 @Tool 注解的方法
  • 自动生成工具描述信息
  • 将 JSON-RPC 参数映射到 Java 方法参数
  • 执行方法并序列化返回值

使用方式

1. 项目配置

Maven 依赖
xml 复制代码
<dependencies>
    <!-- MCP SSE Server -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
    </dependency>
    
    <!-- Spring Web (可选,用于 HTTP 客户端) -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
    </dependency>
</dependencies>
应用配置 (application.yml)
yaml 复制代码
spring:
  ai:
    mcp:
      server:
        name: my-sse-server
        version: 0.0.1

server:
  port: 8088  # SSE 服务器端口

配置说明

  • spring.ai.mcp.server.name: MCP 服务器名称
  • spring.ai.mcp.server.version: 服务器版本
  • server.port: Web 服务器端口(默认 8088)

注意:与 STDIO 服务器不同,SSE 服务器需要启动 Web 应用,可以保留 banner 和日志输出。

2. 创建工具服务

定义服务类
java 复制代码
@Service
public class UserToolService {
    
    // 用户数据(示例)
    Map<String, Double> userScore = Map.of(
        "xushu", 99.0,
        "zhangsan", 2.0,
        "lisi", 3.0
    );
    
    /**
     * 获取用户分数工具方法
     * 
     * @Tool 注解标记此方法为 MCP 工具
     * description 提供工具描述,供 AI 模型理解工具功能
     */
    @Tool(description = "获取用户分数")
    public String getScore(String username) {
        if (userScore.containsKey(username)) {
            return userScore.get(username).toString();
        }
        return "未检索到当前用户";
    }
}
注册工具服务
java 复制代码
@SpringBootApplication
public class McpSSEApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(McpSSEApplication.class, args);
    }
    
    /**
     * 注册工具服务为 MCP 工具
     * 
     * MethodToolCallbackProvider 会自动扫描 userToolService 中
     * 所有标记了 @Tool 注解的方法,并将它们注册为 MCP 工具
     */
    @Bean
    public ToolCallbackProvider userTools(UserToolService userToolService) {
        return MethodToolCallbackProvider.builder()
            .toolObjects(userToolService)
            .build();
    }
}

3. 启动服务器

bash 复制代码
# 方式 1: 使用 Maven
cd 08mcp-sse-server
mvn spring-boot:run

# 方式 2: 打包后运行
mvn clean package
java -jar target/08mcp-sse-server-0.0.1-xs.jar

启动后

  • 服务器监听 http://localhost:8088
  • MCP 端点自动创建(由 Spring AI 管理)
  • 可以通过 HTTP 客户端连接

4. 客户端连接

使用 Spring AI MCP Client
java 复制代码
@Configuration
public class McpClientConfig {
    
    @Bean
    public McpClient mcpClient() {
        // 配置 SSE 连接
        SseClientTransport transport = new SseClientTransport(
            URI.create("http://localhost:8088")
        );
        
        return McpClient.sync(transport).build();
    }
}
使用 HTTP 客户端(JavaScript)
javascript 复制代码
// 创建 EventSource 连接
const eventSource = new EventSource('http://localhost:8088/mcp/events');

// 监听工具调用结果
eventSource.addEventListener('tool-result', function(event) {
    const data = JSON.parse(event.data);
    console.log('工具调用结果:', data);
});

// 监听错误
eventSource.onerror = function(event) {
    console.error('SSE 连接错误:', event);
};
使用 curl 测试
bash 复制代码
# 初始化连接
curl -X POST http://localhost:8088/mcp/init \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"initialize","params":{}}'

# 列出工具
curl -X POST http://localhost:8088/mcp/tools/list \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","params":{}}'

# 调用工具(通过 SSE 接收响应)
curl -X POST http://localhost:8088/mcp/tools/call \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"tools/call",
    "params":{
      "name":"getScore",
      "arguments":{"username":"xushu"}
    }
  }'

核心组件分析

1. McpSSEApplication

位置com.xs.ai.mcp.sse.server.McpSSEApplication

职责

  • Spring Boot 应用入口
  • 配置 Spring 容器
  • 注册工具服务 Bean

关键代码

java 复制代码
@SpringBootApplication
public class McpSSEApplication {
    public static void main(String[] args) {
        SpringApplication.run(McpSSEApplication.class, args);
    }
    
    @Bean
    public ToolCallbackProvider userTools(UserToolService userToolService) {
        return MethodToolCallbackProvider.builder()
            .toolObjects(userToolService)
            .build();
    }
}

2. UserToolService

位置com.xs.ai.mcp.sse.server.UserToolService

职责

  • 实现业务逻辑
  • 使用 @Tool 注解暴露方法为 MCP 工具
  • 处理工具调用请求

关键代码

java 复制代码
@Service
public class UserToolService {
    
    @Tool(description = "获取用户分数")
    public String getScore(String username) {
        // 业务逻辑实现
        if (userScore.containsKey(username)) {
            return userScore.get(username).toString();
        }
        return "未检索到当前用户";
    }
}

3. Spring AI MCP Server 自动配置

自动配置类:Spring AI 会自动配置以下组件:

  1. HTTP 端点

    • /mcp/init: 初始化连接
    • /mcp/tools/list: 列出可用工具
    • /mcp/tools/call: 调用工具
    • /mcp/events: SSE 事件流
  2. JSON-RPC 处理器

    • 解析 JSON-RPC 请求
    • 路由到对应的工具
    • 序列化响应
  3. SSE 事件推送

    • 管理 SSE 连接
    • 推送事件到客户端
    • 处理连接断开和重连

与 STDIO 服务器对比

传输方式对比

特性 SSE 服务器 STDIO 服务器
通信协议 HTTP/SSE 标准输入/输出
连接方式 HTTP 长连接 进程管道
启动方式 独立进程 子进程
网络支持 ✅ 支持跨网络 ❌ 仅本地
Web 集成 ✅ 原生支持 ❌ 不支持
日志输出 ✅ 可以输出 ❌ 必须禁用
Banner ✅ 可以显示 ❌ 必须禁用
多客户端 ✅ 支持并发 ❌ 单客户端
调试 ✅ 易于调试 ⚠️ 较困难
部署 ✅ 标准 Web 部署 ⚠️ 需要进程管理

配置对比

SSE 服务器配置
yaml 复制代码
spring:
  ai:
    mcp:
      server:
        name: my-sse-server
        version: 0.0.1

server:
  port: 8088
# 可以保留 banner 和日志
STDIO 服务器配置
yaml 复制代码
spring:
  main:
    web-application-type: none  # 必须禁用 Web
    banner-mode: off            # 必须禁用 Banner

logging:
  pattern:
    console: ""                 # 必须禁用控制台日志

spring:
  ai:
    mcp:
      server:
        name: my-stdio-server
        version: 0.0.1

使用场景选择

选择 SSE 服务器

  • ✅ Web 应用需要调用工具服务
  • ✅ 微服务架构中的工具服务
  • ✅ 需要跨网络访问
  • ✅ 需要实时反馈和流式响应
  • ✅ 需要支持多客户端并发

选择 STDIO 服务器

  • ✅ 本地进程间通信
  • ✅ 嵌入式场景
  • ✅ 资源受限环境
  • ✅ 简单的工具集成
  • ✅ 单客户端场景

最佳实践

1. 工具设计

单一职责原则
java 复制代码
// ✅ 好的设计:每个工具只做一件事
@Tool(description = "获取用户分数")
public String getScore(String username) { }

@Tool(description = "更新用户分数")
public String updateScore(String username, double score) { }

// ❌ 不好的设计:一个工具做多件事
@Tool(description = "获取或更新用户分数")
public String getOrUpdateScore(String username, String action, ...) { }
明确的描述
java 复制代码
// ✅ 好的描述:清晰明确
@Tool(description = "根据用户名查询用户分数,返回0-100之间的数值")

// ❌ 不好的描述:过于简单
@Tool(description = "获取分数")
合理的参数设计
java 复制代码
// ✅ 好的参数:简单明了
@Tool(description = "查询用户信息")
public UserInfo getUser(String userId) { }

// ❌ 不好的参数:参数过多
@Tool(description = "查询用户信息")
public UserInfo getUser(String userId, boolean includeHistory, 
                        boolean includePreferences, ...) { }

2. 错误处理

java 复制代码
@Tool(description = "获取用户分数")
public String getScore(String username) {
    try {
        // 参数验证
        if (username == null || username.isEmpty()) {
            return "错误:用户名不能为空";
        }
        
        // 业务逻辑
        if (userScore.containsKey(username)) {
            return userScore.get(username).toString();
        }
        
        return "未检索到当前用户";
    } catch (Exception e) {
        // 返回友好的错误信息
        return "获取用户分数失败: " + e.getMessage();
    }
}

3. 性能优化

添加缓存
java 复制代码
@Service
public class UserToolService {
    
    @Cacheable("userScore")
    @Tool(description = "获取用户分数")
    public String getScore(String username) {
        // 实现逻辑
    }
}
异步处理
java 复制代码
@Tool(description = "处理大量数据")
public CompletableFuture<String> processData(String data) {
    return CompletableFuture.supplyAsync(() -> {
        // 异步处理逻辑
        return result;
    });
}

4. 安全性

参数验证
java 复制代码
@Tool(description = "查询用户信息")
public UserInfo getUser(String userId) {
    // 验证用户 ID 格式
    if (userId == null || !userId.matches("^[a-zA-Z0-9]+$")) {
        throw new IllegalArgumentException("无效的用户ID");
    }
    // 实现逻辑
}
权限控制
java 复制代码
@Tool(description = "删除用户")
public void deleteUser(String userId) {
    // 检查权限
    if (!hasPermission("DELETE_USER")) {
        throw new SecurityException("无权限删除用户");
    }
    // 实现逻辑
}

5. 日志和监控

java 复制代码
@Tool(description = "获取用户分数")
public String getScore(String username) {
    log.info("查询用户分数: username={}", username);
    try {
        String result = // 实现逻辑
        log.info("查询成功: username={}, score={}", username, result);
        return result;
    } catch (Exception e) {
        log.error("查询失败: username={}", username, e);
        throw e;
    }
}

常见问题

Q1: SSE 连接断开怎么办?

A: SSE 客户端(如浏览器)会自动重连。服务器端应该:

  • 设置合理的 retry 间隔
  • 处理连接断开事件
  • 实现心跳机制保持连接

Q2: 如何支持多客户端并发?

A: SSE 服务器天然支持多客户端:

  • 每个客户端建立独立的 HTTP 连接
  • Spring Boot 默认支持并发请求
  • 可以通过配置调整线程池大小

Q3: SSE 和 WebSocket 如何选择?

A:

  • SSE: 适合服务器向客户端推送数据(单向)
  • WebSocket: 适合需要双向实时通信的场景

对于 MCP 工具调用,SSE 通常足够,因为主要是服务器返回结果。

Q4: 如何调试 SSE 连接?

A:

  1. 使用浏览器开发者工具查看 Network 标签
  2. 检查 EventSource 连接状态
  3. 查看服务器日志
  4. 使用 curl 测试端点

Q5: 性能如何优化?

A:

  1. 添加缓存减少重复计算
  2. 使用异步处理长时间任务
  3. 合理设置连接超时
  4. 监控连接数和资源使用

参考文档

官方文档

  1. Spring AI 官方文档

  2. Server-Sent Events 规范

  3. Model Context Protocol

相关资源

  1. Spring WebFlux 文档

  2. JSON-RPC 规范

  3. 项目示例


总结

Spring AI MCP SSE 服务器提供了一个强大而灵活的解决方案,用于将 Java 业务逻辑暴露为 AI 可调用的工具。通过使用标准的 HTTP/SSE 协议,它能够:

  • ✅ 轻松集成到 Web 应用和微服务架构
  • ✅ 支持实时数据推送和流式响应
  • ✅ 提供标准化的工具调用接口
  • ✅ 支持多客户端并发访问
  • ✅ 易于调试和监控

通过遵循最佳实践和合理的设计,您可以构建出高效、可靠的 MCP SSE 服务器,为 AI 应用提供强大的工具支持。


文档版本 : 1.0
最后更新 : 2025年1月
维护者: brianxiadong

测试

调用端配置sse 配置

服务端配置获取分数工具类

开始测试 :

MCP Sever

如何做权限控制?

MCP 鉴权机制详解

MCP 官方文档 : https://modelcontextprotocol.io/
MCP GitHub : https://github.com/modelcontextprotocol
最后更新: 2025年1月


📋 目录

  1. 概述
  2. 鉴权机制
  3. [OAuth 2.0 认证](#OAuth 2.0 认证)
  4. [API Key 认证](#API Key 认证)
  5. [JWT Token 认证](#JWT Token 认证)
  6. [STDIO 服务器鉴权](#STDIO 服务器鉴权)
  7. [SSE 服务器鉴权](#SSE 服务器鉴权)
  8. [Spring AI MCP 鉴权实现](#Spring AI MCP 鉴权实现)
  9. 安全最佳实践
  10. 常见问题

概述

什么是 MCP 鉴权?

MCP 鉴权是 Model Context Protocol (MCP) 中用于验证客户端身份和控制资源访问权限的安全机制。它确保只有经过授权的客户端才能访问 MCP 服务器提供的工具和数据。

鉴权的重要性

  • 数据安全:保护敏感数据和资源不被未授权访问
  • 访问控制:控制哪些客户端可以调用哪些工具
  • 审计追踪:记录访问日志,便于审计和监控
  • 合规要求:满足企业级安全合规要求

MCP 鉴权架构

复制代码
┌─────────────────────────────────────────────────────────┐
│                    MCP Client                            │
│  - 携带认证信息(Token/API Key)                          │
│  - 发送请求到 MCP Server                                 │
└──────────────────┬──────────────────────────────────────┘
                   │ HTTP Request
                   │ Authorization: Bearer <token>
                   │
        ┌──────────▼──────────┐
        │   MCP Server        │
        │  - 验证认证信息      │
        │  - 检查权限          │
        │  - 执行工具调用      │
        └─────────────────────┘

鉴权机制

MCP 支持多种鉴权机制,根据不同的使用场景和安全需求选择:

1. OAuth 2.0 认证(推荐)

适用场景

  • 企业级应用
  • 需要细粒度权限控制
  • 多用户、多客户端场景
  • 高安全要求

特点

  • ✅ 安全性高
  • ✅ 支持细粒度权限控制
  • ✅ 标准化协议
  • ✅ 支持令牌刷新
  • ⚠️ 实现复杂度较高

2. API Key 认证

适用场景

  • 简单的服务调用
  • 单用户或少量用户
  • 对安全要求较低的场景
  • 快速原型开发

特点

  • ✅ 实现简单
  • ✅ 易于使用
  • ✅ 适合开发测试
  • ❌ 安全性较低
  • ❌ 权限控制有限

3. JWT Token 认证

适用场景

  • 分布式系统
  • 无状态认证
  • 微服务架构
  • 需要跨域认证

特点

  • ✅ 无状态
  • ✅ 支持跨域
  • ✅ 包含用户信息
  • ✅ 可设置过期时间
  • ⚠️ 需要密钥管理

OAuth 2.0 认证

概述

OAuth 2.0 是 MCP 推荐的认证方式,特别适用于企业级应用和高安全要求的场景。在 MCP 中,服务器既充当资源服务器 ,也充当授权服务器

工作原理

1. 角色定义
  • 资源服务器(Resource Server):MCP 服务器,提供工具和数据资源
  • 授权服务器(Authorization Server):MCP 服务器,负责验证和授权
  • 客户端(Client):MCP 客户端,请求访问资源
  • 资源所有者(Resource Owner):用户,拥有资源的访问权限
2. 认证流程
复制代码
┌──────────┐                                    ┌──────────┐
│  Client  │                                    │   MCP    │
│          │                                    │  Server  │
└────┬─────┘                                    └────┬─────┘
     │                                                │
     │  1. 请求授权(带 Client ID)                    │
     │──────────────────────────────────────────────>│
     │                                                │
     │  2. 重定向到授权页面                            │
     │<──────────────────────────────────────────────│
     │                                                │
     │  3. 用户登录并授权                              │
     │──────────────────────────────────────────────>│
     │                                                │
     │  4. 返回授权码(Authorization Code)            │
     │<──────────────────────────────────────────────│
     │                                                │
     │  5. 用授权码换取访问令牌(Access Token)         │
     │──────────────────────────────────────────────>│
     │                                                │
     │  6. 返回访问令牌(JWT 或随机字符串)             │
     │<──────────────────────────────────────────────│
     │                                                │
     │  7. 使用访问令牌调用工具                        │
     │──────────────────────────────────────────────>│
     │     Authorization: Bearer <access_token>      │
     │                                                │
     │  8. 验证令牌并返回结果                          │
     │<──────────────────────────────────────────────│

实现步骤

步骤 1:注册 OAuth 应用

在 OAuth 提供者(如 GitHub、Google 等)上注册应用:

  1. 访问 OAuth 提供者开发者平台

  2. 创建 OAuth 应用

    • 填写应用名称
    • 设置回调 URL(Redirect URI)
    • 获取 Client IDClient Secret
  3. 配置权限范围(Scopes)

    • 根据需求设置访问权限范围
    • 例如:read:user, read:repo
步骤 2:配置环境变量
bash 复制代码
# OAuth 2.0 配置
export OAUTH_CLIENT_ID="your_client_id"
export OAUTH_CLIENT_SECRET="your_client_secret"
export OAUTH_REDIRECT_URI="http://localhost:8080/oauth/callback"
export OAUTH_AUTHORIZATION_URL="https://github.com/login/oauth/authorize"
export OAUTH_TOKEN_URL="https://github.com/login/oauth/access_token"
步骤 3:MCP 服务器实现

Python 示例

python 复制代码
from flask import Flask, request, redirect, session
import requests
import os

app = Flask(__name__)
app.secret_key = os.urandom(24)

# OAuth 配置
CLIENT_ID = os.getenv('OAUTH_CLIENT_ID')
CLIENT_SECRET = os.getenv('OAUTH_CLIENT_SECRET')
REDIRECT_URI = os.getenv('OAUTH_REDIRECT_URI')
AUTHORIZATION_URL = os.getenv('OAUTH_AUTHORIZATION_URL')
TOKEN_URL = os.getenv('OAUTH_TOKEN_URL')

@app.route('/oauth/authorize')
def authorize():
    """OAuth 授权端点"""
    # 生成 state 参数,防止 CSRF 攻击
    state = os.urandom(16).hex()
    session['oauth_state'] = state
    
    # 构建授权 URL
    auth_url = f"{AUTHORIZATION_URL}?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&state={state}&scope=read:user"
    
    return redirect(auth_url)

@app.route('/oauth/callback')
def callback():
    """OAuth 回调端点"""
    code = request.args.get('code')
    state = request.args.get('state')
    
    # 验证 state 参数
    if state != session.get('oauth_state'):
        return "Invalid state parameter", 400
    
    # 用授权码换取访问令牌
    token_response = requests.post(TOKEN_URL, data={
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET,
        'code': code,
        'redirect_uri': REDIRECT_URI
    })
    
    access_token = token_response.json().get('access_token')
    session['access_token'] = access_token
    
    return "Authorization successful!"

@app.route('/mcp/tools/call', methods=['POST'])
def call_tool():
    """MCP 工具调用端点(需要认证)"""
    # 验证访问令牌
    auth_header = request.headers.get('Authorization')
    if not auth_header or not auth_header.startswith('Bearer '):
        return "Unauthorized", 401
    
    token = auth_header.split(' ')[1]
    
    # 验证令牌(这里简化处理,实际应该验证 JWT 或查询令牌服务器)
    if token != session.get('access_token'):
        return "Invalid token", 401
    
    # 执行工具调用
    # ...
    
    return {"result": "success"}

Java/Spring Boot 示例

java 复制代码
@RestController
@RequestMapping("/mcp")
public class McpOAuthController {
    
    @Value("${oauth.client.id}")
    private String clientId;
    
    @Value("${oauth.client.secret}")
    private String clientSecret;
    
    @GetMapping("/oauth/authorize")
    public ResponseEntity<String> authorize(HttpSession session) {
        // 生成 state 参数
        String state = UUID.randomUUID().toString();
        session.setAttribute("oauth_state", state);
        
        // 构建授权 URL
        String authUrl = String.format(
            "https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&state=%s",
            clientId, redirectUri, state
        );
        
        return ResponseEntity.ok(authUrl);
    }
    
    @PostMapping("/tools/call")
    public ResponseEntity<?> callTool(@RequestHeader("Authorization") String authHeader) {
        // 验证 Bearer Token
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        
        String token = authHeader.substring(7);
        
        // 验证令牌(实际应该验证 JWT 或查询令牌服务器)
        if (!isValidToken(token)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        
        // 执行工具调用
        // ...
        
        return ResponseEntity.ok(result);
    }
    
    private boolean isValidToken(String token) {
        // 实现令牌验证逻辑
        // 可以验证 JWT 签名、检查过期时间等
        return true;
    }
}
步骤 4:客户端连接

JavaScript 示例

javascript 复制代码
// 1. 发起授权请求
function authorize() {
    window.location.href = 'http://mcp-server:8080/oauth/authorize';
}

// 2. 在回调页面获取访问令牌
// 回调 URL: http://localhost:8080/oauth/callback?code=xxx&state=xxx

// 3. 使用访问令牌调用工具
async function callTool(toolName, params) {
    const response = await fetch('http://mcp-server:8080/mcp/tools/call', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${accessToken}`
        },
        body: JSON.stringify({
            name: toolName,
            arguments: params
        })
    });
    
    return await response.json();
}

OAuth 2.0 配置示例

GitHub OAuth 配置
yaml 复制代码
# application.yml
oauth:
  provider: github
  client-id: ${GITHUB_CLIENT_ID}
  client-secret: ${GITHUB_CLIENT_SECRET}
  redirect-uri: http://localhost:8080/oauth/callback
  authorization-url: https://github.com/login/oauth/authorize
  token-url: https://github.com/login/oauth/access_token
  scopes:
    - read:user
    - read:repo
Google OAuth 配置
yaml 复制代码
oauth:
  provider: google
  client-id: ${GOOGLE_CLIENT_ID}
  client-secret: ${GOOGLE_CLIENT_SECRET}
  redirect-uri: http://localhost:8080/oauth/callback
  authorization-url: https://accounts.google.com/o/oauth2/v2/auth
  token-url: https://oauth2.googleapis.com/token
  scopes:
    - https://www.googleapis.com/auth/userinfo.email
    - https://www.googleapis.com/auth/userinfo.profile

API Key 认证

概述

API Key 是一种简单的认证方式,适用于对安全要求较低的场景。客户端在请求中携带 API Key,服务器验证 Key 的有效性。

实现方式

1. 请求头方式
http 复制代码
GET /mcp/tools/list HTTP/1.1
Host: mcp-server:8080
X-API-Key: your_api_key_here
Content-Type: application/json
2. 查询参数方式
http 复制代码
GET /mcp/tools/list?api_key=your_api_key_here HTTP/1.1
Host: mcp-server:8080
Content-Type: application/json
3. 环境变量方式
bash 复制代码
# 客户端配置
export MCP_API_KEY="your_api_key_here"

# 服务器配置
export MCP_SERVER_API_KEY="your_api_key_here"

服务器端实现

Spring Boot 示例

java 复制代码
@Component
public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {
    
    @Value("${mcp.server.api-key}")
    private String serverApiKey;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain) 
            throws ServletException, IOException {
        
        // 从请求头获取 API Key
        String apiKey = request.getHeader("X-API-Key");
        
        // 或者从查询参数获取
        if (apiKey == null) {
            apiKey = request.getParameter("api_key");
        }
        
        // 验证 API Key
        if (apiKey == null || !apiKey.equals(serverApiKey)) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.getWriter().write("Invalid API Key");
            return;
        }
        
        // 继续处理请求
        filterChain.doFilter(request, response);
    }
}

配置类

java 复制代码
@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/mcp/**").authenticated()
                .anyRequest().permitAll()
            )
            .addFilterBefore(new ApiKeyAuthenticationFilter(), 
                           UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

客户端使用

curl 示例

bash 复制代码
# 使用请求头
curl -X POST http://mcp-server:8080/mcp/tools/call \
  -H "X-API-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "getWeather", "arguments": {"city": "北京"}}'

# 使用查询参数
curl -X POST "http://mcp-server:8080/mcp/tools/call?api_key=your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "getWeather", "arguments": {"city": "北京"}}'

JavaScript 示例

javascript 复制代码
const apiKey = 'your_api_key_here';

fetch('http://mcp-server:8080/mcp/tools/call', {
    method: 'POST',
    headers: {
        'X-API-Key': apiKey,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        name: 'getWeather',
        arguments: { city: '北京' }
    })
})
.then(response => response.json())
.then(data => console.log(data));

JWT Token 认证

概述

JWT (JSON Web Token) 是一种无状态的认证方式,令牌中包含用户信息和权限信息,服务器可以验证令牌的有效性而无需查询数据库。

JWT 结构

JWT 由三部分组成,用 . 分隔:

复制代码
Header.Payload.Signature

示例

复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT 内容

Header(头部)
json 复制代码
{
  "alg": "HS256",
  "typ": "JWT"
}
Payload(载荷)
json 复制代码
{
  "sub": "user123",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622,
  "roles": ["USER", "ADMIN"]
}
Signature(签名)
复制代码
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

实现方式

1. 生成 JWT Token

Java 示例

java 复制代码
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;

@Service
public class JwtTokenService {
    
    private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private static final long EXPIRATION_TIME = 3600000; // 1小时
    
    public String generateToken(String username, List<String> roles) {
        return Jwts.builder()
            .setSubject(username)
            .claim("roles", roles)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
            .signWith(SECRET_KEY)
            .compact();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(SECRET_KEY)
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    public String getUsernameFromToken(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(SECRET_KEY)
            .build()
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
}
2. 验证 JWT Token

Spring Security 过滤器

java 复制代码
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenService jwtTokenService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain) 
            throws ServletException, IOException {
        
        String authHeader = request.getHeader("Authorization");
        
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7);
            
            if (jwtTokenService.validateToken(token)) {
                String username = jwtTokenService.getUsernameFromToken(token);
                
                // 设置认证信息
                UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(username, null, null);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        
        filterChain.doFilter(request, response);
    }
}
3. 客户端使用
javascript 复制代码
// 1. 登录获取 JWT Token
const loginResponse = await fetch('http://mcp-server:8080/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username: 'user', password: 'pass' })
});

const { token } = await loginResponse.json();

// 2. 使用 Token 调用工具
const toolResponse = await fetch('http://mcp-server:8080/mcp/tools/call', {
    method: 'POST',
    headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        name: 'getWeather',
        arguments: { city: '北京' }
    })
});

STDIO 服务器鉴权

特点

STDIO 服务器通过标准输入/输出通信,鉴权方式相对简单:

  1. 进程隔离:服务器作为子进程启动,天然隔离
  2. 环境变量:通过环境变量传递认证信息
  3. 启动参数:通过命令行参数传递配置

实现方式

1. 环境变量方式
json 复制代码
{
  "mcpServers": {
    "my-server": {
      "command": "java",
      "args": ["-jar", "mcp-server.jar"],
      "env": {
        "MCP_API_KEY": "your_api_key_here",
        "MCP_AUTH_TYPE": "api_key"
      }
    }
  }
}
2. 服务器端验证
java 复制代码
@SpringBootApplication
public class McpStdioServerApplication {
    
    public static void main(String[] args) {
        // 从环境变量读取 API Key
        String apiKey = System.getenv("MCP_API_KEY");
        
        if (apiKey == null || apiKey.isEmpty()) {
            System.err.println("Error: MCP_API_KEY not set");
            System.exit(1);
        }
        
        // 验证 API Key(简化示例)
        if (!isValidApiKey(apiKey)) {
            System.err.println("Error: Invalid API Key");
            System.exit(1);
        }
        
        SpringApplication.run(McpStdioServerApplication.class, args);
    }
    
    private static boolean isValidApiKey(String apiKey) {
        // 实现 API Key 验证逻辑
        // 可以从配置文件、数据库等读取有效 Key 列表
        return true;
    }
}

SSE 服务器鉴权

特点

SSE 服务器通过 HTTP 通信,支持标准的 HTTP 认证方式:

  1. HTTP 标准:可以使用标准的 HTTP 认证头
  2. 灵活性强:支持多种认证方式
  3. 易于集成:与 Web 安全框架集成方便

实现方式

1. Bearer Token 认证
java 复制代码
@RestController
@RequestMapping("/mcp")
public class McpSSEController {
    
    @Autowired
    private JwtTokenService jwtTokenService;
    
    @PostMapping("/tools/call")
    public ResponseEntity<?> callTool(
            @RequestHeader(value = "Authorization", required = false) String authHeader,
            @RequestBody ToolCallRequest request) {
        
        // 验证 Bearer Token
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("error", "Missing or invalid Authorization header"));
        }
        
        String token = authHeader.substring(7);
        
        if (!jwtTokenService.validateToken(token)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("error", "Invalid token"));
        }
        
        // 执行工具调用
        String result = executeTool(request.getName(), request.getArguments());
        
        return ResponseEntity.ok(Map.of("result", result));
    }
}
2. API Key 认证
java 复制代码
@Component
public class ApiKeyInterceptor implements HandlerInterceptor {
    
    @Value("${mcp.server.api-key}")
    private String serverApiKey;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        String apiKey = request.getHeader("X-API-Key");
        
        if (apiKey == null || !apiKey.equals(serverApiKey)) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.getWriter().write("{\"error\":\"Invalid API Key\"}");
            return false;
        }
        
        return true;
    }
}
3. 配置拦截器
java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private ApiKeyInterceptor apiKeyInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiKeyInterceptor)
            .addPathPatterns("/mcp/**");
    }
}

Spring AI MCP 鉴权实现

Spring Security 集成

Spring AI MCP 可以与 Spring Security 集成,实现完整的认证和授权:

1. 配置 Spring Security
java 复制代码
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class McpSecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/mcp/oauth/**").permitAll()
                .requestMatchers("/mcp/**").authenticated()
                .anyRequest().permitAll()
            )
            .oauth2Login(oauth2 -> oauth2
                .authorizationEndpoint(auth -> auth
                    .baseUri("/mcp/oauth/authorize")
                )
                .tokenEndpoint(token -> token
                    .baseUri("/mcp/oauth/token")
                )
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .decoder(jwtDecoder())
                )
            );
        
        return http.build();
    }
    
    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withJwkSetUri("https://your-oauth-provider.com/.well-known/jwks.json")
            .build();
    }
}
2. 工具方法权限控制
java 复制代码
@Service
public class ToolService {
    
    @Tool(description = "获取用户信息")
    @PreAuthorize("hasRole('USER')")
    public String getUserInfo(String userId) {
        // 实现逻辑
        return "User info";
    }
    
    @Tool(description = "删除用户")
    @PreAuthorize("hasRole('ADMIN')")
    public String deleteUser(String userId) {
        // 实现逻辑
        return "User deleted";
    }
}
3. 获取当前用户信息
java 复制代码
@Service
public class ToolService {
    
    @Tool(description = "获取当前用户信息")
    public String getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication.getName();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        
        return String.format("User: %s, Roles: %s", username, authorities);
    }
}

安全最佳实践

1. 令牌管理

令牌存储
java 复制代码
// ✅ 推荐:使用安全的令牌存储
@Service
public class TokenStorageService {
    
    // 使用 Redis 存储令牌(支持过期)
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public void storeToken(String token, String username, long expirationSeconds) {
        redisTemplate.opsForValue().set(
            "token:" + token, 
            username, 
            expirationSeconds, 
            TimeUnit.SECONDS
        );
    }
    
    public String getUsernameFromToken(String token) {
        return redisTemplate.opsForValue().get("token:" + token);
    }
    
    public void revokeToken(String token) {
        redisTemplate.delete("token:" + token);
    }
}
令牌刷新
java 复制代码
@Service
public class TokenRefreshService {
    
    public TokenResponse refreshToken(String refreshToken) {
        // 验证刷新令牌
        if (!isValidRefreshToken(refreshToken)) {
            throw new SecurityException("Invalid refresh token");
        }
        
        // 生成新的访问令牌
        String newAccessToken = generateAccessToken(refreshToken);
        String newRefreshToken = generateRefreshToken(refreshToken);
        
        return new TokenResponse(newAccessToken, newRefreshToken);
    }
}

2. 权限控制

细粒度权限
java 复制代码
@Service
public class ToolService {
    
    @Tool(description = "查询订单")
    @PreAuthorize("hasPermission(#orderId, 'Order', 'READ')")
    public Order getOrder(String orderId) {
        // 实现逻辑
    }
    
    @Tool(description = "删除订单")
    @PreAuthorize("hasPermission(#orderId, 'Order', 'DELETE')")
    public void deleteOrder(String orderId) {
        // 实现逻辑
    }
}
自定义权限评估器
java 复制代码
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    
    @Override
    public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
        String username = auth.getName();
        String resource = (String) targetDomainObject;
        String action = (String) permission;
        
        // 实现自定义权限检查逻辑
        return checkPermission(username, resource, action);
    }
    
    private boolean checkPermission(String username, String resource, String action) {
        // 从数据库或配置中检查权限
        return true;
    }
}

3. 安全配置

HTTPS 配置
yaml 复制代码
# application.yml
server:
  port: 8443
  ssl:
    enabled: true
    key-store: classpath:keystore.jks
    key-store-password: changeit
    key-store-type: JKS
    key-alias: mcp-server
CORS 配置
java 复制代码
@Configuration
public class CorsConfig {
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("https://trusted-domain.com"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/mcp/**", configuration);
        
        return source;
    }
}

4. 审计日志

java 复制代码
@Aspect
@Component
public class SecurityAuditAspect {
    
    private static final Logger log = LoggerFactory.getLogger(SecurityAuditAspect.class);
    
    @Around("@annotation(org.springframework.ai.tool.annotation.Tool)")
    public Object auditToolCall(ProceedingJoinPoint joinPoint) throws Throwable {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String username = auth != null ? auth.getName() : "anonymous";
        String toolName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        
        log.info("Tool call - User: {}, Tool: {}, Args: {}", username, toolName, Arrays.toString(args));
        
        try {
            Object result = joinPoint.proceed();
            log.info("Tool call success - User: {}, Tool: {}", username, toolName);
            return result;
        } catch (Exception e) {
            log.error("Tool call failed - User: {}, Tool: {}, Error: {}", username, toolName, e.getMessage());
            throw e;
        }
    }
}

常见问题

Q1: STDIO 服务器如何实现鉴权?

A: STDIO 服务器可以通过以下方式实现鉴权:

  1. 环境变量:在启动时通过环境变量传递 API Key
  2. 配置文件:读取配置文件中的认证信息
  3. 初始化握手:在 MCP 初始化阶段进行认证

示例

json 复制代码
{
  "mcpServers": {
    "my-server": {
      "command": "java",
      "args": ["-jar", "mcp-server.jar"],
      "env": {
        "MCP_API_KEY": "your_api_key_here"
      }
    }
  }
}

Q2: SSE 服务器如何实现 OAuth 2.0?

A: SSE 服务器实现 OAuth 2.0 的步骤:

  1. 配置 OAuth 提供者:注册应用,获取 Client ID 和 Secret
  2. 实现授权端点/oauth/authorize 重定向到 OAuth 提供者
  3. 实现回调端点/oauth/callback 接收授权码,换取访问令牌
  4. 验证令牌 :在工具调用时验证 Authorization 头中的令牌

Q3: 如何实现令牌刷新?

A: 实现令牌刷新机制:

  1. 生成刷新令牌:在登录时同时生成访问令牌和刷新令牌
  2. 刷新端点 :提供 /oauth/refresh 端点
  3. 验证刷新令牌:检查刷新令牌的有效性
  4. 生成新令牌:生成新的访问令牌和刷新令牌

Q4: 如何实现细粒度权限控制?

A: 实现细粒度权限控制:

  1. 使用 Spring Security:集成 Spring Security
  2. 自定义权限评估器 :实现 PermissionEvaluator 接口
  3. 方法级权限 :使用 @PreAuthorize 注解
  4. 资源级权限:在业务逻辑中检查权限

Q5: 生产环境的安全建议?

A: 生产环境安全建议:

  1. 使用 HTTPS:所有通信使用 HTTPS
  2. 令牌加密:敏感信息加密存储
  3. 定期轮换:定期更换 API Key 和密钥
  4. 审计日志:记录所有访问和操作
  5. 限流保护:实现请求限流,防止滥用
  6. 监控告警:监控异常访问,及时告警

参考文档

官方文档

  1. MCP 官方文档

  2. OAuth 2.0 规范

  3. Spring Security 文档

相关资源

  1. MCP GitHub 仓库

  2. 安全最佳实践


总结

MCP 鉴权机制提供了多种认证方式,可以根据不同的使用场景和安全需求选择:

  • OAuth 2.0:适用于企业级应用,安全性高,支持细粒度权限控制
  • API Key:适用于简单场景,实现简单,易于使用
  • JWT Token:适用于分布式系统,无状态,支持跨域

无论选择哪种方式,都应该遵循安全最佳实践,确保系统的安全性和可靠性。


文档版本 : 1.0
最后更新: 2025年1月

相关推荐
知乎的哥廷根数学学派2 小时前
面向可信机械故障诊断的自适应置信度惩罚深度校准算法(Pytorch)
人工智能·pytorch·python·深度学习·算法·机器学习·矩阵
yangminlei2 小时前
Spring Boot3集成LiteFlow!轻松实现业务流程编排
java·spring boot·后端
qq_318121592 小时前
互联网大厂Java面试故事:从Spring Boot到微服务架构的技术挑战与解答
java·spring boot·redis·spring cloud·微服务·面试·内容社区
且去填词2 小时前
DeepSeek :基于 Schema 推理与自愈机制的智能 ETL
数据仓库·人工智能·python·语言模型·etl·schema·deepseek
待续3012 小时前
订阅了 Qoder 之后,我想通过这篇文章分享一些个人使用心得和感受。
人工智能
weixin_397578022 小时前
人工智能发展历史
人工智能
J_liaty2 小时前
Spring Boot整合Nacos:从入门到精通
java·spring boot·后端·nacos
Mr__Miss2 小时前
保持redis和数据库一致性(双写一致性)
数据库·redis·spring
强盛小灵通专卖员2 小时前
基于深度学习的山体滑坡检测科研辅导:从论文实验到系统落地的完整思路
人工智能·深度学习·sci·小论文·山体滑坡