跳转到主要内容

教程:构建一个待办事项管理器

在本教程中,我们将构建一个带有用户认证 (Authentication) 和授权 (Authorization) 的待办事项管理器 MCP 服务器。

完成本教程后,你将获得:

  • ✅ 对如何在 MCP 服务器中设置基于角色的访问控制 (RBAC) 有基本了解。
  • ✅ 一个可以管理个人待办事项列表的 MCP 服务器。
注释

在开始之前,如果你对 MCP 服务器和 OAuth 2 不熟悉,强烈建议你先阅读 Who am I 教程

概览

本教程将涉及以下组件:

  • MCP 服务器:一个使用 MCP 官方 SDK 处理请求的简单 MCP 服务器,集成了用于管理用户待办事项的 Todo 服务。
  • MCP inspector:一个用于 MCP 服务器的可视化测试工具。它还充当 OAuth / OIDC 客户端,用于发起授权流程并获取访问令牌 (Access token)。
  • 授权 (Authorization) 服务器:一个 OAuth 2.1 或 OpenID Connect 提供商,负责管理用户身份并签发访问令牌 (Access token)。

以下是这些组件之间交互的高级流程图:

了解你的授权 (Authorization) 服务器

带权限 (Scopes) 的访问令牌 (Access tokens)

要在 MCP 服务器中实现基于角色的访问控制 (RBAC),你的授权 (Authorization) 服务器需要支持签发带有权限 (Scopes) 的访问令牌 (Access token)。权限 (Scopes) 代表用户被授予的权限 (Permissions)。

Logto 通过其 API 资源和角色功能(符合 RFC 8707: OAuth 2.0 的资源指示器)提供 RBAC 支持。设置方法如下:

  1. 登录 Logto Console(或你的自托管 Logto Console)

  2. 创建 API 资源和权限 (Scopes):

    • 进入“API 资源”
    • 创建一个名为“Todo Manager”的新 API 资源
    • 添加以下权限 (Scopes):
      • create:todos:“创建新的待办事项”
      • read:todos:“读取所有待办事项”
      • delete:todos:“删除任意待办事项”
  3. 创建角色(推荐,便于管理):

    • 进入“角色”
    • 创建一个“Admin”角色并分配所有权限 (Scopes)(create:todosread:todosdelete:todos
    • 创建一个“User”角色,仅分配 create:todos 权限 (Scope)
  4. 分配权限 (Permissions):

    • 进入“用户”
    • 选择一个用户
    • 你可以:
      • 在“角色”标签页分配角色(推荐)
      • 或在“权限”标签页直接分配权限 (Scopes)

这些权限 (Scopes) 会作为空格分隔的字符串包含在 JWT 访问令牌 (Access token) 的 scope 声明 (Claim) 中。

验证令牌并检查权限 (Permissions)

当你的 MCP 服务器收到请求时,需要:

  1. 验证访问令牌 (Access token) 的签名和过期时间
  2. 从已验证的令牌中提取权限 (Scopes)
  3. 检查令牌是否包含所请求操作所需的权限 (Scopes)

例如,如果用户想创建新的待办事项,他们的访问令牌 (Access token) 必须包含 create:todos 权限 (Scope)。流程如下:

动态客户端注册 (Dynamic Client Registration)

本教程不要求动态客户端注册 (Dynamic Client Registration),但如果你想自动化 MCP 客户端在授权 (Authorization) 服务器的注册流程,可以参考 是否需要动态客户端注册?

了解待办事项管理器中的 RBAC

为了演示,我们将在待办事项管理器 MCP 服务器中实现一个简单的基于角色的访问控制 (RBAC) 系统。这将向你展示 RBAC 的基本原理,同时保持实现简洁。

注释

虽然本教程演示了基于 RBAC 的权限 (Scope) 管理,但需要注意,并非所有认证 (Authentication) 提供商都通过角色实现权限 (Scope) 管理。有些提供商可能有自己独特的访问控制和权限 (Permission) 管理机制。

工具与权限 (Scopes)

我们的待办事项管理器 MCP 服务器提供三个主要工具:

  • create-todo:创建新的待办事项
  • get-todos:列出所有待办事项
  • delete-todo:按 ID 删除待办事项

为了控制对这些工具的访问,我们定义以下权限 (Scopes):

  • create:todos:允许创建新的待办事项
  • delete:todos:允许删除现有待办事项
  • read:todos:允许查询和获取所有待办事项列表

角色与权限 (Roles and permissions)

我们将定义两个具有不同访问级别的角色:

角色 (Role)create:todosread:todosdelete:todos
Admin
User
  • User:普通用户,可以创建待办事项,并且只能查看或删除自己的待办事项
  • Admin:管理员,可以创建、查看和删除所有待办事项,无论归属谁

资源归属 (Resource ownership)

虽然上表显示了每个角色明确分配的权限 (Scopes),但还有一个重要的资源归属原则:

  • User 没有 read:todosdelete:todos 权限 (Scopes),但他们仍然可以:
    • 查看自己的待办事项
    • 删除自己的待办事项
  • Admin 拥有全部权限 (read:todosdelete:todos),可以:
    • 查看系统中所有待办事项
    • 删除任意待办事项,无论归属谁

这展示了 RBAC 系统中的常见模式:资源归属为用户自己的资源隐式授予权限 (Permission),而管理员角色则获得所有资源的显式权限 (Permission)。

了解更多

想深入了解 RBAC 概念和最佳实践,请查阅 精通 RBAC:一个全面的真实案例

在你的提供商中配置授权 (Authorization)

要实现我们前面描述的访问控制系统,你需要在授权 (Authorization) 服务器中配置所需的权限 (Scopes)。不同提供商的配置方法如下:

Logto 通过其 API 资源和角色功能提供 RBAC 支持。设置方法如下:

  1. 登录 Logto Console(或你的自托管 Logto Console)

  2. 创建 API 资源和权限 (Scopes):

    • 进入“API 资源”
    • 创建一个名为“Todo Manager”的新 API 资源,并使用 https://todo.mcp-server.app(仅作演示)作为指示器。
    • 创建以下权限 (Scopes):
      • create:todos:“创建新的待办事项”
      • read:todos:“读取所有待办事项”
      • delete:todos:“删除任意待办事项”
  3. 创建角色(推荐,便于管理):

    • 进入“角色”
    • 创建一个“Admin”角色并分配所有权限 (Scopes)(create:todosread:todosdelete:todos
    • 创建一个“User”角色,仅分配 create:todos 权限 (Scope)
    • 在“User”角色详情页,切换到“常规”标签,并将“User”角色设置为“默认角色”。
  4. 管理用户角色和权限 (Permissions):

    • 新用户:
      • 由于我们设置了默认角色,他们会自动获得“User”角色
    • 已有用户:
      • 进入“用户管理”
      • 选择一个用户
      • 在“角色”标签页为用户分配角色
编程方式管理角色

你也可以使用 Logto 的 Management API 以编程方式管理用户角色。这对于自动化用户管理或构建管理面板特别有用。

请求访问令牌 (Access token) 时,Logto 会根据用户角色权限 (Permissions) 在令牌的 scope 声明 (Claim) 中包含相应权限 (Scopes)。

配置好授权 (Authorization) 服务器后,用户将获得包含其已授予权限 (Scopes) 的访问令牌 (Access token)。MCP 服务器将使用这些权限 (Scopes) 来判断:

  • 用户是否可以创建新的待办事项(create:todos
  • 用户是否可以查看所有待办事项(read:todos)或仅查看自己的
  • 用户是否可以删除任意待办事项(delete:todos)或仅删除自己的

搭建 MCP 服务器

我们将使用 MCP 官方 SDK 创建我们的待办事项管理器 MCP 服务器。

创建新项目

mkdir mcp-server
cd mcp-server
uv init # 或使用 `pipenv` 或 `poetry` 创建新的虚拟环境

安装 MCP SDK 及依赖

pip install "mcp[cli]" starlette uvicorn

或使用你喜欢的包管理器,如 uvpoetry

创建 MCP 服务器

首先,让我们创建一个带有工具定义的基础 MCP 服务器:

创建名为 todo-manager.py 的文件,并添加如下代码:

from typing import Any
from mcp.server.fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.routing import Mount

mcp = FastMCP("Todo Manager")

@mcp.tool()
def create_todo(content: str) -> dict[str, Any]:
    """Create a new todo."""
    return {"error": "Not implemented"}

@mcp.tool()
def get_todos() -> dict[str, Any]:
    """List all todos."""
    return {"error": "Not implemented"}

@mcp.tool()
def delete_todo(id: str) -> dict[str, Any]:
    """Delete a todo by id."""
    return {"error": "Not implemented"}

app = Starlette(
    routes=[Mount('/', app=mcp.sse_app())]
)

运行服务器:

uvicorn todo_manager:app --host 0.0.0.0 --port 3001

检查 MCP 服务器

克隆并运行 MCP inspector

现在 MCP 服务器已运行,我们可以使用 MCP inspector 检查 whoami 工具是否可用。

由于当前实现的限制,我们 fork 了 MCP inspector,使其在认证 (Authentication) 和授权 (Authorization) 方面更灵活、更易扩展。我们也已向原仓库提交了 pull request。

运行 MCP inspector:

git clone https://github.com/mcp-auth/inspector.git
cd inspector
npm install
npm run dev

然后在浏览器中打开 http://localhost:6274/(或终端显示的其他 URL)访问 MCP inspector。

连接 MCP inspector 到 MCP 服务器

在继续之前,请检查 MCP inspector 的以下配置:

  • Transport Type:设置为 SSE
  • URL:设置为你的 MCP 服务器地址,本例为 http://localhost:3001/sse

现在你可以点击“Connect”按钮,查看 MCP inspector 是否能连接 MCP 服务器。如果一切正常,你将在 MCP inspector 中看到“Connected”状态。

检查点:运行待办事项管理工具

  1. 在 MCP inspector 顶部菜单点击“Tools”标签。
  2. 点击“List Tools”按钮。
  3. 你应该能在页面上看到 create-todoget-todosdelete-todo 工具。点击可查看工具详情。
  4. 你将在右侧看到“Run Tool”按钮。点击并输入所需参数运行工具。
  5. 你将看到工具返回的 JSON 响应 {"error": "Not implemented"}

MCP inspector 首次运行

集成你的授权 (Authorization) 服务器

完成本节需要考虑以下事项:

你的授权 (Authorization) 服务器的发行者 (Issuer) URL

通常是你的授权 (Authorization) 服务器的基础 URL,如 https://auth.example.com。有些提供商可能是 https://example.logto.app/oidc,请查阅你的提供商文档。

如何获取授权 (Authorization) 服务器元数据
  • 如果你的授权 (Authorization) 服务器符合 OAuth 2.0 授权服务器元数据OpenID Connect 发现,你可以使用 MCP Auth 内置工具自动获取元数据。
  • 如果不符合这些标准,你需要在 MCP 服务器配置中手动指定元数据 URL 或端点。请查阅你的提供商文档。
如何将 MCP inspector 注册为授权 (Authorization) 服务器的客户端
  • 如果你的授权 (Authorization) 服务器支持 动态客户端注册 (Dynamic Client Registration),可以跳过此步骤,MCP inspector 会自动注册为客户端。
  • 如果不支持动态客户端注册 (Dynamic Client Registration),你需要手动将 MCP inspector 注册为客户端。
了解令牌请求参数

向不同授权 (Authorization) 服务器请求访问令牌 (Access token) 时,指定目标资源和权限 (Permissions) 的方式各异。主要模式如下:

  • 基于资源指示器 (Resource indicator)

    • 使用 resource 参数指定目标 API(见 RFC 8707: OAuth 2.0 的资源指示器
    • 现代 OAuth 2.0 实现常用
    • 示例请求:
      {
        "resource": "https://todo.mcp-server.app",
        "scope": "create:todos read:todos"
      }
    • 服务器签发专门绑定到请求资源的令牌
  • 基于受众 (Audience)

    • 使用 audience 参数指定令牌的目标接收方
    • 与资源指示器类似,但语义不同
    • 示例请求:
      {
        "audience": "todo-api",
        "scope": "create:todos read:todos"
      }
  • 纯权限 (Scope) 模式

    • 仅依赖权限 (Scopes),无资源/受众参数
    • 传统 OAuth 2.0 方式
    • 示例请求:
      {
        "scope": "todo-api:create todo-api:read openid profile"
      }
    • 通常使用前缀权限 (Scopes) 进行命名空间隔离
    • 适用于简单的 OAuth 2.0 实现
最佳实践
  • 查阅你的提供商文档,了解支持哪些参数
  • 有些提供商同时支持多种方式
  • 资源指示器 (Resource indicator) 通过受众限制提升安全性
  • 如有条件,优先使用资源指示器 (Resource indicator) 以获得更好的访问控制

虽然每个提供商可能有自己的具体要求,以下步骤将指导你如何结合 MCP inspector 和 MCP 服务器进行针对不同提供商的配置。

注册 MCP inspector 为客户端

将待办事项管理器集成到 Logto 非常简单,因为它是支持资源指示器 (Resource indicator) 和权限 (Scopes) 的 OpenID Connect 提供商,可以用 https://todo.mcp-server.app 作为资源指示器保护你的 todo API。

由于 Logto 目前不支持动态客户端注册 (Dynamic Client Registration),你需要手动在 Logto 租户中注册 MCP inspector 为客户端:

  1. 打开 MCP inspector,点击 “OAuth Configuration” 按钮。复制 Redirect URL (auto-populated),如 http://localhost:6274/oauth/callback
  2. 登录 Logto Console(或你的自托管 Logto Console)。
  3. 进入“应用程序”标签,点击“创建应用程序”。在页面底部点击“Create app without framework”。
  4. 填写应用详情,然后点击“创建应用程序”:
    • 选择应用类型:选择“单页应用程序”
    • 应用名称:如 “MCP Inspector”
  5. 在“设置 / Redirect URIs”部分,粘贴你从 MCP inspector 复制的 Redirect URL (auto-populated),然后点击底部栏的“保存更改”。
  6. 在顶部卡片中,你会看到 “App ID” 值。复制它。
  7. 回到 MCP inspector,在 “OAuth Configuration” 的 “Client ID” 字段粘贴 “App ID”。
  8. 在 “Auth Params” 字段输入 {"scope": "create:todos read:todos delete:todos", "resource": "https://todo.mcp-server.app"}。这样 Logto 返回的访问令牌 (Access token) 就包含访问待办事项管理器所需的权限 (Scopes)。

配置 MCP Auth

在你的 MCP 服务器项目中,需要安装 MCP Auth SDK 并配置其使用你的授权 (Authorization) 服务器元数据。

首先安装 mcpauth 包:

pip install mcpauth

或使用你喜欢的包管理器,如 uvpoetry

MCP Auth 需要授权 (Authorization) 服务器元数据才能初始化。根据你的提供商:

发行者 (Issuer) URL 可在 Logto Console 的应用详情页 “Endpoints & Credentials / Issuer endpoint” 部分找到,格式如 https://my-project.logto.app/oidc

更新 todo-manager.py,加入 MCP Auth 配置:

from mcpauth import MCPAuth
from mcpauth.config import AuthServerType
from mcpauth.utils import fetch_server_config

auth_issuer = '<issuer-endpoint>'  # 替换为你的发行者 (Issuer) 端点
auth_server_config = fetch_server_config(auth_issuer, type=AuthServerType.OIDC)
mcp_auth = MCPAuth(server=auth_server_config)

更新 todo-manager.py,加入 MCP Auth 配置:

from mcpauth import MCPAuth
from mcpauth.config import AuthServerType
from mcpauth.utils import fetch_server_config

auth_issuer = '<issuer-endpoint>'  # 替换为你的发行者 (Issuer) 端点
auth_server_config = fetch_server_config(auth_issuer, type=AuthServerType.OIDC)
mcp_auth = MCPAuth(server=auth_server_config)

更新 MCP 服务器

我们快完成了!现在需要更新 MCP 服务器,应用 MCP Auth 路由和中间件,并基于用户权限 (Scopes) 实现待办事项工具的权限 (Permission) 控制。

@mcp.tool()
def create_todo(content: str) -> dict[str, Any]:
    """Create a new todo."""
    return (
        mcp_auth.auth_info.scopes
        if mcp_auth.auth_info # 由 Bearer auth 中间件填充
        else {"error": "Not authenticated"}
    )

# ...

bearer_auth = Middleware(mcp_auth.bearer_auth_middleware("jwt"))
app = Starlette(
    routes=[
        # 添加元数据路由 (`/.well-known/oauth-authorization-server`)
        mcp_auth.metadata_route(),
        # 用 Bearer auth 中间件保护 MCP 服务器
        Mount('/', app=mcp.sse_app(), middleware=[bearer_auth]),
    ],
)

接下来,让我们实现具体的工具。

首先,创建一个简单的 todo 服务,在内存中提供基本的 CRUD 操作。

# service.py

"""
一个简单的 Todo 服务,仅用于演示。
使用内存列表存储 todos。
"""

from datetime import datetime
from typing import List, Optional, Dict, Any
import random
import string

class Todo:
"""表示一个待办事项。"""

    def __init__(self, id: str, content: str, owner_id: str, created_at: str):
        self.id = id
        self.content = content
        self.owner_id = owner_id
        self.created_at = created_at

    def to_dict(self) -> Dict[str, Any]:
        """将 todo 转为字典以便 JSON 序列化。"""
        return {
            "id": self.id,
            "content": self.content,
            "ownerId": self.owner_id,
            "createdAt": self.created_at
        }

class TodoService:
"""一个简单的 Todo 服务,仅用于演示。"""

    def __init__(self):
        self._todos: List[Todo] = []

    def get_all_todos(self, owner_id: Optional[str] = None) -> List[Dict[str, Any]]:
        """
        获取所有 todos,可选按 owner_id 过滤。

        Args:
            owner_id: 如提供,仅返回该用户拥有的 todos

        Returns:
            todo 字典列表
        """
        if owner_id:
            filtered_todos = [todo for todo in self._todos if todo.owner_id == owner_id]
            return [todo.to_dict() for todo in filtered_todos]
        return [todo.to_dict() for todo in self._todos]

    def get_todo_by_id(self, todo_id: str) -> Optional[Todo]:
        """
        按 ID 获取 todo。

        Args:
            todo_id: 要获取的 todo ID

        Returns:
            找到则返回 Todo 对象,否则为 None
        """
        for todo in self._todos:
            if todo.id == todo_id:
                return todo
        return None

    def create_todo(self, content: str, owner_id: str) -> Dict[str, Any]:
        """
        创建新 todo。

        Args:
            content: todo 内容
            owner_id: 拥有该 todo 的用户 ID

        Returns:
            创建的 todo 字典
        """
        todo = Todo(
            id=self._generate_id(),
            content=content,
            owner_id=owner_id,
            created_at=datetime.now().isoformat()
        )
        self._todos.append(todo)
        return todo.to_dict()

    def delete_todo(self, todo_id: str) -> Optional[Dict[str, Any]]:
        """
        按 ID 删除 todo。

        Args:
            todo_id: 要删除的 todo ID

        Returns:
            删除的 todo 字典,如未找到则为 None
        """
        for i, todo in enumerate(self._todos):
            if todo.id == todo_id:
                deleted_todo = self._todos.pop(i)
                return deleted_todo.to_dict()
        return None

    def _generate_id(self) -> str:
        """生成随机 todo ID。"""
        return ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))

然后在工具层,根据用户权限 (Scopes) 判断操作是否允许:

# todo-manager.py

from typing import Any, Optional
from mcpauth.errors import MCPAuthBearerAuthError

def assert_user_id(auth_info: Optional[dict]) -> str:
    """从 auth info 提取并校验用户 ID。"""
    subject = auth_info.get('subject') if auth_info else None
    if not subject:
        raise ValueError('Invalid auth info')
    return subject

def has_required_scopes(user_scopes: list[str], required_scopes: list[str]) -> bool:
    """检查用户是否拥有所有所需权限 (Scopes)。"""
    return all(scope in user_scopes for scope in required_scopes)

# 创建 TodoService 实例
todo_service = TodoService()

@mcp.tool()
def create_todo(content: str) -> dict[str, Any]:
    """创建新 todo。

    只有拥有 'create:todos' 权限 (Scope) 的用户才能创建 todo。
    """
    # 获取认证 (Authentication) 信息
    auth_info = mcp_auth.auth_info

    # 校验用户 ID
    try:
        user_id = assert_user_id(auth_info)
    except ValueError as e:
        return {"error": str(e)}

    # 检查用户是否有所需权限 (Permissions)
    if not has_required_scopes(auth_info.scopes if auth_info else [], ['create:todos']):
        raise MCPAuthBearerAuthError('missing_required_scopes')

    # 创建新 todo
    created_todo = todo_service.create_todo(content=content, owner_id=user_id)

    # 返回创建的 todo
    return created_todo.__dict__

# ...

你可以查看我们的 示例代码 获取其他详细实现。

检查点:运行 todo-manager 工具

重启你的 MCP 服务器,并在浏览器中打开 MCP inspector。点击“Connect”按钮后,你会被重定向到授权 (Authorization) 服务器的登录页面。

登录后回到 MCP inspector,重复上一个检查点的操作运行待办事项管理工具。此时,你可以用已认证 (Authentication) 的用户身份使用这些工具。工具的行为将根据分配给你的角色和权限 (Permissions) 而变化:

  • 如果你以 User(仅有 create:todos 权限 (Scope))身份登录:

    • 可以用 create-todo 工具创建新待办事项
    • 只能查看和删除自己的待办事项
    • 无法查看或删除其他用户的待办事项
  • 如果你以 Admin(拥有全部权限:create:todosread:todosdelete:todos)身份登录:

    • 可以创建新待办事项
    • 可以用 get-todos 工具查看系统中所有待办事项
    • 可以用 delete-todo 工具删除任意待办事项,无论归属谁

你可以通过以下方式测试不同权限级别:

  1. 退出当前会话(点击 MCP inspector 的“Disconnect”按钮)
  2. 用拥有不同角色/权限的其他用户账号登录
  3. 再次尝试相同工具,观察用户权限变化带来的行为差异

这展示了基于角色的访问控制 (RBAC) 在实际中的工作方式,不同用户对系统功能有不同访问级别。

MCP inspector 待办事项管理工具结果

信息

完整 MCP 服务器(OIDC 版本)代码请参考 MCP Auth Python SDK 仓库

结语

🎊 恭喜你!你已成功完成本教程。让我们回顾一下所做的内容:

  • 搭建了一个带有待办事项管理工具(create-todoget-todosdelete-todo)的基础 MCP 服务器
  • 实现了基于角色的访问控制 (RBAC),为用户和管理员设置了不同权限级别
  • 使用 MCP Auth 将 MCP 服务器集成到授权 (Authorization) 服务器
  • 配置 MCP Inspector 认证 (Authentication) 用户,并用带权限 (Scopes) 的访问令牌 (Access token) 调用工具

欢迎查阅其他教程和文档,充分利用 MCP Auth 的强大功能。