diff --git a/services/mcp-server/src/modules/a2a/keyboard-diagnostics-pipeline.ts b/services/mcp-server/src/modules/a2a/keyboard-diagnostics-pipeline.ts new file mode 100644 index 00000000..c660e356 --- /dev/null +++ b/services/mcp-server/src/modules/a2a/keyboard-diagnostics-pipeline.ts @@ -0,0 +1,282 @@ +/** + * KeyboardDiagnosticsAgent — A2A pipeline for LysnrAI iOS keyboard extension diagnostics. + * + * Agent roster (3 steps): + * 1. KeyboardErrorScannerAgent — query telemetry for 'error' events from keyboard install IDs + * 2. DiagnosticsSessionAgent — open a diagnostics session targeting each erroring installId + * 3. BugReportDraftAgent — assemble structured bug report per install ID with error details + * + * MCP tools: + * lysnrai.keyboard.diagnose(from?, to?, minErrors?) — run pipeline + */ + +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; +import { registerTool } from '../tools/registry.js'; +import type { McpToolRequest } from '../tools/types.js'; +import { + telemetryQuery, + diagnosticsCreateSession, + diagnosticsUpdateSession, + type DebugSession, +} from '../../lib/platform-client.js'; + +// ── Types ────────────────────────────────────────────────────────────────────── + +interface KeyboardErrorGroup { + anonymousInstallId: string; + errorCount: number; + errorTypes: string[]; + firstSeen: string; + lastSeen: string; + rawEvents: Array>; +} + +interface DiagnosticsResult { + anonymousInstallId: string; + session: DebugSession | null; + skipReason?: string; +} + +interface BugReportDraft { + anonymousInstallId: string; + diagnosticsSessionId: string | null; + errorCount: number; + errorTypes: string[]; + firstSeen: string; + lastSeen: string; + reproSteps: string; + severity: 'critical' | 'high' | 'medium'; +} + +export interface KeyboardDiagnosticsReport { + runId: string; + productId: 'lysnrai'; + from: string; + to: string; + installsScanned: number; + erroringInstalls: number; + diagnosticsOpened: number; + bugReports: BugReportDraft[]; + summary: string; + generatedAt: string; +} + +// ── Step 1: KeyboardErrorScannerAgent ───────────────────────────────────────── + +async function scanKeyboardErrors( + fromTime: string, + toTime: string, + minErrors: number, + opts: { token?: string; requestId?: string } +): Promise { + const installMap = new Map(); + + try { + const result = await telemetryQuery( + { + productId: 'lysnrai', + eventType: 'error', + from: fromTime, + to: toTime, + limit: 500, + }, + { ...opts, productId: 'lysnrai' } + ); + + const events = result.events as Array>; + + for (const event of events) { + const props = (event['properties'] as Record) ?? {}; + const surface = String(props['surface'] ?? event['surface'] ?? '').toLowerCase(); + if (!surface.includes('keyboard')) continue; + + const installId = String( + props['anonymousInstallId'] ?? + event['anonymousId'] ?? + event['anonymousInstallId'] ?? + 'unknown' + ); + const errorType = String( + props['errorType'] ?? props['error_type'] ?? event['eventType'] ?? 'unknown' + ); + const timestamp = String( + event['timestamp'] ?? event['createdAt'] ?? new Date().toISOString() + ); + + const group = installMap.get(installId) ?? { + anonymousInstallId: installId, + errorCount: 0, + errorTypes: [], + firstSeen: timestamp, + lastSeen: timestamp, + rawEvents: [], + }; + + group.errorCount++; + if (!group.errorTypes.includes(errorType)) group.errorTypes.push(errorType); + if (timestamp < group.firstSeen) group.firstSeen = timestamp; + if (timestamp > group.lastSeen) group.lastSeen = timestamp; + group.rawEvents.push(event); + + installMap.set(installId, group); + } + } catch { + // best-effort + } + + return Array.from(installMap.values()).filter(g => g.errorCount >= minErrors); +} + +// ── Step 2: DiagnosticsSessionAgent ─────────────────────────────────────────── + +async function openDiagnosticsSession( + group: KeyboardErrorGroup, + opts: { token?: string; requestId?: string } +): Promise { + try { + const session = await diagnosticsCreateSession( + { + productId: 'lysnrai', + targetAnonymousId: group.anonymousInstallId, + collectionLevel: 'debug', + captureLogs: true, + }, + opts + ); + return { anonymousInstallId: group.anonymousInstallId, session }; + } catch (err) { + return { + anonymousInstallId: group.anonymousInstallId, + session: null, + skipReason: err instanceof Error ? err.message : String(err), + }; + } +} + +// ── Step 3: BugReportDraftAgent ─────────────────────────────────────────────── + +async function closeDiagnosticsAndDraft( + group: KeyboardErrorGroup, + diagnosticsResult: DiagnosticsResult, + opts: { requestId?: string } +): Promise { + if (diagnosticsResult.session) { + try { + await diagnosticsUpdateSession(diagnosticsResult.session.id, { status: 'completed' }, opts); + } catch { + // best-effort + } + } + + const severity: 'critical' | 'high' | 'medium' = + group.errorCount >= 10 ? 'critical' : group.errorCount >= 5 ? 'high' : 'medium'; + + const reproSteps = [ + `1. Open LysnrAI iOS keyboard extension on device with installId ${group.anonymousInstallId}.`, + `2. Trigger dictation — error types observed: ${group.errorTypes.join(', ')}.`, + `3. First seen ${group.firstSeen}, last seen ${group.lastSeen} (${group.errorCount} occurrences).`, + diagnosticsResult.session + ? `4. Diagnostics session ${diagnosticsResult.session.id} opened — check logs for stack traces.` + : '4. Diagnostics session could not be opened automatically.', + ].join('\n'); + + return { + anonymousInstallId: group.anonymousInstallId, + diagnosticsSessionId: diagnosticsResult.session?.id ?? null, + errorCount: group.errorCount, + errorTypes: group.errorTypes, + firstSeen: group.firstSeen, + lastSeen: group.lastSeen, + reproSteps, + severity, + }; +} + +// ── Pipeline runner ──────────────────────────────────────────────────────────── + +async function runKeyboardDiagnosticsPipeline( + fromTime: string, + toTime: string, + minErrors: number, + req: McpToolRequest +): Promise { + const runId = randomUUID(); + const opts = { token: req.headers.authorization?.slice(7), requestId: req.id }; + + req.log.info( + { runId, stepId: 'scan', fromTime, toTime, minErrors }, + 'KeyboardErrorScannerAgent start' + ); + const errorGroups = await scanKeyboardErrors(fromTime, toTime, minErrors, opts); + req.log.info( + { runId, stepId: 'scan', erroringInstalls: errorGroups.length }, + 'KeyboardErrorScannerAgent done' + ); + + req.log.info( + { runId, stepId: 'diagnostics', count: errorGroups.length }, + 'DiagnosticsSessionAgent start' + ); + const diagnosticsResults: DiagnosticsResult[] = []; + for (const group of errorGroups) { + const result = await openDiagnosticsSession(group, opts); + diagnosticsResults.push(result); + } + const opened = diagnosticsResults.filter(r => r.session !== null).length; + req.log.info({ runId, stepId: 'diagnostics', opened }, 'DiagnosticsSessionAgent done'); + + req.log.info({ runId, stepId: 'draft', count: errorGroups.length }, 'BugReportDraftAgent start'); + const bugReports: BugReportDraft[] = []; + for (let i = 0; i < errorGroups.length; i++) { + const draft = await closeDiagnosticsAndDraft(errorGroups[i]!, diagnosticsResults[i]!, { + requestId: req.id, + }); + bugReports.push(draft); + } + + const summary = + errorGroups.length === 0 + ? `No keyboard errors with >= ${minErrors} occurrence(s) found in the specified window.` + : `${errorGroups.length} install(s) with keyboard errors detected. ${opened} diagnostics session(s) opened. ${bugReports.filter(r => r.severity === 'critical').length} critical, ${bugReports.filter(r => r.severity === 'high').length} high severity.`; + + req.log.info({ runId, stepId: 'draft', summary }, 'BugReportDraftAgent done'); + + return { + runId, + productId: 'lysnrai', + from: fromTime, + to: toTime, + installsScanned: errorGroups.length, + erroringInstalls: errorGroups.length, + diagnosticsOpened: opened, + bugReports, + summary, + generatedAt: new Date().toISOString(), + }; +} + +// ── MCP tool registration ───────────────────────────────────────────────────── + +registerTool({ + name: 'lysnrai.keyboard.diagnose', + description: + 'A2A pipeline: scans LysnrAI telemetry for error events from the iOS keyboard extension, groups errors by anonymousInstallId, opens a diagnostics session per affected install, and drafts a structured bug report. Returns severity-ranked reports with repro steps. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + from: z.string().datetime().optional().describe('Start of telemetry window (default: 48h ago)'), + to: z.string().datetime().optional().describe('End of telemetry window (default: now)'), + minErrors: z.coerce + .number() + .int() + .min(1) + .default(3) + .describe('Minimum error count per install to include in report (default 3)'), + }), + async execute(args, req) { + const now = new Date(); + const toTime = args.to ?? now.toISOString(); + const fromTime = args.from ?? new Date(now.getTime() - 48 * 60 * 60 * 1000).toISOString(); + return runKeyboardDiagnosticsPipeline(fromTime, toTime, args.minErrors, req); + }, +}); diff --git a/services/mcp-server/src/modules/a2a/nl-parser-eval-pipeline.ts b/services/mcp-server/src/modules/a2a/nl-parser-eval-pipeline.ts new file mode 100644 index 00000000..e8d8ecbf --- /dev/null +++ b/services/mcp-server/src/modules/a2a/nl-parser-eval-pipeline.ts @@ -0,0 +1,266 @@ +/** + * NLParserEvalAgent — A2A pipeline for ChronoMind natural-language timer parsing evaluation. + * + * Agent roster (3 steps): + * 1. PhraseSamplerAgent — assemble canonical NL phrase test suite + * 2. ParseEvalAgent — run each phrase through extraction service 'timer-parse' task + * 3. RegressionReportAgent — validate parsed fields, flag failures, produce pass/fail scorecard + * + * MCP tools: + * chronomind.nlParser.eval(customPhrases?) — run eval pipeline; returns pass/fail scorecard + */ + +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; +import { registerTool } from '../tools/registry.js'; +import type { McpToolRequest } from '../tools/types.js'; +import { extractionRun } from '../../lib/extraction-client.js'; + +// ── Canonical test suite ────────────────────────────────────────────────────── + +interface NLTestCase { + phrase: string; + expectedFields: string[]; + description: string; +} + +const CANONICAL_TEST_CASES: NLTestCase[] = [ + { + phrase: 'Remind me in 30 minutes', + expectedFields: ['duration'], + description: 'Simple countdown', + }, + { + phrase: 'Wake me up at 7am tomorrow', + expectedFields: ['targetTime'], + description: 'Alarm at specific time', + }, + { + phrase: 'Meeting in 2 hours and 15 minutes', + expectedFields: ['duration'], + description: 'Compound duration', + }, + { + phrase: 'Call mom every day at 6pm', + expectedFields: ['targetTime', 'recurrence'], + description: 'Recurring alarm', + }, + { + phrase: 'Study session for 45 minutes then break for 10', + expectedFields: ['duration'], + description: 'Linked timers', + }, + { + phrase: 'Pomodoro 25 minutes focus', + expectedFields: ['duration', 'timerType'], + description: 'Pomodoro type', + }, + { + phrase: 'Take meds at 8am, noon, and 8pm', + expectedFields: ['targetTime', 'recurrence'], + description: 'Multi-time alarm', + }, + { + phrase: 'Boil eggs for 7 minutes', + expectedFields: ['duration', 'label'], + description: 'Labeled timer', + }, + { + phrase: 'Set a 1 hour countdown', + expectedFields: ['duration'], + description: 'Explicit countdown', + }, + { + phrase: 'Dentist appointment in 3 days', + expectedFields: ['targetTime'], + description: 'Days-ahead event', + }, +]; + +// ── Types ────────────────────────────────────────────────────────────────────── + +type EvalOutcome = 'pass' | 'fail' | 'partial' | 'error'; + +interface ParseEvalResult { + phrase: string; + description: string; + expectedFields: string[]; + parsedFields: string[]; + missingFields: string[]; + outcome: EvalOutcome; + errorMessage?: string; + rawExtraction: unknown; +} + +export interface NLParserEvalReport { + runId: string; + productId: 'chronomind'; + totalCases: number; + passed: number; + partial: number; + failed: number; + errored: number; + passRate: number; + results: ParseEvalResult[]; + regressions: string[]; + summary: string; + generatedAt: string; +} + +// ── Step 1: PhraseSamplerAgent ──────────────────────────────────────────────── + +function assemblePhrases(customPhrases: string[]): NLTestCase[] { + const custom: NLTestCase[] = customPhrases.map(phrase => ({ + phrase, + expectedFields: ['duration'], + description: 'Custom phrase', + })); + return [...CANONICAL_TEST_CASES, ...custom]; +} + +// ── Step 2: ParseEvalAgent ──────────────────────────────────────────────────── + +async function evalPhrase( + testCase: NLTestCase, + opts: { token?: string; requestId?: string } +): Promise { + try { + const result = await extractionRun({ text: testCase.phrase, taskId: 'timer-parse' }, opts); + + // Extract field names from the extraction result + const parsedFields: string[] = []; + for (const item of result.extractions) { + const itemRecord = item as unknown as Record; + const type = String(itemRecord['type'] ?? itemRecord['entity'] ?? '').toLowerCase(); + if (type) parsedFields.push(type); + // Also check properties for known timer fields + const props = itemRecord['properties'] as Record | undefined; + if (props) { + for (const key of ['duration', 'targetTime', 'timerType', 'recurrence', 'label']) { + if (key in props && !parsedFields.includes(key)) parsedFields.push(key); + } + } + } + + const missingFields = testCase.expectedFields.filter( + f => !parsedFields.some(p => p.includes(f.toLowerCase())) + ); + const outcome: EvalOutcome = + missingFields.length === 0 + ? 'pass' + : missingFields.length < testCase.expectedFields.length + ? 'partial' + : 'fail'; + + return { + phrase: testCase.phrase, + description: testCase.description, + expectedFields: testCase.expectedFields, + parsedFields, + missingFields, + outcome, + rawExtraction: result.extractions, + }; + } catch (err) { + return { + phrase: testCase.phrase, + description: testCase.description, + expectedFields: testCase.expectedFields, + parsedFields: [], + missingFields: testCase.expectedFields, + outcome: 'error', + errorMessage: err instanceof Error ? err.message : String(err), + rawExtraction: null, + }; + } +} + +// ── Step 3: RegressionReportAgent ───────────────────────────────────────────── + +function buildEvalReport(runId: string, results: ParseEvalResult[]): NLParserEvalReport { + const passed = results.filter(r => r.outcome === 'pass').length; + const partial = results.filter(r => r.outcome === 'partial').length; + const failed = results.filter(r => r.outcome === 'fail').length; + const errored = results.filter(r => r.outcome === 'error').length; + const passRate = results.length > 0 ? passed / results.length : 0; + + const regressions = results + .filter(r => r.outcome === 'fail' || r.outcome === 'error') + .map(r => `"${r.phrase}" — missing: ${r.missingFields.join(', ') || r.errorMessage}`); + + const summary = + results.length === 0 + ? 'No test cases evaluated.' + : `${passed}/${results.length} passed (${(passRate * 100).toFixed(0)}%). ${partial} partial, ${failed} failed, ${errored} errored.${regressions.length > 0 ? ` Regressions: ${regressions.length}.` : ' No regressions detected.'}`; + + return { + runId, + productId: 'chronomind', + totalCases: results.length, + passed, + partial, + failed, + errored, + passRate, + results, + regressions, + summary, + generatedAt: new Date().toISOString(), + }; +} + +// ── Pipeline runner ──────────────────────────────────────────────────────────── + +async function runNLParserEvalPipeline( + customPhrases: string[], + req: McpToolRequest +): Promise { + const runId = randomUUID(); + const opts = { token: req.headers.authorization?.slice(7), requestId: req.id }; + + req.log.info( + { runId, stepId: 'sample', customCount: customPhrases.length }, + 'PhraseSamplerAgent start' + ); + const testCases = assemblePhrases(customPhrases); + req.log.info( + { runId, stepId: 'sample', totalCases: testCases.length }, + 'PhraseSamplerAgent done' + ); + + req.log.info({ runId, stepId: 'eval', totalCases: testCases.length }, 'ParseEvalAgent start'); + const results: ParseEvalResult[] = []; + for (const testCase of testCases) { + const result = await evalPhrase(testCase, opts); + results.push(result); + } + const passed = results.filter(r => r.outcome === 'pass').length; + req.log.info({ runId, stepId: 'eval', passed, total: results.length }, 'ParseEvalAgent done'); + + req.log.info({ runId, stepId: 'report' }, 'RegressionReportAgent start'); + const report = buildEvalReport(runId, results); + req.log.info( + { runId, stepId: 'report', passRate: report.passRate, regressions: report.regressions.length }, + 'RegressionReportAgent done' + ); + + return report; +} + +// ── MCP tool registration ───────────────────────────────────────────────────── + +registerTool({ + name: 'chronomind.nlParser.eval', + description: + 'A2A pipeline: evaluates the ChronoMind natural-language timer parser by submitting a canonical phrase test suite (plus optional custom phrases) to the extraction service timer-parse task, validates parsed fields, and produces a pass/fail regression scorecard. Run after nl-parser changes to detect regressions. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + customPhrases: z + .array(z.string().min(1)) + .default([]) + .describe('Additional phrases to test beyond the built-in canonical suite'), + }), + async execute(args, req) { + return runNLParserEvalPipeline(args.customPhrases, req); + }, +}); diff --git a/services/mcp-server/src/modules/a2a/reflection-synthesis-pipeline.ts b/services/mcp-server/src/modules/a2a/reflection-synthesis-pipeline.ts new file mode 100644 index 00000000..cde896f6 --- /dev/null +++ b/services/mcp-server/src/modules/a2a/reflection-synthesis-pipeline.ts @@ -0,0 +1,203 @@ +/** + * ReflectionSynthesisAgent — A2A pipeline for MindLyst weekly reflection synthesis. + * + * Agent roster (3 steps): + * 1. ReflectionCollectorAgent — fetch recent reflections + * 2. ThemeExtractionAgent — run reflection-enrichment extraction on concatenated reflection text + * 3. SynthesisReportAgent — surface recurring themes, propose weekly summary text + * + * MCP tools: + * mindlyst.reflections.synthesize(limit?) — run pipeline + */ + +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; +import { registerTool } from '../tools/registry.js'; +import type { McpToolRequest } from '../tools/types.js'; +import { mindlystReflectionsList, type ReflectionDoc } from '../../lib/mindlyst-client.js'; +import { extractionRun, type ExtractionItem } from '../../lib/extraction-client.js'; + +// ── Types ────────────────────────────────────────────────────────────────────── + +interface ThemeFrequency { + theme: string; + occurrences: number; + sources: string[]; +} + +export interface ReflectionSynthesisReport { + runId: string; + productId: 'mindlyst'; + reflectionsAnalyzed: number; + topThemes: ThemeFrequency[]; + highlights: string[]; + challenges: string[]; + extractedEntities: ExtractionItem[]; + proposedSummary: string; + generatedAt: string; +} + +// ── Step 1: ReflectionCollectorAgent ────────────────────────────────────────── + +async function collectReflections( + limit: number, + opts: { token?: string; requestId?: string } +): Promise { + try { + const result = await mindlystReflectionsList({ limit }, opts); + return result.items; + } catch { + return []; + } +} + +// ── Step 2: ThemeExtractionAgent ────────────────────────────────────────────── + +async function extractThemes( + reflections: ReflectionDoc[], + opts: { token?: string; requestId?: string } +): Promise<{ entities: ExtractionItem[]; rawText: string }> { + if (reflections.length === 0) return { entities: [], rawText: '' }; + + const allThemes = reflections.flatMap(r => r.themes); + const allHighlights = reflections.flatMap(r => r.highlights); + const allChallenges = reflections.flatMap(r => r.challenges); + const summaries = reflections.map(r => r.summary).filter(Boolean) as string[]; + + const rawText = [ + 'Themes: ' + allThemes.join(', '), + 'Highlights: ' + allHighlights.join('. '), + 'Challenges: ' + allChallenges.join('. '), + summaries.length > 0 ? 'Summaries: ' + summaries.join(' ') : '', + ] + .filter(Boolean) + .join('\n'); + + try { + const result = await extractionRun({ text: rawText, taskId: 'reflection-enrichment' }, opts); + return { entities: result.extractions, rawText }; + } catch { + return { entities: [], rawText }; + } +} + +// ── Step 3: SynthesisReportAgent ────────────────────────────────────────────── + +function buildThemeFrequencies(reflections: ReflectionDoc[]): ThemeFrequency[] { + const frequencyMap = new Map(); + + for (const reflection of reflections) { + for (const theme of reflection.themes) { + const entry = frequencyMap.get(theme) ?? { count: 0, sources: [] }; + entry.count++; + entry.sources.push(reflection.weekStartDate); + frequencyMap.set(theme, entry); + } + } + + return Array.from(frequencyMap.entries()) + .map(([theme, { count, sources }]) => ({ theme, occurrences: count, sources })) + .sort((a, b) => b.occurrences - a.occurrences) + .slice(0, 10); +} + +function buildProposedSummary( + topThemes: ThemeFrequency[], + highlights: string[], + challenges: string[], + reflectionCount: number +): string { + if (reflectionCount === 0) return 'No reflections found to synthesize.'; + + const topThemeNames = topThemes + .slice(0, 3) + .map(t => t.theme) + .join(', '); + const summaryParts: string[] = [ + `Across ${reflectionCount} reflection(s), recurring themes include: ${topThemeNames || 'none identified'}.`, + ]; + if (highlights.length > 0) { + summaryParts.push(`Key highlights: ${highlights.slice(0, 3).join('; ')}.`); + } + if (challenges.length > 0) { + summaryParts.push(`Challenges to address: ${challenges.slice(0, 2).join('; ')}.`); + } + return summaryParts.join(' '); +} + +// ── Pipeline runner ──────────────────────────────────────────────────────────── + +async function runReflectionSynthesisPipeline( + limit: number, + req: McpToolRequest +): Promise { + const runId = randomUUID(); + const opts = { token: req.headers.authorization?.slice(7), requestId: req.id }; + + req.log.info({ runId, stepId: 'collect', limit }, 'ReflectionCollectorAgent start'); + const reflections = await collectReflections(limit, opts); + req.log.info( + { runId, stepId: 'collect', count: reflections.length }, + 'ReflectionCollectorAgent done' + ); + + req.log.info( + { runId, stepId: 'extract', count: reflections.length }, + 'ThemeExtractionAgent start' + ); + const { entities } = await extractThemes(reflections, opts); + req.log.info( + { runId, stepId: 'extract', entityCount: entities.length }, + 'ThemeExtractionAgent done' + ); + + req.log.info({ runId, stepId: 'synthesize' }, 'SynthesisReportAgent start'); + const topThemes = buildThemeFrequencies(reflections); + const allHighlights = reflections.flatMap(r => r.highlights); + const allChallenges = reflections.flatMap(r => r.challenges); + const proposedSummary = buildProposedSummary( + topThemes, + allHighlights, + allChallenges, + reflections.length + ); + + const report: ReflectionSynthesisReport = { + runId, + productId: 'mindlyst', + reflectionsAnalyzed: reflections.length, + topThemes, + highlights: allHighlights, + challenges: allChallenges, + extractedEntities: entities, + proposedSummary, + generatedAt: new Date().toISOString(), + }; + req.log.info( + { runId, stepId: 'synthesize', themeCount: topThemes.length }, + 'SynthesisReportAgent done' + ); + + return report; +} + +// ── MCP tool registration ───────────────────────────────────────────────────── + +registerTool({ + name: 'mindlyst.reflections.synthesize', + description: + 'A2A pipeline: collects recent MindLyst reflections, runs reflection-enrichment extraction to surface entities and patterns, identifies recurring themes, and proposes a weekly summary text. Returns top themes by frequency, extracted entities, and a draft weekly summary. Requires admin role.', + requiredRole: 'admin', + inputSchema: z.object({ + limit: z.coerce + .number() + .int() + .min(1) + .max(52) + .default(8) + .describe('Number of recent reflections to include (default 8, max 52 = 1 year)'), + }), + async execute(args, req) { + return runReflectionSynthesisPipeline(args.limit, req); + }, +}); diff --git a/services/mcp-server/src/server.ts b/services/mcp-server/src/server.ts index ab51590e..9d17827a 100644 --- a/services/mcp-server/src/server.ts +++ b/services/mcp-server/src/server.ts @@ -48,6 +48,9 @@ import './modules/a2a/triage-quality-pipeline.js'; import './modules/a2a/stt-fallback-monitor-pipeline.js'; import './modules/a2a/progress-analyst-pipeline.js'; import './modules/a2a/brain-overflow-pipeline.js'; +import './modules/a2a/reflection-synthesis-pipeline.js'; +import './modules/a2a/keyboard-diagnostics-pipeline.js'; +import './modules/a2a/nl-parser-eval-pipeline.js'; import './modules/mindlyst/mindlyst-tools.js'; import './modules/lysnrai/lysnrai-tools.js'; import './modules/jarvis/jarvis-tools.js';