fix(icon): emit URL-form serverInfo.icons entry when PUBLIC_BASE_URL is set#59
Merged
Conversation
…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>
This was referenced May 28, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 URLsrcin theserverInfo.iconsarray: clients withimg-srcCSP restrictions skip data URIs.This PR emits both shapes, URL first.
Change
PUBLIC_BASE_URL)[{src: data:..., sizes: [...]}]PUBLIC_BASE_URLset)[{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.tswas previously generated byscripts/build-icon.mjsfromassets/icon.svg, and ALSO contained theICONSarray constant. Now:src/icon.tsis pure generated data:ICON_SVG+ICON_DATA_URIonly.src/icon-builder.ts(hand-edited) exportsbuildIcons(publicBaseUrl?).The shape of the icons array can now evolve without touching the generator.
src/server.tsswitches fromicons: ICONStoicons: buildIcons(process.env.PUBLIC_BASE_URL).Test plan
tests/icon-builder.test.ts):https://host/→https://host/icon.svg(nothost//)PUBLIC_BASE_URLfalls back to stdio shapetests/icon-source.test.tsdrift-guard still passes — generator + generated file in sync.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].srcliterally 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