【AI】开发笔记:一文入门MCP开发,架起LLM与业务系统桥梁
1、什么是MCP协议?
MCP(Model Context Protocol,模型上下文协议) 是由 Anthropic 于去年11月正式提出的一种开放标准,旨在通过统一的客户端-服务器架构解决 LLM 应用与数据源连接的难题,MCP 使得 AI 应用能够安全地访问和操作本地及远程数据,为 AI 应用提供了连接万物的接口,提升了智能体Agent的开发效率。截止目前,已上千种MCP工具诞生,在强悍的MCP生态加持下,人人手搓Manus的时代即将到来。
2、MCP技术架构
MCP 遵循客户端-服务器架构(client-server),其中包含以下几个核心概念:
- MCP 主机(MCP Hosts):发起请求的 LLM 应用程序(例如 Claude Desktop、IDE 或 AI 工具)。
- MCP 客户端(MCP Clients):在主机程序内部,与 MCP server 保持 1:1 的连接。
- MCP 服务器(MCP Servers):为 MCP client 提供上下文、工具和 prompt 信息。
- 本地资源(Local Resources):本地计算机中可供 MCP server 安全访问的资源(例如文件、数据库)。
- 远程资源(Remote Resources):MCP server 可以连接到的远程资源(例如通过 API)。
3、MCP Client 工作流程
- MCP client 首先从 MCP server 获取可用的工具列表。
- 将用户的查询连同工具描述通过 function calling 一起发送给 LLM。
- LLM 决定是否需要使用工具以及使用哪些工具。
- 如果需要使用工具,MCP client 会通过 MCP server 执行相应的工具调用。
- 工具调用的结果会被发送回 LLM。
- LLM 基于所有信息生成自然语言响应。
- 最后将响应展示给用户。
4、MCP Server 关键组件
- 工具(Tools):定义允许 LLM 自动调用的函数(需要用户批准),包括查询和写入操作。
- 资源(Resources):定义 LLM 可以访问的只读数据源,为 LLM 提供上下文内容,如帮助文档、日志信息等。
- 提示(Prompts):定义可重用的提示模板,帮助用更好的引导 LLM 以标准化的方式完成任务。
这些功能使 MCP server 能够为 AI 应用提供丰富的上下文信息和操作能力,从而增强 LLM 的实用性和灵活性。
5、MCP 通信方式
-
标准输入输出(Standard Input/Output, stdio):客户端通过启动服务器子进程并使用标准输入(stdin)和标准输出(stdout)建立双向通信,一个服务器进程只能与启动它的客户端通信(1:1 关系)。stdio 适用于本地快速集成的场景,在本文中,我们将使用这种方式来编写 MCP 客户端。
-
服务器发送事件(Server-Sent Events, SSE):是一种基于 HTTP 协议的技术,允许服务器向客户端单向、实时地推送数据。服务器作为独立进程运行,客户端和服务器代码完全解耦,支持多个客户端随时连接和断开。
6、 本地Server(STDIO)开发实例
6.1、server端开发
创建server代码:server_stdio.py
from mcp.server.fastmcp import FastMCP
from utils.date import get_time
from utils.weather import get_weather, format_weather
# 初始化 FastMCP 服务器,命名为 “MyMCPServer”
mcp = FastMCP("MyMCPServer")
# 定义工具函数,获取指定城市的天气
@mcp.tool()
async def fetch_weather(city: str) -> str:
"""
输入指定城市的名称,返回今日天气查询结果。
:param city: 城市名称(需使用中文)
:return: 格式化后的天气信息
"""
data = await get_weather(city)
return format_weather(data)
# 定义工具函数,返回当前的日期和时间(ISO格式)
@mcp.tool()
def fetch_time():
"""返回当前的日期和时间。 """
return get_time()
# 定义只读资源,返回当前项目的README.md文件内容
@mcp.resource("file://README.md")
def get_file() -> str:
"""Return the contents of README.md file"""
with open("README.md", "r") as f:
return f.read()
# 定义提示词模板,结合用户入参生成预置格式的提示词
@mcp.prompt()
def role_prompt(role: str) -> str:
"""Create a prompt for role"""
return f"""You are a '{role}' with 10-year experience and an elite expert with deep knowledge, you can helper user in your filed
"""
#运行服务器,使用标准输入输出传输
if __name__ == "__main__":
mcp.run(transport='stdio')
utils/date.py:
import datetime
def get_timezone():
"""返回服务器的时区设置"""
# 获取当前本地时间
now = datetime.datetime.now()
# 获取时区信息
timezone = now.astimezone().tzinfo
return(f"当前时区信息: {timezone}")
def get_time():
"""返回服务器的日期和时间,格式为ISO 8601。 """
return datetime.datetime.now().isoformat()
#print(get_timezone())
utils/weather.py:
import json
import httpx
from typing import Dict, Any
import asyncio
async def get_weather(city: str):
"""返回指定城市的天气信息。"""
weather_api_url = "https://shanhe.kim/api/za/tianqi.php"
params = {
"city": city,
"type": "json" # json text
}
async with httpx.AsyncClient() as client:
try:
response = await client.get(weather_api_url, params=params, timeout=30.0)
response.raise_for_status()
return response.json() # 返回字典类型
except httpx.HTTPStatusError as e:
return {"error": f"HTTP 错误: {e.response.status_code}"}
except Exception as e:
return {"error": f"请求失败: {str(e)}"}
def format_weather(data: dict[str, Any] | str) -> str:
"""
将天气数据格式化为易读文本。
:param data: 天气数据(可以是字典或 JSON 字符串)
:return: 格式化后的天气信息字符串
"""
# 如果传入的是字符串,则先转换为字典
if isinstance(data, str):
try:
data = json.loads(data)
except Exception as e:
return f"无法解析天气数据: {e}"
# 如果数据中包含错误信息,直接返回错误提示
if "error" in data:
return f"⚠️ {data['error']}"
# 提取数据时做容错处理
#city = data.get("name", "未知")
city = data.get("data", {}).get("city", "未知")
#country = data.get("sys", {}).get("country", "未知")
temp = data.get("data", {}).get("current", {}).get("temp", "N/A")
humidity = data.get("data", {}).get("current", {}).get("humidity", "N/A")
wind_speed = data.get("data", {}).get("current", {}).get("windSpeed", "N/A")
# weather 可能为空列表,因此用 [0] 前先提供默认字典
#weather_list = data.get("weather", [{}])
#description = weather_list[0].get("description", "未知")
weather = data['data']['weather']
return (
f"城市:{city},当前温度: {temp}°C,湿度: {humidity},风速: {wind_speed} m/s,天气: {weather}\n"
)
"""
async def query_weather(city: str) -> str:
data = await get_weather(city)
return format_weather(data)
#测试代码
async def main():
# 测试函数
print("正在获取天气信息...")
weather_info = await query_weather('杭州')
print(weather_info)
asyncio.run(main())
"""
6.2、client端开发
创建client代码:client_stdio.py
import asyncio
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from dotenv import load_dotenv
import os
import json
from openai import OpenAI
# 加载 .env 文件,确保 API Key 受到保护
load_dotenv()
class MCPClient:
def __init__(self):
"""初始化 MCP 客户端"""
self.exit_stack = AsyncExitStack()
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
#self.anthropic = Anthropic()
# 创建OpenAI client
self.openai_api_key = os.getenv("DEEPSEEK_API_KEY") # 读取 OpenAI API Key
self.base_url = os.getenv("BASE_URL") # 读取 BASE YRL
self.model = os.getenv("MODEL") # 读取 model
if not self.openai_api_key:
raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置OPENAI_API_KEY")
self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url)
async def connect_to_server(self, server_script_path: str):
"""连接到 MCP 服务器并列出可用工具
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("服务器脚本必须是 .py 或 .js 文件")
command = "python" if is_python else "node"
server_params = StdioServerParameters(
command=command,
args=[server_script_path],
env=None
)
# 启动 MCP 服务器并建立通信
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()
# 列出 MCP 服务器上的工具
response = await self.session.list_tools()
tools = response.tools
print("\n已连接到服务器,支持以下工具:", [tool.name for tool in tools])
async def process_query(self, query: str) -> str:
"""
使用大模型处理查询并调用可用的 MCP 工具
"""
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.client.chat.completions.create(
model=self.model,
messages=messages,
tools=available_tools # Function Calling
)
# 处理返回的内容
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")
# 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入messages中
messages.append(content.message.model_dump())
messages.append({
"role": "tool",
"content": result.content[0].text,
"tool_call_id": tool_call.id,
})
# 将上面的结果再返回给大模型用于生产最终的结果
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
)
return response.choices[0].message.content
return content.message.content
async def chat_loop(self):
"""运行交互式聊天循环"""
print("\n🤖 MCP 客户端已启动!输入 'quit' 退出")
while True:
try:
query = input("\n你: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query) # 发送用户输入到 OpenAI API
print(f"\n🤖 OpenAI: {response}")
except Exception as e:
print(f"\n⚠️ 发生错误: {str(e)}")
async def cleanup(self):
"""清理资源"""
await self.exit_stack.aclose()
# main主入口
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())
# 启动客户端:uv run client_stdio.py server_stdio.py
6.3、自开发客户端测试
启动客户端,同时启动服务端子进程:
uv run client_stdio.py server_stdio.py
演示如下:
(mcp-demo) (base) root@ubuntu2204-dev:/data/Developer/Workspace/Python/MCP/mcp-demo# uv run client_stdio.py server_stdio.py
[04/12/25 01:20:13] INFO Processing request of type ListToolsRequest server.py:534
已连接到服务器,支持以下工具: ['fetch_weather', 'fetch_time']
🤖 MCP 客户端已启动!输入 'quit' 退出
你: 当前天气
[04/12/25 01:20:28] INFO Processing request of type ListToolsRequest server.py:534
🤖 OpenAI: 请告诉我您想查询哪个城市的天气(请使用中文城市名称),我可以为您提供最新的天气信息。
你: 杭州
[04/12/25 01:20:38] INFO Processing request of type ListToolsRequest server.py:534
[04/12/25 01:20:42] INFO Processing request of type CallToolRequest server.py:534
INFO HTTP Request: GET https://shanhe.kim/api/za/tianqi.php?city=%E6%9D%AD%E5%B7%9E&type=json "HTTP/1.1 200 OK" _client.py:1740
[Calling tool fetch_weather with args {'city': '杭州'}]
🤖 OpenAI: 杭州当前天气情况如下:
- **温度**:19.7°C
- **湿度**:90%
- **风速**:1级(约1米/秒)
- **天气状况**:阴转中雨
建议出行携带雨具,注意路面湿滑。如需其他信息,可随时告知!
你: 当前时间
[04/12/25 01:20:54] INFO Processing request of type ListToolsRequest server.py:534
[04/12/25 01:20:59] INFO Processing request of type CallToolRequest server.py:534
[Calling tool fetch_time with args {}]
🤖 OpenAI: 当前时间是 **2025年4月12日 01:20**(UTC+8)。如果需要更具体的时间或时区调整,请告诉我!
你: quit
6.4、Cherry Studio客户端测试:
6.4.1、添加本地 MCP 服务器:
参数设置如下:
--directory
D:\Workspace\JupyterSpace\MCP\mcp-demo
run
server_stdio.py
6.4.2、对话测试
7、远程 Server(SSE)开发实例
SSE 的主要特点包括:
- 单向通信:服务器主动向客户端推送数据,客户端无法通过同一连接向服务器发送数据。
- 基于 HTTP 协议:利用现有的 HTTP 协议,无需额外的协议支持,易于实现和部署。
- 轻量级:实现简单,适用于需要实时更新的应用场景,如新闻推送、股票行情等。
- 自动重连:客户端在连接断开时会自动尝试重新连接,确保数据传输的连续性。
7.1、server端开发
创建python文件:mcp-server-sse.py
from mcp.server.fastmcp import FastMCP
from datetime import datetime
from starlette.applications import Starlette
from starlette.routing import Route, Mount
from starlette.requests import Request
from starlette.responses import JSONResponse
from mcp.server.sse import SseServerTransport # 假设 SseServerTransport 在此处
from mcp.server import Server
import argparse
import uvicorn
from utils.weather import get_weather, format_weather
from utils.date import get_time
# 初始化 FastMCP 服务器,命名为 “MyMCPServer”
mcp = FastMCP("MyMCPServer")
# 定义工具函数,获取指定城市的天气
@mcp.tool()
async def fetch_weather(city: str) -> str:
"""
输入指定城市的名称,返回今日天气查询结果。
:param city: 城市名称(需使用中文)
:return: 格式化后的天气信息
"""
data = await get_weather(city)
return format_weather(data)
# 定义工具函数,返回当前的日期和时间(ISO格式)
@mcp.tool()
def fetch_time():
"""返回当前的日期和时间。 """
return get_time()
async def hello(request):
return JSONResponse({"message": "Welcome to MyMCPServer!"})
# 定义只读资源,返回当前项目的README.md文件内容
@mcp.resource("file://README.md")
def get_file() -> str:
"""Return the contents of README.md file"""
with open("README.md", "r") as f:
return f.read()
# 定义提示词模板,结合用户入参生成预置格式的提示词
@mcp.prompt()
def role_prompt(role: str) -> str:
"""Create a prompt for role"""
return f"""You are a '{role}' with 10-year experience and an elite expert with deep knowledge, you can helper user in your filed
"""
# 创建服务
def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette:
"""Create a Starlette application that can server the provied mcp server with SSE."""
sse = SseServerTransport("/messages/")
async def handle_sse(request: Request) -> None:
async with sse.connect_sse(
request.scope,
request.receive,
request._send,
) as (read_stream, write_stream):
await mcp_server.run(
read_stream,
write_stream,
mcp_server.create_initialization_options(),
)
return Starlette(
debug=debug,
routes=[
Route("/", endpoint=hello),
Route("/sse", endpoint=handle_sse),
Mount("/messages/", app=sse.handle_post_message),
],
)
#运行服务器,使用sse传输
if __name__ == "__main__":
mcp_server = mcp._mcp_server
parser = argparse.ArgumentParser(description='Run MCP SSE-based server')
parser.add_argument('--host', default='192.168.110.201', help='Host to bind to')
parser.add_argument('--port', type=int, default=8501, help='Port to listen on')
args = parser.parse_args()
# Bind SSE request handling to MCP server
starlette_app = create_starlette_app(mcp_server, debug=True)
uvicorn.run(starlette_app, host=args.host, port=args.port)
# 启动服务:python server_sse.py
7.2、client端开发
创建client代码:client_sse.py
import asyncio
import os
import json
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.sse import sse_client
# 加载 .env 文件,确保 API Key 受到保护
load_dotenv()
class MCPClient:
def __init__(self):
"""初始化 MCP 客户端"""
self.exit_stack = AsyncExitStack()
self.openai_api_key = os.getenv("DEEPSEEK_API_KEY") # 读取 OpenAI API Key
self.base_url = os.getenv("BASE_URL") # 读取 BASE YRL
self.model = os.getenv("MODEL") # 读取 model
if not self.openai_api_key:
raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置OPENAI_API_KEY")
self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url)
# 创建OpenAI client
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
async def connect_to_sse_server(self, server_url: str):
"""Connect to an MCP server running with SSE transport"""
# 创建 SSE 客户端连接上下文管理器
self._streams_context = sse_client(url=server_url)
# 异步初始化 SSE 连接,获取数据流对象
streams = await self._streams_context.__aenter__()
# 使用数据流创建 MCP 客户端会话上下文
self._session_context = ClientSession(*streams)
# 初始化客户端会话对象
self.session: ClientSession = await self._session_context.__aenter__()
# 执行 MCP 协议初始化握手
await self.session.initialize()
# 列出 MCP 服务器上的工具
response = await self.session.list_tools()
tools = response.tools
print("\n已连接到服务器,支持以下工具:", [tool.name for tool in tools])
async def process_query(self, query: str) -> str:
"""
使用大模型处理查询并调用可用的 MCP 工具 (Function Calling)
"""
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]
print(available_tools)
response = self.client.chat.completions.create(
model=self.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")
# 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入messages中
messages.append(content.message.model_dump())
messages.append({
"role": "tool",
"content": result.content[0].text,
"tool_call_id": tool_call.id,
})
# 将上面的结果再返回给大模型用于生产最终的结果
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
)
return response.choices[0].message.content
return content.message.content
async def chat_loop(self):
"""运行交互式聊天循环"""
print("\n🤖 MCP 客户端已启动!输入 'quit' 退出")
while True:
try:
query = input("\n你: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query) # 发送用户输入到 OpenAI API
print(f"\n🤖 OpenAI: {response}")
except Exception as e:
print(f"\n⚠️ 发生错误: {str(e)}")
async def cleanup(self):
"""清理资源"""
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_sse_server(sys.argv[1])
await client.chat_loop()
finally:
await client.cleanup()
if __name__ == "__main__":
import sys
asyncio.run(main())
# 启动客户端:uv run client_sse.py http://192.168.110.201:8501/sse
7.3、自开发客户端测试
7.3.1、首先启动Linux服务端
python server_sse.py
显示如下:
(mcp-demo) (base) root@ubuntu2204-dev:/data/Developer/Workspace/Python/MCP/mcp-demo# python server_sse.py
INFO: Started server process [924723]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://192.168.110.201:8501 (Press CTRL+C to quit)
7.3.2、然后启动Windows本地客户端
需指定服务端url:
uv run client_sse.py http://192.168.110.201:8501/sse
测试过程如下:
(mcp-demo) PS D:\Workspace\JupyterSpace\MCP\mcp-demo> uv run client_sse.py http://192.168.110.201:8501/sse
已连接到服务器,支持以下工具: ['fetch_weather', 'fetch_time']
🤖 MCP 客户端已启动!输入 'quit' 退出
你: 当前时间
[{'type': 'function', 'function': {'name': 'fetch_weather', 'description': '\n 输入指定城市的名称,返回今日天气查询结果。\n :param city: 城市名称(需使用 中文)\n :return: 格式化后的天气信息\n ', 'input_schema': {'properties': {'city': {'title': 'City', 'type': 'string'}}, 'required': ['city'], 'title': 'fetch_weatherArguments', 'type': 'object'}}}, {'type': 'function', 'function': {'name': 'fetch_time', 'description': '返回当前的日期和时间。 ', 'input_schema': {'properties': {}, 'title': 'fetch_timeArguments', 'type': 'object'}}}]
[Calling tool fetch_time with args {}]
🤖 OpenAI: 当前时间是2025年4月12日,凌晨1点05分(UTC时间)。如果需要转换为您的时区,请告诉我您所在的城市或时区。
你: 杭州天气
[{'type': 'function', 'function': {'name': 'fetch_weather', 'description': '\n 输入指定城市的名称,返回今日天气查询结果。\n :param city: 城市名称(需使用 中文)\n :return: 格式化后的天气信息\n ', 'input_schema': {'properties': {'city': {'title': 'City', 'type': 'string'}}, 'required': ['city'], 'title': 'fetch_weatherArguments', 'type': 'object'}}}, {'type': 'function', 'function': {'name': 'fetch_time', 'description': '返回当前的日期和时间。 ', 'input_schema': {'properties': {}, 'title': 'fetch_timeArguments', 'type': 'object'}}}]
[Calling tool fetch_weather with args {'city': '杭州'}]
🤖 OpenAI: 杭州当前天气为阴转中雨,气温19.9°C,湿度89%,风速1级(约1米/秒)。出行建议携带雨具,注意路面湿滑。
你: quit
7.4、Cherry Studio 客户端测试:
7.4.1、添加远程 MCP 服务器:
7.4.2、对话测试:
效果同上。
8、总结
参考
[1]: Model Context Protocol 官方文档
[2]: MCP Client 开发文档