imcp 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { NpmInstaller } from './NpmInstaller.js';
2
2
  import { PipInstaller } from './PipInstaller.js';
3
+ import { NugetInstaller } from './NugetInstaller.js';
3
4
  import { CommandInstaller } from './CommandInstaller.js';
4
5
  import { GeneralInstaller } from './GeneralInstaller.js';
5
6
  import { exec } from 'child_process';
@@ -25,6 +26,7 @@ export class InstallerFactory {
25
26
  registerDefaultInstallers() {
26
27
  this.registerInstaller(new NpmInstaller(this.execPromise));
27
28
  this.registerInstaller(new PipInstaller(this.execPromise));
29
+ this.registerInstaller(new NugetInstaller(this.execPromise));
28
30
  this.registerInstaller(new CommandInstaller(this.execPromise));
29
31
  this.registerInstaller(new GeneralInstaller(this.execPromise));
30
32
  }
@@ -0,0 +1,37 @@
1
+ import { RequirementConfig, RequirementStatus, ServerInstallOptions } from '../../metadatas/types.js';
2
+ import { BaseInstaller } from './BaseInstaller.js';
3
+ import { InstallOperationManager } from '../../loaders/InstallOperationManager.js';
4
+ /**
5
+ * Installer implementation for .NET packages using NuGet
6
+ */
7
+ export declare class NugetInstaller extends BaseInstaller {
8
+ /**
9
+ * Check if this installer can handle the given requirement type
10
+ * @param requirement The requirement to check
11
+ * @returns True if this installer can handle the requirement
12
+ */
13
+ canHandle(requirement: RequirementConfig): boolean;
14
+ supportCheckUpdates(): boolean;
15
+ /**
16
+ * Get the latest version available for the NuGet package.
17
+ * @param requirement The requirement to check.
18
+ * @param _options Optional server install options (not used for NuGet).
19
+ * @returns The latest version string, or undefined if not found or not applicable.
20
+ */
21
+ getLatestVersion(requirement: RequirementConfig, _options?: ServerInstallOptions): Promise<string | undefined>;
22
+ /**
23
+ * Check if the .NET tool is already installed
24
+ * @param requirement The requirement to check
25
+ * @param _options Optional server install options (not used for NuGet)
26
+ * @returns The status of the requirement
27
+ */
28
+ checkInstallation(requirement: RequirementConfig, _options?: ServerInstallOptions): Promise<RequirementStatus>;
29
+ /**
30
+ * Install the .NET tool
31
+ * @param requirement The requirement to install
32
+ * @param recorder Optional InstallOperationManager for recording steps
33
+ * @param _options Optional server install options (not used for NuGet)
34
+ * @returns The status of the installation
35
+ */
36
+ install(requirement: RequirementConfig, recorder: InstallOperationManager, _options?: ServerInstallOptions): Promise<RequirementStatus>;
37
+ }
@@ -0,0 +1,189 @@
1
+ import { BaseInstaller } from './BaseInstaller.js';
2
+ import { handleGitHubRelease, getGitHubLatestVersion } from '../../../utils/githubUtils.js';
3
+ import { compareVersions } from '../../../utils/versionUtils.js';
4
+ import { Logger } from '../../../utils/logger.js';
5
+ import * as RecordingConstants from '../../metadatas/recordingConstants.js';
6
+ import { ensureDotnetToolsInPath } from '../../../utils/osUtils.js';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ /**
10
+ * Installer implementation for .NET packages using NuGet
11
+ */
12
+ export class NugetInstaller extends BaseInstaller {
13
+ /**
14
+ * Check if this installer can handle the given requirement type
15
+ * @param requirement The requirement to check
16
+ * @returns True if this installer can handle the requirement
17
+ */
18
+ canHandle(requirement) {
19
+ return requirement.type === 'nuget';
20
+ }
21
+ supportCheckUpdates() {
22
+ return true;
23
+ }
24
+ /**
25
+ * Get the latest version available for the NuGet package.
26
+ * @param requirement The requirement to check.
27
+ * @param _options Optional server install options (not used for NuGet).
28
+ * @returns The latest version string, or undefined if not found or not applicable.
29
+ */
30
+ async getLatestVersion(requirement, _options) {
31
+ if (requirement.registry && requirement.registry.githubRelease) {
32
+ return getGitHubLatestVersion(this.execPromise, requirement.registry.githubRelease.repository);
33
+ }
34
+ // Artifacts registry is not supported for nuget tools
35
+ if (requirement.registry && requirement.registry.artifacts) {
36
+ Logger.warn(`Artifacts registry is not supported for NuGet tool '${requirement.name}'.`);
37
+ return undefined;
38
+ }
39
+ // Default behavior: Nuget tools are often specific versions from specific sources,
40
+ // or global tools might not have a central "latest version" query like pip/npm.
41
+ // Returning current version if specified, otherwise undefined.
42
+ Logger.warn(`Direct latest version check for NuGet tool '${requirement.name}' without a GitHub release registry is not supported. Please specify a version or use a GitHub release.`);
43
+ return requirement.version || undefined;
44
+ }
45
+ /**
46
+ * Check if the .NET tool is already installed
47
+ * @param requirement The requirement to check
48
+ * @param _options Optional server install options (not used for NuGet)
49
+ * @returns The status of the requirement
50
+ */
51
+ async checkInstallation(requirement, _options) {
52
+ try {
53
+ // Command: dotnet tool list -g
54
+ // Output:
55
+ // Package Id Version Commands
56
+ // -----------------------------------------
57
+ // jarvistools 1.0.0 jarvistools
58
+ const { stdout } = await this.execPromise(`dotnet tool list -g`);
59
+ const lines = stdout.split('\n');
60
+ let installedVersion;
61
+ let isInstalled = false;
62
+ for (const line of lines) {
63
+ const parts = line.trim().split(/\s+/);
64
+ if (parts.length >= 2 && parts[0].toLowerCase() === requirement.name.toLowerCase()) {
65
+ installedVersion = parts[1];
66
+ isInstalled = true;
67
+ break;
68
+ }
69
+ }
70
+ return {
71
+ name: requirement.name,
72
+ type: 'nuget',
73
+ installed: isInstalled,
74
+ version: installedVersion,
75
+ inProgress: false,
76
+ };
77
+ }
78
+ catch (error) {
79
+ // If 'dotnet tool list -g' fails, it might mean dotnet CLI is not properly installed or configured.
80
+ // Or it could mean no tools are installed, which in some dotnet versions might return non-zero exit code.
81
+ // We'll assume not installed in case of error, but log it.
82
+ Logger.debug(`Error checking NuGet tool installation for ${requirement.name}: ${error instanceof Error ? error.message : String(error)}`);
83
+ return {
84
+ name: requirement.name,
85
+ type: 'nuget',
86
+ installed: false,
87
+ error: `Failed to check installation: ${error instanceof Error ? error.message : String(error)}`,
88
+ inProgress: false,
89
+ };
90
+ }
91
+ }
92
+ /**
93
+ * Install the .NET tool
94
+ * @param requirement The requirement to install
95
+ * @param recorder Optional InstallOperationManager for recording steps
96
+ * @param _options Optional server install options (not used for NuGet)
97
+ * @returns The status of the installation
98
+ */
99
+ async install(requirement, recorder, _options) {
100
+ return await recorder.recording(async () => {
101
+ const status = await this.checkInstallation(requirement, _options);
102
+ if (status.installed && status.version && requirement.version &&
103
+ compareVersions(status.version, requirement.version) === 0 &&
104
+ !requirement.version.toLowerCase().includes('latest')) {
105
+ Logger.log(`NuGet tool ${requirement.name}==${status.version} already installed.`);
106
+ return status;
107
+ }
108
+ let command;
109
+ if (requirement.registry && requirement.registry.githubRelease) {
110
+ const result = await handleGitHubRelease(requirement, requirement.registry.githubRelease);
111
+ // Nuget package name might be different from the requirement name if alias is used.
112
+ // However, dotnet tool install uses the package ID from the nupkg.
113
+ // We assume requirement.name is the package ID.
114
+ const packageId = requirement.name;
115
+ const resolvedDir = fs.existsSync(result.resolvedPath) && fs.lstatSync(result.resolvedPath).isDirectory() ? result.resolvedPath : path.dirname(result.resolvedPath);
116
+ if (requirement.version && !requirement.version.toLowerCase().includes('latest')) {
117
+ command = `dotnet tool install --global --add-source "${resolvedDir}" ${packageId} --version ${requirement.version}`;
118
+ }
119
+ else {
120
+ // Install latest from the source
121
+ command = `dotnet tool install --global --add-source "${resolvedDir}" ${packageId}`;
122
+ }
123
+ }
124
+ else if (requirement.registry && requirement.registry.artifacts) {
125
+ const errorMessage = `Artifacts registry is not supported for NuGet tool yet'${requirement.name}'. Only GitHubRelease is supported.`;
126
+ Logger.error(errorMessage);
127
+ await recorder.recordStep('NugetInstaller:RegistryConfig', 'failed', errorMessage);
128
+ throw new Error(errorMessage);
129
+ }
130
+ else {
131
+ // Default installation from nuget.org or configured feeds
132
+ if (requirement.version && !requirement.version.toLowerCase().includes('latest')) {
133
+ command = `dotnet tool install --global ${requirement.name} --version ${requirement.version}`;
134
+ }
135
+ else {
136
+ command = `dotnet tool install --global ${requirement.name}`;
137
+ }
138
+ }
139
+ return await recorder.recording(async () => {
140
+ const { stdout, stderr } = await this.execPromise(command);
141
+ if (stderr && !stdout.toLowerCase().includes('already installed')) { // Some warnings might go to stderr
142
+ Logger.debug(`NuGet tool installation stderr for ${requirement.name}: ${stderr}`);
143
+ // Check if it was actually an error or just a warning
144
+ const checkStatus = await this.checkInstallation(requirement, _options);
145
+ if (!checkStatus.installed) {
146
+ Logger.error(`NuGet tool ${requirement.name} not found after install command, stderr: ${stderr}`);
147
+ throw new Error(`NuGet tool installation failed with: ${stderr}`);
148
+ }
149
+ }
150
+ const finalStatus = await this.checkInstallation(requirement, _options);
151
+ if (!finalStatus.installed) {
152
+ throw new Error(`NuGet tool ${requirement.name} failed to install. Please check logs.`);
153
+ }
154
+ // After successful installation, ensure .NET tools path is in system PATH
155
+ await ensureDotnetToolsInPath();
156
+ return {
157
+ name: requirement.name,
158
+ type: 'nuget',
159
+ installed: true,
160
+ version: finalStatus.version || requirement.version, // Use checked version if available
161
+ inProgress: false,
162
+ };
163
+ }, {
164
+ stepName: `${RecordingConstants.STEP_INSTALL_COMMAND_PREFIX}: ${requirement.name} : ${requirement.version || 'latest'}`,
165
+ inProgressMessage: `Running: ${command}`,
166
+ endMessage: (result) => result.installed ? `Succeeded: ${command}` : `Failed: ${command}`,
167
+ });
168
+ }, {
169
+ stepName: RecordingConstants.STEP_NUGET_INSTALLER_INSTALL,
170
+ inProgressMessage: `Installing NuGet tool: ${requirement.name}`,
171
+ endMessage: (result) => result.installed
172
+ ? `Install completed for ${requirement.name} with version ${result.version}`
173
+ : `Install failed for ${requirement.name}`,
174
+ onError: (error) => {
175
+ return {
176
+ result: {
177
+ name: requirement.name,
178
+ type: 'nuget',
179
+ installed: false,
180
+ error: error instanceof Error ? error.message : String(error),
181
+ inProgress: false,
182
+ },
183
+ message: error instanceof Error ? error.message : String(error),
184
+ };
185
+ },
186
+ });
187
+ }
188
+ }
189
+ //# sourceMappingURL=NugetInstaller.js.map
@@ -12,6 +12,8 @@ export declare const STEP_PROCESS_REQUIREMENT_UPDATES = "Processing all requirem
12
12
  export declare const STEP_CHECKING_REQUIREMENT_STATUS = "Checking the status of requirement";
13
13
  /** Step for installing requirements in the background process. */
14
14
  export declare const STEP_INSTALLING_REQUIREMENTS_IN_BACKGROUND = "Installing requirements in the background";
15
+ /** Step for running the install logic in the NugetInstaller. */
16
+ export declare const STEP_NUGET_INSTALLER_INSTALL = "Running install in NugetInstaller";
15
17
  /** Step for checking and installing all requirements as needed. */
16
18
  export declare const STEP_CHECK_AND_INSTALL_REQUIREMENTS = "Checking and installing all requirements";
17
19
  /** Step for running the install logic in the CommandInstaller. */
@@ -12,6 +12,8 @@ export const STEP_PROCESS_REQUIREMENT_UPDATES = 'Processing all requirement upda
12
12
  export const STEP_CHECKING_REQUIREMENT_STATUS = 'Checking the status of requirement';
13
13
  /** Step for installing requirements in the background process. */
14
14
  export const STEP_INSTALLING_REQUIREMENTS_IN_BACKGROUND = 'Installing requirements in the background';
15
+ /** Step for running the install logic in the NugetInstaller. */
16
+ export const STEP_NUGET_INSTALLER_INSTALL = 'Running install in NugetInstaller';
15
17
  /** Step for checking and installing all requirements as needed. */
16
18
  export const STEP_CHECK_AND_INSTALL_REQUIREMENTS = 'Checking and installing all requirements';
17
19
  /** Step for running the install logic in the CommandInstaller. */
@@ -121,7 +121,7 @@ export interface RegistryConfig {
121
121
  }
122
122
  export interface RequirementConfig {
123
123
  name: string;
124
- type: 'npm' | 'pip' | 'command' | 'extension' | 'other';
124
+ type: 'npm' | 'pip' | 'command' | 'extension' | 'nuget' | 'other';
125
125
  alias?: string;
126
126
  version: string;
127
127
  registry?: RegistryConfig;
@@ -91,28 +91,7 @@ export class FeedOnboardService {
91
91
  * @returns A promise that resolves to the operation status.
92
92
  */
93
93
  async _initiateOperation(config, operationType, serverList, forExistingCategory) {
94
- // First, check for existing non-completed operations
95
- let existingOperation = await onboardStatusManager._findExistingNonCompletedOperation(config.name, operationType);
96
- if (existingOperation) {
97
- const fiveMinutesInMs = 5 * 60 * 1000;
98
- const lastUpdateTimestamp = existingOperation.lastUpdated ? new Date(existingOperation.lastUpdated).getTime() : 0;
99
- const currentTime = new Date().getTime();
100
- if (lastUpdateTimestamp > 0 && (currentTime - lastUpdateTimestamp) > fiveMinutesInMs) {
101
- Logger.log(`WARNING: [${existingOperation.onboardingId}] Found stale ${operationType} operation for feed: ${config.name} (last updated at: ${existingOperation.lastUpdated}). Proceeding to create a new operation.`);
102
- existingOperation = undefined; // Treat as no existing operation for starting a new one
103
- }
104
- else {
105
- Logger.log(`[${existingOperation.onboardingId}] Found existing non-completed ${operationType} operation for feed: ${config.name}. Returning its status.`);
106
- const lastStep = existingOperation.steps && existingOperation.steps.length > 0 ? existingOperation.steps[existingOperation.steps.length - 1].stepName : 'N/A';
107
- return {
108
- onboardingId: existingOperation.onboardingId,
109
- status: existingOperation.status,
110
- message: `An ${operationType} process for this feed (${existingOperation.onboardingId}) is already in status: ${existingOperation.status}. Last step: ${lastStep}`,
111
- lastQueried: new Date().toISOString(),
112
- };
113
- }
114
- }
115
- // Then, check for successful operations with matching configuration
94
+ // Check for successful operations with matching configuration
116
95
  const succeededOperation = await onboardStatusManager.findSucceededOperation(config.name, operationType, config);
117
96
  if (succeededOperation) {
118
97
  Logger.log(`[${succeededOperation.onboardingId}] Found existing successful ${operationType} operation for feed: ${config.name} with matching configuration.`);
@@ -235,11 +235,6 @@ export class StdioServerValidator {
235
235
  const fullCommand = server.installation.command;
236
236
  const [baseCommand, ...defaultArgs] = fullCommand.split(' ');
237
237
  const args = [...defaultArgs, ...(server.installation.args || [])];
238
- // Validate command exists and is executable
239
- const isExecutable = await this.isCommandExecutable(baseCommand);
240
- if (!isExecutable) {
241
- throw new Error(`Command not found or not executable: ${baseCommand}`);
242
- }
243
238
  // Validate required environment variables if specified
244
239
  const envVars = server.installation.env;
245
240
  if (envVars) {
@@ -264,6 +259,11 @@ export class StdioServerValidator {
264
259
  }
265
260
  }
266
261
  }
262
+ // Validate command exists and is executable
263
+ const isExecutable = await this.isCommandExecutable(baseCommand);
264
+ if (!isExecutable) {
265
+ throw new Error(`Command not found or not executable: ${baseCommand}`);
266
+ }
267
267
  // Test server startup
268
268
  const serverStarted = await this.testServerStartup(baseCommand, args, config.requirements?.find(r => r.type === 'npm'));
269
269
  if (!serverStarted) {
@@ -36,3 +36,14 @@ export declare function getGlobalNPMPackagePath(): string;
36
36
  * Returns the directory containing the npm executable, or a platform-appropriate default if not found.
37
37
  */
38
38
  export declare function getNpmExecutablePath(): Promise<string>;
39
+ /**
40
+ * Ensures the .NET global tools path is added to the system PATH for the current user
41
+ * on macOS and Linux by modifying shell configuration files.
42
+ * This function is idempotent and will not add the path if it already exists.
43
+ */
44
+ export declare function ensureDotnetToolsInPath(): Promise<void>;
45
+ /**
46
+ *
47
+ * @returns The path to the .dotnet global tools directory.
48
+ */
49
+ export declare function getDotnetGlobalToolsPath(): string;
@@ -562,4 +562,104 @@ export async function getNpmExecutablePath() {
562
562
  }
563
563
  }
564
564
  }
565
+ /**
566
+ * Ensures the .NET global tools path is added to the system PATH for the current user
567
+ * on macOS and Linux by modifying shell configuration files.
568
+ * This function is idempotent and will not add the path if it already exists.
569
+ */
570
+ export async function ensureDotnetToolsInPath() {
571
+ const osType = getOSType();
572
+ Logger.debug({
573
+ action: 'ensure_dotnet_tools_in_path',
574
+ osType
575
+ });
576
+ if (osType === OSType.Windows) {
577
+ Logger.debug('.NET tools PATH is usually handled by the installer on Windows. Skipping explicit PATH modification.');
578
+ return;
579
+ }
580
+ if (osType === OSType.MacOS || osType === OSType.Linux) {
581
+ const dotnetToolsPath = getDotnetGlobalToolsPath(); // This is typically ~/.dotnet/tools
582
+ const exportLine = `export PATH="${dotnetToolsPath}:$PATH"`;
583
+ // Determine shell configuration files to check/update
584
+ const shellConfigFiles = [];
585
+ const shell = process.env.SHELL;
586
+ if (shell && shell.includes('zsh')) {
587
+ shellConfigFiles.push(path.join(os.homedir(), '.zshrc'));
588
+ }
589
+ else if (shell && shell.includes('bash')) {
590
+ shellConfigFiles.push(path.join(os.homedir(), '.bashrc'));
591
+ // .bash_profile is often sourced by .bashrc or used for login shells
592
+ shellConfigFiles.push(path.join(os.homedir(), '.bash_profile'));
593
+ }
594
+ else {
595
+ // Fallback for other shells or if SHELL is not set
596
+ shellConfigFiles.push(path.join(os.homedir(), '.profile'));
597
+ shellConfigFiles.push(path.join(os.homedir(), '.bashrc'));
598
+ shellConfigFiles.push(path.join(os.homedir(), '.zshrc'));
599
+ }
600
+ let updatedAnyFile = false;
601
+ for (const configFile of shellConfigFiles) {
602
+ try {
603
+ if (fs.existsSync(configFile)) {
604
+ const content = await fs.promises.readFile(configFile, 'utf-8');
605
+ // Check if the exact line or a line setting dotnetToolsPath in PATH exists
606
+ // A more robust check might involve parsing, but this covers common cases.
607
+ // Regex to check if dotnetToolsPath is part of an PATH export, avoiding duplicates.
608
+ // It looks for `export PATH=...$dotnetToolsPath...` or `export PATH=...${HOME}/.dotnet/tools...`
609
+ const pathRegex = new RegExp(`export\\s+PATH=.*${dotnetToolsPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*`);
610
+ const homePathRegex = new RegExp(`export\\s+PATH=.*\\$HOME\\/\\.dotnet\\/tools.*`);
611
+ if (!pathRegex.test(content) && !homePathRegex.test(content)) {
612
+ Logger.debug(`Adding .NET tools path to ${configFile}`);
613
+ await fs.promises.appendFile(configFile, `\n# Add .NET Core SDK tools to PATH\n${exportLine}\n`);
614
+ Logger.log(`Appended '${exportLine}' to ${configFile}. You may need to source this file or restart your terminal.`);
615
+ updatedAnyFile = true;
616
+ }
617
+ else {
618
+ Logger.debug(`.NET tools path already configured in ${configFile}`);
619
+ }
620
+ }
621
+ else if (shellConfigFiles.length === 1 || configFile === path.join(os.homedir(), '.profile')) {
622
+ // If it's the only config file determined or it's .profile, and it doesn't exist, create it.
623
+ Logger.debug(`${configFile} does not exist. Creating and adding .NET tools path.`);
624
+ await fs.promises.writeFile(configFile, `# Add .NET Core SDK tools to PATH\n${exportLine}\n`);
625
+ Logger.log(`Created ${configFile} and added '${exportLine}'. You may need to source this file or restart your terminal.`);
626
+ updatedAnyFile = true;
627
+ }
628
+ }
629
+ catch (error) {
630
+ Logger.error(`Failed to update ${configFile} for .NET tools PATH: ${error instanceof Error ? error.message : String(error)}`);
631
+ }
632
+ }
633
+ if (updatedAnyFile) {
634
+ Logger.log(`Dotnet tools path has been added to shell configuration. Please source the relevant file (e.g., 'source ~/.zshrc') or restart your terminal session for changes to take effect.`);
635
+ }
636
+ // Also update the current process's PATH environment variable
637
+ if (process.env.PATH && !process.env.PATH.includes(dotnetToolsPath)) {
638
+ process.env.PATH = `${dotnetToolsPath}:${process.env.PATH}`;
639
+ Logger.debug(`Updated current process.env.PATH to include: ${dotnetToolsPath}`);
640
+ }
641
+ else if (!process.env.PATH) {
642
+ process.env.PATH = dotnetToolsPath;
643
+ Logger.debug(`Set current process.env.PATH to: ${dotnetToolsPath}`);
644
+ }
645
+ }
646
+ }
647
+ /**
648
+ *
649
+ * @returns The path to the .dotnet global tools directory.
650
+ */
651
+ export function getDotnetGlobalToolsPath() {
652
+ const homeDir = os.homedir();
653
+ if (process.platform === 'win32') {
654
+ // Example: C:\Users\YourName\.dotnet\tools
655
+ return path.join(homeDir, '.dotnet', 'tools');
656
+ }
657
+ else if (process.platform === 'darwin' || process.platform === 'linux') {
658
+ // macOS or Linux: ~/.dotnet/tools
659
+ return path.join(homeDir, '.dotnet', 'tools');
660
+ }
661
+ else {
662
+ throw new Error(`Unsupported platform: ${process.platform}`);
663
+ }
664
+ }
565
665
  //# sourceMappingURL=osUtils.js.map
@@ -235,6 +235,7 @@ export const serverRequirementTemplate = (serverIndex, reqIndex, isReadOnly = fa
235
235
  <option value="">Select Type</option>
236
236
  <option value="npm">NPM Package</option>
237
237
  <option value="pip">PIP Package</option>
238
+ <option value="nuget">Nuget Package</option>
238
239
  <option value="command">Command</option>
239
240
  </select>
240
241
  </div>
@@ -158,16 +158,16 @@ function renderServerCategoryList(servers) {
158
158
 
159
159
  return `
160
160
  <div class="server-item border border-gray-200 p-3 rounded hover:bg-gray-50 cursor-pointer transition duration-150 ease-in-out ${pinnedClass}"
161
- data-server-name="${server.name}">
161
+ data-server-name="${server.name}" onclick="navigateToCategory('${server.name}')">
162
162
  <div class="flex justify-between items-center">
163
- <h3 class="font-semibold text-gray-800" onclick="navigateToCategory('${server.name}')">${server.displayName || server.name}</h3>
163
+ <h3 class="font-semibold text-gray-800">${server.displayName || server.name}</h3>
164
164
  <div class="flex items-center">
165
165
  <div class="pin-button ${pinnedClass}" onclick="togglePinCategoryItem('${server.name}', event)" title="${isPinned ? 'Unpin' : 'Pin'} this category">
166
166
  ${pinIcon}
167
167
  </div>
168
168
  </div>
169
169
  </div>
170
- <div class="text-sm text-gray-500 flex items-center mt-1" onclick="navigateToCategory('${server.name}')">
170
+ <div class="text-sm text-gray-500 flex items-center mt-1">
171
171
  ${statusHtml}
172
172
  ${systemTagsHtml}
173
173
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Node.js SDK for Model Context Protocol (MCP)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -2,6 +2,7 @@ import { RequirementConfig, RequirementStatus, ServerInstallOptions } from '../.
2
2
  import { RequirementInstaller } from './RequirementInstaller.js';
3
3
  import { NpmInstaller } from './NpmInstaller.js';
4
4
  import { PipInstaller } from './PipInstaller.js';
5
+ import { NugetInstaller } from './NugetInstaller.js';
5
6
  import { CommandInstaller } from './CommandInstaller.js';
6
7
  import { GeneralInstaller } from './GeneralInstaller.js';
7
8
  import { exec } from 'child_process';
@@ -30,6 +31,7 @@ export class InstallerFactory {
30
31
  private registerDefaultInstallers(): void {
31
32
  this.registerInstaller(new NpmInstaller(this.execPromise));
32
33
  this.registerInstaller(new PipInstaller(this.execPromise));
34
+ this.registerInstaller(new NugetInstaller(this.execPromise));
33
35
  this.registerInstaller(new CommandInstaller(this.execPromise));
34
36
  this.registerInstaller(new GeneralInstaller(this.execPromise));
35
37
  }
@@ -0,0 +1,203 @@
1
+ import { RequirementConfig, RequirementStatus, ServerInstallOptions } from '../../metadatas/types.js';
2
+ import { BaseInstaller } from './BaseInstaller.js';
3
+ import { handleGitHubRelease, getGitHubLatestVersion } from '../../../utils/githubUtils.js';
4
+ import { compareVersions } from '../../../utils/versionUtils.js';
5
+ import { Logger } from '../../../utils/logger.js';
6
+ import { InstallOperationManager } from '../../loaders/InstallOperationManager.js';
7
+ import * as RecordingConstants from '../../metadatas/recordingConstants.js';
8
+ import { ensureDotnetToolsInPath } from '../../../utils/osUtils.js';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+
12
+ /**
13
+ * Installer implementation for .NET packages using NuGet
14
+ */
15
+ export class NugetInstaller extends BaseInstaller {
16
+ /**
17
+ * Check if this installer can handle the given requirement type
18
+ * @param requirement The requirement to check
19
+ * @returns True if this installer can handle the requirement
20
+ */
21
+ canHandle(requirement: RequirementConfig): boolean {
22
+ return requirement.type === 'nuget';
23
+ }
24
+
25
+ supportCheckUpdates(): boolean {
26
+ return true;
27
+ }
28
+
29
+ /**
30
+ * Get the latest version available for the NuGet package.
31
+ * @param requirement The requirement to check.
32
+ * @param _options Optional server install options (not used for NuGet).
33
+ * @returns The latest version string, or undefined if not found or not applicable.
34
+ */
35
+ async getLatestVersion(requirement: RequirementConfig, _options?: ServerInstallOptions): Promise<string | undefined> {
36
+ if (requirement.registry && requirement.registry.githubRelease) {
37
+ return getGitHubLatestVersion(this.execPromise, requirement.registry.githubRelease.repository);
38
+ }
39
+ // Artifacts registry is not supported for nuget tools
40
+ if (requirement.registry && requirement.registry.artifacts) {
41
+ Logger.warn(`Artifacts registry is not supported for NuGet tool '${requirement.name}'.`);
42
+ return undefined;
43
+ }
44
+ // Default behavior: Nuget tools are often specific versions from specific sources,
45
+ // or global tools might not have a central "latest version" query like pip/npm.
46
+ // Returning current version if specified, otherwise undefined.
47
+ Logger.warn(`Direct latest version check for NuGet tool '${requirement.name}' without a GitHub release registry is not supported. Please specify a version or use a GitHub release.`);
48
+ return requirement.version || undefined;
49
+ }
50
+
51
+ /**
52
+ * Check if the .NET tool is already installed
53
+ * @param requirement The requirement to check
54
+ * @param _options Optional server install options (not used for NuGet)
55
+ * @returns The status of the requirement
56
+ */
57
+ async checkInstallation(requirement: RequirementConfig, _options?: ServerInstallOptions): Promise<RequirementStatus> {
58
+ try {
59
+ // Command: dotnet tool list -g
60
+ // Output:
61
+ // Package Id Version Commands
62
+ // -----------------------------------------
63
+ // jarvistools 1.0.0 jarvistools
64
+ const { stdout } = await this.execPromise(`dotnet tool list -g`);
65
+ const lines = stdout.split('\n');
66
+ let installedVersion: string | undefined;
67
+ let isInstalled = false;
68
+
69
+ for (const line of lines) {
70
+ const parts = line.trim().split(/\s+/);
71
+ if (parts.length >= 2 && parts[0].toLowerCase() === requirement.name.toLowerCase()) {
72
+ installedVersion = parts[1];
73
+ isInstalled = true;
74
+ break;
75
+ }
76
+ }
77
+
78
+ return {
79
+ name: requirement.name,
80
+ type: 'nuget',
81
+ installed: isInstalled,
82
+ version: installedVersion,
83
+ inProgress: false,
84
+ };
85
+ } catch (error) {
86
+ // If 'dotnet tool list -g' fails, it might mean dotnet CLI is not properly installed or configured.
87
+ // Or it could mean no tools are installed, which in some dotnet versions might return non-zero exit code.
88
+ // We'll assume not installed in case of error, but log it.
89
+ Logger.debug(`Error checking NuGet tool installation for ${requirement.name}: ${error instanceof Error ? error.message : String(error)}`);
90
+ return {
91
+ name: requirement.name,
92
+ type: 'nuget',
93
+ installed: false,
94
+ error: `Failed to check installation: ${error instanceof Error ? error.message : String(error)}`,
95
+ inProgress: false,
96
+ };
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Install the .NET tool
102
+ * @param requirement The requirement to install
103
+ * @param recorder Optional InstallOperationManager for recording steps
104
+ * @param _options Optional server install options (not used for NuGet)
105
+ * @returns The status of the installation
106
+ */
107
+ async install(requirement: RequirementConfig, recorder: InstallOperationManager, _options?: ServerInstallOptions): Promise<RequirementStatus> {
108
+ return await recorder.recording(
109
+ async (): Promise<RequirementStatus> => {
110
+ const status = await this.checkInstallation(requirement, _options);
111
+ if (status.installed && status.version && requirement.version &&
112
+ compareVersions(status.version, requirement.version) === 0 &&
113
+ !requirement.version.toLowerCase().includes('latest')) {
114
+ Logger.log(`NuGet tool ${requirement.name}==${status.version} already installed.`);
115
+ return status;
116
+ }
117
+
118
+ let command: string;
119
+
120
+ if (requirement.registry && requirement.registry.githubRelease) {
121
+ const result = await handleGitHubRelease(requirement, requirement.registry.githubRelease);
122
+ // Nuget package name might be different from the requirement name if alias is used.
123
+ // However, dotnet tool install uses the package ID from the nupkg.
124
+ // We assume requirement.name is the package ID.
125
+ const packageId = requirement.name;
126
+ const resolvedDir = fs.existsSync(result.resolvedPath) && fs.lstatSync(result.resolvedPath).isDirectory() ? result.resolvedPath : path.dirname(result.resolvedPath);
127
+
128
+ if (requirement.version && !requirement.version.toLowerCase().includes('latest')) {
129
+ command = `dotnet tool install --global --add-source "${resolvedDir}" ${packageId} --version ${requirement.version}`;
130
+ } else {
131
+ // Install latest from the source
132
+ command = `dotnet tool install --global --add-source "${resolvedDir}" ${packageId}`;
133
+ }
134
+ } else if (requirement.registry && requirement.registry.artifacts) {
135
+ const errorMessage = `Artifacts registry is not supported for NuGet tool yet'${requirement.name}'. Only GitHubRelease is supported.`;
136
+ Logger.error(errorMessage);
137
+ await recorder.recordStep('NugetInstaller:RegistryConfig', 'failed', errorMessage);
138
+ throw new Error(errorMessage);
139
+ } else {
140
+ // Default installation from nuget.org or configured feeds
141
+ if (requirement.version && !requirement.version.toLowerCase().includes('latest')) {
142
+ command = `dotnet tool install --global ${requirement.name} --version ${requirement.version}`;
143
+ } else {
144
+ command = `dotnet tool install --global ${requirement.name}`;
145
+ }
146
+ }
147
+
148
+ return await recorder.recording(
149
+ async () => {
150
+ const { stdout, stderr } = await this.execPromise(command);
151
+ if (stderr && !stdout.toLowerCase().includes('already installed')) { // Some warnings might go to stderr
152
+ Logger.debug(`NuGet tool installation stderr for ${requirement.name}: ${stderr}`);
153
+ // Check if it was actually an error or just a warning
154
+ const checkStatus = await this.checkInstallation(requirement, _options);
155
+ if (!checkStatus.installed) {
156
+ Logger.error(`NuGet tool ${requirement.name} not found after install command, stderr: ${stderr}`);
157
+ throw new Error(`NuGet tool installation failed with: ${stderr}`);
158
+ }
159
+ }
160
+
161
+ const finalStatus = await this.checkInstallation(requirement, _options);
162
+ if (!finalStatus.installed) {
163
+ throw new Error(`NuGet tool ${requirement.name} failed to install. Please check logs.`);
164
+ }
165
+ // After successful installation, ensure .NET tools path is in system PATH
166
+ await ensureDotnetToolsInPath();
167
+ return {
168
+ name: requirement.name,
169
+ type: 'nuget',
170
+ installed: true,
171
+ version: finalStatus.version || requirement.version, // Use checked version if available
172
+ inProgress: false,
173
+ };
174
+ },
175
+ {
176
+ stepName: `${RecordingConstants.STEP_INSTALL_COMMAND_PREFIX}: ${requirement.name} : ${requirement.version || 'latest'}`,
177
+ inProgressMessage: `Running: ${command}`,
178
+ endMessage: (result) => result.installed ? `Succeeded: ${command}` : `Failed: ${command}`,
179
+ }
180
+ );
181
+ },
182
+ {
183
+ stepName: RecordingConstants.STEP_NUGET_INSTALLER_INSTALL,
184
+ inProgressMessage: `Installing NuGet tool: ${requirement.name}`,
185
+ endMessage: (result) => result.installed
186
+ ? `Install completed for ${requirement.name} with version ${result.version}`
187
+ : `Install failed for ${requirement.name}`,
188
+ onError: (error) => {
189
+ return {
190
+ result: {
191
+ name: requirement.name,
192
+ type: 'nuget',
193
+ installed: false,
194
+ error: error instanceof Error ? error.message : String(error),
195
+ inProgress: false,
196
+ },
197
+ message: error instanceof Error ? error.message : String(error),
198
+ };
199
+ },
200
+ }
201
+ );
202
+ }
203
+ }
@@ -16,6 +16,9 @@ export const STEP_CHECKING_REQUIREMENT_STATUS = 'Checking the status of requirem
16
16
  /** Step for installing requirements in the background process. */
17
17
  export const STEP_INSTALLING_REQUIREMENTS_IN_BACKGROUND = 'Installing requirements in the background';
18
18
 
19
+ /** Step for running the install logic in the NugetInstaller. */
20
+ export const STEP_NUGET_INSTALLER_INSTALL = 'Running install in NugetInstaller';
21
+
19
22
  /** Step for checking and installing all requirements as needed. */
20
23
  export const STEP_CHECK_AND_INSTALL_REQUIREMENTS = 'Checking and installing all requirements';
21
24
 
@@ -139,7 +139,7 @@ export interface RegistryConfig {
139
139
 
140
140
  export interface RequirementConfig {
141
141
  name: string;
142
- type: 'npm' | 'pip' | 'command' | 'extension' | 'other'; // Add other requirement types if needed
142
+ type: 'npm' | 'pip' | 'command' | 'extension' | 'nuget' | 'other'; // Add other requirement types if needed
143
143
  alias?: string; // Alias for the command type
144
144
  version: string;
145
145
  registry?: RegistryConfig;
@@ -106,30 +106,7 @@ export class FeedOnboardService {
106
106
  * @returns A promise that resolves to the operation status.
107
107
  */
108
108
  private async _initiateOperation(config: FeedConfiguration, operationType: OperationType, serverList: string[], forExistingCategory?: boolean): Promise<OperationStatus & { feedConfiguration?: FeedConfiguration }> {
109
- // First, check for existing non-completed operations
110
- let existingOperation = await onboardStatusManager._findExistingNonCompletedOperation(config.name, operationType);
111
-
112
- if (existingOperation) {
113
- const fiveMinutesInMs = 5 * 60 * 1000;
114
- const lastUpdateTimestamp = existingOperation.lastUpdated ? new Date(existingOperation.lastUpdated).getTime() : 0;
115
- const currentTime = new Date().getTime();
116
-
117
- if (lastUpdateTimestamp > 0 && (currentTime - lastUpdateTimestamp) > fiveMinutesInMs) {
118
- Logger.log(`WARNING: [${existingOperation.onboardingId}] Found stale ${operationType} operation for feed: ${config.name} (last updated at: ${existingOperation.lastUpdated}). Proceeding to create a new operation.`);
119
- existingOperation = undefined; // Treat as no existing operation for starting a new one
120
- } else {
121
- Logger.log(`[${existingOperation.onboardingId}] Found existing non-completed ${operationType} operation for feed: ${config.name}. Returning its status.`);
122
- const lastStep = existingOperation.steps && existingOperation.steps.length > 0 ? existingOperation.steps[existingOperation.steps.length - 1].stepName : 'N/A';
123
- return {
124
- onboardingId: existingOperation.onboardingId,
125
- status: existingOperation.status,
126
- message: `An ${operationType} process for this feed (${existingOperation.onboardingId}) is already in status: ${existingOperation.status}. Last step: ${lastStep}`,
127
- lastQueried: new Date().toISOString(),
128
- };
129
- }
130
- }
131
-
132
- // Then, check for successful operations with matching configuration
109
+ // Check for successful operations with matching configuration
133
110
  const succeededOperation = await onboardStatusManager.findSucceededOperation(config.name, operationType, config);
134
111
  if (succeededOperation) {
135
112
  Logger.log(`[${succeededOperation.onboardingId}] Found existing successful ${operationType} operation for feed: ${config.name} with matching configuration.`);
@@ -262,11 +262,6 @@ export class StdioServerValidator implements IServerValidator {
262
262
  const [baseCommand, ...defaultArgs] = fullCommand.split(' ');
263
263
  const args = [...defaultArgs, ...(server.installation.args || [])];
264
264
 
265
- // Validate command exists and is executable
266
- const isExecutable = await this.isCommandExecutable(baseCommand);
267
- if (!isExecutable) {
268
- throw new Error(`Command not found or not executable: ${baseCommand}`);
269
- }
270
265
 
271
266
  // Validate required environment variables if specified
272
267
  const envVars = server.installation.env;
@@ -295,6 +290,12 @@ export class StdioServerValidator implements IServerValidator {
295
290
  }
296
291
  }
297
292
  }
293
+ // Validate command exists and is executable
294
+ const isExecutable = await this.isCommandExecutable(baseCommand);
295
+ if (!isExecutable) {
296
+ throw new Error(`Command not found or not executable: ${baseCommand}`);
297
+ }
298
+
298
299
  // Test server startup
299
300
  const serverStarted = await this.testServerStartup(baseCommand, args, config.requirements?.find(r => r.type === 'npm'));
300
301
  if (!serverStarted) {
@@ -594,4 +594,107 @@ export async function getNpmExecutablePath(): Promise<string> {
594
594
  return '/usr/local/bin';
595
595
  }
596
596
  }
597
- }
597
+ }
598
+
599
+ /**
600
+ * Ensures the .NET global tools path is added to the system PATH for the current user
601
+ * on macOS and Linux by modifying shell configuration files.
602
+ * This function is idempotent and will not add the path if it already exists.
603
+ */
604
+ export async function ensureDotnetToolsInPath(): Promise<void> {
605
+ const osType = getOSType();
606
+ Logger.debug({
607
+ action: 'ensure_dotnet_tools_in_path',
608
+ osType
609
+ });
610
+
611
+ if (osType === OSType.Windows) {
612
+ Logger.debug('.NET tools PATH is usually handled by the installer on Windows. Skipping explicit PATH modification.');
613
+ return;
614
+ }
615
+
616
+ if (osType === OSType.MacOS || osType === OSType.Linux) {
617
+ const dotnetToolsPath = getDotnetGlobalToolsPath(); // This is typically ~/.dotnet/tools
618
+ const exportLine = `export PATH="${dotnetToolsPath}:$PATH"`;
619
+
620
+ // Determine shell configuration files to check/update
621
+ const shellConfigFiles: string[] = [];
622
+ const shell = process.env.SHELL;
623
+
624
+ if (shell && shell.includes('zsh')) {
625
+ shellConfigFiles.push(path.join(os.homedir(), '.zshrc'));
626
+ } else if (shell && shell.includes('bash')) {
627
+ shellConfigFiles.push(path.join(os.homedir(), '.bashrc'));
628
+ // .bash_profile is often sourced by .bashrc or used for login shells
629
+ shellConfigFiles.push(path.join(os.homedir(), '.bash_profile'));
630
+ } else {
631
+ // Fallback for other shells or if SHELL is not set
632
+ shellConfigFiles.push(path.join(os.homedir(), '.profile'));
633
+ shellConfigFiles.push(path.join(os.homedir(), '.bashrc'));
634
+ shellConfigFiles.push(path.join(os.homedir(), '.zshrc'));
635
+ }
636
+
637
+ let updatedAnyFile = false;
638
+
639
+ for (const configFile of shellConfigFiles) {
640
+ try {
641
+ if (fs.existsSync(configFile)) {
642
+ const content = await fs.promises.readFile(configFile, 'utf-8');
643
+ // Check if the exact line or a line setting dotnetToolsPath in PATH exists
644
+ // A more robust check might involve parsing, but this covers common cases.
645
+ // Regex to check if dotnetToolsPath is part of an PATH export, avoiding duplicates.
646
+ // It looks for `export PATH=...$dotnetToolsPath...` or `export PATH=...${HOME}/.dotnet/tools...`
647
+ const pathRegex = new RegExp(`export\\s+PATH=.*${dotnetToolsPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*`);
648
+ const homePathRegex = new RegExp(`export\\s+PATH=.*\\$HOME\\/\\.dotnet\\/tools.*`);
649
+
650
+ if (!pathRegex.test(content) && !homePathRegex.test(content)) {
651
+ Logger.debug(`Adding .NET tools path to ${configFile}`);
652
+ await fs.promises.appendFile(configFile, `\n# Add .NET Core SDK tools to PATH\n${exportLine}\n`);
653
+ Logger.log(`Appended '${exportLine}' to ${configFile}. You may need to source this file or restart your terminal.`);
654
+ updatedAnyFile = true;
655
+ } else {
656
+ Logger.debug(`.NET tools path already configured in ${configFile}`);
657
+ }
658
+ } else if (shellConfigFiles.length === 1 || configFile === path.join(os.homedir(), '.profile')) {
659
+ // If it's the only config file determined or it's .profile, and it doesn't exist, create it.
660
+ Logger.debug(`${configFile} does not exist. Creating and adding .NET tools path.`);
661
+ await fs.promises.writeFile(configFile, `# Add .NET Core SDK tools to PATH\n${exportLine}\n`);
662
+ Logger.log(`Created ${configFile} and added '${exportLine}'. You may need to source this file or restart your terminal.`);
663
+ updatedAnyFile = true;
664
+ }
665
+ } catch (error) {
666
+ Logger.error(`Failed to update ${configFile} for .NET tools PATH: ${error instanceof Error ? error.message : String(error)}`);
667
+ }
668
+ }
669
+ if (updatedAnyFile) {
670
+ Logger.log(`Dotnet tools path has been added to shell configuration. Please source the relevant file (e.g., 'source ~/.zshrc') or restart your terminal session for changes to take effect.`);
671
+ }
672
+
673
+ // Also update the current process's PATH environment variable
674
+ if (process.env.PATH && !process.env.PATH.includes(dotnetToolsPath)) {
675
+ process.env.PATH = `${dotnetToolsPath}:${process.env.PATH}`;
676
+ Logger.debug(`Updated current process.env.PATH to include: ${dotnetToolsPath}`);
677
+ } else if (!process.env.PATH) {
678
+ process.env.PATH = dotnetToolsPath;
679
+ Logger.debug(`Set current process.env.PATH to: ${dotnetToolsPath}`);
680
+ }
681
+ }
682
+ }
683
+ /**
684
+ *
685
+ * @returns The path to the .dotnet global tools directory.
686
+ */
687
+ export function getDotnetGlobalToolsPath(): string {
688
+ const homeDir = os.homedir();
689
+
690
+ if (process.platform === 'win32') {
691
+ // Example: C:\Users\YourName\.dotnet\tools
692
+ return path.join(homeDir, '.dotnet', 'tools');
693
+ } else if (process.platform === 'darwin' || process.platform === 'linux') {
694
+ // macOS or Linux: ~/.dotnet/tools
695
+ return path.join(homeDir, '.dotnet', 'tools');
696
+ } else {
697
+ throw new Error(`Unsupported platform: ${process.platform}`);
698
+ }
699
+ }
700
+
@@ -235,6 +235,7 @@ export const serverRequirementTemplate = (serverIndex, reqIndex, isReadOnly = fa
235
235
  <option value="">Select Type</option>
236
236
  <option value="npm">NPM Package</option>
237
237
  <option value="pip">PIP Package</option>
238
+ <option value="nuget">Nuget Package</option>
238
239
  <option value="command">Command</option>
239
240
  </select>
240
241
  </div>
@@ -158,16 +158,16 @@ function renderServerCategoryList(servers) {
158
158
 
159
159
  return `
160
160
  <div class="server-item border border-gray-200 p-3 rounded hover:bg-gray-50 cursor-pointer transition duration-150 ease-in-out ${pinnedClass}"
161
- data-server-name="${server.name}">
161
+ data-server-name="${server.name}" onclick="navigateToCategory('${server.name}')">
162
162
  <div class="flex justify-between items-center">
163
- <h3 class="font-semibold text-gray-800" onclick="navigateToCategory('${server.name}')">${server.displayName || server.name}</h3>
163
+ <h3 class="font-semibold text-gray-800">${server.displayName || server.name}</h3>
164
164
  <div class="flex items-center">
165
165
  <div class="pin-button ${pinnedClass}" onclick="togglePinCategoryItem('${server.name}', event)" title="${isPinned ? 'Unpin' : 'Pin'} this category">
166
166
  ${pinIcon}
167
167
  </div>
168
168
  </div>
169
169
  </div>
170
- <div class="text-sm text-gray-500 flex items-center mt-1" onclick="navigateToCategory('${server.name}')">
170
+ <div class="text-sm text-gray-500 flex items-center mt-1">
171
171
  ${statusHtml}
172
172
  ${systemTagsHtml}
173
173
  </div>