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
This commit is contained in:
parent
1e47b07d2c
commit
890beed75a
6 changed files with 114 additions and 35 deletions
22
README.md
22
README.md
|
|
@ -210,8 +210,12 @@ npm run start:auth-server
|
|||
# Run linting
|
||||
npm run lint
|
||||
|
||||
# Run tests
|
||||
# Run unit tests
|
||||
npm test
|
||||
|
||||
# Run end-to-end tests (automated server management)
|
||||
npm run test:e2e:integrated # Test integrated mode OAuth + features
|
||||
npm run test:e2e:separate # Test separate mode OAuth + features
|
||||
```
|
||||
|
||||
### Testing with MCP Inspector
|
||||
|
|
@ -283,14 +287,16 @@ The `scripts/` directory contains automated test scripts that verify the complet
|
|||
|
||||
#### Usage
|
||||
```bash
|
||||
# Test integrated mode
|
||||
./scripts/test-integrated-e2e.sh
|
||||
# Recommended: Automated testing (handles server lifecycle)
|
||||
npm run test:e2e:integrated # Tests integrated mode
|
||||
npm run test:e2e:separate # Tests separate mode
|
||||
|
||||
# Test separate mode
|
||||
# Advanced: Manual script execution (requires manual server setup)
|
||||
./scripts/test-integrated-e2e.sh
|
||||
./scripts/test-separate-e2e.sh
|
||||
```
|
||||
|
||||
**Prerequisites:** Scripts check for Redis and servers, providing setup instructions if missing.
|
||||
The npm scripts automatically start required servers, run tests, and clean up. Manual scripts require you to start Redis and servers first.
|
||||
|
||||
### Interactive Testing
|
||||
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
|
|||
│ ├── auth-core.ts # Core auth logic
|
||||
│ ├── redis-auth.ts # Redis auth operations
|
||||
│ └── types.ts # Shared type definitions
|
||||
├── scripts/ # Automated testing scripts
|
||||
│ ├── test-integrated-e2e.sh # End-to-end test for integrated mode
|
||||
│ └── test-separate-e2e.sh # End-to-end test for separate mode
|
||||
├── scripts/ # End-to-end testing scripts
|
||||
│ ├── test-integrated-e2e.sh # OAuth + feature verification (integrated)
|
||||
│ └── test-separate-e2e.sh # OAuth + feature verification (separate)
|
||||
├── docs/
|
||||
│ ├── streamable-http-design.md # SHTTP implementation details
|
||||
│ └── user-id-system.md # Authentication flow documentation
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
|
||||
import { EverythingAuthProvider } from '../src/auth/provider.js';
|
||||
import { handleFakeAuthorize, handleFakeAuthorizeRedirect } from '../src/handlers/fakeauth.js';
|
||||
|
|
@ -59,8 +60,21 @@ app.use(mcpAuthRouter({
|
|||
}
|
||||
}));
|
||||
|
||||
// Rate limiting for custom endpoints
|
||||
const introspectRateLimit = rateLimit({
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
limit: 100, // 100 introspections per 5 minutes
|
||||
message: { error: 'too_many_requests', error_description: 'Token introspection rate limit exceeded' }
|
||||
});
|
||||
|
||||
const fakeAuthRateLimit = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
limit: 20, // 20 auth attempts per minute
|
||||
message: { error: 'too_many_requests', error_description: 'Authentication rate limit exceeded' }
|
||||
});
|
||||
|
||||
// Token introspection endpoint (RFC 7662)
|
||||
app.post('/introspect', express.urlencoded({ extended: false }), async (req, res) => {
|
||||
app.post('/introspect', introspectRateLimit, express.urlencoded({ extended: false }), async (req, res) => {
|
||||
try {
|
||||
const { token } = req.body;
|
||||
|
||||
|
|
@ -96,8 +110,8 @@ app.post('/introspect', express.urlencoded({ extended: false }), async (req, res
|
|||
});
|
||||
|
||||
// Fake upstream auth endpoints (for user authentication simulation)
|
||||
app.get('/fakeupstreamauth/authorize', cors(), handleFakeAuthorize);
|
||||
app.get('/fakeupstreamauth/callback', cors(), handleFakeAuthorizeRedirect);
|
||||
app.get('/fakeupstreamauth/authorize', fakeAuthRateLimit, cors(), handleFakeAuthorize);
|
||||
app.get('/fakeupstreamauth/callback', fakeAuthRateLimit, cors(), handleFakeAuthorizeRedirect);
|
||||
|
||||
// Static assets (for auth page styling)
|
||||
import path from 'path';
|
||||
|
|
|
|||
|
|
@ -1,30 +1,45 @@
|
|||
# Design Document: Streamable HTTP Transport Implementation
|
||||
# Design Document: Transport Implementation for Example Remote Server
|
||||
|
||||
## Current Implementation
|
||||
|
||||
### Dual Transport Architecture
|
||||
The example remote server implements both MCP transport methods to support different client needs.
|
||||
|
||||
The example remote server implements both transport methods:
|
||||
### Shared Infrastructure
|
||||
|
||||
All transports share the same foundational components:
|
||||
|
||||
1. **Authentication**: Uses `requireBearerAuth` middleware with mode-dependent auth providers
|
||||
2. **Redis Integration**: Messages published/subscribed through Redis channels using session IDs
|
||||
3. **Session Management**: Session ownership tracked via Redis for multi-user isolation
|
||||
4. **MCP Server**: Common `createMcpServer()` provides tools, resources, and prompts
|
||||
|
||||
### SSE Transport Architecture
|
||||
|
||||
Legacy transport for backwards compatibility:
|
||||
|
||||
**Legacy SSE Transport:**
|
||||
1. **SSE Endpoint**: `/sse` - Creates SSE connection using `SSEServerTransport`
|
||||
2. **Message Endpoint**: `/message` - Receives POST requests and forwards them via Redis
|
||||
|
||||
**Modern Streamable HTTP Transport:**
|
||||
1. **Unified Endpoint**: `/mcp` - Handles GET, POST, DELETE with `StreamableHTTPServerTransport`
|
||||
2. **Stateful Sessions**: Requires initialization and session ID tracking
|
||||
3. **SSE Response Format**: Returns results via Server-Sent Events streams
|
||||
|
||||
**Shared Infrastructure:**
|
||||
1. **Redis Integration**: Messages published/subscribed through Redis channels using session IDs
|
||||
2. **Auth**: Uses `requireBearerAuth` middleware with mode-dependent auth providers
|
||||
3. **Session Management**: Session ownership tracked via Redis for multi-user isolation
|
||||
3. **Session ID**: Generated by `SSEServerTransport`, used for Redis channel keys
|
||||
4. **Message Flow**: POST to `/message` → Redis pub/sub → SSE stream to client
|
||||
|
||||
**Key Files:**
|
||||
- `/src/index.ts:156-162` - SSE and Streamable HTTP endpoints with auth
|
||||
- `/src/index.ts:236` - SSE endpoint registration
|
||||
- `/src/handlers/sse.ts` - SSE connection handler with Redis integration
|
||||
- `/src/handlers/shttp.ts` - Streamable HTTP handler
|
||||
- `/src/services/mcp.ts` - MCP server implementation with tools, resources, prompts
|
||||
- `/src/handlers/sse.ts:75-95` - Message POST handler
|
||||
|
||||
### Streamable HTTP Transport Architecture
|
||||
|
||||
Modern unified transport:
|
||||
|
||||
1. **Unified Endpoint**: `/mcp` - Handles GET, POST, DELETE with `StreamableHTTPServerTransport`
|
||||
2. **Stateful Sessions**: Requires initialization with session ID tracking
|
||||
3. **Response Format**: Returns results via Server-Sent Events streams
|
||||
4. **Session Management**: Custom session ownership integration via Redis
|
||||
|
||||
**Key Files:**
|
||||
- `/src/index.ts:240-242` - Streamable HTTP endpoint registration
|
||||
- `/src/handlers/shttp.ts` - Complete Streamable HTTP handler
|
||||
- `/src/services/redisTransport.ts` - Redis-backed transport integration
|
||||
|
||||
### Streamable HTTP Transport Specification (2025-03-26)
|
||||
|
||||
|
|
|
|||
34
package-lock.json
generated
34
package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^8.0.1",
|
||||
"raw-body": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -1777,6 +1778,21 @@
|
|||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/express-rate-limit": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
|
||||
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||
|
|
@ -3850,10 +3866,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
|
||||
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express-rate-limit/-/express-rate-limit-8.0.1.tgz",
|
||||
"integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
|
|
@ -4455,6 +4474,15 @@
|
|||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@
|
|||
"lint": "eslint src/ auth-server/",
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
||||
"test:integrated": "AUTH_MODE=integrated npm test",
|
||||
"test:separate": "AUTH_MODE=separate npm test"
|
||||
"test:separate": "AUTH_MODE=separate npm test",
|
||||
"test:e2e:integrated": "concurrently --kill-others --success first \"npm run dev:integrated\" \"sleep 4 && ./scripts/test-integrated-e2e.sh\"",
|
||||
"test:e2e:separate": "concurrently --kill-others --success first \"npm run dev:with-separate-auth\" \"sleep 6 && ./scripts/test-separate-e2e.sh\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
|
|
@ -42,6 +44,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^8.0.1",
|
||||
"raw-body": "^3.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
|
|
|
|||
21
src/index.ts
21
src/index.ts
|
|
@ -1,6 +1,7 @@
|
|||
import { BearerAuthMiddlewareOptions, requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
||||
import { AuthRouterOptions, getOAuthProtectedResourceMetadataUrl, mcpAuthRouter, mcpAuthMetadataRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
||||
import cors from "cors";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
|
@ -122,6 +123,18 @@ app.use(baseSecurityHeaders);
|
|||
// Enable CORS pre-flight requests
|
||||
app.options('*', cors(corsOptions));
|
||||
|
||||
// Rate limiting for custom endpoints
|
||||
const fakeAuthRateLimit = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
limit: 20, // 20 auth attempts per minute
|
||||
message: { error: 'too_many_requests', error_description: 'Authentication rate limit exceeded' }
|
||||
});
|
||||
|
||||
const staticFileRateLimit = rateLimit({
|
||||
windowMs: 10 * 60 * 1000, // 10 minutes
|
||||
limit: 25, // 25 requests per 10 minutes for static files
|
||||
message: { error: 'too_many_requests', error_description: 'Static file rate limit exceeded' }
|
||||
});
|
||||
|
||||
// Mode-dependent auth configuration
|
||||
let bearerAuth: express.RequestHandler;
|
||||
|
|
@ -242,12 +255,12 @@ app.post("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTT
|
|||
app.delete("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTTP);
|
||||
|
||||
// Static assets
|
||||
app.get("/mcp-logo.png", (req, res) => {
|
||||
app.get("/mcp-logo.png", staticFileRateLimit, (req, res) => {
|
||||
const logoPath = path.join(__dirname, "static", "mcp.png");
|
||||
res.sendFile(logoPath);
|
||||
});
|
||||
|
||||
app.get("/styles.css", (req, res) => {
|
||||
app.get("/styles.css", staticFileRateLimit, (req, res) => {
|
||||
const cssPath = path.join(__dirname, "static", "styles.css");
|
||||
res.setHeader('Content-Type', 'text/css');
|
||||
res.sendFile(cssPath);
|
||||
|
|
@ -261,8 +274,8 @@ app.get("/", (req, res) => {
|
|||
|
||||
// Upstream auth routes (only in integrated mode)
|
||||
if (AUTH_MODE === 'integrated') {
|
||||
app.get("/fakeupstreamauth/authorize", cors(corsOptions), handleFakeAuthorize);
|
||||
app.get("/fakeupstreamauth/callback", cors(corsOptions), handleFakeAuthorizeRedirect);
|
||||
app.get("/fakeupstreamauth/authorize", fakeAuthRateLimit, cors(corsOptions), handleFakeAuthorize);
|
||||
app.get("/fakeupstreamauth/callback", fakeAuthRateLimit, cors(corsOptions), handleFakeAuthorizeRedirect);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue