day 313233 MCP快速入门实战

MCP快速入门实战

一、MCP技术体系介绍

MCP,全称是Model Context Protocol,模型上下文协议,由Claude母公司Anthropic于2024年11月正式提出。

image-20250606095015790

MCP刚发布的时候不温不火,直到今年Agent大爆发才被广泛关注。而在今年2月,Cursor正式宣布加入MCP功能支持,一举将MCP推到了全体开发人员面前。从本质上来说,MCP是一种技术协议,一种智能体Agent开发过程中共同约定的一种规范。

总的来说MCP解决的最大痛点,就是Agent开发中调用外部工具的技术门槛过高的问题。

image-20250606095103347

MCP把大模型运行环境称作 MCP Client,也就是MCP客户端,同时,把外部函数运行环境称作MCP Server,也就是MCP服务器。

image-20250606101351140

然后,统一MCP客户端和服务器的运行规范,并且要求MCP客户端和服务器之间,也统一按照某个既定的提示词模板进行通信。

2025年注定是智能体爆发的一年啊而MCP技术则必然会大幅加快这个进程!

CherryStudio+MCP综合实践

第一步:先下载安装一个CherryStudio

这个是我个人最推荐的零基础入门大模型首选的客户端,相比OpenWebUI、AnythingLLM等CherryStudio安装部署简单、页面简洁美观、各种功能齐全,并且还是最先支持MCP的客户端,可以说是零基础搭建专属智能体的不二之选了。

CherryStudio官网链接:https://docs.cherry-ai.com/

CherryStudio适用于Windows、macOS以及Linux三种操作环境,你可以根据自己的环境选择合适的安装包。

注意:如果第一次在使用Cherry Studui的时候出现uv 和 bun未安装的话,可以手动下载安装:

image-20250723101543308

下载路径:

下载成功后,将下载的文件存放在如下目录中:

  • Windows: C:\Users\用户名\.cherrystudio\bin

  • macOS、Linux: ~/.cherrystudio/bin

第二步:获取大模型API-Key

CherryStudio可以接入各种不同类型的大模型,包括国外的OpenAI、Gemini等公司的大模型,国内的智谱、深度求索等公司的大模型,同时兼容通过 Ollama 部署的本地大模型。功能很强大!

image-20250723083646040

第三步:配置MCP服务器

高德地图 MCP 工具是高德地图基于 MCP 协议构建的服务器,整合了高德开放平台的地图服务与智能算法,为企业及开发者提供全场景的地图服务解决方案。通过高德地图 MCP 工具,AI 智能体可以直接调用高德地图的各项服务,实现如位置查询、路线规划、实时路况查询等功能,提升用户体验和服务效率。

image-20250723084128324
  • step3:设置CherryStudio MCP服务器(stdio类型)

    打开CherryStudio客户端,点击左下角“设置”按钮,点击“MCP服务器”,进入到MCP服务器编辑页面。

image-20250723094033345

​ 参数来源和环境变量来源:https://lbs.amap.com/api/mcp-server/gettingstarted

(高德开放平台:4、Cursor 平台 Node.js I/O 模式接入 MCP 服务)

image-20250723094250195
  • step3:设置CherryStudio MCP服务器(sse类型)
image-20250723094514924

URL来源:https://lbs.amap.com/api/mcp-server/gettingstarted

(高德开放平台:2、Cursor 平台 SSE 方式接入 MCP 服务)

image-20250723094555303
  • step3:设置CherryStudio MCP服务器(添加 ModelScope MCP 服务器)

    在 v1.2.9 版本中,Cherry Studio 与 ModelScope 魔搭 达成官方合作,大幅简化了 MCP 服务器添加的操作步骤,避免配置过程出错,而且可以在 ModelScope 社区发现海量 MCP 服务器。接下来跟随操作步骤,一起看下如何在 Cherry Studio 中同步 ModelScope 的 MCP 服务器。

    操作步骤1:点击设置中的 MCP 服务器设置,选 同步服务器

    image-20250723095239086

​ 操作步骤2:选择 ModelScope,并浏览发现 MCP 服务

image-20250723095339905

​ 操作步骤3:注册登录 ModelScope,并查看 MCP 服务详情 (12306-MCP车票查询工具)

image-20250723095404054

​ 操作步骤4:在 MCP 服务详情中,选择连接服务

image-20250723095423948

​ 操作步骤5:申请并复制粘贴 api 令牌

image-20250723095453427 image-20250723095521976 image-20250723095533609

​ 操作步骤6:成功同步

image-20250723095731104

二、MCP客户端Client开发流程

1. uv工具入门使用指南

1.1 uv入门介绍

MCP开发要求借助uv进行虚拟环境创建和依赖管理。uv 是一个Python 依赖管理工具,类似于 pipconda,但它更快、更高效,并且可以更好地管理 Python 虚拟环境和依赖项。它的核心目标是替代 pip,提供更好的性能和更低的管理开销。

1.2 uv安装流程

使用 pip 安装(适用于已安装 pip 的系统)

pip install uv

1.3 uv的基本用法介绍

安装 uv 后,你可以像 pip 一样使用它,但它的语法更简洁,速度也更快。注意,以下为使用语法示例,不用实际运行。

  • 创建虚拟环境

    uv venv myenv

  • 激活虚拟环境

    source myenv/bin/activate  # Linux/macOS
    myenv\Scripts\activate     # Windows
    
  • 安装 Python 依赖

    uv pip install xxx

  • 运行python项目

    uv run python xxx.py

2.MCP极简客户端搭建流程

接下来我们尝试先构建一个 MCP 客户端,确保基本逻辑可用,然后再逐步搭建 MCP 服务器进行联调,这样可以分阶段排查问题,避免一上来就涉及太多复杂性。

2.1 创建 MCP 客户端项目

# 创建项目目录
uv init mcp-client
cd mcp-client

image-20250625103045752

文件/文件夹 作用
.git/ Git 版本控制目录
.venv/ 虚拟环境
.gitignore Git 忽略规则
.python-version Python版本声明
main.py 主程序入口
pyproject.toml 项目配置文件
README.md 项目说明文档

2.2 创建MCP客户端虚拟环境

# 创建虚拟环境
uv venv

# 激活虚拟环境
source .venv/bin/activate #mac/linux
.venv\Scripts\activate #windows

这里需要注意的是,相比pip,uv会自动识别当前项目主目录并创建虚拟环境。然后即可通过add方法在虚拟环境中安装相关的库。首先安装MCP SDK:

MCP SDK 是一个开发工具包,帮助开发者快速构建符合 MCP 标准的客户端或服务器,实现 AI 模型与外部工具的安全高效连接,支持动态适配工具参数变化并减少重复开发。

# 安装 MCP SDK
uv add mcp

编写项目源码

接下来在主目录下创建/src/pro_name作为代码主目录,在其内部创建server和client脚本文件。

2.3 编写基础 MCP 客户端

然后在当前项目主目录中**创建 client.py **

type nul > client.py #windows
touch client.py #mac/linux

然后写入如下通用client的代码:

#Python 内置的异步编程库,让 MCP 可以非阻塞地执行任务(比如聊天、查询)。
import asyncio
#用于管理 MCP 客户端会话(但目前我们先不连接 MCP 服务器)。
from mcp import ClientSession 
#自动管理资源,确保程序退出时正确关闭 MCP 连接。
from contextlib import AsyncExitStack

#定义 MCPClient 类
class MCPClient:
    def __init__(self):
        """初始化 MCP 客户端"""
        self.session = None #存储与 MCP 服务器的会话对象.暂时不连接 MCP 服务器,后续可以修改来真正连接。
        #创建资源管理器,管理 MCP 客户端的资源,确保程序退出时可以正确释放资源。
        self.exit_stack = AsyncExitStack()
    
    #模拟连接 MCP 服务器,这是一个异步方法,目前只是打印一条消息,表示客户端已初始化但未真正连接到服务器。在实际应用中,这里会调用 ClientSession 来建立与真实 MCP 服务器的连接。
    async def connect_to_mock_server(self):
        """模拟 MCP 服务器的连接(暂不连接真实服务器)"""
        print("✅ MCP 客户端已初始化,但未连接到服务器")

    #多轮对话:这是客户端的核心功能,允许用户通过命令行与 AI 模型交互。
    async def chat_loop(self):
        """运行交互式聊天循环"""
        print("\nMCP 客户端已启动!输入 'quit' 退出")

        while True:
            try:
                query = input("\nQuery: ").strip()
                if query.lower() == 'quit':
                    break
                print(f"\n🤖 [Mock Response] 你说的是:{query}")
            except Exception as e:
                print(f"\n⚠️ 发生错误: {str(e)}")
    #清理资源:关闭所有通过 AsyncExitStack 管理的异步资源(比如会话、文件句柄等),确保程序退出时不会留下未释放的资源和正确关闭 MCP 连接(尽管目前没有真正的连接)。
    async def cleanup(self):
        """清理资源"""
        await self.exit_stack.aclose()

#主函数:创建 MCPClient 实例。
async def main():
    #创建一个 MCP 客户端实例。
    client = MCPClient()
    try:
        #初始化 MCP 客户端(暂不连接服务器)。
        await client.connect_to_mock_server()
        #启动交互式聊天。
        await client.chat_loop()
    finally:
        #确保 不管程序是否异常退出,都会正确释放资源。
        await client.cleanup()

if __name__ == "__main__":
    #运行整体程序
    asyncio.run(main())

MCP中一个基础的客户端代码结构总结如下

代码部分 作用
MCPClient.__init__() 初始化 MCP 客户端
connect_to_mock_server() 模拟 MCP 服务器连接
chat_loop() 提供交互式聊天界面
cleanup() 释放资源
main() 启动客户端
asyncio.run(main()) 运行程序

2.5 运行 MCP 客户端

然后尝试运行这个极简的MCP客户端:

uv run client.py

3. MCP客户端接入DeepSeek在线模型

接下来尝试在客户端中接入OpenAI和DeepSeek等在线模型进行对话。需要注意的是,由于OpenAI和DeepSeek调用方法几乎完全一样,因此这套服务器client代码可以同时适用于GPT模型和DeepSeek模型。

3.1 新增依赖

为了支持调用在线模型,以及在环境变量中读取API-KEY等信息,需要先安装如下依赖:

uv add mcp openai python-dotenv

3.2 创建.env文件

创建.env文件,并写入大模型的API-Key,以及base url地址和模型名称等。

type nul > .env #windows touch .env #mac/linux

BASE_URL=https://api.deepseek.com
MODEL=deepseek-chat      
OPENAI_API_KEY="DeepSeek API-Key"

3.3 修改client.py代码

接下来修改客户端代码:

import asyncio
import os
from openai import OpenAI
from dotenv import load_dotenv
from contextlib import AsyncExitStack

# 自动加载 .env 文件,避免在代码中直接暴露 API Key。
load_dotenv()

class MCPClient:
    def __init__(self):
        
        """初始化 MCP 客户端"""
        self.exit_stack = AsyncExitStack()
        
        # 读取 OpenAI API Key
        self.openai_api_key = os.getenv("OPENAI_API_KEY") 
        # 读取 BASE YRL
        self.base_url = os.getenv("BASE_URL")
        # 读取 model
        self.model = os.getenv("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 process_query(self, query: str) -> str:
        """调用 OpenAI API 处理用户查询"""
        messages = [{"role": "system", "content": "你是一个智能助手,帮助用户回答问题。"},
                    {"role": "user", "content": query}]
        
        try:
            # 调用 OpenAI API
            response = await asyncio.get_event_loop().run_in_executor(
                None,
                lambda: self.client.chat.completions.create(
                    model=self.model,
                    messages=messages
                )
            )
            return response.choices[0].message.content
        except Exception as e:
            return f"⚠️ 调用 OpenAI API 时出错: {str(e)}"

    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():
    client = MCPClient()
    try:
        await client.chat_loop()
    finally:
        await client.cleanup()

if __name__ == "__main__":
    asyncio.run(main())

3.4 运行client.py

然后即可输入如下命令开始运行对话客户端:

uv run client.py

三、MCP天气查询服务器server与使用

1. MCP服务器通讯机制

Model Context Protocol(MCP)是一种 开源的协议,根据 MCP 的规范,当前支持3种传输方式。我们首先学习使用---标准输入输出(stdio)。

在 Model Context Protocol(MCP)中,标准输入输出(stdio)模式是一种用于本地通信的传输方式。在这种模式下,MCP 客户端会将服务器程序作为子进程启动,双方通过标准输入(stdin)和标准输出(stdout)进行数据交换。这种方式适用于客户端和服务器在同一台机器上运行的场景,确保了高效、低延迟的通信。

具体而言,客户端通过标准输入发送请求,服务器通过标准输出返回响应。这种直接的数据传输方式减少了网络延迟和传输开销,适合需要快速响应的本地应用。

相比之下,MCP 还支持基于 HTTP 和服务器推送事件(SSE)的传输方式,适用于客户端和服务器位于不同物理位置的场景。在这种模式下,客户端和服务器通过 HTTP 协议进行通信,利用 SSE 实现服务器向客户端的实时数据推送。

总的来说,stdio 模式提供了一种简单、高效的本地通信方式,适用于客户端和服务器在同一环境下运行的情况。而对于分布式或远程部署的场景,基于 HTTP 和 SSE 的传输方式则更为合适。

接下来我们尝试一个入门级的示例,那就是创建一个天气查询的服务器。通过使用OpenWeather API,创建一个能够实时查询天气的服务器(server),并使用stdio方式进行通信。

2. 天气查询服务器Server创建流程

2.1 服务器依赖安装

由于我们需要使用http请求来查询天气,因此需要在当前虚拟环境中添加如下依赖

uv add mcp httpx

2.2 服务器代码编写

接下来尝试创建服务器代码,此时MCP基本执行流程如下:

image-20250625123603960

对应server服务器代码如下:

import json
import httpx
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 = "YOUR_API_KEY"  # 请替换为你自己的 OpenWeather API Key
#请求载体身份标识,可以自定义
USER_AGENT = "weather-app/1.0"

#异步获取天气数据
async def fetch_weather(city: str) -> dict[str, Any] | None:
    """
    从 OpenWeather API 获取天气信息。
    :param city: 城市名称(需使用英文,如 Beijing)
    :return: 天气数据字典;若出错返回包含 error 信息的字典
    """
    #定义请求参数
    params = {
        "q": city,
        "appid": API_KEY,
        "units": "metric",
        "lang": "zh_cn"
    }
    #定义请求头
    headers = {"User-Agent": USER_AGENT}

    #发送异步 GET 请求到 OpenWeather API。
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(OPENWEATHER_API_BASE, params=params, headers=headers, timeout=30.0)
            #用于自动化处理 HTTP 请求的错误状态码。当服务器返回的HTTP状态码不属于2xx(成功范围)时,该方法会抛出一个HTTPError异常。
            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", "未知")
    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 可能为空列表,因此用 [0] 前先提供默认字典
    weather_list = data.get("weather", [{}])
    description = weather_list[0].get("description", "未知")

    return (
        f"🌍 {city}, {country}\n"
        f"🌡 温度: {temp}°C\n"
        f"💧 湿度: {humidity}%\n"
        f"🌬 风速: {wind_speed} m/s\n"
        f"🌤 天气: {description}\n"
    )

#定义MCP工具函数
@mcp.tool()
async def query_weather(city: str) -> str:
    """
    输入指定城市的英文名称,返回今日天气查询结果。
    :param city: 城市名称(需使用英文)
    :return: 格式化后的天气信息
    """
    data = await fetch_weather(city)
    return format_weather(data)

if __name__ == "__main__":
    # 以标准 I/O 方式运行 MCP 服务器
    mcp.run(transport='stdio')

上述代码有两个注意事项,

  1. query_weather函数的函数说明至关重要,相当于是此后客户端对函数进行识别的基本依据,因此需要谨慎编写;
  2. 当指定 transport='stdio' 运行 MCP 服务器时,客户端必须在启动时同时启动当前这个脚本,否则无法顺利通信。这是因为 stdio 模式是一种本地进程间通信方式,它需要服务器作为子进程运行,并通过标准输入输出(stdin/stdout)进行数据交换。

当我们编写完服务器后,并不能直接调用这个服务器,而是需要创建一个对应的能够进行stdio的客户端,才能顺利进行通信。

3. 天气查询客户端client创建流程

3.1 代码编写

创建.env文件,并写入大模型的API-Key,以及base url地址和模型名称等。

type nul > .env #windows touch .env #mac/linux

BASE_URL=https://api.deepseek.com
MODEL=deepseek-chat      
OPENAI_API_KEY="DeepSeek API-Key"
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.stdio import stdio_client

# 加载 .env 文件,确保 API Key 受到保护
load_dotenv()

class MCPClient:
    def __init__(self):
        """初始化 MCP 客户端"""
        self.exit_stack = AsyncExitStack()
        self.openai_api_key = os.getenv("OPENAI_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
        #用于保存 MCP 的客户端会话,默认是 None,稍后通过 connect_to_server 进行连接。
        self.session: Optional[ClientSession] = None
	
    #连接指定server端,参数为server端文件路径
    async def connect_to_server(self, server_script_path: str):
        #判断服务器脚本是 Python 还是 Node.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"
        
        #告诉 MCP 客户端如何启动服务器。
        server_params = StdioServerParameters(
            command=command,
            args=[server_script_path],
            env=None
        )

        # 启动服务器进程,并建立 标准 I/O 通信管道
        stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
        
        #拿到读写流。
        self.stdio, self.write = stdio_transport
        
        #创建 MCP 客户端会话,与服务器交互。
        self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
		
        #发送初始化消息给服务器,等待服务器就绪。
        await self.session.initialize()

        # 向 MCP 服务器请求所有已注册的工具(用 @mcp.tool() 标记)。
        response = await self.session.list_tools()
        tools = response.tools
        print("\n已连接到服务器,支持以下工具:", [tool.name for tool in tools])     
        
    #使用大模型处理查询并调用可用的 MCP 工具 (Function Calling)
    async def process_query(self, query: str) -> str:
       
        #收到用户输入后,先把它组装进一个 messages 列表,目前只包含用户信息({"role": "user", "content": query})。
        messages = [{"role": "user", "content": query}]
        
        # 向 MCP 服务器请求所有已注册的工具(用 @mcp.tool() 标记)。
        response = await self.session.list_tools()
        
        #获取服务器上的工具,再转换成 available_tools 的格式。方便后面发给模型,告诉它:可以调用这些工具。
        available_tools = [{
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "input_schema": tool.inputSchema
            }
        } for tool in response.tools]
        # print(available_tools)
        
        #模型调用first response
        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,
            })
            
            # 将上面的结果再返回给大模型用于生产最终的结果
            #second response
            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():
    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())

3.2 测试运行

uv run client.py server.py

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

以上MCP服务器都是stdio传输方式,而除此之外,目前MCP服务器还支持SSE传输和基于HTTP的流式传输。这两种传输方式也有非常广泛的实际用途,接下来详细介绍如何构建基于SSE和HTTP流式传输的MCP服务器。

1. 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 流)。当请求数据较多或需要多次交互时,服务器可以通过长连接和分批推送的方式进行数据传输。
  • 应用场景:适用于需要支持高并发、低延迟通信的分布式系统,尤其是跨服务或跨网络的应用。适合高并发的场景,如实时流媒体、在线游戏、金融交易系统等。

2. 基于SSE传输的MCP服务器创建流程

2.1 项目完善

尽管此前我们已经完成了几个示例项目的核心脚本编写,但其仍然不算是一个结构完整的项目。核心脚本的调用关系并不明确,同时项目说明也不够完善。因此这里我们首先需要先完善项目的主体内容,再考虑进行部署或上线发布。

src layout项目结构

其实在此之前,我们可以将代码都放在src内的某个文件夹里,这种项目结构也被称作src layout项目结构,这是一种非常通用、同时也便于代码维护的项目结构。

接下来我们还需要在src/pro_name中创建两个py脚本,其一是__init__.py,使当前文件夹可以作为Python的一个库进行导入,需要在__init__.py写入如下代码:

from .server import main

同时再创建一个__main__.py,用于实际执行主函数调用流程:

from pro_name import main
main()
修改pyproject.toml

创建完基本项目结构后,让我们回到当前项目主目录下,删除main.py(如果有的话),然后修改项目配置文件pyproject.toml该配置文件原有内容如下:

注意:mcp-client为项目名称,需要大家自行替换成自己的项目名称!

[project]
name = "mcp-client"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "httpx>=0.28.1",
    "mcp>=1.9.4",
    "openai>=1.91.0",
    "python-dotenv>=1.1.1",
]

该原有配置含义如下:

  • 定义项目的元数据,包括:

    • 项目名称、版本、描述

    • Python 最低版本要求

    • 自动安装的依赖项(相当于 requirements.txt

需要在原有内容头尾各自添加相关内容,完整配置如下:

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "mcp-client"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "httpx>=0.28.1",
    "mcp>=1.9.4",
    "openai>=1.91.0",
    "python-dotenv>=1.1.1",
]

[project.scripts]
mcp-client = "mcp-client:main"

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools.packages.find]
where = ["src"]

具体配置解释如下:[build-system]

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

告诉 Python 构建工具:

  • 要使用 setuptools 来构建项目
  • 同时依赖 wheel,因为你要构建 .whl

具体配置解释如下:[project.scripts]命令行入口

[project.scripts]
mcp-client = "mcp-client:main"

它会自动调用你包内的pro_name/__main__.py 里的 main() 函数

具体配置解释如下:[tool.setuptools]和[tool.setuptools.packages.find]

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools.packages.find]
where = ["src"]

明确告诉 setuptools:

  • 你的项目源代码都在 src/ 目录下
  • 请去 src/ 中查找 Python 包(即包含 __init__.py 的目录)

配置总结

区块 作用
[build-system] 告诉构建工具如何构建项目
[project] 定义项目基本信息和依赖
[project.scripts] 定义命令行工具
[tool.setuptools] 指定源码位置

2.2 SSE传输的MCP服务器创建

进行MCP开发过程中,实现stdio和SSE传输方式较为简单,但要实现流式传输的HTTP流程则会非常复杂。这里我们先介绍相对简单的SSE传输方式的实现方法。当我们使用MCP Python SDK开发MCP服务器时,只需要在此处进行设置:

mcp.run(transport='sse')

即可让MCP服务器开启SSE模式,非常简单。这里我们以创建一个查询天气MCP服务器为例进行演示。

  • 创建基础项目结构
uv init mcp-get-weather
cd mcp-get-weather

# 创建虚拟环境
uv venv

# 激活虚拟环境
source .venv/bin/activate

uv add mcp httpx

然后删除主目录下的main.py文件,并创建代码文件夹:

mkdir -p ./src/mcp_get_weather
cd ./src/mcp_get_weather
  • 创建服务器核心代码,其中server.py主要负责进行天气查询

  • 创建服务器核心代码:在src/mcp_get_weather中创建三个代码文件:__init__.py、__main__.py和server.py

    • 其中server.py主要负责进行天气查询。

      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 获取天气信息。
          """
          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()
                  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:
          """
          将天气数据格式化为易读文本。
          """
          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", "未知")
      
          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:
          """
          输入指定城市的英文名称,返回今日天气查询结果。
          """
          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')
      
      if __name__ == "__main__":
          main()	
      
  • 此外需要在__init__.py中写入

    from .server import main
    
  • 而在__main__.py中写入:

    from mcp_get_weather import main
    
    main()
    
  • 同时回到主目录,修改项目配置文件pyproject.toml

    [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"]
    

​ 至此即完成了整个项目的代码编写工作!

3. 基于SSE的MCP服务器发布流程

  • 运行server端程序
uv run server.py --api_key 1e6d3a1dc54f267a5a72e5ccab3f615a

五、HTTP流式传输

1.简介

长期以来,MCP工具的异地通信主要是以SSE传输方式为主,这种通信方法既没有多通道并发同时也不够稳定,很难真正的适用于企业级应用场景。因此,在2025年2月初,在MCP官方GitHub项目主页上,就有开发者提出了采用更先进的流式HTTP传输方法代替SSE传输的技术方案,相比SSE传输,HTTP流式传输并发更高、通信更加稳定,同时也更容易集成和部署,这也是当代服务器与客户端异地通信的最佳解决方案。

image-20250729082032856

但流式HTTP MCP服务器功能复杂,相关SDK的研发难度也很高,因此在今年的3月,MCP官方首先发布了MCP的流式HTTP通信协议,其中详细说明了HTTP流式传输的基本协议。

https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http

然后在5月9号的1.8.0版本更新中,正式在SDK中加入了HTTP流式MCP服务器的相关功能支持。自此开发者就可以通过MCP SDK,高效快速开发流式HTTP MCP服务器,并顺利进行多通道并发的企业级MCP工具部署。毫无疑问,这将是MCP技术迈向企业级应用的至关重要的一步。

https://github.com/modelcontextprotocol/python-sdk/releases/tag/v1.8.0

image-20250729082218585

2.基于HTTP流式传输的MCP服务器开发流程

2.1 创建项目文件:

#升级mcp的sdk
uv add mcp==1.8.0

uv init mcp_weather_http
cd mcp_weather_http

# 创建虚拟环境
uv venv

# 激活虚拟环境
source .venv/bin/activate#linux
.venv\Scripts\activate #windows

uv add httpx

src_layer的风格进行项目文件编排:删除main.py,创建src文件夹在其内部创建一个项目同名子文件夹,例如:mcp_weather_http

image-20250729082718240

__init__.py中写入

from .server import main

__main__.py中写入:

from mcp_weather_http import main

main()

2.2 编写server端代码:

weather_mcp_streamable_http_server.py

原始代码:后面还有解释代码

import contextlib
import logging
import os
from collections.abc import AsyncIterator

import anyio
import click
import httpx
import mcp.types as types
from mcp.server.lowlevel import Server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette.types import Receive, Scope, Send

# ---------------------------------------------------------------------------
# Weather helpers
# ---------------------------------------------------------------------------
OPENWEATHER_URL = "https://api.openweathermap.org/data/2.5/weather"
DEFAULT_UNITS = "metric"  # use Celsius by default
DEFAULT_LANG = "zh_cn"  # Chinese descriptions


async def fetch_weather(city: str, api_key: str) -> dict[str, str]:
    """Call OpenWeather API and return a simplified weather dict.

    Raises:
        httpx.HTTPStatusError: if the response has a non-2xx status.
    """
    params = {
        "q": city,
        "appid": api_key,
        "units": DEFAULT_UNITS,
        "lang": DEFAULT_LANG,
    }
    async with httpx.AsyncClient(timeout=10) as client:
        r = await client.get(OPENWEATHER_URL, params=params)
        r.raise_for_status()
        data = r.json()
    # Extract a concise summary
    weather_main = data["weather"][0]["main"]
    description = data["weather"][0]["description"]
    temp = data["main"]["temp"]
    feels_like = data["main"]["feels_like"]
    humidity = data["main"]["humidity"]
    return {
        "city": city,
        "weather": weather_main,
        "description": description,
        "temp": f"{temp}°C",
        "feels_like": f"{feels_like}°C",
        "humidity": f"{humidity}%",
    }


@click.command()
@click.option("--port", default=3000, help="Port to listen on for HTTP")
@click.option(
    "--api-key",
    envvar="OPENWEATHER_API_KEY",
    required=True,
    help="OpenWeather API key (or set OPENWEATHER_API_KEY env var)",
)
@click.option(
    "--log-level",
    default="INFO",
    help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
)
@click.option(
    "--json-response",
    is_flag=True,
    default=False,
    help="Enable JSON responses instead of SSE streams",
)
def main(port: int, api_key: str, log_level: str, json_response: bool) -> int:
    """Run an MCP weather server using Streamable HTTP transport."""

    # ---------------------- Configure logging ----------------------
    logging.basicConfig(
        level=getattr(logging, log_level.upper()),
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )
    logger = logging.getLogger("weather-server")

    # ---------------------- Create MCP Server ----------------------
    app = Server("mcp-streamable-http-weather")

    # ---------------------- Tool implementation -------------------
    @app.call_tool()
    async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
        """Handle the 'get-weather' tool call."""
        ctx = app.request_context
        city = arguments.get("location")
        if not city:
            raise ValueError("'location' is required in arguments")

        # Send an initial log message so the client sees streaming early.
        await ctx.session.send_log_message(
            level="info",
            data=f"Fetching weather for {city}…",
            logger="weather",
            related_request_id=ctx.request_id,
        )

        try:
            weather = await fetch_weather(city, api_key)
        except Exception as err:
            # Stream the error to the client and re-raise so MCP returns error.
            await ctx.session.send_log_message(
                level="error",
                data=str(err),
                logger="weather",
                related_request_id=ctx.request_id,
            )
            raise

        # Stream a success notification (optional)
        await ctx.session.send_log_message(
            level="info",
            data="Weather data fetched successfully!",
            logger="weather",
            related_request_id=ctx.request_id,
        )

        # Compose human-readable summary for the final return value.
        summary = (
            f"{weather['city']}:{weather['description']},温度 {weather['temp']},"
            f"体感 {weather['feels_like']},湿度 {weather['humidity']}。"
        )

        return [
            types.TextContent(type="text", text=summary),
        ]

    # ---------------------- Tool registry -------------------------
    @app.list_tools()
    async def list_tools() -> list[types.Tool]:
        """Expose available tools to the LLM."""
        return [
            types.Tool(
                name="get-weather",
                description="查询指定城市的实时天气(OpenWeather 数据)",
                inputSchema={
                    "type": "object",
                    "required": ["location"],
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "城市的英文名称,如 'Beijing'",
                        }
                    },
                },
            )
        ]

    # ---------------------- Session manager -----------------------
    session_manager = StreamableHTTPSessionManager(
        app=app,
        event_store=None,  # 无状态;不保存历史事件
        json_response=json_response,
        stateless=True,
    )

    async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:  # noqa: D401,E501
        await session_manager.handle_request(scope, receive, send)

    # ---------------------- Lifespan Management --------------------
    @contextlib.asynccontextmanager
    async def lifespan(app: Starlette) -> AsyncIterator[None]:
        async with session_manager.run():
            logger.info("Weather MCP server started! 🚀")
            try:
                yield
            finally:
                logger.info("Weather MCP server shutting down…")

    # ---------------------- ASGI app + Uvicorn ---------------------
    starlette_app = Starlette(
        debug=False,
        routes=[Mount("/mcp", app=handle_streamable_http)],
        lifespan=lifespan,
    )

    import uvicorn

    uvicorn.run(starlette_app, host="0.0.0.0", port=port)

    return 0


if __name__ == "__main__":
    main()

解释代码:

'''
- 这些都是“工具箱”,你可以理解为在写一个天气小程序之前,先导入一些现成的功能组件:
  - httpx: 用来联网查天气。
  - mcp: 提供了让大模型调用工具的能力。
  - starlette: 帮你搭建 HTTP 网络服务。
  - click: 帮你写命令行参数,比如 --api-key=xxx。
  - 其它模块则帮助你处理异步任务、打印日志、读取环境变量等。
'''
import contextlib
import logging
import os
from collections.abc import AsyncIterator

import anyio
import click
import httpx
import mcp.types as types
from mcp.server.lowlevel import Server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette.types import Receive, Scope, Send

'''
查询天气函数定义:它用 httpx.AsyncClient 这个工具联网发请求,返回的结果包括:天气情况、气温、湿度等等。
'''
OPENWEATHER_URL = "https://api.openweathermap.org/data/2.5/weather"
DEFAULT_UNITS = "metric"  # use Celsius by default
DEFAULT_LANG = "zh_cn"  # Chinese descriptions


async def fetch_weather(city: str, api_key: str) -> dict[str, str]:

    params = {
        "q": city,
        "appid": api_key,
        "units": DEFAULT_UNITS,
        "lang": DEFAULT_LANG,
    }
    async with httpx.AsyncClient(timeout=10) as client:
        r = await client.get(OPENWEATHER_URL, params=params)
        r.raise_for_status()
        data = r.json()
    # Extract a concise summary
    weather_main = data["weather"][0]["main"]
    description = data["weather"][0]["description"]
    temp = data["main"]["temp"]
    feels_like = data["main"]["feels_like"]
    humidity = data["main"]["humidity"]
    return {
        "city": city,
        "weather": weather_main,
        "description": description,
        "temp": f"{temp}°C",
        "feels_like": f"{feels_like}°C",
        "humidity": f"{humidity}%",
    }

'''
启动服务器的主函数 main()的实现操作,且main函数使用了 Python 的 click 库()来定义一个命令行接口,用于构建可交互的命令行工具。click 库作用是通过简洁的装饰器和API帮助开发者轻松构建用户友好的命令行应用程序。
- @click.command():表示将被装饰的main函数注册为一个独立的 CLI 命令。当你运行程序时,main函数就是实际执行的主体逻辑入口点。
- @click.option: 用于为命令行工具添加可选参数(选项)
- 以后运行时只需要:python xxx.py --port 3000 --api-key xxxx 带上命令行参数即可
'''
@click.command()
#option参数:参数名称、default默认值、help描述信息,required是否为必要参数默认为False
@click.option("--port", default=3000, help="Port to listen on for HTTP")
@click.option(
    "--api-key",
    envvar="OPENWEATHER_API_KEY",
    required=True,
    help="OpenWeather API key (or set OPENWEATHER_API_KEY env var)",
)
#控制日志级别设置,调试阶段使用INFO,生产环境可使用WARNING或更高日志级别
@click.option(
    "--log-level",
    default="INFO",
    help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
)
#当加上这个选项时 (--json-response),程序将以 JSON 格式返回数据;否则使用服务器发送事件流(SSE)进行实时通信。is_flag=True是一个开关性质的选项,不需要赋值。只要出现就视为 True。False → 默认情况下不启用 JSON 格式响应。
@click.option(
    "--json-response",
    is_flag=True,
    default=False,
    help="Enable JSON responses instead of SSE streams",
)
#被装饰函数
def main(port: int, api_key: str, log_level: str, json_response: bool) -> int:
    """
    这段代码使用了标准库中的 logging 模块来配置日志记录系统。
    - logging.basicConfig(...):这是对整个应用程序的日志系统进行基础配置。
    """
    logging.basicConfig(
        #获取日志等级,log_level 是一个变量来自于命令行参数
        level=getattr(logging, log_level.upper()),
        #日志格式化模板,包含以下占位符:
        # -%(asctime)s:当前时间(ISO格式自动生成)
        # -%(name)s:记录器的名字(这里是 "weather-server")
        # -%(levelname)s:日志等级的文字表示
        # -%(message)s	:你实际传入的业务消息文本(help参数的值)	
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )
     #创建一个名为 "weather-server" 的专属日志记录器对象。
    logger = logging.getLogger("weather-server")
	# 初始化 MCP 服务器,服务器名称可以自定义
    app = Server("mcp-streamable-http-weather")

    '''
    MCP工具注册:让大模型能调用 get-weather。该函数的主要职责是:
    	- 接收用户传入的城市名称 (location);
    	- 调用外部 API 获取该城市的天气数据;
    	- 通过日志流实时向客户端反馈进度和结果;
    	- 返回格式化后的文本摘要作为最终响应。
    '''
    @app.call_tool()
    #参数name: 工具的名称(这里是固定值 'get-weather')。
    #参数arguments: 用户传递的参数字典,必须包含键 "location"(即目标城市名)。
    #返回类型: 一个包含 types.TextContent 对象的列表,用于封装最终展示给用户的文字内容。
    async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
        #获取MCP服务器的上下文对象。这个对象是 MCP 服务器处理客户端请求的核心载体。
        ctx = app.request_context
        #从 arguments 中提取 "location" 对应的值作为目标城市。
        city = arguments.get("location")
        if not city:
            raise ValueError("'location' is required in arguments")

        #发送初始日志消息,让用户第一时间看到操作已启动的证据,提升交互体验。
        
        await ctx.session.send_log_message(
            level="info",
            #动态插入城市名的自然语言提示
            data=f"Fetching weather for {city}…",
            #指定记录器模块名为 “weather”
            logger="weather",
            #关联当前请求的唯一标识符
            related_request_id=ctx.request_id,
        )

        try:
            #异步调用外部函数 fetch_weather() 完成气象查询
            weather = await fetch_weather(city, api_key)
        except Exception as err:
            # Stream the error to the client and re-raise so MCP returns error.
            await ctx.session.send_log_message(
                level="error",
                data=str(err),
                logger="weather",
                related_request_id=ctx.request_id,
            )
            raise

        #成功通知(可选但推荐的实践):当一切顺利时,再次发送一条确认消息。这不仅有助于审计,也让用户明确知道任务已完成。
        await ctx.session.send_log_message(
            level="info",
            data="Weather data fetched successfully!",
            logger="weather",
            related_request_id=ctx.request_id,
        )

        #构造人类可读的摘要文本。根据原始 JSON 响应组装本地化的字符串描述。例如:“北京:晴朗,温度 28°C,体感 30°C,湿度 65%。”。使用了中文标点符号和单位习惯,更符合国内用户的阅读预期。
        summary = (
            f"{weather['city']}:{weather['description']},温度 {weather['temp']},"
            f"体感 {weather['feels_like']},湿度 {weather['humidity']}。"
        )
		#将上述摘要包装成 types.TextContent 实例,并放入列表中返回。这种类型通常被前端组件识别为纯文本块,适合直接显示在界面上。
        return [
            types.TextContent(type="text", text=summary),
        ]

    #MCP告诉模型有哪些工具(工具列表)
    @app.list_tools()
    async def list_tools() -> list[types.Tool]:
        """Expose available tools to the LLM."""
        return [
            types.Tool(
                name="get-weather",
                description="查询指定城市的实时天气(OpenWeather 数据)",
                inputSchema={
                    "type": "object",
                    "required": ["location"],
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "城市的英文名称,如 'Beijing'",
                        }
                    },
                },
            )
        ]

    #流式会话管理器配置
    session_manager = StreamableHTTPSessionManager(
        app=app,
        event_store=None,  # 无状态;不保存历史事件,表示不保存历史对话,每次都是新请求;
        json_response=json_response,
        stateless=True,#表示用流式 SSE
    )
	'''
	固定写法:这段代码是一个异步函数 handle_streamable_http,它是 MCP服务器 中用于处理可流式传输的 HTTP 请求的核心入口点。
	'''
    async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:  # noqa: D401,E501
        await session_manager.handle_request(scope, receive, send)

    #固定写法:定义了一个异步上下文管理器(asynccontextmanager),用于管理 MCP服务器的生命周期。
    @contextlib.asynccontextmanager
    async def lifespan(app: Starlette) -> AsyncIterator[None]:
        async with session_manager.run():
            logger.info("Weather MCP server started! 🚀")
            try:
                yield
            finally:
                logger.info("Weather MCP server shutting down…")

    #构建 Starlette Web 应用。这是将 MCP 服务挂载到你的网站 /mcp 路径上,用户访问这个路径时,就会进入刚才的HTTP 请求的核心入口点handle_streamable_http中。
    starlette_app = Starlette(
        debug=False,
        routes=[Mount("/mcp", app=handle_streamable_http)],
        lifespan=lifespan,
    )
	#启动mcp服务器
    import uvicorn
    uvicorn.run(starlette_app, host="0.0.0.0", port=port)

    return 0


if __name__ == "__main__":
    main()

2.3修改项目配置文件

pyproject.toml:

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "mcp-weather-http"
version = "1.1.0"
description = "输入OpenWeather-API-KEY,获取天气信息。"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "httpx>=0.28.1",
    "mcp>=1.8.0",
]

[project.scripts]
mcp-weather-http = "mcp_weather_http:main"

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools.packages.find]
where = ["src"]

3.HTTP流式传输MCP服务器开启与测试

在创建完server.py后,我们可以开启服务并进行测试。需要注意的是,我们需要先开启流式HTTP MCP服务器。

# 回到项目主目录
# cd /xxx/MCP/mcp-weather-http
uv run ./src/mcp_weather_http/server.py --api-key YOUR_KEY

4.流式HTTP MCP服务器异地调用

接下来即可在异地环境(也可以是本地)通过HTTP方式调用MCP服务了。这里以本地安装的Cherry Studio为例,进行调用演示。此处需要保持远程流式HTTP MCP服务器处于开启状态,然后按照如下流程进行调用。

image-20250729110218030

然后输入服务器名称mcp-weather-http,并选择流式传输类型,并选择服务器地址:http://localhost:3000/mcp ,然后点击保存:

image-20250729110236234

若显示服务器更新成功,则表示已经连接上MCP服务器:

image-20250729110251650

Agents SDK+MCP智能体开发

MCP入门介绍与接入Agents SDK基本流程

2025年3月27号,Agents SDK正式官宣支持MCP使用,这也使得Agents SDK的实际应用场景得到拓展:

image-20250807094033921

现在,我们仅需在创建Agent的时候,将MCP服务器视作为一项工具,即可顺利调用MCP服务器进行Agent开发。而实际在借助Agents SDK调用MCP的流程也非常简单,我们只需将MCP视作tools,即可进行调用。换而言之,就是如果使用Agents SDK作为Agent开发框架,则可以零门槛快速接入MCP海量服务器生态。

MCP+Agents SDK基础调用流程

在新版的Agents SDK中,Agents SDK可以将某个对应的Agent封装为client与外部定义好的server进行通信。基本实现流程如下,还是查询天气的server.py,现在将其复制到jupyter运行主目录下,并修改名称为weather_server.py:(mcp的server端脚本程序)

image-20250807095508711

创建一个run_agent.py文件然后导入相关的库:

import asyncio
import os
import shutil
import subprocess
import time
from typing import Any

from agents import Agent, Runner, gen_trace_id, trace
from agents.mcp import MCPServer, MCPServerStdio
from agents.model_settings import ModelSettings

同时定义Agent+MCP运行函数,要求带入MCPServer对象,且带入mcp_servers中,作为类似tools参数带入到当前Agent运行过程中:

async def run(mcp_server: MCPServer):
    
    agent = Agent(
        name="Assistant",
        instructions="你是一名助人为乐的助手",
        mcp_servers=[mcp_server],
        model=deepseek_model
    )

    message = "请帮我查询北京今天天气如何?"
    print(f"Running: {message}")
    result = await Runner.run(starting_agent=agent, input=message)
    print(result.final_output)

然后创建mcp_run函数,负责开启外部server并运行Agent:

async def mcp_run():
    async with MCPServerStdio(
        name="Weather Server",
        cache_tools_list=True,
        params = {"command": "uv","args": ["run", "weather_server.py"]} 
    ) as server:
        await run(server)

关键组件解释:

  • async with MCPServerStdio(...) as server: 启动一个 MCP 工具服务器进程,使用标准输入输出(stdio)作为通信协议,并在上下文中运行(退出时会自动关闭)。
  • name="Weather Server" 给这个 MCP Server 起名为“天气服务器”,这只是用于日志和识别用的标识符。
  • cache_tools_list=True 意思是:首次加载工具时缓存工具列表,后续不需要重新请求工具元数据(提升效率)。
  • params = {"command": "uv", "args": ["run", "weather_server.py"]} 这是启动 MCP 工具服务器的 命令行参数

最后测试运行:

await mcp_run()

Agents SDK接入多个MCP服务器流程

如何将Agents SDK同时接入多个MCP服务器,理论上,MCP一个服务器能同时运行多个外部函数,而一个MCP Client则可以连接多个MCP服务器。

image-20250807100559366

这里我们尝试创建一个“写入本地文档”和“天气查询”的MCP服务器:write_server.py和weather_server.py,并将其放在Jupyter主目录下。代码如下:

#write_server.py
import json
import httpx
from typing import Any
from mcp.server.fastmcp import FastMCP

# 初始化 MCP 服务器
mcp = FastMCP("WriteServer")
USER_AGENT = "write-app/1.0"

@mcp.tool()
async def write_file(content: str) -> str:
    """
    将指定内容写入本地文件。
    :param content: 必要参数,字符串类型,用于表示需要写入文档的具体内容。
    :return:是否成功写入
    """
    return "已成功写入本地文件。"

if __name__ == "__main__":
    # 以标准 I/O 方式运行 MCP 服务器
    mcp.run(transport='stdio')
#weather_server.py
import json
import httpx
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 = "YOUR_API_KEY"  # 请替换为你自己的 OpenWeather API Key
USER_AGENT = "weather-app/1.0"

async def fetch_weather(city: str) -> dict[str, Any] | None:
    """
    从 OpenWeather API 获取天气信息。
    :param city: 城市名称(需使用英文,如 Beijing)
    :return: 天气数据字典;若出错返回包含 error 信息的字典
    """
    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()
            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", "未知")
    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 可能为空列表,因此用 [0] 前先提供默认字典
    weather_list = data.get("weather", [{}])
    description = weather_list[0].get("description", "未知")

    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:
    """
    输入指定城市的英文名称,返回今日天气查询结果。
    :param city: 城市名称(需使用英文)
    :return: 格式化后的天气信息
    """
    data = await fetch_weather(city)
    return format_weather(data)

if __name__ == "__main__":
    # 以标准 I/O 方式运行 MCP 服务器
    mcp.run(transport='stdio')

同时调用多个server:

async def mcp_run_multi(servers_params, message):
    # 使用 AsyncExitStack 自动管理多个上下文退出
    async with AsyncExitStack() as stack:
        servers = []
        # 创建并进入所有 server 上下文
        for p in servers_params:
            server = MCPServerStdio(
                name=p.get("name", "Unnamed Server"),
                cache_tools_list=True,
                params={
                    "command": "uv",
                    "args": ["run", p["script"]],
                },
            )
            entered_server = await stack.enter_async_context(server)
            servers.append(entered_server)
        
        # 构造 agent,传入多个 server
        agent = Agent(
            name="Assistant",
            instructions="你是一名助人为乐的助手",
            mcp_servers=servers,
            model_settings=ModelSettings(tool_choice="required"),
            model=deepseek_model
        )
        
        print(f"Running: {message}")
        result = await Runner.run(starting_agent=agent, input=message)
        print(result.final_output)

        return result
# 示例调用:传入多个 server 的配置
result = await mcp_run_multi(
    servers_params=[
        {"name": "Weather Server", "script": "weather_server.py"},
        {"name": "Writer Server", "script": "write_server.py"}
    ],
    message="请帮我查询北京天气,并写入本地文档。"
)
posted @ 2025-08-31 15:23  凫弥  阅读(142)  评论(0)    收藏  举报