Skip to main content
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"],
)

Per-tool Scopes

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,
)
OptionDefaultDescription
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

Access Claims in Tools

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

  1. Client calls server without token
  2. Server returns 401 with metadata URL
  3. Client fetches protected resource metadata
  4. Client runs OAuth flow with authorization server
  5. Client retries with bearer token
  6. Server validates JWT and processes request

Protected Resource Metadata

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()