imcp 0.0.17 → 0.0.18

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 (46) hide show
  1. package/dist/cli/commands/serve.js +2 -1
  2. package/dist/core/installers/clients/BaseClientInstaller.d.ts +25 -2
  3. package/dist/core/installers/clients/BaseClientInstaller.js +121 -0
  4. package/dist/core/installers/clients/ClineInstaller.d.ts +1 -6
  5. package/dist/core/installers/clients/ClineInstaller.js +1 -94
  6. package/dist/core/installers/clients/GithubCopilotInstaller.d.ts +1 -6
  7. package/dist/core/installers/clients/GithubCopilotInstaller.js +1 -94
  8. package/dist/core/installers/clients/MSRooCodeInstaller.d.ts +1 -5
  9. package/dist/core/installers/clients/MSRooCodeInstaller.js +1 -94
  10. package/dist/core/loaders/ConfigurationLoader.d.ts +4 -1
  11. package/dist/core/loaders/ConfigurationLoader.js +24 -3
  12. package/dist/core/loaders/ConfigurationProvider.d.ts +4 -1
  13. package/dist/core/loaders/ConfigurationProvider.js +13 -4
  14. package/dist/core/loaders/ServerSchemaLoader.d.ts +15 -4
  15. package/dist/core/loaders/ServerSchemaLoader.js +86 -20
  16. package/dist/core/loaders/ServerSchemaProvider.d.ts +2 -5
  17. package/dist/core/loaders/ServerSchemaProvider.js +32 -62
  18. package/dist/core/metadatas/types.d.ts +3 -2
  19. package/dist/core/onboard/FeedOnboardService.d.ts +14 -7
  20. package/dist/core/onboard/FeedOnboardService.js +214 -129
  21. package/dist/core/onboard/OnboardProcessor.d.ts +7 -1
  22. package/dist/core/onboard/OnboardProcessor.js +52 -8
  23. package/dist/core/onboard/OnboardStatus.d.ts +6 -1
  24. package/dist/core/onboard/OnboardStatusManager.d.ts +70 -24
  25. package/dist/core/onboard/OnboardStatusManager.js +230 -46
  26. package/dist/core/validators/FeedValidator.d.ts +7 -2
  27. package/dist/core/validators/FeedValidator.js +61 -7
  28. package/dist/core/validators/StdioServerValidator.js +84 -32
  29. package/dist/services/MCPManager.d.ts +2 -1
  30. package/dist/services/MCPManager.js +5 -1
  31. package/dist/services/ServerService.js +2 -2
  32. package/dist/utils/logger.d.ts +2 -0
  33. package/dist/utils/logger.js +10 -0
  34. package/dist/web/public/js/modal/installation.js +1 -1
  35. package/dist/web/public/js/onboard/ONBOARDING_PAGE_DESIGN.md +41 -9
  36. package/dist/web/public/js/onboard/formProcessor.js +200 -34
  37. package/dist/web/public/js/onboard/index.js +2 -2
  38. package/dist/web/public/js/onboard/publishHandler.js +30 -22
  39. package/dist/web/public/js/onboard/templates.js +34 -40
  40. package/dist/web/public/js/onboard/uiHandlers.js +175 -84
  41. package/dist/web/public/js/onboard/validationHandlers.js +147 -64
  42. package/dist/web/public/js/serverCategoryDetails.js +19 -4
  43. package/dist/web/public/js/serverCategoryList.js +13 -1
  44. package/dist/web/public/onboard.html +1 -1
  45. package/dist/web/server.js +19 -6
  46. package/package.json +1 -1
@@ -102,15 +102,57 @@ export class OnboardProcessor {
102
102
  * @param onboardingId The ID of the onboarding process.
103
103
  * @param config The feed configuration to save.
104
104
  * @param repoDir The root directory of the cloned repository.
105
+ * @param serverList The list of MCP server names to process.
106
+ * @returns A promise that resolves to an object containing the path to the saved feed file and the category-specific schemas directory.
105
107
  * @throws An error if saving the configuration fails, with a 'step' property set to 'saveFeedConfigToRepo'.
106
108
  */
107
- async saveFeedConfigToRepo(onboardingId, config, repoDir) {
109
+ async saveFeedConfigToRepo(onboardingId, config, repoDir, serverList) {
108
110
  try {
109
111
  const feedsDir = path.join(repoDir, GITHUB_REPO.feedsPath);
110
112
  await fs.mkdir(feedsDir, { recursive: true });
111
- const configPath = path.join(feedsDir, `${config.name}.json`);
112
- await fs.writeFile(configPath, JSON.stringify(config, null, 2));
113
- Logger.debug(`[${onboardingId}] Saved feed configuration to ${configPath}`);
113
+ const categoryName = config.name; // Feed name is the category name
114
+ const categorySchemasPath = path.join(feedsDir, 'schemas', categoryName);
115
+ await fs.mkdir(categorySchemasPath, { recursive: true }); // Ensure category schema directory exists
116
+ // Process schemas for each MCP server
117
+ // Create a mutable copy of mcpServers to update schema paths
118
+ const updatedMcpServers = [];
119
+ for (const server of config.mcpServers) {
120
+ if (!serverList.includes(server.name))
121
+ continue;
122
+ let updatedServer = { ...server }; // Shallow copy server config
123
+ if (updatedServer.schemas && typeof updatedServer.schemas === 'string' && updatedServer.schemas.trim() !== '') {
124
+ const originalSchemaPath = updatedServer.schemas;
125
+ const serverName = updatedServer.name;
126
+ const newSchemaFileName = `${serverName}.json`;
127
+ // Schemas are now directly under categorySchemasPath
128
+ const newSchemaPathInRepo = path.join(categorySchemasPath, newSchemaFileName);
129
+ try {
130
+ // Read content from the original schema path
131
+ const schemaContent = await fs.readFile(originalSchemaPath, 'utf-8');
132
+ // Write content to the new schema file in the repo
133
+ await fs.writeFile(newSchemaPathInRepo, schemaContent);
134
+ Logger.debug(`[${onboardingId}] Copied schema for server '${serverName}' from '${originalSchemaPath}' to '${newSchemaPathInRepo}'`);
135
+ // Update the schemas property to the new filename, as per instruction "rename schemas in McpServer as serverName].json"
136
+ updatedServer.schemas = newSchemaFileName;
137
+ }
138
+ catch (schemaError) {
139
+ const errorMsg = `Error processing schema for server '${serverName}' (source: ${originalSchemaPath}): ${schemaError instanceof Error ? schemaError.message : String(schemaError)}`;
140
+ Logger.error(`[${onboardingId}] ${errorMsg}`);
141
+ // Propagate the error to fail the onboarding step
142
+ throw new Error(errorMsg);
143
+ }
144
+ }
145
+ updatedMcpServers.push(updatedServer);
146
+ }
147
+ // Create a new config object with the updated mcpServers
148
+ const processedConfig = {
149
+ ...config,
150
+ mcpServers: updatedMcpServers
151
+ };
152
+ const feedFilePath = path.join(feedsDir, `${config.name}.json`);
153
+ await fs.writeFile(feedFilePath, JSON.stringify(processedConfig, null, 2));
154
+ Logger.debug(`[${onboardingId}] Saved feed configuration (with processed schemas) to ${feedFilePath}`);
155
+ return { feedFilePath, categorySchemasPath };
114
156
  }
115
157
  catch (error) {
116
158
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -149,7 +191,7 @@ export class OnboardProcessor {
149
191
  await execAsync('git fetch upstream');
150
192
  Logger.debug(`[${onboardingId}] Fetched from upstream.`);
151
193
  try {
152
- await execAsync('git checkout main');
194
+ await execAsync('git checkout main --force');
153
195
  Logger.debug(`[${onboardingId}] Switched to existing local branch 'main'.`);
154
196
  }
155
197
  catch (checkoutMainError) {
@@ -159,7 +201,7 @@ export class OnboardProcessor {
159
201
  errStdErr.includes('pathspec \'main\' did not match any file(s) known to git')) {
160
202
  Logger.debug(`[${onboardingId}] Local branch 'main' not found or invalid. Attempting to create it from 'upstream/main'.`);
161
203
  try {
162
- await execAsync('git checkout -b main upstream/main');
204
+ await execAsync('git checkout -b main --force upstream/main');
163
205
  Logger.debug(`[${onboardingId}] Created and switched to local branch 'main' from 'upstream/main'.`);
164
206
  }
165
207
  catch (createMainError) {
@@ -237,16 +279,18 @@ export class OnboardProcessor {
237
279
  const body = `Add new feed configuration:\n\n- Name: ${config.name}\n- Display Name: ${config.displayName}\n- Description: ${config.description}`;
238
280
  const username = await this.getGitHubUsername();
239
281
  const explicitHeadRef = `${username}:${branchName}`;
240
- const prCheckCommand = `gh pr list --repo ${GITHUB_REPO.repoName} --head "${explicitHeadRef}" --state open --json url --limit 1`;
282
+ const prCheckCommand = `gh pr list --repo ${GITHUB_REPO.repoName} --head ${branchName} --author ${username} --state open --json url --limit 1`;
241
283
  Logger.debug(`[${onboardingId}] Checking for existing PR with command: ${prCheckCommand}`);
242
284
  const { stdout: existingPrJson } = await execAsync(prCheckCommand);
243
285
  let prUrlToReturn;
244
286
  const createPRCommand = `gh pr create --repo ${GITHUB_REPO.repoName} --base main --head ${explicitHeadRef} --title "${title}" --body "${body}"`;
287
+ let prExists = false;
245
288
  if (existingPrJson && existingPrJson.trim() !== '[]' && existingPrJson.trim() !== '') {
246
289
  try {
247
290
  const prs = JSON.parse(existingPrJson);
248
291
  if (prs.length > 0 && prs[0].url) {
249
292
  prUrlToReturn = prs[0].url;
293
+ prExists = true;
250
294
  Logger.log(`[${onboardingId}] Found existing open PR for ${explicitHeadRef} on ${GITHUB_REPO.repoName}: ${prUrlToReturn}`);
251
295
  }
252
296
  else {
@@ -267,7 +311,7 @@ export class OnboardProcessor {
267
311
  prUrlToReturn = prUrl.trim();
268
312
  }
269
313
  Logger.debug(`[${onboardingId}] Pull request operation completed. PR URL: ${prUrlToReturn}`);
270
- return { url: prUrlToReturn, branchName };
314
+ return { url: prUrlToReturn, branchName: branchName, prExists: prExists };
271
315
  }
272
316
  catch (error) {
273
317
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -16,7 +16,12 @@ export interface ServerValidationResult {
16
16
  export interface OnboardStatus {
17
17
  onboardingId: string;
18
18
  status: OnboardingProcessStatus;
19
- currentStep?: string;
19
+ steps?: {
20
+ stepName: string;
21
+ timestamp: string;
22
+ status?: OnboardingProcessStatus;
23
+ errorMessage?: string;
24
+ }[];
20
25
  validationStatus?: {
21
26
  isValid: boolean;
22
27
  message?: string;
@@ -1,5 +1,5 @@
1
1
  import { FeedConfiguration } from '../metadatas/types.js';
2
- import { OnboardStatus, OperationType } from './OnboardStatus.js';
2
+ import { OnboardStatus, OnboardingProcessStatus, OperationType } from './OnboardStatus.js';
3
3
  export declare class OnboardStatusManager {
4
4
  private static instance;
5
5
  private activeCategoryOperations;
@@ -15,43 +15,89 @@ export declare class OnboardStatusManager {
15
15
  * @param categoryName The name of the category (acting as operationId).
16
16
  * @param config The FeedConfiguration to save.
17
17
  */
18
- saveFeedConfiguration(categoryName: string, config: FeedConfiguration): Promise<void>;
18
+ saveFeedConfiguration(categoryName: string, operationType: OperationType, config: FeedConfiguration): Promise<void>;
19
19
  /**
20
20
  * Retrieves the feed configuration associated with a category's active operation.
21
21
  * @param categoryName The name of the category (acting as operationId).
22
22
  * @returns A promise that resolves to the FeedConfiguration, or undefined if not found.
23
23
  */
24
- getFeedConfiguration(categoryName: string): Promise<FeedConfiguration | undefined>;
24
+ getFeedConfiguration(categoryName: string, operationType: OperationType): Promise<FeedConfiguration | undefined>;
25
25
  /**
26
- * Retrieves the status of a category's active operation.
27
- * @param categoryName The name of the category (acting as operationId).
28
- * @returns A promise that resolves to the OnboardStatus, or undefined if no active operation.
26
+ * Retrieves the status of a specific operation for a category.
27
+ * @param categoryName The name of the category.
28
+ * @param operationType The type of operation.
29
+ * @returns A promise that resolves to the OnboardStatus, or undefined if no such operation or category.
29
30
  */
30
- getStatus(categoryName: string): Promise<OnboardStatus | undefined>;
31
+ getStatus(categoryName: string, operationType: OperationType): Promise<OnboardStatus | undefined>;
31
32
  /**
32
- * Retrieves all active category operation statuses.
33
- * @returns A promise that resolves to a record of all active OnboardStatus objects, keyed by categoryName.
33
+ * Retrieves all active category operation statuses, structured by category and then by operation type.
34
+ * @returns A promise that resolves to a record of all active OnboardStatus objects.
34
35
  */
35
- getAllStatuses(): Promise<Record<string, OnboardStatus>>;
36
+ getAllStatuses(): Promise<Record<string, Partial<Record<OperationType, OnboardStatus>>>>;
36
37
  /**
37
- * Updates the status of a category's active operation.
38
- * If no operation exists for the category, it might create one based on the updates,
39
- * or fail if `onboardingId` (categoryName) is not present in `newStatus`.
40
- * For clarity, it's better if `createInitialStatus` is called first.
41
- * @param categoryName The name of the category (acting as operationId).
42
- * @param updates Partial OnboardStatus object with fields to update.
38
+ * Updates the status of a specific operation for a category.
39
+ * If a `stepName` is provided in `updates`, it will also add a new step.
40
+ * @param categoryName The name of the category.
41
+ * @param operationType The type of operation.
42
+ * @param updates Partial OnboardStatus object with fields to update. Can include `stepName` to record a new step.
43
+ * @param stepName Optional. If provided, a new step with this name will be recorded.
44
+ * @param stepStatus Optional. Status for the new step, if `stepName` is provided.
45
+ * @param errorMessage Optional. Error message for the new step, if `stepName` is provided.
43
46
  * @returns A promise that resolves to the updated OnboardStatus.
44
47
  */
45
- updateStatus(categoryName: string, updates: Partial<OnboardStatus>): Promise<OnboardStatus>;
48
+ updateStatus(categoryName: string, operationType: OperationType, updates: Partial<Omit<OnboardStatus, 'steps'>>, // Exclude steps from direct updates here
49
+ stepName?: string, stepStatus?: OnboardingProcessStatus, errorMessage?: string): Promise<OnboardStatus>;
46
50
  /**
47
- * Creates an initial status for a new category-wide operation.
48
- * The operation is identified by the categoryName.
49
- * @param categoryName The name of the category (also used as feedName and operationId).
50
- * @param operationType The type of operation (e.g., FULL_ONBOARDING, VALIDATION_ONLY).
51
- * @param serverName Optional: if the operation has a specific server context (e.g. validating a single new server in an existing category).
52
- * However, the primary operation tracking is still by categoryName.
53
- * @returns A promise that resolves to the initial OnboardStatus.
51
+ * A more specific method to record a step, which can also update the overall status.
52
+ * This is intended to replace the local `updateStatus` functions in validators/services.
53
+ * @param categoryName The name of the category (e.g., feed name).
54
+ * @param operationType The type of operation.
55
+ * @param stepName The description of the current step.
56
+ * @param serverName Optional server context for this step.
57
+ * @param newStatus Optional overall status to set for the operation.
58
+ * @param errorMessage Optional error message if this step resulted in a failure.
59
+ * @returns A promise that resolves to the updated OnboardStatus.
54
60
  */
61
+ recordStep(categoryName: string, operationType: OperationType, stepName: string, serverName?: string, // Added serverName to be part of the step if relevant
62
+ newStatus?: OnboardingProcessStatus, errorMessage?: string): Promise<OnboardStatus>;
63
+ /**
64
+ * Creates an operation ID that combines category name and operation type.
65
+ * @param categoryName The name of the category.
66
+ * @param operationType The type of operation.
67
+ * @returns Combined operation ID string.
68
+ */
69
+ private createOperationId;
70
+ /**
71
+ * Creates an initial status for a new category-wide operation.
72
+ * The operation is identified by the categoryName.
73
+ * @param categoryName The name of the category (also used as feedName and operationId).
74
+ * @param operationType The type of operation (e.g., FULL_ONBOARDING, VALIDATION_ONLY).
75
+ * @param serverName Optional: if the operation has a specific server context (e.g. validating a single new server in an existing category).
76
+ * However, the primary operation tracking is still by categoryName.
77
+ * @returns A promise that resolves to the initial OnboardStatus.
78
+ */
55
79
  createInitialStatus(categoryName: string, operationType: OperationType, serverName?: string): Promise<OnboardStatus>;
80
+ /**
81
+ * Finds a succeeded operation for a given feed name and operation type that has a matching feed configuration.
82
+ * @param feedName The name of the feed.
83
+ * @param operationType The type of operation.
84
+ * @param currentConfig The current feed configuration to compare against.
85
+ * @returns A promise that resolves to the OnboardStatus of the succeeded operation, or undefined if not found.
86
+ */
87
+ findSucceededOperation(feedName: string, operationType: OperationType, currentConfig: FeedConfiguration): Promise<OnboardStatus | undefined>;
88
+ /**
89
+ * Compares two feed configurations for equality.
90
+ * @param config1 First feed configuration.
91
+ * @param config2 Second feed configuration.
92
+ * @returns True if the configurations are identical, false otherwise.
93
+ */
94
+ private isFeedConfigurationEqual;
95
+ /**
96
+ * Finds an existing non-completed operation for a given feed name, server name, and operation type.
97
+ * @param feedName The name of the feed.
98
+ * @param operationType The type of operation.
99
+ * @returns A promise that resolves to the OnboardStatus of the existing operation, or undefined if not found.
100
+ */
101
+ _findExistingNonCompletedOperation(feedName: string, operationType: OperationType): Promise<OnboardStatus | undefined>;
56
102
  }
57
103
  export declare const onboardStatusManager: OnboardStatusManager;
@@ -4,8 +4,14 @@ import { SETTINGS_DIR } from '../metadatas/constants.js';
4
4
  import { OnboardingProcessStatus } from './OnboardStatus.js';
5
5
  import { Logger } from '../../utils/logger.js';
6
6
  const ONBOARD_STATUS_DIR = path.join(SETTINGS_DIR, 'onboard');
7
- const CATEGORY_OPERATIONS_STATUS_FILE = path.join(ONBOARD_STATUS_DIR, 'category_operations_status.json');
7
+ const CATEGORY_OPERATIONS_STATUS_FILE = path.join(ONBOARD_STATUS_DIR, 'OnboardStatus.json');
8
8
  const FEED_CONFIG_DIR = path.join(ONBOARD_STATUS_DIR, 'feed_configs'); // Staging for feed configs during operation
9
+ const NON_COMPLETED_ONBOARDING_STATUSES = [
10
+ OnboardingProcessStatus.PENDING,
11
+ OnboardingProcessStatus.VALIDATING,
12
+ OnboardingProcessStatus.VALIDATED,
13
+ OnboardingProcessStatus.PR_CREATING,
14
+ ];
9
15
  export class OnboardStatusManager {
10
16
  static instance;
11
17
  activeCategoryOperations = {};
@@ -61,12 +67,13 @@ export class OnboardStatusManager {
61
67
  * @param categoryName The name of the category (acting as operationId).
62
68
  * @param config The FeedConfiguration to save.
63
69
  */
64
- async saveFeedConfiguration(categoryName, config) {
70
+ async saveFeedConfiguration(categoryName, operationType, config) {
65
71
  await this.withLock(async () => {
66
72
  try {
67
73
  await fs.mkdir(FEED_CONFIG_DIR, { recursive: true });
68
74
  // Suffix `_feed.json` to distinguish from potential status files if names overlap
69
- const configPath = path.join(FEED_CONFIG_DIR, `${categoryName}_feed.json`);
75
+ const operationId = this.createOperationId(categoryName, operationType);
76
+ const configPath = path.join(FEED_CONFIG_DIR, `${operationId}_feed.json`);
70
77
  await fs.writeFile(configPath, JSON.stringify(config, null, 2));
71
78
  Logger.debug(`Saved feed configuration for category ${categoryName} to ${configPath}`);
72
79
  }
@@ -81,10 +88,11 @@ export class OnboardStatusManager {
81
88
  * @param categoryName The name of the category (acting as operationId).
82
89
  * @returns A promise that resolves to the FeedConfiguration, or undefined if not found.
83
90
  */
84
- async getFeedConfiguration(categoryName) {
91
+ async getFeedConfiguration(categoryName, operationType) {
85
92
  return await this.withLock(async () => {
86
93
  try {
87
- const configPath = path.join(FEED_CONFIG_DIR, `${categoryName}_feed.json`);
94
+ const operationId = this.createOperationId(categoryName, operationType);
95
+ const configPath = path.join(FEED_CONFIG_DIR, `${operationId}_feed.json`);
88
96
  const data = await fs.readFile(configPath, 'utf-8');
89
97
  return JSON.parse(data);
90
98
  }
@@ -99,76 +107,252 @@ export class OnboardStatusManager {
99
107
  });
100
108
  }
101
109
  /**
102
- * Retrieves the status of a category's active operation.
103
- * @param categoryName The name of the category (acting as operationId).
104
- * @returns A promise that resolves to the OnboardStatus, or undefined if no active operation.
110
+ * Retrieves the status of a specific operation for a category.
111
+ * @param categoryName The name of the category.
112
+ * @param operationType The type of operation.
113
+ * @returns A promise that resolves to the OnboardStatus, or undefined if no such operation or category.
105
114
  */
106
- async getStatus(categoryName) {
107
- await this.loadStatuses(); // Ensure latest statuses are loaded (though constructor does it initially)
108
- return this.activeCategoryOperations[categoryName];
115
+ async getStatus(categoryName, operationType) {
116
+ await this.loadStatuses(); // Ensure latest statuses are loaded
117
+ return this.activeCategoryOperations[categoryName]?.[operationType];
109
118
  }
110
119
  /**
111
- * Retrieves all active category operation statuses.
112
- * @returns A promise that resolves to a record of all active OnboardStatus objects, keyed by categoryName.
120
+ * Retrieves all active category operation statuses, structured by category and then by operation type.
121
+ * @returns A promise that resolves to a record of all active OnboardStatus objects.
113
122
  */
114
123
  async getAllStatuses() {
115
124
  await this.loadStatuses();
116
125
  return this.activeCategoryOperations;
117
126
  }
118
127
  /**
119
- * Updates the status of a category's active operation.
120
- * If no operation exists for the category, it might create one based on the updates,
121
- * or fail if `onboardingId` (categoryName) is not present in `newStatus`.
122
- * For clarity, it's better if `createInitialStatus` is called first.
123
- * @param categoryName The name of the category (acting as operationId).
124
- * @param updates Partial OnboardStatus object with fields to update.
128
+ * Updates the status of a specific operation for a category.
129
+ * If a `stepName` is provided in `updates`, it will also add a new step.
130
+ * @param categoryName The name of the category.
131
+ * @param operationType The type of operation.
132
+ * @param updates Partial OnboardStatus object with fields to update. Can include `stepName` to record a new step.
133
+ * @param stepName Optional. If provided, a new step with this name will be recorded.
134
+ * @param stepStatus Optional. Status for the new step, if `stepName` is provided.
135
+ * @param errorMessage Optional. Error message for the new step, if `stepName` is provided.
125
136
  * @returns A promise that resolves to the updated OnboardStatus.
126
137
  */
127
- async updateStatus(categoryName, updates) {
138
+ async updateStatus(categoryName, operationType, updates, // Exclude steps from direct updates here
139
+ stepName, stepStatus, errorMessage) {
128
140
  return await this.withLock(async () => {
129
- const existingStatus = this.activeCategoryOperations[categoryName] || {
130
- onboardingId: categoryName, // Ensure onboardingId is set to categoryName if creating new
131
- feedName: categoryName, // Ensure feedName is set to categoryName
132
- status: OnboardingProcessStatus.PENDING, // Default status if creating new
133
- lastUpdated: new Date().toISOString(),
134
- operationType: updates.operationType || 'FULL_ONBOARDING', // Default operation type
135
- ...updates // Apply initial updates if creating new
136
- };
137
- const updatedStatus = {
141
+ const operationId = this.createOperationId(categoryName, operationType);
142
+ const categoryOps = this.activeCategoryOperations[categoryName] || {};
143
+ let existingStatus = categoryOps[operationType];
144
+ if (!existingStatus) {
145
+ // If no status exists, create an initial one.
146
+ // This might happen if updateStatus is called before createInitialStatus,
147
+ // though ideally createInitialStatus should be the entry point.
148
+ existingStatus = {
149
+ onboardingId: operationId,
150
+ feedName: categoryName,
151
+ status: updates.status || OnboardingProcessStatus.PENDING,
152
+ lastUpdated: new Date().toISOString(),
153
+ operationType: operationType,
154
+ steps: [],
155
+ };
156
+ }
157
+ // Apply general updates
158
+ let updatedStatus = {
138
159
  ...existingStatus,
139
160
  ...updates,
161
+ onboardingId: operationId,
162
+ feedName: categoryName,
163
+ operationType: operationType,
140
164
  lastUpdated: new Date().toISOString(),
141
165
  };
142
- this.activeCategoryOperations[categoryName] = updatedStatus;
166
+ // If stepName is provided, add it as a new step
167
+ if (stepName) {
168
+ const newStep = {
169
+ stepName,
170
+ timestamp: new Date().toISOString(),
171
+ status: stepStatus ?? updates.status ?? existingStatus.status,
172
+ errorMessage: errorMessage ?? updates.errorMessage,
173
+ };
174
+ updatedStatus.steps = [...(existingStatus.steps || []), newStep];
175
+ // If the step has a specific status, it might also update the overall status
176
+ if (newStep.status && newStep.status !== updatedStatus.status) {
177
+ updatedStatus.status = newStep.status;
178
+ }
179
+ if (newStep.errorMessage && !updatedStatus.errorMessage) {
180
+ updatedStatus.errorMessage = newStep.errorMessage;
181
+ }
182
+ }
183
+ categoryOps[operationType] = updatedStatus;
184
+ this.activeCategoryOperations[categoryName] = categoryOps;
143
185
  await this.saveStatuses();
144
186
  return updatedStatus;
145
187
  });
146
188
  }
147
189
  /**
148
- * Creates an initial status for a new category-wide operation.
149
- * The operation is identified by the categoryName.
150
- * @param categoryName The name of the category (also used as feedName and operationId).
151
- * @param operationType The type of operation (e.g., FULL_ONBOARDING, VALIDATION_ONLY).
152
- * @param serverName Optional: if the operation has a specific server context (e.g. validating a single new server in an existing category).
153
- * However, the primary operation tracking is still by categoryName.
154
- * @returns A promise that resolves to the initial OnboardStatus.
190
+ * A more specific method to record a step, which can also update the overall status.
191
+ * This is intended to replace the local `updateStatus` functions in validators/services.
192
+ * @param categoryName The name of the category (e.g., feed name).
193
+ * @param operationType The type of operation.
194
+ * @param stepName The description of the current step.
195
+ * @param serverName Optional server context for this step.
196
+ * @param newStatus Optional overall status to set for the operation.
197
+ * @param errorMessage Optional error message if this step resulted in a failure.
198
+ * @returns A promise that resolves to the updated OnboardStatus.
155
199
  */
200
+ async recordStep(categoryName, operationType, stepName, serverName, // Added serverName to be part of the step if relevant
201
+ newStatus, errorMessage) {
202
+ return await this.withLock(async () => {
203
+ const operationId = this.createOperationId(categoryName, operationType);
204
+ const categoryOps = this.activeCategoryOperations[categoryName] || {};
205
+ let currentStatus = categoryOps[operationType];
206
+ if (!currentStatus) {
207
+ currentStatus = await this.createInitialStatus(categoryName, operationType, serverName);
208
+ }
209
+ const stepToAdd = {
210
+ stepName,
211
+ timestamp: new Date().toISOString(),
212
+ status: newStatus ?? (errorMessage ? OnboardingProcessStatus.FAILED : currentStatus.status), // Step inherits current or becomes FAILED
213
+ errorMessage: errorMessage,
214
+ ...(serverName && { serverName }), // Include serverName in the step if provided
215
+ };
216
+ const updatedSteps = [...(currentStatus.steps || []), stepToAdd];
217
+ const statusUpdatePayload = {
218
+ steps: updatedSteps,
219
+ lastUpdated: new Date().toISOString(),
220
+ };
221
+ if (newStatus) {
222
+ statusUpdatePayload.status = newStatus;
223
+ }
224
+ else if (errorMessage && currentStatus.status !== OnboardingProcessStatus.FAILED) {
225
+ // If an error occurs and we're not already FAILED, mark as FAILED.
226
+ statusUpdatePayload.status = OnboardingProcessStatus.FAILED;
227
+ }
228
+ if (errorMessage && !currentStatus.errorMessage) { // Store the first error message
229
+ statusUpdatePayload.errorMessage = errorMessage;
230
+ }
231
+ if (serverName && !currentStatus.serverName) { // Store serverName if not already set at top level
232
+ statusUpdatePayload.serverName = serverName;
233
+ }
234
+ const updatedStatus = {
235
+ ...currentStatus,
236
+ ...statusUpdatePayload,
237
+ };
238
+ categoryOps[operationType] = updatedStatus;
239
+ this.activeCategoryOperations[categoryName] = categoryOps;
240
+ await this.saveStatuses();
241
+ return updatedStatus;
242
+ });
243
+ }
244
+ /**
245
+ * Creates an operation ID that combines category name and operation type.
246
+ * @param categoryName The name of the category.
247
+ * @param operationType The type of operation.
248
+ * @returns Combined operation ID string.
249
+ */
250
+ createOperationId(categoryName, operationType) {
251
+ return `${categoryName}_${operationType}`;
252
+ }
253
+ /**
254
+ * Creates an initial status for a new category-wide operation.
255
+ * The operation is identified by the categoryName.
256
+ * @param categoryName The name of the category (also used as feedName and operationId).
257
+ * @param operationType The type of operation (e.g., FULL_ONBOARDING, VALIDATION_ONLY).
258
+ * @param serverName Optional: if the operation has a specific server context (e.g. validating a single new server in an existing category).
259
+ * However, the primary operation tracking is still by categoryName.
260
+ * @returns A promise that resolves to the initial OnboardStatus.
261
+ */
156
262
  async createInitialStatus(categoryName, operationType, serverName) {
157
- // The operationId is the categoryName itself for category-wide operations.
158
- const operationId = categoryName;
263
+ // Create operation ID that includes both category and operation type
264
+ const operationId = this.createOperationId(categoryName, operationType);
159
265
  const initialStatus = {
160
- onboardingId: operationId, // This is the categoryName
161
- feedName: categoryName, // Feed name is the category name
162
- serverName, // Optional server context
266
+ onboardingId: operationId,
267
+ feedName: categoryName,
268
+ serverName,
163
269
  status: OnboardingProcessStatus.PENDING,
164
- currentStep: 'Initiated',
165
- lastUpdated: new Date().toISOString(), // timestamp was an error, OnboardStatus uses lastUpdated
270
+ steps: [{ stepName: 'Initiated', timestamp: new Date().toISOString() }],
271
+ lastUpdated: new Date().toISOString(),
166
272
  operationType,
167
273
  errorMessage: undefined,
168
274
  prInfo: undefined
169
275
  };
170
- // Use updateStatus to ensure it's saved correctly and lock is handled
171
- return this.updateStatus(operationId, initialStatus);
276
+ // Save directly without using updateStatus, into the new structure
277
+ await this.withLock(async () => {
278
+ const categoryOps = this.activeCategoryOperations[categoryName] || {};
279
+ categoryOps[operationType] = initialStatus;
280
+ this.activeCategoryOperations[categoryName] = categoryOps;
281
+ await this.saveStatuses();
282
+ });
283
+ return initialStatus;
284
+ }
285
+ /**
286
+ * Finds a succeeded operation for a given feed name and operation type that has a matching feed configuration.
287
+ * @param feedName The name of the feed.
288
+ * @param operationType The type of operation.
289
+ * @param currentConfig The current feed configuration to compare against.
290
+ * @returns A promise that resolves to the OnboardStatus of the succeeded operation, or undefined if not found.
291
+ */
292
+ async findSucceededOperation(feedName, operationType, currentConfig) {
293
+ return await this.withLock(async () => {
294
+ for (const categoryName in this.activeCategoryOperations) {
295
+ const operations = this.activeCategoryOperations[categoryName];
296
+ if (operations) {
297
+ const specificOperationStatus = operations[operationType];
298
+ if (specificOperationStatus &&
299
+ specificOperationStatus.feedName === feedName &&
300
+ specificOperationStatus.status === OnboardingProcessStatus.SUCCEEDED) {
301
+ // The FeedConfiguration is stored in the result of the SUCCEEDED operation
302
+ const savedConfig = specificOperationStatus.result?.feedConfiguration;
303
+ if (savedConfig && this.isFeedConfigurationEqual(savedConfig, currentConfig)) {
304
+ return specificOperationStatus;
305
+ }
306
+ }
307
+ }
308
+ }
309
+ return undefined;
310
+ });
311
+ }
312
+ /**
313
+ * Compares two feed configurations for equality.
314
+ * @param config1 First feed configuration.
315
+ * @param config2 Second feed configuration.
316
+ * @returns True if the configurations are identical, false otherwise.
317
+ */
318
+ isFeedConfigurationEqual(config1, config2) {
319
+ // Compare basic properties
320
+ if (config1.name !== config2.name)
321
+ return false;
322
+ // Compare arrays with specific order handling
323
+ const compareArrays = (arr1, arr2, sortFn) => {
324
+ if (arr1.length !== arr2.length)
325
+ return false;
326
+ const sorted1 = sortFn ? [...arr1].sort(sortFn) : arr1;
327
+ const sorted2 = sortFn ? [...arr2].sort(sortFn) : arr2;
328
+ return JSON.stringify(sorted1) === JSON.stringify(sorted2);
329
+ };
330
+ // Compare requirements array
331
+ if (!compareArrays(config1.requirements || [], config2.requirements || []))
332
+ return false;
333
+ // Compare MCP servers with name-based sorting
334
+ const sortByName = (a, b) => (a.name || '').localeCompare(b.name || '');
335
+ if (!compareArrays(config1.mcpServers || [], config2.mcpServers || [], sortByName))
336
+ return false;
337
+ return true;
338
+ }
339
+ /**
340
+ * Finds an existing non-completed operation for a given feed name, server name, and operation type.
341
+ * @param feedName The name of the feed.
342
+ * @param operationType The type of operation.
343
+ * @returns A promise that resolves to the OnboardStatus of the existing operation, or undefined if not found.
344
+ */
345
+ async _findExistingNonCompletedOperation(feedName, operationType) {
346
+ const allCategoryStatuses = await this.getAllStatuses();
347
+ // feedName is the categoryName
348
+ const categoryOperations = allCategoryStatuses[feedName];
349
+ if (categoryOperations) {
350
+ const specificOperationStatus = categoryOperations[operationType];
351
+ if (specificOperationStatus && NON_COMPLETED_ONBOARDING_STATUSES.includes(specificOperationStatus.status)) {
352
+ return specificOperationStatus;
353
+ }
354
+ }
355
+ return undefined;
172
356
  }
173
357
  }
174
358
  // Export singleton instance
@@ -1,4 +1,5 @@
1
1
  import { FeedConfiguration, McpConfig } from "../metadatas/types.js";
2
+ import { OperationType } from "../onboard/OnboardStatus.js";
2
3
  /**
3
4
  * Validates feed configurations to ensure they meet required criteria
4
5
  */
@@ -6,15 +7,19 @@ export declare class FeedValidator {
6
7
  /**
7
8
  * Validates a feed configuration
8
9
  * @param config The feed configuration to validate
10
+ * @param categoryName The name of the category (feed name) for status updates
11
+ * @param operationType The type of operation for status updates
9
12
  * @returns true if valid, throws error if invalid
10
13
  */
11
- validate(config: FeedConfiguration): boolean;
14
+ validate(config: FeedConfiguration, categoryName: string, operationType: OperationType): Promise<boolean>;
12
15
  /**
13
16
  * Validates a single MCP server configuration using the appropriate validator
14
17
  * @param server The MCP server configuration to validate
15
18
  * @param config The feed configuration containing shared requirements
19
+ * @param categoryName The name of the category (feed name) for status updates
20
+ * @param operationType The type of operation for status updates
16
21
  * @returns true if valid, throws error if invalid
17
22
  */
18
- validateServer(server: McpConfig, config: FeedConfiguration): Promise<boolean>;
23
+ validateServer(server: McpConfig, config: FeedConfiguration, categoryName: string, operationType: OperationType): Promise<boolean>;
19
24
  }
20
25
  export declare const feedValidator: FeedValidator;