> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grantex.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Auth Server

> OAuth 2.1 + PKCE endpoint package for MCP servers, including current v2.0.2 limitations.

## Overview

MCP clients use OAuth authorization-server metadata, Dynamic Client Registration,
PKCE, token exchange, introspection, and revocation endpoints.
`@grantex/mcp-auth` registers those six routes on a Fastify instance and provides
Express and Hono JWT-verification middleware.

Current published release: **`@grantex/mcp-auth@2.0.2`**.

```bash theme={null}
npm install @grantex/mcp-auth@2.0.2 @grantex/sdk@0.3.13
```

This exact command is reproducible. See [Release Status](/release-status) before
upgrading either independently versioned package.

<Info>
  The supplied `Grantex` client can point at Grantex Cloud or a self-hosted
  Grantex API. That backend choice does not make the MCP authorization server's
  own client-registration or authorization-code state managed or durable.
</Info>

<Warning>
  Release `2.0.2` is a single-process evaluation release, not a turnkey
  production deployment. Client registrations default to process memory and
  authorization codes always use a non-configurable in-memory store, so restarts
  lose state and multi-replica authorize/token flows can fail. `consentUi` only
  adds discovery metadata; no `/consent` page is registered. The package also
  does not persist the `auth.code` returned by the Grantex SDK and can fall back
  to an authorization-request ID during token exchange, which is not a valid
  exchange code. Do not rely on end-to-end token issuance until a corrected
  package release is published and validated with your backend.
</Warning>

## Deployment behavior in 2.0.2

| Concern                  | Current behavior                                                 |
| ------------------------ | ---------------------------------------------------------------- |
| Grantex backend          | Cloud or self-hosted, selected through the supplied SDK client   |
| Client registrations     | In-memory by default; a custom `ClientStore` can persist clients |
| Authorization codes      | Always process-local `InMemoryCodeStore`; no `codeStore` option  |
| Horizontal scaling       | Unsupported for authorize/token flows in `2.0.2`                 |
| Consent UI               | Metadata only; the package registers no consent route or page    |
| Rate limiting            | Fixed Fastify limits; not configurable through `McpAuthConfig`   |
| Middleware/introspection | Signature and claim validation; no live revocation lookup        |

## Quick Start

The following starts the published endpoint surface for local inspection and
evaluation. It does not remove the limitations above.

### 1. Create the auth server

```typescript theme={null}
import { Grantex } from '@grantex/sdk';
import { createMcpAuthServer } from '@grantex/mcp-auth';

const grantex = new Grantex({
  baseUrl: 'https://api.grantex.dev',
  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:

```typescript theme={null}
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:

```bash theme={null}
curl https://your-mcp-server.example.com/.well-known/oauth-authorization-server
```

## Endpoints

`createMcpAuthServer()` registers six 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  | JWT signature/claim introspection; no live revocation lookup |
| `/revoke`                                 | `POST` | RFC 7009  | Requests backend revocation by JWT `jti`                     |

### Well-Known Metadata

The metadata endpoint returns a JSON document that MCP clients use to discover all other endpoints:

```json theme={null}
{
  "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

The endpoint verifies JWT signatures and claims against the issuer JWKS. In `2.0.2` it does not query Grantex for current revocation state, so a revoked but otherwise valid token can remain `active` here until expiry:

```bash theme={null}
curl -X POST https://your-mcp-server.example.com/introspect \
  -H "Content-Type: application/json" \
  -d '{"token": "eyJhbGciOiJSUzI1NiIs..."}'
```

Active token response:

```json theme={null}
{
  "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

The revocation endpoint decodes the token `jti` and requests revocation from Grantex. It returns `200 OK` even if that backend call fails, and the package middleware/introspection paths do not consult the resulting revocation state:

```bash theme={null}
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:

```typescript theme={null}
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 used at two implemented points in `2.0.2`:

1. **Authorization request**: The package forwards the requested scopes to the configured Grantex client.
2. **Middleware**: `requireMcpAuth()` rejects tokens that lack required scopes.

The package does not render a consent page. A complete human-consent handoff must
be implemented outside this release.

## Consent metadata (not a rendered page)

`consentUi` copies display metadata into the discovery document under
`grantex_extensions.consent_ui_config`:

```typescript theme={null}
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',
  },
});
```

| Property     | Type     | Metadata meaning                                    |
| ------------ | -------- | --------------------------------------------------- |
| `appName`    | `string` | Application label advertised to discovery consumers |
| `appLogo`    | `string` | Advertised application-logo URL                     |
| `privacyUrl` | `string` | Advertised privacy-policy URL                       |
| `termsUrl`   | `string` | Advertised terms URL                                |

<Warning>
  `2.0.2` does not register the `/consent` URL advertised in
  `grantex_extensions.consent_ui`, and `consentUi` does not render or host a
  consent page. Supply that interaction separately and validate the complete
  authorization flow before use.
</Warning>

## Lifecycle hooks

Only `hooks.onRevocation` is invoked by `2.0.2`:

```typescript theme={null}
const authServer = await createMcpAuthServer({
  // ...required fields...
  hooks: {
    onRevocation: async (jti) => {
      console.log(`Token ${jti} was submitted for revocation`);
    },
  },
});
```

The exported configuration type also declares `onTokenIssued`, but the token
endpoint in `2.0.2` does not call it. Do not depend on that hook until a corrected
release is published.

## Express Middleware

<Note>
  Express and Hono middleware verify JWT signatures, claims, algorithms, and
  configured scopes. They do not perform an online revocation check in `2.0.2`.
</Note>

```typescript theme={null}
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)`

| 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

```typescript theme={null}
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 process memory. A custom `ClientStore` can persist registrations, but authorization codes remain in a non-configurable process-local store in `2.0.2`; a custom client store alone does not make the server durable or horizontally scalable:

```typescript theme={null}
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 accepts Bronze, Silver, and Gold certification applications for registered MCP servers. Applications begin in `pending_conformance_test`; submission alone is not certification. See the [MCP Certification Applications guide](/guides/mcp-certification) for the current API and limitations.

| Requested level | Application value |
| --------------- | ----------------- |
| **Bronze**      | `bronze`          |
| **Silver**      | `silver`          |
| **Gold**        | `gold`            |

<Warning>
  Using `@grantex/mcp-auth` does not automatically certify a server. Treat the status returned by the certification API as authoritative and do not publish a badge for a pending application.
</Warning>

## Configuration Reference

### `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                                                                         |
| `allowedRedirectUris`   | `string[]`    | No       | `[]`                  | Declared by the type but not enforced in `2.0.2`; authorization checks each client's DCR-provided URI |
| `allowedResources`      | `string[]`    | No       | `[]`                  | Allowed resource indicators (RFC 8707)                                                                |
| `clientStore`           | `ClientStore` | No       | `InMemoryClientStore` | Custom client-registration store only; authorization codes remain in memory                           |
| `codeExpirationSeconds` | `number`      | No       | `600`                 | Authorization code TTL                                                                                |
| `consentUi`             | `object`      | No       | --                    | Discovery metadata only; does not create a consent page                                               |
| `hooks`                 | `object`      | No       | --                    | `onRevocation` runs; declared `onTokenIssued` is not invoked in `2.0.2`                               |

## Rate Limits

Default per-endpoint rate limits:

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

## Security Considerations

`@grantex/mcp-auth` implements the following request and token-validation controls. These controls do not remove the deployment and end-to-end flow limitations documented above:

* **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.

## Related

* [MCP Certification Applications](/guides/mcp-certification) -- Application API and current limitations
* [MCP Server (17 tools)](/integrations/mcp) -- Grantex MCP server for Claude Desktop/Cursor
* [Express Middleware](/integrations/express) -- Grantex Express.js middleware
* [Self-Hosting Guide](/guides/self-hosting) -- Deploy the full Grantex stack
* [Security Best Practices](/guides/security-best-practices) -- Token verification, key rotation
