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
|
MINDLYST_BACKEND_URL=http://localhost:4014
|
||||||
LYSNRAI_BACKEND_URL=http://localhost:4015
|
LYSNRAI_BACKEND_URL=http://localhost:4015
|
||||||
JARVISJR_BACKEND_URL=http://localhost:4012
|
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)
|
# Auth — same JWT_SECRET as platform-service (tokens issued there are validated here)
|
||||||
JWT_SECRET=change-me-in-production
|
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'),
|
MINDLYST_BACKEND_URL: z.string().default('http://localhost:4014'),
|
||||||
LYSNRAI_BACKEND_URL: z.string().default('http://localhost:4015'),
|
LYSNRAI_BACKEND_URL: z.string().default('http://localhost:4015'),
|
||||||
JARVISJR_BACKEND_URL: z.string().default('http://localhost:4012'),
|
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) */
|
/** Max items returned per tool call query (hard cap) */
|
||||||
QUERY_MAX_LIMIT: z.coerce.number().default(100),
|
QUERY_MAX_LIMIT: z.coerce.number().default(100),
|
||||||
/** Default items per tool call query */
|
/** 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
|
* mindlyst.* — memory, brains, briefs, streaks, reflections, extractions
|
||||||
* lysnrai.* — transcripts, sessions, orgs, apiTokens, stt
|
* lysnrai.* — transcripts, sessions, orgs, apiTokens, stt
|
||||||
* jarvis.* — agents, sessions, memory (JarvisJr coaching platform)
|
* 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).
|
* Auth: JWT Bearer tokens issued by platform-service (same JWT_SECRET).
|
||||||
* Role gating: viewer / admin / super_admin per tool.
|
* 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/mindlyst/mindlyst-tools.js';
|
||||||
import './modules/lysnrai/lysnrai-tools.js';
|
import './modules/lysnrai/lysnrai-tools.js';
|
||||||
import './modules/jarvis/jarvis-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({
|
const app = await createServiceApp({
|
||||||
name: 'mcp-server',
|
name: 'mcp-server',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user