diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bb33d9d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# Docker ignore file for web-search-mcp +node_modules +npm-debug.log +Dockerfile +.dockerignore +README.md +.git +.gitignore +.env +.env.local +.env.development +.env.test +.env.production +coverage +.nyc_output +.DS_Store +*.log +logs +*.tgz +*.tar.gz +dist +build +.cache +.vscode +.idea +*.swp +*.swo +*~ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2ea3b16 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,78 @@ +# Multi-stage build for efficient Docker image +FROM node:20-alpine AS builder + +# Install system dependencies for Playwright +RUN apk add --no-cache \ + chromium \ + nss \ + freetype \ + freetype-dev \ + harfbuzz \ + ca-certificates \ + ttf-freefont + +# Set up Playwright environment +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + +# Create app directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY tsconfig.json ./ + +# Install ALL dependencies for build (including devDependencies) +RUN npm ci + +# Copy source code +COPY src/ ./src/ + +# Build the application +RUN npm run build + +# Production stage +FROM node:20-alpine as production + +# Install system dependencies for Playwright +RUN apk add --no-cache \ + chromium \ + nss \ + freetype \ + freetype-dev \ + harfbuzz \ + ca-certificates \ + ttf-freefont + +# Set up Playwright environment +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + +# Create app directory and user +WORKDIR /app +RUN addgroup -g 1001 -S nodejs +RUN adduser -S mcpserver -u 1001 + +# Copy package files for production install +COPY package*.json ./ + +# Install only production dependencies +RUN npm ci --only=production && npm cache clean --force + +# Copy built application +COPY --from=builder --chown=mcpserver:nodejs /app/dist ./dist + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3001 +ENV HOST=0.0.0.0 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD node -e "const http = require('http'); const options = { host: '0.0.0.0', port: 3001, path: '/health', timeout: 2000 }; const request = http.request(options, (res) => { if (res.statusCode === 200) { process.exit(0); } else { process.exit(1); } }); request.on('error', () => process.exit(1)); request.end();" + +# Switch to non-root user +USER mcpserver + +# Start the application +CMD ["node", "dist/streamable-server.js"] \ No newline at end of file diff --git a/README-STREAMABLE-DEPLOY.md b/README-STREAMABLE-DEPLOY.md new file mode 100644 index 0000000..b4d1e1d --- /dev/null +++ b/README-STREAMABLE-DEPLOY.md @@ -0,0 +1,436 @@ +# Web Search MCP Server - StreamableHttp Docker Deploy + +Questo documento descrive come utilizzare il server MCP Web Search con il nuovo transport **StreamableHttp** dockerizzato per l'integrazione con LiteLLM e altri gateway AI. + +## πŸš€ Panoramica + +Il server MCP Web Search supporta: + +- βœ… **StreamableHttp transport** (nuovo standard MCP 2025-06-18) +- βœ… **HTTP/REST endpoints** per chiamate dirette ai tools +- βœ… **Docker containerization** con multi-stage build +- βœ… **Health checks** e monitoring +- βœ… **Integrazione con LiteLLM** e altri gateway + +## πŸ“ Struttura del Progetto + +``` +web-search-mcp/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ index.ts # Server stdio originale +β”‚ β”œβ”€β”€ streamable-server.ts # Server StreamableHttp +β”‚ └── ... # Altri file esistenti +β”œβ”€β”€ Dockerfile # Multi-stage Docker build +β”œβ”€β”€ docker-compose.yml # Server MCP StreamableHttp +β”œβ”€β”€ mcp.json # Configurazioni MCP (stdio + StreamableHttp) +└── README-SSE-DEPLOY.md # Questa documentazione +``` + +## πŸ”§ Setup e Deployment + +### 1. Installazione Dipendenze + +```bash +# Le dipendenze sono giΓ  configurate nel package.json +npm install +``` + +### 2. Build e Test Locale + +```bash +# Build del progetto +npm run build + +# Test locale del server StreamableHttp +npm run dev:streamable + +# Il server sarΓ  disponibile su http://localhost:3001 +``` + +### 3. Deploy con Docker + +```bash +# Deploy solo server MCP StreamableHttp +docker-compose up -d +``` + +### 4. Verifica Deployment + +```bash +# Health check +curl http://localhost:3001/health + +# Info server MCP +curl http://localhost:3001/mcp/info + +# Test endpoint StreamableHttp (esempio inizializzazione) +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' +``` + +## 🌐 Endpoints del Server StreamableHttp + +| Endpoint | Metodo | Descrizione | +|----------|--------|-------------| +| `/health` | GET | Health check del server | +| `/mcp/info` | GET | Informazioni sul server MCP | +| `/mcp` | POST | Endpoint StreamableHttp per comunicazione MCP | + +### Esempi di chiamate MCP via StreamableHttp: + +**Inizializzazione:** +```bash +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' +``` + +**Lista tools disponibili:** +```bash +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' +``` + +**Chiamata tool:** +```bash +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc":"2.0", + "id":3, + "method":"tools/call", + "params":{ + "name":"get-web-search-summaries", + "arguments":{"query":"latest AI news","limit":3} + } + }' +``` + +## πŸ”— Integrazione con LiteLLM + +### 1. Configurazione LiteLLM + +Il file `litellm-config.yaml` dovrebbe essere configurato cosΓ¬: + +```yaml +model_list: + - model_name: gpt-4 + litellm_params: + model: openai/gpt-4 + api_key: "your-openai-api-key" + + - model_name: claude-3 + litellm_params: + model: anthropic/claude-3-sonnet-20240229 + api_key: "your-anthropic-api-key" + +tools: + - name: web-search-mcp + type: mcp + config: + transport: + type: http + url: "http://web-search-mcp:3001/mcp" + +general_settings: + master_key: "your-master-key" +``` + +### 2. Configurazione MCP Client + +**Per Claude Desktop, aggiorna `claude_desktop_config.json`:** + +```json +{ + "mcpServers": { + "web-search-local": { + "transport": { + "type": "http", + "url": "http://localhost:3001/mcp" + } + }, + "web-search-remote": { + "transport": { + "type": "http", + "url": "https://your-domain.com/mcp" + } + } + } +} +``` + +### 3. Test Integrazione LiteLLM + +```bash +# Test con tool web search via LiteLLM +curl -X POST http://localhost:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-master-key" \ + -d '{ + "model": "gpt-4", + "messages": [ + {"role": "user", "content": "Search for latest news about artificial intelligence"} + ], + "tools": [ + { + "type": "function", + "function": { + "name": "full-web-search", + "description": "Search the web and fetch complete page content", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string"}, + "limit": {"type": "number", "default": 5} + }, + "required": ["query"] + } + } + } + ], + "tool_choice": "auto" + }' +``` + +## πŸ” Configurazione Produzione + +### 1. Variabili d'Ambiente + +Crea il file `.env`: + +```bash +# Server configuration +NODE_ENV=production +PORT=3001 +HOST=0.0.0.0 + +# Optional: Search engine configuration +SEARCH_TIMEOUT=30000 +MAX_CONTENT_LENGTH=50000 +CONCURRENT_REQUESTS=3 +``` + +### 2. Deploy Produzione + +```bash +# Deploy in produzione (il reverse proxy SSL Γ¨ esterno) +docker-compose up -d + +# Oppure con LiteLLM integrato +docker-compose -f docker-compose-full.yml up -d +``` + +## πŸ”„ Aggiornamenti e Manutenzione + +### 1. Update del Container + +```bash +# Rebuild e restart +docker-compose down +docker-compose build --no-cache +docker-compose up -d +``` + +### 2. Logs e Monitoring + +```bash +# Visualizza logs +docker-compose logs -f web-search-mcp-sse + +# Monitoring risorse +docker stats web-search-mcp-sse + +# Health check periodico +watch -n 30 'curl -s http://localhost:3001/health | jq' +``` + +### 3. Backup e Recovery + +```bash +# Backup configurazione +tar -czf backup-$(date +%Y%m%d).tar.gz \ + docker-compose.yml nginx.conf .env ssl/ + +# Restore +tar -xzf backup-YYYYMMDD.tar.gz +``` + +## πŸ› Troubleshooting + +### Problemi Comuni + +1. **Container non si avvia**: + ```bash + docker logs web-search-mcp-sse + ``` + +2. **SSE connection fails**: + - Verifica CORS headers + - Controlla firewall/proxy settings + - Test con curl -N + +3. **Tool calls falliscono**: + - Verifica formato JSON request + - Controlla parametri richiesti + - Test endpoint diretto HTTP + +4. **Performance issues**: + - Aumenta timeout configurazioni + - Limita concurrent requests + - Monitora memoria Docker + +### Log Analysis + +```bash +# Analisi errori comuni +docker logs web-search-mcp-sse 2>&1 | grep ERROR +docker logs web-search-mcp-sse 2>&1 | grep "Tool call" +docker logs web-search-mcp-sse 2>&1 | grep "SSE" +``` + +## πŸ“Š Configurazione MCP Client + +### Per Claude Desktop + +Aggiorna `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "web-search-sse-local": { + "transport": { + "type": "sse", + "url": "http://localhost:3001/mcp/sse" + } + }, + "web-search-sse-remote": { + "transport": { + "type": "sse", + "url": "https://your-domain.com/mcp/sse" + } + } + } +} +``` + +### Per Altri Client MCP + +```javascript +// Esempio connessione MCP SSE +const client = new McpClient({ + transport: { + type: 'sse', + url: 'https://your-domain.com/mcp/sse' + } +}); + +await client.connect(); + +// Chiamata tool +const result = await client.callTool('full-web-search', { + query: 'latest AI news', + limit: 5 +}); +``` + +## πŸš€ Deploy su Cloud Providers + +### AWS ECS + +```yaml +# task-definition.json per ECS +{ + "family": "web-search-mcp-sse", + "networkMode": "awsvpc", + "cpu": "256", + "memory": "512", + "containerDefinitions": [ + { + "name": "web-search-mcp", + "image": "your-ecr-repo/web-search-mcp-sse:latest", + "portMappings": [ + { + "containerPort": 3001, + "protocol": "tcp" + } + ], + "environment": [ + {"name": "NODE_ENV", "value": "production"}, + {"name": "PORT", "value": "3001"} + ] + } + ] +} +``` + +### Google Cloud Run + +```yaml +# cloudrun.yaml +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: web-search-mcp-sse +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/maxScale: "10" + spec: + containers: + - image: gcr.io/your-project/web-search-mcp-sse + ports: + - containerPort: 3001 + env: + - name: NODE_ENV + value: production + - name: PORT + value: "3001" +``` + +### Azure Container Instances + +```bash +# Deploy su Azure +az container create \ + --resource-group myResourceGroup \ + --name web-search-mcp-sse \ + --image your-registry.azurecr.io/web-search-mcp-sse:latest \ + --dns-name-label web-search-mcp \ + --ports 3001 \ + --environment-variables NODE_ENV=production PORT=3001 +``` + +## πŸ“ˆ Monitoraggio e OsservabilitΓ  + +### Metriche da Monitorare + +- Request rate per endpoint +- Response time tool calls +- SSE connection count +- Memory/CPU usage +- Error rate per tool + +### Setup Prometheus + Grafana + +Aggiungi al `docker-compose.yml`: + +```yaml + prometheus: + image: prom/prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + + grafana: + image: grafana/grafana + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin +``` + +Con questo setup completo, hai un server MCP web search completamente dockerizzato, scalabile e integrabile con qualsiasi gateway AI che supporti il protocollo MCP via SSE! πŸŽ‰ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a878df5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +services: + web-search-mcp: + build: . + container_name: web-search-mcp + ports: + - "3001:3001" + environment: + - NODE_ENV=production + - PORT=3001 + - HOST=0.0.0.0 + # Optional environment variables for search configuration + # - SEARCH_TIMEOUT=30000 + # - MAX_CONTENT_LENGTH=50000 + # - CONCURRENT_REQUESTS=3 + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + #volumes: + # Optional: Mount logs directory if you want persistent logs + # - ./logs:/app/logs + # Optional: Mount cache directory for browser cache + # - ./cache:/app/cache + labels: + - "traefik.enable=true" + - "traefik.http.routers.web-search-mcp.rule=Host(`your-domain.com`)" + - "traefik.http.routers.web-search-mcp.entrypoints=websecure" + - "traefik.http.routers.web-search-mcp.tls.certresolver=letsencrypt" + - "traefik.http.services.web-search-mcp.loadbalancer.server.port=3001" + # CORS headers for MCP + - "traefik.http.middlewares.cors-headers.headers.accesscontrolalloworigin=*" + - "traefik.http.middlewares.cors-headers.headers.accesscontrolallowmethods=GET,POST,OPTIONS" + - "traefik.http.middlewares.cors-headers.headers.accesscontrolallowheaders=Content-Type,Authorization,Cache-Control" + - "traefik.http.routers.web-search-mcp.middlewares=cors-headers" + networks: + - web-search-network + +networks: + web-search-network: + driver: bridge \ No newline at end of file diff --git a/litellm-config.yaml b/litellm-config.yaml new file mode 100644 index 0000000..c9c1133 --- /dev/null +++ b/litellm-config.yaml @@ -0,0 +1,75 @@ +# LiteLLM Configuration for Web Search MCP Integration +# Updated for StreamableHttp transport + +model_list: + # OpenAI models + - model_name: gpt-4 + litellm_params: + model: openai/gpt-4 + api_key: "your-openai-api-key" + + - model_name: gpt-3.5-turbo + litellm_params: + model: openai/gpt-3.5-turbo + api_key: "your-openai-api-key" + + # Anthropic models + - model_name: claude-3-sonnet + litellm_params: + model: anthropic/claude-3-sonnet-20240229 + api_key: "your-anthropic-api-key" + + - model_name: claude-3-haiku + litellm_params: + model: anthropic/claude-3-haiku-20240307 + api_key: "your-anthropic-api-key" + +# MCP Tools Configuration - Updated for StreamableHttp +tools: + # Local development + - name: web-search-mcp-local + type: mcp + config: + transport: + type: http + url: "http://localhost:3001/mcp" + timeout: 30000 + + # Docker container (same network) + - name: web-search-mcp-docker + type: mcp + config: + transport: + type: http + url: "http://web-search-mcp:3001/mcp" + timeout: 30000 + + # Remote production deployment + - name: web-search-mcp-remote + type: mcp + config: + transport: + type: http + url: "https://your-domain.com/mcp" + timeout: 30000 + headers: + Authorization: "Bearer your-optional-auth-token" + +# General settings +general_settings: + master_key: "your-master-key-here" + + # Rate limiting + rpm_limit_per_user: 100 + tpm_limit_per_user: 40000 + + # Logging + success_callback: ["prometheus", "webhook"] + failure_callback: ["prometheus", "webhook"] + +# Router settings for load balancing +router_settings: + routing_strategy: "least-busy" + model_group_alias: + gpt-4-group: ["gpt-4"] + claude-group: ["claude-3-sonnet", "claude-3-haiku"] \ No newline at end of file diff --git a/mcp.json b/mcp.json index 82c2846..d1a1e82 100644 --- a/mcp.json +++ b/mcp.json @@ -1,10 +1,22 @@ { "mcpServers": { - "web-search": { + "web-search-stdio": { "command": "node", "args": [ - "/Users/mark/Projects/web-search-mcp/dist/simple-test.js" + "/Users/nicola/Devel/github/web-search-mcp/dist/index.js" ] + }, + "web-search-streamable": { + "transport": { + "type": "http", + "url": "http://localhost:3001/mcp" + } + }, + "web-search-streamable-remote": { + "transport": { + "type": "http", + "url": "https://your-domain.com/mcp" + } } } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 02f58a0..ab60c02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,31 @@ { "name": "web-search-mcp-server", - "version": "0.2.2", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "web-search-mcp-server", - "version": "0.2.2", + "version": "0.3.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.15.0", "axios": "^1.6.8", "cheerio": "^1.0.0-rc.12", + "cors": "^2.8.5", + "express": "^4.19.2", "p-limit": "^6.2.0", "p-retry": "^6.2.1", - "playwright": "^1.48.0" + "playwright": "^1.48.0", + "zod": "^3.22.0" }, "bin": { "web-search-mcp": "dist/index.js" }, "devDependencies": { "@eslint/js": "^9.30.1", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", "@types/node": "^24.0.10", "@typescript-eslint/eslint-plugin": "^8.35.1", "@typescript-eslint/parser": "^8.35.1", @@ -722,9 +727,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.0.tgz", - "integrity": "sha512-67hnl/ROKdb03Vuu0YOr+baKTvf1/5YBHBm9KnZdjdAh8hjt4FRCPD5ucwxGB237sBpzlqQsLy1PFu7z/ekZ9Q==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.1.tgz", + "integrity": "sha512-d//GE8/Yh7aC3e7p+kZG8JqqEAwwDUmAfvH1quogtbk+ksS6E0RR6toKKESPYYZVre0meqkJb27zb+dhqE9Sgw==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -744,6 +749,245 @@ "node": ">=18" } }, + "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==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "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/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/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-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/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/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -782,6 +1026,37 @@ "node": ">= 8" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -789,6 +1064,39 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -796,6 +1104,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.0.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", @@ -806,12 +1121,49 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "license": "MIT" }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.35.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", @@ -1047,13 +1399,13 @@ } }, "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==", + "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": "^3.0.0", - "negotiator": "^1.0.0" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { "node": ">= 0.6" @@ -1121,48 +1473,100 @@ "dev": true, "license": "Python-2.0" }, + "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/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "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": "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": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "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.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "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==", + "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", - "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", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" }, "engines": { - "node": ">=18" + "node": ">= 0.8" } }, "node_modules/boolbase": { @@ -1341,9 +1745,9 @@ "license": "MIT" }, "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" @@ -1362,22 +1766,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", @@ -1476,6 +1877,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/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -1936,41 +2347,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", @@ -1992,6 +2407,21 @@ "express": ">= 4.11" } }, + "node_modules/express/node_modules/debug": { + "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.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2078,22 +2508,38 @@ } }, "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" } }, + "node_modules/finalhandler/node_modules/debug": { + "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.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2168,27 +2614,6 @@ "node": ">= 6" } }, - "node_modules/form-data/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/form-data/node_modules/mime-types": { - "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.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2199,12 +2624,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/fsevents": { @@ -2422,15 +2847,6 @@ "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", @@ -2642,22 +3058,19 @@ } }, "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" } @@ -2672,6 +3085,15 @@ "node": ">= 8" } }, + "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/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2686,22 +3108,34 @@ "node": ">=8.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", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "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/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" @@ -2737,9 +3171,9 @@ "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.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" @@ -2985,13 +3419,10 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } + "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/picomatch": { "version": "2.3.1", @@ -3114,12 +3545,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" @@ -3229,6 +3660,16 @@ "node": ">= 18" } }, + "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/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3293,40 +3734,66 @@ } }, "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/debug": { + "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.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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/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": { @@ -3429,9 +3896,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" @@ -3532,14 +3999,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" @@ -3593,6 +4059,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 fd7724d..ab039f0 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,15 @@ "scripts": { "build": "tsc && echo 'βœ… TypeScript compilation complete: dist/index.js'", "dev": "tsx watch src/index.ts", + "dev:streamable": "tsx watch src/streamable-server.ts", "start": "node ./dist/index.js", + "start:streamable": "node ./dist/streamable-server.js", "lint": "eslint \"src/**/*.ts\"", - "format": "prettier --write ." + "format": "prettier --write .", + "docker:build": "docker build -t web-search-mcp-streamable .", + "docker:run": "docker run -p 3001:3001 --name web-search-mcp-streamable web-search-mcp-streamable", + "docker:compose": "docker-compose up -d", + "docker:stop": "docker-compose down" }, "keywords": [ "mcp", @@ -33,7 +39,9 @@ "p-limit": "^6.2.0", "p-retry": "^6.2.1", "playwright": "^1.48.0", - "zod": "^3.22.0" + "zod": "^3.22.0", + "express": "^4.19.2", + "cors": "^2.8.5" }, "devDependencies": { "@eslint/js": "^9.30.1", @@ -44,6 +52,8 @@ "eslint": "^9.30.1", "prettier": "^3.2.5", "tsx": "^4.7.0", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "@types/express": "^4.17.21", + "@types/cors": "^2.8.17" } } diff --git a/src/streamable-server.ts b/src/streamable-server.ts new file mode 100644 index 0000000..c3d2bb6 --- /dev/null +++ b/src/streamable-server.ts @@ -0,0 +1,527 @@ +#!/usr/bin/env node +console.log('Web Search MCP Server (StreamableHttp) starting...'); + +import express from 'express'; +import cors from 'cors'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; +import { SearchEngine } from './search-engine.js'; +import { EnhancedContentExtractor } from './enhanced-content-extractor.js'; +import { + WebSearchToolInput, + WebSearchToolOutput, + SearchResult, + SearchSummaryResult, + SearchSummaryOutput, + SinglePageContentOutput +} from './types.js'; +import { isPdfUrl } from './utils.js'; + +interface StreamableHttpOptions { + port?: number; + host?: string; +} + +class WebSearchMCPStreamableServer { + private server: McpServer; + private searchEngine: SearchEngine; + private contentExtractor: EnhancedContentExtractor; + private app: express.Application; + private port: number; + private host: string; + + constructor(options: StreamableHttpOptions = {}) { + this.port = options.port || parseInt(process.env.PORT || '3001'); + this.host = options.host || process.env.HOST || '0.0.0.0'; + + this.server = new McpServer({ + name: 'web-search-mcp-streamable', + version: '0.3.1', + }); + + this.searchEngine = new SearchEngine(); + this.contentExtractor = new EnhancedContentExtractor(); + this.app = express(); + + this.setupExpress(); + this.setupTools(); + this.setupStreamableHttp(); + this.setupGracefulShutdown(); + } + + private setupExpress(): void { + // Enable CORS for all routes + this.app.use(cors({ + origin: true, // Allow all origins + credentials: true, + methods: ['GET', 'POST', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'Cache-Control'] + })); + + // Parse JSON bodies + this.app.use(express.json({ limit: '50mb' })); + this.app.use(express.urlencoded({ extended: true })); + + // Basic endpoints + this.setupBasicEndpoints(); + } + + private setupBasicEndpoints(): void { + // Health check endpoint + this.app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + server: 'web-search-mcp-streamable', + version: '0.3.1', + timestamp: new Date().toISOString(), + transport: 'streamable-http' + }); + }); + + // MCP server info endpoint + this.app.get('/mcp/info', (req, res) => { + res.json({ + name: 'web-search-mcp-streamable', + version: '0.3.1', + transport: 'streamable-http', + endpoints: { + health: '/health', + info: '/mcp/info', + streamable: '/mcp', + }, + tools: [ + 'full-web-search', + 'get-web-search-summaries', + 'get-single-web-page-content' + ] + }); + }); + + // Root endpoint for basic info + this.app.get('/', (req, res) => { + res.json({ + message: 'Web Search MCP Server (StreamableHttp)', + version: '0.3.1', + transport: 'streamable-http', + endpoints: { + health: '/health', + info: '/mcp/info', + mcp: '/mcp' + } + }); + }); + } + + private setupStreamableHttp(): void { + // StreamableHttp endpoint according to MCP specification + this.app.post('/mcp', async (req, res) => { + console.log('StreamableHttp request received'); + + try { + // Set headers for streaming response + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + // Handle the MCP request + const mcpRequest = req.body; + console.log('MCP Request:', JSON.stringify(mcpRequest, null, 2)); + + // Process the request through the MCP server + const response = await this.handleMcpRequest(mcpRequest); + + console.log('MCP Response:', JSON.stringify(response, null, 2)); + res.json(response); + + } catch (error) { + console.error('StreamableHttp error:', error); + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + data: error instanceof Error ? error.message : String(error) + } + }); + } + }); + + // Handle OPTIONS for CORS + this.app.options('/mcp', (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.status(200).end(); + }); + } + + private async handleMcpRequest(request: any): Promise { + // Handle different MCP request types + switch (request.method) { + case 'initialize': + return this.handleInitialize(request); + + case 'tools/list': + return this.handleListTools(request); + + case 'tools/call': + return this.handleCallTool(request); + + case 'resources/list': + return this.handleListResources(request); + + case 'ping': + return { + jsonrpc: '2.0', + id: request.id, + result: {} + }; + + default: + return { + jsonrpc: '2.0', + id: request.id, + error: { + code: -32601, + message: `Method not found: ${request.method}` + } + }; + } + } + + private handleInitialize(request: any): any { + return { + jsonrpc: '2.0', + id: request.id, + result: { + protocolVersion: '2024-11-05', + capabilities: { + tools: {}, + resources: {} + }, + serverInfo: { + name: 'web-search-mcp-streamable', + version: '0.3.1' + }, + instructions: 'Web search server with content extraction capabilities' + } + }; + } + + private handleListTools(request: any): any { + return { + jsonrpc: '2.0', + id: request.id, + result: { + tools: [ + { + name: 'full-web-search', + description: 'Search the web and fetch complete page content for multiple results', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query' + }, + limit: { + type: 'number', + description: 'Maximum number of results to return (default: 5)', + default: 5 + } + }, + required: ['query'] + } + }, + { + name: 'get-web-search-summaries', + description: 'Search the web and return summaries of the results', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query' + }, + limit: { + type: 'number', + description: 'Maximum number of results to return (default: 5)', + default: 5 + } + }, + required: ['query'] + } + }, + { + name: 'get-single-web-page-content', + description: 'Extract complete content from a single web page URL', + inputSchema: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'URL of the web page to extract content from' + } + }, + required: ['url'] + } + } + ] + } + }; + } + + private async handleCallTool(request: any): Promise { + const { name, arguments: args } = request.params; + + try { + let result; + + switch (name) { + case 'full-web-search': + result = await this.fullWebSearch(args); + break; + + case 'get-web-search-summaries': + result = await this.getWebSearchSummaries(args); + break; + + case 'get-single-web-page-content': + result = await this.getSingleWebPageContent(args); + break; + + default: + return { + jsonrpc: '2.0', + id: request.id, + error: { + code: -32601, + message: `Tool not found: ${name}` + } + }; + } + + return { + jsonrpc: '2.0', + id: request.id, + result: { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + } + }; + + } catch (error) { + console.error(`Error calling tool ${name}:`, error); + return { + jsonrpc: '2.0', + id: request.id, + error: { + code: -32603, + message: `Tool execution error: ${error instanceof Error ? error.message : String(error)}` + } + }; + } + } + + private handleListResources(request: any): any { + return { + jsonrpc: '2.0', + id: request.id, + result: { + resources: [] + } + }; + } + + // Tool implementations (adapted for correct interfaces) + private async fullWebSearch(args: WebSearchToolInput): Promise { + const { query, limit = 5 } = args; + console.log(`Performing full web search for: "${query}" (limit: ${limit})`); + + try { + const searchResponse = await this.searchEngine.search({ query, numResults: limit }); + + if (!searchResponse || !searchResponse.results || searchResponse.results.length === 0) { + return { + results: [], + total_results: 0, + search_time_ms: 0, + query, + status: `No search results found for query: "${query}"` + }; + } + + const processedResults: SearchResult[] = []; + + for (const result of searchResponse.results) { + try { + let content = ''; + + if (isPdfUrl(result.url)) { + console.log(`Extracting PDF content from: ${result.url}`); + // For PDF, use the URL directly with the content extractor + content = await this.contentExtractor.extractContent({ url: result.url }); + } else { + console.log(`Extracting web page content from: ${result.url}`); + content = await this.contentExtractor.extractContent({ url: result.url }); + } + + processedResults.push({ + title: result.title, + url: result.url, + description: result.description, + fullContent: content || result.description, + contentPreview: content ? content.substring(0, 500) + '...' : result.description, + wordCount: content ? content.split(' ').length : result.description.split(' ').length, + timestamp: result.timestamp, + fetchStatus: 'success' + }); + + } catch (error) { + console.warn(`Failed to extract content from ${result.url}:`, error); + processedResults.push({ + title: result.title, + url: result.url, + description: result.description, + fullContent: result.description, + contentPreview: result.description, + wordCount: result.description.split(' ').length, + timestamp: result.timestamp, + fetchStatus: 'error', + error: `Content extraction failed: ${error instanceof Error ? error.message : String(error)}` + }); + } + } + + return { + results: processedResults, + total_results: processedResults.length, + search_time_ms: Date.now() - new Date(searchResponse.results[0]?.timestamp || Date.now()).getTime(), + query, + status: `Found ${processedResults.length} results for "${query}". Successfully extracted content from ${processedResults.filter(r => r.fetchStatus === 'success').length} pages.` + }; + + } catch (error) { + console.error('Full web search error:', error); + throw new Error(`Search failed: ${error instanceof Error ? error.message : String(error)}`); + } + } + + private async getWebSearchSummaries(args: WebSearchToolInput): Promise { + const { query, limit = 5 } = args; + console.log(`Getting web search summaries for: "${query}" (limit: ${limit})`); + + try { + const searchResponse = await this.searchEngine.search({ query, numResults: limit }); + + if (!searchResponse || !searchResponse.results || searchResponse.results.length === 0) { + return { + results: [], + total_results: 0, + search_time_ms: 0, + query + }; + } + + const summaryResults: SearchSummaryResult[] = searchResponse.results.map(result => ({ + title: result.title, + url: result.url, + description: result.description, + timestamp: result.timestamp + })); + + return { + results: summaryResults, + total_results: summaryResults.length, + search_time_ms: Date.now() - new Date(searchResponse.results[0]?.timestamp || Date.now()).getTime(), + query + }; + + } catch (error) { + console.error('Web search summaries error:', error); + throw new Error(`Search failed: ${error instanceof Error ? error.message : String(error)}`); + } + } + + private async getSingleWebPageContent(args: { url: string }): Promise { + const { url } = args; + console.log(`Extracting content from single page: ${url}`); + + try { + const content = await this.contentExtractor.extractContent({ url }); + const wordCount = content ? content.split(' ').length : 0; + + return { + url, + title: 'Extracted Content', // We don't have title extraction in current extractor + content: content || 'No content could be extracted', + contentPreview: content ? content.substring(0, 500) + '...' : 'No content available', + wordCount, + timestamp: new Date().toISOString(), + fetchStatus: 'success' + }; + + } catch (error) { + console.error(`Content extraction error for ${url}:`, error); + return { + url, + title: 'Error', + content: '', + contentPreview: 'Content extraction failed', + wordCount: 0, + timestamp: new Date().toISOString(), + fetchStatus: 'error', + error: `Content extraction failed: ${error instanceof Error ? error.message : String(error)}` + }; + } + } + + private setupTools(): void { + // Tools are handled directly in StreamableHttp mode + console.log('Tools configured for StreamableHttp transport'); + } + + private setupGracefulShutdown(): void { + const gracefulShutdown = () => { + console.log('Received shutdown signal, closing server gracefully...'); + process.exit(0); + }; + + process.on('SIGTERM', gracefulShutdown); + process.on('SIGINT', gracefulShutdown); + process.on('SIGUSR1', gracefulShutdown); + process.on('SIGUSR2', gracefulShutdown); + } + + public async start(): Promise { + return new Promise((resolve) => { + this.app.listen(this.port, this.host, () => { + console.log(`Web Search MCP Server (StreamableHttp) started on ${this.host}:${this.port}`); + console.log(`Server timestamp: ${new Date().toISOString()}`); + console.log(`Health check: http://${this.host}:${this.port}/health`); + console.log(`MCP Info: http://${this.host}:${this.port}/mcp/info`); + console.log(`StreamableHttp Endpoint: http://${this.host}:${this.port}/mcp`); + console.log('Server ready to accept connections...'); + resolve(); + }); + }); + } +} + +// Start the server if this file is run directly +if (import.meta.url === `file://${process.argv[1]}`) { + const server = new WebSearchMCPStreamableServer(); + server.start().catch((error) => { + console.error('Failed to start server:', error); + process.exit(1); + }); +} + +export default WebSearchMCPStreamableServer; \ No newline at end of file