Skip to content
Merged
111 changes: 111 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,117 @@ await test.cleanupItems();
await test.removeUser('test@example.com');
```

## MCP Auth

CDK Serverless includes built-in support for adding [Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io) OAuth authentication to your API. This enables MCP clients like Claude and ChatGPT to authenticate with your API using standard OAuth 2.0 flows.

When activated, the following endpoints are automatically added to your OpenAPI spec:

| Endpoint | Method | RFC | Purpose |
|----------|--------|-----|---------|
| `/.well-known/oauth-protected-resource` | GET | RFC 9728 | Resource metadata discovery |
| `/.well-known/oauth-authorization-server` | GET | RFC 8414 | Authorization server metadata |
| `/oauth/authorize` | GET | — | Authorize proxy (redirect to upstream) |
| `/oauth/token` | POST | — | Token proxy (forward to upstream) |
| `/oauth/register` | POST | RFC 7591 | Dynamic client registration |

All MCP auth endpoints are anonymous (no API authorizer applied).

### With Cognito

MCP clients like Claude enforce that all OAuth endpoints (discovery, authorize, token, register) live on the **same origin** as the API itself. Cognito's hosted UI lives on a different domain (`auth.example.com`), so MCP clients cannot talk to it directly. Additionally:

- Cognito Managed Login v2 requires the RFC 8707 `resource` parameter for custom scopes in authorize requests — MCP clients do not send this parameter.
- MCP clients construct the token endpoint URL from the OAuth metadata `issuer` field rather than using the explicit `token_endpoint` — so the token exchange must be proxied through the API domain.
- Dynamic Client Registration (RFC 7591) is not natively supported by Cognito.

The MCP auth proxy solves all three problems: it serves discovery metadata on the API domain, strips unsupported parameters before forwarding to Cognito, and implements a simplified registration endpoint that returns a pre-provisioned client ID.

If your API already uses `CognitoAuthentication`, add MCP auth with minimal config — a dedicated user pool client is created automatically:

```typescript
const api = new TestApiRestApi(this, 'Api', {
stageName: 'dev',
domainName: 'example.com',
apiHostname: 'api',
authentication: cognitoAuth,
cors: true,
mcpAuth: {
cognito: {
auth: cognitoAuth, // your CognitoAuthentication construct
authDomain: 'auth.example.com', // Cognito custom/hosted domain (required)
},
serverInfo: { name: 'my-mcp-server', version: '1.0.0' },
},
});
```

The `apiDomain` is derived from the RestApi's own domain config (`apiHostname` + `domainName`), and `stageName` is reused — no duplication needed.

### With any OAuth2 provider

For non-Cognito providers, use `generic` mode with explicit endpoint URLs:

```typescript
const api = new TestApiRestApi(this, 'Api', {
// ...
mcpAuth: {
generic: {
authorizeEndpoint: 'https://auth.example.com/authorize',
tokenEndpoint: 'https://auth.example.com/token',
clientId: 'my-pre-provisioned-client-id',
},
serverInfo: { name: 'my-mcp-server', version: '1.0.0' },
},
});
```

### Configuration options

| Option | Default | Description |
|--------|---------|-------------|
| `serverInfo` | (required) | `{ name, version }` returned in MCP initialize |
| `allowedRedirectUris` | Claude + ChatGPT callbacks | Allowlist for dynamic registration |
| `protocolVersions` | `['2025-11-25', '2025-03-26', '2024-11-05']` | Supported MCP versions |
| `scopes` | `['openid', 'email', 'profile']` | Advertised OAuth scopes |
| `stripParameters` | `['resource']` | Params stripped from authorize/token proxying |
| `lambdaOptions` | — | Lambda config for MCP auth handlers |

### MCP Server runtime

The `cdk-serverless/mcp-auth` module also exports a JSON-RPC server for implementing the MCP tool endpoint itself:

```typescript
import { createMcpServer } from 'cdk-serverless/mcp-auth';

const server = createMcpServer({
serverInfo: { name: 'my-server', version: '1.0.0' },
protocolVersions: ['2025-11-25'],
resolver: {
async resolve(headers) {
// Validate Bearer token, return principal or throw McpUnauthorizedError
const token = headers.authorization?.replace('Bearer ', '');
if (!token) throw new McpUnauthorizedError('Bearer');
return verifyToken(token);
},
},
tools: [
{
name: 'search',
description: 'Search documents',
inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
async invoke(principal, args) {
const results = await search(principal, (args as any).query);
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
},
},
],
});

// In your Lambda handler:
const response = await server.handle(parsedBody, event.headers);
```

## Breaking Change: `axios` Removed

CDK Serverless no longer depends on `axios`. The library now uses the
Expand Down
2 changes: 1 addition & 1 deletion docs/constructs/assets/hierarchy.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/constructs/assets/navigation.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/constructs/assets/search.js

Large diffs are not rendered by default.

Loading