Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions src/vs/platform/agentFinder/common/agentFinderRestProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class AgentFinderRestProvider implements ICustomizationMarketplaceProvide
}

private async resolveMcpIcons(page: ICustomizationMarketplaceSourcePage, token: CancellationToken): Promise<ICustomizationMarketplaceSourcePage> {
if (!page.items.some(item => !item.icon && item.installation?.kind === 'mcp' && item.externalUrl)) {
if (!page.items.some(item => (!item.icon || !item.publisher) && item.installation?.kind === 'mcp' && item.externalUrl)) {
return page;
}

Expand All @@ -137,15 +137,15 @@ export class AgentFinderRestProvider implements ICustomizationMarketplaceProvide

try {
const items = await Promise.all(page.items.map(item =>
!item.icon && item.installation?.kind === 'mcp' && item.externalUrl
? limiter.queue(() => this.resolveMcpIcon(item, token, cancellation.token))
(!item.icon || !item.publisher) && item.installation?.kind === 'mcp' && item.externalUrl
? limiter.queue(() => this.resolveMcpMetadata(item, token, cancellation.token))
: item
));
if (token.isCancellationRequested) {
throw new CancellationError();
}
if (timedOut) {
this.logService.warn('[AgentFinderRestProvider] Timed out resolving MCP catalog icons.');
this.logService.warn('[AgentFinderRestProvider] Timed out resolving MCP catalog metadata.');
}
return { ...page, items };
} finally {
Expand All @@ -154,7 +154,7 @@ export class AgentFinderRestProvider implements ICustomizationMarketplaceProvide
}
}

private async resolveMcpIcon(item: ICustomizationMarketplaceEntry, queryToken: CancellationToken, iconToken: CancellationToken): Promise<ICustomizationMarketplaceEntry> {
private async resolveMcpMetadata(item: ICustomizationMarketplaceEntry, queryToken: CancellationToken, iconToken: CancellationToken): Promise<ICustomizationMarketplaceEntry> {
const installation = item.installation;
if (installation?.kind !== 'mcp' || !item.externalUrl || queryToken.isCancellationRequested || iconToken.isCancellationRequested) {
return item;
Expand All @@ -166,10 +166,16 @@ export class AgentFinderRestProvider implements ICustomizationMarketplaceProvide
if (server?.icon?.light && !icon) {
this.logService.warn(`[AgentFinderRestProvider] Ignoring an invalid MCP catalog icon for '${installation.name}'.`);
}
return icon ? { ...item, icon } : item;
const publisher = server?.publisherDisplayName ?? server?.publisher;
const repository = githubRepository(parseHttpUri(server?.repositoryUrl));
const repositoryOwner = repository?.path.split('/')[1];
const publisherUrl = parseHttpUri(server?.publisherUrl) ?? (repositoryOwner ? URI.from({ scheme: Schemas.https, authority: 'github.com', path: `/${repositoryOwner}` }) : undefined);
return icon || publisher || publisherUrl || repository
? { ...item, ...(icon ? { icon } : {}), ...(publisher ? { publisher } : {}), ...(publisherUrl ? { publisherUrl } : {}), ...(repository ? { repository } : {}) }
: item;
} catch (error) {
if (!queryToken.isCancellationRequested && !iconToken.isCancellationRequested) {
this.logService.warn(`[AgentFinderRestProvider] Failed to resolve the MCP catalog icon for '${installation.name}'.`, error);
this.logService.warn(`[AgentFinderRestProvider] Failed to resolve MCP catalog metadata for '${installation.name}'.`, error);
}
return item;
}
Expand Down Expand Up @@ -301,6 +307,7 @@ function parseResource(value: unknown): ICustomizationMarketplaceEntry {
const sourceSet = text(metadata?.sourceSet);
const repository = (sourceSet && !sourceSet.includes('://') ? githubRepository(parseHttpUri(`https://github.com/${sourceSet}`), true) : undefined) ?? githubRepository(url);
const publisher = repository?.path.split('/')[1];
const publisherUrl = publisher ? URI.from({ scheme: Schemas.https, authority: 'github.com', path: `/${publisher}` }) : undefined;
const installation = parseInstallation(mediaType, metadata, url, externalUrl);
const readmeUri = getReadmeUri(installation);
return {
Expand All @@ -317,6 +324,7 @@ function parseResource(value: unknown): ICustomizationMarketplaceEntry {
...(readmeUri ? { readmeUri } : {}),
icon: publisher ? URI.from({ scheme: Schemas.https, authority: 'github.com', path: `/${publisher}.png`, query: 'size=64' }) : undefined,
publisher,
...(publisherUrl ? { publisherUrl } : {}),
version: text(value.version) ?? text(metadata?.version),
score: value.score,
installation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ function resourceSnapshot(resource: ICustomizationMarketplaceEntry) {
url: resource.url?.toString(),
repository: resource.repository?.toString(),
icon: resource.icon?.toString(),
...(resource.publisherUrl ? { publisherUrl: resource.publisherUrl.toString() } : {}),
};
}

Expand Down Expand Up @@ -141,6 +142,7 @@ suite('AgentFinderRestProvider', () => {
repository: 'https://github.com/ChromeDevTools/chrome-devtools-mcp',
icon: 'https://github.com/ChromeDevTools.png?size%3D64',
publisher: 'ChromeDevTools',
publisherUrl: 'https://github.com/ChromeDevTools',
version: undefined,
score: undefined,
installation: { kind: 'skill', repository: 'ChromeDevTools/chrome-devtools-mcp', ref: 'main', path: 'skills/a11y-debugging' },
Expand All @@ -159,7 +161,7 @@ suite('AgentFinderRestProvider', () => {
});
});

test('resolves an MCP registry icon through the fixed Agent Finder registry', async () => {
test('resolves MCP registry metadata through the fixed Agent Finder registry', async () => {
const lookups: { url: string; manifestUrl: string | undefined; manifestVersion: string | undefined; cancelled: boolean }[] = [];
const mcpGalleryService = upcastPartial<IMcpGalleryService>({
getMcpServer: async (url, manifest, token) => {
Expand All @@ -174,6 +176,9 @@ suite('AgentFinderRestProvider', () => {
light: 'https://avatars.githubusercontent.com/u/213697801',
dark: 'https://avatars.githubusercontent.com/u/213697801',
},
publisher: 'pgEdge',
publisherDisplayName: 'pgEdge, Inc.',
repositoryUrl: 'https://github.com/pgEdge/postgres-mcp',
});
},
});
Expand All @@ -187,8 +192,9 @@ suite('AgentFinderRestProvider', () => {
version: page.items[0].version,
url: page.items[0].url?.toString(),
externalUrl: page.items[0].externalUrl,
repository: page.items[0].repository,
repository: page.items[0].repository?.toString(),
publisher: page.items[0].publisher,
publisherUrl: page.items[0].publisherUrl?.toString(),
icon: page.items[0].icon?.toString(),
stars: page.items[0].stars,
},
Expand All @@ -200,8 +206,9 @@ suite('AgentFinderRestProvider', () => {
version: '1.0.0',
url: 'https://api.mcp.github.com/oss/v0.1/servers/io.github.pgEdge/postgres-mcp/versions/latest',
externalUrl: mcpServer.url,
repository: undefined,
publisher: undefined,
repository: 'https://github.com/pgEdge/postgres-mcp',
publisher: 'pgEdge, Inc.',
publisherUrl: 'https://github.com/pgEdge',
icon: 'https://avatars.githubusercontent.com/u/213697801',
stars: undefined,
},
Expand All @@ -214,7 +221,7 @@ suite('AgentFinderRestProvider', () => {
});
});

test('keeps the MCP result and logs when registry icon resolution fails', async () => {
test('keeps the MCP result and logs when registry metadata resolution fails', async () => {
const mcpGalleryService = upcastPartial<IMcpGalleryService>({
getMcpServer: async () => { throw new Error('offline'); },
});
Expand All @@ -228,7 +235,7 @@ suite('AgentFinderRestProvider', () => {
warnings: warning.args.map(args => args[0]),
}, {
icon: undefined,
warnings: ['[AgentFinderRestProvider] Failed to resolve the MCP catalog icon for \'io.github.pgEdge/postgres-mcp\'.'],
warnings: ['[AgentFinderRestProvider] Failed to resolve MCP catalog metadata for \'io.github.pgEdge/postgres-mcp\'.'],
});
});

Expand All @@ -252,7 +259,7 @@ suite('AgentFinderRestProvider', () => {
}, {
icon: undefined,
iconCancelled: true,
warnings: ['[AgentFinderRestProvider] Timed out resolving MCP catalog icons.'],
warnings: ['[AgentFinderRestProvider] Timed out resolving MCP catalog metadata.'],
});
}));

Expand Down Expand Up @@ -751,8 +758,8 @@ suite('AgentFinderRestProvider', () => {
const page = await service.query({}, CancellationToken.None);
const item = page.items[0];

assert.deepStrictEqual({ repository: item.repository?.toString(), icon: item.icon?.toString(), publisher: item.publisher, version: item.version }, {
repository: 'https://github.com/Owner/Repository', icon: 'https://github.com/Owner.png?size%3D64', publisher: 'Owner', version: '2.0',
assert.deepStrictEqual({ repository: item.repository?.toString(), icon: item.icon?.toString(), publisher: item.publisher, publisherUrl: item.publisherUrl?.toString(), version: item.version }, {
repository: 'https://github.com/Owner/Repository', icon: 'https://github.com/Owner.png?size%3D64', publisher: 'Owner', publisherUrl: 'https://github.com/Owner', version: '2.0',
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export interface ICustomizationMarketplaceEntry {
readonly readmeUri?: URI;
readonly icon?: CustomizationMarketplaceIcon;
readonly publisher?: string;
readonly publisherUrl?: URI;
/** Source-supplied origin within a feed, distinct from the feed's display name. */
readonly originLabel?: string;
readonly version?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function toMarketplaceEntry(server: IGalleryMcpServer, registry: 'custom' | 'def
const url = webUrl ?? repository;
const lightIcon = safeWebUri(server.icon?.light);
const darkIcon = safeWebUri(server.icon?.dark);
const publisherUrl = safeWebUri(server.publisherUrl);
const icon: CustomizationMarketplaceIcon | undefined = lightIcon && darkIcon
? { light: lightIcon, dark: darkIcon }
: lightIcon ?? darkIcon;
Expand All @@ -86,6 +87,7 @@ function toMarketplaceEntry(server: IGalleryMcpServer, registry: 'custom' | 'def
...(repository ? { repository } : {}),
...(icon ? { icon } : {}),
publisher: server.publisherDisplayName ?? server.publisher,
...(publisherUrl ? { publisherUrl } : {}),
version: server.version,
stars: server.starsCount,
priority: registry === 'custom' ? 1 : 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ suite('McpGalleryMarketplaceProvider', () => {
isLatest: true,
status: GalleryMcpServerStatus.Active,
publisher: 'Owner',
publisherUrl: 'https://registry.test/publishers/owner',
topics: ['database'],
starsCount: 42,
webUrl: 'https://registry.test/servers/io.github.owner%2Fserver',
Expand Down Expand Up @@ -81,13 +82,13 @@ suite('McpGalleryMarketplaceProvider', () => {
url: first.items[0].url, externalUrl: server.webUrl,
repository: first.items[0].repository,
icon: first.items[0].icon,
publisher: 'Owner', version: '1.0.0', stars: 42, priority: 1,
publisher: 'Owner', publisherUrl: first.items[0].publisherUrl, version: '1.0.0', stars: 42, priority: 1,
installation: { kind: 'mcpGallery', name: server.name, registry: 'custom', registryUrl: customUrl },
}],
total: 2, nextCursor: 'opaque+/=',
},
lastCursor: undefined,
entryFields: ['identifier', 'displayName', 'description', 'mediaType', 'tags', 'capabilities', 'representativeQueries', 'url', 'externalUrl', 'repository', 'icon', 'publisher', 'version', 'stars', 'priority', 'installation'],
entryFields: ['identifier', 'displayName', 'description', 'mediaType', 'tags', 'capabilities', 'representativeQueries', 'url', 'externalUrl', 'repository', 'icon', 'publisher', 'publisherUrl', 'version', 'stars', 'priority', 'installation'],
iconUris: {
light: 'https://registry.test/icon-light.png',
dark: 'https://registry.test/icon-dark.png',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,7 @@ function scoreConnectorField(word: string, value: string): number | undefined {
}

export function toCopilotConnectorMarketplaceEntry(connector: ICopilotConnector): ICustomizationMarketplaceEntry {
const publisherUrl = connector.homepage ?? connector.author?.url;
return {
identifier: connector.name,
displayName: connector.displayName,
Expand All @@ -906,6 +907,7 @@ export function toCopilotConnectorMarketplaceEntry(connector: ICopilotConnector)
externalUrl: connector.documentation?.toString(true),
icon: connector.icon,
publisher: localize('copilotConnectors.publisher', "GitHub Copilot"),
...(publisherUrl ? { publisherUrl } : {}),
installation: { kind: 'copilotConnector', name: connector.name },
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class CustomizationDiscoveryAccessibleView implements IAccessibleViewImplementat
localize('customizationDiscovery.help.browse', "Clear the search to browse. Show All on a section applies its type filter and moves to search results."),
localize('customizationDiscovery.help.sources', "Use the source picker to search all available marketplace feeds or one feed. Configure Marketplaces opens the related settings."),
localize('customizationDiscovery.help.navigation', "Use Tab and Shift+Tab between controls. In search results, use the arrow keys, Home, and End to navigate the list. Press Enter or Space on an available item to open its in-product details. Press Enter to open an installed item."),
localize('customizationDiscovery.help.install', "Review an available item's source, then choose Install. VS Code records the exact installed target instead of associating same-name local items. If recorded skill files are missing, choose Repair to restore only those files without overwriting existing files. Installed marketplace items provide an Uninstall action. MCP servers that require unsupported local prerequisites provide View Setup to open the publisher's instructions instead. VS Code continues to apply destination, trust, policy, and compatibility checks."),
localize('customizationDiscovery.help.install', "Review an available item's source, then choose Install. For connectors, choose Connect, Reconnect, or Disconnect to manage the connection. VS Code records the exact installed target instead of associating same-name local items. If recorded skill files are missing, choose Repair to restore only those files without overwriting existing files. Installed marketplace items provide an Uninstall action. MCP servers that require unsupported local prerequisites provide View Setup to open the publisher's instructions instead. VS Code continues to apply destination, trust, policy, and compatibility checks."),
localize('customizationDiscovery.help.links', "Available customization names no longer open external resources. Open an item's details to review its metadata, then use the explicitly labeled resource or repository actions for external navigation. Back or Escape returns to Discover and restores focus."),
localize('customizationDiscovery.help.paging', "Scrolling near the end of search results loads another page without removing loaded items. Retry repeats a failed marketplace request."),
localize('customizationDiscovery.help.sourceFailures', "Unavailable sources show a warning and Retry action above the results. Scrolling continues healthy sources. Retrying a source reloads all sources from the first page to restore relevance order."),
Expand Down
Loading
Loading