/** * ChronoMind MCP tools — chronomind.timers.*, chronomind.routines.*, chronomind.syncStatus * * Backed by: chronomind-backend (port 4011). * All tools require admin role. */ import { z } from 'zod'; import { registerTool } from '../tools/registry.js'; import { config } from '../../lib/config.js'; import { chronomindTimerCreate, chronomindTimersList, chronomindTimerDelete, chronomindRoutineGet, chronomindRoutinesList, chronomindSyncStatus, chronomindHouseholdsList, chronomindSharedTimerShare, } from '../../lib/chronomind-client.js'; import type { McpToolRequest } from '../tools/types.js'; const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7); // ── chronomind.timers.create ─────────────────────────────────────────────── registerTool({ name: 'chronomind.timers.create', description: 'Create a new cloud-synced timer. The id and syncVersion must be provided by the caller (client-generated UUID and current version). Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ id: z.string().min(1).describe('Client-generated timer UUID'), label: z.string().min(1).describe('Timer display name'), type: z.enum(['alarm', 'countdown', 'pomodoro', 'event']), urgency: z.enum(['critical', 'important', 'standard', 'gentle', 'passive']).default('standard'), duration: z.coerce.number().optional().describe('Duration in seconds (countdown/pomodoro)'), targetTime: z.string().optional().describe('ISO 8601 target time (alarm/event)'), category: z.string().optional().describe('Timer category label'), syncVersion: z.coerce.number().default(1), deviceId: z.string().optional().describe('Origin device ID for conflict detection'), }), async execute(args, req) { return chronomindTimerCreate( { ...args, state: 'idle' }, { token: tokenOf(req), requestId: req.id } ); }, }); // ── chronomind.timers.list ──────────────────────────────────────────────── registerTool({ name: 'chronomind.timers.list', description: 'List cloud-synced timers for the authenticated user. Filter by type (alarm/countdown/pomodoro/event) or state (idle/active/paused/warning/fired/completed). Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ type: z .enum(['alarm', 'countdown', 'pomodoro', 'event']) .optional() .describe('Filter by timer type'), state: z .enum(['idle', 'active', 'paused', 'warning', 'fired', 'completed']) .optional() .describe('Filter by timer state'), limit: z.coerce.number().min(1).max(config.QUERY_MAX_LIMIT).default(config.QUERY_DEFAULT_LIMIT), offset: z.coerce.number().min(0).default(0), }), async execute(args, req) { return chronomindTimersList(args, { token: tokenOf(req), requestId: req.id }); }, }); // ── chronomind.timers.delete ────────────────────────────────────────────── registerTool({ name: 'chronomind.timers.delete', description: 'Delete a timer by ID. Use with care — deletion is permanent. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ timerId: z.string().min(1).describe('Timer ID to delete'), }), async execute(args, req) { return chronomindTimerDelete(args.timerId, { token: tokenOf(req), requestId: req.id }); }, }); // ── chronomind.routines.list ────────────────────────────────────────────── registerTool({ name: 'chronomind.routines.list', description: 'List cloud-synced routines for the authenticated user. Optionally filter to template routines only. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ isTemplate: z .boolean() .optional() .describe('true = only templates, false = only user routines'), limit: z.coerce.number().min(1).max(config.QUERY_MAX_LIMIT).default(config.QUERY_DEFAULT_LIMIT), offset: z.coerce.number().min(0).default(0), }), async execute(args, req) { return chronomindRoutinesList(args, { token: tokenOf(req), requestId: req.id }); }, }); // ── chronomind.syncStatus ───────────────────────────────────────────────── registerTool({ name: 'chronomind.syncStatus', description: 'Instant sync health check: total timers, active count, pending count, unsynced count, and last sync timestamp. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({}), async execute(_args, req) { return chronomindSyncStatus({ token: tokenOf(req), requestId: req.id }); }, }); // ── chronomind.sharedTimers.share ────────────────────────────────────────── registerTool({ name: 'chronomind.sharedTimers.share', description: 'Share a timer with one or more household members or specific user IDs. Creates a shared-timer record in the shared-timers module. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ timerId: z.string().min(1).describe('ID of the timer to share'), targets: z .array( z.object({ userId: z.string().optional().describe('Target user ID'), householdId: z .string() .optional() .describe('Target household ID (shares with all members)'), }) ) .min(1) .describe('Recipients — provide userId or householdId for each target'), }), async execute(args, req) { return chronomindSharedTimerShare(args.timerId, args.targets, { token: tokenOf(req), requestId: req.id, }); }, }); // ── chronomind.routines.validate ───────────────────────────────────────── registerTool({ name: 'chronomind.routines.validate', description: 'Validate a routine for structural integrity: checks that total duration does not exceed the maximum, that all steps have non-zero duration, and that the routine has at least one step. Returns pass/fail with a list of issues. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ routineId: z.string().min(1).describe('Routine ID to validate'), maxDurationMinutes: z.coerce .number() .min(1) .max(1440) .default(480) .describe('Maximum allowed total duration in minutes (default 480 = 8h)'), }), async execute(args, req) { let routine; try { routine = await chronomindRoutineGet(args.routineId, { token: tokenOf(req), requestId: req.id, }); } catch { return { valid: false, routineId: args.routineId, issues: ['Routine not found'] }; } const issues: string[] = []; if (!routine.steps || routine.steps.length === 0) issues.push('Routine has no steps'); if (routine.totalDurationMinutes > args.maxDurationMinutes) { issues.push( `Total duration ${routine.totalDurationMinutes}min exceeds max ${args.maxDurationMinutes}min` ); } return { valid: issues.length === 0, routineId: routine.id, routineName: routine.name, stepCount: routine.steps.length, totalDurationMinutes: routine.totalDurationMinutes, maxDurationMinutes: args.maxDurationMinutes, issues, }; }, }); // ── chronomind.households.list ────────────────────────────────────────────── registerTool({ name: 'chronomind.households.list', description: 'List ChronoMind Family-tier households the authenticated user belongs to, including member list and invite codes. Requires admin role.', requiredRole: 'admin', inputSchema: z.object({ limit: z.coerce.number().min(1).max(config.QUERY_MAX_LIMIT).default(config.QUERY_DEFAULT_LIMIT), offset: z.coerce.number().min(0).default(0), }), async execute(args, req) { return chronomindHouseholdsList(args, { token: tokenOf(req), requestId: req.id }); }, });