From 2708a6ff5f70bca6babd976af25266072a1f9585 Mon Sep 17 00:00:00 2001 From: gpt-5-codex 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