第三方 MCP 服务器集成
本页提供使用 Casdoor 做 OAuth 2.1 的 MCP 服务器的完整可运行示例:受保护资源元数据、JWT 校验和 scope 检查。每个示例几分钟就能跑起来。
先决条件
- 配置了 MCP 应用的 Casdoor(配置)
- 用于测试的 MCP 客户端(例如 Claude Desktop)
Python 示例
本示例使用官方的 mcp SDK 和 PyJWT 校验令牌。
安装
pip install mcp PyJWT cryptography requests
完整服务器代码
"""
MCP Server with Casdoor OAuth Authentication
Demonstrates Protected Resource Metadata, JWT validation, and scope enforcement
"""
import asyncio
import json
from typing import Any
import jwt
import requests
from jwt import PyJWKClient
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# Configuration - Replace with your Casdoor instance
CASDOOR_URL = "https://your-casdoor.com"
MCP_SERVER_URL = "https://your-mcp-server.com"
JWKS_URL = f"{CASDOOR_URL}/.well-known/jwks"
# Initialize JWKS client for token validation
jwks_client = PyJWKClient(JWKS_URL)
# Create MCP server instance
app = Server("example-mcp-server")
def validate_token(token: str) -> dict:
"""
Validate JWT token from Casdoor using JWKS.
Returns decoded token with claims if valid.
Raises jwt.InvalidTokenError if invalid.
"""
try:
# Get signing key from JWKS
signing_key = jwks_client.get_signing_key_from_jwt(token)
# Verify and decode token
decoded = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=MCP_SERVER_URL, # Verify audience matches our server
options={
"verify_signature": True,
"verify_exp": True,
"verify_aud": True,
}
)
return decoded
except jwt.InvalidTokenError as e:
raise ValueError(f"Invalid token: {e}")
def check_scope(token_data: dict, required_scope: str) -> None:
"""
Check if token contains required scope.
Raises PermissionError if scope is missing.
"""
scopes = token_data.get("scope", "").split()
if required_scope not in scopes:
raise PermissionError(f"Missing required scope: {required_scope}")
@app.list_tools()
async def list_tools() -> list[Tool]:
"""List available tools"""
return [
Tool(
name="read_file",
description="Read contents of a file",
inputSchema={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to read"
}
},
"required": ["path"]
}
),
Tool(
name="write_file",
description="Write content to a file",
inputSchema={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to write"
},
"content": {
"type": "string",
"description": "Content to write to the file"
}
},
"required": ["path", "content"]
}
),
Tool(
name="list_files",
description="List files in a directory",
inputSchema={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory path to list"
}
},
"required": ["path"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""
Handle tool calls with OAuth token validation and scope enforcement.
In production, extract token from request context.
For this example, we'll show the validation logic.
"""
# In a real MCP server, extract token from the request headers
# token = request.headers.get("Authorization", "").replace("Bearer ", "")
# For demonstration, we'll skip actual token extraction
# Example token validation (uncomment in production):
# try:
# token_data = validate_token(token)
# except ValueError as e:
# return [TextContent(type="text", text=f"Authentication failed: {e}")]
# Handle each tool with appropriate scope checks
if name == "read_file":
# Requires files:read scope
# check_scope(token_data, "files:read")
path = arguments.get("path")
try:
with open(path, 'r') as f:
content = f.read()
return [TextContent(
type="text",
text=f"File contents:\n{content}"
)]
except Exception as e:
return [TextContent(
type="text",
text=f"Error reading file: {e}"
)]
elif name == "write_file":
# Requires files:write scope
# check_scope(token_data, "files:write")
path = arguments.get("path")
content = arguments.get("content")
try:
with open(path, 'w') as f:
f.write(content)
return [TextContent(
type="text",
text=f"Successfully wrote to {path}"
)]
except Exception as e:
return [TextContent(
type="text",
text=f"Error writing file: {e}"
)]
elif name == "list_files":
# Requires files:list scope
# check_scope(token_data, "files:list")
import os
path = arguments.get("path")
try:
files = os.listdir(path)
return [TextContent(
type="text",
text=f"Files in {path}:\n" + "\n".join(files)
)]
except Exception as e:
return [TextContent(
type="text",
text=f"Error listing files: {e}"
)]
return [TextContent(type="text", text=f"Unknown tool: {name}")]
async def serve_protected_resource_metadata():
"""
Serve Protected Resource Metadata endpoint.
In production, integrate this with your HTTP framework.
"""
metadata = {
"resource": MCP_SERVER_URL,
"authorization_servers": [CASDOOR_URL],
"scopes_supported": [
"files:read",
"files:write",
"files:list"
],
"bearer_methods_supported": ["header"]
}
return metadata
async def main():
"""Run the MCP server"""
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())
测试 Python 服务器
-
把代码 保存 为
mcp_server.py -
配置 Claude Desktop:添加到
~/Library/Application Support/Claude/claude_desktop_config.json(macOS):
{
"mcpServers": {
"example-files": {
"command": "python",
"args": ["/path/to/mcp_server.py"]
}
}
}
- 重启 Claude Desktop,确认工具出现在 MCP 工具列表中