Skip to content

Commit 831dfa7

Browse files
authored
Use .NET SDK to search for NuGet packages, emit events, add tests for all package types (#546)
* Extract server.json from NuGet package during assisted install * Use .NET CLI as much as possible * Add tests, use GitHub Actions for .NET setup * Localize user facing strings * Update src/extension/mcp/vscode-node/nuget.ts * Update src/extension/mcp/vscode-node/commands.ts * switch to enum * Use IFetcherService for npm, PyPI, and Docker Hub, use local package source for NuGet
1 parent 3b7ed3c commit 831dfa7

File tree

14 files changed

+774
-96
lines changed

14 files changed

+774
-96
lines changed

.github/workflows/pr.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ jobs:
8989
python-version: '3.12'
9090
architecture: 'x64'
9191

92+
- name: Setup .NET
93+
uses: actions/setup-dotnet@v4
94+
with:
95+
dotnet-version: '10.0'
96+
9297
- name: Install setuptools
9398
run: pip install setuptools
9499

@@ -116,9 +121,6 @@ jobs:
116121
mkdir -p .build/build_cache
117122
tar -czf .build/build_cache/cache.tgz --files-from .build/build_cache_list.txt
118123
119-
- name: Install dotnet cli
120-
run: npm run setup:dotnet
121-
122124
- name: TypeScript type checking
123125
run: npm run typecheck
124126

@@ -171,6 +173,11 @@ jobs:
171173
python-version: '3.12'
172174
architecture: 'x64'
173175

176+
- name: Setup .NET
177+
uses: actions/setup-dotnet@v4
178+
with:
179+
dotnet-version: '10.0'
180+
174181
- name: Install setuptools
175182
run: pip install setuptools
176183

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3591,8 +3591,8 @@
35913591
"simulate-gc": "node dist/simulationMain.js --require-cache --gc",
35923592
"setup": "npm run get_env && npm run get_token",
35933593
"setup:dotnet": "run-script-os",
3594-
"setup:dotnet:darwin:linux": "curl -O https://raw.githubusercontent.com/dotnet/install-scripts/main/src/dotnet-install.sh && chmod u+x dotnet-install.sh && ./dotnet-install.sh --version latest --quality GA --channel STS && rm dotnet-install.sh",
3595-
"setup:dotnet:win32": "Invoke-WebRequest -Uri https://raw.githubusercontent.com/dotnet/install-scripts/main/src/dotnet-install.ps1 && chmod u+x dotnet-install.ps1 && ./dotnet-install.ps1 --version latest --quality GA --channel STS && rm dotnet-install.ps1",
3594+
"setup:dotnet:darwin:linux": "curl -O https://raw.githubusercontent.com/dotnet/install-scripts/main/src/dotnet-install.sh && chmod u+x dotnet-install.sh && ./dotnet-install.sh --channel 10.0 && rm dotnet-install.sh",
3595+
"setup:dotnet:win32": "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"Invoke-WebRequest -Uri https://raw.githubusercontent.com/dotnet/install-scripts/main/src/dotnet-install.ps1 -OutFile dotnet-install.ps1; ./dotnet-install.ps1 -channel 10.0; Remove-Item dotnet-install.ps1\"",
35963596
"create_venv": "tsx script/setup/createVenv.mts",
35973597
"package": "vsce package",
35983598
"web": "vscode-test-web --headless --extensionDevelopmentPath=. ."
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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 fs from 'fs/promises';
7+
import path from 'path';
8+
import { beforeEach, describe, expect, it } from 'vitest';
9+
import { ILogService } from '../../../../platform/log/common/logService';
10+
import { FetchOptions, IAbortController, IFetcherService, Response } from '../../../../platform/networking/common/fetcherService';
11+
import { ITestingServicesAccessor, TestingServiceCollection } from '../../../../platform/test/node/services';
12+
import { createExtensionUnitTestingServices } from '../../../test/node/services';
13+
import { McpSetupCommands } from '../../vscode-node/commands';
14+
15+
describe('get MCP server info', { timeout: 30_000 }, () => {
16+
let testingServiceCollection: TestingServiceCollection;
17+
let accessor: ITestingServicesAccessor;
18+
let logService: ILogService;
19+
let emptyFetcherService: FixtureFetcherService;
20+
21+
beforeEach(() => {
22+
testingServiceCollection = createExtensionUnitTestingServices();
23+
accessor = testingServiceCollection.createTestingAccessor();
24+
logService = accessor.get(ILogService);
25+
emptyFetcherService = new FixtureFetcherService(404);
26+
});
27+
28+
it('npm returns package metadata', async () => {
29+
const fetcherService = new FixtureFetcherService(200, 'npm-modelcontextprotocol-server-everything.json');
30+
const result = await McpSetupCommands.validatePackageRegistry({ type: 'npm', name: '@modelcontextprotocol/server-everything' }, logService, fetcherService);
31+
expect(fetcherService.lastUrl).toBe('https://registry.npmjs.org/%40modelcontextprotocol%2Fserver-everything');
32+
expect(result.state).toBe('ok');
33+
if (result.state === 'ok') {
34+
expect(result.name).toBe('@modelcontextprotocol/server-everything');
35+
expect(result.version).toBeDefined();
36+
expect(result.publisher).toContain('jspahrsummers');
37+
} else {
38+
expect.fail();
39+
}
40+
});
41+
42+
it('npm handles missing package', async () => {
43+
const result = await McpSetupCommands.validatePackageRegistry({ type: 'npm', name: '@modelcontextprotocol/does-not-exist' }, logService, emptyFetcherService);
44+
expect(emptyFetcherService.lastUrl).toBe('https://registry.npmjs.org/%40modelcontextprotocol%2Fdoes-not-exist');
45+
expect(result.state).toBe('error');
46+
if (result.state === 'error') {
47+
expect(result.error).toBeDefined();
48+
expect(result.errorType).toBe('NotFound');
49+
} else {
50+
expect.fail();
51+
}
52+
});
53+
54+
it('pip returns package metadata', async () => {
55+
const fetcherService = new FixtureFetcherService(200, 'pip-mcp-server-fetch.json');
56+
const result = await McpSetupCommands.validatePackageRegistry({ type: 'pip', name: 'mcp-server-fetch' }, logService, fetcherService);
57+
expect(fetcherService.lastUrl).toBe('https://pypi.org/pypi/mcp-server-fetch/json');
58+
expect(result.state).toBe('ok');
59+
if (result.state === 'ok') {
60+
expect(result.name).toBe('mcp-server-fetch');
61+
expect(result.version).toBeDefined();
62+
expect(result.publisher).toContain('Anthropic');
63+
} else {
64+
expect.fail();
65+
}
66+
});
67+
68+
it('pip handles missing package', async () => {
69+
const result = await McpSetupCommands.validatePackageRegistry({ type: 'pip', name: 'mcp-server-that-does-not-exist' }, logService, emptyFetcherService);
70+
expect(emptyFetcherService.lastUrl).toBe('https://pypi.org/pypi/mcp-server-that-does-not-exist/json');
71+
expect(result.state).toBe('error');
72+
if (result.state === 'error') {
73+
expect(result.error).toBeDefined();
74+
expect(result.errorType).toBe('NotFound');
75+
} else {
76+
expect.fail();
77+
}
78+
});
79+
80+
it('docker returns package metadata', async () => {
81+
const fetcherService = new FixtureFetcherService(200, 'docker-mcp-node-code-sandbox.json');
82+
const result = await McpSetupCommands.validatePackageRegistry({ type: 'docker', name: 'mcp/node-code-sandbox' }, logService, fetcherService);
83+
expect(fetcherService.lastUrl).toBe('https://hub.docker.com/v2/repositories/mcp/node-code-sandbox');
84+
expect(result.state).toBe('ok');
85+
if (result.state === 'ok') {
86+
expect(result.name).toBe('mcp/node-code-sandbox');
87+
expect(result.version).toBeUndefined(); // currently not populated
88+
expect(result.publisher).toBe("mcp");
89+
} else {
90+
expect.fail();
91+
}
92+
});
93+
94+
it('docker handles missing package', async () => {
95+
const result = await McpSetupCommands.validatePackageRegistry({ type: 'docker', name: 'mcp/server-that-does-not-exist' }, logService, emptyFetcherService);
96+
expect(emptyFetcherService.lastUrl).toBe('https://hub.docker.com/v2/repositories/mcp/server-that-does-not-exist');
97+
expect(result.state).toBe('error');
98+
if (result.state === 'error') {
99+
expect(result.error).toBeDefined();
100+
expect(result.errorType).toBe('NotFound');
101+
} else {
102+
expect.fail();
103+
}
104+
});
105+
});
106+
107+
class FixtureFetcherService implements IFetcherService {
108+
lastUrl?: string;
109+
110+
constructor(readonly status: number = 404, readonly fileName?: string) { }
111+
112+
fetch(url: string, options: FetchOptions): Promise<Response> {
113+
this.lastUrl = url;
114+
// Simulate a successful response
115+
return Promise.resolve({
116+
ok: this.status === 200,
117+
status: this.status,
118+
json: async () => {
119+
if (this.fileName) {
120+
const filePath = path.join(__dirname, 'fixtures', 'snapshots', this.fileName);
121+
return JSON.parse(await fs.readFile(filePath, 'utf-8'));
122+
} else {
123+
return {};
124+
}
125+
},
126+
} as Response);
127+
}
128+
129+
_serviceBrand: undefined;
130+
getUserAgentLibrary(): string { throw new Error('Method not implemented.'); }
131+
disconnectAll(): Promise<unknown> { throw new Error('Method not implemented.'); }
132+
makeAbortController(): IAbortController { throw new Error('Method not implemented.'); }
133+
isAbortError(e: any): boolean { throw new Error('Method not implemented.'); }
134+
isInternetDisconnectedError(e: any): boolean { throw new Error('Method not implemented.'); }
135+
isFetcherError(e: any): boolean { throw new Error('Method not implemented.'); }
136+
getUserMessageForFetcherError(err: any): string { throw new Error('Method not implemented.'); }
137+
}
Binary file not shown.
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"user":"mcp","name":"node-code-sandbox","namespace":"mcp","repository_type":null,"status":1,"status_description":"active","description":"A Node.js–based Model Context Protocol server that spins up disposable Docker containers to exe...","is_private":false,"is_automated":false,"star_count":4,"pull_count":8909,"last_updated":"2025-07-07T06:15:47.12968Z","last_modified":"2025-08-13T06:22:59.359735Z","date_registered":"2025-05-13T10:29:11.448259Z","collaborator_count":0,"affiliation":null,"hub_user":"mcp","has_starred":false,"full_description":"# Node.js Sandbox MCP Server\n\nA Node.js–based Model Context Protocol server that spins up disposable Docker containers to execute arbitrary JavaScript.\n\n[What is an MCP Server?](https://www.anthropic.com/news/model-context-protocol)\n\n## Characteristics\nAttribute|Details|\n|-|-|\n**Docker Image**|[mcp/node-code-sandbox](https://hub.docker.com/repository/docker/mcp/node-code-sandbox)\n**Author**|[alfonsograziano](https://github.com/alfonsograziano)\n**Repository**|https://github.com/alfonsograziano/node-code-sandbox-mcp\n**Dockerfile**|https://github.com/alfonsograziano/node-code-sandbox-mcp/blob/master/Dockerfile\n**Docker Image built by**|Docker Inc.\n**Docker Scout Health Score**| ![Docker Scout Health Score](https://api.scout.docker.com/v1/policy/insights/org-image-score/badge/mcp/node-code-sandbox)\n**Verify Signature**|`COSIGN_REPOSITORY=mcp/signatures cosign verify mcp/node-code-sandbox --key https://raw.githubusercontent.com/docker/keyring/refs/heads/main/public/mcp/latest.pub`\n**Licence**|\n\n## Available Tools (7)\nTools provided by this Server|Short Description\n-|-\n`get_dependency_types`|Given an array of npm package names (and optional versions), fetch whether each package ships its own TypeScript definitions or has a corresponding @types/… package, and return the raw .d.ts text.|\n`run_js`|Install npm dependencies and run JavaScript code inside a running sandbox container.|\n`run_js_ephemeral`|Run a JavaScript snippet in a temporary disposable container with optional npm dependencies, then automatically clean up.|\n`sandbox_exec`|Execute one or more shell commands inside a running sandbox container.|\n`sandbox_initialize`|Start a new isolated Docker container running Node.js.|\n`sandbox_stop`|Terminate and remove a running sandbox container.|\n`search_npm_packages`|Search for npm packages by a search term and get their name, description, and a README snippet.|\n\n---\n## Tools Details\n\n#### Tool: **`get_dependency_types`**\nGiven an array of npm package names (and optional versions), \n fetch whether each package ships its own TypeScript definitions \n or has a corresponding @types/… package, and return the raw .d.ts text.\n\n Useful whenwhen you're about to run a Node.js script against an unfamiliar dependency \n and want to inspect what APIs and types it exposes.\nParameters|Type|Description\n-|-|-\n`dependencies`|`array`|\n\n---\n#### Tool: **`run_js`**\nInstall npm dependencies and run JavaScript code inside a running sandbox container.\n After running, you must manually stop the sandbox to free resources.\n The code must be valid ESModules (import/export syntax). Best for complex workflows where you want to reuse the environment across multiple executions.\n When reading and writing from the Node.js processes, you always need to read from and write to the \"./files\" directory to ensure persistence on the mounted volume.\nParameters|Type|Description\n-|-|-\n`code`|`string`|JavaScript code to run inside the container.\n`container_id`|`string`|Docker container identifier\n`dependencies`|`array` *optional*|A list of npm dependencies to install before running the code. Each item must have a `name` (package) and `version` (range). If none, returns an empty array.\n`listenOnPort`|`number` *optional*|If set, leaves the process running and exposes this port to the host.\n\n---\n#### Tool: **`run_js_ephemeral`**\nRun a JavaScript snippet in a temporary disposable container with optional npm dependencies, then automatically clean up. \n The code must be valid ESModules (import/export syntax). Ideal for simple one-shot executions without maintaining a sandbox or managing cleanup manually.\n When reading and writing from the Node.js processes, you always need to read from and write to the \"./files\" directory to ensure persistence on the mounted volume.\n This includes images (e.g., PNG, JPEG) and other files (e.g., text, JSON, binaries).\n\n Example:\n ```js\n import fs from \"fs/promises\";\n await fs.writeFile(\"./files/hello.txt\", \"Hello world!\");\n console.log(\"Saved ./files/hello.txt\");\n ```\nParameters|Type|Description\n-|-|-\n`code`|`string`|JavaScript code to run inside the ephemeral container.\n`dependencies`|`array` *optional*|A list of npm dependencies to install before running the code. Each item must have a `name` (package) and `version` (range). If none, returns an empty array.\n`image`|`string` *optional*|Docker image to use for ephemeral execution. e.g. - **node:lts-slim**: Node.js LTS version, slim variant. (Lightweight and fast for JavaScript execution tasks.)\n- **mcr.microsoft.com/playwright:v1.53.2-noble**: Playwright image for browser automation. (Preconfigured for running Playwright scripts.)\n- **alfonsograziano/node-chartjs-canvas:latest**: Chart.js image for chart generation and mermaid charts generation. ('Preconfigured for generating charts with chartjs-node-canvas and Mermaid. Minimal Mermaid example:\n import fs from \"fs\";\n import { run } from \"@mermaid-js/mermaid-cli\";\n fs.writeFileSync(\"./files/diagram.mmd\", \"graph LR; A--\u003eB;\", \"utf8\");\n await run(\"./files/diagram.mmd\", \"./files/diagram.svg\");)\n\n---\n#### Tool: **`sandbox_exec`**\nExecute one or more shell commands inside a running sandbox container. Requires a sandbox initialized beforehand.\nParameters|Type|Description\n-|-|-\n`commands`|`array`|\n`container_id`|`string`|\n\n---\n#### Tool: **`sandbox_initialize`**\nStart a new isolated Docker container running Node.js. Used to set up a sandbox session for multiple commands and scripts.\nParameters|Type|Description\n-|-|-\n`image`|`string` *optional*|\n`port`|`number` *optional*|If set, maps this container port to the host\n\n---\n#### Tool: **`sandbox_stop`**\nTerminate and remove a running sandbox container. Should be called after finishing work in a sandbox initialized with sandbox_initialize.\nParameters|Type|Description\n-|-|-\n`container_id`|`string`|\n\n---\n#### Tool: **`search_npm_packages`**\nSearch for npm packages by a search term and get their name, description, and a README snippet.\nParameters|Type|Description\n-|-|-\n`searchTerm`|`string`|The term to search for in npm packages. Should contain all relevant context. Should ideally be text that might appear in the package name, description, or keywords. Use plus signs (+) to combine related terms (e.g., \"react+components\" for React component libraries). For filtering by author, maintainer, or scope, use the qualifiers field instead of including them in the search term. Examples: \"express\" for Express.js, \"ui+components\" for UI component packages, \"testing+jest\" for Jest testing utilities.\n`qualifiers`|`object` *optional*|Optional qualifiers to filter the search results. For example, { not: \"insecure\" } will exclude insecure packages, { author: \"sindresorhus\" } will only show packages by that author, { scope: \"@vue\" } will only show Vue.js scoped packages.\n\n---\n## Use this MCP Server\n\n```json\n{\n \"mcpServers\": {\n \"node-code-sandbox\": {\n \"command\": \"docker\",\n \"args\": [\n \"run\",\n \"-i\",\n \"--rm\",\n \"mcp/node-code-sandbox\"\n ]\n }\n }\n}\n```\n\n[Why is it safer to run MCP Servers with Docker?](https://www.docker.com/blog/the-model-context-protocol-simplifying-building-ai-apps-with-anthropic-claude-desktop-and-docker/)\n","permissions":{"read":true,"write":false,"admin":false},"media_types":["application/vnd.oci.image.index.v1+json"],"content_types":["image"],"categories":[{"name":"Machine learning \u0026 AI","slug":"machine-learning-and-ai"}],"immutable_tags_settings":{"enabled":false,"rules":[".*"]},"storage_size":1107034273}

src/extension/mcp/test/vscode-node/fixtures/snapshots/npm-modelcontextprotocol-server-everything.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

src/extension/mcp/test/vscode-node/fixtures/snapshots/pip-mcp-server-fetch.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)