SSE传输方式的MCP服务器创建流程

一. stdio、SSE与基于HTTP的流式传输形式对比

1.1 MCP通信协议介绍

MCP(Model Context Protocol)是一种为了统一大规模模型和工具间通信而设计的协议,它定义了消息格式和通信方式。MCP 协议支持多种传输机制,其中包括 stdioServer-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协议实现。接下来,我们将介绍这个给案例是如何实现的

  1. 注册成为高德开发者。可以直接通过淘宝或支付宝账号进行创建。访问lbs.amap.com/进入"控制台"。
  2. 点击"应用管理"->"我的应用",点击"创建新应用",输入应用名称和应用类型,点击创建即可。
  3. 点击添加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脚本文件。
  • client.py

    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客户端:

    arduino 复制代码
    uv run client.py

2.2.启动已有项目流程

进入你的 MCP 框架 UV 项目根目录(包含 pyproject.toml/uv.lockrequirements.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主要负责进行天气查询。

      python 复制代码
      import 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中写入

    Python 复制代码
    from .server import main
  • 而在__main__.py中写入:

    Python 复制代码
    from 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"服务影响,验证时可能会出现问题。可以尝试更换大模型或者稍后重试。

相关推荐
B站_计算机毕业设计之家5 小时前
python招聘数据 求职就业数据可视化平台 大数据毕业设计 BOSS直聘数据可视化分析系统 Flask框架 Echarts可视化 selenium爬虫技术✅
大数据·python·深度学习·考研·信息可视化·数据分析·flask
子夜江寒5 小时前
Python 学习-Day9-pandas数据导入导出操作
python·学习·pandas
码农很忙5 小时前
让复杂AI应用构建像搭积木:Spring AI Alibaba Graph深度指南与源码拆解
开发语言·人工智能·python
黑客思维者6 小时前
突破 Python 多线程限制:GIL 问题的 4 种实战解法
服务器·数据库·python·gil
FY_20187 小时前
Stable Baselines3中调度函数转换器get_schedule_fn 函数
开发语言·人工智能·python·算法
Coder_Boy_7 小时前
【物联网技术】- 基础理论-0001
java·python·物联网·iot
FY_20187 小时前
SubprocVecEnv 原理、详细使用方法
人工智能·python·机器学习
czliutz7 小时前
使用pdfplumber库处理pdf文件获取文本图片作者等信息
python·pdf
Sunhen_Qiletian7 小时前
《Python开发之语言基础》第七集:库--时间库
前端·数据库·python