Skip to content

Commit ee74311

Browse files
author
Lasim
committed
style(frontend): reorder source filter tabs and bulk actions toolbar
feat(frontend): add source filter for MCP server catalog feat(backend): add source filter for MCP server listing and queries feat(frontend): implement bulk delete functionality for MCP servers feat(backend): add bulk delete functionality for global MCP servers feat(frontend): implement server deletion functionality with confirmation dialog feat(frontend): add enable/disable functionality for MCP servers feat(backend): add 'disabled' status to MCP server management feat(backend): enhance cron job management with immediate job creation feat(backend): add support for 'disabled' status in MCP server management feat(frontend): enhance configuration schema handling in forms refactor(frontend): improve remote and package validation logic
1 parent e990f58 commit ee74311

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3069
-453
lines changed

services/backend/api-spec.json

Lines changed: 614 additions & 8 deletions
Large diffs are not rendered by default.

services/backend/api-spec.yaml

Lines changed: 432 additions & 0 deletions
Large diffs are not rendered by default.

services/backend/src/cron/cronManager.ts

Lines changed: 71 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,112 @@
11
import * as cron from 'node-cron';
22
import type { FastifyBaseLogger } from 'fastify';
3+
import type { JobQueueService } from '../services/jobQueueService';
34

45
/**
56
* CronJob interface - each cron job file must export this structure
7+
*
8+
* The CronManager will automatically create a job in the queue when the cron fires.
9+
* This ensures job records exist immediately for visibility in the admin panel,
10+
* even if something fails during job creation or execution.
611
*/
712
export interface CronJob {
813
/** Cron schedule expression (e.g., every 2 minutes, hourly, daily) */
914
schedule: string;
10-
11-
/** Optional job name for logging */
12-
name?: string;
13-
14-
/** The task function to execute */
15-
task: () => void | Promise<void>;
15+
16+
/** Job name for logging and identification (required) */
17+
name: string;
18+
19+
/** The job type that will be created in the queue (must match a registered worker) */
20+
jobType: string;
21+
22+
/**
23+
* Payload to pass to the worker. Can be a static object or a function that returns an object.
24+
* Use a function when payload needs to be computed at cron fire time.
25+
*/
26+
payload?: Record<string, unknown> | (() => Record<string, unknown>);
27+
28+
/** Maximum retry attempts for this job (default: 3) */
29+
maxAttempts?: number;
1630
}
1731

1832
/**
1933
* CronManager - Manages all cron jobs in the application
20-
*
34+
*
2135
* This class discovers, registers, and manages all cron jobs.
2236
* It provides lifecycle management including graceful shutdown.
37+
*
38+
* Key design: When a cron schedule fires, the CronManager immediately creates
39+
* a job record in the database queue. This ensures visibility in the admin panel
40+
* even if something fails during job creation or worker execution.
2341
*/
2442
export class CronManager {
2543
private jobs: Map<string, cron.ScheduledTask> = new Map();
26-
private logger: FastifyBaseLogger;
44+
private readonly logger: FastifyBaseLogger;
45+
private readonly jobQueueService: JobQueueService;
2746

28-
constructor(logger: FastifyBaseLogger) {
47+
constructor(logger: FastifyBaseLogger, jobQueueService: JobQueueService) {
2948
this.logger = logger;
49+
this.jobQueueService = jobQueueService;
3050
}
3151

3252
/**
3353
* Register a new cron job
34-
*
54+
*
55+
* When the cron schedule fires, this method immediately creates a job record
56+
* in the database queue. The job is then processed by the JobProcessorService.
57+
*
3558
* @param jobDefinition - The cron job definition
3659
*/
3760
register(jobDefinition: CronJob): void {
38-
const jobName = jobDefinition.name || 'unnamed-job';
61+
const { name: jobName, schedule, jobType, payload, maxAttempts } = jobDefinition;
3962

4063
try {
41-
const task = cron.schedule(jobDefinition.schedule, async () => {
42-
this.logger.info({
43-
job: jobName,
44-
schedule: jobDefinition.schedule
45-
}, 'Executing cron job');
64+
const task = cron.schedule(schedule, async () => {
65+
this.logger.info({
66+
job: jobName,
67+
jobType,
68+
schedule,
69+
}, 'Cron triggered, creating job in queue');
4670

4771
try {
48-
await jobDefinition.task();
49-
this.logger.debug({ job: jobName }, 'Cron job completed successfully');
72+
// Calculate payload - can be static object or function
73+
const resolvedPayload = typeof payload === 'function'
74+
? payload()
75+
: payload ?? {};
76+
77+
// Create job in queue immediately - this is the key change
78+
const job = await this.jobQueueService.createJob(
79+
jobType,
80+
resolvedPayload,
81+
maxAttempts ? { maxAttempts } : undefined
82+
);
83+
84+
this.logger.info({
85+
job: jobName,
86+
jobId: job.id,
87+
jobType,
88+
}, 'Cron job queued successfully');
5089
} catch (error) {
51-
this.logger.error({
52-
job: jobName,
53-
error
54-
}, 'Cron job execution failed');
90+
this.logger.error({
91+
job: jobName,
92+
jobType,
93+
error,
94+
}, 'Failed to queue cron job');
5595
}
5696
});
5797

5898
this.jobs.set(jobName, task);
59-
this.logger.info({
60-
job: jobName,
61-
schedule: jobDefinition.schedule
99+
this.logger.info({
100+
job: jobName,
101+
jobType,
102+
schedule,
62103
}, 'Cron job registered');
63-
64104
} catch (error) {
65-
this.logger.error({
66-
job: jobName,
67-
schedule: jobDefinition.schedule,
68-
error
105+
this.logger.error({
106+
job: jobName,
107+
jobType,
108+
schedule,
109+
error,
69110
}, 'Failed to register cron job');
70111
}
71112
}

services/backend/src/cron/index.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ import { createCleanupSatelliteHeartbeatsJob } from './jobs/cleanupSatelliteHear
99

1010
/**
1111
* Initialize and register all cron jobs
12-
*
12+
*
1313
* This function is called during server startup to set up all cron jobs.
1414
* Add new cron jobs by:
1515
* 1. Creating a job file in the jobs/ directory
1616
* 2. Importing it here
1717
* 3. Registering it with the CronManager
18-
*
18+
*
19+
* The CronManager handles job creation automatically when cron schedules fire.
20+
* Each cron job definition specifies the jobType and payload, and the CronManager
21+
* creates the job record in the queue immediately.
22+
*
1923
* @param jobQueueService - Job queue service for creating background jobs
2024
* @param logger - Logger instance
2125
* @returns CronManager instance
@@ -24,27 +28,27 @@ export function initializeCronJobs(
2428
jobQueueService: JobQueueService,
2529
logger: FastifyBaseLogger
2630
): CronManager {
27-
const cronManager = new CronManager(logger);
31+
const cronManager = new CronManager(logger, jobQueueService);
2832

2933
// MCP client activity metrics cleanup (every 30 minutes)
30-
cronManager.register(createMcpClientActivityMetricsCleanupJob(jobQueueService));
34+
cronManager.register(createMcpClientActivityMetricsCleanupJob());
3135

3236
// Cleanup old queue jobs (every 6 hours)
33-
cronManager.register(createCleanupOldJobsJob(jobQueueService));
37+
cronManager.register(createCleanupOldJobsJob());
3438

3539
// Refresh expiring OAuth tokens for MCP servers (every 5 minutes)
36-
cronManager.register(createRefreshOAuthTokensJob(jobQueueService));
40+
cronManager.register(createRefreshOAuthTokensJob());
3741

3842
// Cleanup satellite heartbeats (every 3 minutes)
39-
cronManager.register(createCleanupSatelliteHeartbeatsJob(jobQueueService));
43+
cronManager.register(createCleanupSatelliteHeartbeatsJob());
4044

4145
// Example cron job - commented out, uncomment to test
42-
// cronManager.register(createExampleCronJob(jobQueueService));
46+
// cronManager.register(createExampleCronJob());
4347

4448
// Add your cron jobs here
4549
// Example:
46-
// cronManager.register(createDailyBackupJob(jobQueueService));
47-
// cronManager.register(createHourlyCleanupJob(jobQueueService));
50+
// cronManager.register(createDailyBackupJob());
51+
// cronManager.register(createHourlyCleanupJob());
4852

4953
return cronManager;
5054
}
Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import type { CronJob } from '../cronManager';
2-
import type { JobQueueService } from '../../services/jobQueueService';
32

4-
export function createCleanupOldJobsJob(jobQueueService: JobQueueService): CronJob {
3+
/**
4+
* Cleanup Old Jobs Cron Job
5+
*
6+
* Runs every 6 hours to clean up old completed/failed jobs from the queue.
7+
*
8+
* Schedule: Every 6 hours
9+
* Job Type: cleanup_old_jobs
10+
* Retention: 14 days
11+
*/
12+
export function createCleanupOldJobsJob(): CronJob {
513
return {
614
name: 'cleanup-old-jobs',
7-
schedule: '0 */6 * * *', // Every 6 hours
8-
9-
task: async () => {
10-
await jobQueueService.createJob('cleanup_old_jobs', {
11-
olderThanDays: 14
12-
});
13-
}
15+
schedule: '0 */6 * * *',
16+
jobType: 'cleanup_old_jobs',
17+
payload: {
18+
olderThanDays: 14,
19+
},
1420
};
1521
}
Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import type { CronJob } from '../cronManager';
2-
import type { JobQueueService } from '../../services/jobQueueService';
32

43
/**
54
* Satellite Heartbeat Cleanup Cron Job
65
*
76
* Runs every 3 minutes to clean up old satellite heartbeat records.
87
* Different retention limits apply based on satellite type:
98
* - Global satellites: 1000 records (shared infrastructure, more aggressive cleanup)
10-
* - Team satellites: 500 records (dedicated infrastructure, shorter retention due to fewer satellites)
9+
* - Team satellites: 500 records (dedicated infrastructure, shorter retention)
1110
*
1211
* This cleanup was moved out of the heartbeat route to prevent
1312
* blocking the main event loop with expensive window function queries,
@@ -16,16 +15,14 @@ import type { JobQueueService } from '../../services/jobQueueService';
1615
* Schedule: Every 3 minutes
1716
* Job Type: cleanup_satellite_heartbeats
1817
*/
19-
export function createCleanupSatelliteHeartbeatsJob(jobQueueService: JobQueueService): CronJob {
20-
return {
21-
name: 'cleanup-satellite-heartbeats',
22-
schedule: '*/3 * * * *', // Every 3 minutes
23-
24-
task: async () => {
25-
await jobQueueService.createJob('cleanup_satellite_heartbeats', {
26-
globalSatelliteLimit: 1000,
27-
teamSatelliteLimit: 500,
28-
});
29-
},
30-
};
18+
export function createCleanupSatelliteHeartbeatsJob(): CronJob {
19+
return {
20+
name: 'cleanup-satellite-heartbeats',
21+
schedule: '*/3 * * * *',
22+
jobType: 'cleanup_satellite_heartbeats',
23+
payload: {
24+
globalSatelliteLimit: 1000,
25+
teamSatelliteLimit: 500,
26+
},
27+
};
3128
}
Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
import type { CronJob } from '../cronManager';
2-
import type { JobQueueService } from '../../services/jobQueueService';
32

43
/**
54
* Example Cron Job - Runs every 2 minutes
6-
*
7-
* This job demonstrates how to push work to the job queue system
8-
* from a cron schedule. Every 2 minutes, it creates a new job in
9-
* the queue which will be processed by the ExampleCronWorker.
10-
*
5+
*
6+
* This job demonstrates how to define a cron job that creates work
7+
* in the job queue system. Every 2 minutes, a job is created
8+
* which will be processed by the ExampleCronWorker.
9+
*
1110
* The worker will log: "hello from queue every 2min"
11+
*
12+
* To use this example:
13+
* 1. Uncomment the import and registration in cron/index.ts
14+
* 2. Uncomment the worker registration in workers/index.ts
1215
*/
13-
export function createExampleCronJob(jobQueueService: JobQueueService): CronJob {
16+
export function createExampleCronJob(): CronJob {
1417
return {
1518
name: 'example-every-2min',
16-
schedule: '*/2 * * * *', // Every 2 minutes
17-
18-
task: async () => {
19-
await jobQueueService.createJob('example_cron_job', {
20-
message: 'hello from queue every 2min'
21-
});
22-
}
19+
schedule: '*/2 * * * *',
20+
jobType: 'example_cron_job',
21+
payload: {
22+
message: 'hello from queue every 2min',
23+
},
2324
};
2425
}
Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,22 @@
11
import type { CronJob } from '../cronManager';
2-
import type { JobQueueService } from '../../services/jobQueueService';
32

43
/**
54
* MCP Client Activity Metrics Cleanup Cron Job
6-
*
5+
*
76
* Runs every 30 minutes to clean up old MCP client activity metrics data.
8-
*
9-
* This job creates a background job in the queue that will delete all
10-
* metric buckets from the mcpClientActivityMetrics table that are older
11-
* than 3 days (hardcoded retention period).
12-
*
13-
* Schedule: Every 30 minutes (*\/30 * * * *)
7+
*
8+
* This job deletes all metric buckets from the mcpClientActivityMetrics table
9+
* that are older than 3 days (hardcoded retention period in worker).
10+
*
11+
* Schedule: Every 30 minutes
1412
* Job Type: cleanup_mcp_client_activity_metrics
15-
* Retention: 3 days (hardcoded)
13+
* Retention: 3 days (hardcoded in worker)
1614
*/
17-
export function createMcpClientActivityMetricsCleanupJob(
18-
jobQueueService: JobQueueService
19-
): CronJob {
15+
export function createMcpClientActivityMetricsCleanupJob(): CronJob {
2016
return {
2117
name: 'mcp-client-activity-metrics-cleanup',
22-
schedule: '*/30 * * * *', // Every 30 minutes
23-
24-
task: async () => {
25-
// Create cleanup job in queue
26-
// Empty payload - retention period is hardcoded to 3 days in worker
27-
await jobQueueService.createJob('cleanup_mcp_client_activity_metrics', {});
28-
}
18+
schedule: '*/30 * * * *',
19+
jobType: 'cleanup_mcp_client_activity_metrics',
20+
// Empty payload - retention period is hardcoded in worker
2921
};
3022
}
Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,24 @@
11
import type { CronJob } from '../cronManager';
2-
import type { JobQueueService } from '../../services/jobQueueService';
32

43
/**
54
* OAuth Token Refresh Cron Job
65
*
76
* Runs every 5 minutes to refresh expiring OAuth tokens for MCP servers.
87
*
9-
* This job creates a background job in the queue that will:
8+
* The worker will:
109
* - Find tokens expiring within next 10 minutes
1110
* - Refresh them using their refresh_token
1211
* - Update encrypted tokens in database
1312
*
14-
* Schedule: Every 5 minutes (*\/5 * * * *)
13+
* Schedule: Every 5 minutes
1514
* Job Type: refresh_oauth_tokens
1615
* Threshold: 10 minutes before expiry
1716
*/
18-
export function createRefreshOAuthTokensJob(jobQueueService: JobQueueService): CronJob {
19-
return {
20-
name: 'refresh-oauth-tokens',
21-
schedule: '*/5 * * * *', // Every 5 minutes
22-
23-
task: async () => {
24-
// Create refresh job in queue
25-
// Worker will call refreshExpiringOAuthTokens() to process
26-
await jobQueueService.createJob('refresh_oauth_tokens', {});
27-
},
28-
};
17+
export function createRefreshOAuthTokensJob(): CronJob {
18+
return {
19+
name: 'refresh-oauth-tokens',
20+
schedule: '*/5 * * * *',
21+
jobType: 'refresh_oauth_tokens',
22+
// Empty payload - worker handles token discovery
23+
};
2924
}

0 commit comments

Comments
 (0)