TypeScript
TL;DR — Five packages.
@authplane/mcptargets the official MCP TypeScript SDK.@authplane/fastmcptargets punkpeye/fastmcp.@authplane/honoand@authplane/nestjstarget their respective web frameworks.@authplane/sdkis 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
Options (highlights):
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
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
Context shape (c.get("auth"))
After bearerAuth runs, c.get("auth") returns VerifiedClaims — sub, 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)
@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,TokenRevokedDPoPBindingMismatch,DPoPReplayDetected,DPoPProofMissing,InvalidDPoPProofConsentRequiredError(hasconsentUrl)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.
Related
- SDKs overview — pick between adapters
- Quickstart —
@authplane/fastmcpend-to-end in 10 minutes - Guides: Enable DPoP end-to-end
- Guides: Wire up the Token Vault
- Full user-guides in the ts-sdk repo: