Skip to content

Commit 8bc037d

Browse files
authored
Resolve all favicons through integrations.sh/logo (#1400)
* Resolve all favicons through integrations.sh/logo * Match preset icons on exact tokens only * Drop fuzzy name matching from the preset icon matcher
1 parent 04ce4b4 commit 8bc037d

3 files changed

Lines changed: 90 additions & 215 deletions

File tree

packages/plugins/mcp/src/sdk/presets.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const mcpPresets: readonly McpPreset[] = [
3030
summary: "Deterministic MCP fixtures for validating native text and image content.",
3131
url: "https://emulators.dev/mcp/query/mcp?token=demo-token",
3232
endpoint: "https://emulators.dev/mcp/query/mcp?token=demo-token",
33-
icon: "https://emulators.dev/favicon.ico",
33+
icon: "https://integrations.sh/logo/emulators.dev",
3434
},
3535
{
3636
id: "deepwiki",
@@ -142,7 +142,7 @@ export const mcpPresets: readonly McpPreset[] = [
142142
id: "chrome-devtools",
143143
name: "Chrome DevTools",
144144
summary: "Debug a live Chrome browser session via local stdio.",
145-
icon: "https://www.google.com/chrome/static/images/favicons/favicon-32x32.png",
145+
icon: "https://integrations.sh/logo/chrome.com",
146146
featured: true,
147147
transport: "stdio",
148148
command: "npx",

packages/react/src/components/integration-favicon.test.tsx

Lines changed: 75 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { describe, expect, it } from "@effect/vitest";
22

33
import {
4-
integrationFaviconFallbackUrl,
54
integrationFaviconSrc,
65
integrationFaviconUrl,
76
integrationInferredUrl,
@@ -10,22 +9,15 @@ import {
109
} from "./integration-favicon";
1110

1211
describe("IntegrationFavicon", () => {
13-
it("uses the integrations.sh logo proxy as the primary favicon source", () => {
12+
it("resolves favicons only through the integrations.sh logo proxy", () => {
1413
expect(integrationFaviconUrl("https://api.github.com/graphql", 20)).toBe(
1514
"https://integrations.sh/logo/github.com?sz=40",
1615
);
1716
});
1817

19-
it("falls back to the Google favicon service", () => {
20-
expect(integrationFaviconFallbackUrl("https://api.github.com/graphql", 20)).toBe(
21-
"https://www.google.com/s2/favicons?domain=github.com&sz=40",
22-
);
23-
});
24-
2518
it("does not request favicons for local URLs", () => {
2619
expect(integrationFaviconUrl("http://localhost:3000/private", 20)).toBeNull();
2720
expect(integrationFaviconUrl("http://127.0.0.1:3000/private", 20)).toBeNull();
28-
expect(integrationFaviconFallbackUrl("http://localhost:3000/private", 20)).toBeNull();
2921
});
3022

3123
it("sends only the registrable domain to the logo proxy", () => {
@@ -34,13 +26,11 @@ describe("IntegrationFavicon", () => {
3426
);
3527
});
3628

37-
it("walks the cascade from the proxy to the favicon service on failure", () => {
29+
it("falls through to the placeholder when the proxy fails", () => {
3830
const url = "https://api.github.com/graphql";
3931
const primary = integrationFaviconSrc({ url, size: 20 });
4032
expect(primary).toBe("https://integrations.sh/logo/github.com?sz=40");
41-
expect(integrationFaviconSrc({ url, size: 20, failedSrcs: [primary ?? ""] })).toBe(
42-
"https://www.google.com/s2/favicons?domain=github.com&sz=40",
43-
);
33+
expect(integrationFaviconSrc({ url, size: 20, failedSrcs: [primary ?? ""] })).toBeNull();
4434
});
4535

4636
it("uses the Executor favicon for the built-in executor integration", () => {
@@ -102,63 +92,11 @@ describe("IntegrationFavicon", () => {
10292
).toBe("https://example.com/sheets.svg");
10393
});
10494

105-
it("finds preset icons from display names with suffixes", () => {
106-
expect(
107-
integrationPresetIconUrl(
108-
{
109-
id: "google_search_console_api",
110-
kind: "googleDiscovery",
111-
name: "Google Search Console API",
112-
},
113-
[
114-
{
115-
key: "google",
116-
label: "Google",
117-
add: () => null,
118-
edit: () => null,
119-
presets: [
120-
{
121-
id: "google-search-console",
122-
name: "Google Search Console",
123-
summary: "Search performance.",
124-
icon: "https://example.com/google.svg",
125-
},
126-
],
127-
},
128-
],
129-
),
130-
).toBe("https://example.com/google.svg");
131-
});
132-
133-
it("finds preset icons from an integration id when the URL is missing", () => {
134-
expect(
135-
integrationPresetIconUrl(
136-
{
137-
id: "sentry",
138-
kind: "mcp",
139-
name: "Sentry MCP",
140-
},
141-
[
142-
{
143-
key: "mcp",
144-
label: "MCP",
145-
add: () => null,
146-
edit: () => null,
147-
presets: [
148-
{
149-
id: "sentry",
150-
name: "Sentry",
151-
summary: "Errors.",
152-
icon: "https://example.com/sentry.png",
153-
},
154-
],
155-
},
156-
],
157-
),
158-
).toBe("https://example.com/sentry.png");
159-
});
160-
161-
it("matches migrated MCP slugs with host/suffix noise", () => {
95+
it("does not fuzzy-match preset icons from names or slugs", () => {
96+
// Name/slug token matching is gone: without an exact defaultSlug or URL
97+
// match, the preset matcher declines and the cascade resolves through the
98+
// integrations.sh favicon instead, which is derived from the integration's
99+
// own domain and therefore cannot render the wrong brand.
162100
const presets = [
163101
{
164102
key: "mcp",
@@ -167,63 +105,25 @@ describe("IntegrationFavicon", () => {
167105
edit: () => null,
168106
presets: [
169107
{
170-
id: "posthog",
171-
name: "PostHog",
172-
summary: "Analytics.",
173-
icon: "https://example.com/posthog.png",
174-
},
175-
{
176-
id: "linear",
177-
name: "Linear",
178-
summary: "Issues.",
179-
icon: "https://example.com/linear.png",
180-
},
181-
{
182-
id: "planetscale",
183-
name: "PlanetScale",
184-
summary: "Databases.",
185-
icon: "https://example.com/pscale.png",
108+
id: "sentry",
109+
name: "Sentry",
110+
summary: "Errors.",
111+
icon: "https://example.com/sentry.png",
186112
},
187113
],
188114
},
189115
];
190116

191117
expect(
192-
integrationPresetIconUrl(
193-
{ id: "mcp_posthog_com", kind: "mcp", name: "mcp.posthog.com" },
194-
presets,
195-
),
196-
).toBe("https://example.com/posthog.png");
197-
expect(
198-
integrationPresetIconUrl(
199-
{ id: "mcp_linear_app", kind: "mcp", name: "mcp.linear.app" },
200-
presets,
201-
),
202-
).toBe("https://example.com/linear.png");
203-
expect(
204-
integrationPresetIconUrl({ id: "pscale_mcp", kind: "mcp", name: "Pscale MCP" }, presets),
205-
).toBe("https://example.com/pscale.png");
206-
});
207-
208-
it("matches migrated OpenAPI slugs with API/REST suffixes", () => {
209-
expect(
210-
integrationPresetIconUrl({ id: "stripe_api", kind: "openapi", name: "Stripe API" }, [
211-
{
212-
key: "openapi",
213-
label: "OpenAPI",
214-
add: () => null,
215-
edit: () => null,
216-
presets: [
217-
{
218-
id: "stripe",
219-
name: "Stripe",
220-
summary: "Payments.",
221-
icon: "https://example.com/stripe.png",
222-
},
223-
],
224-
},
225-
]),
226-
).toBe("https://example.com/stripe.png");
118+
integrationPresetIconUrl({ id: "sentry", kind: "mcp", name: "Sentry MCP" }, presets),
119+
).toBeNull();
120+
121+
// Migrated host-shaped integrations resolve through the inferred URL:
122+
const inferred = integrationInferredUrl({ id: "mcp_posthog_com", name: "mcp.posthog.com" });
123+
expect(inferred).toBe("https://mcp.posthog.com");
124+
expect(integrationFaviconSrc({ url: inferred ?? undefined, size: 16 })).toBe(
125+
"https://integrations.sh/logo/posthog.com?sz=32",
126+
);
227127
});
228128

229129
it("prefers exact catalog defaultSlug icons for OpenAPI provider services", () => {
@@ -248,7 +148,25 @@ describe("IntegrationFavicon", () => {
248148
).toBe("https://example.com/gmail.png");
249149
});
250150

251-
it("does not split generic words into brand matches", () => {
151+
it("matches presets on exact URL equality, not names", () => {
152+
const presets = [
153+
{
154+
key: "openapi",
155+
label: "OpenAPI",
156+
add: () => null,
157+
edit: () => null,
158+
presets: [
159+
{
160+
id: "spotify",
161+
name: "Spotify",
162+
summary: "Music.",
163+
url: "https://api.spotify.com/v1",
164+
icon: "https://example.com/spotify.png",
165+
},
166+
],
167+
},
168+
];
169+
252170
expect(
253171
integrationPresetIconUrl(
254172
{
@@ -257,30 +175,43 @@ describe("IntegrationFavicon", () => {
257175
name: "Spotify Web API",
258176
url: "https://api.spotify.com/v1",
259177
},
260-
[
261-
{
262-
key: "openapi",
263-
label: "OpenAPI",
264-
add: () => null,
265-
edit: () => null,
266-
presets: [
267-
{
268-
id: "exa-websets",
269-
name: "Exa Websets",
270-
summary: "Web data.",
271-
icon: "https://example.com/exa.png",
272-
},
273-
{
274-
id: "spotify",
275-
name: "Spotify",
276-
summary: "Music.",
277-
icon: "https://example.com/spotify.png",
278-
},
279-
],
280-
},
281-
],
178+
presets,
282179
),
283180
).toBe("https://example.com/spotify.png");
181+
182+
// Same name, different URL: no match — the URL-derived favicon takes over.
183+
expect(
184+
integrationPresetIconUrl(
185+
{
186+
id: "spotify_web_api",
187+
kind: "openapi",
188+
name: "Spotify Web API",
189+
url: "https://api.spotify.com/v2",
190+
},
191+
presets,
192+
),
193+
).toBeNull();
194+
});
195+
196+
it("does not match a different brand sharing a word fragment (ClickHouse Cloud vs Cloudflare)", () => {
197+
expect(
198+
integrationPresetIconUrl({ id: "clickhouse", kind: "mcp", name: "ClickHouse Cloud" }, [
199+
{
200+
key: "mcp",
201+
label: "MCP",
202+
add: () => null,
203+
edit: () => null,
204+
presets: [
205+
{
206+
id: "cloudflare",
207+
name: "Cloudflare",
208+
summary: "Workers, KV, D1, R2, and DNS management via MCP.",
209+
icon: "https://integrations.sh/logo/cloudflare.com",
210+
},
211+
],
212+
},
213+
]),
214+
).toBeNull();
284215
});
285216

286217
it("infers favicon URLs from migrated host-shaped MCP names and slugs", () => {

0 commit comments

Comments
 (0)