diff --git a/.gitignore b/.gitignore index 701fb9cc62..4d7246f7e6 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ project.lock.json *.ipch *.js !.eslintrc.js +!dotnet-install-mcp.js *.d.ts *.js.map .build/ diff --git a/sample/yarn.lock b/sample/yarn.lock index 91d8771029..2d5d0cf913 100644 --- a/sample/yarn.lock +++ b/sample/yarn.lock @@ -249,46 +249,6 @@ https-proxy-agent "^7.0.0" tslib "^2.6.2" -"@vscode/vsce-sign-alpine-arm64@2.0.5": - version "2.0.5" - resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.5.tgz" - integrity sha1-40y/kfToamz1KrwubnUISuGPbEo= - -"@vscode/vsce-sign-alpine-x64@2.0.5": - version "2.0.5" - resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.5.tgz" - integrity sha1-dEPA6DnnTwP84MwxRTMPDSqAzIc= - -"@vscode/vsce-sign-darwin-arm64@2.0.5": - version "2.0.5" - resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.5.tgz" - integrity sha1-LqusfYNxKSqNIqFbP/V/GYjCnWs= - -"@vscode/vsce-sign-darwin-x64@2.0.5": - version "2.0.5" - resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.5.tgz" - integrity sha1-lvsDKcijZxhMID1iV0+akhkwItg= - -"@vscode/vsce-sign-linux-arm@2.0.5": - version "2.0.5" - resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.5.tgz" - integrity sha1-vwc0DbH+Ncs6iiIrLaSqJTEO4lE= - -"@vscode/vsce-sign-linux-arm64@2.0.5": - version "2.0.5" - resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.5.tgz" - integrity sha1-wEUCMqukP76t/1MJg4pWVdxwOcg= - -"@vscode/vsce-sign-linux-x64@2.0.5": - version "2.0.5" - resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.5.tgz" - integrity sha1-I4KZJPQIZ+kNXju4Yejo+gResO4= - -"@vscode/vsce-sign-win32-arm64@2.0.5": - version "2.0.5" - resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.5.tgz" - integrity sha1-GO8nH199mzHAMSdYLBscUfJuI7Q= - "@vscode/vsce-sign-win32-x64@2.0.5": version "2.0.5" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.5.tgz" diff --git a/vscode-dotnet-runtime-extension/MCP-README.md b/vscode-dotnet-runtime-extension/MCP-README.md new file mode 100644 index 0000000000..081bf1bcc9 --- /dev/null +++ b/vscode-dotnet-runtime-extension/MCP-README.md @@ -0,0 +1,102 @@ +# .NET MCP Server + +This extension provides a Model Context Protocol (MCP) server for .NET installation and management tools. The MCP server allows AI assistants to install, manage, and query .NET SDKs and runtimes. + +## Architecture + +The .NET MCP implementation consists of three main components: + +### 1. VS Code Command Executor (`vscode-command-executor.ts`) +- Runs within the VS Code extension host +- Listens on a named pipe for commands from external processes +- Executes VS Code extension commands (`dotnet.acquireGlobalSDK`, `dotnet.uninstall`, etc.) +- Provides secure communication between the MCP server and VS Code APIs + +### 2. MCP Server Tool (`dotnet-install-mcp.js`) +- Standalone Node.js process that implements the MCP protocol +- Communicates with AI assistants via stdio (JSON-RPC) +- Connects to VS Code Command Executor via named pipes +- Can also be used as a standalone CLI tool + +### 3. MCP Provider (`dotnet-mcp-provider.ts`) +- Integrates with VS Code's MCP infrastructure +- Attempts to register with VS Code MCP API if available +- Provides fallback configuration for manual setup +- Manages the lifecycle of MCP components + +## Usage + +### Automatic Setup (VS Code MCP API) +If your VS Code version supports the MCP API, the server will be automatically registered and available in the MCP view. + +### Manual Setup +If automatic registration is not available, add this configuration to your VS Code `mcp.json`: + +```json +{ + "servers": { + "dotnet-installer": { + "type": "stdio", + "command": "dotnet-install-mcp-server", + "args": [] + } + } +} +``` + +### VS Code Extension API +The extension also exposes the standard VS Code commands that can be called programmatically: + +```typescript +// Install .NET runtime +await vscode.commands.executeCommand('dotnet.acquire', { + version: '8.0', + requestingExtensionId: 'your-extension-id', + mode: 'runtime' +}); + +// Install .NET SDK globally +await vscode.commands.executeCommand('dotnet.acquireGlobalSDK', { + version: '8.0', + requestingExtensionId: 'your-extension-id' +}); +``` + +## Setup + +1. The extension automatically starts the VSCode Command Executor when activated +2. The MCP command-line tool is added to PATH in `~/.dotnet-mcp/bin/` +3. The MCP server infrastructure is set up for stdio communication + +## Communication Flow + +``` +AI Assistant -> MCP Protocol -> dotnet-install-mcp.js -> Named Pipe -> vscode-command-executor.ts -> VS Code Commands -> .NET Installation +``` + +## File Structure + +- `vscode-command-executor.ts` - Pipe server that executes VS Code commands +- `dotnet-install-mcp.js` - Standalone CLI tool for pipe communication +- `extension.ts` - Contains `setupMCPServer()` function for integration +- `mcp-package.json` - Package configuration for the MCP tool + +## Security Considerations + +- The named pipe is local to the machine +- Commands are validated before execution +- Only specific VS Code commands are allowed +- Authentication could be added via pipe permissions + +## Error Handling + +- Connection timeouts (5 minutes) +- Command validation +- Proper error propagation through the pipe +- MCP protocol error responses + +## Dependencies + +- VS Code Extension: Uses existing extension dependencies +- MCP Tool: Only requires Node.js and `uuid` package +- No external dependencies for basic operation diff --git a/vscode-dotnet-runtime-extension/package-lock.json b/vscode-dotnet-runtime-extension/package-lock.json index 517d9a93dd..77a9725acb 100644 --- a/vscode-dotnet-runtime-extension/package-lock.json +++ b/vscode-dotnet-runtime-extension/package-lock.json @@ -23,6 +23,7 @@ "shelljs": "^0.8.5", "ts-loader": "^9.5.1", "typescript": "^5.5.4", + "uuid": "^9.0.0", "vscode-dotnet-runtime-library": "file:../vscode-dotnet-runtime-library", "webpack-permissions-plugin": "^1.0.9" }, @@ -3603,6 +3604,19 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "license": "MIT" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha1-4YjUyIU8xyIiA5LEJM1jfzIpPzA=", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vscode-dotnet-runtime-library": { "resolved": "../vscode-dotnet-runtime-library", "link": true diff --git a/vscode-dotnet-runtime-extension/package.json b/vscode-dotnet-runtime-extension/package.json index bd2a79f3d6..6dce14b6b5 100644 --- a/vscode-dotnet-runtime-extension/package.json +++ b/vscode-dotnet-runtime-extension/package.json @@ -129,7 +129,13 @@ "description": "Enable this to show more detailed output from our extension." } } - } + }, + "mcpServerDefinitionProviders": [ + { + "id": "dotnetInstaller", + "label": ".NET Installation Tools" + } + ] }, "scripts": { "vscode:prepublish": "npm run compile-all && npm install && webpack --mode production && dotnet build ../msbuild/signJs --property jsOutputPath=..\\..\\vscode-dotnet-runtime-extension\\dist -bl -v:normal", @@ -156,6 +162,7 @@ "shelljs": "^0.8.5", "ts-loader": "^9.5.1", "typescript": "^5.5.4", + "uuid": "^9.0.0", "vscode-dotnet-runtime-library": "file:../vscode-dotnet-runtime-library", "webpack-permissions-plugin": "^1.0.9" }, @@ -176,4 +183,4 @@ "publisherId": "d05e23de-3974-4ff0-8d47-23ee77830092", "isPreReleaseVersion": false } -} \ No newline at end of file +} diff --git a/vscode-dotnet-runtime-extension/src/dotnet-install-mcp.js b/vscode-dotnet-runtime-extension/src/dotnet-install-mcp.js new file mode 100644 index 0000000000..0adf9d4158 --- /dev/null +++ b/vscode-dotnet-runtime-extension/src/dotnet-install-mcp.js @@ -0,0 +1,537 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ + +const net = require('net'); +const {v4: uuidv4} = require('uuid'); + +/** + * Standalone MCP Command Line Tool for .NET Installation + * + * This is a standalone Node.js script that can work in two modes: + * 1. CLI mode: Communicates with VSCode extension via named pipes + * 2. MCP stdio mode: Implements MCP protocol over stdio for AI assistants + * + * Usage: + * dotnet-install-mcp acquire --version 8.0 --mode runtime + * dotnet-install-mcp acquire-global --version 8.0 + * dotnet-install-mcp uninstall --version 8.0 --mode runtime + * dotnet-install-mcp --mcp-stdio (for MCP mode) + */ + +class DotnetInstallMCP +{ + constructor(pipeName = 'dotnet-vscode-executor') + { + this.pipeName = process.platform === 'win32' + ? `\\\\.\\pipe\\${pipeName}` + : `/tmp/${pipeName}.sock`; + this.isStdioMode = false; + } + + /** + * Run in MCP stdio mode + */ + async runStdioMode() + { + this.isStdioMode = true; + console.error('Starting .NET MCP Server in stdio mode'); + + // Handle incoming JSON-RPC messages from stdin + let buffer = ''; + process.stdin.on('data', (chunk) => + { + buffer += chunk.toString(); + + // Process complete lines + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // Keep incomplete line in buffer + + for (const line of lines) + { + if (line.trim()) + { + this.handleMcpMessage(line.trim()); + } + } + }); + + process.stdin.on('end', () => + { + process.exit(0); + }); + + // Send initial capabilities + this.sendMcpResponse({ + jsonrpc: '2.0', + id: null, + result: { + protocolVersion: '2024-11-05', + capabilities: { + tools: {} + }, + serverInfo: { + name: 'dotnet-install-mcp-server', + version: '1.0.0' + } + } + }); + } + + /** + * Handle MCP protocol messages + */ + async handleMcpMessage(messageStr) + { + try + { + const message = JSON.parse(messageStr); + let response; + + switch (message.method) + { + case 'initialize': + response = { + jsonrpc: '2.0', + id: message.id, + result: { + protocolVersion: '2024-11-05', + capabilities: { + tools: {} + }, + serverInfo: { + name: 'dotnet-install-mcp-server', + version: '1.0.0' + } + } + }; + break; + + case 'tools/list': + response = { + jsonrpc: '2.0', + id: message.id, + result: { + tools: [ + { + name: 'install_dotnet_runtime', + description: 'Install .NET runtime locally for VS Code extensions', + inputSchema: { + type: 'object', + properties: { + version: {type: 'string', description: '.NET version to install (e.g., 8.0, 6.0, latest)'}, + architecture: {type: 'string', description: 'Target architecture (x64, arm64, x86)', default: 'current'} + }, + required: ['version'] + } + }, + { + name: 'install_dotnet_sdk', + description: 'Install .NET SDK globally on the system', + inputSchema: { + type: 'object', + properties: { + version: {type: 'string', description: '.NET SDK version to install (e.g., 8.0, 6.0, latest)'}, + architecture: {type: 'string', description: 'Target architecture (x64, arm64, x86)', default: 'current'} + }, + required: ['version'] + } + }, + { + name: 'uninstall_dotnet', + description: 'Uninstall .NET runtime or SDK', + inputSchema: { + type: 'object', + properties: { + version: {type: 'string', description: '.NET version to uninstall'}, + mode: {type: 'string', description: 'runtime, sdk, or aspnetcore', default: 'runtime'}, + architecture: {type: 'string', description: 'Target architecture (x64, arm64, x86)', default: 'current'} + }, + required: ['version'] + } + } + ] + } + }; + break; + + case 'tools/call': + response = await this.handleMcpToolCall(message.params, message.id); + break; + + default: + response = { + jsonrpc: '2.0', + id: message.id, + error: { + code: -32601, + message: `Method not found: ${message.method}` + } + }; + } + + this.sendMcpResponse(response); + + } catch (error) + { + const errorResponse = { + jsonrpc: '2.0', + id: null, + error: { + code: -32603, + message: 'Internal error', + data: error.message + } + }; + this.sendMcpResponse(errorResponse); + } + } + + /** + * Handle MCP tool calls + */ + async handleMcpToolCall(params, messageId) + { + const {name, arguments: args} = params; + + try + { + let command; + let cliArgs = []; + + switch (name) + { + case 'install_dotnet_runtime': + command = 'acquire'; + cliArgs = ['--version', args.version, '--mode', 'runtime']; + if (args.architecture && args.architecture !== 'current') + { + cliArgs.push('--architecture', args.architecture); + } + break; + + case 'install_dotnet_sdk': + command = 'acquire-global'; + cliArgs = ['--version', args.version]; + if (args.architecture && args.architecture !== 'current') + { + cliArgs.push('--architecture', args.architecture); + } + break; + + case 'uninstall_dotnet': + command = 'uninstall'; + cliArgs = ['--version', args.version, '--mode', args.mode || 'runtime']; + if (args.architecture && args.architecture !== 'current') + { + cliArgs.push('--architecture', args.architecture); + } + break; + + default: + throw new Error(`Unknown tool: ${name}`); + } + + // Execute the command via the pipe interface + const result = await this.executeViaPipe(command, cliArgs); + + return { + jsonrpc: '2.0', + id: messageId, + result: { + content: [ + { + type: 'text', + text: `Successfully executed ${name}.\n\nCommand: ${command} ${cliArgs.join(' ')}\n\nResult:\n${JSON.stringify(result, null, 2)}` + } + ] + } + }; + + } catch (error) + { + return { + jsonrpc: '2.0', + id: messageId, + error: { + code: -32603, + message: `Tool execution failed: ${error.message}` + } + }; + } + } + + /** + * Execute command via the named pipe to VS Code extension + */ + async executeViaPipe(command, args) + { + const {command: cmdName, options} = this.parseArgs([command, ...args]); + const context = this.createContext(cmdName, options); + return await this.sendCommand(cmdName, context); + } + + /** + * Send MCP response to stdout + */ + sendMcpResponse(response) + { + process.stdout.write(JSON.stringify(response) + '\n'); + } + + /** + * Parse command line arguments + */ + parseArgs(args) + { + const command = args[0]; + const options = {}; + + for (let i = 1; i < args.length; i += 2) + { + const key = args[i]?.replace('--', ''); + const value = args[i + 1]; + if (key && value) + { + options[key] = value; + } + } + + return {command, options}; + } + + /** + * Create a dotnet acquire context from parsed options + */ + createContext(command, options) + { + const context = { + version: options.version || 'latest', + requestingExtensionId: 'dotnet-mcp-server', + architecture: options.architecture || this.getDefaultArchitecture(), + installType: command === 'acquire-global' ? 'global' : 'local', + mode: options.mode || (command === 'acquire-global' ? 'sdk' : 'runtime') + }; + + return context; + } + + /** + * Get the default architecture for the current platform + */ + getDefaultArchitecture() + { + const arch = process.arch; + switch (arch) + { + case 'x64': return 'x64'; + case 'arm64': return 'arm64'; + case 'ia32': return 'x86'; + default: return 'x64'; + } + } + + /** + * Map command to VSCode command name + */ + getVSCodeCommand(command) + { + switch (command) + { + case 'acquire': return 'dotnet.acquire'; + case 'acquire-global': return 'dotnet.acquireGlobalSDK'; + case 'uninstall': return 'dotnet.uninstall'; + default: + throw new Error(`Unknown command: ${command}`); + } + } + + /** + * Send command to VSCode extension via pipe + */ + async sendCommand(command, context) + { + return new Promise((resolve, reject) => + { + const client = net.createConnection(this.pipeName); + const requestId = uuidv4(); + let responseBuffer = ''; + + const request = { + id: requestId, + command: this.getVSCodeCommand(command), + context + }; + + client.on('connect', () => + { + if (!this.isStdioMode) + { + console.log(`Connected to VSCode extension`); + } + client.write(JSON.stringify(request) + '\n'); + }); + + client.on('data', (data) => + { + responseBuffer += data.toString(); + + // Check if we have a complete response (newline-terminated) + const lines = responseBuffer.split('\n'); + for (let i = 0; i < lines.length - 1; i++) + { + const line = lines[i].trim(); + if (line) + { + try + { + const response = JSON.parse(line); + if (response.id === requestId) + { + client.end(); + if (response.success) + { + resolve(response.result); + } else + { + reject(new Error(response.error)); + } + return; + } + } catch (error) + { + // Continue processing other lines + } + } + } + + // Keep the last incomplete line for next data event + responseBuffer = lines[lines.length - 1]; + }); + + client.on('error', (error) => + { + reject(new Error(`Failed to connect to VSCode extension: ${error.message}`)); + }); + + client.on('close', () => + { + if (responseBuffer.trim()) + { + reject(new Error('Connection closed without receiving complete response')); + } + }); + + // Set timeout for the operation + setTimeout(() => + { + client.destroy(); + reject(new Error('Command timeout')); + }, 300000); // 5 minutes timeout + }); + } + + /** + * Run the MCP command in CLI mode + */ + async run(args) + { + try + { + const {command, options} = this.parseArgs(args); + + if (!command) + { + throw new Error('No command specified'); + } + + console.log(`Executing ${command} with options:`, options); + + const context = this.createContext(command, options); + const result = await this.sendCommand(command, context); + + console.log('Command completed successfully'); + if (result && typeof result === 'object') + { + if (result.dotnetPath) + { + console.log(`Path: ${result.dotnetPath}`); + } + console.log('Result:', JSON.stringify(result, null, 2)); + } else if (result) + { + console.log('Result:', result); + } + + return result; + + } catch (error) + { + console.error('Error:', error.message); + process.exit(1); + } + } + + /** + * Print usage information + */ + printUsage() + { + console.log(` +Usage: dotnet-install-mcp [options] + +Commands: + acquire Acquire .NET runtime locally + acquire-global Acquire .NET SDK globally + uninstall Uninstall .NET + +Options: + --version .NET version (e.g., 8.0, 6.0.201, latest) + --mode runtime, sdk, or aspnetcore (default: runtime for acquire, sdk for acquire-global) + --architecture x64, arm64, x86 (default: current platform architecture) + --mcp-stdio Run in MCP stdio mode for AI assistants + +Examples: + dotnet-install-mcp acquire --version 8.0 --mode runtime + dotnet-install-mcp acquire-global --version 8.0 + dotnet-install-mcp uninstall --version 8.0 --mode runtime --architecture x64 + dotnet-install-mcp --mcp-stdio (for MCP mode) + `); + } +} + +// Main execution +if (require.main === module) +{ + const args = process.argv.slice(2); + + if (args.length === 0 || args[0] === '--help' || args[0] === '-h') + { + const mcp = new DotnetInstallMCP(); + mcp.printUsage(); + process.exit(0); + } + + const mcp = new DotnetInstallMCP(); + + // Check if running in MCP stdio mode + if (args[0] === '--mcp-stdio') + { + mcp.runStdioMode().catch((error) => + { + console.error('Fatal error in MCP stdio mode:', error); + process.exit(1); + }); + } else + { + // Regular CLI mode + mcp.run(args).catch((error) => + { + console.error('Fatal error:', error); + process.exit(1); + }); + } +} + +module.exports = {DotnetInstallMCP}; diff --git a/vscode-dotnet-runtime-extension/src/dotnet-mcp-provider.ts b/vscode-dotnet-runtime-extension/src/dotnet-mcp-provider.ts new file mode 100644 index 0000000000..37c90cff76 --- /dev/null +++ b/vscode-dotnet-runtime-extension/src/dotnet-mcp-provider.ts @@ -0,0 +1,246 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { VSCodeCommandExecutor } from './vscode-command-executor'; + +/** + * MCP Server Definition Provider for .NET Installation Tools + * + * This sets up the infrastructure for MCP (Model Context Protocol) support. + * Since VS Code MCP APIs may not be available in all versions, this provides + * a fallback that creates the necessary infrastructure and configuration. + */ +export class DotnetMcpProvider +{ + private executor: VSCodeCommandExecutor | null = null; + private isSetup = false; + + constructor(private context: vscode.ExtensionContext, private eventStream: any) {} + + /** + * Setup the MCP infrastructure + */ + async setup(): Promise + { + if (this.isSetup) + { + return; + } + + try + { + console.log('Setting up .NET MCP infrastructure...'); + + // Start the VSCode command executor + this.executor = new VSCodeCommandExecutor(); + await this.executor.start(); + + // Add cleanup when extension is deactivated + this.context.subscriptions.push({ + dispose: () => this.executor?.stop() + }); + + // Try to register with VS Code MCP API if available + await this.tryRegisterMcpProvider(); + + // Create MCP configuration for manual setup + this.createMcpConfiguration(); + + this.isSetup = true; + console.log('.NET MCP infrastructure setup completed successfully'); + + } catch (error) + { + console.error('Failed to setup .NET MCP infrastructure:', error); + throw error; + } + } + + /** + * Try to register with VS Code MCP API (if available) + */ + private async tryRegisterMcpProvider(): Promise + { + try + { + // Check if the VS Code MCP API is available + const vscodeAny = vscode as any; + + if (vscodeAny.lm && vscodeAny.lm.registerMcpServerDefinitionProvider) + { + console.log('VS Code MCP API detected, registering provider...'); + + const provider = { + onDidChangeMcpServerDefinitions: new vscode.EventEmitter().event, + + provideMcpServerDefinitions: async () => + { + const mcpToolPath = path.join(__dirname, 'dotnet-install-mcp.js'); + + if (!fs.existsSync(mcpToolPath)) + { + console.error(`MCP tool not found at: ${mcpToolPath}`); + return []; + } + + // Create server definition using the VS Code MCP API + const McpStdioServerDefinition = vscodeAny.McpStdioServerDefinition; + if (McpStdioServerDefinition) + { + const serverDefinition = new McpStdioServerDefinition( + '.NET Installation Tools', + 'node', + [mcpToolPath, '--mcp-stdio'], + { + VSCODE_COMMAND_PIPE: this.executor?.getPipeName() || '', + DOTNET_EVENT_STREAM_CONTEXT: 'true' + } + ); + return [serverDefinition]; + } + + return []; + }, + + resolveMcpServerDefinition: async (definition: any) => + { + console.log('.NET MCP server starting via VS Code API...'); + return definition; + } + }; + + const registration = vscodeAny.lm.registerMcpServerDefinitionProvider('dotnetInstaller', provider); + this.context.subscriptions.push(registration); + + console.log('.NET MCP server registered with VS Code API successfully'); + } else + { + console.log('VS Code MCP API not available, using fallback configuration'); + } + } catch (error) + { + const errorMessage = error instanceof Error ? error.message : String(error); + console.log('Failed to register with VS Code MCP API, using fallback:', errorMessage); + } + } + + /** + * Create MCP configuration for manual setup + */ + private createMcpConfiguration(): void + { + const mcpToolPath = path.join(__dirname, 'dotnet-install-mcp.js'); + + const mcpConfig = { + servers: { + "dotnet-installer": { + "type": "stdio", + "command": "node", + "args": [mcpToolPath, "--mcp-stdio"], + "description": ".NET installation and management tools", + "env": { + "VSCODE_COMMAND_PIPE": this.executor?.getPipeName() || '', + "DOTNET_EVENT_STREAM_CONTEXT": "true" + } + } + } + }; + + console.log('\n=== .NET MCP Server Configuration ==='); + console.log('To use the .NET MCP server, add this to your VS Code mcp.json:'); + console.log(JSON.stringify(mcpConfig, null, 2)); + console.log('=====================================\n'); + + // Write configuration to global storage + try + { + const configPath = path.join(this.context.globalStoragePath, 'mcp-config.json'); + const configDir = path.dirname(configPath); + + if (!fs.existsSync(configDir)) + { + fs.mkdirSync(configDir, { recursive: true }); + } + + fs.writeFileSync(configPath, JSON.stringify(mcpConfig, null, 2)); + console.log(`MCP configuration saved to: ${configPath}`); + } catch (error) + { + console.error('Failed to save MCP configuration:', error); + } + + // Also create a workspace configuration hint + this.createWorkspaceConfigurationHint(mcpConfig); + } + + /** + * Create a hint for workspace-specific MCP configuration + */ + private createWorkspaceConfigurationHint(mcpConfig: any): void + { + try + { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) + { + const workspaceRoot = workspaceFolders[0].uri.fsPath; + const vscodePath = path.join(workspaceRoot, '.vscode'); + const mcpConfigPath = path.join(vscodePath, 'mcp.json'); + + // Don't overwrite existing config, just provide information + if (!fs.existsSync(mcpConfigPath)) + { + console.log(`To enable .NET MCP for this workspace, create: ${mcpConfigPath}`); + console.log('With the configuration shown above.'); + } else + { + console.log(`Workspace MCP config exists at: ${mcpConfigPath}`); + console.log('You can add the .NET MCP server configuration to it.'); + } + } + } catch (error) + { + console.error('Failed to create workspace configuration hint:', error); + } + } + + /** + * Get the command executor pipe name for external processes + */ + public getPipeName(): string | null + { + return this.executor?.getPipeName() || null; + } + + /** + * Dispose of resources + */ + public dispose(): void + { + this.executor?.stop(); + } +} + +/** + * Setup and initialize the .NET MCP infrastructure + */ +export function setupDotnetMcp(context: vscode.ExtensionContext, eventStream: any): DotnetMcpProvider +{ + const mcpProvider = new DotnetMcpProvider(context, eventStream); + + // Add to subscriptions for cleanup + context.subscriptions.push(mcpProvider); + + // Setup the infrastructure asynchronously + mcpProvider.setup().catch((error) => + { + console.error('Failed to setup .NET MCP:', error); + }); + + return mcpProvider; +} diff --git a/vscode-dotnet-runtime-extension/src/extension.ts b/vscode-dotnet-runtime-extension/src/extension.ts index 028fb9a554..46e445cb07 100644 --- a/vscode-dotnet-runtime-extension/src/extension.ts +++ b/vscode-dotnet-runtime-extension/src/extension.ts @@ -86,6 +86,7 @@ import } from 'vscode-dotnet-runtime-library'; import { InstallTrackerSingleton } from 'vscode-dotnet-runtime-library/dist/Acquisition/InstallTrackerSingleton'; import { dotnetCoreAcquisitionExtensionId } from './DotnetCoreAcquisitionId'; +import { setupDotnetMcp } from './dotnet-mcp-provider'; import open = require('open'); const packageJson = require('../package.json'); @@ -908,6 +909,9 @@ We will try to install .NET, but are unlikely to be able to connect to the serve // Preemptively install .NET for extensions who tell us to in their package.json const jsonInstaller = new JsonInstaller(globalEventStream, vsCodeExtensionContext); + // Setup MCP Server + setupDotnetMcp(vsCodeContext, globalEventStream); + // Exposing API Endpoints vsCodeContext.subscriptions.push( dotnetAcquireRegistration, diff --git a/vscode-dotnet-runtime-extension/src/mcp-package.json b/vscode-dotnet-runtime-extension/src/mcp-package.json new file mode 100644 index 0000000000..528127b2ef --- /dev/null +++ b/vscode-dotnet-runtime-extension/src/mcp-package.json @@ -0,0 +1,26 @@ +{ + "name": "dotnet-install-mcp", + "version": "1.0.0", + "description": "Standalone MCP command-line tool for .NET installation via VS Code extension", + "main": "dotnet-install-mcp.js", + "bin": { + "dotnet-install-mcp": "./dotnet-install-mcp.js" + }, + "scripts": { + "start": "node dotnet-install-mcp.js" + }, + "dependencies": { + "uuid": "^9.0.0" + }, + "keywords": [ + "dotnet", + "mcp", + "vscode", + "installation" + ], + "author": ".NET Foundation", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } +} diff --git a/vscode-dotnet-runtime-extension/src/vscode-command-executor.ts b/vscode-dotnet-runtime-extension/src/vscode-command-executor.ts new file mode 100644 index 0000000000..0fd8b37b09 --- /dev/null +++ b/vscode-dotnet-runtime-extension/src/vscode-command-executor.ts @@ -0,0 +1,210 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ + +import * as net from 'net'; +import * as vscode from 'vscode'; +import { IDotnetAcquireContext, IDotnetAcquireResult } from 'vscode-dotnet-runtime-library'; + +interface CommandRequest +{ + id: string; + command: 'dotnet.acquireGlobalSDK' | 'dotnet.uninstall' | 'dotnet.acquire'; + context: IDotnetAcquireContext; +} + +interface CommandResponse +{ + id: string; + success: boolean; + result?: IDotnetAcquireResult | string; + error?: string; +} + +/** + * VSCode Command Executor Process + * + * This process runs within the VSCode extension host and listens on a named pipe + * for commands to execute. It can run dotnet.acquireGlobalSDK, dotnet.uninstall, + * and dotnet.acquire commands with the provided context. + */ +export class VSCodeCommandExecutor +{ + private server: net.Server | null = null; + private readonly pipeName: string; + + constructor(pipeName: string = 'dotnet-vscode-executor') + { + this.pipeName = process.platform === 'win32' + ? `\\\\.\\pipe\\${pipeName}` + : `/tmp/${pipeName}.sock`; + } + + /** + * Start the command executor server + */ + public async start(): Promise + { + return new Promise((resolve, reject) => + { + this.server = net.createServer((socket) => + { + console.log('Client connected to VSCode command executor'); + + socket.on('data', async (data) => + { + try + { + const requests = data.toString().trim().split('\n'); + + for (const requestStr of requests) + { + if (!requestStr.trim()) continue; + + const request: CommandRequest = JSON.parse(requestStr); + const response = await this.executeCommand(request); + + socket.write(JSON.stringify(response) + '\n'); + } + } catch (error) + { + const errorResponse: CommandResponse = { + id: 'unknown', + success: false, + error: `Failed to parse request: ${error}` + }; + socket.write(JSON.stringify(errorResponse) + '\n'); + } + }); + + socket.on('error', (error) => + { + console.error('Socket error:', error); + }); + + socket.on('close', () => + { + console.log('Client disconnected from VSCode command executor'); + }); + }); + + this.server.on('error', (error) => + { + reject(error); + }); + + this.server.listen(this.pipeName, () => + { + console.log(`VSCode command executor listening on ${this.pipeName}`); + resolve(); + }); + }); + } + + /** + * Get the pipe name for external processes to connect + */ + public getPipeName(): string + { + return this.pipeName; + } + + /** + * Stop the command executor server + */ + public async stop(): Promise + { + return new Promise((resolve) => + { + if (this.server) + { + this.server.close(() => + { + console.log('VSCode command executor stopped'); + resolve(); + }); + } else + { + resolve(); + } + }); + } + + /** + * Execute a VSCode command with the provided context + */ + private async executeCommand(request: CommandRequest): Promise + { + try + { + console.log(`Executing command: ${request.command} with context:`, request.context); + + let result: any; + + switch (request.command) + { + case 'dotnet.acquireGlobalSDK': + result = await vscode.commands.executeCommand('dotnet.acquireGlobalSDK', request.context); + break; + + case 'dotnet.acquire': + result = await vscode.commands.executeCommand('dotnet.acquire', request.context); + break; + + case 'dotnet.uninstall': + result = await vscode.commands.executeCommand('dotnet.uninstall', request.context); + break; + + default: + throw new Error(`Unsupported command: ${request.command}`); + } + + return { + id: request.id, + success: true, + result + }; + + } catch (error) + { + console.error(`Error executing command ${request.command}:`, error); + + return { + id: request.id, + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + } +} + +// If this file is run directly (for testing purposes) +if (require.main === module) +{ + const executor = new VSCodeCommandExecutor(); + + executor.start().then(() => + { + console.log('VSCode command executor started successfully'); + + // Handle graceful shutdown + process.on('SIGINT', async () => + { + console.log('Shutting down VSCode command executor...'); + await executor.stop(); + process.exit(0); + }); + + process.on('SIGTERM', async () => + { + console.log('Shutting down VSCode command executor...'); + await executor.stop(); + process.exit(0); + }); + }).catch((error) => + { + console.error('Failed to start VSCode command executor:', error); + process.exit(1); + }); +} diff --git a/vscode-dotnet-runtime-extension/webpack.config.js b/vscode-dotnet-runtime-extension/webpack.config.js index 33547fe3df..59356403fd 100644 --- a/vscode-dotnet-runtime-extension/webpack.config.js +++ b/vscode-dotnet-runtime-extension/webpack.config.js @@ -4,68 +4,72 @@ const path = require('path'); const CopyPlugin = require('copy-webpack-plugin'); -const { exec } = require('node:child_process'); +const {exec} = require('node:child_process'); const PermissionsOutputPlugin = require('webpack-permissions-plugin'); /**@type {import('webpack').Configuration}*/ const config = { - target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ + target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ - entry: './/src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ - output: { - // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ - path: path.resolve(__dirname, 'dist'), - filename: 'extension.js', - libraryTarget: 'commonjs2', - devtoolModuleFilenameTemplate: '../[resource-path]' - }, - node: { - __dirname: false, - __filename: false, - }, - devtool: 'source-map', - externals: { - vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ - 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics' // ignored because we don't ship native module - }, - resolve: { - // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader - extensions: ['.ts', '.js'] - }, - module: { - rules: [ - { - test: /\.ts$/, - exclude: /node_modules/, - use: [ - { - loader: 'ts-loader' - } + entry: './/src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ + output: { + // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ + path: path.resolve(__dirname, 'dist'), + filename: 'extension.js', + libraryTarget: 'commonjs2', + devtoolModuleFilenameTemplate: '../[resource-path]' + }, + node: { + __dirname: false, + __filename: false, + }, + devtool: 'source-map', + externals: { + vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ + 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics' // ignored because we don't ship native module + }, + resolve: { + // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader + extensions: ['.ts', '.js'] + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + loader: 'ts-loader' + } + ] + } ] - } + }, + plugins: [ + new CopyPlugin({ + patterns: [ + {from: path.resolve(__dirname, '../vscode-dotnet-runtime-library/install scripts'), to: path.resolve(__dirname, 'dist', 'install scripts')}, + {from: path.resolve(__dirname, '../vscode-dotnet-runtime-library/distro-data'), to: path.resolve(__dirname, 'dist', 'distro-data')}, + {from: path.resolve(__dirname, '../images'), to: path.resolve(__dirname, 'images')}, + {from: path.resolve(__dirname, '../LICENSE.txt'), to: path.resolve(__dirname, 'LICENSE.txt')}, + {from: path.resolve(__dirname, 'src/dotnet-mcp-provider.js'), to: path.resolve(__dirname, 'dist', 'dotnet-mcp-provider.js')}, + {from: path.resolve(__dirname, 'src/mcp-package.json'), to: path.resolve(__dirname, 'dist', 'mcp-package.json')} + ] + }), + new PermissionsOutputPlugin({ + buildFolders: [ + ], + buildFiles: [ + { + path: path.resolve(__dirname, 'dist', 'distro-data', 'distro-support.json'), + fileMode: '544' + }, + { + path: path.resolve(__dirname, 'dist', 'install scripts', 'interprocess-communicator.sh'), + fileMode: '500' + } + ] + }) ] - }, - plugins: [ - new CopyPlugin({ patterns: [ - { from: path.resolve(__dirname, '../vscode-dotnet-runtime-library/install scripts'), to: path.resolve(__dirname, 'dist', 'install scripts') }, - { from: path.resolve(__dirname, '../vscode-dotnet-runtime-library/distro-data'), to: path.resolve(__dirname, 'dist', 'distro-data') }, - { from: path.resolve(__dirname, '../images'), to: path.resolve(__dirname, 'images') }, - { from: path.resolve(__dirname, '../LICENSE.txt'), to: path.resolve(__dirname, 'LICENSE.txt') } - ]}), - new PermissionsOutputPlugin({ - buildFolders: [ - ], - buildFiles: [ - { - path: path.resolve(__dirname, 'dist', 'distro-data', 'distro-support.json'), - fileMode: '544' - }, - { - path: path.resolve(__dirname, 'dist', 'install scripts', 'interprocess-communicator.sh'), - fileMode: '500' - } - ] - }) - ] }; -module.exports = config; \ No newline at end of file +module.exports = config; diff --git a/vscode-dotnet-runtime-extension/yarn.lock b/vscode-dotnet-runtime-extension/yarn.lock index 5a070dd2ce..e4dc31c26a 100644 --- a/vscode-dotnet-runtime-extension/yarn.lock +++ b/vscode-dotnet-runtime-extension/yarn.lock @@ -2053,6 +2053,11 @@ util-deprecate@~1.0.1: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@^9.0.0: + version "9.0.1" + resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/uuid/-/uuid-9.0.1.tgz" + integrity sha1-4YjUyIU8xyIiA5LEJM1jfzIpPzA= + "vscode-dotnet-runtime-library@file:../vscode-dotnet-runtime-library": version "1.0.0" resolved "file:../vscode-dotnet-runtime-library" diff --git a/vscode-dotnet-runtime-library/yarn.lock b/vscode-dotnet-runtime-library/yarn.lock index 4c60fc0b9b..b8ce67e3d2 100644 --- a/vscode-dotnet-runtime-library/yarn.lock +++ b/vscode-dotnet-runtime-library/yarn.lock @@ -637,11 +637,6 @@ fs.realpath@^1.0.0: resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^2.3.3: - version "2.3.3" - resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fsevents/-/fsevents-2.3.3.tgz" - integrity sha1-ysZAd4XQNnWipeGlMFxpezR9kNY= - function-bind@^1.1.2: version "1.1.2" resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/function-bind/-/function-bind-1.1.2.tgz"