npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@grantex/mcp-auth

v2.0.1

Published

OAuth 2.1 + PKCE authorization server for MCP servers, powered by Grantex

Downloads

69

Readme

@grantex/mcp-auth

npm version License npm downloads

OAuth 2.1 + PKCE authorization server for MCP servers, powered by Grantex.

Turn any Model Context Protocol server into a fully-compliant OAuth 2.1 authorization server in under 10 lines of code. Built on the Grantex delegated authorization protocol, @grantex/mcp-auth handles token issuance, introspection, revocation, Dynamic Client Registration (DCR), and PKCE -- so you can focus on building tools, not auth infrastructure.

Why @grantex/mcp-auth?

The MCP specification mandates OAuth 2.1 for transport-level auth. Implementing it correctly is hard:

  • PKCE S256 is mandatory (no plain, no implicit flow)
  • Dynamic Client Registration (RFC 7591) for zero-config MCP clients
  • Token introspection (RFC 7662) for resource servers to validate tokens
  • Token revocation (RFC 7009) for secure logout
  • Rate limiting on all sensitive endpoints
  • Grantex integration for delegated, auditable, scope-controlled authorization

@grantex/mcp-auth handles all of this out of the box, with a single function call.

Installation

npm install @grantex/mcp-auth

Quick Start

1. Create the 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',
});

2. Start listening

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

3. Protect your MCP server routes

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

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

That's it. MCP clients can now discover your auth server via /.well-known/oauth-authorization-server, register dynamically, and obtain tokens.

Endpoints

createMcpAuthServer registers the following endpoints on the Fastify instance:

| Endpoint | Method | RFC | Description | |----------|--------|-----|-------------| | /.well-known/oauth-authorization-server | GET | RFC 8414 | Authorization server metadata discovery | | /register | POST | RFC 7591 | Dynamic Client Registration | | /authorize | GET | OAuth 2.1 | Authorization endpoint (PKCE required) | | /token | POST | OAuth 2.1 | Token endpoint (authorization_code, refresh_token) | | /introspect | POST | RFC 7662 | Token introspection | | /revoke | POST | RFC 7009 | Token revocation |

API Reference

createMcpAuthServer(config)

Creates and returns a Fastify instance with all OAuth 2.1 endpoints registered.

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

const server = await createMcpAuthServer(config);

McpAuthConfig

| Property | Type | Required | Default | Description | |----------|------|----------|---------|-------------| | grantex | Grantex | Yes | - | Grantex SDK client instance | | agentId | string | Yes | - | Agent ID for Grantex authorization | | scopes | string[] | Yes | - | Scopes to request from Grantex | | issuer | string | Yes | - | Base URL for this auth server (used in metadata) | | allowedRedirectUris | string[] | No | [] | Allowed redirect URIs (empty = all allowed) | | allowedResources | string[] | No | [] | Allowed resource indicators (RFC 8707) | | clientStore | ClientStore | No | InMemoryClientStore | Custom client registration store | | codeExpirationSeconds | number | No | 600 | Authorization code TTL in seconds | | consentUi | object | No | - | Consent UI customization (appName, appLogo, privacyUrl, termsUrl) | | hooks | object | No | - | Lifecycle hooks (onTokenIssued, onRevocation) |

Consent UI

Customize the consent page shown to users:

const server = await createMcpAuthServer({
  // ...required fields...
  consentUi: {
    appName: 'My MCP Server',
    appLogo: 'https://example.com/logo.png',
    privacyUrl: 'https://example.com/privacy',
    termsUrl: 'https://example.com/terms',
  },
});

Lifecycle Hooks

React to authorization events:

const server = 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}`);
      // Send to your analytics, audit log, etc.
    },
    onRevocation: async (jti) => {
      console.log(`Token ${jti} was revoked`);
      // Invalidate cached sessions, notify downstream, etc.
    },
  },
});

Custom Client Store

By default, client registrations are stored in memory. For production, implement the ClientStore interface backed by your database:

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;
  }
}

const server = await createMcpAuthServer({
  // ...
  clientStore: new PostgresClientStore(),
});

Express.js Middleware

Protect your Express routes with JWT validation:

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

const app = express();

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

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

app.listen(3000);

requireMcpAuth(options) (Express)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | issuer | string | Yes | - | Issuer URL (JWKS fetched from {issuer}/.well-known/jwks.json) | | scopes | string[] | No | [] | Required scopes (all must be present) | | algorithms | string[] | No | ['RS256', 'ES256', 'PS256', 'EdDSA'] | Allowed JWT algorithms |

McpGrant (decoded token claims)

| Property | Type | Description | |----------|------|-------------| | sub | string | Subject (principal ID) | | iss | string | Issuer | | jti | string | Token ID | | scopes | string[] | Granted scopes | | agentDid | string? | Agent DID | | developerId | string? | Developer ID | | grantId | string? | Grant ID | | delegationDepth | number? | Delegation depth (0 = root) | | exp | number | Expiry (Unix timestamp) | | iat | number | Issued at (Unix timestamp) | | raw | JWTPayload | All raw JWT claims |

Hono Middleware

Same protection for Hono applications:

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

const app = new Hono();

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

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

export default app;

Token Introspection (RFC 7662)

Resource servers can 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..."}'

Response for a valid token:

{
  "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_delegation_depth": 0,
  "grantex_grant_id": "grnt_01HXYZ"
}

Response for an invalid/expired token:

{
  "active": false
}

Client Authentication

Introspection optionally accepts Basic auth for client identification:

curl -X POST https://your-mcp-server.example.com/introspect \
  -u "client_id:client_secret" \
  -H "Content-Type: application/json" \
  -d '{"token": "eyJhbGciOiJSUzI1NiIs..."}'

Token Revocation (RFC 7009)

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 or unknown.

Managed vs Self-Hosted

| Feature | Managed (Grantex Cloud) | Self-Hosted | |---------|------------------------|-------------| | Setup | createMcpAuthServer({ grantex, ... }) | Same API, your infrastructure | | Client Store | In-memory (stateless, horizontal scale) | Bring your own (Postgres, Redis, etc.) | | JWKS | Hosted by Grantex | Your JWKS endpoint | | Token Signing | Grantex signs tokens | Grantex signs tokens (delegated) | | Rate Limiting | Built-in per-endpoint limits | Built-in, configurable | | Consent UI | Grantex-hosted consent page | Custom consent page via consentUi config | | Audit Trail | Full audit via Grantex events | Full audit via Grantex events | | Uptime SLA | 99.9% | Your responsibility | | Compliance | SOC 2, GDPR ready | Your responsibility |

Managed Mode (Recommended)

Use the Grantex Cloud auth service. Zero infrastructure to manage:

const server = await createMcpAuthServer({
  grantex: new Grantex({
    baseUrl: 'https://grantex-auth-dd4mtrt2gq-uc.a.run.app',
    apiKey: process.env.GRANTEX_API_KEY!,
  }),
  agentId: 'ag_your_server',
  scopes: ['tools:read', 'tools:execute'],
  issuer: 'https://your-domain.example.com',
});

Self-Hosted Mode

Run your own Grantex auth service and point the SDK at it:

const server = await createMcpAuthServer({
  grantex: new Grantex({
    baseUrl: 'https://auth.your-company.internal',
    apiKey: process.env.GRANTEX_API_KEY!,
  }),
  agentId: 'ag_internal_server',
  scopes: ['internal:read', 'internal:write'],
  issuer: 'https://auth.your-company.internal',
  clientStore: new PostgresClientStore(),  // Persistent storage
});

MCP Server Certification

Grantex offers a certification program for MCP servers that implement OAuth 2.1 correctly:

Bronze

  • OAuth 2.1 + PKCE S256 for all flows
  • Dynamic Client Registration (RFC 7591)
  • Server metadata discovery (RFC 8414)
  • Rate limiting on token and authorize endpoints

Silver

All Bronze requirements, plus:

  • Token introspection (RFC 7662)
  • Token revocation (RFC 7009)
  • Consent UI customization
  • Lifecycle hooks for audit logging

Gold

All Silver requirements, plus:

  • Custom client store (persistent, production-grade)
  • Resource indicators (RFC 8707)
  • Delegation support (Grantex SPEC Section 9)
  • Budget enforcement
  • Full Grantex conformance suite pass

Using @grantex/mcp-auth with all features enabled gets you to Gold certification automatically.

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 for token verification.
  • Rate limiting is applied to all endpoints (configurable per-endpoint).
  • Client secrets are generated using crypto.randomBytes(32).
  • JWKS verification uses the jose library with remote key set fetching and caching.

Algorithm Policy

The introspection and middleware endpoints only accept tokens signed with:

  • RS256 (RSA PKCS#1 v1.5)
  • ES256 (ECDSA P-256)
  • PS256 (RSA-PSS)
  • EdDSA (Ed25519)

Symmetric algorithms (HS256, HS384, HS512) are explicitly rejected.

Discovery

MCP clients discover your auth server via the well-known metadata endpoint:

curl https://your-mcp-server.example.com/.well-known/oauth-authorization-server
{
  "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"],
  "introspection_endpoint_auth_methods_supported": ["client_secret_basic", "none"],
  "revocation_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
  "scopes_supported": ["tools:read", "tools:execute", "resources:read"],
  "grantex_extensions": {
    "consent_ui": "https://your-mcp-server.example.com/consent",
    "audit_stream": "https://your-mcp-server.example.com/events/stream"
  }
}

Full Example: MCP Server with Auth

import { Grantex } from '@grantex/sdk';
import { createMcpAuthServer } from '@grantex/mcp-auth';
import express from 'express';
import { requireMcpAuth } from '@grantex/mcp-auth/express';
import type { McpAuthRequest } from '@grantex/mcp-auth/express';

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

// 2. Start OAuth 2.1 auth server
const authServer = await createMcpAuthServer({
  grantex,
  agentId: 'ag_calendar_mcp',
  scopes: ['calendar:read', 'calendar:write'],
  issuer: 'https://calendar-mcp.example.com',
  hooks: {
    onTokenIssued: async (event) => {
      await grantex.audit.log({
        action: 'mcp.token.issued',
        agentId: event.agentDid,
        grantId: event.grantId,
        scopes: event.scopes,
      });
    },
    onRevocation: async (jti) => {
      await grantex.audit.log({
        action: 'mcp.token.revoked',
        tokenId: jti,
      });
    },
  },
});

await authServer.listen({ port: 3001 });

// 3. Create MCP tool server with auth middleware
const app = express();

app.use('/mcp', requireMcpAuth({
  issuer: 'https://calendar-mcp.example.com',
  scopes: ['calendar:read'],
}));

app.post('/mcp/tools/list', (req: McpAuthRequest, res) => {
  res.json({
    tools: [
      { name: 'get_events', description: 'Get calendar events' },
      { name: 'create_event', description: 'Create a calendar event' },
    ],
  });
});

app.post('/mcp/tools/call', requireMcpAuth({
  issuer: 'https://calendar-mcp.example.com',
  scopes: ['calendar:write'],
}), (req: McpAuthRequest, res) => {
  const grant = req.mcpGrant!;
  // grant.agentDid, grant.scopes, grant.sub are available
  res.json({ result: 'Event created' });
});

app.listen(3000, () => {
  console.log('MCP Tool Server on :3000, Auth Server on :3001');
});

Troubleshooting

"JWKS fetch failed"

The middleware fetches JWKS from {issuer}/.well-known/jwks.json. Ensure:

  1. Your issuer URL is correct and accessible
  2. The JWKS endpoint returns valid JSON with a keys array
  3. Network connectivity allows outbound HTTPS from your server

"Token verification failed" / active: false

Common causes:

  • Token expired -- check the exp claim
  • Wrong issuer -- the token's iss claim must match
  • Algorithm mismatch -- only RS256, ES256, PS256, EdDSA are accepted
  • Key rotation -- JWKS is cached; restart or wait for cache refresh

"Invalid client" on introspect/revoke

Client authentication uses Basic auth (Authorization: Basic base64(client_id:client_secret)) or body parameters. Verify your client credentials match what was returned by /register.

Rate limiting (429)

Default limits per endpoint:

| Endpoint | Max requests | Window | |----------|-------------|--------| | /authorize | 10 | 1 minute | | /token | 20 | 1 minute | | /introspect | 30 | 1 minute | | /revoke | 20 | 1 minute | | All others | 100 | 1 minute |

Related Packages

| Package | Description | |---------|-------------| | @grantex/sdk | Core TypeScript SDK | | @grantex/express | Express.js middleware for Grantex | | @grantex/gateway | Reverse-proxy gateway with YAML config | | @grantex/mcp | MCP server with 13 Grantex tools | | @grantex/cli | CLI for managing grants, tokens, and agents | | @grantex/conformance | Protocol conformance test suite |

License

Apache-2.0