Skip to main content

Overview

The MCP specification mandates OAuth 2.1 for transport-level authorization. Implementing it correctly requires PKCE S256, Dynamic Client Registration (RFC 7591), token introspection (RFC 7662), token revocation (RFC 7009), rate limiting, and proper key management. @grantex/mcp-auth handles all of this in a single function call. It creates a fully-compliant OAuth 2.1 authorization server that MCP clients auto-discover via the well-known metadata endpoint.
npm install @grantex/mcp-auth @grantex/sdk
@grantex/mcp-auth works in both managed (Grantex Cloud) and self-hosted modes. The API is identical — just point the Grantex SDK at a different base URL.

Managed vs Self-Hosted

FeatureManaged (Grantex Cloud)Self-Hosted
SetupcreateMcpAuthServer({ grantex, ... })Same API, your infrastructure
Client StoreIn-memory (stateless, horizontal scale)Bring your own (Postgres, Redis, etc.)
JWKSHosted by GrantexYour JWKS endpoint
Token SigningGrantex signs tokensGrantex signs tokens (delegated)
Rate LimitingBuilt-in per-endpoint limitsBuilt-in, configurable
Consent UIGrantex-hosted consent pageCustom consent page via consentUi config
Audit TrailFull audit via Grantex eventsFull audit via Grantex events
Uptime SLA99.9%Your responsibility
ComplianceSOC 2, GDPR readyYour responsibility

Quick Start

1. Create the auth server

import { Grantex } from '@grantex/sdk';
import { createMcpAuthServer } from '@grantex/mcp-auth';

const grantex = new Grantex({
  baseUrl: 'https://grantex-auth-dd4mtrt2gq-uc.a.run.app',
  apiKey: process.env.GRANTEX_API_KEY!,
});

const authServer = await createMcpAuthServer({
  grantex,
  agentId: 'ag_your_mcp_server',
  scopes: ['tools:read', 'tools:execute', 'resources:read'],
  issuer: 'https://your-mcp-server.example.com',
});

await authServer.listen({ port: 3001 });
console.log('MCP Auth Server running on http://localhost:3001');

2. Protect your MCP routes

Use the Express or Hono middleware to validate tokens on every request:
import express from 'express';
import { requireMcpAuth } from '@grantex/mcp-auth/express';
import type { McpAuthRequest } from '@grantex/mcp-auth/express';

const app = express();

app.use('/mcp', requireMcpAuth({
  issuer: 'https://your-mcp-server.example.com',
  scopes: ['tools:execute'],
}));

app.post('/mcp/tools/call', (req: McpAuthRequest, res) => {
  const grant = req.mcpGrant!;
  console.log(`Agent: ${grant.agentDid}`);
  console.log(`Scopes: ${grant.scopes.join(', ')}`);
  res.json({ result: 'tool executed' });
});

app.listen(3000);

3. Verify it works

MCP clients discover your auth server automatically:
curl https://your-mcp-server.example.com/.well-known/oauth-authorization-server

Endpoints

createMcpAuthServer() registers six endpoints on the Fastify instance:
EndpointMethodRFCDescription
/.well-known/oauth-authorization-serverGETRFC 8414Authorization server metadata discovery
/registerPOSTRFC 7591Dynamic Client Registration
/authorizeGETOAuth 2.1Authorization endpoint (PKCE required)
/tokenPOSTOAuth 2.1Token endpoint (authorization_code, refresh_token)
/introspectPOSTRFC 7662Token introspection
/revokePOSTRFC 7009Token revocation

Well-Known Metadata

The metadata endpoint returns a JSON document that MCP clients use to discover all other endpoints:
{
  "issuer": "https://your-mcp-server.example.com",
  "authorization_endpoint": "https://your-mcp-server.example.com/authorize",
  "token_endpoint": "https://your-mcp-server.example.com/token",
  "registration_endpoint": "https://your-mcp-server.example.com/register",
  "introspection_endpoint": "https://your-mcp-server.example.com/introspect",
  "revocation_endpoint": "https://your-mcp-server.example.com/revoke",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": [
    "client_secret_post", "client_secret_basic", "none"
  ],
  "scopes_supported": ["tools:read", "tools:execute", "resources:read"]
}

Token Introspection

Resource servers validate tokens by calling the introspection endpoint:
curl -X POST https://your-mcp-server.example.com/introspect \
  -H "Content-Type: application/json" \
  -d '{"token": "eyJhbGciOiJSUzI1NiIs..."}'
Active token response:
{
  "active": true,
  "scope": "tools:read tools:execute",
  "sub": "user_abc",
  "exp": 1743670800,
  "iat": 1743667200,
  "jti": "grnt_01HXYZ",
  "token_type": "bearer",
  "grantex_agent_did": "did:grantex:ag_01HXYZ",
  "grantex_grant_id": "grnt_01HXYZ"
}

Token Revocation

Revoke tokens when a user logs out or an agent is deauthorized:
curl -X POST https://your-mcp-server.example.com/revoke \
  -u "client_id:client_secret" \
  -H "Content-Type: application/json" \
  -d '{"token": "eyJhbGciOiJSUzI1NiIs..."}'
Per RFC 7009, the endpoint always returns 200 OK, even if the token was already revoked.

Scope Definition

Define the scopes your MCP server supports when creating the auth server:
const authServer = await createMcpAuthServer({
  grantex,
  agentId: 'ag_your_server',
  scopes: [
    'tools:read',       // List available tools
    'tools:execute',    // Call tools
    'resources:read',   // Read resources
    'resources:write',  // Create/update resources
    'prompts:read',     // List prompts
    'prompts:execute',  // Execute prompts
  ],
  issuer: 'https://your-mcp-server.example.com',
});
Scopes are enforced at two levels:
  1. Authorization: Users see the requested scopes on the consent page and can approve or deny
  2. Middleware: The requireMcpAuth() middleware rejects requests that lack required scopes
Customize the consent page shown to users during authorization:
const authServer = await createMcpAuthServer({
  grantex,
  agentId: 'ag_your_server',
  scopes: ['tools:read', 'tools:execute'],
  issuer: 'https://your-mcp-server.example.com',
  consentUi: {
    appName: 'My Calendar MCP',
    appLogo: 'https://example.com/logo.png',
    privacyUrl: 'https://example.com/privacy',
    termsUrl: 'https://example.com/terms',
  },
});
PropertyTypeDescription
appNamestringApplication name displayed on the consent page
appLogostringURL to your application logo
privacyUrlstringLink to your privacy policy
termsUrlstringLink to your terms of service

Lifecycle Hooks

React to authorization events for audit logging, analytics, or downstream notifications:
const authServer = await createMcpAuthServer({
  // ...required fields...
  hooks: {
    onTokenIssued: async (event) => {
      console.log(`Token issued for client ${event.clientId}`);
      console.log(`Scopes: ${event.scopes.join(', ')}`);
      console.log(`Grant ID: ${event.grantId}`);
    },
    onRevocation: async (jti) => {
      console.log(`Token ${jti} was revoked`);
    },
  },
});

Express Middleware

import { requireMcpAuth } from '@grantex/mcp-auth/express';
import type { McpAuthRequest, McpGrant } from '@grantex/mcp-auth/express';

app.use('/mcp', requireMcpAuth({
  issuer: 'https://your-mcp-server.example.com',
  scopes: ['tools:execute'],
}));

requireMcpAuth(options)

OptionTypeRequiredDefaultDescription
issuerstringYesIssuer URL (JWKS fetched from {issuer}/.well-known/jwks.json)
scopesstring[]No[]Required scopes (all must be present)
algorithmsstring[]No['RS256', 'ES256', 'PS256', 'EdDSA']Allowed JWT algorithms

McpGrant (decoded token claims)

PropertyTypeDescription
substringSubject (principal ID)
issstringIssuer
jtistringToken ID
scopesstring[]Granted scopes
agentDidstring?Agent DID
developerIdstring?Developer ID
grantIdstring?Grant ID
delegationDepthnumber?Delegation depth (0 = root)
expnumberExpiry (Unix timestamp)
iatnumberIssued at (Unix timestamp)
rawJWTPayloadAll raw JWT claims

Hono Middleware

import { Hono } from 'hono';
import { requireMcpAuth } from '@grantex/mcp-auth/hono';

const app = new Hono();

app.use('/mcp/*', requireMcpAuth({
  issuer: 'https://your-mcp-server.example.com',
  scopes: ['tools:execute'],
}));

app.post('/mcp/tools/call', (c) => {
  const grant = c.get('mcpGrant');
  return c.json({ agent: grant.agentDid, scopes: grant.scopes });
});

Custom Client Store

By default, client registrations are stored in memory. For production, implement the ClientStore interface:
import type { ClientStore, ClientRegistration } from '@grantex/mcp-auth';

class PostgresClientStore implements ClientStore {
  async get(clientId: string): Promise<ClientRegistration | undefined> {
    const row = await db.query('SELECT * FROM oauth_clients WHERE id = $1', [clientId]);
    return row ?? undefined;
  }

  async set(clientId: string, reg: ClientRegistration): Promise<void> {
    await db.query(
      'INSERT INTO oauth_clients (id, data) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET data = $2',
      [clientId, JSON.stringify(reg)],
    );
  }

  async delete(clientId: string): Promise<boolean> {
    const result = await db.query('DELETE FROM oauth_clients WHERE id = $1', [clientId]);
    return result.rowCount > 0;
  }
}

Certification Program

Grantex offers three certification tiers for MCP servers. See the MCP Certification Guide for full details.
TierKey RequirementsBenefits
BronzeOAuth 2.1 + PKCE, DCR, metadata discoveryRegistry listing
Silver+ Introspection, revocation, consent UI, audit hooksFeatured placement
Gold+ Persistent store, delegation, budget enforcement, conformance passTop placement + badge
Using @grantex/mcp-auth with all features enabled (custom client store, hooks, consent UI) achieves Gold certification automatically.

Configuration Reference

McpAuthConfig

PropertyTypeRequiredDefaultDescription
grantexGrantexYesGrantex SDK client instance
agentIdstringYesAgent ID for Grantex authorization
scopesstring[]YesScopes to request from Grantex
issuerstringYesBase URL for this auth server
allowedRedirectUrisstring[]No[]Allowed redirect URIs (empty = all)
allowedResourcesstring[]No[]Allowed resource indicators (RFC 8707)
clientStoreClientStoreNoInMemoryClientStoreCustom client registration store
codeExpirationSecondsnumberNo600Authorization code TTL
consentUiobjectNoConsent UI customization
hooksobjectNoLifecycle hooks

Rate Limits

Default per-endpoint rate limits:
EndpointMax RequestsWindow
/authorize101 minute
/token201 minute
/introspect301 minute
/revoke201 minute
All others1001 minute

Security Considerations

@grantex/mcp-auth enforces OAuth 2.1 security requirements:
  • PKCE S256 is mandatory. The plain method and implicit grant are rejected.
  • No password grant. The password grant type is not supported.
  • No implicit grant. Only response_type=code is accepted.
  • Authorization codes are single-use. Replayed codes are rejected.
  • HS256 rejected. Only asymmetric algorithms (RS256, ES256, PS256, EdDSA) are accepted.
  • Client secrets are generated using crypto.randomBytes(32).
  • JWKS verification uses the jose library with remote key set fetching and caching.