Hmbown/CodeWhale · error · ConversionError

Invalid MCP endpoint URL.

Error message

Invalid MCP endpoint URL.

What it means

remote_server() validates MCP server endpoint URLs and raises this ConversionError when Python's urlsplit itself fails to parse the URL — malformed syntax that urlsplit cannot turn into a structured result.

Solutions

  1. Fix the URL so urlsplit parses it: bracket IPv6 hosts, use a numeric port
  2. Remove template placeholders and interpolate real values before conversion
  3. Prefer a canonical https://host:port form and re-run the converter

Example fix

// before
url: "http://[::1:notaport"
// after
url: "http://[::1]:8080"
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlsplit
p = urlsplit(url)
assert p.scheme in ("http", "https") and p.hostname and p.port is not None

Type guard

def looks_like_valid_url(url: str) -> bool:
    try:
        return bool(urlsplit(url).hostname)
    except ValueError:
        return False

Try / catch

try:
    validate_mcp_url(url)
except ValueError as e:
    log.error("malformed MCP URL %r: %s", url, e)

Prevention

When it happens

Trigger: A URL that makes parsed.port or hostname access raise ValueError: unparseable port (e.g. 'https://host:notaport'), malformed IPv6 brackets, or otherwise invalid URL syntax.

Common situations: Typos in the port field; unbracketed IPv6 literals; template placeholders left in config like {{PORT}}; URLs copied with trailing garbage.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/5e81466577e9ae59. Report an issue: GitHub.

Appendix: source

Thrown at scripts/convert-plugin.py:229

def remote_server(config, dialect, defaults):
    mapping(config)
    if dialect == "dsh":
        require(config.get("transport") == "streamable-http", "Unsupported MCP transport.")
        mapping(config, {"serverName", "transport", "url", "headers", "toolCallTimeoutMs", "failOnStartupError"})
    else:
        require(config.get("type") == "remote", "Unsupported MCP transport.")
        mapping(config, {"type", "url", "headers", "oauth", "enabled" if dialect == "opencode-v1" else "disabled", "timeout"})
        require(config.get("oauth") is False, "Set oauth:false explicitly; plugin OAuth and upstream auto-OAuth cannot be converted.")
    extension = server_options(config, dialect, defaults)
    url = config.get("url")
    require(isinstance(url, str) and not re.search(r"[\s\\{}]", url), "MCP URL must be a literal endpoint without interpolation.")
    try:
        parsed = urlsplit(url)
        host = parsed.hostname
        require(bool(host) and parsed.port != 0, "MCP URL needs a valid host and port.")
    except ValueError:
        raise ConversionError("Invalid MCP endpoint URL.") from None
    require(parsed.scheme == "https" or (parsed.scheme == "http" and host in {"localhost", "127.0.0.1", "::1"}),
            "MCP endpoints need HTTPS (or explicit loopback HTTP).")
    require(parsed.username is None and parsed.password is None and not parsed.query and not parsed.fragment,
            "MCP URLs must not contain credentials, query strings or fragments.")
    require(host.isascii() and len(url) <= 4096, "Use an ASCII MCP hostname and URL of at most 4096 characters.")
    # URL implementations disagree on shorthand/hex IPv4 spellings. Emit only
    # canonical numeric hosts so the reviewed native host set is identical.
    if ":" in host or re.fullmatch(r"(?:[0-9]+|0x[0-9a-f]+)", host.rsplit(".", 1)[-1]):
        try:
            address = ipaddress.ip_address(host)
            require(str(address) == host, "Use a canonical numeric MCP address.")
            host = f"[{host}]" if address.version == 6 else host
        except ValueError:
            raise ConversionError("Use a canonical numeric MCP address.") from None
    headers = mapping(config.get("headers", {}))
    require(len(headers) <= 64, "At most 64 environment-backed headers are supported.")
    env_headers = {}
    seen_headers = set()

View on GitHub (pinned to 433685b202)