跳轉到主要內容

教學:打造待辦事項管理器 (Tutorial: Build a todo manager)

在本教學中,我們將建立一個具備使用者驗證 (Authentication) 與授權 (Authorization) 的 todo manager MCP 伺服器。

完成本教學後,你將會:

  • ✅ 基本瞭解如何在 MCP 伺服器中設定角色型存取控制 (RBAC, Role-based Access Control)
  • ✅ 擁有一個可以管理個人待辦清單的 MCP 伺服器
註解

在開始之前,如果你對 MCP 伺服器與 OAuth 2 不熟悉,強烈建議先閱讀 Who am I 教學

概覽 (Overview)

本教學將包含以下元件:

  • MCP 伺服器:一個簡單的 MCP 伺服器,使用 MCP 官方 SDK 處理請求,並整合 Todo 服務來管理使用者的待辦事項。
  • MCP inspector:一個 MCP 伺服器的視覺化測試工具,同時作為 OAuth / OIDC 用戶端,啟動授權流程並取得存取權杖 (Access token)。
  • 授權伺服器 (Authorization server):一個 OAuth 2.1 或 OpenID Connect 提供者,負責管理使用者身分並簽發存取權杖 (Access token)。

以下是這些元件間互動的高階圖示:

瞭解你的授權伺服器 (Understand your authorization server)

具有權限範圍 (Scopes) 的存取權杖 (Access tokens)

要在 MCP 伺服器中實作 角色型存取控制 (RBAC, Role-based Access Control),你的授權伺服器需支援簽發帶有權限範圍 (Scopes) 的存取權杖 (Access tokens)。權限範圍 (Scopes) 代表使用者被授予的權限。

Logto 透過 API 資源 (API resources)(符合 RFC 8707: Resource Indicators for OAuth 2.0)與角色 (Roles) 功能支援 RBAC。設定方式如下:

  1. 登入 Logto Console(或你的自架 Logto Console)

  2. 建立 API 資源與權限範圍 (Scopes):

    • 前往「API 資源 (API Resources)」
    • 建立一個名為「Todo Manager」的新 API 資源
    • 新增以下權限範圍:
      • create:todos:「建立新待辦事項」
      • read:todos:「讀取所有待辦事項」
      • delete:todos:「刪除任一待辦事項」
  3. 建立角色(建議以便管理):

    • 前往「角色 (Roles)」
    • 建立「Admin」角色並指派所有權限範圍(create:todosread:todosdelete:todos
    • 建立「User」角色並僅指派 create:todos 權限範圍
  4. 指派權限:

    • 前往「使用者 (Users)」
    • 選擇一位使用者
    • 你可以:
      • 在「角色 (Roles)」分頁指派角色(建議)
      • 或直接在「權限 (Permissions)」分頁指派權限範圍

這些權限範圍會以空格分隔字串的形式包含在 JWT 存取權杖的 scope 宣告 (Claim) 中。

權杖驗證與權限檢查 (Validating tokens and checking permissions)

當 MCP 伺服器收到請求時,需執行:

  1. 驗證存取權杖的簽章與有效期限
  2. 從驗證後的權杖中擷取權限範圍 (Scopes)
  3. 檢查權杖是否具備執行該操作所需的權限範圍

例如,若使用者要建立新待辦事項,其存取權杖必須包含 create:todos 權限範圍。流程如下:

動態用戶端註冊 (Dynamic Client Registration)

本教學不強制需要動態用戶端註冊,但若你想自動化 MCP 用戶端註冊流程,可參考 是否需要 Dynamic Client Registration?

瞭解 todo manager 的 RBAC (Understand RBAC in todo manager)

為了示範,我們會在 todo manager MCP 伺服器中實作一個簡單的角色型存取控制 (RBAC) 系統,讓你瞭解 RBAC 的基本原則,同時保持實作簡單。

註解

雖然本教學以 RBAC 權限範圍管理為例,但並非所有驗證 (Authentication) 提供者都透過角色 (Role) 來管理權限範圍 (Scope)。有些提供者可能有自己獨特的存取控制與權限管理機制。

工具與權限範圍 (Tools and scopes)

我們的 todo manager MCP 伺服器提供三個主要工具:

  • create-todo:建立新待辦事項
  • get-todos:列出所有待辦事項
  • delete-todo:依 ID 刪除待辦事項

為了控制這些工具的存取,我們定義以下權限範圍:

  • create:todos:允許建立新待辦事項
  • delete:todos:允許刪除現有待辦事項
  • read:todos:允許查詢並取得所有待辦事項清單

角色與權限 (Roles and permissions)

我們將定義兩個不同存取層級的角色:

角色 (Role)create:todosread:todosdelete:todos
Admin
User
  • User:一般使用者,可建立待辦事項,僅能檢視或刪除自己的待辦事項
  • Admin:管理員,可建立、檢視及刪除所有待辦事項,不論擁有者為誰

資源擁有權 (Resource ownership)

雖然上表顯示每個角色明確被指派的權限範圍,但還有一個重要的「資源擁有權」原則:

  • User 沒有 read:todosdelete:todos 權限範圍,但仍可:
    • 讀取自己的待辦事項
    • 刪除自己的待辦事項
  • Admin 擁有完整權限(read:todosdelete:todos),可:
    • 檢視系統中所有待辦事項
    • 刪除任何待辦事項,不論擁有者

這展現了 RBAC 系統中常見的模式:資源擁有權會隱含授予使用者對自己資源的權限,而管理角色則獲得對所有資源的明確權限。

進一步瞭解

想深入瞭解 RBAC 概念與最佳實踐,請參閱 精通 RBAC:完整實例解析 (Mastering RBAC: A Comprehensive Real-World Example)

在你的提供者中設定授權 (Configure authorization in your provider)

要實作上述存取控制系統,你需要在授權伺服器中設定所需的權限範圍。以下是不同提供者的設定方式:

Logto 透過 API 資源 (API resources) 與角色 (Roles) 功能支援 RBAC。設定方式如下:

  1. 登入 Logto Console(或你的自架 Logto Console)

  2. 建立 API 資源與權限範圍:

    • 前往「API 資源 (API Resources)」
    • 建立一個名為「Todo Manager」的新 API 資源,並以 https://todo.mcp-server.app(僅供示範)作為資源標示符 (indicator)。
    • 建立以下權限範圍:
      • create:todos:「建立新待辦事項」
      • read:todos:「讀取所有待辦事項」
      • delete:todos:「刪除任一待辦事項」
  3. 建立角色(建議以便管理):

    • 前往「角色 (Roles)」
    • 建立「Admin」角色並指派所有權限範圍(create:todosread:todosdelete:todos
    • 建立「User」角色並僅指派 create:todos 權限範圍
    • 在「User」角色詳細頁切換到「一般 (General)」分頁,將「User」設為「預設角色 (Default role)」
  4. 管理使用者角色與權限:

    • 新使用者:
      • 會自動獲得「User」角色(因已設為預設角色)
    • 現有使用者:
      • 前往「使用者管理 (User management)」
      • 選擇一位使用者
      • 在「角色 (Roles)」分頁指派角色
程式化角色管理 (Programmatic Role Management)

你也可以透過 Logto 的 Management API 程式化管理使用者角色。這對自動化使用者管理或建立管理後台特別有用。

請求存取權杖時,Logto 會根據使用者角色權限將權限範圍包含在權杖的 scope 宣告中。

設定好授權伺服器後,使用者將取得包含其授權 scopes 的存取權杖。MCP 伺服器會根據這些 scopes 判斷:

  • 使用者是否能建立新待辦事項(create:todos
  • 使用者是否能檢視所有待辦事項(read:todos)或僅能檢視自己的
  • 使用者是否能刪除任一待辦事項(delete:todos)或僅能刪除自己的

設定 MCP 伺服器 (Set up the MCP server)

我們將使用 MCP 官方 SDK 建立 todo manager MCP 伺服器。

建立新專案 (Create a new project)

mkdir mcp-server
cd mcp-server
uv init # 或使用 `pipenv` 或 `poetry` 建立新虛擬環境

安裝 MCP SDK 與相依套件 (Install the MCP SDK and dependencies)

pip install "mcp[cli]" starlette uvicorn

或你偏好的其他套件管理工具,如 uvpoetry

建立 MCP 伺服器 (Create the MCP server)

首先,建立一個基本的 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 伺服器 (Inspect the MCP server)

複製並執行 MCP inspector

現在 MCP 伺服器已啟動,可以用 MCP inspector 檢查 whoami 工具是否可用。

由於現有實作限制,我們 fork 了 MCP inspector 以提升其彈性與可擴展性,並已向原專案提交 PR。

執行 MCP inspector:

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

然後在瀏覽器開啟 http://localhost:6274/(或終端機顯示的其他網址)即可存取 MCP inspector。

連接 MCP inspector 與 MCP 伺服器

繼續前請檢查 MCP inspector 設定:

  • Transport Type:設為 SSE
  • URL:設為 MCP 伺服器的 URL,本例為 http://localhost:3001/sse

現在點擊「Connect」按鈕,檢查 MCP inspector 是否能連上 MCP 伺服器。若一切正常,應會看到 MCP inspector 顯示「Connected」狀態。

檢查點:執行 todo manager 工具

  1. 在 MCP inspector 頂部選單點選「Tools」分頁
  2. 點擊「List Tools」按鈕
  3. 應會看到 create-todoget-todosdelete-todo 工具列於頁面,點擊可檢視工具詳情
  4. 右側會有「Run Tool」按鈕,點擊並輸入必要參數執行工具
  5. 應會看到工具回傳結果為 {"error": "Not implemented"} 的 JSON

MCP inspector 首次執行畫面

與授權伺服器整合 (Integrate with your authorization server)

完成本節需考慮以下幾點:

你的授權伺服器的簽發者 (Issuer) URL

通常是授權伺服器的基礎網址,如 https://auth.example.com。有些提供者可能會是 https://example.logto.app/oidc 這類路徑,請查閱提供者文件。

如何取得授權伺服器 metadata
如何將 MCP inspector 註冊為授權伺服器用戶端
  • 若授權伺服器支援 Dynamic Client Registration,MCP inspector 會自動註冊為用戶端,可略過此步驟。
  • 若不支援,需手動將 MCP inspector 註冊為用戶端。
瞭解權杖請求參數

向不同授權伺服器請求存取權杖時,指定目標資源與權限的方式可能不同,主要有:

  • 基於資源標示符 (Resource indicator based)

    • 使用 resource 參數指定目標 API(參見 RFC 8707: Resource Indicators for OAuth 2.0
    • 現代 OAuth 2.0 常見
    • 範例請求:
      {
        "resource": "https://todo.mcp-server.app",
        "scope": "create:todos read:todos"
      }
    • 伺服器會簽發專屬於該資源的權杖
  • 基於受眾 (Audience based)

    • 使用 audience 參數指定權杖預期接收者
    • 與資源標示符類似但語意不同
    • 範例請求:
      {
        "audience": "todo-api",
        "scope": "create:todos read:todos"
      }
  • 純權限範圍 (Pure scope based)

    • 僅依 scopes,不帶 resource/audience 參數
    • 傳統 OAuth 2.0 作法
    • 範例請求:
      {
        "scope": "todo-api:create todo-api:read openid profile"
      }
    • 常用前綴命名空間權限
    • 簡單 OAuth 2.0 常見
最佳實踐 (Best Practices)
  • 查閱提供者文件確認支援哪些參數
  • 有些提供者同時支援多種方式
  • 資源標示符可提升安全性(限制受眾)
  • 建議有支援時優先採用資源標示符

每個提供者細節不同,以下步驟可協助你依提供者設定 MCP inspector 與 MCP 伺服器。

註冊 MCP inspector 為用戶端

將 todo manager 與 Logto 整合很簡單,因為它是支援資源標示符與權限範圍的 OpenID Connect 提供者,可用 https://todo.mcp-server.app 作為資源標示符保護 todo API。

Logto 尚未支援 Dynamic Client Registration,因此需手動將 MCP inspector 註冊為用戶端:

  1. 開啟 MCP inspector,點擊「OAuth Configuration」按鈕,複製 Redirect URL (auto-populated),應類似 http://localhost:6274/oauth/callback
  2. 登入 Logto Console(或你的自架 Logto Console)
  3. 前往「應用程式 (Applications)」分頁,點擊「建立應用程式 (Create application)」,頁面底部點「Create app without framework」
  4. 填寫應用程式資訊後點「建立應用程式 (Create application)」:
    • 選擇應用程式類型:選「單頁應用程式 (Single-page application)」
    • 應用程式名稱:如「MCP Inspector」
  5. 在「設定 / Redirect URIs」區塊貼上剛才複製的 Redirect URL,然後點底部「儲存變更 (Save changes)」
  6. 頂部卡片會顯示「App ID」,複製它
  7. 回到 MCP inspector,將「App ID」貼到「OAuth Configuration」的「Client ID」
  8. 在「Auth Params」欄位輸入 {"scope": "create:todos read:todos delete:todos", "resource": "https://todo.mcp-server.app"},確保 Logto 回傳的存取權杖包含存取 todo manager 所需的權限範圍

設定 MCP Auth

在 MCP 伺服器專案中,需安裝 MCP Auth SDK 並設定使用你的授權伺服器 metadata。

先安裝 mcpauth 套件:

pip install mcpauth

或你偏好的其他套件管理工具,如 uvpoetry

MCP Auth 需要授權伺服器 metadata 來初始化。依你的提供者而定:

簽發者 (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>'  # 請替換為你的簽發者端點
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 endpoint
auth_server_config = fetch_server_config(auth_issuer, type=AuthServerType.OIDC)
mcp_auth = MCPAuth(server=auth_server_config)

更新 MCP 伺服器 (Update MCP server)

快完成了!現在要更新 MCP 伺服器,套用 MCP Auth 路由與中介軟體,並根據使用者權限範圍實作 todo manager 工具的權限存取控制。

@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 middleware 填入
        else {"error": "Not authenticated"}
    )

# ...

bearer_auth = Middleware(mcp_auth.bearer_auth_middleware("jwt"))
app = Starlette(
    routes=[
        # 加入 metadata 路由 (`/.well-known/oauth-authorization-server`)
        mcp_auth.metadata_route(),
        # 用 Bearer auth middleware 保護 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:
"""代表一個 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]:
        """轉換為 dict 以便 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 dict 清單
        """
        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: 擁有者 ID

        Returns:
            建立的 todo dict
        """
        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 dict,否則 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))

然後在工具層根據使用者權限範圍判斷是否允許操作:

# todo-manager.py

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

def assert_user_id(auth_info: Optional[dict]) -> str:
    """從 auth info 擷取並驗證 user 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:
    """檢查使用者是否擁有所有必要權限範圍。"""
    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' 權限範圍的使用者才能建立 todo。
    """
    # 取得驗證資訊
    auth_info = mcp_auth.auth_info

    # 驗證 user ID
    try:
        user_id = assert_user_id(auth_info)
    except ValueError as e:
        return {"error": str(e)}

    # 檢查是否有必要權限
    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」後,應會被導向授權伺服器登入頁。

登入並返回 MCP inspector 後,重複前述步驟執行 todo manager 工具。這次你將以已驗證的使用者身分操作,工具行為會依你被指派的角色與權限而異:

  • 若以 User(僅有 create:todos 權限範圍)登入:

    • 可用 create-todo 工具建立新待辦事項
    • 只能檢視與刪除自己的待辦事項
    • 無法看到或刪除其他使用者的待辦事項
  • 若以 Admin(擁有所有權限範圍:create:todosread:todosdelete:todos)登入:

    • 可建立新待辦事項
    • 可用 get-todos 工具檢視系統所有待辦事項
    • 可用 delete-todo 工具刪除任何待辦事項,不論擁有者

你可以這樣測試不同權限層級:

  1. 登出目前會話(點 MCP inspector 的「Disconnect」)
  2. 以不同角色/權限的帳號登入
  3. 重複操作觀察工具行為如何隨使用者權限變化

這展示了角色型存取控制 (RBAC) 的實際運作,不同使用者對系統功能有不同存取層級。

MCP inspector todo manager 工具結果

資訊

完整 MCP 伺服器(OIDC 版)程式碼請參考 MCP Auth Python SDK repository

結語 (Closing notes)

🎊 恭喜你!已順利完成本教學。讓我們回顧一下:

  • 建立具備 todo 管理工具(create-todoget-todosdelete-todo)的基本 MCP 伺服器
  • 實作不同使用者與管理員權限層級的角色型存取控制 (RBAC)
  • 透過 MCP Auth 將 MCP 伺服器與授權伺服器整合
  • 設定 MCP Inspector 以驗證使用者並用帶權限範圍的存取權杖呼叫工具

歡迎參閱其他教學與文件,充分發揮 MCP Auth 的強大功能。