UNPKG

reminder-mcp

Version:

A mcp server for scheduling and triggering reminders

137 lines (136 loc) 5.45 kB
import { env } from '../env.js'; import { DeletionType, RequestMethod, } from '../types/index.js'; import { NotificationPlatform } from '../types/notification-platform.js'; import { CRON_JOB_API_BASE, makeCronJobRequest } from './cron-job-api.js'; export class ReminderService { /** * Fetch all reminders (jobs) from the Cron Job API. */ async getAllReminders() { const allJobsEndpoint = `${CRON_JOB_API_BASE}/jobs`; const allJobsData = await makeCronJobRequest(allJobsEndpoint); if (!allJobsData) { throw new Error('Failed to retrieve reminders data'); } return allJobsData.jobs || []; } /** * Create a new reminder job. * @param title - The reminder title * @param datetime - The date and time to trigger the reminder * @returns The created job's ID */ async createReminder(title, datetime) { this.validateReminderInput(title, datetime); const createJobEndpoint = `${CRON_JOB_API_BASE}/jobs`; const schedule = this.buildJobSchedule(datetime); const { url, requestBody } = this.getPlatformPayload(title); const job = { enabled: true, title, url, schedule, requestMethod: RequestMethod.POST, saveResponses: true, extendedData: { headers: { 'Content-Type': 'application/json' }, body: requestBody, }, }; const createJobData = await makeCronJobRequest(createJobEndpoint, 'PUT', { job }); if (!createJobData?.jobId) { throw new Error('Failed to create reminder'); } return createJobData.jobId; } /** * Delete reminders by type (title, processed, or all) * @param deletionType - The type of deletion * @param title - The title to match (if deletionType is Title) * @returns The number of deleted jobs */ async deleteReminders(deletionType, title) { const jobs = await this.getAllReminders(); const jobsToDelete = this.filterJobsForDeletion(jobs, deletionType, title); if (jobsToDelete.length === 0) return 0; let deletedCount = 0; for (const job of jobsToDelete) { if (!job.jobId) continue; const deleteJobEndpoint = `${CRON_JOB_API_BASE}/jobs/${job.jobId}`; const result = await makeCronJobRequest(deleteJobEndpoint, 'DELETE'); if (result) deletedCount++; } return deletedCount; } /** * Validate reminder input parameters. */ validateReminderInput(title, datetime) { if (!title || typeof title !== 'string') { throw new Error('Reminder title must be a non-empty string.'); } const now = new Date(); if (datetime.getTime() - now.getTime() < 1.5 * 60 * 1000) { throw new Error('Reminders must be scheduled at least 2 minutes in the future.'); } } /** * Build the job schedule object for the Cron Job API. */ buildJobSchedule(datetime) { const expiresAtDate = new Date(datetime.getTime() + 5 * 60 * 1000); // 5 minutes after trigger const pad = (n) => n.toString().padStart(2, '0'); const expiresAt = parseInt(`${expiresAtDate.getFullYear()}${pad(expiresAtDate.getMonth() + 1)}${pad(expiresAtDate.getDate())}${pad(expiresAtDate.getHours())}${pad(expiresAtDate.getMinutes())}${pad(expiresAtDate.getSeconds())}`); return { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, hours: [datetime.getHours()], mdays: [datetime.getDate()], minutes: [datetime.getMinutes()], months: [datetime.getMonth() + 1], wdays: [-1], expiresAt, }; } /** * Get the notification platform payload (URL and request body). */ getPlatformPayload(title) { switch (env.NOTIFICATION_PLATFORM) { case NotificationPlatform.SLACK: return { url: env.SLACK_WEBHOOK_URL, requestBody: JSON.stringify({ text: `🔔 Reminder: ${title}` }), }; case NotificationPlatform.TELEGRAM: return { url: `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`, requestBody: JSON.stringify({ chat_id: env.TELEGRAM_CHAT_ID, text: `🔔 Reminder: ${title}`, }), }; default: throw new Error('Invalid notification platform'); } } /** * Filter jobs for deletion based on the deletion type and title. */ filterJobsForDeletion(jobs, deletionType, title) { switch (deletionType) { case DeletionType.Title: if (!title) throw new Error('Title is required when deletionType is title.'); return jobs.filter((job) => job.title && job.title.toLowerCase() === title.toLowerCase()); case DeletionType.Processed: return jobs.filter((job) => job.nextExecution === null); case DeletionType.All: return jobs; default: throw new Error('Invalid deletion type'); } } }