Skip to content

Commit 890beed

Browse files
committed
Add rate limiting and improve e2e testing workflow
- Add express-rate-limit to custom endpoints for security: - /introspect: 100 requests per 5 minutes - /fakeupstreamauth/*: 20 requests per minute - Static files: 25 requests per 10 minutes - Add automated e2e testing npm scripts using concurrently: - npm run test:e2e:integrated: Auto-starts server and runs test - npm run test:e2e:separate: Auto-starts both servers and runs test - Update README with automated testing approach documentation - Update docs/streamable-http-design.md with current file structure - Remove redundancy in README e2e testing documentation - Verified: All tests, linting, and e2e scripts pass with rate limiting
1 parent 1e47b07 commit 890beed

File tree

6 files changed

+114
-35
lines changed

6 files changed

+114
-35
lines changed

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,12 @@ npm run start:auth-server
210210
# Run linting
211211
npm run lint
212212

213-
# Run tests
213+
# Run unit tests
214214
npm test
215+
216+
# Run end-to-end tests (automated server management)
217+
npm run test:e2e:integrated # Test integrated mode OAuth + features
218+
npm run test:e2e:separate # Test separate mode OAuth + features
215219
```
216220

217221
### Testing with MCP Inspector
@@ -283,14 +287,16 @@ The `scripts/` directory contains automated test scripts that verify the complet
283287

284288
#### Usage
285289
```bash
286-
# Test integrated mode
287-
./scripts/test-integrated-e2e.sh
290+
# Recommended: Automated testing (handles server lifecycle)
291+
npm run test:e2e:integrated # Tests integrated mode
292+
npm run test:e2e:separate # Tests separate mode
288293

289-
# Test separate mode
294+
# Advanced: Manual script execution (requires manual server setup)
295+
./scripts/test-integrated-e2e.sh
290296
./scripts/test-separate-e2e.sh
291297
```
292298

293-
**Prerequisites:** Scripts check for Redis and servers, providing setup instructions if missing.
299+
The npm scripts automatically start required servers, run tests, and clean up. Manual scripts require you to start Redis and servers first.
294300

295301
### Interactive Testing
296302
Use the MCP Inspector for interactive testing and debugging of OAuth flows, tool execution, and resource access.
@@ -405,9 +411,9 @@ This creates a logical hierarchy where each layer outlives the layers it support
405411
│ ├── auth-core.ts # Core auth logic
406412
│ ├── redis-auth.ts # Redis auth operations
407413
│ └── types.ts # Shared type definitions
408-
├── scripts/ # Automated testing scripts
409-
│ ├── test-integrated-e2e.sh # End-to-end test for integrated mode
410-
│ └── test-separate-e2e.sh # End-to-end test for separate mode
414+
├── scripts/ # End-to-end testing scripts
415+
│ ├── test-integrated-e2e.sh # OAuth + feature verification (integrated)
416+
│ └── test-separate-e2e.sh # OAuth + feature verification (separate)
411417
├── docs/
412418
│ ├── streamable-http-design.md # SHTTP implementation details
413419
│ └── user-id-system.md # Authentication flow documentation

auth-server/index.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import express from 'express';
22
import cors from 'cors';
3+
import rateLimit from 'express-rate-limit';
34
import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
45
import { EverythingAuthProvider } from '../src/auth/provider.js';
56
import { handleFakeAuthorize, handleFakeAuthorizeRedirect } from '../src/handlers/fakeauth.js';
@@ -59,8 +60,21 @@ app.use(mcpAuthRouter({
5960
}
6061
}));
6162

63+
// Rate limiting for custom endpoints
64+
const introspectRateLimit = rateLimit({
65+
windowMs: 5 * 60 * 1000, // 5 minutes
66+
limit: 100, // 100 introspections per 5 minutes
67+
message: { error: 'too_many_requests', error_description: 'Token introspection rate limit exceeded' }
68+
});
69+
70+
const fakeAuthRateLimit = rateLimit({
71+
windowMs: 60 * 1000, // 1 minute
72+
limit: 20, // 20 auth attempts per minute
73+
message: { error: 'too_many_requests', error_description: 'Authentication rate limit exceeded' }
74+
});
75+
6276
// Token introspection endpoint (RFC 7662)
63-
app.post('/introspect', express.urlencoded({ extended: false }), async (req, res) => {
77+
app.post('/introspect', introspectRateLimit, express.urlencoded({ extended: false }), async (req, res) => {
6478
try {
6579
const { token } = req.body;
6680

@@ -96,8 +110,8 @@ app.post('/introspect', express.urlencoded({ extended: false }), async (req, res
96110
});
97111

98112
// Fake upstream auth endpoints (for user authentication simulation)
99-
app.get('/fakeupstreamauth/authorize', cors(), handleFakeAuthorize);
100-
app.get('/fakeupstreamauth/callback', cors(), handleFakeAuthorizeRedirect);
113+
app.get('/fakeupstreamauth/authorize', fakeAuthRateLimit, cors(), handleFakeAuthorize);
114+
app.get('/fakeupstreamauth/callback', fakeAuthRateLimit, cors(), handleFakeAuthorizeRedirect);
101115

102116
// Static assets (for auth page styling)
103117
import path from 'path';

docs/streamable-http-design.md

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,45 @@
1-
# Design Document: Streamable HTTP Transport Implementation
1+
# Design Document: Transport Implementation for Example Remote Server
22

33
## Current Implementation
44

5-
### Dual Transport Architecture
5+
The example remote server implements both MCP transport methods to support different client needs.
66

7-
The example remote server implements both transport methods:
7+
### Shared Infrastructure
8+
9+
All transports share the same foundational components:
10+
11+
1. **Authentication**: Uses `requireBearerAuth` middleware with mode-dependent auth providers
12+
2. **Redis Integration**: Messages published/subscribed through Redis channels using session IDs
13+
3. **Session Management**: Session ownership tracked via Redis for multi-user isolation
14+
4. **MCP Server**: Common `createMcpServer()` provides tools, resources, and prompts
15+
16+
### SSE Transport Architecture
17+
18+
Legacy transport for backwards compatibility:
819

9-
**Legacy SSE Transport:**
1020
1. **SSE Endpoint**: `/sse` - Creates SSE connection using `SSEServerTransport`
1121
2. **Message Endpoint**: `/message` - Receives POST requests and forwards them via Redis
22+
3. **Session ID**: Generated by `SSEServerTransport`, used for Redis channel keys
23+
4. **Message Flow**: POST to `/message` → Redis pub/sub → SSE stream to client
1224

13-
**Modern Streamable HTTP Transport:**
14-
1. **Unified Endpoint**: `/mcp` - Handles GET, POST, DELETE with `StreamableHTTPServerTransport`
15-
2. **Stateful Sessions**: Requires initialization and session ID tracking
16-
3. **SSE Response Format**: Returns results via Server-Sent Events streams
25+
**Key Files:**
26+
- `/src/index.ts:236` - SSE endpoint registration
27+
- `/src/handlers/sse.ts` - SSE connection handler with Redis integration
28+
- `/src/handlers/sse.ts:75-95` - Message POST handler
1729

18-
**Shared Infrastructure:**
19-
1. **Redis Integration**: Messages published/subscribed through Redis channels using session IDs
20-
2. **Auth**: Uses `requireBearerAuth` middleware with mode-dependent auth providers
21-
3. **Session Management**: Session ownership tracked via Redis for multi-user isolation
30+
### Streamable HTTP Transport Architecture
31+
32+
Modern unified transport:
33+
34+
1. **Unified Endpoint**: `/mcp` - Handles GET, POST, DELETE with `StreamableHTTPServerTransport`
35+
2. **Stateful Sessions**: Requires initialization with session ID tracking
36+
3. **Response Format**: Returns results via Server-Sent Events streams
37+
4. **Session Management**: Custom session ownership integration via Redis
2238

2339
**Key Files:**
24-
- `/src/index.ts:156-162` - SSE and Streamable HTTP endpoints with auth
25-
- `/src/handlers/sse.ts` - SSE connection handler with Redis integration
26-
- `/src/handlers/shttp.ts` - Streamable HTTP handler
27-
- `/src/services/mcp.ts` - MCP server implementation with tools, resources, prompts
40+
- `/src/index.ts:240-242` - Streamable HTTP endpoint registration
41+
- `/src/handlers/shttp.ts` - Complete Streamable HTTP handler
42+
- `/src/services/redisTransport.ts` - Redis-backed transport integration
2843

2944
### Streamable HTTP Transport Specification (2025-03-26)
3045

package-lock.json

Lines changed: 31 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
"lint": "eslint src/ auth-server/",
2121
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
2222
"test:integrated": "AUTH_MODE=integrated npm test",
23-
"test:separate": "AUTH_MODE=separate npm test"
23+
"test:separate": "AUTH_MODE=separate npm test",
24+
"test:e2e:integrated": "concurrently --kill-others --success first \"npm run dev:integrated\" \"sleep 4 && ./scripts/test-integrated-e2e.sh\"",
25+
"test:e2e:separate": "concurrently --kill-others --success first \"npm run dev:with-separate-auth\" \"sleep 6 && ./scripts/test-separate-e2e.sh\""
2426
},
2527
"devDependencies": {
2628
"@eslint/js": "^9.15.0",
@@ -42,6 +44,7 @@
4244
"cors": "^2.8.5",
4345
"dotenv": "^16.4.7",
4446
"express": "^4.21.2",
47+
"express-rate-limit": "^8.0.1",
4548
"raw-body": "^3.0.0"
4649
},
4750
"overrides": {

src/index.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BearerAuthMiddlewareOptions, requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
22
import { AuthRouterOptions, getOAuthProtectedResourceMetadataUrl, mcpAuthRouter, mcpAuthMetadataRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
33
import cors from "cors";
4+
import rateLimit from "express-rate-limit";
45
import express from "express";
56
import path from "path";
67
import { fileURLToPath } from "url";
@@ -122,6 +123,18 @@ app.use(baseSecurityHeaders);
122123
// Enable CORS pre-flight requests
123124
app.options('*', cors(corsOptions));
124125

126+
// Rate limiting for custom endpoints
127+
const fakeAuthRateLimit = rateLimit({
128+
windowMs: 60 * 1000, // 1 minute
129+
limit: 20, // 20 auth attempts per minute
130+
message: { error: 'too_many_requests', error_description: 'Authentication rate limit exceeded' }
131+
});
132+
133+
const staticFileRateLimit = rateLimit({
134+
windowMs: 10 * 60 * 1000, // 10 minutes
135+
limit: 25, // 25 requests per 10 minutes for static files
136+
message: { error: 'too_many_requests', error_description: 'Static file rate limit exceeded' }
137+
});
125138

126139
// Mode-dependent auth configuration
127140
let bearerAuth: express.RequestHandler;
@@ -242,12 +255,12 @@ app.post("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTT
242255
app.delete("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTTP);
243256

244257
// Static assets
245-
app.get("/mcp-logo.png", (req, res) => {
258+
app.get("/mcp-logo.png", staticFileRateLimit, (req, res) => {
246259
const logoPath = path.join(__dirname, "static", "mcp.png");
247260
res.sendFile(logoPath);
248261
});
249262

250-
app.get("/styles.css", (req, res) => {
263+
app.get("/styles.css", staticFileRateLimit, (req, res) => {
251264
const cssPath = path.join(__dirname, "static", "styles.css");
252265
res.setHeader('Content-Type', 'text/css');
253266
res.sendFile(cssPath);
@@ -261,8 +274,8 @@ app.get("/", (req, res) => {
261274

262275
// Upstream auth routes (only in integrated mode)
263276
if (AUTH_MODE === 'integrated') {
264-
app.get("/fakeupstreamauth/authorize", cors(corsOptions), handleFakeAuthorize);
265-
app.get("/fakeupstreamauth/callback", cors(corsOptions), handleFakeAuthorizeRedirect);
277+
app.get("/fakeupstreamauth/authorize", fakeAuthRateLimit, cors(corsOptions), handleFakeAuthorize);
278+
app.get("/fakeupstreamauth/callback", fakeAuthRateLimit, cors(corsOptions), handleFakeAuthorizeRedirect);
266279
}
267280

268281
try {

0 commit comments

Comments
 (0)