Quickstart

TL;DR — Three commands: run the AuthPlane binary, install the SDK for your language, point an MCP client at your server. No account, no signup. SQLite and auto-generated keys make first run zero-config.

Prerequisites

  • Docker (or the standalone binary if you prefer no container)
  • One of: Python 3.11+, Node.js 20+, or Go 1.24+ (Go 1.25+ if you use authplanemcp)
  • An MCP client for testing — MCP Inspector is enough; Claude Desktop, Cursor, or VS Code all work too

1. Run the AuthPlane server

Generate an admin API key and a session secret, then start the container:

$ export AUTHPLANE_ADMIN_API_KEY="$(openssl rand -hex 32)"
$ export AUTHPLANE_SESSION_SECRET="$(openssl rand -hex 32)"
$ docker run -p 9000:9000 -p 9001:9001 \
    -e AUTHPLANE_ADMIN_API_KEY \
    -e AUTHPLANE_SESSION_SECRET \
    -e AUTHPLANE_DPOP_ENABLED=true \
    -v authserver-data:/data \
    authplane/authserver:latest serve

You now have OAuth endpoints live on http://localhost:9000 and the Admin UI on http://localhost:9001/admin/ui/. Open the Admin UI and paste the value of $AUTHPLANE_ADMIN_API_KEY when prompted — that’s the key you just generated, not something the binary prints back.

Note on defaults — SQLite storage lives under /data, signing keys auto-generate on first boot, and there’s no config file. AUTHPLANE_DPOP_ENABLED=true opts the server into DPoP proof-of-possession so the SDK sections below can enforce it inbound; leave it off for a bearer-only setup.

Verify the AS is up:

$ curl -s localhost:9000/.well-known/oauth-authorization-server | jq .issuer
"http://localhost:9000"

2. Protect your MCP server

Install the AuthPlane SDK for your language and wire it in. Pick a tab — your choice sticks across every page in these docs.

pip install authplane-mcp
server.py python
import asyncio
from authplane import InboundDPoPOptions
from authplane_mcp import authplane_mcp_auth, install_request_context, require_scope
from mcp.server.fastmcp import FastMCP


async def main() -> None:
  auth = await authplane_mcp_auth(
      issuer="http://localhost:9000",
      resource="http://localhost:8080/mcp",
      scopes=["tools/read"],
      enforce_scopes_on_all_requests=True,
      inbound_dpop=InboundDPoPOptions(required=True),
      dev_mode=True,  # allow http://localhost; remove in production
  )
  mcp = FastMCP("my-server", port=8080, json_response=True, **auth)
  install_request_context(mcp)  # required for inbound DPoP

  @mcp.tool()
  async def read(query: str) -> str:
      """Return the query back — trivial demo tool."""
      require_scope("tools/read")
      return f"You asked: {query}"

  try:
      await mcp.run_streamable_http_async()
  finally:
      await auth.aclose()


asyncio.run(main())

Start the server:

$ python server.py       # or: node server.js  /  go run main.go

What this snippet delivers, verified against the SDK source: JWT validation (against AuthPlane’s JWKS, cached and rotated), RFC 8414 metadata discovery, RFC 9728 Protected Resource Metadata publication with scopes_supported advertised, RFC 9449 DPoP inbound enforcement (tokens without a valid proof get 401 WWW-Authenticate: DPoP error="invalid_dpop_proof"), and per-tool scope enforcement via the framework’s own scope guard.

3. Connect a client

Point Claude Desktop, Cursor, or VS Code at http://localhost:8080/mcp. Discovery, dynamic client registration (RFC 7591), and consent happen automatically.

For a fast sanity check without configuring a full client, use MCP Inspector:

$ npx @modelcontextprotocol/inspector http://localhost:8080/mcp

The Inspector will:

  1. Fetch the PRM document at http://localhost:8080/.well-known/oauth-protected-resource/mcp (published by the SDK — path-scoped per RFC 9728 §3.1)
  2. Discover the AS at http://localhost:9000 via the PRM’s authorization_servers field
  3. Fetch the AS metadata at http://localhost:9000/.well-known/oauth-authorization-server
  4. Register itself dynamically at /oauth/register
  5. Kick off the authorization code + PKCE flow
  6. Present a token bound to a DPoP key and call your read tool

Done. What you have now

  • An AuthPlane AS on :9000 issuing DPoP-bound, scope-scoped JWTs
  • Admin UI on :9001/admin/ui/ showing clients, users, resources, grants, issuances, signing keys
  • An MCP server on :8080/mcp that validates every request against AuthPlane’s JWKS and enforces tools/read scope
  • A registered resource in AuthPlane (via the PRM advertisement) and a registered client (via DCR)

Next steps

Troubleshooting

  • 401 on the very first tool call, no DPoP header on request — your MCP client didn’t send a DPoP proof, but the SDK is configured with required: true. Either the client doesn’t support DPoP (some early MCP clients don’t; see compatibility matrix) or you’re testing with a plain curl. Remove the inbound_dpop / inboundDPoP / WithInboundDPoP line to accept bearer tokens.
  • invalid_dpop_proof on every DPoP request — you enabled inbound DPoP in the SDK but forgot install_request_context(mcp) (Python only). See Enable DPoP end-to-end.
  • PRM 404 when your client hits /.well-known/oauth-protected-resource — the RFC 9728 doc is served path-scoped: for a resource at http://host/mcp, PRM lives at /.well-known/oauth-protected-resource/mcp (the bare /.well-known/oauth-protected-resource returns 404 even when the handler is mounted). If the path-scoped URL is also 404, the SDK didn’t mount the PRM handler. In Go, that’s the http.Handle(adapter.WellKnownPRMPath(), ...) line. In Python and TS the adapter wires it automatically via the auth result spread.
  • Inspector connects but sees no tools — the URL is wrong; make sure it ends in /mcp (or whatever endpoint you configured), not just the host.
  • Anything elseDebugging checklist.