Sentinel Signal

MCP Verify — Product architecture, dedicated /search route implementation

Source: docs/mcp-verify-product-architecture-track2-search-route-implementation.md

Document Content

MCP Verify — Product architecture, dedicated /search route implementation

Target release: 1.0.540.

Source: Track 2's deferred registry/search split (doc §12, Requirement R2, Phase 4), resumed after the user supplied the full 56-section source doc and answered four scoping questions for the remaining phases:

  • Phases 5 (server-profile tier regrouping), 9 (analytics rename), 10
  • (compare-telemetry aggregation): skip this round — no detail beyond a one-line name was available before the full doc arrived, and once it did, Phase 5's request (Decision/Risks/Compatibility/Evidence/ Governance/History/Technical groups, suppress empty sections) is already substantially satisfied by the existing tier restructure (TASK-09, 5 tiers/43 subsections, shipped 2026-08-04) — deferred for a dedicated audit against the doc's exact 7-group model rather than redone blind.

  • Registry/search split: **additive /search page** — keep / exactly
  • as today, add /search as a new dedicated page, cross-link both.

  • Methodology split: **propose the 3-way boundary from the current page's
  • content**, for a separate implementation pass.

  • Deploy cadence: one phase at a time, each with its own commit/test/
  • deploy/verify cycle.

This document covers the registry/search phase only.

What the doc actually requires (§12, Phase 4)

  • Requirement R1: homepage search stays prominent.
  • Requirement R2: the full registry should live primarily under
  • /search — avoid it dominating the homepage's lower half.

  • Requirement R3: homepage keeps a small sample (5-10 results) plus a
  • "Browse all MCP servers" link.

The user's explicit scope choice ("keep / exactly as today") is a smaller, lower-risk step than R2's literal "primarily" language — Phase 4 in full would also shrink the homepage's own filter panel. That's deliberately left for a possible follow-up once /search has been live and cross-linked for a while; this phase ships the destination page and wires it into navigation without touching /'s own content or result count, matching doc §45's "implement incrementally" instruction and this engagement's established staged-rollout pattern.

Implementation

  • **render_index_page (main.py) gained a page_mode: str = "index"
  • parameter.** No route previously called it with anything but the default, so this is additive. A page_path = "/search" if page_mode == "search" else "/" local drives every route-relative reference in the function: the SEO title/description/canonical URL/structured-data SearchAction target, the filter form's action, the "Clear" link, the two result-scope chip links (?all_servers=1, ?show_metadata_only=1), and the closing analytics script's route_pattern.

  • **New hero_marketing_html branch.** In page_mode="search", this is
  • a compact eyebrow + <h1>Search MCP servers</h1> + one-line description + site nav + footer links — no hero CTA row, no lifecycle section, no coverage-stats panel (those stay homepage-only, matching the doc's own intent that /search is a focused utility page, not a second marketing surface). In page_mode="index" (default), this is byte-for-byte the previous homepage hero content, unchanged.

  • **New cross_link_html**: on /, a line inside the existing search
  • fieldset — "Prefer the dedicated search experience? Open full search →" — pointing at /search. On /search, the mirror — "Looking for the homepage? Back to Verify home." This is the "cross-linked from both" half of the user's scope decision; nothing else on / changed.

  • Route registration: the existing index() handler in main.py
  • (the single function backing /, ~500 lines of filter parsing, default-listing logic, commerce-filter handling, and two-tier caching) is now also registered at /search via a second @app.get decorator on the same function — zero logic duplication. A page_mode = "search" if request.url.path == "/search" else "index" local at the top of the handler threads through to both render_index_page call sites (the cache-hit path's build_index_page_for and the cache-miss-fallback path's build_fast_index_page).

  • Cache-key route-scoping (real bug avoided, not just theoretical):
  • the handler's two cache-key f-strings (site-{VERSION}:filtered:.../site-{VERSION}:default:...) did not vary by path before this change — / and /search requests with identical filter state would have shared one cache entry and served whichever page rendered first to both URLs. Both keys now include a {page_mode} segment. The startup prewarm write for the bare-/ index page (_prewarm_default_route_caches, main.py:16581+) used the pre-page-mode key format and would have silently become an orphaned, never-matched cache entry under the new scheme — the exact bug class a much earlier round (TASK-08b) fixed for build-SHA scoping. Caught before commit and fixed in the same pass by adding the matching :index: segment to the prewarm's key.

  • Primary navigation: MAIN_NAV_ITEMS's ("Search", "/") entry
  • (the 5-item, test-enforced primary nav from the R13 remediation round) now points at ("Search", "/search"). This is the single highest- leverage cross-link — every page rendering site_nav_html now sends its "Search" nav item to the dedicated page — without touching /'s own content, and without exceeding the existing 5-item cap (test_r13_primary_navigation_is_capped_at_five_items still passes unmodified, since it only asserts on labels).

  • Sitemap: /search added to _build_static_sitemap_entries
  • (main.py:2894), the single source for /sitemap.xml.

Deliberately unchanged

  • /'s hero copy, CTA row (still "Search MCP servers" anchoring to the
  • inline #search-mcp-servers form, unchanged), lifecycle section, coverage-stats panel, default result count/limit, and filter-panel size — none of it moved. This is what "keep / exactly as today" was scoped to mean.

  • No redirect was added from any old /?filter=... URL to /search —
  • both remain independently valid, fully-featured registry views. This matches the "additive" (not "full IA move") option chosen, and avoids a real SEO/bookmark risk the more aggressive option would have carried.

Tests

  • verify/tests/test_api.py::test_arch_track2_search_route_is_dedicated_registry_page_home_stays_unchanged
  • (new): asserts /search renders 200 with the compact header, the filter form's action="/search", no hero/lifecycle/coverage-stats content, and the reciprocal "Back to Verify home" link; asserts / renders unchanged (hero copy, anchor CTA, action="/", lifecycle section all still present) plus the new "Open full search" cross-link; asserts the primary nav's "Search" item resolves to /search on both pages; asserts /sitemap.xml lists /search.

  • Manually verified via a throwaway build_test_client() script (not
  • committed) before writing the assertions above, to confirm the actual rendered behavior matched the design rather than writing tests against assumptions.

  • PYTHONPATH=verify/src pytest verify/tests -q — 492 passed (up from
  • 491), 1 new.

  • python3 scripts/export_openapi.py --check — HTML page routes are not
  • part of the OpenAPI schema at all (only JSON API routes are), so no content diff is expected from this change beyond the version bump itself; regenerated and synced separately.

Remaining Track 2 phases — still not started

Methodology 3-way split (next phase, doc §25-28/Phase 7 — this session's proposal is pending in a follow-up commit), server-profile tier audit against the doc's exact 7-group model (Phase 5, likely already satisfied, needs confirmation not a rewrite), analytics classification work (Phase 9), compare-telemetry parent/child metric cleanup (Phase 10), and the literal "shrink /'s own registry dominance" half of Phase 4 that this round deliberately deferred.