URL: /guides/entity-map/mcp

---
title: "The entity map for agents"
sidebarTitle: "For agents"
icon: "bot"
description: "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:

1. **Find the problem.** `list_entities` with `problem: "no-id"` returns the entities a site declares on several pages with nothing to tie them together.
2. **Fix it.** Give each one an absolute `@id` and reference it everywhere else. [Fixing structured data identity](/guides/entity-map/fixing) has the change for each generator.
3. **Re-audit.** `run_audit`, or `squirrel audit` locally.
4. **Prove it landed.** `compare_entities` and check that `gainedId` contains the keys you fixed, each with `coverage: "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.

<Warning>
`gainedId` on its own says the entity changed, not that the broken pages were fixed. Matching happens on type and name, so a re-audit of a different part of the site pairs the two and reports the transition while the pages you were sent to fix still carry the old markup. Every entry carries a `coverage` field for that reason:

- `proven`: the newer audit visited every page that declared the old version **and** found the replacement on all of them.
- `partial`: one of those could not be established.

Both conditions, because either alone is satisfied by a site that is not fixed. Recrawling the broken pages proves nothing if the markup was deleted there and the identified entity appeared somewhere else. Finding it somewhere else proves nothing about the pages that were never revisited.

Re-audit the same scope you audited the first time, or treat `partial` as work still open.
</Warning>

<Note>
The reverse does not hold: a missing `gainedId` entry is not proof the fix failed. The match needs the type and the name to be unchanged, so correcting the `@id` and the name in one edit appears as a removal plus an addition. Change the identifier first, confirm it, then rename.
</Note>

<Note>
Every tool description states this loop, so an agent that calls one of the five learns the other four exist and in what order.
</Note>

## The five tools

Available on both the local `squirrel mcp` server, reading your project store, and the [hosted server](/developers/mcp), 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](#which-audit-you-actually-got) and [Site-wide totals](#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.

<Note>
The `jsonld` rendering gives every entity an `@id`, inventing one for each entity the site left anonymous, because a `@graph` member without an identifier is not much use in a validator. JSON-LD has nowhere to mark an identifier as a placeholder, so `generatedIds` lists them: the entity key and the `@id` this export made up for it. Those identifiers are not on the site. Read that list before concluding from the export that a site's entities are identified, which is the exact question that usually sends you here.
</Note>

### `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.

<Note>
An entity is only reported as `removed` when every page that declared it was crawled again. Anything unproven is reported separately as `notCrawled`, so a smaller crawl never reads as a site that deleted its structured data. If you are checking whether a fix landed, `notCrawled` is the field that tells you the audit did not cover the evidence.
</Note>

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.

<Warning>
The keys and pages on a finding are a **sample**. The rule that produced the finding clipped its own lists before this tool saw them, typically to five pages, and nothing in the pipeline records how many were dropped. `truncation.truncated` is therefore always true here. Use `list_entities` with the matching `problem` filter for the complete set of affected entities, and that entity's `pages` for where it is declared.
</Warning>

`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" }
```

```json
{
  "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.

<Note>
`warnings` is about what could not be read or had to be guessed around. `truncation` is about what was cut from an answer that was otherwise complete. They are separate fields because they call for different responses: re-audit or name a run, versus ask for another page.
</Note>

**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](/guides/entity-map): what it is and how to read it
- [The entity map on your machine](/guides/entity-map/cli): the same queries from the command line
- [The entity map in the cloud](/guides/entity-map/cloud): the dashboard and the API
- [The entity map format](/guides/entity-map/format): the documents these tools return
- [Fixing structured data identity](/guides/entity-map/fixing): the change to make in each generator
- [MCP server guide](/developers/mcp): connecting an agent
