feat(mcp): Phase A.3 — 5 new ChronoMind MCP tools + client functions

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.
This commit is contained in:
saravanakumardb1 2026-03-31 23:41:43 -07:00
parent 0bae1d6d5a
commit efde14ba6e
2 changed files with 249 additions and 0 deletions

View File

@ -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<TimerDoc> {
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<RoutineDoc> {
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<string, unknown>;
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<string, unknown>;
},
opts: ChronoMindClientOptions
): Promise<AgentActionDoc> {
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<AgentActionDoc> {
return chronomindFetch(`/agent-actions/${actionId}/approve`, { method: 'POST' }, opts);
}

View File

@ -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 });
},
});