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=trueopts 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-mcpimport 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())npm install @authplane/sdk @authplane/fastmcp fastmcp zodimport { FastMCP, requireScopes } from "fastmcp";
import { authplaneFastMcpAuth, type AuthplaneFastMcpSession } from "@authplane/fastmcp";
import { z } from "zod";
const auth = await authplaneFastMcpAuth({
issuer: "http://localhost:9000",
resource: "http://localhost:8080/mcp",
scopes: ["tools/read"],
requiredScopes: ["tools/read"], // request-layer enforcement
inboundDPoP: { required: true },
devMode: true, // allow http://localhost; remove in production
});
const server = new FastMCP<AuthplaneFastMcpSession>({
name: "my-server",
version: "1.0.0",
authenticate: auth.authenticate,
oauth: auth.oauth,
});
server.addTool({
name: "read",
description: "Return the query back — trivial demo tool.",
parameters: z.object({ query: z.string() }),
canAccess: requireScopes("tools/read"),
execute: async ({ query }) => ({ content: [{ type: "text", text: `You asked: ${query}` }] }),
});
await server.start({
transportType: "httpStream",
httpStream: { port: 8080, endpoint: "/mcp" },
});go get github.com/authplane/go-sdk/mcppackage main
import (
"context"
"fmt"
"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, // allow http://localhost; remove in production
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))
fmt.Println("MCP server listening on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}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:
- 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) - Discover the AS at
http://localhost:9000via the PRM’sauthorization_serversfield - Fetch the AS metadata at
http://localhost:9000/.well-known/oauth-authorization-server - Register itself dynamically at
/oauth/register - Kick off the authorization code + PKCE flow
- Present a token bound to a DPoP key and call your
readtool
Done. What you have now
- An AuthPlane AS on
:9000issuing DPoP-bound, scope-scoped JWTs - Admin UI on
:9001/admin/ui/showing clients, users, resources, grants, issuances, signing keys - An MCP server on
:8080/mcpthat validates every request against AuthPlane’s JWKS and enforcestools/readscope - A registered resource in AuthPlane (via the PRM advertisement) and a registered client (via DCR)
Next steps
- Understand what just happened — Your first MCP server walks each line of the snippet above.
- Pick a topology — Choose your topology if you’re going beyond one agent + one MCP server.
- Federate to your IdP — OIDC Federation guide.
- Move to Postgres and go to prod — Operate overview.
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 plaincurl. Remove theinbound_dpop/inboundDPoP/WithInboundDPoPline to accept bearer tokens. invalid_dpop_proofon every DPoP request — you enabled inbound DPoP in the SDK but forgotinstall_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 athttp://host/mcp, PRM lives at/.well-known/oauth-protected-resource/mcp(the bare/.well-known/oauth-protected-resourcereturns 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 thehttp.Handle(adapter.WellKnownPRMPath(), ...)line. In Python and TS the adapter wires it automatically via theauthresult spread. - Inspector connects but sees no tools — the URL is wrong; make sure it ends in
/mcp(or whateverendpointyou configured), not just the host. - Anything else — Debugging checklist.