Authdog

MCP SDK: Go

Last updated Oct 13, 2026
View as Markdown

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.git

In your go.mod:

require github.com/authdog/mcp-sdk/go v0.0.0

replace github.com/authdog/mcp-sdk/go => ../mcp-sdk/go

Once a release is published, the README gives:

go get github.com/authdog/mcp-sdk/go
import 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.