The Rust MCP SDK (authdog-mcp) is a bearer-token verifier for Model Context Protocol 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 (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:
git clone https://github.com/authdog/mcp-sdk.git[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. |
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>:
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:
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.