The generic MCP server (RFC-095) exposes the platform's read capabilities as composable,
provenance-bearing tools any MCP-aware agent can call — Claude Desktop, Claude Code, Cursor,
autoresearch. It wraps the Python library directly (no HTTP server needed); a corpus directory
is its read context. This guide is the single place a human or an agent can learn what
tools exist, how to chain them, how to configure it in Claude, and how to maintain it.
- Design / rationale: RFC-095 · spec:
PRD-034 · e2e testing:
docs/wip/MCP-E2E-GUIDE.md.
- 38 tools, stdio transport, read-only. Every tool returns a uniform envelope:
{ok, data, note} (ok=False on a clean error — never a crash; note says why a result
is empty so an agent never confuses "no data" with "feature off").
Run it
# against any corpus dir (one with metadata + .gi.json / .kg.json + a search/ index)
python -m podcast_scraper.cli mcp --corpus /path/to/corpus
Needs the .[dev,search] extras (the MCP SDK rides in [dev]). It waits on stdio.
Claude Code (this repo):
claude mcp add podcast-corpus -- \
python -m podcast_scraper.cli mcp --corpus "$PWD/tests/fixtures/app-validation-corpus/v3"
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"podcast-corpus": {
"command": "/ABS/PATH/.venv/bin/python",
"args": ["-m", "podcast_scraper.cli", "mcp", "--corpus", "/ABS/PATH/to/corpus"]
}
}
}
The core idea: resolve → pivot → chain
Two rules make the whole surface fluid:
- Resolve names to ids first. Most tools take canonical ids (
person:…, topic:…,
org:…, podcast:…) — call resolve_entity("Sam Altman") to get one, or take an id from
a search_corpus hit / any tool's output.
- Every output carries the ids that are other tools' inputs (referential parity). A
search_corpus hit carries a pivot block {id, kind, expand_with} naming the id and
which tools consume it. So you chain across surfaces without guessing:
search → insight_detail → entity_neighborhood → compare_subjects.
The insight_detail tool is the bridge: it turns a search insight-hit's pivot.id into
that insight's topics + mentioned entities (each id-bearing), so search results flow into the
graph.
Entry
| tool |
use |
resolve_entity(name, kind?) |
name → canonical id. Call FIRST on a freeform name. |
search_corpus(query, tier?, speaker?, topic?, episode_id?, grounded_only?, top_k?) |
hybrid two-tier search; each hit carries a pivot handle. tier: insight|segment|both. |
corpus_briefing_pack(query, …, max_tokens?) |
one assembled, LITM-ordered brief (RFC-093) instead of raw hits. |
Momentum
| tool |
use |
corpus_trending(kind?, limit?) |
what's rising corpus-wide (EWMA velocity); each entity's entity_id pivots into the graph. |
topic_perspective_leaders(limit?) |
topics by distinct-speaker count — the most-debated nodes (centrality proxy). |
Relational (canonical ids in)
| tool |
use |
person_positions(person_id) |
what a person stated. |
insights_about_entity(entity_id) |
what insights say about a person/org. |
who_said_about_topic(topic_id) |
insights grouped by speaker. |
cross_show_synthesis(topic_id) |
top insight per distinct show — the corpus differentiator. |
topic_entities(topic_id) |
entities a topic's insights mention. |
related_insights(insight_id) |
sibling insights. |
insight_detail(insight_id) |
the pivot bridge: an insight's text + quotes + topics + entities. |
show_episodes(podcast_id) |
a show's episodes. |
CIL intelligence
| tool |
use |
person_profile(person_id) |
grounded insights across episodes. |
topic_timeline(topic_id) |
insights about a topic over time. |
position_arc(person_id, topic_id) |
how a person's position evolves. |
topic_conversation_arc(topic_id, insight_types?) |
weekly volume + sentiment arc. |
GI / grounded insight
| tool |
use |
explore_insights(topic?, speaker?, grounded_only?, min_confidence?, sort_by?, limit?) |
faceted cross-episode discovery. |
episode_insights(metadata_path, limit?) |
salience-ranked insights for one episode, with quotes. |
compare_subjects(subject_a, subject_b, q?, insight_types?) |
two-subject compare: a briefing pack per side + a deterministic judge summary. Only carrier of the insight_type filter. |
Search operators
| tool |
use |
cluster_search(query, top_k?) |
search, then group hits by topic/theme cluster. |
consensus_search(query, top_k?, max_pairs?) |
search, then cross-speaker consensus pairs among the surfaced topics. |
Enrichment & speaker
| tool |
use |
corpus_enrichment_signals() |
corpus-scope RFC-088 envelopes (similarity, consensus, grounding_rate, …). |
episode_enrichment_signals(metadata_path) |
per-episode signals (sentiment, density, co-occurrence). |
episode_speaker_roster(metadata_path) |
diarized talk-share / roster (who spoke, %, host/guest) — no HTTP route; MCP-only. |
Connectivity / graph
| tool |
use |
entity_neighborhood(entity_id, k?) |
everything connected to an entity in one call — the exploration keystone. |
person_topics(person_id) |
topics a person engages. |
co_occurring_entities(entity_id) |
who's discussed alongside an entity. |
bridge(entity_a, entity_b) |
how two entities relate (shared topics + co-occur). |
related_topics(topic_id) |
co-occurring topics. |
ego_network(entity_id, max_hops?, k?) |
multi-hop KG-proximity neighborhood (variable depth). |
topic_clusters(topic_id) |
semantic + theme cluster siblings. |
Catalog / navigation
| tool |
use |
list_feeds() · list_episodes(feed?, since?, limit?) · episode_detail(metadata_path) · top_people(limit?) |
browse the corpus. |
Composites (multipliers — one call, many surfaces)
| tool |
use |
entity_dossier(entity_id, k?) |
the full person/topic-page fan-out in one call (profile + positions/timeline + neighborhood + arc + clusters). |
episode_digest(metadata_path, insight_limit?) |
one call: detail + insights + enrichment signals + speaker roster. |
Worked example — the golden cross-surface case
"What's the most contested topic, who's on each side, how has it evolved, and how do the
two loudest voices differ?"
topic_perspective_leaders() → topic:ai-development (centrality)
corpus_trending(kind="topic") → is it also rising? (momentum)
who_said_about_topic(topic_id) → the sides, grouped by speaker (relational)
topic_conversation_arc(topic_id) → volume + sentiment over time (temporal)
search_corpus(q, topic=topic_id) → grounded hits, each w/ pivot (search)
insight_detail(hit.pivot.id) → that insight's entities/topics (the bridge)
entity_neighborhood(entity_id) → expand a key voice (graph)
compare_subjects(personA, personB) → contrast the two voices (compare)
Or the one-call shortcuts: entity_dossier(topic_id) / episode_digest(metadata_path).
Maintenance (for the next agent)
- Tools are plain functions in
src/podcast_scraper/mcp/tools/*.py (search, resolve,
relational, cil, gi, operators, enrichment, connectivity, catalog, composites, trending,
briefing_pack). They wrap the same capability functions the HTTP routes call — no
duplicated logic. Adding a tool = add the function + register it.
- Registration is split into per-family
_register_*(server, ctx) helpers in
src/podcast_scraper/mcp/server.py, called from build_server. Add your tool's
@server.tool() wrapper to the right registrar (keeps build_server under the complexity
limit).
- The envelope is applied by
_enveloped / _safe in server.py — return a plain dict
(or an {ok, …} dict for connectivity-style tools); the wrapper normalizes it.
- Referential parity is the contract: a new tool's output should carry the canonical ids
(
entity_id / topic_id / insight_id / metadata_path) that other tools consume, so it
keeps the chain intact.
- Tests:
tests/unit/mcp/ (per-tool + protocol dispatch + registered-tool list) and
tests/integration/test_mcp_pivot_chain_e2e.py (the golden chain + episode-scoped + search
operators/compare against the committed synthetic corpus, index built at setup). Update the
registered-tools set in test_server.py when you add a tool.