191 lines
6.4 KiB
TypeScript
191 lines
6.4 KiB
TypeScript
/**
|
|
* MCP tools for rSchedule (cron jobs, reminders, workflows).
|
|
*
|
|
* Tools: rschedule_list_jobs, rschedule_list_reminders,
|
|
* rschedule_list_workflows, rschedule_create_reminder
|
|
*/
|
|
|
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import { z } from "zod";
|
|
import type { SyncServer } from "../local-first/sync-server";
|
|
import { scheduleDocId } from "../../modules/rschedule/schemas";
|
|
import type { ScheduleDoc } from "../../modules/rschedule/schemas";
|
|
import { resolveAccess, accessDeniedResponse } from "./_auth";
|
|
|
|
export function registerScheduleTools(server: McpServer, syncServer: SyncServer) {
|
|
server.tool(
|
|
"rschedule_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<ScheduleDoc>(scheduleDocId(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(
|
|
"rschedule_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<ScheduleDoc>(scheduleDocId(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(
|
|
"rschedule_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<ScheduleDoc>(scheduleDocId(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(
|
|
"rschedule_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 = scheduleDocId(space);
|
|
const doc = syncServer.getDoc<ScheduleDoc>(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<ScheduleDoc>(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 }) }] };
|
|
},
|
|
);
|
|
}
|