Go

TL;DR — Four modules. authplanemcp targets the official MCP Go SDK. mark3labs targets the mark3labs/mcp-go community library. http is a generic net/http adapter for any Go HTTP server. core is the framework-agnostic primitive package all others build on. Every adapter shares the same Options{Issuer, Resource, Scopes, DevMode, ClientOptions, VerifierOptions} shape and delivers Bearer + DPoP verification, RFC 9728 PRM, RFC 6750 WWW-Authenticate challenges, and RFC 8693 token exchange.

Install

# Official MCP Go SDK
go get github.com/authplane/go-sdk/mcp

# mark3labs/mcp-go community SDK
go get github.com/authplane/go-sdk/mark3labs

# Generic net/http (any Go server)
go get github.com/authplane/go-sdk/http

# Core primitives only
go get github.com/authplane/go-sdk/core

Compatibility:

PackageMin GoNotes
go-sdk/mcp1.25+Pulls in modelcontextprotocol/go-sdk.
go-sdk/mark3labs1.25.5+Minimum required by mark3labs/mcp-go v0.54.0. Adapter embeds authplanehttp.Adapter.
go-sdk/http1.24+Standalone net/http middleware.
go-sdk/core1.24+Shared primitive package.

All adapters delegate to go-sdk/core for the JWKS cache, metadata discovery, JWT validation, DPoP proof verification, token-exchange client, and introspection.


authplanemcp — Official MCP Go SDK adapter

Quickstart

package main

import (
    "context"
    "net/http"

    "github.com/authplane/go-sdk/core/resource/verifier"
    "github.com/authplane/go-sdk/mcp/pkg/authplanemcp"
    "github.com/modelcontextprotocol/go-sdk/mcp"
)

func main() {
    ctx := context.Background()

    adapter, err := authplanemcp.NewAdapter(ctx, authplanemcp.Options{
        Issuer:   "http://localhost:9000",
        Resource: "http://localhost:8080/mcp",
        Scopes:   []string{"tools/read"},
        DevMode:  true,
        VerifierOptions: []verifier.Option{
            verifier.WithInboundDPoP(verifier.InboundDPoPOptions{Required: true}),
        },
    })
    if err != nil {
        panic(err)
    }
    defer adapter.Close()

    server := mcp.NewServer(
        &mcp.Implementation{Name: "my-server", Version: "1.0.0"}, nil,
    )
    handler := mcp.NewStreamableHTTPHandler(
        func(_ *http.Request) *mcp.Server { return server }, nil,
    )

    http.Handle(adapter.WellKnownPRMPath(), adapter.ProtectedResourceMetadataHandler())
    http.Handle("/mcp", adapter.AuthMiddleware(handler))

    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}

Options reference

FieldTypePurpose
Issuerstring (required)AS URL for RFC 8414 discovery.
Resourcestring (required)JWT aud and PRM resource.
Scopes[]stringAdvertised in PRM.
DevModeboolPrepends authplane.WithFetchSettings(authplane.DevModeFetchSettings()) to ClientOptions. Also honors AUTHPLANE_DEV_MODE=1.
ClientOptions[]authplane.OptionClient-level: WithClientCredentials, WithClientAuthentication, WithJWKSCacheTTL, WithCircuitBreaker, WithDPoP. Introspection auto-wires as revocation checker when credentials are present.
VerifierOptions[]verifier.OptionVerifier-level: WithAlgorithms, WithClockSkew, WithInboundDPoP, WithRevocationChecker (overrides auto-wired introspection).

WithVerifierOptions replaces (not appends to) the auto-wired option list. If you set VerifierOptions while ClientOptions also wires introspection, you must include the introspection checker yourself or accept that revocation is off.

Main API

SymbolPurpose
NewAdapter(ctx, Options) (*Adapter, error)Constructor. Performs metadata discovery + warms JWKS.
adapter.AuthMiddleware(next http.Handler) http.HandlerVerifies token, enforces scopes, injects claims into request context. Writes RFC 6750 401 on failure with WWW-Authenticate that advertises the PRM URL via resource_metadata= (RFC 9728 §5.1).
adapter.WellKnownPRMPath() stringPath for the PRM handler (/.well-known/oauth-protected-resource/mcp).
adapter.ProtectedResourceMetadataHandler() http.HandlerServes the PRM JSON.
adapter.Close() errorReleases JWKS + metadata refresh goroutines. defer adapter.Close() after NewAdapter.
authplanemcp.ClaimsFromContext(ctx) *verifier.VerifiedClaimsRead claims injected by AuthMiddleware. Returns nil when absent.
authplanemcp.TokenFromContext(ctx) stringRead the raw bearer token (useful for adapter.TokenExchange). Returns "" when absent.

Per-tool scope enforcement

The official MCP Go SDK doesn’t ship a per-tool scope guard — check inside your handler:

func writeHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    claims := authplanemcp.ClaimsFromContext(ctx)
    if claims == nil {
        return nil, fmt.Errorf("no claims in context")
    }
    if !claims.HasScope("tools/write") {
        return nil, fmt.Errorf("insufficient scope: tools/write required")
    }
    // ... your logic
    return &mcp.CallToolResult{}, nil
}

Token exchange

Call TokenExchange on the adapter — this is the path that auto-maps ConsentRequiredError to the MCP JSON-RPC -32042 elicitation error. Calling TokenExchange on the raw client returned by adapter.Client() does not auto-map.

resp, err := adapter.TokenExchange(ctx, authplane.TokenExchangeInput{
    SubjectToken: inboundToken,
    Resources:    []string{"https://api.example.com/downstream"},
    Scopes:       []string{"tools/write"},
})
// resp.AccessToken, resp.TokenType ("Bearer" or "DPoP"), resp.ExpiresIn

If the exchange fails with ConsentRequiredError (contains a ConsentURL field), the adapter maps it to the MCP JSON-RPC -32042 elicitation error for the client. Your handler doesn’t need try/except-style handling.

Sharing a pre-built client

For multi-resource deployments where one client backs several adapters, build authplane.NewClient yourself and pass it via ClientOptions — the adapter will not own its lifecycle in that case. See the full user-guide §10.


authplanemark3labs — mark3labs/mcp-go adapter

Quickstart

package main

import (
    "context"
    "net/http"

    "github.com/authplane/go-sdk/mark3labs/pkg/authplanemark3labs"
    "github.com/mark3labs/mcp-go/server"
)

func main() {
    ctx := context.Background()

    adapter, err := authplanemark3labs.NewAdapter(ctx, authplanemark3labs.Options{
        Issuer:   "https://auth.example.com",
        Resource: "https://mcp.example.com/mcp",
        Scopes:   []string{"tools/query", "tools/write"},
    })
    if err != nil {
        panic(err)
    }
    defer adapter.Close()

    mcpServer := server.NewMCPServer("my-server", "1.0.0",
        server.WithToolCapabilities(false),
        server.WithRecovery(),
    )

    streamable := server.NewStreamableHTTPServer(mcpServer,
        server.WithHTTPContextFunc(adapter.HTTPContextFunc()),
    )

    http.Handle(adapter.WellKnownPRMPath(), adapter.ProtectedResourceMetadataHandler())
    http.Handle("/mcp", adapter.AuthMiddleware(streamable))

    http.ListenAndServe(":8080", nil)
}

Two coordinated hooks

mark3labs/mcp-go has an HTTP request context and a per-tool-call MCP context — they don’t share by default. The adapter bridges them via two required hooks:

HookPurpose
adapter.AuthMiddleware(next)Standard http.Handler middleware. Parses Authorization: Bearer … or DPoP …, verifies, injects claims + raw token into the HTTP request context. On failure writes RFC 6750 401 with the PRM URL in WWW-Authenticate. Auto-excludes the PRM well-known path.
server.WithHTTPContextFunc(adapter.HTTPContextFunc())Forwards claims + token from the HTTP request context into the per-tool-call MCP context. Without it, tool handlers receive a fresh context with no claims.

Per-tool scope enforcement

Scope enforcement is per-tool, not per-request, matching the MCP protocol requirement that initialize succeed with any authenticated client:

import "github.com/authplane/go-sdk/mark3labs/pkg/authplanemark3labs"

func writeHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    claims := authplanemark3labs.ClaimsFromContext(ctx)
    if claims == nil {
        return mcp.NewToolResultError("no claims"), nil
    }
    if !claims.HasScope("tools/write") {
        return mcp.NewToolResultError("insufficient scope: tools/write required"), nil
    }
    // ... your logic
    return mcp.NewToolResultText("done"), nil
}

Under the hood

*Adapter embeds *authplanehttp.Adapter — the same middleware, scope guard, DPoP/bearer parsing, and WWW-Authenticate shape as the HTTP adapter below. mark3labs-only additions are the context bridge and URL-elicitation mapping.


authplanehttpnet/http adapter

For any Go HTTP server — not tied to an MCP framework. Use when your resource server is a regular net/http service and you want AuthPlane token validation without pulling in an MCP SDK.

Quickstart

package main

import (
    "context"
    "net/http"

    "github.com/authplane/go-sdk/core/authplane"
    "github.com/authplane/go-sdk/core/resource"
    "github.com/authplane/go-sdk/http/pkg/authplanehttp"
)

func main() {
    ctx := context.Background()

    client, err := authplane.NewClient(ctx, "https://auth.example.com",
        authplane.WithClientCredentials("my-client", "s3cret"),
    )
    if err != nil {
        panic(err)
    }
    defer client.Close()

    res, err := client.Resource("https://api.example.com",
        resource.WithScopes("read", "write"),
    )
    if err != nil {
        panic(err)
    }

    adapter := authplanehttp.New(res)

    mux := http.NewServeMux()
    mux.Handle(adapter.WellKnownPRMPath(), adapter.PRMHandler())
    mux.Handle("/api/admin", adapter.RequireScopes("admin")(http.HandlerFunc(adminHandler)))
    mux.Handle("/api/", http.HandlerFunc(readHandler))

    http.ListenAndServe(":8080", adapter.Middleware()(mux))
}

Three things happen per request

  1. Middleware() extracts the Authorization header, verifies the token (Bearer or DPoP), and injects *verifier.VerifiedClaims + raw token into the request context.
  2. RequireScopes(...) middleware (optional, per-route) checks for required scopes.
  3. The well-known PRM path is auto-excluded from token checks so unauthenticated clients can discover the AS.

On failure, the adapter writes an RFC 6750 error response: correct HTTP status, WWW-Authenticate with the right scheme (Bearer or DPoP), error code, and JSON body.

Adapter does not own client lifecycle

Unlike the MCP adapters, authplanehttp is a thin wrapper around *resource.Resource — it holds no cleanup state. The caller owns the *authplane.Client and calls client.Close(). One client typically backs many resources and adapters in a single process.


authplane — Core primitives

Framework-agnostic package. Use it when:

  • You want direct control over the JWKS cache, DPoP outbound, or token-exchange flow.
  • You’re building a custom framework adapter.
  • You need to verify tokens outside a request context.

Build a client + resource

import (
    "context"
    "github.com/authplane/go-sdk/core/authplane"
    "github.com/authplane/go-sdk/core/resource"
)

client, err := authplane.NewClient(ctx, "https://auth.example.com",
    authplane.WithClientCredentials("my-client", "s3cret"),
)
if err != nil { panic(err) }
defer client.Close()

res, err := client.Resource("https://api.example.com/mcp",
    resource.WithScopes("tools/query"),
)
if err != nil { panic(err) }

Verify a token

claims, err := res.VerifyToken(ctx, accessToken)  // add resource.WithDPoP(dpopCtx) for DPoP
if err != nil {
    // sentinel errors: verifier.ErrTokenExpired, verifier.ErrInsufficientScope,
    //   verifier.ErrDPoPBindingMismatch, etc. — check with errors.Is.
}
// claims.Sub(), claims.Scopes(), claims.Audience(), claims.Cnf(), claims.Act(), claims.AgentID(), ...

Token exchange

resp, err := client.TokenExchange(ctx, authplane.TokenExchangeInput{
    SubjectToken: userToken,
    Resources:    []string{"https://api.example.com/downstream"},
    Scopes:       []string{"tools/write"},
})
// resp.AccessToken, resp.TokenType ("Bearer" | "DPoP"), resp.ExpiresIn

If the exchange requires user consent that hasn’t happened yet, client.TokenExchange returns a *authplane.ConsentRequiredError — its ConsentURL field is what an MCP adapter would map to the -32042 elicitation error.

Inbound DPoP

Build a verifier.DPoPContext from your framework’s request (method, URL, DPoP header values) and pass it as an option: res.VerifyToken(ctx, token, resource.WithDPoP(dpopCtx)). See core user-guide §9 Inbound DPoP Summary.

Error handling

Sentinel errors from github.com/authplane/go-sdk/core/resource/verifier, checked with errors.Is:

  • ErrTokenMissing, ErrTokenExpired, ErrInvalidSignature, ErrInvalidClaims, ErrInsufficientScope, ErrTokenRevoked
  • ErrDPoPBindingMismatch, ErrDPoPReplayDetected, ErrDPoPInvalid
  • ErrMetadataUnavailable, ErrJWKSUnavailable

Plus authplane.ConsentRequiredError for token-exchange consent failures.

Each sentinel maps to an OAuth error code suitable for a WWW-Authenticate challenge — the HTTP adapter uses these to build the response automatically; direct users of core construct their own.

Cleanup

client.Close(). Idempotent. Releases JWKS and metadata refresh goroutines.