diff --git a/docs/NGROK.md b/docs/NGROK.md new file mode 100644 index 0000000..a5e50af --- /dev/null +++ b/docs/NGROK.md @@ -0,0 +1,41 @@ +# Publishing mcp-browser with ngrok + +The `scripts/run_mcp_ngrok.sh` helper launches `mcp-browser` in +streamable-http mode and exposes it through an ngrok tunnel. By default it uses +the `builtin-only` server profile so only the minimal proxy tools are visible. + +```bash +./scripts/run_mcp_ngrok.sh \ + --allow-origin https://platform.openai.com \ + --ngrok-basic-auth "user:pass" +``` + +Key behaviours: + +- Picks a free local port and starts `mcp-browser --mode streamable-http` with + `--http-path /mcp`. +- Starts `ngrok http` pointing at that port and prints the public URL once the +tunnel is ready. +- Writes logs to temporary files (paths shown on startup). +- Cleans up both processes when interrupted. + +Useful options: + +- `--server chrome` – publish a different server entry from your config. +- `--config ~/.claude/mcp-browser/config.yaml` – point at a custom config file. +- `--ngrok-region eu` or `--ngrok-domain your-name.ngrok.app` – choose a region + or reserved domain. +- `--ngrok-basic-auth user:pass` – require HTTP basic auth. Strongly recommended + if the tunnel is exposed to the public. + +Additional `mcp-browser` arguments can be passed after `--`, for example to +connect to a streamable HTTP upstream: + +```bash +./scripts/run_mcp_ngrok.sh -- \ + --transport streamable-http \ + --transport-url http://127.0.0.1:12306/mcp +``` + +The resulting public URL terminates at `/mcp`. Configure your MCP client (e.g. +OpenAI’s MCP interface) with that URL and the optional basic auth credentials. diff --git a/scripts/run_mcp_ngrok.sh b/scripts/run_mcp_ngrok.sh new file mode 100755 index 0000000..b7cff29 --- /dev/null +++ b/scripts/run_mcp_ngrok.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: run_mcp_ngrok.sh [options] [-- additional mcp-browser args] + +Launch mcp-browser in streamable-http mode and expose it via an ngrok HTTPS +endpoint. Designed to publish only the minimal built-in API (builtin-only +server) unless overridden. + +Options: + --config PATH Path to mcp-browser config file. + --server NAME Server entry to use (default: builtin-only). + --http-host HOST Local interface for the gateway (default: 127.0.0.1). + --http-port PORT Local port for the gateway (default: auto). + --http-path PATH HTTP path prefix (default: /mcp). + --allow-origin ORIGIN Value for Access-Control-Allow-Origin header + (default: https://platform.openai.com). + --ngrok-domain DOMAIN Reserved ngrok domain to use (optional). + --ngrok-region REGION ngrok region code (optional). + --ngrok-basic-auth USER:PASS Require HTTP basic auth on the tunnel. + --ngrok-inspect true|false Enable ngrok inspector (default: false). + --mcp-arg ARG Extra argument passed to mcp-browser (repeatable). + --ngrok-arg ARG Extra argument passed to ngrok (repeatable). + -h, --help Show this help message. + +Environment overrides: + MCP_BROWSER_BIN (default: mcp-browser) + NGROK_BIN (default: ngrok) + +The script keeps both processes running and forwards termination signals. +USAGE +} + +MCP_BIN=${MCP_BROWSER_BIN:-mcp-browser} +NGROK_BIN=${NGROK_BIN:-ngrok} +CONFIG_PATH="" +SERVER_NAME="builtin-only" +HTTP_HOST="127.0.0.1" +HTTP_PORT="" +HTTP_PATH="/mcp" +ALLOW_ORIGIN="https://platform.openai.com" +NGROK_DOMAIN="" +NGROK_REGION="" +NGROK_BASIC_AUTH="" +NGROK_INSPECT="false" +MCP_EXTRA_ARGS=() +NGROK_EXTRA_ARGS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --config) + CONFIG_PATH=$2; shift 2;; + --server) + SERVER_NAME=$2; shift 2;; + --http-host) + HTTP_HOST=$2; shift 2;; + --http-port) + HTTP_PORT=$2; shift 2;; + --http-path) + HTTP_PATH=$2; shift 2;; + --allow-origin) + ALLOW_ORIGIN=$2; shift 2;; + --ngrok-domain) + NGROK_DOMAIN=$2; shift 2;; + --ngrok-region) + NGROK_REGION=$2; shift 2;; + --ngrok-basic-auth) + NGROK_BASIC_AUTH=$2; shift 2;; + --ngrok-inspect) + NGROK_INSPECT=$2; shift 2;; + --mcp-arg) + MCP_EXTRA_ARGS+=("$2"); shift 2;; + --ngrok-arg) + NGROK_EXTRA_ARGS+=("$2"); shift 2;; + -h|--help) + usage; exit 0;; + --) + shift + if [[ $# -gt 0 ]]; then + for arg in "$@"; do + MCP_EXTRA_ARGS+=("$arg") + done + fi + break;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1;; + esac +done + +if ! command -v "$MCP_BIN" >/dev/null 2>&1; then + echo "Error: mcp-browser binary '$MCP_BIN' not found" >&2 + exit 1 +fi +if ! command -v "$NGROK_BIN" >/dev/null 2>&1; then + echo "Error: ngrok binary '$NGROK_BIN' not found" >&2 + exit 1 +fi + +if [[ -z "$HTTP_PORT" ]]; then + HTTP_PORT=$(python3 - <<'PY' +import socket +s = socket.socket() +s.bind(('127.0.0.1', 0)) +port = s.getsockname()[1] +s.close() +print(port) +PY +) +fi + +HTTP_PATH="/${HTTP_PATH#/}" + +MCP_CMD=("$MCP_BIN" --mode streamable-http --server "$SERVER_NAME" \ + --http-host "$HTTP_HOST" --http-port "$HTTP_PORT" \ + --http-path "$HTTP_PATH" --http-allow-origin "$ALLOW_ORIGIN") +if [[ -n "$CONFIG_PATH" ]]; then + MCP_CMD+=(--config "$CONFIG_PATH") +fi +[[ ${#MCP_EXTRA_ARGS[@]} -gt 0 ]] && MCP_CMD+=("${MCP_EXTRA_ARGS[@]}") + +MCP_LOG=$(mktemp -t mcp-browser-gateway.XXXXXX.log) +NGROK_LOG=$(mktemp -t ngrok-mcp.XXXXXX.log) + +cleanup() { + [[ -n "${NGROK_PID:-}" ]] && kill "$NGROK_PID" >/dev/null 2>&1 || true + [[ -n "${MCP_PID:-}" ]] && kill "$MCP_PID" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM + +"${MCP_CMD[@]}" >"$MCP_LOG" 2>&1 & +MCP_PID=$! + +python3 - <&2 + exit 1 +} + +NGROK_CMD=("$NGROK_BIN" http --scheme=http "http://$HTTP_HOST:$HTTP_PORT") +NGROK_CMD+=(--request-header-add "X-MCP-Gateway:true") +NGROK_CMD+=(--response-header-add "Cache-Control:no-store") +NGROK_CMD+=(--inspect="$NGROK_INSPECT") +if [[ -n "$NGROK_DOMAIN" ]]; then + NGROK_CMD+=(--domain "$NGROK_DOMAIN") +fi +if [[ -n "$NGROK_REGION" ]]; then + NGROK_CMD+=(--region "$NGROK_REGION") +fi +if [[ -n "$NGROK_BASIC_AUTH" ]]; then + NGROK_CMD+=(--basic-auth "$NGROK_BASIC_AUTH") +fi +[[ ${#NGROK_EXTRA_ARGS[@]} -gt 0 ]] && NGROK_CMD+=("${NGROK_EXTRA_ARGS[@]}") + +"${NGROK_CMD[@]}" >"$NGROK_LOG" 2>&1 & +NGROK_PID=$! + +# Wait for ngrok API to report tunnel URL +PUBLIC_URL="" +for _ in {1..60}; do + sleep 0.5 + PUBLIC_URL=$(python3 - <<'PY' +import json +import sys +import urllib.request +try: + with urllib.request.urlopen('http://127.0.0.1:4040/api/tunnels') as resp: + data = json.load(resp) +except Exception: + sys.exit(1) +for tunnel in data.get('tunnels', []): + url = tunnel.get('public_url') + if url: + print(url) + sys.exit(0) +sys.exit(1) +PY +) && break + PUBLIC_URL="" +done + +if [[ -z "$PUBLIC_URL" ]]; then + echo "ngrok tunnel did not start. See $NGROK_LOG" >&2 + exit 1 +fi + +cat <