From e4ece6e528292da1b6b942d24284069b48587151 Mon Sep 17 00:00:00 2001 From: Logan Besecker Date: Sat, 19 Sep 2026 12:03:41 -0700 Subject: [PATCH 1/3] Add a landing page for the book at /book MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sidebar slot sent visitors from a 250px tile straight to a product page, which is a cold click with no context. It now lands here, and the affiliate link and its disclosure move to this page. - Lead with the reader's symptom, not the cover: a server that runs but never gets connected to - Set out the ten problem areas and a thirty-day playbook, so the scope is legible before anyone spends $29.99 - Offer proof that can be checked on this site -- llms.txt, the JSON API and a listing's metadata are what the book describes - Mark it up as schema.org/Book with both editions, and deliberately no aggregateRating: the book has no ratings, and inventing one is a lie a reader can check in a single click Two bugs found while building it. Inside ~H, `@asin` reads assigns rather than the module attribute, so every fact was an assign that did not exist. And HEEx does not interpolate curly braces inside a + + <%!-- --- Hero: cover, promise, price, CTA above the fold ------------- --%> +
+
+ {"Cover +
+ +
+

+ For developers shipping MCP servers +

+ +
+

+ Your MCP server works. Agents still can't find it. +

+

+ It runs on your machine. The tools return clean results. Then nothing happens — no + connects, no repeat use, and a registry listing that agents skip straight past. + {@title} + is the {@pages}-page repair manual for the gap between a server that runs and a + server that gets adopted. +

+
+ +
+ <.button + variant="primary" + href={@buy_url} + rel="sponsored noopener noreferrer" + target="_blank" + class="px-6 py-3 text-base" + > + Get the book on Amazon <.icon name="hero-arrow-top-right-on-square" class="size-4" /> + +

+ Paperback {@paperback} + · Hardcover {@hardcover} +

+
+ +
    +
  • + <.icon name="hero-check-micro" class="mt-0.5 size-4 shrink-0 text-success" /> + {point} +
  • +
+
+
+ + <%!-- --- The problem, named specifically ---------------------------- --%> +
+
+

the symptom

+

+ A server that runs is not a server that gets used +

+
+ +
+
+

{title}

+

{body}

+
+
+
+ + <%!-- --- What's inside ---------------------------------------------- --%> +
+
+

what is inside

+

+ Ten problems, in the order they bite +

+
+ +
    +
  1. +

    + {index} · {title} +

    +

    {body}

    +
  2. +
+
+ + <%!-- --- Credibility, the honest kind ------------------------------- --%> +
+
+
+

why take its word

+

+ The book describes this site, and you can check it +

+
+ +

+ {@author} writes and runs MCP Harbor. Every technique in the book is visible in the + registry you are reading it on — so you can audit the advice before you buy it, which + is more than a review score would tell you. +

+ +
    +
  • + <.link + href={href} + class="group flex h-full flex-col gap-1 rounded-field border border-rule bg-canvas p-3 transition-colors hover:border-brand/40" + > + {label} + {note} + +
  • +
+
+
+ + <%!-- --- Formats, specs, FAQ ---------------------------------------- --%> +
+
+
+

+ Before you buy +

+ +
+

{q}

+

{a}

+
+
+
+ + +
+ + <%!-- --- Closing CTA ------------------------------------------------ --%> +
+
+
+

+ Pick one friction and fix it today +

+

+ {@pages} pages, ten chapters, and a thirty-day order of operations for getting your + server found, trusted and connected. +

+
+ <.button + variant="primary" + href={@buy_url} + rel="sponsored noopener noreferrer" + target="_blank" + class="shrink-0 px-6 py-3 text-base" + > + Get the book <.icon name="hero-arrow-right-micro" class="size-4" /> + +
+ + <%!-- Both disclosures belong here: the link earns a commission, and + the author runs this site. Saying so costs a little trust and + buys back more. --%> +

+ Written by {@author}, who also maintains MCP Harbor. Links to Amazon are affiliate links + and may earn a commission at no extra cost to you. +

+
+ + """ + end + + # schema.org/Book, so the listing can earn a rich result. No aggregateRating: + # the book has none yet, and marking up a rating that does not exist is both + # a lie and a manual-action risk. + defp structured_data do + # escape: :html_safe writes `<`, `>` and `&` as unicode escapes, so nothing + # in the data can close the |s, html) + + assert {:ok, data} = body |> String.trim() |> Jason.decode() + assert data["@type"] == "Book" + assert data["isbn"] == "979-8174638471" + assert data["numberOfPages"] == 244 + assert length(data["offers"]) == 2 + refute Map.has_key?(data, "aggregateRating") + end + + test "the sidebar slot points here rather than straight at Amazon", %{conn: conn} do + server = McpRegistry.RegistryFixtures.server_fixture() + {:ok, _view, html} = live(conn, "/servers/#{server.name}") + + # A cold click from a 250px tile to a product page converts badly; the + # landing page is the intermediate step that does the selling. + assert html =~ ~s(href="/book") + refute html =~ "amzn.to" + end +end diff --git a/test/render_dump_test.exs b/test/render_dump_test.exs index 6cd26f5..ef4aef6 100644 --- a/test/render_dump_test.exs +++ b/test/render_dump_test.exs @@ -48,10 +48,12 @@ defmodule McpRegistryWeb.RenderDumpTest do {:ok, _view, detail} = live(conn, "/servers/#{server.name}") {:ok, _view, catalogue} = live(conn, ~p"/servers") {:ok, _view, landing} = live(conn, ~p"/") + {:ok, _view, book} = live(conn, ~p"/book") dump("detail", detail) dump("catalogue", catalogue) dump("landing", landing) + dump("book", book) end # The page links `/assets/css/app.css`, which a file:// URL cannot resolve, From 565762126888f18ce06d2a29adbd68f5aa65951e Mon Sep 17 00:00:00 2001 From: Logan Besecker Date: Sat, 19 Sep 2026 16:42:24 -0700 Subject: [PATCH 2/3] Keep only the analytics link external in the footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop the two GitHub links, source and wiki - Put the book in their place, so the Project column is not a single item - Tie the DB-IP credit to whether the geo database is actually loaded The DB-IP credit is a CC BY 4.0 licence condition on using the data, not a recommendation, so deleting it outright would have been wrong the moment geo blocking came back. Geo blocking is currently off, nothing loads that database, and the credit is therefore not shown -- and it reappears by itself if GEO_BLOCK_ENABLED is ever set, rather than depending on someone remembering. A test pins both directions, and another fails if any host other than the analytics one appears in the footer. --- Pages affected: - [MCP Registry](https://ai.mcpharbor.dev/) — home page carrying the revised footer. - [Browse MCP servers](https://ai.mcpharbor.dev/servers) — the catalogue of Model Context Protocol servers. - [MCP Server Optimization](https://ai.mcpharbor.dev/book) — the book, now linked from the footer. - [Submit a server](https://ai.mcpharbor.dev/submit) — add your own MCP server listing. Co-Authored-By: Claude Opus 5 --- lib/mcp_registry_web/components/layouts.ex | 41 +++++++++------ .../components/footer_test.exs | 50 +++++++++++++++++++ 2 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 test/mcp_registry_web/components/footer_test.exs diff --git a/lib/mcp_registry_web/components/layouts.ex b/lib/mcp_registry_web/components/layouts.ex index 15e036f..7056140 100644 --- a/lib/mcp_registry_web/components/layouts.ex +++ b/lib/mcp_registry_web/components/layouts.ex @@ -132,10 +132,7 @@ defmodule McpRegistryWeb.Layouts do <.footer_column title="Project"> - <:link_item href="https://github.com/lbesecker195/MCP_Registry"> - Source on GitHub - - <:link_item href="https://github.com/lbesecker195/MCP_Registry/wiki">Wiki + <:link_item navigate={~p"/book"}>The book <:link_item href="https://seriouslysimpleanalytics.com"> Seriously Simple Analytics @@ -145,17 +142,22 @@ defmodule McpRegistryWeb.Layouts do
Copyright LoganBesecker.com 2026 · Apache 2.0 - <%!-- DB-IP's free database is CC BY 4.0; this credit is a condition - of the licence, not a recommendation. Removing it means the - geo data can no longer be used. --%> - · IP data from - - DB-IP - + <%!-- DB-IP's free database is CC BY 4.0, and the credit is a + condition of that licence rather than a recommendation. It is + therefore tied to whether the data is actually loaded: geo + blocking is off, so nothing here uses DB-IP and no credit is + owed. Switch GEO_BLOCK_ENABLED on and the credit comes back + by itself, which is safer than remembering to re-add it. --%> + + · IP data from + + DB-IP + + Analytics by @@ -176,6 +178,15 @@ defmodule McpRegistryWeb.Layouts do """ end + # True only when a geo database is actually configured to load. The DB-IP + # credit is a licence condition on *use* of the data, so it follows this + # rather than being hardcoded. + defp geo_data_in_use? do + Application.get_env(:mcp_registry, :geo_block, []) + |> Keyword.get(:cities, []) + |> Enum.any?() + end + attr :current, :boolean, default: false attr :rest, :global, include: ~w(href navigate patch) slot :inner_block, required: true diff --git a/test/mcp_registry_web/components/footer_test.exs b/test/mcp_registry_web/components/footer_test.exs new file mode 100644 index 0000000..2b01216 --- /dev/null +++ b/test/mcp_registry_web/components/footer_test.exs @@ -0,0 +1,50 @@ +defmodule McpRegistryWeb.FooterTest do + use McpRegistryWeb.ConnCase, async: false + + import Phoenix.LiveViewTest + + # Everything the footer is allowed to point at off-site. + @allowed_hosts ["seriouslysimpleanalytics.com"] + + defp footer_html(conn) do + {:ok, _view, html} = live(conn, ~p"/servers") + [_, footer] = Regex.run(~r|(.*?)|s, html) + footer + end + + test "the footer links off-site only to Seriously Simple Analytics", %{conn: conn} do + hosts = + footer_html(conn) + |> then(&Regex.scan(~r|href="https?://([^/"]+)|, &1)) + |> Enum.map(fn [_, host] -> String.replace_prefix(host, "www.", "") end) + |> Enum.uniq() + + assert hosts -- @allowed_hosts == [], + "unexpected external hosts in the footer: #{inspect(hosts -- @allowed_hosts)}" + + assert "seriouslysimpleanalytics.com" in hosts + end + + test "the DB-IP credit follows whether the data is actually loaded", %{conn: conn} do + original = Application.get_env(:mcp_registry, :geo_block) + + on_exit(fn -> + if original, + do: Application.put_env(:mcp_registry, :geo_block, original), + else: Application.delete_env(:mcp_registry, :geo_block) + end) + + # Off: nothing uses the database, so no credit is owed and none is shown. + Application.put_env(:mcp_registry, :geo_block, loader: :geoip_city, cities: []) + refute footer_html(conn) =~ "db-ip.com" + + # On: the credit is a CC BY 4.0 licence condition and must reappear by + # itself, rather than depending on someone remembering to re-add it. + Application.put_env(:mcp_registry, :geo_block, + loader: :geoip_city, + cities: [%{city: "Mountain View", country: "US"}] + ) + + assert footer_html(conn) =~ "db-ip.com" + end +end From bfc27a3d007353ec50d9f5256f0fe61c64ada749 Mon Sep 17 00:00:00 2001 From: Logan Besecker Date: Sat, 19 Sep 2026 20:59:31 -0700 Subject: [PATCH 3/3] Link the MCP Gateway from the header and footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gateway.mcpharbor.dev is a sister property on the same root domain -- one Model Context Protocol endpoint covering 139 tools across 16 servers -- so it belongs beside the registry's own agent-facing entrypoints rather than being treated as an outbound link. - Add "gateway" to the header nav, next to llms.txt - Add "MCP Gateway" to the footer's For agents column, beside the MCP endpoint - Vary the anchor text between the two placements - Widen the footer's allowed-host list to cover it, and assert the header carries it too --- Pages affected: - [MCP Registry](https://ai.mcpharbor.dev/) — home page carrying the revised navigation. - [Browse MCP servers](https://ai.mcpharbor.dev/servers) — searchable catalogue of MCP servers. - [MCP endpoint](https://ai.mcpharbor.dev/mcp) — the registry's own MCP server, linked beside the gateway. - [Agent instructions](https://ai.mcpharbor.dev/llms.txt) — plain-text guide for agents. Co-Authored-By: Claude Opus 5 --- lib/mcp_registry_web/components/layouts.ex | 2 ++ .../components/footer_test.exs | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/mcp_registry_web/components/layouts.ex b/lib/mcp_registry_web/components/layouts.ex index 7056140..1205fcb 100644 --- a/lib/mcp_registry_web/components/layouts.ex +++ b/lib/mcp_registry_web/components/layouts.ex @@ -73,6 +73,7 @@ defmodule McpRegistryWeb.Layouts do <.nav_link navigate={~p"/servers"} current={@active == :servers}>servers <.nav_link href={~p"/api/v0/servers"} current={false}>api <.nav_link href={~p"/llms.txt"} current={false}>llms.txt + <.nav_link href="https://gateway.mcpharbor.dev" current={false}>gateway <.theme_toggle /> <.link @@ -129,6 +130,7 @@ defmodule McpRegistryWeb.Layouts do <:link_item href={~p"/llms.txt"}>llms.txt <:link_item href={~p"/api/v0/servers"}>JSON API <:link_item href={~p"/mcp"}>MCP endpoint + <:link_item href="https://gateway.mcpharbor.dev">MCP Gateway <.footer_column title="Project"> diff --git a/test/mcp_registry_web/components/footer_test.exs b/test/mcp_registry_web/components/footer_test.exs index 2b01216..aad4bfd 100644 --- a/test/mcp_registry_web/components/footer_test.exs +++ b/test/mcp_registry_web/components/footer_test.exs @@ -3,8 +3,10 @@ defmodule McpRegistryWeb.FooterTest do import Phoenix.LiveViewTest - # Everything the footer is allowed to point at off-site. - @allowed_hosts ["seriouslysimpleanalytics.com"] + # Everything the footer is allowed to point at off-site. gateway.mcpharbor.dev + # is ours -- a sister property on the same root domain -- so it counts as an + # internal link in spirit even though the host differs. + @allowed_hosts ["seriouslysimpleanalytics.com", "gateway.mcpharbor.dev"] defp footer_html(conn) do {:ok, _view, html} = live(conn, ~p"/servers") @@ -12,7 +14,9 @@ defmodule McpRegistryWeb.FooterTest do footer end - test "the footer links off-site only to Seriously Simple Analytics", %{conn: conn} do + test "the footer links off-site only to our own properties and the analytics vendor", %{ + conn: conn + } do hosts = footer_html(conn) |> then(&Regex.scan(~r|href="https?://([^/"]+)|, &1)) @@ -23,6 +27,14 @@ defmodule McpRegistryWeb.FooterTest do "unexpected external hosts in the footer: #{inspect(hosts -- @allowed_hosts)}" assert "seriouslysimpleanalytics.com" in hosts + assert "gateway.mcpharbor.dev" in hosts + end + + test "the header links to the gateway", %{conn: conn} do + {:ok, _view, html} = live(conn, ~p"/servers") + [header] = Regex.run(~r||s, html) + + assert header =~ "https://gateway.mcpharbor.dev" end test "the DB-IP credit follows whether the data is actually loaded", %{conn: conn} do