From d68885cff84739d6e564ca4409959ae08cee67ec Mon Sep 17 00:00:00 2001 From: Lee Hanken Date: Sun, 26 Oct 2025 10:57:39 +0000 Subject: [PATCH] Add HTTP/SSE transport with graceful degradation for public deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Features: - HTTP/SSE server (server-http.js) for network access - Express-based web server with MCP SSE transport - Rate limiting (50 req/min per IP) - Request timeouts (30s) - Concurrent request limiting (max 10) - Circuit breaker pattern for failure handling - Memory monitoring (450MB threshold) - Gzip compression for responses - CORS support for cross-origin requests - Health check endpoint (/health) Infrastructure: - Updated package.json with new dependencies (express, cors, compression, rate-limit) - New npm script: start:http for HTTP server - Comprehensive deployment guide (DEPLOYMENT.md) - Updated README with deployment instructions Graceful Degradation: - Automatically rejects requests when at capacity - Circuit breaker opens after 5 failures - Memory-aware request handling - Per-IP rate limiting to prevent abuse The original stdio server (index.js) remains unchanged for local use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- DEPLOYMENT.md | 422 +++++++++++++++++++++++++ README.md | 79 ++++- package-lock.json | 749 ++++++++++++++++++++++++++++++++++---------- package.json | 7 +- server-http.js | 779 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1868 insertions(+), 168 deletions(-) create mode 100644 DEPLOYMENT.md create mode 100644 server-http.js diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..9906bd2 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,422 @@ +# Deployment Guide + +This guide provides step-by-step instructions for deploying the HPR Knowledge Base MCP Server to various hosting platforms. + +## Prerequisites + +- Git installed locally +- Account on your chosen hosting platform +- Node.js 18+ installed locally for testing + +## Quick Start + +1. Install dependencies: +```bash +npm install +``` + +2. Test locally: +```bash +npm run start:http +``` + +3. Verify the server is running: +```bash +curl http://localhost:3000/health +``` + +## Deployment Options + +### Option 1: Render.com (Recommended) + +**Cost**: Free tier available, $7/mo for always-on service + +**Steps**: + +1. Create a Render account at https://render.com + +2. Push your code to GitHub/GitLab/Bitbucket + +3. In Render Dashboard: + - Click "New +" → "Web Service" + - Connect your repository + - Configure: + - **Name**: `hpr-knowledge-base` + - **Environment**: `Node` + - **Build Command**: `npm install` + - **Start Command**: `npm run start:http` + - **Instance Type**: Free or Starter ($7/mo) + +4. Add environment variable (optional): + - `PORT` is automatically set by Render + +5. Click "Create Web Service" + +6. Your server will be available at: `https://hpr-knowledge-base.onrender.com` + +**Health Check Configuration**: +- Path: `/health` +- Success Codes: 200 + +**Auto-scaling**: Available on paid plans + +--- + +### Option 2: Railway.app + +**Cost**: $5 free credit/month, then pay-per-usage (~$5-10/mo for small services) + +**Steps**: + +1. Create a Railway account at https://railway.app + +2. Install Railway CLI (optional): +```bash +npm install -g @railway/cli +railway login +``` + +3. Deploy via CLI: +```bash +railway init +railway up +``` + +4. Or deploy via Dashboard: + - Click "New Project" + - Choose "Deploy from GitHub repo" + - Select your repository + - Railway auto-detects Node.js and runs `npm install` + +5. Add start command in Railway: + - Go to project settings + - Add custom start command: `npm run start:http` + +6. Your service URL will be generated automatically + +**Advantages**: +- Pay only for what you use +- Scales to zero when idle +- Simple deployment process + +--- + +### Option 3: Fly.io + +**Cost**: Free tier (256MB RAM), ~$3-5/mo beyond that + +**Steps**: + +1. Install Fly CLI: +```bash +curl -L https://fly.io/install.sh | sh +``` + +2. Login to Fly: +```bash +fly auth login +``` + +3. Create `fly.toml` in project root: +```toml +app = "hpr-knowledge-base" + +[build] + builder = "heroku/buildpacks:20" + +[env] + PORT = "8080" + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + +[[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 256 +``` + +4. Create `Procfile`: +``` +web: npm run start:http +``` + +5. Deploy: +```bash +fly launch --no-deploy +fly deploy +``` + +6. Your app will be at: `https://hpr-knowledge-base.fly.dev` + +**Advantages**: +- Global edge deployment +- Auto-scaling +- Good free tier + +--- + +### Option 4: Vercel (Serverless) + +**Cost**: Free tier generous (100GB bandwidth), Pro at $20/mo + +**Note**: Serverless functions have cold starts and are less ideal for MCP's persistent connection model, but can work. + +**Steps**: + +1. Install Vercel CLI: +```bash +npm install -g vercel +``` + +2. Create `vercel.json`: +```json +{ + "version": 2, + "builds": [ + { + "src": "server-http.js", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "server-http.js" + } + ] +} +``` + +3. Deploy: +```bash +vercel +``` + +4. Your app will be at: `https://hpr-knowledge-base.vercel.app` + +**Limitations**: +- 10 second timeout on hobby plan +- Cold starts may affect first request +- Less suitable for SSE persistent connections + +--- + +### Option 5: Self-Hosted VPS + +**Cost**: $4-6/mo (DigitalOcean, Hetzner, Linode) + +**Steps**: + +1. Create a VPS with Ubuntu 22.04 + +2. SSH into your server: +```bash +ssh root@your-server-ip +``` + +3. Install Node.js: +```bash +curl -fsSL https://deb.nodesource.com/setup_20.x | bash - +apt-get install -y nodejs +``` + +4. Clone your repository: +```bash +git clone https://github.com/yourusername/hpr-knowledge-base.git +cd hpr-knowledge-base +npm install +``` + +5. Install PM2 for process management: +```bash +npm install -g pm2 +``` + +6. Start the server: +```bash +pm2 start server-http.js --name hpr-mcp +pm2 save +pm2 startup +``` + +7. Set up nginx reverse proxy: +```bash +apt-get install nginx +``` + +Create `/etc/nginx/sites-available/hpr-mcp`: +```nginx +server { + listen 80; + server_name your-domain.com; + + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} +``` + +Enable site: +```bash +ln -s /etc/nginx/sites-available/hpr-mcp /etc/nginx/sites-enabled/ +nginx -t +systemctl restart nginx +``` + +8. (Optional) Add SSL with Let's Encrypt: +```bash +apt-get install certbot python3-certbot-nginx +certbot --nginx -d your-domain.com +``` + +--- + +## Configuration + +### Environment Variables + +- `PORT`: Server port (default: 3000) +- All other configuration is in `server-http.js` + +### Adjusting Limits + +Edit these constants in `server-http.js`: + +```javascript +const MAX_CONCURRENT_REQUESTS = 10; // Max concurrent connections +const REQUEST_TIMEOUT_MS = 30000; // Request timeout (30s) +const RATE_LIMIT_MAX_REQUESTS = 50; // Requests per minute per IP +const MEMORY_THRESHOLD_MB = 450; // Memory limit before rejection +const CIRCUIT_BREAKER_THRESHOLD = 5; // Failures before circuit opens +``` + +## Monitoring + +### Health Checks + +All platforms should use the `/health` endpoint: +```bash +curl https://your-server.com/health +``` + +### Logs + +**Render/Railway/Fly**: Check platform dashboard for logs + +**Self-hosted**: Use PM2: +```bash +pm2 logs hpr-mcp +``` + +### Metrics to Monitor + +- Response time +- Error rate +- Memory usage +- Active connections +- Rate limit hits + +## Troubleshooting + +### High Memory Usage + +The server loads ~35MB of data on startup. If memory usage exceeds 450MB: +- Increase server RAM +- Reduce `MAX_CONCURRENT_REQUESTS` +- Check for memory leaks + +### Circuit Breaker Opening + +If the circuit breaker opens frequently: +- Check error logs +- Verify data files are not corrupted +- Increase `CIRCUIT_BREAKER_THRESHOLD` + +### Rate Limiting Issues + +If legitimate users hit rate limits: +- Increase `RATE_LIMIT_MAX_REQUESTS` +- Implement authentication for higher limits +- Consider using API keys + +### Connection Timeouts + +If requests timeout: +- Increase `REQUEST_TIMEOUT_MS` +- Check server performance +- Verify network connectivity + +## Security Considerations + +1. **CORS**: Currently allows all origins. Restrict in production: +```javascript +app.use(cors({ + origin: 'https://your-allowed-domain.com' +})); +``` + +2. **Rate Limiting**: Adjust based on expected traffic + +3. **HTTPS**: Always use HTTPS in production + +4. **Environment Variables**: Use platform secrets for sensitive config + +## Updating the Server + +### Platform Deployments + +Most platforms auto-deploy on git push. Otherwise: + +**Render**: Push to git, auto-deploys + +**Railway**: `railway up` or push to git + +**Fly**: `fly deploy` + +**Vercel**: `vercel --prod` + +### Self-Hosted + +```bash +cd hpr-knowledge-base +git pull +npm install +pm2 restart hpr-mcp +``` + +## Cost Estimates + +| Platform | Free Tier | Paid Tier | Best For | +|----------|-----------|-----------|----------| +| Render | Yes (sleeps) | $7/mo | Always-on, simple | +| Railway | $5 credit | ~$5-10/mo | Pay-per-use | +| Fly.io | 256MB RAM | ~$3-5/mo | Global deployment | +| Vercel | 100GB bandwidth | $20/mo | High traffic | +| VPS | No | $4-6/mo | Full control | + +## Support + +For deployment issues: +- Check platform documentation +- Review server logs +- Test locally first +- Open an issue on GitHub + +## Next Steps + +After deployment: +1. Test the `/health` endpoint +2. Try the `/sse` endpoint with an MCP client +3. Monitor logs for errors +4. Set up alerts for downtime +5. Document your deployment URL diff --git a/README.md b/README.md index ebce28a..4742418 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,27 @@ chmod +x index.js ## Usage -### Running Locally +### Running Locally (Stdio Mode) -You can test the server directly: +You can test the stdio server directly (for local MCP clients like Claude Desktop): ```bash npm start ``` +### Running as HTTP Server (Network Access) + +For network access and public deployment, use the HTTP/SSE server: + +```bash +npm run start:http +``` + +This starts an HTTP server on port 3000 (configurable via `PORT` environment variable) with: +- **SSE endpoint**: `http://localhost:3000/sse` +- **Health check**: `http://localhost:3000/health` +- Built-in rate limiting, compression, and graceful degradation + ### Using with Claude Desktop Add this to your Claude Desktop configuration file: @@ -183,11 +196,71 @@ knowledge_base/ └── ... ``` +## Deployment + +The HTTP/SSE server (`server-http.js`) is designed for public deployment with graceful degradation features: + +### Features + +- **Rate Limiting**: 50 requests per minute per IP address +- **Request Timeouts**: 30-second timeout per request +- **Concurrent Request Limiting**: Maximum 10 concurrent requests +- **Circuit Breaker**: Automatically stops accepting requests if failure rate is too high +- **Memory Monitoring**: Rejects requests if memory usage exceeds 450MB +- **Compression**: Gzip compression for all responses +- **CORS**: Enabled for cross-origin requests + +### Recommended Hosting Options + +#### Render.com (Recommended) +```bash +# Free tier available, $7/mo for always-on +# Auto-scaling and health checks built-in +``` + +#### Railway.app +```bash +# $5 free credit/month, pay-per-usage +# Scales to zero when idle +``` + +#### Fly.io +```bash +# Free tier: 256MB RAM +# Global edge deployment +``` + +### Environment Variables + +- `PORT`: Server port (default: 3000) + +### Health Check + +The server provides a health check endpoint at `/health` for monitoring: + +```bash +curl http://localhost:3000/health +``` + +Returns: +```json +{ + "status": "ok", + "memory": { + "used": "45.23MB", + "threshold": "450MB" + }, + "activeRequests": 2, + "circuitBreaker": "CLOSED" +} +``` + ## Development ### Project Structure -- `index.js` - Main MCP server implementation +- `index.js` - Stdio MCP server (for local use) +- `server-http.js` - HTTP/SSE MCP server (for network deployment) - `data-loader.js` - Data loading and searching functionality - `package.json` - Node.js package configuration diff --git a/package-lock.json b/package-lock.json index 860a80b..75fd87c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,22 @@ { - "name": "knowledge_base", + "name": "hpr-knowledge-base-mcp", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "knowledge_base", + "name": "hpr-knowledge-base-mcp", "version": "1.0.0", - "license": "ISC", + "license": "CC-BY-SA", "dependencies": { - "@modelcontextprotocol/sdk": "^1.20.2" + "@modelcontextprotocol/sdk": "^1.20.2", + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5" + }, + "bin": { + "hpr-mcp": "index.js" } }, "node_modules/@modelcontextprotocol/sdk": { @@ -35,7 +42,7 @@ "node": ">=18" } }, - "node_modules/accepts": { + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", @@ -48,6 +55,280 @@ "node": ">= 0.6" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -64,24 +345,49 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" }, "engines": { - "node": ">=18" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, "node_modules/bytes": { @@ -122,10 +428,40 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -144,22 +480,19 @@ } }, "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" }, "node_modules/cors": { "version": "2.8.5", @@ -189,20 +522,12 @@ } }, "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "ms": "2.0.0" } }, "node_modules/depd": { @@ -214,6 +539,16 @@ "node": ">= 0.8" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -310,41 +645,45 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "engines": { - "node": ">= 18" + "node": ">= 0.10.0" }, "funding": { "type": "opencollective", @@ -379,17 +718,18 @@ "license": "MIT" }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" @@ -405,12 +745,12 @@ } }, "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/function-bind": { @@ -511,22 +851,13 @@ "node": ">= 0.8" } }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "safer-buffer": ">= 2.1.2 < 3" }, "engines": { "node": ">=0.10.0" @@ -575,26 +906,44 @@ } }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", - "engines": { - "node": ">=18" - }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -605,27 +954,36 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" } }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -664,6 +1022,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -692,14 +1059,10 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/pkce-challenge": { "version": "5.0.0", @@ -733,12 +1096,12 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -803,6 +1166,39 @@ "node": ">= 18" } }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -830,40 +1226,57 @@ "license": "MIT" }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" }, "engines": { - "node": ">= 18" + "node": ">= 0.8.0" } }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "license": "MIT", "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" }, "engines": { - "node": ">= 18" + "node": ">= 0.8.0" } }, "node_modules/setprototypeof": { @@ -966,9 +1379,9 @@ } }, "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -984,14 +1397,13 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" }, "engines": { "node": ">= 0.6" @@ -1015,6 +1427,15 @@ "punycode": "^2.1.0" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 6636502..9c51bdb 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,17 @@ }, "scripts": { "start": "node index.js", + "start:http": "node server-http.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": ["mcp", "hacker-public-radio", "hpr", "podcast", "knowledge-base"], "author": "", "license": "CC-BY-SA", "dependencies": { - "@modelcontextprotocol/sdk": "^1.20.2" + "@modelcontextprotocol/sdk": "^1.20.2", + "express": "^4.18.2", + "cors": "^2.8.5", + "compression": "^1.7.4", + "express-rate-limit": "^7.1.5" } } diff --git a/server-http.js b/server-http.js new file mode 100644 index 0000000..ad947fe --- /dev/null +++ b/server-http.js @@ -0,0 +1,779 @@ +#!/usr/bin/env node + +import express from 'express'; +import cors from 'cors'; +import compression from 'compression'; +import rateLimit from 'express-rate-limit'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import HPRDataLoader from './data-loader.js'; + +// Configuration +const PORT = process.env.PORT || 3000; +const MAX_CONCURRENT_REQUESTS = 10; +const REQUEST_TIMEOUT_MS = 30000; +const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute +const RATE_LIMIT_MAX_REQUESTS = 50; // 50 requests per minute per IP +const MEMORY_THRESHOLD_MB = 450; +const CIRCUIT_BREAKER_THRESHOLD = 5; +const CIRCUIT_BREAKER_TIMEOUT_MS = 60000; + +// Initialize data loader +console.error('Loading HPR knowledge base data...'); +const dataLoader = new HPRDataLoader(); +await dataLoader.load(); +console.error('Data loaded successfully!'); + +// Circuit Breaker class for graceful degradation +class CircuitBreaker { + constructor(threshold = CIRCUIT_BREAKER_THRESHOLD, timeout = CIRCUIT_BREAKER_TIMEOUT_MS) { + this.failures = 0; + this.threshold = threshold; + this.timeout = timeout; + this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN + this.nextAttempt = Date.now(); + } + + async execute(fn) { + if (this.state === 'OPEN') { + if (Date.now() < this.nextAttempt) { + throw new Error('Service temporarily unavailable. Please try again later.'); + } + this.state = 'HALF_OPEN'; + } + + try { + const result = await fn(); + if (this.state === 'HALF_OPEN') { + this.state = 'CLOSED'; + this.failures = 0; + } + return result; + } catch (error) { + this.failures++; + if (this.failures >= this.threshold) { + this.state = 'OPEN'; + this.nextAttempt = Date.now() + this.timeout; + console.error(`Circuit breaker opened after ${this.failures} failures`); + } + throw error; + } + } + + reset() { + this.failures = 0; + this.state = 'CLOSED'; + } +} + +const circuitBreaker = new CircuitBreaker(); + +// Request timeout wrapper +function withTimeout(promise, timeoutMs = REQUEST_TIMEOUT_MS) { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Request timeout')), timeoutMs) + ), + ]); +} + +// Concurrent request limiter +let activeRequests = 0; + +function checkConcurrency() { + if (activeRequests >= MAX_CONCURRENT_REQUESTS) { + throw new Error('Server at capacity. Please try again later.'); + } +} + +// Memory monitoring +let memoryWarning = false; + +function checkMemory() { + const usage = process.memoryUsage(); + const heapUsedMB = usage.heapUsed / 1024 / 1024; + + if (heapUsedMB > MEMORY_THRESHOLD_MB) { + if (!memoryWarning) { + console.error(`High memory usage: ${heapUsedMB.toFixed(2)}MB`); + memoryWarning = true; + } + throw new Error('Server under high load. Please try again later.'); + } else if (memoryWarning && heapUsedMB < MEMORY_THRESHOLD_MB * 0.8) { + memoryWarning = false; + } +} + +setInterval(() => { + const usage = process.memoryUsage(); + const heapUsedMB = usage.heapUsed / 1024 / 1024; + console.error(`Memory: ${heapUsedMB.toFixed(2)}MB, Active requests: ${activeRequests}`); +}, 30000); + +// Helper function to strip HTML tags +function stripHtml(html) { + return html + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/"/g, '"') + .trim(); +} + +// Helper to format episode for display +function formatEpisode(episode, includeNotes = false) { + const host = dataLoader.getHost(episode.hostid); + const seriesInfo = episode.series !== 0 ? dataLoader.getSeries(episode.series) : null; + + let result = `# HPR${String(episode.id).padStart(4, '0')}: ${episode.title} + +**Date:** ${episode.date} +**Host:** ${host?.host || 'Unknown'} (ID: ${episode.hostid}) +**Duration:** ${Math.floor(episode.duration / 60)}:${String(episode.duration % 60).padStart(2, '0')} +**Tags:** ${episode.tags} +**License:** ${episode.license} +**Downloads:** ${episode.downloads} + +## Summary +${episode.summary}`; + + if (seriesInfo) { + result += `\n\n## Series +**${seriesInfo.name}**: ${stripHtml(seriesInfo.description)}`; + } + + if (includeNotes && episode.notes) { + result += `\n\n## Host Notes\n${stripHtml(episode.notes)}`; + } + + return result; +} + +// Create MCP server factory +function createMCPServer() { + const server = new Server( + { + name: 'hpr-knowledge-base', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + resources: {}, + }, + } + ); + + // List available resources + server.setRequestHandler(ListResourcesRequestSchema, async () => { + return { + resources: [ + { + uri: 'hpr://stats', + mimeType: 'text/plain', + name: 'HPR Statistics', + description: 'Overall statistics about the HPR knowledge base', + }, + { + uri: 'hpr://episodes/recent', + mimeType: 'text/plain', + name: 'Recent Episodes', + description: 'List of 50 most recent HPR episodes', + }, + { + uri: 'hpr://hosts/all', + mimeType: 'text/plain', + name: 'All Hosts', + description: 'List of all HPR hosts', + }, + { + uri: 'hpr://series/all', + mimeType: 'text/plain', + name: 'All Series', + description: 'List of all HPR series', + }, + ], + }; + }); + + // Read a resource + server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const uri = request.params.uri; + + if (uri === 'hpr://stats') { + const stats = dataLoader.getStats(); + return { + contents: [ + { + uri, + mimeType: 'text/plain', + text: `# Hacker Public Radio Statistics + +**Total Episodes:** ${stats.totalEpisodes} +**Total Hosts:** ${stats.totalHosts} +**Total Comments:** ${stats.totalComments} +**Total Series:** ${stats.totalSeries} +**Transcripts Available:** ${stats.totalTranscripts} + +**Date Range:** ${stats.dateRange.earliest} to ${stats.dateRange.latest} + +Hacker Public Radio is a community-driven podcast released under Creative Commons licenses. +All content is contributed by the community, for the community.`, + }, + ], + }; + } + + if (uri === 'hpr://episodes/recent') { + const recent = dataLoader.searchEpisodes('', { limit: 50 }); + const text = recent.map(ep => { + const host = dataLoader.getHost(ep.hostid); + return `**HPR${String(ep.id).padStart(4, '0')}** (${ep.date}) - ${ep.title} by ${host?.host || 'Unknown'}`; + }).join('\n'); + + return { + contents: [ + { + uri, + mimeType: 'text/plain', + text: `# Recent Episodes\n\n${text}`, + }, + ], + }; + } + + if (uri === 'hpr://hosts/all') { + const hosts = dataLoader.hosts + .filter(h => h.valid === 1) + .map(h => { + const episodeCount = dataLoader.getEpisodesByHost(h.hostid).length; + return `**${h.host}** (ID: ${h.hostid}) - ${episodeCount} episodes`; + }) + .join('\n'); + + return { + contents: [ + { + uri, + mimeType: 'text/plain', + text: `# All HPR Hosts\n\n${hosts}`, + }, + ], + }; + } + + if (uri === 'hpr://series/all') { + const series = dataLoader.series + .filter(s => s.valid === 1 && s.private === 0) + .map(s => { + const episodeCount = dataLoader.getEpisodesInSeries(s.id).length; + return `**${s.name}** (ID: ${s.id}) - ${episodeCount} episodes\n ${stripHtml(s.description)}`; + }) + .join('\n\n'); + + return { + contents: [ + { + uri, + mimeType: 'text/plain', + text: `# All HPR Series\n\n${series}`, + }, + ], + }; + } + + throw new Error(`Unknown resource: ${uri}`); + }); + + // List available tools + server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'search_episodes', + description: 'Search HPR episodes by keywords in title, summary, tags, or host notes. Can filter by host, series, tags, and date range.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query (searches title, summary, tags, and notes)', + }, + limit: { + type: 'number', + description: 'Maximum number of results to return (default: 20)', + }, + hostId: { + type: 'number', + description: 'Filter by host ID', + }, + seriesId: { + type: 'number', + description: 'Filter by series ID', + }, + tag: { + type: 'string', + description: 'Filter by tag', + }, + fromDate: { + type: 'string', + description: 'Filter episodes from this date (YYYY-MM-DD)', + }, + toDate: { + type: 'string', + description: 'Filter episodes to this date (YYYY-MM-DD)', + }, + }, + required: [], + }, + }, + { + name: 'get_episode', + description: 'Get detailed information about a specific HPR episode including transcript if available', + inputSchema: { + type: 'object', + properties: { + episodeId: { + type: 'number', + description: 'Episode ID number', + }, + includeTranscript: { + type: 'boolean', + description: 'Include full transcript if available (default: true)', + }, + includeComments: { + type: 'boolean', + description: 'Include community comments (default: true)', + }, + }, + required: ['episodeId'], + }, + }, + { + name: 'search_transcripts', + description: 'Search through episode transcripts for specific keywords or phrases', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query to find in transcripts', + }, + limit: { + type: 'number', + description: 'Maximum number of episodes to return (default: 20)', + }, + contextLines: { + type: 'number', + description: 'Number of lines of context around matches (default: 3)', + }, + }, + required: ['query'], + }, + }, + { + name: 'get_host_info', + description: 'Get information about an HPR host including all their episodes', + inputSchema: { + type: 'object', + properties: { + hostId: { + type: 'number', + description: 'Host ID number', + }, + hostName: { + type: 'string', + description: 'Host name (will search if hostId not provided)', + }, + includeEpisodes: { + type: 'boolean', + description: 'Include list of all episodes by this host (default: true)', + }, + }, + required: [], + }, + }, + { + name: 'get_series_info', + description: 'Get information about an HPR series including all episodes in the series', + inputSchema: { + type: 'object', + properties: { + seriesId: { + type: 'number', + description: 'Series ID number', + }, + }, + required: ['seriesId'], + }, + }, + ], + }; + }); + + // Handle tool calls + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + if (name === 'search_episodes') { + const results = dataLoader.searchEpisodes(args.query || '', { + limit: args.limit || 20, + hostId: args.hostId, + seriesId: args.seriesId, + tag: args.tag, + fromDate: args.fromDate, + toDate: args.toDate, + }); + + const text = results.length > 0 + ? results.map(ep => formatEpisode(ep, false)).join('\n\n---\n\n') + : 'No episodes found matching your search criteria.'; + + return { + content: [ + { + type: 'text', + text: `# Search Results (${results.length} episodes found)\n\n${text}`, + }, + ], + }; + } + + if (name === 'get_episode') { + const episode = dataLoader.getEpisode(args.episodeId); + + if (!episode) { + return { + content: [ + { + type: 'text', + text: `Episode ${args.episodeId} not found.`, + }, + ], + }; + } + + let text = formatEpisode(episode, true); + + // Add transcript if requested and available + if (args.includeTranscript !== false) { + const transcript = dataLoader.getTranscript(args.episodeId); + if (transcript) { + text += `\n\n## Transcript\n\n${transcript}`; + } else { + text += `\n\n## Transcript\n\n*No transcript available for this episode.*`; + } + } + + // Add comments if requested + if (args.includeComments !== false) { + const comments = dataLoader.getCommentsForEpisode(args.episodeId); + if (comments.length > 0) { + text += `\n\n## Comments (${comments.length})\n\n`; + text += comments.map(c => + `**${c.comment_author_name}** (${c.comment_timestamp})${c.comment_title ? ` - ${c.comment_title}` : ''}\n${c.comment_text}` + ).join('\n\n---\n\n'); + } + } + + return { + content: [ + { + type: 'text', + text, + }, + ], + }; + } + + if (name === 'search_transcripts') { + const results = dataLoader.searchTranscripts(args.query, { + limit: args.limit || 20, + contextLines: args.contextLines || 3, + }); + + if (results.length === 0) { + return { + content: [ + { + type: 'text', + text: `No transcripts found containing "${args.query}".`, + }, + ], + }; + } + + const text = results.map(result => { + const { episode, matches } = result; + const host = dataLoader.getHost(episode.hostid); + + let episodeText = `# HPR${String(episode.id).padStart(4, '0')}: ${episode.title} +**Host:** ${host?.host || 'Unknown'} | **Date:** ${episode.date} + +**Matches found:** ${matches.length} + +`; + + matches.forEach(match => { + episodeText += `### Line ${match.lineNumber} +\`\`\` +${match.context} +\`\`\` + +`; + }); + + return episodeText; + }).join('\n---\n\n'); + + return { + content: [ + { + type: 'text', + text: `# Transcript Search Results (${results.length} episodes)\n\nSearching for: "${args.query}"\n\n${text}`, + }, + ], + }; + } + + if (name === 'get_host_info') { + let host; + + if (args.hostId) { + host = dataLoader.getHost(args.hostId); + } else if (args.hostName) { + const hosts = dataLoader.searchHosts(args.hostName); + host = hosts[0]; + } + + if (!host) { + return { + content: [ + { + type: 'text', + text: 'Host not found.', + }, + ], + }; + } + + let text = `# ${host.host} + +**Host ID:** ${host.hostid} +**Email:** ${host.email} +**License:** ${host.license} +**Profile:** ${stripHtml(host.profile)} +`; + + if (args.includeEpisodes !== false) { + const episodes = dataLoader.getEpisodesByHost(host.hostid); + text += `\n**Total Episodes:** ${episodes.length}\n\n## Episodes\n\n`; + + // Sort by date (newest first) + episodes.sort((a, b) => b.date.localeCompare(a.date)); + + text += episodes.map(ep => + `**HPR${String(ep.id).padStart(4, '0')}** (${ep.date}) - ${ep.title}\n ${ep.summary}` + ).join('\n\n'); + } + + return { + content: [ + { + type: 'text', + text, + }, + ], + }; + } + + if (name === 'get_series_info') { + const series = dataLoader.getSeries(args.seriesId); + + if (!series) { + return { + content: [ + { + type: 'text', + text: `Series ${args.seriesId} not found.`, + }, + ], + }; + } + + const episodes = dataLoader.getEpisodesInSeries(args.seriesId); + + let text = `# ${series.name} + +**Series ID:** ${series.id} +**Description:** ${stripHtml(series.description)} +**Total Episodes:** ${episodes.length} + +## Episodes in Series + +`; + + // Sort by date + episodes.sort((a, b) => a.date.localeCompare(b.date)); + + text += episodes.map((ep, index) => { + const host = dataLoader.getHost(ep.hostid); + return `${index + 1}. **HPR${String(ep.id).padStart(4, '0')}** (${ep.date}) - ${ep.title} by ${host?.host || 'Unknown'}\n ${ep.summary}`; + }).join('\n\n'); + + return { + content: [ + { + type: 'text', + text, + }, + ], + }; + } + + throw new Error(`Unknown tool: ${name}`); + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error: ${error.message}`, + }, + ], + isError: true, + }; + } + }); + + return server; +} + +// Create Express app +const app = express(); + +// Enable CORS +app.use(cors()); + +// Enable compression +app.use(compression()); + +// Rate limiting +const limiter = rateLimit({ + windowMs: RATE_LIMIT_WINDOW_MS, + max: RATE_LIMIT_MAX_REQUESTS, + message: 'Too many requests from this IP, please try again later.', + standardHeaders: true, + legacyHeaders: false, +}); + +app.use(limiter); + +// Health check endpoint +app.get('/health', (req, res) => { + const usage = process.memoryUsage(); + const heapUsedMB = usage.heapUsed / 1024 / 1024; + + res.json({ + status: 'ok', + memory: { + used: `${heapUsedMB.toFixed(2)}MB`, + threshold: `${MEMORY_THRESHOLD_MB}MB`, + }, + activeRequests, + circuitBreaker: circuitBreaker.state, + }); +}); + +// SSE endpoint for MCP +app.get('/sse', async (req, res) => { + try { + // Check system health + checkMemory(); + checkConcurrency(); + + activeRequests++; + console.error(`New SSE connection. Active requests: ${activeRequests}`); + + // Create a new MCP server instance for this connection + const server = createMCPServer(); + + // Create SSE transport + const transport = new SSEServerTransport('/message', res); + + // Connect server with timeout and circuit breaker + await withTimeout( + circuitBreaker.execute(() => server.connect(transport)), + REQUEST_TIMEOUT_MS + ); + + // Handle connection close + req.on('close', () => { + activeRequests--; + console.error(`SSE connection closed. Active requests: ${activeRequests}`); + }); + + } catch (error) { + activeRequests--; + console.error('SSE connection error:', error.message); + + if (!res.headersSent) { + res.status(503).json({ + error: error.message, + circuitBreaker: circuitBreaker.state, + }); + } + } +}); + +// POST endpoint for MCP messages +app.post('/message', express.json(), async (req, res) => { + try { + // SSE transport handles this internally + res.status(200).send(); + } catch (error) { + console.error('Message error:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// Error handling middleware +app.use((err, req, res, next) => { + console.error('Express error:', err); + res.status(500).json({ + error: 'Internal server error', + message: err.message, + }); +}); + +// Start server +app.listen(PORT, () => { + console.error(`HPR Knowledge Base MCP Server running on http://localhost:${PORT}`); + console.error(`SSE endpoint: http://localhost:${PORT}/sse`); + console.error(`Health check: http://localhost:${PORT}/health`); + console.error(`Configuration:`); + console.error(` - Max concurrent requests: ${MAX_CONCURRENT_REQUESTS}`); + console.error(` - Request timeout: ${REQUEST_TIMEOUT_MS}ms`); + console.error(` - Rate limit: ${RATE_LIMIT_MAX_REQUESTS} requests per ${RATE_LIMIT_WINDOW_MS / 1000}s`); + console.error(` - Memory threshold: ${MEMORY_THRESHOLD_MB}MB`); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + console.error('SIGTERM received, shutting down gracefully...'); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.error('SIGINT received, shutting down gracefully...'); + process.exit(0); +});