mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
feat(mcp): support OAuth authorization for remote MCP servers
This commit is contained in:
@@ -1320,6 +1320,7 @@ class WebChannel(ChatChannel):
|
||||
'/api/messages/delete', 'MessageDeleteHandler',
|
||||
'/api/logs', 'LogsHandler',
|
||||
'/api/version', 'VersionHandler',
|
||||
'/mcp/oauth/callback', 'McpOAuthCallbackHandler',
|
||||
'/assets/(.*)', 'AssetsHandler',
|
||||
)
|
||||
app = web.application(urls, globals(), autoreload=False)
|
||||
@@ -1383,6 +1384,64 @@ class HealthHandler:
|
||||
return json.dumps({"status": "ok"})
|
||||
|
||||
|
||||
class McpOAuthCallbackHandler:
|
||||
"""OAuth redirect target for MCP servers requiring authorization.
|
||||
|
||||
The browser lands here after the user authorizes a remote MCP server.
|
||||
We exchange the authorization code for tokens and bring the server
|
||||
online. Unauthenticated by design: the OAuth `state` param is the
|
||||
single-use secret that binds this request to a pending authorization.
|
||||
"""
|
||||
|
||||
def GET(self):
|
||||
web.header('Content-Type', 'text/html; charset=utf-8')
|
||||
params = web.input(code="", state="", error="", error_description="")
|
||||
|
||||
def _page(title: str, message: str) -> str:
|
||||
return (
|
||||
"<!doctype html><html><head><meta charset='utf-8'>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||
f"<title>{title}</title></head>"
|
||||
"<body style='font-family:-apple-system,Segoe UI,Roboto,sans-serif;"
|
||||
"max-width:520px;margin:64px auto;padding:0 20px;text-align:center;color:#1f2328'>"
|
||||
f"<h2>{title}</h2><p style='color:#57606a'>{message}</p></body></html>"
|
||||
)
|
||||
|
||||
if params.error:
|
||||
logger.warning(f"[MCP-OAuth] callback error: {params.error} {params.error_description}")
|
||||
return _page("授权失败", f"{params.error}: {params.error_description or ''}")
|
||||
|
||||
if not params.code or not params.state:
|
||||
return _page("参数缺失", "回调缺少 code 或 state 参数。")
|
||||
|
||||
try:
|
||||
from agent.tools.mcp.mcp_oauth import pop_pending
|
||||
from agent.tools.mcp.mcp_client import notify_server_authorized
|
||||
except Exception as e:
|
||||
logger.warning(f"[MCP-OAuth] callback import failed: {e}")
|
||||
return _page("内部错误", "OAuth 模块不可用。")
|
||||
|
||||
handler = pop_pending(params.state)
|
||||
if handler is None:
|
||||
return _page("会话已过期", "授权请求不存在或已过期,请重新触发授权。")
|
||||
|
||||
try:
|
||||
ok = handler.finish_authorization(params.code)
|
||||
except Exception as e:
|
||||
logger.warning(f"[MCP-OAuth] token exchange crashed: {e}")
|
||||
ok = False
|
||||
|
||||
if not ok:
|
||||
return _page("授权失败", "换取令牌失败,请重试。")
|
||||
|
||||
notify_server_authorized(handler.server_name)
|
||||
logger.info(f"[MCP-OAuth] Server '{handler.server_name}' authorized via web callback")
|
||||
return _page(
|
||||
"授权成功",
|
||||
f"MCP 服务 “{handler.server_name}” 已授权,可以返回聊天继续使用了。",
|
||||
)
|
||||
|
||||
|
||||
class AuthCheckHandler:
|
||||
def GET(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
|
||||
Reference in New Issue
Block a user