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:
parent
0bae1d6d5a
commit
efde14ba6e
@ -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);
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user