> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai mcp serve --transport http-stream --api-key YOUR_SECRET_KEY
```

### Generate API Key

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai mcp auth generate-key --name "my-key" --scopes "tools:read,tools:call"
```

### Validate API Key

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai mcp auth validate-key YOUR_KEY
```

## Scoped 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.

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai mcp serve --transport http-stream --keys-file ./keys.json
```

A minimal keys file:

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
[
  { "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 `scopes` field 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.

<Note>
  The scalar `--api-key` is wrapped internally as a **single wildcard-scoped key**, so `--api-key` and `--keys-file` share the exact same enforcement path.
</Note>

### 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:

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "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"
```

<Note>
  **Backward compatible.** When no auth is configured, `granted_scopes` is `None` and scope enforcement is a no-op ("allow all") — existing setups are unchanged.
</Note>

### One shared key or per-team scopes?

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{How many callers,<br/>what trust level?}:::agent
    Q -->|One shared, fully trusted| A["--api-key<br/>(wildcard)"]:::process
    Q -->|Per-team, least-privilege| B["--keys-file<br/>(scoped keys)"]:::config

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef config fill:#6366F1,stroke:#7C90A0,color:#fff
```

***

## 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

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

1. **Always use PKCE** - Required for OAuth 2.1
2. **Validate origins** - Use `--allowed-origins` for HTTP transport
3. **Rotate API keys** - Regularly rotate keys
4. **Minimal scopes** - Request only needed scopes
5. **Secure storage** - Never commit keys to version control

## Environment Variables

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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_key
```

## Related

* [MCP Elicitation](/docs/mcp/mcp-elicitation) - URL mode for OAuth flows
* [PraisonAI MCP Server](/docs/mcp/praisonai-mcp-server) - Full documentation
* [MCP Tasks API](/docs/mcp/mcp-tasks-api) - Protected operations
