在构建由大型语言模型(LLM)驱动的智能应用时,如何让 AI 不仅仅停留在文本生成,而是能够与外部世界的数据和工具进行交互,是一个核心挑战。Anthropic 推出的 Model Context Protocol (MCP) 正是为了解决这一问题,它提供了一个标准化的框架,让 AI 应用能够安全、高效地获取上下文信息并调用外部功能。
本文将深入探讨 MCP 的核心概念、三大组件、完整实现、使用流程与传统插件机制的区别。
一、 什么是MCP?
MCP (Model Context Protocol) 是一个开源标准和框架,由Anthropic 在2024年11月份提出,旨在连接 AI 应用程序与外部系统。它为 AI 助手提供了一种标准化的方式,使其能够无缝地与外部数据源(如内容管理系统、数据库、企业应用程序等)和各种工具进行集成。
简单来说,MCP 使得 LLM 应用程序能够:
- 获取实时或领域特定的上下文信息,超越其训练数据的限制。
- 执行外部操作,例如搜索网页、查询数据库、发送邮件等。
它解决了 AI 助手在与外部世界交互时,如何安全、高效、标准化地获取和利用上下文信息的问题。
二、 MCP 的三大核心组件
MCP 架构由三个关键角色组成,它们协同工作,共同实现了 AI 应用与外部世界的连接:
1. Host (AI 应用本体)
- 角色: 承载 AI 核心逻辑的应用程序,例如 Cursor、Claude Desktop、Dify、Gptbots等或者您自己开发的 AI应用。
- 核心功能:
- 管理用户界面 (UI) 和对话历史: 提供用户交互界面,并维护与用户的对话记录。
- 调用 LLM: 负责与底层的大型语言模型(如 OpenAI 的 GPT 系列、Anthropic 的 Claude 系列等)进行交互,发送提示并接收响应。
- 挂载 MCP Client: 在其内部集成 MCP Client,将 MCP Server 暴露的工具映射成 LLM 可以理解和调用的
tools(Function Calling)。 - 处理
tool_calls: 当 LLM 决定调用某个工具时,Host 会将模型生成的tool_calls请求,转发给 MCP Client,进而触发对 MCP Server 的实际调用。
2. Client (MCP Client,Host 内的一层运行时)
- 角色: 位于 Host 内部的运行时层,负责实现 MCP 协议并管理与 MCP Server 的连接。
- 核心功能:
- 协议实现者 + 进程/连接管理者:
-
本地 stdio 模式: 负责启动本地的 MCP Server 进程(例如通过
uvx mcp-server-time或python mcp_server.py命令),并通过标准输入/输出 (stdin/stdout) 使用 MCP JSON-RPC 协议进行通信。 -
示例:获取当前时间的 MCP Server
比如使用 https://mcp.so 提供的一个获取当前时间的 mcp-server,其配置如下:json{ "mcpServers": { "time": { "command": "uvx", "args": [ "mcp-server-time", "--local-timezone=America/New_York" ] } } }执行原理:
-
先判断命令 Host 会从 PyPI 上下载
mcp-server-time这个包。这块需要强调下协议本身不限制 command 是什么,Host 就是 spawn(command, args) 开子进程;所以你可以内置任意多种:python、uvx、node、npx、docker、自家 CLI......都行,但是host端必须存在对应工具,比如mcp是docker命令执行一个镜像,那host端必须安装了docker,否则启动进程会报错。
-
找到它在 pyproject.toml 里声明的 console_scripts,例如
[project.scripts]
mcp-server-time = "mcp-server-time.server:main" -
Host 的 MCP Client 会启动一个新进程,执行
my_mcp_demo.server:main()。 -
在这个
main()函数内部,会调用server.run_stdio(),从而使这个新进程成为一个 MCP Server。 -
这个
mcp-server-time服务内部都是基于标准 MCP 协议开发的,例如包含list_tools、call_tool等方法。
-
-
远程 HTTP/SSE 模式: 建立与远程 MCP Server 的 HTTP(S) + SSE(Server-Sent Events)长连接,将 MCP JSON-RPC 消息封装在事件流中发送。
-
- 协议实现者 + 进程/连接管理者:
3. Server (MCP Server)
- 角色: 真正定义"有哪些工具"以及如何执行这些工具的一方。
- 核心功能:
- 实现 MCP 协议规定的方法: 响应
initialize、tools/list、tools/call等协议方法。 - 内部注册工具: 注册具体的工具函数,例如
get_current_time(获取当前时间)、web_search(网页搜索) 等,并为每个工具提供其参数的 JSON Schema 定义。 - 返回工具元数据: 当收到
tools/list请求时,返回所有注册工具的元数据(包括工具名name、描述description和参数parameters)。 - 执行工具: 当收到
tools/call请求时,根据工具名和参数执行相应的内部工具,并将执行结果返回。 外部工具,工具方内部已经实现server了我们直接用即可,内部工具需要我们自己去写server。
- 实现 MCP 协议规定的方法: 响应
三、 MCP 的案例完整实现(可直接使用)
安装完包直接启动mcp_host即可。
项目地址gitee:mcp-test
项目目录

项目依赖
[project]
name = "mcp-test"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"dotenv>=0.9.9",
"openai>=1.0.0",
"mcp>=0.1.0",
]
mcp_client.py
python
import asyncio
from typing import Any, Dict, List
from mcp import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
class SDKMCPClient:
"""
当前实现仅支持 stdio 模式(command + args 启动本地进程),
如果要支持 URL/HTTP 形式,可以在此基础上再用 SDK 的 HTTP Transport 拓展。
"""
def __init__(self, command: str, args: List[str]) -> None:
self._command = command
self._args = args
async def _run_with_session(self, coro):
server_params = StdioServerParameters(command=self._command, args=self._args)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
return await coro(session)
def list_tools(self) -> List[Dict[str, Any]]:
async def _list(session: ClientSession):
tools = await session.list_tools()
# SDK 返回的是 Tool 对象列表,这里简单转成 dict 方便和方案二统一
tools_meta: List[Dict[str, Any]] = []
for t in tools.tools:
tools_meta.append(
{
"name": t.name,
"description": t.description or "",
"parameters": t.inputSchema or {}, # type: ignore[attr-defined]
}
)
return tools_meta
return asyncio.run(self._run_with_session(_list))
def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
async def _call(session: ClientSession):
result = await session.call_tool(name, arguments)
# result.content 里通常是 TextContent / JsonContent,这里简单返回原始结构
return [c.model_dump() for c in result.content]
return asyncio.run(self._run_with_session(_call))
def close(self) -> None:
# SDK 客户端使用 async context,每次调用都会创建/关闭连接,这里无需额外清理
return None
__all__ = ["SDKMCPClient"]
mcp_host.py
python
import json
import os
import sys
from typing import Any, Dict, List, Tuple
from dotenv import load_dotenv
from openai import OpenAI
from mcp_sdk_client import SDKMCPClient
def build_openai_tools_from_mcp(
server_name: str, tools_meta: List[Dict[str, Any]]
) -> Tuple[List[Dict[str, Any]], Dict[str, str]]:
"""
把某个 MCP Server 返回的工具描述,转换成 OpenAI Chat Completions 的 tools 结构。
同时返回 tool_name -> server_name 的映射。
"""
tools_for_openai: List[Dict[str, Any]] = []
tool_to_server: Dict[str, str] = {}
for t in tools_meta:
name = t["name"]
tool_to_server[name] = server_name
tools_for_openai.append(
{
"type": "function",
"function": {
"name": name,
"description": t.get("description", ""),
"parameters": t.get(
"parameters",
{"type": "object", "properties": {}, "required": []},
),
},
}
)
return tools_for_openai, tool_to_server
def load_mcp_servers_from_files() -> Dict[str, Dict[str, Any]]:
"""
从两个地方聚合 MCP Server 配置:
1. mcp_multi.json (可选,需要维护)
2. 自定义mcp工具
只保留带 command/args 的本地进程型 MCP server。
"""
servers: Dict[str, Dict[str, Any]] = {}
def merge_from_mcpservers_obj(obj: Any, src: str) -> None:
"""从一个可能包含 mcpServers 字段的对象里合并配置。"""
if not isinstance(obj, dict):
return
servers_cfg = obj.get("mcpServers") or {}
if not isinstance(servers_cfg, dict):
print(f"[Console-Host] 跳过 {src}:mcpServers 必须是对象。")
return
for name, cfg in servers_cfg.items():
if not isinstance(cfg, dict):
continue
command = cfg.get("command")
args = cfg.get("args", [])
if not command or not isinstance(args, list):
continue
servers[name] = {
"command": str(command),
"args": [str(a) for a in args],
}
def load_from_path(path: str) -> None:
"""
从JSON 文件里合并 MCP Server 配置。[ {"mcpServers": {...}}, {"mcpServers": {...}}, ... ]
"""
if not os.path.exists(path):
return
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
for idx, item in enumerate(data):
merge_from_mcpservers_obj(item, f"{path}[{idx}]")
except Exception as exc:
print(f"[Console-Host] 读取 {path} 失败: {exc}")
# 1) 本地项目内的配置
load_from_path(os.path.join(os.path.dirname(__file__), "mcp_multi.json"))
# 2) 内置两个 demo MCP Server(始终可用)
base_dir = os.path.dirname(__file__)
servers.setdefault(
"demo-math",
{
"command": sys.executable,
"args": [os.path.join(base_dir, "demo_math_server.py")],
},
)
servers.setdefault(
"demo-text",
{
"command": sys.executable,
"args": [os.path.join(base_dir, "demo_text_server.py")],
},
)
return servers
def main() -> None:
"""
- 从 ./mcp_multi.json 聚合所有本地自定义 MCP server
- 对每个 server 通过 SDKMCPClient.list_tools() 拉取工具
- 聚合所有工具为一个大 tools 列表
- 控制台循环读入用户输入,让 OpenAI 模型按需调用这些工具
"""
load_dotenv("dev.env")
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
print("请先设置环境变量 OPENAI_API_KEY,例如:")
print(" set OPENAI_API_KEY=你的key (PowerShell: $env:OPENAI_API_KEY=\"...\")")
return
model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
oa_client = OpenAI(api_key=api_key)
servers = load_mcp_servers_from_files()
if not servers:
print("[Console-Host] 没有找到任何 MCP Server 配置。")
print("你可以在 mcp_multi.json 中配置 mcpServers。")
return
print(f"[Console-Host] 已加载 MCP Server 配置: {list(servers.keys())}")
# 启动所有 MCP server,聚合 tools
clients: Dict[str, SDKMCPClient] = {}
server_tools: Dict[str, List[Dict[str, Any]]] = {}
tools_for_openai: List[Dict[str, Any]] = []
tool_to_server: Dict[str, str] = {}
for name, cfg in servers.items():
command = cfg["command"]
args = cfg["args"]
print(f"[Console-Host] 连接 MCP Server '{name}',命令: {[command] + args}")
try:
client = SDKMCPClient(command=command, args=args) # 启动server进程
tools_meta = client.list_tools()
except Exception as exc: # noqa: BLE001
print(f"[Console-Host] 连接 '{name}' 失败: {exc}")
continue
clients[name] = client
server_tools[name] = tools_meta
part_tools, mapping = build_openai_tools_from_mcp(name, tools_meta)
tools_for_openai.extend(part_tools)
tool_to_server.update(mapping)
print(f"[Console-Host] Server '{name}' 工具: {[t['name'] for t in tools_meta]}")
if not clients:
print("[Console-Host] 所有 MCP Server 都连接失败,退出。")
return
print(f"[Console-Host] 最终聚合工具数: {len(tools_for_openai)}")
print("现在可以开始对话了,输入 exit / quit 结束。")
messages: List[Dict[str, Any]] = []
while True:
try:
user_input = input("你:").strip()
except (EOFError, KeyboardInterrupt):
print()
break
if not user_input:
continue
if user_input.lower() in {"exit", "quit"}:
break
messages.append({"role": "user", "content": user_input})
try:
response = oa_client.chat.completions.create(
model=model,
messages=messages,
tools=tools_for_openai,
tool_choice="auto",
)
except Exception as exc: # noqa: BLE001
print(f"[Console-Host] 调用 OpenAI 出错: {exc}")
messages = []
continue
msg = response.choices[0].message
tool_calls = msg.tool_calls or []
if tool_calls:
print("[Console-Host] 模型决定调用 MCP 工具:")
assistant_tool_calls_payload: List[Dict[str, Any]] = []
for tc in tool_calls:
print(
f" - 工具名: {tc.function.name}, "
f"arguments: {tc.function.arguments}"
)
assistant_tool_calls_payload.append(
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments,
},
}
)
messages.append(
{
"role": "assistant",
"tool_calls": assistant_tool_calls_payload,
"content": None,
}
)
# 路由到对应的 MCP Server
for tc in tool_calls:
args = json.loads(tc.function.arguments or "{}")
tool_name = tc.function.name
server_name = tool_to_server.get(tool_name)
client = clients.get(server_name) if server_name else None
if not client:
print(
f"[Console-Host] 找不到处理工具 '{tool_name}' 的 MCP Server(映射缺失)。"
)
continue
print(
f"[Console-Host] 调用 MCP Server '{server_name}' 工具 '{tool_name}',参数: {args}"
)
try:
tool_result = client.call_tool(tool_name, args)
except Exception as exc: # noqa: BLE001
print(
f"[Console-Host] 调用 MCP Server '{server_name}' 工具 '{tool_name}' 失败: {exc}"
)
continue
print(
f"[Console-Host] 工具 '{tool_name}' 返回结果: {tool_result}"
)
messages.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(tool_result, ensure_ascii=False),
}
)
try:
response = oa_client.chat.completions.create(
model=model,
messages=messages,
)
except Exception as exc: # noqa: BLE001
print(f"[Console-Host] 第二次调用 OpenAI 出错: {exc}")
continue
final_msg = response.choices[0].message
answer = final_msg.content or ""
else:
answer = msg.content or ""
print(f"助手:{answer}")
messages.append({"role": "assistant", "content": answer})
if __name__ == "__main__":
main()
mcp_multi.json
json
[
{
"mcpServers": {
"context7": {
"command": "npx",
"args": [
"-y",
"@upstash/context7-mcp",
"--api-key",
"ctx7sk-9db997d9-9eeb-4c4b-9b0d-7b4f2656a8db"
]
}
}
},
{
"mcpServers": {
"time": {
"command": "uvx",
"args": [
"mcp-server-time",
"--local-timezone=America/New_York"
]
}
}
},
{
"mcpServers": {
"howtocook-mcp": {
"command": "npx",
"args": [
"-y",
"howtocook-mcp"
]
}
}
}
]
可以自行去mcp.so官网找一些可以启本地进程的mcp
demo_math_server.py
python
import asyncio
import random
from typing import Annotated
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("demo-math")
@mcp.tool()
async def add(
a: Annotated[float, "第一个数字"],
b: Annotated[float, "第二个数字"],
) -> Annotated[float, "a + b 的结果"]:
"""把两个数字相加。"""
await asyncio.sleep(0) # 协程占位
return a + b
@mcp.tool()
async def random_int(
minimum: Annotated[int, "最小整数(包含)"],
maximum: Annotated[int, "最大整数(包含)"],
) -> Annotated[int, "生成的随机整数"]:
"""在给定区间内生成一个随机整数。"""
await asyncio.sleep(0)
if minimum > maximum:
minimum, maximum = maximum, minimum
return random.randint(minimum, maximum)
def main() -> None:
"""
demo 数学 MCP Server:
- 工具 add(a, b)
- 工具 random_int(minimum, maximum)
通过 stdio 运行,满足 MCP 协议,可被 SDK 客户端连接。
"""
mcp.run()
if __name__ == "__main__":
main()
demo_text_server.py
python
import asyncio
from typing import Annotated
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("demo-text")
@mcp.tool()
async def upper(
text: Annotated[str, "要转换为大写的字符串"],
) -> Annotated[str, "大写结果"]:
"""把字符串转成大写。"""
await asyncio.sleep(0)
return text.upper()
@mcp.tool()
async def word_count(
text: Annotated[str, "要统计的文本"],
) -> Annotated[int, "按空格分词后的词数"]:
"""统计一句话里有多少个词(按空格粗略切分)。"""
await asyncio.sleep(0)
return len(text.split())
def main() -> None:
"""
demo 文本 MCP Server:
- 工具 upper(text)
- 工具 word_count(text)
通过 stdio 运行,满足 MCP 协议,可被 SDK 客户端连接。
"""
mcp.run()
if __name__ == "__main__":
main()
随便写了两个内置工具,可以自行修改测试
dev.env
OPENAI_API_KEY=sk-proj-xxxx
OPENAI_MODEL=gpt-4o
四、 典型的 MCP 使用流程
- 用户配置 MCP Server 信息: 用户在 AI 应用(Host)的配置中(例如一个
mcp.json文件)定义了有哪些 MCP Server,以及如何连接它们(是本地进程 stdio 模式还是远程 URL+SSE 模式)。 - Host 建立连接并获取工具列表:
- Host 读取配置后,通过其内部的 MCP Client 建立与 MCP Server 的连接(启动本地进程或连接远程 URL)。
- Client 按照 MCP 协议向 Server 发送
tools/list请求。 - MCP Server 响应请求,返回其内部注册的所有工具的列表,包括每个工具的名称、描述和参数的 JSON Schema。
- Host 映射工具为 LLM 可调用格式: Host 接收到 MCP Server 返回的工具列表后,将这些工具的元数据转换成 LLM(如 OpenAI 的 Function Calling 机制)可以理解和调用的
tools格式。这相当于"将 MCP 世界的工具安装进模型的插件系统"。 - 用户对话,LLM 判断是否需要工具:
- 用户与 AI 应用进行对话。
- Host 将用户输入和已映射的工具定义一同发送给 LLM(通过
chat.completionsAPI 调用,并带上tools参数)。 - LLM 根据对话内容和工具定义,判断是否需要调用某个工具来完成任务。如果需要,模型会返回一个
tool_calls响应,指明它"想用 MCP 里的某个工具"以及相应的参数。
- Host 用 MCP Client 调 tools/call 真正执行工具:
- 当 Host 收到 LLM 返回的
tool_calls时,它会通过 MCP Client 再次向 MCP Server 发送tools/call请求,真正执行模型指定的工具,并传入模型生成的参数。 - MCP Server 执行相应的内部工具,并将执行结果返回给 MCP Client。
- 当 Host 收到 LLM 返回的
- Host 把结果塞回对话,再让 LLM 给最终回答:
- Host 收到工具执行结果后,将这个结果作为新的上下文信息,再次塞回给 LLM(作为
tool_outputs)。 - LLM 结合之前的对话历史和工具执行结果,生成最终的回答并返回给用户。
- Host 收到工具执行结果后,将这个结果作为新的上下文信息,再次塞回给 LLM(作为
举例来说:
用户在 Cursor(Host)中配置了一个新的 MCP Server。Cursor 内置的 MCP Client 会立即启动这个 MCP 进程(如果是本地模式)或建立远程连接。Host 随后通过 Client 拉取 MCP Server 暴露的所有工具,并将它们转换为 OpenAI 的 tools 格式。当用户提问时,模型可能会决定调用一个 MCP 工具(例如 web_search),Host 通过 tools/call 将请求转发给 MCP Server。Server 执行搜索并返回结果,Host 再将搜索结果提供给模型,让模型生成最终的最终回答。
五、 MCP 与传统插件机制的区别
实际mcp就是提供了一种标准化的访问外部数据源的方式,他能做的插件也能做,但是插件与模型厂商挂钩,协议格式各不相同,对接复杂,mcp简化了这种流程,且更加开放 标准了 别人写好了mcp工具我们直接就能用。
function_call 与 MCP 的核心区别
在构建基于大型语言模型(LLM)的应用时,理解 function_call 和 Model Context Protocol (MCP) 这两个概念至关重要。它们虽然都与工具集成相关,但作用的"层级"和解决的问题截然不同。
1. function_call 是"LLM API 级别"的能力
function_call(或类似的工具调用机制,如 Anthropic 的 tool_use)是 LLM 提供商在其 API 中内置的一种能力。它解决的核心问题是:"这个模型如何在一次 API 调用里请求某个函数、传参,并让调用者拿到结果?"
对于开发者来说,使用 function_call 时,你需要自己搞定以下这些"手工工作":
- 工具发现: 去哪里找到这些可供 LLM 调用的函数(工具)?多个 server、几十上百个工具,怎么声明、分类、动态启用/禁用?
- 工具传输和生命周期: 这些函数(工具)是本地运行的、需要通过远程 HTTP API 调用的,还是通过命令行接口(CLI)执行的?连接、心跳、长任务、cancel、错误码处理?
- 服务厂商差异化处理: 如果有多个工具服务提供商,它们的 API schema/鉴权/错误码可能各不相同,如何统一处理?
因此,在没有 MCP 这样的协议层时,你的 AI 应用(Host)里,实际上做了很多繁琐且定制化的工作:
- 从 JSON 配置文件或特定的 MCP Server 拉取工具元数据。
- 把它们"翻译"成 OpenAI 或其他 LLM 平台所要求的
tools(Function Calling)格式。 - 收到
tool_calls再自己路由到不同的 server 去执行。
2. MCP 是"工具/Agent 生态层"的协议
Model Context Protocol (MCP) 则是一个更高层级的协议,它旨在解决更宏观的问题:"世界上所有想给 LLM 用的工具/Agent,要用什么统一的方式把自己挂出来,让任何 AI 应用都能方便地发现和使用?"
MCP 协议提供了一套标准化的机制,包括:
- 外部工具能力标准化(发现 + 传输)
- 工具发现:mcpServers 配置 + tools/list → Host 不需要为每个服务商自定义"列出我有哪些能力"的协议。
- tools/call + JSON‑RPC over stdio/HTTP/SSE → 不同传输方式下,语义和报文结构是一致的。
- 工具协议处理(鉴权 / 请求 / 响应 / 错误码)
- 各家服务商在自己的 MCP Server 里,把乱七八糟的内部 API(鉴权、数据结构、错误码)统统"翻译"为统一的 MCP 形状;
- Host 只跟 MCP Server 说话,看到的是统一的:
- initialize / tools/list / tools/call
- 标准 JSON‑RPC 报文
- 标准 error.code / error.message / error.data。
- 多端复用
- 同一个 MCP Server,可以被不同的 AI 应用(Host),如 Cursor、Claude Desktop 或你自己的自定义 Host,直接连接并复用,极大地提高了工具的生态复用性。
对于 AI 应用(Host)来说,有了 MCP 这一层协议之后,其工作变得更加简化和标准化:
-
通过client按照mcp协议连上 mcp-server;
-
host连接client获取server的所有工具挂到 llm-function_call 里;
-
按规范把 tools.call 转发出去就行,不用管每家怎么实现。
可以这么说:在"能不能让模型调工具"这件事上,function_call 理论上都能做到 MCP 能做的事,但两者不在一个层级,MCP 是把一整层东西"标准化 + 外包"了。
总结,function_call 是 LLM 本身具备的"调用函数"的能力,而 MCP 则是一套"如何标准化地组织、发现和调用这些函数"的协议和生态系统。MCP 极大地简化了 Host(AI应用) 在工具集成方面的工作,将复杂性下沉到 Client 和 Server 层,从而促进了 AI 工具生态的繁荣和互操作性。