MCP servers can require OAuth 2.1 tokens. Choose between DAuth (Dedalus Auth) for managed authentication with credential isolation, or bring your own authorization server.
DAuth (Dedalus Auth)
DAuth is Dedalus’s managed authorization system. It provides OAuth 2.1 token issuance with a key security property: credentials never leave a sealed execution boundary .
Why DAuth?
Traditional credential handling exposes secrets to your application code. DAuth isolates credentials in a secure enclave—your MCP server receives an opaque connection handle, not raw API keys.
Credentials never exposed — Encrypted client-side, decrypted only in a sealed execution boundary
Opaque handles — Your code references connections by handle, never sees raw secrets
Sender-constrained tokens — Tokens are cryptographically bound to the client; stolen tokens are unusable
Networkless execution — Credential decryption and API calls happen entirely within an isolated enclave; raw secrets never traverse the network
How DAuth Works Learn how credential isolation and sealed execution protect your secrets.
Quick Start
from dedalus_mcp import MCPServer
from dedalus_mcp.server import AuthorizationConfig
server = MCPServer(
"protected-server" ,
authorization = AuthorizationConfig(
enabled = True ,
required_scopes = [ "read" ],
),
)
By default, authorization_servers points to https://as.dedaluslabs.ai (the DAuth control plane).
Unauthenticated requests get 401 with a WWW-Authenticate challenge pointing to the protected resource metadata.
Server-level Scopes
All requests must have these scopes:
authorization = AuthorizationConfig(
enabled = True ,
required_scopes = [ "read" , "write" ],
)
Gate sensitive tools with additional scope requirements:
from dedalus_mcp import tool
@tool ( description = "List files" )
def list_files (path: str ) -> list[ str ]:
return os.listdir(path) # No extra scopes needed
@tool ( description = "Delete file" , required_scopes = [ "files:delete" ])
def delete_file (path: str ) -> dict :
os.remove(path)
return { "deleted" : path}
A token with read can call list_files. Calling delete_file without files:delete returns an error:
{
"isError" : true ,
"content" : [{
"type" : "text" ,
"text" : "Tool \" delete_file \" requires scopes: ['files:delete']. Missing: ['files:delete']"
}]
}
Configuration Options
AuthorizationConfig(
enabled = True ,
authorization_servers = [ "https://as.dedaluslabs.ai" ], # DAuth (default)
required_scopes = [ "read" ],
metadata_path = "/.well-known/oauth-protected-resource" ,
cache_ttl = 300 ,
fail_open = False ,
)
Option Default Description enabledFalseEnable authorization enforcement authorization_servers["https://as.dedaluslabs.ai"]DAuth or custom OAuth AS URLs required_scopes[]Scopes required for all requests metadata_path/.well-known/oauth-protected-resourcePRM endpoint path cache_ttl300Cache duration for metadata fail_openFalseAllow requests when validation fails
Inspect the authenticated user in your tools:
from dedalus_mcp import tool, get_context
@tool ( description = "Get current user" )
def whoami () -> dict :
ctx = get_context()
auth = ctx.auth # AuthorizationContext or None
if auth is None :
return { "user" : "anonymous" }
return {
"subject" : auth.subject,
"scopes" : auth.scopes,
"claims" : auth.claims,
}
DPoP Support
DAuth uses DPoP (Demonstrating Proof-of-Possession) by default. Tokens are cryptographically bound to the client’s key—even if a token is stolen, it’s useless without the corresponding private key.
server = MCPServer(
"dpop-server" ,
authorization = AuthorizationConfig(
enabled = True ,
dpop_required = True ,
),
)
External Authorization Servers
Use your own OAuth 2.1 provider instead of DAuth. This is useful when integrating with existing identity infrastructure.
External authorization servers don’t provide the sealed execution model. Your MCP server will handle credentials directly.
Custom Provider
from dedalus_mcp.server import AuthorizationConfig, JWTValidatorConfig, JWTValidator
server = MCPServer(
"my-server" ,
authorization = AuthorizationConfig(
enabled = True ,
authorization_servers = [ "https://auth.mycompany.com" ],
required_scopes = [ "api:access" ],
),
)
# Configure JWT validation for your provider
jwt_config = JWTValidatorConfig(
jwks_uri = "https://auth.mycompany.com/.well-known/jwks.json" ,
issuer = "https://auth.mycompany.com" ,
audience = "https://my-mcp-server.example.com" ,
)
server.set_authorization_provider(JWTValidator(jwt_config))
Auth0
jwt_config = JWTValidatorConfig(
jwks_uri = "https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json" ,
issuer = "https://YOUR_DOMAIN.auth0.com/" ,
audience = "https://my-mcp-api" ,
)
Okta
jwt_config = JWTValidatorConfig(
jwks_uri = "https://YOUR_DOMAIN.okta.com/oauth2/default/v1/keys" ,
issuer = "https://YOUR_DOMAIN.okta.com/oauth2/default" ,
audience = "api://my-mcp-server" ,
)
Keycloak
jwt_config = JWTValidatorConfig(
jwks_uri = "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs" ,
issuer = "https://keycloak.example.com/realms/myrealm" ,
audience = "my-mcp-client" ,
)
How It Works
Client calls server without token
Server returns 401 with metadata URL
Client fetches protected resource metadata
Client runs OAuth flow with authorization server
Client retries with bearer token
Server validates JWT and processes request
When authorization is enabled, the server exposes RFC 9728 metadata:
curl https://my-server.example.com/.well-known/oauth-protected-resource
{
"resource" : "https://my-server.example.com" ,
"authorization_servers" : [ "https://as.dedaluslabs.ai" ],
"scopes_supported" : [ "read" , "write" ]
}
Testing
Test authorization with the full server and client:
import pytest
from dedalus_mcp import MCPServer, tool
from dedalus_mcp.server import AuthorizationConfig
from dedalus_mcp.client import MCPClient, BearerAuth
@tool ( description = "Delete file" , required_scopes = [ "files:delete" ])
def delete_file (path: str ) -> dict :
return { "deleted" : path}
@pytest.fixture
async def protected_server ():
server = MCPServer(
"test" ,
authorization = AuthorizationConfig( enabled = True , required_scopes = [ "read" ]),
)
server.collect(delete_file)
task = asyncio.create_task(server.serve())
await asyncio.sleep( 0.1 )
yield server
task.cancel()
async def test_with_valid_token (protected_server):
# Use a test token with required scopes
client = await MCPClient.connect(
"http://127.0.0.1:8000/mcp" ,
auth = BearerAuth( access_token = "test-token-with-scopes" )
)
result = await client.call_tool( "delete_file" , { "path" : "/tmp/test.txt" })
await client.close()