CultriX-Github/mcp-forsale
If you are the rightful owner of mcp-forsale and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to dayong@mcphub.com.
The For-Sale MCP Server is a lightweight Model Context Protocol server that allows clients to check if a domain name is advertised as for sale using the standardized '_for-sale' DNS TXT record.
🏷️ For-Sale MCP Server (Cloudflare Worker)
A lightweight Model Context Protocol (MCP) server that lets clients check whether a domain name advertises itself as for sale using the standardized _for-sale DNS TXT record.
The server runs entirely on Cloudflare Workers, making it globally available, serverless, and free to operate at scale.
✨ What’s new
-
Built-in, fail-closed validator (server-side): Every resolver tool now always runs a strict validator for
_for-saleTXT records and returns a normalized validator report. No client/system prompt required; logic is enforced in the Worker. -
Deterministic JSON schema: The validator outputs a fixed JSON shape with
valid,errors[],record_info,parsed, andexplanations(with per-rule pass/fail). It applies single-string TXT, exact version prefix, exactly one tag, decoding policy, URI allowlist, IDNA, and tag-specific rules. -
IDNA (punycode) support for
furihosts: Validator reports both Unicode and punycode host forms. -
New tools:
validate_for_sale_txt(low-level: validate a single TXT RDATA you already resolved)resolve_and_validate_for_sale(one-shot: resolve + validate)
All existing tools (check_for_sale, check_for_sale_structured, natural_language_check) now return validation results alongside resolver output.
✨ Features
-
Implements the
_for-saledraft specification Parsesv=FORSALE1;TXT records and extracts structured content fields (furi,ftxt,fval,fcod). -
Strict, fail-closed validation
- Exact prefix:
v=FORSALE1; - Exactly one tag from
{fcod, ftxt, furi, fval} - TXT RDATA must be a single character-string ≤255 octets (no concatenation)
- Unknown/additional tags (including via decoding) → reject
- Decoding policy: at most one percent-decode pass and only for
furi furimust be absolute and scheme must be allowlisted (https, optionalhttp,mailto,tel), length ≤2048 after decoding- Hostnames in
furi: IDNA applied and both Unicode and punycode reported fcod:^[A-Z0-9_-]+$, ≤32 charsftxt: opaque, ≤2048 chars, never decoded or linkifiedfval:^[A-Z]{3}[0-9]+(?:\.[0-9]{1,18})?$→ canonicalized as"CUR amount"- Aliases/wildcards: cross-zone heuristic & notes included
- Returns only JSON; values are tainted and must be HTML-escaped downstream
- Exact prefix:
-
Public MCP interface (SSE + Streamable HTTP) Works with any MCP client (Claude Desktop, Cursor, Windsurf, MCP Inspector).
-
Natural-language querying Provide prompts like “Is example.nl for sale?” — domain is extracted, resolved, and validated automatically.
-
Dual resolver support
- Default: Cloudflare DoH JSON
- Optional: custom JSON resolver endpoint (
resolver.j78workers.workers.dev)
-
Runs serverlessly on Cloudflare Workers — no backend needed.
🧠 Background
This project implements the operational convention described in:
Davids, M. (2025) — The
_for-saleUnderscored and Globally Scoped DNS Node Name — IETF Internet-Draft:draft-davids-forsalereg-11
It enables a domain holder to signal sale availability through DNS itself, without third-party marketplaces or WHOIS scraping.
🧰 Tools Exposed
Each resolver tool now returns both the classic resolver data and an array of validator reports:
Top-level (resolver) fields:
domain: the queried registrable namequeried_name:_for-sale.<domain>method:"doh"or"resolver"is_for_sale: basic presence check (anyv=FORSALE1;TXT found)records: the raw TXT strings that begin withv=FORSALE1;all_txt: all TXT payloads found under the nodestructured: lenient extraction summary (pre-validator)raw: full DNS/resolver payloadvalidations: array of validator outputs (one perrecords[i])
1) Tool: check_for_sale
Description: Resolve _for-sale.<domain> and always validate any discovered v=FORSALE1; records.
Example: {"domain": "example.nl", "method": "doh"}
2) Tool: check_for_sale_structured
Description: Same as above; returns the same resolver data plus validations (and the structured summary).
Example: {"domain": "example.nl"}
3) Tool: natural_language_check
Description: Accepts a free-text prompt, extracts a domain, resolves and validates.
Example: {"prompt": "Is example.nl for sale?"}
4) Tool: resolve_and_validate_for_sale
Description: One-shot “resolve + validate” convenience endpoint.
Example: {"domain": "example.nl", "method": "doh"}
5) Tool: validate_for_sale_txt
Description: Low-level validator for a single TXT RDATA you already have (e.g., from your own resolver). Example input shape:
{
"input": {
"query_fqdn": "_for-sale.example.org.",
"final_fqdn": "_for-sale.example.org.",
"alias_chain": [],
"is_wildcard": false,
"rdata_len_bytes": 96,
"txt": "v=FORSALE1;furi=https://broker.example/offer?id=123%2D456"
}
}
✅ Validator Output Schema
Validator returns only JSON in this exact shape:
{
"valid": true,
"errors": [],
"record_info": {
"query_fqdn": "string",
"final_fqdn": "string",
"cross_zone": false,
"is_wildcard": false,
"alias_chain": ["string"]
},
"parsed": {
"tag": "fcod|ftxt|furi|fval",
"value_raw": "string",
"value_norm": "string|null",
"extras": {
"uri": {
"scheme": "string|null",
"host_unicode": "string|null",
"host_punycode": "string|null",
"path": "string|null",
"query": "string|null"
},
"price": {
"currency": "string|null",
"amount_decimal": "string|null"
}
}
},
"explanations": {
"summary": "string",
"rule_evaluations": [
{
"rule": "string",
"status": "pass|fail|not_applicable",
"why": "string"
}
]
}
}
Example A — Valid furi (https)
{
"valid": true,
"errors": [],
"record_info": {
"query_fqdn": "_for-sale.example.org.",
"final_fqdn": "_for-sale.example.org.",
"cross_zone": false,
"is_wildcard": false,
"alias_chain": []
},
"parsed": {
"tag": "furi",
"value_raw": "https://broker.example/offer?id=123%2D456",
"value_norm": "https://broker.example/offer?id=123-456",
"extras": {
"uri": {
"scheme": "https",
"host_unicode": "broker.example",
"host_punycode": "broker.example",
"path": "/offer",
"query": "id=123-456"
},
"price": { "currency": null, "amount_decimal": null }
}
},
"explanations": {
"summary": "Record is valid: correct version prefix, exactly one allowed tag, absolute https URI with single percent-decoding, and no aliasing concerns.",
"rule_evaluations": [
{"rule": "prefix v=FORSALE1;", "status": "pass", "why": "String begins with exact prefix."},
{"rule": "exactly one tag", "status": "pass", "why": "Only 'furi=' found after prefix."},
{"rule": "single-string ≤255 octets", "status": "pass", "why": "Length within limit; single character-string."},
{"rule": "URI scheme allowlist", "status": "pass", "why": "Scheme is https (allowlisted)."},
{"rule": "at most one percent-decoding (furi only)", "status": "pass", "why": "Decoded %2D to '-' once; no further decoding applied."},
{"rule": "absolute URL required", "status": "pass", "why": "URI includes scheme and authority."},
{"rule": "aliases/wildcards cross-zone check", "status": "pass", "why": "No cross-zone detected."}
]
}
}
Example B — Invalid: disallowed scheme
{
"valid": false,
"errors": ["furi scheme not allowed"],
"record_info": {
"query_fqdn": "_for-sale.example.org.",
"final_fqdn": "_for-sale.example.org.",
"cross_zone": false,
"is_wildcard": false,
"alias_chain": []
},
"parsed": null,
"explanations": {
"summary": "Invalid: the URI uses a non-allowlisted scheme which is explicitly rejected for security.",
"rule_evaluations": [
{"rule": "prefix v=FORSALE1;", "status": "pass", "why": "Prefix is present."},
{"rule": "URI scheme allowlist", "status": "fail", "why": "Scheme 'javascript' is excluded."}
]
}
}
🧪 Example Resolver Output (now with validations)
{
"domain": "example.nl",
"queried_name": "_for-sale.example.nl",
"method": "doh",
"is_for_sale": true,
"records": [
"v=FORSALE1;furi=https://broker.example/offer?id=123%2D456"
],
"structured": {
"furi": ["https://broker.example/offer?id=123%2D456"]
},
"validations": [
{
"valid": true,
"errors": [],
"record_info": {
"query_fqdn": "_for-sale.example.nl.",
"final_fqdn": "_for-sale.example.nl.",
"cross_zone": false,
"is_wildcard": false,
"alias_chain": []
},
"parsed": {
"tag": "furi",
"value_raw": "https://broker.example/offer?id=123%2D456",
"value_norm": "https://broker.example/offer?id=123-456",
"extras": {
"uri": {
"scheme": "https",
"host_unicode": "broker.example",
"host_punycode": "broker.example",
"path": "/offer",
"query": "id=123-456"
},
"price": { "currency": null, "amount_decimal": null }
}
},
"explanations": { "summary": "Record is valid...", "rule_evaluations": [/* … */] }
}
]
}
🚀 Deployment
1) Clone and install
git clone https://github.com/<yourname>/mcp-forsale-server.git
cd mcp-forsale-server
npm install
# If you forked: ensure dependency "punycode" is present (used by validator for IDNA)
2) Develop locally
npm start
This runs the Worker at http://127.0.0.1:8788.
MCP endpoints:
- SSE:
http://127.0.0.1:8788/sse - HTTP:
http://127.0.0.1:8788/mcp
Test locally using MCP Inspector:
npx @modelcontextprotocol/inspector
# → open http://localhost:5173, connect to http://127.0.0.1:8788/sse
3) Deploy to Cloudflare
npx wrangler deploy
Public endpoints:
- SSE:
https://my-mcp-forsale.j78workers.workers.dev/sse - HTTP:
https://my-mcp-forsale.j78workers.workers.dev/mcp
🔍 Testing the Public Endpoint
Basic availability:
curl -i https://my-mcp-forsale.j78workers.workers.dev/sse
Interactive (recommended):
npx @modelcontextprotocol/inspector
# connect → https://my-mcp-forsale.j78workers.workers.dev/sse
CLI (lightweight adapter):
npx mcp-remote https://my-mcp-forsale.j78workers.workers.dev/sse
Inside the REPL:
/tools
/call check_for_sale_structured {"domain":"example.nl"}
⚙️ Configuration
(These can be added to wrangler.toml or Worker secrets if desired.)
1) Env: FORSALE_QUERY_METHOD
- Purpose: Preferred resolver:
"doh"or"resolver" - Default:
"doh"
2) Env: CORS_ALLOWED_ORIGINS
- Purpose: Optional list of allowed origins for browser MCP clients
- Default:
*
🧩 MCP Client Notes
- You cannot push a system prompt from an MCP server into the client’s LLM. This Worker enforces validation server-side and returns deterministic JSON, so clients don’t need to trust prompts.
Claude Desktop example
Create or edit ~/.config/Claude/claude_desktop_config.json (macOS/Linux) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"forSaleChecker": {
"command": "npx",
"args": ["mcp-remote", "https://my-mcp-forsale.j78workers.workers.dev/sse"]
}
}
}
Restart Claude Desktop. You’ll see tools:
check_for_sale, check_for_sale_structured, natural_language_check, resolve_and_validate_for_sale, validate_for_sale_txt.
If you later protect your Worker with a token or Cloudflare Access, add the appropriate headers in your proxy or use the client’s native remote-MCP configuration to supply Authorization.
🛡️ Security / Privacy
- No user data is stored.
- All DNS queries are public via DoH or your configured resolver.
- The Worker can be fronted by Cloudflare Access or require
Authorization: Bearer <token>.
🧑💻 Author
Jesse — Nijmegen, Netherlands
📄 License
MIT License © 2025 Jesse
This project builds on concepts described in the public IETF Internet-Draft draft-davids-forsalereg-11.
Example live instance
https://my-mcp-forsale.j78workers.workers.dev/sse
Accessible from any compliant MCP client. Happy hacking — and may your domains sell swiftly! 🪩