From efde14ba6eb2da74a574dad71288919c5e52ce10 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 31 Mar 2026 23:41:43 -0700 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20Phase=20A.3=20=E2=80=94=205=20new?= =?UTF-8?q?=20ChronoMind=20MCP=20tools=20+=20client=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend centralized MCP server with 5 new ChronoMind tools: - chronomind.timers.reschedule — shift timer by delta or set new target time - chronomind.timers.availability — find free time slots in a window - chronomind.routines.start — start a routine from ready/template status - chronomind.agentActions.list — list agent action audit trail - chronomind.agentActions.approve — approve a proposed agent action Client functions added to chronomind-client.ts: - chronomindTimerReschedule, chronomindTimerAvailability, chronomindRoutineStart - chronomindAgentActionCreate, chronomindAgentActionsList, chronomindAgentActionApprove Write tools (reschedule, routines.start) record agent actions for audit trail. Audit recording is fail-open — failures don't block the actual operation. MCP server typecheck passes. No breaking changes to existing tools. --- .../mcp-server/src/lib/chronomind-client.ts | 93 +++++++++++ .../modules/chronomind/chronomind-tools.ts | 156 ++++++++++++++++++ 2 files changed, 249 insertions(+) diff --git a/services/mcp-server/src/lib/chronomind-client.ts b/services/mcp-server/src/lib/chronomind-client.ts index 3eec98a5..85aa28c4 100644 --- a/services/mcp-server/src/lib/chronomind-client.ts +++ b/services/mcp-server/src/lib/chronomind-client.ts @@ -167,3 +167,96 @@ export function chronomindTimerDelete( ): Promise<{ success: boolean }> { return chronomindFetch(`/timers/${timerId}`, { method: 'DELETE' }, opts); } + +// ── Phase A.3: New MCP-supporting client functions ──────────────────────── + +export function chronomindTimerReschedule( + timerId: string, + input: { deltaSeconds?: number; targetTime?: string; syncVersion: number }, + opts: ChronoMindClientOptions +): Promise { + return chronomindFetch( + `/timers/${timerId}/reschedule`, + { method: 'POST', body: JSON.stringify(input) }, + opts + ); +} + +export interface FreeSlot { + start: string; + end: string; + durationMinutes: number; +} + +export function chronomindTimerAvailability( + params: { start: string; end: string; minSlotMinutes?: number }, + opts: ChronoMindClientOptions +): Promise<{ slots: FreeSlot[]; totalFreeMinutes: number }> { + const qs = new URLSearchParams(); + qs.set('start', params.start); + qs.set('end', params.end); + if (params.minSlotMinutes !== undefined) qs.set('minSlotMinutes', String(params.minSlotMinutes)); + return chronomindFetch(`/timers/availability?${qs.toString()}`, { method: 'GET' }, opts); +} + +export function chronomindRoutineStart( + routineId: string, + opts: ChronoMindClientOptions +): Promise { + return chronomindFetch(`/routines/${routineId}/start`, { method: 'POST' }, opts); +} + +// ── Agent Actions ───────────────────────────────────────────────────────── + +export interface AgentActionDoc { + id: string; + userId: string; + productId: string; + actorId: string; + actorType: 'agent' | 'user' | 'mcp'; + toolName: string; + actionType: string; + state: 'proposed' | 'approved' | 'applied' | 'rejected'; + reason?: string; + payload: Record; + createdAt: string; + reviewedAt?: string; + reviewedBy?: string; +} + +export function chronomindAgentActionCreate( + input: { + id: string; + actorId: string; + actorType: 'agent' | 'user' | 'mcp'; + toolName: string; + actionType: string; + state?: string; + reason?: string; + payload?: Record; + }, + opts: ChronoMindClientOptions +): Promise { + return chronomindFetch('/agent-actions', { method: 'POST', body: JSON.stringify(input) }, opts); +} + +export function chronomindAgentActionsList( + params: { state?: string; actorId?: string; toolName?: string; limit?: number; offset?: number }, + opts: ChronoMindClientOptions +): Promise<{ items: AgentActionDoc[]; total: number }> { + const qs = new URLSearchParams(); + if (params.state) qs.set('state', params.state); + if (params.actorId) qs.set('actorId', params.actorId); + if (params.toolName) qs.set('toolName', params.toolName); + if (params.limit !== undefined) qs.set('limit', String(params.limit)); + if (params.offset !== undefined) qs.set('offset', String(params.offset)); + const q = qs.toString(); + return chronomindFetch(`/agent-actions${q ? `?${q}` : ''}`, { method: 'GET' }, opts); +} + +export function chronomindAgentActionApprove( + actionId: string, + opts: ChronoMindClientOptions +): Promise { + return chronomindFetch(`/agent-actions/${actionId}/approve`, { method: 'POST' }, opts); +} diff --git a/services/mcp-server/src/modules/chronomind/chronomind-tools.ts b/services/mcp-server/src/modules/chronomind/chronomind-tools.ts index eea4d614..1e924843 100644 --- a/services/mcp-server/src/modules/chronomind/chronomind-tools.ts +++ b/services/mcp-server/src/modules/chronomind/chronomind-tools.ts @@ -12,11 +12,17 @@ import { chronomindTimerCreate, chronomindTimersList, chronomindTimerDelete, + chronomindTimerReschedule, + chronomindTimerAvailability, chronomindRoutineGet, chronomindRoutinesList, + chronomindRoutineStart, chronomindSyncStatus, chronomindHouseholdsList, chronomindSharedTimerShare, + chronomindAgentActionCreate, + chronomindAgentActionsList, + chronomindAgentActionApprove, } from '../../lib/chronomind-client.js'; import type { McpToolRequest } from '../tools/types.js'; @@ -209,3 +215,153 @@ registerTool({ return chronomindHouseholdsList(args, { token: tokenOf(req), requestId: req.id }); }, }); + +// ══════════════════════════════════════════════════════════════════════════════ +// Phase A.3 — New MCP tools for agentic AI features +// ══════════════════════════════════════════════════════════════════════════════ + +// ── chronomind.timers.reschedule ──────────────────────────────────────────── + +registerTool({ + name: 'chronomind.timers.reschedule', + description: + 'Reschedule a timer by shifting it forward/backward (deltaSeconds) or setting a new absolute target time. Provide exactly one of deltaSeconds or targetTime. Records an agent action for audit trail. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + timerId: z.string().min(1).describe('Timer ID to reschedule'), + deltaSeconds: z + .number() + .int() + .optional() + .describe('Shift by N seconds (positive = later, negative = earlier)'), + targetTime: z.string().optional().describe('New absolute target time (ISO 8601)'), + syncVersion: z.coerce.number().min(1).describe('Current sync version for conflict check'), + reason: z.string().optional().describe('Reason for rescheduling (recorded in audit trail)'), + }), + async execute(args, req) { + const opts = { token: tokenOf(req), requestId: req.id }; + + // Record agent action audit trail + await chronomindAgentActionCreate( + { + id: `aa_${crypto.randomUUID()}`, + actorId: 'mcp-server', + actorType: 'mcp', + toolName: 'chronomind.timers.reschedule', + actionType: 'timer.reschedule', + reason: args.reason, + payload: { + timerId: args.timerId, + deltaSeconds: args.deltaSeconds, + targetTime: args.targetTime, + }, + }, + opts + ).catch(() => { + /* fail-open: audit failure should not block the action */ + }); + + return chronomindTimerReschedule( + args.timerId, + { + deltaSeconds: args.deltaSeconds, + targetTime: args.targetTime, + syncVersion: args.syncVersion, + }, + opts + ); + }, +}); + +// ── chronomind.timers.availability ────────────────────────────────────────── + +registerTool({ + name: 'chronomind.timers.availability', + description: + 'Find free time slots within a given time window. Returns gaps between existing timers that are at least minSlotMinutes long. Useful for scheduling new timers without conflicts. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + start: z.string().describe('Start of time window (ISO 8601)'), + end: z.string().describe('End of time window (ISO 8601)'), + minSlotMinutes: z.coerce + .number() + .min(1) + .max(480) + .default(15) + .describe('Minimum free slot duration in minutes'), + }), + async execute(args, req) { + return chronomindTimerAvailability(args, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── chronomind.routines.start ─────────────────────────────────────────────── + +registerTool({ + name: 'chronomind.routines.start', + description: + 'Start a routine that is in "ready" or "template" status. Transitions its status to "active" and sets the first step as active. Records an agent action for audit trail. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + routineId: z.string().min(1).describe('Routine ID to start'), + reason: z.string().optional().describe('Reason for starting (recorded in audit trail)'), + }), + async execute(args, req) { + const opts = { token: tokenOf(req), requestId: req.id }; + + // Record agent action audit trail + await chronomindAgentActionCreate( + { + id: `aa_${crypto.randomUUID()}`, + actorId: 'mcp-server', + actorType: 'mcp', + toolName: 'chronomind.routines.start', + actionType: 'routine.start', + reason: args.reason, + payload: { routineId: args.routineId }, + }, + opts + ).catch(() => { + /* fail-open: audit failure should not block the action */ + }); + + return chronomindRoutineStart(args.routineId, opts); + }, +}); + +// ── chronomind.agentActions.list ───────────────────────────────────────────── + +registerTool({ + name: 'chronomind.agentActions.list', + description: + 'List agent action audit trail entries. Filter by state (proposed/approved/applied/rejected), actorId, or toolName. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + state: z + .enum(['proposed', 'approved', 'applied', 'rejected']) + .optional() + .describe('Filter by action state'), + actorId: z.string().optional().describe('Filter by actor ID'), + toolName: z.string().optional().describe('Filter by MCP tool name'), + 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 chronomindAgentActionsList(args, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── chronomind.agentActions.approve ────────────────────────────────────────── + +registerTool({ + name: 'chronomind.agentActions.approve', + description: + 'Approve a proposed agent action. Only actions in "proposed" state can be approved. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + actionId: z.string().min(1).describe('Agent action ID to approve'), + }), + async execute(args, req) { + return chronomindAgentActionApprove(args.actionId, { token: tokenOf(req), requestId: req.id }); + }, +});