使用 Python 入门 Model Context Protocol(MCP)——构建 SSE 服务器

到目前为止,你已经看到了如何使用 STDIO 作为传输方式来构建 MCP 服务器。对于在本地运行的服务器来说,这是一个很好的选择。然而,如果你希望通过 HTTP 远程连接到服务器,或者希望从 LLM 获得流式 响应,那么还有一种更适合该场景的传输方式------服务器发送事件(Server-Sent Events,SSE)

在本章中,我们将专注于使用 SSE 作为传输方式来构建和测试 MCP 服务器。

本章涵盖以下主题:

  • SSE 概念
  • 将 SSE 服务器构建为 Web 应用
  • 使用 SSE 进行测试
  • 创建一个 SSE 服务器

现在让我们深入了解 SSE 的细节,以及如何使用它来构建服务器。

SSE 概念

在开始构建服务器之前,需要先理解一些概念。首先,使用 SSE 作为传输方式的服务器是可以通过 HTTP 访问的。这意味着即便它可以本地运行,也可以被远程访问。其结果是:我们需要通过 Web 服务器来对外暴露它。

SSE 是一种基于单个、长连接 HTTP 通道的从服务器到客户端的单向通信标准。它允许服务器向客户端推送实时更新,而无需客户端轮询。更多细节如下:

  • 协议 :SSE 使用标准 HTTP,MIME 类型为 text/event-stream
  • 客户端 API :浏览器使用 EventSource API 接收事件
  • 格式 :消息以纯文本发送,包含 eventdataid 等字段,以两个换行结尾

它通常用于对实时更新要求较高的仪表盘类应用。

MCP 中,SSE 被拆分为两部分:一部分用于连接与初始化(例如握手),另一部分用于处理消息(对服务器进行读写)。因此我们需要实现如下端点:

  • SSE 端点:用于在客户端与服务器之间进行握手;客户端向此端点发起请求后,服务器会返回一个保持打开的响应。
  • 消息端点:用于把消息路由到 MCP 服务器及其功能上。

需要说明的是,具体是否要自己实现这些端点取决于所选运行时与 SDK;有些运行时会在底层替你完成。但无论如何,理解这些端点在做什么是有益的。

将 SSE 服务器构建为 Web 应用

与 STDIO 最大的不同在于:我们需要把 SSE 服务器 作为一个 Web 应用 对外提供服务。无论使用 Python 还是 TypeScript ,都需要实现相应的 HTTP 端点。

Python 而言,我们需要借助支持 ASGI(Asynchronous Server Gateway Interface) 的框架。ASGI 允许 Python Web 框架同时处理异步与同步代码,非常适合现代 Web 应用。下面来看看 Starlette

Starlette

Starlette 是一个轻量级的 ASGI 框架,我们将用它来构建 SSE 服务器。前文提到需要实现的端点,Starlette 可以帮助我们完成。它提供了创建 ASGI 应用、处理路由、中间件等功能的简便方式。

典型的 Starlette 应用如下:

python 复制代码
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route

async def homepage(request):
    return JSONResponse({'hello': 'world'})

app = Starlette(debug=True, routes=[
    Route('/', homepage),
])

在上述代码中,我们:

  • 导入了 Starlette 所需模块;
  • 定义了一个返回 JSON 的简单 homepage 函数。

接下来运行它。我们可以使用 uvicorn 来启动 Starlette 应用:

css 复制代码
uvicorn main:app

这里用 uvicorn <文件名>:<应用实例名> 的语法运行应用。本例中文件名为 main.py,应用实例为 app

Starlette 与 MCP

要在 Starlette 中使用 MCP,可以创建如下应用:

ini 复制代码
from starlette.applications import Starlette
from starlette.routing import Mount, Host
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("My App")

# Mount the SSE server to the existing ASGI server
app = Starlette(
    routes=[
        Mount('/', app=mcp.sse_app()),
    ]
)

# or dynamically mount as host
app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app()))

在上述代码中,我们完成了以下工作:

  • 从 Starlette 与 MCP 导入必要模块;
  • 创建名为 My AppFastMCP 实例。特别注意 mcp.sse_app() 方法:它会创建 SSE 服务器 并将其挂载到现有的 ASGI 服务器下。其底层会为你创建 SSE 端点消息端点

如果你想进一步查看 Python SDK 是如何实现的,可以参阅
https://github.com/modelcontextprotocol/python-sdk/blob/e80c0150e1c2e45f66195d3cf7d209be31ce6e5d/src/mcp/server/fastmcp/server.py#L747,你会在 sse_app() 方法中看到如下代码片段的一部分:

ini 复制代码
routes.append(
    Route(
        self.settings.sse_path,
        endpoint=sse_endpoint,
        methods=["GET"],
    )
)
routes.append(
    Mount(
        self.settings.message_path,
        app=sse.handle_post_message,
    )
)

如你所见,只需调用 sse_app() 方法,即可为你创建 SSE 端点消息端点

至于基于 SSE 的功能,是否与 STDIO 的用法一致?是的 。你可以使用相同的装饰器与方法来创建功能。唯一的区别是:需要通过 mcp.sse_app() 来创建 SSE 服务器,并将其挂载到已有的 ASGI 服务器上。

使用 SSE 进行测试

不过,使用 SSE 的测试工具在工作方式上与以往有所不同。下面列出差异点:

Inspector 工具 :Inspector 是一个命令行工具,既支持可视化界面 也支持命令行界面 来测试服务器。与 STDIO 的不同在于,你需要在可视化界面中将 Transport Type 设置为 SSE ,并将 URL 设置为
http://<address>:<port>/sse

对于 CLI 模式 ,你需要指定一个 URL ,而不是"如何启动服务器"。因此,如果你的服务器运行在 localhost:8000,可以使用下面的命令(确保服务器已在该地址端口运行):

bash 复制代码
npx @modelcontextprotocol/inspector --cli http:localhost:8000/sse --method tools/list

来看一下可视化界面的不同之处:

图 4.1------Inspector 工具,可视化模式,SSE

小提示:需要查看这张图片的高清版本?请在下一代 Packt Reader 中打开本书,或查看 PDF/ePub 版本。购买本书即包含下一代 Packt Reader 与免费 PDF/ePub 副本。扫描二维码或访问 packtpub.com/unlock,然后用书名搜索。请核对版本以确保获取正确的版本。

请注意,此处 Transport Type 设为 SSEURL 设为 http://localhost:8000/sse。回想在 STDIO 模式下,我们没有 URL 字段,而是填写如何运行服务器的命令------这正是使用 Inspector 时,STDIO 与 SSE 的差异所在。

Web 客户端 :由于 SSE 服务器运行在 HTTP 上,你可以使用任何 HTTP 客户端进行测试,包括 PostmancURL,甚至浏览器。使用 cURL 的示例如下。

获取会话 ID:

bash 复制代码
export MCP_SERVER="http://0.0.0.0:8000"
curl "${MCP_SERVER}/sse"

这会产生类似如下的响应:

bash 复制代码
event: endpoint
data: http://localhost:5001/messages?session_id=<my session   id>

使用该会话 ID 在 message 端点向服务器发送消息。

请确保在另一个终端 中发送。对此请求的回答会显示在第一个终端中。

ini 复制代码
export MCP_ENDPOINT="http://localhost:8000/messages?session_id=<my session id>"

在与初始化请求不同的终端里,向服务器发送一条消息(例如列出工具):

vbnet 复制代码
curl -X POST "${MCP_ENDPOINT}" -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}'

因此,确实可以用 cURL 完成这一流程,但会稍显繁琐,这也使得 Inspector 成为测试服务器(包括 SSE)的极佳选择。

创建 SSE 服务器

好了,我们已经理解了 SSE 的概念、如何构建服务器,以及测试工具与 STDIO 的差异。现在是动手构建我们自己的 SSE 服务器的时候了。我们将学习如何:

  • 搭建项目
  • 添加服务器代码
  • 测试服务器

创建项目

按如下方式新建项目:

创建虚拟环境:

复制代码
python -m venv venv

激活虚拟环境:

bash 复制代码
source venv/bin/activate

安装依赖:

arduino 复制代码
pip install "mcp[cli]"

这样你就为开始构建 SSE 服务器做好准备了。

添加服务器代码

现在把以下代码加入到你的项目中。

server.py 中添加:

ini 复制代码
from starlette.applications import Starlette
from starlette.routing import Mount, Host
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("My App")

# Mount the SSE server to the existing ASGI server
app = Starlette(
    routes=[
        Mount('/', app=mcp.sse_app()),
    ]
)

上述代码完成了以下工作:

  • 从 Starlette 和 MCP 导入必要的模块。
  • 创建名为 My AppFastMCP 实例。需要特别注意 mcp.sse_app() 方法,它帮助我们挂载用于 SSE 握手消息路由的端点。

添加功能(Features)

下一步是给你的服务器添加功能。正如前面所说,就"功能"而言,STDIO 与 SSE 的用法没有差异。你可以使用同样的装饰器与方法来创建功能。

在同一文件中加入:

python 复制代码
@mcp.tool()
def add(a: int, b: int) -> int:
    """calc"""
    return a + b

稍后你还有机会添加更多功能;现在你已经拥有了一个能把两个数字相加的可用服务器功能。

运行

接下来运行服务器。

使用命令行启动服务器:

css 复制代码
uvicorn main:app --port 3000

语法为 uvicorn <filename>:<app>,其中 <filename> 是你的 Python 文件名,<app> 是 ASGI 应用实例名。

(译注:如果你用的是 server.py,则命令应为 uvicorn server:app --port 3000。)

使用命令行启动 Inspector 工具:

vbscript 复制代码
mcp dev server.py

在 UI 中设置以下字段:

  • Transport Type:SSE
  • URLhttp://localhost:3000/sse

像往常一样尝试你的功能,但这次是通过 SSE 服务器

测试

我们将用三种方式测试服务器:

  1. Inspector(可视化界面) :直观地测试并观察服务器行为。
  2. Inspector(CLI 选项) :在命令行直接获得响应,适合在 CI/CD 流水线中测试。
  3. 使用客户端 :这里使用 cURL 验证服务器响应请求,便于快速测试。

Inspector 工具(可视化)

在服务器运行的前提下,打开一个新的终端并执行:

bash 复制代码
npx @modelcontextprotocol/inspector

在 UI 中设置:

  • Transport Type:SSE
  • URLhttp://localhost:3000/sse

Tools 区域选择 add,并输入参数 ab

复制代码
5
10

Inspector 工具(CLI 选项)

这次像之前一样使用 Inspector,但加上 --cli 选项以 CLI 模式运行。与 UI 不同,响应会直接返回到命令行:

bash 复制代码
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:3000/sse --method tools/call --tool-name add --tool-arg a=5 --tool-arg b=10

你应能看到如下输出:

json 复制代码
{
  "content": [
    {
      "type": "text",
      "text": "15"
    }
  ],
  "structuredContent": {
    "result": 15
  },
  "isError": false
}

使用 curl

要用 cURL 测试,我们需要进行三次调用:

  1. 调用 /sse:应返回一个会话 ID:
arduino 复制代码
curl http://127.0.0.1:3000/sse

你会看到类似输出:

ini 复制代码
event: endpoint
data: /messages/?session_id=53ddee76d5ec4b4aaa9420f24462210a
  1. 调用 /messages(带会话 ID)并发送初始化的 MCP 消息

以下命令应在另一个终端中执行:

vbnet 复制代码
curl -X POST "http://127.0.0.1:3000/messages/?session_id=53ddee76d5ec4b4aaa9420f24462210a" -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}'

这会告诉服务器我们已准备好通信。

  1. 功能调用(示例:列出工具)
    下面的 curl 命令请求列出 MCP 服务器上的工具;应在与上一个命令相同的终端中执行:
vbnet 复制代码
curl -X POST "http://127.0.0.1:3000/messages/?sessionId=53ddee76d5ec4b4aaa9420f24462210a" -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}'

此时你应在最初用于初始化连接的那个终端里看到响应,类似如下:

kotlin 复制代码
event:message
data: {"result":{"tools":[{"name":"products","description":"get products by category","inputSchema":{"type":"object","properties":{"category":{"type":"string"}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"cart-list","description":"get products in cart","inputSchema":{"type":"object","properties":{},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"cart-add","description":"Adding products to cart","inputSchema":{"type":"object","properties":{"title":{"type":"string"}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"add","inputSchema":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}]},"jsonrpc":"2.0","id":1}

建议 :我更倾向使用 Inspector ,因为它体验更好、更易用。curl 更像是底层 方式,适合用来获取初始会话 ID,或在调试时了解底层协议的工作方式,这会很有帮助。

(译注:示例里同时出现了 session_idsessionId 两种写法,实际应保持一致;此外若文件名为 server.pyuvicorn 启动命令应改为 uvicorn server:app。)

总结

本章我们学习了 SSE ,以及如何用它来构建服务器。

我们还了解了在测试工具方面 STDIO 与 SSE 的差异 ,以及如何在 SSE 场景下使用 Inspector 工具。区别在于:STDIO 监听 stdin/stdout ,而 SSE 通过 HTTP 请求 通信;此外,SSE 还可用于从 LLM 流式返回响应。

最后,我们动手构建了自己的 SSE 服务器 ,并使用 InspectorcURL 完成了测试。

在下一章中,我们将介绍另一种传输方式 Streamable HTTP ------当你需要通过 URL 对外暴露服务器时,这是首选的传输方案。

相关推荐
Baihai_IDP7 小时前
探讨超长上下文推理的潜力
人工智能·面试·llm
DO_Community7 小时前
裸金属 vs. 虚拟化 GPU 服务器:AI 训练与推理应该怎么选
运维·服务器·人工智能·llm·大语言模型
少林码僧9 小时前
1.1 大语言模型调用方式与函数调用(Function Calling):从基础到实战
人工智能·ai·语言模型·自然语言处理·llm·1024程序员节
扯蛋43817 小时前
LangChain的学习之路( 一 )
前端·langchain·mcp
oe101919 小时前
好文与笔记分享 A Survey of Context Engineering for Large Language Models(上)
数据库·笔记·语言模型·agent·上下文工程
杨成功20 小时前
大语言模型(LLM)学习笔记
人工智能·llm
亚里随笔1 天前
AsyPPO_ 轻量级mini-critics如何提升大语言模型推理能力
人工智能·语言模型·自然语言处理·llm·agentic
后端小肥肠1 天前
从 Coze 到 n8n:我用 n8n 实现了10w+小林漫画的爆款流水线生产
人工智能·aigc·agent