Set up Casdoor for your MCP server
This guide explains how to make Casdoor the authorization server of your own MCP server: first the configuration in Casdoor, then the changes to your MCP server.
Learning outcomes
- Create an application with custom scopes for your MCP server.
- Let MCP clients register themselves.
- Publish Protected Resource Metadata and challenge unauthenticated requests.
- Validate tokens and enforce scopes in your tools.
What you need
- A running Casdoor instance and administrator access to it. See Install the Casdoor server.
- The public URL of your MCP server, for example
https://your-mcp-server.com
Configure Casdoor
Create the application
-
In the Casdoor admin console, go to Identity > Applications and add an application.
-
Fill in the Basic tab:
Field Value Name A descriptive name, for example my-files-mcp-serverOrganization The organization whose users use the MCP server Category Agent. This turns on custom scopes. See Application categoriesType MCP. Casdoor sets it when you selectAgent
Add the redirect URLs
Add the callback URLs of the MCP clients that use your server to Redirect URLs, for example https://mcp.example.com/oauth/callback. Casdoor sends users back to these URLs after authorization.
Casdoor compares redirect URLs exactly, including the scheme, the port, and the path. Clients that register themselves through dynamic client registration bring their own redirect URLs, so you don't add those here.
Set the grant types
In Grant types, turn on:
- Authorization Code: The flow that MCP uses.
- Refresh Token: Lets clients stay signed in without a new authorization.
Leave the implicit grant and the password grant off. MCP doesn't use them.
Define scopes
Define one scope for each capability of your tools.
-
In Scopes, add a row for each scope:
Column Value Name The scope in the form resource:action, for examplefiles:readDisplay name A short name for users, for example Read FilesDescription What the scope allows. Users read it on the consent screen -
Save the application.
For a file server, the scopes could be:
| Name | Display Name | Description |
|---|---|---|
files:read | Read Files | View and download files from your storage |
files:write | Write Files | Create, modify, and delete files in your storage |
files:list | List Files | See file names and metadata in directories |
For a database server:
| Name | Display Name | Description |
|---|---|---|
db:query | Query Database | Execute read-only database queries |
db:modify | Modify Database | Create, update, and delete database records |
db:admin | Database Admin | Manage schemas, tables, and database settings |
Keep the scopes fine-grained, so that users can grant part of the access. Start with few scopes and add more as your tools grow. See Define custom scopes.
Understand the consent screen
Because the application defines custom scopes, Casdoor shows a consent screen when a client requests scopes that the user hasn't granted before. The screen lists the scopes with their display names and descriptions. Casdoor remembers the grant, and users and administrators can revoke it on the Consents page.
Allow dynamic client registration
MCP clients such as Claude Desktop register themselves on first use. To allow this:
- Open the edit page of the organization in which the clients register. Without an
organizationparameter in the registration URL, this isbuilt-in. - Turn on Enable dynamic client registration and save.
Anyone can then register a client through /api/oauth/register. See Register clients dynamically.
Check the discovery URLs
Your MCP server uses the following URLs of Casdoor:
| URL | Purpose |
|---|---|
https://your-casdoor.com/.well-known/oauth-authorization-server | Authorization Server Metadata |
https://your-casdoor.com/.well-known/openid-configuration | OpenID Connect Discovery |
https://your-casdoor.com/.well-known/jwks | Public keys for token validation |
Check that they respond:
# Check OAuth metadata
curl https://your-casdoor.com/.well-known/oauth-authorization-server
# Check OIDC discovery
curl https://your-casdoor.com/.well-known/openid-configuration
# Check JWKS (JSON Web Key Set for token validation)
curl https://your-casdoor.com/.well-known/jwks
Configure the MCP server
Publish Protected Resource Metadata
Serve the following JSON document at GET /.well-known/oauth-protected-resource:
{
"resource": "https://your-mcp-server.com",
"authorization_servers": ["https://your-casdoor.com"],
"scopes_supported": [
"files:read",
"files:write",
"files:list"
],
"bearer_methods_supported": ["header"]
}
| Field | Value |
|---|---|
resource | Public URL of your MCP server. Tokens for your server carry it in the aud claim |
authorization_servers | The URL of Casdoor |
scopes_supported | The scopes that you defined in Casdoor |
bearer_methods_supported | ["header"]. Clients send the token in the Authorization header |
In Python with Flask:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/.well-known/oauth-protected-resource')
def protected_resource_metadata():
return jsonify({
"resource": "https://your-mcp-server.com",
"authorization_servers": ["https://your-casdoor.com"],
"scopes_supported": ["files:read", "files:write", "files:list"],
"bearer_methods_supported": ["header"]
})
In Node.js with Express:
const express = require('express');
const app = express();
app.get('/.well-known/oauth-protected-resource', (req, res) => {
res.json({
resource: 'https://your-mcp-server.com',
authorization_servers: ['https://your-casdoor.com'],
scopes_supported: ['files:read', 'files:write', 'files:list'],
bearer_methods_supported: ['header']
});
});
In Go with net/http:
package main
import (
"encoding/json"
"net/http"
)
type ProtectedResourceMetadata struct {
Resource string `json:"resource"`
AuthorizationServers []string `json:"authorization_servers"`
ScopesSupported []string `json:"scopes_supported"`
BearerMethodsSupported []string `json:"bearer_methods_supported"`
}
func protectedResourceHandler(w http.ResponseWriter, r *http.Request) {
metadata := ProtectedResourceMetadata{
Resource: "https://your-mcp-server.com",
AuthorizationServers: []string{"https://your-casdoor.com"},
ScopesSupported: []string{"files:read", "files:write", "files:list"},
BearerMethodsSupported: []string{"header"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(metadata)
}
func main() {
http.HandleFunc("/.well-known/oauth-protected-resource", protectedResourceHandler)
http.ListenAndServe(":8080", nil)
}
Challenge unauthenticated requests
When a request has no valid token, answer with HTTP 401 and a WWW-Authenticate header whose resource_metadata parameter points to your metadata document:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="MCP Server", resource_metadata="https://your-mcp-server.com/.well-known/oauth-protected-resource"
Content-Type: application/json
With a body such as:
{
"error": "unauthorized",
"message": "Authentication required. Use OAuth 2.0 with the authorization server listed in the Protected Resource Metadata."
}