different-ai/openwork · error · LocalManagedMcpPrivateUrlError

embedded URL credentials are not allowed

Error message

embedded URL credentials are not allowed

What it means

LocalManagedMcpPrivateUrlError thrown by parseHttpUrl when the configured MCP URL embeds userinfo credentials (username or password, e.g. https://user:pass@host). Embedding credentials in URLs is rejected to prevent credential leakage into logs, error messages, and downstream fetches.

Source

Thrown at apps/server/src/local-managed-mcp-url-guard.ts:112

export function isLocalManagedMcpPrivateAddress(address: string): boolean {
  const version = isIP(address);
  if (version === 4) return isPrivateIpv4(address);
  if (version === 6) return isPrivateIpv6(address);
  return true;
}

function parseHttpUrl(rawUrl: string): URL {
  let url: URL;
  try {
    url = new URL(rawUrl);
  } catch {
    throw new LocalManagedMcpPrivateUrlError(rawUrl, "not a valid URL");
  }
  if (url.protocol !== "http:" && url.protocol !== "https:") {
    throw new LocalManagedMcpPrivateUrlError(rawUrl, `protocol "${url.protocol}" is not allowed`);
  }
  if (url.username || url.password) {
    throw new LocalManagedMcpPrivateUrlError(rawUrl, "embedded URL credentials are not allowed");
  }
  return url;
}

function allowPrivateUrls(): boolean {
  return process.env.OPENWORK_DEV_MODE === "1" || process.env.OPENWORK_ALLOW_PRIVATE_MCP_URLS === "1";
}

type ResolveAddresses = (hostname: string, options: LookupAllOptions) => Promise<LookupAddress[]>;

const resolveAddresses: ResolveAddresses = (hostname, options) => lookup(hostname, options);

function validateResolvedAddresses(hostname: string, addresses: LookupAddress[]): void {
  if (addresses.length === 0) {
    throw new LocalManagedMcpPrivateUrlError(`https://${hostname}/`, "the hostname does not resolve");
  }
  if (allowPrivateUrls()) return;
  for (const { address } of addresses) {

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Remove user:pass@ from the URL and pass credentials via HTTP headers (e.g. Authorization) in the server config
  2. If the MCP server requires basic auth, use its header/config option instead of URL userinfo
  3. Rotate the credential if it was embedded in a URL — URLs get logged and shared
  4. Store the secret in an env var and reference it in the config rather than inline

Example fix

// before
{ "url": "https://user:hunter2@mcp.example.com/sse" }
// after
{ "url": "https://mcp.example.com/sse", "headers": { "Authorization": "Basic ..." } }
Defensive patterns

Strategy: validation

Validate before calling

function assertNoUrlCredentials(raw: string): void {
  const u = new URL(raw);
  if (u.username || u.password) throw new Error("move credentials out of the URL into headers");
}

Type guard

function hasEmbeddedCredentials(raw: string): boolean {
  try { const u = new URL(raw); return u.username !== "" || u.password !== ""; } catch { return false; }
}

Try / catch

try {
  await mcp.addServer({ url: rawUrl });
} catch (e) {
  if (e instanceof LocalManagedMcpPrivateUrlError && e.message.includes("credentials")) {
    // strip userinfo, reconfigure auth via headers, rotate the leaked secret
  } else throw e;
}

Prevention

When it happens

Trigger: A local managed MCP server URL of the form scheme://user:pass@host/... is parsed: url.username or url.password is non-empty after new URL(rawUrl).

Common situations: Copy-pasting a URL with basic-auth credentials from another tool; putting API keys in the URL instead of headers; legacy configs that predate header-based auth.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/eee9fb16be1e77fe. Report an issue: GitHub.