MCP(Model Context Protocol,模型上下文协议)是Anthropic公司于2024年11月提出的开放标准协议,旨在解决AI系统与数据源、工具及物理设备间的互联难题,被称为"AI领域的USB-C接口"。
MCP可以帮助我们在LLM之上构建智能体和复杂的工作流,通过MCP可以提供给LLM可用的工具的列表以及能力信息,并且提供了标准化的接口。
MCP采用了客户端-服务器端的架构设计,一个Host可以连接多个MCP Server,MCP的架构如下:
在上图中
- MCP Hosts: 如 Claude Desktop、IDE 或 AI 工具,希望通过 MCP 访问数据的程序
- MCP Clients: 维护与服务器一对一连接的协议客户端
- MCP Servers: 轻量级程序,通过标准的 Model Context Protocol 提供特定能力
- 本地数据源: MCP 服务器可安全访问的计算机文件、数据库和服务
- 远程服务: MCP 服务器可连接的互联网上的外部系统(如通过 APIs)
MCP服务器
下面开始动手实践,编写一个简单的MCP服务器,提供查询城市天气预报的能力。按照官网的介绍,可以采用uv来创建虚拟环境和python项目,运行以下命令
pip install uv
uv init mcp-server-demo
uv add "mcp[cli]" httpx openai python-dotenv
另外还需要安装node,我是Ubuntu 18的环境,在安装最新版本的Node提示GLIBC版本不是2.28,因此还需要升级GLIBC,运行以下命令来添加软件源和安装libc6
deb http://security.debian.org/debian-security buster/updates main
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 112695A0E562B32A 54404762BBB6E853
sudo apt install libc6-dev libc6
为了提供天气预报的能力,我使用了高德天气提供的API,需要在高德地图的官网上基础 API 文档-开发指南-Web服务 API | 高德地图API进行注册,拿到API key。高德天气API需要传入地区的adcode来进行天气查询,例如广州市的编码是440100,这个信息可以在相关下载-Web服务 API | 高德地图API进行下载。
在mcp-server-demo目录下新建一个server.py文件,这个mcp server可以提供查询天气预报的工具能力,代码如下:
# server.py
import httpx
import json
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("Weather")
API_BASE = "https://restapi.amap.com/v3/weather/weatherInfo"
city_code_mapping = {}
async def make_weather_request(city: str) -> dict[str, any] | None:
params = {
"key": "XXXXX",
"city": city,
"extensions": "all"
}
async with httpx.AsyncClient() as client:
try:
response = await client.get(API_BASE, params=params, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
@mcp.tool()
async def get_forecast(city: str) -> str:
"""获取城市的天气预报
Args:
city: 城市名称
"""
try:
city_code = city_code_mapping[city]
except KeyError:
return "无法获取该城市的天气预报"
data = await make_weather_request(city_code)
if not data:
return "无法获取天气预报"
forecast_data= [city + '的天气预报为:']
for item in data['forecasts'][0]['casts']:
forecast_data.append(f"日期: {item['date']}, 白天天气: {item['dayweather']}, 夜晚天气: {item['nightweather']}, 白天气温: {item['daytemp']}, 夜晚气温: {item['nighttemp']}")
return "\n".join(forecast_data)
if __name__ == "__main__":
with open("city_code.json", "r", encoding="utf-8") as f:
city_code_mapping = json.load(f)
# Initialize and run the server
mcp.run(transport='stdio')
在以上代码中,@mcp.tool注解表示这个函数是一个MCP工具,MCP server会把这个注解的函数的描述暴露出去,这样子LLM就可以了解工具接受到的参数以及用途。
另外这个mcp server是以stdio方式运行,即在本地以进程通讯的方式,通过标准输入输出方式实现server与client的通讯。另外一种运行方式是sse,基于HTTP协议,通过Server-Sent-Events提供单向实时推送。当前先以stdio方式来进行测试。
MCP客户端
接着我们可以构建一个MCP客户端,在这个客户端里面我将调用火山引擎提供的DeepSeek V3大模型,通过提供MCP工具的信息给大模型,对用户的提问进行分析看是否要调用工具来完成回复。注意在大模型选择上要选择支持工具调用的大模型,例如DeepSeek V3, Qwen QwQ之类的,DeepSeek R1不是原生支持工具调用的,因为在训练时没有引入工具调用相关的结构化数据。新建一个名为client.py的文件,以下是代码:
import asyncio
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from openai import OpenAI
from dotenv import load_dotenv
import os
import json
load_dotenv() # load environment variables from .env
class MCPClient:
def __init__(self):
# Initialize session and client objects
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.openai = OpenAI(api_key = os.getenv("DEEPSEEK_KEY"), base_url = "https://ark.cn-beijing.volces.com/api/v3")
self.model = "deepseek-v3-250324"
async def connect_to_server(self, server_script_path: str):
"""Connect to an MCP server
Args:
server_script_path: Path to the server script (.py or .js)
"""
is_python = server_script_path.endswith('.py')
is_js = server_script_path.endswith('.js')
if not (is_python or is_js):
raise ValueError("Server script must be a .py or .js file")
command = "python" if is_python else "node"
server_params = StdioServerParameters(
command=command,
args=[server_script_path],
env=None
)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize()
# List available tools
response = await self.session.list_tools()
tools = response.tools
print("\nConnected to server with tools:", [tool.name for tool in tools])
async def process_query(self, query: str) -> str:
"""Process a query using Claude and available tools"""
messages = [
{
"role": "user",
"content": query
}
]
response = await self.session.list_tools()
available_tools = [{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
}
} for tool in response.tools]
response = self.openai.chat.completions.create(
model = self.model, # your model endpoint ID
messages = messages,
tools = available_tools
)
content = response.choices[0]
print(content)
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")
# 将结果存入消息历史
messages.append(content.message.model_dump())
messages.append({
"role": "tool",
"content": result.content[0].text,
"tool_call_id": tool_call.id,
})
# 将结果返回给大模型生成最终响应
response = self.openai.chat.completions.create(
model = self.model,
messages = messages,
temperature=0.6
)
return response.choices[0].message.content
return response.choices[0].message.content
async def chat_loop(self):
"""Run an interactive chat loop"""
print("\nMCP Client Started!")
print("Type your queries or 'quit' to exit.")
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:
print(f"\nError: {str(e)}")
async def cleanup(self):
"""Clean up resources"""
await self.exit_stack.aclose()
async def main():
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
sys.exit(1)
client = MCPClient()
try:
await client.connect_to_server(sys.argv[1])
await client.chat_loop()
finally:
await client.cleanup()
if __name__ == "__main__":
import sys
asyncio.run(main())
在以上的代码中,在connect_to_server函数中, 启动mcp服务器脚本,获取服务器提供的工具列表,包含工具的描述和参数信息。在chat_loop函数中,当与大模型对话时,提供工具列表信息给大模型,这样大模型就可以根据用户的问题选择是否使用工具来提供辅助信息,如果需要调用工具,通过mcpclientsession进行工具调用,然后把结果附加到历史对话消息中,大模型就会根据工具提供的信息来进行回复。
运行效果
通过以下命令启动
python client.py server.py
然后在Query中输入问题,例如我询问广州市未来的天气预报
从以上截图可以看到,大模型根据用户的问题判断需要调用预测天气的工具,从打印出的消息finish_reason='tool_calls', tool_calls=[ChatCompletionMessageToolCall(id='call_k18tfi38hscct2m7p9cnqsus', function=Function(arguments='{"city":"广州市"}', name='get_forecast'), type='function')])),可以看到大模型正确的判断了为了回复用户的问题,需要调用get_forecast这个mcp工具,并传入参数city=广州市
在之后的Info中也可以看到成功调用了高德天气的API并返回了结果。最后大模型根据工具返回的结果正确的给出了未来几天的天气预报信息。