feat(mcp-server): Phase 3 — product namespaces for MindLyst, LysnrAI, JarvisJr (22 new tools)

This commit is contained in:
saravanakumardb1 2026-03-05 13:01:05 -08:00
parent 852cb18a5c
commit f780f0b409
9 changed files with 1065 additions and 1 deletions

View File

@ -7,6 +7,11 @@ LOG_LEVEL=info
PLATFORM_SERVICE_URL=http://localhost:4003
EXTRACTION_SERVICE_URL=http://localhost:4005
# Product-specific backend URLs (Phase 3 product namespaces)
MINDLYST_BACKEND_URL=http://localhost:4014
LYSNRAI_BACKEND_URL=http://localhost:4015
JARVISJR_BACKEND_URL=http://localhost:4012
# Auth — same JWT_SECRET as platform-service (tokens issued there are validated here)
JWT_SECRET=change-me-in-production

View File

@ -9,6 +9,10 @@ const envSchema = z.object({
JWT_SECRET: z.string().min(1, 'JWT_SECRET is required'),
PLATFORM_SERVICE_URL: z.string().default('http://localhost:4003'),
EXTRACTION_SERVICE_URL: z.string().default('http://localhost:4005'),
/** Product-specific backend URLs */
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'),
/** 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,177 @@
/**
* JarvisJr backend client typed HTTP wrappers for the jarvisjr-backend (port 4012).
*
* Auth: Bearer token from the caller's JWT (same JWT_SECRET as platform-service).
*/
import { config } from './config.js';
export interface JarvisClientOptions {
token?: string;
requestId?: string;
}
// ── Shared fetch helper ────────────────────────────────────────────────────
async function jarvisFetch<T>(
path: string,
init: RequestInit,
opts: JarvisClientOptions
): 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.JARVISJR_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(`jarvisjr-backend ${init.method ?? 'GET'} ${path}${res.status}: ${body}`);
}
return res.json() as Promise<T>;
}
// ── Agents ─────────────────────────────────────────────────────────────────
export interface JarvisAgentDoc {
id: string;
userId: string;
productId: string;
name: string;
role: string;
systemPrompt: string;
voiceId?: string;
coachingFramework?: string;
accentColor?: string;
welcomeMessage?: string;
sessionLength?: number;
difficultyLevel?: string;
language?: string;
privacyLevel?: string;
checkInSchedule?: string;
isTemplate?: boolean;
templateSource?: string;
totalSessions: number;
lastSessionAt: string | null;
createdAt: string;
updatedAt: string;
}
export function jarvisAgentsList(
params: { limit?: number; offset?: number },
opts: JarvisClientOptions
): Promise<{ agents: JarvisAgentDoc[]; 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));
const q = qs.toString();
return jarvisFetch(`/jarvis/agents${q ? `?${q}` : ''}`, { method: 'GET' }, opts);
}
export function jarvisAgentDuplicate(
agentId: string,
opts: JarvisClientOptions
): Promise<JarvisAgentDoc> {
return jarvisFetch(`/jarvis/agents/${agentId}/duplicate`, { method: 'POST' }, opts);
}
// ── Sessions ───────────────────────────────────────────────────────────────
export interface JarvisSessionDoc {
id: string;
userId: string;
productId: string;
agentId: string;
mode: string;
status: 'active' | 'completed';
summary?: string;
coachingNotes?: string[];
skillMetrics?: Record<string, number>;
duration?: number;
messageCount: number;
createdAt: string;
completedAt: string | null;
}
export interface JarvisSessionStats {
totalSessions: number;
totalDurationMinutes: number;
currentStreak: number;
longestStreak: number;
perAgent: Record<string, { sessions: number; avgDuration: number }>;
}
export function jarvisSessionsList(
params: { limit?: number; offset?: number; agentId?: string },
opts: JarvisClientOptions
): Promise<{ sessions: JarvisSessionDoc[]; 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.agentId) qs.set('agentId', params.agentId);
const q = qs.toString();
return jarvisFetch(`/jarvis/sessions${q ? `?${q}` : ''}`, { method: 'GET' }, opts);
}
export function jarvisSessionsGetStats(opts: JarvisClientOptions): Promise<JarvisSessionStats> {
return jarvisFetch('/jarvis/sessions/stats', { method: 'GET' }, opts);
}
// ── Memory ─────────────────────────────────────────────────────────────────
export interface JarvisMemoryDoc {
id: string;
agentId: string;
userId: string;
productId: string;
sessionId?: string;
type: 'skill_note' | 'preference' | 'goal' | 'context' | 'exercise';
content: string;
importance: number;
tags?: string[];
createdAt: string;
expiresAt?: string;
}
export function jarvisMemoryList(
agentId: string,
params: {
type?: string;
minImportance?: number;
limit?: number;
offset?: number;
},
opts: JarvisClientOptions
): Promise<{ memories: JarvisMemoryDoc[]; total: number }> {
const qs = new URLSearchParams();
if (params.type) qs.set('type', params.type);
if (params.minImportance !== undefined) qs.set('minImportance', String(params.minImportance));
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 jarvisFetch(
`/jarvis/agents/${agentId}/memory${q ? `?${q}` : ''}`,
{ method: 'GET' },
opts
);
}
export function jarvisMemoryPrune(
agentId: string,
opts: JarvisClientOptions
): Promise<{ pruned: number }> {
return jarvisFetch(`/jarvis/agents/${agentId}/memory/prune`, { method: 'POST' }, opts);
}
export function jarvisMemoryGetContext(
agentId: string,
limit: number | undefined,
opts: JarvisClientOptions
): Promise<{ memories: JarvisMemoryDoc[]; count: number }> {
const qs = limit !== undefined ? `?limit=${limit}` : '';
return jarvisFetch(`/jarvis/agents/${agentId}/memory/context${qs}`, { method: 'GET' }, opts);
}

View File

@ -0,0 +1,167 @@
/**
* LysnrAI backend client typed HTTP wrappers for the lysnrai-backend (port 4015).
*
* Auth: Bearer token from the caller's JWT (same JWT_SECRET as platform-service).
*/
import { config } from './config.js';
export interface LysnraiClientOptions {
token?: string;
requestId?: string;
}
// ── Shared fetch helper ────────────────────────────────────────────────────
async function lysnraiFetch<T>(
path: string,
init: RequestInit,
opts: LysnraiClientOptions
): 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.LYSNRAI_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(`lysnrai-backend ${init.method ?? 'GET'} ${path}${res.status}: ${body}`);
}
return res.json() as Promise<T>;
}
// ── Transcripts ────────────────────────────────────────────────────────────
export interface TranscriptDoc {
id: string;
userId: string;
productId: string;
rawText?: string;
cleanedText?: string;
wordCount?: number;
duration?: number;
tokensUsed?: number;
context?: string;
source?: string;
language?: string;
extractions?: unknown[];
extractionMetadata?: Record<string, unknown>;
extractedAt?: string;
createdAt: string;
}
export function lysnraiTranscriptsList(
params: { limit?: number; offset?: number },
opts: LysnraiClientOptions
): Promise<{ transcripts: TranscriptDoc[]; 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));
const q = qs.toString();
return lysnraiFetch(`/transcripts${q ? `?${q}` : ''}`, { method: 'GET' }, opts);
}
export function lysnraiTranscriptRunExtraction(
transcriptId: string,
opts: LysnraiClientOptions
): Promise<TranscriptDoc> {
return lysnraiFetch(`/transcripts/${transcriptId}/extract`, { method: 'POST' }, opts);
}
// ── STT status ─────────────────────────────────────────────────────────────
export interface SttStatusResult {
productId: string;
primary: 'azure' | 'whisper' | 'none';
azureConfigured: boolean;
whisperConfigured: boolean;
azureRegion: string | null;
generatedAt: string;
}
export function lysnraiSttGetBackendStatus(opts: LysnraiClientOptions): Promise<SttStatusResult> {
return lysnraiFetch('/transcripts/stt-status', { method: 'GET' }, opts);
}
// ── Sessions ───────────────────────────────────────────────────────────────
export interface SessionDoc {
id: string;
userId: string;
productId: string;
title: string;
status: 'active' | 'composed' | 'archived';
entries: unknown[];
composedText: string | null;
composedAt: string | null;
compositionRating: number | null;
createdAt: string;
updatedAt: string;
}
export function lysnraiSessionsList(
params: { limit?: number; offset?: number; status?: string },
opts: LysnraiClientOptions
): Promise<{ sessions: SessionDoc[]; 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);
const q = qs.toString();
return lysnraiFetch(`/sessions${q ? `?${q}` : ''}`, { method: 'GET' }, opts);
}
// ── Organisations ──────────────────────────────────────────────────────────
export interface OrgDoc {
id: string;
name: string;
ownerId: string;
productId: string;
createdAt: string;
}
export function lysnraiOrgsList(
params: { limit?: number; offset?: number },
opts: LysnraiClientOptions
): Promise<{ organizations: OrgDoc[]; 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));
const q = qs.toString();
return lysnraiFetch(`/organizations${q ? `?${q}` : ''}`, { method: 'GET' }, opts);
}
// ── API tokens ─────────────────────────────────────────────────────────────
export interface ApiTokenDoc {
id: string;
userId: string;
productId: string;
name: string;
lastUsedAt: string | null;
createdAt: string;
}
export function lysnraiApiTokensList(
params: { limit?: number; offset?: number },
opts: LysnraiClientOptions
): Promise<{ tokens: ApiTokenDoc[]; 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));
const q = qs.toString();
return lysnraiFetch(`/api-tokens${q ? `?${q}` : ''}`, { method: 'GET' }, opts);
}
export function lysnraiApiTokenRotate(
tokenId: string,
opts: LysnraiClientOptions
): Promise<{ token: string; id: string }> {
return lysnraiFetch(`/api-tokens/${tokenId}/rotate`, { method: 'POST' }, opts);
}

View File

@ -0,0 +1,221 @@
/**
* MindLyst backend client typed HTTP wrappers for the mindlyst-backend (port 4014).
*
* Auth: Bearer token from the caller's JWT (same JWT_SECRET as platform-service).
* Admin callers may pass x-admin-user-id to operate on behalf of any user.
*/
import { config } from './config.js';
export interface MindlystClientOptions {
token?: string;
requestId?: string;
/** When set (admin only) — overrides the JWT sub used as userId in backend routes */
adminUserId?: string;
}
// ── Shared fetch helper ────────────────────────────────────────────────────
async function mindlystFetch<T>(
path: string,
init: RequestInit,
opts: MindlystClientOptions
): 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.adminUserId ? { 'x-admin-user-id': opts.adminUserId } : {}),
};
const res = await fetch(`${config.MINDLYST_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(`mindlyst-backend ${init.method ?? 'GET'} ${path}${res.status}: ${body}`);
}
return res.json() as Promise<T>;
}
// ── Memory items ───────────────────────────────────────────────────────────
export interface MemoryItemDoc {
id: string;
productId: string;
userId: string;
sourceType: string;
captureSurface: string;
rawContent: string;
triageResult: {
contentType: string;
summary?: string;
urgencyScore: number;
emotionScore: number;
confidenceScore: number;
suggestedBrainId: string;
entities: string[];
suggestedActions: string[];
};
brainIds: string[];
actedOn: boolean;
actedOnAt: string | null;
nudgeCount: number;
reminderAt?: string;
isSensitive: boolean;
createdAt: string;
updatedAt: string;
}
export function mindlystMemoryList(
params: {
brainId?: string;
filter?: 'forgotten' | 'completed_today';
limit?: number;
offset?: number;
productId?: string;
},
opts: MindlystClientOptions
): Promise<{ items: MemoryItemDoc[]; limit: number; offset: number }> {
const qs = new URLSearchParams();
if (params.brainId) qs.set('brainId', params.brainId);
if (params.filter) qs.set('filter', params.filter);
if (params.limit !== undefined) qs.set('limit', String(params.limit));
if (params.offset !== undefined) qs.set('offset', String(params.offset));
if (params.productId) qs.set('productId', params.productId);
const q = qs.toString();
return mindlystFetch(`/memory-items${q ? `?${q}` : ''}`, { method: 'GET' }, opts);
}
export function mindlystMemoryRetriage(
itemId: string,
opts: MindlystClientOptions
): Promise<MemoryItemDoc> {
return mindlystFetch(`/memory-items/${itemId}/retriage`, { method: 'POST' }, opts);
}
export function mindlystMemoryPatch(
itemId: string,
action: 'mark_done' | 'mark_undone' | 'increment_nudge' | 'set_reminder',
reminderAt: string | undefined,
opts: MindlystClientOptions
): Promise<MemoryItemDoc> {
return mindlystFetch(
`/memory-items/${itemId}`,
{ method: 'PATCH', body: JSON.stringify({ action, ...(reminderAt ? { reminderAt } : {}) }) },
opts
);
}
export function mindlystMemoryReassign(
itemId: string,
newBrainId: string,
opts: MindlystClientOptions
): Promise<MemoryItemDoc> {
return mindlystFetch(
`/memory-items/${itemId}/reassign`,
{ method: 'PUT', body: JSON.stringify({ newBrainId }) },
opts
);
}
// ── Brains ─────────────────────────────────────────────────────────────────
export interface BrainDoc {
id: string;
userId: string;
productId: string;
name: string;
rolePrompt?: string;
tone?: string;
colorFrom?: string;
colorTo?: string;
createdAt: string;
updatedAt: string | null;
}
export function mindlystBrainsList(
params: { limit?: number; offset?: number },
opts: MindlystClientOptions
): Promise<{ items: BrainDoc[]; total: number; limit: number; offset: 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));
const q = qs.toString();
return mindlystFetch(`/brains${q ? `?${q}` : ''}`, { method: 'GET' }, opts);
}
// ── Daily briefs ───────────────────────────────────────────────────────────
export interface DailyBriefDoc {
id: string;
userId: string;
productId: string;
date: string;
greeting?: string;
priorityItems: unknown[];
brainSummaries: Record<string, string>;
streakMessage?: string;
motivationalQuote?: string;
createdAt: string;
}
export function mindlystBriefsGetToday(opts: MindlystClientOptions): Promise<DailyBriefDoc> {
return mindlystFetch('/daily-briefs/today', { method: 'GET' }, opts);
}
export function mindlystBriefsList(
params: { limit?: number; offset?: number },
opts: MindlystClientOptions
): Promise<{ items: DailyBriefDoc[]; 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));
const q = qs.toString();
return mindlystFetch(`/daily-briefs${q ? `?${q}` : ''}`, { method: 'GET' }, opts);
}
// ── Streaks ────────────────────────────────────────────────────────────────
export interface StreakDoc {
id: string;
userId: string;
productId: string;
currentStreak: number;
longestStreak: number;
lastActiveDate: string;
streakFreezeAvailable: boolean;
totalActiveDays: number;
createdAt: string;
updatedAt: string;
}
export function mindlystStreaksGet(opts: MindlystClientOptions): Promise<StreakDoc> {
return mindlystFetch('/streaks', { method: 'GET' }, opts);
}
// ── Reflections ────────────────────────────────────────────────────────────
export interface ReflectionDoc {
id: string;
userId: string;
productId: string;
weekStartDate: string;
themes: string[];
highlights: string[];
challenges: string[];
summary?: string;
createdAt: string;
}
export function mindlystReflectionsList(
params: { limit?: number; offset?: number },
opts: MindlystClientOptions
): Promise<{ items: ReflectionDoc[]; 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));
const q = qs.toString();
return mindlystFetch(`/reflections${q ? `?${q}` : ''}`, { method: 'GET' }, opts);
}

View File

@ -0,0 +1,150 @@
/**
* JarvisJr MCP tools jarvis.agents.*, jarvis.sessions.*, jarvis.memory.*
*
* Backed by: jarvisjr-backend (port 4012).
* All tools require admin role.
*/
import { z } from 'zod';
import { registerTool } from '../tools/registry.js';
import { config } from '../../lib/config.js';
import {
jarvisAgentsList,
jarvisAgentDuplicate,
jarvisSessionsList,
jarvisSessionsGetStats,
jarvisMemoryList,
jarvisMemoryPrune,
jarvisMemoryGetContext,
} from '../../lib/jarvis-client.js';
import type { McpToolRequest } from '../tools/types.js';
const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7);
// ── jarvis.agents.list ────────────────────────────────────────────────────
registerTool({
name: 'jarvis.agents.list',
description:
'List coaching agents for the authenticated user (name, role, coachingFramework, totalSessions). Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
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 jarvisAgentsList(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── jarvis.agents.duplicate ───────────────────────────────────────────────
registerTool({
name: 'jarvis.agents.duplicate',
description:
'Duplicate an existing agent (creates a copy with " (Copy)" suffix and zero session count). Useful for iterating on agent prompts. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
agentId: z.string().min(1).describe('Agent ID to duplicate'),
}),
async execute(args, req) {
return jarvisAgentDuplicate(args.agentId, { token: tokenOf(req), requestId: req.id });
},
});
// ── jarvis.sessions.list ──────────────────────────────────────────────────
registerTool({
name: 'jarvis.sessions.list',
description:
'List coaching sessions for the authenticated user. Filter by agentId. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
agentId: z.string().optional().describe('Filter to a specific agent ID'),
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 jarvisSessionsList(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── jarvis.sessions.stats ─────────────────────────────────────────────────
registerTool({
name: 'jarvis.sessions.stats',
description:
'Get coaching session statistics: total sessions, total duration, current streak, per-agent breakdown. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({}),
async execute(_args, req) {
return jarvisSessionsGetStats({ token: tokenOf(req), requestId: req.id });
},
});
// ── jarvis.memory.list ────────────────────────────────────────────────────
registerTool({
name: 'jarvis.memory.list',
description:
'List persistent memories for a coaching agent. Filter by type (skill_note, preference, goal, context, exercise) or minimum importance score. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
agentId: z.string().min(1).describe('Agent ID to query memories for'),
type: z
.enum(['skill_note', 'preference', 'goal', 'context', 'exercise'])
.optional()
.describe('Filter by memory type'),
minImportance: z.coerce
.number()
.min(0)
.max(1)
.optional()
.describe('Minimum importance score (0.01.0)'),
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) {
const { agentId, ...params } = args;
return jarvisMemoryList(agentId, params, { token: tokenOf(req), requestId: req.id });
},
});
// ── jarvis.memory.prune ───────────────────────────────────────────────────
registerTool({
name: 'jarvis.memory.prune',
description:
'Prune expired memories for a coaching agent (removes entries where expiresAt < now). Returns count of pruned entries. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
agentId: z.string().min(1).describe('Agent ID to prune memories for'),
}),
async execute(args, req) {
return jarvisMemoryPrune(args.agentId, { token: tokenOf(req), requestId: req.id });
},
});
// ── jarvis.memory.getContext ──────────────────────────────────────────────
registerTool({
name: 'jarvis.memory.getContext',
description:
'Retrieve the top-N memories for a coaching agent to use as session context (sorted by importance). Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
agentId: z.string().min(1).describe('Agent ID'),
limit: z.coerce
.number()
.min(1)
.max(50)
.default(20)
.describe('Number of context memories to return'),
}),
async execute(args, req) {
return jarvisMemoryGetContext(args.agentId, args.limit, {
token: tokenOf(req),
requestId: req.id,
});
},
});

View File

@ -0,0 +1,133 @@
/**
* LysnrAI MCP tools lysnrai.transcripts.*, lysnrai.stt.*, lysnrai.sessions.*,
* lysnrai.orgs.*, lysnrai.apiTokens.*
*
* Backed by: lysnrai-backend (port 4015).
* All tools require admin role.
*/
import { z } from 'zod';
import { registerTool } from '../tools/registry.js';
import { config } from '../../lib/config.js';
import {
lysnraiTranscriptsList,
lysnraiTranscriptRunExtraction,
lysnraiSttGetBackendStatus,
lysnraiSessionsList,
lysnraiOrgsList,
lysnraiApiTokensList,
lysnraiApiTokenRotate,
} from '../../lib/lysnrai-client.js';
import type { McpToolRequest } from '../tools/types.js';
const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7);
// ── lysnrai.transcripts.list ──────────────────────────────────────────────
registerTool({
name: 'lysnrai.transcripts.list',
description: 'List dictation transcripts for the authenticated user. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
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 lysnraiTranscriptsList(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── lysnrai.transcripts.runExtraction ────────────────────────────────────
registerTool({
name: 'lysnrai.transcripts.runExtraction',
description:
'Run extraction-service enrichment on a transcript — populates extractions[] and extractedAt. Best-effort: returns partial if extraction-service is unavailable. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
transcriptId: z.string().min(1).describe('Transcript ID (trx_... prefix)'),
}),
async execute(args, req) {
return lysnraiTranscriptRunExtraction(args.transcriptId, {
token: tokenOf(req),
requestId: req.id,
});
},
});
// ── lysnrai.stt.getBackendStatus ──────────────────────────────────────────
registerTool({
name: 'lysnrai.stt.getBackendStatus',
description:
'Report which speech-to-text backend is active: azure (Azure Speech SDK) or whisper (local Whisper model). Useful for ops health checks. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({}),
async execute(_args, req) {
return lysnraiSttGetBackendStatus({ token: tokenOf(req), requestId: req.id });
},
});
// ── lysnrai.sessions.list ─────────────────────────────────────────────────
registerTool({
name: 'lysnrai.sessions.list',
description: 'List dictation sessions for the authenticated user. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
status: z
.enum(['active', 'composed', 'archived'])
.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 lysnraiSessionsList(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── lysnrai.orgs.list ─────────────────────────────────────────────────────
registerTool({
name: 'lysnrai.orgs.list',
description: 'List organisations for the authenticated user. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
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 lysnraiOrgsList(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── lysnrai.apiTokens.list ────────────────────────────────────────────────
registerTool({
name: 'lysnrai.apiTokens.list',
description: 'List API tokens for the authenticated user. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
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 lysnraiApiTokensList(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── lysnrai.apiTokens.rotate ──────────────────────────────────────────────
registerTool({
name: 'lysnrai.apiTokens.rotate',
description:
'Rotate an API token — invalidates the current secret and issues a new one. Returns the new plaintext token (only visible once). Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
tokenId: z.string().min(1).describe('API token ID to rotate'),
}),
async execute(args, req) {
return lysnraiApiTokenRotate(args.tokenId, { token: tokenOf(req), requestId: req.id });
},
});

View File

@ -0,0 +1,201 @@
/**
* MindLyst MCP tools mindlyst.memory.*, mindlyst.brains.*, mindlyst.briefs.*,
* mindlyst.streaks.*, mindlyst.reflections.*, mindlyst.extractions.*
*
* Backed by: mindlyst-backend (port 4014) + extraction-service (port 4005).
* All tools require admin role.
*/
import { z } from 'zod';
import { registerTool } from '../tools/registry.js';
import { config } from '../../lib/config.js';
import {
mindlystMemoryList,
mindlystMemoryRetriage,
mindlystMemoryPatch,
mindlystMemoryReassign,
mindlystBrainsList,
mindlystBriefsGetToday,
mindlystBriefsList,
mindlystStreaksGet,
mindlystReflectionsList,
} from '../../lib/mindlyst-client.js';
import { extractionRun } from '../../lib/extraction-client.js';
import type { McpToolRequest } from '../tools/types.js';
const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7);
// ── mindlyst.memory.list ──────────────────────────────────────────────────
registerTool({
name: 'mindlyst.memory.list',
description:
'List MindLyst memory items for the authenticated user. Filter by brain, status, or use free-text. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
brainId: z.string().optional().describe('Filter to a specific Brain ID (e.g. "work", "home")'),
filter: z
.enum(['forgotten', 'completed_today'])
.optional()
.describe('forgotten = actedOn=false, completed_today = actedOn=true today'),
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 mindlystMemoryList(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── mindlyst.memory.retriage ──────────────────────────────────────────────
registerTool({
name: 'mindlyst.memory.retriage',
description:
'Re-run extraction on a single memory item to refresh its triage result (urgencyScore, suggestedBrainId, entities). Best-effort: returns current item if extraction-service is unavailable. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
itemId: z.string().min(1).describe('Memory item ID'),
}),
async execute(args, req) {
return mindlystMemoryRetriage(args.itemId, { token: tokenOf(req), requestId: req.id });
},
});
// ── mindlyst.memory.patch ─────────────────────────────────────────────────
registerTool({
name: 'mindlyst.memory.patch',
description:
'Update a memory item status: mark_done, mark_undone, increment_nudge, or set_reminder. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
itemId: z.string().min(1).describe('Memory item ID'),
action: z.enum(['mark_done', 'mark_undone', 'increment_nudge', 'set_reminder']),
reminderAt: z
.string()
.datetime()
.optional()
.describe('ISO 8601 datetime — required when action is set_reminder'),
}),
async execute(args, req) {
return mindlystMemoryPatch(args.itemId, args.action, args.reminderAt, {
token: tokenOf(req),
requestId: req.id,
});
},
});
// ── mindlyst.memory.reassign ──────────────────────────────────────────────
registerTool({
name: 'mindlyst.memory.reassign',
description:
'Reassign a memory item to a different Brain. Records a userCorrection for training. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
itemId: z.string().min(1).describe('Memory item ID'),
newBrainId: z.string().min(1).describe('Target Brain ID'),
}),
async execute(args, req) {
return mindlystMemoryReassign(args.itemId, args.newBrainId, {
token: tokenOf(req),
requestId: req.id,
});
},
});
// ── mindlyst.brains.list ──────────────────────────────────────────────────
registerTool({
name: 'mindlyst.brains.list',
description:
'List all Brains for the authenticated user (role-based life OS categories). Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
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 mindlystBrainsList(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── mindlyst.briefs.getToday ──────────────────────────────────────────────
registerTool({
name: 'mindlyst.briefs.getToday',
description:
"Get today's Daily Brief for the authenticated user. Returns 404 error if none generated yet. Requires admin role.",
requiredRole: 'admin',
inputSchema: z.object({}),
async execute(_args, req) {
return mindlystBriefsGetToday({ token: tokenOf(req), requestId: req.id });
},
});
// ── mindlyst.briefs.list ──────────────────────────────────────────────────
registerTool({
name: 'mindlyst.briefs.list',
description: 'List past Daily Briefs for the authenticated user. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
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 mindlystBriefsList(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── mindlyst.streaks.get ──────────────────────────────────────────────────
registerTool({
name: 'mindlyst.streaks.get',
description:
'Get the current engagement streak for the authenticated user (currentStreak, longestStreak, lastActiveDate). Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({}),
async execute(_args, req) {
return mindlystStreaksGet({ token: tokenOf(req), requestId: req.id });
},
});
// ── mindlyst.reflections.list ─────────────────────────────────────────────
registerTool({
name: 'mindlyst.reflections.list',
description: 'List reflection journal entries for the authenticated user. Requires admin role.',
requiredRole: 'admin',
inputSchema: z.object({
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 mindlystReflectionsList(args, { token: tokenOf(req), requestId: req.id });
},
});
// ── mindlyst.extractions.run ──────────────────────────────────────────────
registerTool({
name: 'mindlyst.extractions.run',
description: [
'Run text through the MindLyst extraction pipeline with one of the three task IDs:',
' • triage — extract brain signals, entities, actions (used for new memory items)',
' • memory-insight — summarise a set of memory items into brain-level insights',
' • reflection-enrichment — extract themes and highlights from journal text',
'Requires admin role.',
].join('\n'),
requiredRole: 'admin',
inputSchema: z.object({
text: z.string().min(1).describe('Text to extract from'),
taskId: z
.enum(['triage', 'memory-insight', 'reflection-enrichment'])
.default('triage')
.describe('MindLyst-specific extraction task ID'),
}),
async execute(args, req) {
return extractionRun({ text: args.text, taskId: args.taskId }, { requestId: req.id });
},
});

View File

@ -6,6 +6,9 @@
* platform.diagnostics.* manage debug sessions, read logs/traces, cancel
* extraction.* run extraction, list models, cache stats, sidecar health
* support.* compound tools (createDebugPack, runIncidentPipeline)
* mindlyst.* memory, brains, briefs, streaks, reflections, extractions
* lysnrai.* transcripts, sessions, orgs, apiTokens, stt
* jarvis.* agents, sessions, memory (JarvisJr coaching platform)
*
* Auth: JWT Bearer tokens issued by platform-service (same JWT_SECRET).
* Role gating: viewer / admin / super_admin per tool.
@ -23,12 +26,15 @@ import './modules/platform/diagnostics-tools.js';
import './modules/extraction/extraction-tools.js';
import './modules/support/debug-pack.js';
import './modules/a2a/pipeline-tool.js';
import './modules/mindlyst/mindlyst-tools.js';
import './modules/lysnrai/lysnrai-tools.js';
import './modules/jarvis/jarvis-tools.js';
const app = await createServiceApp({
name: 'mcp-server',
version: '0.1.0',
description:
'ByteLyst MCP Server — platform.telemetry.*, platform.diagnostics.*, extraction.*, support.*',
'ByteLyst MCP Server — platform.*, extraction.*, support.*, mindlyst.*, lysnrai.*, jarvis.*',
corsOrigin: config.CORS_ORIGIN,
logLevel: config.LOG_LEVEL,
});