rspace-online/server/mcp-tools/rminders.ts

191 lines
6.3 KiB
TypeScript

/**
* 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<MindersDoc>(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<MindersDoc>(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<MindersDoc>(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<MindersDoc>(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<MindersDoc>(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 }) }] };
},
);
}