Package authdogmcp verifies the bearer token an MCP client presents to your Go MCP server. Verifier.VerifyToken calls GET {BaseURL}/v1/userinfo with the token and returns an *AuthInfo. Source is `go/` in authdog/mcp-sdk. It needs Go 1.21+ and uses only the standard library.
This is the MCP token verifier. For the management and userinfo client, see the Go SDK.
Availability
This module is source-available in github.com/authdog/mcp-sdk and may not resolve from the Go module proxy yet. The repository has no release tags, so go get cannot select a version. Use a replace directive against a local checkout until a release is published.
Install
The module path is github.com/authdog/mcp-sdk/go (the module lives in the go/ subdirectory). From source:
git clone https://github.com/authdog/mcp-sdk.gitIn your go.mod:
require github.com/authdog/mcp-sdk/go v0.0.0
replace github.com/authdog/mcp-sdk/go => ../mcp-sdk/goOnce a release is published, the README gives:
go get github.com/authdog/mcp-sdk/goimport authdogmcp "github.com/authdog/mcp-sdk/go"Configure
verifier := authdogmcp.NewVerifier(authdogmcp.VerifierConfig{
BaseURL: "https://api.authdog.com",
Timeout: 10 * time.Second,
})| Field | Type | Default | Behavior |
|---|---|---|---|
BaseURL |
string |
none | Authdog API base URL. Used as given: a trailing slash is not stripped and the value is not validated. |
Timeout |
time.Duration |
10s |
Per-request timeout on the internal http.Client. Zero means 10 seconds. |
NewVerifier returns *Verifier. Each request sends Content-Type: application/json, User-Agent: authdog-mcp-go-sdk/0.1.0, and Authorization: Bearer <token>. You cannot inject your own http.Client.
Verify a token
info, err := verifier.VerifyToken(ctx, "the-bearer-access-token")
if err != nil {
if authdogmcp.IsAuthenticationError(err) {
log.Printf("token rejected: %v", err)
} else if authdogmcp.IsAPIError(err) {
log.Printf("verification failed: %v", err)
} else {
log.Printf("unexpected error: %v", err)
}
return
}
fmt.Printf("subject: %s\n", info.Subject)
fmt.Printf("username: %s\n", info.Username)
fmt.Printf("email: %s\n", info.Email)
if info.ExpiresAt != nil {
fmt.Printf("expires: %s\n", info.ExpiresAt.Format(time.RFC3339))
}VerifyToken(ctx context.Context, accessToken string) (*AuthInfo, error) uses ctx for cancellation. It does not pre-check for an empty token.
AuthInfo field |
Type | Value |
|---|---|---|
Token |
string |
The token you passed in |
Subject |
string |
User.ID |
Username |
string |
User.UserName |
Email |
string |
First User.Emails value, or "" |
Scopes |
[]string |
Never populated (nil/empty) |
ExpiresAt |
*time.Time |
Now plus Session.RemainingSeconds when that is greater than 0, else nil |
User |
User |
Full user record |
Scopes is empty because the userinfo endpoint does not return scopes; the SDK never sets it. Do not use it for scope-based authorization. Authentication identifies the caller; apply authorization separately.
Wire into an MCP server
Streamable HTTP MCP servers receive the token in the Authorization: Bearer ... header. Verify once in middleware and put the *AuthInfo on the request context:
package main
import (
"context"
"net/http"
"strings"
authdogmcp "github.com/authdog/mcp-sdk/go"
)
type ctxKey struct{}
// AuthInfoFromContext returns the verified identity attached by the middleware.
func AuthInfoFromContext(ctx context.Context) (*authdogmcp.AuthInfo, bool) {
info, ok := ctx.Value(ctxKey{}).(*authdogmcp.AuthInfo)
return info, ok
}
// RequireAuth verifies the bearer token on each request and attaches the
// resolved AuthInfo to the context before invoking the MCP handler.
func RequireAuth(verifier *authdogmcp.Verifier, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if token == "" {
w.Header().Set("WWW-Authenticate", "Bearer")
http.Error(w, "missing bearer token", http.StatusUnauthorized)
return
}
info, err := verifier.VerifyToken(r.Context(), token)
if err != nil {
if authdogmcp.IsAuthenticationError(err) {
w.Header().Set("WWW-Authenticate", "Bearer")
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
http.Error(w, "auth backend error", http.StatusBadGateway)
return
}
// Attach the verified identity; handlers read it via AuthInfoFromContext.
ctx := context.WithValue(r.Context(), ctxKey{}, info)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func main() {
verifier := authdogmcp.NewVerifier(authdogmcp.VerifierConfig{
BaseURL: "https://api.authdog.com",
})
// mcpHandler is your MCP server's Streamable HTTP handler.
var mcpHandler http.Handler = http.NewServeMux()
http.Handle("/mcp", RequireAuth(verifier, mcpHandler))
_ = http.ListenAndServe(":8080", nil)
}Errors
Each error type is a struct with a Message field, returned as a pointer.
| Type | Check | When |
|---|---|---|
*AuthenticationError |
IsAuthenticationError |
HTTP 401 (Unauthorized - invalid or expired token) |
*APIError |
IsAPIError |
Other non-2xx (HTTP error <status>: <body>), request build, transport, body-read, or JSON decode failure |
*AuthdogError |
IsAuthdogError |
Base type; VerifyToken itself never returns it |
The Is* helpers use a direct type assertion. They do not unwrap, and IsAuthdogError does not match *AuthenticationError or *APIError. Check the specific types first.
Related
- MCP SDK: language overview
- MCP authentication: how MCP clients obtain tokens
- Node.js and Python verifiers
- Go SDK: management and userinfo client