/** * MCP tools for rMinders (cron jobs, reminders, workflows). * * Tools: rminders_list_jobs, rminders_list_reminders, * rminders_list_workflows, rminders_create_reminder */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { SyncServer } from "../local-first/sync-server"; import { mindersDocId } from "../../modules/rminders/schemas"; import type { MindersDoc } from "../../modules/rminders/schemas"; import { resolveAccess, accessDeniedResponse } from "./_auth"; export function registerMindersTools(server: McpServer, syncServer: SyncServer) { server.tool( "rminders_list_jobs", "List cron/scheduled jobs in a space", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), enabled_only: z.boolean().optional().describe("Only show enabled jobs (default false)"), }, async ({ space, token, enabled_only }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(mindersDocId(space)); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No schedule data found" }) }] }; } let jobs = Object.values(doc.jobs || {}); if (enabled_only) jobs = jobs.filter(j => j.enabled); const summary = jobs.map(j => ({ id: j.id, name: j.name, description: j.description, enabled: j.enabled, cronExpression: j.cronExpression, timezone: j.timezone, actionType: j.actionType, lastRunAt: j.lastRunAt, lastRunStatus: j.lastRunStatus, nextRunAt: j.nextRunAt, runCount: j.runCount, })); return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] }; }, ); server.tool( "rminders_list_reminders", "List reminders in a space", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), completed: z.boolean().optional().describe("Filter by completed status"), upcoming_days: z.number().optional().describe("Only show reminders firing in next N days"), limit: z.number().optional().describe("Max results (default 50)"), }, async ({ space, token, completed, upcoming_days, limit }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(mindersDocId(space)); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No schedule data found" }) }] }; } let reminders = Object.values(doc.reminders || {}); if (completed !== undefined) { reminders = reminders.filter(r => r.completed === completed); } if (upcoming_days) { const now = Date.now(); const cutoff = now + upcoming_days * 86400000; reminders = reminders.filter(r => r.remindAt >= now && r.remindAt <= cutoff); } reminders.sort((a, b) => a.remindAt - b.remindAt); reminders = reminders.slice(0, limit || 50); const summary = reminders.map(r => ({ id: r.id, title: r.title, description: r.description, remindAt: r.remindAt, allDay: r.allDay, completed: r.completed, notified: r.notified, sourceModule: r.sourceModule, sourceLabel: r.sourceLabel, })); return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] }; }, ); server.tool( "rminders_list_workflows", "List automation workflows in a space (summaries only, omits node/edge graph)", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), }, async ({ space, token }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(mindersDocId(space)); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No schedule data found" }) }] }; } const workflows = Object.values(doc.workflows || {}).map(w => ({ id: w.id, name: w.name, enabled: w.enabled, nodeCount: w.nodes?.length ?? 0, edgeCount: w.edges?.length ?? 0, lastRunAt: w.lastRunAt, lastRunStatus: w.lastRunStatus, runCount: w.runCount, createdAt: w.createdAt, updatedAt: w.updatedAt, })); return { content: [{ type: "text", text: JSON.stringify(workflows, null, 2) }] }; }, ); server.tool( "rminders_create_reminder", "Create a new reminder (requires auth token + space membership)", { space: z.string().describe("Space slug"), token: z.string().describe("JWT auth token"), title: z.string().describe("Reminder title"), remind_at: z.number().describe("When to fire (epoch ms)"), description: z.string().optional().describe("Reminder description"), all_day: z.boolean().optional().describe("All-day reminder"), source_module: z.string().optional().describe("Originating module (e.g. 'rtasks')"), source_label: z.string().optional().describe("Source display label"), }, async ({ space, token, title, remind_at, description, all_day, source_module, source_label }) => { const access = await resolveAccess(token, space, true); if (!access.allowed) return accessDeniedResponse(access.reason!); const docId = mindersDocId(space); const doc = syncServer.getDoc(docId); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No schedule data found" }) }], isError: true }; } const reminderId = `rem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const now = Date.now(); syncServer.changeDoc(docId, `Create reminder ${title}`, (d) => { if (!d.reminders) (d as any).reminders = {}; d.reminders[reminderId] = { id: reminderId, title, description: description || "", remindAt: remind_at, allDay: all_day || false, timezone: "UTC", notifyEmail: null, notified: false, completed: false, sourceModule: source_module || null, sourceEntityId: null, sourceLabel: source_label || null, sourceColor: null, cronExpression: null, calendarEventId: null, createdBy: access.claims?.did ?? "", createdAt: now, updatedAt: now, } as any; }); return { content: [{ type: "text", text: JSON.stringify({ id: reminderId, created: true }) }] }; }, ); }