yt-dlp-mcp/src/http/middleware.mts
yachi 8892f3df92
feat: Add Streamable HTTP remote server support (#4)
Add production-ready HTTP server for remote access to yt-dlp-mcp tools
using the official MCP Streamable HTTP transport protocol.

Features:
- MCP spec-compliant Streamable HTTP transport with SSE
- API key authentication with timing-safe comparison
- CORS configuration with credential handling
- Rate limiting per session (60 req/min default)
- Session management with automatic cleanup
- Request size limits (4MB) and input validation
- Health check endpoint
- Graceful shutdown handling
- Memory leak protection (1000 events/session max)

New binary:
- yt-dlp-mcp-http - HTTP server (separate from stdio server)

Configuration via environment variables:
- YTDLP_HTTP_PORT (default: 3000)
- YTDLP_HTTP_HOST (default: 0.0.0.0)
- YTDLP_API_KEY (recommended for production)
- YTDLP_CORS_ORIGIN (default: *)
- YTDLP_RATE_LIMIT (default: 60)
- YTDLP_SESSION_TIMEOUT (default: 1 hour)

Endpoints:
- POST/GET/DELETE /mcp - MCP protocol endpoint
- GET /health - Health check endpoint

Documentation:
- Comprehensive guide in docs/remote-server.md
- Quick start, deployment examples, security best practices
- Client configuration for Claude Desktop and Cline
2025-10-19 18:17:15 +01:00

72 lines
1.8 KiB
TypeScript

/**
* Express middleware for authentication and rate limiting
*/
import type { Request, Response, NextFunction } from "express";
import rateLimit from "express-rate-limit";
import { timingSafeEqual } from "crypto";
import { API_KEY, RATE_LIMIT } from "./config.mjs";
/**
* Validate API key using constant-time comparison to prevent timing attacks
*/
function validateApiKey(req: Request): boolean {
if (!API_KEY) return true;
const authHeader = req.headers.authorization;
if (!authHeader) return false;
const token = authHeader.replace(/^Bearer\s+/i, '');
// Constant-time comparison to prevent timing attacks
if (token.length !== API_KEY.length) return false;
try {
return timingSafeEqual(
Buffer.from(token),
Buffer.from(API_KEY)
);
} catch {
return false;
}
}
/**
* API key authentication middleware
*/
export function apiKeyMiddleware(req: Request, res: Response, next: NextFunction): void {
if (req.path === '/health') {
return next();
}
if (!validateApiKey(req)) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
next();
}
/**
* Rate limiting middleware using express-rate-limit
*/
export const rateLimitMiddleware = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: RATE_LIMIT,
keyGenerator: (req: Request) => {
// Use session ID for per-session rate limiting
const sessionId = Array.isArray(req.headers['mcp-session-id'])
? req.headers['mcp-session-id'][0]
: req.headers['mcp-session-id'];
return sessionId || req.ip || 'anonymous';
},
standardHeaders: true,
legacyHeaders: false,
handler: (_req: Request, res: Response) => {
res.status(429).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Rate limit exceeded' },
});
},
});