教學:打造待辦事項管理器 (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
- OAuth 2.0 / OIDC
Logto 透過 API 資源 (API resources)(符合 RFC 8707: Resource Indicators for OAuth 2.0)與角色 (Roles) 功能支援 RBAC。設定方式如下:
-
登入 Logto Console(或你的自架 Logto Console)
-
建立 API 資源與權限範圍 (Scopes):
- 前往「API 資源 (API Resources)」
- 建立一個名為「Todo Manager」的新 API 資源
- 新增以下權限範圍:
create:todos
:「建立新待辦事項」read:todos
:「讀取所有待辦事項」delete:todos
:「刪除任一待辦事項」
-
建立角色(建議以便管理):
- 前往「角色 (Roles)」
- 建立「Admin」角色並指派所有權限範圍(
create:todos
、read:todos
、delete:todos
) - 建立「User」角色並僅指派
create:todos
權限範圍
-
指派權限:
- 前往「使用者 (Users)」
- 選擇一位使用者
- 你可以:
- 在「角色 (Roles)」分頁指派角色(建議)
- 或直接在「權限 (Permissions)」分頁指派權限範圍
這些權限範圍會以空格分隔字串的形式包含在 JWT 存取權杖的 scope
宣告 (Claim) 中。
OAuth 2.0 / OIDC 提供者通常支援基於權限範圍 (Scope) 的存取控制。實作 RBAC 時:
- 在授權伺服器中定義所需的權限範圍
- 設定用戶端在授權流程中請求這些權限範圍
- 確保授權伺服器將授予的權限範圍包含在存取權杖中
- 權限範圍通常會包含在 JWT 存取權杖的
scope
宣告 (Claim) 中
請查閱你的提供者文件以瞭解:
- 如何定義與管理權限範圍
- 權限範圍如何包含在存取權杖中
- 是否有額外的 RBAC 功能如角色管理
權杖驗證與權限檢查 (Validating tokens and checking permissions)
當 MCP 伺服器收到請求時,需執行:
- 驗證存取權杖的簽章與有效期限
- 從驗證後的權杖中擷取權限範圍 (Scopes)
- 檢查權杖是否具備執行該操作所需的權限範圍
例如,若使用者要建立新待辦事項,其存取權杖必須包含 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:todos | read:todos | delete: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
- Keycloak
- OAuth 2 / OIDC
Logto 透過 API 資源 (API resources) 與角色 (Roles) 功能支援 RBAC。設定方式如下:
-
登入 Logto Console(或你的自架 Logto Console)
-
建立 API 資源與權限範圍:
- 前往「API 資源 (API Resources)」
- 建立一個名為「Todo Manager」的新 API 資源,並以
https://todo.mcp-server.app
(僅供示範)作為資源標示符 (indicator)。 - 建立以下權限範圍:
create:todos
:「建立新待辦事項」read:todos
:「讀取所有待辦事項」delete:todos
:「刪除任一待辦事項」
-
建立角色(建議以便管理):
- 前往「角色 (Roles)」
- 建立「Admin」角色並指派所有權限範圍(
create:todos
、read:todos
、delete:todos
) - 建立「User」角色並僅指派
create:todos
權限範圍 - 在「User」角色詳細頁切換到「一般 (General)」分頁,將「User」設為「預設角色 (Default role)」
-
管理使用者角色與權限:
- 新使用者:
- 會自動獲得「User」角色(因已設為預設角色)
- 現有使用者:
- 前往「使用者管理 (User management)」
- 選擇一位使用者
- 在「角色 (Roles)」分頁指派角色
- 新使用者:
你也可以透過 Logto 的 Management API 程式化管理使用者角色。這對自動化使用者管理或建立管理後台特別有用。
請求存取權杖時,Logto 會根據使用者角色權限將權限範圍包含在權杖的 scope
宣告中。
在 Keycloak 中,你可以透過 client scopes 設定所需權限:
-
建立 client scopes:
- 在你的 realm 中,前往「Client scopes」
- 建立三個新 client scopes:
create:todos
read:todos
delete:todos
-
設定用戶端:
- 前往你的 client 設定
- 在「Client scopes」分頁新增所有已建立的 scopes
- 確認 token mapper 已設定為包含 scopes
-
選用:使用角色方便管理
- 若偏好角色型管理:
- 建立不同存取層級的 realm roles
- 將 scopes 映射到角色
- 指派角色給使用者
- 否則可直接指派 scopes 給使用者或透過 client-level permissions
- 若偏好角色型管理:
Keycloak 會將授予的 scopes 包含在存取權杖的 scope
宣告中。
對於 OAuth 2.0 或 OpenID Connect 提供者,你需要設定代表不同權限的 scopes。具體步驟依提供者而異,但一般流程如下:
-
定義 scopes:
- 設定授權伺服器支援:
create:todos
read:todos
delete:todos
- 設定授權伺服器支援:
-
設定用戶端:
- 註冊或更新用戶端以請求這些 scopes
- 確認 scopes 會包含在存取權杖中
-
指派權限:
- 透過提供者介面授予使用者適當的 scopes
- 有些提供者支援角色型管理,有些則直接指派 scopes
- 請查閱提供者文件以獲得建議做法
大多數提供者會將授予的 scopes 包含在存取權杖的 scope
宣告中,格式通常為空格分隔的 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)
- Python
- Node.js
mkdir mcp-server
cd mcp-server
uv init # 或使用 `pipenv` 或 `poetry` 建立新虛擬環境
建立新的 Node.js 專案:
mkdir mcp-server
cd mcp-server
npm init -y # 或使用 `pnpm init`
npm pkg set type="module"
npm pkg set main="todo-manager.ts"
npm pkg set scripts.start="node --experimental-strip-types todo-manager.ts"
我們範例中使用 TypeScript,因為 Node.js v22.6.0+ 原生支援 --experimental-strip-types
執行 TypeScript。如果你用 JavaScript,程式碼也很類似——只要確保 Node.js 版本為 v22.6.0 或以上。詳情請參閱 Node.js 文件。
安裝 MCP SDK 與相依套件 (Install the MCP SDK and dependencies)
- Python
- Node.js
pip install "mcp[cli]" starlette uvicorn
或你偏好的其他套件管理工具,如 uv
或 poetry
。
npm install @modelcontextprotocol/sdk express zod
或你偏好的其他套件管理工具,如 pnpm
或 yarn
。
建立 MCP 伺服器 (Create the MCP server)
首先,建立一個基本的 MCP 伺服器並定義工具:
- Python
- Node.js
建立 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 inspector 尚未支援授權流程,因此我們採用 SSE 方式設定 MCP 伺服器。待 MCP inspector 支援授權流程後會更新此處程式碼。
你也可以用 pnpm
或 yarn
。
建立 todo-manager.ts
檔案並加入以下程式碼:
// todo-manager.ts
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';
// 建立 MCP 伺服器
const server = new McpServer({
name: 'Todo Manager',
version: '0.0.0',
});
server.tool('create-todo', 'Create a new todo', { content: z.string() }, async ({ content }) => {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Not implemented' }) }],
};
});
server.tool('get-todos', 'List all todos', async () => {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Not implemented' }) }],
};
});
server.tool('delete-todo', 'Delete a todo by id', { id: z.string() }, async ({ id }) => {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Not implemented' }) }],
};
});
// 以下為 MCP SDK 文件範例樣板
const PORT = 3001;
const app = express();
const transports = {};
app.get('/sse', async (_req, res) => {
const transport = new SSEServerTransport('/messages', res);
transports[transport.sessionId] = transport;
res.on('close', () => {
delete transports[transport.sessionId];
});
await server.connect(transport);
});
app.post('/messages', async (req, res) => {
const sessionId = String(req.query.sessionId);
const transport = transports[sessionId];
if (transport) {
await transport.handlePostMessage(req, res, req.body);
} else {
res.status(400).send('No transport found for sessionId');
}
});
app.listen(PORT);
啟動伺服器:
npm start
檢查 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 工具
- 在 MCP inspector 頂部選單點選「Tools」分頁
- 點擊「List Tools」按鈕
- 應會看到
create-todo
、get-todos
、delete-todo
工具列於頁面,點擊可檢視工具詳情 - 右側會有「Run Tool」按鈕,點擊並輸入必要參數執行工具
- 應會看到工具回傳結果為
{"error": "Not implemented"}
的 JSON
與授權伺服器整合 (Integrate with your authorization server)
完成本節需考慮以下幾點:
你的授權伺服器的簽發者 (Issuer) URL
通常是授權伺服器的基礎網址,如 https://auth.example.com
。有些提供者可能會是 https://example.logto.app/oidc
這類路徑,請查閱提供者文件。
如何取得授權伺服器 metadata
- 若你的授權伺服器符合 OAuth 2.0 Authorization Server Metadata 或 OpenID Connect Discovery,可用 MCP Auth 內建工具自動抓取 metadata。
- 若不符合,需手動指定 metadata URL 或端點於 MCP 伺服器設定,請查閱提供者文件。
如何將 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 常見
- 查閱提供者文件確認支援哪些參數
- 有些提供者同時支援多種方式
- 資源標示符可提升安全性(限制受眾)
- 建議有支援時優先採用資源標示符
每個提供者細節不同,以下步驟可協助你依提供者設定 MCP inspector 與 MCP 伺服器。
註冊 MCP inspector 為用戶端
- Logto
- OAuth 2.0 / OIDC
將 todo manager 與 Logto 整合很簡單,因為它是支援資源標示符與權限範圍的 OpenID Connect 提供者,可用 https://todo.mcp-server.app
作為資源標示符保護 todo API。
Logto 尚未支援 Dynamic Client Registration,因此需手動將 MCP inspector 註冊為用戶端:
- 開啟 MCP inspector,點擊「OAuth Configuration」按鈕,複製 Redirect URL (auto-populated),應類似
http://localhost:6274/oauth/callback
- 登入 Logto Console(或你的自架 Logto Console)
- 前往「應用程式 (Applications)」分頁,點擊「建立應用程式 (Create application)」,頁面底部點「Create app without framework」
- 填寫應用程式資訊後點「建立應用程式 (Create application)」:
- 選擇應用程式類型:選「單頁應用程式 (Single-page application)」
- 應用程式名稱:如「MCP Inspector」
- 在「設定 / Redirect URIs」區塊貼上剛才複製的 Redirect URL,然後點底部「儲存變更 (Save changes)」
- 頂部卡片會顯示「App ID」,複製它
- 回到 MCP inspector,將「App ID」貼到「OAuth Configuration」的「Client ID」
- 在「Auth Params」欄位輸入
{"scope": "create:todos read:todos delete:todos", "resource": "https://todo.mcp-server.app"}
,確保 Logto 回傳的存取權杖包含存取 todo manager 所需的權限範圍
這是通用 OAuth 2.0 / OpenID Connect 提供者整合指引。兩者步驟類似,因 OIDC 建立於 OAuth 2.0 之上。請查閱你的提供者文件以獲得細節。
若你的提供者支援 Dynamic Client Registration,可直接跳到第 8 步設定 MCP inspector;否則需手動註冊 MCP inspector 為用戶端:
-
開啟 MCP inspector,點擊「OAuth Configuration」按鈕,複製 Redirect URL (auto-populated),應類似
http://localhost:6274/oauth/callback
-
登入你的提供者管理後台
-
前往「應用程式 (Applications)」或「用戶端 (Clients)」區塊,建立新應用程式或用戶端
-
若需選擇用戶端類型,請選「單頁應用程式 (Single-page application)」或「公開用戶端 (Public client)」
-
建立應用程式後,需設定 redirect URI,貼上剛才複製的 Redirect URL
-
找到新應用程式的「Client ID」或「Application ID」並複製
-
回到 MCP inspector,將「Client ID」貼到「OAuth Configuration」的「Client ID」
-
在「Auth Params」欄位輸入以下內容以請求 todo 操作所需權限範圍:
{ "scope": "create:todos read:todos delete:todos" }
設定 MCP Auth
在 MCP 伺服器專案中,需安裝 MCP Auth SDK 並設定使用你的授權伺服器 metadata。
- Python
- Node.js
先安裝 mcpauth
套件:
pip install mcpauth
或你偏好的其他套件管理工具,如 uv
或 poetry
。
先安裝 mcp-auth
套件:
npm install mcp-auth
MCP Auth 需要授權伺服器 metadata 來初始化。依你的提供者而定:
- Logto
- OAuth 2.0 / OIDC
簽發者 (Issuer) URL 可在 Logto Console 應用程式詳情頁「Endpoints & Credentials / Issuer endpoint」區塊找到,格式如 https://my-project.logto.app/oidc
。
- Python
- Node.js
將 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.ts
更新,加入 MCP Auth 設定:
import { MCPAuth, fetchServerConfig } from 'mcp-auth';
const authIssuer = '<issuer-endpoint>'; // 請替換為你的簽發者端點
const mcpAuth = new MCPAuth({
server: await fetchServerConfig(authIssuer, { type: 'oidc' }),
});
對於 OAuth 2.0 提供者,你需要:
- 查閱提供者文件取得授權伺服器 URL(常稱 issuer URL 或 base URL)
- 有些提供者會在
https://{your-domain}/.well-known/oauth-authorization-server
提供 - 也可在管理後台 OAuth/API 設定中找到
- Python
- Node.js
將 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.OAUTH) # 或 AuthServerType.OIDC
mcp_auth = MCPAuth(server=auth_server_config)
將 todo-manager.ts
更新以加入 MCP Auth 設定:
import { MCPAuth, fetchServerConfig } from 'mcp-auth';
const authIssuer = '<issuer-endpoint>'; // 請替換為你的簽發者 (Issuer) 端點
const mcpAuth = new MCPAuth({
server: await fetchServerConfig(authIssuer, { type: 'oauth' }), // 或 { type: 'oidc' }
});
在某些情況下,提供者的回應可能格式錯誤或不符合預期的中繼資料格式。如果你確信該提供者是相容的,可以透過設定選項轉譯中繼資料:
- Python
- Node.js
mcp_auth = MCPAuth(
server=fetch_server_config(
# ...other options
transpile_data=lambda data: {**data, 'response_types_supported': ['code']}
)
)
const mcpAuth = new MCPAuth({
server: await fetchServerConfig(authIssuer, {
// ...other options
transpileData: (data) => ({ ...data, response_types_supported: ['code'] }),
}),
});
如果你的提供者不支援 OAuth 2.0 授權伺服器中繼資料 (Authorization Server Metadata),你可以手動指定中繼資料 URL 或端點。詳情請參閱 其他初始化 MCP Auth 的方式。
- Python
- Node.js
更新 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)
更新 todo-manager.ts
加入 MCP Auth 設定:
// todo-manager.ts
import { MCPAuth, fetchServerConfig } from 'mcp-auth';
const authIssuer = '<issuer-endpoint>'; // 請替換為你的 issuer endpoint
const mcpAuth = new MCPAuth({
server: await fetchServerConfig(authIssuer, { type: 'oidc' }),
});
更新 MCP 伺服器 (Update MCP server)
快完成了!現在要更新 MCP 伺服器,套用 MCP Auth 路由與中介軟體,並根據使用者權限範圍實作 todo manager 工具的權限存取控制。
- Python
- Node.js
@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]),
],
)
server.tool(
'create-todo',
'Create a new todo',
{ content: z.string() },
async ({ content, authInfo }) => {
return {
content: [
{ type: 'text', text: JSON.stringify(authInfo?.scopes ?? { error: 'Not authenticated' }) },
],
};
}
);
// ...
app.use(mcpAuth.delegatedRouter());
app.use(mcpAuth.bearerAuth('jwt'));
接下來實作具體工具。
首先建立一個簡單的 todo 服務,提供記憶體內的 CRUD 操作。
- Python
- Node.js
# 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-service.ts
type Todo = {
id: string;
content: string;
ownerId: string;
createdAt: string;
};
/**
* 簡易 Todo 服務,僅供示範。
* 以記憶體陣列儲存 todos
*/
export class TodoService {
private readonly todos: Todo[] = [];
getAllTodos(ownerId?: string): Todo[] {
if (ownerId) {
return this.todos.filter((todo) => todo.ownerId === ownerId);
}
return this.todos;
}
getTodoById(id: string): Todo | undefined {
return this.todos.find((todo) => todo.id === id);
}
createTodo({ content, ownerId }: { content: string; ownerId: string }): Todo {
const todo: Todo = {
id: this.genId(),
content,
ownerId,
createdAt: new Date().toISOString(),
};
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
this.todos.push(todo);
return todo;
}
deleteTodo(id: string): Todo | undefined {
const index = this.todos.findIndex((todo) => todo.id === id);
if (index === -1) {
return undefined;
}
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
const [deleted] = this.todos.splice(index, 1);
return deleted;
}
private genId(): string {
return Math.random().toString(36).slice(2, 10);
}
}
然後在工具層根據使用者權限範圍判斷是否允許操作:
- Python
- Node.js
# 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.ts
// ... 其他 import
import assert from 'node:assert';
import { type AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
import { TodoService } from './todo-service.js';
const todoService = new TodoService();
const assertUserId = (authInfo?: AuthInfo) => {
const { subject } = authInfo ?? {};
assert(subject, 'Invalid auth info');
return subject;
};
/**
* 檢查使用者是否擁有所有必要權限範圍
*/
const hasRequiredScopes = (userScopes: string[], requiredScopes: string[]): boolean => {
return requiredScopes.every((scope) => userScopes.includes(scope));
};
server.tool(
'create-todo',
'Create a new todo',
{ content: z.string() },
({ content }: { content: string }, { authInfo }) => {
const userId = assertUserId(authInfo);
/**
* 只有擁有 'create:todos' 權限範圍的使用者才能建立 todo
*/
if (!hasRequiredScopes(authInfo?.scopes ?? [], ['create:todos'])) {
throw new MCPAuthBearerAuthError('missing_required_scopes');
}
const createdTodo = todoService.createTodo({ content, ownerId: userId });
return {
content: [{ type: 'text', text: JSON.stringify(createdTodo) }],
};
}
);
// ...
你可以參考我們的 範例程式碼 取得完整實作。
檢查點:執行 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
工具刪除任何待辦事項,不論擁有者
你可以這樣測試不同權限層級:
- 登出目前會話(點 MCP inspector 的「Disconnect」)
- 以不同角色/權限的帳號登入
- 重複操作觀察工具行為如何隨使用者權限變化
這展示了角色型存取控制 (RBAC) 的實際運作,不同使用者對系統功能有不同存取層級。
- Python
- Node.js
完整 MCP 伺服器(OIDC 版)程式碼請參考 MCP Auth Python SDK repository。
完整 MCP 伺服器(OIDC 版)程式碼請參考 MCP Auth Node.js SDK repository。
結語 (Closing notes)
🎊 恭喜你!已順利完成本教學。讓我們回顧一下:
- 建立具備 todo 管理工具(
create-todo
、get-todos
、delete-todo
)的基本 MCP 伺服器 - 實作不同使用者與管理員權限層級的角色型存取控制 (RBAC)
- 透過 MCP Auth 將 MCP 伺服器與授權伺服器整合
- 設定 MCP Inspector 以驗證使用者並用帶權限範圍的存取權杖呼叫工具
歡迎參閱其他教學與文件,充分發揮 MCP Auth 的強大功能。