🔌 MCP 是什么?它解决了什么问题?¶
MCP(Model Context Protocol,大模型上下文协议)是一套开放的标准化通信协议,专门用来让 LLM 应用与外部工具、数据源、资源进行安全、可发现的交互。
你可以把它想象成 “AI 世界的 USB-C”。

它解决的问题:工具与数据集的碎片化、集成成本高。
在 MCP 出现之前,每个 LLM 应用想要调用外部工具(数据库、文件系统、API)都需要:
-
为每个工具写一套适配器(插件、函数调用封装)。
-
不同应用之间无法复用这些集成。
-
模型选择工具时缺乏统一描述和发现机制,导致“信息孤岛”。
MCP 做了一件事: 把工具提供者(Server)和 AI 应用(Host/Client)解耦。你写一个 MCP Server 暴露工具,所有支持 MCP 的 AI 应用都能直接发现并使用,就像你的 USB-C 充电器可以给手机、平板、笔记本充电一样。

⚙️ 核心工作原理、对比传统 API 的优势、适用场景¶
2.1 核心原理:以工具为中心的发现-调用模式¶
MCP 基于 JSON-RPC 2.0 协议,定义了三条核心原语:
-
Tools(工具):模型可调用的函数。Server 暴露一个工具列表,每个工具包含名称、描述、参数 JSON Schema。LLM 根据描述决定是否调用。
-
Resources(资源):可供 Client 读取的数据,比如文件内容、数据库记录、实时信息。类似 REST 的 GET,但通过统一协议暴露。
-
Prompts(提示模板):预定义的对话模板,帮助用户更好地与 LLM 互动。
调用流程:

2.2 与传统的 API 调用对比¶
优势总结:
-
互操作性:任何 MCP 客户端可以与任何 MCP 服务器对话。
-
可发现性:LLM 能自己“看到”有哪些工具可用,从而自主决定调用哪一个。这是 Agent 自动化的基础。
-
安全性:用户可以通过 Host 授权,控制 Client 能访问哪些 Server,并且支持人在回路(human-in-the-loop)审批。
-
未来扩展:协议支持流式传输、资源订阅、通知,能适应复杂 Agent 场景。
2.3 适用场景¶
-
构建 Agent 平台:你的 Agent 需要访问多个工具(日历、邮件、数据库)。用 MCP 让每个工具提供者只写一个 Server,平台统一接入。
-
IDE / 代码助手:你希望用户在自己的编辑器里直接操作数据库、调用云服务,集成 MCP Server 即可,无需为每个IDE重新开发插件。
-
企业内部 LLM 网关:统一管理公司内的数据源和工具,AI 应用通过 MCP 安全、可控地访问它们。
-
个人 AI 助理:你用 Claude Desktop,同时连接了文件系统 MCP Server、天气 MCP Server、笔记 MCP Server,模型可以自主查阅文件、获取天气并写入笔记。
🏗️ MCP 的三层架构(Host / Client / Server)是如何工作的?¶
MCP 采用分层架构,把用户交互、协议管理、后端执行彻底分开。

① Host(宿主应用)
-
运行在用户侧的 AI 程序,如 Claude Desktop、VS Code 插件、自研 Web 界面。
-
职责:
- 解析
mcp_servers.json配置文件,为每个 Server 启动一个子进程(stdio)或建立 HTTP 连接。 - 持有多个
MCP Client实例,管理它们的生命周期。 - 将 LLM 的“工具调用意图”路由到对应的 Client,并将结果返回给 LLM。
- 控制权限:用户在 Host 上看到“是否允许调用此工具?”的提示。
② Client(协议客户端)
-
是 Host 内部的一个通信管理器,每个 Client 与一个 Server 一对一绑定。
-
职责:
- 维护与 Server 之间的传输通道(stdio、HTTP + SSE)。
- 发送 JSON-RPC 请求:
initialize、tools/list、tools/call、resources/read等。 - 接收并解析 Server 的响应,以及可能的推送通知。
- 处理重连、错误、超时。
③ Server(工具提供者)
-
一个实现了 MCP 协议的独立进程,通常由工具开发者编写。
-
职责:
- 声明自己的
capabilities(支持工具、资源、还是提示)。 - 处理来自 Client 的请求:
tools/list:返回工具列表(名称、描述、参数 schema)。tools/call:执行工具,返回结果。resources/read:读取资源(如文件、数据库)。
- 通常连接后端的真实数据源(数据库、REST API、文件系统)。
完整请求示例(Client → Server):
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"city": "Beijing"
}
}
}
Server 响应:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "Beijing: 22°C, sunny."
}
]
}
}
💻 4. 代码示例:用 Python 实现一个天气 MCP Server¶
(使用 mcp 官方库,安装 pip install mcp)
# weather_server.py
import asyncio
from mcp.server import Server, stdio_server
from mcp.types import Tool, TextContent
# 创建 Server 实例
server = Server("weather-server")
# 注册工具:列出可用的工具
@server.list_tools()
async def list_tools():
return [
Tool(
name="get_weather",
description="获取指定城市的当前天气",
inputSchema={
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 Beijing"
}
},
"required": ["city"]
}
)
]
# 实现工具调用
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "get_weather":
city = arguments["city"]
# 模拟查询天气(实际可调 API)
weather = f"{city}: 22°C, sunny."
return [TextContent(type="text", text=weather)]
raise ValueError(f"未知工具: {name}")
# 启动 Server(使用 stdio 传输)
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
配置 Claude Desktop 使用这个 Server:
// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
{
"mcp_servers": {
"weather": {
"command": "python",
"args": ["path/to/weather_server.py"]
}
}
}
重启 Claude Desktop 后,问“北京天气怎么样?”,模型会自动发现 get_weather 工具,传入参数并得到天气回答。
🔌 MCP 的工具调用和普通 HTTP API 调用有什么本质区别?¶
表面上看,两者都是客户端发请求、服务端返回结果。但它们的设计哲学完全不同,这个差异决定了为什么 Agent 时代需要 MCP。
用一个对比图先看全貌:

四个本质区别:
用代码感受一下差异:
调用一个天气查询,传统方式:
import requests
resp = requests.get(f"https://api.weather.com?city=Beijing&key=sk-xxx")
data = resp.json() # 开发者自己写解析逻辑,然后拼进 prompt
MCP 方式(客户端侧):
# 1. 自动发现工具(不需要提前知道 URL 和参数名)
tools = mcp_client.list_tools()
# 返回: [Tool(name="get_weather", description="...", inputSchema={...})]
# 2. LLM 决定调用后,Client 自动序列化参数
result = mcp_client.call_tool("get_weather", {"city": "Beijing"})
# 返回结构化的 TextContent,可直接注入上下文
最大的区别在于:普通 API 是“给人看的”,MCP 是“给模型看的”。 后者提供了足够的语义信息,让 LLM 能够像人类阅读菜单一样,自主决定“我需要调用哪个工具、参数怎么填”。这是 Agent 能够进行自主规划和行动的基础。
⚙️ 2. 如何在 SpringAI 中接入 MCP Server?动态工具发现是如何实现的?¶
Spring AI 从 1.0.0-M5 开始提供了对 MCP 的原生支持。核心思路:把 MCP Server 提供的工具列表,动态转换成 Spring AI 的 Tool 定义,然后注入到 ChatClient 中,让模型可以自动调用。
架构示意图:
Spring AI 应用
├── ChatClient
│ └── ToolCallingAutoConfiguration
│ └── McpToolCallbackProvider ← 桥梁
│ └── McpClient ← 与 MCP Server 通信
│ └── stdio / SSE 传输
└── MCP Server (独立进程)
└── 暴露 get_weather, query_db 等工具
接入步骤与代码示例:
第一步:引入依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp</artifactId>
<version>1.0.0-M5</version>
</dependency>
第二步:配置 MCP Client 连接(以 stdio 方式为例)
@Configuration
public class McpConfig {
@Bean
public McpClient mcpClient() {
// 通过子进程启动 Python 编写的 MCP Server
ProcessBuilder pb = new ProcessBuilder("python", "/path/to/weather_server.py");
return McpClient.using(pb).sync(); // 或 .async()
}
@Bean
public ToolCallbackProvider weatherTools(McpClient mcpClient) {
// 动态发现并注册所有工具
return new McpToolCallbackProvider(mcpClient);
}
}
第三步:在 ChatClient 中使用
@RestController
public class ChatController {
@Autowired
private ChatClient chatClient;
@Autowired
private ToolCallbackProvider weatherTools;
@GetMapping("/ask")
public String ask(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.tools(weatherTools.getToolCallbacks()) // 注入动态工具
.call()
.content();
}
}
动态工具发现是如何实现的?
当你创建 McpToolCallbackProvider 时,它会:
-
调用 MCP Client 的
listTools()方法,向 Server 发送tools/list请求。 -
Server 返回一个工具列表,每个工具包含
name、description、inputSchema(JSON Schema)。 -
Provider 遍历这个列表,把每个工具转换成 Spring AI 的
ToolCallback对象——里面封装了工具名、参数定义,以及实际调用时通过mcpClient.callTool()执行的具体逻辑。 -
当模型决定调用某个工具时,Spring AI 会匹配对应的
ToolCallback,执行它,并将返回结果追加到对话上下文中。
整个过程对开发者完全透明,你不需要为每个工具单独写一个适配函数。 新增工具只需在 MCP Server 侧添加,Spring AI 应用重启后自动发现,这极大降低了工具集成的维护成本。
🧰 MCP Server 的三类能力(Resources/Tools/Prompts)有什么区别?¶
MCP 把 Server 能提供的能力抽象成了三个互补的元语,它们共同覆盖了“数据读取”、“行动执行”和“行为引导”三大需求。

① Resources(资源)
-
本质:模型可读、但不可修改的数据端点。类似 REST 的 GET,但用统一协议暴露。
-
特点:有唯一的 URI 标识(如
file:///documents/report.pdf),支持多种 MIME 类型。可以是被动读取,也支持通过subscriptions订阅变更通知。 -
何时用:你的 Agent 需要访问文件系统、数据库视图、企业内部知识库等只读数据时。
② Tools(工具)
-
本质:模型可以主动调用执行的函数,通常会产生副作用或返回计算结果。
-
特点:包含完整的 JSON Schema 参数定义,使得 LLM 可以根据描述自主决定调用。执行结果返回为结构化内容(文本、图片等)。
-
何时用:需要模型去“做某事”——发邮件、订机票、操作数据库、调用外部 API。
③ Prompts(提示模板)
-
本质:预定义的对话模板,可以包含参数,帮助用户或模型快速启动特定类型的任务。
-
特点:可以带参数(如
{topic}),并由 Server 提供默认值。一个 Server 可以暴露多个 Prompt,Host 能列出并让用户选择。 -
何时用:你想要引导用户与 LLM 以特定方式交互。例如,一个“周报生成器”Server 可以提供“写周报”Prompt,用户只需填入项目名称,Prompt 自动组合出完整的指导语。
一个 Server 可以同时提供这三者。 比如一个“GitHub MCP Server”:
-
Resources:暴露仓库的文件树、README 内容(只读)。
-
Tools:提供
create_issue、merge_pr等可执行操作。 -
Prompts:提供“生成 Commit Message”或“总结 Pull Request”的对话模板。
收尾:
这三类能力就像餐厅里的菜单(Prompts)、厨具(Tools) 和食材(Resources)。菜单告诉你能点什么,厨具帮你加工,食材是原材料。MCP 把它们标准化后,任何会“点菜”的 AI 都能在任意厨房里做出佳肴——这就是生态的力量。
🔷 MCP 和 Function Calling 的本质区别是什么?¶
一句话概括:
Function Calling 是模型能力边界的“扩展接口”,而 MCP 是连接模型与万物的“标准化总线”。
我们用一张图看清两者的定位差异:

五个维度的本质差异:
用代码直观感受:
Function Calling 的典型流程(以 OpenAI 为例):
# 硬编码工具定义
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取天气",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}}
}
}]
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "北京天气"}],
tools=tools
)
# 手动解析调用结果,再去请求真正的天气 API
MCP 的流程(模型无关):
# 1. 动态发现(不需要提前写死)
tools = mcp_client.list_tools()
# 2. LLM 看到工具列表后自主决策
decision = llm.think(user_input, available_tools=tools)
# 3. 统一执行
result = mcp_client.call_tool(decision.tool_name, decision.arguments)
本质总结:
Function Calling 是“厂家给你的遥控器”,只能控制厂家指定的几个电器。
MCP 是“万能遥控器 + 红外编码标准”,任何电器只要支持这个标准,就能被任何遥控器控制。
前者解决“能不能做”,后者解决“能不能一起做”。
🛡️MCP 工具调用的权限控制和安全设计如何做?¶
MCP 让模型能够自主调用外部工具,这带来了巨大的安全挑战。我们必须遵循纵深防御原则,在多个层级上设置权限检查点。
安全架构总览:

第一层:Host 层 — 身份认证与会话管理
-
谁在操作? 必须在 Host 层面确认当前用户身份。对于 Web 应用,这通常是 JWT 或 OAuth2 令牌;对于桌面应用,可以是操作系统用户身份。
-
安全实践:Host 初始化时要求用户登录,将用户 ID 或 Access Token 注入到 MCP Client 的上下文里,后续所有请求都携带该身份。
// Spring Security + JWT 获取当前用户
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String userId = auth.getName();
// 传递给 MCP Client
mcpClient.setUserContext(userId);
第二层:Client 层 — 工具权限过滤 & 人在回路
-
哪些工具可用? 不是所有 Server 暴露的工具都应该对当前用户开放。Client 需要维护一份工具权限映射表。
-
实现方式:在
ToolCallbackProvider或 Client 的listTools()之后,根据用户角色过滤结果。 -
人在回路(Human-in-the-loop):对高风险操作(如转账、删除数据),Host 应弹出确认对话框,让用户明确授权后才真正发送
tools/call。
public List<Tool> getFilteredTools(McpClient client, String userId) {
List<Tool> allTools = client.listTools();
UserPermissions perms = permissionService.getPermissions(userId);
return allTools.stream()
.filter(tool -> perms.isAllowed(tool.getName()))
.collect(Collectors.toList());
}
// 执行前检查
if (tool.isHighRisk() && !userConfirmed(tool.getName())) {
throw new SecurityException("用户未授权高风险操作: " + tool.getName());
}
第三层:Server 层 — 参数校验 & 作用域控制
-
输入必须被视为不可信。 即使 Client 做了初步过滤,Server 也必须对每次
tools/call的参数进行严格校验。 -
使用 JSON Schema 进行声明式校验(MCP 本身提供了
inputSchema,Server 应强制执行)。 -
作用域与限流:Server 可以限制某个用户或 Client 的调用频率,防止滥用。
@PostMapping("/tools/call")
public ResponseEntity<CallToolResult> callTool(@RequestBody CallToolRequest request) {
// 1. 校验工具是否存在
Tool tool = toolRegistry.get(request.getName());
if (tool == null) throw new ToolNotFoundException();
// 2. 用 JSON Schema 校验参数
SchemaValidator validator = new SchemaValidator(tool.getInputSchema());
if (!validator.validate(request.getArguments())) {
throw new InvalidArgumentsException();
}
// 3. 检查作用域 (例如,只能查询自己的数据)
String userId = request.getMeta("user_id");
if (!tool.isAllowedForUser(userId, request.getArguments())) {
throw new ForbiddenException();
}
// 4. 执行并记录审计日志
auditLog.record(userId, request.getName(), request.getArguments());
return ResponseEntity.ok(tool.execute(request.getArguments()));
}
附加防线:
-
传输层安全:生产环境中,Client-Server 之间使用 HTTPS + API Key 或 mTLS,绝不能在公网裸奔。
-
内容安全:Server 返回的数据可能包含敏感信息,应在返回前做脱敏处理(如手机号中间四位打星)。
-
审计追踪:每一次
tools/call都要记录谁、什么时间、调用了什么工具、参数是什么,用于事后追溯和合规。
一句话记住: MCP 的威力在于“开放性”,而开放性的代价是“攻击面”。你在每一层都要问自己一个问题:“如果一个恶意 Prompt 要求调用这个工具,它能造成多大的破坏?” 答案越小,你的设计就越安全。
☕ 如何用 Java 实现一个 MCP Server?¶
我们基于 Spring Boot 和 Spring AI 的 MCP 支持,来实现一个可以提供“文件读取”和“系统信息”两个工具的 MCP Server。采用 WebFlux + SSE 传输模式,实现异步、流式通信。
步骤一:创建 Spring Boot 项目,引入依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
<version>1.0.0-M5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
步骤二:配置 application.yml
spring:
ai:
mcp:
server:
name: "Utility MCP Server"
version: "1.0.0"
type: "SYNC" # 或 ASYNC
tools:
- name: read_file
description: "读取本地文件内容"
- name: system_info
description: "获取当前系统信息"
server:
port: 8081
步骤三:实现工具服务
import org.springframework.ai.mcp.server.McpServer;
import org.springframework.ai.mcp.server.McpServerProperties;
import org.springframework.ai.mcp.server.annotation.McpTool;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
@Component
public class UtilityTools {
private final ObjectMapper objectMapper = new ObjectMapper();
@McpTool(name = "read_file", description = "读取指定路径的文本文件内容")
public String readFile(String path) throws IOException {
// 安全加固:只允许读取 /tmp 或特定沙箱目录
Path filePath = Path.of(path).toAbsolutePath().normalize();
if (!filePath.startsWith("/tmp/") && !filePath.startsWith("./data/")) {
throw new SecurityException("不允许读取该路径: " + filePath);
}
return Files.readString(filePath);
}
@McpTool(name = "system_info", description = "获取当前服务器的操作系统、CPU和内存信息")
public String systemInfo() throws IOException {
Runtime rt = Runtime.getRuntime();
Map<String, String> info = Map.of(
"os.name", System.getProperty("os.name"),
"os.arch", System.getProperty("os.arch"),
"availableProcessors", String.valueOf(rt.availableProcessors()),
"freeMemory", rt.freeMemory() / (1024*1024) + " MB",
"totalMemory", rt.totalMemory() / (1024*1024) + " MB"
);
return objectMapper.writeValueAsString(info);
}
}
步骤四:启动 Server
MCP Server 会自动扫描 @McpTool 注解,生成工具列表并向连接上来的 Client 暴露。启动类无需特殊配置。
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}
步骤五:与 Client 交互验证
启动 Server 后,可用 MCP 官方调试工具或一个简单的 Client 来测试。
// Client 端调用示例(伪代码)
McpClient client = McpClient.using("http://localhost:8081").sync();
List<Tool> tools = client.listTools(); // 自动发现 read_file, system_info
System.out.println(tools);
// LLM 决策后,Client 执行工具调用
String result = client.callTool("system_info", Map.of());
System.out.println(result); // 输出 CPU、内存等 JSON
关键设计考量:
-
传输方式:示例选择了 WebFlux(SSE),适合需要流式更新(如长任务进度)的场景。如果是简单的请求-响应,可以用
spring-ai-starter-mcp-server-webmvc。 -
安全加固:文件读取工具中加入了路径白名单,避免路径遍历攻击。生产环境还需要进一步集成用户认证。
-
工具发现:所有加了
@McpTool的方法都会自动注册,无需额外配置,符合 MCP 的“动态发现”哲学。
收尾:
用 Java 实现 MCP Server 的核心,是让工具逻辑干净地聚焦在业务上,而注册、发现、传输、校验都由框架和协议兜底。你写的每一行代码,都不再是给某个应用打的专属补丁,而是在构建一块可以被任何 AI 复用的乐高积木。
🔌MCP Server 最核心的两个接口是什么?¶
如果从协议交互的“必须”和“高频”角度来选,MCP Server 最核心的两个接口是:
-
tools/list:让 Agent 知道你能做什么 -
tools/call:让 Agent 能够真的去做事

为什么是这两个?
-
tools/list是动态发现的基础。没有它,Agent 就不知道有什么工具可用,只能靠人把工具描述硬编码在 prompt 里。MCP 最核心的创新之一就是让 LLM 自己看菜单点菜,而tools/list就是菜单本身。 -
tools/call是价值落地的关键。Agent 通过tools/list知道了工具,但最终必须通过tools/call去执行数据库查询、文件读取、API 调用等具体操作。这是 MCP 从“描述”到“行动”的桥梁。
协议层面的典型消息:
请求 tools/list:
响应:
{
"tools": [
{
"name": "query_db",
"description": "执行安全的 SQL 查询,只支持 SELECT",
"inputSchema": {
"type": "object",
"properties": {
"sql": {"type": "string", "description": "要执行的 SQL 语句"}
},
"required": ["sql"]
}
}
]
}
请求 tools/call:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "query_db",
"arguments": {"sql": "SELECT name, stock FROM products WHERE stock < 10"}
}
}
响应:
虽然 MCP 还定义了 resources/read、prompts/get 等接口,但对于绝大多数 Agent 应用场景来说,tools/list 和 tools/call 是让模型从“说”到“做”的最关键的两个。 理解这两个接口的职责和协作方式,就掌握了 MCP 的主动脉。
🐢开发了一个 MCP Server 提供数据库查询工具,上线后 Agent 调用响应很慢,如何排查优化?¶
现象: Agent 每次调用 query_db 都要等好几秒才返回,用户体验极差。我们需要系统性地定位瓶颈。
排查思路:分层定位法

第一层:工具内部埋点计时(定位是 Server 慢还是 DB 慢)
@McpTool(name = "query_db", description = "执行 SELECT 查询")
public String queryDb(String sql) {
long start = System.currentTimeMillis();
// 参数校验...
long validEnd = System.currentTimeMillis();
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql);
long queryEnd = System.currentTimeMillis();
String json = objectMapper.writeValueAsString(result);
long jsonEnd = System.currentTimeMillis();
log.info("query_db 耗时: 校验={}ms, SQL={}ms, JSON序列化={}ms, 总={}ms",
validEnd - start, queryEnd - validEnd, jsonEnd - queryEnd, jsonEnd - start);
return json;
}
常见瓶颈及解决方案:
第二层:连接池配置优化
// application.yml
spring.datasource.hikari:
maximum-pool-size: 20 # 根据并发调高
minimum-idle: 5
connection-timeout: 3000 # 3秒
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 10000 # 10秒未归还则告警
第三层:Agent 侧增加约束(从源头减少慢查询)
在工具描述中直接约定:
{
"name": "query_db",
"description": "执行 SQL 查询。注意:只支持 SELECT,必须包含 LIMIT 子句,最多返回 50 行。",
"inputSchema": {
"properties": {
"sql": {
"type": "string",
"description": "SQL 查询语句,必须包含 LIMIT 且 LIMIT <= 50"
}
}
}
}
并在 Server 端做强制检查:
if (!sql.toUpperCase().contains("LIMIT") || sql.contains("LIMIT 0")) {
sql += " LIMIT 50"; // 强制追加,或直接返回错误提示
}
第四层:启用缓存
对相同的 SQL 查询,短时间内直接返回缓存:
@Cacheable(value = "queryCache", key = "#sql", unless = "#result.length() > 10000")
public String queryDb(String sql) { ... }
收束: 慢查询排查要像剥洋葱,一层层从内到外,先确定是代码、SQL、连接池还是网络。加上必要的防御性编程(强加 LIMIT),才能让 Agent 既灵活又不会打垮数据库。
🏢 如何设计一个支持多租户的 MCP Server?¶
多租户的核心要求是:一个 MCP Server 实例为多个客户服务,但数据严格隔离,工具行为可能因租户而异。
设计原则:
-
上下文传递:每次工具调用必须携带租户 ID。
-
数据隔离:数据库查询自动附加租户过滤,或动态切换数据源。
-
工具差异化:不同租户可能看到不同的工具集(如 VIP 租户开放高级分析工具)。
-
无状态 Server + 请求级隔离:不能把租户上下文存成全局变量。
架构图:

实现步骤(Spring Boot 示例):
第一步:定义租户上下文
public class TenantContext {
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
public static void set(String tenantId) { CURRENT_TENANT.set(tenantId); }
public static String get() { return CURRENT_TENANT.get(); }
public static void clear() { CURRENT_TENANT.remove(); }
}
第二步:用 Filter 从 MCP 请求头中提取租户 ID
在 MCP 的传输层(比如 HTTP SSE 或 stdio 无法带自定义头,所以通常在 HTTP 模式下使用 Header)。如果使用 Spring WebFlux,可以:
@Component
public class TenantFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String tenantId = exchange.getRequest().getHeaders().getFirst("X-Tenant-Id");
if (tenantId == null) {
return Mono.error(new SecurityException("Missing X-Tenant-Id header"));
}
TenantContext.set(tenantId);
return chain.filter(exchange).doFinally(signal -> TenantContext.clear());
}
}
第三步:在工具方法中使用租户上下文
@McpTool(name = "query_customers", description = "查询当前租户的客户列表")
public String queryCustomers() {
String tenantId = TenantContext.get();
// 方案A:手动拼接租户过滤
String sql = "SELECT * FROM customers WHERE tenant_id = '" + tenantId + "'";
// 方案B:使用 MyBatis-Plus 多租户插件,自动拦截
return jdbcTemplate.queryForList(sql).toString();
}
第四步:工具差异化(可选)
public List<Tool> getToolsForTenant(String tenantId) {
List<Tool> tools = new ArrayList<>(baseTools);
if ("VIP".equals(tenantService.getPlan(tenantId))) {
tools.add(new Tool("advanced_analytics", "高级分析", ...));
}
return tools;
}
这样不同租户看到的 tools/list 返回结果不同。
多租户设计的核心就是一句话:让租户 ID 像一条隐形的水印,流淌在每一次工具调用的全链路中,数据层自动过滤,业务层按需定制。 永远不要在 Server 里缓存租户相关的全局状态,用 ThreadLocal 或请求上下文保持隔离,才能安全地为成百上千个客户服务。
🏭公司有一套内部系统(CRM、ERP、文档服务),需要让 Agent 能调用,如何设计接入方案?¶
这是一个典型的企业级 Agent 落地场景。目标是:用最小改造成本,把现有的内部系统能力暴露为 MCP 工具,供 Agent 统一调度。
核心方案:MCP Gateway + 适配器模式
不要修改现有系统,而是在它们前面架设一个 MCP 网关,由它负责协议翻译和调用编排。

设计要点:
- 统一认证层
MCP Gateway 负责对接 SSO,接收到 Agent 请求时,将用户身份映射为各个内部系统的访问令牌(可能不同系统使用不同凭证)。这种映射关系可以配置在网关中。
- 适配器抽象
为每个内部系统编写一个适配器,将系统的 API 翻译成 MCP Tool 定义。
public interface SystemAdapter {
List<Tool> getTools(); // 返回该系统提供的工具列表
Object callTool(String toolName, Map<String, Object> args); // 执行调用
}
CRM 适配器实现示例:
@Component
public class CrmAdapter implements SystemAdapter {
@Override
public List<Tool> getTools() {
return List.of(
new Tool("search_customers", "搜索客户", Map.of("keyword", "string")),
new Tool("get_customer_detail", "获取客户详情", Map.of("customerId", "string"))
);
}
@Override
public Object callTool(String toolName, Map<String, Object> args) {
String token = ContextAuth.getToken("CRM");
return switch (toolName) {
case "search_customers" -> crmClient.search(args.get("keyword").toString(), token);
case "get_customer_detail" -> crmClient.getById(args.get("customerId").toString(), token);
default -> throw new ToolNotFoundException();
};
}
}
- 工具聚合与动态注册
MCP Server 启动时,扫描所有
SystemAdapterBean,汇聚所有工具到一个统一列表。当tools/list被调用时,返回全公司所有可用工具。
@Bean
public List<Tool> aggregateTools(List<SystemAdapter> adapters) {
return adapters.stream()
.flatMap(adapter -> adapter.getTools().stream())
.collect(Collectors.toList());
}
- 调用路由
收到
tools/call时,根据工具名称找到对应的适配器,委托执行。
@McpTool(name = "*") // 可以用动态路由
public Object routeCall(String toolName, Map<String, Object> args) {
for (SystemAdapter adapter : adapters) {
for (Tool tool : adapter.getTools()) {
if (tool.getName().equals(toolName)) {
return adapter.callTool(toolName, args);
}
}
}
throw new ToolNotFoundException();
}
- 容错与降级
引入 Resilience4j 进行熔断。当一个内部系统不可用时,及时切断,返回友好的错误提示,而不是让 Agent 无限等待。
@CircuitBreaker(name = "crm", fallbackMethod = "crmFallback")
public Object callCrmTool(String toolName, Map<String, Object> args) {
// 调用逻辑
}
public Object crmFallback(Exception e) {
return "CRM系统暂时不可用,请稍后重试。";
}
这个方案的价值在于: 内部系统的工程师不需要学 MCP,他们只需要维护自己的 API;而 AI 平台的工程师只需要写一次适配器,所有 Agent 就能立即“看懂”并使用整个公司的数字资产。这是一种非侵入式、可扩展、能渐进式建设的企业 AI 基础设施架构。