Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpIcons.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { getMediaMime } from '../../../../base/common/mime.js';
7
import { URI } from '../../../../base/common/uri.js';
8
import { ILogger } from '../../../../platform/log/common/log.js';
9
import { Dto } from '../../../services/extensions/common/proxyIdentifier.js';
10
import { IMcpIcons, McpServerLaunch, McpServerTransportType } from './mcpTypes.js';
11
import { MCP } from './modelContextProtocol.js';
12
13
const mcpAllowableContentTypes: readonly string[] = [
14
'image/webp',
15
'image/png',
16
'image/jpeg',
17
'image/jpg',
18
'image/gif'
19
];
20
21
const enum IconTheme {
22
Light,
23
Dark,
24
Any,
25
}
26
27
interface IIcon {
28
/** URI the image can be loaded from */
29
src: URI;
30
/** Theme for this icon. */
31
theme: IconTheme;
32
/** Sizes of the icon in ascending order. */
33
sizes: { width: number; height: number }[];
34
}
35
36
export type ParsedMcpIcons = IIcon[];
37
export type StoredMcpIcons = Dto<IIcon>[];
38
39
40
function validateIcon(icon: MCP.Icon, launch: McpServerLaunch, logger: ILogger): URI | undefined {
41
const mimeType = icon.mimeType?.toLowerCase() || getMediaMime(icon.src);
42
if (!mimeType || !mcpAllowableContentTypes.includes(mimeType)) {
43
logger.debug(`Ignoring icon with unsupported mime type: ${icon.src} (${mimeType}), allowed: ${mcpAllowableContentTypes.join(', ')}`);
44
return;
45
}
46
47
const uri = URI.parse(icon.src);
48
if (uri.scheme === 'data') {
49
return uri;
50
}
51
52
if (uri.scheme === 'https' || uri.scheme === 'http') {
53
if (launch.type !== McpServerTransportType.HTTP) {
54
logger.debug(`Ignoring icon with HTTP/HTTPS URL: ${icon.src} as the MCP server is not launched with HTTP transport.`);
55
return;
56
}
57
58
const expectedAuthority = launch.uri.authority.toLowerCase();
59
if (uri.authority.toLowerCase() !== expectedAuthority) {
60
logger.debug(`Ignoring icon with untrusted authority: ${icon.src}, expected authority: ${expectedAuthority}`);
61
return;
62
}
63
64
return uri;
65
}
66
67
if (uri.scheme === 'file') {
68
if (launch.type !== McpServerTransportType.Stdio) {
69
logger.debug(`Ignoring icon with file URL: ${icon.src} as the MCP server is not launched as a local process.`);
70
return;
71
}
72
73
return uri;
74
}
75
76
logger.debug(`Ignoring icon with unsupported scheme: ${icon.src}. Allowed: data:, http:, https:, file:`);
77
return;
78
}
79
80
export function parseAndValidateMcpIcon(icons: MCP.Icons, launch: McpServerLaunch, logger: ILogger): ParsedMcpIcons {
81
const result: ParsedMcpIcons = [];
82
for (const icon of icons.icons || []) {
83
const uri = validateIcon(icon, launch, logger);
84
if (!uri) {
85
continue;
86
}
87
88
// check for sizes as string for back-compat with early 2025-11-25 drafts
89
const sizesArr = typeof icon.sizes === 'string' ? (icon.sizes as string).split(' ') : Array.isArray(icon.sizes) ? icon.sizes : [];
90
result.push({
91
src: uri,
92
theme: icon.theme === 'light' ? IconTheme.Light : icon.theme === 'dark' ? IconTheme.Dark : IconTheme.Any,
93
sizes: sizesArr.map(size => {
94
const [widthStr, heightStr] = size.toLowerCase().split('x');
95
return { width: Number(widthStr) || 0, height: Number(heightStr) || 0 };
96
}).sort((a, b) => a.width - b.width)
97
});
98
}
99
100
result.sort((a, b) => a.sizes[0]?.width - b.sizes[0]?.width);
101
102
return result;
103
}
104
105
export class McpIcons implements IMcpIcons {
106
public static fromStored(icons: StoredMcpIcons | undefined) {
107
return McpIcons.fromParsed(icons?.map(i => ({ src: URI.revive(i.src), theme: i.theme, sizes: i.sizes })));
108
}
109
110
public static fromParsed(icons: ParsedMcpIcons | undefined) {
111
return new McpIcons(icons || []);
112
}
113
114
protected constructor(private readonly _icons: IIcon[]) { }
115
116
getUrl(size: number): { dark: URI; light?: URI } | undefined {
117
const dark = this.getSizeWithTheme(size, IconTheme.Dark);
118
if (dark?.theme === IconTheme.Any) {
119
return { dark: dark.src };
120
}
121
122
const light = this.getSizeWithTheme(size, IconTheme.Light);
123
if (!light && !dark) {
124
return undefined;
125
}
126
127
return { dark: (dark || light)!.src, light: light?.src };
128
}
129
130
private getSizeWithTheme(size: number, theme: IconTheme): IIcon | undefined {
131
let bestOfAnySize: IIcon | undefined;
132
133
for (const icon of this._icons) {
134
if (icon.theme === theme || icon.theme === IconTheme.Any || icon.theme === undefined) { // undefined check for back compat
135
bestOfAnySize = icon;
136
137
const matchingSize = icon.sizes.find(s => s.width >= size);
138
if (matchingSize) {
139
return { ...icon, sizes: [matchingSize] };
140
}
141
}
142
}
143
return bestOfAnySize;
144
}
145
}
146
147