@pipeworx/canoncannon

Connect: https://gateway.pipeworx.io/canoncannon/mcp · Install: one-click buttons

Tools: 8

What gets repeated across 162 tracked podcasts — claims extracted from 10,000+ episodes, each carrying the measurement behind its status: how many independent podcasts and speakers repeat it, excluding whoever originated it.

LIVE, BYO-ONLY. Promoted out of _incubator and wired into the gateway on 2026-09-14 (fleet #1958), once Bruce’s partner key existed and all eight exported tools returned non-empty payloads against the real API.

It ships BYO-only even though a Pipeworx platform key exists, and that is a deliberate choice, not an oversight. The gateway is AT Cloudflare’s limit of 250 text bindings, so wrangler secret put PLATFORM_CANONCANNON_KEY is rejected outright — “Too many text bindings, found a total of 251, they exceed the limit of 250” (code 10055). Declaring platformKeyEnv without the secret actually present would un-sink this pack in routing and send podcast-consensus questions to a tool that can only refuse, which is strictly worse than an honest no_match — the fda-inspections precedent, fleet #617. So the pack declares no platformKeyEnv, keyBlockedTools() keeps it out of ask_pipeworx retrieval, and a caller with their own CanonCannon key can still call it directly with _apiKey.

To finish it: free one text binding on the gateway, then land the wrangler secret put and platformKeyEnv: 'PLATFORM_CANONCANNON_KEY' in the SAME change. The key itself is already issued, stored in the canonical checkout’s gitignored .env, and verified live. canon_citations is separately held — see the integrity gate.

Tools

Eight are exported. canon_citations is implemented and deliberately held — see the integrity gate.

  • canon_search(query?, tier?, label?, limit?, cursor?) — find recurring ideas. Each row carries tier and its measurement.
  • canon_get(slug) — the full entry: claim, evidence, nuance, gaps, related, books, plus the measurement spelled out in one sentence beside it.
  • canon_episode_search(query?, podcast?, since?, limit?, cursor?) — the episode corpus.
  • canon_episode_get(slug) — one episode with its extracted ideas, guests and books.
  • canon_person_get(slug) — what one person has actually said, across shows.
  • canon_podcast_list(limit?, cursor?) — the tracked-show roster, i.e. the denominator of the canon bar.
  • canon_podcast_get(slug) — one tracked show.
  • canon_corpus_stats() — corpus counts and the bar currently in force.
  • canon_citations(slug)HELD. Every episode citing an idea, with speakers and originator involvement.

Auth

BYO only today: _apiKey (Authorization: Bearer cc_live_...). Every /api/v1 endpoint refuses without one — GET /stats with no key is a clean 401 {"error":"Missing or malformed API key. Send Authorization: Bearer cc_live_..."}, so a missing credential is unambiguous rather than looking like an outage.

There is no platformKeyEnv because we hold no key. When Bruce issues the partner key, wire PLATFORM_CANONCANNON_KEY on the gateway in the SAME change that adds the MCP_PACKS entry (fda-inspections precedent, fleet #617) — a declared-but-unset platform key sinks every tool in the pack under the key rule and reads to a caller as a broken pack rather than an unfunded one.

The citations endpoint is additionally restricted to an internal-tier key, so a partner key gets 403 there. That is upstream policy, not our gate.

The measurement envelope — the whole contract

tier is never served without a measurement sibling, in one of exactly two shapes:

{ "available": true, "measured_at": "…", "independent_podcasts": 23,
  "independent_speakers": 41, "independent_episodes": 36,
  "threshold": 9, "tracked_podcasts": 162 }

{ "available": false, "reason": "not_yet_measured" | "not_yet_persisted" }

We reproduce that nesting exactly instead of flattening it into top-level nullable fields. The point is that a caller can tell “not computed” from “computed, and the answer is zero”. Zero independent speakers is a strong claim about an entry; unavailable is no claim at all; a null reads as both, and next to a populated tier it reads as “ignore this, trust the tier”.

The two unavailable reasons are different claims and we never collapse them:

reasonwhat it meanshow often
not_yet_measuredthis entry is newer than the last nightly recomputeper-row, permanent by design — true of every entry added today
not_yet_persistedthe counts are not being stored at all right nowcorpus-wide, a real but temporary upstream deficiency

Render both as a generic “measurement unavailable” and a caller reads broken for something that is fine — manufacturing a false defect report about a healthy pipeline, immediately after we went to some length not to manufacture a false consensus count.

measured_at is load-bearing, not metadata. CanonCannon’s bar is bidirectional: an entry can lose canon status without losing a single endorsement, purely because the corpus grew around it (the bar is ceil(5% of tracked_podcasts)). And per-row measured_at is the only thing that makes not_yet_measured detectable from outside the system at all.

threshold is never reported without tracked_podcasts. “9 of 162” is a claim; “9” alone is a number.

Integrity gate on canon_citations

CITATIONS_VERIFICATION_LANDED = false in src/index.ts keeps the tool out of the exported tools array, and callTool refuses it with the reason.

CanonCannon’s extractor was using canon entries as filing labels, so an entry’s own summary text could count as an endorsement of itself. canon_citations returns a consensus count — the shape a caller is least able to check — so publishing it over contaminated links would put a confident wrong number in front of every agent that calls it, under the Pipeworx name. A verification pass over 4,218 links is running upstream.

Before flipping the flag:

  1. Confirm the verification pass has completed. “The API is live” does not imply it — the pass is a separate pipeline job on its own schedule.
  2. Check rows come back verified: true. The endpoint carries the flag per row, so the gate is enforced upstream rather than by anyone remembering it.
  3. Do not hard-code the exclusion rule. Whether the fix changes the rule’s shape or only its inputs was still open when this was written, so the tool reports the rule the API states, never the intended one.

Why every tool is canon_-prefixed

episode_get, person_get, podcast_list and corpus_stats were the names in CanonCannon’s own API-PLAN.md §3, and they are clear across mcps/* and the live gateway today (checked 2026-09-14). They are also generic enough that the next podcast pack anyone builds — Listen Notes, Podcast Index, a transcript source — would want four of them.

That matters because of how the gateway resolves a bare tools/call: while a name is unique to one pack it is exposed bare, and the moment a second pack exports it BOTH become reachable only as <pack>_<name>. So a future pack would silently rename this pack’s live tools, breaking its tool-examples.json keys and every caller that had learned the bare name. Nine central-bank packs all exporting exchange_rates is the measured precedent, and before it was swept a “Bank of Japan policy rate” question was answered with Israel’s number.

The canon_ prefix costs nothing now and cannot be added later without a breaking rename. Re-run the check at promotion anyway — it is cheap:

for n in canon_search canon_get canon_citations canon_episode_search          canon_episode_get canon_person_get canon_podcast_list          canon_podcast_get canon_corpus_stats; do
  grep -rl "name: '$n'" mcps/*/src/ | head -3
done
scripts/pwcall.sh names canon_          # and against the live gateway

The name check now applies: the pack lives at mcps/canoncannon/, which pnpm check:collisions and check:error-body-leak do glob. All nine canon_* names were confirmed unique across mcps/*/src at promotion.

Data sources

Specced upstream and deliberately not wrapped yet: GET /people (list), GET /search (cross-corpus), GET /changes?since= (cheap polling). Add tools when a caller asks for them; every tool needs a tool-examples.json entry recorded from a call that actually returned rows, and we cannot record one today.

Things the next person would otherwise rediscover

  • Cursor pagination, never offset. ?cursor= is keyset. Terminate on next_cursor: null or an empty page — never on a short one, because a short page is what silent truncation looks like. Their convention came from the same PostgREST 1,000-row cap that has bitten us; see docs/postgrest-row-cap.md. This pack makes one upstream request per tool call and hands the cursor back, so a caller can never be given a truncated list that looks complete.
  • We are the only consumer of /api/v1 on the wire. CanonCannon’s own pages render in-process (a Worker fetching its own zone re-enters the Worker), so their production traffic never exercises these routes. We will find the API’s bugs. Treat the OpenAPI artifact as intent and the live endpoint as truth — which is why every response here can carry contract_warnings, and why those warnings ride out to the caller instead of being swallowed.
  • A contract bug there already survived twenty green assertions (canon_related has two foreign keys to canon, the embed was ambiguous, every detail request 500’d in production while every stubbed test passed). The detail endpoints have no declared response schema, which is why this pack passes their payloads through rather than hand-mapping fields nobody here has observed.
  • citation_count is not exposed and must not be reintroduced. It was a text column of model prose wearing a number’s name.
  • No direct Supabase access, ever. The canon-cannon Supabase project (admbyodxaxsikiczgfjf) is a separate project; Pipeworx must never point a migration, an ingest or the data-pipeline worker at it. Going over HTTP is what makes us inherit the bar when it moves instead of drifting from it. Do not “optimise” this into a direct read, and do not ingest their corpus into a Pipeworx table.
  • Their api_keys table briefly shipped without RLS (anon-readable and anon-writable; caught and closed within the hour, independently re-probed). Recorded because the key we will hold is issued from that table.

Tests

src/index.test.ts — 25 assertions over fixtures taken from the published OpenAPI artifact. They prove our handling of the declared contract: the two unavailable reasons staying distinct, tier never passing bare, null being flagged rather than forwarded, a page count never presented as a total, the refusal saying “requires an API key” in those words.

They do not prove the live API returns these shapes — nothing here has made a live call. Replace the fixture payloads with recorded live ones when the key lands, before anyone calls this pack verified.

What promotion found (2026-09-14)

All eight exported tools were smoke-tested live against the real API before the gateway wiring landed, and two defects surfaced that only a live call could:

  1. canon_corpus_stats told every caller the bar was unreported while both of its numbers sat in the payload. /stats nests the bar as { corpus: {...}, bar: { threshold, tracked_podcasts, computed_at, rule } }; the pack read threshold and tracked_podcasts at the top level, found neither, and emitted “CanonCannon did not report both the threshold and the tracked-podcast count”. It now reads the nested shape first and keeps the flat one as a fallback. This is the pack’s own header rule — treat the OpenAPI artifact as INTENT and the live endpoint as TRUTH — failing in the window before a key existed to check it.
  2. Three of the eight documented examples pointed at slugs that 404. zone-2-base-training, huberman-lab-sleep-toolkit and the-drive are all invented; the real ones are circadian-rhythm-health-foundation, pa-392-sleep-apnea and peter-attia-drive. canon_search({query: "zone 2 training"}) also returned zero rows, because upstream q is a PHRASE match, not a token AND — "zone 2" matches, "zone 2 training" does not. Every example in the tool descriptions and in tool-examples.json is now a call that actually returned rows.

What is still held

canon_citations stays unexported (CITATIONS_VERIFICATION_LANDED = false). The gate is enforced upstream as well as here: as of 2026-09-14 GET /canon/{slug}/citations returns HTTP 403“Citations are not yet public: the underlying links are pending a verification pass. This endpoint currently requires an internal key.” Confirm with CanonCannon that the pass has landed, check rows come back verified: true, then flip the flag and record an example from a call that returned rows.

Tools

  • canon_search — Search CanonCannon for ideas that recur across podcasts — claims extracted from 10,000+ episodes of 162 tracked shows, each with the MEASUREMENT behind its status: how many independent podcasts and sp
  • canon_get — The full CanonCannon entry for one idea — the claim, the evidence behind it, the nuance and gaps, related entries and books — alongside its measurement (independent podcast count, independent speaker
  • canon_episode_search — Search the CanonCannon episode corpus — 10,000+ podcast episodes with the ideas, guests and books extracted from each. Filter by show, by publication date, or by free text. Requires a CanonCannon API
  • canon_episode_get — One podcast episode from CanonCannon with everything extracted from it — the ideas it raised, its guests, and the books mentioned. Get the slug from canon_episode_search. Requires a CanonCannon API ke
  • canon_person_get — What one person has actually said across podcasts, per CanonCannon — their appearances by show and the ideas attributed to them, drawn from 3,800+ people in the corpus. Answers “does this person keep
  • canon_podcast_list — Every podcast CanonCannon tracks, with its episode count — the roster that forms the denominator of the canon bar (an idea needs ceil(5%) of these shows to clear it). Requires a CanonCannon API key vi
  • canon_podcast_get — One tracked show from CanonCannon, with its episode count and metadata. Get the slug from canon_podcast_list. Requires a CanonCannon API key via _apiKey. Example: canon_podcast_get({ slug: “peter-atti
  • canon_corpus_stats — CanonCannon corpus size and the canon bar CURRENTLY in force — entry, episode, idea and person counts, the tracked-podcast count, and the independent-podcast threshold an idea must clear. Call this to

Tools

  • canon_corpus_stats — CanonCannon corpus size and the canon bar CURRENTLY in force — entry, episode, idea and person counts, the tracked-podcast count, and the independent-podcast threshold an idea must clear. Call this to
  • canon_episode_get — One podcast episode from CanonCannon with everything extracted from it — the ideas it raised, its guests, and the books mentioned. Get the slug from canon_episode_search. Requires a CanonCannon API ke
  • canon_episode_search — Search the CanonCannon episode corpus — 10,000+ podcast episodes with the ideas, guests and books extracted from each. Filter by show, by publication date, or by free text. Requires a CanonCannon API
  • canon_get — The full CanonCannon entry for one idea — the claim, the evidence behind it, the nuance and gaps, related entries and books — alongside its measurement (independent podcast count, independent speaker
  • canon_person_get — What one person has actually said across podcasts, per CanonCannon — their appearances by show and the ideas attributed to them, drawn from 3,800+ people in the corpus. Answers does this person keep m
  • canon_podcast_get — One tracked show from CanonCannon, with its episode count and metadata. Get the slug from canon_podcast_list. Requires a CanonCannon API key via _apiKey. Example: canon_podcast_get({ slug: peter-attia
  • canon_podcast_list — Every podcast CanonCannon tracks, with its episode count — the roster that forms the denominator of the canon bar (an idea needs ceil(5%) of these shows to clear it). Requires a CanonCannon API key vi
  • canon_search — Search CanonCannon for ideas that recur across podcasts — claims extracted from 10,000+ episodes of 162 tracked shows, each with the MEASUREMENT behind its status: how many independent podcasts and sp

Regenerated from source · build September 15, 2026