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 }> {
|
): Promise<{ success: boolean }> {
|
||||||
return chronomindFetch(`/timers/${timerId}`, { method: 'DELETE' }, opts);
|
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,
|
chronomindTimerCreate,
|
||||||
chronomindTimersList,
|
chronomindTimersList,
|
||||||
chronomindTimerDelete,
|
chronomindTimerDelete,
|
||||||
|
chronomindTimerReschedule,
|
||||||
|
chronomindTimerAvailability,
|
||||||
chronomindRoutineGet,
|
chronomindRoutineGet,
|
||||||
chronomindRoutinesList,
|
chronomindRoutinesList,
|
||||||
|
chronomindRoutineStart,
|
||||||
chronomindSyncStatus,
|
chronomindSyncStatus,
|
||||||
chronomindHouseholdsList,
|
chronomindHouseholdsList,
|
||||||
chronomindSharedTimerShare,
|
chronomindSharedTimerShare,
|
||||||
|
chronomindAgentActionCreate,
|
||||||
|
chronomindAgentActionsList,
|
||||||
|
chronomindAgentActionApprove,
|
||||||
} from '../../lib/chronomind-client.js';
|
} from '../../lib/chronomind-client.js';
|
||||||
import type { McpToolRequest } from '../tools/types.js';
|
import type { McpToolRequest } from '../tools/types.js';
|
||||||
|
|
||||||
@ -209,3 +215,153 @@ registerTool({
|
|||||||
return chronomindHouseholdsList(args, { token: tokenOf(req), requestId: req.id });
|
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