一. stdio、SSE与基于HTTP的流式传输形式对比
1.1 MCP通信协议介绍
MCP(Model Context Protocol)是一种为了统一大规模模型和工具间通信而设计的协议,它定义了消息格式和通信方式。MCP 协议支持多种传输机制,其中包括 stdio、Server-Sent Events(SSE) 和 Streamable HTTP。每种通信方法在不同的应用场景中具有不同的优劣势,适用于不同的需求。
1.2 Stdio 传输(Standard Input/Output)
stdio 传输方式是最简单的通信方式,通常在本地工具之间进行消息传递时使用。它利用标准输入输出(stdin/stdout)作为数据传输通道,适用于本地进程间的交互。
- 工作方式:客户端和服务器通过标准输入输出流(stdin/stdout)进行通信。客户端向服务器发送命令和数据,服务器执行并通过标准输出返回结果。
- 应用场景:适用于本地开发、命令行工具、调试环境,或者模型和工具服务在同一进程内运行的情况。
1.3 Server-Sent Events(SSE)
SSE 是基于 HTTP 协议的流式传输机制,它允许服务器通过 HTTP 单向推送事件到客户端。SSE 适用于客户端需要接收服务器推送的场景,通常用于实时数据更新。
- 工作方式:客户端通过 HTTP GET 请求建立与服务器的连接,服务器以流式方式持续向客户端发送数据,客户端通过解析流数据来获取实时信息。
- 应用场景:适用于需要服务器主动推送数据的场景,如实时聊天、天气预报、新闻更新等。
1.4 Streamable HTTP
Streamable HTTP 是 MCP 协议中新引入的一种传输方式,它基于 HTTP 协议支持双向流式传输。与传统的 HTTP 请求响应模型不同,Streamable HTTP 允许服务器在一个长连接中实时向客户端推送数据,并且可以支持多个请求和响应的流式传输。
- 工作方式:客户端通过 HTTP POST 向服务器发送请求,并可以接收流式响应(如 JSON-RPC 响应或 SSE 流)。当请求数据较多或需要多次交互时,服务器可以通过长连接和分批推送的方式进行数据传输。
- 应用场景:适用于需要支持高并发、低延迟通信的分布式系统,尤其是跨服务或跨网络的应用。适合高并发的场景,如实时流媒体、在线游戏、金融交易系统等。
二. 基于SSE远程连接MCP Server流程
高德MCP服务通过SSE协议实现。接下来,我们将介绍这个给案例是如何实现的
- 注册成为高德开发者。可以直接通过淘宝或支付宝账号进行创建。访问lbs.amap.com/进入"控制台"。
- 点击"应用管理"->"我的应用",点击"创建新应用",输入应用名称和应用类型,点击创建即可。
- 点击添加Key,Key名称可以随意填写,服务平台选择Web服务,接下来就可以点击提交,创建一个key。
2.1. 从0到1创建项目流程
-
创建项目目录
bash# 创建项目目录 uv init mcp-amap-client cd mcp-amap-client -
创建且激活虚拟环境
bash# 创建虚拟环境 uv venv # 激活虚拟环境 source .venv/bin/activate #mac/linux .venv\Scripts\activate #windows #下载openai uv add openai -
安装sdk
csharp# 安装 MCP SDK(默认源) uv add mcp # 安装 MCP SDK(指定源) set UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple && uv add mcp -
编写项目源码
- 接下来在主目录下创建
/src/pro_name作为代码主目录,在其内部创建client脚本文件。
- 接下来在主目录下创建
-
python
import json import asyncio import os import re import sys from typing import Optional from contextlib import AsyncExitStack from mcp import ClientSession, StdioServerParameters from mcp.client.sse import sse_client from dotenv import load_dotenv from openai import AsyncOpenAI, OpenAI # 自动加载 .env 文件,避免在代码中直接暴露 API Key。 load_dotenv() def format_tools_for_llm(tool) -> str: """对tool进行格式化 Returns: 格式化之后的tool描述 """ # 遍历处理参数描述 args_desc = [] if "properties" in tool.inputSchema: for param_name, param_info in tool.inputSchema["properties"].items(): arg_desc = ( f"- {param_name}: {param_info.get('description', 'No description')}" ) if param_name in tool.inputSchema.get("required", []): arg_desc += " (required)" args_desc.append(arg_desc) return f"Tool: {tool.name}\nDescription: {tool.description}\nArguments:\n{chr(10).join(args_desc)}" class Client: def __init__(self): self._exit_stack: Optional[AsyncExitStack] = None self.session: Optional[ClientSession] = None self._lock = asyncio.Lock() self.is_connected = False #重点1:模型客户端,可自行适配不同模型和其对应的base_url、apikey和模型名称 self.client = AsyncOpenAI( base_url="https://api.deepseek.com", api_key=os.getenv("OPENAI_API_KEY") , ) self.model = "deepseek-chat" self.messages = [] #server连接函数 async def connect_server(self, server_config): async with self._lock: #重点2:提取servers_config.json配置文件中基于sse模式mcp server的远程连接url url = server_config["mcpServers"]["amap-amap-sse"]["url"] print(f"尝试连接到: {url}") self._exit_stack = AsyncExitStack() sse_cm = sse_client(url) streams = await self._exit_stack.enter_async_context(sse_cm) print("SSE 流已获取。") session_cm = ClientSession(streams[0], streams[1]) self.session = await self._exit_stack.enter_async_context(session_cm) print("ClientSession 已创建。") await self.session.initialize() print("Session 已初始化。") #获取并存储mcp server的工具列表 response = await self.session.list_tools() self.tools = {tool.name: tool for tool in response.tools} print(f"成功获取 {len(self.tools)} 个工具:") for name, tool in self.tools.items(): print(f" - {name}: {tool.description[:50]}...") # 打印部分描述 print("连接成功并准备就绪。") # 列出可用工具 response = await self.session.list_tools() tools = response.tools tools_description = "\n".join([format_tools_for_llm(tool) for tool in tools]) #定义系统提示 system_prompt = ( "你是一个有用的助手,可以使用以下工具:\n\n" f"{tools_description}\n" "请根据用户的问题选择合适的工具。如果不需要使用任何工具,请直接回复。\n\n" "重要提示:当你需要使用工具时,必须仅使用以下确切的JSON对象格式回复,不要包含其他任何内容:\n" "{\n" ' "tool": "工具名称",\n' ' "arguments": {\n' ' "参数名称": "参数值"\n' " }\n" "}\n\n" "不允许使用 ```json 标记\n" "在收到工具响应后:\n" "1. 将原始数据转换为自然、对话式的回复\n" "2. 保持回复简洁但信息丰富\n" "3. 专注于最相关的信息\n" "4. 使用用户问题中的适当上下文\n" "5. 避免简单地重复原始数据\n\n" "请仅使用上面明确定义的工具。" ) self.messages.append({"role": "system", "content": system_prompt}) async def disconnect(self): """关闭 Session 和连接。""" async with self._lock: await self._exit_stack.aclose() async def chat(self, prompt, role="user"): """与LLM进行交互""" self.messages.append({"role": role, "content": prompt}) # 初始化 LLM API 调用 response = await self.client.chat.completions.create( model=self.model, messages=self.messages, ) llm_response = response.choices[0].message.content return llm_response #mcp server的工具调用函数(包含工具则调用工具,否则返回原始数据) async def execute_tool(self, llm_response: str): import json try: pattern = r"```json\n(.*?)\n?```" match = re.search(pattern, llm_response, re.DOTALL) if match: llm_response = match.group(1) tool_call = json.loads(llm_response) if "tool" in tool_call and "arguments" in tool_call: # result = await self.session.call_tool(tool_name, tool_args) response = await self.session.list_tools() tools = response.tools if any(tool.name == tool_call["tool"] for tool in tools): try: print(f"[提示]:正在调用工具 {tool_call['tool']}") result = await self.session.call_tool( tool_call["tool"], tool_call["arguments"] ) if isinstance(result, dict) and "progress" in result: progress = result["progress"] total = result["total"] percentage = (progress / total) * 100 print(f"Progress: {progress}/{total} ({percentage:.1f}%)") # print(f"[执行结果]: {result}") return f"Tool execution result: {result}" except Exception as e: error_msg = f"Error executing tool: {str(e)}" print(error_msg) return error_msg return f"No server found with tool: {tool_call['tool']}" return llm_response except json.JSONDecodeError: return llm_response async def chat_loop(self): """运行交互式聊天循环""" print("MCP 客户端启动") print("输入 exit 退出") while True: prompt = input(">>> ").strip() if "exit" in prompt.lower(): break response = await self.chat(prompt) self.messages.append({"role": "assistant", "content": response}) result = await self.execute_tool(response) # 如果工具执行结果与初始响应不同,则继续对话直到不再需要调用工具 while result != response: response = await self.chat(result, "system") self.messages.append( {"role": "assistant", "content": response} ) result = await self.execute_tool(response) print(response) #加载servers_config.json配置文件函数 def load_server_config(config_file): with open(config_file) as f: return json.load(f) async def main(): try: server_config = load_server_config("servers_config.json") client = Client() await client.connect_server(server_config) await client.chat_loop() except Exception as e: print(f"主程序发生错误: {type(e).__name__}: {e}") finally: # 无论如何,最后都要尝试断开连接并清理资源 print("\n正在关闭客户端...") await client.disconnect() print("客户端已关闭。") if __name__ == '__main__': # 我要去北京出差,请你查询附近5km的酒店,为我安排行程 asyncio.run(main()) -
servers_config.json
json{ "mcpServers": { "amap-amap-sse": { "url": "https://mcp.amap.com/sse?key=<你的key>" } } } -
然后尝试运行MCP客户端:
arduinouv run client.py
2.2.启动已有项目流程
进入你的 MCP 框架 UV 项目根目录(包含 pyproject.toml/uv.lock 或 requirements.txt 的目录),执行以下步骤:
1. (可选)创建并激活虚拟环境
UV 推荐使用虚拟环境隔离依赖,避免污染全局环境:
bash
# 创建虚拟环境(默认生成 .venv 目录)
uv venv
# 激活虚拟环境
# Linux/macOS
source .venv/bin/activate
# Windows
.venv\Scripts\activate
2. 安装依赖
UV 优先识别 pyproject.toml + uv.lock(锁文件保证依赖版本一致),若项目只有 requirements.txt 也可兼容:
情况 1:项目有 pyproject.toml(推荐)
bash
# 同步 lock 文件,安装所有依赖(最快,推荐)
uv sync
# 若没有 uv.lock,生成 lock 并安装
uv sync --generate-lockfile
情况 2:项目只有 requirements.txt
bash
# 从 requirements.txt 安装依赖
uv pip install -r requirements.txt
# 可选:将 requirements.txt 转换为 pyproject.toml(推荐)
uv convert requirements.txt -o pyproject.toml
3. 运行MCP客户端:
bash
# 进入项目目录
cd src/mcp-amap-client
# 运行MCP客户端
uv run client.py
使用效果

三. 基于SSE传输的MCP服务器创建流程
3.1 项目完善
尽管此前我们已经完成了几个示例项目的核心脚本编写,但其仍然不算是一个结构完整的项目。核心脚本的调用关系并不明确,同时项目说明也不够完善。因此这里我们首先需要先完善项目的主体内容,再考虑进行部署或上线发布。
src layout项目结构
其实在此之前,我们可以将代码都放在src内的某个文件夹里,这种项目结构也被称作src layout项目结构,这是一种非常通用、同时也便于代码维护的项目结构。
接下来我们还需要在src/pro_name中创建两个py脚本,其一是__init__.py,使当前文件夹可以作为Python的一个库进行导入,需要在__init__.py写入如下代码:
python
from .server import main
同时再创建一个__main__.py,用于实际执行主函数调用流程:
python
from pro_name import main
main()
修改pyproject.toml
创建完基本项目结构后,让我们回到当前项目主目录下,删除main.py(如果有的话),然后修改项目配置文件pyproject.toml该配置文件原有内容如下:
注意:mcp-client为项目名称,需要大家自行替换成自己的项目名称!
ini
[project]
name = "mcp-get-weather"
version = "1.1.0"
description = "输入OpenWeather-API-KEY,获取天气信息。"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.28.1",
"mcp>=1.6.0",
"openai>=1.75.0",
"python-dotenv>=1.1.0",
]
该原有配置含义如下:
- 定义项目的元数据,包括:
- 项目名称、版本、描述
- Python 最低版本要求
- 自动安装的依赖项(相当于
requirements.txt)
需要在原有内容头尾各自添加相关内容,完整配置如下:
ini
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mcp-get-weather"
version = "1.1.0"
description = "输入OpenWeather-API-KEY,获取天气信息。"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.28.1",
"mcp>=1.6.0",
"openai>=1.75.0",
"python-dotenv>=1.1.0",
]
[project.scripts]
mcp-get-weather = "mcp_get_weather:main"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
具体配置解释如下:[build-system] 段
ini
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
告诉 Python 构建工具:
- 要使用
setuptools来构建项目 - 同时依赖
wheel,因为你要构建.whl包
具体配置解释如下:[project.scripts]命令行入口
ini
[project.scripts]
mcp-get-weather = "mcp_get_weather:main"
注意:因Python中变量、函数、类、模块的命名不能包含连字符(
-),所以这里要使用下划线。
它会自动调用你包内的pro_name/__main__.py 里的 main() 函数
具体配置解释如下:[tool.setuptools]和[tool.setuptools.packages.find]
ini
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
明确告诉 setuptools:
- 你的项目源代码都在
src/目录下 - 请去
src/中查找 Python 包(即包含__init__.py的目录)
配置总结
| 区块 | 作用 |
|---|---|
[build-system] |
告诉构建工具如何构建项目 |
[project] |
定义项目基本信息和依赖 |
[project.scripts] |
定义命令行工具 |
[tool.setuptools] |
指定源码位置 |
3.2 SSE传输的MCP服务器创建
进行MCP开发过程中,实现stdio和SSE传输方式较为简单,但要实现流式传输的HTTP流程则会非常复杂。这里我们先介绍相对简单的SSE传输方式的实现方法。当我们使用MCP Python SDK开发MCP服务器时,只需要在此处进行设置:
Python
mcp.run(transport='sse')
即可让MCP服务器开启SSE模式,非常简单。这里我们以创建一个查询天气MCP服务器为例进行演示。
- 创建基础项目结构
Bash
uv init mcp-get-weather
cd mcp-get-weather
# 创建虚拟环境
uv venv
# 激活虚拟环境
source .venv/bin/activate #mac linux
.venv\Scripts\activate #windows
uv add mcp httpx
然后删除主目录下的main.py文件,并创建代码文件夹:
Bash
创建src文件夹,在其内部创建mcp_get_weather子文件夹
cmd进入到子文件夹中:cd ./src/mcp_get_weather
-
创建服务器核心代码,其中server.py主要负责进行天气查询
-
创建服务器核心代码:在src/mcp_get_weather中创建三个代码文件:
__init__.py、__main__.py和server.py-
其中server.py主要负责进行天气查询。
pythonimport json import httpx import argparse from typing import Any from mcp.server.fastmcp import FastMCP # 初始化 MCP 服务器 mcp = FastMCP("WeatherServer") # OpenWeather API 配置 OPENWEATHER_API_BASE = "https://api.openweathermap.org/data/2.5/weather" API_KEY = None USER_AGENT = "weather-app/1.0" async def fetch_weather(city: str) -> dict[str, Any] | None: """ 从 OpenWeather API 获取天气信息。 """ print(f"fetch_weather: 获取城市 {city} 的天气信息") if API_KEY is None: return {"error": "API_KEY 未设置,请提供有效的 OpenWeather API Key。"} params = { "q": city, "appid": API_KEY, "units": "metric", "lang": "zh_cn" } headers = {"User-Agent": USER_AGENT} async with httpx.AsyncClient() as client: try: response = await client.get(OPENWEATHER_API_BASE, params=params, headers=headers, timeout=30.0) response.raise_for_status() print(f"fetch_weather: 成功获取城市 {city} 的天气信息, 响应: {response.text}") return response.json() except httpx.HTTPStatusError as e: print(f"fetch_weather: HTTP 错误获取城市 {city} 的天气信息, 状态码: {e.response.status_code}") return {"error": f"HTTP 错误: {e.response.status_code}"} except Exception as e: print(f"fetch_weather: 请求异常获取城市 {city} 的天气信息, 错误: {str(e)}") return {"error": f"请求失败: {str(e)}"} def format_weather(data: dict[str, Any] | str) -> str: """ 将天气数据格式化为易读文本。 """ 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", "未知") country = data.get("sys", {}).get("country", "未知") temp = data.get("main", {}).get("temp", "N/A") humidity = data.get("main", {}).get("humidity", "N/A") wind_speed = data.get("wind", {}).get("speed", "N/A") weather_list = data.get("weather", [{}]) description = weather_list[0].get("description", "未知") print(f"format_weather: 格式化城市 {city} 的天气信息", data) return ( f"🌍 {city}, {country}\n" f"🌡 温度: {temp}°C\n" f"💧 湿度: {humidity}%\n" f"🌬 风速: {wind_speed} m/s\n" f"🌤 天气: {description}\n" ) @mcp.tool() async def query_weather(city: str) -> str: """ 输入指定城市的英文名称,返回今日天气查询结果。 """ print(f"调用query_weather,查询城市: {city}") data = await fetch_weather(city) return format_weather(data) def main(): parser = argparse.ArgumentParser(description="Weather Server") parser.add_argument("--api_key", type=str, required=True, help="你的 OpenWeather API Key") args = parser.parse_args() global API_KEY API_KEY = args.api_key mcp.run(transport='sse') # 如果 mcp.run 返回,阻塞主线程以避免进程退出(便于调试/保持服务长期运行) print("mcp.run 已返回,进程将保持运行(按 Ctrl+C 退出)") try: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: print("收到中断,准备退出") if __name__ == "__main__": main()
-
-
此外需要在
__init__.py中写入Pythonfrom .server import main -
而在
__main__.py中写入:Pythonfrom mcp_get_weather import main main() -
同时回到主目录,修改项目配置文件
pyproject.toml:Plaintext[build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "mcp-get-weather" version = "1.1.0" description = "输入OpenWeather-API-KEY,获取天气信息。" readme = "README.md" requires-python = ">=3.10" dependencies = [ "httpx>=0.28.1", "mcp>=1.6.0", "openai>=1.75.0", "python-dotenv>=1.1.0", ] [project.scripts] mcp-get-weather = "mcp_get_weather:main" [tool.setuptools] package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"]
至此即完成了整个项目的代码编写工作!
四. 基于SSE的MCP服务器发布流程
-
先下载安装一个CherryStudio
- 这个是零基础入门大模型首选的客户端,相比OpenWebUI、AnythingLLM等CherryStudio安装部署简单、页面简洁美观、各种功能齐全,并且还是最先支持MCP的客户端,可以说是零基础搭建专属智能体的不二之选了。
- CherryStudio官网链接:docs.cherry-ai.com/
- CherryStudio适用于Windows、macOS以及Linux三种操作环境,你可以根据自己的环境选择合适的安装包。
-
运行server端程序
Bash
uv run server.py --api_key openweather的API KEY
-
配置MCP服务:使用Cherry studio进行连接:即可使用Cherry studio连接SSE模式下的MCP服务器,这里只需要输入服务器地址即可:

-
在对话界面选择MCP工具

-
开始对话,查看MCP调用情况

具体效果会受"大模型"功能和 "openweathermap"服务影响,验证时可能会出现问题。可以尝试更换大模型或者稍后重试。