跳转到主要内容

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

在本教程中,我们将构建一个带有用户认证 (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:todos、read:todos、delete: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:todos 或 delete:todos 权限 (Scopes),但他们仍然可以:
    • 查看自己的待办事项
    • 删除自己的待办事项
  • Admin 拥有全部权限 (read:todos 和 delete: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:todos、read:todos、delete: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

或使用你喜欢的包管理器,如 uv 或 poetry。

创建 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-todo、get-todos 和 delete-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

或使用你喜欢的包管理器,如 uv 或 poetry。

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:todos、read:todos、delete:todos)身份登录:

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

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

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

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

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

信息

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

结语​

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

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

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