使用 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 接收事件
  • 格式 :消息以纯文本发送,包含 event、data、id 等字段,以两个换行结尾

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

在 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 App 的 FastMCP 实例。特别注意 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 设为 SSE ,URL 设为 http://localhost:8000/sse。回想在 STDIO 模式下,我们没有 URL 字段,而是填写如何运行服务器的命令------这正是使用 Inspector 时,STDIO 与 SSE 的差异所在。

Web 客户端 :由于 SSE 服务器运行在 HTTP 上,你可以使用任何 HTTP 客户端进行测试,包括 Postman 、cURL,甚至浏览器。使用 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 App 的 FastMCP 实例。需要特别注意 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
  • URL :http://localhost:3000/sse

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

测试

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

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

Inspector 工具(可视化)

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

bash 复制代码
npx @modelcontextprotocol/inspector

在 UI 中设置:

  • Transport Type:SSE
  • URL :http://localhost:3000/sse

在 Tools 区域选择 add,并输入参数 a 与 b:

复制代码
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_id 与 sessionId 两种写法,实际应保持一致;此外若文件名为 server.py,uvicorn 启动命令应改为 uvicorn server:app。)

总结

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

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

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

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

相关推荐
一 铭7 小时前
Pi实战 05:本地模型 · MCP · 安全沙箱篇
人工智能·ai·agent·harness
码林拾遗WJ8 小时前
你的 Prompt 没有测试覆盖:聊聊 LLM 应用的评估集工程
llm·prompt·评估
墨心@9 小时前
Coding Agent 与通用 Agent
自然语言处理·agent·harness
可乐ea11 小时前
GitHub Security Lab 开源 Fuzzing Taskflow:让 LLM 智能体自己跑完 C/C++ 模糊测试
jvm·c++·github·模糊测试·mcp·代码agent·智能体自动化
网络毒刘12 小时前
Token 账单的「隐形税」:系统提示、工具定义与历史滚动为何比生成贵
agent·token·cursor·成本·mcp
吃饱了得干活13 小时前
Agent 的记忆与工具:从上下文窗口到 MCP
python·agent·mcp
燐妤13 小时前
LangGraph-复习总览
python·ai·面试·agent·学习方法·langgraph
VIP_CQCRE13 小时前
让 Claude 实时联网搜索:Ace Data Cloud Serp MCP 接入指南
ai·claude·搜索·mcp·acedatacloud
与海boy15 小时前
Agent tool
agent
小盆女神节奶粉16 小时前
对于LangGraph的时间旅行底层机制的理解
agent