Skip to content

Commit 2101218

Browse files
Add support for NuGet as an MCP package source (#256)
* Start on NuGet support for MCP * Improve * Try to fetch readme * force a response in the MCP tool calling loop * mcp: better enforce setup's tool calling loop turn limit --------- Co-authored-by: Joel Verhagen <[email protected]>
1 parent a9a319f commit 2101218

File tree

3 files changed

+70
-3
lines changed

3 files changed

+70
-3
lines changed

src/extension/mcp/vscode-node/commands.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Conversation, Turn } from '../../prompt/common/conversation';
1919
import { McpToolCallingLoop } from './mcpToolCallingLoop';
2020
import { McpPickRef } from './mcpToolCallingTools';
2121

22-
type PackageType = 'npm' | 'pip' | 'docker';
22+
type PackageType = 'npm' | 'pip' | 'docker' | 'nuget';
2323

2424
interface IValidatePackageArgs {
2525
type: PackageType;
@@ -52,6 +52,14 @@ interface PyPiPackageResponse {
5252
};
5353
}
5454

55+
interface NuGetServiceIndexResponse {
56+
resources?: Array<{ "@id": string; "@type": string }>;
57+
}
58+
59+
interface NuGetSearchResponse {
60+
data?: Array<{ id: string; version: string; description?: string; owners?: Array<string> }>;
61+
}
62+
5563
interface DockerHubResponse {
5664
user?: string;
5765
name?: string;
@@ -99,7 +107,7 @@ export class McpSetupCommands extends Disposable {
99107
const done = (async () => {
100108
const fakePrompt = `Generate an MCP configuration for ${packageName}`;
101109
const mcpLoop = this.instantiationService.createInstance(McpToolCallingLoop, {
102-
toolCallLimit: 5,
110+
toolCallLimit: 100, // limited via `getAvailableTools` in the loop
103111
conversation: new Conversation(generateUuid(), [new Turn(undefined, { type: 'user', message: fakePrompt })]),
104112
request: {
105113
attempt: 0,
@@ -194,6 +202,60 @@ export class McpSetupCommands extends Disposable {
194202
const version = data.info?.version;
195203
this.enqueuePendingSetup(args.targetConfig, args.name, args.type, data.info?.description, version);
196204
return { state: 'ok', publisher: data.info?.author || data.info?.author_email || 'unknown', version };
205+
} else if (args.type === 'nuget') {
206+
// read the service index to find the search URL
207+
// https://learn.microsoft.com/en-us/nuget/api/service-index
208+
const serviceIndexUrl = `https://api.nuget.org/v3/index.json`;
209+
const serviceIndexResponse = await fetch(serviceIndexUrl);
210+
if (!serviceIndexResponse.ok) {
211+
return { state: 'error', error: `Unable to load the NuGet.org registry service index (${serviceIndexUrl})` };
212+
}
213+
214+
// find the search query URL
215+
// https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource
216+
const serviceIndex = await serviceIndexResponse.json() as NuGetServiceIndexResponse;
217+
const searchBaseUrl = serviceIndex.resources?.find(resource => resource['@type'] === 'SearchQueryService/3.5.0')?.['@id'];
218+
if (!searchBaseUrl) {
219+
return { state: 'error', error: `Package search URL not found in the NuGet.org registry service index` };
220+
}
221+
222+
// search for the package by ID
223+
// https://learn.microsoft.com/en-us/nuget/consume-packages/finding-and-choosing-packages#search-syntax
224+
const searchQueryUrl = `${searchBaseUrl}?q=packageid:${encodeURIComponent(args.name)}&prerelease=true&semVerLevel=2.0.0`;
225+
const searchResponse = await fetch(searchQueryUrl);
226+
if (!searchResponse.ok) {
227+
return { state: 'error', error: `Failed to search for ${args.name} in then NuGet.org registry` };
228+
}
229+
const data = await searchResponse.json() as NuGetSearchResponse;
230+
if (!data.data?.[0]) {
231+
return { state: 'error', error: `Package ${args.name} not found on NuGet.org` };
232+
}
233+
234+
const id = data.data[0].id ?? args.name;
235+
let version = data.data[0].version;
236+
if (version.indexOf('+') !== -1) {
237+
// NuGet versions can have a + sign for build metadata, we strip it for MCP config and API calls
238+
// e.g. 1.0.0+build123 -> 1.0.0
239+
version = version.split('+')[0];
240+
}
241+
const publisher = data.data[0].owners ? data.data[0].owners.join(', ') : 'unknown';
242+
243+
// Try to fetch the package readme
244+
// https://learn.microsoft.com/en-us/nuget/api/readme-template-resource
245+
const readmeTemplate = serviceIndex.resources?.find(resource => resource['@type'] === 'ReadmeUriTemplate/6.13.0')?.['@id'];
246+
let description = data.data[0].description || undefined;
247+
if (readmeTemplate) {
248+
const readmeUrl = readmeTemplate
249+
.replace('{lower_id}', encodeURIComponent(id.toLowerCase()))
250+
.replace('{lower_version}', encodeURIComponent(version.toLowerCase()));
251+
const readmeResponse = await fetch(readmeUrl);
252+
if (readmeResponse.ok) {
253+
description = await readmeResponse.text();
254+
}
255+
}
256+
257+
this.enqueuePendingSetup(args.targetConfig, id, args.type, description, version);
258+
return { state: 'ok', publisher, version };
197259
} else if (args.type === 'docker') {
198260
// Docker Hub API uses namespace/repository format
199261
// Handle both formats: 'namespace/repository' or just 'repository' (assumes 'library/' namespace)

src/extension/mcp/vscode-node/mcpToolCallingLoop.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export class McpToolCallingLoop extends ToolCallingLoop<IMcpToolCallingLoopOptio
6464
}
6565

6666
protected async getAvailableTools(): Promise<LanguageModelToolInformation[]> {
67+
if (this.options.conversation.turns.length > 5) {
68+
return []; // force a response
69+
}
70+
6771
return [{
6872
description: QuickInputTool.description,
6973
name: QuickInputTool.ID,

src/extension/mcp/vscode-node/mcpToolCallingLoopPrompt.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { McpPickRef, QuickInputTool, QuickPickTool } from './mcpToolCallingTools
1515

1616
export interface IMcpToolCallingLoopPromptContext {
1717
packageName: string;
18-
packageType: 'npm' | 'pip' | 'docker';
18+
packageType: 'npm' | 'pip' | 'docker' | 'nuget';
1919
packageReadme: string | undefined;
2020
packageVersion: string | undefined;
2121
targetSchema: JsonSchema;
@@ -28,6 +28,7 @@ const packageTypePreferredCommands = {
2828
pip: (name: string, version: string | undefined) => `uvx ${name.replaceAll('-', '_')}` + (version ? `==${version}` : ''),
2929
npm: (name: string, version: string | undefined) => `npx ${name}` + (version ? `@${version}` : ''),
3030
docker: (name: string, _version: string | undefined) => `docker run -i --rm ${name}`,
31+
nuget: (name: string, version: string | undefined) => `dnx ${name}` + (version ? `@${version}` : '') + ` --yes`,
3132
};
3233

3334
export class McpToolCallingLoopPrompt extends PromptElement<IMcpToolCallingLoopProps> {

0 commit comments

Comments
 (0)