MCP 实战——高级服务器架构

在上一章中,你已经掌握了将 MCP 服务器推进到生产环境的要点:你学会了用 JWT 进行身份验证来保护工具,并将应用部署到现代的无服务器平台和传统虚拟机上。你已经构建、加固并部署了独立的 MCP 服务器。

但如果你的 MCP 服务器不需要 独立存在呢?如果你想把可由 AI 调用的工具 添加到现有 Web 应用 里呢?如果你想在每一次工具调用 时进行拦截,以添加日志或自定义授权逻辑呢?又或者,你能否在不手写任何工具 的情况下,直接为整套 REST API 生成一个 MCP 接口?

本章将超越独立服务器,深入高级架构 的世界。你将学会如何把 MCP 无缝融入 Python Web 开发的更大生态中。我们不再把 MCP 服务器当作孤立小岛,而是把它作为更大系统中的强力组件

在本章结束时,你将能够:

  • 使用 Starlette 将 MCP 服务器集成到标准 ASGI Web 应用中。
  • 编写强大的中间件(Middleware) ,用来检查与处理 MCP 消息。
  • OpenAPI 规范 自动生成完整的 MCP 工具套件

你已经建好了工坊。现在,让我们把它融入城市

集成 ASGI:让你的 MCP 成为"公民"

到目前为止,当你运行 mcp.run(transport="http") 时,你启用的是一个完整且自包含 的 Web 服务器。但现代 Python Web 应用通常基于 ASGI(Asynchronous Server Gateway Interface) 标准构建。ASGI 是让 Web 服务器(如 Uvicorn)与 Web 框架(如 Starlette、FastAPI、Django)对话的通用语言

如果你想让同一个 Web 应用 既能在 / 提供常规网页,又能在 /api/mcp 提供 MCP 服务,该怎么办?答案是挂载(Mounting) 。挂载允许你将一个完整的 ASGI 应用"插入"到另一应用的某个 URL 路径下。

为演示这一点,我们使用 Starlette ------ 一个轻量且强大的 ASGI 框架,非常适合构建高性能的异步服务。

你的第一个 Starlette 应用

首先,为项目添加 Starlette 与 Uvicorn 服务器。

图 81. 终端

csharp 复制代码
> uv add starlette uvicorn httpx

接着创建一个最小可用的 Starlette 应用。它在 / 暴露一个返回 JSON 的端点。新建 hello_starlette.py

图 82. hello_starlette.py

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),
])

这段代码定义了一个简单的 Web 应用。app 是主要的 ASGI 应用对象。它有一条 / 路由,由 homepage 函数处理。

运行时不直接执行该 Python 文件,而是使用 ASGI 服务器(如 Uvicorn),并告诉它去哪里找到 app 对象。

图 83. 终端

vbnet 复制代码
> uvicorn hello_starlette:app --port 9000
INFO:     Started server process [34804]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:9000 (Press CTRL+C to quit)

hello_starlette:app 的语法告诉 Uvicorn:"在 hello_starlette.py 文件中寻找名为 app 的变量。"

可以用一个简单客户端进行测试。创建 starlette_client.py

图 84. starlette_client.py

vbscript 复制代码
import httpx

response = httpx.get("http://127.0.0.1:9000/")
print("Received response:")
print(response.json())

在 Uvicorn 服务器运行的情况下,打开新的终端执行客户端:

图 85. 终端

css 复制代码
> python .\starlette_client.py
Received response:
{'hello': 'world'}

很好!你已经成功运行了一个标准的 ASGI Web 应用。现在,让我们给它加上 MCP

挂载 MCP 服务器:fastmcp 方式

fastmcp 让你能非常轻松地从 MCP 服务器实例获取一个兼容 ASGI 的应用对象。

创建一个新文件,将 Starlette 应用与 MCP 服务器结合起来。

图 86. mount_asgi_server_mcp_server.py(fastmcp)

python 复制代码
from fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.routing import Mount, Route
from starlette.responses import JSONResponse

# 1. Create your MCP server as usual.
mcp = FastMCP("My App")

@mcp.tool
async def call_api_somewhere() -> str:
    print("Calling API...")
    return "Done!"

# 2. A standard Starlette route handler.
async def homepage(request):
    return JSONResponse({'hello': 'world'})

# 3. Get the ASGI application for your MCP server.
mcp_asgi_app = mcp.http_app()

# 4. Create the main Starlette app.
app = Starlette(
    debug=True,
    # 5. Hook up the MCP server's lifespan to the main app.
    lifespan=mcp_asgi_app.lifespan,
    routes=[
        # 6. Define the routes for the combined application.
        Route('/', homepage),
        Mount('/secret_mcp_server', app=mcp_asgi_app),
    ]
)

要点解析:

  • MCP 服务器 :像往常一样定义 mcp 实例与工具。
  • mcp.http_app() :关键点。该方法返回一个完整、自包含的 ASGI 应用,代表你的 MCP 服务器。
  • lifespan :MCP 服务器需要管理后台任务与会话。lifespan 处理启动与关闭事件。把 MCP 应用的 lifespan 传给主 Starlette 应用,可确保 MCP 的会话管理器正确启动/停止。
  • 挂载(Mount) :Starlette 的 Mount 用于把一个应用插到另一个应用里。我们告诉 Starlette:"凡是以 /secret_mcp_server 开头的请求,都交给 mcp_asgi_app 处理。"

用 Uvicorn 运行这个组合服务器。

图 87. 终端

vbnet 复制代码
> uvicorn mount_asgi_server_mcp_server:app --port 9000
INFO:     Started server process [32980]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:9000 (Press CTRL+C to quit)

现在,单一的服务器进程监听 9000 端口,既能处理常规 HTTP 请求,也能处理 MCP 请求。用一个新的 MCP 客户端来验证:

图 88. mount_asgi_server_mcp_client.py(fastmcp)

python 复制代码
import asyncio
from fastmcp import Client
from fastmcp.client.transports import (
    StreamableHttpTransport,
)

# The URL now points to the mounted path.
client = Client(transport=StreamableHttpTransport("http://localhost:9000/secret_mcp_server/mcp"))

async def main():
    async with client:
        data = await client.call_tool("call_api_somewhere")
        print(data)

asyncio.run(main())

客户端唯一的变化是 URL :它包含了我们挂载 MCP 应用的路径 /secret_mcp_server,后接 fastmcp 使用的标准端点 /mcp

运行客户端:

图 89. 终端

python 复制代码
> python .\mount_asgi_server_mcp_client.py
CallToolResult(content=[TextContent(type='text', text='Done!', annotations=None, meta=None)], structured_content={'result': 'Done!'}, data='Done!', is_error=False)

成功!你已经把 MCP 服务器集成进了更大的 Web 应用。

挂载 MCP 服务器:mcp 库方式

使用 mcp 库的流程非常相似,但一如既往会更显式一些。

图 90. mount_asgi_server_mcp_server.py(mcp)

python 复制代码
from starlette.applications import Starlette
from starlette.routing import Mount, Route
from mcp.server.fastmcp import FastMCP
import contextlib
from collections.abc import AsyncIterator
from starlette.responses import JSONResponse

mcp = FastMCP("My App")

@mcp.tool()
async def call_api_somewhere() -> str:
    print("Calling API...")
    return "Done!"

# This is an alternative, more explicit way to define the lifespan.
@contextlib.asynccontextmanager
async def mcp_lifespan(app: Starlette) -> AsyncIterator[None]:
    async with mcp.session_manager.run():
        print("MCP Session Manager started.")
        yield
    print("MCP Session Manager stopped.")

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

# The method to get the ASGI app is named differently.
mcp_asgi_app = mcp.streamable_http_app()

app = Starlette(
    debug=True,
    # You can pass the lifespan context directly from the MCP app.
    lifespan=mcp_asgi_app.router.lifespan_context,
    routes=[
        Route('/', homepage),
        Mount('/secret_mcp_server', app=mcp_asgi_app),
    ]
)

核心概念相同,但有少许差异:

  • 获取 ASGI 应用的方法名为 mcp.streamable_http_app()
  • lifespan 通过 mcp_asgi_app.router.lifespan_context 获取。示例中也展示了如何用 @contextlib.asynccontextmanager 手动定义,以获得更多控制力。

停止之前的服务器,运行新的:

图 91. 终端

vbnet 复制代码
> uvicorn mount_asgi_server_mcp_server:app --port 9000
INFO:     Started server process [10656]
INFO:     Waiting for application startup.
[07/14/25 22:07:35] INFO     StreamableHTTP session manager started                       streamable_http_manager.py:112
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:9000 (Press CTRL+C to quit)

客户端代码也与独立版本非常相似。

图 92. mount_asgi_server_mcp_client.py(mcp)

python 复制代码
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

# Note the updated URL.
MCP_SERVER_URL = "http://127.0.0.1:9000/secret_mcp_server/mcp"

async def run():
    async with streamablehttp_client(MCP_SERVER_URL) as (read, write, _):
        async with ClientSession(
            read,
            write
        ) as session:
            await session.initialize()

            result = await session.call_tool("call_api_somewhere")
            print(result)


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

运行客户端,你会得到预期结果。

图 93. 终端

python 复制代码
> python .\mount_asgi_server_mcp_client.py
meta=None content=[TextContent(type='text', text='Done!', annotations=None, meta=None)] structuredContent={'result': 'Done!'} isError=False

到这里,你已经成为 ASGI 小能手,能把 MCP 功能编织进任何现代 Python Web 框架。

用中间件为服务器"增压"

**中间件(Middleware)**位于请求/响应链路上,包裹你的主业务逻辑。它是处理"横切关注点"的强大模式,例如:

  • 记录每个入站请求;
  • 校验认证令牌;
  • 给每个响应添加自定义头;
  • 统计请求处理耗时。

fastmcp 为 MCP 服务器提供了一个简洁优雅 的中间件系统。我们来写一个中间件,在工具被调用前记录其细节

注:本节聚焦 fastmcp 的中间件系统。虽然你也可以用标准 ASGI 中间件模式为基础 mcp 库实现中间件,但 fastmcp 为此提供了更高层、更方便的 API。

创建 middleware_mcp_server.py

图 94. middleware_mcp_server.py

python 复制代码
from fastmcp import FastMCP
from fastmcp.server.middleware import Middleware, MiddlewareContext

# 1. Define a custom middleware class.
class ToolMiddleware(Middleware):
    
    # 2. Implement the hook for the 'call_tool' event.
    async def on_call_tool(self, context: MiddlewareContext, call_next):
        if context.fastmcp_context:
            # 3. Access information from the context.
            tool = await context.fastmcp_context.fastmcp.get_tool(context.message.name)
            print(tool)

        # 4. Pass control to the next middleware or the tool itself.
        return await call_next(context)

# 5. Create the server and add the middleware.
mcp = FastMCP("MyServer")
mcp.add_middleware(ToolMiddleware())


@mcp.tool
async def call_api_somewhere() -> str:
    print("Calling API...")
    return "Done!"

if __name__ == "__main__":
    mcp.run(transport="http", port=9000)

逐点剖析:

  • 中间件类 :继承自 fastmcp.server.middleware.Middleware
  • 事件钩子 :该基类提供多个 on_* 方法可重写,如 on_notificationon_call_toolon_read_resource 等。我们要拦截工具调用,因此实现 on_call_tool
  • MiddlewareContext:包含当前请求的大量信息:原始消息、服务器实例等。这里我们用它根据工具名获取工具定义。
  • call_next :至关重要。必须调用它才能将控制权向下传递 。如果不调用,请求处理会被短路,实际的工具函数将不会执行(例如授权失败时可短路)。
  • add_middleware() :将自定义中间件注册到 FastMCP 实例上。

以独立脚本方式启动该服务器:

图 95. 终端

markdown 复制代码
> python .\middleware_mcp_server.py

你会看到熟悉的 fastmcp 启动横幅。接着是客户端。它只是一个标准的 fastmcp 客户端,并不知道服务器上跑着中间件。

图 96. middleware_mcp_client.py

python 复制代码
import asyncio
from fastmcp import Client
from fastmcp.client.transports import (
    StreamableHttpTransport,
)

client = Client(transport=StreamableHttpTransport("http://localhost:9000/mcp"))

async def main():
    async with client:
        data = await client.call_tool("call_api_somewhere")
        print(data)

asyncio.run(main())

运行客户端:

图 97. 终端

python 复制代码
> python .\middleware_mcp_client.py
CallToolResult(content=[TextContent(type='text', text='Done!', annotations=None, meta=None)], structured_content={'result': 'Done!'}, data='Done!', is_error=False)

客户端如期获得响应。现在看看服务器终端窗口的输出:

图 98. 服务器日志

sql 复制代码
...
INFO:     127.0.0.1:57437 - "POST /mcp/ HTTP/1.1" 200 OK
name='call_api_somewhere' title=None description=None tags=set() enabled=True parameters={'properties': {}, 'type': 'object'} output_schema={'properties': {'result': {'title': 'Result', 'type': 'string'}}, 'required': ['result'], 'title': '_WrappedResult', 'type': 'object', 'x-fastmcp-wrap-result': True} annotations=None serializer=None fn=<function call_api_somewhere at 0x0000023A50DAB400>
Calling API...
...

看到了吗!就在工具打印 Calling API... 之前,你看到了工具对象 的详细信息。我们的中间件成功拦截了调用、执行了自定义逻辑,然后把控制权传递给了工具本身。

一步将 OpenAPI 变成 MCP

你已经学会了把 MCP 集成进现有应用,并用中间件扩展它的功能。接下来是一个能为你节省数小时甚至数天 工作的特性:从 OpenAPI 规范自动生成 MCP 工具

OpenAPI 是定义 REST API 的行业标准。许多服务会发布一个 openapi.json 文件,以可编程的方式描述其所有可用的端点、参数与响应。

fastmcp 提供了一个相当惊艳的功能:它可以读取这些文件,并即时 创建一个对应的 MCP 服务器------为每个 API 端点生成一个工具 ,而无需你手写哪怕一个 @mcp.tool 装饰器。

我们来用经典示例 Swagger Petstore API 试试。交互式文档在:petstore3.swagger.io/

该 API 拥有管理宠物、订单与用户的数十个端点。我们只用几行代码就把它们全部变成 MCP 工具。

创建文件 openapi_mcp_server.py

图 99. openapi_mcp_server.py

ini 复制代码
import httpx
from fastmcp import FastMCP

# 1. Create an HTTP client to talk to the real API.
client = httpx.AsyncClient(base_url="https://petstore3.swagger.io/api/v3")

# 2. Fetch the OpenAPI specification file.
openapi_spec = httpx.get("https://petstore3.swagger.io/api/v3/openapi.json").json()

# 3. Create the MCP server from the specification.
mcp = FastMCP.from_openapi(
    openapi_spec=openapi_spec,
    client=client,
    name="My API MCP Server"
)

if __name__ == "__main__":
    mcp.run(transport="http", port=9000)

这段代码很简洁,作用如下:

  • httpx.AsyncClient:创建一个异步 HTTP 客户端;MCP 服务器会用它在底层向 Petstore API 发起真实请求。
  • 获取规范 :用普通的 GET 请求下载 openapi.json
  • FastMCP.from_openapi() :本章"主角"。把 JSON 规范和 httpx 客户端传进去,它会完成解析规范、为每个端点创建工具,并正确连线 HTTP 调用的全部繁琐工作。

运行该服务器。

图 100. 终端

markdown 复制代码
> python .\openapi_mcp_server.py

你的 MCP 服务器现在已经运行,充当 Petstore API 的"智能代理"。我们写个客户端来调用自动生成的工具。查看 OpenAPI 规范,有一个端点叫 findPetsByStatusfastmcp 会把它生成为同名工具

图 101. openapi_mcp_client.py

python 复制代码
import asyncio
from fastmcp import Client
from fastmcp.client.transports import (
    StreamableHttpTransport,
)

client = Client(transport=StreamableHttpTransport("http://localhost:9000/mcp"))

async def main():
    async with client:
        # Call the auto-generated tool.
        pets = await client.call_tool("findPetsByStatus", {"status": "available"})
        print("One of the pets:\n", pets.content[0].text)

asyncio.run(main())

运行客户端。

图 102. 终端

css 复制代码
> python .\openapi_mcp_client.py
One of the pets:
 {
  "result": [
    {
      "id": 4,
      "category": {
        "id": 1,
        "name": "Dogs"
      },
      "name": "Dog 1",
      "photoUrls": [
        "url1",
        "url2"
      ],
...
    {
          "id": 5612,
          "name": "EAMBkLajcZsGvWYDRU"
        }
      ],
      "status": "available"
    }
  ]
}

完美运行 !你的客户端调用了本地 MCP 服务器上的 findPetsByStatus 工具;服务器随后使用其 httpx 客户端向
https://petstore3.swagger.io/api/v3/pet/findByStatus?status=available 发起 GET 请求,收到 JSON 响应后,作为工具结果回传给客户端。

你刚刚让 AI 具备了与一个复杂 REST API 交互的能力,且没有手写任何一个工具 。仅此一项功能,就能成倍加速为既有服务"工具化"的过程。

关键要点(Key Takeaways)

本章把你的 MCP 技能提升到新的复杂度层级,展示了如何把你的工作融入更广阔的 Web 开发世界。

  • MCP 是 ASGI 生态的一等公民 :MCP 服务器不必是独立个体。你可以把它们挂载进任意 ASGI 兼容框架(如 Starlette、FastAPI),构建同时服务传统 Web 流量MCP 工具的统一应用。
  • 中间件带来"超能力" :你可以通过中间件拦截 MCP 事件,为服务器优雅地添加日志、认证、缓存等横切关注点。
  • OpenAPI 是巨大的捷径FastMCP.from_openapi 是真正的游戏规则改变者。对于任何公开 OpenAPI 规范的 REST API,你都能一键生成 MCP 接口,省去海量样板代码。

你不再只是构建 MCP 服务器,而是在设计 MCP 架构

下一章 我们将把这些技能用于实战:走进一系列真实世界、开源的 MCP 服务器 Community Spotlights 。你会看到开发者如何利用这些先进技巧构建与文件系统数据库 交互,甚至从网页抓取内容的强大工具。是时候看看,当社区用这些强力工具开工后,究竟能创造出什么了。

相关推荐
许泽宇的技术分享1 小时前
ReAct Agent:让AI像人类一样思考与行动的革命性框架
人工智能·agent·react
rocksun2 小时前
MCP利用流式HTTP实现实时AI工具交互
人工智能·mcp
老顾聊技术3 小时前
老顾深度解析【字节跳动的AI项目DeerFlow】源码之工程结构(六)
llm·agent
亚马逊云开发者3 小时前
在 Amazon Bedrock 中结合 RAG 与 MCP 高效缓解提示词膨胀问题
llm
数据智能老司机3 小时前
MCP 实战——MCP 服务器的身份验证与部署
llm·agent·mcp
DevYK15 小时前
企业级 Agent 开发实战(一) LangGraph 快速入门
后端·llm·agent
Ethan.Yuan15 小时前
【深度长文】Anthropic发布Prompt Engineering全新指南
大模型·llm·prompt·提示工程
zhayujie18 小时前
RAG优化实战 - LinkAI智能体平台的知识库升级之路
ai·大模型·agent·知识库·rag
AI大模型20 小时前
基于 Docker 的 LLaMA-Factory 全流程部署指南
docker·llm·llama