diff --git a/CHANGELOG.md b/CHANGELOG.md
index fef0e0b..ea14003 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,29 @@ versions adhere to [Semantic Versioning](https://semver.org).
### Fixed
+- **`serverInfo.icons` now emits a URL-form entry first when
+ `PUBLIC_BASE_URL` is set (HTTP+OAuth deploys), with the existing
+ data-URI form retained as a second fallback entry.** Some client
+ UIs (observed: Claude.ai's connector list on the Cloud Run URL)
+ silently skip `data:` image srcs — plausibly a CSP / `img-src`
+ policy that disallows data URIs. A sibling MCP server fronted by
+ a custom domain rendered its icon correctly with the URL form
+ while capsulemcp on `*.run.app` didn't, even though the SVG bytes
+ were identical. The fix emits both shapes (URL first so clients
+ iterating the array pick it; data URI second for stdio + CSP-
+ tolerant clients). Stdio installs (no `PUBLIC_BASE_URL`) see no
+ behavioural change — still data-URI-only.
+
+ Refactor: the icons array shape now lives in
+ `src/icon-builder.ts` (hand-edited) instead of inside the SVG
+ generator at `scripts/build-icon.mjs`. `src/icon.ts` is now pure
+ generated data (`ICON_SVG`, `ICON_DATA_URI`); the orchestration
+ (URL vs data URI ordering, sizes hints) is separate. The `ICONS`
+ named export from `src/icon.ts` is gone — replaced by
+ `buildIcons(publicBaseUrl?)` in `src/icon-builder.ts`. No external
+ consumer relied on `ICONS` (single internal call site in
+ `src/server.ts`).
+
- **Tool annotations now emit `{readOnlyHint, destructiveHint}`
explicitly on every tool — never rely on MCP spec defaults.** Per
spec, `destructiveHint` defaults to `true`. The pre-fix
diff --git a/scripts/build-icon.mjs b/scripts/build-icon.mjs
index af2d893..586b047 100644
--- a/scripts/build-icon.mjs
+++ b/scripts/build-icon.mjs
@@ -43,29 +43,23 @@ const generated = `/**
* edit this file directly** — edit the SVG and re-run \`npm run
* build:icon\` (or \`npm run build\`, which chains it).
*
- * Exposed two ways:
+ * This file is generated DATA: the raw SVG plus its \`data:\` URI
+ * form. The \`serverInfo.icons\` ARRAY shape (URL form vs data URI
+ * form vs both, ordering, sizes hints) is hand-edited orchestration
+ * and lives in \`src/icon-builder.ts\` — kept out of this generator
+ * so the icon-array shape can evolve without touching the SVG.
+ *
+ * Exposed two ways at runtime:
* - Embedded as a \`data:\` URI in the MCP \`serverInfo.icons\` array
- * (spec-compliant; works without any HTTP route).
+ * (spec-compliant; works without any HTTP route — stdio path).
* - Served at \`/icon.svg\` and \`/favicon.ico\` by the HTTP entry,
- * in case the consuming client prefers a URL it can fetch.
+ * so clients that prefer to fetch a URL get a real HTTPS resource
+ * (some UIs' CSP blocks \`data:\` image srcs — URL form survives).
*/
export const ICON_SVG = \`${escapedSvg}\`;
export const ICON_DATA_URI = \`data:image/svg+xml;base64,\${Buffer.from(ICON_SVG, "utf8").toString("base64")}\`;
-
-/**
- * Shaped for MCP's \`serverInfo.icons\` field. Single 64x64 SVG that
- * scales cleanly to any size; \`sizes: ["any"]\` tells the client it
- * works at every render size.
- */
-export const ICONS = [
- {
- src: ICON_DATA_URI,
- mimeType: "image/svg+xml",
- sizes: ["64x64", "any"],
- },
-];
`;
writeFileSync(TS_PATH, generated, "utf8");
diff --git a/src/icon-builder.ts b/src/icon-builder.ts
new file mode 100644
index 0000000..2a36ad6
--- /dev/null
+++ b/src/icon-builder.ts
@@ -0,0 +1,65 @@
+/**
+ * Builds the `serverInfo.icons` array shape for MCP's `initialize`
+ * response. Lives separately from the generated `src/icon.ts` so the
+ * shape can evolve (URL form, multiple sizes, fallback ordering)
+ * without touching the SVG generator.
+ *
+ * Why two forms (URL + data URI):
+ *
+ * - URL form (`https:///icon.svg`) — preferred by client UIs
+ * whose Content-Security-Policy blocks `data:` image srcs in
+ * `` tags. Empirically: a sibling MCP server (Discourse-
+ * flavored, served from a custom domain) renders its icon
+ * correctly in Claude.ai's connector list while capsulemcp on
+ * a `*.run.app` URL did not — the difference traced to URL-vs-
+ * data-URI src shape, not the icon bytes themselves.
+ * - Data URI form — host-independent fallback. The stdio entry
+ * has no HTTP route to serve the SVG from, so the data URI is
+ * the only viable shape there. Also useful for clients that
+ * prefer inline bytes (no extra fetch round-trip).
+ *
+ * Ordering rule: URL first when available, then data URI. Clients
+ * that iterate the array and pick the first usable entry get the
+ * URL form by default and never see the data URI; clients that
+ * filter by `mimeType` or `sizes` see both and can pick either.
+ *
+ * When `publicBaseUrl` is undefined (stdio invocation, or HTTP
+ * deploy that hasn't configured `PUBLIC_BASE_URL`), only the data
+ * URI entry is emitted — preserves the pre-v1.6.x behaviour exactly.
+ */
+
+import { ICON_DATA_URI } from "./icon.js";
+
+interface IconEntry {
+ src: string;
+ mimeType: string;
+ sizes: string[];
+}
+
+export function buildIcons(publicBaseUrl?: string): IconEntry[] {
+ const icons: IconEntry[] = [];
+
+ if (publicBaseUrl) {
+ // Strip trailing slash so the URL doesn't render as
+ // `https://host//icon.svg` if the caller passes a base ending in /.
+ // (Express's iconHandler at /icon.svg responds to either form, but
+ // the wire shape should look clean to humans reading the
+ // initialize response.)
+ const base = publicBaseUrl.replace(/\/+$/, "");
+ icons.push({
+ src: `${base}/icon.svg`,
+ mimeType: "image/svg+xml",
+ sizes: ["any"],
+ });
+ }
+
+ // Data URI: host-independent fallback. Always present — covers
+ // stdio (no HTTP route) and clients that prefer inline bytes.
+ icons.push({
+ src: ICON_DATA_URI,
+ mimeType: "image/svg+xml",
+ sizes: ["64x64", "any"],
+ });
+
+ return icons;
+}
diff --git a/src/icon.ts b/src/icon.ts
index 0804cdb..ad2780a 100644
--- a/src/icon.ts
+++ b/src/icon.ts
@@ -7,11 +7,18 @@
* edit this file directly** — edit the SVG and re-run `npm run
* build:icon` (or `npm run build`, which chains it).
*
- * Exposed two ways:
+ * This file is generated DATA: the raw SVG plus its `data:` URI
+ * form. The `serverInfo.icons` ARRAY shape (URL form vs data URI
+ * form vs both, ordering, sizes hints) is hand-edited orchestration
+ * and lives in `src/icon-builder.ts` — kept out of this generator
+ * so the icon-array shape can evolve without touching the SVG.
+ *
+ * Exposed two ways at runtime:
* - Embedded as a `data:` URI in the MCP `serverInfo.icons` array
- * (spec-compliant; works without any HTTP route).
+ * (spec-compliant; works without any HTTP route — stdio path).
* - Served at `/icon.svg` and `/favicon.ico` by the HTTP entry,
- * in case the consuming client prefers a URL it can fetch.
+ * so clients that prefer to fetch a URL get a real HTTPS resource
+ * (some UIs' CSP blocks `data:` image srcs — URL form survives).
*/
export const ICON_SVG = `