diff --git a/apps/mobile/assets/services/airops.png b/apps/mobile/assets/services/airops.png deleted file mode 100644 index a70365de09..0000000000 Binary files a/apps/mobile/assets/services/airops.png and /dev/null differ diff --git a/apps/mobile/assets/services/atlassian.svg b/apps/mobile/assets/services/atlassian.svg deleted file mode 100644 index 8a9e9f402e..0000000000 --- a/apps/mobile/assets/services/atlassian.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/mobile/assets/services/attio.png b/apps/mobile/assets/services/attio.png deleted file mode 100644 index 3a17902fe4..0000000000 Binary files a/apps/mobile/assets/services/attio.png and /dev/null differ diff --git a/apps/mobile/assets/services/box.svg b/apps/mobile/assets/services/box.svg deleted file mode 100644 index 5eec50e55d..0000000000 --- a/apps/mobile/assets/services/box.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/mobile/assets/services/browserbase.svg b/apps/mobile/assets/services/browserbase.svg deleted file mode 100644 index 5a168aa0ef..0000000000 --- a/apps/mobile/assets/services/browserbase.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/mobile/assets/services/canva.svg b/apps/mobile/assets/services/canva.svg deleted file mode 100644 index 938fcd63e0..0000000000 --- a/apps/mobile/assets/services/canva.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/mobile/assets/services/circle.png b/apps/mobile/assets/services/circle.png deleted file mode 100644 index a020b87f5a..0000000000 Binary files a/apps/mobile/assets/services/circle.png and /dev/null differ diff --git a/apps/mobile/assets/services/cisco_thousandeyes.png b/apps/mobile/assets/services/cisco_thousandeyes.png deleted file mode 100644 index 5460ea5ad4..0000000000 Binary files a/apps/mobile/assets/services/cisco_thousandeyes.png and /dev/null differ diff --git a/apps/mobile/assets/services/clerk.svg b/apps/mobile/assets/services/clerk.svg deleted file mode 100644 index 6870089b04..0000000000 --- a/apps/mobile/assets/services/clerk.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/apps/mobile/assets/services/clickhouse.svg b/apps/mobile/assets/services/clickhouse.svg deleted file mode 100644 index 6484dc6b01..0000000000 --- a/apps/mobile/assets/services/clickhouse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/mobile/assets/services/cloudflare.svg b/apps/mobile/assets/services/cloudflare.svg deleted file mode 100644 index 11ff6493a9..0000000000 --- a/apps/mobile/assets/services/cloudflare.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/apps/mobile/assets/services/context7.svg b/apps/mobile/assets/services/context7.svg deleted file mode 100644 index 664bf9d94a..0000000000 --- a/apps/mobile/assets/services/context7.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/apps/mobile/assets/services/datadog.svg b/apps/mobile/assets/services/datadog.svg deleted file mode 100644 index bb83b63370..0000000000 --- a/apps/mobile/assets/services/datadog.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/apps/mobile/assets/services/figma.svg b/apps/mobile/assets/services/figma.svg deleted file mode 100644 index 5c8bb0d731..0000000000 --- a/apps/mobile/assets/services/figma.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/mobile/assets/services/firetiger.svg b/apps/mobile/assets/services/firetiger.svg deleted file mode 100644 index d9c1a670f6..0000000000 --- a/apps/mobile/assets/services/firetiger.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/mobile/assets/services/github.svg b/apps/mobile/assets/services/github.svg deleted file mode 100755 index 4f46bf3571..0000000000 --- a/apps/mobile/assets/services/github.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/mobile/assets/services/gitlab.svg b/apps/mobile/assets/services/gitlab.svg deleted file mode 100644 index e0e22ac32e..0000000000 --- a/apps/mobile/assets/services/gitlab.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - Group - Created with Sketch. - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/mobile/assets/services/granola.svg b/apps/mobile/assets/services/granola.svg deleted file mode 100644 index e7d62b1455..0000000000 --- a/apps/mobile/assets/services/granola.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/apps/mobile/assets/services/hex.svg b/apps/mobile/assets/services/hex.svg deleted file mode 100644 index f9aeebe334..0000000000 --- a/apps/mobile/assets/services/hex.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/mobile/assets/services/hubspot.svg b/apps/mobile/assets/services/hubspot.svg deleted file mode 100644 index 9423b8af9e..0000000000 --- a/apps/mobile/assets/services/hubspot.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - Group 29 - Created with Sketch. - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/mobile/assets/services/launchdarkly.png b/apps/mobile/assets/services/launchdarkly.png deleted file mode 100644 index 75a86dda1b..0000000000 Binary files a/apps/mobile/assets/services/launchdarkly.png and /dev/null differ diff --git a/apps/mobile/assets/services/linear.svg b/apps/mobile/assets/services/linear.svg deleted file mode 100644 index 53d625598d..0000000000 --- a/apps/mobile/assets/services/linear.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/mobile/assets/services/mem0.svg b/apps/mobile/assets/services/mem0.svg deleted file mode 100644 index 13a9616ae3..0000000000 --- a/apps/mobile/assets/services/mem0.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/mobile/assets/services/monday.svg b/apps/mobile/assets/services/monday.svg deleted file mode 100644 index c2feb2b4c0..0000000000 --- a/apps/mobile/assets/services/monday.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/apps/mobile/assets/services/neon.svg b/apps/mobile/assets/services/neon.svg deleted file mode 100644 index eddf840522..0000000000 --- a/apps/mobile/assets/services/neon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/apps/mobile/assets/services/notion.svg b/apps/mobile/assets/services/notion.svg deleted file mode 100644 index bf6442f76a..0000000000 --- a/apps/mobile/assets/services/notion.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/mobile/assets/services/pagerduty.svg b/apps/mobile/assets/services/pagerduty.svg deleted file mode 100644 index 477f6bb3df..0000000000 --- a/apps/mobile/assets/services/pagerduty.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 216px copy - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/apps/mobile/assets/services/planetscale.svg b/apps/mobile/assets/services/planetscale.svg deleted file mode 100644 index 2f3a95c3c6..0000000000 --- a/apps/mobile/assets/services/planetscale.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/apps/mobile/assets/services/postman.svg b/apps/mobile/assets/services/postman.svg deleted file mode 100644 index 87ed4ac39a..0000000000 --- a/apps/mobile/assets/services/postman.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/mobile/assets/services/prisma.svg b/apps/mobile/assets/services/prisma.svg deleted file mode 100644 index fbf24acc72..0000000000 --- a/apps/mobile/assets/services/prisma.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/mobile/assets/services/render.svg b/apps/mobile/assets/services/render.svg deleted file mode 100644 index cfb5a52e5c..0000000000 --- a/apps/mobile/assets/services/render.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/apps/mobile/assets/services/sanity.svg b/apps/mobile/assets/services/sanity.svg deleted file mode 100644 index 613194b4c2..0000000000 --- a/apps/mobile/assets/services/sanity.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/apps/mobile/assets/services/sentry.svg b/apps/mobile/assets/services/sentry.svg deleted file mode 100644 index d810538965..0000000000 --- a/apps/mobile/assets/services/sentry.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/mobile/assets/services/slack.png b/apps/mobile/assets/services/slack.png deleted file mode 100644 index b034210831..0000000000 Binary files a/apps/mobile/assets/services/slack.png and /dev/null differ diff --git a/apps/mobile/assets/services/stripe.png b/apps/mobile/assets/services/stripe.png deleted file mode 100644 index 26850e852e..0000000000 Binary files a/apps/mobile/assets/services/stripe.png and /dev/null differ diff --git a/apps/mobile/assets/services/supabase.svg b/apps/mobile/assets/services/supabase.svg deleted file mode 100644 index cb6632ff5d..0000000000 --- a/apps/mobile/assets/services/supabase.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/apps/mobile/assets/services/svelte.png b/apps/mobile/assets/services/svelte.png deleted file mode 100644 index 7008ac16e6..0000000000 Binary files a/apps/mobile/assets/services/svelte.png and /dev/null differ diff --git a/apps/mobile/assets/services/wix.png b/apps/mobile/assets/services/wix.png deleted file mode 100644 index ee0a13b0d3..0000000000 Binary files a/apps/mobile/assets/services/wix.png and /dev/null differ diff --git a/apps/mobile/src/app/mcp-servers/installation/[id].tsx b/apps/mobile/src/app/mcp-servers/installation/[id].tsx index 21c5edf5be..d0dd6fcd02 100644 --- a/apps/mobile/src/app/mcp-servers/installation/[id].tsx +++ b/apps/mobile/src/app/mcp-servers/installation/[id].tsx @@ -158,7 +158,11 @@ export default function McpInstallationDetailScreen() { > {/* Header */} - + {installation.display_name || installation.name} diff --git a/apps/mobile/src/app/mcp-servers/template/[id].tsx b/apps/mobile/src/app/mcp-servers/template/[id].tsx index 1bc61b2e92..78e138c19a 100644 --- a/apps/mobile/src/app/mcp-servers/template/[id].tsx +++ b/apps/mobile/src/app/mcp-servers/template/[id].tsx @@ -121,7 +121,11 @@ export default function McpTemplateDetailScreen() { }} > - + {template.name} diff --git a/apps/mobile/src/features/mcp/components/McpServerRow.tsx b/apps/mobile/src/features/mcp/components/McpServerRow.tsx index a2526fa348..0cfd05775f 100644 --- a/apps/mobile/src/features/mcp/components/McpServerRow.tsx +++ b/apps/mobile/src/features/mcp/components/McpServerRow.tsx @@ -16,7 +16,8 @@ interface McpServerRowProps { isStdio?: boolean; needsReauth?: boolean; installed?: boolean; - iconKey?: string | null; + iconDomain?: string | null; + serverUrl?: string | null; onPress: () => void; } @@ -35,7 +36,8 @@ export function McpServerRow({ isStdio, needsReauth, installed, - iconKey, + iconDomain, + serverUrl, onPress, }: McpServerRowProps) { const themeColors = useThemeColors(); @@ -46,7 +48,7 @@ export function McpServerRow({ onPress={onPress} className="flex-row items-center gap-3 border-gray-5 border-b bg-card px-4 py-3 active:bg-gray-2" > - + onPress(template), }; } @@ -137,7 +140,8 @@ export function installationToRowProps( isStdio: isStdioServer(installation), needsReauth: installation.needs_reauth, installed: true, - iconKey: installation.icon_key, + iconDomain: installation.icon_domain, + serverUrl: installation.url, onPress: () => onPress(installation), }; } diff --git a/apps/mobile/src/features/mcp/components/ServerIcon.tsx b/apps/mobile/src/features/mcp/components/ServerIcon.tsx index f0fe08ef5d..ccfb78feb7 100644 --- a/apps/mobile/src/features/mcp/components/ServerIcon.tsx +++ b/apps/mobile/src/features/mcp/components/ServerIcon.tsx @@ -1,35 +1,73 @@ +import { useColorScheme } from "nativewind"; import { PuzzlePiece } from "phosphor-react-native"; +import { useState } from "react"; import { Image, View } from "react-native"; +import { useAuthStore } from "@/features/auth"; import { useThemeColors } from "@/lib/theme"; -import { resolveServerLogo } from "./serverIcons"; +import { iconDomainFromServerUrl } from "../iconDomain"; interface ServerIconProps { - iconKey?: string | null; + /** The template's brand domain (`icon_domain`). Falls back to deriving one from serverUrl. */ + iconDomain?: string | null; + /** The MCP server URL — lets custom installs without a template still get a brand icon. */ + serverUrl?: string | null; size?: number; className?: string; } /** - * Renders the brand logo for an MCP server, keyed by `icon_key` from the - * PostHog cloud schema. Falls back to a generic plug glyph when the icon - * key is missing or doesn't match the bundled set. + * Renders the brand logo for an MCP server through the authenticated logo.dev + * icon proxy (`mcp_servers/icon/`), keyed by the template's `icon_domain` with + * a best-effort domain derived from the server URL as fallback. Falls back to + * a generic plug glyph when no domain resolves or the proxy has no icon (404). */ -export function ServerIcon({ iconKey, size = 32, className }: ServerIconProps) { +export function ServerIcon({ + iconDomain, + serverUrl, + size = 32, + className, +}: ServerIconProps) { const themeColors = useThemeColors(); - const logo = resolveServerLogo(iconKey); + const { colorScheme } = useColorScheme(); + const oauthAccessToken = useAuthStore((state) => state.oauthAccessToken); + const cloudRegion = useAuthStore((state) => state.cloudRegion); + const projectId = useAuthStore((state) => state.projectId); + const getCloudUrlFromRegion = useAuthStore( + (state) => state.getCloudUrlFromRegion, + ); + + const domain = iconDomain || iconDomainFromServerUrl(serverUrl); + // logo.dev picks the logo variant suited to the active background theme. + const theme = colorScheme === "dark" ? "dark" : "light"; + // Failure latches per (domain, theme) — the unit the request URL varies + // over — so a transient failure in one theme doesn't blank the other and a + // theme flip retries. + const iconCacheKey = `${domain}|${theme}`; + const [failedIconKey, setFailedIconKey] = useState(null); + + const iconUrl = + domain && + oauthAccessToken && + cloudRegion && + projectId && + failedIconKey !== iconCacheKey + ? `${getCloudUrlFromRegion(cloudRegion)}/api/environments/${projectId}/mcp_servers/icon/?domain=${encodeURIComponent(domain)}&theme=${theme}` + : null; return ( - {logo?.kind === "svg" && typeof logo.component === "function" ? ( - - ) : logo?.kind === "png" ? ( + {iconUrl ? ( setFailedIconKey(iconCacheKey)} /> ) : ( `. -const AiropsPng: ImageSourcePropType = require("../../../../assets/services/airops.png"); -const AttioPng: ImageSourcePropType = require("../../../../assets/services/attio.png"); -const CirclePng: ImageSourcePropType = require("../../../../assets/services/circle.png"); -const CiscoThousandeyesPng: ImageSourcePropType = require("../../../../assets/services/cisco_thousandeyes.png"); -const LaunchDarklyPng: ImageSourcePropType = require("../../../../assets/services/launchdarkly.png"); -const SlackPng: ImageSourcePropType = require("../../../../assets/services/slack.png"); -const StripePng: ImageSourcePropType = require("../../../../assets/services/stripe.png"); -const SveltePng: ImageSourcePropType = require("../../../../assets/services/svelte.png"); -const WixPng: ImageSourcePropType = require("../../../../assets/services/wix.png"); - -export type ServerLogo = - | { kind: "svg"; component: ComponentType } - | { kind: "png"; source: ImageSourcePropType }; - -function svg(component: ComponentType): ServerLogo { - if (typeof component !== "function") { - log.warn("SVG import resolved as non-component", { - type: typeof component, - }); - } - return { kind: "svg", component }; -} - -function png(source: ImageSourcePropType): ServerLogo { - return { kind: "png", source }; -} - -/** Lookup map keyed by `McpServerInstallation.icon_key` / - * `McpRecommendedServer.icon_key`. Mirrors the desktop `BRAND_ICONS`. */ -export const SERVER_LOGOS: Record = { - airops: png(AiropsPng), - atlassian: svg(AtlassianSvg), - attio: png(AttioPng), - box: svg(BoxSvg), - browserbase: svg(BrowserbaseSvg), - canva: svg(CanvaSvg), - circle: png(CirclePng), - cisco_thousandeyes: png(CiscoThousandeyesPng), - clerk: svg(ClerkSvg), - clickhouse: svg(ClickHouseSvg), - cloudflare: svg(CloudflareSvg), - context7: svg(Context7Svg), - datadog: svg(DatadogSvg), - figma: svg(FigmaSvg), - firetiger: svg(FiretigerSvg), - github: svg(GitHubSvg), - gitlab: svg(GitLabSvg), - granola: svg(GranolaSvg), - hex: svg(HexSvg), - hubspot: svg(HubSpotSvg), - launchdarkly: png(LaunchDarklyPng), - linear: svg(LinearSvg), - mem0: svg(Mem0Svg), - monday: svg(MondaySvg), - neon: svg(NeonSvg), - notion: svg(NotionSvg), - pagerduty: svg(PagerDutySvg), - planetscale: svg(PlanetScaleSvg), - postman: svg(PostmanSvg), - prisma: svg(PrismaSvg), - render: svg(RenderSvg), - sanity: svg(SanitySvg), - sentry: svg(SentrySvg), - slack: png(SlackPng), - stripe: png(StripePng), - supabase: svg(SupabaseSvg), - svelte: png(SveltePng), - wix: png(WixPng), -}; - -export function resolveServerLogo( - iconKey: string | null | undefined, -): ServerLogo | null { - if (!iconKey) return null; - return SERVER_LOGOS[iconKey] ?? null; -} diff --git a/apps/mobile/src/features/mcp/iconDomain.test.ts b/apps/mobile/src/features/mcp/iconDomain.test.ts new file mode 100644 index 0000000000..9cdcffb786 --- /dev/null +++ b/apps/mobile/src/features/mcp/iconDomain.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { iconDomainFromServerUrl } from "./iconDomain"; + +// Mirrors packages/core/src/mcp-servers/iconDomain.test.ts — the helper is +// duplicated because mobile doesn't depend on @posthog/core. +describe("iconDomainFromServerUrl", () => { + it.each([ + ["strips mcp. from deep hosts", "https://mcp.linear.app/sse", "linear.app"], + ["strips api. from deep hosts", "https://api.acme.com/mcp", "acme.com"], + ["strips www. from deep hosts", "https://www.acme.com/mcp", "acme.com"], + ["keeps bare brand domains", "https://notion.com/mcp", "notion.com"], + [ + "keeps two-label hosts named like a prefix", + "https://mcp.com/x", + "mcp.com", + ], + [ + "keeps unrelated subdomains", + "https://server.smithery.ai/mcp", + "server.smithery.ai", + ], + ["lowercases the host", "https://MCP.Linear.APP/sse", "linear.app"], + ])("%s", (_name, url, expected) => { + expect(iconDomainFromServerUrl(url)).toBe(expected); + }); + + it.each([ + ["null", null], + ["undefined", undefined], + ["an empty string", ""], + ["a non-URL", "not a url"], + ["a dotless host", "https://localhost:3000/mcp"], + ])("returns null for %s", (_name, url) => { + expect(iconDomainFromServerUrl(url)).toBeNull(); + }); +}); diff --git a/apps/mobile/src/features/mcp/iconDomain.ts b/apps/mobile/src/features/mcp/iconDomain.ts new file mode 100644 index 0000000000..a5c5f89265 --- /dev/null +++ b/apps/mobile/src/features/mcp/iconDomain.ts @@ -0,0 +1,33 @@ +// Mirrors `@posthog/core/mcp-servers/iconDomain` (mobile doesn't depend on +// @posthog/core, same as the hand-mirrored types in ./types.ts). + +// Machine-facing subdomains stripped when deriving a brand domain from a +// server URL, so a custom install at https://mcp.linear.app/mcp still +// resolves the vendor's brand (linear.app). +const STRIPPED_SUBDOMAINS = ["mcp.", "api.", "www."]; + +/** + * Best-effort brand domain for an MCP server without a template-provided + * `icon_domain`: the server URL's hostname with machine-facing subdomains + * stripped. Null when no plausible brand domain exists (invalid URL, or a + * dotless host like localhost). + */ +export function iconDomainFromServerUrl( + serverUrl: string | null | undefined, +): string | null { + if (!serverUrl) { + return null; + } + let host: string; + try { + host = new URL(serverUrl).hostname.toLowerCase(); + } catch { + return null; + } + for (const prefix of STRIPPED_SUBDOMAINS) { + if (host.startsWith(prefix) && host.split(".").length >= 3) { + return host.slice(prefix.length); + } + } + return host.includes(".") ? host : null; +} diff --git a/apps/mobile/src/features/mcp/types.ts b/apps/mobile/src/features/mcp/types.ts index b8cdb4c439..74f259ef0c 100644 --- a/apps/mobile/src/features/mcp/types.ts +++ b/apps/mobile/src/features/mcp/types.ts @@ -15,7 +15,9 @@ export interface McpRecommendedServer { docs_url?: string; description?: string; auth_type?: McpAuthType; - icon_key?: string; + /** The vendor's brand domain (e.g. "linear.app"), rendered via the + * logo.dev icon proxy. Empty when no brand icon is known. */ + icon_domain?: string; category?: string; /** Some templates expose a `transport_type` ("stdio" | "streamable_http"); when * absent, treat as HTTP. Stdio servers can't run on mobile; we badge them. */ @@ -27,7 +29,9 @@ export interface McpServerInstallation { id: string; template_id: string | null; name: string; - icon_key: string; + /** Brand domain from the linked template, rendered via the logo.dev icon + * proxy. Empty if custom install (no template). */ + icon_domain?: string; display_name?: string; url?: string; description?: string; diff --git a/packages/api-client/src/generated.ts b/packages/api-client/src/generated.ts index 4f16fdb00c..fc57711b82 100644 --- a/packages/api-client/src/generated.ts +++ b/packages/api-client/src/generated.ts @@ -9711,7 +9711,10 @@ export namespace Schemas { id: string; template_id: string | null; name: string; + /** Deprecated: use icon_domain instead. Lowercase key from the linked template for clients that still render bundled icon assets. Empty if custom install (no template). */ icon_key: string; + /** Brand domain from the linked template, rendered via the logo.dev icon proxy. Empty if custom install (no template). */ + icon_domain: string; display_name?: string | undefined; url?: string | undefined; description?: string | undefined; @@ -9744,7 +9747,10 @@ export namespace Schemas { docs_url?: string | undefined; description?: string | undefined; auth_type?: MCPAuthTypeEnum | undefined; - icon_key?: string | undefined; + /** Deprecated: use icon_domain instead. Lowercase key for clients that still render bundled icon assets. */ + icon_key: string; + /** The vendor's brand domain (e.g. 'linear.app'), resolved to an icon at render time via the logo.dev proxy endpoint. Empty when no brand icon is known. */ + icon_domain: string; category?: CategoryEnum | undefined; }; export type MarkToleratedInput = { snapshot_id: string }; diff --git a/packages/api-client/src/posthog-client.test.ts b/packages/api-client/src/posthog-client.test.ts index 927fd90fc2..9eb76b1cfe 100644 --- a/packages/api-client/src/posthog-client.test.ts +++ b/packages/api-client/src/posthog-client.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from "vitest"; +import { ApiRequestError } from "./fetcher"; import { PostHogAPIClient } from "./posthog-client"; describe("PostHogAPIClient", () => { @@ -1763,4 +1764,70 @@ describe("PostHogAPIClient", () => { }); }); }); + + describe("getMcpServerIconUrl", () => { + function makeClient(fetch: ReturnType) { + const client = new PostHogAPIClient( + "http://localhost:8000", + async () => "token", + async () => "token", + 123, + ); + ( + client as unknown as { + api: { baseUrl: string; fetcher: { fetch: typeof fetch } }; + } + ).api = { baseUrl: "http://localhost:8000", fetcher: { fetch } }; + return client; + } + + it("requests the icon proxy and returns an object URL for the bytes", async () => { + const fetch = vi + .fn() + .mockResolvedValue( + new Response(new Blob(["png"], { type: "image/png" })), + ); + const client = makeClient(fetch); + + const url = await client.getMcpServerIconUrl("linear.app", "dark"); + + expect(url).toMatch(/^blob:/); + expect(fetch.mock.calls[0][0].url.toString()).toBe( + "http://localhost:8000/api/environments/123/mcp_servers/icon/?domain=linear.app&theme=dark", + ); + }); + + it("omits the theme param when none is given", async () => { + const fetch = vi + .fn() + .mockResolvedValue( + new Response(new Blob(["png"], { type: "image/png" })), + ); + const client = makeClient(fetch); + + await client.getMcpServerIconUrl("linear.app"); + + expect(fetch.mock.calls[0][0].url.toString()).toBe( + "http://localhost:8000/api/environments/123/mcp_servers/icon/?domain=linear.app", + ); + }); + + it("treats the proxy's 404 as a definitive no-icon null, not a failure", async () => { + const fetch = vi.fn().mockRejectedValue(new ApiRequestError(404, "{}")); + const client = makeClient(fetch); + + await expect( + client.getMcpServerIconUrl("no-logo.example"), + ).resolves.toBeNull(); + }); + + it("propagates non-404 failures so callers can retry", async () => { + const fetch = vi.fn().mockRejectedValue(new ApiRequestError(500, "{}")); + const client = makeClient(fetch); + + await expect(client.getMcpServerIconUrl("linear.app")).rejects.toThrow( + "Failed request: [500]", + ); + }); + }); }); diff --git a/packages/api-client/src/posthog-client.ts b/packages/api-client/src/posthog-client.ts index bc33a43596..e4d994957b 100644 --- a/packages/api-client/src/posthog-client.ts +++ b/packages/api-client/src/posthog-client.ts @@ -96,7 +96,7 @@ import { type HogQLGrid, shapeAgentAnalytics, } from "./agent-analytics"; -import { buildApiFetcher } from "./fetcher"; +import { buildApiFetcher, requestErrorStatus } from "./fetcher"; import { createApiClient, type Schemas } from "./generated"; import type { SpendAnalysisResponse } from "./spend-analysis"; export interface ApiClientLogger { @@ -4200,6 +4200,40 @@ export class PostHogAPIClient { return data.results ?? []; } + /** + * Object URL for an MCP server's brand icon, proxied from logo.dev by the + * authenticated `mcp_servers/icon/` endpoint. Returns null when no brand + * icon exists for the domain (the endpoint 404s so callers render their own + * fallback glyph, e.g. on self-hosted instances without a logo.dev token). + */ + async getMcpServerIconUrl( + domain: string, + theme?: "light" | "dark", + ): Promise { + const teamId = await this.getTeamId(); + const path = `/api/environments/${teamId}/mcp_servers/icon/`; + const url = new URL(`${this.api.baseUrl}${path}`); + url.searchParams.set("domain", domain); + if (theme) { + url.searchParams.set("theme", theme); + } + let response: Response; + try { + response = await this.api.fetcher.fetch({ + method: "get", + url, + path, + }); + } catch (error) { + // 404 is the endpoint's definitive "no icon for this domain" answer, + // not a failure; anything else propagates so callers can retry. + if (requestErrorStatus(error) === 404) return null; + throw error; + } + const blob = await response.blob(); + return URL.createObjectURL(blob); + } + async getMcpServerInstallations(): Promise { const teamId = await this.getTeamId(); const url = new URL( diff --git a/packages/core/src/mcp-servers/iconDomain.test.ts b/packages/core/src/mcp-servers/iconDomain.test.ts new file mode 100644 index 0000000000..b32786a330 --- /dev/null +++ b/packages/core/src/mcp-servers/iconDomain.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { iconDomainFromServerUrl } from "./iconDomain"; + +describe("iconDomainFromServerUrl", () => { + it.each([ + ["strips mcp. from deep hosts", "https://mcp.linear.app/sse", "linear.app"], + ["strips api. from deep hosts", "https://api.acme.com/mcp", "acme.com"], + ["strips www. from deep hosts", "https://www.acme.com/mcp", "acme.com"], + ["keeps bare brand domains", "https://notion.com/mcp", "notion.com"], + [ + "keeps two-label hosts named like a prefix", + "https://mcp.com/x", + "mcp.com", + ], + [ + "keeps unrelated subdomains", + "https://server.smithery.ai/mcp", + "server.smithery.ai", + ], + ["lowercases the host", "https://MCP.Linear.APP/sse", "linear.app"], + ])("%s", (_name, url, expected) => { + expect(iconDomainFromServerUrl(url)).toBe(expected); + }); + + it.each([ + ["null", null], + ["undefined", undefined], + ["an empty string", ""], + ["a non-URL", "not a url"], + ["a dotless host", "https://localhost:3000/mcp"], + ])("returns null for %s", (_name, url) => { + expect(iconDomainFromServerUrl(url)).toBeNull(); + }); +}); diff --git a/packages/core/src/mcp-servers/iconDomain.ts b/packages/core/src/mcp-servers/iconDomain.ts new file mode 100644 index 0000000000..8f7506c0ca --- /dev/null +++ b/packages/core/src/mcp-servers/iconDomain.ts @@ -0,0 +1,30 @@ +// Machine-facing subdomains stripped when deriving a brand domain from a +// server URL, so a custom install at https://mcp.linear.app/mcp still +// resolves the vendor's brand (linear.app). +const STRIPPED_SUBDOMAINS = ["mcp.", "api.", "www."]; + +/** + * Best-effort brand domain for an MCP server without a template-provided + * `icon_domain`: the server URL's hostname with machine-facing subdomains + * stripped. Null when no plausible brand domain exists (invalid URL, or a + * dotless host like localhost). + */ +export function iconDomainFromServerUrl( + serverUrl: string | null | undefined, +): string | null { + if (!serverUrl) { + return null; + } + let host: string; + try { + host = new URL(serverUrl).hostname.toLowerCase(); + } catch { + return null; + } + for (const prefix of STRIPPED_SUBDOMAINS) { + if (host.startsWith(prefix) && host.split(".").length >= 3) { + return host.slice(prefix.length); + } + } + return host.includes(".") ? host : null; +} diff --git a/packages/core/src/mcp-servers/resolveServerName.test.ts b/packages/core/src/mcp-servers/resolveServerName.test.ts index 2960eecc37..93d5a76459 100644 --- a/packages/core/src/mcp-servers/resolveServerName.test.ts +++ b/packages/core/src/mcp-servers/resolveServerName.test.ts @@ -16,7 +16,7 @@ function installation( id: "inst-1", template_id: null, name: "", - icon_key: "", + icon_domain: "", proxy_url: "https://proxy.example.com/inst-1", tool_count: 0, created_at: "2026-01-01T00:00:00Z", @@ -59,19 +59,33 @@ describe("resolveServerName", () => { describe("resolveServerDetails", () => { it("resolves description/docs/icon/auth fallbacks", () => { const out = resolveServerDetails( - installation({ name: "N", icon_key: "" }), + installation({ + name: "N", + icon_domain: "", + url: "https://mcp.acme.dev/mcp", + }), template({ description: "desc", docs_url: "https://docs", - icon_key: "k", + icon_domain: "linear.app", }), ); expect(out.name).toBe("N"); expect(out.description).toBe("desc"); expect(out.docsUrl).toBe("https://docs"); - expect(out.iconKey).toBe("k"); + expect(out.iconDomain).toBe("linear.app"); + expect(out.serverUrl).toBe("https://mcp.acme.dev/mcp"); expect(out.authType).toBe("oauth"); }); + + it("falls back to the template url when the installation has none", () => { + const out = resolveServerDetails( + null, + template({ icon_domain: "", url: "https://mcp.linear.app/sse" }), + ); + expect(out.iconDomain).toBeNull(); + expect(out.serverUrl).toBe("https://mcp.linear.app/sse"); + }); }); describe("sortInstallationsByName", () => { diff --git a/packages/core/src/mcp-servers/resolveServerName.ts b/packages/core/src/mcp-servers/resolveServerName.ts index f63e540b1e..e7facc61da 100644 --- a/packages/core/src/mcp-servers/resolveServerName.ts +++ b/packages/core/src/mcp-servers/resolveServerName.ts @@ -20,7 +20,8 @@ export interface ResolvedServerDetails { name: string; description: string; docsUrl: string | null; - iconKey: string | null; + iconDomain: string | null; + serverUrl: string | null; authType: McpRecommendedServer["auth_type"] | undefined; } @@ -37,7 +38,8 @@ export function resolveServerDetails( "Server", description: installation?.description || template?.description || "", docsUrl: template?.docs_url || null, - iconKey: installation?.icon_key || template?.icon_key || null, + iconDomain: installation?.icon_domain || template?.icon_domain || null, + serverUrl: installation?.url || template?.url || null, authType: installation?.auth_type || template?.auth_type, }; } diff --git a/packages/core/src/mcp-servers/status.test.ts b/packages/core/src/mcp-servers/status.test.ts index 8b6d02c9ca..23bb277a1b 100644 --- a/packages/core/src/mcp-servers/status.test.ts +++ b/packages/core/src/mcp-servers/status.test.ts @@ -10,6 +10,7 @@ function makeInstallation( template_id: null, name: "Test", icon_key: "", + icon_domain: "", proxy_url: "https://proxy.example.com/inst-1", tool_count: 0, created_at: "2026-01-01T00:00:00Z", diff --git a/packages/ui/src/assets/services/airops.png b/packages/ui/src/assets/services/airops.png deleted file mode 100644 index a70365de09..0000000000 Binary files a/packages/ui/src/assets/services/airops.png and /dev/null differ diff --git a/packages/ui/src/assets/services/atlassian.svg b/packages/ui/src/assets/services/atlassian.svg deleted file mode 100644 index 8a9e9f402e..0000000000 --- a/packages/ui/src/assets/services/atlassian.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/ui/src/assets/services/attio.png b/packages/ui/src/assets/services/attio.png deleted file mode 100644 index 6065fbe897..0000000000 Binary files a/packages/ui/src/assets/services/attio.png and /dev/null differ diff --git a/packages/ui/src/assets/services/box.svg b/packages/ui/src/assets/services/box.svg deleted file mode 100644 index 5eec50e55d..0000000000 --- a/packages/ui/src/assets/services/box.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/ui/src/assets/services/browserbase.svg b/packages/ui/src/assets/services/browserbase.svg deleted file mode 100644 index 5a168aa0ef..0000000000 --- a/packages/ui/src/assets/services/browserbase.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/packages/ui/src/assets/services/canva.svg b/packages/ui/src/assets/services/canva.svg deleted file mode 100644 index 938fcd63e0..0000000000 --- a/packages/ui/src/assets/services/canva.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/ui/src/assets/services/circle.png b/packages/ui/src/assets/services/circle.png deleted file mode 100644 index a020b87f5a..0000000000 Binary files a/packages/ui/src/assets/services/circle.png and /dev/null differ diff --git a/packages/ui/src/assets/services/cisco_thousandeyes.png b/packages/ui/src/assets/services/cisco_thousandeyes.png deleted file mode 100644 index 5460ea5ad4..0000000000 Binary files a/packages/ui/src/assets/services/cisco_thousandeyes.png and /dev/null differ diff --git a/packages/ui/src/assets/services/clerk.svg b/packages/ui/src/assets/services/clerk.svg deleted file mode 100644 index 6870089b04..0000000000 --- a/packages/ui/src/assets/services/clerk.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/packages/ui/src/assets/services/clickhouse.svg b/packages/ui/src/assets/services/clickhouse.svg deleted file mode 100644 index 6484dc6b01..0000000000 --- a/packages/ui/src/assets/services/clickhouse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/ui/src/assets/services/cloudflare.svg b/packages/ui/src/assets/services/cloudflare.svg deleted file mode 100644 index 11ff6493a9..0000000000 --- a/packages/ui/src/assets/services/cloudflare.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/packages/ui/src/assets/services/context7.svg b/packages/ui/src/assets/services/context7.svg deleted file mode 100644 index 664bf9d94a..0000000000 --- a/packages/ui/src/assets/services/context7.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/packages/ui/src/assets/services/datadog.svg b/packages/ui/src/assets/services/datadog.svg deleted file mode 100644 index bb83b63370..0000000000 --- a/packages/ui/src/assets/services/datadog.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/ui/src/assets/services/figma.svg b/packages/ui/src/assets/services/figma.svg deleted file mode 100644 index 5c8bb0d731..0000000000 --- a/packages/ui/src/assets/services/figma.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/ui/src/assets/services/firetiger.svg b/packages/ui/src/assets/services/firetiger.svg deleted file mode 100644 index d9c1a670f6..0000000000 --- a/packages/ui/src/assets/services/firetiger.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/ui/src/assets/services/github.svg b/packages/ui/src/assets/services/github.svg deleted file mode 100755 index 4f46bf3571..0000000000 --- a/packages/ui/src/assets/services/github.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/packages/ui/src/assets/services/gitlab.svg b/packages/ui/src/assets/services/gitlab.svg deleted file mode 100644 index e0e22ac32e..0000000000 --- a/packages/ui/src/assets/services/gitlab.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - Group - Created with Sketch. - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/ui/src/assets/services/granola.svg b/packages/ui/src/assets/services/granola.svg deleted file mode 100644 index e7d62b1455..0000000000 --- a/packages/ui/src/assets/services/granola.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/packages/ui/src/assets/services/hex.svg b/packages/ui/src/assets/services/hex.svg deleted file mode 100644 index f9aeebe334..0000000000 --- a/packages/ui/src/assets/services/hex.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/ui/src/assets/services/hubspot.svg b/packages/ui/src/assets/services/hubspot.svg deleted file mode 100644 index 9423b8af9e..0000000000 --- a/packages/ui/src/assets/services/hubspot.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - Group 29 - Created with Sketch. - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/ui/src/assets/services/launchdarkly.png b/packages/ui/src/assets/services/launchdarkly.png deleted file mode 100644 index 75a86dda1b..0000000000 Binary files a/packages/ui/src/assets/services/launchdarkly.png and /dev/null differ diff --git a/packages/ui/src/assets/services/linear.svg b/packages/ui/src/assets/services/linear.svg deleted file mode 100644 index 53d625598d..0000000000 --- a/packages/ui/src/assets/services/linear.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/ui/src/assets/services/mem0.svg b/packages/ui/src/assets/services/mem0.svg deleted file mode 100644 index d05ad55dd6..0000000000 --- a/packages/ui/src/assets/services/mem0.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/packages/ui/src/assets/services/monday.svg b/packages/ui/src/assets/services/monday.svg deleted file mode 100644 index c2feb2b4c0..0000000000 --- a/packages/ui/src/assets/services/monday.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/ui/src/assets/services/neon.svg b/packages/ui/src/assets/services/neon.svg deleted file mode 100644 index eddf840522..0000000000 --- a/packages/ui/src/assets/services/neon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/packages/ui/src/assets/services/notion.svg b/packages/ui/src/assets/services/notion.svg deleted file mode 100644 index bf6442f76a..0000000000 --- a/packages/ui/src/assets/services/notion.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ui/src/assets/services/pagerduty.svg b/packages/ui/src/assets/services/pagerduty.svg deleted file mode 100644 index 477f6bb3df..0000000000 --- a/packages/ui/src/assets/services/pagerduty.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 216px copy - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/packages/ui/src/assets/services/planetscale.svg b/packages/ui/src/assets/services/planetscale.svg deleted file mode 100644 index 2f3a95c3c6..0000000000 --- a/packages/ui/src/assets/services/planetscale.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/packages/ui/src/assets/services/postman.svg b/packages/ui/src/assets/services/postman.svg deleted file mode 100644 index 87ed4ac39a..0000000000 --- a/packages/ui/src/assets/services/postman.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/ui/src/assets/services/prisma.svg b/packages/ui/src/assets/services/prisma.svg deleted file mode 100644 index fbf24acc72..0000000000 --- a/packages/ui/src/assets/services/prisma.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/ui/src/assets/services/render.svg b/packages/ui/src/assets/services/render.svg deleted file mode 100644 index cfb5a52e5c..0000000000 --- a/packages/ui/src/assets/services/render.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/ui/src/assets/services/sanity.svg b/packages/ui/src/assets/services/sanity.svg deleted file mode 100644 index 613194b4c2..0000000000 --- a/packages/ui/src/assets/services/sanity.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/packages/ui/src/assets/services/sentry.svg b/packages/ui/src/assets/services/sentry.svg deleted file mode 100644 index d810538965..0000000000 --- a/packages/ui/src/assets/services/sentry.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/ui/src/assets/services/slack.png b/packages/ui/src/assets/services/slack.png deleted file mode 100644 index b034210831..0000000000 Binary files a/packages/ui/src/assets/services/slack.png and /dev/null differ diff --git a/packages/ui/src/assets/services/stripe.png b/packages/ui/src/assets/services/stripe.png deleted file mode 100644 index 26850e852e..0000000000 Binary files a/packages/ui/src/assets/services/stripe.png and /dev/null differ diff --git a/packages/ui/src/assets/services/supabase.svg b/packages/ui/src/assets/services/supabase.svg deleted file mode 100644 index cb6632ff5d..0000000000 --- a/packages/ui/src/assets/services/supabase.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/packages/ui/src/assets/services/svelte.png b/packages/ui/src/assets/services/svelte.png deleted file mode 100644 index 7008ac16e6..0000000000 Binary files a/packages/ui/src/assets/services/svelte.png and /dev/null differ diff --git a/packages/ui/src/assets/services/wix.png b/packages/ui/src/assets/services/wix.png deleted file mode 100644 index ee0a13b0d3..0000000000 Binary files a/packages/ui/src/assets/services/wix.png and /dev/null differ diff --git a/packages/ui/src/features/mcp-server-manager/useMcpConnect.ts b/packages/ui/src/features/mcp-server-manager/useMcpConnect.ts index e7717232ae..0a4e171d56 100644 --- a/packages/ui/src/features/mcp-server-manager/useMcpConnect.ts +++ b/packages/ui/src/features/mcp-server-manager/useMcpConnect.ts @@ -18,6 +18,8 @@ import { useCallback, useMemo } from "react"; export const mcpKeys = { servers: ["mcp", "servers"] as const, installations: ["mcp", "installations"] as const, + icon: (domain: string, theme: "light" | "dark") => + ["mcp", "icon", domain, theme] as const, tools: (installationId: string) => ["mcp", "installations", installationId, "tools"] as const, }; diff --git a/packages/ui/src/features/mcp-servers/components/parts/MarketplaceView.tsx b/packages/ui/src/features/mcp-servers/components/parts/MarketplaceView.tsx index 861754946e..9abd722638 100644 --- a/packages/ui/src/features/mcp-servers/components/parts/MarketplaceView.tsx +++ b/packages/ui/src/features/mcp-servers/components/parts/MarketplaceView.tsx @@ -73,8 +73,8 @@ export function MarketplaceView({ Marketplace - Browse and connect MCP servers that extend your agent with tools, - data and integrations. + Connect MCP servers to make them available to in PostHog on desktop + and in the cloud.