Skip to content

feat(mcp): Tier-1+2 coverage — 6 new tools + parity guard (38 total)#3658

Merged
koala73 merged 10 commits into
mainfrom
feat/mcp-coverage-tier1-tier2
May 11, 2026
Merged

feat(mcp): Tier-1+2 coverage — 6 new tools + parity guard (38 total)#3658
koala73 merged 10 commits into
mainfrom
feat/mcp-coverage-tier1-tier2

Conversation

@koala73

@koala73 koala73 commented May 11, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the BOOTSTRAP_KEYS → TOOL_REGISTRY coverage gap audited 2026-05-10 after a disease-outbreaks lookup failed via MCP despite the data being seeded. Adds 6 new tools (32 → 38) covering brainstorm-promised + high-value missing domains, plus a Tier-3 parity test that prevents this drift from happening again.

Plan: docs/plans/2026-05-11-001-feat-mcp-coverage-tier1-tier2-plan.md — Codex-approved over 6 review rounds.

Tools added

Unit Tool Pattern Cache keys Notes
U1 `get_displacement_data` CacheToolDef 1 Tier-1 regression — present in 2026-03-27 brainstorm inventory but missing from registry. Dynamic UTC-year cache key resolved at module load.
U2 `get_health_signals` CacheToolDef 2 Disease outbreaks (the trigger case) + air-quality. Air-quality stays in `get_climate_data` too (regression-tested).
U3 `get_energy_intelligence` CacheToolDef 9 Broadest bundle — EIA petroleum, electricity (Ember + prices), gas storage, fuel shortages, disruptions, crisis policies, fossil/renewable shares. Per-key freshness budgets spanning hourly to ~400d.
U4 `get_consumer_prices` Hybrid `_execute` 5 (per country) Required `country_code` param (only `ae` seeded today). Result-level error (NOT `-32603`) on missing/unsupported/malformed input. Spread freshness reads producer's actual `seed-meta:consumer-prices:spread:` key (NOT `api/health.js`'s drifted shape).
U5 `get_tariff_trends` CacheToolDef 4 US tariff trends + BigMac index + FAO FFPI + national debt.
U6 `get_chokepoint_status` CacheToolDef 6 Maritime chokepoints — transit summaries, transits, port activity, baselines, ref, flows. Payload-verified live (combined ~18.5KB, well under thresholds).

Tier-3 parity test (U7)

`tests/mcp-bootstrap-parity.test.mjs` — every key in BOOTSTRAP_KEYS ∪ STANDALONE_KEYS must be either (a) in some tool's `_cacheKeys`/`_coverageKeys` OR (b) in `EXCLUDED_FROM_MCP` with a non-empty documented reason.

  • 109 `EXCLUDED_FROM_MCP` entries with reasons across 7 categories (intermediate, cascade-mirror, on-demand, deferred follow-up tools, recovery-pillar stubs, shared-token-panels, operational)
  • 4 live structural assertions + 7 fixture-based meta-tests verifying the predicate logic itself fires on synthetic invalid inputs (added in residual review pass)
  • New `_coverageKeys?: string[]` field on `RpcToolDef` so hybrid `_execute` tools (U4) declare what they cover
  • Added `BOOTSTRAP_KEYS` + `STANDALONE_KEYS` to `api/health.js::testing` export

Docs (U8)

  • `docs/mcp-server.mdx` — count bumps 32 → 38 at two anchor points, 6 new catalog rows across Markets & economy / Energy / Health / Humanitarian sections

Tier-2 code review fixes (residual commit)

Three specialist reviewers (code-reviewer + correctness-reviewer + testing-reviewer) ran against the 8-commit feature diff. Findings applied in the final commit:

  1. U3 zero behavioral tests — broadest 9-key bundle was missing from both the registration-name assertion AND had no `tools/call` test. Added 3: 9-fresh happy path with label-walked slice assertions, one-key-stale → aggregate stale=true, all-9-null → -32603.
  2. U7 parity test had no meta-tests — the test that prevents future drift had no test of its own predicate logic. Refactored 4 inline checks into 6 pure exported helpers; added 7 fixture-based meta-tests asserting each fires on synthetic invalid inputs.
  3. U4 hybrid skipped `cache_all_null` guard — every CacheToolDef throws when all reads return null so `dispatchToolsCall`'s catch fires `proRollback` (DECR). Hybrid silently returned success → Pro user's daily counter incremented on a useless 5-null result. Added the symmetric guard.
  4. U4 `country_code` loose validation — `.slice(0,2)` silently coerced `'aexxx'` / `'AE-DXB'` / `'1AE'` to `'ae'`. Tightened to `/^[a-z]{2}$/` after lowercase.

Findings explicitly accepted (not patched)

  • Tariffs `data['all']` label (plan-asserted by the test)
  • U1 year-boundary module-load pinning (plan-documented design choice; Vercel cold-start cadence bounds it)
  • `_countries`/`ref` label brittleness (no collision today, speculative)
  • Tool count 38 not cross-locked to docs (drift not present)
  • Producer-side key parity for `:v1` shapes (out of scope; mock-vs-real is a separate hardening track)

Tests

  • `tests/mcp.test.mjs` — 77 tests (was 73): +3 U3 behavioral + 1 new U4 strict-shape + 6 existing-but-modified U4 cases + the count-assertion bump
  • `tests/mcp-bootstrap-parity.test.mjs` — 11 tests (was 4): 4 live structural + 7 meta-tests of the predicate helpers
  • 84/84 pass. TypeScript clean on both `tsconfig.json` and `tsconfig.api.json`.

Deploy notes

  • No env-var changes. Pure additive cache exposure.
  • No migrations. No new tables / Convex shape changes / Redis writes.
  • No new Pro quota cost. Tools count as 1 `tools/call` each; existing 50/day Pro cap unchanged.
  • No breaking changes to existing tools. `get_climate_data` keeps air-quality (regression-tested); `get_supply_chain_data` keeps its 3 slices (regression-tested).

Future Tier-4 follow-up candidates

`EXCLUDED_FROM_MCP` flags ~40 entries as "deferred to a future expanded X tool" — VPD tracker (health), EIA weekly series (energy supplementary), gold metals (markets), resilience ranking, token panels (crypto), Cloudflare Radar (cyber/internet-health), per-country JODI (energy), EU gas storage, intelligence/OSINT feeds. Worth one more pass once a target audience surfaces.

Test plan

  • CI green (typecheck + tests on both `tsconfig.json` and `tsconfig.api.json`)
  • Verify `tools/list` returns 38 tools with all 6 new names registered
  • Smoke-test one new tool via authenticated MCP session (`get_displacement_data` is lowest-risk single-key)
  • Verify `/docs/mcp-server` renders the new catalog rows correctly post-deploy

🤖 Generated with Claude Code

koala73 added 9 commits May 11, 2026 08:20
Restores the UNHCR refugee/IDP cache-tool that was in the 2026-03-27
brainstorm v1 inventory but missing from PR #3646's TOOL_REGISTRY.

- New cache-tool entry in api/mcp.ts::TOOL_REGISTRY (after
  get_sanctions_data). Single cache key displacement:summary:v1:<UTC year>
  resolved at module-load via new Date().getUTCFullYear() — mirrors
  api/health.js:147's STANDALONE_KEYS pattern.
- _seedMetaKey: seed-meta:displacement:summary (writer at
  scripts/seed-displacement-summary.mjs:232).
- _maxStaleMin: 3600 (mirrors seeder line 238).
- Current-year only — including displacementPrev would collide on the
  'summary' label produced by executeTool's numeric-suffix-stripping
  label-walk at api/mcp.ts:796.

Tests: 32→33 count + registration check + happy/cache-empty smoke tests.
53/53 mcp.test.mjs pass.

Plan unit U1: docs/plans/2026-05-11-001-feat-mcp-coverage-tier1-tier2-plan.md
After U1-U6 added get_displacement_data, get_health_signals,
get_energy_intelligence, get_consumer_prices, get_tariff_trends, and
get_chokepoint_status to TOOL_REGISTRY, public docs need to match.

- Count flips at lines 12 and 164: "32 tools" → "38 tools" (2 sites).
- 6 new catalog rows added in domain sections:
  - Markets & economy: get_consumer_prices, get_tariff_trends,
    get_chokepoint_status (sibling of get_supply_chain_data)
  - New `### Energy` subsection: get_energy_intelligence
  - New `### Health` subsection: get_health_signals
  - New `### Humanitarian & displacement` subsection: get_displacement_data

Descriptions copied from each tool's TOOL_REGISTRY description field
(lightly trimmed for table-cell readability). MDX lint clean (126/126).

Plan unit U8.
Three findings from the senior + correctness + testing review of the 8-commit
Tier-1+2 MCP coverage diff. All address real divergences from the F6
quota-correctness contract or coverage gaps the live registry would not
catch.

### U4 — `cache_all_null` parity guard (correctness)
Hybrid `_execute` for `get_consumer_prices` skipped the F6 cache_all_null
contract that every CacheToolDef path enforces in executeTool:1139-1144.
Result: on degenerate-empty Redis responses (transient/stampede/pre-seed),
the Pro quota counter incremented for a useless 5-null result while every
sibling cache-tool refunded via the throw → DECR rollback path. Added the
same guard at the end of `_execute` to restore symmetric contract behavior.

### U4 — strict ISO 3166-1 alpha-2 country_code validation (correctness)
`.toLowerCase().slice(0,2)` silently coerced "aexxx" / "AE-DXB" / "1AE" to
"ae" → served AE data with no error, masking client-side bugs. Tightened to
`/^[a-z]{2}$/` after lowercase; oversized/non-alpha now returns a
result-level error with a usable message.

### U3 — `get_energy_intelligence` behavioral tests (coverage gap)
Broadest 9-key bundle in the diff had zero `tools/call` coverage AND was
missing from the registration-name assert block (only the `tools.length ===
38` count check would have caught a deregistration). Added 3 behavioral
tests mirroring the U6 pattern: 9-fresh happy path with label-walked slice
assertions, one-key-stale → aggregate stale=true, all-9-null → -32603.

### U7 — meta-tests for parity predicate logic (coverage gap)
Parity test's 4 live assertions only fire when real drift exists; nothing
verified the predicates THEMSELVES would catch regressions (predicate
inversion, off-by-one, early return). Refactored predicate logic into 6
pure exported helpers and added 7 fixture-based meta-tests asserting each
helper fires on synthetic invalid inputs (uncovered key, empty/whitespace
reason, dead exclusion, redundant exclusion, _cacheKeys+_coverageKeys
aggregation, BOOTSTRAP+STANDALONE dedupe).

### Test counts
- tests/mcp.test.mjs: 73 → 77 (+3 U3 + flipped 1 U4 all-null + 1 new U4 strict-shape)
- tests/mcp-bootstrap-parity.test.mjs: 4 → 11 (+7 meta-tests)
- All 84 pass; TypeScript clean.

### Findings explicitly accepted (not patched)
- Tariffs `data['all']` label (plan-asserted)
- U1 year-boundary module-load pinning (plan-documented design choice)
- `_countries`/`ref` label brittleness (no collision today, speculative)
- Tool count 38 not cross-locked to docs (drift not present)
- Producer-side key parity for `:v1` shapes (out of scope)
@vercel

vercel Bot commented May 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Preview May 11, 2026 5:55am

Request Review

@mintlify

mintlify Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
WorldMonitor 🟢 Ready View Preview May 11, 2026, 5:42 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@greptile-apps

greptile-apps Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes a BOOTSTRAP_KEYS → TOOL_REGISTRY coverage gap by adding 6 new MCP tools (32 → 38) and a Tier-3 parity test that enforces coverage going forward. All new tools follow established cache-tool or hybrid _execute patterns, with per-key freshness budgets, correct label-walk labels, and the cache_all_null Pro-quota rollback guard.

  • 6 new tools across displacement (U1), health signals (U2), energy intelligence (U3, 9 keys), consumer prices (U4, hybrid with country_code param), tariff trends (U5), and maritime chokepoints (U6), each with corresponding _freshnessChecks and behavioral tests.
  • U7 Tier-3 parity test (tests/mcp-bootstrap-parity.test.mjs) with 109 documented EXCLUDED_FROM_MCP entries and 7 meta-tests verifying each predicate helper fires on synthetic invalid fixtures.
  • Residual fixes: U3 behavioral tests added, U4 cache_all_null guard added, U4 country-code validation tightened to strict /^[a-z]{2}$/ regex.

Confidence Score: 4/5

Safe to merge — all 6 new tools are pure additive cache reads with no schema changes or breaking modifications to existing tools.

Each new tool has behavioral tests, freshness budgets, and the cache_all_null guard. The Tier-3 parity test with meta-tests is a strong structural addition. One minor maintenance gap: the get_consumer_prices error message hardcodes 'Available: ae' rather than deriving it from SUPPORTED_CONSUMER_PRICES_COUNTRIES.

api/mcp.ts — the hardcoded error message in get_consumer_prices around the country-not-supported branch.

Important Files Changed

Filename Overview
api/health.js Adds BOOTSTRAP_KEYS and STANDALONE_KEYS to testing export — minimal, additive, no behavioral change.
api/mcp.ts Adds 6 new MCP tools. One P2: the hardcoded 'Available: ae' error message in get_consumer_prices will drift when new countries are added to SUPPORTED_CONSUMER_PRICES_COUNTRIES.
tests/mcp-bootstrap-parity.test.mjs New U7 Tier-3 parity test with 109 EXCLUDED_FROM_MCP entries and 7 meta-tests. Comprehensive drift guard.
tests/mcp.test.mjs 77 tests (+4): registration assertions for all 6 new tools, 3 U3 behavioral tests, expanded U4 validation and cache_all_null tests.
docs/mcp-server.mdx Count bumped 32 → 38 at two anchors; 6 new catalog rows across correct sections.

Comments Outside Diff (1)

  1. api/mcp.ts, line 243-245 (link)

    P2 Hardcoded "Available" list will silently drift from the Set

    The error message embeds ae as a string literal, but the source of truth is SUPPORTED_CONSUMER_PRICES_COUNTRIES. When a second country (e.g. us) is added to the Set, the validation will accept the new code but the error presented to callers who pass an unsupported code will still say "Available: ae" — giving MCP clients misleading guidance about what codes to use.

Reviews (1): Last reviewed commit: "test+fix(mcp): residual Tier-2 review fi..." | Re-trigger Greptile

…gnitive complexity

CI biome lint surfaced two violations from the residual review-fix commit:

1. `lint/suspicious/noExportsInTest` × 6 in tests/mcp-bootstrap-parity.test.mjs —
   the U7 meta-test refactor exported 6 predicate helpers so they could be
   meta-tested. Test files must not export. Helpers are only called from
   describe blocks in the same file, so the `export` keyword was always
   gratuitous — dropped it.

2. `lint/complexity/noExcessiveCognitiveComplexity` in U3 happy-path test —
   the 18-branch if-chain in the fetch mock (9 data keys + 9 meta keys)
   exceeded biome's cognitive-complexity ceiling. Replaced with a single
   Map<cache-key, payload> + for-of loop. Functionally identical, much lower
   cyclomatic weight.

Pre-existing biome violations in unrelated files (api/mcp-proxy.js,
tests/webmcp.test.mjs, api/health.js:678) are NOT in this PR's diff and
remain for a separate cleanup.
@koala73
koala73 merged commit 7b6bdd2 into main May 11, 2026
11 checks passed
@koala73
koala73 deleted the feat/mcp-coverage-tier1-tier2 branch May 11, 2026 06:53
koala73 added a commit that referenced this pull request May 11, 2026
…-key overlap (+ 3rd false-positive)

The previous audit logic classified an API op as `covered-via-cache-key`
when ANY of the handler's cache keys overlapped with ANY tool's
_cacheKeys, even if the handler also read 4+ uncovered keys. This is the
trap that produced the get-gold-intelligence and basket-series false-
positives in the prior commit, and a third one (get-risk-scores) it now
surfaces.

### Audit script logic fix (scripts/audit-mcp-api-coverage.mjs)

Per-op cross-reference now partitions the handler's cache keys into
(covered, missing) sets per candidate tool, then classifies:

  - fullyCoveredByCacheKey      handler keys ⊆ tool's _cacheKeys
  - partiallyCoveredByCacheKey  some shared, some uncovered (per tool)

categorize() routes:

  - any fully-covered tool       → 'covered-via-cache-key' (unchanged semantics)
  - only partial overlap         → NEW 'partial-cache-key-overlap' category
  - no overlap (pure-read)       → 'manual-mapping' or 'deferred-to-future-tool'
                                   (unchanged)

The partial-cache-key-overlap category is the explicit handoff to the
implementer: decide between (a) extending the tool's _cacheKeys to cover
the missing keys, vs (b) excluding as deferred-to-future-tool with the
specific receiving-tool hint.

Hint output now prints per-tool partial-overlap detail:
  "partial overlap with <tool>: covered=[k1,k2] missing=[k3,k4,k5]"

so a reader sees the exact gap shape without re-reading the handler.

### Updated audit breakdown

  Before this commit:
    covered-via-cache-key:  50   ← contained 3 false-positives
    (no partial-overlap category)

  After this commit:
    covered-via-cache-key:  47   ← only TRULY-fully-covered ops
    partial-cache-key-overlap: 3 ← implementer-decides bucket
    (gold-intelligence, risk-scores, theater-posture)

### 3rd false-positive fix (api/mcp.ts get_conflict_events._apiPaths)

The new partial-overlap surface flagged GET /api/intelligence/v1/get-risk-scores
as another mis-claim. Handler at server/worldmonitor/intelligence/v1/
get-risk-scores.ts:242-256 reads 12 cross-domain keys (conflict + infra +
climate + cyber + wildfires + GPS-jam + OREF + advisories + displacement
+ news) only 3 of which are in get_conflict_events._cacheKeys.

A user calling get_conflict_events via MCP gets conflict events + the
stale risk-scores variant — but NOT the rich cross-domain composite the
risk-scores API returns. Moved to deferred-to-future-tool with hint
pointing at a future expanded_risk_scores composite tool.

Note on theater-posture (3rd partial-overlap row): handler reads 3
variants (live + stale + backup) of the same canonical payload, only the
stale variant is in get_military_posture._cacheKeys. The cascade-mirror
pattern from PR #3658 already documents these as equivalent payloads
(same data, freshness preference). The current _apiPaths claim is valid;
audit's partial-overlap warning is over-cautious here. No change.

### Coverage breakdown (final)

  Before earlier commit: 69 covered / 121 excluded
  After 2 false-positive fixes: 67 / 123
  After this commit (3rd fix):  66 covered / 124 excluded / 190 total
                                deferred-to-future-tool: 49 → 52

### Tests

  21/21 parity tests pass (live + meta)
  73/73 mcp.test.mjs tests pass
  TypeScript clean on both tsconfigs

Future implementers using scripts/audit-mcp-api-coverage.mjs as
`_apiPaths` seed input now see the partial-cache-key-overlap category
explicitly and make an informed extend-vs-defer decision, preventing the
same false-positive class from re-introducing.
koala73 added a commit that referenced this pull request May 11, 2026
Two findings from continued code review:

### P1: theater-posture cascade-mirror equivalence

The audit script's new partial-cache-key-overlap detector correctly
surfaced GET /api/military/v1/get-theater-posture as a partial overlap —
the API handler reads 3 cascade variants (live + stale + backup) while
get_military_posture._cacheKeys only contains the stale variant.

PR #3658's U7 already documented this as cascade-mirror equivalence:
  'theater-posture:sebuf:v1'        → 'cascade-mirror: live counterpart
                                      of theater_posture:sebuf:stale:v1
                                      (covered by get_military_posture)'
  'theater-posture:sebuf:backup:v1' → 'cascade-mirror: backup counterpart
                                      ...'

The cascade keys are MIRRORS — same payload shape, different freshness.
The MCP tool reading only the stale variant is the documented canonical
reader pattern; the data IS structurally served.

Fix: scripts/audit-mcp-api-coverage.mjs gains a CASCADE_MIRROR_EXEMPT
Set that re-routes partial-overlap → covered-via-cache-key for known
cascade-equivalent ops. The set is explicit (METHOD-path entries), not a
generic pattern matcher, so it's auditable and can't accidentally
suppress real false-positives.

api/mcp.ts gets an inline CASCADE-MIRROR EQUIVALENCE comment on
get_military_posture._apiPaths cross-referencing both the audit-script
exemption AND U7's documented cascade-mirror exclusions.

### P2: risk-scores category mismatch

The previous commit moved GET /api/intelligence/v1/get-risk-scores to
deferred-to-future-tool. But the parity test's category docs explicitly
define deferred-to-future-tool as "pure-read with literal key, no
covering tool yet" — and the handler at server/.../get-risk-scores.ts:600
uses cachedFetchJsonWithMeta with ACLED + auxiliary fetches on cache
miss. That's the fetch-on-miss shape, not pure-read.

Re-categorized to fetch-on-miss with paid-upstream secondary (ACLED is
rate-limited external). The cross-domain composite intent (future
expanded_risk_scores tool) is preserved in the reason text as
implementer-hint context.

### Updated breakdown

  Audit script:
    Before:  covered-via-cache-key 47 | partial-overlap 3
    After:   covered-via-cache-key 48 | partial-overlap 2
             (theater-posture promoted via cascade exemption)

  Parity test:
    Before:  fetch-on-miss 30 | deferred-to-future-tool 52
    After:   fetch-on-miss 31 | deferred-to-future-tool 51
             (risk-scores recategorized)

  Coverage:  66 covered / 124 excluded / 190 total (unchanged math)

### Tests

  21/21 parity tests pass (live + meta)
  73/73 mcp.test.mjs tests pass
  TypeScript clean on both tsconfigs

The audit's remaining partial-cache-key-overlap entries (gold-intelligence,
risk-scores) are correctly flagged — the parity test handles both via
deferred-to-future-tool / fetch-on-miss exclusion entries with named
future-tool hints. Future PRs that bundle the deferred entries will move
them from EXCLUDED_FROM_MCP_PARITY into a new tool's _apiPaths.
koala73 added a commit that referenced this pull request May 11, 2026
…3662)

* test(mcp-api-parity): U1 — OpenAPI operation inventory walker

Adds tests/mcp-api-parity.test.mjs scaffolding with the OpenAPI inventory
walker as its first piece. Returns a Set of canonical "METHOD path"
strings (e.g., "GET /api/economic/v1/get-bis-credit"), filtering path
objects through an HTTP-method allowlist so OpenAPI siblings (parameters,
summary, description) don't inflate the count.

Confirmed against real docs/api/: 190 operations across 34 services.

3 live structural tests (count, canonical shape, anchor presence) + 3
fixture-based meta-tests (non-existent dir, sibling filtering, malformed
spec graceful skip). 6/6 pass. Walker treats the OpenAPI path key
opaquely, so /api/v2/<svc>/<op> shapes work without code changes.

U2 (handler audit helper) builds on this in the next commit.

* chore(mcp-api-parity): U2 — handler audit script

Adds scripts/audit-mcp-api-coverage.mjs as a one-shot audit producing
the categorized handler ↔ cache-key inventory that seeds U3's _apiPaths
population + U4's EXCLUDED_FROM_MCP_PARITY map.

The audit walks every OpenAPI operation in docs/api/*.openapi.json,
resolves each to its per-method handler via the service's handler.ts
import map (NOT filename guessing — Codex caught that some OpenAPI op
slugs differ from handler filenames), classifies the handler by its
server/_shared/redis imports, and emits a categorized report:

  190 ops total
    covered-via-cache-key:    50
    covered-via-_execute:     10
    fetch-on-miss:            32
    mutating:                 11
    llm-passthrough:           2  (classify-event, analyze-stock)
    admin:                     0  (as plan predicted)
    manual-mapping:           23  (parameterized/computed keys)
    deferred-to-future-tool:  50  (legitimate gaps awaiting expansion PRs)
    unknown:                  12  (need manual triage in U4)

This is NOT a test-time dependency — the parity test (U5) only reads
the explicit _apiPaths declarations on TOOL_REGISTRY entries and the
EXCLUDED_FROM_MCP_PARITY map, never source code.

Pre-U3 findings flagged during audit (not fixed in this commit):
  - classify-event is declared GET in OpenAPI, not POST (plan was wrong)
  - get_news_intelligence is a cache-tool, not _execute passthrough
  - get_country_brief._execute POSTs to a GET-only OpenAPI endpoint
    (real method-drift bug — surfaces in U3 and worth a PR-followup)

* feat(mcp-api-parity): U3 — _apiPaths required field + 38-tool populate

Adds required `_apiPaths: string[]` field to both `CacheToolDef` and
`RpcToolDef` discriminated-union variants in `api/mcp.ts`. Populates the
field on every one of 38 existing tools in `TOOL_REGISTRY` based on the
U2 audit's `covered-via-cache-key` and `covered-via-_execute` sections.

Final state:
  69 _apiPaths entries across 30 tools with non-empty coverage
   8 tools intentionally empty (bootstrap aggregates whose underlying
     cache keys aren't served by any 1:1 OpenAPI op — get_aviation_status,
     get_cyber_threats, get_country_macro, the 3 EU eurostat tools,
     generate_forecasts, get_commodity_geo). U4 covers their domains'
     orphan API ops via fetch-on-miss / mutating / manual-mapping
     exclusion categories.

4 manual additions beyond the audit (handlers using cachedFetchJson
that the audit's hint logic couldn't statically cross-reference):
  - get_displacement_data → /api/displacement/v1/get-displacement-summary
  - get_positive_events   → /api/positive-events/v1/list-positive-geo-events
  - get_research_signals  → /api/research/v1/list-tech-events
  - get_consumer_prices   → all 6 consumer-prices ops (hybrid coverage)

TypeScript-enforced at compile time on both tsconfig.json and
tsconfig.api.json — future contributors get a compile error when
registering a tool without _apiPaths.

The new field is automatically stripped from `tools/list` MCP output
via the existing `{name, description, inputSchema}` destructure in
TOOL_LIST_RESPONSE. tests/mcp.test.mjs gains a parallel assertion
mirroring the existing _cacheKeys/_execute/_coverageKeys strip checks.

All 89 existing tests pass.

Pre-U4 known issues surfaced during U3:
  - api/mcp.ts get_country_brief._execute POSTs to a GET-only OpenAPI
    endpoint (/api/intelligence/v1/get-country-intel-brief). _apiPaths
    uses the OpenAPI-declared method (GET) — the test's source-of-truth
    is OpenAPI. Method-drift bug worth a separate follow-up PR.
  - classify-event is declared GET in OpenAPI (not POST as plan said).

* test(mcp-api-parity): U4 — EXCLUDED_FROM_MCP_PARITY seed inventory

Adds 121-entry Map<"METHOD path", category-prefixed reason> to
tests/mcp-api-parity.test.mjs covering every OpenAPI operation that
isn't (and shouldn't be) declared in some tool's _apiPaths.

Auto-generated from scripts/audit-mcp-api-coverage.mjs output then
hand-tuned for first-pass coverage. Breakdown:

  mutating:                 11
  llm-passthrough:           2  (classify-event, analyze-stock)
  fetch-on-miss:            30  (every entry carries a closed-allowlist
                                secondary: paid-upstream / high-cardinality-input)
  admin:                     0  (no admin endpoints in current OpenAPI surface,
                                as plan predicted — Pro/Premium ≠ admin)
  manual-mapping:           29  (parameterized cache keys, inline-redis/Convex
                                handlers — covered at prefix level by sibling tools)
  deferred-to-future-tool:  49  (legitimate gaps for follow-up expansion PRs:
                                BIS series, EU-FSI, hyperliquid flow, token
                                panels, CF Radar, etc.)

The "already-covered-by-rpc-tool" secondary is structurally forbidden
(enforced in U5's findForbiddenFetchOnMissSecondaries predicate) — if
an op is covered, it belongs in a tool's _apiPaths, not the exclusion
map. Codex round-2 caught this loophole.

Math: 190 total OpenAPI ops = 69 covered (via _apiPaths) + 121 excluded
(via this map). Parity test (U5) asserts this exact split.

Some "mutating" entries may be more accurately classified as "fetch-on-miss"
(GET handlers that cache writes on miss). U5's test runs will surface any
miscategorizations via concrete failure messages; iterative cleanup is
preferable to over-engineering the auto-classifier.

* test(mcp-api-parity): U5 — 7 live assertions + 11 meta-tests via 8 predicates

Wires the parity assertions in tests/mcp-api-parity.test.mjs that turn the
EXCLUDED_FROM_MCP_PARITY map + TOOL_REGISTRY._apiPaths declarations into a
CI-enforced contract.

7 live structural assertions:
  - every OpenAPI op covered by some tool's _apiPaths OR explicitly excluded
    (fails with remediation message naming both fix paths)
  - every exclusion has non-empty reason starting with one of 6 valid prefixes
  - no dead exclusions (excluded ops absent from live OpenAPI inventory)
  - no dead _apiPaths (declared tool metadata pointing at vanished ops)
  - no bare fetch-on-miss: reason (every entry needs a closed-allowlist
    secondary: paid-upstream / high-cardinality-input / llm-cost)
  - no FORBIDDEN fetch-on-miss: already-covered-by-rpc-tool secondary
    (Codex round-2 loophole-blocker)
  - emits categorized count to CI logs + verifies covered+excluded = total

11 fixture-based meta-tests verify each predicate fires on synthetic
invalid inputs (per Codex round-2 critique of U7's missing meta-tests):
  - collectApiOperations: 3 fixture scenarios (non-existent dir, non-HTTP
    method sibling filtering, malformed-spec graceful skip)
  - collectDeclaredApiPaths: 1 fixture (aggregates across cache + RPC tools)
  - findUncoveredApiOps: 2 fixtures (positive + negative)
  - findEmptyOrUnprefixedReasons: 1 fixture (empty/whitespace/unprefixed)
  - findDeadExclusions: 1 fixture
  - findDeadApiPaths: 1 fixture
  - findBareFetchOnMissReasons: 1 fixture (bare + unknown-secondary + valid)
  - findForbiddenFetchOnMissSecondaries: 1 fixture (loophole catcher)

CI output line on PASS:
  [mcp-api-parity] 69 covered / 121 excluded
    (mutating:11 llm-passthrough:2 fetch-on-miss:30 admin:0
     manual-mapping:29 deferred-to-future-tool:49) / 190 total ops

Test counts: 18/18 pass (this file), 102/102 across all MCP suites.
TypeScript clean on both tsconfig.json and tsconfig.api.json.

The 49 deferred-to-future-tool entries are the actionable inventory for
follow-up coverage PRs (PR-B expanded_economic_data, PR-C expanded_intelligence,
etc.) — heaviest-first per the brainstorm's prioritization frame.

* docs(mcp-server): U6 — Coverage invariants section

Adds 'Coverage invariants' subsection to docs/mcp-server.mdx between the
tool catalog and the JSON-RPC example. Documents the two CI-enforced
parity tests (cache-key parity from PR #3658 U7, API parity from this
PR's U1-U5) and explains the contributor-side process for both:

  - Adding a new MCP tool: declare _apiPaths string[] (required, TypeScript-
    enforced) with canonical "METHOD path" entries
  - Adding a new OpenAPI op not meant to be MCP-callable: add to
    EXCLUDED_FROM_MCP_PARITY with one of 6 category prefixes
    (mutating | llm-passthrough | fetch-on-miss + closed-allowlist
    secondary | admin | manual-mapping | deferred-to-future-tool)

Pre-merge CI verification (all clean):
  - npx biome lint tests/mcp-api-parity.test.mjs api/mcp.ts scripts/
    audit-mcp-api-coverage.mjs → 0 errors
  - npx tsc --noEmit + npx tsc --noEmit -p tsconfig.api.json → clean
  - npx tsx --test tests/mcp.test.mjs tests/mcp-bootstrap-parity.test.mjs
    tests/mcp-api-parity.test.mjs → 102/102 pass

* test+fix(mcp-api-parity): residual Tier-2 review fixes

Four findings from the 3-reviewer Tier-2 pass on the 6-commit Tier-4 diff.
All reviewers verdict: APPROVE to ship, zero BLOCKING. These are
convergent SUGGESTION-level improvements applied before PR open.

### 1. Tmp dir cleanup in mkSpecFixture (all 3 reviewers)
Fixture helper called mkdtempSync without cleanup; every `tsx --test` run
leaked 2 directories in os.tmpdir(). Threaded TestContext.t through the
3 fixture-using meta-tests, registered best-effort `t.after(rmSync)`.

### 2. findDoubleCoveredOps predicate + live assertion + meta-test
  (testing + correctness reviewers)
Double-coverage (an op listed in BOTH some tool's _apiPaths AND
EXCLUDED_FROM_MCP_PARITY) was only caught indirectly by the
sum-equality count check, whose failure message ('covered + excluded
must equal total ops') gave operators no path to find the offender.
Added a dedicated `findDoubleCoveredOps` predicate, a live assertion
with a remediation-shaped failure message, and a fixture-based
meta-test. Coverage is now exclusive — an op is in _apiPaths XOR in
EXCLUDED_FROM_MCP_PARITY, never both.

### 3. Malformed-paths meta-test (testing reviewer)
collectApiOperations has a defensive guard at the paths-level for three
shapes (missing key, paths: null, paths: primitive), but the original
meta-tests only exercised the non-existent-dir + malformed-JSON paths.
Added a single fixture-based meta-test that loops over all three shapes
in one run.

### 4. Method-drift comment on get_country_brief._apiPaths
  (code reviewer)
The tool's _execute body POSTs /api/intelligence/v1/get-country-intel-brief
but OpenAPI declares only GET on that path. The gateway routes by path
not method, so POST works at runtime; _apiPaths uses the OpenAPI-
declared method (GET) per the parity test's source-of-truth contract.
Added inline comment to api/mcp.ts so the next reader doesn't think
the spec was misread.

Test count: 21/21 pass (was 18, +3: 1 new live double-coverage
assertion, 1 new meta-test, 1 new malformed-paths meta-test).

Findings explicitly NOT applied (accepted-risk per commit messages):
  - mutating: vs fetch-on-miss: reclassification (auto-classifier
    accepted as iterative-cleanup-preferred-over-overengineering)
  - em-dash brittleness in findBareFetchOnMissReasons (defensive code,
    no current failures)
  - audit-script EXECUTE_PASSTHROUGH_FETCHES hand-maintained drift
    (one-shot script, low decay risk)

* fix(mcp-api-parity): correct two false-positive _apiPaths coverage claims

Code-review-expert pass surfaced two P1 false-positives in the
auto-generated _apiPaths declarations — handlers that read MULTIPLE
cache keys where only ONE key overlapped with the claimed tool's
_cacheKeys. The audit script's hint logic was too permissive on
single-key overlap, causing the parity test to claim coverage that
doesn't exist at the response-payload level.

### get-gold-intelligence (was in get_market_data._apiPaths)
Handler reads 5 keys at server/worldmonitor/market/v1/get-gold-intelligence.ts:189-194:
  - market:commodities-bootstrap:v1  ← only key shared with get_market_data
  - COT_KEY                          ← uncovered
  - GOLD_EXTENDED_KEY                ← uncovered
  - GOLD_ETF_FLOWS_KEY               ← uncovered
  - GOLD_CB_RESERVES_KEY             ← uncovered

A user calling get_market_data via MCP gets headline commodity quotes
but NOT COT positioning, gold-extended panels, ETF flows, or CB
reserves that the API endpoint serves. Moved to deferred-to-future-tool.

### get-consumer-price-basket-series (was in get_consumer_prices._apiPaths)
Handler reads one key at server/worldmonitor/consumer-prices/v1/get-consumer-price-basket-series.ts:22:
  - consumer-prices:basket-series:${market}:${basket}:${range}

This parameterized time-series key is NOT in get_consumer_prices._coverageKeys
(which only carries overview/categories/movers/spread/freshness). Zero
overlap — the original claim was an outright false-positive. Moved to
deferred-to-future-tool.

### Updated coverage breakdown

  Before:  69 covered / 121 excluded / 190 total
  After:   67 covered / 123 excluded / 190 total
           (deferred-to-future-tool: 49 → 51)

Inline NOTE comments added to both tools' _apiPaths declarations
explaining why each op is intentionally excluded, so a future reader
doesn't re-add them.

21/21 parity + 73/73 mcp tests pass. TypeScript clean.

Future PR-B / PR-C plans should include expanded_commodities (COT +
gold-extended + ETF-flows + CB-reserves) and expanded_consumer_prices
(basket-series) as named work-list items.

* fix(mcp-api-parity): audit script — distinguish full vs partial cache-key overlap (+ 3rd false-positive)

The previous audit logic classified an API op as `covered-via-cache-key`
when ANY of the handler's cache keys overlapped with ANY tool's
_cacheKeys, even if the handler also read 4+ uncovered keys. This is the
trap that produced the get-gold-intelligence and basket-series false-
positives in the prior commit, and a third one (get-risk-scores) it now
surfaces.

### Audit script logic fix (scripts/audit-mcp-api-coverage.mjs)

Per-op cross-reference now partitions the handler's cache keys into
(covered, missing) sets per candidate tool, then classifies:

  - fullyCoveredByCacheKey      handler keys ⊆ tool's _cacheKeys
  - partiallyCoveredByCacheKey  some shared, some uncovered (per tool)

categorize() routes:

  - any fully-covered tool       → 'covered-via-cache-key' (unchanged semantics)
  - only partial overlap         → NEW 'partial-cache-key-overlap' category
  - no overlap (pure-read)       → 'manual-mapping' or 'deferred-to-future-tool'
                                   (unchanged)

The partial-cache-key-overlap category is the explicit handoff to the
implementer: decide between (a) extending the tool's _cacheKeys to cover
the missing keys, vs (b) excluding as deferred-to-future-tool with the
specific receiving-tool hint.

Hint output now prints per-tool partial-overlap detail:
  "partial overlap with <tool>: covered=[k1,k2] missing=[k3,k4,k5]"

so a reader sees the exact gap shape without re-reading the handler.

### Updated audit breakdown

  Before this commit:
    covered-via-cache-key:  50   ← contained 3 false-positives
    (no partial-overlap category)

  After this commit:
    covered-via-cache-key:  47   ← only TRULY-fully-covered ops
    partial-cache-key-overlap: 3 ← implementer-decides bucket
    (gold-intelligence, risk-scores, theater-posture)

### 3rd false-positive fix (api/mcp.ts get_conflict_events._apiPaths)

The new partial-overlap surface flagged GET /api/intelligence/v1/get-risk-scores
as another mis-claim. Handler at server/worldmonitor/intelligence/v1/
get-risk-scores.ts:242-256 reads 12 cross-domain keys (conflict + infra +
climate + cyber + wildfires + GPS-jam + OREF + advisories + displacement
+ news) only 3 of which are in get_conflict_events._cacheKeys.

A user calling get_conflict_events via MCP gets conflict events + the
stale risk-scores variant — but NOT the rich cross-domain composite the
risk-scores API returns. Moved to deferred-to-future-tool with hint
pointing at a future expanded_risk_scores composite tool.

Note on theater-posture (3rd partial-overlap row): handler reads 3
variants (live + stale + backup) of the same canonical payload, only the
stale variant is in get_military_posture._cacheKeys. The cascade-mirror
pattern from PR #3658 already documents these as equivalent payloads
(same data, freshness preference). The current _apiPaths claim is valid;
audit's partial-overlap warning is over-cautious here. No change.

### Coverage breakdown (final)

  Before earlier commit: 69 covered / 121 excluded
  After 2 false-positive fixes: 67 / 123
  After this commit (3rd fix):  66 covered / 124 excluded / 190 total
                                deferred-to-future-tool: 49 → 52

### Tests

  21/21 parity tests pass (live + meta)
  73/73 mcp.test.mjs tests pass
  TypeScript clean on both tsconfigs

Future implementers using scripts/audit-mcp-api-coverage.mjs as
`_apiPaths` seed input now see the partial-cache-key-overlap category
explicitly and make an informed extend-vs-defer decision, preventing the
same false-positive class from re-introducing.

* fix(mcp-api-parity): cascade-mirror exemption + recategorize risk-scores

Two findings from continued code review:

### P1: theater-posture cascade-mirror equivalence

The audit script's new partial-cache-key-overlap detector correctly
surfaced GET /api/military/v1/get-theater-posture as a partial overlap —
the API handler reads 3 cascade variants (live + stale + backup) while
get_military_posture._cacheKeys only contains the stale variant.

PR #3658's U7 already documented this as cascade-mirror equivalence:
  'theater-posture:sebuf:v1'        → 'cascade-mirror: live counterpart
                                      of theater_posture:sebuf:stale:v1
                                      (covered by get_military_posture)'
  'theater-posture:sebuf:backup:v1' → 'cascade-mirror: backup counterpart
                                      ...'

The cascade keys are MIRRORS — same payload shape, different freshness.
The MCP tool reading only the stale variant is the documented canonical
reader pattern; the data IS structurally served.

Fix: scripts/audit-mcp-api-coverage.mjs gains a CASCADE_MIRROR_EXEMPT
Set that re-routes partial-overlap → covered-via-cache-key for known
cascade-equivalent ops. The set is explicit (METHOD-path entries), not a
generic pattern matcher, so it's auditable and can't accidentally
suppress real false-positives.

api/mcp.ts gets an inline CASCADE-MIRROR EQUIVALENCE comment on
get_military_posture._apiPaths cross-referencing both the audit-script
exemption AND U7's documented cascade-mirror exclusions.

### P2: risk-scores category mismatch

The previous commit moved GET /api/intelligence/v1/get-risk-scores to
deferred-to-future-tool. But the parity test's category docs explicitly
define deferred-to-future-tool as "pure-read with literal key, no
covering tool yet" — and the handler at server/.../get-risk-scores.ts:600
uses cachedFetchJsonWithMeta with ACLED + auxiliary fetches on cache
miss. That's the fetch-on-miss shape, not pure-read.

Re-categorized to fetch-on-miss with paid-upstream secondary (ACLED is
rate-limited external). The cross-domain composite intent (future
expanded_risk_scores tool) is preserved in the reason text as
implementer-hint context.

### Updated breakdown

  Audit script:
    Before:  covered-via-cache-key 47 | partial-overlap 3
    After:   covered-via-cache-key 48 | partial-overlap 2
             (theater-posture promoted via cascade exemption)

  Parity test:
    Before:  fetch-on-miss 30 | deferred-to-future-tool 52
    After:   fetch-on-miss 31 | deferred-to-future-tool 51
             (risk-scores recategorized)

  Coverage:  66 covered / 124 excluded / 190 total (unchanged math)

### Tests

  21/21 parity tests pass (live + meta)
  73/73 mcp.test.mjs tests pass
  TypeScript clean on both tsconfigs

The audit's remaining partial-cache-key-overlap entries (gold-intelligence,
risk-scores) are correctly flagged — the parity test handles both via
deferred-to-future-tool / fetch-on-miss exclusion entries with named
future-tool hints. Future PRs that bundle the deferred entries will move
them from EXCLUDED_FROM_MCP_PARITY into a new tool's _apiPaths.

* fix(mcp-api-parity): address 3 greptile-apps P2 review comments

### P2.1: Recategorize leads writes from manual-mapping to mutating
greptile-apps flagged that POST /api/leads/v1/register-interest and
POST /api/leads/v1/submit-contact were tagged `manual-mapping:` but
their reasons explicitly said they write to Convex. The manual-mapping
category is documented as "parameterized cache key not statically
resolvable; equivalent data covered by sibling tool" — wrong fit for
write endpoints. A contributor scanning the exclusion list by category
to audit write endpoints would have missed these two.

Recategorized to `mutating: writes to Convex (not server/_shared/redis)
— [lead registration write|contact form write]`. Moved physical
location into the mutating: section for readability. Counts:
manual-mapping 29 → 27, mutating 11 → 13.

### P2.2: Split count-invariant assertion into its own it() block
greptile-apps flagged that `declared.size + EXCLUDED.size === apiOps.size`
was buried inside the "emits a categorized count report" test. A future
failure would look like a CI logging format problem when it's actually
the math-correctness invariant.

Extracted into a dedicated `it('covered + excluded ops must equal total
OpenAPI op count')` block. Failure message now names the LHS/RHS values
so the offending op is findable via the existing findUncoveredApiOps /
findDoubleCoveredOps assertions. Reporting test keeps the console.log
emission only — no assertion side-effect.

### P2.3: Tighten RpcToolDef._apiPaths comment
greptile-apps flagged that the comment said empty _apiPaths is "valid
for tools that don't hit any HTTP endpoint" — misleading because
`generate_forecasts` DOES hit `/api/forecast/v1/get-forecasts` (POST),
empty only because the spec declares GET. A future contributor could
read the comment and leave `_apiPaths: []` on a tool whose endpoint IS
in the spec, silently creating a coverage gap.

Tightened comment to enumerate the TWO valid empty-array cases:
  (a) Tool hits no HTTP endpoint (e.g. get_commodity_geo static JSON)
  (b) Tool's _execute method drifts from OpenAPI AND a sibling tool
      with matching method already covers the spec-declared op
      (e.g. generate_forecasts POST + get_forecast_predictions GET)
Plus an explicit "MUST list" reminder for the common case.

### Verification

  21/21 parity tests pass (now includes dedicated count-invariant test)
  73/73 mcp.test.mjs tests pass
  TypeScript clean on both tsconfigs

  Coverage breakdown:
    66 covered / 124 excluded / 190 total
    (mutating:13 llm-passthrough:2 fetch-on-miss:31 admin:0
     manual-mapping:27 deferred-to-future-tool:51)

* fix(mcp-api-parity): audit script — partial-overlap is pure-read only

greptile-apps follow-up P2: the audit's partial-cache-key-overlap check
fired BEFORE the structural-classification checks (fetch-on-miss /
mutating / llm-passthrough), so any fetch-on-miss handler with
incidental shared keys got bucketed as partial-overlap. That made the
audit disagree with the parity test on GET /api/intelligence/v1/get-risk-scores —
audit put it in partial-cache-key-overlap; parity test correctly classified
it as fetch-on-miss: paid-upstream.

Fix: move the structural-classification checks (llm-passthrough,
mutating, fetch-on-miss) BEFORE the partial-overlap check, AND restrict
partial-overlap to `classification === 'pure-read'`. The handler's
structural shape is the dominant signal — fetch-on-miss + incidental
cache-key overlap is NOT a coverage gap, just upstream-fetch behavior
that happens to share a key with some tool.

Audit breakdown after:
  partial-cache-key-overlap: 2 → 1  (only gold-intelligence; pure-read)
  fetch-on-miss:             32 → 33 (risk-scores moved here)

Audit + parity test now agree:
  - covered-via-_execute (highest precedence — direct API proxy)
  - covered-via-cache-key (full coverage, including cascade-exempt)
  - llm-passthrough / mutating / fetch-on-miss (structural shape wins
                                                 over cache-key proximity)
  - partial-cache-key-overlap (pure-read only — real implementer-decide)
  - manual-mapping / deferred-to-future-tool (pure-read residual)

The category-doc comment block above already lists fetch-on-miss BEFORE
manual-mapping; the code ordering now matches the documentation.

21/21 parity tests still pass. TypeScript clean.
koala73 added a commit that referenced this pull request May 11, 2026
…nners

The MCP server-card was previously minimal (21 lines, just serverInfo +
transport + capabilities + auth). Discovery scanners like Cloudflare MCP,
isitagentready.com, and Claude Desktop's connector picker get richer
listings when the card includes protocol version, tool count, doc
links, rate limits, and tags. None of those fields existed before.

### New fields

- `serverInfo.version` bumped 1.0 → 1.1.0 reflecting the Tier-1+2
  expansion (6 new tools landed via PR #3658) and the Tier-4 parity
  invariant (PR #3662)
- `serverInfo.description` — 1-line summary surfaced by scanners
- `serverInfo.vendor`, `serverInfo.homepage` — branding
- `protocolVersion: "2025-03-26"` — MCP spec version (was implicit;
  scanners check this before connecting)
- `tools.count: 38` — at-a-glance breadth without forcing a tools/list
- `tools.categories` — 8 domain categories matching docs/mcp-server.mdx
  Tool catalog grouping
- `authentication.alternative_methods` — surfaces the wm_live_... key
  path (X-WorldMonitor-Key header) for API-tier subscribers
- `documentation.overview` → /docs/mcp-server
- `documentation.toolReference` → /docs/mcp-tools-reference (new in
  this PR)
- `documentation.apiReference` → /docs/api-reference
- `rateLimits` block — perMinute + dailyByPlan + clarifying note
  that tools/list and initialize don't count against daily quota
- `tags` — discovery-index keywords
- `metadata.toolListNote` — points clients at runtime tools/list as
  the authoritative source

Kept static (Vercel-served from public/.well-known/). Unlike
oauth-protected-resource.ts which is host-dynamic, this card uses
absolute URLs throughout, so single-host consistency isn't a problem.

Tool count of 38 is hand-set and will need a bump if TOOL_REGISTRY
gains/loses tools. Could automate via a build step, but the field is
discovery-only (not a contract), so manual sync is fine for now.
koala73 added a commit that referenced this pull request May 11, 2026
* docs(mcp-server): user-facing API coverage table

Replaces the contributor-facing "Coverage invariants" subsection (added
on PR #3662 U6 as a brief mention of the parity tests + EXCLUDED_FROM_MCP_PARITY
internals) with a user-facing "API coverage" reference table mapping
every MCP tool to the REST/RPC endpoints it serves.

The previous subsection covered parity-test mechanics + the _apiPaths
field + the 6 exclusion category prefixes — contributor-only detail. A
developer choosing which MCP tool to call doesn't need any of that.

The new table is direct: for each of 30 tools with public API coverage,
list the canonical "METHOD path" entries. Two use cases:
  - Forward: "I want X data via MCP → which tool delivers it?"
  - Reverse: "I know /api/economic/v1/get-bis-credit — Cmd/Ctrl+F the
    table for the MCP equivalent."

The remaining 8 of 38 tools are bootstrap-aggregate cache tools that
return live data via tools/call but don't 1:1 map to a single API
endpoint (e.g. get_aviation_status reads aviation:delays-bootstrap:v1).
Documented in a trailing note pointing back at the Tool catalog.

No mention of deferred-to-future-tool gaps, internal parity-test
mechanics, or the EXCLUDED_FROM_MCP_PARITY exclusion map — those are
contributor-only concerns and remain documented in
tests/mcp-api-parity.test.mjs comments.

* docs(mcp): per-tool reference page with params, freshness, timeouts, examples

Adds docs/mcp-tools-reference.mdx — a complete per-tool reference for
all 38 MCP tools. Each tool section carries:

- Full description (from inputSchema description in api/mcp.ts)
- Parameters table: name | type | required | description (extracted
  from inputSchema.properties; enum values inline)
- API endpoints served (from _apiPaths; cross-links to API Reference)
- Kind: cache read / hybrid / live RPC (with edge-runtime timeout
  when applicable)
- Freshness budget: humanized (e.g. "30 min", "8 h", "60 d") from
  _maxStaleMin
- Concrete curl example with realistic sample arguments

Grouped by domain (Markets & economy, Energy, Geopolitical & security,
Movement & infrastructure, Environment & science, Health, Humanitarian
& displacement, AI intelligence) mirroring the Tool catalog in the MCP
Server overview.

Generated programmatically from TOOL_REGISTRY metadata in api/mcp.ts
(see scripts/audit-mcp-api-coverage.mjs for a related reading approach).
The generator is one-shot at /tmp; future regenerations can re-run it
when tools are added/changed.

### Linked from

- docs/mcp-server.mdx Tool catalog now carries a Tip block pointing
  at the reference page.
- docs/docs.json registers the new page in the "MCP & Integrations"
  group right after mcp-server (so it appears as the natural
  next-step doc).

### What's NOT documented yet

- Per-tool response shapes (the exact JSON payload structure each
  tool returns). The MCP content-block envelope is documented at the
  top of the reference page; per-tool field-level shapes belong in
  the underlying API Reference docs at /api-reference and are
  reachable from each tool's "API endpoints" bullet.
- Sample responses with concrete data (would need to be hand-written
  per tool from live tools/call invocations).

These are intentionally deferred — the parameter / freshness /
endpoint / curl-example layer addresses the largest doc-completeness
gap (params especially: 12 of 38 tools have non-empty inputSchema with
required params that were previously only discoverable via runtime
tools/list).

### Tool count: 38 (unchanged)
Lines: 1,039 (new file)

* docs(mcp): enrich /.well-known/mcp/server-card.json for discovery scanners

The MCP server-card was previously minimal (21 lines, just serverInfo +
transport + capabilities + auth). Discovery scanners like Cloudflare MCP,
isitagentready.com, and Claude Desktop's connector picker get richer
listings when the card includes protocol version, tool count, doc
links, rate limits, and tags. None of those fields existed before.

### New fields

- `serverInfo.version` bumped 1.0 → 1.1.0 reflecting the Tier-1+2
  expansion (6 new tools landed via PR #3658) and the Tier-4 parity
  invariant (PR #3662)
- `serverInfo.description` — 1-line summary surfaced by scanners
- `serverInfo.vendor`, `serverInfo.homepage` — branding
- `protocolVersion: "2025-03-26"` — MCP spec version (was implicit;
  scanners check this before connecting)
- `tools.count: 38` — at-a-glance breadth without forcing a tools/list
- `tools.categories` — 8 domain categories matching docs/mcp-server.mdx
  Tool catalog grouping
- `authentication.alternative_methods` — surfaces the wm_live_... key
  path (X-WorldMonitor-Key header) for API-tier subscribers
- `documentation.overview` → /docs/mcp-server
- `documentation.toolReference` → /docs/mcp-tools-reference (new in
  this PR)
- `documentation.apiReference` → /docs/api-reference
- `rateLimits` block — perMinute + dailyByPlan + clarifying note
  that tools/list and initialize don't count against daily quota
- `tags` — discovery-index keywords
- `metadata.toolListNote` — points clients at runtime tools/list as
  the authoritative source

Kept static (Vercel-served from public/.well-known/). Unlike
oauth-protected-resource.ts which is host-dynamic, this card uses
absolute URLs throughout, so single-host consistency isn't a problem.

Tool count of 38 is hand-set and will need a bump if TOOL_REGISTRY
gains/loses tools. Could automate via a build step, but the field is
discovery-only (not a contract), so manual sync is fine for now.

* fix(mcp-docs): address fresh-eyes review findings

Five fixes from a fresh-eyes review of PR #3664:

### 1. get_consumer_prices example country_code "US" → "AE" (P2)
The example in mcp-tools-reference.mdx:219 used "US" but the tool only
supports "AE" — users copying the curl would hit "Country not yet
supported. Available: ae". Switched to "AE".

### 2. get_world_brief + get_country_brief timeout budgets (P3)
Generator captured only the FIRST AbortSignal.timeout(...) in each
_execute body, understating the worst-case total:
  - get_world_brief: shown "6.0s" → actual ~24s (6s digest + 18s LLM)
  - get_country_brief: shown "2.0s" → actual ~24s (2s context + 22s brief)
Both tools chain two sequential fetches. Updated docs to show the
worst-case total budget with the breakdown.

### 3. "Every MCP tool wraps..." softening (P3)
docs/mcp-server.mdx:250 claimed "Every MCP tool wraps one or more
REST/RPC endpoints" but the API coverage table only covers 30 of 38
tools (8 are bootstrap-aggregate cache reads with no 1:1 API path).
Reworded to "Most MCP tools wrap one or more..." with a pointer to the
38-tool MCP Tools Reference for the complete set.

### 4. SERVER_VERSION drift between card and runtime initialize (P3 / B1)
The server-card was bumped 1.0 → 1.1.0 in the previous commit but
api/mcp.ts:35 SERVER_VERSION still returned '1.0' from initialize. A
discovery scanner that cross-checks would see two different versions
for the same server. Bumped SERVER_VERSION to '1.1.0' with an inline
comment cross-referencing the server-card for future maintainers.
(api/mcp-proxy.js clientInfo strings stay '1.0' — those are the proxy
identifying itself AS A CLIENT to upstream MCP servers, different
semantic.)

### 5. server-card rateLimits.notes wording (S1)
Old text only said tools/list and initialize are exempt from per-day
quota, but didn't clarify that they DO count toward the per-minute
60/min throttle. A scanner running a tight discovery probe loop on
tools/list could trip the throttle and misread it as a quota issue.
Tightened wording to enumerate the per-day-exempt methods (initialize,
tools/list, ping, notifications/initialized) and explicitly state that
per-minute counts ALL methods.

### Findings deferred (server-card reviewer, intentionally accepted)

- S2: apiBusiness quota disclosure — the same 10,000/day number is
  already public in docs/mcp-server.mdx Plans & limits table, so the
  card is consistent. Hiding it here would create inconsistency.
- S5: split-origin OAuth claim — the static server-card uses different
  origins for resource (apex) vs authorization_servers (api subdomain)
  while the dynamic /oauth-protected-resource endpoint keeps them
  same-origin. RFC 9728 §3 permits the split; the dynamic pattern
  exists for stricter scanner constraints on protected-resource
  specifically, not on server-cards.
- B2: transport.type wire string "streamableHttp" — MCP spec doesn't
  codify the field, no specific scanner-rejection evidence. Leaving as
  camelCase until a real scanner rejects it.

### Tests + lint

  npx tsc --noEmit -p tsconfig.api.json clean
  Server-card JSON parses cleanly via jq

* fix(mcp-docs): apply round-2 reviewer findings

Continued fresh-eyes review surfaced 4 pre-existing accuracy bugs in
mcp-server.mdx + 4 gaps in the new reference doc. All fixed.

### S4 — pre-existing accuracy bugs in mcp-server.mdx

These were live BEFORE this PR but the reviewer flagged that since the
PR is editing the very file these bugs live in, and serves the same
audience the bugs mislead, they belong in scope.

1. `get_airspace` catalog row said `iso2` / `filter` — actual schema is
   `country_code` / `type` (api/mcp.ts:978). Fixed.
2. `get_maritime_activity` catalog row said `iso2` — actual is
   `country_code`. Fixed.
3. JSON-RPC example used `{"iso2":"IR"}` for get_country_risk — actual
   required field is `country_code` (api/mcp.ts:838). A user copying
   the example got a 400-class error.
4. Response-shape description said `_meta with fetchedAt and staleness`
   — actual fields are `cached_at` (ISO timestamp) and `stale` (boolean)
   per api/mcp.ts:1340-1378. The new mcp-tools-reference.mdx used the
   correct naming; the sibling doc still had the old. Now consistent.

### S3 — get_positive_events category mismatch

mcp-server.mdx Tool catalog put it under Geopolitical & security (the
live, published categorization). My generator's CATEGORY map put it
under Humanitarian & displacement in the new reference doc. Moved the
section in mcp-tools-reference.mdx to match the published canon —
both docs now agree.

### S1 — $WM_KEY setup preamble

mcp-tools-reference.mdx has 38 curl examples all using $WM_KEY but the
variable was never defined in the file. Added a setup snippet near the
top with both the API-key export AND the bearer-token alternative.
Users landing on a per-tool section via deep link now see the auth
context immediately.

### S2 — missing/misleading freshness budgets

- `get_consumer_prices` (hybrid `_execute`) had no freshness budget
  shown — the generator's regex only matched `_maxStaleMin`, missed
  the per-slice `_freshnessChecks` array. Added "up to 25 h per slice
  (24 h cron + 1 h grace)" reflecting api/mcp.ts:893-897.
- `get_chokepoint_status` showed "30 min" which is the SMALLEST per-
  slice budget — bundle includes data up to 400 days old (static
  baselines). Rewrote as a per-slice breakdown so users don't assume
  ALL the data is 30-min-fresh. The bundle's `cached_at` already
  reflects the oldest contributing seed; this just explains it.

### Findings deferred

- S5 (drift-guard tests in tests/deploy-config.test.mjs for
  tools.count and protocolVersion): real value but expands scope into
  test infrastructure. Worth its own PR.
- S6 (the "Recently added (May 2026)" Tip will rot): tag the team to
  remove by end of Q3-2026.
- S7 (1.1.0 bump is cosmetic): already addressed in the previous
  commit — SERVER_VERSION in api/mcp.ts now also reads "1.1.0", so the
  bump now propagates through runtime `initialize` responses.
- S8 (disclaimer wording for tools-not-in-coverage-table): low-impact
  precision issue, leaving as-is.
- N1 (vercel.json non-IANA rel): pre-existing, not from this PR.
- N2 (single-child CodeGroup): generator-template-level decision, fine
  for now since the tab UI still renders the language label as a chip.

* fix(mcp-docs): round-3 accuracy fixes — 3 doc/reality mismatches

### P2.1 — generate_forecasts wrongly described as bootstrap-aggregate

Previous text: "API endpoints: none directly — reads from a bootstrap-
aggregate cache key (no 1:1 REST endpoint)."

Reality (api/mcp.ts:1175): the _execute body POSTs `${base}/api/forecast/v1/get-forecasts`
on every call. The OpenAPI spec declares only GET on that path (which
is covered by get_forecast_predictions). The POST is a method-drift
sibling — runtime works, spec doesn't list it.

Fixed: "no public OpenAPI row; runtime proxies POST /api/forecast/v1/get-forecasts
(the OpenAPI spec only declares GET on that path, which is covered by
get_forecast_predictions — this tool's POST variant runs a fresh
forecast)."

### P3.2 — get_commodity_geo wrongly described as cache/RPC

Previous text: "Kind: live RPC — proxies a fetch to the WorldMonitor API
on each call."

Reality (api/mcp.ts:1286): the _execute body just filters the bundled
`MINING_SITES_RAW` constant — no Redis, no HTTP fetch, runs entirely
from the edge bundle's in-memory data.

Fixed: "Kind: static registry — filters the bundled MINING_SITES_RAW
constant (in-memory, ships with the MCP server's edge bundle). Sub-
millisecond, no upstream call. The dataset updates only when the MCP
server is redeployed with a refreshed registry."

### P3.3 — Server identifier line drift in mcp-server.mdx:27

The previous commit bumped SERVER_VERSION in api/mcp.ts (1.0 → 1.1.0)
and the server-card.json version field, but missed this one human-
readable mention. A scanner / human reader could still see v1.0 here
while the runtime initialize returns v1.1.0. Aligned.

All three fixes are correctness-of-prose changes; no underlying code or
behavior changes. CI should re-run cleanly.

* fix(mcp-docs): broaden API coverage table disclaimers to cover all 3 omission categories

Previous text in the API coverage section's intro (mcp-server.mdx:250)
and trailing note (mcp-server.mdx:285) framed table-omitted tools as
"a handful of cache-backed bootstrap aggregates" — accurate for most
of the 8 omitted tools but not all. The omitted set actually contains
three distinct categories, and the round-3 reviewer correctly flagged
that the disclaimer doesn't cover get_commodity_geo (static in-memory
registry) or generate_forecasts (live POST without public OpenAPI row).

Rewrote both:

- The intro now points readers at the trailing note for the categories
  instead of asserting a single shape.
- The trailing note now enumerates all three buckets with named-example
  tools per category:

  1. Cache-backed bootstrap aggregates — Redis keys seeded by Railway
     crons (get_aviation_status, get_cyber_threats, get_country_macro,
     the 3 EU Eurostat tools).
  2. Static in-memory registries — filter a constant bundled with the
     edge binary, no upstream call (get_commodity_geo).
  3. Live tools without a public OpenAPI row — runtime proxies an HTTP
     call whose method drifts from the public spec, where a sibling
     tool already covers the spec-declared method (generate_forecasts
     POSTs /api/forecast/v1/get-forecasts while get_forecast_predictions
     owns the GET).

This is purely a prose-accuracy fix; no underlying behavior or other
docs are affected. Pairs cleanly with the round-3 per-tool fixes for
generate_forecasts (mcp-tools-reference.mdx:1006) and get_commodity_geo
(mcp-tools-reference.mdx:245).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant