[v2] Decouple server from express and hono - http framework-agnostic MCP server (#1326)

This commit is contained in:
Konstantin Konstantinov 2026-01-16 12:13:25 +02:00 • committed by GitHub
commit f495077e08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 8193 additions and 15757 deletions

250
examples/shared/src/auth.ts Normal file
View file

@ -0,0 +1,250 @@
/**
* Better Auth configuration for MCP demo servers
*
* DEMO ONLY - NOT FOR PRODUCTION
*
* This configuration uses in-memory SQLite and auto-approves all logins.
* For production use, configure a proper database and authentication flow.
*/
import { randomBytes } from 'node:crypto';
import type { BetterAuthOptions } from 'better-auth';
import { betterAuth } from 'better-auth';
import { mcp } from 'better-auth/plugins';
import Database from 'better-sqlite3';
// Generate a random password for the demo user (new each time the server starts)
const DEMO_PASSWORD = randomBytes(16).toString('base64url');
// Create the in-memory database once (module-level singleton)
// This avoids the type export issue and ensures the same DB is used
let _db: InstanceType<typeof Database> | null = null;
function getDatabase(): InstanceType<typeof Database> {
if (!_db) {
_db = new Database(':memory:');
initializeSchema(_db);
}
return _db;
}
/**
* Initialize the database schema for better-auth + MCP plugin.
* This creates all required tables for the demo.
*
* Schema based on:
* - https://www.better-auth.com/docs/concepts/database#core-schema
* - https://www.better-auth.com/docs/plugins/oidc-provider#schema
*/
function initializeSchema(db: InstanceType<typeof Database>): void {
// Core better-auth tables
db.exec(`
CREATE TABLE IF NOT EXISTS user (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
emailVerified INTEGER NOT NULL DEFAULT 0,
image TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS session (
id TEXT PRIMARY KEY,
token TEXT NOT NULL UNIQUE,
expiresAt TEXT NOT NULL,
ipAddress TEXT,
userAgent TEXT,
userId TEXT NOT NULL REFERENCES user(id),
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS account (
id TEXT PRIMARY KEY,
accountId TEXT NOT NULL,
providerId TEXT NOT NULL,
userId TEXT NOT NULL REFERENCES user(id),
accessToken TEXT,
refreshToken TEXT,
idToken TEXT,
accessTokenExpiresAt TEXT,
refreshTokenExpiresAt TEXT,
scope TEXT,
password TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS verification (
id TEXT PRIMARY KEY,
identifier TEXT NOT NULL,
value TEXT NOT NULL,
expiresAt TEXT NOT NULL,
createdAt TEXT,
updatedAt TEXT
);
`);
// OIDC/MCP plugin tables
db.exec(`
CREATE TABLE IF NOT EXISTS oauthApplication (
id TEXT PRIMARY KEY,
name TEXT,
icon TEXT,
metadata TEXT,
clientId TEXT NOT NULL UNIQUE,
clientSecret TEXT,
redirectUrls TEXT NOT NULL,
type TEXT NOT NULL,
disabled INTEGER NOT NULL DEFAULT 0,
userId TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS oauthAccessToken (
id TEXT PRIMARY KEY,
accessToken TEXT NOT NULL UNIQUE,
refreshToken TEXT UNIQUE,
accessTokenExpiresAt TEXT NOT NULL,
refreshTokenExpiresAt TEXT,
clientId TEXT NOT NULL,
userId TEXT,
scopes TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS oauthRefreshToken (
id TEXT PRIMARY KEY,
refreshToken TEXT NOT NULL UNIQUE,
accessTokenId TEXT NOT NULL,
expiresAt TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS oauthAuthorizationCode (
id TEXT PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
clientId TEXT NOT NULL,
userId TEXT,
scopes TEXT NOT NULL,
redirectURI TEXT NOT NULL,
codeChallenge TEXT,
codeChallengeMethod TEXT,
expiresAt TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS oauthConsent (
id TEXT PRIMARY KEY,
clientId TEXT NOT NULL,
userId TEXT NOT NULL,
scopes TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
consentGiven INTEGER NOT NULL DEFAULT 0
);
`);
console.log('[Auth] In-memory database schema initialized');
console.log('[Auth] ========================================');
console.log('[Auth] Demo user credentials (auto-login):');
console.log(`[Auth] Email: ${DEMO_USER_CREDENTIALS.email}`);
console.log(`[Auth] Password: ${DEMO_USER_CREDENTIALS.password}`);
console.log('[Auth] ========================================');
}
/**
* Demo user credentials for auto-login.
* Password is randomly generated each time the server starts.
* Used by authServer.ts to create and sign in the demo user.
*/
export const DEMO_USER_CREDENTIALS = {
email: 'demo@example.com',
password: DEMO_PASSWORD,
name: 'Demo User'
};
export interface CreateDemoAuthOptions {
baseURL: string;
resource?: string;
loginPage?: string;
demoMode: boolean;
}
/**
* Creates a better-auth instance configured for MCP OAuth demo.
*
* @param options - Configuration options
* @param options.baseURL - The base URL for the auth server (e.g., http://localhost:3001)
* @param options.resource - The MCP resource server URL (for protected resource metadata)
* @param options.loginPage - Path to login page (defaults to /sign-in)
*
* @see https://www.better-auth.com/docs/plugins/mcp
*/
export function createDemoAuth(options: CreateDemoAuthOptions) {
const { baseURL, resource, loginPage = '/sign-in', demoMode } = options;
// Use in-memory SQLite database for demo purposes
// Note: All data is lost on restart - demo only!
const db = getDatabase();
// MCP plugin configuration
const mcpPlugin = mcp({
loginPage,
resource,
oidcConfig: {
loginPage,
codeExpiresIn: 600, // 10 minutes
accessTokenExpiresIn: 3600, // 1 hour
refreshTokenExpiresIn: 604800, // 7 days
defaultScope: 'openid',
scopes: ['openid', 'profile', 'email', 'offline_access'],
allowDynamicClientRegistration: true,
metadata: {
scopes_supported: ['openid', 'profile', 'email', 'offline_access']
}
}
});
return betterAuth({
baseURL,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
database: db as any, // Type cast to avoid exposing better-sqlite3 in exported types
trustedOrigins: [baseURL.toString()],
// Basic email+password for demo
emailAndPassword: {
enabled: true,
requireEmailVerification: false
},
plugins: [mcpPlugin],
// Enable verbose logging for demo/debugging
logger: demoMode
? {
disabled: false,
level: 'debug',
log: (level, message, ...args) => {
const timestamp = new Date().toISOString();
const prefix = `[Auth ${level.toUpperCase()}]`;
if (args.length > 0) {
console.log(`${timestamp} ${prefix} ${message}`, ...args);
} else {
console.log(`${timestamp} ${prefix} ${message}`);
}
}
}
: undefined
} satisfies BetterAuthOptions);
}
/**
* Type for the auth instance returned by createDemoAuth.
* Note: Due to plugin type inference complexity, we use a generic type.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type DemoAuth = ReturnType<typeof createDemoAuth>;

View file

@ -0,0 +1,87 @@
/**
* Auth Middleware for MCP Demo Servers
*
* 🚨 DEMO ONLY - NOT FOR PRODUCTION
*
* This provides bearer auth middleware for MCP servers.
*/
import type { NextFunction, Request, Response } from 'express';
import { verifyAccessToken } from './authServer.js';
export interface RequireBearerAuthOptions {
requiredScopes?: string[];
resourceMetadataUrl?: URL;
strictResource?: boolean;
expectedResource?: URL;
}
/**
* Express middleware that requires a valid Bearer token.
* Sets `req.app.locals.auth` on success.
*/
export function requireBearerAuth(
options: RequireBearerAuthOptions = {}
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
const { requiredScopes = [], resourceMetadataUrl, strictResource = false, expectedResource } = options;
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
const wwwAuthenticate = resourceMetadataUrl ? `Bearer resource_metadata="${resourceMetadataUrl.toString()}"` : 'Bearer';
res.set('WWW-Authenticate', wwwAuthenticate);
res.status(401).json({
error: 'unauthorized',
error_description: 'Missing or invalid Authorization header'
});
return;
}
const token = authHeader.slice(7); // Remove 'Bearer ' prefix
try {
const authInfo = await verifyAccessToken(token, {
strictResource,
expectedResource
});
// Check required scopes
if (requiredScopes.length > 0) {
const hasAllScopes = requiredScopes.every(scope => authInfo.scopes.includes(scope));
if (!hasAllScopes) {
res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${requiredScopes.join(', ')}`
});
return;
}
}
req.app.locals.auth = authInfo;
next();
} catch (error) {
const wwwAuthenticate = resourceMetadataUrl
? `Bearer error="invalid_token", resource_metadata="${resourceMetadataUrl.toString()}"`
: 'Bearer error="invalid_token"';
res.set('WWW-Authenticate', wwwAuthenticate);
res.status(401).json({
error: 'invalid_token',
error_description: error instanceof Error ? error.message : 'Invalid token'
});
}
};
}
/**
* Helper to get the protected resource metadata URL from a server URL.
*/
export function getOAuthProtectedResourceMetadataUrl(serverUrl: URL): URL {
const metadataUrl = new URL(serverUrl);
// Insert well-known between host and path per RFC 9728 Section 3
metadataUrl.pathname = `/.well-known/oauth-protected-resource${serverUrl.pathname}`;
return metadataUrl;
}

View file

@ -0,0 +1,310 @@
/**
* Better Auth Server Setup for MCP Demo
*
* DEMO ONLY - NOT FOR PRODUCTION
*
* This creates a standalone OAuth Authorization Server using better-auth
* that MCP clients can use to obtain access tokens.
*
* See: https://www.better-auth.com/docs/plugins/mcp
*/
import { toNodeHandler } from 'better-auth/node';
import { oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata } from 'better-auth/plugins';
import type { Request, Response as ExpressResponse, Router } from 'express';
import express from 'express';
import type { DemoAuth } from './auth.js';
import { createDemoAuth, DEMO_USER_CREDENTIALS } from './auth.js';
export interface SetupAuthServerOptions {
authServerUrl: URL;
mcpServerUrl: URL;
strictResource?: boolean;
/**
* Examples should be used for **demo** only and not for production purposes, however this mode disables some logging and other features.
*/
demoMode: boolean;
}
// Store auth instance globally so it can be used for token verification
let globalAuth: DemoAuth | null = null;
let demoUserCreated = false;
/**
* Gets the global auth instance (must call setupAuthServer first)
*/
export function getAuth(): DemoAuth {
if (!globalAuth) {
throw new Error('Auth not initialized. Call setupAuthServer first.');
}
return globalAuth;
}
/**
* Ensures the demo user exists by calling signUpEmail (creates user with proper password hash)
* Returns true if successful, false if user already exists (which is fine)
*/
async function ensureDemoUserExists(auth: DemoAuth): Promise<void> {
if (demoUserCreated) return;
try {
// Try to sign up the demo user
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (auth.api as any).signUpEmail({
body: {
email: DEMO_USER_CREDENTIALS.email,
password: DEMO_USER_CREDENTIALS.password,
name: DEMO_USER_CREDENTIALS.name
}
});
console.log('[Auth] Demo user created via signUpEmail');
demoUserCreated = true;
} catch (error) {
// User might already exist, which is fine
const message = error instanceof Error ? error.message : String(error);
if (message.includes('already') || message.includes('exists') || message.includes('unique')) {
console.log('[Auth] Demo user already exists');
demoUserCreated = true;
} else {
console.error('[Auth] Failed to create demo user:', error);
throw error;
}
}
}
/**
* Sets up and starts the OAuth Authorization Server on a separate port.
*
* @param options - Server configuration
*/
export function setupAuthServer(options: SetupAuthServerOptions): void {
const { authServerUrl, mcpServerUrl, demoMode } = options;
// Create better-auth instance with MCP plugin
const auth = createDemoAuth({
baseURL: authServerUrl.toString().replace(/\/$/, ''),
resource: mcpServerUrl.toString(),
loginPage: '/sign-in',
demoMode: demoMode
});
// Store globally for token verification
globalAuth = auth;
// Create Express app for auth server
const authApp = express();
// Enable CORS for all origins (demo only) - must be before other middleware
authApp.use((_req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Expose-Headers', 'WWW-Authenticate');
if (_req.method === 'OPTIONS') {
res.sendStatus(200);
return;
}
next();
});
// Request logging middleware for OAuth endpoints
authApp.use('/api/auth', (req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`${timestamp} [Auth Request] ${req.method} ${req.url}`);
if (req.method === 'POST') {
console.log(`${timestamp} [Auth Request] Content-Type: ${req.headers['content-type']}`);
}
if (demoMode) {
// Log response when it finishes
const originalSend = res.send.bind(res);
res.send = function (body) {
console.log(`${timestamp} [Auth Response] ${res.statusCode} ${req.url}`);
if (res.statusCode >= 400 && body) {
try {
const parsed = typeof body === 'string' ? JSON.parse(body) : body;
console.log(`${timestamp} [Auth Response] Error:`, parsed);
} catch {
// Not JSON, log as-is if short
if (typeof body === 'string' && body.length < 200) {
console.log(`${timestamp} [Auth Response] Body: ${body}`);
}
}
}
return originalSend(body);
};
}
next();
});
// Mount better-auth handler BEFORE body parsers
// toNodeHandler reads the raw request body, so Express must not consume it first
authApp.all('/api/auth/{*splat}', toNodeHandler(auth));
// OAuth metadata endpoints using better-auth's built-in handlers
authApp.get('/.well-known/oauth-authorization-server', toNodeHandler(oAuthDiscoveryMetadata(auth)));
// Body parsers for non-better-auth routes (like /sign-in)
authApp.use(express.json());
authApp.use(express.urlencoded({ extended: true }));
// Auto-login page that creates a real better-auth session
// This simulates a user logging in and approving the OAuth request
authApp.get('/sign-in', async (req: Request, res: ExpressResponse) => {
// Get the OAuth authorization parameters from the query string
const queryParams = new URLSearchParams(req.query as Record<string, string>);
const redirectUri = queryParams.get('redirect_uri');
const clientId = queryParams.get('client_id');
if (!redirectUri || !clientId) {
res.status(400).send(`
<!DOCTYPE html>
<html>
<head><title>Demo Login</title></head>
<body>
<h1>Demo OAuth Server</h1>
<p>Missing required OAuth parameters. This page should be accessed via OAuth flow.</p>
</body>
</html>
`);
return;
}
try {
// Ensure demo user exists (creates with proper password hash)
await ensureDemoUserExists(auth);
// Create a session using better-auth's signIn API with asResponse to get Set-Cookie headers
const signInResponse = await auth.api.signInEmail({
body: {
email: DEMO_USER_CREDENTIALS.email,
password: DEMO_USER_CREDENTIALS.password
},
asResponse: true
});
console.log('[Auth] Sign-in response status:', signInResponse.status);
// Forward all Set-Cookie headers from better-auth's response
const setCookieHeaders = signInResponse.headers.getSetCookie();
console.log('[Auth] Set-Cookie headers:', setCookieHeaders);
for (const cookie of setCookieHeaders) {
res.append('Set-Cookie', cookie);
}
console.log(`[Auth Server] Session created, redirecting to authorize`);
// Redirect to the authorization endpoint
const authorizeUrl = new URL('/api/auth/mcp/authorize', authServerUrl);
authorizeUrl.search = queryParams.toString();
res.redirect(authorizeUrl.toString());
} catch (error) {
console.error('[Auth Server] Failed to create session:', error);
res.status(500).send(`
<!DOCTYPE html>
<html>
<head><title>Demo Login Error</title></head>
<body>
<h1>Demo OAuth Server - Error</h1>
<p>Failed to create demo session: ${error instanceof Error ? error.message : 'Unknown error'}</p>
<pre>${error instanceof Error ? error.stack : ''}</pre>
</body>
</html>
`);
}
});
// Start the auth server
const authPort = parseInt(authServerUrl.port, 10);
authApp.listen(authPort, (error?: Error) => {
if (error) {
console.error('Failed to start auth server:', error);
process.exit(1);
}
console.log(`OAuth Authorization Server listening on port ${authPort}`);
console.log(` Authorization: ${authServerUrl}api/auth/mcp/authorize`);
console.log(` Token: ${authServerUrl}api/auth/mcp/token`);
console.log(` Metadata: ${authServerUrl}.well-known/oauth-authorization-server`);
});
}
/**
* Creates an Express router that serves OAuth Protected Resource Metadata
* on the MCP server using better-auth's built-in handler.
*
* This is needed because MCP clients discover the auth server by first
* fetching protected resource metadata from the MCP server.
*
* See: https://www.better-auth.com/docs/plugins/mcp#oauth-protected-resource-metadata
*/
export function createProtectedResourceMetadataRouter(): Router {
const auth = getAuth();
const router = express.Router();
// Serve at the standard well-known path
router.get('/.well-known/oauth-protected-resource', toNodeHandler(oAuthProtectedResourceMetadata(auth)));
return router;
}
/**
* Verifies an access token using better-auth's getMcpSession.
* This can be used by MCP servers to validate tokens.
*/
export async function verifyAccessToken(
token: string,
options?: { strictResource?: boolean; expectedResource?: URL }
): Promise<{
token: string;
clientId: string;
scopes: string[];
expiresAt: number;
}> {
const auth = getAuth();
try {
// Create a mock request with the Authorization header
const headers = new Headers();
headers.set('Authorization', `Bearer ${token}`);
// Use better-auth's getMcpSession API
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const session = await (auth.api as any).getMcpSession({
headers
});
if (!session) {
throw new Error('Invalid token');
}
// OAuthAccessToken has:
// - accessToken, refreshToken: string
// - accessTokenExpiresAt, refreshTokenExpiresAt: Date
// - clientId, userId: string
// - scopes: string (space-separated)
const scopes = typeof session.scopes === 'string' ? session.scopes.split(' ') : ['openid'];
const expiresAt = session.accessTokenExpiresAt
? Math.floor(new Date(session.accessTokenExpiresAt).getTime() / 1000)
: Math.floor(Date.now() / 1000) + 3600;
// Note: better-auth's OAuthAccessToken doesn't have a resource field
// Resource validation would need to be done at a different layer
if (options?.strictResource && options.expectedResource) {
// For now, we skip resource validation as it's not in the session
// In production, you'd store and validate this separately
console.warn('[Auth] Resource validation requested but not available in better-auth session');
}
return {
token,
clientId: session.clientId,
scopes,
expiresAt
};
} catch (error) {
throw new Error(`Token verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}

View file

@ -1,254 +0,0 @@
import { randomUUID } from 'node:crypto';
import type {
AuthInfo,
AuthorizationParams,
OAuthClientInformationFull,
OAuthMetadata,
OAuthRegisteredClientsStore,
OAuthServerProvider,
OAuthTokens
} from '@modelcontextprotocol/server';
import { createOAuthMetadata, InvalidRequestError, mcpAuthRouter, resourceUrlFromServerUrl } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
import express from 'express';
export class DemoInMemoryClientsStore implements OAuthRegisteredClientsStore {
private clients = new Map<string, OAuthClientInformationFull>();
async getClient(clientId: string) {
return this.clients.get(clientId);
}
async registerClient(clientMetadata: OAuthClientInformationFull) {
this.clients.set(clientMetadata.client_id, clientMetadata);
return clientMetadata;
}
}
/**
* 🚨 DEMO ONLY - NOT FOR PRODUCTION
*
* This example demonstrates MCP OAuth flow but lacks some of the features required for production use,
* for example:
* - Persistent token storage
* - Rate limiting
*/
export class DemoInMemoryAuthProvider implements OAuthServerProvider {
clientsStore = new DemoInMemoryClientsStore();
private codes = new Map<
string,
{
params: AuthorizationParams;
client: OAuthClientInformationFull;
}
>();
private tokens = new Map<string, AuthInfo>();
constructor(private validateResource?: (resource?: URL) => boolean) {}
async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void> {
const code = randomUUID();
const searchParams = new URLSearchParams({
code
});
if (params.state !== undefined) {
searchParams.set('state', params.state);
}
this.codes.set(code, {
client,
params
});
// Simulate a user login
// Set a secure HTTP-only session cookie with authorization info
if (res.cookie) {
const authCookieData = {
userId: 'demo_user',
name: 'Demo User',
timestamp: Date.now()
};
res.cookie('demo_session', JSON.stringify(authCookieData), {
httpOnly: true,
secure: false, // In production, this should be true
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000, // 24 hours - for demo purposes
path: '/' // Available to all routes
});
}
if (!client.redirect_uris.includes(params.redirectUri)) {
throw new InvalidRequestError('Unregistered redirect_uri');
}
const targetUrl = new URL(params.redirectUri);
targetUrl.search = searchParams.toString();
res.redirect(targetUrl.toString());
}
async challengeForAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string): Promise<string> {
// Store the challenge with the code data
const codeData = this.codes.get(authorizationCode);
if (!codeData) {
throw new Error('Invalid authorization code');
}
return codeData.params.codeChallenge;
}
async exchangeAuthorizationCode(
client: OAuthClientInformationFull,
authorizationCode: string,
// Note: code verifier is checked in token.ts by default
// it's unused here for that reason.
_codeVerifier?: string
): Promise<OAuthTokens> {
const codeData = this.codes.get(authorizationCode);
if (!codeData) {
throw new Error('Invalid authorization code');
}
if (codeData.client.client_id !== client.client_id) {
throw new Error(`Authorization code was not issued to this client, ${codeData.client.client_id} != ${client.client_id}`);
}
if (this.validateResource && !this.validateResource(codeData.params.resource)) {
throw new Error(`Invalid resource: ${codeData.params.resource}`);
}
this.codes.delete(authorizationCode);
const token = randomUUID();
const tokenData = {
token,
clientId: client.client_id,
scopes: codeData.params.scopes || [],
expiresAt: Date.now() + 3600000, // 1 hour
resource: codeData.params.resource,
type: 'access'
};
this.tokens.set(token, tokenData);
return {
access_token: token,
token_type: 'bearer',
expires_in: 3600,
scope: (codeData.params.scopes || []).join(' ')
};
}
async exchangeRefreshToken(
_client: OAuthClientInformationFull,
_refreshToken: string,
_scopes?: string[],
_resource?: URL
): Promise<OAuthTokens> {
throw new Error('Not implemented for example demo');
}
async verifyAccessToken(token: string): Promise<AuthInfo> {
const tokenData = this.tokens.get(token);
if (!tokenData || !tokenData.expiresAt || tokenData.expiresAt < Date.now()) {
throw new Error('Invalid or expired token');
}
return {
token,
clientId: tokenData.clientId,
scopes: tokenData.scopes,
expiresAt: Math.floor(tokenData.expiresAt / 1000),
resource: tokenData.resource
};
}
}
export const setupAuthServer = ({
authServerUrl,
mcpServerUrl,
strictResource
}: {
authServerUrl: URL;
mcpServerUrl: URL;
strictResource: boolean;
}): OAuthMetadata => {
// Create separate auth server app
// NOTE: This is a separate app on a separate port to illustrate
// how to separate an OAuth Authorization Server from a Resource
// server in the SDK. The SDK is not intended to be provide a standalone
// authorization server.
const validateResource = strictResource
? (resource?: URL) => {
if (!resource) return false;
const expectedResource = resourceUrlFromServerUrl(mcpServerUrl);
return resource.toString() === expectedResource.toString();
}
: undefined;
const provider = new DemoInMemoryAuthProvider(validateResource);
const authApp = express();
authApp.use(express.json());
// For introspection requests
authApp.use(express.urlencoded());
// Add OAuth routes to the auth server
// NOTE: this will also add a protected resource metadata route,
// but it won't be used, so leave it.
authApp.use(
mcpAuthRouter({
provider,
issuerUrl: authServerUrl,
scopesSupported: ['mcp:tools']
})
);
authApp.post('/introspect', async (req: Request, res: Response) => {
try {
const { token } = req.body;
if (!token) {
res.status(400).json({ error: 'Token is required' });
return;
}
const tokenInfo = await provider.verifyAccessToken(token);
res.json({
active: true,
client_id: tokenInfo.clientId,
scope: tokenInfo.scopes.join(' '),
exp: tokenInfo.expiresAt,
aud: tokenInfo.resource
});
return;
} catch (error) {
res.status(401).json({
active: false,
error: 'Unauthorized',
error_description: `Invalid token: ${error}`
});
}
});
const auth_port = authServerUrl.port;
// Start the auth server
authApp.listen(auth_port, error => {
if (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
console.log(`OAuth Authorization Server listening on port ${auth_port}`);
});
// Note: we could fetch this from the server, but then we end up
// with some top level async which gets annoying.
const oauthMetadata: OAuthMetadata = createOAuthMetadata({
provider,
issuerUrl: authServerUrl,
scopesSupported: ['mcp:tools']
});
oauthMetadata.introspection_endpoint = new URL('/introspect', authServerUrl).href;
return oauthMetadata;
};

View file

@ -1 +1,11 @@
export * from './demoInMemoryOAuthProvider.js';
// Auth configuration
export type { CreateDemoAuthOptions, DemoAuth } from './auth.js';
export { createDemoAuth } from './auth.js';
// Auth middleware
export type { RequireBearerAuthOptions } from './authMiddleware.js';
export { getOAuthProtectedResourceMetadataUrl, requireBearerAuth } from './authMiddleware.js';
// Auth server setup
export type { SetupAuthServerOptions } from './authServer.js';
export { createProtectedResourceMetadataRouter, getAuth, setupAuthServer, verifyAccessToken } from './authServer.js';