Skip to content

feat: persist catalog MCP icons on mcp_metadata and render them in the dashboard - #5042

Open
AshGodfrey wants to merge 2 commits into
mainfrom
ash/age-2993-persist-catalog-mcp-icons
Open

feat: persist catalog MCP icons on mcp_metadata and render them in the dashboard#5042
AshGodfrey wants to merge 2 commits into
mainfrom
ash/age-2993-persist-catalog-mcp-icons

Conversation

@AshGodfrey

@AshGodfrey AshGodfrey commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Closes AGE-2993, AGE-3097.

Catalog installs dropped the registry icon (ExternalMCPServerEntry.icon_url), so an installed server rendered a generic network glyph everywhere. mcp_metadata already supports mcp_server_id-backed rows with logo_id → assets, so no schema change is needed — nothing was populating it.

Persist

  • New assets.fetchImageFromURL management endpoint. A browser-side fetch of a third-party icon usually hits CORS, so ingestion has to happen server-side. It's a thin wrapper over the existing in-process FetchImageAssetFromURL used by the unproxied favicon default, and it reuses the same guardian HTTP policy, 10s timeout, and 4 MiB cap as fetchOpenAPIv3FromURL.
  • The catalog install workflow ingests the registry icon and sets it as the new server's metadata logo, on both the remote and unproxied paths. Best-effort: a logo failure is logged and never fails the install.

Render

  • MCP server detail sidebar now renders the persisted logo via SourceMcpIcon. MCPServerCard / MCPServerTableRow already read it (landed in feat: unproxied MCP server source #4886), so they're untouched.
  • collections.listServers returns icon_url from the persisted logo for both toolset-backed and mcp_server-backed attachments, which the collection detail page already maps onto its cards.

Note for reviewers

One behaviour change reaches beyond the ticket: fetched images now take their content type from http.DetectContentType over the first 512 bytes rather than from the declared Content-Type header. Catalog hosts routinely serve icons as application/octet-stream, or serve an HTML error page with a 200 and a .png path — the latter used to be stored as an image and render broken. Non-images are now rejected rather than assumed to be PNGs. This also affects the pre-existing unproxied-favicon ingestion path.

Two details worth confirming are intentional rather than accidental:

  • The server-side favicon default (setDefaultUnproxiedIcon) runs backgrounded and uses SetDefaultLogoIfUnset, while the install's mcpMetadata.set is unconditional. Either interleaving ends with the catalog icon winning over the gstatic favicon, which is the outcome we want.
  • The new LEFT JOIN mcp_metadata in both listServers queries can't multiply rows: mcp_metadata has partial unique indexes on toolset_id and on mcp_server_id.

Scope

Scoped-down re-land of #5008. Deliberately left for follow-ups: SEP-973 icon discovery and favicon fallback for registry entries that ship no icon, and initialize-response logo injection for MCP clients (AGE-3098). SourceCard's externalmcp branch still renders a placeholder despite carrying an iconUrl — out of scope here, since catalog installs are remote-MCP-backed as of AGE-2881.

Testing

  • New server/internal/assets/fetchimagefromurl_test.go: success, hash dedup, content type detected from bytes, non-image rejected, SVG rejected, upstream error, invalid scheme, unauthorized.
  • New server/internal/collections/listservers_icon_test.go: icon_url populated for toolset-backed and mcp_server-backed attachments, and nil with no logo.
  • mise lint:server and pnpm -F dashboard type-check pass.

Summary by cubic

Persist catalog MCP icons on install and render them across the dashboard so installed servers no longer use a generic icon. Implements AGE-2993 by saving the registry icon to mcp_metadata and serving it in UI surfaces.

  • New Features
    • Added assets.fetchImageFromURL to ingest third-party icons server-side; install workflow sets the server’s mcp_metadata logo for remote and unproxied paths (best-effort).
    • UI renders the persisted logo via SourceMcpIcon; collections now surface icon_url for toolset-backed and mcp_server-backed entries.
    • Detect image type from response bytes and reject non-images to avoid broken icons.
    • SDK/React Query and CLI clients wired for assets.fetchImageFromURL.

Written for commit 0f6eb11. Summary will update on new commits.

Review in cubic

@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

AGE-2993

AGE-3097

@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0f6eb11

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
server Minor
dashboard Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@AshGodfrey AshGodfrey added enhancement New feature or request go Pull requests that update go code javascript Pull requests that update javascript code labels Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

…e dashboard

Catalog installs dropped the registry icon, so installed servers fell back
to a generic glyph. Main already renders a server's mcp_metadata logo and
supports uploading one by hand; nothing populated it automatically.

- assets.fetchImageFromURL management endpoint for browser-initiated icon
  ingestion, since third-party icons are usually blocked by CORS
- Catalog installs persist the registry icon as the server's metadata
  logo, best-effort so an icon failure never fails the install
- Fetched images take their content type from the first bytes rather than
  the declared header, so mislabelled responses store correctly and
  non-images are rejected instead of assumed to be PNGs
- MCP server detail sidebar renders the persisted logo (cards and table
  rows landed on main in #4886)
- Collections listServers returns icon_url from the persisted logo,
  rendered by the collection detail page
- CLI assets client wired for the new endpoint

Scoped-down re-land of #5008. Deliberately left out, to land separately:
SEP-973 icon discovery and favicon fallback for registry entries with no
icon, and initialize-response logo injection for MCP clients (AGE-3098).

AGE-2993, AGE-3097
Mock the getMcpMetadata react-query module so invalidateAllGetMcpMetadata
no longer runs against the empty useQueryClient stub, and add the assets
and mcpMetadata SDK surfaces to the mock client so the icon path is
actually reachable. Covers upload-and-attach on both the remote and
unproxied install paths, the no-iconUrl short circuit, and that a failing
upload or metadata write still leaves the install completed.
@AshGodfrey
AshGodfrey force-pushed the ash/age-2993-persist-catalog-mcp-icons branch from 0f072fd to 0f6eb11 Compare August 7, 2026 12:51
@AshGodfrey
AshGodfrey marked this pull request as ready for review August 7, 2026 13:31
@AshGodfrey
AshGodfrey requested review from a team as code owners August 7, 2026 13:31

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

3 issues found across 36 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="server/internal/assets/fetchimagefromurl_test.go">

<violation number="1" location="server/internal/assets/fetchimagefromurl_test.go:46">
P2: Add a test for the oversized-response path when the upstream omits a reliable Content-Length. Every other asset ingest endpoint (UploadImage, UploadOpenAPIv3, UploadFunctions, UploadChatAttachment) has a ContentTooLarge test, and FetchImageFromURL was built for third-party icons that frequently lack a declared length — here the body is capped at MaxFileSizeImage by a LimitReader and the overflow-guard (read one byte past the limit) rejects it only when an extra byte is present. Without a test, the silent-truncation regression this guard prevents (per feedback to read one byte beyond 4 MiB) can slip back in.</violation>
</file>

<file name="server/internal/collections/queries.sql">

<violation number="1" location="server/internal/collections/queries.sql:142">
P2: Collection responses can advertise broken `icon_url` values when `mcp_metadata.logo_id` points to a valid non-image asset. Restrict both asset joins to `la.kind = 'image'` so only renderable logo assets are returned.</violation>
</file>

<file name="client/dashboard/src/pages/catalog/useRemoteMcpInstallWorkflow.ts">

<violation number="1" location="client/dashboard/src/pages/catalog/useRemoteMcpInstallWorkflow.ts:293">
P2: persistServerIconBestEffort is awaited synchronously in both install paths, so a slow or hanging third-party icon URL (the fetch can take up to the 10s timeout and 4 MiB download) blocks the install from reaching the "completed" status. Headless/collection installs and the detail page stay stuck on "creating" while the best-effort logo download runs; a best-effort side effect should not sit on the install's critical path.</violation>
</file>

Linked issue analysis

Linked issue: AGE-2993: Persist catalog MCP icons on mcp_metadata and render them in the dashboard

Status Acceptance criteria Notes
Add server-side ingestion endpoint to fetch an image from a URL (assets.fetchImageFromURL). The PR adds the endpoint in service design, generated server/client plumbing, SDK/react-query bindings, and unit tests for the fetch path.
During catalog install, ingest the registry icon and set the created MCP server's mcp_metadata.logo_id (best-effort; does not fail install). Client install workflow now calls a best-effort persist function that invokes assets.fetchImageFromURL and sets mcp metadata; tests and mocks added for these calls.
Render the persisted logo in MCP server UI surfaces (MCPServerCard / MCPServerTableRow / MCP server details sidebar). Sidebar now imports/uses the persisted SourceMcpIcon; MCPServerCard/TableRow already read the persisted logo in earlier change and remain compatible.
collections.listServers returns icon_url populated for toolset-backed and mcp_server-backed attachments so collection pages can show logos. Collection query and server code were updated to left-join mcp_metadata/assets and map logo to an icon_url; tests cover both attachment types and the nil case.
Image content-type handling: detect content type from response bytes and reject non-images (and SVGs) to avoid broken icons. Tests explicitly cover content-type sniffing from bytes, non-image rejection, and SVG rejection; implementation exposes the new fetch image behavior and updates existing unproxied favicon path to use the same asset ingestion function.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

@@ -0,0 +1,220 @@
package assets_test

@cubic-dev-ai cubic-dev-ai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Add a test for the oversized-response path when the upstream omits a reliable Content-Length. Every other asset ingest endpoint (UploadImage, UploadOpenAPIv3, UploadFunctions, UploadChatAttachment) has a ContentTooLarge test, and FetchImageFromURL was built for third-party icons that frequently lack a declared length — here the body is capped at MaxFileSizeImage by a LimitReader and the overflow-guard (read one byte past the limit) rejects it only when an extra byte is present. Without a test, the silent-truncation regression this guard prevents (per feedback to read one byte beyond 4 MiB) can slip back in.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/assets/fetchimagefromurl_test.go, line 46:

<comment>Add a test for the oversized-response path when the upstream omits a reliable Content-Length. Every other asset ingest endpoint (UploadImage, UploadOpenAPIv3, UploadFunctions, UploadChatAttachment) has a ContentTooLarge test, and FetchImageFromURL was built for third-party icons that frequently lack a declared length — here the body is capped at MaxFileSizeImage by a LimitReader and the overflow-guard (read one byte past the limit) rejects it only when an extra byte is present. Without a test, the silent-truncation regression this guard prevents (per feedback to read one byte beyond 4 MiB) can slip back in.</comment>

<file context>
@@ -0,0 +1,220 @@
+	return srv
+}
+
+func TestService_FetchImageFromURL_Success(t *testing.T) {
+	t.Parallel()
+
</file context>
Fix with cubic

JOIN organization_mcp_collection_server_attachments rt ON t.id = rt.toolset_id
JOIN organization_mcp_collections c ON c.id = rt.collection_id
LEFT JOIN mcp_metadata m ON m.toolset_id = t.id
LEFT JOIN assets la ON la.id = m.logo_id AND la.deleted IS FALSE

@cubic-dev-ai cubic-dev-ai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Collection responses can advertise broken icon_url values when mcp_metadata.logo_id points to a valid non-image asset. Restrict both asset joins to la.kind = 'image' so only renderable logo assets are returned.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/collections/queries.sql, line 142:

<comment>Collection responses can advertise broken `icon_url` values when `mcp_metadata.logo_id` points to a valid non-image asset. Restrict both asset joins to `la.kind = 'image'` so only renderable logo assets are returned.</comment>

<file context>
@@ -135,9 +135,11 @@ WHERE
 JOIN organization_mcp_collection_server_attachments rt ON t.id = rt.toolset_id
 JOIN organization_mcp_collections c ON c.id = rt.collection_id
+LEFT JOIN mcp_metadata m ON m.toolset_id = t.id
+LEFT JOIN assets la ON la.id = m.logo_id AND la.deleted IS FALSE
 WHERE
   rt.collection_id = @collection_id
</file context>
Fix with cubic

throw linkError instanceof Error ? linkError : new Error(String(linkError));
}

await persistServerIconBestEffort(client, target, mcpServer.id, reqOpts);

@cubic-dev-ai cubic-dev-ai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: persistServerIconBestEffort is awaited synchronously in both install paths, so a slow or hanging third-party icon URL (the fetch can take up to the 10s timeout and 4 MiB download) blocks the install from reaching the "completed" status. Headless/collection installs and the detail page stay stuck on "creating" while the best-effort logo download runs; a best-effort side effect should not sit on the install's critical path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/dashboard/src/pages/catalog/useRemoteMcpInstallWorkflow.ts, line 293:

<comment>persistServerIconBestEffort is awaited synchronously in both install paths, so a slow or hanging third-party icon URL (the fetch can take up to the 10s timeout and 4 MiB download) blocks the install from reaching the "completed" status. Headless/collection installs and the detail page stay stuck on "creating" while the best-effort logo download runs; a best-effort side effect should not sit on the install's critical path.</comment>

<file context>
@@ -254,6 +290,8 @@ async function installUnproxiedTarget(
     throw linkError instanceof Error ? linkError : new Error(String(linkError));
   }
 
+  await persistServerIconBestEffort(client, target, mcpServer.id, reqOpts);
+
   return { mcpServer, mcpEndpointUrl: undefined, authConfigured: false };
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request go Pull requests that update go code javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant