Auth-specific code: 5 lines · Total example: 56 lines · SDK: ts-sdk 0.2.0
When your MCP server needs to call another resource — another Mint MCP
server, an admin service, any backend that trusts Authplane — it
acquires a token via the @authplane/sdk Auth Client. This example
isolates that outbound OAuth call so you can see what the SDK does:
build a client, request a client_credentials token, present it on the
call. In your real MCP server, this code lives inside the tool handler
that needs to talk to the other resource.
Pairs with tier 01. Tier 01 plays "the resource being called" — its authserver (
:9000/:9001) and MCP server (:8080) must be running beforemake verifyhere. Bring it up first withmake runin../01-mcp-server-basic/.The standalone
agent.tsshape is purely for readability: in production the same SDK calls live inside an MCP server's tool handler, not in a sibling process.
- How to acquire a
client_credentialsaccess token from Authplane using the@authplane/sdkAuthplaneClient - How to bind the issued token to a specific
Resource via the
resource=parameter so the JWTaudclaim matches the server you're calling - How to attach the bearer token to an MCP JSON-RPC request and parse the streamable-http response
- How the agent side of a machine-to-machine MCP flow stays tiny when the SDK owns metadata discovery, token caching, and the circuit breaker
| Time to run | ~2 minutes (after tier 01 is already up) |
| Prereqs | Docker 24+, docker compose, curl, jq, Node.js 22+ (only if you run outside Docker), tier 01 running |
| SDK | @authplane/sdk 0.2.0 (npm) |
| Runtime | Node.js 22 + tsx, plain fetch against the MCP streamable-http transport |
cp .env.example .env
make run
make verifymake run builds the agent's container image but does not start any
long-running service — the agent is a one-shot client. make verify
waits for tier 01 to be up, registers an OAuth client + Mint resource
against the tier-01 authserver, then runs the agent container once. The
agent mints a token and calls the tier-01 echo tool. make clean
tears down anything docker compose brought up and removes the .env
file the run target created.
The make verify script automates every step below; the bullets here
describe what's happening so you can reproduce the flow by hand.
If you want to run the curl examples manually instead of via
make verify, first load the env vars and capture client credentials as you go:set -a; source .env; set +a # exports AUTHPLANE_ADMIN_API_KEY etc.Step 3 emits
client_idandclient_secretin its response; assign each to a shell variable (CLIENT_ID=...,CLIENT_SECRET=...) before steps 4 and 5 use them. Step 5 hands those credentials to the agent, which mints the access token itself inside the SDK — you never need to see the raw token to verify the flow works.
-
Bring up tier 01. This example is a pure client — the authserver and MCP server live in the tier-01 compose project.
cd ../01-mcp-server-basic make run curl -fsS http://localhost:9000/.well-known/oauth-authorization-server -
Register a Mint Resource. The Resource URI must match the JWT audience the tier-01 MCP server expects (
AUTHPLANE_RESOURCE, defaulting tohttp://localhost:8080/mcp).curl -sS -X POST http://localhost:9001/admin/resources \ -H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "slug": "demo-mcp", "uri": "http://localhost:8080/mcp", "backend_kind": "mint", "display_name": "Demo MCP", "scopes": [{"name": "mcp:echo", "description": "echo tool"}] }'
The same Resource can be created via the CLI inside the tier-01 authserver container. The canonical path is
admin resource create(seedocs/reference/cli.md#cli-admin-resource-create):docker compose -f ../01-mcp-server-basic/docker-compose.yml exec authserver \ /authserver admin resource create \ --slug demo-mcp \ --uri http://localhost:8080/mcp \ --backend-kind mint \ --display-name "Demo MCP" \ --scopes 'mcp:echo||echo tool'
Note the pipe-delimited
'name|upstream|description'tuple syntax — the double-empty middle is intentional for Mint scopes (no upstream mapping). If the resource already exists from a prior tier-01 run, the AS returns 409 andverify.shcontinues. -
Register an OAuth client for the agent. A
client_credentialsmachine client needs the matching grant type and a confidential auth method.curl -sS -X POST http://localhost:9001/admin/clients \ -H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_name": "demo-agent-ts", "grant_types": ["client_credentials"], "token_endpoint_auth_method": "client_secret_basic", "scope": "mcp:echo" }'
The response carries
client_idandclient_secret. The secret is shown once — capture it intoCLIENT_ID/CLIENT_SECRET. -
Run the agent. Export the captured credentials and invoke the agent —
@authplane/sdkperforms RFC 8414 discovery, requests aclient_credentialstoken atPOST /oauth/token, and the agent then POSTs an authenticated JSON-RPCtools/callto the MCP server.The agent container joins tier-01's compose network so it can resolve the service hostnames directly. The SDK enforces that the discovered issuer matches the configured one, so use the same
http://authserver:9000value tier 01's AS was bootstrapped with — nothttp://localhost:9000.docker compose run --rm \ -e AUTHPLANE_ISSUER=http://authserver:9000 \ -e AUTHPLANE_RESOURCE=http://localhost:8080/mcp \ -e AUTHPLANE_CLIENT_ID="$CLIENT_ID" \ -e AUTHPLANE_CLIENT_SECRET="$CLIENT_SECRET" \ -e MCP_URL=http://mcp-server:8080/mcp \ agent
AUTHPLANE_RESOURCEstays at the publichttp://localhost:8080/mcpURI — it is a logical audience identifier (the token'saudclaim and the registered Mint resource URI), not a network address. The tier-01 MCP server validates inbound JWTs against this exact value.Expected output: two
[agent] ... OKlines — one for theinitializehandshake and one for theechotool call that bounces theECHO_TEXTstring back.
An MCP client without auth — a single unauthenticated POST:
- const res = await fetch(MCP_URL, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params }),
- });The same client with Authplane token acquisition:
+ import { AuthplaneClient } from "@authplane/sdk/core";
+ const ap = await AuthplaneClient.create({
+ issuer: process.env.AUTHPLANE_ISSUER!,
+ auth: { clientId: process.env.AUTHPLANE_CLIENT_ID!, clientSecret: process.env.AUTHPLANE_CLIENT_SECRET! },
+ });
+ const token = await ap.clientCredentials(["mcp:echo"], [process.env.AUTHPLANE_RESOURCE!]);
const res = await fetch(MCP_URL, {
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: {
+ "Authorization": `Bearer ${token.accessToken}`,
+ "Content-Type": "application/json",
+ "Accept": "application/json, text/event-stream",
+ },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params }),
});Five lines of auth code, inside the // authplane:begin / // authplane:end
markers in agent.ts. Run
go run ./tools/loccount examples/typescript/02-agent-basic from the
repo root to see the count.
AuthplaneClient.create() performs RFC 8414 AS metadata discovery against
issuer, learns the token endpoint, and primes an internal token cache
and circuit breaker. clientCredentials(scopes, resources) then POSTs to
/oauth/token with grant_type=client_credentials, the requested scope,
and one resource= value per element of the resources array (RFC 8707
resource indicators). Authplane validates the client and the requested scope against the
target Resource, then mints a JWT whose aud claim equals the resource
URI and returns it as access_token. The agent attaches the token as a
Bearer Authorization header on the MCP request. The tier-01 MCP
server's bearerAuth middleware validates the JWT signature against
the AS's JWKS, then checks aud, exp, and the required mcp:echo
scope before dispatching to the echo tool. The audience-bound token is
what makes this safe: even if leaked, it is only accepted by the resource
whose URI was bound at mint time.
Tier 03 — FastMCP server + agent with DPoP
layers RFC 9449 DPoP onto this flow, binding access tokens to a per-agent
keypair so a stolen token cannot be replayed from another host, and adds
per-tool scope enforcement.
Tier 04 puts an MCP server in front of a Broker
upstream (GitHub) with ConsentRequiredError handling.
This example does not start its own authserver by default — tier 01 owns
that. If you want to run tier 02 standalone (no tier 01 process), bring
up the gated authserver service in this directory's docker-compose.yml
with docker compose --profile standalone up -d. To build the AS from
this checkout rather than pulling
authplane/authserver:latest, follow the LOCAL BUILD ESCAPE
HATCH comment block in
../../_shared/docker-compose.authserver.yml.
Mirror the change in this example's docker-compose.yml (the
authserver service mirrors the same definition) — replace the image:
line with the build: block shown in the shared file.