MCP Authentication
OAuth 2.1, OIDC Discovery, and API Key authentication per MCP 2025-11-25
MCP Authentication
PraisonAI MCP Server supports multiple authentication methods per the MCP 2025-11-25 specification, including OAuth 2.1 with PKCE, OpenID Connect Discovery, and API Key authentication.
Protocol Version
This feature implements MCP Protocol Version 2025-11-25.
Authentication Methods
| Method | Description | Use Case |
|---|---|---|
| OAuth 2.1 | Full OAuth flow with PKCE | Web applications, user auth |
| OIDC Discovery | Auto-discover OAuth endpoints | Enterprise SSO |
| API Key | Simple bearer token | CLI tools, scripts |
Python API
OIDC Discovery
import asyncio
from praisonai.mcp_server.auth.oidc import OIDCDiscovery
async def main():
discovery = OIDCDiscovery()
# Discover from any OIDC provider
config = await discovery.discover("https://accounts.google.com")
if config:
print(f"Issuer: {config.issuer}")
print(f"Auth endpoint: {config.authorization_endpoint}")
print(f"Token endpoint: {config.token_endpoint}")
print(f"JWKS URI: {config.jwks_uri}")
asyncio.run(main())OAuth 2.1 with PKCE
from praisonai.mcp_server.auth.oauth import OAuthConfig, OAuthManager
# Configure OAuth
config = OAuthConfig(
authorization_endpoint="https://auth.example.com/authorize",
token_endpoint="https://auth.example.com/token",
client_id="my-mcp-client",
default_scopes=["openid", "profile", "tools:read"],
use_pkce=True, # Required for OAuth 2.1
)
oauth = OAuthManager(config)
# Create authorization URL with PKCE
auth_url, auth_request = oauth.create_authorization_url(
scopes=["openid", "profile", "tools:call"],
)
print(f"Auth URL: {auth_url}")
print(f"State: {auth_request.state}")
print(f"Code verifier: {auth_request.code_verifier}")
print(f"Code challenge: {auth_request.code_challenge}")
# After user authorizes, exchange code for token
# token = await oauth.exchange_code(code, auth_request)API Key Authentication
from praisonai.mcp_server.auth.api_key import APIKeyAuth
api_key_auth = APIKeyAuth()
# Generate a new API key
raw_key, api_key = api_key_auth.generate_key(
name="my-api-key",
scopes=["tools:read", "tools:call", "resources:read"],
)
print(f"Key: {raw_key}") # mcp_xxx... (store securely!)
print(f"Key ID: {api_key.key_id}")
print(f"Scopes: {api_key.scopes}")
# Validate a key
is_valid, validated_key = api_key_auth.validate(raw_key)
print(f"Valid: {is_valid}")
# Validate from Authorization header
is_valid, key = api_key_auth.validate_header("Bearer mcp_xxx...")Scope Management
from praisonai.mcp_server.auth.scopes import ScopeManager
scope_manager = ScopeManager()
# Check if required scopes are granted
required = ["tools:read"]
granted = ["tools:call"] # tools:call implies tools:read
is_valid, challenge = scope_manager.validate_scopes(required, granted)
print(f"Valid: {is_valid}")
# Expand scopes (show implied scopes)
expanded = scope_manager.expand_scopes(["tools:call"])
print(f"Expanded: {expanded}") # {'tools:call', 'tools:read'}
# Admin scope expands to all
admin_expanded = scope_manager.expand_scopes(["admin"])
print(f"Admin expands to: {list(admin_expanded)[:5]}...")CLI Usage
Start Server with API Key
praisonai mcp serve --transport http-stream --api-key YOUR_SECRET_KEYGenerate API Key
praisonai mcp auth generate-key --name "my-key" --scopes "tools:read,tools:call"Validate API Key
praisonai mcp auth validate-key YOUR_KEYScoped API keys via --keys-file
Give each caller a least-privilege key. praisonai mcp serve --keys-file <path> loads a JSON array of per-key scopes and enforces them per JSON-RPC method.
praisonai mcp serve --transport http-stream --keys-file ./keys.jsonA minimal keys file:
[
{ "key": "read-only-monitor", "name": "monitor", "scopes": ["tools:read"] },
{ "key": "runner", "name": "ci-runner", "scopes": ["tools:read", "tools:call"] },
{ "key": "admin-key", "name": "ops", "scopes": ["*"] }
]- A key whose
scopesfield is omitted defaults to wildcard (["*"]). - An explicit empty list (
"scopes": []) is honored as a no-permission key — it is not widened to wildcard. - A
"*"grant satisfies every requirement.
Operation → scope map
Each JSON-RPC method maps to a required scope (OPERATION_SCOPES):
| JSON-RPC method | Required scope |
|---|---|
tools/list | tools:read |
tools/call | tools:call |
resources/list | resources:read |
resources/read | resources:read |
resources/subscribe | resources:subscribe |
prompts/list | prompts:read |
prompts/get | prompts:read |
sampling/createMessage | sampling:create |
tasks/create | tasks:write |
tasks/get | tasks:read |
tasks/list | tasks:read |
tasks/cancel | tasks:write |
Insufficient scope response
When a caller’s key lacks the required scope, the server returns JSON-RPC error -32001 insufficient_scope, and (over HTTP) a WWW-Authenticate challenge carrying the missing scope:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32001,
"message": "insufficient_scope",
"data": {
"required_scopes": ["tools:call"],
"granted_scopes": ["tools:read"],
"missing_scopes": ["tools:call"],
"error": "insufficient_scope"
}
}
}WWW-Authenticate: Bearer scope="tools:call", error="insufficient_scope"One shared key or per-team scopes?
Available Scopes
| Scope | Description | Implies |
|---|---|---|
tools:read | List and describe tools | – |
tools:call | Execute tools | tools:read |
resources:read | Read resources | – |
resources:subscribe | Subscribe to resource updates | resources:read |
prompts:read | List and get prompts | – |
prompts:execute | Execute prompts | prompts:read |
tasks:read | View tasks | – |
tasks:write | Create/cancel tasks | tasks:read |
admin | Full access | All scopes |
WWW-Authenticate Challenge
challenge = oauth.create_www_authenticate_challenge(
required_scopes=["admin"],
error="insufficient_scope",
error_description="Admin access required",
)
# Bearer scope="admin", error="insufficient_scope", error_description="Admin access required"Security Best Practices
- Always use PKCE – Required for OAuth 2.1
- Validate origins – Use
--allowed-originsfor HTTP transport - Rotate API keys – Regularly rotate keys
- Minimal scopes – Request only needed scopes
- Secure storage – Never commit keys to version control
Environment Variables
# OAuth configuration
export OAUTH_CLIENT_ID=your_client_id
export OAUTH_CLIENT_SECRET=your_client_secret # Optional for public clients
export OIDC_ISSUER_URL=https://auth.example.com
# API key (for server)
export MCP_API_KEY=your_api_keyRelated
- MCP Elicitation – URL mode for OAuth flows
- PraisonAI MCP Server – Full documentation
- MCP Tasks API – Protected operations