11.3 客户端 OAuth:OAuthClientProvider 全流程 本节摘要:本节讲客户端侧的 OAuth—— 抽象。它让 在连接受保护的服务端时,自动获取与刷新令牌,你不用手动管 token。流程是:发现元数据 → 浏览器跳转授权 → 用户同意 → 本地回调收令牌 → 后续请求带令牌 → 过期自动刷新。本节讲透你要实现的几个回调,以及这套机制如何让「连受保护服务」像「连普通服务」一样简单。 一、客户端 OAuth 的挑战 连一个受 OAuth 保护的服务,客户端要做的事不少: 这些步骤繁琐且易错。如果每个客户端都自己写一遍,代码重复且不安全。 把这些抽象成一个接口——你实现几个回调,SDK 跑全套 OAuth 流程。
本节摘要:本节讲客户端侧的 OAuth——
OAuthClientProvider抽象。它让Client在连接受保护的服务端时,自动获取与刷新令牌,你不用手动管 token。流程是:发现元数据 → 浏览器跳转授权 → 用户同意 → 本地回调收令牌 → 后续请求带令牌 → 过期自动刷新。本节讲透你要实现的几个回调,以及这套机制如何让「连受保护服务」像「连普通服务」一样简单。
连一个受 OAuth 保护的服务,客户端要做的事不少:
1. 发现服务端的 OAuth 元数据(端点在哪) 2. 引导用户浏览器跳转到授权页 3. 用户同意后,接收本地回调(拿授权码) 4. 用授权码换令牌 5. 持令牌访问服务 6. 令牌过期时自动刷新 7. 持久化令牌(下次启动不用重新授权)
这些步骤繁琐且易错。如果每个客户端都自己写一遍,代码重复且不安全。OAuthClientProvider 把这些抽象成一个接口——你实现几个回调,SDK 跑全套 OAuth 流程。
OAuthClientProvider 定义了几个回调,你实现它们:
from mcp.client.auth import OAuthClientProvider, OAuthClientMetadata class MyOAuthProvider(OAuthClientProvider): # 1. 发现:获取服务端的 OAuth 元数据 async def discover(self, url) -> OAuthServerMetadata: return await fetch_metadata(url) # 2. 跳转:引导浏览器去授权页 async def redirect(self, auth_url): webbrowser.open(auth_url) # 打开浏览器 # 3. 回调:接收授权码(本地起服务接收) async def listen_callback(self) -> str: code = await run_local_server() # 起本地 HTTP 服务收回调 return code # 4. 存取令牌:持久化(下次启动复用) async def save_token(self, token): await store.save(token) async def load_token(self): return await store.load()
每个回调对应 OAuth 流程的一步。SDK 按顺序调用它们,跑完整套流程。
实现 provider 后,挂到 Client:
from mcp import Client provider = MyOAuthProvider(...) async with Client( "http://mcp.example.com/mcp", auth=provider, # ← 挂上 OAuth provider ) as client: # 第一次连接:SDK 自动跑 OAuth 流程 # - discover 元数据 # - redirect 浏览器 # - 用户同意 # - listen_callback 收授权码 # - 换令牌、存令牌 # 后续调用:SDK 自动带令牌 result = await client.call_tool("get_user_data", {...})
挂上 provider 后,Client 自动处理 OAuth——你照常调 call_tool 等方法,SDK 在背后加 Authorization: Bearer <token> 头、过期自动刷新。对调用者完全透明。
完整的 OAuth 流程在 async with Client(...) 进入时发生:
async with Client(url, auth=provider) as client: │ ▼ 1. 检查是否有存的有效令牌(load_token) 有且未过期 → 直接用,跳到 6 无或过期 → 继续 OAuth 流程 │ ▼ 2. discover:获取服务端 OAuth 元数据 知道 authorize、token 端点在哪 │ ▼ 3. redirect:浏览器跳转到授权页 用户在浏览器看到授权页 │ ▼ 4. 用户登录、同意授权 服务端把授权码重定向回 redirect_uri │ ▼ 5. listen_callback:本地收授权码 用授权码换令牌(POST /token) save_token:持久化令牌 │ ▼ 6. 令牌就绪 后续 call_tool 等调用自动带 Authorization 头 │ ▼ 调用过程中,令牌过期 SDK 自动用 refresh_token 刷新(load/save 新令牌) 调用者无感
整个流程 SDK 自动驱动,你只实现回调。
listen_callback 通常要起一个本地 HTTP 服务接收回调:
async def listen_callback(self) -> str: """起本地服务,等授权码回调。""" from aiohttp import web code_future = asyncio.get_event_loop().create_future() async def handle(request): code = request.query.get("code") code_future.set_result(code) return web.Response(text="授权成功,可关闭此页面") app = web.Application() app.add_routes([web.get("/callback", handle)]) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, "localhost", 8765) await site.start() code = await code_future # 等回调 await runner.cleanup() return code
这个本地服务的端口,要与你在授权服务器注册的 redirect_uri 一致(如 http://localhost:8765/callback)。
💡 技巧:本地回调服务是 OAuth 客户端的标准模式,不是 MCP 特有。很多桌面应用(Slack、VS Code 扩展)都这么干——起一个临时本地服务收 OAuth 回调。理解了这个模式,你就理解了桌面应用 OAuth 的通用做法。
save_token / load_token 让令牌跨会话复用——下次启动客户端,不用重新授权:
class MyOAuthProvider(OAuthClientProvider): async def save_token(self, token): # 存到本地文件(或系统钥匙串,更安全) with open("~/.mcp_token", "w") as f: json.dump(token, f) async def load_token(self): try: with open("~/.mcp_token") as f: return json.load(f) except FileNotFoundError: return None # 没存过,走完整 OAuth
持久化让用户体验更好——授权一次,后续启动直接用。
⚠️ 注意:令牌要安全存储。access_token/refresh_token 等于访问权限,泄露=账号被盗。别明文存普通文件——用系统钥匙串(macOS Keychain、Windows Credential Manager)、加密存储,或至少设严格文件权限。
OAuthClientProvider 的核心价值:让连受保护服务像连普通服务一样简单。
# 连普通服务 async with Client(url) as client: result = await client.call_tool(...) # 连受 OAuth 保护的服务(只多一个 auth 参数) async with Client(url, auth=my_provider) as client: result = await client.call_tool(...) # ← 业务代码完全一样
业务代码完全相同,SDK 在背后处理 OAuth。这是良好抽象的体现——复杂度藏在框架里,调用者享受简洁。
OAuthClientProvider 让 Client 自动处理 OAuth,你实现回调即可。auth=provider,业务代码照常。客户端 OAuth 清楚了,下一节讲机器到机器场景——客户端凭证扩展。