feat(mcp-server): Phase 3 — product namespaces for ChronoMind, NomGap, PeakPulse (13 more tools)
This commit is contained in:
parent
f780f0b409
commit
fcb3befa23
@ -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
|
||||
|
||||
116
services/mcp-server/src/lib/chronomind-client.ts
Normal file
116
services/mcp-server/src/lib/chronomind-client.ts
Normal file
@ -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<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
opts: ChronoMindClientOptions
|
||||
): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
'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<string, string>) ?? {}), ...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<T>;
|
||||
}
|
||||
|
||||
// ── 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);
|
||||
}
|
||||
@ -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 */
|
||||
|
||||
128
services/mcp-server/src/lib/nomgap-client.ts
Normal file
128
services/mcp-server/src/lib/nomgap-client.ts
Normal file
@ -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<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
opts: NomGapClientOptions
|
||||
): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
'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<string, string>) ?? {}), ...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<T>;
|
||||
}
|
||||
|
||||
// ── 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<Record<string, unknown>> {
|
||||
return nomgapFetch('/fasting/stats', { method: 'GET' }, opts);
|
||||
}
|
||||
|
||||
export function nomgapFastingGetWeeklyStats(
|
||||
opts: NomGapClientOptions
|
||||
): Promise<Record<string, unknown>> {
|
||||
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<string, string>;
|
||||
status: 'pending' | 'sent' | 'skipped' | 'failed';
|
||||
scheduledAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export function nomgapPushFire(
|
||||
input: {
|
||||
type: PushTriggerType;
|
||||
userId: string;
|
||||
variables?: Record<string, string>;
|
||||
scheduledAt?: string;
|
||||
},
|
||||
opts: NomGapClientOptions
|
||||
): Promise<PushTriggerDoc> {
|
||||
return nomgapFetch('/push-triggers', { method: 'POST', body: JSON.stringify(input) }, opts);
|
||||
}
|
||||
|
||||
export function nomgapPushGetStats(opts: NomGapClientOptions): Promise<Record<string, unknown>> {
|
||||
return nomgapFetch('/push-triggers/stats', { method: 'GET' }, opts);
|
||||
}
|
||||
|
||||
export function nomgapPushGetPending(opts: NomGapClientOptions): Promise<PushTriggerDoc[]> {
|
||||
return nomgapFetch('/push-triggers/pending', { method: 'GET' }, opts);
|
||||
}
|
||||
145
services/mcp-server/src/lib/peakpulse-client.ts
Normal file
145
services/mcp-server/src/lib/peakpulse-client.ts
Normal file
@ -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<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
opts: PeakPulseClientOptions
|
||||
): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
'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<string, string>) ?? {}), ...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<T>;
|
||||
}
|
||||
|
||||
// ── 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<PeakSessionExport> {
|
||||
return peakpulseFetch(`/peak/sessions/${sessionId}/export`, { method: 'GET' }, opts);
|
||||
}
|
||||
|
||||
export function peakpulseGetStats(opts: PeakPulseClientOptions): Promise<Record<string, unknown>> {
|
||||
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<PeakRouteDoc> {
|
||||
return peakpulseFetch(`/peak/routes/${sessionId}`, { method: 'GET' }, opts);
|
||||
}
|
||||
@ -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 });
|
||||
},
|
||||
});
|
||||
144
services/mcp-server/src/modules/nomgap/nomgap-tools.ts
Normal file
144
services/mcp-server/src/modules/nomgap/nomgap-tools.ts
Normal file
@ -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 });
|
||||
},
|
||||
});
|
||||
86
services/mcp-server/src/modules/peakpulse/peakpulse-tools.ts
Normal file
86
services/mcp-server/src/modules/peakpulse/peakpulse-tools.ts
Normal file
@ -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 });
|
||||
},
|
||||
});
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user