MCP server
Drive your AI agent (Claude Desktop, Cursor, custom client) directly against alitheia's data + analytical surfaces. Same Team-tier API keys as the REST API; same scopes; same provenance.
Overview
The Model Context Protocol (MCP) server exposes alitheia as a tool surface for AI agents. It speaks JSON-RPC over HTTP using the streamable transport defined by the MCP 2025-03-26 spec, and ships:
- 33 atomic tools — resolvers, list/detail readers, lenses, Pro signals, watchlist mutations. Direct map to the REST surface.
- 9 composed tools — intent-driven workflows (
build_actor_dossier,compare_actors,find_angle_gap,surface_underreported_spikes,quote_finder,trace_money_and_influence,frame_narrative,expand_section,refine) that fan out across multiple atomic queries and return summary +query_idfor follow-ups. - 5 persona prompts — named slash commands the agent client renders for the user.
- 5 resources — subscribable JSON feeds plus a static methodology document.
Every successful response carries a citation envelope. Provenance is the safety story; alitheia returns evidence, the agent decides framing.
Endpoint
This deployment advertises the MCP endpoint at:
https://openbrief.co.nz/mcp
Operator notes — binary configuration:
| Env var | Default | Purpose |
|---|---|---|
MCP_LISTEN_ADDR |
:8090 |
Bind address for the streamable HTTP transport. Required (binary refuses to boot without it). |
MCP_ADVERTISED_URL |
derived from PUBLIC_BASE_URL |
Public URL surfaced in setup snippets on this page and on /account/api-keys. Override when the MCP listener sits behind a different host (e.g. mcp.example.com) or when the public base URL has no canonical /mcp path. |
API_TOKEN_FORMAT |
opaque |
Shared with the REST API. opaque = DB-backed sha256-hashed strings; jwt = RS256-signed JWTs with JWKS at /.well-known/jwks.json. |
DATABASE_URL |
— | Same Postgres as cmd/api. The MCP binary doesn't issue keys (issuance lives on the website); it validates tokens minted there. |
Auth
Bearer tokens. Tier team required — non-Team tokens get a 401 with a tier-mismatch hint in the body.
Mint a key from /account/api-keys; it works for both the REST API and the MCP endpoint. Per-tool scopes apply:
read:core— people, organisations, donations, speeches, topics, elections, audio appearances. The atomic data surface.read:discourse— commentary corpus signals (Discourse Lens).read:press— news corpus signals (Press Lens).read:pro— daily brief, narratives, actor snapshots, alerts, watchlist.write:watchlist—add_watchlist/remove_watchlist.
Tools that span corpora (e.g. find_angle_gap, frame_narrative) require both read:discourse and read:press. Scope mismatches return a tool-level scope_required error rather than a transport 403, so the agent gets a structured signal instead of a connection failure.
Setup
Claude Desktop — paste into claude_desktop_config.json:
{
"mcpServers": {
"alitheia": {
"url": "https://openbrief.co.nz/mcp",
"headers": { "Authorization": "Bearer YOUR_KEY_HERE" }
}
}
}
Generic MCP client — configure the streamable HTTP transport with:
- Endpoint:
https://openbrief.co.nz/mcp - Header:
Authorization: Bearer YOUR_KEY_HERE
Replace YOUR_KEY_HERE with the raw token shown once after key creation. The key is opaque (ali_…) by default; JWT mode is also supported.
Atomic tools (33)
Direct map to the REST API. Each tool returns the citation envelope with one or more citations[] entries.
resolve_person(query) — free-text → person_id candidates.resolve_topic(query) — phrase → topic_slug candidates.resolve_organisation(query, kind?) — partial name → org candidates with NZBN.
list_mps/get_mp— sitting MPs + full briefing.list_people/get_person— broader anchored people set.list_organisations/get_organisation— parties, companies, charities, lobby principals.search_donations— the >$20k disclosure register.search_speeches/get_speech— Hansard.list_topics/get_topic— canonical topic taxonomy.list_elections/get_election_year— per-cycle stats.list_candidates/get_candidate— per-cycle candidate roster + full candidacy briefing.get_electorate— seat profile: sitting MP + contested history.person_appearances/get_episode_appearance— audio episodes + diarised turns.
discourse_digest— top topics this week with framing samples.discourse_heatmap— topic-by-week intensity grid.
press_digest— top news topics this week.press_heatmap— topic-by-week intensity for the news corpus.press_topic_detail(slug) — per-topic news detail.
pro_brief_today— daily alert cards.pro_narratives_active— top 7-day narratives.pro_actor_snapshots— daily per-actor snapshots.pro_alerts— watchlist-scoped anomaly feed.list_watchlist— caller's watchlist items.
add_watchlist(kind, ref, label, sublabel?) — watch an MP / topic / source.remove_watchlist(id) — drop a watchlist row.
Composed tools (9)
Fan out across multiple atomic queries, return summary + per-section top-N + a query_id. Drill into a section without re-paying retrieval cost via expand_section; layer extra filters via refine.
build_actor_dossier(person_id|name, lookback_days?, include?, angle?, top_n?) — identity, interests, directorships, meetings, recent speeches, press/commentary mentions, audio appearances, focus topics. Keystone tool.compare_actors(person_ids[], topic_slug|topic_query, window_days?) — 2..5 person side-by-side on a topic axis with directorship + interest overlap sets.find_angle_gap(topic_slug|topic_query, window_days?, direction?) — cross-corpus discourse-vs-press counts, modal framings per side, candidate angles. Requires bothread:discourseandread:press.surface_underreported_spikes(within_hours?, min_zscore?, corpus?, kinds?, limit?) — today's anomaly feed re-ranked by spike-vs-press-coverage asymmetry.quote_finder(person_id, topic_slug|topic_query, since?, until?, corpus[]?, limit?) — verbatim Hansard / audio / commentary / news quotes by person.trace_money_and_influence(person_id|org_id, depth?, min_amount_cents?, year?) — donation + directorship + interest + ministerial-meeting graph. Always carriesdonations_election_year_only.frame_narrative(topic_slug|topic_query, agenda?, lookback_days?) — modal framings per corpus, framing drift (week-by-week alias chain), adjacent topics, stance breakdown, recent press headlines. No content generation — alitheia returns evidence, the agent composes the framing. Requires both lens scopes.expand_section(query_id, section, offset?, limit?) — paginate one section of a prior composed-tool result.refine(query_id, patch) — layer extra filters on top of the original inputs and re-issue.
Prompts (5)
Persona-anchored slash commands. The agent client renders these to the user; selecting one pushes a primed instruction into the conversation that names the composed tools to chain.
/dossier(strategist) — opposition dossier on {person} for {electorate?}, weighted toward {topic_focus}. Chainsbuild_actor_dossier→compare_actors→frame_narrative→find_angle_gap./morning_brief(newsroom) —surface_underreported_spikes→ drill top 3 → output 3 pitchable angles each with ≥2 source URLs./topic_deep_dive(researcher) — quote_finder per relevant MP + trace_money on top declared interests, ends with stance-by-party table./counter_narrative(PR) — frame_narrative → find_angle_gap → quote_finder per opposing voice; output 3 reframes each grounded in ≥3 verbatim quotes./follow_money(cross-cutting) — trace_money_and_influence depth=2 → quote_finder per notable edge endpoint, with explicit cycle-gap callout.
Resources (5)
Read-only URIs. The agent client subscribes / pulls; alitheia returns JSON (or markdown for the methodology doc). Pull-only today; SSE / push is a future add.
alitheia://brief/today— today's Pro brief cards. Requiresread:pro.alitheia://narratives/active— top 7-day cross-corpus narratives. Requiresread:pro.alitheia://watchlist/alerts— caller's watchlist alert feed. Requiresread:pro.alitheia://taxonomy/topics— canonical topic catalogue (slug → label + aliases). Use to resolve free-text phrases without a tool call. Requiresread:core.alitheia://methodology— static markdown reference: stance scale, anomaly z-score thresholds, donations cycle caveat, elections.nz block caveat, the >$20k threshold. Public.
Citation envelope
Every successful tool response returns a structured envelope:
{
"data": <tool-specific>,
"citations": [
{
"source": "elections.nz",
"source_label": "Electoral Commission — Donations exceeding $20,000",
"url": "https://elections.nz/...",
"fetched_at": "2026-05-08T12:34:56Z",
"extraction_method": "scraper",
"caveats": ["donations_election_year_only"]
}
],
"retrieved_at": "2026-05-08T12:34:56Z",
"query_id": "qry_AbC123_xY",
"next_cursor": null
}
- citations[] is non-optional on success. If a tool returns a synthesised summary without per-claim citations, that's a bug.
- caveats[] on a citation is a list of stable slugs. See Caveats below.
- query_id is the handle for
expand_sectionandrefine. Continuations are scoped to the api key; a peer's query_id is invisible. - next_cursor is opaque. Pass back to
expand_sectionverbatim.
Caveats
Caveats are stable slugs surfaced as citation.caveats[]. Propagate them when summarising or quoting; they encode known data-quality bounds the agent must not gloss over.
| Slug | Meaning |
|---|---|
donations_election_year_only |
The Electoral Commission's >$20,000 disclosure register publishes per electoral cycle, not annually. Non-election years (e.g. 2024, 2025) have no rows by design — absence means "not yet disclosed for this cycle", not "no donations". Annual returns PDFs would fill the gap; alitheia doesn't ingest them yet. |
elections_nz_blocked |
elections.nz is intermittently hard-blocked by Imperva at the network edge. Candidate rosters for blocked cycles surface this caveat instead of silently returning partial data. |
Read alitheia://methodology from your agent for the full methodology + corpus-bound reference.
Open the REST API docs for the equivalent JSON / OpenAPI surface, or jump to /account/api-keys to mint a key.