feat(mcp-server): Phase 3 — product namespaces for ChronoMind, NomGap, PeakPulse (13 more tools)

This commit is contained in:
saravanakumardb1 2026-03-05 13:05:03 -08:00
parent f780f0b409
commit fcb3befa23
9 changed files with 721 additions and 0 deletions

View File

@ -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

View 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);
}

View File

@ -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 */

View 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);
}

View 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);
}

View File

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

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

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

View File

@ -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',