> For the complete documentation index, see [llms.txt](https://docs.heron-intelligence.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.heron-intelligence.com/overview/mcp.md).

# MCP Server

## Heron Intelligence MCP

### What is MCP?

The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) is an open standard that lets large language models talk to external tools and data sources through a single uniform interface. Once a server speaks MCP, any compatible client -- Claude.ai, Claude Desktop, Claude Code, Cursor, Windsurf -- can use it without custom integration work.

### Heron Intelligence's MCP

The Heron Intelligence MCP exposes the same research catalog used by our investment-grade due diligence customers: management and expert diligence interviews, coverage statistics, watchlists, scheduled monitors, and a guided intake flow for custom research projects. Bring institutional-grade research directly into your AI workflow -- no glue code, no scraping.

### Highlights

* **Remote HTTPS transport** — one URL, zero install for browser clients.
* **OAuth 2.1** — standards-compliant auth with Dynamic Client Registration (RFC 7591) and PKCE S256.
* **Subsidiary & legacy-name resolution** — search by ticker, name, or description; the server auto-maps spinoffs and rebrands to their current parent so coverage is never missed.
* **Read vs write tool annotations** — read-only tools auto-approve in supporting clients; write tools always require explicit user confirmation.

### Server

```
https://mcp.heron-intelligence.com/mcp
```

### Skill

An optional Claude Code Skill for this MCP is available at [`skills/heron-intelligence/`](/overview/mcp/skills/skill.md). See [`INSTALL.md`](/overview/mcp/skills/install.md) for setup.

### Quickstart

#### Claude.ai (recommended)

1. Open Claude.ai → **Settings → Connectors**.
2. Click **Add custom connector**.
3. Enter the server URL above.
4. Click **Authorize**. You will be redirected to Heron Intelligence to sign in.
5. Approve the requested scopes (`mcp:read`, `mcp:write`).
6. Start a new chat and ask Claude to use Heron Intelligence.

#### Claude Desktop

Open the config file at:

* **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
* **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`

Add the entry:

```json
{
  "mcpServers": {
    "heron": {
      "command": "uvx",
      "args": ["--from", "HeronIntelligence-mcp", "heronintelligence-mcp"],
      "env": { "HERON_API_KEY": "hk-xxxx" }
    }
  }
}
```

Quit Claude Desktop completely and reopen it.

#### Claude Code

Recommended -- connect to the remote server (OAuth, no API key):

```bash
claude mcp add --transport http heron https://mcp.heron-intelligence.com/mcp -s user
```

Alternative -- local stdio with an API key:

```bash
claude mcp add -e HERON_API_KEY=hk-xxxx -s user heron -- uvx --from HeronIntelligence-mcp heronintelligence-mcp
```

#### Cursor / Windsurf

```json
{
  "mcpServers": {
    "heron": {
      "command": "uvx",
      "args": ["--from", "HeronIntelligence-mcp", "heronintelligence-mcp"],
      "env": { "HERON_API_KEY": "hk-xxxx" }
    }
  }
}
```

### Authentication

The remote MCP server uses **OAuth 2.1** with PKCE (S256) and **Dynamic Client Registration** (RFC 7591). When you connect from Claude.ai:

1. Claude registers itself silently as an OAuth client against the MCP server.
2. You are redirected to Heron Intelligence to sign in.
3. After consent, Claude receives a short-lived access token plus a refresh token. The refresh token is single-use and rotates on every refresh.
4. The MCP scopes `mcp:read` and `mcp:write` map to read-only and write tools respectively. Read-only tools may auto-approve in clients that support it; write tools always prompt for explicit user confirmation.

For local **stdio** mode (Claude Desktop / Claude Code / Cursor / Windsurf), authenticate with a static API key via the `HERON_API_KEY` environment variable. Request a key from your Heron Intelligence account dashboard.

***

## Tools

The server exposes 33 tools in six families. Every tool is annotated **read** or **write**; write tools always require explicit user confirmation, and two of them additionally require a `confirm: true` argument.

| Family                                            | Read | Write |
| ------------------------------------------------- | ---- | ----- |
| [Companies](#companies)                           | 5    | —     |
| [Transcripts and people](#transcripts-and-people) | 3    | —     |
| [Discovery](#discovery)                           | 3    | —     |
| [Watchlists](#watchlists)                         | 2    | 5     |
| [Alerts](#alerts)                                 | 3    | 5     |
| [Research projects](#research-projects)           | 1    | 6     |

### Efficiency contracts

These rules are enforced by design and apply across the read tools. Ignoring them costs quota and latency without returning additional data.

* **Widen in one call, never loop.** Pass every company at once (`company_ids=[...]`) and every source at once (`data_provider_names=[...]`). Do not call a tool once per company or once per provider.
* **One call per scope per turn.** A *scope* is the combination of companies and providers requested. Calling `get_recent_transcripts` a second time for the same scope returns the same transcripts.
* **`query` ranks, it does not fetch.** On `get_recent_transcripts`, the `query` parameter filters and ranks the set already matched by the scope. A repeat call with a different `query` will not surface transcripts the first call did not return. Iterate over what you received.
* **Pagination is bounded.** `offset + limit` must be `<= 1000`. To reach different transcripts, narrow the scope or widen `quarters` — do not paginate to recount.

### Which count is authoritative

`get_company_overview` and `get_recent_transcripts` return different totals **by design**. Quoting the wrong one misstates coverage.

| Tool                     | Window                                    | Use its counts for                          |
| ------------------------ | ----------------------------------------- | ------------------------------------------- |
| `get_company_overview`   | 40 quarters (10 years), sample limit 1000 | **Authoritative** all-time coverage totals. |
| `get_recent_transcripts` | `quarters` (default **3**), paginated     | The page you just retrieved — nothing more. |

`get_recent_transcripts` also returns `total_in_window` (the exact count for the scope, safe to report as-is) and `capped: true` when the page is a partial slice of it.

***

### Companies

| Tool                     | Type | Purpose                                                                  |
| ------------------------ | ---- | ------------------------------------------------------------------------ |
| `whoami`                 | read | Returns the email of the user authenticated on the current MCP session.  |
| `search_company`         | read | Resolves a ticker, name, or description to a company id. Accepts a list. |
| `search_companies`       | read | Resolves many companies to ids in one call.                              |
| `get_company_overview`   | read | Authoritative coverage summary for one company.                          |
| `get_companies_overview` | read | Coverage summary for many companies in one call.                         |

#### `search_company`

Resolves a ticker, name, or free-text description to the company id used by every other company-scoped tool. Subsidiaries and legacy names auto-resolve to their current parent.

* `query` accepts **either a single string or a list**. The list form is routed through the batch path and answers with the `search_companies` shape (including `resolved_ids`). `limit` does not apply to the list form.
* `search_type: "ticker"` pins exact-ticker matches to rank 1 with `match_reason: "exact_ticker"`. Use it whenever the query *is* a ticker. If no company has that ticker the search falls through to standard ranking rather than erroring.
* Cross-listed tickers (for example `SAP` on XTRA and TSX) produce multiple exact matches — set `limit > 1` to receive all of them and disambiguate.

#### `search_companies`

The batch resolver. Preferred whenever more than one company is in play — a watchlist, a peer set, a sector basket, a comparison.

Returns a top-level `resolved_ids` array (deduped) that feeds directly into `get_recent_transcripts(company_ids=...)` or `get_companies_overview(company_ids=...)`, plus `matched_count`, `unmatched_count`, and a `duplicates` array identifying queries that collapsed to the same company.

#### `get_company_overview` / `get_companies_overview`

Coverage shape without per-transcript metadata: `total_transcripts`, date range, `quarters_available`, `recommended_quarters`, and breakdowns by `by_dd_type`, `by_transcript_type`, and `by_data_provider`.

`breakdowns_truncated: true` flags the rare case where breakdowns were computed over a sample smaller than `total_transcripts`.

The batch form returns one entry per company in `overviews`, plus `companies_covered`, `companies_requested`, and `total_transcripts_across`. On partial failure it returns an `errors` array rather than failing the call.

***

### Transcripts and people

| Tool                     | Type | Purpose                                                |
| ------------------------ | ---- | ------------------------------------------------------ |
| `get_recent_transcripts` | read | Lists transcript metadata with filters and pagination. |
| `get_transcript_content` | read | Returns the content of one transcript, with selectors. |
| `search_people`          | read | Finds executives, experts, and interviewees.           |

#### `get_recent_transcripts`

Returns transcript **metadata only** — use `get_transcript_content` to read verbatim Q\&A from any returned id.

Every scope is optional and all of them compose:

| Scope passed                 | Result                                                                  |
| ---------------------------- | ----------------------------------------------------------------------- |
| `company_id` / `company_ids` | Those companies, subsidiaries included.                                 |
| `data_provider_names`        | Those datasets. See [Data Providers](/documentation/data-providers.md). |
| Both                         | That dataset, for those companies.                                      |
| Neither                      | The latest across all of your coverage.                                 |

Filters: `due_diligence_type`, `source_tone`, `call_quality`, `query`. Paging: `quarters` (default 3), `limit` (default 20), `offset`.

Responses carry `provenance` (per-provider counts for the returned set), `total_in_window`, `capped`, `applied_constraints`, and a `coverage_note`.

`provenance` is keyed by `data_provider_name` and is the durable provenance contract — build every source count from it.

{% hint style="danger" %}
**Do not surface `provenance_note` verbatim.** The response also carries `provenance_note`, a pre-rendered English sentence generated from `provenance`. It currently merges `Heron AI Events` into `Heron AI`, labels `Paragon` as a "third-party provider", and states page-scoped counts without saying they are page-scoped. Derive your own wording from `provenance`, `capped` and `total_in_window` instead. See [Data Providers](/documentation/data-providers.md#known-issues-in-server-generated-provenance-text).
{% endhint %}

#### `get_transcript_content`

{% hint style="warning" %}
**Choose a selector before you call.** A full transcript is typically 30–60 KB (10,000+ tokens). Fetch the whole payload only when you need verbatim text for citation.
{% endhint %}

| Selector                 | Returns                                                     | Typical size |
| ------------------------ | ----------------------------------------------------------- | ------------ |
| `summary_only: true`     | Metadata + `summary`; drops `questions_answers` and `tocs`. | \~2 KB       |
| `qa_ids: [...]`          | Only the Q\&A entries with those ids.                       | varies       |
| `limit_qa: N`            | Only the first N Q\&A / TOC entries.                        | varies       |
| `max_qa_answer_chars: N` | Each `answer` truncated to N characters (min 100).          | varies       |
| *(none)*                 | The complete verbatim transcript.                           | 30–60 KB     |

Use `summary_only` when scanning many transcripts to decide which are relevant. Use `qa_ids` when a previous response already gave you the ids you need. `limit_qa: 5` with `max_qa_answer_chars: 400` yields roughly 5 KB.

Selectors compose, except that `summary_only` overrides the others. When any selector is applied the response includes `selectors_applied`.

#### `search_people`

Finds a person by name, title, or description and returns their associated transcript ids.

* Handles **"company + role"** phrases directly — `"Amazon CEO"`, `"the CFO at Pfizer"`. Pass the whole phrase as `query`; do not resolve the company first. The response reports which company it scoped to in `scoped_to_company_name`.
* `company_id` narrows to an already-resolved company and suppresses that automatic detection.
* `data_provider_names` restricts to people with at least one transcript from those datasets.
* `limit` defaults to 10, maximum 50.

***

### Discovery

Enumeration tools. All three are free — no backend call, no quota — and exist so that you never have to guess an enum value.

| Tool                       | Type | Purpose                                                            |
| -------------------------- | ---- | ------------------------------------------------------------------ |
| `list_data_providers`      | read | The valid `data_provider_names` strings.                           |
| `list_analysis_categories` | read | Valid `analysis_catalog_category_name` values for analysis alerts. |
| `list_report_templates`    | read | Valid `report_template_name` values for report alerts.             |

#### `list_data_providers`

Call this before filtering by data source if you are not certain of a name. Returns each provider's `name` (pass verbatim), `produced_by`, and `description`.

{% hint style="warning" %}
**`name` is trustworthy; `produced_by` and `description` are not.** Both still carry pre-acquisition metadata for `Paragon`, which is reported as `produced_by: "third_party"` with a description calling it a licensed third-party feed. Paragon is a Heron Intelligence company producing human-conducted MDD. Take how a dataset is produced from [Data Providers](/documentation/data-providers.md), not from this tool's payload.
{% endhint %}

This is the **catalog**, not your entitlements — it does not tell you which datasets your subscription includes. See [Data Providers](/documentation/data-providers.md) for the definitions of each dataset and for the difference between "not entitled" and "no coverage".

#### `list_report_templates`

Returns templates in two scopes: `user` (owned by you) and `system` (available to everyone). User-owned templates are listed first.

***

### Watchlists

Named sets of companies. Watchlists are addressed **by name**, not by id, and names are matched case-insensitively.

| Tool                    | Type  | Purpose                                        |
| ----------------------- | ----- | ---------------------------------------------- |
| `list_watchlists`       | read  | Watchlists owned by the authenticated user.    |
| `get_watchlist`         | read  | The companies in one watchlist.                |
| `create_watchlist`      | write | Creates a watchlist, optionally pre-populated. |
| `add_to_watchlist`      | write | Adds companies by ticker or name.              |
| `remove_from_watchlist` | write | Removes companies by ticker or name.           |
| `rename_watchlist`      | write | Renames; membership untouched.                 |
| `delete_watchlist`      | write | **Permanent.** Requires `confirm: true`.       |

**Behaviour**

* **Atomic resolution.** If any company identifier fails to resolve — ambiguous or unknown — the entire operation is rejected with no partial change.
* **Idempotent membership.** `add_to_watchlist` silently skips companies already present; `remove_from_watchlist` skips ones not present. Both report `added_count` / `removed_count` and `skipped_count`.
* **Names are unique per user**, case-insensitively.
* `list_watchlists` is personal-scoped in V1 — firm-level and public watchlists are not exposed.

**Permissions**

Watchlists you own can always be modified. Watchlists shared with you with `can_edit: true` can be modified when they are not firm- or team-visible; otherwise the operation returns `forbidden_no_edit_permission`. Deletion is owner-only — **a share never grants delete rights**, even with `can_edit: true`.

{% hint style="danger" %}
**Template chains cascade across users.** If a watchlist is a template (`is_template: true`) or an assigned copy of one, then adding, removing or renaming propagates the change to **every other user on that chain** — and `delete_watchlist` permanently removes the template *and all of its assignments across all users*. This is irreversible. Always confirm with the user before mutating a watchlist you suspect belongs to a template chain.
{% endhint %}

**Errors:** `watchlist_not_found`, `watchlist_name_taken`, `ambiguous_company` (carries a `candidates` array), `company_not_found`, `forbidden_no_edit_permission`, `forbidden_not_owner`, `confirmation_required`.

***

### Alerts

An **alert** is a saved monitor. It watches a scope of companies, fires on a cadence or when a threshold of matching transcripts is reached, and produces a feed item — optionally an email.

| Tool                | Type  | Purpose                                                      |
| ------------------- | ----- | ------------------------------------------------------------ |
| `list_alerts`       | read  | Every alert you can see, with `visibility` and `is_owner`.   |
| `get_recent_alerts` | read  | Firings across all visible alerts (`days`, 1–90, default 7). |
| `get_alert_history` | read  | Firing history for one alert (`limit`, 1–100, default 20).   |
| `create_alert`      | write | Creates an alert with `status: active`.                      |
| `update_alert`      | write | Partial update. See replace semantics below.                 |
| `pause_alert`       | write | Stops firing; configuration preserved.                       |
| `resume_alert`      | write | Resumes, and is how a **draft** is activated.                |
| `delete_alert`      | write | Soft-delete. Requires `confirm: true`.                       |

Existing alerts are addressed **by name** (`alert_name`), matched case-insensitively.

{% hint style="info" %}
**Alerts were previously called Drops.** The old tool names (`list_drops`, `create_drop`, `update_drop`, …) and the old `drop_name` argument are still accepted, so a cached tool list or an older Skill keeps working: the call is served by the matching Alert tool and the response adds `renamed_to` with the new name. Only the Alert names appear in the tool list. Use them in new integrations.
{% endhint %}

#### Types and trigger rules

Each alert has a `type` and a `trigger_mode`. The valid combinations mirror the portal:

| `type`         | `trigger_mode`           | `cadence`                       | Also required                                                        |
| -------------- | ------------------------ | ------------------------------- | -------------------------------------------------------------------- |
| `notification` | `cadence` only           | `realtime`, `daily`, `weekly`   | —                                                                    |
| `analysis`     | `cadence` or `threshold` | `weekly`, `biweekly`, `monthly` | Exactly one of `analysis_prompt` or `analysis_catalog_category_name` |
| `report`       | `cadence` or `threshold` | `weekly`, `biweekly`, `monthly` | `report_template_name`                                               |

When `trigger_mode` is `threshold`, `threshold_count` must be between **1 and 25** — the cap protects the firing engine when many transcripts match in quick succession.

Invalid combinations return `invalid_type_trigger_cadence`.

#### Scope and filters

At least one list in `scope` must be non-empty.

```json
{
  "scope": {
    "watchlists": ["Tech Megacaps"],
    "companies": ["XOM", "CVX"],
    "industries": ["Metals and Mining"],
    "sectors": ["Materials"]
  },
  "content_filters": {
    "diligence_types": ["MDD", "BDD"],
    "call_tones": ["Positive", "Negative", "Neutral", "Mixed"],
    "interview_sources": ["Blue Heron", "Heron AI"],
    "geographies": ["United States"]
  }
}
```

`diligence_types`, `call_tones` and `interview_sources` are validated against fixed whitelists; an unknown value returns `invalid_<field>_value` together with the valid set. `interview_sources` takes the dataset names documented in [Data Providers](/documentation/data-providers.md).

#### Status lifecycle

`active`, `paused`, or `draft`. Drafts originate in the portal — the MCP only creates `active` alerts. A draft accepts any change (including `type`) and is activated with `resume_alert`; there is no separate activate tool. Drafts can also be deleted directly without activating first.

Once an alert is `active` or `paused` its **`type` is locked**; changing it returns `type_locked`. To change type, delete the alert and create a new one.

{% hint style="danger" %}
**`update_alert` replaces grouped fields wholesale.** Passing `scope` replaces the entire scope object — any sub-list you omit is **emptied**. The same applies to `content_filters`. To preserve existing members, read the current alert with `list_alerts` first and pass back the full set you want to keep.
{% endhint %}

Switching `trigger_mode` to `cadence` requires `cadence`; switching to `threshold` requires `threshold_count`. The other side of the trigger is cleared automatically.

#### Visibility and ownership

`list_alerts` returns your own alerts plus any firm- or team-shared alerts you can read, each carrying `visibility` (`user` / `firm` / `team`) and `is_owner`. **Only owned alerts can be mutated** — update, pause, resume and delete on an alert you do not own return `forbidden_not_owner` with an explanatory `message`. Surface that message as written rather than paraphrasing it as "not found".

`delete_alert` is a soft delete: the alert stops firing and leaves your feed, but its firing history is preserved for analytics, and the name becomes immediately reusable.

***

### Research projects

A four-step guided intake for commissioning custom research.

Research project tools are not available on every plan. When your plan does not include them they are left out of the tool list, and a direct call returns `tool_not_in_plan`.

| Tool                                 | Type  | Step                   | Purpose                                         |
| ------------------------------------ | ----- | ---------------------- | ----------------------------------------------- |
| `get_project_status`                 | read  | —                      | Read-only snapshot of flow state.               |
| `start_research_project`             | write | —                      | Creates the project, returns `project_id`.      |
| `define_research_brief`              | write | 1 — Scope              | Processes one turn of the brief conversation.   |
| `prepopulate_research_configuration` | write | 1 → 2                  | Infers Step 2 fields from the brief.            |
| `configure_research_project`         | write | 2 — Setup              | Processes one turn of configuration.            |
| `generate_research_questions`        | write | 3 — Topics & Questions | Generates and curates topics and questions.     |
| `submit_research_project`            | write | 4                      | Submits to a Heron Intelligence representative. |

The Research Brief artifact has five elements: Research Objective, Primary Use, Scope, Desired Insight, and Preferred Experts. Each turn returns `global_readiness` and per-element `element_readiness`.

`generate_research_questions` supports three actions — `generate_all`, `regenerate_all`, and `regenerate_single` (with `target_item_id`). Combined items are capped at 15 by the backend.

`submit_research_project` accepts four `submission_type` values: `full`, `bh_review_only` (brief only), `partial_step2`, and `partial_step3`. Project state is preserved on partial submissions so the flow can be resumed.

**Recovering from an error.** Tools in this flow return structured `409 wrong_step` responses carrying `current_step`, `expected_steps` and a `hint`, and `404 project_not_found` for an invalid id. `get_project_status` is the canonical recovery tool — call it to re-orient, then resume at `current_step` (`scope` / `setup` / `questions` / `submitted`).

***

### Example prompts

> *"How much research coverage does Heron Intelligence have on Amazon?"*

> *"Compare coverage across my Tech Megacaps watchlist and tell me where it's thinnest."*

> *"Show me only the management due diligence transcripts available for Amazon from the last two quarters."*

> *"Set up a weekly notification for any new Blue Heron interviews on my energy names."*

### Support

* Email: <dfernandez@heron-intelligence.com>
