Skip to content
Agents & MCP
Skip navigation

Reference

Every tool, resource and prompt the MCP server exposes — arguments, batch shapes, response fields, the evidence model, and coverage.

Published Last updated

On this page (47)

Reference Documentation

Reference for everything the Whisper MCP server exposes: seven tools, all read-only, the resources and prompts behind them, the evidence model returned with every result, and example questions agents can answer. The query language is here too — the error envelope, the ten safety rules, self-correction and the callable procedures; Query language is the shorter read on what query does and when to reach for it. For client-by-client install instructions, OAuth scopes and data handling, see the Setup guide.

The graph behind the connector holds 3.8B stored nodes and 31.8B stored relationships, plus 8B more computed at query time — 7.5B nodes and 39.8B edges in total — with 12.7M threat-intel edges across 134 feed sources and 33 categories, over 42 node labels and 55 edge types. Roughly two thirds of those edge types are synthesized at query time by the virtual data plane rather than stored, in whole or in part — a distinction that matters less for volume: the computed edges are about a fifth of the total. The whisper://stats resource returns current totals, per-layer freshness and per-layer coverage, and is the source of truth for the totals and freshness on this page. Where a per-layer edge-type breakdown is needed, it is built from each layer's own observed, non-zero edge counts rather than from a layer's self-declared type list — a layer can compute an edge type without declaring it, so the observed count is the one to trust.

Read coverage before band. Only known-clean — coverage: known-clean. In coverage, no malicious evidence. licenses the word "clean"; no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at. means unknown, which is a different thing again; malicious-evidenced — coverage: malicious-evidenced. In coverage, with positive evidence of malice. and ambiguous — coverage: ambiguous. In coverage, and the evidence points both ways. mean there is evidence, whatever the band says. Full contract: Coverage — what we looked at.

The tool surface

Seven tools, all read-only. Every one of them is annotated readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true. The annotations are uniform, and that uniformity is the honest shape of this surface rather than a shortcut: nothing on this server writes to the graph, so there is no read/write distinction left to draw, and every tool reaches out to the graph engine (or the docs site, or the gallery) over the network rather than answering from a closed local model. A client turns those hints into permissions, so they say exactly what the surface does — reads, against a read-only, network-backed engine — and nothing more.

The surface does not vary. Every deployment advertises the same seven tools; there is no profile, tier, or setting that adds or removes one. tools/list is the contract, and it is the same contract wherever you connect.

ToolArgumentsBatch shapeReturnsEvidence
querycypher required; params?, format? (table | graph | compact), page? / pageSize?one query per call; a cypher string over 32,768 characters is refused up front; pageSize pages count-first rather than truncatingcolumns, rows, statistics{rowCount, executionTimeMs}. compact adds rowFormat:"arrays" and droppedNullColumns?; pageSize adds pagination{page, pageSize, totalCount, totalPages, hasMore}. An over-budget result is fitted rather than dropped: rows trimmed to what fits, truncated / budgetTruncated: true, plus a pagination continuation pointerevidence{rowCount, executionTimeMs, asOf, serverVersion, planTier, servedBy, cypher} and references on every call — there is no layersAvailable on query; it stays on explain_indicator and identify
explain_indicatorindicators[] required (indicator, a single string, is the accepted alias); detail? (auto | full | band)32 per call at full, 333 at band — a larger batch is refused up front, not truncated; auto takes full at 8 indicators or fewer{rows[], layersAvailable} — one row per indicator in input order, each echoing the detail it came from; level and coverage are present on every row, and a row the engine could not score reads level: UNSCORED, score: nullper row: factors[], sources[], a source tag and a coverage{} block
explain_schemalabel?, uppercase [A-Z][A-Z0-9_]{0,63}one label per call; no argument returns the whole catalogueno argument → {labels[{label, count, scale}], labelCount, edgeCount, hint}. With a label → {label, exists, count, scale, properties[], outboundEdges[], inboundEdges[], sampleTraversal{intent, cypher}, notes[]}— the entity card is the evidence
read_docsone of path (fetch), query (search), or nothing (list)index-relative paths only — never a caller-supplied URLlist → the index, each entry carrying path, title, section, summary, mdUrl; search → ranked matches[], top 15; fetch → {path, url, mdUrl, title, section, markdown, fetchedAt, cached}—
list_workflowskeyword?, persona?, task?, layer?, kind?every filter optional and combinable; no argument returns the whole catalogue, uncapped{workflows[{slug, kind, title, summary, description?, personas, task, layers, useCases?, inputs[], params[], stepCount, requiresCapability?, expectedOutput, outputKind?, icon?, docPath?}], count}—
run_workflowruns[{slug, input?, params?}] required; format?, profile?, output?one or more slugs, and one or more entities, per callper run: steps[], evidence[], complete, coverage{}, incompleteSteps[], warnings[], markdown?, truncations[], profileWarnings[], profile, derived, primaryQuery, totalLatencyMs, graph? — plus a top-level references{schema, cypherGuide, apiReference, slugsRun[]}full trail: one evidence entry per step, with its Cypher, rowCount and executionTimeMs
identifyhosts[] required; neighbourhood? (default true), depth? 1–6 (default 2), budget_ms? (default 3000, per arm)≤256 hosts — a larger batch is rejected, not truncated; 16 neighbourhood walks per call{rows, layersAvailable, neighbourhoodTruncated?} — host, vendor_id, canonical_name, is_canonical, confidence, category, roles, band, host_class, evidenceper-row evidence, plus arms on any neighbourhood block

input carries the entity; params carries the settings. list_workflows publishes this per field: every entry in a workflow's inputs[] has a name and a passAs of either "input" or "params". Read passAs and place the value there. A passAs: "input" field sent inside params is not a validation error — the workflow runs, against a default it was never given.

The read-only guarantee is absolute, and it is the headline property of this surface. query is the only place a caller supplies Cypher, and a read-only pre-check runs ahead of the ten safety rules — including under an EXPLAIN prefix — rejecting every write and admin clause (CREATE, MERGE, DELETE, SET, REMOVE, FOREACH, LOAD CSV) and every mutating or admin CALL procedure. run_workflow takes a slug and parameter values, never Cypher, and the gallery behind it carries read steps only. There is no tool here that writes to the graph, under any scope or deployment.

The awkward case is worth stating, because it is what makes the guarantee true rather than merely asserted: whisper.submit and whisper.watch are live procedures on the graph engine. Neither is reachable through this server — no tool calls them, and the pre-check denies them by name. There is no contribution path on this surface at all.

The schema and the docs are discovered on demand rather than dumped into the model's context up front.

The seven tools

query

Runs an arbitrary read-only Cypher query and returns columns, rows, statistics, plus an evidence block and a top-level references object.

ArgumentTypeNotes
cypherstring, requiredValidated and possibly auto-corrected before it runs — see Query language. A string longer than 32,768 characters is refused ahead of everything else
paramsobjectBinds to $name placeholders. Bind rather than concatenate: safer, and it keeps the plan cache warm
formattable | graph | compacttable (default) → columns + rows. graph → nodes + edges, for results that return whole paths. compact → array-of-arrays rows, plus rowFormat:"arrays" and droppedNullColumns?, to minimise tokens
page / pageSizeintCount-first pagination: the first page returns the total row count alongside the slice, so an agent knows how many pages exist before walking them

Every response also carries the self-correction fields where they apply (autoLimited, rewritten, fix, truncated, budgetTruncated) and an evidence block: rowCount, executionTimeMs, asOf, serverVersion, planTier, servedBy (the graph instance that answered) and the Cypher that actually ran — present on every call, whether or not the engine rewrote anything, so a cited result is readable on its own without a caller having to remember what it originally sent. A result that would exceed the response budget is fitted rather than dropped — rows are trimmed to what fits, truncated and budgetTruncated are set, and a pagination continuation pointer is attached. Narrow the request (a tighter LIMIT, a pageSize, format: "compact") and continue from there.

The input side is bounded too. cypher text longer than 32,768 characters is rejected before validation begins. The ceiling sits far above any query written by hand — it is there for a query assembled by concatenation, where a generator can produce a body no engine should be asked to plan. Error model covers what a rejection looks like.

explain_indicator

Threat assessment for one or more indicators — IPv4, IPv6, hostname, CIDR or ASN, mixed freely in one call.

ArgumentTypeNotes
indicatorsstring[]The primary form. indicator (a single string) is the accepted singular alias
detailauto | full | bandfull scores each indicator and is the only mode that serves IPs, CIDRs and ASNs — 32 per call. band returns a coarse band per hostname for breadth — 333 per call. auto (default) picks full at 8 indicators or fewer, band above; a non-hostname indicator always takes full

A full row carries score, level (UNSCORED, then NONE up to CRITICAL), explanation, factors[], sources[], a source tag (live-explain / node-cache / unavailable), and a coverage block whose dataCoverage is always present — unknown never reads as clean. Each sources[] entry names its feed category and a threatCategory flag, and the row carries threatFeedCount and nonThreatFeedCount, so a roster or compliance listing (popularity, VPN, Tor, proxy, ad-tracking, sanctions) is not counted as a threat. Every advisory is also spelled out in explanation, and a row listed in a threat feed never reads "No known risk". On a hostname the row also carries band, plus verdictDisagreement when level and band disagree. An ASN row adds a breakdown{} of composite sub-scores.

A band row carries host, band, label, sub_labels, signals, coverage, evidence[] — and deliberately no score or level. Every row echoes the detail it came from, so a band row is never mistaken for a scored one.

Why band stops at 333. The number is derived, not arbitrary: it is the response budget divided by a worst-case band row, so a full batch is guaranteed to fit in one response. That is why it sits well below the figure the graph engine's own whisper.assess procedure accepts — that procedure is not reachable through this connector, and the budget is what binds here. A batch over the cap comes back as a typed refusal row naming the limit, before anything is scored; nothing is silently truncated, so a caller never receives a partial answer shaped like a whole one. Split larger sweeps into successive calls.

factors[] is the arithmetic, not a summary of it: ["Listed in 6 source(s) with combined weight 6.00", "Base score: 6.00 × log₂(6 + 1) = 16.84", "Recency boost: ×1.2", "Age boost: ×1.05", "Final score: … = 21.32"]. You can check the number rather than trusting it.

On a CIDR or an ASN, score is an aggregate over the whole range — listed addresses, threat density, inherited subnet scores — and factors[] shows the arithmetic, so read both. When the engine has evidence but no aggregate to report, the row says so rather than reading clean: score: null, level: UNSCORED, scoreUnavailable: true, with recoveredScore only when the recovered number is on score's own scale. An ASN's reputation composite travels separately as reputation{value, scale, direction, category} — a 0–100 scale where higher is better, never a threat score — and verdictDisagreement states the contradiction when two fields disagree. A low or missing score on a CIDR or ASN is not a clean network on its own. On an IP or a hostname, score is the value.

Keep full batches small when every row must be scored promptly. A batch is scored a few indicators at a time so each row's coverage enrichment completes; band is the mode built for breadth.

explain_schema

The schema, on demand, cached server-side and returning in milliseconds.

  • No argument → the label catalogue: {labels: [{label, count, scale}], labelCount, edgeCount, hint}.
  • With a label (uppercase [A-Z][A-Z0-9_]{0,63}) → the entity card: {label, exists, count, scale, properties[], outboundEdges[], inboundEdges[], sampleTraversal{intent, cypher}, notes[]}.

Call it before writing a query when you are unsure whether the canonical property is h.name or h.fqdn, or whether to anchor on HOSTNAME or DOMAIN (there is no DOMAIN label — only HOSTNAME). It is the cheapest way to avoid the most common query bug: a defensive MATCH (h) WHERE h:HOSTNAME OR h:DOMAIN OR h:FQDN … against a billion-node label, which the validator rejects.

text
User:     What labels does the Whisper graph have?
Agent:    [calls explain_schema]                 (no argument)
          → {labels: [{label: "HOSTNAME", count: 2,790,530,429, scale: "2.8B"},
                      {label: "IPV4", count: 622,191,309, scale: "622M"},
                      {label: "EMAIL", count: 237,065,663, scale: "237M"},
                      ... 38 more], labelCount: 42, edgeCount: 55}
Agent:    The graph has 42 labels. The biggest are HOSTNAME (2.8B), IPV4 (622M), and
          EMAIL (237M). What do you want to look at?

User:     Show me the threat properties on a hostname.
Agent:    [calls explain_schema {label: "HOSTNAME"}]
          → {label: "HOSTNAME", exists: true, count: 2,790,530,429, scale: "2.8B",
             properties: [{name: "name", type: "String"},
                          {name: "threatScore", type: "Double"},
                          {name: "threatLevel", type: "String"},
                          {name: "isThreat", type: "Boolean"},
                          {name: "isC2", type: "Boolean"}, ...],
             outboundEdges: [{type: "RESOLVES_TO", to: ["IPV4", "IPV6"], pattern: "(h:HOSTNAME)-[:RESOLVES_TO]->(:IPV4)"},
                             {type: "CHILD_OF", to: ["HOSTNAME", "TLD"]}, ...],
             inboundEdges:  [{type: "NAMESERVER_FOR", from: ["HOSTNAME"]},
                             {type: "MAIL_FOR", from: ["HOSTNAME"]}, ...],
             sampleTraversal: {intent: "...", cypher: 'MATCH (h:HOSTNAME {name: "example.com"}) RETURN h LIMIT 1'}}

read_docs

Pulls the Whisper docs (whisper.security/docs/**) on demand, so the Cypher reference and the cookbook stay out of the always-on context. Three modes:

CallReturns
no argumentthe section map: {sections: [{section, pageCount, pages[]}], count, source}
{query: "cypher"}ranked search over path, title, section and summary — top 15. It does not index page bodies, so an empty result means no page is titled that, not that the topic is undocumented
{path: "whisper-graph/schema"}that page as markdown: {path, url, mdUrl, title, section, markdown, fetchedAt, cached}

Paths are index-relative, never a caller-supplied URL — that is the SSRF guard. Every doc page is also served as raw markdown at its URL plus .md, surfaced as mdUrl on each entry. Markdown is cached six hours; the catalogue is rebuilt from the live sitemap, so it always reflects what is actually published. Resolve a path with list or search rather than hardcoding one.

list_workflows

Searches the shared workflow and recipe gallery and returns each item's summary plus its complete parameter space. Filters — all optional, all combinable: keyword, persona, task, layer, kind (workflow | recipe). With no arguments it returns the entire catalogue, uncapped.

Each entry: {slug, kind, title, summary, description?, personas, task, layers, useCases?, inputs[], params[], stepCount, requiresCapability?, expectedOutput, outputKind?, icon?, docPath?}. Because every dial is described (kind / options / min / max / default), an agent can run any variant rather than only the default. See the Workflow gallery.

run_workflow

Runs one or more gallery workflows by slug in a single call — a multi-step investigation collapsed into one tool call.

ArgumentTypeNotes
runs[{slug, input?, params?}], requiredMultiple workflows and multiple entities in one call. An empty array is rejected. Out-of-range params are coerced, never rejected, and the effective values echo back in paramValues
formatcompact | table | graphcompact (default) is token-minimized; table/graph return per-step rows
profileconsole | website | mcp | rawDefault mcp. A top-level sibling of output, never nested inside it
output{emit?, slices?}An override applied on top of profile — sending it at all replaces the profile's slice list and overrides its emit; it does not merge. Omit it entirely on the default path or you lose the profile's markdown

Under the default profile: "mcp" the result carries a ready, budgeted markdown report — verdict, ## Findings, correlation tables, and a numbered ## Evidence appendix mapping each [n] citation to its fact, step id and exact Cypher — as results[].markdown. Relay it verbatim rather than re-summarizing it.

Alongside it: steps[], evidence[], complete, a coverage map, incompleteSteps[], warnings[], truncations[] (what the budget dropped — never silent), profileWarnings[], derived, primaryQuery and totalLatencyMs.

The coverage map is computed over the workflow's declared step list, so a step that never reported back shows as skipped rather than vanishing. If the entity arrives inside params under the input's own name it is still used and inputSource says so; a key that matches no declared param comes back in ignoredParams rather than being dropped. Each run stays inside a wall-clock budget; on expiry it returns a successful partial — success: true, partial: true, and guidance in warnings[] — never a hang.

Two gallery workflows are not advertised as prompts yet, and they behave differently here. indicator has a fixed step count and completes inside the budget, so it runs like any other slug. attack-surface cannot finish inside a tool call on any input and is refused up front instead of started: {slug, success: true, notRun: true, reason, howToNarrow} — a successful, actionable refusal, not an error.

identify

Who runs a set of hostnames — vendor and role attribution, not a threat verdict. A host on AWS is not malicious because AWS also hosts malware; use explain_indicator for the verdict.

ArgumentTypeNotes
hostsstring[], requiredHostnames, not IPs. ≤256 — a larger batch is rejected, not truncated
neighbourhoodboolDefault true. Walks the structural neighbourhood of any host the atlas cannot attribute
depthint 1–6Default 2, clamped server-side
budget_msintDefault 3000, per arm. On expiry the block returns partial data with arms.deadline_hit:true, never an error

One row per host in input order: host, vendor_id, canonical_name, is_canonical, confidence, category, roles, band, host_class, evidence. Roles are things like DNS_OPERATOR, MAIL_RECEIVER, ORIGIN_AS.

The attribution bands, strongest first, with the confidence each carries:

Bandconfidencevendor_idis_canonicalWhat it means
DIRECTabove the DERIVED rangesettrueA direct match on the host itself.
DERIVED0.70–0.89, a rangesettrueReached by traversal — RESOLVES_TO -> IPV4 -> DELEGATED_TO -> VENDOR — the normal answer for a host that resolves to a known vendor's address space.
HEURISTIC0.4nullfalseA resemblance, not an attribution — usually a single weak signal, an origin-AS organisation name with nothing to reconcile it against. Treat it as a lead to corroborate, not a fact to act on.
UNKNOWN0.0nullnullNo match. Every other column is null. A populated row, not an error and not an empty result — one unknown host never fails the batch.

Read canonical_name together with is_canonical. When is_canonical is false, canonical_name names the nearest known vendor by that one weak signal — not this host's operator — and vendor_id stays null because nothing was reconciled. Live, 2026-09-03: identify({hosts: ["www.apple.com"]}) returns canonical_name: "akamai", is_canonical: false, vendor_id: null, band: "HEURISTIC", confidence: 0.4 — Apple's own domain, attributed to Akamai on nothing but an ORIGIN_AS organisation match, because Apple's edge happens to route through Akamai's address space at query time. Reading canonical_name alone and reporting "apple.com is run by Akamai" would be wrong; is_canonical: false is the tool saying so.

A novel host degrades to band:"UNKNOWN" / confidence:0.0 — it never fails the batch — and that row also gains a neighbourhood{no_atlas_match, nearest_known_vendors, siblings, coverage, arms} block placing it structurally. Only novel hosts are walked, and only the first 16 per call; the rest carry neighbourhood.skipped:"fallback-cap" alongside a top-level neighbourhoodTruncated:true.

Check arms.deadline_hit and arms_truncated before reading an empty siblings list as "no neighbours".

This is the tool for an estate-shaped question — a vendor inventory, an egress log, a third-party-risk CSV — rather than a single-indicator one.

Error model

query returns a typed error envelope that clients and LLMs can branch on instead of parsing free text.

FieldMeaning
successtrue on success, false on error
errorHuman-readable error message
suggestionA concrete fix the agent can apply and retry — always set on failure
errorCodeMachine-readable code (below)
retryableWhether re-running the same query could succeed — read straight off the code

There are eleven codes:

CodeCauseRetryable
SCHEMA_ERRORBad label, property, relationship type, or column nameNo — fix the query
SYNTAX_ERRORThe Cypher itself is malformed (bad token, unknown function)No — fix the query
LIMIT_ERRORLIMIT is missing or malformed (usually carries a CLAMP_LIMIT fix)No — fix the query
VALIDATION_REJECTEDThe query failed one of the ten safety rules — or the 32,768-character input cap that runs ahead of them, which carries a SHORTEN_QUERY fixNo — fix the query
QUERY_TOO_EXPENSIVEStopped for size, not duration — the engine's element/row budget, or the cost gate classifying the plan as DECOMPOSENo — narrow it
QUERY_UNSERVABLEThe engine refused to plan the shape at all — an unanchored full-label scan, a global edge count, or an unanchored traversal of a query-time edgeNo — reshape it
DEPTH_EXCEEDEDThe engine refused the traversal for its length, not its syntax. The Cypher is valid: shorten it, or read whisper://quota for what your key allows. It is never an authentication problem — an unrecognised key is rejected with a 401 before any tool runsNo — shorten it
DB_TIMEOUTThe query ran past its time budgetYes — narrow it
RATE_LIMITEDThe graph engine declined to serve the query at that momentYes
ENGINE_ERRORThe engine faulted while serving the query; the response carries a request idYes
DB_UNAVAILABLEThe graph database is unreachableYes

QUERY_TOO_EXPENSIVE is the size guard, distinct from DB_TIMEOUT's duration guard — a query can be cheap per row and still touch too many elements.

There is no CYPHER_SYNTAX_ERROR code, and there never has been on this server. A syntax problem classifies as SCHEMA_ERROR (bad label / property / column) or SYNTAX_ERROR (malformed Cypher), and a LIMIT the engine will not serve as LIMIT_ERROR. If your client branches on CYPHER_SYNTAX_ERROR, that branch is dead code.

The MCP server does not throttle. A failure at its own layer is 401 (auth failure) or 503 (auth backend degraded); anything else reached you from the graph engine underneath it.

The ten safety rules

The validator runs them in order; the first failure wins; an EXPLAIN <query> is exempt. String literals, // and /* */ comments, and backtick-quoted identifiers are stripped before matching, so a value or a comment can never trip a rule.

Ahead of all ten sits the 32,768-character input cap on the raw query text. It is not one of the rules and nothing exempts it — an EXPLAIN is measured the same way, and so is a body that is mostly comment, because the cap is applied before anything is stripped. Over-length comes back as VALIDATION_REJECTED with a SHORTEN_QUERY fix; split the query into smaller anchored steps.

#RuleRejectsAllows
1Shortest-path boundunbounded shortestPath / allShortestPathsbounded [*1..N] — an unbounded [*] returns a BOUND_PATH fix proposing [*1..6]
2Limit clampnothing — this rule never rejectsany LIMIT; one the engine will not serve is auto-clamped, and the query runs
3Unlabeled matchstandalone MATCH (h) with no label or anchora relationship traversal, or a {name: …} anchor
4Label disjunctionsame-variable WHERE n:A OR n:Bthe label-pipe form (n:A|B), or a property OR
5Unanchored virtual-edge scana fixed-length hop across a query-time edge with both endpoints bare — the engine cannot serve it at alleither endpoint labelled, anchored, or bound by an earlier clause; any [*1..N] form
6Untyped prefix expansiona fixed-length, untyped outgoing expansion from an ANNOUNCED_PREFIX or REGISTERED_PREFIX anchor — a shape the engine cannot servea typed relationship (-[r:ROUTES]->), an expansion into the anchor (<-[r]-), any [*1..N] form, or a PREFIX-labelled anchor
7Unindexed text opCONTAINS / STARTS WITH / ENDS WITH on a property other than .nametext ops on .name; = on any property
8Unanchored label scanan unanchored scan of a large label; plus FEED_SOURCE / CATEGORY at any sizesmall labels, traversals, indexed WHERE, aggregations
9id() ordering comparisonid(n) > x and the other three ordering operators — id() returns a String, so the predicate evaluates to null and the query succeeds while matching nothingid(a) = id(b), <>, ordering on ordinary properties
10Limit requiredan exploration query with no LIMITaggregations, {name: …}/.name = anchored queries, EXPLAIN — and a missing LIMIT is auto-injected rather than rejected

Two further guards run alongside the rules rather than as rules, because they depend on the bound parameters rather than the query text: a null LIMIT/SKIP parameter (which the engine reads as unbounded, silently) and an UNWIND over five or more values feeding a CALL {} subquery. Both return a typed error with a fix.

The cost gate

A query can pass all ten rules and still be a blow-up — a Cartesian product from accidentally-disconnected patterns, or an unbounded variable-length expansion off a huge label scan, only shows up in the plan. The gate fetches that plan (an EXPLAIN; nothing executes) and classifies it:

VerdictTriggerOutcome
SAFEindexed lookup, no risky operator, or the plan is unavailable (fail-open)executes normally
PAGINATEa scan of a large label — big but cheap and streamingexecutes normally; page the output
DECOMPOSEa Cartesian product over a large label scan, or a variable-length expansion rooted at one of the billion-scale labelsrejected as QUERY_TOO_EXPENSIVE with the plan-derived reason

Only DECOMPOSE is rejected, because paging cannot help: the engine materializes the join or expansion before any LIMIT trims it. Rewrite it — anchor a node, connect the patterns, or stage the traversal. The gate is fail-open end to end, so it can only add a rejection on top of the ten rules, never silently drop a query that would otherwise have run.

Self-correcting queries

The validator does more than reject. Where it safely can, it bounds or rewrites the query, runs it anyway, and tells you what it did.

FieldWhen it's setWhat it means
autoLimited: trueYou omitted LIMIT on an exploration queryThe server appended the default LIMIT and ran it. rewrite holds the original and effective Cypher.
rewritten: trueThe server safely auto-corrected and ran the corrected formBounding rewrites that only narrow the result (CLAMP_LIMIT), and confident schema-driven corrections (TYPE_NORMALIZED).
fixA rule failed and the correction would change which rows matchReturned for the agent to apply — never auto-run. {kind, rewrittenCypher?, confidence, safeToAutoRetry}.
truncated: trueThe server returned a bounded prefix rather than the whole resultA partial result that looks complete unless you read this field.
advisories[]The run succeeded but the engine has something to sayFor example projection-verdict-omitted, which says a whole-node projection left the reconciled verdict fields out and tells you how to ask for them.
engineSuggestions[]On failureThe engine's own remediation list. An entry whose rewrite is runnable Cypher is also promoted into fix; an illustrative one is relayed but not offered as executable.

Two classes of correction. Bounding rewrites run automatically — they trim the result to a bounded prefix, same rows, just fewer of them. Semantic rewrites come back as a fix and are never auto-run, because they change which rows match: EXACT_MATCH (an unindexed CONTAINS → =), BOUND_PATH, and ADD_LABEL / PICK_LABEL / ANCHOR_MATCH, which carry no rewrittenCypher because they need a value only you have.

No validator rejection is ever a bare error. It is auto-fixed, auto-bounded, or it carries a fix.

Traversal landmines

These return a wrong answer rather than an error, which makes them worth more attention than the rules above.

Edge direction

Four edges point the opposite way to intuition, and traversing them backwards returns an empty result rather than an error:

  • RESOLVES_TO is forward only, HOSTNAME → IPV4. Reverse DNS is (ip)<-[:RESOLVES_TO]-(h).
  • NAMESERVER_FOR and MAIL_FOR point server → domain. A domain's MX is (domain)<-[:MAIL_FOR]-(mx).
  • LOCATED_IN is IPV4|IPV6 → CITY only. For the country, chain HAS_COUNTRY; an IPV4 → COUNTRY hop returns nothing.
  • CHILD_OF runs child → parent (HOSTNAME → HOSTNAME → TLD).

Anchor feeds and categories on .id

.name carries a display form that varies, so a filter on it can silently return nothing. Filter and project on .id, the stable slug (c2, tor, ad-tracking). And never scan FEED_SOURCE or CATEGORY directly — rule 8 rejects it at any size; reach them via LISTED_IN from an anchored node.

CONTAINS at scale

STARTS WITH and ENDS WITH on .name are indexed and fast at any scale. CONTAINS on .name is allowed, but with no LIMIT or a low-selectivity substring it degrades to a whole-label scan and hits the execution deadline. On ASN.name specifically it times out — use STARTS WITH "AS…". Pair an unanchored CONTAINS with an anchoring predicate.

Virtual edges need one anchored endpoint

Most of the 55 edge types, roughly two thirds, are synthesized at query time in whole or in part — including ROUTES, HAS_NAME, CONFLICTS_WITH, ANNOUNCED_BY, LISTED_IN, TAGGED_AS, BGP_PATH, HAS_COUNTRY, LOCATED_IN, and the physical-infrastructure and RPKI edges. They traverse normally, including inside [*1..N], but a fully bare MATCH (a)-[:TYPE]->(b) is rejected (rule 5) rather than returned empty.

Labels that exist but have no edges

DNS_ROOT_INSTANCE, DWI_DOMAIN and RIR are node-only: a traversal from them returns zero rows because there is nothing to traverse, not because your pattern is wrong. RIR in particular is listable and correct — its five nodes are AFRINIC, APNIC, ARIN, LACNIC and RIPENCC — and still unjoinable: no edge reaches it, so you cannot walk from a prefix or an ASN to its registry. Read the registry off the node you already have instead: ASN carries it as autNumSourceRir (AS13335 → ARIN). RDAP_ENTITY used to be on this list and no longer is: a prefix or an ASN reaches its registrant handle through REGISTERED_TO_ENTITY.

Procedures callable inside query

Beyond the dedicated tools, query accepts these procedures directly inside Cypher, so you can compose them into a larger query.

ProcedureWhat it does
CALL explain("indicator")Threat assessment for an IP / hostname / ASN / CIDR — also the explain_indicator tool
CALL whisper.history("indicator")Historical WHOIS / BGP snapshots. whisper.history.whois(...) and whisper.history.bgp(...) take one arm at a time
CALL whisper.variants("name" [, "LABEL"] [, checkExisting])Typosquat / lookalike variant generation
CALL whisper.identify($hosts)Host identity — vendor, canonical name, host_class
CALL whisper.assess($hosts)Coverage-qualified verdict per host — a list or a single string; a URL folds to its host. Columns host, label, band, sub_labels, signals, coverage, evidence, verdictScore
CALL whisper.walk($host, $depth, $budgetMs)Structural neighbourhood when identify has no direct match
CALL whisper.origins($domain)Candidate true-origin IPs behind a CDN or proxy, each with a 0–1 confidence and the methods that found it
CALL whisper.enrich($indicators)Owner, country, ASN, band and prevalence for a list of hosts or IPs in one call — rows are keyed by name, not by input position
CALL whisper.resolve("host")The A and AAAA records the graph holds for a host, with the freshest observation time and a coverage value
CALL whisper.danglingCname($hosts)CNAME targets whose apex is unregistered — the subdomain-takeover check; zero rows on a clean host
CALL whisper.cve.byPackage("cpe:2.3:…") · whisper.vulnPosture(...)Known CVEs for a CPE 2.3 package spec, and a one-row exposure roll-up for a CVE list, a package spec or an ASN
CALL whisper.export({label: "malicious", limit: 1000})Paged bulk export of a verdict tier (malicious, ambiguous — coverage: ambiguous. In coverage, and the evidence points both ways., benign-allowlisted); continue with the returned next_cursor
CALL whisper.asnThreatDensity("AS…") · whisper.asnCountries(n) · whisper.bgpDegreeDistribution()Listed-IP density for one network, ASN counts per country, and the peering-degree histogram
CALL whisper.version()Version and build time of the engine that answered — the cheapest liveness probe
CALL whisper.search("token")Bounded, type-aware lookup of an unclassified token (IP / host / ASN / CIDR / prefix / suffix) instead of an unanchored scan
CALL whisper.explain.bundle(...)Several explain arms in one round-trip
CALL whisper.lookupTorRelay(...) · whisper.lookupTlsFingerprint(...)Tor-relay and TLS-fingerprint lookups
CALL whisper.psl.tldPlusOne(...) · whisper.psl.isPublicSuffix(...)Public-suffix arithmetic on a hostname
CALL whisper.topAsnsByPrefixCount(...)Ranked ASNs by announced-prefix count
CALL whisper.audit.malformedHostnames(...)Data-quality sweep for malformed hostnames
CALL whisper.quota()The caller's own service context as key/value rows — the same data the whisper://quota resource carries
CALL db.labels() · db.relationshipTypes() · db.schema("json")Schema introspection

whisper.assess and whisper.walk are procedures, not tools — their tool-level equivalents were folded into explain_indicator and identify, but the CALL form is still live and still the right way to compose them inside a larger query.

whisper.variants() also works in expression position, not only as a top-level CALL — RETURN size(whisper.variants("paypal.com")) works. explain() and whisper.history() are CALL-only.

For a packaged, multi-step version of any of these, run a gallery workflow with run_workflow.

Resources

The schema and the docs are discovered on demand through explain_schema and read_docs rather than always listed — that is what keeps the always-listed surface small. Four lightweight descriptors stay available as resources:

ResourceContents
whisper://schema/fullThe full schema reference: node labels with counts, edge types with directions, and the edge-direction landmines — also surfaced inline by explain_schema, so an agent rarely needs the raw resource
whisper://statsLive database statistics — nested node and edge counts (physical / virtual / total), the threatIntel block, and per-layer freshness: each query-time layer's lastRefresh, ageSeconds and coverage verdict (OK / DEGRADED / EMPTY). Layers refresh on different cadences, so a finding is as fresh as the layer it came from, not as the response
whisper://quotaThe graph engine's own read-only account descriptor for the caller, surfaced verbatim — the MCP server adds nothing of its own to it. Read it at the start of a session so an agent knows what it is working with rather than inferring it
whisper://serverServer / deployment descriptor: serverVersion, deploymentName, the live readyLayers[] (which capability layers are active here), and a schemaHash for drift detection

Resource annotations carry MCP audience / priority hints — schema/full at priority 1.0 (assistant), the three dynamic resources at 0.5 (user + assistant).

How current is this: whisper://stats per-layer freshness

Query-time (virtual data plane) layers refresh on different cadences — one may have run minutes ago, another hours ago — so a finding's age is the age of the layer it came from, not the age of the response you got it in. whisper://stats answers that per layer rather than once for the whole graph: each entry under vdp.layers[] carries coverage (OK / DEGRADED / EMPTY) and, where the engine timestamps that layer, lastRefresh and ageSeconds. Live, 2026-09-03: a DNS or threat-signal layer read coverage: "OK" with ageSeconds in the low thousands (minutes-to-hours old), while some physical-infrastructure layers (submarine cable, CDN PoP, cloud region) read coverage: "OK" with no lastRefresh or ageSeconds at all — those fields are omitted, never sent as 0, on a layer the engine does not timestamp. Read the absence as "not time-boxed", not "just refreshed".

Prompts

The connector advertises thirteen prompts (measured 2026-10-11), one per gallery workflow that is open to a one-click run, in the order prompts/list returns them. Each wraps a single run_workflow call, embeds that workflow's per-step plan, and ends with an evidence directive: support each finding with the per-step prompts, Cypher and row counts, and treat a coverage gap as a finding rather than a clean result.

Arguments come from the workflow's inputs; every prompt argument is optional, and the tunable params are listed in the prompt body — call list_workflows for their options and ranges.

PromptArgumentWhat it does
supply-chainvalueMap what a domain depends on — every external provider by function (DNS, mail, email delivery, hosting, physical), with the dependency chains and single-vendor (SPOF) signals
email-security-posturevalueSPF, DKIM, DMARC, MX and DNSSEC, graded on presence and hygiene, never on enforcement strength
nameserver-hijack-dns-consistencyvalueCheck a domain's name servers for the misconfigurations that enable DNS hijacking
xrayvalueEverything a domain stands on in one read: verdict, web, nameserver and mail strands, networks, subdomains, physical layer and look-alikes
threat-actor-attributionvalueFrom an indicator to a named actor, or from an actor name to their techniques and infrastructure — direct graph attribution, never a tag-derived guess
typosquatdomainRegistered look-alikes of a brand, scored for which are dangerous
build-takedown-evidence-packagedomainAssemble a ready-to-submit dossier for taking down a scam or phishing domain
infrastructure-mappingvalueAttribute one indicator to its true owner and enumerate the owned estate — apex, subdomains, other domains, networks and physical footprint — up to the vendor border
anycast-dns-root-sovereigntycountryA country's DNS-root resilience if it were cut off from the world
bgp-hijack-exposurevalueGrade a network's routing security and trace conflicts to the domains they'd expose
subdomain-takeovervalueFind subdomains pointing at abandoned services an attacker could claim
indicator-enrichmentvalueOne domain or IP into a full context card — owner, hosting, mail, location, reputation
route-healthtargetProfile a network or address block into a full routing and reachability health card

Fifteen workflows, thirteen prompts. attack-surface and indicator are withheld from the prompt surface for now — a one-click prompt is a stronger promise than a tool call. indicator runs to completion through run_workflow; attack-surface cannot finish inside a tool call and is refused up front with guidance on how to narrow it.

A prompt's depth depends on which capability layers are live on the deployment you are connected to — readyLayers[] in the whisper://server resource is the authoritative list.

The four Agent Skills

Agent Skills are client-side playbooks built on this surface: they choose which investigation to run and how to read what comes back. Four ship in the open-source repository.

SkillThe jobIt loads when you say
whisper-investigateTriage one indicator: pick the right workflow, read the coverage, stop when the question is answered"is this domain malicious", "who owns this IP", "map our attack surface", "can this subdomain be taken over"
whisper-bulk-triageA list from a SIEM, EDR or spreadsheet, ranked — with never-seen and check-failed kept out of the ranked table"here are 200 IPs from Splunk", "which of these matter", "triage this blocklist"
whisper-cypherCustom queries for the questions no workflow covers, written to pass the server's validator first time"write a query for…", "my query was rejected", "what edge direction do I use", "why did this return nothing"
whisper-brand-protectionLookalike domains, separated into weaponised, suspicious and merely registered — then a takedown package"find typosquats of our brand", "who is impersonating us", "build a takedown case"

Each loads on its own when your question matches. There is nothing to invoke.

Skills, workflows and prompts

Three layers of playbook ship with the connector, and they are complements rather than alternatives.

LayerWhere it livesHow you reach itBest for
Promptsthe connectortrigger one explicitly from your client's prompt menua quick one-off when you already know what you want
Workflow gallerythe serverlist_workflows to find one, run_workflow to run ita whole investigation in one call, with an evidence trail, no install
Agent Skillsyour clientinstall once; they load automaticallyrepeatable work where choosing the right investigation, and reading the result honestly, is most of the job

Reach for a gallery workflow when you know the investigation you want. Install a skill when you want the assistant to pick it — and the right follow-up — on its own.

Evidence and provenance

A Whisper answer carries its own provenance. Every query and run_workflow result ships an evidence block so an agent can cite the query and the rows behind each claim, rather than asking you to take a verdict on faith.

  • The evidence block lists, per step that ran, the exact Cypher executed, the rowCount it returned, and the executionTimeMs it took. On run_workflow there is one entry per step, plus a top-level references object and the coverage signals below. On query it records the (possibly self-corrected) Cypher that actually ran, plus the provenance a result needs to be reproduced later — asOf, serverVersion and servedBy, the graph instance that answered — because two people running the same Cypher can get different rows, and this is how they find out why.
  • Threat verdicts return {score, level, factors[], sources[]} plus a source tag. live-explain is fresh scoring. node-cache is the reconciled node verdict the server falls back to when live scoring is briefly down — a valid, labelled verdict, not an error. unavailable means neither path could answer.
  • Feed listings ride on LISTED_IN edges carrying firstSeen, lastSeen and weight, so an agent can tell a three-year-old sighting from a fresh one.
  • query results are typed data, not free-form text. Rows and graph projections come back as structured fields, so returned values — domain names, WHOIS strings, registrant fields, canonical_name — are data to reason over, never instructions to follow. Treat every returned value as untrusted input.

coverage means three different things

The word names three structurally different objects. Gate your logic on the right one.

WhereShapeValues
explain_indicator rowobject{granularity, scope, sharedHost, dataCoverage, advisories?, interpretation}. scope: "node-only" means the address itself — not its prefix and not its ASN
whisper.assess / whisper.assessUrlflat string enum, verdict axisknown-clean — coverage: known-clean. In coverage, no malicious evidence. · malicious-evidenced — coverage: malicious-evidenced. In coverage, with positive evidence of malice. · ambiguous — coverage: ambiguous. In coverage, and the evidence points both ways. · no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at.. On assessUrl the value describes path coverage and read no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at. on every URL sampled 2026-08-09
whisper.walkflat string enum, presence axis — no verdict impliedstructural-onlywalk — a whisper.walk value describing atlas adjacency. Not an assess coverage value. Do not gate on it. · no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at. · deadline-hit. Never gate a verdict on a walk row
run_workflow resultper-step map{stepsTotal, stepsWithData, stepsEmpty, stepsSkipped, stepsError, byStep}

assess and walk are different vocabularies on different axes, and reading them as one enum is the easiest mistake to make here: structural-onlywalk — a whisper.walk value describing atlas adjacency. Not an assess coverage value. Do not gate on it. is never an assess verdict, and malicious-evidenced — coverage: malicious-evidenced. In coverage, with positive evidence of malice. is never a walk result.

Two rules that follow from it:

  • Gate on coverage, never on band or level alone. band:"UNKNOWN" / coverage:"no-data" and band:"NONE" / coverage:"known-clean" both look like "nothing found" and mean opposite things. level: NONE means not listed; band: UNKNOWN means never seen — and the reverse also happens: band:"LOW" with coverage:"malicious-evidenced" looks unremarkable and is not. Measured on 140.82.121.3, 2026-08-09.
  • A no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at. result is a populated row that says no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at. — never zero rows. So zero rows means your query is wrong, not that the host is clean. See the traversal landmines below.

sharedHost: true (*.googleapis.com, *.s3.amazonaws.com, and similar multi-tenant apexes) means a hostname-level verdict is structurally uninformative: it cannot clear any one tenant underneath it.

What you can ask

The graph has DNS, BGP routing, IP allocation, GeoIP, WHOIS (237M emails, 60.2M phone numbers), email infrastructure (MX, full SPF chains, DMARC, DKIM signers), DNSSEC, certificate-transparency observations, TLS fingerprints, Tor-exit relays, vendor egress, prefix-level RPKI, phishing-kit URL paths, threat actors and their ATT&CK techniques, CVE exposure, and 134 intelligence feeds, 112 of them threat feeds. All of it is connected, and the assistant walks the edges between them in one conversation.

Incident response

You got an IP or domain from an alert. Start here.

  • "Investigate 185.220.101.42 -- who owns it, where is it, is it on any threat feeds, and what else is hosted there?"
  • "This domain showed up in our logs: secure-login-update.com. Is it live? Who registered it? Does the registrant own other domains?"
  • "We're seeing traffic to 104.16.132.229. Trace it: IP to prefix to ASN to org. Then check if any co-hosted domains are flagged."
  • "Here are 20 IPs from our SIEM. Which ones are Tor exits, C2, or on blocklists?"

Threat hunting

Any threat feed can tell you an IP is bad. The graph lets you pivot — follow a bad IP to its ASN, find the other prefixes, check what's hosted there, pull WHOIS on the domains, and see if the registrant has other infrastructure. One conversation. Your first investigation walks one of these end to end.

  • "Find every domain registered by the same WHOIS contact as secure-login-update.com. Do any share IPs or nameservers?"
  • "Check AS60729 -- how many of its prefixes have threat-listed IPs? What's the threat density?"
  • "Are there MOAS conflicts on this prefix? Which ASNs are announcing it?"
  • "Find all IPs in 185.220.101.0/24 that appear on threat feeds. Group by category."
  • "What domains resolve to IPs on the Tor exit-node feed? Cross-reference with their WHOIS registrants."

Brand protection and typosquatting

Run the typosquat gallery workflow over a domain — it runs 14 mutation algorithms and returns the lookalikes that are actually registered (homoglyphs, bitsquats, TLD swaps, omissions, and more). Pivot the hits through threat intel and WHOIS to see which ones are live attacks.

  • "Run the typosquat sweep for paypal.com."
  • "Generate lookalike domains for our brand, then check which ones are on threat feeds or resolve to live IPs."
  • "Which registered variants of microsoft.com share a registrant email or nameserver with each other?"
  • "Run a typosquat sweep on stripe.com -- who registered the lookalikes and is any of their infrastructure flagged?"

Attack surface

Everything an attacker would look for: subdomains, IPs, mail servers, SPF authorization chains, nameservers, WHOIS.

  • "Map tesla.com -- subdomains, IPs, ASNs, nameservers, mail servers, SPF includes, and WHOIS registrant."
  • "What third-party services can send email as netflix.com? Walk the full SPF include chain."
  • "Find every subdomain of example.com, resolve them, and group by ASN. How many hosting providers?"
  • "Where does the CNAME chain for www.example.com end up? Who hosts the final target?"

WHOIS and registrant pivoting

This is where investigations get interesting. WHOIS gives you a registrant email or phone number. The graph has 237M emails and 60.2M phones, so you can follow that contact to every other domain they registered, then check whether those domains share hosting.

  • "Find the WHOIS registrant for secure-login-update.com, then every other domain they registered. Do any share infrastructure?"
  • "What domains use this contact email? Show their IPs and ASNs, and flag any that are threat-listed."
  • "Has google.com changed registrars? Show the history."
  • "Find domains registered with the same phone number. Any overlap in hosting?"
  • "Compare WHOIS for these five domains -- same registrant? Same email? Same registrar?"

BGP and routing

116K ASNs, 2.5M prefixes, and the peering topology.

  • "If AS16509 (Amazon) went down, how many prefixes and peers are affected? What domains go dark?"
  • "Which ASNs peer with both Cloudflare and Google?"
  • "Show the BGP routing history for 8.8.8.0/24. Has the announcing ASN changed?"
  • "Find prefixes with MOAS conflicts announced by AS60729. Any of them hosting threat-listed IPs?"
  • "Show the signed ROAs for this prefix -- the authorizing ASN, max-length, and trust anchor."

Comparing infrastructure

The thing that's hard to do anywhere else: checking whether two domains share anything. Same IPs, same ASN, same nameservers, same registrant email, same phone number. The graph checks all of it at once.

  • "Do pandas-crossing.com and afterlifeevents.com share any infrastructure?"
  • "These three phishing domains were reported separately. Any shared nameservers, IPs, ASNs, or WHOIS contacts?"
  • "Compare the hosting and email setup of these two competing SaaS products."
  • "Find domains that share both the same registrant email and the same IP range as this known-bad domain."

Email, SPF, and DMARC

The graph stores the full SPF record structure — includes, ip4, a, mx, exists, redirect — as separate edges, plus MX and DMARC. So you can walk the authorization chain rather than parsing TXT records by hand. (DKIM is covered at the signing-vendor level via DKIM_SIGNED_BY; it carries small row counts today, so treat it as a spot-check signal, not a broad enrichment layer.)

  • "Who can send email as shopify.com? Walk the SPF chain."
  • "What domains use the same SPF include targets as this phishing domain?"
  • "Does this domain have MX records? SPF? a DMARC policy? Give me the full email setup."
  • "Where does this domain send its DMARC aggregate reports?"

GeoIP and data residency

622M IPv4 addresses mapped to cities and countries.

  • "Where are all the IPs that example.com resolves to? List by country."
  • "Does this company host anything in sanctioned countries? Check all their domain IPs."
  • "Find all IPs in this ASN that geolocate to Russia."

Phishing kits and URL paths

URL paths that phishing kits reuse are nodes in their own right, joined to the hosts that serve them.

  • "Which hosts serve the same distinctive login path as this phishing page? Cluster them by apex."
  • "Rank the kit paths seen on this domain by how rare they are across the graph."
  • "Is this URL malicious even though its apex is clean? Check the path, not just the host."

DNSSEC

  • "Is cloudflare.com signed with DNSSEC? What algorithm?"
  • "What percentage of domains under this nameserver use DNSSEC?"

History

WHOIS and BGP changes over time.

  • "Show the WHOIS history for google.com -- registrar changes, nameserver updates, ownership."
  • "BGP routing history for 8.8.8.8 -- has the announcing ASN or prefix changed?"
  • "When was this domain registered? Has it changed hands?"

Host identity and de-cloaking

Whose infrastructure is this, and where does it really live? Identity is answered separately from verdict, and the origin-IP recipe finds the server behind a CDN.

  • "Whose infrastructure is github.com and raw.githubusercontent.com? Are either of them dangerous?"
  • "Here are 40 hostnames from our egress log. Who runs each one, and flag anything you can't place."
  • "What are the candidate true-origin IPs behind www.cloudflare.com? Which can I reach directly, behind the WAF?"
  • "Score these 200 hostnames for breadth first, then give me the full evidence on the worst ten."

Egress, fingerprints, and transparency

Who really operates a netblock, what an IP's TLS fingerprint reveals, which subdomains certificate transparency has seen, Tor-exit identity, and DMARC posture.

  • "Does 185.220.101.1 run a Tor exit relay? Show the relay fingerprints and its anonymizer flags."
  • "Who operates the netblock 104.16.0.0/13 -- the egress vendor, not just the WHOIS owner?"
  • "Read the TLS fingerprint of this IP and find every other IP that shares it."
  • "What hostnames has certificate transparency seen for this domain, including CT-only names that never resolve?"
  • "Where does apple.com send its DMARC reports? Show the rua/ruf recipients and the SPF mechanisms."

Try these prompts

Four that are worth running first. All four are advertised prompts; three were run against production on 2026-08-09 and route-health again on 2026-09-02.

indicator-enrichment

One domain or IP into a full context card — registrant, hosting, mail, location and a reputation read, in one call.

text
/indicator-enrichment github.com

19 steps. Returns the registration identity, the hosting and mail posture, the network behind it, and a verdict — with the Cypher behind each step in the evidence block. Measured 37.5 s cold, all steps complete.

typosquat

Brand-protection sweep — registered lookalikes of a domain across 14 mutation algorithms, enriched with threat-feed listings and pivoted through WHOIS to identify who registered the suspicious ones.

text
/typosquat paypal.com

A live run returned 152 registered look-alikes, 3 of them feed-listed, 7 defensive registrations traced back to the brand's own registrant, 5 operator clusters sharing a registrant email or nameserver, and 25 unregistered variants still available. exists means registered or observed, not malicious.

build-takedown-evidence-package

The one that ends in a deliverable rather than an insight: a dossier you can hand to a registrar.

text
/build-takedown-evidence-package ickaoex.com

7 steps, every one returning data, measured 7.4 s — the fastest and most self-contained of the four.

route-health

A network or address block's routing and reachability health card — announced prefixes, RPKI ROA coverage, peers and upstream transit, MOAS conflicts, and the facilities and exchanges it sits in.

text
/route-health AS3356

A live run returned success: true in 1.6 s with five of the ten steps carrying data. It reports complete: false, and honestly: the prefix-scoped steps (BGP status, MOAS conflicts, RIR allocation, prefix hierarchy) only run when the input is a prefix, so on an ASN they are listed as skipped in incompleteSteps[] and warnings[] rather than passed off as a clean result. That is the behaviour to expect — read When a workflow returns nothing.