跳轉到主要內容

教學:打造待辦事項管理器 (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:todos、read:todos、delete: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:todos 或 delete:todos 權限範圍,但仍可:
    • 讀取自己的待辦事項
    • 刪除自己的待辦事項
  • Admin 擁有完整權限(read:todos 與 delete: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:todos、read:todos、delete: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

或你偏好的其他套件管理工具,如 uv 或 poetry。

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

或你偏好的其他套件管理工具,如 uv 或 poetry。

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:todos、read:todos、delete: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-todo、get-todos、delete-todo)的基本 MCP 伺服器
  • 實作不同使用者與管理員權限層級的角色型存取控制 (RBAC)
  • 透過 MCP Auth 將 MCP 伺服器與授權伺服器整合
  • 設定 MCP Inspector 以驗證使用者並用帶權限範圍的存取權杖呼叫工具

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