example-remote-server/src/index.ts
guro 67ffeb0b93
Increase rate limits across server endpoints
Raise request limits to better accommodate legitimate traffic patterns:
- Splash page: 50 -> 200 req/min
- Auth endpoints: 20 -> 200 req/min
- Static assets (auth & mcp): 100 -> 500 req/min
- OAuth metadata: 100 -> 300 req/5s
- Client registration: 10 -> 60 req/min
2026-03-17 14:52:21 +00:00

298 lines
No EOL
11 KiB
TypeScript

/**
* MCP Feature Reference Server - Unified Entry Point
*
* This server demonstrates the recommended pattern for MCP servers with OAuth:
* - Auth functionality is always architecturally separate from MCP
* - In 'internal' mode: Auth server runs in-process for convenience
* - In 'external' mode: Auth server runs separately (Auth0, Okta, or standalone)
*
* The auth module acts as a stand-in for an external OAuth server, even when
* running internally. This is NOT the deprecated integrated auth pattern.
*/
import express from 'express';
import cors from 'cors';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import rateLimit from 'express-rate-limit';
import { config } from './config.js';
import { AuthModule } from './modules/auth/index.js';
import { MCPModule } from './modules/mcp/index.js';
import { ExampleAppsModule, AVAILABLE_EXAMPLES } from './modules/example-apps/index.js';
import { ExternalTokenValidator, InternalTokenValidator, ITokenValidator } from './interfaces/auth-validator.js';
import { redisClient } from './modules/shared/redis.js';
import { logger } from './modules/shared/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Format slug to human-readable name (e.g., 'budget-allocator' -> 'Budget Allocator')
function formatServerName(slug: string): string {
return slug.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}
async function main() {
// Determine server type based on auth mode
const isAuthServerOnly = config.auth.mode === 'auth_server';
const serverType = isAuthServerOnly ? 'OAuth Authorization Server' : 'MCP Feature Reference Server';
console.log('');
console.log('========================================');
console.log(serverType);
console.log('========================================');
const app = express();
// Trust proxy headers (X-Forwarded-For, etc.) when behind reverse proxy (Cloudflare, etc.)
// This is required for rate limiting to work correctly with real client IPs
app.set('trust proxy', true);
// Basic middleware
// Intentionally permissive CORS for public MCP reference server
// This allows any MCP client to test against this reference implementation
app.use(cors({ origin: true, credentials: true }));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(logger.middleware());
// Connect to Redis if configured
if (config.redis.enabled && config.redis.url) {
try {
await redisClient.connect();
console.log('Connected to Redis');
} catch (error) {
logger.error('Failed to connect to Redis', error as Error);
if (config.nodeEnv === 'production') {
process.exit(1);
}
console.log('WARNING: Continuing without Redis (development mode)');
}
}
// OAuth metadata discovery endpoint
// Only served by MCP servers (not standalone auth servers)
if (config.auth.mode !== 'auth_server') {
app.get('/.well-known/oauth-authorization-server', (req, res) => {
// Log the metadata discovery request
logger.info('OAuth metadata discovery', {
userAgent: req.get('user-agent'),
authMode: config.auth.mode,
ip: req.ip
});
// Determine the auth server URL based on mode
const authServerUrl = config.auth.mode === 'internal'
? config.baseUri // Internal mode: auth is in same process
: config.auth.externalUrl!; // External mode: separate auth server
res.json({
issuer: authServerUrl,
authorization_endpoint: `${authServerUrl}/authorize`,
token_endpoint: `${authServerUrl}/token`,
registration_endpoint: `${authServerUrl}/register`,
introspection_endpoint: `${authServerUrl}/introspect`,
revocation_endpoint: `${authServerUrl}/revoke`,
token_endpoint_auth_methods_supported: ['none'],
response_types_supported: ['code'],
grant_types_supported: ['authorization_code', 'refresh_token'],
code_challenge_methods_supported: ['S256'],
service_documentation: 'https://modelcontextprotocol.io'
});
});
}
// Initialize modules based on auth mode
let tokenValidator: ITokenValidator | undefined;
if (config.auth.mode === 'internal' || config.auth.mode === 'auth_server') {
// ========================================
// INTERNAL MODE or AUTH_SERVER MODE: Mount auth endpoints
// ========================================
if (config.auth.mode === 'auth_server') {
console.log('Mode: STANDALONE AUTH SERVER');
console.log(' Serving OAuth 2.0 endpoints only');
} else {
console.log('Auth Mode: INTERNAL (all-in-one)');
console.log(' Running auth server in-process for demo/development');
}
console.log('');
// Create auth module
const authModule = new AuthModule({
baseUri: config.baseUri,
authServerUrl: config.baseUri, // Points to itself
redisUrl: config.redis.url
});
// Mount auth routes
app.use('/', authModule.getRouter());
// Create internal token validator for MCP (if not auth-only mode)
if (config.auth.mode === 'internal') {
tokenValidator = new InternalTokenValidator(authModule);
}
console.log('Auth Endpoints:');
console.log(` Register Client: POST ${config.baseUri}/register`);
console.log(` Authorize: GET ${config.baseUri}/authorize`);
console.log(` Get Token: POST ${config.baseUri}/token`);
console.log(` Introspect: POST ${config.baseUri}/introspect`);
console.log(` Revoke: POST ${config.baseUri}/revoke`);
} else if (config.auth.mode === 'external') {
// ========================================
// EXTERNAL MODE: MCP only, auth elsewhere
// ========================================
console.log('Auth Mode: EXTERNAL');
console.log(` Using external auth server: ${config.auth.externalUrl}`);
console.log('');
// Create external token validator (HTTP calls)
tokenValidator = new ExternalTokenValidator(config.auth.externalUrl!);
}
// ========================================
// MCP Module (skip for standalone auth server)
// ========================================
if (config.auth.mode !== 'auth_server') {
if (!tokenValidator) {
throw new Error('Token validator not initialized');
}
const mcpModule = new MCPModule(
{
baseUri: config.baseUri,
redisUrl: config.redis.url
},
tokenValidator
);
// Mount MCP routes
app.use('/', mcpModule.getRouter());
// Mount Example Apps module (MCP Apps servers at /:slug/mcp)
const exampleAppsModule = new ExampleAppsModule(
{ baseUri: config.baseUri },
);
app.use('/', exampleAppsModule.getRouter());
console.log('');
console.log('MCP Endpoints:');
console.log(` Streamable HTTP: ${config.baseUri}/mcp`);
console.log(` SSE (legacy): ${config.baseUri}/sse`);
console.log(` OAuth Metadata: ${config.baseUri}/.well-known/oauth-authorization-server`);
console.log('');
console.log('MCP App Example Servers:');
for (const slug of AVAILABLE_EXAMPLES) {
console.log(` ${config.baseUri}/${slug}/mcp`);
}
}
// Rate limiter for splash page (moderate limit)
const splashPageLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 200, // 200 requests per minute
message: 'Too many requests to splash page',
standardHeaders: true,
legacyHeaders: false,
});
// Splash page (customize based on mode)
app.get('/', splashPageLimiter, (req, res) => {
if (config.auth.mode === 'auth_server') {
// Simple splash page for standalone auth server
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>OAuth Authorization Server</title>
<style>
body {
font-family: system-ui, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
h1 { color: #333; }
.endpoint {
background: #f5f5f5;
padding: 10px;
margin: 5px 0;
font-family: monospace;
}
</style>
</head>
<body>
<h1>OAuth Authorization Server</h1>
<p>This is a demo standalone OAuth 2.0 authorization server for MCP.</p>
<h2>Available Endpoints</h2>
<div class="endpoint">POST ${config.baseUri}/register - Register OAuth client</div>
<div class="endpoint">GET ${config.baseUri}/authorize - Authorization endpoint</div>
<div class="endpoint">POST ${config.baseUri}/token - Token endpoint</div>
<div class="endpoint">POST ${config.baseUri}/introspect - Token introspection</div>
<div class="endpoint">POST ${config.baseUri}/revoke - Token revocation</div>
</body>
</html>
`);
} else {
const srcStaticDir = path.join(__dirname, 'static');
const splashPath = path.join(srcStaticDir, 'index.html');
let html = fs.readFileSync(splashPath, 'utf8');
// Inject example server endpoints with copy buttons
const baseUrl = config.baseUri.replace(/\/$/, ''); // Remove trailing slash if present
const exampleServersHtml = AVAILABLE_EXAMPLES.map(slug => {
const fullUrl = `${baseUrl}/${slug}/mcp`;
return `
<div class="endpoint endpoint-with-copy">
<div class="endpoint-info">
<span class="method post">POST</span>
<span>/${slug}/mcp - ${formatServerName(slug)} MCP App Server</span>
</div>
<button class="copy-btn" onclick="copyUrl(this, '${fullUrl}')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
<span class="btn-text">Copy</span>
</button>
</div>`;
}).join('');
html = html.replace('<!-- EXAMPLE_SERVERS_PLACEHOLDER -->', exampleServersHtml);
res.send(html);
}
});
// Start server
app.listen(config.port, () => {
console.log('');
console.log('========================================');
console.log(`Server running at: ${config.baseUri}`);
console.log('========================================');
console.log('');
if (config.auth.mode === 'auth_server') {
console.log('This server provides OAuth 2.0 endpoints only.');
console.log('To use with an MCP server:');
console.log(' 1. Start MCP server with AUTH_MODE=external');
console.log(` 2. Set AUTH_SERVER_URL=${config.baseUri}`);
} else if (config.auth.mode === 'internal') {
console.log('To switch to external auth:');
console.log(' 1. Start auth server separately');
console.log(' 2. Set AUTH_MODE=external');
console.log(' 3. Set AUTH_SERVER_URL=<auth-server-url>');
console.log(' 4. Restart this server');
} else if (config.auth.mode === 'external') {
console.log('To switch to internal auth:');
console.log(' 1. Set AUTH_MODE=internal');
console.log(' 2. Restart this server');
}
console.log('');
});
}
// Start the server
main().catch(error => {
console.error('Failed to start server:', error);
process.exit(1);
});