MCP 编程入门¶
模型上下文协议(MCP)是一个开源协议,用标准化方式让大语言模型(LLM)连接各种数据源与工具, 相当于 AI 应用的 USB-C 接口。
本页从零编写 MCP 服务器与客户端:FastMCP 工具 · Inspector 调试 · Sampling 人工监督 · Claude Desktop 配置 · DeepSeek / LangChain / cline 集成。
🟢 MCP 速览¶
Tools · Prompts · Resources · Sampling · Roots · Transports · stdio · SSE
| 核心功能 | 说明 |
|---|---|
| Tools 工具 | 本文重点,服务于通用大语言模型 |
| Prompts 提示词 | 生成 Prompt 模板 |
| Resources 资源 | 客户端可选择的预设资源,支持自定义协议 |
| Sampling 采样 | 在工具执行前后提供接口,可用于人工确认 |
| Roots 根目录 | — |
| Transports 传输层 | stdio(标准输入/输出)与 SSE(服务器发送事件) |
大部分功能服务于 Claude 客户端。本文更希望编写的 MCP 服务器服务于通用大语言模型, 因此以「工具」为重点,其余功能放在最后简单讲解。
传输层中 stdio 更为常用,本文以 stdio 为例。环境:Python 3.11,用 uv 管理项目。
graph LR
LLM[大语言模型<br/>DeepSeek / GPT-4o] <--> C[MCP 客户端]
C <-->|stdio| S[MCP 服务器]
S --> T[外部工具 / API<br/>智谱搜索 / FLUX 出图]
🛠️ 开发 MCP 服务器¶
本节实现一个用于网络搜索的 MCP 服务器。首先用 uv 初始化项目,
uv 官方文档见 docs.astral.sh/uv。
# 初始化项目
uv init mcp_getting_started
cd mcp_getting_started
# 创建虚拟环境并进入虚拟环境
uv venv
.venv\Scripts\activate.bat
# 安装依赖
uv add "mcp[cli]" httpx openai
激活命令与平台相关
示例中的 .venv\Scripts\activate.bat 是 Windows 路径,Linux/macOS 对应 .venv/bin/activate。
MCP 提供了两个对象:mcp.server.FastMCP 和 mcp.server.Server,其中 FastMCP 是更高层的封装,
这里使用它。创建 web_search.py:
import httpx
from mcp.server import FastMCP
# 初始化 FastMCP 服务器
app = FastMCP('web-search')
实现工具方法非常简单:用 @app.tool() 装饰器装饰函数即可。函数名作为工具名称,参数作为工具参数,
注释用于描述工具、参数及返回值。
搜索直接使用智谱接口,它不仅能搜索到相关结果链接,还会生成对应链接中文章总结后的内容,且现阶段免费。
| 资源 | 链接 |
|---|---|
| Web Search Pro 文档 | bigmodel.cn |
| API Key 生成 | 用户中心 |
- 换成自己在用户中心申请的 API Key。
- 逐层解析
choices → tool_calls → search_result,拼接所有content。
添加运行服务器的代码:
🔍 调试 MCP 服务器¶
使用官方提供的 Inspector 可视化工具调试服务器(需先安装 Node 环境)。
运行成功后打开提示的地址,点击左侧的 Connect 按钮连接服务,然后切换到 Tools 栏,
点击 List Tools 即可看到刚写的工具并开始调试。
- 启动 Inspector
- 点击
Connect连接服务 - 切换到
Tools→List Tools调试
🧩 开发 MCP 客户端¶
直连调用工具¶
先看如何在客户端调用刚才开发的 MCP 服务器中的工具:
import asyncio
from mcp.client.stdio import stdio_client
from mcp import ClientSession, StdioServerParameters
# 为 stdio 连接创建服务器参数
server_params = StdioServerParameters(
# 服务器执行的命令,这里使用 uv 来运行 web_search.py
command='uv',
# 运行的参数
args=['run', 'web_search.py'],
# 环境变量,默认为 None,表示使用当前环境变量
# env=None
)
async def main():
# 创建 stdio 客户端
async with stdio_client(server_params) as (stdio, write):
# 创建 ClientSession 对象
async with ClientSession(stdio, write) as session:
# 初始化 ClientSession
await session.initialize()
# 列出可用的工具
response = await session.list_tools()
print(response)
# 调用工具
response = await session.call_tool('web_search', {'query': '今天杭州天气'})
print(response)
if __name__ == '__main__':
asyncio.run(main())
由于 Python 脚本需要在虚拟环境中运行,这里通过 uv 启动脚本。
让 DeepSeek 调用 MCP 工具¶
用 dotenv 管理相关环境变量:
OPENAI_API_KEY=sk-89baxxxxxxxxxxxxxxxxxx
OPENAI_BASE_URL=https://api.deepseek.com
OPENAI_MODEL=deepseek-chat
Key 已脱敏
上面的 OPENAI_API_KEY 是示例值,使用时请换成自己的 Key。
首先编写 MCPClient 类:
import json
import asyncio
import os
from typing import Optional
from contextlib import AsyncExitStack
from openai import OpenAI
from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
load_dotenv()
class MCPClient:
def __init__(self):
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.client = OpenAI()
添加 connect_to_server 方法初始化 MCP 服务器的 session:
async def connect_to_server(self):
server_params = StdioServerParameters(
command='uv',
args=['run', 'web_search.py'],
env=None
)
stdio_transport = await self.exit_stack.enter_async_context(
stdio_client(server_params))
stdio, write = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(stdio, write))
await self.session.initialize()
再实现一个用于调用 MCP 服务器、处理与 DeepSeek 交互的方法:
async def process_query(self, query: str) -> str:
# 这里需要通过 system prompt 约束大语言模型,
# 否则会出现不调用工具、自己乱回答的情况
system_prompt = (
"You are a helpful assistant."
"You have the function of online search. "
"Please MUST call web_search tool to search the Internet content before answering."
"Please do not lose the user's question information when searching,"
"and try to maintain the completeness of the question content as much as possible."
"When there is a date related question in the user's question,"
"please use the search function directly to search and PROHIBIT inserting specific time."
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": query}
]
# 获取所有 mcp 服务器工具列表信息
response = await self.session.list_tools()
# 生成 function call 的描述信息
available_tools = [{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
}
} for tool in response.tools]
# 请求 deepseek,function call 的描述信息通过 tools 参数传入
response = self.client.chat.completions.create(
model=os.getenv("OPENAI_MODEL"),
messages=messages,
tools=available_tools
)
# 处理返回的内容
content = response.choices[0]
if content.finish_reason == "tool_calls":
# 如果需要使用工具,就解析工具
tool_call = content.message.tool_calls[0]
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
# 执行工具
result = await self.session.call_tool(tool_name, tool_args)
print(f"\n\n[Calling tool {tool_name} with args {tool_args}]\n\n")
# 将 deepseek 返回的调用工具数据和工具执行完成后的数据都存入 messages 中
messages.append(content.message.model_dump())
messages.append({
"role": "tool",
"content": result.content[0].text,
"tool_call_id": tool_call.id,
})
# 将上面的结果再返回给 deepseek,用于生成最终结果
response = self.client.chat.completions.create(
model=os.getenv("OPENAI_MODEL"),
messages=messages,
)
return response.choices[0].message.content
return content.message.content
接着实现循环提问,以及在最后退出后关闭 session:
async def chat_loop(self):
while True:
try:
query = input("\nQuery: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query)
print("\n" + response)
except Exception as e:
import traceback
traceback.print_exc()
async def cleanup(self):
"""Clean up resources"""
await self.exit_stack.aclose()
最后完成运行客户端的代码:
async def main():
client = MCPClient()
try:
await client.connect_to_server()
await client.chat_loop()
finally:
await client.cleanup()
if __name__ == "__main__":
import sys
asyncio.run(main())
连接多个 MCP 服务器
这是最精简的代码,没有实现记录上下文消息等功能,目的只是用最简单的代码了解如何通过大语言模型调动
MCP 服务器。这里只演示连接单个服务器;若需连接多个,只需循环 connect_to_server 中的代码,
将其封装成类,把所有服务器中的工具遍历生成一个大的 available_tools,再根据大语言模型的返回
结果调用即可。
✋ Sampling:工具执行前后的人工监督¶
MCP 提供的 Sampling 功能,本质是在工具执行前后提供一个接口,可以在工具执行前后执行一些操作。
例如调用本地文件删除工具时,通常希望用户确认后再删除,此时即可使用该功能。
下面实现一个人工监督的小功能。首先创建模拟删除文件的 MCP 服务器:
# 服务端
from mcp.server import FastMCP
from mcp.types import SamplingMessage, TextContent
app = FastMCP('file_server')
@app.tool()
async def delete_file(file_path: str):
# 创建 SamplingMessage 用于触发 sampling callback 函数
result = await app.get_context().session.create_message(
messages=[
SamplingMessage(
role='user', content=TextContent(
type='text', text=f'是否要删除文件: {file_path} (Y)')
)
],
max_tokens=100
)
# 获取 sampling callback 函数的返回值,并根据返回值进行处理
if result.content.text == 'Y':
return f'文件 {file_path} 已被删除!!'
if __name__ == '__main__':
app.run(transport='stdio')
这里最重要的是通过 create_message 方法创建一个 SamplingMessage 类型的 message,
它会将该 message 发送给 sampling callback 对应的函数。
接着创建客户端代码:
# 客户端
import asyncio
from mcp.client.stdio import stdio_client
from mcp import ClientSession, StdioServerParameters
from mcp.shared.context import RequestContext
from mcp.types import (
TextContent,
CreateMessageRequestParams,
CreateMessageResult,
)
server_params = StdioServerParameters(
command='uv',
args=['run', 'file_server.py'],
)
async def sampling_callback(
context: RequestContext[ClientSession, None],
params: CreateMessageRequestParams,
):
# 获取工具发送的消息并显示给用户
input_message = input(params.messages[0].content.text)
# 将用户输入发送回工具
return CreateMessageResult(
role='user',
content=TextContent(
type='text',
text=input_message.strip().upper() or 'Y'
),
model='user-input',
stopReason='endTurn'
)
async def main():
async with stdio_client(server_params) as (stdio, write):
async with ClientSession(
stdio, write,
# 设置 sampling_callback 对应的方法
sampling_callback=sampling_callback
) as session:
await session.initialize()
res = await session.call_tool(
'delete_file',
{'file_path': 'C:/xxx.txt'}
)
# 获取工具最后执行完的返回结果
print(res)
if __name__ == '__main__':
asyncio.run(main())
stdio 下看不到工具内的 print
目前在工具里面打印的内容使用 stdio_client 无法显示到命令行窗口。调试时可以使用
mcp.shared.memory.create_connected_server_and_client_session:
# 客户端
from mcp.shared.memory import (
create_connected_server_and_client_session as create_session
)
# 这里需要引入服务端的 app 对象
from file_server import app
async def sampling_callback(context, params):
...
async def main():
async with create_session(
app._mcp_server,
sampling_callback=sampling_callback
) as client_session:
await client_session.call_tool(
'delete_file',
{'file_path': 'C:/xxx.txt'}
)
if __name__ == '__main__':
asyncio.run(main())
🖥️ Claude Desktop 加载 MCP Server¶
打开配置:点击 Developer 菜单,再点击 Edit Config 按钮,
打开 Claude 桌面端的配置文件 claude_desktop_config.json。
添加服务器,服务器需放在 mcpServers 层级下,参数有 command、args、env,
与 StdioServerParameters 初始化时的参数一致。
{
"mcpServers": {
"web-search-server": {
"command": "uv",
"args": [
"--directory",
"D:/projects/mcp_getting_started",
"run",
"web_search.py"
]
}
}
}
保存文件后重启 Claude 桌面端即可看到插件。也可以直接在插件目录下运行以下命令安装:
📦 其他功能:Prompt 与 Resource¶
MCP 提供了生成 Prompt 模板的功能,使用 prompt 装饰器即可:
from mcp.server import FastMCP
app = FastMCP('prompt_and_resources')
@app.prompt('翻译专家')
async def translate_expert(
target_language: str = 'Chinese',
) -> str:
return f'你是一个翻译专家,擅长将任何语言翻译成{target_language}。请翻译以下内容:'
if __name__ == '__main__':
app.run(transport='stdio')
用配置 Claude 桌面端 MCP 服务器的方法添加该服务器后,点击右下角的图标即可使用。 它会提示设置传入的参数,并在聊天窗口生成一个附件。
可以在 Claude 客户端上选择为用户提供的预设资源,同时也支持自定义协议:
from mcp.server import FastMCP
app = FastMCP('prompt_and_resources')
@app.resource('echo://static')
async def echo_resource():
# 返回的是,当用户使用这个资源时,资源的内容
return 'Echo!'
@app.resource('greeting://{name}')
async def get_greeting(name):
return f'Hello, {name}!'
if __name__ == '__main__':
app.run(transport='stdio')
通配符路径暂不支持
目前 Claude 桌面端无法读取资源装饰器设置的 greeting://{name} 这类通配符路径,未来将支持;
但在客户端代码中可以当作资源模板使用。
import asyncio
from pydantic import AnyUrl
from mcp.client.stdio import stdio_client
from mcp import ClientSession, StdioServerParameters
server_params = StdioServerParameters(
command='uv',
args=['run', 'prompt_and_resources.py'],
)
async def main():
async with stdio_client(server_params) as (stdio, write):
async with ClientSession(stdio, write) as session:
await session.initialize()
# 获取无通配符的资源列表
res = await session.list_resources()
print(res)
# 获取有通配符的资源列表(资源模板)
res = await session.list_resource_templates()
print(res)
# 读取资源,会匹配通配符
res = await session.read_resource(AnyUrl('greeting://liming'))
print(res)
# 获取 Prompt 模板列表
res = await session.list_prompts()
print(res)
# 使用 Prompt 模板
res = await session.get_prompt(
'翻译专家', arguments={'target_language': '英语'})
print(res)
if __name__ == '__main__':
asyncio.run(main())
🦜 在 LangChain 中使用 MCP 服务器¶
langchain-mcp-adapters 可以很方便地将 MCP 服务器集成到 LangChain 中:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")
server_params = StdioServerParameters(
command='uv',
args=['run', 'web_search.py'],
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# 获取工具列表
tools = await load_mcp_tools(session)
# 创建并使用 ReAct agent
agent = create_react_agent(model, tools)
agent_response = await agent.ainvoke({'messages': '杭州今天天气怎么样?'})
更详细的使用方法参考 langchain-mcp-adapters。
🎨 实战:DeepSeek + cline + 自定义 MCP = 图文大师¶
使用 VS Code 的 cline 插件,通过 DeepSeek 和自定义的图片生成 MCP 服务器构建一个图文大师应用。
图片生成使用 HuggingFace 上的 FLUX.1-schnell 模型
(模型空间)。
不使用 gradio_client 库,而是用 httpx 手搓,因为 gradio_client 可能出现编码错误的 bug。
# image_server.py
import json
import httpx
from mcp.server import FastMCP
app = FastMCP('image_server')
@app.tool()
async def image_generation(image_prompt: str):
"""
生成图片
:param image_prompt: 图片描述,需要是英文
:return: 图片保存到的本地路径
"""
async with httpx.AsyncClient() as client:
data = {'data': [image_prompt, 0, True, 512, 512, 3]}
# 创建生成图片任务
response1 = await client.post(
'https://black-forest-labs-flux-1-schnell.hf.space/call/infer',
json=data,
headers={"Content-Type": "application/json"}
)
# 解析响应获取事件 ID
response_data = response1.json()
event_id = response_data.get('event_id')
if not event_id:
return '无法获取事件 ID'
# 通过流式的方式拿到返回数据
url = f'https://black-forest-labs-flux-1-schnell.hf.space/call/infer/{event_id}'
full_response = ''
async with client.stream('GET', url) as response2:
async for chunk in response2.aiter_text():
full_response += chunk
return json.loads(full_response.split('data: ')[-1])[0]['url']
if __name__ == '__main__':
app.run(transport='stdio')
在虚拟环境下使用以下命令打开 MCP Inspector 调试工具:
接着在 VS Code 中安装 cline 插件,安装完成后配置 DeepSeek 的 API Key。点击右上角的
MCP Server 按钮打开 MCP Server 列表,切换到 Installed 标签,点击 Configure MCP Servers
编辑自定义 MCP 服务器:
{
"mcpServers": {
"image_server": {
"command": "uv",
"args": [
"--directory",
"D:/projects/mcp_getting_started",
"run",
"image_server.py"
],
"env": {},
"disabled": false,
"autoApprove": []
}
}
}
保存后,服务器连接成功时状态点会显示为绿色,此时即可开始使用:在输入框中输入要写的文章内容, cline 会自动调用工具生成图片并最终输出文章。