到目前为止,你已经看到了如何使用 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 服务器。
测试
我们将用三种方式测试服务器:
- Inspector(可视化界面) :直观地测试并观察服务器行为。
 - Inspector(CLI 选项) :在命令行直接获得响应,适合在 CI/CD 流水线中测试。
 - 使用客户端 :这里使用 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 测试,我们需要进行三次调用:
- 调用 
/sse:应返回一个会话 ID: 
            
            
              arduino
              
              
            
          
          curl http://127.0.0.1:3000/sse
        你会看到类似输出:
            
            
              ini
              
              
            
          
          event: endpoint
data: /messages/?session_id=53ddee76d5ec4b4aaa9420f24462210a
        - 调用 
/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"
}'
        这会告诉服务器我们已准备好通信。
- 功能调用(示例:列出工具) :
下面的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 对外暴露服务器时,这是首选的传输方案。