欢迎光临
我们一直在努力

MCP 工具权限设计

为 MCP 服务器加上认证与权限控制:一套完整的架构实践

前言

随着大语言模型和 AI Agent 的快速发展,MCP(Model Context Protocol)正在成为 AI 应用连接外部工具的标准协议。然而,当 MCP 服务器暴露出越来越多的工具能力时,一个现实问题浮出水面:如何控制谁能访问哪些工具?

本文以一个实际的 MCP 认证授权系统为例,分享如何为 MCP 工具服务器添加基于 JWT 的用户认证和细粒度的工具级权限控制。项目代码量不大(核心约 400 行),但架构完整、职责清晰,适合作为理解 MCP 服务端开发的入门参考。

项目要解决什么问题

设想这样一个场景:你搭建了一个 MCP 服务器,上面有 20 个工具——文件读写、数据库查询、邮件发送、应用部署等等。你希望:

  • 管理员可以创建用户,并为每个用户分配不同的工具访问权限
  • AI Agent 连接 MCP 服务器时,需要携带身份凭证(JWT Token)
  • 服务器根据身份自动过滤工具列表——用户只能看到和调用被授权的工具

这就是本项目实现的核心能力。

技术栈选型

层面技术选型理由
后端 API FastAPI 异步高性能,原生支持依赖注入和 OpenAPI 文档
MCP 服务器 FastMCP MCP 协议的 Python 服务端实现,支持中间件机制
管理后台 Streamlit Python 原生 Web UI,快速搭建管理界面
数据库 MySQL 成熟的关系型数据库,适合存储用户和权限关系
ORM SQLModel 同时承担 ORM 模型和 Pydantic Schema,减少重复代码
认证 PyJWT + bcrypt JWT 无状态认证 + 密码安全哈希

整体架构

项目采用三服务分离架构,三个进程通过 HTTP 松耦合连接,各自独立部署和启动:

+———————–+
| Streamlit 管理后台 | 端口 8501
+———–+———–+
|
HTTP (httpx)
|
v
+——————-+ +———————–+ +——————-+
| AI Agent / MCP |–>| FastAPI 后端 API |–>| MySQL 数据库 |
| 客户端 | | 端口 8000 | | (mcp_auth) |
+———+———+ +———–+———–+ +——————-+
| ^
MCP 协议 | HTTP 内部调用
| | (验证权限)
v |
+———+————————-+—+
| FastMCP 服务器 |
| 端口 8001 |
| +————————————+ |
| | PermissionMiddleware | |
| | – on_list_tools: 过滤工具列表 | |
| | – on_call_tool: 鉴权工具调用 | |
| +————————————+ |
+—————————————–+

三个服务各自的职责:

  • API 服务(FastAPI):业务逻辑中心,负责用户管理、工具注册、JWT 签发与验证、权限分配
  • MCP 服务(FastMCP):协议层,面向 AI Agent 暴露 MCP 协议接口,通过中间件实现权限过滤
  • 管理后台(Streamlit):展示层,提供可视化的用户和工具管理界面

这种分离设计的好处是,每个服务都可以独立扩展和部署。比如 MCP 服务器可以部署在网关侧,API 服务可以独立扩容,管理后台只在内部网络使用。

数据模型设计

数据库设计非常简洁,核心只有三张表:

┌──────────┐ ┌──────────────┐ ┌──────────┐
│ User │ │ UserTool │ │ Tool │
├──────────┤ ├──────────────┤ ├──────────┤
│ id │──┐ │ user_id (FK)│ ┌──│ id │
│ username │ └────│ tool_id (FK)│────┘ │ name │
│ password │ └──────────────┘ │ desc │
│ is_active│ │ enabled │
└──────────┘ └──────────┘

  • User:用户表,存储用户名、bcrypt 哈希密码、启用状态
  • Tool:工具注册表,记录 MCP 工具的名称、描述、是否启用
  • UserTool:用户-工具多对多关联表,通过 user_id + tool_id 联合主键实现

用 SQLModel 定义这些模型非常简洁,同一个类既可以作为数据库表模型(table=True),也可以派生出 API 请求/响应的 Schema(table=False):

class Tool(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(max_length=100)
description: str = Field(default="", max_length=500)
enabled: bool = Field(default=True)

class ToolCreate(SQLModel):
"""工具创建请求 Schema"""
name: str
description: str
enabled: bool = True

这种双重角色设计减少了模型定义的重复代码,是 SQLModel 相比单独使用 SQLAlchemy + Pydantic 的一大优势。

JWT 认证:无状态但有实时性

认证流程采用经典的 JWT 方案:

  • 用户通过 /api/login 登录,服务端验证密码后签发 JWT
  • JWT 的 payload 中嵌入 tool_ids 字段,记录用户被授权的工具 ID 列表
  • 后续请求通过 Authorization: Bearer <token> 携带身份凭证
  • 一个有趣的设计权衡是:JWT 本身是无状态的(Token 中已经包含了权限信息),但 MCP 中间件在每次工具调用时仍然会通过 HTTP 请求 API 服务来验证权限。这看起来似乎矛盾,但实际上兼顾了两个需求:

    • JWT 中的 tool_ids:减少了基础验证时对数据库的查询
    • 实时调用 API 验证:确保管理员修改权限后立即生效,不需要等 Token 过期

    # MCP 中间件中的权限验证流程
    async def on_list_tools(ctx, call_next):
    # 1. 从 HTTP 请求头提取 JWT
    token = ctx.request.headers.get("authorization", "")
    # 2. 调用 API 服务获取授权工具列表
    async with httpx.AsyncClient() as client:
    resp = await client.get(
    "http://localhost:8000/api/tools",
    headers={"Authorization": token}
    )
    # 3. 过滤工具列表,只返回授权工具
    authorized = {t["name"] for t in resp.json()}
    tools = [t for t in tools if t.name in authorized]
    return await call_next(ctx, tools)

    权限中间件:MCP 服务器的核心创新

    整个项目最有意思的部分是 MCP 服务器的权限中间件设计。FastMCP 提供了中间件机制,可以在工具列表和工具调用两个环节插入自定义逻辑。

    项目实现了 PermissionMiddleware,拦截两个关键事件:

    • on_list_tools:当 AI Agent 请求工具列表时,先通过 JWT 获取授权工具集合,过滤后只返回用户有权访问的工具。Agent 甚至不知道其他工具的存在。
    • on_call_tool:当 Agent 调用某个工具时,再次验证该工具是否在授权列表中。这是一道安全兜底——即使 Agent 通过某种方式知道了工具名称,没有权限也无法调用。

    这种设计遵循了关注点分离原则:权限逻辑以中间件的形式横切关注,不侵入任何工具的实现代码。每个工具的函数只需要专注于自己的业务逻辑,完全不需要关心"谁有权限调用我"这个问题。

    # 工具定义完全不需要关心权限
    @mcp.tool()
    def read_file(path: str) > str:
    """读取文件内容"""
    return f"Contents of {path}"

    # 权限控制由中间件统一处理

    如果未来需要增加新的权限维度(比如基于时间的访问控制、调用频率限制等),只需要新增中间件,不需要修改任何工具代码。

    管理后台:Streamlit 快速搭建

    管理后台使用 Streamlit 构建,提供两个核心功能页签:

    工具管理:展示所有已注册的工具列表,支持启用/禁用切换,支持添加新工具。

    用户管理:展示所有用户(可展开查看详情),支持以下操作:

    • 为用户生成 MCP Token
    • 通过复选框分配/取消工具权限
    • 修改密码、启用/禁用用户
    • 创建新用户

    Streamlit 的优势在于,纯 Python 就能写出交互式的 Web 界面,不需要写任何前端代码。对于内部管理系统来说,开发效率非常高。

    种子数据:模拟真实场景

    项目预置了 20 个模拟工具和 3 个角色分明的用户,方便快速体验和测试:

    用户角色定位授权工具
    alice 数据分析师(只读为主) 文件读写、数据库查询、邮件查看、日志查看、数据分析
    bob 运维工程师(管理为主) 应用部署、回滚、日志管理、用户管理、系统配置
    charlie 全栈开发(全面操作) 文件全套操作、数据库全套操作、邮件收发、日历管理

    这三个角色的工具权限互有重叠又各有侧重,很好地演示了多对多权限模型的灵活性。

    项目目录结构

    ai-mcp-auth/
    ├── admin/
    │ └── app.py # Streamlit 管理后台
    ├── api/
    │ └── main.py # FastAPI 后端 API(核心业务逻辑)
    ├── database.py # 数据库连接与会话管理
    ├── models.py # SQLModel 数据模型 + API Schema
    ├── init_db.py # 数据库初始化脚本(建库建表 + 种子数据)
    ├── mcp_server.py # MCP 工具服务器(带权限中间件)
    ├── requirements.txt # Python 依赖清单
    ├── start.bat # 一键启动脚本
    └── .env.example # 环境变量模板

    核心文件只有 6 个,各司其职,没有过度分层。

    数据流:一次完整的工具调用

    让我们跟踪一次完整的 MCP 工具调用流程,看看各个组件是如何协作的:

    1. AI Agent 携带 JWT Token 连接 MCP 服务器


    2. PermissionMiddleware.on_list_tools 拦截

    ├── 提取 JWT Token
    ├── 调用 API 服务: GET /api/tools (带 Token)
    ├── API 验证 JWT,返回授权工具列表


    3. 过滤后只返回授权工具给 Agent


    4. Agent 调用某个工具 (如 read_file)


    5. PermissionMiddleware.on_call_tool 拦截

    ├── 验证 JWT 有效性
    ├── 检查 read_file 是否在授权列表中
    ├── 通过 → 执行工具逻辑
    └── 未通过 → 抛出 PermissionError

    总结与思考

    这个项目虽然规模不大,但完整地覆盖了 MCP 服务器认证授权的几个关键问题:

  • 身份认证:基于 JWT 的无状态认证,Token 中嵌入权限信息
  • 权限管理:用户-工具多对多模型,支持灵活的工具分配
  • 权限执行:MCP 中间件模式,在协议层统一拦截和过滤
  • 管理界面:Streamlit 快速搭建可视化管理后台
  • 几个值得分享的设计决策:

    • 中间件模式做权限:权限逻辑与工具实现完全解耦,新增工具零成本
    • 三服务分离:API、MCP、Admin 各自独立,通过 HTTP 松耦合
    • SQLModel 双重角色:一套模型同时服务 ORM 和 API Schema,减少样板代码
    • 无状态 + 实时验证:JWT 减少基础查询,实时 API 调用保证权限时效性

    如果你正在开发 MCP 服务器并需要认证授权能力,希望这个项目的架构思路能给你一些参考。

    赞(0)
    未经允许不得转载:171主机测评 » MCP 工具权限设计
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址