Python20_MCP添加鉴权

Python20_MCP添加鉴权

MCP 鉴权的核心思路

MCP 官方 SDK 提供了灵活的中间件机制,可以在 Transport 层Server 层 添加鉴权。以下是几种常用方案:


方案一:SSE Transport 层鉴权(推荐用于 HTTP 场景)

如果你的 MCP Server 使用 SSE (Server-Sent Events) 作为传输层,可以在连接建立时验证请求头:

python 复制代码
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import Route
from mcp.server import Server
import asyncio

# 创建 MCP Server
app = Server("authenticated-server")

# 简单的 Token 验证函数
async def verify_token(request: Request) -> bool:
    auth_header = request.headers.get("Authorization", "")
    # 这里可以对接你的鉴权系统(JWT、API Key、OAuth 等)
    expected_token = "Bearer sk-your-secret-key"
    return auth_header == expected_token

# 自定义 SSE 处理,添加鉴权
async def handle_sse(request: Request) -> Response:
    # 1. 鉴权检查
    if not await verify_token(request):
        return Response(
            content="Unauthorized",
            status_code=401,
            headers={"WWW-Authenticate": "Bearer"}
        )
    
    # 2. 鉴权通过,建立 SSE 连接
    transport = SseServerTransport("/messages")
    async with transport.connect_sse(
        request.scope, 
        request.receive, 
        request._send  # 注意:实际生产环境需要更优雅的方式
    ) as (read_stream, write_stream):
        await app.run(
            read_stream,
            write_stream,
            app.create_initialization_options()
        )
    return Response(status_code=200)

# 消息端点也需要鉴权
async def handle_messages(request: Request) -> Response:
    if not await verify_token(request):
        return Response("Unauthorized", status_code=401)
    
    # 处理 POST 消息...
    pass

starlette_app = Starlette(
    routes=[
        Route("/sse", endpoint=handle_sse),
        Route("/messages", endpoint=handle_messages, methods=["POST"]),
    ]
)

方案二:使用 Starlette/FastAPI 中间件(更优雅的方案)

python 复制代码
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware import Middleware
from mcp.server.sse import SseServerTransport
from mcp.server import Server
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.requests import Request
from starlette.responses import Response, JSONResponse
import secrets

app = Server("secure-mcp-server")

# 配置你的 API Keys(生产环境应从环境变量或密钥管理服务读取)
VALID_API_KEYS = {"sk-prod-xxx", "sk-dev-yyy"}

class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # 排除健康检查端点
        if request.url.path == "/health":
            return await call_next(request)
        
        # 验证 API Key
        api_key = request.headers.get("X-API-Key") or request.headers.get("Authorization", "").replace("Bearer ", "")
        
        if not api_key or api_key not in VALID_API_KEYS:
            return JSONResponse(
                {"error": "Unauthorized", "message": "Invalid or missing API Key"},
                status_code=401
            )
        
        # 将用户信息存入 request.state,供后续使用
        request.state.user = {"api_key": api_key}
        return await call_next(request)

# SSE 端点
async def sse_endpoint(request: Request):
    transport = SseServerTransport("/messages")
    
    async with transport.connect_sse(
        request.scope,
        request.receive,
        request._send
    ) as streams:
        # 可以在这里访问 request.state.user 获取鉴权信息
        await app.run(
            streams[0], 
            streams[1],
            app.create_initialization_options()
        )

async def messages_endpoint(request: Request):
    # 由中间件处理鉴权,这里直接处理消息
    transport = SseServerTransport("/messages")
    # ... 处理消息逻辑

# 应用中间件
starlette_app = Starlette(
    middleware=[Middleware(AuthMiddleware)],
    routes=[
        Route("/sse", endpoint=sse_endpoint),
        Route("/messages", endpoint=messages_endpoint, methods=["POST"]),
        Route("/health", endpoint=lambda r: JSONResponse({"status": "ok"})),
    ]
)

方案三:Stdio Transport + 环境变量鉴权(本地场景)

如果你使用 stdio 传输(如 Claude Desktop 本地集成),可以通过环境变量传递凭证:

python 复制代码
import os
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server

app = Server("local-secure-server")

# 启动时验证环境变量
REQUIRED_TOKEN = os.environ.get("MCP_AUTH_TOKEN")
if not REQUIRED_TOKEN:
    raise RuntimeError("MCP_AUTH_TOKEN environment variable is required")

@app.call_tool()
async def secure_tool(name: str, arguments: dict) -> list:
    # 每个工具调用都可以检查调用上下文
    # 注意:stdio 模式下需要客户端配合传递 token
    client_token = arguments.get("_auth_token")
    if client_token != REQUIRED_TOKEN:
        raise ValueError("Invalid authentication token")
    
    # 清理内部参数后执行业务逻辑
    clean_args = {k: v for k, v in arguments.items() if not k.startswith("_")}
    # ... 实际工具逻辑
    return [{"type": "text", "text": "Success"}]

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await app.run(
            read_stream,
            write_stream,
            app.create_initialization_options()
        )

if __name__ == "__main__":
    asyncio.run(main())

方案四:JWT 鉴权(适合企业级部署)

python 复制代码
import jwt
from datetime import datetime, timedelta
from functools import wraps

# JWT 配置
JWT_SECRET = "your-secret-key"  # 生产环境使用强密钥
JWT_ALGORITHM = "HS256"

def create_token(user_id: str, expires_hours: int = 24) -> str:
    payload = {
        "sub": user_id,
        "iat": datetime.utcnow(),
        "exp": datetime.utcnow() + timedelta(hours=expires_hours),
        "scope": "mcp:read,mcp:write"  # 权限范围
    }
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)

def verify_jwt_token(token: str) -> dict:
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
        return payload
    except jwt.ExpiredSignatureError:
        raise ValueError("Token expired")
    except jwt.InvalidTokenError:
        raise ValueError("Invalid token")

# 在 SSE 处理中使用
async def handle_sse_with_jwt(request: Request):
    auth_header = request.headers.get("Authorization", "")
    if not auth_header.startswith("Bearer "):
        return Response("Unauthorized", status_code=401)
    
    token = auth_header[7:]  # 去掉 "Bearer "
    try:
        user_info = verify_jwt_token(token)
        # 可以检查 scope 权限
        if "mcp:read" not in user_info.get("scope", ""):
            return Response("Forbidden", status_code=403)
    except ValueError as e:
        return Response(f"Auth error: {e}", status_code=401)
    
    # 鉴权通过,继续建立连接...
    # 可以将 user_info 存入上下文供工具调用使用

关键最佳实践

场景 推荐方案 说明
远程 HTTP 部署 方案二(中间件) 统一入口,代码整洁
Claude Desktop 本地 方案三(环境变量) 配合 claude_desktop_config.json
企业级/多租户 方案四(JWT) 支持过期、权限范围
快速验证 方案一(简单 Header) 适合内部测试

客户端配置示例(Claude Desktop)

json 复制代码
{
  "mcpServers": {
    "secure-server": {
      "command": "python",
      "args": ["server.py"],
      "env": {
        "MCP_AUTH_TOKEN": "sk-your-secret-key"
      },
      "headers": {
        "Authorization": "Bearer sk-your-secret-key"
      }
    }
  }
}

相关推荐
bzmK1DTbd3 小时前
Git版本控制:Java项目中的分支管理与合并策略
java·开发语言·git
Rust研习社4 小时前
为什么 Rust 没有空指针?
开发语言·后端·rust
landyjzlai4 小时前
蓝迪哥玩转Ai(8)---端侧AI:RK3588 端侧大语言模型(LLM)开发实战指南
人工智能·python
kyriewen114 小时前
WebAssembly:前端界的“外挂”,让C++代码在浏览器里跑起来
开发语言·前端·javascript·c++·单元测试·ecmascript
我叫黑大帅5 小时前
如何通过 Python 实现招聘平台自动投递
后端·python·面试
其实防守也摸鱼6 小时前
CTF密码学综合教学指南--第九章
开发语言·网络·python·安全·网络安全·密码学·ctf
砚底藏山河6 小时前
Python量化开发:2026最佳实时股票数据API接口推荐与对比
开发语言·windows·python
AlunYegeer7 小时前
JAVA,以后端的视角理解前端。在全栈的路上迈出第一步。
java·开发语言·前端
研究点啥好呢7 小时前
专为求职者开发的“面馆”!!!摆脱面试焦虑!!!
python·面试·开源·reactjs·求职招聘·fastapi
hixiong1237 小时前
C# OpenvinoSharp使用DINOv2模型进行图像相似度计算
开发语言·c#