fix: prevent Invalid URL crash when APP_URL lacks a protocol (#972)

* fix: prevent Invalid URL crash when APP_URL lacks a protocol (issue #970)

- Add getMcpSafeUrl() to notifications.ts: wraps getAppUrl() and
  guarantees a result that satisfies the MCP SDK's checkIssuerUrl
  requirement (https:// or http://localhost). Non-HTTPS, non-localhost
  URLs fall back to http://localhost:{PORT} instead of propagating an
  "Issuer URL must be HTTPS" error.
- Switch app.ts, mcp/index.ts, mcp/oauthProvider.ts, and oauthService.ts
  to import getMcpSafeUrl instead of getAppUrl for all MCP resource URL
  construction, so a misconfigured APP_URL never crashes the metadata
  router initialisation.
- Restrict the SDK metadata router middleware to /.well-known/* paths
  only. Previously it was invoked on every request; in production the
  lazy getMetaRouter() init ran on GET / and threw "Invalid URL" when
  APP_URL had no scheme, returning 500 for every page load.
- Log a startup warning when APP_URL is set but not usable, and include
  the resolved App URL in the startup banner so operators can confirm
  the correct value at a glance.
- Update oauth.test.ts mock to target notifications.getMcpSafeUrl.

* fix: show getAppUrl in banner and add two separate APP_URL startup checks

- Banner now displays getAppUrl() (the resolved app URL) rather than
  getMcpSafeUrl() so operators see the actual configured value
- Two independent startup warnings after the banner when APP_URL is set:
  1. whether APP_URL is a valid URL (parseable by new URL())
  2. whether APP_URL is MCP-safe (https:// or http://localhost)
- Fix getMcpSafeUrl() fallback port to use Number(PORT) || 3001,
  consistent with how index.ts parses PORT

* fix: update oidc.ts to import getAppUrl from notifications
This commit is contained in:
Julien G.
2026-05-07 13:49:39 +02:00
committed by GitHub
parent 9f1d05e886
commit de6c0fb781
9 changed files with 81 additions and 39 deletions
+32 -3
View File
@@ -46,13 +46,42 @@ function getSmtpConfig(): SmtpConfig | null {
// Exported for use by notificationService
export function getAppUrl(): string {
if (process.env.APP_URL) return process.env.APP_URL;
if (process.env.APP_URL) {
try {
const _ = new URL(process.env.APP_URL);
return process.env.APP_URL.replace(/\/+$/, '');
} catch (_ignored) {
}
}
const origins = process.env.ALLOWED_ORIGINS;
if (origins) {
const first = origins.split(',')[0]?.trim();
if (first) return first.replace(/\/+$/, '');
if (first) {
try {
const _ = new URL(first);
return first.replace(/\/+$/, '');
} catch (_ignored) {
}
}
}
const port = process.env.PORT || '3000';
const port = Number(process.env.PORT) || 3001;
return `http://localhost:${port}`;
}
/** Returns a URL guaranteed to satisfy the MCP SDK's issuer requirements (HTTPS or localhost).
* Falls back to http://localhost:{PORT} when APP_URL/ALLOWED_ORIGINS use a non-HTTPS, non-localhost scheme
* that would cause checkIssuerUrl to throw "Issuer URL must be HTTPS". */
export function getMcpSafeUrl(): string {
const candidate = getAppUrl();
try {
const u = new URL(candidate);
if (u.protocol === 'https:' || u.hostname === 'localhost' || u.hostname === '127.0.0.1') {
return candidate;
}
} catch {
// candidate was somehow invalid — fall through to localhost
}
const port = Number(process.env.PORT) || 3001;
return `http://localhost:${port}`;
}