get_dashboard
Retrieve the study progress dashboard with mastery levels, exam history, activity timeline, and capstone progress for certification preparation.
Instructions
Open the study progress dashboard in Claude Preview. Shows mastery levels, exam history, activity timeline, and capstone progress.
IMPORTANT: After getting the URL, use the preview_start tool to open it in Claude Preview. If the user says "show dashboard" or "open dashboard", call this tool.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/tools/dashboard.ts:8-33 (registration)Registration of the 'get_dashboard' tool via server.tool() with the name 'get_dashboard', description, empty schema, and async handler.
export function registerDashboard(server: McpServer, db: Database.Database, userConfig: UserConfig): void { server.tool( 'get_dashboard', 'Open the study progress dashboard in Claude Preview. Shows mastery levels, exam history, activity timeline, and capstone progress.\n\nIMPORTANT: After getting the URL, use the preview_start tool to open it in Claude Preview. If the user says "show dashboard" or "open dashboard", call this tool.', {}, async () => { if (!cachedServer) { cachedServer = await startDashboardServer(db, userConfig); } const url = `http://127.0.0.1:${cachedServer.port}/dashboard`; // Also build a text summary for non-Preview clients const summary = buildTextSummary(db, userConfig.userId); return { content: [ { type: 'text' as const, text: `Dashboard ready at: ${url}\n\nUse preview_start to open this URL in Claude Preview.\n\n${summary}`, }, ], }; } ); } - src/tools/dashboard.ts:13-31 (handler)Handler function that lazily starts the dashboard HTTP server, builds the URL and a text summary, then returns the content with a prompt to use preview_start.
async () => { if (!cachedServer) { cachedServer = await startDashboardServer(db, userConfig); } const url = `http://127.0.0.1:${cachedServer.port}/dashboard`; // Also build a text summary for non-Preview clients const summary = buildTextSummary(db, userConfig.userId); return { content: [ { type: 'text' as const, text: `Dashboard ready at: ${url}\n\nUse preview_start to open this URL in Claude Preview.\n\n${summary}`, }, ], }; } - src/tools/dashboard.ts:35-91 (helper)buildTextSummary helper function that queries domain_mastery and exam_attempts tables to generate a plain-text summary of overall readiness, domain progress, and exam stats for non-Preview clients.
function buildTextSummary(db: Database.Database, userId: string): string { const DOMAIN_NAMES: Readonly<Record<number, string>> = { 1: 'Agentic Architecture', 2: 'Tool Design & MCP', 3: 'Claude Code Config', 4: 'Prompt Engineering', 5: 'Context & Reliability', }; interface DomainSummaryRow { readonly domainId: number; readonly avgAccuracy: number; readonly totalAttempts: number; } const domainRows = db.prepare(` SELECT domainId, AVG(accuracyPercent) as avgAccuracy, SUM(totalAttempts) as totalAttempts FROM domain_mastery WHERE userId = ? GROUP BY domainId ORDER BY domainId ASC `).all(userId) as readonly DomainSummaryRow[]; const domainMap = new Map(domainRows.map(r => [r.domainId, r])); const domainLines = [1, 2, 3, 4, 5].map(id => { const row = domainMap.get(id); const mastery = row ? Math.round(row.avgAccuracy) : 0; const answered = row ? row.totalAttempts : 0; return ` D${id}: ${DOMAIN_NAMES[id]} — ${mastery}% mastery, ${answered} answered`; }); const overallMastery = domainRows.length > 0 ? Math.round(domainRows.reduce((sum, r) => sum + r.avgAccuracy, 0) / 5) : 0; interface ExamCountRow { readonly total: number; readonly passed: number; } const examStats = db.prepare(` SELECT COUNT(*) as total, SUM(CASE WHEN passed THEN 1 ELSE 0 END) as passed FROM exam_attempts WHERE userId = ? AND completedAt IS NOT NULL `).get(userId) as ExamCountRow; return [ '--- TEXT SUMMARY ---', `Overall Readiness: ${overallMastery}%`, '', 'Domain Progress:', ...domainLines, '', `Practice Exams: ${examStats.total} taken, ${examStats.passed ?? 0} passed`, ].join('\n'); } - src/ui/server.ts:225-305 (helper)startDashboardServer function that creates an HTTP server serving the dashboard HTML, JSON API data at /api/data, and static files.
export function startDashboardServer( db: Database.Database, userConfig: UserConfig, ): Promise<{ port: number; close: () => void }> { // In bundled mode, __dirname is dist/. UI files are at dist/ui/. // In dev mode, __dirname is src/ui/. UI files are at ../../dist/ui/. const bundledPath = path.resolve(__dirname, 'ui'); const devPath = path.resolve(__dirname, '..', '..', 'dist', 'ui'); const distUiDir = fs.existsSync(bundledPath) ? bundledPath : devPath; const userId = userConfig.userId; const server = http.createServer((req, res) => { const url = new URL(req.url ?? '/', `http://localhost`); const pathname = url.pathname; // CORS headers for local dev res.setHeader('Access-Control-Allow-Origin', '*'); if (pathname === '/api/data') { const data = buildDashboardData(db, userId); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(data)); return; } if (pathname === '/dashboard' || pathname === '/') { const htmlPath = path.join(distUiDir, 'dashboard.html'); let html: string; try { html = fs.readFileSync(htmlPath, 'utf-8'); } catch { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('dashboard.html not found'); return; } const data = buildDashboardData(db, userId); const injection = `<script>window.__DASHBOARD_DATA__ = ${JSON.stringify(data)};</script>`; const injectedHtml = html.replace('</head>', `${injection}\n</head>`); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(injectedHtml); return; } // Static file serving const safePath = path.normalize(pathname).replace(/^(\.\.[/\\])+/, ''); const filePath = path.join(distUiDir, safePath); // Prevent directory traversal if (!filePath.startsWith(distUiDir)) { res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Forbidden'); return; } try { const content = fs.readFileSync(filePath); res.writeHead(200, { 'Content-Type': getMimeType(filePath) }); res.end(content); } catch { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not found'); } }); return new Promise((resolve, reject) => { server.on('error', reject); server.listen(0, '127.0.0.1', () => { const addr = server.address(); if (!addr || typeof addr === 'string') { reject(new Error('Failed to get server address')); return; } resolve({ port: addr.port, close: () => server.close(), }); }); }); } - src/ui/server.ts:97-197 (helper)buildDashboardData function that queries database to assemble the full dashboard data (domain mastery, exam history, recent activity, capstone progress) returned as JSON or embedded into HTML.
function buildDashboardData(db: Database.Database, userId: string): DashboardData { // Domain mastery aggregated by domainId const domainRows = db.prepare(` SELECT domainId, SUM(totalAttempts) as totalAttempts, AVG(accuracyPercent) as avgAccuracy, MIN(masteryLevel) as masteryLevel, COUNT(*) as taskCount FROM domain_mastery WHERE userId = ? GROUP BY domainId ORDER BY domainId ASC `).all(userId) as readonly DomainAggRow[]; const domainMap = new Map(domainRows.map(r => [r.domainId, r])); const domains: readonly DashboardDomain[] = ALL_DOMAIN_IDS.map(id => { const row = domainMap.get(id); return { id, name: DOMAIN_NAMES[id] ?? `Domain ${id}`, mastery: row ? Math.round(row.avgAccuracy) : 0, level: row ? deriveDomainLevel(row.avgAccuracy) : 'unassessed', answered: row ? row.totalAttempts : 0, total: row ? row.taskCount : 0, }; }); const overallReadiness = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.mastery, 0) / domains.length) : 0; // Exam history const examRows = db.prepare(` SELECT completedAt, score, passed FROM exam_attempts WHERE userId = ? AND completedAt IS NOT NULL ORDER BY completedAt DESC `).all(userId) as readonly ExamRow[]; const examHistory: readonly DashboardExamEntry[] = examRows.map(r => ({ date: r.completedAt, score: r.score, passed: Boolean(r.passed), })); // Recent activity (last 10 answers) const answerRows = db.prepare(` SELECT questionId, domainId, isCorrect, answeredAt FROM answers WHERE userId = ? ORDER BY answeredAt DESC LIMIT 10 `).all(userId) as readonly AnswerRow[]; const recentActivity: readonly DashboardActivity[] = answerRows.map(r => ({ questionId: r.questionId, domain: DOMAIN_NAMES[r.domainId] ?? `Domain ${r.domainId}`, correct: Boolean(r.isCorrect), timestamp: r.answeredAt, })); // Capstone build const capstoneRow = db.prepare(` SELECT id, theme, currentStep FROM capstone_builds WHERE userId = ? AND status IN ('shaping', 'building') ORDER BY createdAt DESC LIMIT 1 `).get(userId) as CapstoneRow | undefined; let capstoneBuild: DashboardCapstone | null = null; if (capstoneRow) { const steps = db.prepare(` SELECT buildCompleted, taskStatements FROM capstone_build_steps WHERE buildId = ? ORDER BY stepIndex ASC `).all(capstoneRow.id) as readonly StepRow[]; const coveredCriteria = new Set<string>(); for (const step of steps) { if (!step.buildCompleted) continue; const taskStatements = JSON.parse(step.taskStatements) as readonly string[]; for (const ts of taskStatements) { coveredCriteria.add(ts); } } capstoneBuild = { theme: capstoneRow.theme, currentStep: capstoneRow.currentStep, totalSteps: BUILD_STEPS.length, criteriaCompleted: coveredCriteria.size, totalCriteria: BUILD_STEPS.reduce((sum, s) => sum + s.taskStatements.length, 0), }; } return { overallReadiness, domains, examHistory, recentActivity, capstoneBuild }; }