Skip to content

Commit 499433b

Browse files
joelverhagenCopilotconnor4312
authored
Allow latest server.json schema in assisted NuGet MCP install flow (#1268)
* Align to latest schema * Handle more schema differences * Add tests * Update src/extension/mcp/vscode-node/nuget.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Prepare the manifest for VS Code integration --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Connor Peet <connor@peet.io>
1 parent 832d2c7 commit 499433b

File tree

2 files changed

+145
-20
lines changed

2 files changed

+145
-20
lines changed

src/extension/mcp/test/vscode-node/nuget.stub.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,73 @@ describe('get nuget MCP server info using fake CLI', { timeout: 30_000 }, () =>
3232
nuget = new NuGetMcpSetup(logService, fetcherService, commandExecutor);
3333
});
3434

35+
it('converts legacy schema version', async () => {
36+
const manifest = {
37+
"$schema": "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json",
38+
packages: [{ registry_name: 'nuget', name: 'MismatchId', version: '0.1.0' }]
39+
};
40+
const expected = {
41+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
42+
packages: [{ registry_name: 'nuget', name: 'CorrectId', version: '0.2.0' }],
43+
"_meta": { "io.modelcontextprotocol.registry/official": {} }
44+
};
45+
46+
const actual = nuget.prepareServerJson(manifest, "CorrectId", "0.2.0");
47+
48+
expect(actual).toEqual(expected);
49+
});
50+
51+
it('handles original 2025-07-09 schema version', async () => {
52+
const manifest = {
53+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
54+
packages: [{ registry_name: 'nuget', name: 'MismatchId', version: '0.1.0' }]
55+
};
56+
const expected = {
57+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
58+
packages: [{ registry_name: 'nuget', name: 'CorrectId', version: '0.2.0' }],
59+
"_meta": { "io.modelcontextprotocol.registry/official": {} }
60+
};
61+
62+
const actual = nuget.prepareServerJson(manifest, "CorrectId", "0.2.0");
63+
64+
expect(actual).toEqual(expected);
65+
});
66+
67+
it('handles latest 2025-07-09 schema version', async () => {
68+
const manifest = {
69+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
70+
packages: [{ registry_type: 'nuget', name: 'MismatchId', version: '0.1.0' }]
71+
};
72+
const expected = {
73+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
74+
packages: [{ registry_type: 'nuget', name: 'CorrectId', version: '0.2.0' }],
75+
"_meta": { "io.modelcontextprotocol.registry/official": {} }
76+
};
77+
78+
const actual = nuget.prepareServerJson(manifest, "CorrectId", "0.2.0");
79+
80+
expect(actual).toEqual(expected);
81+
});
82+
83+
it('handles latest 2025-09-29 schema version', async () => {
84+
const manifest = {
85+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
86+
packages: [{ registryType: 'nuget', name: 'MismatchId', version: '0.1.0' }]
87+
};
88+
const expected = {
89+
server: {
90+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
91+
packages: [{ registryType: 'nuget', name: 'CorrectId', version: '0.2.0' }],
92+
"_meta": {},
93+
},
94+
"_meta": { "io.modelcontextprotocol.registry/official": {} },
95+
};
96+
97+
const actual = nuget.prepareServerJson(manifest, "CorrectId", "0.2.0");
98+
99+
expect(actual).toEqual(expected);
100+
});
101+
35102
it('returns package metadata', async () => {
36103
commandExecutor.fullCommandToResultMap.set(
37104
'dotnet package search basetestpackage.DOTNETTOOL --source https://api.nuget.org/v3/index.json --prerelease --format json',

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

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ interface DotnetCli {
3737
args: Array<string>;
3838
}
3939

40+
const MCP_SERVER_SCHEMA_2025_07_09_GH = "https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json";
41+
const MCP_SERVER_SCHEMA_2025_07_09 = "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json";
42+
const MCP_SERVER_SCHEMA_2025_09_29 = "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json";
43+
4044
export class NuGetMcpSetup {
4145
constructor(
4246
public readonly logService: ILogService,
@@ -136,7 +140,7 @@ export class NuGetMcpSetup {
136140
const localInstallSuccess = await this.installLocalTool(id, version, cwd);
137141
if (!localInstallSuccess) { return undefined; }
138142

139-
return await this.readServerManifest(packagesDir, id, version, this.logService);
143+
return await this.readServerManifest(packagesDir, id, version);
140144
} catch (e) {
141145
this.logService.warn(`
142146
Failed to install NuGet package ${id}@${version}. Proceeding without server.json.
@@ -268,37 +272,91 @@ stderr: ${installResult.stderr}`);
268272
return true;
269273
}
270274

271-
async readServerManifest(packagesDir: string, id: string, version: string, logService: ILogService): Promise<string | undefined> {
272-
const serverJsonPath = path.join(packagesDir, id.toLowerCase(), version.toLowerCase(), ".mcp", "server.json");
273-
try {
274-
await fs.access(serverJsonPath, fs.constants.R_OK);
275-
} catch {
276-
logService.info(`No server.json found at ${serverJsonPath}. Proceeding without server.json for ${id}@${version}.`);
277-
return undefined;
278-
}
279-
280-
const json = await fs.readFile(serverJsonPath, 'utf8');
281-
const manifest = JSON.parse(json);
282-
275+
prepareServerJson(manifest: any, id: string, version: string): any {
283276
// Force the ID and version of matching NuGet package in the server.json to the one we installed.
284277
// This handles cases where the server.json in the package is stale.
285278
// The ID should match generally, but we'll protect against unexpected package IDs.
279+
// We handle old and new schema formats:
280+
// - https://modelcontextprotocol.io/schemas/draft/2025-07-09/server.json (only hosted in GitHub)
281+
// - https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json (had several breaking changes over time)
282+
// - https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json
286283
if (manifest?.packages) {
287284
for (const pkg of manifest.packages) {
288-
if (pkg?.registry_name === "nuget") {
289-
if (pkg.name.toUpperCase() !== id.toUpperCase()) {
290-
logService.warn(`Package ID mismatch in NuGet.mcp / server.json: expected ${id}, found ${pkg.name}.`);
285+
if (!pkg) { continue; }
286+
const registryType = pkg.registryType ?? pkg.registry_type ?? pkg.registry_name;
287+
if (registryType === "nuget") {
288+
if (pkg.name && pkg.name !== id) {
289+
this.logService.warn(`Package name mismatch in NuGet.mcp / server.json: expected ${id}, found ${pkg.name}.`);
290+
pkg.name = id;
291291
}
292-
if (pkg.version.toUpperCase() !== version.toUpperCase()) {
293-
logService.warn(`Package version mismatch in NuGet.mcp / server.json: expected ${version}, found ${pkg.version}.`);
292+
293+
if (pkg.identifier && pkg.identifier !== id) {
294+
this.logService.warn(`Package identifier mismatch in NuGet.mcp / server.json: expected ${id}, found ${pkg.identifier}.`);
295+
pkg.identifier = id;
294296
}
295297

296-
pkg.name = id;
297-
pkg.version = version;
298+
if (pkg.version !== version) {
299+
this.logService.warn(`Package version mismatch in NuGet.mcp / server.json: expected ${version}, found ${pkg.version}.`);
300+
pkg.version = version;
301+
}
298302
}
299303
}
300304
}
301305

306+
// the original .NET MCP server project template used a schema URL that is deprecated
307+
if (manifest["$schema"] === MCP_SERVER_SCHEMA_2025_07_09_GH) {
308+
manifest["$schema"] = MCP_SERVER_SCHEMA_2025_07_09;
309+
}
310+
311+
if (manifest["$schema"] !== MCP_SERVER_SCHEMA_2025_07_09
312+
&& manifest["$schema"] !== MCP_SERVER_SCHEMA_2025_09_29) {
313+
this.logService.info(`NuGet package server.json has unrecognized schema version: '${manifest["$schema"]}'.`);
314+
}
315+
316+
// provide empty publisher provided metadata to enable VS Code data mapping
317+
if (!manifest["_meta"]) {
318+
manifest["_meta"] = {};
319+
}
320+
321+
// starting from 2025-09-29, the server.json schema root was changed to have a "server" property
322+
if (manifest["$schema"] === MCP_SERVER_SCHEMA_2025_09_29) {
323+
manifest = { server: manifest };
324+
}
325+
326+
// provide empty registry metadata to enable VS Code data mapping
327+
if (!manifest["_meta"]) {
328+
manifest["_meta"] = {};
329+
}
330+
331+
if (!manifest["_meta"]["io.modelcontextprotocol.registry/official"]) {
332+
manifest["_meta"]["io.modelcontextprotocol.registry/official"] = {};
333+
}
334+
302335
return manifest;
303336
}
337+
338+
async readServerManifest(packagesDir: string, id: string, version: string): Promise<string | undefined> {
339+
const serverJsonPath = path.join(packagesDir, id.toLowerCase(), version.toLowerCase(), ".mcp", "server.json");
340+
try {
341+
await fs.access(serverJsonPath, fs.constants.R_OK);
342+
} catch {
343+
this.logService.info(`No server.json found at ${serverJsonPath}. Proceeding without server.json for ${id}@${version}.`);
344+
return undefined;
345+
}
346+
347+
const json = await fs.readFile(serverJsonPath, 'utf8');
348+
let manifest;
349+
try {
350+
manifest = JSON.parse(json);
351+
} catch {
352+
this.logService.warn(`Invalid JSON in NuGet package server.json at ${serverJsonPath}. Proceeding without server.json for ${id}@${version}.`);
353+
return undefined;
354+
}
355+
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) {
356+
this.logService.warn(`Invalid JSON in NuGet package server.json at ${serverJsonPath}. Proceeding without server.json for ${id}@${version}.`);
357+
return undefined;
358+
}
359+
360+
return this.prepareServerJson(manifest, id, version);
361+
}
304362
}

0 commit comments

Comments
 (0)