From 2708a6ff5f70bca6babd976af25266072a1f9585 Mon Sep 17 00:00:00 2001
From: gpt-5-codex <elope@esus.name>
Date: Fri, 10 Oct 2025 23:55:27 +0200
Subject: [PATCH] feat: support legacy streamable-http config entries

---
 examples/mcp.conf                 | 40 ++++++++++++++-------
 mcp_browser/config.py             | 58 ++++++++++++++++++++++++++++---
 systemd/mcp-browser-proxy.service | 41 ++++++++++++++--------
 3 files changed, 108 insertions(+), 31 deletions(-)

diff --git a/examples/mcp.conf b/examples/mcp.conf
index f27547e..f7930c1 100644
--- a/examples/mcp.conf
+++ b/examples/mcp.conf
@@ -1,11 +1,14 @@
 # Example NGINX front-end for MCP Browser
 #
-# Replace the placeholder values (domain, certificate paths, slug, upstream
-# ports) with values that match your deployment. This configuration exposes:
-#   * OAuth metadata + token endpoints implemented via an njs helper
-#   * An authenticated Streamable HTTP MCP endpoint
-#   * An unauthenticated debug endpoint (optional) that forwards to the
-#     mcp-browser SSE bridge
+# This template follows the MCP specification (2025-03-26) section 2.4 on dynamic
+# client registration:
+#   https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#2-4-dynamic-client-registration
+#
+# Replace the placeholder values (domain, certificate paths, hash slug, upstream
+# ports) with values that match your deployment. The configuration provides:
+#   * OAuth metadata + register/token endpoints backed by an njs helper
+#   * A protected Streamable HTTP MCP endpoint (auth_request + OAuth)
+#   * An optional unauthenticated SSE bridge for local debugging
 #
 # The example assumes that:
 #   * An njs script lives at /etc/nginx/njs/mcp_oauth.js providing handlers
@@ -45,14 +48,17 @@ server {
 
   add_header X-Robots-Tag "noindex, nofollow, noarchive" always;
 
-  # --- OAuth discovery + token endpoints (served by njs helper) -------------
-  # Replace __MCP_SLUG__ with the slug you publish in OAuth metadata.
-  location = /.well-known/oauth-authorization-server/__MCP_SLUG__/mcp {
+  # Resource identifiers MUST match what your metadata document returns.
+  # Use the same hash/digest in mcp_oauth.metadata().
+  set $mcp_resource "__MCP_RESOURCE_HASH__";
+
+  # --- OAuth discovery + token/register endpoints per §2.4 ------------------
+  location = /.well-known/oauth-authorization-server/$mcp_resource/mcp {
     default_type application/json;
     js_content mcp_oauth.metadata;
   }
 
-  location = /.well-known/oauth-protected-resource/__MCP_SLUG__/mcp {
+  location = /.well-known/oauth-protected-resource/$mcp_resource/mcp {
     default_type application/json;
     js_content mcp_oauth.metadata;
   }
@@ -77,6 +83,16 @@ server {
     js_content mcp_oauth.token;
   }
 
+  # The metadata served above should advertise this /authorize endpoint.
+  # Update the upstream target to whatever Authorization Server you use.
+  location = /authorize {
+    proxy_http_version 1.1;
+    proxy_set_header Host $host;
+    proxy_set_header X-Forwarded-Proto $scheme;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_pass https://idp.example.com/oauth2/authorize;
+  }
+
   # Internal auth_request hook used by the protected MCP endpoint.
   location = /_oauth_check {
     internal;
@@ -85,7 +101,7 @@ server {
 
   # --- Protected Streamable HTTP MCP endpoint --------------------------------
   # Requires successful OAuth check before proxying to the upstream server.
-  location ^~ /__MCP_SLUG__/mcp/ {
+  location ^~ /$mcp_resource/mcp/ {
     auth_request /_oauth_check;
 
     proxy_http_version 1.1;
@@ -109,7 +125,7 @@ server {
   }
 
   # --- Optional unauthenticated SSE bridge for local debugging ---------------
-  location ^~ /__MCP_SLUG__/mcp-browser/ {
+  location ^~ /$mcp_resource/mcp-browser/ {
     proxy_http_version 1.1;
     proxy_set_header Connection "";
     proxy_set_header Host $host;
diff --git a/mcp_browser/config.py b/mcp_browser/config.py
index 2958465..7a500b7 100644
--- a/mcp_browser/config.py
+++ b/mcp_browser/config.py
@@ -109,6 +109,23 @@ class ConfigLoader:
         servers = {}
         for name, server_config in config_data.get("servers", {}).items():
             transport_data = server_config.get("transport", {}) or {}
+
+            # Merge transport metadata from legacy locations
+            raw_transport_type = (
+                transport_data.get("type")
+                or server_config.get("transportType")
+                or server_config.get("type")
+            )
+            normalized_type = "stdio"
+            if isinstance(raw_transport_type, str):
+                lowered = raw_transport_type.replace("_", "-").lower()
+                if lowered in {"streamable-http", "streamablehttp"}:
+                    normalized_type = "streamable-http"
+                elif lowered in {"stdio", "standard"}:
+                    normalized_type = "stdio"
+                else:
+                    normalized_type = lowered
+
             oauth_data = transport_data.get("oauth") or {}
             oauth_config = None
             if oauth_data:
@@ -121,12 +138,43 @@ class ConfigLoader:
                     extra_params=oauth_data.get("extra_params") or oauth_data.get("extraParams", {}),
                     access_token=oauth_data.get("access_token") or oauth_data.get("accessToken"),
                 )
+
+            transport_url = transport_data.get("url") or server_config.get("url")
+            headers = transport_data.get("headers") or server_config.get("headers") or {}
+            timeout = (
+                transport_data.get("timeout")
+                or server_config.get("timeout")
+                or 30.0
+            )
+            sse_timeout = (
+                transport_data.get("sse_timeout")
+                or transport_data.get("sseTimeout")
+                or server_config.get("sse_timeout")
+                or server_config.get("sseTimeout")
+                or 300.0
+            )
+
+            # Legacy args: ["url", "http://..."] or ["--url=http://..."]
+            args = server_config.get("args", [])
+            if not transport_url and isinstance(args, list):
+                for idx, arg in enumerate(args):
+                    if not isinstance(arg, str):
+                        continue
+                    stripped = arg.strip()
+                    if stripped in {"url", "--url", "--transport-url"}:
+                        if idx + 1 < len(args):
+                            transport_url = args[idx + 1]
+                            break
+                    elif stripped.startswith(("url=", "--url=", "--transport-url=")):
+                        transport_url = stripped.split("=", 1)[1]
+                        break
+
             transport_config = TransportConfig(
-                type=transport_data.get("type", "stdio"),
-                url=transport_data.get("url"),
-                headers=transport_data.get("headers", {}),
-                timeout=transport_data.get("timeout", 30.0),
-                sse_timeout=transport_data.get("sse_timeout") or transport_data.get("sseTimeout", 300.0),
+                type=normalized_type,
+                url=transport_url,
+                headers=headers,
+                timeout=timeout,
+                sse_timeout=sse_timeout,
                 oauth=oauth_config
             )
             servers[name] = MCPServerConfig(
diff --git a/systemd/mcp-browser-proxy.service b/systemd/mcp-browser-proxy.service
index 9f883a7..3503f3a 100644
--- a/systemd/mcp-browser-proxy.service
+++ b/systemd/mcp-browser-proxy.service
@@ -1,5 +1,5 @@
 [Unit]
-Description=Expose MCP Browser via HTTP bridge
+Description=Run mcp-browser in MCP server mode
 Documentation=https://github.com/Xilope0/mcp-browser
 After=network-online.target
 Wants=network-online.target
@@ -7,20 +7,33 @@ Wants=network-online.target
 [Service]
 Type=simple
 Environment="PYTHONUNBUFFERED=1"
-Environment="MCP_PROXY_BIN=%h/.local/bin/mcp-proxy"
-Environment="MCP_PROXY_ALLOW_ORIGIN=https://platform.openai.com"
-Environment="MCP_PROXY_PORT=14001"
-Environment="MCP_PROXY_CONFIG=%h/.config/mcp-browser/proxy.yaml"
-Environment="MCP_PROXY_EXTRA_ARGS="
-EnvironmentFile=-%h/.config/mcp-browser/proxy.env
+Environment="MCP_BROWSER_BIN=%h/.local/bin/mcp-browser"
+Environment="MCP_BROWSER_CONFIG=%h/.config/mcp-browser/config.yaml"
+Environment="MCP_BROWSER_SERVER=default"
+Environment="MCP_BROWSER_MODE=server"
+Environment="MCP_BROWSER_NO_BUILTIN=false"
+Environment="MCP_BROWSER_TRANSPORT="
+Environment="MCP_BROWSER_TRANSPORT_URL="
+Environment="MCP_BROWSER_EXTRA_ARGS="
+EnvironmentFile=-%h/.config/mcp-browser/browser.env
 
-ExecStart=/usr/bin/env sh -c '\
-  set -eu; \
-  exec "$MCP_PROXY_BIN" \
-    --allow-origin "$MCP_PROXY_ALLOW_ORIGIN" \
-    --port "$MCP_PROXY_PORT" \
-    --named-server-config "$MCP_PROXY_CONFIG" \
-    $MCP_PROXY_EXTRA_ARGS'
+ExecStart=/usr/bin/env bash -lc 'set -eu
+ARGS=()
+if [[ "$MCP_BROWSER_NO_BUILTIN" == "true" ]]; then
+  ARGS+=(--no-builtin)
+fi
+if [[ -n "$MCP_BROWSER_TRANSPORT" ]]; then
+  ARGS+=(--transport "$MCP_BROWSER_TRANSPORT")
+fi
+if [[ -n "$MCP_BROWSER_TRANSPORT_URL" ]]; then
+  ARGS+=(--transport-url "$MCP_BROWSER_TRANSPORT_URL")
+fi
+exec "$MCP_BROWSER_BIN" \
+  --mode "$MCP_BROWSER_MODE" \
+  --config "$MCP_BROWSER_CONFIG" \
+  --server "$MCP_BROWSER_SERVER" \
+  "${ARGS[@]}" \
+  $MCP_BROWSER_EXTRA_ARGS'
 
 Restart=on-failure
 RestartSec=5
