`AuthdogVerifier` from the `authdog_mcp` package verifies the bearer token an MCP client presents to your Python MCP server. It calls `GET {base_url}/v1/userinfo` with the token and returns an `AuthInfo` dataclass. Source is [`python/`](https://github.com/authdog/mcp-sdk/tree/main/python) in [authdog/mcp-sdk](https://github.com/authdog/mcp-sdk). It depends on `requests>=2.28.0`; `setup.py` declares `python_requires>=3.8`.

## Availability

This package is source-available in [github.com/authdog/mcp-sdk](https://github.com/authdog/mcp-sdk) and may not resolve from registries yet. At the time of writing `authdog-mcp` returns 404 on PyPI, and the repository has no release tags. Version is `0.1.0`. Install from source until a release is published.

## Install

From source:

```bash
pip install "git+https://github.com/authdog/mcp-sdk.git#subdirectory=python"
```

Or from a local clone:

```bash
git clone https://github.com/authdog/mcp-sdk.git
pip install ./mcp-sdk/python
```

Once a release is published, the README gives:

```bash
pip install authdog-mcp
```

## Configure

```python
from authdog_mcp import AuthdogVerifier

verifier = AuthdogVerifier(base_url="https://api.authdog.com", timeout=10.0)
```

| Argument | Type | Default | Behavior |
| --- | --- | --- | --- |
| `base_url` | `str` | required | Authdog API base URL. Trailing slashes are stripped. Not validated for emptiness. |
| `timeout` | `float` | `10.0` | Request timeout in seconds, passed to `requests.get`. |

Each request sends `Authorization: Bearer <token>` and `User-Agent: authdog-mcp-python-sdk/0.1.0`.

## Verify a token

```python
from authdog_mcp import AuthdogVerifier, AuthenticationError, APIError

verifier = AuthdogVerifier(base_url="https://api.authdog.com")

try:
    info = verifier.verify_token("incoming-access-token")
    print(f"Subject:  {info.subject}")
    print(f"Username: {info.username}")
    print(f"Email:    {info.email}")
    print(f"Expires:  {info.expires_at}")
except AuthenticationError as e:
    print(f"Authentication failed: {e}")
except APIError as e:
    print(f"API error: {e}")
```

`verify_token(access_token: str) -> AuthInfo` is synchronous and blocking (it uses `requests`). It does not pre-check for an empty token; an empty string is sent and the server decides.

`AuthInfo` is a dataclass:

| Field | Type | Value |
| --- | --- | --- |
| `token` | `str` | The token you passed in |
| `subject` | `str` | `user.id` |
| `username` | `str` | `user.userName` |
| `email` | `Optional[str]` | First non-empty `emails[].value`, or `None` |
| `scopes` | `List[str]` | Always `[]` |
| `expires_at` | `Optional[datetime]` | UTC time, now plus `session.remainingSeconds`; `None` only when that key is absent or null |
| `user` | `dict` | Raw `user` object from the response |

`scopes` is empty because the `userinfo` endpoint does not return scopes; the SDK hard-codes `[]`. Do not use it for scope-based authorization. Authentication identifies the caller; apply [authorization](/docs/concepts/authorization) separately.

If a 2xx response has no `user`, the SDK does not raise: `subject` and `username` are `None`. Check `info.subject` before trusting the result if you need that guarantee.

## Wire into an MCP server

The [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) authenticates requests through a `TokenVerifier`. Adapt `AuthdogVerifier` to it:

```python
from mcp.server.auth.provider import AccessToken, TokenVerifier
from authdog_mcp import AuthdogVerifier, AuthdogError

authdog = AuthdogVerifier(base_url="https://api.authdog.com")


class AuthdogTokenVerifier(TokenVerifier):
    async def verify_token(self, token: str) -> AccessToken | None:
        try:
            info = authdog.verify_token(token)
        except AuthdogError:
            return None  # invalid token or upstream failure -> unauthenticated

        return AccessToken(
            token=info.token,
            client_id=info.subject,
            scopes=info.scopes,
            expires_at=(
                int(info.expires_at.timestamp()) if info.expires_at else None
            ),
        )


# Pass the verifier to your FastMCP / MCP server's auth configuration.
```

Two notes on this example. The `AccessToken | None` annotation needs Python 3.10 or newer (or `from __future__ import annotations`), even though the SDK itself declares 3.8. And `authdog.verify_token` blocks the event loop inside an `async def`; for high concurrency, run it with `asyncio.to_thread`.

## Errors

Exported from `authdog_mcp`, all extending `Exception`:

| Class | Raised when |
| --- | --- |
| `AuthdogError` | Base class for the others |
| `AuthenticationError` | HTTP 401 (`Unauthorized - invalid or expired token`) |
| `APIError` | `requests` failure or timeout (`Request failed: ...`), other non-2xx (`HTTP error <status>: <body>`), or a non-JSON body (`Invalid JSON response: ...`) |

## Related

- [MCP SDK](/docs/mcp-sdk): language overview
- [MCP authentication](/docs/mcp-authentication): how MCP clients obtain tokens
- [Node.js](/docs/mcp-sdk/node) and [Go](/docs/mcp-sdk/go) verifiers
