#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 时,选择合适的传输方式至关重要。本文档介绍两种主要的传输方式:Stdio 和 Streamable (HTTP),帮助开发者根据应用场景做出正确选择。
MCP 传输方式原理
MCP Server 通过不同的传输机制与客户端进行通信,每种方式都有其特定的实现原理和适用场景。
一、Stdio 传输方式
工作原理
- 启动机制:客户端通过命令方式启动 MCP Server 包(可以是 Java、Python 或 Node.js 开发的包)
- 通信方式:通过标准输入输出(stdin/stdout)进行数据传输
- 进程关系:一个客户端对应一个 MCP Server 进程,一对一绑定
- 权限控制:通过环境变量(env)传递用户信息
核心特性
- ✅ 本地运行:绑定在客户端进程中,无需 HTTP 环境
- ✅ 成本较低:适合开发辅助性工具
- ✅ 简单直接:适合桌面应用和个人用户场景
- ❌ 不支持并发:一个客户端只能启动一个 Server 实例
- ❌ 不支持动态切换:无法在运行时切换用户权限
- ❌ 升级复杂:需要客户端重启并重新获取更新包
适用场景
- ✅ 面向个人用户的桌面应用
- ✅ 本地辅助工具开发
- ✅ 单用户场景
- ✅ 需要读取本地文件等客户端资源
不适用场景
- ❌ 基于浏览器的 Web 应用
- ❌ 需要多客户端并发访问的场景
- ❌ 需要动态权限切换的场景
- ❌ 需要热更新的生产环境
二、Streamable (HTTP) 传输方式
工作原理
- 架构模式:客户端 → Web 服务器 → MCP Server(三层架构)
- 通信协议:基于 HTTP 进行通信
- 服务模式:MCP Server 面向服务,而非直接面向用户
- 认证机制:通过 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 方式的注意事项
-
Web 应用不适用
- 如果开发基于浏览器的 Web 应用,不要选择 stdio 传输方式
- 原因:无法支持多客户端并发和动态权限切换
-
环境依赖
- 客户端必须安装对应语言环境(Java/Python/Node.js)
- 否则无法启动 MCP Server 包
-
权限限制
- 权限信息通过环境变量传递,在启动时确定
- 无法在运行时动态切换用户权限
- 多个客户端无法共享同一个 Server 实例
-
升级流程
- MCP Server 包升级后,客户端需要:
- 重新获取更新后的包
- 重启客户端程序
- 升级过程相对复杂
- MCP Server 包升级后,客户端需要:
⚠️ Streamable 方式的注意事项
-
架构设计
- 需要设计 Web 服务器层作为中间代理
- 客户端不直接连接 MCP Server
-
认证实现
- 需要在 HTTP 请求头中实现认证机制
- 建议使用 JWT 等标准认证方案
-
部署要求
- 需要网络环境支持
- 需要部署 Web 服务器
六、选择建议
选择 Stdio 的场景
- ✅ 开发桌面应用或本地工具
- ✅ 单用户使用场景
- ✅ 需要访问客户端本地资源(如文件系统)
- ✅ 对部署复杂度要求较低
- ✅ 不需要高并发支持
选择 Streamable 的场景
- ✅ 开发 Web 应用
- ✅ 需要多用户并发访问
- ✅ 需要动态权限管理
- ✅ 需要热更新能力
- ✅ 服务化架构
七、MCP 协议原理补充
MCP 的核心组件
- MCP Host(主机):提供 AI 交互环境的应用程序
- MCP Client(客户端):在主机内运行,负责与 MCP Server 通信
- MCP Server(服务器):提供工具、资源和提示模板
MCP 的通信流程
- 客户端通过传输层(Stdio 或 HTTP)向 Server 发送请求
- Server 执行相应操作或检索数据
- Server 将结果返回给客户端
- 客户端将结果传递给 AI 模型
传输层的作用
传输层是 MCP 协议的基础设施,负责:
- 建立客户端与服务器之间的通信通道
- 传递请求和响应数据
- 处理认证和权限控制
- 管理连接生命周期
八、开发建议
- 明确应用场景:根据目标用户和应用类型选择传输方式
- 考虑扩展性:如果未来可能需要支持 Web 访问,优先考虑 Streamable
- 评估部署成本:Stdio 部署简单但扩展性差,Streamable 需要更多基础设施
- 权限设计:提前规划权限控制方案,避免后期重构
- 升级策略:考虑未来更新和维护的便利性
参考资料
- Model Context Protocol 官方文档
- MCP 传输方式技术规范
最后更新:根据实际开发经验整理
使用
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 官网
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
- 访问 百度地图开放平台
- 注册并创建服务端 API Key (AK)
- 重要:确保启用 "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_places、map_directions 和 map_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_traffic 和 map_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 开源许可证。
🔗 相关链接
- GitHub 仓库 : https://github.com/baidu-maps/mcp
- 官方文档 : lbsyun.baidu.com/faq/api?title=mcpserver/base
- 中文文档 : README_zh.md
- 百度地图开放平台 : https://lbsyun.baidu.com/
💡 使用建议
最佳实践
- 使用 SSE 访问方式:推荐使用 Server-Sent Events 方式访问,获得更好的性能和稳定性
- API Key 安全:不要在代码中硬编码 API Key,使用环境变量或配置文件
- 错误处理:实现适当的错误处理和重试机制
- 缓存策略:对于不经常变化的数据(如 POI 信息),考虑实现缓存机制
- 请求频率:注意 API 调用频率限制,避免超出配额
性能优化
- 使用批量 API(如
map_directions_matrix)减少请求次数 - 实现合理的缓存策略
- 使用 SSE 方式访问以获得更好的性能
- 考虑异步处理长时间运行的操作
安全建议
- 保护 API Key,不要提交到版本控制系统
- 使用环境变量或密钥管理服务
- 实现适当的访问控制和权限管理
- 监控 API 使用情况,防止滥用
🐛 问题反馈
如果遇到问题或有建议,请通过以下方式反馈:
- GitHub Issues : https://github.com/baidu-maps/mcp/issues
- 官方文档 : 查看 FAQ
📝 更新日志
最新版本: v0.2.4 (2025年8月22日)
查看 GitHub Releases 获取详细的版本更新信息。
最后更新 : 2025年1月
文档版本: 1.0
安装node
Node.js 安装指南(macOS)
目录
概述
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:安装
- 双击下载的 .pkg 文件
- 按照安装向导完成安装
- 安装完成后,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:企业环境(推荐官方安装包)
原因:
- 官方提供,稳定可靠
- 图形界面,操作简单
- 适合统一部署
安装步骤:
- 访问 https://nodejs.org/
- 下载 LTS 版本安装包
- 双击安装
场景 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:智能代码审查
需求:自动审查代码质量,生成审查报告
实现:
- AI 读取代码文件
- 分析代码结构、风格、潜在问题
- 生成详细的审查报告
- 提出改进建议
价值:
- 提高代码质量
- 减少人工审查时间
- 统一代码标准
场景 2:自动化文档生成
需求:根据代码自动生成 API 文档
实现:
- AI 读取源代码
- 分析函数、类、接口
- 生成 Markdown 或 HTML 文档
- 保存到指定目录
价值:
- 保持文档与代码同步
- 减少文档维护成本
- 提高开发效率
场景 3:智能项目管理
需求:AI 助手帮助理解和管理项目
实现:
- AI 浏览项目目录结构
- 分析项目架构
- 回答项目相关问题
- 协助项目重构
价值:
- 快速理解大型项目
- 提高团队协作效率
- 降低项目维护成本
场景 4:数据分析和报告
需求:读取数据文件,生成分析报告
实现:
- AI 读取 CSV、JSON 等数据文件
- 分析数据内容
- 生成可视化报告
- 保存报告文件
价值:
- 自动化数据分析
- 快速生成报告
- 提高决策效率
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 官方文档 : https://modelcontextprotocol.io/
- MCP Servers 仓库 : https://github.com/modelcontextprotocol/servers
- MCP 规范 : https://spec.modelcontextprotocol.io/
- Claude Desktop : https://claude.ai/download
📝 总结
MCP 文件系统服务器为 AI 模型提供了标准化、安全、高效的文件系统访问能力。通过使用 MCP 协议,开发者可以:
- 快速集成:通过简单配置即可集成文件系统功能
- 提高效率:减少重复开发,专注于业务逻辑
- 确保安全:通过权限管理和安全机制保护系统
- 扩展能力:使 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<String> 流式响应,通过 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 证书,通常是因为:
- 缺少 CA 证书:系统缺少证书颁发机构(CA)的根证书
- 证书路径配置错误:Node.js 无法找到正确的证书存储位置
- 网络环境问题:代理、防火墙或企业网络环境导致证书验证失败
- Node.js 版本问题:旧版本的 Node.js 可能存在证书验证问题
解决方案
方案 1:更新 CA 证书(推荐)
macOS 系统
-
更新系统证书
bash# 更新 Homebrew(如果使用) brew update # 安装/更新 CA 证书 brew install ca-certificates -
证书文件位置
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 -
-
设置环境变量
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
📖 目录
什么是 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<String> 流式响应,通过 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}¤t=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月
📋 目录
- 项目概述
- [SSE 技术原理](#SSE 技术原理)
- [MCP SSE 服务器架构](#MCP SSE 服务器架构)
- 使用方式
- 核心组件分析
- [与 STDIO 服务器对比](#与 STDIO 服务器对比)
- 最佳实践
- 常见问题
- 参考文档
项目概述
什么是 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 优势
- 简单易用:基于标准 HTTP,无需额外协议
- 自动重连:浏览器自动处理连接断开和重连
- 文本格式:易于调试和查看
- HTTP 兼容:可以通过代理和防火墙
- 流式传输:支持实时数据流传输
SSE 限制
- 单向通信:只能服务器向客户端推送
- 文本格式:只支持文本数据(JSON 需要序列化)
- 连接数限制:浏览器对同一域名有连接数限制(通常 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 会自动配置以下组件:
-
HTTP 端点:
/mcp/init: 初始化连接/mcp/tools/list: 列出可用工具/mcp/tools/call: 调用工具/mcp/events: SSE 事件流
-
JSON-RPC 处理器:
- 解析 JSON-RPC 请求
- 路由到对应的工具
- 序列化响应
-
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:
- 使用浏览器开发者工具查看 Network 标签
- 检查 EventSource 连接状态
- 查看服务器日志
- 使用 curl 测试端点
Q5: 性能如何优化?
A:
- 添加缓存减少重复计算
- 使用异步处理长时间任务
- 合理设置连接超时
- 监控连接数和资源使用
参考文档
官方文档
-
Spring AI 官方文档
-
Server-Sent Events 规范
-
Model Context Protocol
相关资源
-
Spring WebFlux 文档
-
JSON-RPC 规范
-
项目示例
总结
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月
📋 目录
- 概述
- 鉴权机制
- [OAuth 2.0 认证](#OAuth 2.0 认证)
- [API Key 认证](#API Key 认证)
- [JWT Token 认证](#JWT Token 认证)
- [STDIO 服务器鉴权](#STDIO 服务器鉴权)
- [SSE 服务器鉴权](#SSE 服务器鉴权)
- [Spring AI MCP 鉴权实现](#Spring AI MCP 鉴权实现)
- 安全最佳实践
- 常见问题
概述
什么是 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 等)上注册应用:
-
访问 OAuth 提供者开发者平台
-
创建 OAuth 应用
- 填写应用名称
- 设置回调 URL(Redirect URI)
- 获取
Client ID和Client Secret
-
配置权限范围(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. 环境变量方式
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 认证方式:
- HTTP 标准:可以使用标准的 HTTP 认证头
- 灵活性强:支持多种认证方式
- 易于集成:与 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 服务器可以通过以下方式实现鉴权:
- 环境变量:在启动时通过环境变量传递 API Key
- 配置文件:读取配置文件中的认证信息
- 初始化握手:在 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 的步骤:
- 配置 OAuth 提供者:注册应用,获取 Client ID 和 Secret
- 实现授权端点 :
/oauth/authorize重定向到 OAuth 提供者 - 实现回调端点 :
/oauth/callback接收授权码,换取访问令牌 - 验证令牌 :在工具调用时验证
Authorization头中的令牌
Q3: 如何实现令牌刷新?
A: 实现令牌刷新机制:
- 生成刷新令牌:在登录时同时生成访问令牌和刷新令牌
- 刷新端点 :提供
/oauth/refresh端点 - 验证刷新令牌:检查刷新令牌的有效性
- 生成新令牌:生成新的访问令牌和刷新令牌
Q4: 如何实现细粒度权限控制?
A: 实现细粒度权限控制:
- 使用 Spring Security:集成 Spring Security
- 自定义权限评估器 :实现
PermissionEvaluator接口 - 方法级权限 :使用
@PreAuthorize注解 - 资源级权限:在业务逻辑中检查权限
Q5: 生产环境的安全建议?
A: 生产环境安全建议:
- 使用 HTTPS:所有通信使用 HTTPS
- 令牌加密:敏感信息加密存储
- 定期轮换:定期更换 API Key 和密钥
- 审计日志:记录所有访问和操作
- 限流保护:实现请求限流,防止滥用
- 监控告警:监控异常访问,及时告警
参考文档
官方文档
-
MCP 官方文档
-
OAuth 2.0 规范
-
Spring Security 文档
相关资源
-
MCP GitHub 仓库
-
安全最佳实践
总结
MCP 鉴权机制提供了多种认证方式,可以根据不同的使用场景和安全需求选择:
- OAuth 2.0:适用于企业级应用,安全性高,支持细粒度权限控制
- API Key:适用于简单场景,实现简单,易于使用
- JWT Token:适用于分布式系统,无状态,支持跨域
无论选择哪种方式,都应该遵循安全最佳实践,确保系统的安全性和可靠性。
文档版本 : 1.0
最后更新: 2025年1月