67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
// MCP icons live on a remote origin, which the browser cannot load under the
|
|
// UI's cross-origin isolation. When the user enabled the server's CORS proxy the
|
|
// icons are fetched through it; with the proxy off the origin URL stands in and
|
|
// the components fall back when it fails. Inline data: icons load under any
|
|
// policy and are never proxied.
|
|
|
|
import { mcpStore } from '$lib/stores/mcp/index.svelte';
|
|
import { serverStore } from '$lib/stores/server.svelte';
|
|
import { settingsStore } from '$lib/stores/settings/index.svelte';
|
|
import { buildProxiedIconUrl } from '$lib/utils/cors-proxy';
|
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
// The favicon fallback resolves to the root domain of the server url, so the
|
|
// icons of https://mcp.example.com/mcp are fetched from https://example.com.
|
|
const FAVICON = 'https://example.com/favicon.ico';
|
|
const DATA_ICON = 'data:image/png;base64,iVBORw0KGgo=';
|
|
|
|
function mockProxyEnabled(enabled: boolean) {
|
|
Object.defineProperty(serverStore, 'props', {
|
|
configurable: true,
|
|
get: () => ({ cors_proxy_enabled: enabled }) as unknown as typeof serverStore.props
|
|
});
|
|
}
|
|
|
|
/** Target a proxied icon URL carries, or null when the URL is not proxied. */
|
|
function proxiedTarget(iconUrl: string | null): string | null {
|
|
if (!iconUrl) return null;
|
|
|
|
const url = new URL(iconUrl, window.location.origin);
|
|
|
|
return url.pathname.endsWith('/cors-proxy') ? url.searchParams.get('url') : null;
|
|
}
|
|
|
|
describe('mcp server icon urls', () => {
|
|
beforeEach(() => {
|
|
settingsStore.updateConfig('mcpServers', '[]');
|
|
});
|
|
|
|
it('routes the favicon fallback through the proxy when it is enabled', () => {
|
|
mockProxyEnabled(true);
|
|
|
|
const server = mcpStore.addServer({ enabled: false, url: 'https://mcp.example.com/mcp' });
|
|
|
|
expect(proxiedTarget(mcpStore.getServerFavicon(server.id))).toBe(FAVICON);
|
|
});
|
|
|
|
it('keeps the origin url when the proxy is off', () => {
|
|
mockProxyEnabled(false);
|
|
|
|
const server = mcpStore.addServer({ enabled: false, url: 'https://mcp.example.com/mcp' });
|
|
|
|
expect(mcpStore.getServerFavicon(server.id)).toBe(FAVICON);
|
|
});
|
|
|
|
it('proxies an http icon url too', () => {
|
|
expect(proxiedTarget(buildProxiedIconUrl('http://example.com/favicon.ico', true))).toBe(
|
|
'http://example.com/favicon.ico'
|
|
);
|
|
});
|
|
|
|
it('leaves a data url alone whichever way the proxy is set', () => {
|
|
expect(buildProxiedIconUrl(DATA_ICON, true)).toBe(DATA_ICON);
|
|
expect(buildProxiedIconUrl(DATA_ICON, false)).toBe(DATA_ICON);
|
|
expect(buildProxiedIconUrl(null, true)).toBeNull();
|
|
});
|
|
});
|