Skip to content

Commit 354fb43

Browse files
committed
remove rate limiting from core server, move to express only
1 parent 5958449 commit 354fb43

File tree

16 files changed

+121
-245
lines changed

16 files changed

+121
-245
lines changed

packages/server-express/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ app.use(express.json());
6060
app.use(
6161
mcpAuthRouter({
6262
provider,
63-
issuerUrl: new URL('https://auth.example.com')
63+
issuerUrl: new URL('https://auth.example.com'),
64+
// Optional rate limiting (implemented via express-rate-limit)
65+
rateLimit: { windowMs: 60_000, max: 60 }
6466
})
6567
);
6668
```

packages/server-express/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
},
4444
"dependencies": {
4545
"@modelcontextprotocol/server": "workspace:^",
46-
"express": "catalog:runtimeServerOnly"
46+
"express": "catalog:runtimeServerOnly",
47+
"express-rate-limit": "catalog:runtimeServerOnly"
4748
},
4849
"devDependencies": {
4950
"@modelcontextprotocol/tsconfig": "workspace:^",

packages/server-express/src/auth/router.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ import { Readable } from 'node:stream';
33
import { URL } from 'node:url';
44

55
import type { AuthMetadataOptions, AuthRouterOptions, WebHandlerContext } from '@modelcontextprotocol/server';
6-
import { mcpAuthMetadataRouter as createWebAuthMetadataRouter, mcpAuthRouter as createWebAuthRouter } from '@modelcontextprotocol/server';
6+
import {
7+
mcpAuthMetadataRouter as createWebAuthMetadataRouter,
8+
mcpAuthRouter as createWebAuthRouter,
9+
TooManyRequestsError
10+
} from '@modelcontextprotocol/server';
711
import type { RequestHandler, Response as ExpressResponse } from 'express';
812
import express from 'express';
13+
import { rateLimit } from 'express-rate-limit';
914

1015
type ExpressRequestLike = IncomingMessage & {
1116
method: string;
@@ -112,11 +117,23 @@ async function writeWebResponse(res: ExpressResponse, webResponse: Response): Pr
112117

113118
function toHandlerContext(req: ExpressRequestLike): WebHandlerContext {
114119
return {
115-
parsedBody: req.body,
116-
clientAddress: req.ip
120+
parsedBody: req.body
117121
};
118122
}
119123

124+
export type ExpressAuthRateLimitOptions =
125+
| false
126+
| {
127+
/**
128+
* Window size in ms (default: 60s)
129+
*/
130+
windowMs?: number;
131+
/**
132+
* Max requests per window per client (default: 60)
133+
*/
134+
max?: number;
135+
};
136+
120137
/**
121138
* Express router adapter for the Web-standard `mcpAuthRouter` from `@modelcontextprotocol/server`.
122139
*
@@ -126,12 +143,34 @@ function toHandlerContext(req: ExpressRequestLike): WebHandlerContext {
126143
* app.use(mcpAuthRouter(...))
127144
* ```
128145
*/
129-
export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler {
146+
export function mcpAuthRouter(options: AuthRouterOptions & { rateLimit?: ExpressAuthRateLimitOptions }): RequestHandler {
130147
const web = createWebAuthRouter(options);
131148
const router = express.Router();
132149

150+
const rateLimitOptions = options.rateLimit;
151+
const limiter =
152+
rateLimitOptions === false
153+
? undefined
154+
: rateLimit({
155+
windowMs: rateLimitOptions?.windowMs ?? 60_000,
156+
max: rateLimitOptions?.max ?? 60,
157+
standardHeaders: true,
158+
legacyHeaders: false,
159+
handler: (_req, res) => {
160+
const err = new TooManyRequestsError('Too many requests');
161+
res.status(429).json(err.toResponseObject());
162+
}
163+
});
164+
165+
const isRateLimitedPath = (path: string): boolean =>
166+
path === '/authorize' || path === '/token' || path === '/register' || path === '/revoke';
167+
133168
for (const route of web.routes) {
134-
router.all(route.path, async (req, res, next) => {
169+
const handlers: RequestHandler[] = [];
170+
if (limiter && isRateLimitedPath(route.path)) {
171+
handlers.push(limiter);
172+
}
173+
handlers.push(async (req, res, next) => {
135174
try {
136175
const parsedBodyProvided = (req as ExpressRequestLike).body !== undefined;
137176
const webReq = await expressToWebRequest(req as ExpressRequestLike, parsedBodyProvided);
@@ -141,6 +180,7 @@ export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler {
141180
next(err);
142181
}
143182
});
183+
router.all(route.path, ...handlers);
144184
}
145185

146186
return router;

packages/server-express/test/server/auth/router.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,36 @@ describe('MCP Auth Router', () => {
325325
expect(response.status).not.toBe(404);
326326
});
327327

328+
it('applies rate limiting to token endpoint (express-rate-limit)', async () => {
329+
// Fresh app with a very low rate limit so we can trigger it deterministically
330+
const limitedApp = express();
331+
const options = {
332+
provider: mockProvider,
333+
issuerUrl: new URL('https://auth.example.com'),
334+
rateLimit: { windowMs: 60_000, max: 1 }
335+
} as const;
336+
limitedApp.use(mcpAuthRouter(options));
337+
338+
const first = await supertest(limitedApp).post('/token').type('form').send({
339+
client_id: 'valid-client',
340+
client_secret: 'valid-secret',
341+
grant_type: 'authorization_code',
342+
code: 'valid_code',
343+
code_verifier: 'valid_verifier'
344+
});
345+
expect(first.status).not.toBe(404);
346+
347+
const second = await supertest(limitedApp).post('/token').type('form').send({
348+
client_id: 'valid-client',
349+
client_secret: 'valid-secret',
350+
grant_type: 'authorization_code',
351+
code: 'valid_code',
352+
code_verifier: 'valid_verifier'
353+
});
354+
expect(second.status).toBe(429);
355+
expect(second.body).toEqual(expect.objectContaining({ error: 'too_many_requests' }));
356+
});
357+
328358
it('routes to registration endpoint', async () => {
329359
const response = await supertest(app)
330360
.post('/register')

packages/server/src/server/auth/handlers/authorize.ts

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import { InvalidClientError, InvalidRequestError, OAuthError, ServerError, TooManyRequestsError } from '@modelcontextprotocol/core';
1+
import { InvalidClientError, InvalidRequestError, OAuthError, ServerError } from '@modelcontextprotocol/core';
22
import * as z from 'zod/v4';
33

44
import type { OAuthServerProvider } from '../provider.js';
55
import type { WebHandler } from '../web.js';
6-
import { getClientAddress, getParsedBody, InMemoryRateLimiter, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js';
6+
import { getParsedBody, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js';
77

88
export type AuthorizationHandlerOptions = {
99
provider: OAuthServerProvider;
10-
/**
11-
* Rate limiting configuration for the authorization endpoint.
12-
* Set to false to disable rate limiting for this endpoint.
13-
*/
14-
rateLimit?: Partial<{ windowMs: number; max: number }> | false;
1510
};
1611

1712
// Parameters that must be validated in order to issue redirects.
@@ -33,36 +28,10 @@ const RequestAuthorizationParamsSchema = z.object({
3328
resource: z.string().url().optional()
3429
});
3530

36-
export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: AuthorizationHandlerOptions): WebHandler {
37-
const limiter =
38-
rateLimitConfig === false
39-
? undefined
40-
: new InMemoryRateLimiter({
41-
windowMs: rateLimitConfig?.windowMs ?? 15 * 60 * 1000,
42-
max: rateLimitConfig?.max ?? 100
43-
});
44-
31+
export function authorizationHandler({ provider }: AuthorizationHandlerOptions): WebHandler {
4532
return async (req, ctx) => {
4633
const noStore = noStoreHeaders();
4734

48-
// Rate limit by client address where possible (best-effort).
49-
if (limiter) {
50-
const key = `${getClientAddress(req, ctx) ?? 'global'}:authorize`;
51-
const rl = limiter.consume(key);
52-
if (!rl.allowed) {
53-
return jsonResponse(
54-
new TooManyRequestsError('You have exceeded the rate limit for authorization requests').toResponseObject(),
55-
{
56-
status: 429,
57-
headers: {
58-
...noStore,
59-
...(rl.retryAfterSeconds ? { 'Retry-After': String(rl.retryAfterSeconds) } : {})
60-
}
61-
}
62-
);
63-
}
64-
}
65-
6635
if (req.method !== 'GET' && req.method !== 'POST') {
6736
const resp = methodNotAllowedResponse(req, ['GET', 'POST']);
6837
const body = await resp.text();

packages/server/src/server/auth/handlers/register.ts

Lines changed: 2 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
11
import crypto from 'node:crypto';
22

33
import type { OAuthClientInformationFull } from '@modelcontextprotocol/core';
4-
import {
5-
InvalidClientMetadataError,
6-
OAuthClientMetadataSchema,
7-
OAuthError,
8-
ServerError,
9-
TooManyRequestsError
10-
} from '@modelcontextprotocol/core';
4+
import { InvalidClientMetadataError, OAuthClientMetadataSchema, OAuthError, ServerError } from '@modelcontextprotocol/core';
115

126
import type { OAuthRegisteredClientsStore } from '../clients.js';
137
import type { WebHandler } from '../web.js';
14-
import {
15-
corsHeaders,
16-
corsPreflightResponse,
17-
getClientAddress,
18-
getParsedBody,
19-
InMemoryRateLimiter,
20-
jsonResponse,
21-
methodNotAllowedResponse,
22-
noStoreHeaders
23-
} from '../web.js';
8+
import { corsHeaders, corsPreflightResponse, getParsedBody, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js';
249

2510
export type ClientRegistrationHandlerOptions = {
2611
/**
@@ -35,13 +20,6 @@ export type ClientRegistrationHandlerOptions = {
3520
*/
3621
clientSecretExpirySeconds?: number;
3722

38-
/**
39-
* Rate limiting configuration for the client registration endpoint.
40-
* Set to false to disable rate limiting for this endpoint.
41-
* Registration endpoints are particularly sensitive to abuse and should be rate limited.
42-
*/
43-
rateLimit?: Partial<{ windowMs: number; max: number }> | false;
44-
4523
/**
4624
* Whether to generate a client ID before calling the client registration endpoint.
4725
*
@@ -55,21 +33,12 @@ const DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS = 30 * 24 * 60 * 60; // 30 days
5533
export function clientRegistrationHandler({
5634
clientsStore,
5735
clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS,
58-
rateLimit: rateLimitConfig,
5936
clientIdGeneration = true
6037
}: ClientRegistrationHandlerOptions): WebHandler {
6138
if (!clientsStore.registerClient) {
6239
throw new Error('Client registration store does not support registering clients');
6340
}
6441

65-
const limiter =
66-
rateLimitConfig === false
67-
? undefined
68-
: new InMemoryRateLimiter({
69-
windowMs: rateLimitConfig?.windowMs ?? 60 * 60 * 1000,
70-
max: rateLimitConfig?.max ?? 20
71-
});
72-
7342
const cors = {
7443
allowOrigin: '*',
7544
allowMethods: ['POST', 'OPTIONS'],
@@ -92,23 +61,6 @@ export function clientRegistrationHandler({
9261
});
9362
}
9463

95-
if (limiter) {
96-
const key = `${getClientAddress(req, ctx) ?? 'global'}:register`;
97-
const rl = limiter.consume(key);
98-
if (!rl.allowed) {
99-
return jsonResponse(
100-
new TooManyRequestsError('You have exceeded the rate limit for client registration requests').toResponseObject(),
101-
{
102-
status: 429,
103-
headers: {
104-
...baseHeaders,
105-
...(rl.retryAfterSeconds ? { 'Retry-After': String(rl.retryAfterSeconds) } : {})
106-
}
107-
}
108-
);
109-
}
110-
}
111-
11264
try {
11365
const rawBody = await getParsedBody(req, ctx);
11466
const parseResult = OAuthClientMetadataSchema.safeParse(rawBody);

packages/server/src/server/auth/handlers/revoke.ts

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,19 @@
1-
import {
2-
InvalidRequestError,
3-
OAuthError,
4-
OAuthTokenRevocationRequestSchema,
5-
ServerError,
6-
TooManyRequestsError
7-
} from '@modelcontextprotocol/core';
1+
import { InvalidRequestError, OAuthError, OAuthTokenRevocationRequestSchema, ServerError } from '@modelcontextprotocol/core';
82

93
import { authenticateClient } from '../middleware/clientAuth.js';
104
import type { OAuthServerProvider } from '../provider.js';
115
import type { WebHandler } from '../web.js';
12-
import {
13-
corsHeaders,
14-
corsPreflightResponse,
15-
getClientAddress,
16-
getParsedBody,
17-
InMemoryRateLimiter,
18-
jsonResponse,
19-
methodNotAllowedResponse,
20-
noStoreHeaders
21-
} from '../web.js';
6+
import { corsHeaders, corsPreflightResponse, getParsedBody, jsonResponse, methodNotAllowedResponse, noStoreHeaders } from '../web.js';
227

238
export type RevocationHandlerOptions = {
249
provider: OAuthServerProvider;
25-
/**
26-
* Rate limiting configuration for the token revocation endpoint.
27-
* Set to false to disable rate limiting for this endpoint.
28-
*/
29-
rateLimit?: Partial<{ windowMs: number; max: number }> | false;
3010
};
3111

32-
export function revocationHandler({ provider, rateLimit: rateLimitConfig }: RevocationHandlerOptions): WebHandler {
12+
export function revocationHandler({ provider }: RevocationHandlerOptions): WebHandler {
3313
if (!provider.revokeToken) {
3414
throw new Error('Auth provider does not support revoking tokens');
3515
}
3616

37-
const limiter =
38-
rateLimitConfig === false
39-
? undefined
40-
: new InMemoryRateLimiter({
41-
windowMs: rateLimitConfig?.windowMs ?? 15 * 60 * 1000,
42-
max: rateLimitConfig?.max ?? 50
43-
});
44-
4517
const cors = {
4618
allowOrigin: '*',
4719
allowMethods: ['POST', 'OPTIONS'],
@@ -64,23 +36,6 @@ export function revocationHandler({ provider, rateLimit: rateLimitConfig }: Revo
6436
});
6537
}
6638

67-
if (limiter) {
68-
const key = `${getClientAddress(req, ctx) ?? 'global'}:revoke`;
69-
const rl = limiter.consume(key);
70-
if (!rl.allowed) {
71-
return jsonResponse(
72-
new TooManyRequestsError('You have exceeded the rate limit for token revocation requests').toResponseObject(),
73-
{
74-
status: 429,
75-
headers: {
76-
...baseHeaders,
77-
...(rl.retryAfterSeconds ? { 'Retry-After': String(rl.retryAfterSeconds) } : {})
78-
}
79-
}
80-
);
81-
}
82-
}
83-
8439
try {
8540
const rawBody = await getParsedBody(req, ctx);
8641
const parseResult = OAuthTokenRevocationRequestSchema.safeParse(rawBody);

0 commit comments

Comments
 (0)