python开发Streamable HTTP MCP应用

一、概述

使用python开发,最好的框架是fastmcp,github连接:https://github.com/jlowin/fastmcp

2025 年 5 月 9 日,fastmcp发布v2.3.0版本,正式支持Streamable HTTP

终于等到官方支持了!

 

注意:2.3.0版本有bug,目前最新版本已经修复了

升级到最新版本

pip install --upgrade fastmcp

 

关于Streamable HTTP的介绍,请参考链接:https://www.cnblogs.com/xiao987334176/p/18845151

这里就不再重复了

 

二、Streamable HTTP MCP应用

官方demo

server.py

复制代码
from fastmcp import FastMCP

mcp = FastMCP("Demo 🚀")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000, path="/mcp")
复制代码

通过以上8行代码,就简单实现了Streamable HTTP MCP应用。

 

获取服务器公网ip

通过一个在实际生产环境中,使用的功能,来演示一个Streamable HTTP MCP应用

server.py

复制代码
import json
import requests
from fastmcp import FastMCP
mcp = FastMCP("Demo 🚀")


@mcp.tool()
def get_public_ip_address() -> str:
    """
    获取服务器公网 IP 地址
    返回: 
        str: 当前网络的公网 IP 地址
    """
    try:
        response = requests.get("http://ip-api.com/json")
        response.raise_for_status()  # 检查 HTTP 请求是否成功
        content = json.loads(response.text)
        return content.get("query", "Unknown IP")  # 提供默认值以防字段缺失
    except requests.RequestException as e:
        print(f"请求错误: {e}")
        return "Request Failed"
    except json.JSONDecodeError as e:
        print(f"JSON 解码错误: {e}")
        return "Invalid Response"


if __name__ == "__main__":
    # mcp.run()
    mcp.run(transport="streamable-http",
            host="0.0.0.0", port=9000, path="/mcp")
复制代码

运行代码

python server.py

输出:

复制代码
[05/12/25 10:03:55] INFO     Starting server "public_ip_address"...                                                                                                                                                                                                                         server.py:202
INFO:     Started server process [43312]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:9000 (Press CTRL+C to quit)
复制代码

Cherry Studio测试

请确保Cherry Studio版本是最新的,因为新版本,增加了Streamable HTTP支持

添加mcp服务器

 

名称:public_ip_address_mcp

类型:Streamable HTTP

url:http://localhost:9000/mcp

 添加成功后,点击工具,可以看到工具方法

 创建

 

添加

 

选择助手

 

选择

 

提问公网ip,效果如下:

验证一下公网ip是否正确,打开网页

结果是正确的,没问题。

 

三、SSE转换为Streamable HTTP

代码改造

在前面的文章中,写的mysql应用是SSE模式,链接:https://www.cnblogs.com/xiao987334176/p/18827297

把代码拷贝过来,只需要修改2行代码,就可以无缝转换为Streamable HTTP 

 

将以下2行

mcp = FastMCP("operateMysql", host="0.0.0.0", port=9000)
mcp.run(transport="sse")

修改为:

mcp = FastMCP("operateMysql")
mcp.run(transport="streamable-http",
            host="0.0.0.0", port=9000, path="/mcp")

 

mysql_mcp_server_pro应用,完整代码如下:

server.py

复制代码
from fastmcp import FastMCP
from mysql.connector import connect, Error
import os

mcp = FastMCP("operateMysql")


def get_db_config():
    """从环境变量获取数据库配置信息

    返回:
        dict: 包含数据库连接所需的配置信息
        - host: 数据库主机地址
        - port: 数据库端口
        - user: 数据库用户名
        - password: 数据库密码
        - database: 数据库名称

    异常:
        ValueError: 当必需的配置信息缺失时抛出
    """

    config = {
        "host": os.getenv("MYSQL_HOST", "localhost"),
        "port": int(os.getenv("MYSQL_PORT", "3306")),
        "user": os.getenv("MYSQL_USER"),
        "password": os.getenv("MYSQL_PASSWORD"),
        "database": os.getenv("MYSQL_DATABASE"),
    }
    print(config)
    if not all(
        [
            config["host"],
            config["port"],
            config["user"],
            config["password"],
            config["database"],
        ]
    ):
        raise ValueError("缺少必需的数据库配置")

    return config


@mcp.tool()
def execute_sql(query: str) -> list:
    """执行SQL查询语句

    参数:
        query (str): 要执行的SQL语句,支持多条语句以分号分隔

    返回:
        list: 包含查询结果的TextContent列表
        - 对于SELECT查询:返回CSV格式的结果,包含列名和数据
        - 对于SHOW TABLES:返回数据库中的所有表名
        - 对于其他查询:返回执行状态和影响行数
        - 多条语句的结果以"---"分隔

    异常:
        Error: 当数据库连接或查询执行失败时抛出
    """
    config = get_db_config()
    try:
        with connect(**config) as conn:
            with conn.cursor() as cursor:
                statements = [stmt.strip() for stmt in query.split(";") if stmt.strip()]
                results = []

                for statement in statements:
                    try:
                        cursor.execute(statement)

                        # 检查语句是否返回了结果集 (SELECT, SHOW, EXPLAIN, etc.)
                        if cursor.description:
                            columns = [desc[0] for desc in cursor.description]
                            rows = cursor.fetchall()

                            # 将每一行的数据转换为字符串,特殊处理None值
                            formatted_rows = []
                            for row in rows:
                                formatted_row = [
                                    "NULL" if value is None else str(value)
                                    for value in row
                                ]
                                formatted_rows.append(",".join(formatted_row))

                            # 将列名和数据合并为CSV格式
                            results.append(
                                "\n".join([",".join(columns)] + formatted_rows)
                            )

                        # 如果语句没有返回结果集 (INSERT, UPDATE, DELETE, etc.)
                        else:
                            conn.commit()  # 只有在非查询语句时才提交
                            results.append(f"查询执行成功。影响行数: {cursor.rowcount}")

                    except Error as stmt_error:
                        # 单条语句执行出错时,记录错误并继续执行
                        results.append(
                            f"执行语句 '{statement}' 出错: {str(stmt_error)}"
                        )
                        # 可以在这里选择是否继续执行后续语句,目前是继续

                return ["\n---\n".join(results)]

    except Error as e:
        print(f"执行SQL '{query}' 时出错: {e}")
        return [f"执行查询时出错: {str(e)}"]


@mcp.tool()
def get_table_name(text: str) -> list:
    """根据表的中文注释搜索数据库中的表名

    参数:
        text (str): 要搜索的表中文注释关键词

    返回:
        list: 包含查询结果的TextContent列表
        - 返回匹配的表名、数据库名和表注释信息
        - 结果以CSV格式返回,包含列名和数据
    """
    config = get_db_config()
    sql = "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COMMENT "
    sql += f"FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{config['database']}' AND TABLE_COMMENT LIKE '%{text}%';"
    return execute_sql(sql)


@mcp.tool()
def get_table_desc(text: str) -> list:
    """获取指定表的字段结构信息

    参数:
        text (str): 要查询的表名,多个表名以逗号分隔

    返回:
        list: 包含查询结果的列表
        - 返回表的字段名、字段注释等信息
        - 结果按表名和字段顺序排序
        - 结果以CSV格式返回,包含列名和数据
    """
    config = get_db_config()
    # 将输入的表名按逗号分割成列表
    table_names = [name.strip() for name in text.split(",")]
    # 构建IN条件
    table_condition = "','".join(table_names)
    sql = "SELECT TABLE_NAME, COLUMN_NAME, COLUMN_COMMENT "
    sql += (
        f"FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '{config['database']}' "
    )
    sql += f"AND TABLE_NAME IN ('{table_condition}') ORDER BY TABLE_NAME, ORDINAL_POSITION;"
    return execute_sql(sql)


@mcp.tool()
def get_lock_tables() -> list:
    """
    获取当前mysql服务器InnoDB 的行级锁

    返回:
        list: 包含查询结果的TextContent列表
    """
    sql = """SELECT
    p2.`HOST` AS 被阻塞方host,
    p2.`USER` AS 被阻塞方用户,
    r.trx_id AS 被阻塞方事务id,
    r.trx_mysql_thread_id AS 被阻塞方线程号,
    TIMESTAMPDIFF(SECOND, r.trx_wait_started, CURRENT_TIMESTAMP) AS 等待时间,
    r.trx_query AS 被阻塞的查询,
    l.OBJECT_NAME AS 阻塞方锁住的表,
    m.LOCK_MODE AS 被阻塞方的锁模式,
    m.LOCK_TYPE AS '被阻塞方的锁类型(表锁还是行锁)',
    m.INDEX_NAME AS 被阻塞方锁住的索引,
    m.OBJECT_SCHEMA AS 被阻塞方锁对象的数据库名,
    m.OBJECT_NAME AS 被阻塞方锁对象的表名,
    m.LOCK_DATA AS 被阻塞方事务锁定记录的主键值,
    p.`HOST` AS 阻塞方主机,
    p.`USER` AS 阻塞方用户,
    b.trx_id AS 阻塞方事务id,
    b.trx_mysql_thread_id AS 阻塞方线程号,
    b.trx_query AS 阻塞方查询,
    l.LOCK_MODE AS 阻塞方的锁模式,
    l.LOCK_TYPE AS '阻塞方的锁类型(表锁还是行锁)',
    l.INDEX_NAME AS 阻塞方锁住的索引,
    l.OBJECT_SCHEMA AS 阻塞方锁对象的数据库名,
    l.OBJECT_NAME AS 阻塞方锁对象的表名,
    l.LOCK_DATA AS 阻塞方事务锁定记录的主键值,
    IF(p.COMMAND = 'Sleep', CONCAT(p.TIME, ' 秒'), 0) AS 阻塞方事务空闲的时间
    FROM performance_schema.data_lock_waits w
    INNER JOIN performance_schema.data_locks l ON w.BLOCKING_ENGINE_LOCK_ID = l.ENGINE_LOCK_ID
    INNER JOIN performance_schema.data_locks m ON w.REQUESTING_ENGINE_LOCK_ID = m.ENGINE_LOCK_ID
    INNER JOIN information_schema.INNODB_TRX b ON b.trx_id = w.BLOCKING_ENGINE_TRANSACTION_ID
    INNER JOIN information_schema.INNODB_TRX r ON r.trx_id = w.REQUESTING_ENGINE_TRANSACTION_ID
    INNER JOIN information_schema.PROCESSLIST p ON p.ID = b.trx_mysql_thread_id
    INNER JOIN information_schema.PROCESSLIST p2 ON p2.ID = r.trx_mysql_thread_id
    ORDER BY 等待时间 DESC;"""

    return execute_sql(sql)


if __name__ == "__main__":
    mcp.run(transport="streamable-http",
            host="0.0.0.0", port=9000, path="/mcp")
复制代码

编译镜像

修改Dockerfile,升级fastmcp

复制代码
FROM python:3.13.3-alpine3.21
ADD . /app
RUN pip3 install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple && \
    pip3 install mysql-connector-python fastmcp -i https://pypi.tuna.tsinghua.edu.cn/simple && \
    pip3 install --upgrade fastmcp
WORKDIR /app

EXPOSE 9000

ENTRYPOINT ["python3","/app/server.py"]
复制代码

 

编译镜像

docker build -t mysql_mcp_server_pro:v1 .

 

使用docker-compose启动

docker-compose.yaml

复制代码
services:
  mysql_mcp_server_pro:
    image: mysql_mcp_server_pro:v1
    container_name: mysql_mcp_server_pro
    ports:
      - "9090:9000"
    environment:
      MYSQL_HOST: "192.168.20.128"
      MYSQL_PORT: "3306"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: "abcd@1234"
      MYSQL_DATABASE: "test"
      TZ: Asia/Shanghai
    restart: always
复制代码

注意修改mysql相关环境变量

 

运行

docker-compose up -d

 

Cherry Studio测试

添加MCP服务器

添加智能体mysql8

 提示词和以前一样

使用中文回复。

当用户提问中涉及学生、教师、成绩、班级、课程等实体时,需要使用 MySQL MCP 进行数据查询和操作,表结构说明如下:

# 学生管理系统数据库表结构说明

## 1. 教师表 (teachers)

| 字段名 | 类型 | 描述 | 约束 | 示例 |
|--------|------|------|------|------|
| id | varchar | 教师ID | 主键 | "T001" |
| name | varchar | 教师姓名 | 必填 | "张建国" |
| gender | enum | 性别 | """" | "" |
| subject | varchar | 教授科目 | 必填 | "数学" |
| title | varchar | 职称 | 必填 | "教授" |
| phone | varchar | 联系电话 | 必填 | "13812345678" |
| office | varchar | 办公室位置 | 必填 | "博学楼301" |
| wechat | varchar | 微信(可选) | 可选 | "lily_teacher" |
| isHeadTeacher | enum | 是否为班主任,"true""false" | 可选 | true |

## 2. 班级表 (classes)

| 字段名 | 类型 | 描述 | 约束 | 示例 |
|--------|------|------|------|------|
| id | varchar | 班级ID | 主键 | "202301" |
| className | varchar | 班级名称 | 必填 | "2023级计算机1班" |
| grade | int | 年级 | 必填 | 2023 |
| headTeacherId | varchar | 班主任ID | 外键(teachers.id) | "T003" |
| classroom | varchar | 教室位置 | 必填 | "1号楼302" |
| studentCount | int | 学生人数 | 必填 | 35 |
| remark | varchar | 备注信息 | 可选 | "市级优秀班集体" |

## 3. 课程表 (courses)

| 字段名 | 类型 | 描述 | 约束 | 示例 |
|--------|------|------|------|------|
| id | varchar | 课程ID | 主键 | "C001" |
| courseName | varchar | 课程名称 | 必填 | "高等数学" |
| credit | int | 学分 | 必填 | 4 |
| teacherId | varchar | 授课教师ID | 外键(teachers.id) | "T001" |
| semester | varchar | 学期 | 格式"YYYY-N" | "2023-1" |
| type | enum | 课程类型 | "必修""选修" | "必修" |
| prerequisite | varchar | 先修课程ID | 可选,外键(courses.id) | "C003" |

## 4. 学生表 (students)

| 字段名 | 类型 | 描述 | 约束 | 示例 |
|--------|------|------|------|------|
| id | varchar | 学号 | 主键 | "S20230101" |
| name | varchar | 学生姓名 | 必填 | "王强" |
| gender | enum | 性别 | """" | "" |
| birthDate | date | 出生日期 | 必填 | date("2005-01-15") |
| enrollmentDate | date | 入学日期 | 必填 | date("2023-8-1") |
| classId | varchar | 班级ID | 外键(classes.id) | "202301" |
| phone | varchar | 联系电话 | 必填 | "13812345678" |
| email | varchar | 电子邮箱 | 必填 | "20230101@school.edu.cn" |
| emergencyContact | varchar | 紧急联系人电话 | 必填 | "13876543210" |
| address | varchar | 家庭住址 | 必填 | "北京市海淀区中关村大街1栋101室" |
| height | int | 身高(cm) | 必填 | 175 |
| weight | int | 体重(kg) | 必填 | 65 |
| healthStatus | enum | 健康状况 | 必填,"良好""一般‌""较差""良好" |

## 5. 成绩表 (scores)

| 字段名 | 类型 | 描述 | 约束 | 示例 |
|--------|------|------|------|------|
| id | varchar | 成绩记录ID | 主键 | "S20230101C001" |
| studentId | varchar | 学生ID | 外键(students.id) | "S20230101" |
| courseId | varchar | 课程ID | 外键(courses.id) | "C001" |
| score | int | 综合成绩 | 0-100 | 85 |
| examDate | date | 考试日期 | 必填 | date("2024-5-20") |
| usualScore | int | 平时成绩 | 0-100 | 90 |
| finalScore | int | 期末成绩 | 0-100 | 80 |

### 补考成绩记录说明
补考记录在_id后添加"_M"后缀,如"S20230101C001_M"

## 表关系说明

1. **一对多关系**:
   - 一个班级(classes)对应多个学生(students)
   - 一个教师(teachers)可以教授多门课程(courses)
   - 一个学生(students)有多条成绩记录(scores)

2. **外键约束**:
   - students.classId → classes.id
   - courses.teacherId → teachers.id
   - scores.studentId → students.id
   - scores.courseId → courses.id
   - classes.headTeacherId → teachers.id
View Code

mysql表结构,参考文章:https://www.cnblogs.com/xiao987334176/p/18826422

 

将智能体,添加助手

打开助手,选择MCP

 提一个问题,李华的老师是谁

效果如下:

 

原创作者: xiao987334176 转载于: https://www.cnblogs.com/xiao987334176/p/18872195
<think>我们正在讨论MCP服务器(可能是某种自定义服务器)的流式HTTP处理。根据用户提供的引用,我们看到一个HTTPPOST请求示例,该请求发送到`/messages/`端点,并且服务器返回了202Accepted状态码。但是,用户现在关心的是流式处理(streamableHTTP)的配置或实现。在HTTP中,流式处理通常意味着服务器能够逐步发送响应(例如使用分块传输编码)或者客户端能够发送流式请求(例如上传大文件时边读边传)。在MCP服务器的上下文中,用户可能希望实现或配置类似的功能。根据引用中的示例,我们注意到:-请求是一个完整的POST请求,不是流式上传。-响应是202Accepted,并且响应体很小(只有8字节),这显然不是流式响应。因此,我们需要考虑如何将MCP服务器配置为支持流式处理。###可能的流式处理场景1.**流式响应(Server-SentEvents,WebSocket,或分块传输编码)**:服务器可以分块发送数据,例如实时更新。2.**流式请求**:客户端可以分块上传数据,服务器边接收边处理。###配置或实现建议####1.使用分块传输编码(ChunkedTransferEncoding)在HTTP/1.1中,分块传输编码允许服务器将响应分成多个部分(块)发送。这通常用于当响应内容长度未知时,或者需要实时生成内容时。在Python中,如果你使用像uvicorn这样的ASGI服务器(如引用中所示),并且你的应用是异步的(例如基于Starlette或FastAPI),你可以返回一个生成器来实现流式响应。示例(FastAPI):```pythonfromfastapiimportFastAPIimportasyncioapp=FastAPI()@app.post("/stream_response/")asyncdefstream_response():asyncdefgenerate():foriinrange(5):awaitasyncio.sleep(1)yieldf"Chunk{i}\n"returnStreamingResponse(generate(),media_type="text/plain")```####2.流式请求对于流式请求,客户端可以分块发送数据。服务器需要能够逐步读取请求体。在FastAPI中,你可以使用`Request`对象直接读取请求体流:```pythonfromfastapiimportFastAPI,Requestapp=FastAPI()@app.post("/stream_request/")asyncdefstream_request(request:Request):#逐块读取请求体asyncforchunkinrequest.stream():#处理每个块process_chunk(chunk)```####3.使用WebSocket如果需要双向流式通信,WebSocket可能是更好的选择。但WebSocket是完全不同的协议,不是HTTP。####4.服务器配置在引用中,服务器是uvicorn。uvicorn支持流式响应和请求,但需要应用程序支持(如上例所示)。因此,配置主要在应用程序代码中实现,而不是在服务器配置文件中。###针对MCP服务器的建议由于用户提到的MCP服务器(可能是自定义服务器)已经使用了uvicorn,因此可以按照上述方式实现流式处理。但是,具体到MCP服务器的实现,我们需要知道它的内部架构。如果MCP服务器是基于类似FastAPI的框架,那么实现流式处理是直接的。如果MCP服务器是用户自己开发的,那么需要:-对于流式响应:在生成响应时,使用分块传输编码。在ASGI中,可以通过返回一个异步生成器来实现。-对于流式请求:在读取请求体时,不要一次性读取,而是逐块读取(通过`request.stream()`)。###注意事项-在HTTP/1.1中,分块传输编码是标准的一部分,但客户端必须支持。-在HTTP/2中,流式处理更加高效,因为它支持多路复用。###引用说明引用[1]中展示的MCP服务器当前的处理方式是非流式的(一次性请求,一次性响应)。因此,要实现流式处理,需要修改端点处理逻辑。§§相关问题§§1.如何测试MCP服务器的流式HTTP端点?2.在流式处理中,如何处理错误和中断?3.MCP服务器支持HTTP/2吗?如何配置?</think>### MCP服务器流式HTTP处理分析 根据您提供的引用信息和需求,MCP服务器的流式HTTP处理涉及以下关键点: 1. **现有通信模式分析** 引用[1]显示当前MCP SSE服务器使用标准HTTP短连接POST请求: ```http POST /messages/?session_id=97a44bcff590415e99cf803350ffd542 HTTP/1.1 Host: 10.0.105.64:8080 Content-Type: application/json {"method":"tools/call","params":{...}} ``` 服务器返回`202 Accepted`状态码,表明请求已被接受但处理未完成[^1]。这种模式**不满足流式处理需求**,因为: - 使用短连接而非持久连接 - 响应为完整一次性返回 - 缺少流式数据传输机制 2. **流式HTTP实现方案** 要实现流式处理,建议采用以下技术: **a) 分块传输编码(Chunked Transfer Encoding)** 在HTTP响应头中添加: ```http HTTP/1.1 200 OK Transfer-Encoding: chunked Content-Type: application/x-ndjson ``` 服务器分段发送数据: ```http 1A // 十六进制块大小 {"event":"update","data":"..."} 0 // 结束块 ``` **b) Server-Sent Events(SSE)** 专为服务器到客户端的单向流设计: ```python # FastAPI示例 @app.get("/stream") async def message_stream(): async def event_generator(): while True: if new_data := await queue.get(): yield f"data: {json.dumps(new_data)}\n\n" return EventSourceResponse(event_generator()) ``` 客户端通过`EventSource` API接收: ```javascript const source = new EventSource('/stream'); source.onmessage = (event) => { ... }; ``` **c) WebSocket双向流** 适用于双向实时通信: ```python # uvicorn+WebSockets实现 @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() while True: data = await websocket.receive_text() # 处理并流式返回 await websocket.send_text(process(data)) ``` 3. **MCP服务器配置建议** - **协议升级**:在现有POST端点添加`Accept: text/event-stream`支持 - **会话保持**:利用现有`session_id`参数维持流连接 - **超时设置**:配置`keepalive_timeout`保持长连接 - **缓冲区管理**:调整`uvicorn`的`max_queued_bytes`防止背压 4. **性能优化方向** - 使用异步框架(如Starlette)处理高并发流 - 实施背压控制(backpressure control) - 添加`Last-Event-ID`头支持断线重连 - 压缩流数据:`Content-Encoding: gzip` > **关键区别**:传统HTTP请求(如引用所示)是事务型交互,而流式HTTP建立持久通道持续传输数据片段[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值