feat: persist catalog MCP icons on mcp_metadata and render them in the dashboard - #5042
feat: persist catalog MCP icons on mcp_metadata and render them in the dashboard#5042AshGodfrey wants to merge 2 commits into
Conversation
🦋 Changeset detectedLatest commit: 0f6eb11 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
|
All contributors have signed the CLA ✍️ ✅ |
…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.
0f072fd to
0f6eb11
Compare
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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>
| 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 |
There was a problem hiding this comment.
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>
| throw linkError instanceof Error ? linkError : new Error(String(linkError)); | ||
| } | ||
|
|
||
| await persistServerIconBestEffort(client, target, mcpServer.id, reqOpts); |
There was a problem hiding this comment.
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>
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_metadataalready supportsmcp_server_id-backed rows withlogo_id → assets, so no schema change is needed — nothing was populating it.Persist
assets.fetchImageFromURLmanagement 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-processFetchImageAssetFromURLused by the unproxied favicon default, and it reuses the same guardian HTTP policy, 10s timeout, and 4 MiB cap asfetchOpenAPIv3FromURL.Render
SourceMcpIcon.MCPServerCard/MCPServerTableRowalready read it (landed in feat: unproxied MCP server source #4886), so they're untouched.collections.listServersreturnsicon_urlfrom 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.DetectContentTypeover the first 512 bytes rather than from the declaredContent-Typeheader. Catalog hosts routinely serve icons asapplication/octet-stream, or serve an HTML error page with a200and a.pngpath — 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:
setDefaultUnproxiedIcon) runs backgrounded and usesSetDefaultLogoIfUnset, while the install'smcpMetadata.setis unconditional. Either interleaving ends with the catalog icon winning over the gstatic favicon, which is the outcome we want.LEFT JOIN mcp_metadatain bothlistServersqueries can't multiply rows:mcp_metadatahas partial unique indexes ontoolset_idand onmcp_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'sexternalmcpbranch still renders a placeholder despite carrying aniconUrl— out of scope here, since catalog installs are remote-MCP-backed as of AGE-2881.Testing
server/internal/assets/fetchimagefromurl_test.go: success, hash dedup, content type detected from bytes, non-image rejected, SVG rejected, upstream error, invalid scheme, unauthorized.server/internal/collections/listservers_icon_test.go:icon_urlpopulated for toolset-backed and mcp_server-backed attachments, and nil with no logo.mise lint:serverandpnpm -F dashboard type-checkpass.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_metadataand serving it in UI surfaces.assets.fetchImageFromURLto ingest third-party icons server-side; install workflow sets the server’smcp_metadatalogo for remote and unproxied paths (best-effort).SourceMcpIcon; collections now surfaceicon_urlfor toolset-backed andmcp_server-backed entries.assets.fetchImageFromURL.Written for commit 0f6eb11. Summary will update on new commits.