From bb8637c401c2cbfbcb7f15d1b77b92cf22dd3c60 Mon Sep 17 00:00:00 2001 From: Ben Nevile Date: Mon, 2 Jun 2025 09:05:07 -0400 Subject: [PATCH] Add HTTPS support with environment variable configuration - Implement HTTPS server support for both client and proxy server - Add INSPECTOR_SSL_CERT_PATH and INSPECTOR_SSL_KEY_PATH environment variables - Include fallback to HTTP when SSL certificates are missing or invalid - Add cross-platform SSL certificate generation script - Update documentation with HTTPS configuration instructions --- README.md | 13 +++++ client/bin/client.js | 113 ++++++++++++++++++++++++++++++------------- generate-ssl.sh | 75 ++++++++++++++++++++++++++++ server/src/index.ts | 48 ++++++++++++++++-- 4 files changed, 211 insertions(+), 38 deletions(-) create mode 100755 generate-ssl.sh diff --git a/README.md b/README.md index 9d5017dc..7b93631e 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,19 @@ You can paste the Server Entry into your existing `mcp.json` file under your cho The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. You can override the header name using the input field in the sidebar. +### HTTPS Configuration + +oAuth testing in the browser involves security libraries that require serving the Client UI over a secure connection. To enable HTTPS for both the client and server, set these environment variables: + +- `INSPECTOR_SSL_CERT_PATH`: Path to SSL certificate file (.crt or .pem) +- `INSPECTOR_SSL_KEY_PATH`: Path to SSL private key file (.key or .pem) + +When both variables are set, the inspector will automatically use HTTPS: +- Client UI: `https://localhost:6274` +- Proxy server: `https://localhost:6277` + +If these variables are not set, the inspector defaults to HTTP. + ### Security Considerations The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server. diff --git a/client/bin/client.js b/client/bin/client.js index 7179e19e..616877dc 100755 --- a/client/bin/client.js +++ b/client/bin/client.js @@ -4,47 +4,92 @@ import { join, dirname } from "path"; import { fileURLToPath } from "url"; import handler from "serve-handler"; import http from "http"; +import https from "https"; +import fs from "fs"; const __dirname = dirname(fileURLToPath(import.meta.url)); const distPath = join(__dirname, "../dist"); -const server = http.createServer((request, response) => { - const handlerOptions = { - public: distPath, - rewrites: [{ source: "/**", destination: "/index.html" }], - headers: [ - { - // Ensure index.html is never cached - source: "index.html", - headers: [ - { - key: "Cache-Control", - value: "no-cache, no-store, max-age=0", - }, - ], - }, - { - // Allow long-term caching for hashed assets - source: "assets/**", - headers: [ - { - key: "Cache-Control", - value: "public, max-age=31536000, immutable", - }, - ], - }, - ], - }; +const handlerOptions = { + public: distPath, + rewrites: [{ source: "/**", destination: "/index.html" }], + headers: [ + { + // Ensure index.html is never cached + source: "index.html", + headers: [ + { + key: "Cache-Control", + value: "no-cache, no-store, max-age=0", + }, + ], + }, + { + // Allow long-term caching for hashed assets + source: "assets/**", + headers: [ + { + key: "Cache-Control", + value: "public, max-age=31536000, immutable", + }, + ], + }, + ], +}; +const requestHandler = (request, response) => { return handler(request, response, handlerOptions); -}); +}; + +const port = process.env.CLIENT_PORT || 6274; +const INSPECTOR_SSL_CERT_PATH = process.env.INSPECTOR_SSL_CERT_PATH; +const INSPECTOR_SSL_KEY_PATH = process.env.INSPECTOR_SSL_KEY_PATH; + +let server; + +if (INSPECTOR_SSL_CERT_PATH && INSPECTOR_SSL_KEY_PATH) { + // HTTPS server + try { + const options = { + cert: fs.readFileSync(INSPECTOR_SSL_CERT_PATH), + key: fs.readFileSync(INSPECTOR_SSL_KEY_PATH) + }; + server = https.createServer(options, requestHandler); + server.on("listening", () => { + console.log( + `🔍 MCP Inspector is up and running at https://127.0.0.1:${port} 🚀🔒`, + ); + }); + } catch (error) { + console.error(`❌ Failed to load SSL certificates: ${error instanceof Error ? error.message : String(error)}`); + console.log(`🔄 Falling back to HTTP mode`); + server = http.createServer(requestHandler); + server.on("listening", () => { + console.log( + `🔍 MCP Inspector is up and running at http://127.0.0.1:${port} 🚀 (SSL fallback)`, + ); + }); + } +} else { + // HTTP server (default) + if (!INSPECTOR_SSL_CERT_PATH && !INSPECTOR_SSL_KEY_PATH) { + console.log(`🔓 No SSL certificates configured - using HTTP`); + console.log(`💡 To enable HTTPS, set INSPECTOR_SSL_CERT_PATH and INSPECTOR_SSL_KEY_PATH environment variables`); + } else { + console.log(`⚠️ Incomplete SSL configuration:`); + if (!INSPECTOR_SSL_CERT_PATH) console.log(` Missing INSPECTOR_SSL_CERT_PATH`); + if (!INSPECTOR_SSL_KEY_PATH) console.log(` Missing INSPECTOR_SSL_KEY_PATH`); + console.log(`🔄 Using HTTP mode`); + } + + server = http.createServer(requestHandler); + server.on("listening", () => { + console.log( + `🔍 MCP Inspector is up and running at http://127.0.0.1:${port} 🚀`, + ); + }); +} -const port = process.env.PORT || 6274; -server.on("listening", () => { - console.log( - `🔍 MCP Inspector is up and running at http://127.0.0.1:${port} 🚀`, - ); -}); server.on("error", (err) => { if (err.message.includes(`EADDRINUSE`)) { console.error( diff --git a/generate-ssl.sh b/generate-ssl.sh new file mode 100755 index 00000000..c62fa537 --- /dev/null +++ b/generate-ssl.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +# MCP Inspector SSL Certificate Generator +# This script generates SSL certificates for local HTTPS development + +set -e + +echo "🔐 Generating SSL certificates for MCP Inspector..." + +# Check if mkcert is installed +if ! command -v mkcert &> /dev/null; then + echo "❌ mkcert not found. Installing..." + + # Detect OS and install mkcert + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + if ! command -v brew &> /dev/null; then + echo "❌ Homebrew not found. Please install Homebrew first: https://brew.sh" + exit 1 + fi + brew install mkcert + elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + # Linux + if command -v apt-get &> /dev/null; then + sudo apt-get update && sudo apt-get install -y mkcert + elif command -v dnf &> /dev/null; then + sudo dnf install -y mkcert + elif command -v pacman &> /dev/null; then + sudo pacman -S mkcert + elif command -v zypper &> /dev/null; then + sudo zypper install mkcert + else + echo "❌ Package manager not found. Please install mkcert manually: https://github.com/FiloSottile/mkcert" + exit 1 + fi + elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then + # Windows (Git Bash/WSL) + if command -v choco &> /dev/null; then + choco install mkcert + elif command -v scoop &> /dev/null; then + scoop install mkcert + else + echo "❌ Neither Chocolatey nor Scoop found. Please install one of them or install mkcert manually:" + echo " Chocolatey: https://chocolatey.org" + echo " Scoop: https://scoop.sh" + echo " mkcert: https://github.com/FiloSottile/mkcert" + exit 1 + fi + else + echo "❌ Unsupported OS: $OSTYPE" + echo "Please install mkcert manually: https://github.com/FiloSottile/mkcert" + exit 1 + fi +fi + +# Create ssl directory if it doesn't exist +mkdir -p ssl + +# Generate certificates +echo "📜 Generating certificates for localhost and 127.0.0.1..." +mkcert -key-file ssl/key.pem -cert-file ssl/cert.pem localhost 127.0.0.1 + +# Set permissions +chmod 600 ssl/key.pem +chmod 644 ssl/cert.pem + +echo "✅ SSL certificates generated successfully!" +echo "" +echo "📝 To use HTTPS, set these environment variables:" +echo "export INSPECTOR_SSL_CERT_PATH=$(pwd)/ssl/cert.pem" +echo "export INSPECTOR_SSL_KEY_PATH=$(pwd)/ssl/key.pem" +echo "" +echo "💡 To avoid browser warnings, run: sudo mkcert -install" +echo "" +echo "🚀 Start the inspector with: npm run dev" \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index ef3061d8..56014e94 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -3,6 +3,8 @@ import cors from "cors"; import { parseArgs } from "node:util"; import { parse as shellParseArgs } from "shell-quote"; +import https from "https"; +import fs from "fs"; import { SSEClientTransport, @@ -375,11 +377,49 @@ app.get("/config", (req, res) => { }); const PORT = process.env.PORT || 6277; +const INSPECTOR_SSL_CERT_PATH = process.env.INSPECTOR_SSL_CERT_PATH; +const INSPECTOR_SSL_KEY_PATH = process.env.INSPECTOR_SSL_KEY_PATH; + +let server; + +if (INSPECTOR_SSL_CERT_PATH && INSPECTOR_SSL_KEY_PATH) { + // HTTPS server + try { + const options = { + cert: fs.readFileSync(INSPECTOR_SSL_CERT_PATH), + key: fs.readFileSync(INSPECTOR_SSL_KEY_PATH) + }; + server = https.createServer(options, app); + server.listen(PORT); + server.on("listening", () => { + console.log(`⚙️ Proxy server listening on https://localhost:${PORT} 🔒`); + }); + } catch (error) { + console.error(`❌ Failed to load SSL certificates: ${error instanceof Error ? error.message : String(error)}`); + console.log(`🔄 Falling back to HTTP mode`); + server = app.listen(PORT); + server.on("listening", () => { + console.log(`⚙️ Proxy server listening on http://localhost:${PORT} (SSL fallback)`); + }); + } +} else { + // HTTP server (default) + if (!INSPECTOR_SSL_CERT_PATH && !INSPECTOR_SSL_KEY_PATH) { + console.log(`🔓 No SSL certificates configured - using HTTP`); + console.log(`💡 To enable HTTPS, set INSPECTOR_SSL_CERT_PATH and INSPECTOR_SSL_KEY_PATH environment variables`); + } else { + console.log(`⚠️ Incomplete SSL configuration:`); + if (!INSPECTOR_SSL_CERT_PATH) console.log(` Missing INSPECTOR_SSL_CERT_PATH`); + if (!INSPECTOR_SSL_KEY_PATH) console.log(` Missing INSPECTOR_SSL_KEY_PATH`); + console.log(`🔄 Using HTTP mode`); + } + + server = app.listen(PORT); + server.on("listening", () => { + console.log(`⚙️ Proxy server listening on http://localhost:${PORT}`); + }); +} -const server = app.listen(PORT); -server.on("listening", () => { - console.log(`⚙️ Proxy server listening on port ${PORT}`); -}); server.on("error", (err) => { if (err.message.includes(`EADDRINUSE`)) { console.error(`❌ Proxy Server PORT IS IN USE at port ${PORT} ❌ `);