Skip to content

fix(icon): emit URL-form serverInfo.icons entry when PUBLIC_BASE_URL is set#59

Merged
arapov merged 1 commit into
masterfrom
fix/icon-url-form
May 28, 2026
Merged

fix(icon): emit URL-form serverInfo.icons entry when PUBLIC_BASE_URL is set#59
arapov merged 1 commit into
masterfrom
fix/icon-url-form

Conversation

@arapov
Copy link
Copy Markdown
Collaborator

@arapov arapov commented May 28, 2026

Summary

Claude.ai's connector list renders the icon for a sibling MCP server (Discourse-flavored, fronted by a custom domain) but not for capsulemcp on its Cloud Run URL — despite identical SVG bytes served from both. The difference traced to data: URI vs URL src in the serverInfo.icons array: clients with img-src CSP restrictions skip data URIs.

This PR emits both shapes, URL first.

Change

Setup Pre-fix shape Post-fix shape
Stdio (no PUBLIC_BASE_URL) [{src: data:..., sizes: [...]}] unchanged
HTTP+OAuth (PUBLIC_BASE_URL set) [{src: data:..., sizes: [...]}] [{src: https://.../icon.svg, ...}, {src: data:..., ...}]

Clients iterating the array and picking the first usable entry get the URL form; CSP-tolerant clients see both and can pick either.

Refactor

src/icon.ts was previously generated by scripts/build-icon.mjs from assets/icon.svg, and ALSO contained the ICONS array constant. Now:

  • src/icon.ts is pure generated data: ICON_SVG + ICON_DATA_URI only.
  • New src/icon-builder.ts (hand-edited) exports buildIcons(publicBaseUrl?).

The shape of the icons array can now evolve without touching the generator. src/server.ts switches from icons: ICONS to icons: buildIcons(process.env.PUBLIC_BASE_URL).

Test plan

  • 527 tests passing (+4 in tests/icon-builder.test.ts):
    • stdio shape: 1 entry, data-URI-only
    • HTTP shape: 2 entries, URL first
    • trailing-slash tolerance: https://host/https://host/icon.svg (not host//)
    • empty-string PUBLIC_BASE_URL falls back to stdio shape
  • Existing tests/icon-source.test.ts drift-guard still passes — generator + generated file in sync.
  • Typecheck, lint, format:check, build all clean.
  • Privacy sweep clean (no banned substrings).

Risk

Low. Stdio path is bit-for-bit identical to pre-fix shape. HTTP path adds an array entry — well-behaved MCP clients iterate the array and pick the first one that renders; misbehaved ones that read icons[0].src literally now get a real URL instead of an opaque data URI, which is the goal.

If the hypothesis is wrong and Claude.ai's connector list still skips the URL form, no harm done — the data URI is still in the array and any client that was rendering it before continues to.

🤖 Generated with Claude Code

…is set

Some client UIs (observed: Claude.ai's connector list on the Cloud
Run URL) skip `data:` image srcs in serverInfo.icons — plausibly a
CSP/img-src policy disallowing data URIs. A sibling MCP server
fronted by a custom domain rendered its icon while capsulemcp on
*.run.app didn't, despite identical SVG bytes.

This commit:
  - Adds buildIcons(publicBaseUrl?: string) in src/icon-builder.ts.
    When PUBLIC_BASE_URL is set, returns [URL entry, data-URI entry].
    When unset (stdio), returns [data-URI entry] — preserves the
    pre-fix wire shape exactly so stdio installs see no change.
  - Refactors src/icon.ts to be pure generated DATA (ICON_SVG +
    ICON_DATA_URI). The orchestration (URL ordering, sizes hints)
    moves out of the SVG generator so the icons-array shape can
    evolve without touching the generator.
  - Wires server.ts to call buildIcons(process.env.PUBLIC_BASE_URL).
  - 4 new tests in tests/icon-builder.test.ts: stdio shape, HTTP
    shape with URL-first ordering, trailing-slash tolerance, empty-
    string treated as not-set.

523 → 527 tests. Bundle 165.75 KB stdio / 192.24 KB http (+0.4 KB
for the new helper file).

No external consumer relied on the now-removed `ICONS` const from
src/icon.ts — single internal call site was server.ts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@arapov arapov merged commit a0218d7 into master May 28, 2026
1 check passed
@arapov arapov deleted the fix/icon-url-form branch May 28, 2026 15:59
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