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

> Zero-config OAuth 2.1 for remote MCP servers — no client_id required

OAuth-protected remote MCP servers now Just Work — point at the URL, approve once in your browser, and tokens are cached and refreshed automatically.

> [!NOTE]
>
> No `client_id` is required. When a server exposes standard authorization-server metadata (RFC 8414) and dynamic client registration (RFC 7591), PraisonAI discovers the endpoints, registers a client, runs the PKCE flow, and refreshes tokens on its own.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Zero-config OAuth"
        A[🔗 MCP URL] --> B[🔎 Discover + Register]
        B --> C[🔐 Authorize + Token]
        C --> D[✅ Connected]
    end

    classDef url fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef discover fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef auth fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef done fill:#10B981,stroke:#7C90A0,color:#fff

    class A url
    class B discover
    class C auth
    class D done
```

## Status

| Capability | Status |
| - | - |
| OAuth utilities (PKCE, state, callback) | ✅ |
| Token storage (`~/.praisonai/mcp-auth.json`, 0600 perms) | ✅ |
| Metadata discovery (RFC 8414 + OpenID fallback) | ✅ |
| Dynamic Client Registration (RFC 7591) | ✅ |
| PKCE authorize + token exchange | ✅ |
| Transparent token refresh | ✅ |
| Headless / CI signal (`InteractiveAuthRequired`) | ✅ |
| `praisonai mcp auth` CLI flow | ✅ |

## Quick Start

**Step 1**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, MCP

agent = Agent(
    instructions="You help with GitHub tasks",
    tools=MCP("https://api.githubcopilot.com/mcp/")
)

agent.start("List my open pull requests")
```

The first call opens your browser once so you can approve access. Tokens are cached for future runs — no `client_id` or scopes to configure.

**Step 2**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
agent.start("Close pull request 42")
```

Cached tokens are re-used, and expired ones are refreshed transparently.

**3. Or pre-seed from the CLI**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# One-time interactive login; tokens cached at ~/.praisonai/mcp-auth.json
praisonai mcp auth github
```

Run this once on a workstation, then copy `~/.praisonai/mcp-auth.json` to a CI runner for headless environments.

## How Zero-config OAuth Works

A `401` with a `WWW-Authenticate` challenge triggers discovery, registration, and the PKCE flow — all automatic.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant MCP as MCP Server
    participant Auth as Auth Server
    participant Browser

    User->>Agent: request
    Agent->>MCP: call
    MCP-->>Agent: 401 + WWW-Authenticate: Bearer resource_metadata="…"
    Agent->>Auth: GET /.well-known/oauth-protected-resource
    Agent->>Auth: GET /.well-known/oauth-authorization-server
    Agent->>Auth: POST /register (RFC 7591, if no client stored)
    Agent->>Browser: open authorize URL with PKCE
    User->>Auth: sign in and approve
    Auth-->>Agent: code (loopback callback)
    Agent->>Auth: POST /token (code + verifier)
    Agent->>Agent: persist tokens (0600)
    Agent->>MCP: retry with Bearer token
    MCP-->>Agent: 200

    %%{init: {'theme':'base','themeVariables':{'actorBkg':'#6366F1','actorTextColor':'#fff','actorBorder':'#7C90A0'}}}%%
```

| Step | What happens |
| - | - |
| Discovery | Parses the `WWW-Authenticate` challenge, fetches protected-resource + authorization-server metadata (RFC 8414, OpenID fallback). |
| Registration | Registers a public client via RFC 7591 if none is stored. |
| Authorize | Opens the browser with PKCE and a loopback callback. |
| Token | Exchanges the code for tokens and persists them (0600). |
| Refresh | Renews expired tokens automatically on the next request. |

## Interactive vs Headless

Interactive sessions open a browser; headless environments raise a single actionable exception.

> [tab] Interactive (default)
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents.mcp import MCPOAuthProvider
>
> provider = MCPOAuthProvider(
>     mcp_name="github",
>     server_url="https://api.githubcopilot.com/mcp/",
> )
> token = provider.ensure_authenticated()  # opens a browser the first time
> ```

> [tab] Headless / CI
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents.mcp import MCPOAuthProvider, InteractiveAuthRequired
>
> provider = MCPOAuthProvider(
>     mcp_name="github",
>     server_url="https://api.githubcopilot.com/mcp/",
>     open_browser=False,
> )
>
> try:
>     token = provider.ensure_authenticated()
> except InteractiveAuthRequired:
>     print("Seed ~/.praisonai/mcp-auth.json interactively first.")
> ```

> [!TIP]
>
> For CI, run `praisonai mcp auth <server-name>` once interactively on a workstation to seed `~/.praisonai/mcp-auth.json`, then copy that file to the CI runner (or mount it as a secret). The CLI runs the exact same OAuth 2.1 flow as the Python client, so it's a valid way to satisfy the `InteractiveAuthRequired` prerequisite. With `open_browser=False`, a missing token raises `InteractiveAuthRequired` instead of hanging.

## Configuration Schema

The `oauth:` block is now **optional**. For servers that advertise standard metadata, just supply `type: remote` and `url`.

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
mcp:
  servers:
    github:
      type: remote
      url: https://api.githubcopilot.com/mcp/
```

Keep the explicit block only as an escape hatch for servers without RFC 8414 metadata or dynamic registration:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
mcp:
  servers:
    my-server:
      type: remote
      url: https://mcp.example.com/mcp
      enabled: true
      timeout: 30000  # milliseconds
      oauth:
        client_id: your_client_id
        client_secret: your_client_secret  # optional
        scopes:
          - read
          - write
```

### Remote Server with API Key

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
mcp:
  servers:
    tavily:
      type: remote
      url: https://mcp.tavily.com/mcp
      headers:
        Authorization: Bearer ${TAVILY_API_KEY}
```

## Python SDK

All OAuth helpers are lazy-loaded from `praisonaiagents.mcp`.

### MCPOAuthProvider

Orchestrates discovery, registration, the PKCE flow, and refresh for one server.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.mcp import MCPOAuthProvider

provider = MCPOAuthProvider(
    mcp_name="github",
    server_url="https://api.githubcopilot.com/mcp/",
)
token = provider.ensure_authenticated()  # opens a browser the first time
```

| Parameter | Type | Default | Description |
| - | - | - | - |
| `mcp_name` | `str` | — | Stable identifier used as the storage key. |
| `server_url` | `str` | — | The MCP server URL. |
| `storage` | `Any` | `None` | Optional pre-built `MCPAuthStorage` (created lazily otherwise). |
| `open_browser` | `bool` | `True` | Set `False` for headless/CI to raise `InteractiveAuthRequired`. |

| Method | Signature | Returns |
| - | - | - |
| `get_valid_token` | `get_valid_token()` | `str \| None` — a non-expired token, refreshing if possible. |
| `ensure_authenticated` | `ensure_authenticated(www_authenticate=None, timeout=10.0)` | `str` — a valid bearer token, running the full flow if needed. |

### Discovery and Registration Helpers

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.mcp import (
    discover_oauth_metadata,
    parse_www_authenticate,
    register_client,
)

challenge = parse_www_authenticate(
    'Bearer resource_metadata="https://auth.example.com/.well-known/oauth-protected-resource"'
)

metadata = discover_oauth_metadata("https://api.githubcopilot.com/mcp/")
# {authorization_endpoint, token_endpoint, registration_endpoint, scopes_supported, resource}
```

| Function | Signature | Returns |
| - | - | - |
| `discover_oauth_metadata` | `discover_oauth_metadata(server_url, www_authenticate=None, timeout=10.0)` | `dict \| None` |
| `parse_www_authenticate` | `parse_www_authenticate(header)` | `dict` — `resource_metadata`, `scope`, `error`, etc. |
| `register_client` | `register_client(registration_endpoint, redirect_uris, client_name="PraisonAI Agent", scope=None, timeout=10.0)` | `dict` — at least `{"client_id": …}` (RFC 7591). |

### InteractiveAuthRequired

Raised by `ensure_authenticated()` when interactive auth is required but `open_browser=False`.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.mcp import InteractiveAuthRequired
```

### Auth Storage

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.mcp import MCPAuthStorage

storage = MCPAuthStorage()

entry = storage.get("github")
if entry and entry.get("tokens"):
    print("Authenticated!")

if storage.is_token_expired("github"):
    print("Token expired, need to re-authenticate")
```

### PKCE Utilities

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.mcp import (
    generate_state,
    generate_code_verifier,
    generate_code_challenge,
    get_redirect_url,
)

state = generate_state()
verifier = generate_code_verifier()
challenge = generate_code_challenge(verifier)

redirect_url = get_redirect_url()
print(f"Redirect URL: {redirect_url}")
# Output: http://127.0.0.1:19876/mcp/oauth/callback
```

### OAuth Callback Handler

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.mcp import OAuthCallbackHandler, generate_state
import webbrowser

handler = OAuthCallbackHandler()
state = generate_state()

auth_url = f"https://auth.example.com/authorize?state={state}&..."
webbrowser.open(auth_url)

try:
    code = handler.wait_for_callback(state, timeout=300)
    print(f"Received authorization code: {code[:20]}...")
except TimeoutError:
    print("OAuth flow timed out")
```

## CLI Commands

### Authenticate

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai mcp auth <server-name>
```

Runs the same OAuth 2.1 authorization-code flow (with PKCE) as the Python client. Use it to seed tokens ahead of time — for example, on a workstation before running your agent in CI.

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

The command:

* Opens your browser and waits for the loopback callback.
* Exchanges the code at the discovered `token_endpoint`.
* Persists tokens (with `expires_at`) to `~/.praisonai/mcp-auth.json` (0600 permissions).
* Clears any legacy placeholder entry (`oauth_<...>...`) from a previous CLI run before starting, so a prior failed login recovers automatically — no manual cleanup.
* Reads `server.oauth.client_id`/`client_secret` from `~/.praisonai/config.toml` for servers without dynamic client registration, seeding them into storage before the flow runs.
* Honors `--timeout` (default `300`s).

On success, tokens are written with `access_token`, optional `refresh_token`, and `expires_at`. The Python client picks them up automatically on the next `Agent.start(...)`. On failure, the command exits `1`.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant CLI as praisonai mcp auth
    participant Browser
    participant Auth as Auth Server
    participant Storage as ~/.praisonai/mcp-auth.json

    User->>CLI: praisonai mcp auth github
    CLI->>Storage: purge legacy placeholder if present
    CLI->>Auth: discover metadata (RFC 8414 / OpenID)
    CLI->>Auth: register client if needed (RFC 7591)
    CLI->>Browser: open authorize URL (PKCE + state)
    User->>Auth: sign in and approve
    Auth-->>CLI: code (loopback callback)
    CLI->>Auth: POST /token (code + verifier)
    Auth-->>CLI: access_token, refresh_token, expires_at
    CLI->>Storage: persist (0600)
    CLI-->>User: ✅ Successfully authenticated with github

    %%{init: {'theme':'base','themeVariables':{'actorBkg':'#6366F1','actorTextColor':'#fff','actorBorder':'#7C90A0'}}}%%
```

> [!WARNING]
>
> If you previously ran `praisonai mcp auth <name>` on an older release and saw the success message but the server still rejected your token, you were hit by a bug where the CLI stored a truncated authorization code instead of exchanging it for a real token. Re-running the command on the current release automatically clears the bad entry and completes a real flow — no manual cleanup of `~/.praisonai/mcp-auth.json` is needed.

### Logout

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai mcp logout <server-name>
```

Removes stored OAuth credentials for a server. Use `--yes` to skip the confirmation prompt.

### List Servers

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai mcp list
```

Shows all configured servers with their type (local/remote) and status.

## Token Storage

OAuth tokens are stored in `~/.praisonai/mcp-auth.json` with secure file permissions (0600).

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "github": {
    "server_url": "https://api.githubcopilot.com/mcp/",
    "tokens": {
      "access_token": "gho_xxx...",
      "refresh_token": "ghr_xxx...",
      "expires_at": 1234567890,
      "token_endpoint": "https://auth.example.com/token",
      "scope": "repo user"
    },
    "client_info": {
      "client_id": "xxx"
    }
  }
}
```

## Security

* **HTTPS-only** — every metadata, authorize, token, and registration URL must be HTTPS (loopback `http://127.0.0.1`, `localhost`, `::1` still allowed for testing).
* **Internal-host guard** — on non-loopback hosts, every OAuth URL also passes the SDK's SSRF check (`is_safe_http_url`). An HTTPS endpoint on a private, link-local or cloud-metadata address (e.g. `https://169.254.169.254/`, `https://10.0.0.5/`, `https://[::ffff:169.254.169.254]/`, `https://0177.0.0.1/`) is refused before any request is sent — even when a compromised MCP server advertises it as `token_endpoint` or `registration_endpoint`.
* **Pre-request redirect validation** — discovery follows redirects by hand (max 3 hops) and validates every hop **before** fetching it, so a redirect to an internal or plaintext endpoint never reaches the network.
* **No redirects on credential POSTs** — code-exchange, refresh-token and registration POSTs never follow redirects, so a `307`/`308` can't re-send a refresh token or client secret to another host. A redirect is treated as a failure and saved tokens are preserved.
* **Refresh re-check** — refresh-token grants re-validate the stored `token_endpoint` over HTTPS (defence in depth).
* **Public client** — dynamic registration uses `token_endpoint_auth_method: "none"` and requests `grant_types: ["authorization_code", "refresh_token"]`.
* **PKCE** — all flows use PKCE (`S256`) plus a random `state` for CSRF protection.
* **File permissions** — token storage uses 0600 (owner read/write only).

## Troubleshooting

| Issue | Cause / fix |
| - | - |
| `RuntimeError: Could not discover OAuth metadata for …` | Server doesn't advertise RFC 8414 / OpenID metadata. Supply `oauth.client_id` and `oauth.scopes` in config as a fallback. |
| `RuntimeError: … does not support dynamic client registration` | No `registration_endpoint` in auth-server metadata. Register a client manually and supply `oauth.client_id`. |
| `InteractiveAuthRequired` | Running headless / in CI. Seed `~/.praisonai/mcp-auth.json` from an interactive session first. |
| `ValueError: Refusing OAuth request over insecure transport: …` | Auth server advertises `http://` endpoints. Refused by design — fix the server or use an HTTPS reverse proxy. |
| `ValueError: Refusing OAuth request to a private/internal address: …` | Auth server advertises an HTTPS endpoint on a private / RFC1918 / link-local host (e.g. cloud metadata `169.254.169.254`). Refused by design — fix the server. |
| `ValueError: Too many redirects fetching OAuth metadata: …` | Discovery hopped more than 3 times. Fix the server's redirect chain. |
| `ValueError: Dynamic client registration response missing 'client_id'` | Malformed response from the auth server's `registration_endpoint`. |

## Related

> [card] [Remote MCP](https://praison.ai/docs/mcp/mcp-remote.md)
>
> Connect to remote MCP servers via HTTP, SSE, or WebSocket.

> [card] [MCP Authentication](https://praison.ai/docs/mcp/mcp-auth.md)
>
> Server-side OAuth 2.1, OIDC, and API-key auth for `praisonai-mcp`.

> [card] [MCP Tools](https://praison.ai/docs/mcp/mcp-tools.md)
>
> Using MCP tools with agents.

> [card] [MCP Server](./praisonai-mcp)
>
> Deploy PraisonAI as an MCP server.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.