Skip to main content
Glama
DACdigital

Sales Dashboard

by DACdigital
README.md
# Interactive Sales Dashboard — an MCP App for Claude

[![License: MIT](https://img.shields.io/badge/License-MIT-informational.svg)](LICENSE)
![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)

A polished, **clickable** sales-analytics dashboard that renders **inline inside
Claude** (Cowork and regular chat) via the MCP Apps extension (SEP-1865, the
`ui://` resource pattern), served by a small Python server. Pick a date range,
click a region, or pick a product and the charts and table re-query live data and
re-render — a real app, not a screenshot.

![dashboard](docs/dashboard.png)

- **Server:** [FastMCP](https://gofastmcp.com) (native MCP Apps), streamable-HTTP.
- **Data:** stdlib `sqlite3`, seeded deterministically (~4,500 orders / 24 months).
- **UI:** one self-contained HTML doc — inline CSS/JS, **inline-SVG charts** (no CDN).
- **Reach:** a built-in Cloudflare tunnel exposes your laptop over HTTPS so Claude can reach it.

---

## Quickstart (≈60 seconds)

You need [**uv**](https://docs.astral.sh/uv/) and
[**cloudflared**](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)
(`brew install cloudflared` on macOS). Then:

```bash
uv sync                      # create the venv + install (pinned)
uv run mcp-apps seed         # build db/sales.db (deterministic; safe to re-run)
uv run mcp-apps serve        # start server + open a public HTTPS tunnel
```

`serve` prints a banner — copy the URL:

```
==================================================================
  CONNECTOR URL — paste into Claude → Add custom connector:
      https://<random-words>.trycloudflare.com/mcp
==================================================================
```

Then in Claude: **Settings → Connectors → Add custom connector**, paste that URL
(including the trailing `/mcp`), enable it, and ask:

> **"Open the sales dashboard."**

Leave `serve` running; press **Ctrl-C** to stop the server and tunnel together.

> `serve` always opens a tunnel. If `cloudflared` isn't installed it stops
> immediately with an install hint — Claude runs in the cloud and can't reach
> `localhost`, so a public HTTPS URL is required.

> The `*.trycloudflare.com` URL is **new every run**. For a stable URL on a domain
> you own, see [Stable URL](#stable-url-named-tunnel).

---

## Add it to Claude

1. **Settings → Connectors → Add custom connector.**
2. Name it (e.g. *Sales Dashboard*) and paste the HTTPS URL from `serve`,
   **including `/mcp`**. Save.
3. Enable it for the surface you'll use (Cowork and/or chat), then ask Claude to
   open the dashboard.

> [!IMPORTANT]
> **Reconnect after every server restart.** The server keeps an in-memory
> session; restarting `serve` drops it and Claude shows *"Session not found."*
> Toggle the connector off/on (or remove/re-add it) after each restart. This also
> makes Claude pick up UI changes.

---

## Use it

- "Open the sales dashboard." · "Open the sales dashboard for APAC." (pre-filtered)
- Click a **region bar** → the whole dashboard filters to it.
- **3M / 6M / 12M / All** presets or the date pickers → time range updates.
- Click a **top product** → the orders table filters via a live `list_orders` call.
- **Search** / click column headers to filter and sort; click a chip **×** to clear.

Each click is a live MCP tool call. Open the host devtools console to watch
`[mcp-app] ⇒ host` traffic with a matching `POST /mcp` in the server terminal.

---

## How it works

```
Claude (host)  ──MCP over HTTPS──▶  Cloudflare tunnel  ──▶  FastMCP server  ──▶  SQLite
     │  renders ui:// resource                                    │
     └── sandboxed iframe ◀── postMessage (JSON-RPC) ─────────────┘
```

- **Data tools** (`get_sales_summary`, `sales_by_month`, `sales_by_region`,
  `top_products`, `sales_by_rep`, `list_orders`) run SQL and return typed JSON.
  `get_dashboard_data` bundles everything so a filter change is one round-trip.
- **The app** is one tool, `open_sales_dashboard`, linked to the UI resource
  `ui://sales/dashboard-<hash>` via `_meta.ui.resourceUri`. The server inlines
  HTML/CSS/JS into one document and embeds default data so it paints instantly.
- **Interactivity** flows over the SEP-1865 `postMessage` bridge: the iframe does
  the `ui/initialize` handshake, reports its size, and calls the data tools on
  each filter change.

Built against **MCP Apps spec `2026-01-26`**, verified on FastMCP 3.4.2.

---

## Customize / re-skin

Everything tweakable lives in the **CONFIG block** at the top of
[`src/sales_dashboard/config.py`](src/sales_dashboard/config.py): theme colours,
date range & volume, regions, product catalogue & price bands, currency, server
host/port. After changing data settings, re-run `uv run mcp-apps seed`, then
restart `serve` and reconnect the connector.

---

## Develop & test

```bash
uv run pytest                       # full suite (unit + in-memory integration)
uv run ruff check .                 # lint
uv run mypy src                     # type-check
# coverage gate (the suite keeps ≥90%):
uv run pytest --cov=src/sales_dashboard --cov-report=term-missing --cov-fail-under=90
```

Tests need **no running server** — integration tests drive the server in-memory
via `fastmcp.Client`. For a bare local server (e.g. to poke at it with the MCP
Inspector), run `uv run server.py` and point
`npx @modelcontextprotocol/inspector` at `http://localhost:8000/mcp` (Streamable
HTTP). Editing `src/sales_dashboard/ui/*` changes the content hash in the `ui://`
URI automatically — restart `serve` and reconnect so Claude fetches the new URI.

### Stable URL (named tunnel)

One-time (opens a browser to log in):

```bash
cloudflared tunnel login
cloudflared tunnel create mcp-demo
cloudflared tunnel route dns mcp-demo mcp-demo.example.com
```

Then: `uv run mcp-apps serve --named mcp-demo.example.com` → stable connector URL
`https://mcp-demo.example.com/mcp`.

---

## Troubleshooting

| Symptom | Cause & fix |
|---------|-------------|
| **`serve` exits: "cloudflared was not found"** | Install cloudflared (see Quickstart). The tunnel is required. |
| **"Session not found" (-32600)** | Server was restarted; Claude holds a stale session. **Reconnect the connector.** |
| **App card "Unable to reach …", no iframe** | UI-resource validation. `_meta.ui` is an object and the mimeType is `text/html;profile=mcp-app` (correct here). Re-add the connector; check `curl https://…/mcp` returns MCP JSON. |
| **Iframe mounts but spins forever** | The view must send `ui/notifications/size-changed`; this build does. |
| **Renders but not interactive** | An always-on overlay can eat clicks. This build hides overlays via `[hidden]{display:none!important}`. |
| **UI edits don't show up** | Claude cached the old resource. The URI auto-busts on change — **restart + reconnect**. |

> **Note on exposure:** `serve` publishes your local server on a public HTTPS URL
> with **no authentication** — anyone with the URL can call the tools. The data is
> synthetic and the quick-tunnel URL is ephemeral, but don't point this at real
> data without adding auth.

---

## Project layout

```
server.py                    bare local server (dev / MCP Inspector)
seed.py                      builds the SQLite DB
src/sales_dashboard/
  config.py                  ⭐ CONFIG block — theme / date range / volume / regions
  db.py                      sqlite helpers (injectable path)
  queries.py                 one SQL function per tool
  seed.py                    deterministic data generator
  server.py                  FastMCP: tools + ui:// resource; assembles the HTML
  cli.py                     `mcp-apps` CLI: seed + serve (tunnel)
  ui/                        dashboard.html / .css / bridge.js / charts.js / app.js
tests/
  unit/                      config, db, seed, queries, validation, ui-assembly, cli
  integration/               tools + app resource (in-memory FastMCP client)
docs/                        design specs, plans, screenshot
```

---

## Pinned versions

| Package | Version | | Package | Version |
|---------|---------|-|---------|---------|
| Python | 3.11+ | | pydantic | 2.13.4 |
| fastmcp | 3.4.2 | | starlette | 1.3.1 |
| mcp | 1.27.2 | | uvicorn | 0.49.0 |

MCP Apps is young and moving fast; this targets spec `2026-01-26`. The `bridge.js`
postMessage layer is isolated in one file so it can be adapted if a host's
behaviour changes.

---

*Built by [dac.digital](https://dac.digital) · MIT licensed.*