Skip to content

feat(http): serve landing page at GET / with icon link tags#60

Merged
arapov merged 1 commit into
masterfrom
feat/landing-page-icon-hints
May 28, 2026
Merged

feat(http): serve landing page at GET / with icon link tags#60
arapov merged 1 commit into
masterfrom
feat/landing-page-icon-hints

Conversation

@arapov
Copy link
Copy Markdown
Collaborator

@arapov arapov commented May 28, 2026

Summary

Closes the browser-style favicon-discovery gap. Before: GET / returned Express's default 404 with an empty <head> — favicon checkers and any connector UI that walks <link rel="icon"> tags found no hints and fell back to nothing. After: GET / serves a small static HTML page with proper icon link tags + a human-readable body.

Pairs with the URL-form serverInfo.icons fix shipped earlier so both icon-discovery channels are covered:

Channel How it works Fix
MCP protocol Client reads serverInfo.icons in the initialize response URL-form entry first (data URI fallback second) — PR #59
HTML Client crawls /, parses <head>, follows <link rel="icon"> This PR

The page

Static HTML, ~1.2 KB. Generic content — no deployment-specific URLs or tenant info.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>capsulemcp</title>
<link rel="icon" type="image/svg+xml" href="/icon.svg">
<link rel="apple-touch-icon" href="/icon.svg">
<meta name="description" content="...">
<style>...</style>
</head>
<body>
<h1>capsulemcp</h1>
<p>This is the HTTP+OAuth deployment of <a href="...">capsulemcp</a>...</p>
<p>The MCP endpoint is at <code>/mcp</code>...</p>
<p class="muted">Source: ... · License: Apache-2.0</p>
</body>
</html>

Cache-Control: public, max-age=3600 (1h — shorter than the icon's 24h so copy fixes propagate within the day).

Security

  • No template interpolation, no user input, no XSS surface — the HTML is a literal const, served as-is.
  • Generic content only — no PUBLIC_BASE_URL, no tenant names, no internal infra references.
  • A determinism test (response bytes match across two requests) catches any future refactor that introduces interpolation.

Test plan

  • 527 → 533 tests (+6 in tests/http-app.test.ts):
    • GET / returns 200 + text/html
    • <link rel="icon" type="image/svg+xml" href="/icon.svg"> is present
    • <link rel="apple-touch-icon" href="/icon.svg"> is present
    • Body mentions /mcp (so a dev hitting the URL gets a next-step pointer)
    • Cache-Control = public, max-age=3600
    • Two requests return identical bytes (determinism / no template interpolation)
  • Typecheck, lint, format:check, build all clean
  • Privacy sweep clean — no banned substrings in the diff
  • Bundle 165.75 KB stdio (unchanged) / 192.24 → 193.63 KB http (+1.39 KB for the HTML const)

Side benefit

Devs who hit the URL in a browser by mistake now see a useful "what is this?" page instead of "Cannot GET /". Improves the dev-experience when troubleshooting deployment URLs.

🤖 Generated with Claude Code

Closes the browser-side favicon-discovery gap. Before this change,
hitting `/` returned Express's default 404 HTML with an empty
<head>, so any client doing HTML-shaped icon discovery (favicon
checkers, some connector UIs that walk <link rel="icon"> tags)
found no hints and fell back to nothing.

Pairs with the v1.6.6 URL-form serverInfo.icons fix so we now
cover BOTH icon-discovery channels:
  - MCP protocol channel: serverInfo.icons emits URL first (data
    URI fallback second) — for clients reading the initialize
    response.
  - HTML channel: GET / returns a small HTML page with proper
    <link rel="icon" type="image/svg+xml" href="/icon.svg"> and
    <link rel="apple-touch-icon" href="/icon.svg"> — for clients
    that crawl the root URL like a browser.

The HTML is static bytes: no template interpolation, no user input,
no XSS surface. Generic content only — mentions /mcp, the GitHub
repo, and the license; no deployment-specific URLs or tenant info.
1h Cache-Control (shorter than the icon's 24h so copy fixes
propagate within the day).

Also adds a human-friendly side benefit: if a dev hits the URL in
a browser by mistake they see a useful "what is this?" page
instead of Express's "Cannot GET /" default.

6 new tests in tests/http-app.test.ts: content-type, link-tag
shapes for both <link rel="icon"> and apple-touch-icon, /mcp
reference in body, cache-control header, and a determinism check
(two requests return identical bytes — catches any future
template-interpolation refactor mistake).

527 → 533 tests. Bundle 165.75 KB stdio (unchanged) / 192.24 →
193.63 KB http (+1.39 KB for the landing-page HTML constant).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@arapov arapov merged commit a6c24e0 into master May 28, 2026
1 check passed
@arapov arapov deleted the feat/landing-page-icon-hints branch May 28, 2026 16:16
arapov added a commit that referenced this pull request May 29, 2026
…hanism (#61)

PR #59 emitted a URL-form serverInfo.icons entry on the theory that
Claude.ai's connector UI reads serverInfo.icons and was skipping our
data: URI. Empirical investigation (loomiomcp vs capsulemcp log A/B)
disproved that theory:

  - Claude.ai's connector icon comes from a favicon crawler
    (AWS-hosted, hourly, desktop+mobile Chrome UAs) that fetches
    /favicon.ico DIRECTLY. It never reads serverInfo.icons, and
    never parses the landing-page <link> tags.
  - It only crawls CUSTOM DOMAINS, not Public-Suffix-List platform
    subdomains (*.run.app, *.vercel.app, *.herokuapp.com). That's
    why the sibling loomiomcp on mcp.openssl-communities.org shows
    an icon and capsulemcp on *.run.app doesn't.

So #59 was built on a wrong hypothesis and is not load-bearing. It
was harmless (spec-correct, would only ever matter IF Anthropic
ships serverInfo.icons consumption per modelcontextprotocol#152),
but it added a helper + refactor justified by a disproven premise.
Reverting for cleanliness restores the simple `icons: ICONS`
data-URI shape that predated #59:

  - build-icon.mjs re-emits the ICONS export; icon.ts regenerated.
  - src/icon-builder.ts + tests/icon-builder.test.ts deleted.
  - server.ts back to `import { ICONS }` / `icons: ICONS`.

Kept: PR #60's landing page (independent DX merit — human-readable
root + favicon-checker support), but its CHANGELOG entry is
corrected to NOT claim it fixes Claude icon display, with a scope
note documenting the real favicon-crawler + custom-domain mechanism.

The actual fix for the connector icon (custom-domain mapping) is
infra-side, not a code change in this repo.

533 → 529 tests (−4, icon-builder tests removed). Bundle
165.18 KB stdio / 193.05 KB http. HOWTO test count + bundle figures
resynced (were stale at 523 / ~191 from earlier un-synced merges).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant