The Rust MCP SDK (`authdog-mcp`) is a bearer-token verifier for [Model Context Protocol](https://modelcontextprotocol.io) servers. `AuthdogVerifier` takes the access token an MCP client presents, calls Authdog `GET /v1/userinfo` with it, and returns an `AuthInfo` identity you use to authorize the request. It is async and built on `tokio` and `reqwest`.

## Availability

`authdog-mcp` is source-available in [github.com/authdog/mcp-sdk](https://github.com/authdog/mcp-sdk) (`rust/`). Publication on crates.io is unverified, so `cargo add authdog-mcp` may not resolve yet. Until it does, depend on a local checkout.

## Install

Clone the repository and point a path dependency at the `rust/` directory:

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

```toml
[dependencies]
authdog-mcp = { path = "../mcp-sdk/rust" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```

The crate is version `0.1.0`, edition 2021, MIT licensed. Its dependencies are `reqwest` (with `json`), `serde`, `serde_json`, `tokio`, and `chrono`.

## Configure

`VerifierConfig` has two fields:

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `base_url` | `String` | `https://api.authdog.com` | Base URL of the Authdog API. A trailing `/` is trimmed. |
| `timeout` | `Duration` | 10 seconds | Request timeout. |

```rust
use authdog_mcp::{AuthdogVerifier, VerifierConfig};
use std::time::Duration;

let verifier = AuthdogVerifier::new(VerifierConfig {
    base_url: "https://api.authdog.com".to_string(),
    timeout: Duration::from_secs(10),
});

// or use the defaults
let verifier = AuthdogVerifier::new(VerifierConfig::default());
```

Construct the verifier once and share it, for example behind `Arc`. It holds a pooled HTTP client and sends `User-Agent: authdog-mcp-rust-sdk/0.1.0` on every request.

## Verify a token

`verify_token` sends `GET {base_url}/v1/userinfo` with `Authorization: Bearer <access_token>`:

```rust
match verifier.verify_token("incoming-access-token").await {
    Ok(auth) => {
        println!("subject: {}", auth.subject);
        println!("username: {}", auth.username);
        if let Some(email) = &auth.email {
            println!("email: {}", email);
        }
        if let Some(expires_at) = auth.expires_at {
            println!("expires at: {}", expires_at);
        }
    }
    Err(e) => eprintln!("verification failed: {}", e),
}
```

`AuthInfo` is mapped from the userinfo response:

| Field | Type | Source |
| --- | --- | --- |
| `token` | `String` | The access token that was verified. |
| `subject` | `String` | `user.id` |
| `username` | `String` | `user.userName` |
| `email` | `Option<String>` | First non-empty `user.emails[].value`, or `None`. |
| `scopes` | `Vec<String>` | Always empty. Userinfo does not expose scopes yet. |
| `expires_at` | `Option<DateTime<Utc>>` | Now plus `session.remainingSeconds`. |
| `user` | `User` | The full user object from userinfo. |

## Wire into an MCP server

An HTTP MCP server receives the caller's bearer token in the `Authorization` header. Strip the prefix, verify on each request, and use the returned `AuthInfo` to authorize the call:

```rust
use authdog_mcp::{AuthdogError, AuthInfo, AuthdogVerifier};

async fn authenticate(
    verifier: &AuthdogVerifier,
    authorization_header: Option<&str>,
) -> Result<AuthInfo, AuthdogError> {
    let token = authorization_header
        .and_then(|h| h.strip_prefix("Bearer "))
        .ok_or_else(|| AuthdogError::new("missing bearer token".to_string()))?;

    verifier.verify_token(token).await
}
```

The crate does not ship framework middleware. Call this from your own handler or middleware and map failures to your server's unauthorized response.

## Errors

`verify_token` returns `Result<AuthInfo, AuthdogError>`. `AuthenticationError` and `APIError` each convert into `AuthdogError` via `From`, and the conversion keeps only the message. The returned `AuthdogError` therefore carries no variant: tell a rejected token from an upstream failure by message, not by type.

| Condition | Message |
| --- | --- |
| HTTP 401 | `Unauthorized - invalid or expired token` |
| Other non-2xx response | `HTTP error <status>: <body>` |
| Transport failure, including timeout | `Request failed: <cause>` |
| Success response that is not valid JSON for the userinfo shape | `Invalid JSON response: <cause>` |

The three types (`AuthdogError`, `AuthenticationError`, `APIError`) implement `std::error::Error` and `Display`. `AuthdogError::new(String)` is public.

If the HTTP client cannot be built with your timeout, the verifier falls back to a default `reqwest` client. In that case the configured timeout does not apply.

## Related

- [MCP SDK](/docs/mcp-sdk)
- [MCP authentication](/docs/mcp-authentication)
