New:Socket for Asana Is Now Available.Learn more
Get Started

@grantex/mcp-auth

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@grantex/mcp-auth

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

latest
Source
npmnpm
Version
2.0.2
Version published
Maintainers
1
Created
Source

@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:

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

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

PropertyTypeRequiredDefaultDescription
grantexGrantexYes-Grantex SDK client instance
agentIdstringYes-Agent ID for Grantex authorization
scopesstring[]Yes-Scopes to request from Grantex
issuerstringYes-Base URL for this auth server (used in metadata)
allowedRedirectUrisstring[]No[]Allowed redirect URIs (empty = all allowed)
allowedResourcesstring[]No[]Allowed resource indicators (RFC 8707)
clientStoreClientStoreNoInMemoryClientStoreCustom client registration store
codeExpirationSecondsnumberNo600Authorization code TTL in seconds
consentUiobjectNo-Consent UI customization (appName, appLogo, privacyUrl, termsUrl)
hooksobjectNo-Lifecycle hooks (onTokenIssued, onRevocation)

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)

OptionTypeRequiredDefaultDescription
issuerstringYes-Issuer 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

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

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

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:

  • Your issuer URL is correct and accessible
  • The JWKS endpoint returns valid JSON with a keys array
  • 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:

EndpointMax requestsWindow
/authorize101 minute
/token201 minute
/introspect301 minute
/revoke201 minute
All others1001 minute
PackageDescription
@grantex/sdkCore TypeScript SDK
@grantex/expressExpress.js middleware for Grantex
@grantex/gatewayReverse-proxy gateway with YAML config
@grantex/mcpMCP server with 13 Grantex tools
@grantex/cliCLI for managing grants, tokens, and agents
@grantex/conformanceProtocol conformance test suite

License

Apache-2.0

Keywords

grantex

FAQs

Package last updated on 25 Jun 2026

Related posts