/api/url/parse
MCP tool url_parse Parses a URL exactly as browsers and Cloudflare Workers do (WHATWG URL Standard) and returns every component: scheme, username, host in ASCII and Unicode forms, port and default port, origin, path segments decoded, query parameters as an ordered list (duplicates kept), and fragment. It classifies the host (domain, IPv4, IPv6, localhost, private or other special-purpose range) without resolving DNS. A password, if present, is never returned: hasPassword is true and href shows *** instead. With base, a relative reference is resolved first.
Browser version (runs locally): URL Parser.
Try it
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
input required | string (≤ 16384 chars) | — | The URL, absolute (https://…), or relative (../a?b=1) when base is given. |
base | string (≤ 16384 chars) | — | Absolute base URL for resolving a relative input (RFC 3986 §5 as refined by WHATWG). Ignored when input is absolute. |
Also accepted on every endpoint: format (json | text).
Parameters may come from the query string, a JSON object body, an urlencoded or multipart form,
or (for input) a text/plain body.
Global parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
format | json | text | json | json returns the documented object. text returns only the primary value as text/plain (see "Text output"). |
Response 200 application/json
| Field | Type | Description |
|---|---|---|
href | string | The normalized URL (WHATWG serialization). If the input had a password, it is replaced by ***. |
hrefRedacted | boolean | True when href had its password replaced. |
resolvedAgainstBase | boolean | True when the input was relative and was resolved against base. |
protocol | string | Scheme with the colon, e.g. https:. |
scheme | string | Scheme without the colon, lowercase. |
special | boolean | A WHATWG special scheme (http, https, ws, wss, ftp, file). |
username | object | |
username.raw | string | As serialized in the normalized URL (percent-encoded). |
username.decoded | string | null | Percent-decoded as UTF-8; null if an escape is not valid UTF-8. |
hasPassword | boolean | True when the URL contains a password. The password is never returned. |
host | string | Host plus :port when a non-default port is present (ASCII). |
hostname | string | Host without port, ASCII (Punycode) form. IPv6 includes brackets. |
hostnameUnicode | string | Host with xn-- labels decoded to Unicode (equal to hostname otherwise). |
hostType | "domain" | "ipv4" | "ipv6" | "opaque" | "empty" | "none" | domain, ipv4, ipv6, opaque (host of a non-special scheme), empty (e.g. file:///), or none (no authority, e.g. mailto:). |
idn | boolean | True when any host label is an xn-- (internationalized) label. |
ip | object | null | |
ip.version | integer | 4 or 6. |
ip.address | string | Address as normalized by the URL parser (no brackets). |
ip.expanded | string | IPv6: all eight 4-digit groups. IPv4: dotted decimal. |
ip.range | "public" | "private" | "loopback" | "link-local" | "unspecified" | "shared" | "multicast" | "broadcast" | "documentation" | "benchmarking" | "reserved" | "unique-local" | "ipv4-mapped" | "nat64" | "site-local" | Special-purpose range (IANA registries), or public. |
ip.mappedIpv4 | string | null | Embedded IPv4 address for ::ffff:0:0/96 and 64:ff9b::/96. |
isLocalhost | boolean | localhost, *.localhost (RFC 6761), or a loopback IP. |
isPrivate | boolean | IP in RFC 1918, RFC 6598 shared (100.64/10), fc00::/7 or fec0::/10 (including IPv4-mapped forms). |
isPublic | boolean | null | True for a global unicast IP; false for any special-purpose IP or localhost; null for a domain name (it is not resolved). |
port | integer | null | Explicit non-default port, or null. |
defaultPort | integer | null | Default port of the scheme (http 80, https 443, ws 80, wss 443, ftp 21). |
effectivePort | integer | null | port ?? defaultPort: the port a client would connect to. |
origin | string | null | scheme://host[:port]; null for opaque origins (file:, data:, mailto:, …). |
pathname | string | Path as serialized (percent-encoded). |
opaquePath | boolean | True for URLs like mailto:a@b whose path is not a /-separated list. |
pathSegments | array of object | Path segments split on /; empty for opaque paths. |
pathSegments[].raw | string | As serialized in the normalized URL (percent-encoded). |
pathSegments[].decoded | string | null | Percent-decoded as UTF-8; null if an escape is not valid UTF-8. |
search | string | Query string including ?, or empty. |
query | array of object | Query parameters in order. Duplicate names are kept as separate entries. |
query[].name | string | Decoded name (application/x-www-form-urlencoded: + is a space). |
query[].value | string | Decoded value; empty string when there is no =. |
query[].rawName | string | Name as it appears in the URL. |
query[].rawValue | string | null | Value as it appears in the URL; null when there is no =. |
hash | string | Fragment including #, or empty. |
fragment | object | null | |
fragment.raw | string | As serialized in the normalized URL (percent-encoded). |
fragment.decoded | string | null | Percent-decoded as UTF-8; null if an escape is not valid UTF-8. |
warnings | array of string | Normalizations applied and risks found. |
Text output (format=text, text/plain): The normalized href (password redacted).
Errors
Errors are JSON: {"error": "<message>", "code": "<code>", "param": "<name>"?}.
| Status | Code | When |
|---|---|---|
422 | unprocessable_input | input is not a valid URL (the message suggests the likely cause: no scheme, bad host character, bad port), or base is not a valid absolute URL. |
400 | missing_parameter | A required parameter is absent. |
400 | unknown_parameter | A parameter name is not in the table above (typos are rejected, not ignored). |
400 | invalid_parameter | A value has the wrong type, is out of range, or is not an allowed value. |
413 | payload_too_large | The request body exceeds 1 MiB. |
405 | method_not_allowed | A method other than GET, POST or OPTIONS. |
Behaviour
- Idempotent: the same arguments always return the same result.
-
Requires a client ID and secret (authentication). Each call is
recorded as one metadata row against your client ID; inputs and outputs are never stored or
logged. Responses carry
cache-control: no-store. - CORS: any origin may call this endpoint from a browser, using your credentials.
Examples
Responses below were produced by running each request through this code at build time. Long strings are shortened.
URL with duplicate query parameters and an encoded path
curl 'https://util.caruana.me/api/url/parse?input=https%3A%2F%2Fuser%40Example.COM%3A443%2Fa%2520b%2Fc%3Ftag%3Dx%26tag%3Dy%26q%3Dcaf%25C3%25A9%2Bau%2Blait%23top'
Response 200
{
"href": "https://[email protected]/a%20b/c?tag=x&tag=y&q=caf%C3%A9+au+lait#top",
"hrefRedacted": false,
"resolvedAgainstBase": false,
"protocol": "https:",
"scheme": "https",
"special": true,
"username": {
"raw": "user",
"decoded": "user"
},
"hasPassword": false,
"host": "example.com",
"hostname": "example.com",
"hostnameUnicode": "example.com",
"hostType": "domain",
"idn": false,
"ip": null,
"isLocalhost": false,
"isPrivate": false,
"isPublic": null,
"port": null,
"defaultPort": 443,
"effectivePort": 443,
"origin": "https://example.com",
"pathname": "/a%20b/c",
"opaquePath": false,
"pathSegments": [
{
"raw": "a%20b",
"decoded": "a b"
},
{
"raw": "c",
"decoded": "c"
}
],
"search": "?tag=x&tag=y&q=caf%C3%A9+au+lait",
"query": [
{
"name": "tag",
"value": "x",
"rawName": "tag",
"rawValue": "x"
},
{
"name": "tag",
"value": "y",
"rawName": "tag",
"rawValue": "y"
},
{
"name": "q",
"value": "café au lait",
"rawName": "q",
"rawValue": "caf%C3%A9+au+lait"
}
],
"hash": "#top",
"fragment": {
"raw": "top",
"decoded": "top"
},
"warnings": [
"The URL contains a username (userinfo).",
"The explicit port 443 is the default for https: and was removed from the normalized URL."
]
}
Same call as an MCP tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "url_parse",
"arguments": {
"input": "https://[email protected]:443/a%20b/c?tag=x&tag=y&q=caf%C3%A9+au+lait#top"
}
}
}Resolve a relative reference
curl 'https://util.caruana.me/api/url/parse?input=..%2Fimg%2Flogo.png%3Fv%3D2&base=https%3A%2F%2Fexample.com%2Fdocs%2Fguide%2F'
Response 200
{
"href": "https://example.com/docs/img/logo.png?v=2",
"hrefRedacted": false,
"resolvedAgainstBase": true,
"protocol": "https:",
"scheme": "https",
"special": true,
"username": {
"raw": "",
"decoded": ""
},
"hasPassword": false,
"host": "example.com",
"hostname": "example.com",
"hostnameUnicode": "example.com",
"hostType": "domain",
"idn": false,
"ip": null,
"isLocalhost": false,
"isPrivate": false,
"isPublic": null,
"port": null,
"defaultPort": 443,
"effectivePort": 443,
"origin": "https://example.com",
"pathname": "/docs/img/logo.png",
"opaquePath": false,
"pathSegments": [
{
"raw": "docs",
"decoded": "docs"
},
{
"raw": "img",
"decoded": "img"
},
{
"raw": "logo.png",
"decoded": "logo.png"
}
],
"search": "?v=2",
"query": [
{
"name": "v",
"value": "2",
"rawName": "v",
"rawValue": "2"
}
],
"hash": "",
"fragment": null,
"warnings": []
}
Same call as an MCP tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "url_parse",
"arguments": {
"input": "../img/logo.png?v=2",
"base": "https://example.com/docs/guide/"
}
}
}Private IPv6 host with a non-default port
curl 'https://util.caruana.me/api/url/parse?input=http%3A%2F%2F%5Bfd00%3A%3A1%5D%3A8080%2Fadmin'
Response 200
{
"href": "http://[fd00::1]:8080/admin",
"hrefRedacted": false,
"resolvedAgainstBase": false,
"protocol": "http:",
"scheme": "http",
"special": true,
"username": {
"raw": "",
"decoded": ""
},
"hasPassword": false,
"host": "[fd00::1]:8080",
"hostname": "[fd00::1]",
"hostnameUnicode": "[fd00::1]",
"hostType": "ipv6",
"idn": false,
"ip": {
"version": 6,
"address": "fd00::1",
"expanded": "fd00:0000:0000:0000:0000:0000:0000:0001",
"range": "unique-local",
"mappedIpv4": null
},
"isLocalhost": false,
"isPrivate": true,
"isPublic": false,
"port": 8080,
"defaultPort": 80,
"effectivePort": 8080,
"origin": "http://[fd00::1]:8080",
"pathname": "/admin",
"opaquePath": false,
"pathSegments": [
{
"raw": "admin",
"decoded": "admin"
}
],
"search": "",
"query": [],
"hash": "",
"fragment": null,
"warnings": []
}
Same call as an MCP tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "url_parse",
"arguments": {
"input": "http://[fd00::1]:8080/admin"
}
}
}