The entity map for agents
Five MCP tools that let an agent ask what a site declares, find what is wrong with it, fix it, and prove the fix landed.
The loop
Structured data identity is unusually well suited to an agent, because the whole task is a loop with a verifiable end:
- Find the problem.
list_entitieswithproblem: "no-id"returns the entities a site declares on several pages with nothing to tie them together. - Fix it. Give each one an absolute
@idand reference it everywhere else. Fixing structured data identity has the change for each generator. - Re-audit.
run_audit, orsquirrel auditlocally. - Prove it landed.
compare_entitiesand check thatgainedIdcontains the keys you fixed, each withcoverage: "proven".
Step 4 is the one that matters and the one that is easy to get wrong by hand. An entity that gains an @id changes key, because the key IS the @id when there is one. A naive comparison reports that as one removal plus one addition, which looks like damage. gainedId is the field that says a fix landed.
The five tools
Available on both the local squirrel mcp server, reading your project store, and the hosted server, reading the API. Same names and inputs, and the same outputs apart from the hosted-only fields staleSnapshot and siteTotals, described in Which audit you actually got and Site-wide totals. On the local server all five are free and need no account.
list_entities
The entities a site declares, collapsed across every crawled page into one graph, so an Organization declared identically on 60 pages is one row rather than 60.
Collapsing is by resolved @id, or by type and name when there is no @id. The same real-world thing can still occupy several rows when its declared identity differs between pages: a relative @id like #organization resolves against each page and produces one row per page. That is the split identity the audit rules flag, not a quirk of this tool.
| Input | Meaning |
|---|---|
run_id | A specific audit. Defaults to the latest that stored at least one entity, which is not always the latest audit. See below. |
type | Only these @type values. |
page | Only entities declared on a page matching one of these values. An absolute http(s) URL or a value starting with / is a prefix (of the URL, or of the page path and query), so /blog matches https://example.com/blog/post but not https://example.com/tag/blog. Anything else is a substring, so blog matches both. Case-sensitive, not a glob. |
problem | no-id, conflict, dangling, single-page or split-identity. |
q | Only entities whose name or @id contains this text. |
include_page_local | Include a page’s own WebPage, BreadcrumbList and unnamed images. False by default. |
limit, offset | Paging. Default 25, maximum 100. Ordered by page count descending, then by key. |
Several values in one filter are an OR. Two different filters are an AND: type plus problem keeps the entities that match a listed type and have a listed problem.
Returns the site-wide summary, a filtered block with the nodes and edges counts that survived the filters, a page of nodes, and total and hasMore. summary does not change with the filters. Those last two matter: they are how you tell a page from the whole list before describing a site from 25 of its entities.
Page-local entities are hidden by default because they usually outnumber everything else. On squirrelscan.com they are 233 of 363 entities.
get_entity
One entity as the map recorded it, by key, @id or name: the properties the map keeps, the pages that declare it, the properties whose values disagree between those pages, and the references in and out.
The map keeps nine properties, not all of them: name, url, logo, image, sameAs, telephone, email, address, description, plus @type. A property missing here may still be in the page’s JSON-LD, and a disagreement in a property outside that set is not detected. A Product with conflicting sku values across pages reads as consistent.
The lookup resolves in order of certainty: an exact @id, then an exact key, then the key formed by prefixing your value with id:, then an exact name, and only then a name substring. The first match wins.
Use it after list_entities to see why an entity was flagged before deciding what to change. Edges and declaring pages are capped, and truncation says when.
get_entity_graph
The whole graph, or a filtered part of it, in one of six formats.
| Format | For | Node cap |
|---|---|---|
json | The canonical document. | none |
jsonld | A @graph you can paste into a validator. | none |
mermaid | A diagram in a conversation. | 150 |
dot | Graphviz. | none |
graphml | Gephi, yEd, Cytoscape. | none |
markdown | A table to read. | 50 rows |
No node cap is not the same as complete. Every format renders the stored map, and on the local server that map carries no per-edge page list and no per-page reference list, so those arrays are empty because the CLI never stored them, not because nothing matched. Mermaid’s cap bounds declared entities only: one entity referencing thousands of undeclared ids still draws thousands of placeholder nodes.
Takes the same filters as list_entities. mermaid and markdown exist to fit a context window and say so in truncation when they cut. format defaults to json.
Not every server implements every format. One that does not says so rather than returning an empty or partial graph, so read the error instead of treating a refusal as a site with nothing to draw.
compare_entities
The change set between two audits: entities added and removed, entities that gained or lost an @id, occurrence changes, new and resolved conflicts and dangling references, summary deltas, and the pages each audit saw that the other did not.
Defaults to the previous audit versus the latest, of the same site. When you name both runs they are ordered chronologically whichever field named them, so a diff always reads forward in time. Read fromRunId and toRunId on the result for the direction actually used.
occurrence_threshold is the smallest occurrence change worth reporting, 1 by default. Raise it on a site that publishes constantly, where a site-wide entity moves by one on every audit and would otherwise bury the real change in noise. It affects occurrenceDeltas only: entities added, removed, or changing their @id are reported whatever you set it to, because those are structural changes rather than changes of degree.
The default declines when the older audit stored no entities at all, because zero entities cannot be distinguished from an audit that predates the entity map, and guessing would report a site’s entire graph as brand new. Name both runs when you know which it was.
get_entity_findings
The schema/entity-* rule verdicts for an audit: what is wrong with the entity graph, which entity keys and pages each finding affects, and the fix text for each.
Use it instead of re-deriving the problems from the graph. Each finding names one problem across the whole site rather than one per entity, so a count of 1 can still mean hundreds of pages.
analyzed says whether the rules ran at all. An audit that was crawled but never analyzed returns the same three empty arrays as a site with a flawless entity graph, and only one of those is good news. analyzed: false means no verdict exists yet, not that there is nothing to fix.
A worked example
Finding and fixing a split identity, which is the most common expensive one.
list_entities { "problem": "split-identity" }
{
"site": "https://example.com/",
"runId": "4f2ab91c…",
"entities": [
{ "key": "id:https://example.com/#organization", "name": "Example Ltd",
"types": ["Organization"], "pageCount": 39 },
{ "key": "id:http://data.wordlift.io/wl1/entity/example-ltd", "name": "Example Ltd",
"types": ["Organization"], "pageCount": 36 }
],
"total": 2,
"hasMore": false
}Two declarations of one company, from two plugins. The page counts say which the site actually uses. Look at the one you plan to keep:
get_entity { "key": "https://example.com/#organization" }
Then remove the second declaration or reduce it to a sameAs, re-audit, and confirm:
compare_entities {}
A successful fix shows the removed declaration under removed and nothing under notCrawled. If the entity you kept also gained an @id in the same change, it appears under gainedId rather than as an add and a remove, with coverage: "proven" when the re-audit visited every page that had been wrong.
Which audit you actually got
Every result carries a warnings array. It is empty when there is nothing to say, and it is the only place two facts appear that change what the answer means.
An audit was passed over. The default picks the latest audit that stored at least one entity, not simply the latest. An audit that stored none is skipped, because the store cannot tell a site that declares nothing from an audit that predates the entity map.
That matters most in exactly the case you would least want to miss it. A template change wipes a site’s JSON-LD, today’s audit stores nothing, and the default falls back to yesterday. Without the warning you would be shown yesterday’s healthy graph while asking whether the structured data is fine. The warning names the audit that was skipped so you can read it with run_id.
A project store could not be read. An answer drawn from part of the data says so rather than presenting itself as the whole picture.
A stale snapshot (hosted server only). list_entities, get_entity, get_entity_graph and get_entity_findings carry staleSnapshot. It is null when the newest completed audit was served. It is set when the newest audit has no entity map and an older one was served instead, and then holds:
| Member | Meaning |
|---|---|
servedRunId, servedAuditedAt | The audit whose map you are reading, and when it ran. servedRunId is null for a report published without a run. |
newestRunId, newestAuditedAt | The newest completed audit, which has no usable entity map. |
newerAudits | How many newer audits were passed over. |
A matching entry in warnings says the same in words. Pass run_id to address a specific audit. Do not report a stale snapshot as the site’s current state.
Site-wide totals
On the hosted server list_entities and get_entity_graph carry siteTotals: nodes and edges for the whole site, sampled, droppedNodes and droppedEdges. Read site-wide counts from siteTotals, never from the filtered summary, filtered block or a list length, because those describe what survived your filters.
sampled is true when publishing dropped nodes to fit a size budget. The stored map is then a sample: the entities you can list are fewer than siteTotals.nodes, and a warning says how many were held. Counts you compute over the listed entities understate the site. get_entity_graph with format: "json" also keeps the truncated marker of the stored map, so a sampled graph says so in the document itself.
What the tools will not do
They read. None of the five changes a site, runs an audit or spends credits. run_audit is the tool that re-audits, and on the local server squirrel audit does it.
They also will not tell you a site is clean when they have not looked. A tool that cannot find an entity map says so rather than returning an empty graph, because a site with no structured data and an audit that stored no map produce the same empty answer and only one of them is worth acting on.
References
- The entity map: what it is and how to read it
- The entity map on your machine: the same queries from the command line
- The entity map in the cloud: the dashboard and the API
- The entity map format: the documents these tools return
- Fixing structured data identity: the change to make in each generator
- MCP server guide: connecting an agent