Module 6: MCP Apps and Interactive UI

Dashboards and Visualizations as Tool Output

Dashboards and Visualizations as Tool Output

Capsule description

In the previous capsule you learned the building blocks: content types, formatting patterns, and helpers to turn raw data into visual output. Now we're going to apply all of that to build something concrete: complete dashboards that a developer would use in their day-to-day.

A dashboard isn't an isolated table. It's a composition: a header with an executive summary, thematic sections with formatted data, health indicators, and action suggestions. This capsule teaches you to build dashboards of three types: project status, database analytics, and system monitor. Each demonstrates a different composition pattern.

By the end, you'll have complete implementations in TypeScript and Python that you can copy, adapt, and connect to your own data.


Architecture of an MCP dashboard

The structure that works

After experimenting with different formats, this is the layout that works best for dashboards in MCP:

┌─────────────────────────────────────────────┐
│  HEADER: Title + timestamp + summary        │
│  "📊 Project Dashboard — 142 files, 98% ✅" │
├─────────────────────────────────────────────┤
│  KEY METRICS: 3-5 KPIs in one line          │
│  Files: 142 | Tests: 98% | Coverage: 84%    │
├─────────────────────────────────────────────┤
│  SECTION 1: Table of main data              │
│  (the most important information first)     │
├─────────────────────────────────────────────┤
│  SECTION 2: Secondary table/chart           │
│  (complements the main section)             │
├─────────────────────────────────────────────┤
│  ALERTS: Detected problems                  │
│  (only if something requires attention)     │
├─────────────────────────────────────────────┤
│  FOOTER: Suggested actions + tips           │
│  "Use drill_down('tests') for detail"       │
└─────────────────────────────────────────────┘

Design principles

  1. The most important thing at the top. The executive summary goes first — the developer decides in 2 seconds whether they need to dig deeper.
  2. Independent sections. Each section should be understandable without reading the others.
  3. Visible alerts. If something requires action, it should be obvious (red emojis, a dedicated section).
  4. Clear actions. The footer suggests what to do next — which tool to invoke for more detail.
  5. Data, not noise. If a piece of data doesn't help make a decision, don't include it.

Dashboard 1: Project Status Dashboard

A dashboard that analyzes a project directory and shows key metrics.

TypeScript

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";
import { execSync } from "child_process";

const server = new McpServer({ name: "project-dashboard", version: "1.0.0" });

const SKIP_DIRS = new Set(["node_modules", ".git", "__pycache__", ".venv", "dist", "build"]);

async function scanDirectory(dir: string) {
  const filesByExt: Record<string, number> = {};
  const allFiles: { path: string; size: number; modified: number }[] = [];
  let totalSize = 0;

  async function walk(currentDir: string) {
    const entries = await fs.readdir(currentDir, { withFileTypes: true });
    for (const entry of entries) {
      if (SKIP_DIRS.has(entry.name)) continue;
      const fullPath = path.join(currentDir, entry.name);
      if (entry.isDirectory()) {
        await walk(fullPath);
      } else {
        const stats = await fs.stat(fullPath);
        const ext = path.extname(entry.name) || "(none)";
        filesByExt[ext] = (filesByExt[ext] || 0) + 1;
        totalSize += stats.size;
        allFiles.push({
          path: path.relative(dir, fullPath),
          size: stats.size,
          modified: stats.mtimeMs,
        });
      }
    }
  }

  await walk(dir);
  return { filesByExt, allFiles, totalSize };
}

function sizeStr(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

function timeAgo(ms: number): string {
  const diff = Date.now() - ms;
  const minutes = Math.floor(diff / 60000);
  if (minutes < 60) return `${minutes}min ago`;
  const hours = Math.floor(minutes / 60);
  if (hours < 24) return `${hours}h ago`;
  return `${Math.floor(hours / 24)}d ago`;
}

function progressBar(value: number, max: number, width = 15): string {
  const filled = max > 0 ? Math.round((value / max) * width) : 0;
  return "█".repeat(filled) + "░".repeat(width - filled);
}

server.tool(
  "project_dashboard",
  "Shows a complete dashboard of a project's status: files, sizes, recent activity",
  {
    directory: z.string().describe("The project's root directory"),
    topN: z.number().int().positive().default(8).describe("Number of items in each section"),
  },
  async ({ directory, topN }) => {
    try {
      await fs.access(directory);
    } catch {
      return {
        content: [{ type: "text" as const, text: `Error: directory '${directory}' not accessible` }],
        isError: true,
      };
    }

    const { filesByExt, allFiles, totalSize } = await scanDirectory(directory);
    const totalFiles = allFiles.length;
    const sortedExts = Object.entries(filesByExt).sort(([, a], [, b]) => b - a);
    const maxExtCount = sortedExts[0]?.[1] || 1;
    const largest = [...allFiles].sort((a, b) => b.size - a.size).slice(0, topN);
    const recent = [...allFiles].sort((a, b) => b.modified - a.modified).slice(0, topN);

    let gitInfo = "";
    try {
      const log = execSync("git log --oneline -5 2>/dev/null", { cwd: directory }).toString().trim();
      if (log) {
        gitInfo = "\n---\n\n### 📝 Recent commits\n\n";
        for (const line of log.split("\n")) {
          gitInfo += `- \`${line}\`\n`;
        }
      }
    } catch { /* not a git repo */ }

    let dashboard = `## 📊 Project Dashboard

**Directory:** \`${directory}\`
**Summary:** ${totalFiles} files | ${sizeStr(totalSize)} | ${sortedExts.length} file types

---

### 📁 Distribution by type

| Extension | Files | Distribution |
|-----------|----------|-------------|
`;

    for (const [ext, count] of sortedExts.slice(0, topN)) {
      const bar = progressBar(count, maxExtCount);
      const pct = ((count / totalFiles) * 100).toFixed(1);
      dashboard += `| \`${ext}\` | ${count} (${pct}%) | ${bar} |\n`;
    }

    if (sortedExts.length > topN) {
      const others = sortedExts.slice(topN).reduce((sum, [, c]) => sum + c, 0);
      dashboard += `| *others* | ${others} | — |\n`;
    }

    dashboard += `
---

### 📏 Largest files

| File | Size |
|---------|--------|
`;
    for (const f of largest) {
      dashboard += `| \`${f.path}\` | ${sizeStr(f.size)} |\n`;
    }

    dashboard += `
---

### 🕐 Recent activity

| File | Last modification |
|---------|-------------------|
`;
    for (const f of recent) {
      dashboard += `| \`${f.path}\` | ${timeAgo(f.modified)} |\n`;
    }

    dashboard += gitInfo;

    dashboard += `
---

*Tip: Use this dashboard regularly to detect files that grow out of control or areas of the project with no activity.*`;

    return { content: [{ type: "text" as const, text: dashboard }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Dashboard 2: Database Analytics View

A dashboard that shows statistics of a SQLite database.

Python

import sqlite3
import os
from datetime import datetime
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("db-analytics")


def connect_db(db_path: str) -> sqlite3.Connection:
    if not os.path.exists(db_path):
        raise FileNotFoundError(f"Database not found: {db_path}")
    return sqlite3.connect(db_path)


@mcp.tool()
async def db_dashboard(db_path: str) -> str:
    """Analytics dashboard for a SQLite database."""
    try:
        conn = connect_db(db_path)
    except FileNotFoundError as e:
        return f"Error: {e}"

    cursor = conn.cursor()

    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
    tables = [row[0] for row in cursor.fetchall()]

    if not tables:
        conn.close()
        return f"## 📊 DB Dashboard\n\n**Database:** `{db_path}`\n\n*Empty database — no tables.*"

    table_stats = []
    total_rows = 0
    for table in tables:
        cursor.execute(f"SELECT COUNT(*) FROM [{table}]")
        row_count = cursor.fetchone()[0]
        cursor.execute(f"PRAGMA table_info([{table}])")
        columns = cursor.fetchall()
        col_count = len(columns)
        col_names = [col[1] for col in columns[:5]]
        total_rows += row_count
        table_stats.append({
            "name": table,
            "rows": row_count,
            "columns": col_count,
            "column_names": col_names,
        })

    db_size = os.path.getsize(db_path)
    size_str = f"{db_size / 1024:.1f} KB" if db_size < 1024 * 1024 else f"{db_size / 1024 / 1024:.1f} MB"

    max_rows = max(t["rows"] for t in table_stats) if table_stats else 1

    dashboard = f"""## 📊 Database Analytics

**Database:** `{db_path}`
**Size:** {size_str} | **Tables:** {len(tables)} | **Total rows:** {total_rows:,}

---

### 📋 Tables

| Table | Rows | Columns | Distribution |
|-------|-------|----------|-------------|
"""

    for t in sorted(table_stats, key=lambda x: -x["rows"]):
        bar_len = int(t["rows"] / max_rows * 12) if max_rows > 0 else 0
        bar = "█" * bar_len + "░" * (12 - bar_len)
        dashboard += f"| `{t['name']}` | {t['rows']:,} | {t['columns']} | {bar} |\n"

    dashboard += "\n---\n\n### 🔍 Column detail\n\n"
    for t in table_stats[:6]:
        cols_str = ", ".join(f"`{c}`" for c in t["column_names"])
        extra = f" (+{t['columns'] - 5} more)" if t["columns"] > 5 else ""
        dashboard += f"- **{t['name']}**: {cols_str}{extra}\n"

    empty_tables = [t for t in table_stats if t["rows"] == 0]
    if empty_tables:
        dashboard += "\n---\n\n### ⚠️ Empty tables\n\n"
        for t in empty_tables:
            dashboard += f"- `{t['name']}` ({t['columns']} defined columns, 0 rows)\n"

    large_tables = [t for t in table_stats if t["rows"] > 10000]
    if large_tables:
        dashboard += "\n---\n\n### 📈 Large tables (>10K rows)\n\n"
        for t in large_tables:
            dashboard += f"- `{t['name']}` — **{t['rows']:,}** rows\n"

    dashboard += f"\n---\n\n*Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*"

    conn.close()
    return dashboard


@mcp.tool()
async def table_detail(db_path: str, table_name: str, sample_rows: int = 5) -> str:
    """Detail of a specific table with its schema and a data sample."""
    try:
        conn = connect_db(db_path)
    except FileNotFoundError as e:
        return f"Error: {e}"

    cursor = conn.cursor()

    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
    if not cursor.fetchone():
        conn.close()
        return f"Error: table '{table_name}' not found"

    cursor.execute(f"PRAGMA table_info([{table_name}])")
    columns = cursor.fetchall()
    cursor.execute(f"SELECT COUNT(*) FROM [{table_name}]")
    total_rows = cursor.fetchone()[0]
    cursor.execute(f"SELECT * FROM [{table_name}] LIMIT {sample_rows}")
    rows = cursor.fetchall()
    col_names = [col[1] for col in columns]

    detail = f"""## 🔎 Table: `{table_name}`

**Rows:** {total_rows:,} | **Columns:** {len(columns)}

---

### Schema

| Column | Type | Nullable | PK |
|---------|------|----------|-----|
"""
    for col in columns:
        _, name, type_, notnull, default, pk = col
        nullable = "❌" if notnull else "✅"
        is_pk = "🔑" if pk else ""
        detail += f"| `{name}` | {type_ or 'ANY'} | {nullable} | {is_pk} |\n"

    if rows:
        detail += f"\n---\n\n### Sample ({min(sample_rows, len(rows))} rows)\n\n"
        detail += "| " + " | ".join(f"`{c}`" for c in col_names) + " |\n"
        detail += "| " + " | ".join("---" for _ in col_names) + " |\n"
        for row in rows:
            values = [str(v)[:30] if v is not None else "*NULL*" for v in row]
            detail += "| " + " | ".join(values) + " |\n"
    else:
        detail += "\n*Empty table — no data to show.*\n"

    conn.close()
    return detail


if __name__ == "__main__":
    mcp.run()


Advanced pattern: Drill-down

An effective dashboard lets you "zoom in" — from the general summary to the detail of a specific area. You implement this with multiple tools that reference each other:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("analytics-drilldown")


@mcp.tool()
async def overview() -> str:
    """Summary dashboard — a high-level view."""
    return """## 📊 Overview

| Area | Status | Key metric |
|------|--------|--------------|
| Tests | ✅ 142 passing | 98.6% pass rate |
| Coverage | 🟡 78.3% | Target: 80% |
| Performance | ✅ p95: 230ms | Budget: 500ms |
| Dependencies | ⚠️ 3 outdated | 2 with vulnerabilities |

---

**Available drill-down:**
- `drill_down("tests")` — Test suite detail
- `drill_down("coverage")` — Coverage by file
- `drill_down("deps")` — Outdated dependencies
"""


@mcp.tool()
async def drill_down(area: str) -> str:
    """Shows the detail of a specific area of the dashboard."""
    if area == "tests":
        return """## 🧪 Test Suite — Detail

| Suite | Tests | Pass | Fail | Skip | Duration |
|-------|-------|------|------|------|----------|
| Unit | 98 | 97 | 1 | 0 | 4.2s |
| Integration | 34 | 34 | 0 | 0 | 12.8s |
| E2E | 10 | 9 | 0 | 1 | 45.3s |

### ❌ Failing tests

- `unit/auth.test.ts` > "should reject expired tokens" — AssertionError: expected 401, got 200

### ⏭️ Skipped tests

- `e2e/payment.test.ts` > "process refund" — TODO: mock payment gateway

---

*Return to the overview with `overview()`*
"""
    elif area == "coverage":
        return """## 📊 Coverage — By file

| File | Stmts | Branches | Lines |
|---------|-------|----------|-------|
| src/auth.ts | 🟢 95% | 🟢 90% | 🟢 95% |
| src/api.ts | 🟢 88% | 🟡 75% | 🟢 87% |
| src/db.ts | 🟡 72% | 🔴 55% | 🟡 71% |

🔴 **src/db.ts** — branches 55%. Tests for error paths are missing.

*Return to the overview with `overview()`*"""

    elif area == "deps":
        return """## 📦 Dependencies

| Package | Current | Latest | Notes |
|---------|--------|--------|-------|
| express | 4.18.2 | 4.21.0 | ⚠️ vuln low |
| lodash | 4.17.20 | 4.17.21 | 🔴 vuln medium |
| typescript | 5.2.2 | 5.6.3 | minor update |

*Return to the overview with `overview()`*"""

    return f"Area '{area}' not recognized. Options: tests, coverage, deps"


if __name__ == "__main__":
    mcp.run()

The drill-down pattern turns your MCP App into a navigation tool: the user sees the summary, identifies an area of interest, and asks for detail. It's the equivalent of clicking a section of a web dashboard.


Exercises

Exercise 1: Git dashboard (Easy)

Create a git_dashboard tool that runs git commands and presents a dashboard with: the current branch, the last 5 commits, modified files, and the working tree status.

See solution
import { execSync } from "child_process";

server.tool(
  "git_dashboard",
  "Dashboard of a Git repository's status",
  {
    repoPath: z.string().describe("Path to the repository"),
  },
  async ({ repoPath }) => {
    const run = (cmd: string) => {
      try { return execSync(cmd, { cwd: repoPath }).toString().trim(); }
      catch { return ""; }
    };

    const branch = run("git branch --show-current");
    const log = run("git log --oneline -5");
    const status = run("git status --short");
    const remoteStatus = run("git status --branch --porcelain=v2 | head -3");

    const statusLines = status ? status.split("\n") : [];
    const modified = statusLines.filter(l => l.startsWith(" M") || l.startsWith("M "));
    const added = statusLines.filter(l => l.startsWith("A ") || l.startsWith("??"));
    const deleted = statusLines.filter(l => l.startsWith("D ") || l.startsWith(" D"));

    let dashboard = `## 🔀 Git Dashboard

**Branch:** \`${branch || "detached"}\`
**Working tree:** ${statusLines.length === 0 ? "✅ Clean" : `⚠️ ${statusLines.length} changes`}

---

### 📝 Latest commits

`;
    if (log) {
      for (const line of log.split("\n")) {
        dashboard += `- \`${line}\`\n`;
      }
    } else {
      dashboard += "*No commits*\n";
    }

    if (statusLines.length > 0) {
      dashboard += `\n---\n\n### 📋 Pending changes\n\n`;
      dashboard += `| Type | Count |\n|------|----------|\n`;
      if (modified.length) dashboard += `| Modified | ${modified.length} |\n`;
      if (added.length) dashboard += `| New/Untracked | ${added.length} |\n`;
      if (deleted.length) dashboard += `| Deleted | ${deleted.length} |\n`;
      dashboard += `\n**Files:**\n\n`;
      for (const line of statusLines.slice(0, 10)) {
        dashboard += `- \`${line}\`\n`;
      }
      if (statusLines.length > 10) {
        dashboard += `\n*...and ${statusLines.length - 10} more*\n`;
      }
    }

    return { content: [{ type: "text" as const, text: dashboard }] };
  }
);

Exercise 2: API endpoints dashboard (Medium)

Create a Python tool api_dashboard that takes a list of endpoints with their metrics (path, method, avg_latency_ms, requests_24h, error_rate) and generates a dashboard with a table sorted by error rate, alerts for endpoints with >5% errors, and a health summary.

See solution
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field

mcp = FastMCP("api-dashboard")


class EndpointMetric(BaseModel):
    path: str
    method: str = "GET"
    avg_latency_ms: float
    requests_24h: int
    error_rate: float = Field(ge=0, le=100, description="Error rate in %")


@mcp.tool()
async def api_dashboard(endpoints: list[EndpointMetric]) -> str:
    """Health dashboard for API endpoints."""
    if not endpoints:
        return "## 📡 API Dashboard\n\n*No endpoints to analyze*"

    total_req = sum(e.requests_24h for e in endpoints)
    avg_latency = sum(e.avg_latency_ms for e in endpoints) / len(endpoints)
    healthy = sum(1 for e in endpoints if e.error_rate < 1)
    degraded = sum(1 for e in endpoints if 1 <= e.error_rate <= 5)
    unhealthy = sum(1 for e in endpoints if e.error_rate > 5)

    def status_emoji(rate: float) -> str:
        if rate < 1: return "✅"
        if rate <= 5: return "🟡"
        return "🔴"

    def latency_indicator(ms: float) -> str:
        if ms < 100: return "⚡"
        if ms < 500: return "✅"
        if ms < 2000: return "🟡"
        return "🐌"

    sorted_eps = sorted(endpoints, key=lambda e: -e.error_rate)

    lines = [
        "## 📡 API Health Dashboard",
        "",
        f"**Endpoints:** {len(endpoints)} | ✅ {healthy} healthy | 🟡 {degraded} degraded | 🔴 {unhealthy} unhealthy",
        f"**Requests (24h):** {total_req:,} | **Avg latency:** {avg_latency:.0f}ms",
        "",
        "---",
        "",
        "| Endpoint | Method | Latency | Req/24h | Errors | Status |",
        "|----------|--------|---------|---------|--------|--------|",
    ]

    for e in sorted_eps:
        lines.append(
            f"| `{e.path}` | {e.method} | {latency_indicator(e.avg_latency_ms)} {e.avg_latency_ms:.0f}ms "
            f"| {e.requests_24h:,} | {e.error_rate:.1f}% | {status_emoji(e.error_rate)} |"
        )

    alerts = [e for e in endpoints if e.error_rate > 5]
    if alerts:
        lines.extend(["", "---", "", "### 🔴 Require attention", ""])
        for e in alerts:
            lines.append(
                f"- **{e.method} {e.path}** — {e.error_rate:.1f}% error rate "
                f"({int(e.requests_24h * e.error_rate / 100):,} estimated errors in 24h)"
            )

    slow = [e for e in endpoints if e.avg_latency_ms > 1000]
    if slow:
        lines.extend(["", "---", "", "### 🐌 High latency (>1s)", ""])
        for e in slow:
            lines.append(f"- **{e.method} {e.path}** — {e.avg_latency_ms:.0f}ms average")

    return "\n".join(lines)

if __name__ == "__main__":
    mcp.run()

Exercise 3: Dependencies drill-down (Medium)

Create two tools: deps_overview that reads a package.json and shows a dependency summary (total, by type), and deps_detail that shows the detail of a specific dependency (installed version, whether it's a dev dependency, description).

See solution
import * as fs from "fs/promises";
import * as path from "path";

server.tool(
  "deps_overview",
  "Summary of a Node.js project's dependencies",
  { projectDir: z.string().describe("The project's directory") },
  async ({ projectDir }) => {
    const pkgPath = path.join(projectDir, "package.json");
    try {
      const raw = await fs.readFile(pkgPath, "utf-8");
      const pkg = JSON.parse(raw);
      const deps = Object.keys(pkg.dependencies || {});
      const devDeps = Object.keys(pkg.devDependencies || {});

      let out = `## 📦 Dependencies — ${pkg.name || "project"}\n\n`;
      out += `**Total:** ${deps.length + devDeps.length} | Production: ${deps.length} | Dev: ${devDeps.length}\n\n---\n\n`;
      out += "### Production\n\n| Package | Version |\n|---------|--------|\n";
      for (const dep of deps.slice(0, 15)) {
        out += `| \`${dep}\` | ${pkg.dependencies[dep]} |\n`;
      }
      if (deps.length > 15) out += `\n*...and ${deps.length - 15} more*\n`;
      out += "\n### Dev\n\n| Package | Version |\n|---------|--------|\n";
      for (const dep of devDeps.slice(0, 10)) {
        out += `| \`${dep}\` | ${pkg.devDependencies[dep]} |\n`;
      }
      if (devDeps.length > 10) out += `\n*...and ${devDeps.length - 10} more*\n`;
      out += `\n---\n\n*Use \`deps_detail\` to see info about a specific dependency.*`;
      return { content: [{ type: "text" as const, text: out }] };
    } catch {
      return { content: [{ type: "text" as const, text: `Error: could not read ${pkgPath}` }], isError: true };
    }
  }
);

Exercise 4: Dashboard with sparklines (Hard)

Create a Python tool metrics_trend that takes metrics with historical values (the last 7 days) and shows a dashboard with sparklines, trends, and a comparison with the previous week.

See solution
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field

mcp = FastMCP("metrics-trend")


def sparkline(values: list[float]) -> str:
    if not values: return ""
    chars = "▁▂▃▄▅▆▇█"
    mn, mx = min(values), max(values)
    rng = mx - mn if mx != mn else 1
    return "".join(chars[min(int((v - mn) / rng * 7), 7)] for v in values)


def trend(values: list[float]) -> str:
    if len(values) < 2: return "→"
    first_half = sum(values[:len(values)//2]) / (len(values)//2)
    second_half = sum(values[len(values)//2:]) / (len(values) - len(values)//2)
    if second_half > first_half * 1.05: return "📈 ↑"
    if second_half < first_half * 0.95: return "📉 ↓"
    return "➡️ →"


class MetricSeries(BaseModel):
    name: str
    values: list[float] = Field(min_length=2, description="Daily values (last 7 days)")
    unit: str = ""


@mcp.tool()
async def metrics_trend(metrics: list[MetricSeries]) -> str:
    """Trends dashboard with 7-day sparklines."""
    lines = ["## 📈 Metrics — 7-day trend", "",
             "| Metric | Current | Sparkline | Trend | Min | Max |",
             "|---------|--------|-----------|-------|-----|-----|"]

    for m in metrics:
        current = m.values[-1]
        spark = sparkline(m.values)
        t = trend(m.values)
        mn, mx = min(m.values), max(m.values)
        lines.append(f"| {m.name} | {current:.1f}{m.unit} | {spark} | {t} | {mn:.1f} | {mx:.1f} |")

    return "\n".join(lines)

if __name__ == "__main__":
    mcp.run()

Troubleshooting

"The dashboard looks messy when there's a lot of data"

Cause: You return too many rows in the tables without a limit.

Solution: Always limit the number of items and add a truncation indicator:

items = sorted(data, key=lambda x: -x["value"])[:MAX_ITEMS]
if len(data) > MAX_ITEMS:
    footer = f"\n*Showing the top {MAX_ITEMS} of {len(data)}*"

"The emojis don't display correctly"

Cause: The terminal or font doesn't support all Unicode emojis.

Solution: Use basic emojis that have universal support: ✅ ❌ ⚠️ 📊 📁 🔴 🟢 🟡. Avoid complex emojis or ZWJ combinations.

"execSync causes a timeout in the dashboard"

Cause: System commands like git log or df can hang if the repository is very large or the disk is slow.

Solution:

try {
  const output = execSync("git log --oneline -5", {
    cwd: repoPath,
    timeout: 5000,
  }).toString();
} catch {
  return "(git info not available)";
}

Summary

In this capsule you built three types of dashboards:

  • Project Status Dashboard — a directory analysis with file distribution, sizes, and activity
  • Database Analytics View — SQLite table statistics with schema and data samples
  • System Health Monitor — CPU, memory, disk with usage bars and alerts

And you learned the drill-down pattern — dashboards that let you navigate from the summary to the detail with tools that reference each other.

The key principles: important information at the top, independent sections, visible alerts, clear actions. A dashboard isn't just formatted data — it's a decision-making tool.


Additional resources

  1. Node.js os Module — Operating-system API for system dashboards
  2. Python sqlite3 Module — Interaction with SQLite
  3. MCP TypeScript SDK — Tools — Tools reference
  4. Unicode Block Elements — Characters for bars and charts
  5. Dashboard Design Patterns — Dashboard design principles

Next capsule: Interactive Forms — capturing user data via MCP, multi-step workflows, confirmations, and how to combine tools with prompts for interactive flows.