From 130207410f6ebbf3d9a8014ce9e765cd16fd45d2 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 10 Jul 2026 11:39:14 -0700
Subject: [PATCH 1/3] Resolve all favicons through integrations.sh/logo
---
packages/plugins/mcp/src/sdk/presets.ts | 4 ++--
.../components/integration-favicon.test.tsx | 16 +++-----------
.../src/components/integration-favicon.tsx | 22 +++++--------------
3 files changed, 11 insertions(+), 31 deletions(-)
diff --git a/packages/plugins/mcp/src/sdk/presets.ts b/packages/plugins/mcp/src/sdk/presets.ts
index f2a867541..0cced50e7 100644
--- a/packages/plugins/mcp/src/sdk/presets.ts
+++ b/packages/plugins/mcp/src/sdk/presets.ts
@@ -30,7 +30,7 @@ export const mcpPresets: readonly McpPreset[] = [
summary: "Deterministic MCP fixtures for validating native text and image content.",
url: "https://emulators.dev/mcp/query/mcp?token=demo-token",
endpoint: "https://emulators.dev/mcp/query/mcp?token=demo-token",
- icon: "https://emulators.dev/favicon.ico",
+ icon: "https://integrations.sh/logo/emulators.dev",
},
{
id: "deepwiki",
@@ -142,7 +142,7 @@ export const mcpPresets: readonly McpPreset[] = [
id: "chrome-devtools",
name: "Chrome DevTools",
summary: "Debug a live Chrome browser session via local stdio.",
- icon: "https://www.google.com/chrome/static/images/favicons/favicon-32x32.png",
+ icon: "https://integrations.sh/logo/chrome.com",
featured: true,
transport: "stdio",
command: "npx",
diff --git a/packages/react/src/components/integration-favicon.test.tsx b/packages/react/src/components/integration-favicon.test.tsx
index 853cde425..ea597be51 100644
--- a/packages/react/src/components/integration-favicon.test.tsx
+++ b/packages/react/src/components/integration-favicon.test.tsx
@@ -1,7 +1,6 @@
import { describe, expect, it } from "@effect/vitest";
import {
- integrationFaviconFallbackUrl,
integrationFaviconSrc,
integrationFaviconUrl,
integrationInferredUrl,
@@ -10,22 +9,15 @@ import {
} from "./integration-favicon";
describe("IntegrationFavicon", () => {
- it("uses the integrations.sh logo proxy as the primary favicon source", () => {
+ it("resolves favicons only through the integrations.sh logo proxy", () => {
expect(integrationFaviconUrl("https://api.github.com/graphql", 20)).toBe(
"https://integrations.sh/logo/github.com?sz=40",
);
});
- it("falls back to the Google favicon service", () => {
- expect(integrationFaviconFallbackUrl("https://api.github.com/graphql", 20)).toBe(
- "https://www.google.com/s2/favicons?domain=github.com&sz=40",
- );
- });
-
it("does not request favicons for local URLs", () => {
expect(integrationFaviconUrl("http://localhost:3000/private", 20)).toBeNull();
expect(integrationFaviconUrl("http://127.0.0.1:3000/private", 20)).toBeNull();
- expect(integrationFaviconFallbackUrl("http://localhost:3000/private", 20)).toBeNull();
});
it("sends only the registrable domain to the logo proxy", () => {
@@ -34,13 +26,11 @@ describe("IntegrationFavicon", () => {
);
});
- it("walks the cascade from the proxy to the favicon service on failure", () => {
+ it("falls through to the placeholder when the proxy fails", () => {
const url = "https://api.github.com/graphql";
const primary = integrationFaviconSrc({ url, size: 20 });
expect(primary).toBe("https://integrations.sh/logo/github.com?sz=40");
- expect(integrationFaviconSrc({ url, size: 20, failedSrcs: [primary ?? ""] })).toBe(
- "https://www.google.com/s2/favicons?domain=github.com&sz=40",
- );
+ expect(integrationFaviconSrc({ url, size: 20, failedSrcs: [primary ?? ""] })).toBeNull();
});
it("uses the Executor favicon for the built-in executor integration", () => {
diff --git a/packages/react/src/components/integration-favicon.tsx b/packages/react/src/components/integration-favicon.tsx
index ae4a4be70..1d0feef43 100644
--- a/packages/react/src/components/integration-favicon.tsx
+++ b/packages/react/src/components/integration-favicon.tsx
@@ -14,24 +14,15 @@ const integrationFaviconDomain = (url: string | undefined): string | null => {
};
// integrations.sh/logo proxies context.dev's Logo Link behind an edge cache
-// and is executor's single logo source; Google's favicon service remains in
-// the cascade as the fallback the
onError walks to when the proxy is
-// unreachable or serves nothing for the domain.
+// and is executor's single logo source. Fallbacks (Google's favicon service,
+// a letter placeholder for unknown domains) live inside the proxy, so clients
+// never resolve favicons against a third party directly.
export function integrationFaviconUrl(url: string | undefined, size: number): string | null {
const domain = integrationFaviconDomain(url);
if (!domain) return null;
return `https://integrations.sh/logo/${domain}?sz=${size * 2}`;
}
-export function integrationFaviconFallbackUrl(
- url: string | undefined,
- size: number,
-): string | null {
- const domain = integrationFaviconDomain(url);
- if (!domain) return null;
- return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size * 2}`;
-}
-
export function integrationLocalIconUrl(integrationId: string | undefined): string | null {
if (integrationId !== "executor") return null;
return "/favicon-32.png";
@@ -181,9 +172,9 @@ export function integrationPresetIconUrl(
}
// Resolution cascade for the rendered favicon: first non-null, non-failed of an
-// explicit preset icon, the bundled local icon for a known integration id, the
-// integrations.sh logo proxy derived from the integration URL, then the Google
-// favicon service as a last resort. The built-in executor integration has no preset
+// explicit preset icon, the bundled local icon for a known integration id, then
+// the integrations.sh logo proxy derived from the integration URL (which owns
+// its own upstream fallbacks). The built-in executor integration has no preset
// icon and no URL, so it resolves ONLY through the integrationId branch: callers
// that drop integrationId fall through to the neutral BoxIcon placeholder.
export function integrationFaviconSrc(args: {
@@ -199,7 +190,6 @@ export function integrationFaviconSrc(args: {
args.icon ?? null,
integrationLocalIconUrl(args.integrationId),
integrationFaviconUrl(args.url, args.size),
- integrationFaviconFallbackUrl(args.url, args.size),
].find((candidate) => candidate !== null && !failedSrcs.includes(candidate)) ?? null
);
}
From b569b568305921e3c2a97a87e73d530a51663044 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 10 Jul 2026 12:02:26 -0700
Subject: [PATCH 2/3] Match preset icons on exact tokens only
---
.../components/integration-favicon.test.tsx | 21 +++++++++++++++++++
.../src/components/integration-favicon.tsx | 10 ++++-----
2 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/packages/react/src/components/integration-favicon.test.tsx b/packages/react/src/components/integration-favicon.test.tsx
index ea597be51..59e2234b3 100644
--- a/packages/react/src/components/integration-favicon.test.tsx
+++ b/packages/react/src/components/integration-favicon.test.tsx
@@ -273,6 +273,27 @@ describe("IntegrationFavicon", () => {
).toBe("https://example.com/spotify.png");
});
+ it("does not match a different brand sharing a word fragment (ClickHouse Cloud vs Cloudflare)", () => {
+ expect(
+ integrationPresetIconUrl({ id: "clickhouse", kind: "mcp", name: "ClickHouse Cloud" }, [
+ {
+ key: "mcp",
+ label: "MCP",
+ add: () => null,
+ edit: () => null,
+ presets: [
+ {
+ id: "cloudflare",
+ name: "Cloudflare",
+ summary: "Workers, KV, D1, R2, and DNS management via MCP.",
+ icon: "https://integrations.sh/logo/cloudflare.com",
+ },
+ ],
+ },
+ ]),
+ ).toBeNull();
+ });
+
it("infers favicon URLs from migrated host-shaped MCP names and slugs", () => {
expect(integrationInferredUrl({ id: "mcp_posthog_com", name: "mcp.posthog.com" })).toBe(
"https://mcp.posthog.com",
diff --git a/packages/react/src/components/integration-favicon.tsx b/packages/react/src/components/integration-favicon.tsx
index 1d0feef43..823b0f32c 100644
--- a/packages/react/src/components/integration-favicon.tsx
+++ b/packages/react/src/components/integration-favicon.tsx
@@ -129,12 +129,12 @@ export function integrationInferredUrl(integration: {
return null;
}
+// Exact equality only: substring containment matched unrelated brands that
+// merely share a fragment ("ClickHouse Cloud" ⊂ "Cloudflare" via "cloud"). A
+// missed match is recoverable (the cascade falls through to the domain-derived
+// integrations.sh favicon); a wrong-brand icon is not.
const tokenMatches = (integrationValue: string, presetValue: string): boolean =>
- presetValue.length > 0 &&
- integrationValue.length > 0 &&
- (integrationValue === presetValue ||
- integrationValue.includes(presetValue) ||
- presetValue.includes(integrationValue));
+ presetValue.length > 0 && integrationValue === presetValue;
export function integrationPresetIconUrl(
integration: {
From 556af5193dde95cdfa81bd56fc371a66e1560b58 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Fri, 10 Jul 2026 12:36:07 -0700
Subject: [PATCH 3/3] Drop fuzzy name matching from the preset icon matcher
---
.../components/integration-favicon.test.tsx | 180 +++++-------------
.../src/components/integration-favicon.tsx | 60 +-----
2 files changed, 57 insertions(+), 183 deletions(-)
diff --git a/packages/react/src/components/integration-favicon.test.tsx b/packages/react/src/components/integration-favicon.test.tsx
index 59e2234b3..e9e79cc66 100644
--- a/packages/react/src/components/integration-favicon.test.tsx
+++ b/packages/react/src/components/integration-favicon.test.tsx
@@ -92,63 +92,11 @@ describe("IntegrationFavicon", () => {
).toBe("https://example.com/sheets.svg");
});
- it("finds preset icons from display names with suffixes", () => {
- expect(
- integrationPresetIconUrl(
- {
- id: "google_search_console_api",
- kind: "googleDiscovery",
- name: "Google Search Console API",
- },
- [
- {
- key: "google",
- label: "Google",
- add: () => null,
- edit: () => null,
- presets: [
- {
- id: "google-search-console",
- name: "Google Search Console",
- summary: "Search performance.",
- icon: "https://example.com/google.svg",
- },
- ],
- },
- ],
- ),
- ).toBe("https://example.com/google.svg");
- });
-
- it("finds preset icons from an integration id when the URL is missing", () => {
- expect(
- integrationPresetIconUrl(
- {
- id: "sentry",
- kind: "mcp",
- name: "Sentry MCP",
- },
- [
- {
- key: "mcp",
- label: "MCP",
- add: () => null,
- edit: () => null,
- presets: [
- {
- id: "sentry",
- name: "Sentry",
- summary: "Errors.",
- icon: "https://example.com/sentry.png",
- },
- ],
- },
- ],
- ),
- ).toBe("https://example.com/sentry.png");
- });
-
- it("matches migrated MCP slugs with host/suffix noise", () => {
+ it("does not fuzzy-match preset icons from names or slugs", () => {
+ // Name/slug token matching is gone: without an exact defaultSlug or URL
+ // match, the preset matcher declines and the cascade resolves through the
+ // integrations.sh favicon instead, which is derived from the integration's
+ // own domain and therefore cannot render the wrong brand.
const presets = [
{
key: "mcp",
@@ -157,63 +105,25 @@ describe("IntegrationFavicon", () => {
edit: () => null,
presets: [
{
- id: "posthog",
- name: "PostHog",
- summary: "Analytics.",
- icon: "https://example.com/posthog.png",
- },
- {
- id: "linear",
- name: "Linear",
- summary: "Issues.",
- icon: "https://example.com/linear.png",
- },
- {
- id: "planetscale",
- name: "PlanetScale",
- summary: "Databases.",
- icon: "https://example.com/pscale.png",
+ id: "sentry",
+ name: "Sentry",
+ summary: "Errors.",
+ icon: "https://example.com/sentry.png",
},
],
},
];
expect(
- integrationPresetIconUrl(
- { id: "mcp_posthog_com", kind: "mcp", name: "mcp.posthog.com" },
- presets,
- ),
- ).toBe("https://example.com/posthog.png");
- expect(
- integrationPresetIconUrl(
- { id: "mcp_linear_app", kind: "mcp", name: "mcp.linear.app" },
- presets,
- ),
- ).toBe("https://example.com/linear.png");
- expect(
- integrationPresetIconUrl({ id: "pscale_mcp", kind: "mcp", name: "Pscale MCP" }, presets),
- ).toBe("https://example.com/pscale.png");
- });
+ integrationPresetIconUrl({ id: "sentry", kind: "mcp", name: "Sentry MCP" }, presets),
+ ).toBeNull();
- it("matches migrated OpenAPI slugs with API/REST suffixes", () => {
- expect(
- integrationPresetIconUrl({ id: "stripe_api", kind: "openapi", name: "Stripe API" }, [
- {
- key: "openapi",
- label: "OpenAPI",
- add: () => null,
- edit: () => null,
- presets: [
- {
- id: "stripe",
- name: "Stripe",
- summary: "Payments.",
- icon: "https://example.com/stripe.png",
- },
- ],
- },
- ]),
- ).toBe("https://example.com/stripe.png");
+ // Migrated host-shaped integrations resolve through the inferred URL:
+ const inferred = integrationInferredUrl({ id: "mcp_posthog_com", name: "mcp.posthog.com" });
+ expect(inferred).toBe("https://mcp.posthog.com");
+ expect(integrationFaviconSrc({ url: inferred ?? undefined, size: 16 })).toBe(
+ "https://integrations.sh/logo/posthog.com?sz=32",
+ );
});
it("prefers exact catalog defaultSlug icons for OpenAPI provider services", () => {
@@ -238,7 +148,25 @@ describe("IntegrationFavicon", () => {
).toBe("https://example.com/gmail.png");
});
- it("does not split generic words into brand matches", () => {
+ it("matches presets on exact URL equality, not names", () => {
+ const presets = [
+ {
+ key: "openapi",
+ label: "OpenAPI",
+ add: () => null,
+ edit: () => null,
+ presets: [
+ {
+ id: "spotify",
+ name: "Spotify",
+ summary: "Music.",
+ url: "https://api.spotify.com/v1",
+ icon: "https://example.com/spotify.png",
+ },
+ ],
+ },
+ ];
+
expect(
integrationPresetIconUrl(
{
@@ -247,30 +175,22 @@ describe("IntegrationFavicon", () => {
name: "Spotify Web API",
url: "https://api.spotify.com/v1",
},
- [
- {
- key: "openapi",
- label: "OpenAPI",
- add: () => null,
- edit: () => null,
- presets: [
- {
- id: "exa-websets",
- name: "Exa Websets",
- summary: "Web data.",
- icon: "https://example.com/exa.png",
- },
- {
- id: "spotify",
- name: "Spotify",
- summary: "Music.",
- icon: "https://example.com/spotify.png",
- },
- ],
- },
- ],
+ presets,
),
).toBe("https://example.com/spotify.png");
+
+ // Same name, different URL: no match — the URL-derived favicon takes over.
+ expect(
+ integrationPresetIconUrl(
+ {
+ id: "spotify_web_api",
+ kind: "openapi",
+ name: "Spotify Web API",
+ url: "https://api.spotify.com/v2",
+ },
+ presets,
+ ),
+ ).toBeNull();
});
it("does not match a different brand sharing a word fragment (ClickHouse Cloud vs Cloudflare)", () => {
diff --git a/packages/react/src/components/integration-favicon.tsx b/packages/react/src/components/integration-favicon.tsx
index 823b0f32c..cfe12df43 100644
--- a/packages/react/src/components/integration-favicon.tsx
+++ b/packages/react/src/components/integration-favicon.tsx
@@ -76,46 +76,6 @@ const googleApiServiceFromUrl = (url: string | undefined): string | null => {
return null;
};
-const normalizeToken = (value: string | undefined): string =>
- value?.toLowerCase().replace(/[^a-z0-9]+/g, "") ?? "";
-
-const tokenVariants = (value: string | undefined): readonly string[] => {
- const tokens = new Set();
- const normalized = normalizeToken(value);
- if (normalized.length > 0) tokens.add(normalized);
-
- const parts =
- value
- ?.toLowerCase()
- .split(/[^a-z0-9]+/g)
- .filter(Boolean) ?? [];
- const noise = new Set([
- "api",
- "mcp",
- "openapi",
- "rest",
- "graphql",
- "web",
- "com",
- "net",
- "org",
- "dev",
- ]);
- const cleaned = parts.filter((part) => !noise.has(part));
- const cleanedToken = cleaned.join("");
- if (cleanedToken.length > 0) tokens.add(cleanedToken);
- const aliases: Record = {
- pscale: "planetscale",
- };
- for (const part of cleaned) {
- tokens.add(part);
- const alias = aliases[part];
- if (alias) tokens.add(alias);
- }
-
- return [...tokens];
-};
-
export function integrationInferredUrl(integration: {
readonly id: string;
readonly name?: string;
@@ -129,13 +89,12 @@ export function integrationInferredUrl(integration: {
return null;
}
-// Exact equality only: substring containment matched unrelated brands that
-// merely share a fragment ("ClickHouse Cloud" ⊂ "Cloudflare" via "cloud"). A
-// missed match is recoverable (the cascade falls through to the domain-derived
-// integrations.sh favicon); a wrong-brand icon is not.
-const tokenMatches = (integrationValue: string, presetValue: string): boolean =>
- presetValue.length > 0 && integrationValue === presetValue;
-
+// Exact identity signals only — preset defaultSlug, normalized URL equality,
+// or the same Google discovery service. Fuzzy name/slug token matching used to
+// live here and matched unrelated brands sharing a word fragment ("ClickHouse
+// Cloud" rendered Cloudflare's logo via "cloud"). A missed match is recoverable
+// (the cascade falls through to the domain-derived integrations.sh favicon,
+// which is always the right brand); a wrong-brand icon is not.
export function integrationPresetIconUrl(
integration: {
readonly id: string;
@@ -153,18 +112,13 @@ export function integrationPresetIconUrl(
const integrationUrl = normalizeUrl(integration.url);
const integrationGoogleService = googleApiServiceFromUrl(integration.url);
- const integrationTokens = [...tokenVariants(integration.id), ...tokenVariants(integration.name)];
const preset = presets.find((p) => {
const presetUrl = normalizeUrl(p.url);
const presetGoogleService = googleApiServiceFromUrl(p.url);
- const presetTokens = [...tokenVariants(p.id), ...tokenVariants(p.name)];
return (
(integrationUrl !== null && presetUrl === integrationUrl) ||
- (integrationGoogleService !== null && presetGoogleService === integrationGoogleService) ||
- integrationTokens.some((integrationToken) =>
- presetTokens.some((presetToken) => tokenMatches(integrationToken, presetToken)),
- )
+ (integrationGoogleService !== null && presetGoogleService === integrationGoogleService)
);
});