Skip to content

Commit a4a4817

Browse files
committed
Adding PUBLIC_BASE_URL for public client documentation
1 parent 8f15b6b commit a4a4817

5 files changed

Lines changed: 50 additions & 16 deletions

File tree

.env.example

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,19 @@ FASTMCP_HOST=0.0.0.0
1212
# OAuth Settings
1313
ENABLE_OAUTH="false"
1414

15-
# Public base URL of THIS MCP server as clients reach it — scheme and host,
16-
# no path, no trailing slash. Used for OAuth callbacks and shown on the
17-
# index (/) and /status pages as the connect URL ({BASE_URL}/mcp).
18-
# Behind a TLS-terminating proxy/ingress this MUST be set to the external
19-
# https URL (e.g. https://mcp.example.com); otherwise the server derives it
20-
# from the raw request and reports the internal http hostname.
15+
# Base URL of THIS MCP server — scheme and host, no path, no trailing slash.
16+
# Used in OAuth discovery metadata (authorization_servers, DCR endpoint) and
17+
# as the connect URL ({BASE_URL}/mcp) on the index (/) and /status pages.
18+
# In a cluster this may be the internal service URL (e.g. http://obp-mcp-service).
2119
BASE_URL="http://localhost:9101"
2220

21+
# Optional: the EXTERNAL/public base URL when the server sits behind a
22+
# TLS-terminating proxy/ingress (e.g. https://mcp.example.com).
23+
# When set and different from BASE_URL, the index page shows two endpoints:
24+
# external (PUBLIC_BASE_URL/mcp) for outside clients and internal (BASE_URL/mcp)
25+
# for intra-cluster clients. Connect snippets use the external URL.
26+
# PUBLIC_BASE_URL="https://mcp.example.com"
27+
2328
# Choose authentication provider:
2429
# - "bearer-only": Simple JWT validation only (recommended for Opey/internal agents)
2530
# OAuth flow is handled externally; MCP server just validates tokens

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ services:
2323
AUTH_PROVIDER: ${AUTH_PROVIDER:-bearer-only}
2424
OBP_OIDC_ISSUER_URL: ${OBP_OIDC_ISSUER_URL:-}
2525
BASE_URL: ${BASE_URL:-http://localhost:9100}
26+
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-}
2627

2728
# Python Configuration
2829
PYTHONUNBUFFERED: 1

src/mcp_server_obp/auth.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,11 @@ def get_auth_provider(
941941
return None
942942

943943
# Get base URL
944+
# TODO: OAuth discovery metadata (authorization_servers, DCR endpoint) is
945+
# built from BASE_URL, which in a cluster may be the internal service URL.
946+
# If OAuth is ever enabled for external clients behind a proxy/ingress,
947+
# this should probably prefer PUBLIC_BASE_URL (see status.py) so clients
948+
# are pointed at a URL they can actually reach.
944949
base_url = base_url or os.getenv("BASE_URL")
945950
if not base_url:
946951
raise ValueError("BASE_URL must be set when OAuth is enabled.")

src/mcp_server_obp/status.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ async def build_status() -> dict[str, Any]:
125125
"version": "0.1.0",
126126
"uptime_seconds": round(time.time() - _START_TIME, 1),
127127
"base_url": os.getenv("BASE_URL") or None,
128+
"public_base_url": os.getenv("PUBLIC_BASE_URL") or None,
128129
},
129130
"obp_api": {
130131
"base_url": obp_base_url or None,
@@ -264,7 +265,8 @@ def row(label: str, value: Any) -> str:
264265
{row("Name", srv.get("name"))}
265266
{row("Version", srv.get("version"))}
266267
{row("Uptime", _fmt_uptime(srv.get("uptime_seconds", 0)))}
267-
{row("Public base URL", srv.get("base_url"))}
268+
{row("Base URL (BASE_URL)", srv.get("base_url"))}
269+
{row("Public base URL (PUBLIC_BASE_URL)", srv.get("public_base_url"))}
268270
</table>
269271
</section>
270272
@@ -303,19 +305,37 @@ def row(label: str, value: Any) -> str:
303305
</html>"""
304306

305307

306-
def _server_base_url(request: Request) -> str:
307-
"""Public base URL of this server: BASE_URL env if set, else derived
308-
from the incoming request (scheme://host[:port])."""
309-
configured = os.getenv("BASE_URL", "").rstrip("/")
310-
if configured:
311-
return configured
312-
return f"{request.url.scheme}://{request.url.netloc}"
308+
def _endpoint_urls(request: Request) -> tuple[str, str | None]:
309+
"""Resolve the MCP base URLs to advertise.
310+
311+
Returns (external, internal):
312+
- external: what clients outside the cluster/network should use —
313+
PUBLIC_BASE_URL if set, else BASE_URL, else derived from the request.
314+
- internal: BASE_URL, only when PUBLIC_BASE_URL is also set and differs
315+
(i.e. the server has distinct intra-cluster and public interfaces);
316+
None otherwise.
317+
"""
318+
base_url = os.getenv("BASE_URL", "").rstrip("/")
319+
public_base_url = os.getenv("PUBLIC_BASE_URL", "").rstrip("/")
320+
external = public_base_url or base_url or f"{request.url.scheme}://{request.url.netloc}"
321+
internal = base_url if public_base_url and base_url and base_url != public_base_url else None
322+
return external, internal
313323

314324

315325
def _render_index_html(request: Request) -> str:
316326
esc = _html.escape
317-
base_url = _server_base_url(request)
318-
mcp_url = f"{base_url}/mcp"
327+
external_base, internal_base = _endpoint_urls(request)
328+
mcp_url = f"{external_base}/mcp"
329+
internal_note = (
330+
f"""
331+
<h3>Internal (intra-cluster)</h3>
332+
<p class="endpoint"><code>{esc(internal_base)}/mcp</code></p>
333+
<p>Only reachable from inside the cluster/network — for internal agents
334+
(e.g. Opey) and service-to-service clients.</p>"""
335+
if internal_base
336+
else ""
337+
)
338+
external_heading = "<h3>External (public)</h3>" if internal_base else ""
319339

320340
auth_enabled = os.getenv("ENABLE_OAUTH", "false").lower() == "true"
321341
auth_provider = os.getenv("AUTH_PROVIDER", "none") if auth_enabled else "none"
@@ -383,8 +403,10 @@ def _render_index_html(request: Request) -> str:
383403
384404
<section>
385405
<h2>MCP endpoint</h2>
406+
{external_heading}
386407
<p class="endpoint"><code>{esc(mcp_url)}</code> (Streamable HTTP)</p>
387408
<p>{auth_note}</p>
409+
{internal_note}
388410
{obp_line}
389411
</section>
390412

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def clean_env(monkeypatch):
1111
"ENABLE_OAUTH",
1212
"AUTH_PROVIDER",
1313
"BASE_URL",
14+
"PUBLIC_BASE_URL",
1415
"KEYCLOAK_REALM_URL",
1516
"OBP_OIDC_ISSUER_URL",
1617
"JWKS_URI",

0 commit comments

Comments
 (0)