别再把 MCP 当成大模型的“手脚”:LLM 并不会直接调用 MCP

1. 概述

这是《ALL IN AI》专栏的第二篇文章。本篇的主角,是近年来 AI 领域备受关注的开放协议------MCP。

在上一篇文章《一文带你掌握 LLM、Token、Context、Prompt、RAG、MCP、Skill、Agent 等 AI 核心概念》中,我们已经对 MCP 做过简要介绍。本文将在此基础上进一步展开,系统讲清楚以下几个问题:

  • 为什么 AI 应用需要 MCP?
  • MCP 到底是什么,又不是什么?
  • Host、Client、Server 分别承担什么职责?
  • Tools、Resources、Prompts 有什么区别?
  • 一次 MCP 工具调用是如何完成的?
  • 如何开发并接入一个 MCP Server?
  • 在实际项目中使用 MCP 时,需要遵循哪些最佳实践?

先来看一张 MCP 架构图,对它建立一个整体印象:

2. 为什么需要 MCP?

基础大模型擅长理解、推理和生成内容,但它本身不能主动访问实时数据,也不能直接操作文件、数据库或业务系统。一个模型是否具备联网搜索、文件读写或调用业务接口的能力,取决于承载模型的 AI 应用是否为它接入了相应工具。

例如,在 DeepSeek 中关闭"智能搜索"工具后询问:"明天杭州天气怎么样?",模型无法直接获得实时天气数据:

这说明基础模型与外部世界之间存在一道天然边界。要让模型真正进入业务工作流,AI 应用通常需要连接:

  • 文件系统,用于读取和修改本地文件;
  • 数据库,用于查询和更新业务数据;
  • GitHub、GitLab,用于读取 Issue、PR 和代码;
  • Slack、飞书、Notion、Google Drive,用于获取团队上下文;
  • 浏览器、自动化脚本和内部 API,用于执行真实操作。

在没有统一协议的情况下,每个 AI 应用都需要针对每个外部系统编写一套适配代码:

erlang 复制代码
每个 AI 应用  →  为每个外部系统编写定制集成
​
Claude  ↔  Slack    (一套定制代码)
Claude  ↔  GitHub   (一套定制代码)
Claude  ↔  Jira     (一套定制代码)
ChatGPT ↔  Slack    (另一套定制代码)
ChatGPT ↔  GitHub   (另一套定制代码)
...

如果有 M 个 AI 应用和 N 个外部系统,理论上最多可能产生 M × N 组集成关系。随着应用和工具数量增加,开发、维护、鉴权和升级成本都会迅速上升。

MCP 要解决的核心问题,就是让 AI 应用与外部系统之间形成标准化连接。

可以先用一句话理解 MCP:

MCP(Model Context Protocol,模型上下文协议)是一套开放协议,用于标准化 AI 应用连接外部工具、数据和提示模板的方式。

它并不替代大模型,也不负责模型推理,而是为 AI 应用接入外部能力提供统一的协议层。

3. MCP 到底是什么?

MCP 的全称是 Model Context Protocol,由 Anthropic 发起并开源,现已发展为面向 AI 应用生态的开放标准。

如果把 USB-C 理解为电子设备之间的通用连接标准,那么 MCP 可以理解为 AI 应用与外部能力之间的通用连接标准。只要客户端和服务端都遵循同一套协议,它们就可以通过统一的消息格式完成能力发现、调用和结果返回。

需要特别说明的是,MCP Server 不会直接被大模型调用。真正与模型交互的是 MCP Host,也就是承载 AI 能力的应用。MCP Server 通过 Host 和 Client,将外部数据与能力提供给模型使用。

因此,名称中的 Context 不应只理解为一段文本,而应理解为模型完成任务时可获得的广义上下文,包括:

  • 可读取的数据;
  • 可调用的工具;
  • 可复用的提示模板;
  • 工具执行后返回的结果。

3.1 MCP 不是什么?

为了避免概念混淆,还需要明确 MCP 的能力边界:

  • MCP 不是大模型,也不负责模型训练或推理;
  • MCP 不是 Agent 框架,不负责完整的任务规划和自主循环;
  • MCP 不是某个工具的具体实现,只规定工具如何被描述、发现和调用;
  • MCP 不是权限系统本身,具体的身份认证、授权和审批仍需由 Host、Server 及基础设施共同实现;
  • MCP 不等同于 Function Calling,二者处于不同层次。

Function Calling 主要解决"模型如何用结构化格式表达一次工具调用";MCP 主要解决"AI 应用如何以统一协议发现并连接外部能力"。在实际系统中,Host 可以先让模型通过 Function Calling 生成工具调用,再通过 MCP 将请求路由到对应的 Server。

3.2 三类核心能力

MCP Server 可以向客户端暴露三类核心能力:

能力 主要控制方 作用 示例
Tools 模型 提供可执行函数,用于查询信息或执行操作 查询天气、搜索文档、创建工单、发送消息
Resources 应用 提供由客户端读取和组织的上下文数据 文件内容、数据库 Schema、用户资料、项目文档
Prompts 用户 提供可选择、可复用的提示模板 代码审查模板、故障排查模板、周报生成模板

三者的区别不在于"是否包含数据",而在于它们的交互方式和主要控制方。

Tools:可执行的能力

Tools 是 MCP Server 暴露的可调用函数。它既可以执行只读查询,也可以产生外部副作用:

scss 复制代码
"出行服务 Server"的 Tools:
├── get_weather(city, date)       → 查询目的地天气
├── search_flights(city, date)    → 查询可选航班
├── book_flight(flight_no)        → 预订机票
└── cancel_booking(order_id)      → 取消订单

其中,get_weathersearch_flights 是只读工具,book_flightcancel_booking 则会改变外部系统状态。因此,不能简单地把 Tool 等同于"写操作"。

Resources:应用管理的上下文

Resources 用于向客户端提供可读取的数据:

arduino 复制代码
"出行服务 Server"的 Resources:
├── policy://refund               → 退改签规则
├── profile://traveler/current    → 当前用户的出行偏好
└── itinerary://order/12345       → 指定订单的行程信息

Resources 通常由 Host 或应用逻辑决定何时读取、如何展示,以及是否加入模型上下文。它强调的是"上下文数据的标准化访问",而不是让模型直接操作外部系统。

Prompts:用户选择的提示模板

Prompts 是 Server 提供的可复用提示模板:

scss 复制代码
"出行服务 Server"的 Prompts:
├── plan_trip(destination, days)  → 生成旅行规划提示
└── handle_delay(order_id)        → 生成航班延误处理提示

Prompt 可以包含参数和预设消息,但它本身不等同于自动执行多个工具的工作流。用户选择 Prompt 后,客户端会将生成的消息加入对话,后续是否调用工具仍由模型、Host 策略和用户授权共同决定。

可以用一句话概括:

Resources 提供上下文,Tools 提供可执行能力,Prompts 提供可复用的交互入口。

4. MCP 的基本架构

MCP 采用 Client-Server 架构,主要包含 Host、Client 和 Server 三类角色:

角色 是什么 主要职责 示例
MCP Host 承载 AI 能力的应用 管理会话、调用模型、聚合上下文、控制权限、协调工具调用 Claude Code、Claude Desktop、VS Code、Cursor 等
MCP Client Host 内部的协议客户端 与某个 Server 建立会话、协商版本和能力、发送请求、接收响应与通知 Claude Code 内部的 MCP 连接组件
MCP Server 对外暴露能力的程序或服务 提供 Tools、Resources、Prompts,可运行在本地或远端 Weather MCP Server、GitHub MCP Server

整体关系如下图所示:

一个 Host 可以连接多个 MCP Server。通常情况下,Host 会为每个 Server 创建一个独立的 Client,并保持一对一会话关系:

arduino 复制代码
                     ┌─ MCP Client A ↔ Filesystem MCP Server
用户 ↔ MCP Host ─────┼─ MCP Client B ↔ GitHub MCP Server
                     └─ MCP Client C ↔ Database MCP Server

这种设计有两个重要意义:

  1. 能力隔离:每个 Server 只暴露自己负责的数据和工具;
  2. 安全隔离:Server 之间不应直接看到彼此的数据,也不应默认获得完整对话内容。

Host 是整个架构中的协调者。它决定连接哪些 Server、向模型提供哪些能力、是否要求用户确认,以及将哪些结果重新放入模型上下文。

5. MCP 的协议分层与传输方式

MCP 可以从数据层和传输层两个层次理解。

5.1 数据层

数据层定义客户端与服务端交换消息时使用的结构和语义,主要包括:

  • 基于 JSON-RPC 2.0 的请求、响应和通知;
  • 生命周期管理;
  • 协议版本与能力协商;
  • Tools、Resources、Prompts 等服务端能力;
  • 日志、进度通知等通用机制。

MCP 使用 JSON-RPC 2.0 作为基础消息格式。需要服务端返回结果的消息使用"请求---响应"模式;不需要返回结果的消息则可以使用通知。

例如,客户端初始化连接时会发送 initialize 请求:

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": {},
    "clientInfo": {
      "name": "example-client",
      "version": "1.0.0"
    }
  }
}

这里的版本号表示客户端优先支持的协议版本。Server 会在响应中返回最终采用的版本;如果双方无法协商出兼容版本,则应终止连接。

5.2 传输层

传输层负责承载数据层消息,当前主要包括两种标准传输方式:

方式 说明 常见场景
stdio Client 启动 Server 子进程,并通过标准输入、标准输出交换消息 本地文件系统、开发工具、本地数据库
Streamable HTTP 通过 HTTP POST/GET 通信,可选用 SSE 承载流式消息 SaaS 服务、云数据库、企业内部服务

需要注意,旧版的 HTTP+SSE 传输方式已被 Streamable HTTP 取代。SSE 仍然可以使用,但它是 Streamable HTTP 中可选的流式机制,而不是与 HTTP 并列的独立新方案。

本地和远程是常见的部署方式,stdio 与 Streamable HTTP 是协议定义的传输方式。二者相关,但不是完全等价的分类:

  • 本地 Server 通常使用 stdio;
  • 远程 Server 通常使用 Streamable HTTP;
  • 实际实现仍应根据安全性、部署环境和客户端支持情况选择。

6. MCP Server 实例与应用

下面通过两个案例说明如何开发本地 MCP Server,以及如何连接远程 MCP Server。

6.1 构建并连接本地 MCP Server

本节以天气服务为例,使用 Python 和官方 MCP Python SDK 构建一个可运行的 MCP Server,再通过 Claude Code 进行连接。

环境准备

示例要求安装 Python 3.10 或更高版本。首先安装 uv

arduino 复制代码
curl -LsSf https://astral.sh/uv/install.sh | sh

安装后重新打开终端,然后创建项目:

bash 复制代码
uv init weather
cd weather
​
uv venv
source .venv/bin/activate
​
uv add "mcp[cli]>=1.27,<2" httpx
touch weather.py

编写 Weather MCP Server

本文示例使用 MCP Python SDK 1.x 的稳定 API,因此显式添加了 <2 版本约束。SDK 2.x 对部分接口进行了重新设计,如果你使用的是 2.x,请参考对应版本的迁移文档,不要直接混用不同主版本的代码。

将以下代码写入 weather.py

python 复制代码
import json
import logging
from typing import Any
​
import httpx
from mcp.server.fastmcp import FastMCP
​
mcp = FastMCP("weather")
​
NWS_API_BASE = "https://api.weather.gov"
WTTR_BASE = "https://wttr.in"
USER_AGENT = "weather-mcp/1.0"
​
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
​
​
async def get_json(
    url: str,
    *,
    headers: dict[str, str] | None = None,
    params: dict[str, str] | None = None,
) -> dict[str, Any] | None:
    """请求 JSON 数据,并对网络或解析异常进行统一处理。"""
    try:
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.get(url, headers=headers, params=params)
            response.raise_for_status()
            return response.json()
    except (httpx.HTTPError, json.JSONDecodeError) as exc:
        logger.warning("Weather API request failed: %s", exc)
        return None
​
​
def format_alert(feature: dict[str, Any]) -> str:
    props = feature.get("properties", {})
    return "\n".join(
        [
            f"Event: {props.get('event', 'Unknown')}",
            f"Area: {props.get('areaDesc', 'Unknown')}",
            f"Severity: {props.get('severity', 'Unknown')}",
            f"Description: {props.get('description', 'No description')}",
            f"Instructions: {props.get('instruction', 'No instructions')}",
        ]
    )
​
​
@mcp.tool()
async def get_alerts(state: str) -> str:
    """查询美国指定州的有效天气预警。
​
    Args:
        state: 两位美国州代码,例如 CA、NY。
    """
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json",
    }
    data = await get_json(
        f"{NWS_API_BASE}/alerts/active/area/{state.upper()}",
        headers=headers,
    )
​
    if not data or "features" not in data:
        return "暂时无法获取天气预警,请稍后重试。"
​
    features = data["features"]
    if not features:
        return f"{state.upper()} 当前没有有效天气预警。"
​
    return "\n\n---\n\n".join(format_alert(item) for item in features[:10])
​
​
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """根据经纬度查询美国境内的天气预报。
​
    Args:
        latitude: 纬度。
        longitude: 经度。
    """
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json",
    }
    points = await get_json(
        f"{NWS_API_BASE}/points/{latitude},{longitude}",
        headers=headers,
    )
    if not points:
        return "无法获取该位置的天气网格信息。"
​
    forecast_url = points.get("properties", {}).get("forecast")
    if not forecast_url:
        return "天气服务未返回预报地址。"
​
    forecast = await get_json(forecast_url, headers=headers)
    if not forecast:
        return "无法获取详细天气预报。"
​
    periods = forecast.get("properties", {}).get("periods", [])[:5]
    if not periods:
        return "该位置暂无可用天气预报。"
​
    results = []
    for period in periods:
        results.append(
            "\n".join(
                [
                    f"{period.get('name', 'Unknown')}:",
                    f"Temperature: {period.get('temperature')}°"
                    f"{period.get('temperatureUnit', '')}",
                    f"Wind: {period.get('windSpeed', 'Unknown')} "
                    f"{period.get('windDirection', '')}",
                    f"Forecast: {period.get('detailedForecast', 'Unknown')}",
                ]
            )
        )
​
    return "\n\n---\n\n".join(results)
​
​
@mcp.tool()
async def get_global_forecast(city: str) -> str:
    """查询全球城市的当前天气和未来三天预报。
​
    Args:
        city: 城市名称,可附带国家或地区,例如 Shanghai、London,UK。
    """
    data = await get_json(
        f"{WTTR_BASE}/{city}",
        params={"format": "j1"},
    )
    if not data:
        return f"无法获取 {city} 的天气,请检查城市名称后重试。"
​
    current_list = data.get("current_condition", [])
    if not current_list:
        return f"天气服务未返回 {city} 的实时天气。"
​
    current = current_list[0]
    description = current.get("weatherDesc", [{}])[0].get("value", "Unknown")
​
    result = [
        f"地点:{city}",
        f"当前天气:{description}",
        f"温度:{current.get('temp_C', 'N/A')}°C",
        f"体感温度:{current.get('FeelsLikeC', 'N/A')}°C",
        f"湿度:{current.get('humidity', 'N/A')}%",
        "",
        "未来天气:",
    ]
​
    for day in data.get("weather", [])[:3]:
        result.append(
            f"- {day.get('date', 'Unknown')}:"
            f"{day.get('mintempC', 'N/A')}~{day.get('maxtempC', 'N/A')}°C"
        )
​
    return "\n".join(result)
​
​
if __name__ == "__main__":
    mcp.run(transport="stdio")

这段代码通过 @mcp.tool() 将三个 Python 函数声明为 MCP Tools:

  • get_alerts:查询美国指定州的天气预警;
  • get_forecast:通过经纬度查询美国天气;
  • get_global_forecast:通过城市名称查询全球天气。

函数签名和 Docstring 非常重要。SDK 会据此生成工具名称、描述和输入参数 Schema,而这些信息会直接影响模型能否正确选择工具、填写参数。

连接 Server

在需要使用该 Server 的项目根目录创建 .mcp.json

bash 复制代码
{
  "mcpServers": {
    "weather": {
      "type": "stdio",
      "command": "uv",
      "args": [
        "--directory",
        "${WEATHER_MCP_DIR}",
        "run",
        "weather.py"
      ]
    }
  }
}

先将 WEATHER_MCP_DIR 设置为 Weather 项目的实际路径,再从目标项目目录启动 Claude Code。相比在共享配置中写死个人绝对路径,环境变量更适合跨机器使用。

常用配置字段如下:

字段 必需 说明 示例
type Server 类型,stdio 配置中可显式声明 "stdio"
command 启动命令 "uv""npx""python"
args 传给启动命令的参数数组 ["run", "weather.py"]
env 传给 Server 进程的环境变量 {"API_KEY": "${API_KEY}"}

Claude Code 的启动超时可以通过 MCP_TIMEOUT 环境变量配置。例如,MCP_TIMEOUT=10000 claude 表示将 MCP Server 启动超时设置为 10 秒。具体配置能力可能随客户端版本变化,应以当前客户端文档为准。

进入 Claude Code 后执行 /mcp,即可查看 Server 的连接状态:

调用工具

连接成功后,可以直接询问:

明天杭州的天气如何?

Claude Code 会根据工具描述选择 get_global_forecast,填入城市参数,并通过 MCP 调用 Weather Server:

实际项目中,不一定需要从零开发 MCP Server。可以优先查找官方或可信厂商提供的实现,再根据安全性、维护状态、权限范围和部署方式进行评估。对于第三方 MCP Server,不应仅因为"安装方便"就默认信任。

6.2 连接远程 MCP Server

下面以 GitHub Remote MCP Server 为例,通过 Claude Code 添加远程 HTTP Server:

csharp 复制代码
claude mcp add --transport http github https://api.githubcopilot.com/mcp/

添加后,在 Claude Code 中执行 /mcp,按照界面提示完成 OAuth 认证。相比把长期有效的 Personal Access Token 直接写入命令或配置文件,OAuth 更适合交互式使用场景。

如果确实需要使用 Token,应通过环境变量或安全凭据存储注入,并遵循最小权限原则,避免将凭据写入项目文件、命令历史或日志。

常用管理命令如下:

csharp 复制代码
# 添加远程 MCP Server
claude mcp add --transport http <服务器名> <服务器地址>
​
# 列出所有 MCP Server
claude mcp list
​
# 查看指定 Server 的详情
claude mcp get github
​
# 删除指定 Server
claude mcp remove github

连接并完成认证后,即可让 Claude Code 查询仓库、Issue 或 Pull Request。涉及创建、修改或删除数据的操作时,仍应检查工具参数并保留必要的人工确认。

6.3 配置范围

Claude Code 支持三种 MCP Server 配置范围:

范围 可见性 适用场景 配置示例
local(默认) 仅当前用户、当前项目可用 私人配置、实验性 Server、敏感配置 claude mcp add --scope local ...
project 项目成员共享,保存在 .mcp.json 团队统一使用的项目工具 claude mcp add --scope project ...
user 当前用户的所有项目可用 跨项目使用的个人工具 claude mcp add --scope user ...

同名 Server 的优先级为:

sql 复制代码
local > project > user

需要注意:

  • project 范围的 .mcp.json 可以提交到版本库,但其中不应包含真实密钥;
  • 机器相关路径应通过环境变量提供;
  • 团队成员首次使用项目级 Server 时,应核对即将执行的命令或连接的地址;
  • 不要因为配置来自代码仓库,就默认其中的 Server 是可信的。

7. MCP 的工作原理

下面以一次工具调用为例,看看 MCP 如何完成从连接建立到结果回填的完整闭环:

7.1 初始化:建立连接并协商能力

当 Host 连接 MCP Server 时,会为其创建 MCP Client,并发送 initialize 请求。

初始化阶段主要完成:

  • 协商协议版本;
  • 交换客户端和服务端信息;
  • 声明双方支持的能力;
  • 建立后续通信所需的会话状态。
json 复制代码
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": {},
    "clientInfo": {
      "name": "all-in-ai-client",
      "version": "1.0.0"
    }
  }
}

Server 会返回最终采用的协议版本、服务端信息和能力声明,例如是否支持 Tools、Resources、Prompts,以及是否支持能力列表变化通知。

完成初始化后,客户端还会发送 notifications/initialized,表示初始化阶段已经结束。

7.2 能力发现:获取可用工具

连接建立后,Host 并不知道 Server 提供了哪些工具,需要通过 Client 请求工具列表:

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}

Server 返回的每个工具通常包含:

  • name:工具名称;
  • title:供界面展示的名称,可选;
  • description:工具用途说明;
  • inputSchema:输入参数的 JSON Schema;
  • outputSchema:结构化输出的 JSON Schema,可选;
  • annotations:只读、破坏性、幂等等行为提示,可选。

以天气工具为例,精简后的定义如下:

json 复制代码
{
  "name": "get_global_forecast",
  "description": "查询全球城市的当前天气和未来三天预报。",
  "inputSchema": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "城市名称,可附带国家或地区。"
      }
    },
    "required": ["city"]
  }
}

工具定义不仅是接口文档,也是模型选择工具时的重要依据。含糊的名称、描述和参数设计,会直接降低调用准确率。

7.3 工具选择与请求路由

Host 会以模型所支持的方式,将可用工具提供给模型。当用户提出"明天杭州天气怎么样"时,模型判断需要实时天气数据,于是生成对 get_global_forecast 的工具调用。

需要注意,模型生成的工具调用并不会直接发送给 MCP Server。Host 会先检查工具是否可用、是否满足权限策略、是否需要用户确认,然后将请求路由到对应的 MCP Client。

Client 再向 Server 发送 tools/call

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "get_global_forecast",
    "arguments": {
      "city": "Hangzhou"
    }
  }
}

Server 执行真实逻辑,并返回工具结果:

swift 复制代码
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "地点:Hangzhou\n当前天气:Partly cloudy\n温度:34°C\n未来三天:..."
      }
    ],
    "isError": false
  }
}

7.4 结果回填:重新进入模型上下文

工具返回结果后,Host 会将结果重新加入模型上下文,让模型基于真实数据继续推理并生成最终回答。

完整流程如下:

arduino 复制代码
用户提出问题
  ↓
Host 向模型提供可用工具
  ↓
模型生成工具调用
  ↓
Host 检查权限并进行路由
  ↓
MCP Client 向 MCP Server 发送 tools/call
  ↓
MCP Server 执行逻辑并返回结果
  ↓
Host 将结果放回模型上下文
  ↓
模型生成最终回答,或继续发起下一次调用

这也是 MCP 经常与 Agent 一同出现的原因:Agent 或 Host 负责规划、决策和多步推进,MCP 负责将外部能力以标准化方式接入系统。

下面是用户询问"明天杭州天气怎么样"时的完整调用流程:

8. MCP 开发与使用的最佳实践

MCP 降低了系统集成成本,但"能够连接"不代表"可以不受约束地调用"。在真实项目中,还需要关注工具设计、安全、性能和可维护性。

8.1 设计清晰的工具接口

工具名称、描述和参数 Schema 会直接影响模型的选择结果:

  • 使用明确、稳定的动词命名,例如 get_ordercreate_issue
  • 在描述中说明工具用途、适用条件和限制;
  • 为参数提供清晰的类型、枚举、格式与说明;
  • 避免让一个工具同时承担多个无关职责;
  • 对返回结果定义稳定的结构,必要时提供 outputSchema

例如,与其设计一个含义模糊的 handle_order(action, data),不如拆分为:

scss 复制代码
get_order(order_id)
cancel_order(order_id, reason)
update_shipping_address(order_id, address)

8.2 区分只读操作与写操作

查询、创建、修改和删除操作应尽量拆分为不同工具。这样可以:

  • 让模型更准确地选择能力;
  • 让 Host 实施不同的审批策略;
  • 降低误调用和越权风险;
  • 提高日志与审计的可读性。

Server 可以通过 Tool Annotations 提供 readOnlyHintdestructiveHintidempotentHint 等行为提示,但这些字段只是提示,不是安全边界。客户端不能仅凭未经信任的 Server 声明就自动放行高风险操作。

8.3 对高风险操作保留人工确认

以下操作通常不应在缺少确认的情况下自动执行:

  • 删除文件、数据或云资源;
  • 发送邮件、消息或公开内容;
  • 创建订单、付款或退款;
  • 修改生产环境配置;
  • 合并代码、发布版本或关闭工单;
  • 批量修改外部系统状态。

确认界面应展示工具名称、关键参数、目标对象和可能产生的影响,而不是只显示一个笼统的"是否允许"。

8.4 遵循最小权限原则

无论本地还是远程 Server,都只应获得完成任务所必需的权限:

  • 为不同工具拆分细粒度权限;
  • 默认授予只读权限,按需提升;
  • 缩小文件系统可访问目录;
  • 限制数据库账号可访问的库、表和操作;
  • 为 Token 设置最小 Scope 和合理有效期;
  • 不要让所有 Server 共享同一组高权限凭据。

MCP 会让外部能力更容易组合,因此更需要关注多工具组合后的整体风险。

8.5 安全管理密钥和身份凭据

  • 不要在 .mcp.json、源代码或命令参数中硬编码密钥;
  • 使用环境变量、系统钥匙串或 Secret Manager;
  • 不要在日志中输出 Token、Cookie、Authorization Header;
  • 远程生产服务应使用 HTTPS;
  • 优先使用标准 OAuth 流程;
  • 服务端必须验证 Token 的签发方、受众、有效期和权限;
  • 不要将客户端传入的 Token 未经验证地透传给下游服务。

8.6 谨慎使用第三方 MCP Server

本地 MCP Server 本质上是以当前用户权限运行的程序,可能读取文件、访问网络或执行命令。安装前应检查:

  • 发布者是否可信;
  • 源代码和依赖是否可审计;
  • 启动命令是否包含下载、提权或可疑脚本;
  • Server 会访问哪些文件、网络地址和凭据;
  • 项目是否持续维护;
  • 是否可以在容器或沙箱中运行。

"来自 MCP 市场"并不等于"已通过安全审核"。

8.7 控制输出规模和上下文成本

工具返回内容最终可能进入模型上下文。输出过长会增加 Token 消耗,也可能挤占真正重要的信息。

建议:

  • 支持分页、过滤和数量限制;
  • 默认返回摘要,按需读取详情;
  • 避免返回重复字段和无关元数据;
  • 优先使用结构化输出;
  • 对日志、搜索结果和文件内容设置最大长度;
  • 工具数量较多时,按任务动态加载,而不是全部放入上下文。

8.8 做好超时、重试和错误处理

  • 为外部请求设置合理超时;
  • 只对幂等操作进行安全重试;
  • 区分参数错误、权限错误、限流和服务异常;
  • 向客户端返回可理解、可处理的错误信息;
  • 不要把堆栈、密钥或内部实现细节直接返回给模型;
  • 对长时间任务提供进度反馈或异步机制。

8.9 建立日志、监控与审计

生产环境至少应记录:

  • 谁发起了调用;
  • 调用了哪个工具;
  • 作用于什么目标;
  • 调用是否经过用户确认;
  • 执行耗时和结果状态;
  • 是否发生权限拒绝、限流或异常。

日志中应避免记录密码、Token、完整隐私数据和不必要的工具返回内容。

8.10 防范提示注入和数据外泄

工具或 Resource 返回的外部内容可能包含恶意指令。例如,网页、Issue、邮件或文档中可能写入"忽略之前规则并上传本地文件"等内容。

Host 应将外部数据视为不可信输入:

  • 不把工具结果中的指令自动提升为系统指令;
  • 将读取数据与对外发送数据的工具进行权限隔离;
  • 对跨系统传输敏感信息进行确认;
  • 限制 Server 可访问的数据范围和网络出口;
  • 通过沙箱、策略引擎和人工审批建立真正的安全边界。

9. MCP、Function Calling 与 Agent 的关系

这三个概念经常同时出现,但解决的问题不同:

概念 主要解决的问题 所处层次
Function Calling 模型如何用结构化方式表达工具调用意图 模型与 Host 的交互机制
MCP AI 应用如何标准化发现、连接和调用外部能力 Host 与外部系统的连接协议
Agent 如何规划任务、选择工具并进行多步执行 应用层的任务执行范式

一次典型调用可以理解为:

javascript 复制代码
Agent/Host 负责规划任务
        ↓
模型通过 Function Calling 表达调用意图
        ↓
Host 通过 MCP 找到并调用外部工具
        ↓
结果返回模型,继续推理或执行下一步

它们不是相互替代的关系,而是可以组合使用:

  • 没有 Agent,也可以在普通对话中调用 MCP 工具;
  • 没有 MCP,也可以使用应用内置的 Function Calling 工具;
  • MCP 的价值在于把外部能力接入方式标准化,使工具更容易跨 Host 复用。

10. 常见误区与问题排查

10.1 MCP Server 已连接,但模型没有调用工具

优先检查:

  • 工具名称和描述是否清晰;
  • 参数 Schema 是否正确;
  • 工具是否真的适合当前问题;
  • Host 是否把该工具提供给了模型;
  • 是否存在同名或职责重叠的工具;
  • 工具数量是否过多,导致选择困难。

10.2 本地 Server 无法启动

可以从以下方向排查:

  • command 是否在当前环境的 PATH 中;
  • args 中的路径是否正确;
  • Python 或 Node.js 版本是否满足要求;
  • 依赖是否已安装;
  • Server 是否向 stdout 输出了非协议内容;
  • 启动是否超过客户端超时时间。

stdio 模式下,stdout 应只用于输出 MCP 协议消息,调试日志应写入 stderr

10.3 远程 Server 返回 401 或 403

  • 检查是否完成 OAuth 认证;
  • 检查 Token 是否过期;
  • 检查 Scope 是否包含目标工具所需权限;
  • 检查 Server 是否验证了正确的 Token Audience;
  • 检查组织策略是否禁止连接该 Server。

10.4 工具调用成功,但回答质量不理想

  • 减少无关输出;
  • 返回结构化数据;
  • 明确字段含义和单位;
  • 为错误结果设置 isError
  • 在工具描述中写清适用条件;
  • 让 Host 只把真正需要的信息放回上下文。

11. 总结

MCP 的核心价值,不是让模型突然拥有新的推理能力,而是为 AI 应用连接外部世界提供一套统一协议。

通过 MCP:

  • Host 可以统一管理外部能力和用户会话;
  • Client 可以按照标准协议与 Server 通信;
  • Server 可以用一致的方式暴露 Tools、Resources 和 Prompts;
  • 开发者可以减少重复集成,提升工具的可复用性;
  • 团队可以在统一接口之上继续建设权限、审批、监控和审计能力。

理解 MCP 时,可以抓住三条主线:

  1. 架构上:Host 负责协调,Client 负责通信,Server 负责提供能力;
  2. 协议上:数据层定义消息语义,传输层负责承载消息;
  3. 应用上:模型或 Agent 负责决策,MCP 负责标准化连接,安全策略负责约束执行。

MCP 降低了 AI 应用连接工具和数据的门槛,但它不会自动解决权限、安全和可靠性问题。真正可用于生产环境的 MCP 系统,还需要清晰的工具设计、最小权限、人工确认、凭据保护、输出控制,以及完整的监控审计机制。

当越来越多的 AI 应用和外部服务采用统一协议后,开发者就不必反复为每一种组合编写定制连接代码。这正是 MCP 最值得关注的地方:它正在尝试为 AI 应用建立一套通用、可组合、可治理的能力连接层。

12. 参考资料

相关推荐
SimonKing1 小时前
别再写 setter 了!MapStruct Plus vs MapStruct,谁才是 Bean 转换的真神?
java·后端·程序员
名字还没想好☜1 小时前
Go for-range 循环变量陷阱:goroutine 里全打印同一个值(Go 1.22 前后差异)
开发语言·后端·golang·go
AI多Agent协作实战派1 小时前
AI多Agent协作系统实战(二十五):Spring Boot静态资源同步:为什么你改了代码但线上还是旧的?
后端
码栈研说1 小时前
Go 语言大白话入门 12 - JSON:工作里最常见的数据格式
后端·程序员
达达尼昂1 小时前
AI Native 工程实践:如何为 Claude 5 设计更有效的上下文
android·人工智能·后端
爱丶不疚1 小时前
Code Review「问意图」这件事,在 AI 时代还重要吗?
ai编程·vibecoding
用户2930750976691 小时前
从零搭建 AI 日记助手:Milvus 向量数据库 + RAG 实战
后端
zguigo1 小时前
结合苍穹外卖对redis进行总结和教学
后端
玉宇夕落1 小时前
Milvus向量数据库和cs bs架构学习
后端