imcp 0.0.16 → 0.0.17

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.
Files changed (135) hide show
  1. package/dist/cli/commands/install.js +2 -2
  2. package/dist/cli/commands/list.js +2 -2
  3. package/dist/cli/commands/serve.js +1 -1
  4. package/dist/core/RequirementService.d.ts +0 -12
  5. package/dist/core/RequirementService.js +0 -24
  6. package/dist/core/installers/clients/BaseClientInstaller.d.ts +1 -1
  7. package/dist/core/installers/clients/ClientInstaller.d.ts +1 -1
  8. package/dist/core/installers/clients/ClientInstaller.js +1 -1
  9. package/dist/core/installers/clients/ClientInstallerFactory.js +1 -1
  10. package/dist/core/installers/clients/ClineInstaller.d.ts +1 -1
  11. package/dist/core/installers/clients/ClineInstaller.js +1 -1
  12. package/dist/core/installers/clients/ExtensionInstaller.js +1 -1
  13. package/dist/core/installers/clients/GithubCopilotInstaller.d.ts +1 -1
  14. package/dist/core/installers/clients/GithubCopilotInstaller.js +1 -1
  15. package/dist/core/installers/clients/MSRooCodeInstaller.d.ts +1 -1
  16. package/dist/core/installers/clients/MSRooCodeInstaller.js +1 -1
  17. package/dist/core/installers/requirements/BaseInstaller.d.ts +1 -1
  18. package/dist/core/installers/requirements/BaseInstaller.js +1 -1
  19. package/dist/core/installers/requirements/CommandInstaller.d.ts +1 -1
  20. package/dist/core/installers/requirements/CommandInstaller.js +1 -1
  21. package/dist/core/installers/requirements/GeneralInstaller.d.ts +1 -1
  22. package/dist/core/installers/requirements/InstallerFactory.d.ts +1 -1
  23. package/dist/core/installers/requirements/NpmInstaller.d.ts +1 -1
  24. package/dist/core/installers/requirements/NpmInstaller.js +1 -1
  25. package/dist/core/installers/requirements/PipInstaller.d.ts +1 -1
  26. package/dist/core/installers/requirements/RequirementInstaller.d.ts +1 -1
  27. package/dist/core/loaders/ConfigurationLoader.d.ts +32 -0
  28. package/dist/core/loaders/ConfigurationLoader.js +236 -0
  29. package/dist/core/loaders/ConfigurationProvider.d.ts +35 -0
  30. package/dist/core/loaders/ConfigurationProvider.js +375 -0
  31. package/dist/core/loaders/ServerSchemaLoader.d.ts +11 -0
  32. package/{src/core/ServerSchemaLoader.ts → dist/core/loaders/ServerSchemaLoader.js} +43 -48
  33. package/dist/core/loaders/ServerSchemaProvider.d.ts +17 -0
  34. package/{src/core/ServerSchemaProvider.ts → dist/core/loaders/ServerSchemaProvider.js} +120 -137
  35. package/dist/core/metadatas/constants.d.ts +47 -0
  36. package/dist/core/metadatas/constants.js +94 -0
  37. package/dist/core/metadatas/types.d.ts +166 -0
  38. package/dist/core/metadatas/types.js +16 -0
  39. package/dist/core/onboard/FeedOnboardService.d.ts +1 -1
  40. package/dist/core/onboard/FeedOnboardService.js +1 -1
  41. package/dist/core/onboard/OnboardProcessor.d.ts +1 -1
  42. package/dist/core/onboard/OnboardProcessor.js +1 -1
  43. package/dist/core/onboard/OnboardStatus.d.ts +1 -1
  44. package/dist/core/onboard/OnboardStatusManager.d.ts +1 -1
  45. package/dist/core/onboard/OnboardStatusManager.js +1 -1
  46. package/dist/core/validators/FeedValidator.d.ts +1 -1
  47. package/dist/core/validators/IServerValidator.d.ts +1 -1
  48. package/dist/core/validators/SSEServerValidator.d.ts +1 -1
  49. package/dist/core/validators/ServerValidatorFactory.d.ts +1 -1
  50. package/dist/core/validators/StdioServerValidator.d.ts +1 -1
  51. package/dist/core/validators/StdioServerValidator.js +1 -1
  52. package/dist/index.d.ts +3 -3
  53. package/dist/index.js +3 -3
  54. package/dist/services/InstallationService.d.ts +50 -0
  55. package/dist/services/InstallationService.js +350 -0
  56. package/dist/services/MCPManager.d.ts +28 -0
  57. package/dist/services/MCPManager.js +188 -0
  58. package/dist/services/RequirementService.d.ts +40 -0
  59. package/dist/services/RequirementService.js +110 -0
  60. package/dist/services/ServerService.d.ts +2 -2
  61. package/dist/services/ServerService.js +5 -5
  62. package/dist/utils/adoUtils.d.ts +2 -2
  63. package/dist/utils/adoUtils.js +1 -1
  64. package/dist/utils/feedUtils.js +1 -1
  65. package/dist/utils/githubUtils.d.ts +1 -1
  66. package/dist/utils/githubUtils.js +1 -1
  67. package/dist/utils/logger.js +1 -1
  68. package/dist/utils/macroExpressionUtils.d.ts +1 -1
  69. package/dist/utils/osUtils.d.ts +1 -1
  70. package/dist/utils/osUtils.js +1 -1
  71. package/dist/web/contract/serverContract.d.ts +1 -1
  72. package/dist/web/public/index.html +1 -3
  73. package/dist/web/public/js/api.js +2 -80
  74. package/dist/web/server.js +2 -2
  75. package/package.json +1 -1
  76. package/src/cli/commands/install.ts +3 -3
  77. package/src/cli/commands/list.ts +2 -2
  78. package/src/cli/commands/serve.ts +3 -2
  79. package/src/cli/index.ts +1 -1
  80. package/src/core/installers/clients/BaseClientInstaller.ts +134 -3
  81. package/src/core/installers/clients/ClientInstaller.ts +3 -3
  82. package/src/core/installers/clients/ClientInstallerFactory.ts +1 -1
  83. package/src/core/installers/clients/ClineInstaller.ts +1 -101
  84. package/src/core/installers/clients/ExtensionInstaller.ts +1 -1
  85. package/src/core/installers/clients/GithubCopilotInstaller.ts +1 -101
  86. package/src/core/installers/clients/MSRooCodeInstaller.ts +1 -102
  87. package/src/core/installers/requirements/BaseInstaller.ts +2 -2
  88. package/src/core/installers/requirements/CommandInstaller.ts +1 -1
  89. package/src/core/installers/requirements/GeneralInstaller.ts +1 -1
  90. package/src/core/installers/requirements/InstallerFactory.ts +1 -1
  91. package/src/core/installers/requirements/NpmInstaller.ts +12 -12
  92. package/src/core/installers/requirements/PipInstaller.ts +1 -1
  93. package/src/core/installers/requirements/RequirementInstaller.ts +1 -1
  94. package/src/core/{ConfigurationLoader.ts → loaders/ConfigurationLoader.ts} +31 -7
  95. package/src/core/{ConfigurationProvider.ts → loaders/ConfigurationProvider.ts} +18 -10
  96. package/src/core/loaders/ServerSchemaLoader.ts +117 -0
  97. package/src/core/loaders/ServerSchemaProvider.ts +99 -0
  98. package/src/core/{types.ts → metadatas/types.ts} +3 -2
  99. package/src/core/onboard/FeedOnboardService.ts +270 -146
  100. package/src/core/onboard/OnboardProcessor.ts +60 -11
  101. package/src/core/onboard/OnboardStatus.ts +7 -2
  102. package/src/core/onboard/OnboardStatusManager.ts +270 -43
  103. package/src/core/validators/FeedValidator.ts +65 -9
  104. package/src/core/validators/IServerValidator.ts +1 -1
  105. package/src/core/validators/SSEServerValidator.ts +2 -2
  106. package/src/core/validators/ServerValidatorFactory.ts +1 -1
  107. package/src/core/validators/StdioServerValidator.ts +86 -34
  108. package/src/index.ts +3 -3
  109. package/src/{core → services}/InstallationService.ts +5 -5
  110. package/src/{core → services}/MCPManager.ts +10 -5
  111. package/src/{core → services}/RequirementService.ts +2 -31
  112. package/src/services/ServerService.ts +7 -7
  113. package/src/utils/adoUtils.ts +3 -3
  114. package/src/utils/feedUtils.ts +2 -2
  115. package/src/utils/githubUtils.ts +2 -2
  116. package/src/utils/logger.ts +13 -1
  117. package/src/utils/macroExpressionUtils.ts +1 -1
  118. package/src/utils/osUtils.ts +4 -4
  119. package/src/web/contract/serverContract.ts +2 -2
  120. package/src/web/public/index.html +1 -3
  121. package/src/web/public/js/api.js +2 -80
  122. package/src/web/public/js/modal/installation.js +1 -1
  123. package/src/web/public/js/onboard/ONBOARDING_PAGE_DESIGN.md +41 -9
  124. package/src/web/public/js/onboard/formProcessor.js +200 -34
  125. package/src/web/public/js/onboard/index.js +2 -2
  126. package/src/web/public/js/onboard/publishHandler.js +30 -22
  127. package/src/web/public/js/onboard/templates.js +34 -40
  128. package/src/web/public/js/onboard/uiHandlers.js +175 -84
  129. package/src/web/public/js/onboard/validationHandlers.js +147 -64
  130. package/src/web/public/js/serverCategoryDetails.js +19 -4
  131. package/src/web/public/js/serverCategoryList.js +13 -1
  132. package/src/web/public/onboard.html +1 -1
  133. package/src/web/server.ts +30 -14
  134. package/src/services/InstallRequestValidator.ts +0 -112
  135. /package/src/core/{constants.ts → metadatas/constants.ts} +0 -0
@@ -1,18 +1,12 @@
1
- import { FeedConfiguration, McpConfig } from '../types.js';
2
- import { configProvider, ConfigurationProvider } from '../ConfigurationProvider.js';
1
+ import { FeedConfiguration, McpConfig } from '../metadatas/types.js';
2
+ import { configProvider } from '../loaders/ConfigurationProvider.js';
3
+ import { ServerSchemaProvider } from '../loaders/ServerSchemaProvider.js';
3
4
  import { feedValidator } from '../validators/FeedValidator.js';
4
5
  import { Logger } from '../../utils/logger.js';
5
- import { OnboardStatus, OnboardingProcessStatus, OperationStatus, OperationType, ValidationOperationResult } from './OnboardStatus.js';
6
+ import { OnboardingProcessStatus, OperationStatus, OperationType, ValidationOperationResult } from './OnboardStatus.js';
6
7
  import { onboardStatusManager } from './OnboardStatusManager.js';
7
8
  import { onboardProcessor } from './OnboardProcessor.js';
8
9
 
9
- const NON_COMPLETED_ONBOARDING_STATUSES: OnboardingProcessStatus[] = [
10
- OnboardingProcessStatus.PENDING,
11
- OnboardingProcessStatus.VALIDATING,
12
- OnboardingProcessStatus.VALIDATED,
13
- OnboardingProcessStatus.PR_CREATING,
14
- ];
15
-
16
10
  /**
17
11
  * Service for handling feed onboarding operations
18
12
  */
@@ -23,17 +17,53 @@ export class FeedOnboardService {
23
17
  // No shared tempDir or repoDir at instance level
24
18
  }
25
19
 
20
+ /**
21
+ * Creates an operation ID that combines feed name and operation type.
22
+ * @param feedName The name of the feed.
23
+ * @param operationType The type of operation.
24
+ * @returns Combined operation ID string.
25
+ */
26
+ private createOperationId(feedName: string, operationType: OperationType): string {
27
+ return `${feedName}_${operationType}`;
28
+ }
29
+
26
30
  /**
27
31
  * Onboard a new feed configuration
28
32
  * @param config Feed configuration to onboard
29
33
  */
30
34
  public async onboardFeed(config: FeedConfiguration, forExistingCategory?: boolean): Promise<OperationStatus & { feedConfiguration?: FeedConfiguration }> {
31
35
  // Perform static validation first
32
- await this.validateStaticConfig(config, forExistingCategory);
33
-
34
- const operationStatus = await this._initiateOperation(config, 'FULL_ONBOARDING', forExistingCategory);
36
+ const serverList = await this.validateStaticConfig(config, forExistingCategory);
37
+
38
+ // Check for an existing, succeeded VALIDATION_ONLY operation with an identical configuration.
39
+ const existingSucceededValidation = await onboardStatusManager.findSucceededOperation(config.name, 'VALIDATION_ONLY', config);
40
+
41
+ if (!existingSucceededValidation) {
42
+ // If no matching successful validation is found, instruct user to validate first.
43
+ Logger.log(`Feed ${config.name} has not been successfully validated with the current configuration. Publishing requires prior successful validation.`);
44
+ // Return a specific status/message indicating validation is required.
45
+ // This is not a FAILED state for an operation, but a prerequisite not met.
46
+ // We'll use a custom message and potentially a specific status if we define one for "PREREQUISITE_FAILED" or similar.
47
+ // For now, let's return a FAILED status for the attempted FULL_ONBOARDING initiation.
48
+ // A more robust solution might involve a different return type or error code.
49
+ const onboardingId = this.createOperationId(config.name, 'FULL_ONBOARDING'); // Hypothetical ID
50
+ return {
51
+ onboardingId: onboardingId,
52
+ status: OnboardingProcessStatus.FAILED, // Or a new status like 'VALIDATION_REQUIRED'
53
+ message: 'The feed configuration has not been validated or has changed. Please validate the configuration before publishing.',
54
+ lastQueried: new Date().toISOString(),
55
+ feedConfiguration: config
56
+ };
57
+ }
35
58
 
36
- return { ...operationStatus, feedConfiguration: config };
59
+ Logger.log(`Found existing successful validation for feed: ${config.name}. Proceeding with full onboarding.`);
60
+ // If validation exists and matches, proceed to initiate FULL_ONBOARDING.
61
+ // _initiateOperation will handle checks for existing non-completed or successfully completed FULL_ONBOARDING operations.
62
+ const operationStatus = await this._initiateOperation(config, 'FULL_ONBOARDING', serverList, forExistingCategory);
63
+ return {
64
+ ...operationStatus,
65
+ feedConfiguration: config
66
+ };
37
67
  }
38
68
 
39
69
  /**
@@ -43,9 +73,9 @@ export class FeedOnboardService {
43
73
  */
44
74
  public async validateFeed(config: FeedConfiguration, forExistingCategory?: boolean): Promise<OperationStatus & { feedConfiguration?: FeedConfiguration }> {
45
75
  // Perform static validation first
46
- await this.validateStaticConfig(config, forExistingCategory);
76
+ const serverList = await this.validateStaticConfig(config, forExistingCategory);
47
77
 
48
- const operationStatus = await this._initiateOperation(config, 'VALIDATION_ONLY', forExistingCategory);
78
+ const operationStatus = await this._initiateOperation(config, 'VALIDATION_ONLY', serverList, forExistingCategory);
49
79
 
50
80
  return { ...operationStatus, feedConfiguration: config };
51
81
  }
@@ -56,59 +86,85 @@ export class FeedOnboardService {
56
86
  * If an existing operation is found, its status is returned. Otherwise, a new operation is created and started.
57
87
  * @param config The feed configuration.
58
88
  * @param operationType The type of operation to initiate (FULL_ONBOARDING or VALIDATION_ONLY).
89
+ * @param serverList The list of server names to process.
59
90
  * @returns A promise that resolves to the operation status.
60
91
  */
61
- private async _initiateOperation(config: FeedConfiguration, operationType: OperationType, forExistingCategory?: boolean): Promise<OperationStatus> {
62
- let existingOperation = await this._findExistingNonCompletedOperation(config.name, operationType);
92
+ private async _initiateOperation(config: FeedConfiguration, operationType: OperationType, serverList: string[], forExistingCategory?: boolean): Promise<OperationStatus & { feedConfiguration?: FeedConfiguration }> {
93
+ // First, check for existing non-completed operations
94
+ let existingOperation = await onboardStatusManager._findExistingNonCompletedOperation(config.name, operationType);
63
95
 
64
96
  if (existingOperation) {
65
- // OnboardStatus has a lastUpdated field (ISO string)
66
- // This field is updated by OnboardStatusManager.updateStatus
67
97
  const fiveMinutesInMs = 5 * 60 * 1000;
68
98
  const lastUpdateTimestamp = existingOperation.lastUpdated ? new Date(existingOperation.lastUpdated).getTime() : 0;
69
99
  const currentTime = new Date().getTime();
70
100
 
71
101
  if (lastUpdateTimestamp > 0 && (currentTime - lastUpdateTimestamp) > fiveMinutesInMs) {
72
102
  Logger.log(`WARNING: [${existingOperation.onboardingId}] Found stale ${operationType} operation for feed: ${config.name} (last updated at: ${existingOperation.lastUpdated}). Proceeding to create a new operation.`);
73
-
74
103
  existingOperation = undefined; // Treat as no existing operation for starting a new one
75
104
  } else {
76
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';
77
107
  return {
78
108
  onboardingId: existingOperation.onboardingId,
79
109
  status: existingOperation.status,
80
- message: `An ${operationType} process for this feed (${existingOperation.onboardingId}) is already in status: ${existingOperation.status}. Current step: ${existingOperation.currentStep || 'N/A'}`,
110
+ message: `An ${operationType} process for this feed (${existingOperation.onboardingId}) is already in status: ${existingOperation.status}. Last step: ${lastStep}`,
81
111
  lastQueried: new Date().toISOString(),
82
112
  };
83
113
  }
84
114
  }
85
115
 
86
- // serverName is removed
116
+ // Then, check for successful operations with matching configuration
117
+ const succeededOperation = await onboardStatusManager.findSucceededOperation(config.name, operationType, config);
118
+ if (succeededOperation) {
119
+ Logger.log(`[${succeededOperation.onboardingId}] Found existing successful ${operationType} operation for feed: ${config.name} with matching configuration.`);
120
+ const operationStatus: OperationStatus & { feedConfiguration?: FeedConfiguration } = {
121
+ onboardingId: succeededOperation.onboardingId,
122
+ status: succeededOperation.status,
123
+ message: `A successful ${operationType} already exists for this feed with identical configuration.`,
124
+ lastQueried: new Date().toISOString(),
125
+ };
126
+
127
+ if (config) {
128
+ operationStatus.feedConfiguration = config;
129
+ }
130
+
131
+ return operationStatus;
132
+ }
133
+
134
+ // If no existing or matching operations found, create a new one
135
+ const onboardingId = this.createOperationId(config.name, operationType);
87
136
  const initialStatus = await onboardStatusManager.createInitialStatus(config.name, operationType);
88
- const onboardingId = initialStatus.onboardingId;
89
137
 
90
- await onboardStatusManager.saveFeedConfiguration(onboardingId, config);
138
+ // Use config.name as categoryName for saveFeedConfiguration
139
+ await onboardStatusManager.saveFeedConfiguration(config.name, operationType, config);
91
140
 
92
141
  if (operationType === 'FULL_ONBOARDING') {
93
- this.processFullOnboarding(onboardingId, config).catch(async (error) => {
142
+ // For FULL_ONBOARDING, we assume all servers defined in the config are to be processed for PR creation.
143
+ // The serverList from validateStaticConfig primarily influences validation.
144
+ // If specific servers need to be excluded from the PR, this logic might need adjustment.
145
+ this.processFullOnboarding(onboardingId, config, operationType, serverList).catch(async (error) => {
94
146
  Logger.error(`[${onboardingId}] Full feed onboarding process failed:`, error);
95
- await onboardStatusManager.updateStatus(onboardingId, {
96
- status: OnboardingProcessStatus.FAILED,
97
- errorMessage: error instanceof Error ? error.message : String(error),
98
- currentStep: 'Failed during full onboarding process',
99
- prInfo: undefined,
100
- });
147
+ await onboardStatusManager.recordStep(
148
+ config.name,
149
+ operationType,
150
+ 'Failed during full onboarding process',
151
+ undefined, // serverName
152
+ OnboardingProcessStatus.FAILED,
153
+ error instanceof Error ? error.message : String(error)
154
+ );
101
155
  });
102
156
  } else if (operationType === 'VALIDATION_ONLY') {
103
- this.processValidationOnly(onboardingId, config).catch(async (error) => {
157
+ this.processValidationOnly(onboardingId, config, operationType, serverList).catch(async (error) => {
104
158
  Logger.error(`[${onboardingId}] Feed validation process failed:`, error);
105
- await onboardStatusManager.updateStatus(onboardingId, {
106
- status: OnboardingProcessStatus.FAILED,
107
- errorMessage: error instanceof Error ? error.message : String(error),
108
- currentStep: 'Failed during validation process',
109
- validationStatus: (error as any).validationStatus || { isValid: false, message: error instanceof Error ? error.message : String(error) },
110
- prInfo: undefined,
111
- });
159
+ await onboardStatusManager.recordStep(
160
+ config.name,
161
+ operationType,
162
+ 'Failed during validation process',
163
+ undefined, // serverName
164
+ OnboardingProcessStatus.FAILED,
165
+ error instanceof Error ? error.message : String(error)
166
+ // TODO: Consider how to best store/pass 'validationStatus' if it's still needed at this specific failure point
167
+ );
112
168
  });
113
169
  }
114
170
 
@@ -120,33 +176,14 @@ export class FeedOnboardService {
120
176
  };
121
177
  }
122
178
 
123
- /**
124
- * Finds an existing non-completed operation for a given feed name, server name, and operation type.
125
- * @param feedName The name of the feed.
126
- * @param operationType The type of operation.
127
- * @returns A promise that resolves to the OnboardStatus of the existing operation, or undefined if not found.
128
- */
129
- private async _findExistingNonCompletedOperation(
130
- feedName: string,
131
- operationType: OperationType
132
- ): Promise<OnboardStatus | undefined> {
133
- const allStatuses = await onboardStatusManager.getAllStatuses();
134
- return Object.values(allStatuses).find(
135
- (status) =>
136
- status.feedName === feedName &&
137
- // status.serverName === serverName && // serverName is removed
138
- status.operationType === operationType &&
139
- NON_COMPLETED_ONBOARDING_STATUSES.includes(status.status)
140
- );
141
- }
142
-
143
179
  /**
144
180
  * Performs static validation of the feed configuration.
145
181
  * @param config The feed configuration to validate.
146
182
  * @param forExistingCategory Whether this is for an existing category.
183
+ * @returns A promise that resolves to a list of server names to be processed.
147
184
  * @throws Error if validation fails.
148
185
  */
149
- private async validateStaticConfig(config: FeedConfiguration, forExistingCategory?: boolean): Promise<void> {
186
+ private async validateStaticConfig(config: FeedConfiguration, forExistingCategory?: boolean): Promise<string[]> {
150
187
  // --- Start of moved and enhanced static validation ---
151
188
  if (!config || typeof config !== 'object') { // Check if config itself is a valid object
152
189
  throw new Error('Invalid configuration: Input must be a valid object.');
@@ -163,30 +200,63 @@ export class FeedOnboardService {
163
200
  // --- End of moved static validation ---
164
201
 
165
202
  const existingConfig = await configProvider.getFeedConfiguration(config.name);
203
+ // Note: operationType validation is handled at the MCP server level
204
+
205
+ let serverListToProcess: string[];
166
206
 
167
207
  if (forExistingCategory) {
168
208
  if (!existingConfig) {
169
209
  throw new Error(`Cannot update non-existent category: ${config.name}. Please ensure the category name is correct or create it as a new category.`);
170
210
  }
171
- // Original logic from existing validateStaticConfig:
172
- // This implies that for an existing category, the payload must define mcpServers,
173
- // and at least one of them must be new.
174
- // If config.mcpServers is empty, newServersInPayload.length will be 0, leading to an error.
211
+ // For existing categories, mcpServers list must be present in the payload.
212
+ if (!config.mcpServers || config.mcpServers.length === 0) {
213
+ throw new Error('For an existing category, "mcpServers" must be provided and non-empty in the payload.');
214
+ }
215
+
175
216
  const existingServerNames = new Set(existingConfig.mcpServers.map((s: McpConfig) => s.name));
176
- const newServersInPayload = config.mcpServers.filter((s: McpConfig) => !existingServerNames.has(s.name));
217
+ // Servers to process are those that are new OR existing ones marked as adhoc in the payload.
218
+ const serversToProcessInPayload = config.mcpServers.filter(payloadServer => {
219
+ const isNew = !existingServerNames.has(payloadServer.name);
220
+ const isAdhocInPayload = payloadServer.systemTags?.adhoc === 'true';
221
+ return isNew || (existingServerNames.has(payloadServer.name) && isAdhocInPayload);
222
+ });
177
223
 
178
- if (newServersInPayload.length === 0) {
179
- // This error triggers if config.mcpServers is empty, or if all servers in config.mcpServers already exist.
180
- throw new Error('No new servers provided for the existing category. Ensure the "mcpServers" list in your payload contains at least one server not already in this category, or that the list is not empty if you intend to add servers.');
224
+ if (serversToProcessInPayload.length === 0) {
225
+ // If no new or editable (adhoc) servers are found in the payload for an existing category, it's an error.
226
+ throw new Error(`No new or adhoc (editable) servers provided for the existing category "${config.name}". Ensure the "mcpServers" list in your payload contains at least one server not already in this category or an existing server marked with adhoc:true.`);
181
227
  }
228
+ serverListToProcess = serversToProcessInPayload.map(s => s.name);
182
229
  } else { // For new categories
183
230
  if (existingConfig) {
184
- throw new Error(`Category "${config.name}" already exists. To modify an existing category, please indicate that it is for an existing category.`);
231
+ // If it's not for an existing category, but the category name already exists, this is an error.
232
+ // Adhoc tags on servers are not relevant here as we are creating a new category.
233
+ throw new Error(`Category "${config.name}" already exists. To modify an existing category, please set 'forExistingCategory' to true.`);
185
234
  }
235
+ // For new categories, mcpServers list must be present and non-empty.
186
236
  if (!config.mcpServers || config.mcpServers.length === 0) {
187
237
  throw new Error('Server configuration ("mcpServers") must be provided and non-empty for new categories.');
188
238
  }
239
+ // For new categories, all servers in the payload are considered.
240
+ serverListToProcess = config.mcpServers.map(s => s.name);
189
241
  }
242
+ // Remove systemTags from individual servers
243
+ if (config.mcpServers && Array.isArray(config.mcpServers)) {
244
+ config.mcpServers.forEach(server => {
245
+ if (server.systemTags) {
246
+ delete server.systemTags;
247
+ }
248
+ });
249
+ }
250
+
251
+ // Remove systemTags and PullRequest from the main config object
252
+ if (config.systemTags) {
253
+ delete config.systemTags;
254
+ }
255
+ if (config.PullRequest) {
256
+ delete config.PullRequest;
257
+ }
258
+
259
+ return serverListToProcess;
190
260
  }
191
261
 
192
262
  /**
@@ -194,59 +264,67 @@ export class FeedOnboardService {
194
264
  * Updates the onboarding status to VALIDATING, then to VALIDATED or FAILED based on the validation result.
195
265
  * @param onboardingId The ID of the onboarding process.
196
266
  * @param config The feed configuration to validate.
267
+ * @param operationType The type of operation this validation is for.
268
+ * @param serverList The list of server names to validate.
197
269
  * @throws Error if validation fails, to be caught by the calling process.
198
270
  */
199
- private async _validateFeedConfiguration(onboardingId: string, config: FeedConfiguration): Promise<void> {
200
- await onboardStatusManager.updateStatus(onboardingId, { status: OnboardingProcessStatus.VALIDATING, currentStep: 'Validating feed configuration' });
271
+ private async _validateFeedConfiguration(onboardingId: string, config: FeedConfiguration, operationType: OperationType, serverList: string[]): Promise<void> {
272
+ await onboardStatusManager.recordStep(config.name, operationType, 'Validating feed configuration', undefined, OnboardingProcessStatus.VALIDATING);
201
273
  let overallValidationStatus: ValidationOperationResult['validationStatus'] = { isValid: true, serverResults: [] };
202
274
  try {
203
- feedValidator.validate(config);
204
-
205
- // Then, validate each MCP server
206
- for (const server of config.mcpServers) {
275
+ await feedValidator.validate(config, config.name, operationType);
276
+
277
+ // Then, validate each MCP server specified in the serverList
278
+ for (const serverConfig of config.mcpServers) {
279
+ if (!serverList.includes(serverConfig.name)) {
280
+ Logger.log(`[${onboardingId}] Skipping validation for server ${serverConfig.name} as it's not in the specified list for this operation.`);
281
+ // Optionally, record a skipped status or add to serverResults as skipped
282
+ overallValidationStatus.serverResults?.push({ serverName: serverConfig.name, isValid: true, message: "Skipped (existed)" });
283
+ continue;
284
+ }
207
285
  try {
208
- await feedValidator.validateServer(server, config);
209
- overallValidationStatus.serverResults?.push({ serverName: server.name, isValid: true });
210
- await onboardStatusManager.updateStatus(onboardingId, {
211
- currentStep: `Validated server: ${server.name}`,
212
- validationStatus: overallValidationStatus // Update with intermediate results
213
- });
286
+ await feedValidator.validateServer(serverConfig, config, config.name, operationType);
287
+ overallValidationStatus.serverResults?.push({ serverName: serverConfig.name, isValid: true });
288
+ await onboardStatusManager.recordStep(config.name, operationType, `Validated server: ${serverConfig.name}`, serverConfig.name, OnboardingProcessStatus.VALIDATING);
289
+ await onboardStatusManager.updateStatus(config.name, operationType, { validationStatus: overallValidationStatus });
214
290
  } catch (serverValidationError: any) {
215
291
  overallValidationStatus.isValid = false;
216
292
  overallValidationStatus.serverResults?.push({
217
- serverName: server.name,
293
+ serverName: serverConfig.name,
218
294
  isValid: false,
219
295
  message: serverValidationError.message || String(serverValidationError)
220
296
  });
221
- // Update status immediately upon first server validation failure
222
- await onboardStatusManager.updateStatus(onboardingId, {
223
- status: OnboardingProcessStatus.FAILED,
224
- currentStep: `Validation failed for server: ${server.name}`,
225
- errorMessage: `Server ${server.name}: ${serverValidationError.message || String(serverValidationError)}`,
226
- validationStatus: overallValidationStatus
227
- });
297
+ await onboardStatusManager.recordStep(
298
+ config.name,
299
+ operationType,
300
+ `Validation failed for server: ${serverConfig.name}`,
301
+ serverConfig.name,
302
+ OnboardingProcessStatus.FAILED,
303
+ `Server ${serverConfig.name}: ${serverValidationError.message || String(serverValidationError)}`
304
+ );
305
+ await onboardStatusManager.updateStatus(config.name, operationType, { validationStatus: overallValidationStatus });
228
306
  throw serverValidationError; // Propagate the error to fail the whole validation
229
307
  }
230
308
  }
231
309
 
232
- // If all servers validated successfully
233
- await onboardStatusManager.updateStatus(onboardingId, {
234
- status: OnboardingProcessStatus.VALIDATED,
235
- currentStep: 'All servers in feed configuration validated successfully',
236
- validationStatus: overallValidationStatus
237
- });
310
+ // If all *processed* servers validated successfully
311
+ await onboardStatusManager.recordStep(config.name, operationType, 'All servers in feed configuration validated successfully', undefined, OnboardingProcessStatus.VALIDATED);
312
+ await onboardStatusManager.updateStatus(config.name, operationType, { validationStatus: overallValidationStatus });
238
313
  } catch (error) {
239
314
  const validationError = error as any; // Assuming error might have validation details
240
- // This block will now primarily catch errors from the main feedValidator.validate(config)
241
- // or if a server validation error was re-thrown and not caught by the loop's catch.
242
- await onboardStatusManager.updateStatus(onboardingId, {
243
- status: OnboardingProcessStatus.FAILED,
244
- currentStep: 'Feed configuration validation failed',
245
- errorMessage: validationError.message || String(error),
246
- validationStatus: { // Ensure this reflects the actual state, potentially including partial server results
315
+ await onboardStatusManager.recordStep(
316
+ config.name,
317
+ operationType,
318
+ 'Feed configuration validation failed',
319
+ undefined,
320
+ OnboardingProcessStatus.FAILED,
321
+ validationError.message || String(error)
322
+ );
323
+ await onboardStatusManager.updateStatus(config.name, operationType, {
324
+ validationStatus: {
247
325
  isValid: false,
248
326
  message: validationError.message || String(error),
249
- serverResults: overallValidationStatus.serverResults // Include any server results gathered so far
327
+ serverResults: overallValidationStatus.serverResults // Ensure this is up-to-date
250
328
  }
251
329
  });
252
330
  throw error; // Re-throw to be caught by the calling process
@@ -259,29 +337,37 @@ export class FeedOnboardService {
259
337
  * If validation fails, the error is handled by _validateFeedConfiguration and the calling _initiateOperation method.
260
338
  * @param onboardingId The ID of the onboarding process.
261
339
  * @param config The feed configuration.
340
+ * @param operationType The type of operation (should be 'VALIDATION_ONLY').
341
+ * @param serverList The list of server names to validate.
262
342
  */
263
- private async processValidationOnly(onboardingId: string, config: FeedConfiguration, forExistingCategory?: boolean): Promise<void> {
343
+ private async processValidationOnly(onboardingId: string, config: FeedConfiguration, operationType: OperationType, serverList: string[]): Promise<void> {
264
344
  try {
265
- await this._validateFeedConfiguration(onboardingId, config);
266
- // _validateFeedConfiguration now handles setting VALIDATED or FAILED.
267
- // If it completes without throwing, it means validation was successful.
268
- const finalStatus = await onboardStatusManager.getStatus(onboardingId);
345
+ // Pass operationType and serverList to _validateFeedConfiguration
346
+ await this._validateFeedConfiguration(onboardingId, config, operationType, serverList);
347
+ // Use config.name and operationType for getStatus
348
+ const finalStatus = await onboardStatusManager.getStatus(config.name, operationType);
269
349
  const result: ValidationOperationResult = {
270
- validationStatus: finalStatus?.validationStatus || { isValid: true, serverResults: [] }, // Get the detailed status
350
+ validationStatus: finalStatus?.validationStatus || { isValid: true, serverResults: [] },
271
351
  feedConfiguration: config
272
352
  };
273
- await onboardStatusManager.updateStatus(onboardingId, {
274
- status: OnboardingProcessStatus.SUCCEEDED, // Mark as SUCCEEDED for VALIDATION_ONLY
275
- currentStep: 'Feed configuration validated successfully',
276
- errorMessage: undefined,
277
- prInfo: undefined,
278
- result: result
353
+ // Use config.name and operationType for recordStep and updateStatus
354
+ await onboardStatusManager.recordStep(
355
+ config.name,
356
+ operationType,
357
+ 'Feed configuration validated successfully',
358
+ undefined, // serverName
359
+ OnboardingProcessStatus.SUCCEEDED
360
+ );
361
+ await onboardStatusManager.updateStatus(config.name, operationType, {
362
+ errorMessage: undefined, // Clear any previous error
363
+ prInfo: undefined, // Clear PR info for validation-only
364
+ result: result, // Set the result
365
+ validationStatus: result.validationStatus // Ensure validationStatus is also updated on success
279
366
  });
280
367
  Logger.log(`[${onboardingId}] Successfully validated feed: ${config.name}`);
281
368
  } catch (error) {
282
- // Error handling is done within _validateFeedConfiguration and by the caller of processValidationOnly
283
- Logger.error(`[${onboardingId}] Feed validation process failed for ${config.name}:`, error);
284
- // No need to re-throw here as the caller's catch block will handle it.
369
+ // Error is already logged and status updated by _validateFeedConfiguration or its callers
370
+ Logger.error(`[${onboardingId}] Feed validation process failed for ${config.name} (already handled):`, error);
285
371
  }
286
372
  }
287
373
 
@@ -292,52 +378,90 @@ export class FeedOnboardService {
292
378
  * Manages status updates throughout the process and handles cleanup of temporary directories.
293
379
  * @param onboardingId The ID of the onboarding process.
294
380
  * @param config The feed configuration.
381
+ * @param operationType The type of operation (should be 'FULL_ONBOARDING').
295
382
  * @throws Error if any step of the full onboarding process fails.
296
383
  */
297
- private async processFullOnboarding(onboardingId: string, config: FeedConfiguration, forExistingCategory?: boolean): Promise<void> {
384
+ private async processFullOnboarding(onboardingId: string, config: FeedConfiguration, operationType: OperationType, serverList: string[]): Promise<void> {
298
385
  const { tempDir, repoDir } = onboardProcessor.createDirectories(onboardingId);
299
386
  try {
300
- await this._validateFeedConfiguration(onboardingId, config);
387
+ // Validation is now a prerequisite and assumed to be done if this process is initiated.
388
+ // The status should be PENDING or a similar initial state from _initiateOperation.
389
+ // We directly proceed to PR creation steps.
390
+ // The first step recorded here will likely move status from PENDING to PR_CREATING.
391
+
392
+ await onboardStatusManager.recordStep(config.name, operationType, 'Starting PR creation process (validation confirmed)', undefined, OnboardingProcessStatus.PR_CREATING);
301
393
 
302
- await onboardStatusManager.updateStatus(onboardingId, { status: OnboardingProcessStatus.PR_CREATING, currentStep: 'Forking repository' });
394
+ await onboardStatusManager.recordStep(config.name, operationType, 'Forking repository', undefined, OnboardingProcessStatus.PR_CREATING);
303
395
  await onboardProcessor.forkRepo(onboardingId, repoDir);
304
396
 
305
- await onboardStatusManager.updateStatus(onboardingId, { status: OnboardingProcessStatus.PR_CREATING, currentStep: 'Cloning repository' });
397
+ await onboardStatusManager.recordStep(config.name, operationType, 'Cloning repository', undefined, OnboardingProcessStatus.PR_CREATING);
306
398
  await onboardProcessor.cloneRepo(onboardingId, tempDir, repoDir);
307
399
 
308
- await onboardStatusManager.updateStatus(onboardingId, { status: OnboardingProcessStatus.PR_CREATING, currentStep: 'Setting up branch' });
400
+ await onboardStatusManager.recordStep(config.name, operationType, 'Setting up branch', undefined, OnboardingProcessStatus.PR_CREATING);
309
401
  const branchName = await onboardProcessor.setupBranch(onboardingId, config, repoDir);
310
402
 
311
- await onboardStatusManager.updateStatus(onboardingId, { status: OnboardingProcessStatus.PR_CREATING, currentStep: 'Saving feed configuration to repository' });
312
- await onboardProcessor.saveFeedConfigToRepo(onboardingId, config, repoDir);
403
+ await onboardStatusManager.recordStep(config.name, operationType, 'Saving feed configuration to repository', undefined, OnboardingProcessStatus.PR_CREATING);
404
+ const { feedFilePath, categorySchemasPath } = await onboardProcessor.saveFeedConfigToRepo(onboardingId, config, repoDir, serverList);
313
405
 
314
- await onboardStatusManager.updateStatus(onboardingId, { status: OnboardingProcessStatus.PR_CREATING, currentStep: 'Creating pull request' });
406
+ await onboardStatusManager.recordStep(config.name, operationType, 'Creating pull request', undefined, OnboardingProcessStatus.PR_CREATING);
315
407
  const prInfo = await onboardProcessor.createPullRequest(onboardingId, config, repoDir, branchName);
316
408
 
317
- await onboardStatusManager.updateStatus(onboardingId, {
318
- status: OnboardingProcessStatus.SUCCEEDED,
319
- currentStep: 'Successfully onboarded',
320
- errorMessage: undefined,
409
+ // Step 3: Finalize with SUCCEEDED status for PR creation
410
+ await onboardStatusManager.recordStep(config.name, operationType, prInfo.prExists ? 'Successfully updated to existing pull request' : 'Successfully created pull request', undefined, OnboardingProcessStatus.SUCCEEDED);
411
+ await onboardStatusManager.updateStatus(config.name, operationType, {
412
+ errorMessage: undefined, // Clear any previous errors
321
413
  prInfo: prInfo,
322
- result: prInfo.url
414
+ result: prInfo.url // Store PR URL as the result
323
415
  });
324
- Logger.log(`[${onboardingId}] Successfully onboarded feed: ${config.name}`);
416
+ Logger.log(`[${onboardingId}] Successfully created/updated PR for feed: ${config.name}. PR URL: ${prInfo.url}`);
417
+
418
+ // After successful PR, re-initialize providers
419
+ try {
420
+ await onboardStatusManager.recordStep(config.name, operationType, 'Reloading server schemas', undefined, OnboardingProcessStatus.SUCCEEDED);
421
+ Logger.log(`[${onboardingId}] Initializing ServerSchemaProvider with new schemas at ${categorySchemasPath}`);
422
+ await ServerSchemaProvider.getInstance().initialize(categorySchemasPath);
423
+ Logger.log(`[${onboardingId}] ServerSchemaProvider re-initialized.`);
424
+
425
+ await onboardStatusManager.recordStep(config.name, operationType, 'Reloading configurations', undefined, OnboardingProcessStatus.SUCCEEDED);
426
+ Logger.log(`[${onboardingId}] Initializing ConfigurationProvider with new feed file at ${feedFilePath}`);
427
+ await configProvider.initialize(feedFilePath, { prLink: prInfo.url, adhocServers: serverList }); // configProvider is already an instance
428
+ Logger.log(`[${onboardingId}] ConfigurationProvider re-initialized.`);
429
+
430
+ Logger.log(`[${onboardingId}] Successfully completed full onboarding for feed: ${config.name}`);
431
+ } catch (reinitError) {
432
+ Logger.warn(`[${onboardingId}] Failed to re-initialize providers after PR creation for feed ${config.name}:`);
433
+ Logger.warn(`Skipping re-initialization of providers due to error: ${reinitError}`,);
434
+ }
435
+
325
436
  } catch (error) {
326
- Logger.error(`[${onboardingId}] Full feed onboarding failed:`, error);
437
+ Logger.error(`[${onboardingId}] Full feed onboarding process failed:`, error);
327
438
  const errorMessage = error instanceof Error ? error.message : String(error);
328
- let currentStep = 'Failed during full onboarding process';
329
- if (error instanceof Error && 'step' in error) {
330
- currentStep = `Failed at step: ${error.step}`;
331
- } else if ((await onboardStatusManager.getStatus(onboardingId))?.currentStep?.includes('validation failed')) {
332
- throw error;
333
- }
334
439
 
335
- await onboardStatusManager.updateStatus(onboardingId, {
336
- status: OnboardingProcessStatus.FAILED,
337
- errorMessage: errorMessage,
338
- currentStep: currentStep,
339
- });
340
- throw error;
440
+ // Check if status is already FAILED (e.g., by _validateFeedConfiguration)
441
+ // to avoid overwriting a more specific error message or step from validation.
442
+ const currentStatus = await onboardStatusManager.getStatus(config.name, operationType);
443
+ if (currentStatus?.status !== OnboardingProcessStatus.FAILED) {
444
+ let stepName = 'Failed during full onboarding process';
445
+ if (error instanceof Error && 'step' in error && typeof error.step === 'string') {
446
+ stepName = `Failed at step: ${error.step}`;
447
+ } else if (currentStatus?.steps && currentStatus.steps.length > 0) {
448
+ // If not already failed by validation, use the last known good step before this error
449
+ stepName = `Failed after step: ${currentStatus.steps[currentStatus.steps.length - 1].stepName}`;
450
+ }
451
+ await onboardStatusManager.recordStep(
452
+ config.name,
453
+ operationType,
454
+ stepName,
455
+ undefined, // serverName
456
+ OnboardingProcessStatus.FAILED,
457
+ errorMessage
458
+ );
459
+ } else {
460
+ // If already FAILED, the error and step were likely recorded by _validateFeedConfiguration.
461
+ // Log the current error context if it's different or provides more info.
462
+ Logger.log(`[${onboardingId}] Process was already marked as FAILED. Additional context - Current error: ${errorMessage}. Initial error from status: ${currentStatus.errorMessage}`);
463
+ }
464
+ throw error; // Re-throw to ensure the promise from processFullOnboarding is rejected.
341
465
  }
342
466
  }
343
467
  }