Third-party MCP server integration
This page has full, runnable examples for MCP servers that use Casdoor for OAuth 2.1: Protected Resource Metadata, JWT validation, and scope checks. Each example can be run in a few minutes.
Prerequisites
- Casdoor with an MCP application (Setup)
- An MCP client for testing (e.g. Claude Desktop)
Python Example
This example uses the official mcp SDK and PyJWT for token validation.
Installation
pip install mcp PyJWT cryptography requests
Complete Server Code
"""
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())
Testing the Python Server
-
Save the code as
mcp_server.py -
Configure Claude Desktop by adding to
~/Library/Application Support/Claude/claude_desktop_config.json(macOS):
{
"mcpServers": {
"example-files": {
"command": "python",
"args": ["/path/to/mcp_server.py"]
}
}
}
- Restart Claude Desktop and verify the tools appear in the MCP tools list
Node.js Example
This example uses the official @modelcontextprotocol/sdk and jose for JWT validation.
Installation
npm install @modelcontextprotocol/sdk jose
Complete Server Code
/**
* MCP Server with Casdoor OAuth Authentication
* Demonstrates Protected Resource Metadata, JWT validation, and scope enforcement
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
// Configuration - Replace with your Casdoor instance
const CASDOOR_URL = 'https://your-casdoor.com';
const MCP_SERVER_URL = 'https://your-mcp-server.com';
const JWKS_URL = `${CASDOOR_URL}/.well-known/jwks`;
// Initialize JWKS for token validation
const JWKS = createRemoteJWKSet(new URL(JWKS_URL));
/**
* Validate JWT token from Casdoor using JWKS.
*
* @param {string} token - JWT token to validate
* @returns {Promise<object>} Decoded token payload
* @throws {Error} If token is invalid
*/
async function validateToken(token) {
try {
const { payload } = await jwtVerify(token, JWKS, {
audience: MCP_SERVER_URL,
issuer: CASDOOR_URL,
});
return payload;
} catch (error) {
throw new Error(`Invalid token: ${error.message}`);
}
}
/**
* Check if token contains required scope.
*
* @param {object} tokenData - Decoded token payload
* @param {string} requiredScope - Scope to check for
* @throws {Error} If scope is missing
*/
function checkScope(tokenData, requiredScope) {
const scopes = (tokenData.scope || '').split(' ');
if (!scopes.includes(requiredScope)) {
throw new Error(`Missing required scope: ${requiredScope}`);
}
}
// Create MCP server instance
const server = new Server(
{
name: 'example-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
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'],
},
},
{
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'],
},
},
{
name: 'list_files',
description: 'List files in a directory',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Directory path to list',
},
},
required: ['path'],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// In production, extract token from request context
// const token = request.headers?.authorization?.replace('Bearer ', '');
// const tokenData = await validateToken(token);
try {
switch (name) {
case 'read_file': {
// checkScope(tokenData, 'files:read');
const fs = await import('fs/promises');
const content = await fs.readFile(args.path, 'utf-8');
return {
content: [
{
type: 'text',
text: `File contents:\n${content}`,
},
],
};
}
case 'write_file': {
// checkScope(tokenData, 'files:write');
const fs = await import('fs/promises');
await fs.writeFile(args.path, args.content, 'utf-8');
return {
content: [
{
type: 'text',
text: `Successfully wrote to ${args.path}`,
},
],
};
}
case 'list_files': {
// checkScope(tokenData, 'files:list');
const fs = await import('fs/promises');
const files = await fs.readdir(args.path);
return {
content: [
{
type: 'text',
text: `Files in ${args.path}:\n${files.join('\n')}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
/**
* Protected Resource Metadata
* In production, serve this via HTTP endpoint at /.well-known/oauth-protected-resource
*/
export const protectedResourceMetadata = {
resource: MCP_SERVER_URL,
authorization_servers: [CASDOOR_URL],
scopes_supported: ['files:read', 'files:write', 'files:list'],
bearer_methods_supported: ['header'],
};
// Run the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP Server running on stdio');
}
main().catch(console.error);
Testing the Node.js Server
-
Save the code as
mcp-server.js -
Add
"type": "module"to yourpackage.json:
{
"name": "mcp-example-server",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"jose": "^5.0.0"
}
}
- Configure Claude Desktop:
{
"mcpServers": {
"example-files": {
"command": "node",
"args": ["/path/to/mcp-server.js"]
}
}
}
- Restart Claude Desktop
Go Example
This example demonstrates integration using Casdoor's Go SDK and standard library JWT validation.