feat(mcp): Tier-1+2 coverage — 6 new tools + parity guard (38 total)#3658
Conversation
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
…ational debt (U5)
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)
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Greptile SummaryThis 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
Confidence Score: 4/5Safe 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
|
…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.
…-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.
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.
…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.
…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.
* 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).
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
` key (NOT `api/health.js`'s drifted shape).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.
Docs (U8)
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:
Findings explicitly accepted (not patched)
Tests
Deploy notes
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
🤖 Generated with Claude Code