Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions plugins/mixpanel-mcp-eu/skills/manage-boards/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
name: manage-boards
description: >
Full lifecycle dashboard management for Mixpanel — create, template, clone, clean up,
inventory, and update dashboards across projects and teams. ALWAYS use when a user asks
to: create a dashboard, build a board, template or clone a dashboard, clean up stale
dashboards, audit dashboards, list all dashboards, inventory boards, update or rename a
dashboard, delete a dashboard, or standardize boards across projects. Also trigger on
"duplicate board", "stale dashboards", "empty boards", "board management", or when
dashboards are mentioned with "governance", "cleanup", "template", "onboarding", or
"standardize". Do NOT use for A/B experiments (use `manage-experiment`), feature-flag
rollouts (use `manage-feature-flags`), or Lexicon/event-and-property metadata cleanup (use
`manage-lexicon`). Requires Mixpanel MCP.
compatibility: "Requires Mixpanel MCP (mcp.mixpanel.com or regional variants). No other connectors required."
---

# Manage Boards

Top-level router: validate project → route command → handle return.

This skill uses the **Mixpanel MCP only**. No other connectors are required or referenced.

---

## Execution Philosophy

**Silent execution.** Do not narrate steps, announce phases, or explain what you are about to do. Run the work, process results, present only final output. The user sees:
- Command menu (when needed)
- Confirmation prompts before destructive operations (delete, bulk cleanup)
- Progress indicators during batch operations
- Error messages
- Final output

Nothing else. No "Starting X...", no "I'll now fetch Y...", no "Let me analyze Z...". Just do the work and show the result.

---

## Global Rules

1. **Silent execution** — see the Execution Philosophy section above.
2. **Project ID immutable.** All work uses the confirmed `project_id` unless explicitly cross-project (template command).
3. **Surface MCP failures explicitly.** Never silently skip.
4. **Confirm before destructive ops.** Always preview + explicit user confirmation before: deleting a dashboard, bulk cleanup, or overwriting dashboard content.
5. **'exit' always valid.** Stop, discard uncommitted, return to menu.
6. **No per-command "what next" menus.** Commands return control here. The router shows the menu.
7. **Cross-project operations.** The template command is the only one that works across projects. It validates both source and target project IDs and reconstructs the board in the target project (see `commands/template-dashboard.md`).
8. **Validate every write before reporting success.** After any operation that mutates a board — create, update, duplicate, or a template rebuild — re-read the affected board *including its full layout* and confirm the result matches intent (expected row count, report/text cell counts, title/description). If the readback diverges, do NOT report `✅` — surface what landed vs. what was requested. Layout cells use opaque server-generated IDs, so a write can partially succeed silently; the readback is the only reliable confirmation. Refresh `dashboard_layout_cache` from this readback.
9. **Fetching the dashboard set.** Default to the sortable entity-search capability when listing dashboards — it returns richer metadata and supports sorting. Fall back to the plain dashboard-list capability only if search is unavailable. Cache the result in `dashboard_list_cache` and reuse it across commands rather than re-fetching.
10. **Shared constraints.** Layout limits, the text-card HTML whitelist, and other API gotchas live once in `references/mcp-tool-reference.md`. Command files point to it by name rather than restating.

---

## Session Context

Persist across commands within a session. **Cross-command reuse is mandatory** — never re-fetch what exists in session.

| Variable | Description |
|----------|-------------|
| `project_id` | Immutable after Step 0. |
| `project_name` | Display name. |
| `projects_list` | All accessible projects. |
| `dashboard_list_cache` | `dashboard_id → {title, description, last_modified, ...}` map. Report counts are NOT returned by the list operation — populate them from `dashboard_layout_cache` after reading a board's full layout. |
| `dashboard_layout_cache` | `dashboard_id → {layout, report_count, text_count, row_count}` map (populated on-demand). |

---

## Step 0 — Project Validation

**Direct routing:** If the user's message contains a project ID AND a clear command intent (e.g. "create a dashboard in project 12345", "clean up dashboards in project 67890"), extract both, validate, and route directly — skip the menu.

**Project ID without command:** Validate and show menu.

**No project ID:** Ask which project. On 'list' → retrieve the accessible projects and show them in a table.

**Validation:**
1. Retrieve the list of accessible projects and match the ID. If found → `✅ [Project Name] ([project_id])`, proceed.
2. Not found → error, ask to re-enter or 'list'.

**MCP check:** If the project lookup fails because no Mixpanel capability is available → tell the user to connect the Mixpanel MCP, then stop.

---

## Command Menu

Show only when no direct command was inferred:

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Manage Boards — [Project Name] ([project_id])
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Create Dashboard — Build a new board from description
2. Template Dashboard — Clone a reference board to a new project
3. Cleanup Dashboards — Audit & remove stale/empty boards
4. Dashboard Inventory — Catalog all boards with metadata
5. Duplicate Dashboard — Copy a board within the same project
6. Update Dashboard — Modify metadata, rows, or layout
7. Exit
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

| Choice | Action |
|--------|--------|
| **1** | Read `commands/create-dashboard.md`, execute |
| **2** | Read `commands/template-dashboard.md`, execute |
| **3** | Read `commands/cleanup-dashboards.md`, execute |
| **4** | Read `commands/dashboard-inventory.md`, execute |
| **5** | Read `commands/duplicate-dashboard.md`, execute |
| **6** | Read `commands/update-dashboard.md`, execute |
| **7** | Exit |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A routing matrix would complement this menu section for cases where the agent has to infer routing behavior because the user doesn't exactly follow the choices given.

**Routing matrix** — when the user's phrasing doesn't map to a menu number, route on intent rather than showing the menu:

| User intent (examples) | Route to |
|------------------------|----------|
| "build / make / set up a board", "new dashboard for X" | Create |
| "copy this board to [other project]", "standardize boards across projects" | Template |
| "clean up / audit / find stale / empty / duplicate boards" | Cleanup |
| "list / catalog / inventory all boards", "what boards exist" | Inventory |
| "copy / duplicate this board here", "clone within this project" | Duplicate |
| "rename / edit / add a row / change the layout of a board" | Update |
| "delete [board]" | Cleanup (single-board delete path) |
| Ambiguous or matches multiple commands | Show the menu — do not guess |

---

## Return Handler

When a command completes → `✅ Done.` → re-display command menu. No re-validation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Command: Cleanup Dashboards

Audits the dashboard estate in a project: identifies **stale** (not touched in a long time), duplicate, empty, and sparse boards. Recommends archive or delete.

Staleness is a first-class signal here — a board with many reports that nobody has touched in months is still a cleanup candidate. Do not equate "has reports" with "healthy."

---

## Contents

- Phase 1 — Fetch all dashboards (with recency)
- Phase 2 — Deep inspection
- Phase 3 — Classification (structural + recency flags, duplicates)
- Phase 4 — Report
- Phase 5 — Action (interactive delete paths)
- Output
- Error Handling

---

## Execution

### Phase 1 — Fetch all dashboards (with recency)

1. Fetch the dashboard set per the **Fetching the dashboard set** rule (sortable entity-search preferred,
sorting on the most recent timestamp the API exposes; plain list as fallback).
2. For each dashboard, extract whatever the response provides:
- `id`, `title`, `description`
- `last_modified` (a.k.a. modified/updated), `last_viewed` (if present), `created_at`
- `creator` / `owner` (if available)
3. Cache in `dashboard_list_cache`.

**Recency field discovery (do this once, silently):** Scan the result set — not just the first object — to determine which timestamp fields the API populates, since field coverage can vary board to board. Choose the preferred field by coverage across boards, in priority order: `last_viewed` → `last_modified`/`updated_at` → `created_at`. Per board, if the chosen field is missing, fall back down the same order for that board and label its recency basis accordingly. Record the field(s) used so the report can label the column accurately. If NO board exposes any timestamp field, skip the Stale classification entirely and tell the user in the report header: "Recency data not available from the API — staleness not assessed; showing structural flags only."

### Phase 2 — Deep inspection

For each dashboard from Phase 1, read its full layout. Fire in parallel (batches of 5 to avoid rate limits). Skip any dashboard already in `dashboard_layout_cache`.

From the layout, derive and cache:
- **Report count:** number of report cells
- **Text-only count:** number of text cells
- **Total cells:** sum of all cells
- **Row count:** number of rows

### Phase 3 — Classification

Compute `days_since` from the chosen recency timestamp (today minus that date). Then classify. **Staleness and structure are independent axes** — evaluate both; a board may carry a structural flag *and* a stale flag.

> **Use the bundled helper for staleness and duplicate math.** Run `scripts/dashboard_utils.py` rather than re-deriving title normalization or recency arithmetic by hand each run — it is the single source of truth for `days_since(...)`, `normalize_title(...)`, and `is_probable_duplicate(...)`. This keeps results deterministic across runs.

**Structural flags** (from layout):

| Category | Criteria | Flag |
|----------|----------|------|
| **Empty** | 0 report cells AND 0 text cells (or only a single default text card) | 🟡 Empty |
| **Text-only** | 0 report cells but has text cards | 🟡 Text-only |
| **Sparse** | 1-2 report cells only | 🔵 Sparse |
| **Potential duplicate** | `is_probable_duplicate()` returns true vs. another board | 🟠 Possible dup |
| **Healthy** | 3+ report cells | ✅ Active |

**Recency flag** (from `days_since`, only if a timestamp was found):

| Category | Criteria | Flag |
|----------|----------|------|
| **Stale** | `days_since` ≥ 90 | 🔴 Stale ([N]d) |
| **Aging** | 60 ≤ `days_since` < 90 | 🟤 Aging ([N]d) |
| **Recent** | `days_since` < 60 | (no flag) |

> Default threshold is 90 days (a skill-chosen heuristic). If the user names a different window ("anything untouched for 6 months", "stale = 30 days"), use theirs. State the threshold in the report header.

**Cleanup priority** — order the action list by: 🔴 Stale **and** (Empty/Text-only/Sparse) first → 🔴 Stale + Active → 🟠 duplicates → 🟡 structural-only. The worst offenders are old *and* thin.

**Duplicate detection** — delegated to `scripts/dashboard_utils.py`. Run `is_probable_duplicate(a, b)` pairwise across the board list; flag both members of any matching pair. (It matches on normalized-title similarity ≥ 0.80 or one normalized title contained in the other — the module is the source of truth for the exact rule.)

### Phase 4 — Report

Present the audit as a table. Include the recency column and label it with the actual field used.

```
Dashboard Audit — [Project Name] ([project_id])
Recency basis: Last modified | Stale threshold: 90 days
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ID | Title | Reports | Last modified | Status
---------|---------------------------|---------|---------------|------------------
1234567 | Onboarding Funnel | 6 | 5d ago | ✅ Active
1234568 | Onboarding Funnel (Copy) | 6 | 142d ago | 🔴 Stale · 🟠 dup
1234569 | Test Board | 0 | 311d ago | 🔴 Stale · 🟡 Empty
1234570 | Notes | 0 | 18d ago | 🟡 Text-only
1234571 | Quick Metrics | 1 | 96d ago | 🔴 Stale · 🔵 Sparse
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Summary: 15 dashboards | 4 stale | 3 empty | 2 possible duplicates | 1 sparse | 8 active
Top cleanup candidates: #1234569 (stale + empty), #1234568 (stale + dup), #1234571 (stale + sparse)
```

If recency data was unavailable, drop the "Last modified" column and the stale flags, and note it in the header.

### Phase 5 — Action (interactive)

After showing the audit, ask what the user wants to do:

**Option A: Delete selected boards** — user provides IDs (comma-separated or range). Show confirmation with titles: "Delete these 3 dashboards? [Title A], [Title B], [Title C]". On confirm, delete each sequentially, showing progress: `Deleted 2/3...`

**Option B: Delete all top cleanup candidates** — list all boards flagged 🔴 Stale **and** structurally thin (Empty / Text-only / Sparse) — the safest bulk target. Require explicit confirmation with count: "Delete all 4 stale + thin dashboards?" Then execute.

**Option C: Delete all flagged-empty boards** — list all 🟡 Empty + 🟡 Text-only boards (regardless of age). Require explicit confirmation with count, then execute.

**Option D: Skip — just wanted the audit** — return control to router.

**Never auto-delete.** Every deletion path requires explicit user confirmation — no exceptions.

**Protect high-value stale boards.** Never propose deleting a 🔴 Stale board that still has 3+ reports without flagging that it may be a seasonal/quarterly board worth archiving rather than deleting.

**Verify deletions.** After running deletions, re-fetch the dashboard list (or attempt to read each deleted ID and expect a not-found) to confirm each target is actually gone before reporting the count. Report any ID that still resolves as a failed deletion rather than a success.

---

## Output

After deletions (if any):

```
✅ Cleanup complete
Deleted: [N] dashboards
Remaining: [M] dashboards
Freed: [list of deleted titles]
```

Return control to router.

---

## Error Handling

| Situation | Action |
|-----------|--------|
| Entity-search unavailable | Fall back to the plain dashboard list (no sort); recency still parsed if present |
| No timestamp field in response | Skip Stale/Aging classification, note in header, show structural flags only |
| Fetch returns empty | "No dashboards found in this project." Return. |
| Layout read fails for one board | Mark as "⚠️ Could not inspect", continue with others |
| A deletion fails | Note the failure, continue with remaining deletions |
| User cancels deletion | "No dashboards deleted." Return. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Command: Create Dashboard

Creates a new dashboard from a natural language description.

---

## Intake

The user provides either:
- **A description** — e.g. "create a dashboard tracking onboarding funnel metrics"
- **A structured spec** — title, description, specific reports to include

Extract:
1. **Title** (required) — infer from description if not stated
2. **Description** (optional) — a 1-2 line summary
3. **Report intent** (optional) — what charts/reports the user wants on the board
4. **Visibility** — private or shared (default: shared)
5. **Time filter** — if the user mentions a date range, set it; otherwise omit

---

## Execution

### Path A: Empty Dashboard (no reports specified or no query_ids available)

If the user just wants a board created without pre-built reports:

1. Create the dashboard with the session `project_id`, the extracted title and
description, the chosen visibility, and at minimum one row holding a single
text card, e.g. `<h1>Dashboard Title</h1><p>Description here. Add reports to populate.</p>`.
2. Return the dashboard ID and confirm.

### Path B: Dashboard with Reports

If the user wants specific charts, each report needs a `query_id` minted first.
The flow:

1. For each desired report, run its query with results skipped to mint a
`query_id`. Read the query schema first if unsure about available
events/properties. Fire queries in parallel where possible.
2. Assemble the rows, observing the layout limits and grouping rules in
`references/mcp-tool-reference.md`. Group related reports in the same row;
use text cards as section headers between logical groups.
3. Create the dashboard with the assembled rows.
4. Return the dashboard ID, title, and a summary of what was created.

### Path C: Dashboard from a description (AI-inferred layout)

When the user gives a loose description like "build me a product health dashboard":

1. Infer 4-8 logical report categories from the description.
2. Create the dashboard with text-card section headers for each category and a
placeholder description.
3. Tell the user: "Created [Title] with section scaffolding. Reports need to be
added manually or by minting queries — want me to populate any sections?"

---

## Time Filter

If the user specifies a date range, set a time filter. Common mappings:
- "last 7 days" → last 7 days
- "last month" → last 1 month
- "last quarter" → last 3 months
- Specific range → a "between" range with `from`/`to` dates (YYYY-MM-DD)

---

## Output

```
✅ Dashboard created
ID: [dashboard_id]
Title: [title]
Reports: [N reports] | Empty (scaffold only)
Access: Shared | Private
```

Then re-read the board to confirm the write landed (see the **Validate every
write** rule) before reporting `✅`. Return control to router.

---

## Error Handling

| Situation | Action |
|-----------|--------|
| A report's query fails to mint | Skip that report, note in output, continue with others |
| Dashboard creation fails | Surface full error to user |
| User description too vague | Create scaffold with text-card sections, explain next steps |
Loading