TypeScript

TL;DR — Five packages. @authplane/mcp targets the official MCP TypeScript SDK. @authplane/fastmcp targets punkpeye/fastmcp. @authplane/hono and @authplane/nestjs target their respective web frameworks. @authplane/sdk is the core — everything else builds on it. All adapters share the same option surface (issuer, resource, scopes, requiredScopes, inboundDPoP, revocationChecker, devMode, …) and produce the same conceptual return value (client, verifier, framework-native middleware, PRM handler).

Install

# Official MCP TypeScript SDK
npm install @authplane/sdk @authplane/mcp @modelcontextprotocol/sdk express zod

# punkpeye FastMCP
npm install @authplane/sdk @authplane/fastmcp fastmcp zod

# Hono
npm install @authplane/sdk @authplane/hono hono

# NestJS
npm install @authplane/sdk @authplane/nestjs @nestjs/common @nestjs/core reflect-metadata rxjs

# Core primitives only
npm install @authplane/sdk

Requires Node 20 LTS or newer for @authplane/mcp and @authplane/sdk. @authplane/hono and @authplane/nestjs require Node 22 LTS or newer. @authplane/sdk is a peer of every adapter.


@authplane/mcp — Official MCP TypeScript SDK adapter

Quickstart

import express from "express";
import crypto from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { authplaneMcpAuth } from "@authplane/mcp";
import { z } from "zod";

const server = new McpServer({ name: "my-server", version: "1.0.0" });

server.tool(
  "echo",
  "Echo message",
  { message: z.string() },
  async ({ message }) => ({ content: [{ type: "text", text: message }] }),
);

const auth = await authplaneMcpAuth({
  issuer: "http://localhost:9000",
  resource: "http://localhost:3000/mcp",
  scopes: ["tools/echo"],
});

const app = express();
app.use(express.json());
app.get(auth.protectedResourceMetadataPath, auth.protectedResourceMetadataHandler);

const transports = new Map<string, StreamableHTTPServerTransport>();
app.all("/mcp", auth.bearerAuth, async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;
  if (sessionId && transports.has(sessionId)) {
    await transports.get(sessionId)!.handleRequest(req, res, req.body);
    return;
  }
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => crypto.randomUUID(),
  });
  transports.set(transport.sessionId ?? crypto.randomUUID(), transport);
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000);

authplaneMcpAuth(options) — what you get back

Return fieldTypeUse
clientAuthplaneClientOwns JWKS + metadata refresh; use for client.exchange(), client.introspect(). Call client.close() on shutdown.
verifierAuthplaneResourceDirect token verification if you need to bypass middleware.
bearerAuthExpress middlewareVerifies token, enforces requiredScopes, attaches req.auth.
protectedResourceMetadataPathstringPath for the PRM handler (/.well-known/oauth-protected-resource/mcp).
protectedResourceMetadataHandlerExpress handlerServes the RFC 9728 PRM document.
protectedResourceMetadataobjectThe PRM JSON payload if you want to serve it yourself.

Options (highlights):

OptionDefaultPurpose
issuerrequiredAS URL for RFC 8414 discovery.
resourcerequiredJWT aud and PRM resource.
scopes[]Advertised in PRM. Default requiredScopes.
requiredScopes= scopesEnforced by bearerAuth on every request. Pass [] to disable request-layer enforcement.
inboundDPoPundefinedEnable inbound DPoP verification + PRM advertising.
asCredentialsundefinedEnable introspection/revocation and client.exchange().
revocationCheckerundefinedEnable RFC 7662 introspection revocation.
devModefalseRelax SSRF for local dev.

Per-tool scope enforcement

bearerAuth enforces requiredScopes globally. For per-tool guards use requireScope(scope) from @authplane/mcp inside your tool handler — it reads req.auth and throws if the scope is missing. Details in the full user-guide.

Cleanup

Call await auth.client.close() on shutdown to release JWKS/metadata timers.


@authplane/fastmcp — punkpeye FastMCP adapter

Quickstart

import { 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"],
  inboundDPoP: { required: true },
  devMode: true,
});

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.",
  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" },
});

authplaneFastMcpAuth(options) — what you get back

FieldPurpose
authenticateNonNullable<ServerOptions["authenticate"]> — verifies the bearer token, enforces requiredScopes, returns a typed session.
oauthNonNullable<ServerOptions["oauth"]> — publishes RFC 9728 PRM via FastMCP’s oauth config.
clientUnderlying AuthplaneClient (wrapped for elicitation).
verifier, tokenVerifier, protectedResourceMetadata, protectedResourceMetadataUrlLower-level access.

AuthplaneFastMcpSession is the session shape published by authenticate — its token field carries the VerifiedClaims (subject at session.token.sub, plus clientId, scopes, expiresAt, and the raw claims).

DPoP + FastMCP authenticate double-invocation

FastMCP calls authenticate twice per HTTP request (once at the request gate, once when building the session payload). RFC 9449 inbound replay verification can only run once per proof — the adapter uses Node’s AsyncLocalStorage scoped to the request’s async context to collapse both invocations onto a single verifyAccessToken promise. This means inboundDPoP: { required: true } works out of the box against fastmcp@^3.35 (verified on 3.35.x and 4.0.1). If a future FastMCP version moves the second invocation to a callsite that loses async-context propagation, you may see spurious DPoPReplayDetected errors — file an issue.

Cleanup

await auth.client.close() on shutdown.


@authplane/hono — Hono adapter

Quickstart

import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { authplaneHonoAuth, type HonoAuthVariables } from "@authplane/hono";

const auth = await authplaneHonoAuth({
  issuer: "http://localhost:9000",
  resource: "http://localhost:8090/mcp",
  scopes: ["tools/weather"],
  devMode: true,
});

const app = new Hono<{ Variables: HonoAuthVariables }>();

app.get(auth.protectedResourceMetadataPath, auth.protectedResourceMetadataHandler);
app.use("/mcp", auth.bearerAuth);

app.post("/mcp", (c) => {
  const info = c.get("auth");   // VerifiedClaims
  return c.json({ ok: true, clientId: info.clientId, scopes: info.scopes });
});

serve({ fetch: app.fetch, port: 8090 });

Return shape

FieldPurpose
clientNon-nullable AuthplaneClient (Hono’s factory always creates one).
verifierAuthplaneResource.
bearerAuthMiddlewareHandler<{ Variables: HonoAuthVariables }>.
protectedResourceMetadataPathRoute path for the PRM handler.
protectedResourceMetadataHandlerHono Handler serving the PRM.
protectedResourceMetadataThe PRM JSON payload.

Context shape (c.get("auth"))

After bearerAuth runs, c.get("auth") returns VerifiedClaimssub, clientId, scopes, issuer, audience, expiresAt, issuedAt, notBefore, jti, kid, and raw (the full claim object). Call info.requireScope(scope) to enforce a scope inline; the adapter also exports a requireScope("scope") middleware for per-route enforcement.

Runtime portability

@authplane/hono is runtime-agnostic — the adapter itself has no Node-only APIs. Deploy to Node, Bun, Deno, Cloudflare Workers, or wherever Hono runs. The only Node-specific piece in the quickstart above is @hono/node-server; swap it for the runtime’s Hono entrypoint on other platforms.

Cleanup

await auth.client.close() on shutdown.


@authplane/nestjs — NestJS adapter

Quickstart

import "reflect-metadata";
import { Body, Controller, Module, Post, UseGuards } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import {
  AuthInfo, AuthplaneAuthGuard, AuthplaneModule,
  RequireScopes, type VerifiedClaims,
} from "@authplane/nestjs";

@Controller("mcp")
@UseGuards(AuthplaneAuthGuard)
class McpController {
  @Post("tools/weather")
  @RequireScopes("tools/weather")
  async weather(@AuthInfo() info: VerifiedClaims, @Body() body: { city: string }) {
    return {
      content: [{ type: "text", text: `${body.city}: sunny (caller=${info.clientId})` }],
    };
  }
}

@Module({
  imports: [
    AuthplaneModule.forRoot({
      issuer: "http://localhost:9000",
      resource: "http://localhost:8090/mcp",
      scopes: ["tools/weather"],
      devMode: true,
    }),
  ],
  controllers: [McpController],
})
class AppModule {}

const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();       // AuthplaneShutdownHook runs client.close()
await app.listen(8090);

AuthplaneModule.forRoot / .forRootAsync

forRoot(options) for the sync case. forRootAsync({ useFactory | useClass | useExisting, inject, imports }) for the DI case — composes naturally with ConfigModule:

AuthplaneModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (cfg: ConfigService) => ({
    issuer: cfg.getOrThrow("AUTHPLANE_ISSUER"),
    resource: cfg.getOrThrow("AUTHPLANE_RESOURCE"),
    scopes: cfg.get<string[]>("AUTHPLANE_SCOPES") ?? [],
  }),
})

What the module provides (DI tokens)

TokenBound to
AUTHPLANE_CLIENTAuthplaneClient — for token exchange / introspection.
AUTHPLANE_RESOURCEAuthplaneResource — per-resource verifier.
AUTHPLANE_TOKEN_VERIFIERSame instance; separate token so tests can override with a mock.
AuthplaneAuthGuardCanActivate guard (used with @UseGuards).
AuthplaneExceptionFilterRFC 6750 §3-compliant filter.
AuthplaneShutdownHookRuns client.close() on OnApplicationShutdown.

@RequireScopes("scope1", "scope2") decorator + @AuthInfo() param decorator give you clean per-route guarding and access to VerifiedClaims.

Express vs. Fastify

Both platforms work unchanged — the adapter uses an internal RequestAdapter anti-corruption layer over Express + Fastify. @nestjs/platform-express is the default; @nestjs/platform-fastify is a drop-in.

Cleanup

app.enableShutdownHooks() wires the built-in AuthplaneShutdownHook to release timers on app exit. Skip if you’re managing lifecycle manually and call await client.close() yourself.


@authplane/sdk — Core primitives

Framework-agnostic. Use it when:

  • You’re writing a custom transport or framework integration.
  • You need to verify tokens outside a request context (batch, worker).
  • You want programmatic PRM generation without adapter wiring.

Build a client + resource

import { AuthplaneClient, IntrospectionRevocation } from "@authplane/sdk/core";

const client = await AuthplaneClient.create({
  issuer: "https://auth.example.com",
  auth: { clientId: "my-tool", clientSecret: "…" },
  devMode: false,
});

const resource = client.resource({
  resource: "https://mcp.example.com/mcp",
  scopes: ["tools/query"],
  revocationChecker: new IntrospectionRevocation(),
});

// Later, at shutdown:
await client.close();

Verify a token

const claims = await resource.verify(accessToken);
// claims.sub, claims.scopes, claims.audience, claims.dpopProof, claims.expiresAt, ...
claims.requireScope("tools/query");   // throws InsufficientScope if missing

Token exchange

const response = await client.exchange({
  subjectToken: inboundJwt,
  resources: ["https://api.example.com/downstream"],
  scope: "tools/write",
});
// response.accessToken, response.tokenType ("Bearer" | "DPoP"), response.expiresIn

Inbound DPoP

Build a DPoPRequestContext from your framework’s request (method, url, DPoP header values) and pass it to resource.verify(token, dpopContext). Full shape in the core user-guide DPoP section.

Error types

Every failure mode is a typed exception exported from @authplane/sdk/core:

  • TokenMissing, TokenExpired, InvalidSignature, InvalidClaims, InsufficientScope, TokenRevoked
  • DPoPBindingMismatch, DPoPReplayDetected, DPoPProofMissing, InvalidDPoPProof
  • ConsentRequiredError (has consentUrl)
  • MetadataFetchError, JWKSFetchError

The module also exports httpStatus(error) and wwwAuthenticate(error, {resourceMetadataUrl, scope}) helper functions producing the correct RFC 6750 / RFC 9449 challenge for your response.

Cleanup

await client.close(). Idempotent.