<div align="center">
# 🔮 Seer
**Domain intelligence at your fingertips.**
A high-performance, multi-interface domain utility suite — query WHOIS, RDAP, DNS, SSL, and more from the terminal, Python, REST API, or AI assistants.
[](https://github.com/TheZacillac/seer/actions/workflows/ci.yml)
[](https://crates.io/crates/seer-cli)
[%22&replace=%241&label=homebrew&logo=homebrew&logoColor=white&color=fbb040)](https://github.com/TheZacillac/homebrew-tap)
[](LICENSE)
[](https://www.rust-lang.org)
[](https://www.python.org)
[Features](#-features) · [Quick Start](#-quick-start) · [CLI Usage](#-cli-usage) · [Python](#-python-library) · [REST API](#-rest-api) · [MCP Server](#-mcp-server)
</div>
---
## ✨ Features
<table>
<tr>
<td width="50%">
**🔍 Lookups & Discovery**
- **Smart Lookup** — concurrent RDAP + WHOIS with fallback; a WHOIS-only
result is cross-checked against the live DNS delegation
- **WHOIS** — registrant, registrar, and expiration data
- **RDAP** — modern protocol for domains, IPs, and ASNs
- **Domain Info** — merged RDAP + WHOIS in a flat structure
- **Reverse DNS** — PTR lookups for IP addresses
- **TLD Info** — WHOIS server, RDAP endpoint, and registry
- **Subdomain Enumeration** — CT logs, with live/dead + takeover-risk classification and baseline diffing
- **Domain Availability** — check if a domain is registered
</td>
<td width="50%">
**🌐 DNS & Propagation**
- **DNS Resolution** — dig-style queries over 20 record types: status, CNAME chain, wildcard detection
- **DNS Trace** — follow the delegation down from the root servers (`+trace`)
- **Encrypted DNS** — DoT (`tls://`) and DoH (`https://`) transports
- **DNS Propagation** — 20 public resolvers across North America, Europe and Asia Pacific
- **DNS Monitoring** — track record changes over time
- **DNS Comparison** — compare records across two nameservers
- **DNSSEC Validation** — check DNSSEC configuration
- **NS Delegation Check** — parent delegation vs. zone NS, with lame-server probing
</td>
</tr>
<tr>
<td>
**🛡️ Security & Health**
- **Domain Status** — HTTP status, title, SSL, and expiration
- **SSL Chain Inspection** — full chain, SANs, key details, validity
- **CAA Policy** — who may issue certs, vs. actual issuer
- **Email Posture** — SPF, DMARC, MTA-STS, BIMI, and DANE checks
- **Look-alike Detection** — confusable/homoglyph domains
- **Registration Drift** — diff live registration vs. a baseline
- **Domain Watchlist** — monitor expiring certs and registrations, with optional webhook alerts
- **SSRF Protection** — blocks requests to private/reserved IPs
</td>
<td>
**⚡ Power Features**
- **Bulk Operations** — process domain lists with CSV export
- **Domain Diff** — side-by-side comparison of two domains
- **Field Extraction** — `--fields` (implies `--quiet`) for scriptable output
- **4 Output Formats** — human, JSON, YAML, markdown
- **Interactive REPL** — with tab completion and history
- **Environment Doctor** — one-shot config/DNS/WHOIS/RDAP diagnosis
- **Semantic Exit Codes** — for CI/CD scripting
- **Shell Completions** — bash, zsh, fish, PowerShell (plus man pages)
</td>
</tr>
</table>
### 🏗️ Four Interfaces, One Core
```
┌──────────────────────────────────────────────────────────────────┐
│ User Interfaces │
├─────────────┬─────────────┬──────────────┬───────────────────────┤
│ seer-cli │ seer-py │ seer-api │ seer-api │
│ Terminal │ Python │ REST API │ MCP Server │
└──────┬──────┴──────┬──────┴──────┬───────┴──────────┬────────────┘
│ │ │ │
│ └─────────────┼──────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ seer-core │
│ Core Rust Library │
├──────────┬────────┬──────┬────────┬──────┬──────┬────────────────┤
│ WHOIS │ RDAP │ DNS │ Status │ SSL │ Bulk │ Diff/Watch │
└──────────┴────────┴──────┴────────┴──────┴──────┴────────────────┘
```
---
## 🚀 Quick Start
### Install the CLI
**Homebrew** (macOS/Linux — prebuilt binary, no Rust toolchain required):
```bash
brew install TheZacillac/tap/seer
```
**Cargo** (builds from source):
```bash
cargo install seer-cli
```
Prebuilt binaries and shell/PowerShell installers for each release are also on
the [Releases page](https://github.com/TheZacillac/seer/releases).
### Run your first lookup
```bash
seer lookup example.com # Smart RDAP + WHOIS lookup
seer dig example.com MX # DNS query
seer status example.com # HTTP, SSL, and expiration check
seer ssl example.com # Full SSL chain inspection
seer # Launch interactive REPL
seer tui example.com # Launch the full-screen TUI
```
Using Seer from Rust code instead? See [Rust Library](#-rust-library).
> **Requirements:** Rust 1.89+ · Python 3.10+ (for Python bindings/API)
---
## 📦 Packages
| **seer-core** | Rust library | `cargo add seer-core` | Core library — all business logic |
| **seer-cli** | Rust binary | `brew install TheZacillac/tap/seer` or `cargo install seer-cli` | The `seer` command-line tool |
| **seer-py** | Python extension | `pip install ./seer-py` | Python bindings via PyO3 (`import seer`); PyPI (`domain-seer`) planned |
| **seer-api** | Python package | `pip install -e .` | REST API + MCP server |
<details>
<summary><b>seer-cli vs seer-core — which do I need?</b></summary>
| **What** | Executable binary | Rust library crate |
| **Install** | `brew install TheZacillac/tap/seer` or `cargo install seer-cli` | `cargo add seer-core` |
| **Use** | Run `seer` in your terminal | `use seer_core::*` in Rust code |
| **Provides** | Commands, REPL, formatted output | Structs, clients, async APIs |
| **Depends on** | seer-core internally | Nothing — it's the foundation |
</details>
---
## 💻 CLI Usage
### Interactive TUI
Launch the full-screen, keyboard-first terminal UI (Catppuccin Frappé, with a Latte light theme):
```bash
seer tui # start on the target prompt
seer tui example.com # look up a domain on launch
```
The **lens** sidebar covers every Seer capability, grouped, each pulling live data from the same `seer-core` engine as the CLI with full in-pane controls:
- **LOOKUP** — Overview · WHOIS · RDAP (Domain · IP · ASN) · Reverse DNS · Availability · TLD Info
- **DNS** — Records (+ custom nameserver) · DNSSEC · Compare (two resolvers) · Trace (from the root servers) · Propagation · Follow (live monitor)
- **SECURITY** — SSL / Cert · Status · Subdomains · HTTP Headers · Takeover
- **POWER** — Diff · Bulk (streaming + CSV export) · Watchlist · History
**Keys:** `j`/`k` move · `1`–`9` jump to a lens · `Tab` focus nav⇄pane · `[` `]` sub-tabs · `r` raw output (json/yaml/markdown; `PgUp`/`PgDn` scroll) · `y` copy · `/` look up a domain · `:` command · `?` help · `:q` quit. In-pane: switchers (TLD, nameserver, Compare resolvers, Bulk op) and editable fields (Diff's 2nd domain, Follow interval/count, Bulk file path).
Subdomains and Takeover query CT logs and probe hosts, so they run only when you press `↵` on them (or use their `:` command), never just by moving onto them. Removing a watched domain (`d`) and clearing history (`c`) ask for a second press.
**Commands (`:`):** `lookup`, `whois`, `rdap <domain|ip|AS####>`, `dig [@server] <name> [type] [+trace]`, `ssl`, `status`, `headers`, `takeover`, `reverse <ip>`, `tld <.tld>`, `compare <domain> <nsA> <nsB>`, `diff <a> <b>`, `set output <human|json|yaml|markdown>`, `theme <frappe|latte>`, `copy`, `q`.
The startup theme comes from `[tui] theme` in `~/.seer/config.toml` (`"frappe"` — the default — or `"latte"`; unknown names fall back to Frappé). Switch live with `:theme latte`.
### Command Mode
```bash
# Smart lookup (concurrent RDAP + WHOIS). When only WHOIS has the domain
# (no RDAP record), the parent zone's NS delegation is also queried and
# shown as "Nameservers (DNS)", with a note when WHOIS lags it.
seer lookup example.com
# Comprehensive domain info (merged RDAP + WHOIS)
seer info example.com
# WHOIS / RDAP
seer whois example.com
seer rdap example.com # Domain
seer rdap 8.8.8.8 # IP address
seer rdap AS15169 # ASN
# DNS queries (dig-style: arguments in any order)
seer dig example.com # A records (default)
seer dig example.com MX # Specific record type
seer dig example.com A AAAA MX # Several types, queried concurrently
seer dig example.com MX +short # Values only, one per line (or --short)
seer dig example.com ANY # Common types, fanned out ('*' works too)
seer dig -x 8.8.8.8 # Reverse lookup (PTR)
seer dig www.example.com +trace # Delegation walk from the root servers (or --trace)
seer dig @8.8.8.8 example.com # Custom nameserver (UDP), same as -s 8.8.8.8
seer dig @tls://1.1.1.1 example.com # DNS over TLS
seer dig example.com -s https://cloudflare-dns.com/dns-query # DNS over HTTPS
seer dig '*.example.com' # Wildcard record (quote it so the shell doesn't glob)
# DNS propagation & monitoring
seer prop example.com A
seer follow example.com 20 0.5 # 20 checks, 30s interval
seer follow example.com MX 10 1 --changes-only # arguments in any order
seer follow example.com AAAA @1.1.1.1
# DNSSEC & DNS comparison
seer dnssec example.com
seer compare example.com MX @8.8.8.8 @1.1.1.1 # any order; type defaults to A
seer delegation example.com # Parent delegation vs. zone NS + lame-server probe
# Domain health & SSL
seer status example.com
seer ssl example.com
seer caa example.com # CAA policy vs. actual cert issuer
seer posture example.com # SPF / DMARC / MTA-STS email posture
seer headers example.com # HTTP security headers + cookie flags, graded A+-F
# Security intel
seer confusables example.com # Look-alike / homoglyph domains
seer drift example.com --record # Diff registration vs. baseline, then update it
seer takeover example.com # Subdomain takeover scan (exit 1 on findings)
seer takeover example.com --host app.example.com # Check known hosts, skip CT lookup
# Reverse DNS
seer reverse 8.8.8.8
# Discovery
seer avail example.com
seer subdomains example.com
seer subdomains example.com --resolve # Resolve each name: live/dead + takeover risk
seer subdomains example.com --record # Store a CT-log baseline
seer subdomains example.com --diff # Exit 1 when new names appear (cron/CI)
seer tld .com
# Domain diff
seer diff example.com google.com
# Watchlist
seer watch add example.com www.example.com # several at once; www. is kept
seer watch list
seer --format json watch list
seer watch # Check all watched domains
seer watch --fail-on warning # Exit non-zero at warning severity (default: critical)
seer watch --webhook https://hooks.example.com/seer # Also POST the report as JSON
seer watch remove example.com
# Lookup history
seer history example.com
seer history --clear
# Bulk operations (with CSV export) — ops: lookup, whois, rdap, dig, prop,
# status, avail, info, ssl, caa, posture, confusables
seer bulk lookup domains.txt
seer bulk status domains.txt -o results.csv
seer bulk dig domains.txt MX
seer bulk ssl domains.txt
seer bulk posture domains.txt
# Environment diagnosis (config, DNS, WHOIS port 43, RDAP bootstrap HTTPS)
seer doctor
# Scriptable field extraction
seer --fields registrar lookup example.com # --fields implies --quiet
seer --fields certificate.issuer status example.com
seer --fields status,answers.name dig www.example.com
# Shell completions & man pages
seer completions bash >> ~/.bashrc
seer completions zsh >> ~/.zshrc
seer mangen ./man # Write seer.1 + one page per subcommand
```
`seer watch --webhook` (or `webhook_url` under `[watch]` in `~/.seer/config.toml` — the flag wins) POSTs the check-all report as JSON. Delivery is best-effort: a failed POST prints a stderr warning and never changes the exit code, and the URL passes the same SSRF guard as every other outbound request.
### DNS Queries
`seer dig` reports a query the way dig does: the response status, the header
flags, and the answer with any CNAME chain first, every record under its real
owner name. Like dig, it sends one query per record type straight to the
nameserver (the default upstream, Google Public DNS, unless `@server`/`-s`
names one) and shows the response exactly as the server sent it, header
flags included; the servers are asked in order, IPv4 first, over UDP and
again over TCP when the reply is truncated, and one that does not respond
passes the query to the next, within two DNS timeouts.
```
$ seer dig www.github.com
DNS A Records: www.github.com
─────────────────────────────
status: NOERROR flags: qr rd ra server: default time: 23 ms
Answer:
www.github.com 2944 CNAME github.com.
github.com 60 A 140.82.114.3
Note: DNS responses are not DNSSEC-validated
```
- **NXDOMAIN vs NODATA.** "Name does not exist (NXDOMAIN)" and "No AAAA
records (NODATA — the name exists)" are told apart, each shown with the
zone's SOA when the server sends it. Behind a CNAME chain the chain is
shown and the verdict is about its last target — "The CNAME target
gone.example.net. does not exist (NXDOMAIN)" for a dangling CNAME — or,
from a server that does not recurse, says it returned the CNAME without
following it. SERVFAIL, REFUSED and other response codes (EDNS extended
ones such as BADVERS included) are reported as the status, not as errors.
A referral — an `@server` that serves only a parent zone pointing at the
child zone's nameservers, with the `aa` flag clear — reads "No answer:
referral to <zone>", which says nothing about whether the name exists.
- **Special-use names.** `localhost`, `127.in-addr.arpa` (`-x 127.0.0.1`),
`invalid`, `onion` and the other RFC 6761 names are answered by seer's
resolver itself and never sent to a server; the result says so (`server:
none`, no flags and a note; `answered_locally: true` in JSON).
- **Wildcards.** For a name below its registrable domain (`www.example.com`,
not `example.com`), a random sibling (`seer-probe-….example.com`) is queried
alongside (never for a special-use name). When it resolves too, a note says
a wildcard answers there and whether this answer matches it (likely
wildcard-synthesized) or differs.
- **Record types.** `HTTPS` and `SVCB` (priority, target and parameters such
as `alpn`, `ipv4hint` and `ech`) and the DNSSEC key-rollover records `CDS`
and `CDNSKEY` are supported — see [DNS Record Types](#-dns-record-types).
`ANY` queries A, AAAA, CNAME, MX, NS, TXT, SOA, CAA, HTTPS, DS and DNSKEY
concurrently and merges the answers, since servers may answer a real `ANY`
query minimally (RFC 8482).
- **`+short` / `--short`** prints only the values, one per line (CNAME targets
first), and nothing when there is no answer. It ignores `--format` and
can't be combined with `-q`/`--fields`. A `+trace +short` that stopped
early prints why on stderr and exits `1`.
- **`+trace` / `--trace`** asks one server of each zone directly, from the
root servers down, and shows one hop per delegation level: the server and
address, the status, the referral or the answer, and any servers that
failed. A server that fails or gives a lame (empty, non-authoritative)
reply is passed over for the zone's next one, and each server is asked
over IPv4 first, then over IPv6 when this host has no IPv4 route. The
whole walk gets six DNS timeouts (30s by default) and otherwise stops with
the hops so far and an error. It takes one record type and no nameserver,
and reports a CNAME answer without following it (an NXDOMAIN beside a
CNAME, which the zone's server followed itself, names the target that
does not exist). Every server address is checked against private and
reserved ranges before it is queried.
- **Scripting.** `--format json` / `yaml` prints the result object (`name`,
`record_type`, `server`, `answered_locally`, `status`, `flags`, `answers`,
`authority`, `wildcard`, `query_time_ms`), or an array of them for several
types, and `--quiet --fields` picks values from it. When some of several
types fail, the rest still print, each failure is reported on stderr, and
the command exits `1`.
The REPL's `dig` takes the same arguments. `seer reverse <ip>` is unchanged:
it returns just the PTR records (a JSON list), where `seer dig -x <ip>`
returns the full result.
### Output Formats
```bash
seer --format human lookup example.com # Colored, human-readable (default)
seer --format json lookup example.com # JSON
seer --format yaml lookup example.com # YAML
seer --format markdown lookup example.com # Markdown table
```
### Exit Codes
Every command follows the same contract, so `$?` is safe to gate cron jobs and CI on:
| `0` | Success (and, for check commands, the check passed) |
| `1` | Operational error (network, parse, invalid input) — or a check command's negative result |
| `2` | Usage error (bad flags or arguments) |
Check commands exit `1` on a negative result even when the command itself ran fine:
| `seer status` | HTTP status is missing or non-2xx, the SSL cert is invalid or expires within 30 days, or the domain expires within 30 days |
| `seer avail` | The domain is **not** available (already registered) |
| `seer dnssec` | The zone's status is anything other than `signed` (i.e. `unsigned`, `partial`, or `misconfigured`) |
| `seer compare` | The two nameservers give different answers (response code, CNAME chain or record set) |
| `seer delegation` | The parent's delegation NS set and the zone's own NS RRset are out of sync, or any delegated server answers lamely |
| `seer takeover` | Any host is `vulnerable` or `potential` (a host that could not be checked is `inconclusive` and does not fail the scan) |
| `seer doctor` | Any check reports **FAIL**. `WARN` (degraded but usable, e.g. a malformed config file running on defaults) still exits `0` |
| `seer drift` | Material drift is found vs. the stored baseline (a first run with no baseline exits `0`) |
| `seer subdomains --diff` | New names appeared vs. the stored baseline (removals and a missing baseline exit `0`) |
| `seer watch` | Issues at or above the `--fail-on` threshold: `critical` (default) fails only on critical issues, `warning` fails on warnings too. `add`/`remove`/`list` exit `0` on success |
| `seer bulk` | **Every** domain in the batch failed — partial failures exit `0`, since per-row status is already in the CSV/JSON output |
`seer dig` is not a check command: as with dig, any answer the server gave
exits `0` — NXDOMAIN, NODATA, SERVFAIL and REFUSED included, and a `+trace`
that stopped early reports where. It exits `1` on invalid arguments, a
timeout or other failure to get an answer, when any of several record
types failed, or when a `+trace +short` stopped early (its values alone
could not say so).
All other commands exit `0` on success and `1` on error. With `--format json`, `yaml`, or `markdown`, errors are written to stderr in that format (e.g. `{"error": "..."}`) instead of colored prose.
### Interactive REPL
Launch by running `seer` with no arguments:
```
$ seer
seer> lookup example.com
seer> dig github.com MX
seer> dig @1.1.1.1 www.github.com A AAAA +short
seer> status cloudflare.com
seer> delegation example.com
seer> doctor
seer> set output json
seer> help
seer> exit
```
Features: command history (`~/.seer_history`), tab completion, loading spinners, persistent session state.
---
## 🐍 Python Library
```python
import seer
# Smart lookup
result = seer.lookup("example.com")
# WHOIS / RDAP
whois = seer.whois("example.com")
rdap = seer.rdap("8.8.8.8") # Auto-routes IP / ASN / domain (alias: rdap_auto)
rdap = seer.rdap_domain("example.com")
rdap = seer.rdap_ip("8.8.8.8")
rdap = seer.rdap_asn(15169)
# DNS
answer = seer.dig("example.com", record_type="MX", nameserver="tls://1.1.1.1")
records = answer["answers"] # Also: status, flags, authority, wildcard
trace = seer.dns_trace("www.example.com") # Hops from the root servers down (dig +trace)
propagation = seer.propagation("example.com", record_type="A")
delegation = seer.delegation("example.com") # Parent vs. zone NS + lameness
follow = seer.dns_follow("example.com", "A", iterations=3, interval_minutes=1.0)
seer.cancel_follow() # Stop a running dns_follow from another thread
types = seer.record_types() # The 20 supported record types
# Domain health & SSL
status = seer.status("example.com")
ssl = seer.ssl("example.com")
dnssec = seer.dnssec("example.com")
caa = seer.caa("example.com")
# Security intel
posture = seer.posture("example.com") # SPF / DMARC / MTA-STS
headers = seer.headers("example.com") # HTTP security headers, graded A+-F
takeover = seer.takeover("example.com") # Subdomain takeover scan
confusables = seer.confusables("example.com") # Look-alike domains
# Availability & info
available = seer.availability("example.com")
info = seer.info("example.com")
tld = seer.tld_info(".com") # WHOIS server, RDAP endpoint, registry
tlds = seer.all_tlds() # Every TLD seer knows about
# Comparison & enumeration
diff = seer.diff("example.com", "google.com")
comparison = seer.dns_compare("example.com", "A", "8.8.8.8", "1.1.1.1")
subdomains = seer.subdomains("example.com")
classified = seer.subdomains_classify("example.com") # live/dead + takeover risk
# Bulk operations
results = seer.bulk_lookup(["example.com", "google.com"], concurrency=10)
results = seer.bulk_status(["example.com", "google.com"])
results = seer.bulk_dig(["example.com", "google.com"], record_type="A")
results = seer.bulk_info(["example.com", "google.com"])
results = seer.bulk_ssl(["example.com", "google.com"])
results = seer.bulk_availability(["example.com", "google.com"])
results = seer.bulk_whois(["example.com", "google.com"])
results = seer.bulk_propagation(["example.com"], record_type="A", concurrency=5)
# bulk_* calls return one dict per domain:
# {"operation": {"type": ..., "domain": ...}, "success": bool,
# "data": ... or None, "error": str or None, "duration_ms": int}
# Every bulk_* call takes an optional keyword-only progress callback
results = seer.bulk_lookup(domains, progress=lambda done, total, domain: print(f"{done}/{total} {domain}"))
# SSRF helpers (used by seer-api before any user-supplied connect target)
seer.validate_public_host("example.com", 443) # ValueError on reserved/private addresses
seer.nameserver_target("tls://1.1.1.1") # ("1.1.1.1", 853); None if the spec is invalid
# Errors: ValueError (invalid input), TimeoutError, ConnectionError (WHOIS
# connect), or a seer.SeerError subclass (a RuntimeError): RateLimitedError,
# WhoisServerNotFoundError, DnsError, UpstreamError, LookupFailedError,
# ParseError, TlsError, ConfigError
try:
seer.whois("example.zz")
except seer.WhoisServerNotFoundError:
pass
```
<details>
<summary><b>Example: Check SSL Certificate</b></summary>
```python
status = seer.status("example.com")
if cert := status.get("certificate"):
print(f"SSL Valid: {cert['is_valid']}")
print(f"Expires: {cert['valid_until']}")
print(f"Days left: {cert['days_until_expiry']}")
```
</details>
---
## 🦀 Rust Library
```toml
[dependencies]
seer-core = "0.53"
tokio = { version = "1", features = ["full"] }
```
```rust
use seer_core::{SmartLookup, DnsResolver, RecordType, StatusClient};
#[tokio::main]
async fn main() -> seer_core::Result<()> {
// Smart lookup (RDAP → WHOIS fallback)
let lookup = SmartLookup::new();
let result = lookup.lookup("example.com").await?;
// DNS resolution
let resolver = DnsResolver::new();
let records = resolver.resolve("example.com", RecordType::MX, None).await?;
for record in records {
println!("{}: {}", record.record_type, record.data);
}
// The whole response, as dig reports it: status, flags, CNAME chain, ...
let answer = resolver.query("www.example.com", RecordType::A, None).await?;
println!("{} with {} answers", answer.status, answer.answers.len());
// Domain status check
let client = StatusClient::new();
let status = client.check("example.com").await?;
println!("HTTP: {:?}", status.http_status);
Ok(())
}
```
See [docs.rs/seer-core](https://docs.rs/seer-core) for the full API reference.
---
## 🌍 REST API
```bash
seer-api # Starts on http://127.0.0.1:8000 (loopback-only by default)
```
Each lookup is a `GET` endpoint (`/lookup/{domain}`, `/dns/{domain}/{record_type}`,
`/dns/trace/{domain}`, `/ssl/{domain}`, `/takeover/{domain}`, …). `lookup`,
`whois`, `dns`, `propagation`, `status`, `ssl`, `availability`, and `info`
also accept a `POST …/bulk` domain list, and all but `availability` and
`info` can stream bulk results as Server-Sent Events from `…/bulk/stream`.
The full endpoint table is in
[seer-api/README.md](seer-api/README.md#endpoints).
**Secure by default:** the server binds loopback only, refuses a non-loopback
`SEER_HOST` without `SEER_API_KEY`, serves `/docs` only when
`SEER_DOCS_ENABLED=true`, and refuses multi-worker runs on the in-memory
rate-limit store. See
[Deployment defaults](seer-api/README.md#deployment-defaults).
```bash
# Examples
curl http://localhost:8000/lookup/example.com
curl http://localhost:8000/dns/example.com/MX
curl "http://localhost:8000/dns/trace/www.example.com?record_type=AAAA"
curl -X POST http://localhost:8000/lookup/bulk \
-H "Content-Type: application/json" \
-d '{"domains": ["example.com", "google.com"]}'
```
API docs (when `SEER_DOCS_ENABLED=true`):
[Swagger UI](http://localhost:8000/docs) · [ReDoc](http://localhost:8000/redoc)
---
## 🤖 MCP Server
Integrate Seer with AI assistants via the [Model Context Protocol](https://modelcontextprotocol.io/). Two transports expose the same tools:
```bash
# Local subprocess transport (Claude Desktop, etc.)
seer-mcp # stdio
# Remote / web transport (mounted on the REST API)
seer-api # exposes POST /mcp using MCP Streamable HTTP
```
The Streamable HTTP endpoint runs stateless inside the existing `seer-api`
process and inherits `SEER_API_KEY` auth, request logging, and the body-size
cap. See [seer-api/README.md](seer-api/README.md#streamable-http-transport)
for the wire format.
Generate a random bearer token for the connector:
```bash
# Capture into a shell var
KEY=$(seer generate-key)
# Or eval directly into the current shell
eval "$(seer generate-key --export)"
SEER_API_KEY=$KEY SEER_HOST=0.0.0.0 seer-api
```
**32 tools:** one per lookup (`seer_lookup`, `seer_dig`, `seer_dns_trace`,
`seer_ssl`, `seer_takeover`, …) plus bulk variants (`seer_bulk_status`, …).
The full tool list and the Claude Desktop configuration are in
[seer-api/README.md](seer-api/README.md#available-tools).
---
## 📡 DNS Record Types
| `A` | IPv4 address | | `PTR` | Pointer record |
| `AAAA` | IPv6 address | | `SRV` | Service locator |
| `MX` | Mail exchange | | `NAPTR` | Naming authority pointer |
| `TXT` | Text records | | `DNSKEY` | DNSSEC public key |
| `NS` | Nameserver | | `DS` | Delegation signer |
| `SOA` | Start of authority | | `CDS` | Child DS (DNSSEC key rollover) |
| `CNAME` | Canonical name | | `CDNSKEY` | Child DNSKEY (DNSSEC key rollover) |
| `CAA` | CA authorization | | `SSHFP` | SSH key fingerprint |
| `HTTPS` | HTTPS service binding (ALPN, address hints, ECH) | | `TLSA` | DANE TLS association |
| `SVCB` | General service binding | | `ANY` | A, AAAA, CNAME, MX, NS, TXT, SOA, CAA, HTTPS, DS and DNSKEY, queried concurrently |
---
## 🌏 Global DNS Propagation Servers
Propagation checks query **30 nameservers** across **6 regions**:
| 🇺🇸 **North America** | Google `8.8.8.8` · Cloudflare `1.1.1.1` · OpenDNS · Quad9 · Level3 |
| 🇪🇺 **Europe** | DNS.Watch · Mullvad · dns0.eu · Yandex · UncensoredDNS |
| 🌏 **Asia Pacific** | AliDNS · 114DNS · Tencent DNSPod · TWNIC · HiNet |
| 🌎 **Latin America** | Claro Brasil · Telefonica Brasil · Antel Uruguay · Telmex · CenturyLink |
| 🌍 **Africa** | Liquid Telecom · SEACOM · Safaricom · MTN South Africa · Telecom Egypt |
| 🌐 **Middle East** | Etisalat UAE · STC Saudi · Bezeq Israel · Turk Telekom · Ooredoo Qatar |
---
## ⚙️ Configuration
### Environment Variables
| `RUST_LOG` | Logging level (`trace` / `debug` / `info` / `warn` / `error`) | — |
| `SEER_LOG_LEVEL` | API log level; `ARCANUM_LOG_LEVEL` takes precedence when set | `INFO` |
| `SEER_DOMAIN_ALLOWLIST` | Comma-separated allowlist restricting which domains may be queried | — |
| `SEER_HOST` | API bind host. Non-loopback requires `SEER_API_KEY` (without it, requests arriving on a non-loopback interface get a 503 however the server was started) | `127.0.0.1` |
| `SEER_PORT` | API bind port | `8000` |
| `SEER_API_KEY` | Bearer token required for all non-`/health` requests | — |
| `SEER_CORS_ORIGINS` | Comma-separated CORS origins for the REST API. Unset: no CORS headers, and without `SEER_API_KEY` a request from a non-loopback `Origin` is refused (403) | — |
| `SEER_DOCS_ENABLED` | Expose `/docs`, `/redoc`, `/openapi.json` | `false` |
| `SEER_METRICS_ENABLED` | Expose `/metrics` to non-loopback clients | `false` |
| `SEER_RATE_LIMIT` | Per-client limit for the MCP endpoint (`POST /mcp`) as `<count>/<period>`; `;`-separated limits are all enforced. REST routes keep their own fixed limits | `30/minute` |
| `SEER_RATE_LIMIT_STORAGE` | Rate-limit storage URI (e.g. `redis://host:6379`) | `memory://` |
| `SEER_REQUEST_TIMEOUT` | Per-request deadline (seconds) covering the SSRF pre-check, the core call and bulk SSE streams (slot wait included); on expiry the client gets a 504 (a stream: an `error` event). `0` disables | `0` |
| `SEER_DISPATCH_THREADS` | Max threads in the pool running blocking core calls (REST + `/mcp`) | `50` |
| `SEER_MAX_CONCURRENT_STREAMS` | Max in-flight bulk SSE stream jobs per worker process | `8` |
| `SEER_TRUST_PROXY` | Trust `X-Forwarded-For` from `SEER_TRUSTED_PROXY_IPS` | `false` |
| `SEER_TRUSTED_PROXY_IPS` | Comma-separated IPs allowed to set `X-Forwarded-For` | — |
| `SEER_MCP_ALLOWED_HOSTS` | Comma-separated `Host:` values for the MCP `/mcp` endpoint — setting this turns on DNS-rebinding protection | — |
| `SEER_MCP_ALLOWED_ORIGINS` | Comma-separated `Origin:` values for the MCP `/mcp` endpoint (browser hosts) | — |
| `WEB_CONCURRENCY` | Uvicorn worker count. `>1` requires a non-`memory://` store | `1` |
| `UVICORN_WORKERS` | Fallback worker count when `WEB_CONCURRENCY` is unset (same `>1` store rule) | `1` |
| `SEER_RELOAD` | Uvicorn auto-reload for the `seer-api` entry point (dev only) | `false` |
### Config File
Initialize a config file at `~/.seer/config.toml`:
```bash
seer config --init
```
Settings: default output format, nameserver (UDP, `tls://`, or `https://`),
per-protocol timeouts, bulk concurrency, rate-limit delay — all clamped to
safe ranges — plus a `[watch]` table (`webhook_url` for `seer watch` report
delivery) and a `[tui]` table (`theme = "frappe"` or `"latte"`). Applies to
the CLI and REPL; the TUI reads the output format and theme.
### Timeouts
| WHOIS | 15s |
| RDAP | 15s |
| DNS | 5s (2 retries) |
| HTTP / SSL | 10s |
| CT logs (subdomains) | 30s per request (`ct_secs`); each source at most 3 of them |
| Propagation | 15s |
### Bulk Operations
- **Input:** plain text (one domain per line) or CSV (first column)
- **Comments:** lines starting with `#` are skipped
- **Concurrency:** default 10, max 50
- **API limit:** max 100 domains per request
---
## 🔮 Roadmap
- **Scheduled Monitoring Daemon** — `seer monitor --config monitors.toml` as a background service with notifications (email, Slack, PagerDuty) for expiring domains, SSL certificates, and unexpected DNS changes. The natural evolution of `watch`, `follow`, and `status` into persistent monitoring.
---
## 🛠️ Development
### Building
```bash
cargo build --release # All Rust packages
cd seer-py && maturin develop --release # Python bindings
cd seer-api && pip install -e . # REST API + MCP server
```
### Testing
```bash
cargo test # All Rust tests
cargo test -p seer-core # Core library only
cd seer-api && pytest # Python API tests
RUST_LOG=debug cargo test # With debug logging
```
### Linting
```bash
cargo fmt --all -- --check # Format check
cargo clippy --workspace --all-targets -- -D warnings # Lint
cargo deny check # Supply-chain policy (advisories, licenses, sources)
```
### Project Structure
The four packages are listed under [Packages](#-packages); all business
logic lives in `seer-core`, and the other three are thin interfaces over it.
The annotated per-module map is in
[AGENTS.md → Layout](AGENTS.md#layout).
---
## 🔧 Technology Stack
| **Core** | Rust · Tokio · Reqwest (rustls) · Hickory-resolver (DNSSEC) · Serde · OpenTelemetry |
| **CLI** | Clap v4 · Rustyline · Indicatif · Colored · Crossterm |
| **Python** | PyO3 (ABI3) · FastAPI · Pydantic · MCP |
| **Data** | WHOIS servers from [WooMai/whois-servers](https://github.com/WooMai/whois-servers) · IANA RDAP bootstrap |
---
## 📄 License
MIT License — Copyright (c) 2026 Zac Roach
See [LICENSE](LICENSE) for the full text.