diff --git a/services/mcp-server/.env.example b/services/mcp-server/.env.example index e4431259..a5118432 100644 --- a/services/mcp-server/.env.example +++ b/services/mcp-server/.env.example @@ -11,6 +11,9 @@ EXTRACTION_SERVICE_URL=http://localhost:4005 MINDLYST_BACKEND_URL=http://localhost:4014 LYSNRAI_BACKEND_URL=http://localhost:4015 JARVISJR_BACKEND_URL=http://localhost:4012 +CHRONOMIND_BACKEND_URL=http://localhost:4011 +NOMGAP_BACKEND_URL=http://localhost:4013 +PEAKPULSE_BACKEND_URL=http://localhost:4010 # Auth — same JWT_SECRET as platform-service (tokens issued there are validated here) JWT_SECRET=change-me-in-production diff --git a/services/mcp-server/src/lib/chronomind-client.ts b/services/mcp-server/src/lib/chronomind-client.ts new file mode 100644 index 00000000..d4b9b8c3 --- /dev/null +++ b/services/mcp-server/src/lib/chronomind-client.ts @@ -0,0 +1,116 @@ +/** + * ChronoMind backend client — typed HTTP wrappers for the chronomind-backend (port 4011). + * + * Auth: Bearer token from the caller's JWT (same JWT_SECRET as platform-service). + */ + +import { config } from './config.js'; + +export interface ChronoMindClientOptions { + token?: string; + requestId?: string; +} + +async function chronomindFetch( + path: string, + init: RequestInit, + opts: ChronoMindClientOptions +): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + ...(opts.token ? { Authorization: `Bearer ${opts.token}` } : {}), + ...(opts.requestId ? { 'x-request-id': opts.requestId } : {}), + }; + const res = await fetch(`${config.CHRONOMIND_BACKEND_URL}${path}`, { + ...init, + headers: { ...((init.headers as Record) ?? {}), ...headers }, + signal: AbortSignal.timeout(15_000), + }); + if (!res.ok) { + const body = await res.text().catch(() => ''); + throw new Error(`chronomind-backend ${init.method ?? 'GET'} ${path} → ${res.status}: ${body}`); + } + return res.json() as Promise; +} + +// ── Timers ───────────────────────────────────────────────────────────────── + +export interface TimerDoc { + id: string; + userId: string; + productId: string; + label: string; + description?: string; + type: 'alarm' | 'countdown' | 'pomodoro' | 'event'; + state: 'idle' | 'active' | 'paused' | 'warning' | 'fired' | 'completed'; + urgency: 'critical' | 'important' | 'standard' | 'gentle' | 'passive'; + duration?: number; + targetTime?: string; + category?: string; + createdAt: string; + lastSyncedAt: string; + syncVersion: number; +} + +export function chronomindTimersList( + params: { limit?: number; offset?: number; type?: string; state?: string }, + opts: ChronoMindClientOptions +): Promise<{ items: TimerDoc[]; total: number }> { + const qs = new URLSearchParams(); + if (params.limit !== undefined) qs.set('limit', String(params.limit)); + if (params.offset !== undefined) qs.set('offset', String(params.offset)); + if (params.type) qs.set('type', params.type); + if (params.state) qs.set('state', params.state); + const q = qs.toString(); + return chronomindFetch(`/timers${q ? `?${q}` : ''}`, { method: 'GET' }, opts); +} + +export function chronomindSyncStatus(opts: ChronoMindClientOptions): Promise<{ + userId: string; + productId: string; + totalTimers: number; + active: number; + pending: number; + unsyncedCount: number; + lastSyncedAt: string | null; + generatedAt: string; +}> { + return chronomindFetch('/timers/sync-status', { method: 'GET' }, opts); +} + +// ── Routines ─────────────────────────────────────────────────────────────── + +export interface RoutineDoc { + id: string; + userId: string; + productId: string; + name: string; + description?: string; + steps: unknown[]; + totalDurationMinutes: number; + status: 'idle' | 'running' | 'paused' | 'completed'; + isTemplate: boolean; + category?: string; + createdAt: string; + lastSyncedAt: string; + syncVersion: number; +} + +export function chronomindRoutinesList( + params: { limit?: number; offset?: number; isTemplate?: boolean }, + opts: ChronoMindClientOptions +): Promise<{ items: RoutineDoc[]; total: number }> { + const qs = new URLSearchParams(); + if (params.limit !== undefined) qs.set('limit', String(params.limit)); + if (params.offset !== undefined) qs.set('offset', String(params.offset)); + if (params.isTemplate !== undefined) qs.set('isTemplate', String(params.isTemplate)); + const q = qs.toString(); + return chronomindFetch(`/routines${q ? `?${q}` : ''}`, { method: 'GET' }, opts); +} + +export function chronomindTimerDelete( + timerId: string, + opts: ChronoMindClientOptions +): Promise<{ success: boolean }> { + return chronomindFetch(`/timers/${timerId}`, { method: 'DELETE' }, opts); +} diff --git a/services/mcp-server/src/lib/config.ts b/services/mcp-server/src/lib/config.ts index 769877b8..401c256f 100644 --- a/services/mcp-server/src/lib/config.ts +++ b/services/mcp-server/src/lib/config.ts @@ -13,6 +13,9 @@ const envSchema = z.object({ MINDLYST_BACKEND_URL: z.string().default('http://localhost:4014'), LYSNRAI_BACKEND_URL: z.string().default('http://localhost:4015'), JARVISJR_BACKEND_URL: z.string().default('http://localhost:4012'), + CHRONOMIND_BACKEND_URL: z.string().default('http://localhost:4011'), + NOMGAP_BACKEND_URL: z.string().default('http://localhost:4013'), + PEAKPULSE_BACKEND_URL: z.string().default('http://localhost:4010'), /** Max items returned per tool call query (hard cap) */ QUERY_MAX_LIMIT: z.coerce.number().default(100), /** Default items per tool call query */ diff --git a/services/mcp-server/src/lib/nomgap-client.ts b/services/mcp-server/src/lib/nomgap-client.ts new file mode 100644 index 00000000..83eb7885 --- /dev/null +++ b/services/mcp-server/src/lib/nomgap-client.ts @@ -0,0 +1,128 @@ +/** + * NomGap backend client — typed HTTP wrappers for the nomgap-backend (port 4013). + * + * Auth: Bearer token from the caller's JWT (same JWT_SECRET as platform-service). + */ + +import { config } from './config.js'; + +export interface NomGapClientOptions { + token?: string; + requestId?: string; + productId?: string; +} + +async function nomgapFetch( + path: string, + init: RequestInit, + opts: NomGapClientOptions +): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + ...(opts.token ? { Authorization: `Bearer ${opts.token}` } : {}), + ...(opts.requestId ? { 'x-request-id': opts.requestId } : {}), + ...(opts.productId ? { 'x-product-id': opts.productId } : { 'x-product-id': 'nomgap' }), + }; + const res = await fetch(`${config.NOMGAP_BACKEND_URL}${path}`, { + ...init, + headers: { ...((init.headers as Record) ?? {}), ...headers }, + signal: AbortSignal.timeout(15_000), + }); + if (!res.ok) { + const body = await res.text().catch(() => ''); + throw new Error(`nomgap-backend ${init.method ?? 'GET'} ${path} → ${res.status}: ${body}`); + } + return res.json() as Promise; +} + +// ── Fasting sessions ─────────────────────────────────────────────────────── + +export interface FastingSessionDoc { + id: string; + userId: string; + productId: string; + protocolId: string; + startedAt: string; + targetDurationMs: number; + status: 'active' | 'completed' | 'broken'; + stages: unknown[]; + moodCheckins: unknown[]; + waterIntake: unknown[]; + metrics: { + actualDurationMs: number; + completionRatio: number; + peakAutophagyConfidence: number; + totalPausedMs: number; + moodCheckinCount: number; + averageEnergy: number | null; + averageMood: number | null; + }; + createdAt: string; + updatedAt: string; +} + +export function nomgapFastingSessionsList( + params: { limit?: number; offset?: number; status?: string; from?: string; to?: string }, + opts: NomGapClientOptions +): Promise<{ items: FastingSessionDoc[]; total: number }> { + const qs = new URLSearchParams(); + if (params.limit !== undefined) qs.set('limit', String(params.limit)); + if (params.offset !== undefined) qs.set('offset', String(params.offset)); + if (params.status) qs.set('status', params.status); + if (params.from) qs.set('from', params.from); + if (params.to) qs.set('to', params.to); + const q = qs.toString(); + return nomgapFetch(`/fasting/sessions${q ? `?${q}` : ''}`, { method: 'GET' }, opts); +} + +export function nomgapFastingGetStats(opts: NomGapClientOptions): Promise> { + return nomgapFetch('/fasting/stats', { method: 'GET' }, opts); +} + +export function nomgapFastingGetWeeklyStats( + opts: NomGapClientOptions +): Promise> { + return nomgapFetch('/fasting/stats/weekly', { method: 'GET' }, opts); +} + +// ── Push triggers ────────────────────────────────────────────────────────── + +export type PushTriggerType = + | 'streak_risk' + | 'fast_milestone' + | 'stage_transition' + | 'social_invite' + | 'weekly_digest' + | 'achievement_unlocked' + | 'refeeding_reminder'; + +export interface PushTriggerDoc { + id: string; + productId: string; + type: PushTriggerType; + userId: string; + variables: Record; + status: 'pending' | 'sent' | 'skipped' | 'failed'; + scheduledAt: string; + createdAt: string; +} + +export function nomgapPushFire( + input: { + type: PushTriggerType; + userId: string; + variables?: Record; + scheduledAt?: string; + }, + opts: NomGapClientOptions +): Promise { + return nomgapFetch('/push-triggers', { method: 'POST', body: JSON.stringify(input) }, opts); +} + +export function nomgapPushGetStats(opts: NomGapClientOptions): Promise> { + return nomgapFetch('/push-triggers/stats', { method: 'GET' }, opts); +} + +export function nomgapPushGetPending(opts: NomGapClientOptions): Promise { + return nomgapFetch('/push-triggers/pending', { method: 'GET' }, opts); +} diff --git a/services/mcp-server/src/lib/peakpulse-client.ts b/services/mcp-server/src/lib/peakpulse-client.ts new file mode 100644 index 00000000..d4f1abbb --- /dev/null +++ b/services/mcp-server/src/lib/peakpulse-client.ts @@ -0,0 +1,145 @@ +/** + * PeakPulse backend client — typed HTTP wrappers for the peakpulse-backend (port 4010). + * + * Auth: Bearer token from the caller's JWT (same JWT_SECRET as platform-service). + */ + +import { config } from './config.js'; + +export interface PeakPulseClientOptions { + token?: string; + requestId?: string; + productId?: string; +} + +async function peakpulseFetch( + path: string, + init: RequestInit, + opts: PeakPulseClientOptions +): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + ...(opts.token ? { Authorization: `Bearer ${opts.token}` } : {}), + ...(opts.requestId ? { 'x-request-id': opts.requestId } : {}), + ...(opts.productId ? { 'x-product-id': opts.productId } : { 'x-product-id': 'peakpulse' }), + }; + const res = await fetch(`${config.PEAKPULSE_BACKEND_URL}${path}`, { + ...init, + headers: { ...((init.headers as Record) ?? {}), ...headers }, + signal: AbortSignal.timeout(15_000), + }); + if (!res.ok) { + const body = await res.text().catch(() => ''); + throw new Error(`peakpulse-backend ${init.method ?? 'GET'} ${path} → ${res.status}: ${body}`); + } + return res.json() as Promise; +} + +// ── Sessions ─────────────────────────────────────────────────────────────── + +export interface PeakSessionDoc { + id: string; + clientId?: string; + userId: string; + productId: string; + activityType: 'hiking' | 'skiing' | 'cycling' | 'running'; + status: 'active' | 'completed' | 'paused'; + startTime: string; + endTime?: string; + durationSeconds?: number; + distanceMeters?: number; + maxSpeedMps?: number; + averageSpeedMps?: number; + elevationGainMeters?: number; + elevationLossMeters?: number; + maxElevationMeters?: number; + locationName?: string; + startLatitude?: number; + startLongitude?: number; + unitPreference?: string; + weather?: { + tempCelsius?: number; + conditionCode?: string; + windSpeedKph?: number; + uvIndex?: number; + }; + skiMetrics?: { + runCount?: number; + verticalDescentMeters?: number; + liftTimeSeconds?: number; + skiTimeSeconds?: number; + }; + trackPointCount?: number; + hapticMilestoneCount?: number; + savedToHealthKit?: boolean; + notes?: string; + createdAt: string; + updatedAt: string; +} + +export interface PeakSessionExport { + exportVersion: string; + productId: string; + session: Omit< + PeakSessionDoc, + 'userId' | 'clientId' | 'savedToHealthKit' | 'createdAt' | 'updatedAt' + >; + exportedAt: string; +} + +export function peakpulseSessionsList( + params: { + activityType?: string; + status?: string; + limit?: number; + offset?: number; + }, + opts: PeakPulseClientOptions +): Promise<{ items: PeakSessionDoc[]; total: number }> { + const qs = new URLSearchParams(); + if (params.activityType) qs.set('activityType', params.activityType); + if (params.status) qs.set('status', params.status); + 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 peakpulseFetch(`/peak/sessions${q ? `?${q}` : ''}`, { method: 'GET' }, opts); +} + +export function peakpulseSessionExport( + sessionId: string, + opts: PeakPulseClientOptions +): Promise { + return peakpulseFetch(`/peak/sessions/${sessionId}/export`, { method: 'GET' }, opts); +} + +export function peakpulseGetStats(opts: PeakPulseClientOptions): Promise> { + return peakpulseFetch('/peak/stats', { method: 'GET' }, opts); +} + +// ── Routes ───────────────────────────────────────────────────────────────── + +export interface PeakRouteDoc { + id: string; + sessionId: string; + userId: string; + productId: string; + trackPoints: unknown[]; + hapticEvents: unknown[]; + trackPointCount: number; + hapticEventCount: number; + boundingBox?: { + minLat: number; + maxLat: number; + minLon: number; + maxLon: number; + }; + createdAt: string; + updatedAt: string; +} + +export function peakpulseRouteGet( + sessionId: string, + opts: PeakPulseClientOptions +): Promise { + return peakpulseFetch(`/peak/routes/${sessionId}`, { method: 'GET' }, opts); +} diff --git a/services/mcp-server/src/modules/chronomind/chronomind-tools.ts b/services/mcp-server/src/modules/chronomind/chronomind-tools.ts new file mode 100644 index 00000000..a453b38f --- /dev/null +++ b/services/mcp-server/src/modules/chronomind/chronomind-tools.ts @@ -0,0 +1,90 @@ +/** + * 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 { + chronomindTimersList, + chronomindTimerDelete, + chronomindRoutinesList, + chronomindSyncStatus, +} from '../../lib/chronomind-client.js'; +import type { McpToolRequest } from '../tools/types.js'; + +const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7); + +// ── 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, paused 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 }); + }, +}); diff --git a/services/mcp-server/src/modules/nomgap/nomgap-tools.ts b/services/mcp-server/src/modules/nomgap/nomgap-tools.ts new file mode 100644 index 00000000..02515249 --- /dev/null +++ b/services/mcp-server/src/modules/nomgap/nomgap-tools.ts @@ -0,0 +1,144 @@ +/** + * NomGap MCP tools — nomgap.fasting.*, nomgap.push.* + * + * Backed by: nomgap-backend (port 4013). + * All tools require admin role. + */ + +import { z } from 'zod'; +import { registerTool } from '../tools/registry.js'; +import { config } from '../../lib/config.js'; +import { + nomgapFastingSessionsList, + nomgapFastingGetStats, + nomgapFastingGetWeeklyStats, + nomgapPushFire, + nomgapPushGetStats, + nomgapPushGetPending, +} from '../../lib/nomgap-client.js'; +import type { McpToolRequest } from '../tools/types.js'; + +const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7); + +// ── nomgap.fasting.sessions.list ────────────────────────────────────────── + +registerTool({ + name: 'nomgap.fasting.sessions.list', + description: + 'List fasting sessions for the authenticated user. Filter by status or date range. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + status: z + .enum(['active', 'completed', 'broken']) + .optional() + .describe('Filter by session status'), + from: z.string().datetime().optional().describe('ISO 8601 start of date range'), + to: z.string().datetime().optional().describe('ISO 8601 end of date range'), + 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 nomgapFastingSessionsList(args, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── nomgap.fasting.stats ────────────────────────────────────────────────── + +registerTool({ + name: 'nomgap.fasting.stats', + description: + 'Get aggregated fasting stats for the authenticated user (streak, longestFast, completionRate, totalFasts). Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({}), + async execute(_args, req) { + return nomgapFastingGetStats({ token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── nomgap.fasting.stats.weekly ─────────────────────────────────────────── + +registerTool({ + name: 'nomgap.fasting.stats.weekly', + description: + "Get this week's fasting summary for the authenticated user (sessions, total hours, average duration). Requires admin role.", + requiredRole: 'admin', + inputSchema: z.object({}), + async execute(_args, req) { + return nomgapFastingGetWeeklyStats({ token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── nomgap.push.fire ────────────────────────────────────────────────────── + +registerTool({ + name: 'nomgap.push.fire', + description: [ + 'Fire a push notification trigger for a NomGap user. One of 7 trigger types:', + ' • streak_risk — user is at risk of losing their streak', + ' • fast_milestone — fasting duration milestone reached', + ' • stage_transition — entered a new metabolic stage (ketosis, autophagy…)', + ' • social_invite — invited to a group fast', + ' • weekly_digest — weekly summary push', + ' • achievement_unlocked — badge or achievement earned', + ' • refeeding_reminder — safety-critical refeeding guidance (48h+ fasts)', + 'Requires admin role.', + ].join('\n'), + requiredRole: 'admin', + inputSchema: z.object({ + type: z + .enum([ + 'streak_risk', + 'fast_milestone', + 'stage_transition', + 'social_invite', + 'weekly_digest', + 'achievement_unlocked', + 'refeeding_reminder', + ]) + .describe('Push trigger type'), + userId: z.string().min(1).describe('Target user ID'), + variables: z + .record(z.string()) + .optional() + .default({}) + .describe('Template variables for the notification message'), + scheduledAt: z.string().datetime().optional().describe('ISO 8601 fire time (default: now)'), + }), + async execute(args, req) { + return nomgapPushFire( + { + type: args.type, + userId: args.userId, + variables: args.variables, + scheduledAt: args.scheduledAt, + }, + { token: tokenOf(req), requestId: req.id } + ); + }, +}); + +// ── nomgap.push.stats ───────────────────────────────────────────────────── + +registerTool({ + name: 'nomgap.push.stats', + description: + 'Get push trigger delivery stats by type (sent, skipped, failed counts). Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({}), + async execute(_args, req) { + return nomgapPushGetStats({ token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── nomgap.push.pending ─────────────────────────────────────────────────── + +registerTool({ + name: 'nomgap.push.pending', + description: + 'List push triggers that are pending delivery (scheduledAt <= now, status = pending). Useful for diagnosing delivery backlogs. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({}), + async execute(_args, req) { + return nomgapPushGetPending({ token: tokenOf(req), requestId: req.id }); + }, +}); diff --git a/services/mcp-server/src/modules/peakpulse/peakpulse-tools.ts b/services/mcp-server/src/modules/peakpulse/peakpulse-tools.ts new file mode 100644 index 00000000..77ee34d1 --- /dev/null +++ b/services/mcp-server/src/modules/peakpulse/peakpulse-tools.ts @@ -0,0 +1,86 @@ +/** + * PeakPulse MCP tools — peakpulse.sessions.*, peakpulse.routes.*, peakpulse.stats + * + * Backed by: peakpulse-backend (port 4010). + * All tools require admin role. + */ + +import { z } from 'zod'; +import { registerTool } from '../tools/registry.js'; +import { config } from '../../lib/config.js'; +import { + peakpulseSessionsList, + peakpulseSessionExport, + peakpulseGetStats, + peakpulseRouteGet, +} from '../../lib/peakpulse-client.js'; +import type { McpToolRequest } from '../tools/types.js'; + +const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7); + +// ── peakpulse.sessions.list ─────────────────────────────────────────────── + +registerTool({ + name: 'peakpulse.sessions.list', + description: + 'List adventure sessions for the authenticated user. Filter by activity type (hiking/skiing/cycling/running) or status. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + activityType: z + .enum(['hiking', 'skiing', 'cycling', 'running']) + .optional() + .describe('Filter by activity type'), + status: z + .enum(['active', 'completed', 'paused']) + .optional() + .describe('Filter by session status'), + 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 peakpulseSessionsList(args, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── peakpulse.sessions.export ───────────────────────────────────────────── + +registerTool({ + name: 'peakpulse.sessions.export', + description: + 'Export a session as a portable JSON summary including GPS bounds, ski metrics, weather snapshot, and elevation data. Useful for sharing or route-review flows. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + sessionId: z.string().min(1).describe('Session ID to export'), + }), + async execute(args, req) { + return peakpulseSessionExport(args.sessionId, { token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── peakpulse.stats ─────────────────────────────────────────────────────── + +registerTool({ + name: 'peakpulse.stats', + description: + 'Get aggregated activity stats for the authenticated user (total sessions, distance, elevation gain, per-activity breakdown). Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({}), + async execute(_args, req) { + return peakpulseGetStats({ token: tokenOf(req), requestId: req.id }); + }, +}); + +// ── peakpulse.routes.get ────────────────────────────────────────────────── + +registerTool({ + name: 'peakpulse.routes.get', + description: + 'Retrieve GPS track points and haptic milestone events for a session. Returns bounding box coordinates plus full track point array. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + sessionId: z.string().min(1).describe('Session ID to get route data for'), + }), + async execute(args, req) { + return peakpulseRouteGet(args.sessionId, { token: tokenOf(req), requestId: req.id }); + }, +}); diff --git a/services/mcp-server/src/server.ts b/services/mcp-server/src/server.ts index c37bb7e2..7479dae5 100644 --- a/services/mcp-server/src/server.ts +++ b/services/mcp-server/src/server.ts @@ -9,6 +9,9 @@ * mindlyst.* — memory, brains, briefs, streaks, reflections, extractions * lysnrai.* — transcripts, sessions, orgs, apiTokens, stt * jarvis.* — agents, sessions, memory (JarvisJr coaching platform) + * chronomind.* — timers, routines, syncStatus + * nomgap.* — fasting sessions, push triggers + * peakpulse.* — adventure sessions, GPS routes, stats * * Auth: JWT Bearer tokens issued by platform-service (same JWT_SECRET). * Role gating: viewer / admin / super_admin per tool. @@ -29,6 +32,9 @@ import './modules/a2a/pipeline-tool.js'; import './modules/mindlyst/mindlyst-tools.js'; import './modules/lysnrai/lysnrai-tools.js'; import './modules/jarvis/jarvis-tools.js'; +import './modules/chronomind/chronomind-tools.js'; +import './modules/nomgap/nomgap-tools.js'; +import './modules/peakpulse/peakpulse-tools.js'; const app = await createServiceApp({ name: 'mcp-server',