---
name: glance-publish-dashboards
description: >-
  Publish and update live, multi-widget dashboards to Glance over MCP. Use when
  the user wants to visualize data on a shareable live dashboard, surface
  metrics / charts / tables for their team, or says "show this on the dashboard".
homepage: https://glance.watch
mcp:
  name: glance
  transport: http
  url: https://glance.watch/api/mcp
  auth: Bearer <publisher API key from /dashboard>
---

# Glance — publish live dashboards over MCP

Glance is a surface your agent publishes generative dashboards to; the user's team
views them live in a browser. You send a **declarative layout** (a tree of approved
widgets) plus **data**; you never write UI code. Layouts are validated against a
catalog — unknown node types or raw HTML are rejected.

## 1. One-time setup — add the Glance MCP server

1. **Get a publisher API key.** You cannot mint one yourself — keys are issued
   from a signed-in browser session. Ask the user to sign in at
   `https://glance.watch/login`, open **/dashboard**, and click
   **Generate publisher API key** (shown once; looks like `pk_...`), then paste it
   to you. The key is scoped to their organization (tenant); every call you make
   is tenant-scoped.

2. **Register the server** (HTTP transport, Bearer auth):

   Claude Code:
   ```bash
   claude mcp add --transport http glance https://glance.watch/api/mcp \
     --header "Authorization: Bearer pk_..."
   ```

   `mcp.json` (Claude Desktop, Cursor, others):
   ```json
   {
     "mcpServers": {
       "glance": {
         "type": "http",
         "url": "https://glance.watch/api/mcp",
         "headers": { "Authorization": "Bearer pk_..." }
       }
     }
   }
   ```

3. **Verify**: list the server's tools (Claude Code: run `/mcp` and pick
   **glance**; other clients: open their MCP server list). You should see
   `publish_dashboard`, `update_data`, `patch_dashboard`, `remove_dashboard`,
   `list_dashboards`.

## 2. Publish a dashboard

Call **`publish_dashboard`** with `{ dashboardId, title?, layout }`.
`layout` is `{ title?, root }`; `root` is a node tree. Bind data widgets to a named
`source` string — you fill those with `update_data` (next step).

```jsonc
publish_dashboard {
  "dashboardId": "ops",
  "layout": {
    "title": "Revenue overview",
    "theme": { "mode": "dark", "accent": "#6366f1",
      "chartPalette": ["#6366f1", "#0891b2", "#db2777"] },
    "root": { "type": "grid", "columns": 3, "children": [
      { "type": "kpiCard", "title": "MRR", "source": "mrr", "unit": "USD" },
      { "type": "kpiCard", "title": "Customers", "source": "customers" },
      { "type": "lineChart", "title": "MRR trend", "source": "trend",
        "xKey": "month", "series": [{ "key": "mrr", "label": "MRR" }] }
    ] }
  }
}
```

## 3. Push live data (and keep it fresh)

Call **`update_data`** with `{ dashboardId, values }`. Each key is a widget
`source`; values merge in and widgets update live with no reload or re-layout.
Re-run on your own schedule (cron / scheduled task) to keep the dashboard current.

```jsonc
update_data {
  "dashboardId": "ops",
  "values": {
    "mrr": 48200,
    "customers": 312,
    "trend": [
      { "month": "Jan", "mrr": 41000 },
      { "month": "Feb", "mrr": 43500 },
      { "month": "Mar", "mrr": 44800 }
    ]
  }
}
```

**No MCP client on your schedule?** A plain HTTP `POST` to
`https://glance.watch/api/webhooks/update` with the same
`Authorization: Bearer pk_...` header and a `{ "dashboardId", "values" }` JSON
body does exactly what `update_data` does — handy for cron jobs and
webhook-style routines:

```bash
curl -X POST https://glance.watch/api/webhooks/update \
  -H "Authorization: Bearer pk_..." -H "content-type: application/json" \
  -d '{"dashboardId":"ops","values":{"mrr":51000}}'
```

## 4. Public vs private

Dashboards are **private** by default (only the org's signed-in members can view).
To share one publicly, pass `"visibility": "public"` to `publish_dashboard` or
`patch_dashboard`. The result then includes a `publicId`, and the dashboard becomes
viewable by anyone at `https://glance.watch/p/<publicId>` (no login).
Setting `"visibility": "private"` revokes that link. The org owner can also toggle
this from the dashboard page.

## 5. Theming (optional)

Theme is **layout-level** and **optional** — omit it for the default dark theme.
Add a `theme` object alongside `title`/`root`:

```jsonc
"theme": {
  "mode": "dark",                                  // "light" | "dark"
  "accent": "#6366f1",                             // hex
  "chartPalette": ["#6366f1", "#0891b2", "#db2777"] // hex[]
}
```

- **`mode`** — `light` or `dark` dashboard **surface**; the surrounding app chrome
  is unaffected.
- **`accent`** — a hex color that tints the dashboard title and the KPI card values.
- **`chartPalette`** — hex colors applied to chart series in order; a per-series
  `color` still overrides.

All colors are **hex-only** and validated (e.g. `"#6366f1"`); a non-hex value is
rejected by the trust boundary. See the themed `publish_dashboard` example in §2.

## 6. Widget catalog

Containers (hold `children`):
- `grid` — `columns` (1–12), `children[]`
- `stack` — `direction` (`row` | `column`), `children[]`
- `slides` — `source`, `children[]` (at least 1; unlike `grid`/`stack`, empty is
  rejected). Shows one child at a time; `source` is a **0-based active-slide index**
  (an integer, not a data shape) that you advance with `update_data`. Use a
  dedicated `source` key not shared with any widget binding. The index is clamped to
  range and defaults to slide 0 when unset or out of range.

Widgets:
- `text` — `value`, `variant?` (`h1|h2|h3|body|muted`)
- `kpiCard` — `title`, `source`, `unit?`, `delta?` (source → a single number/string)
- `lineChart` / `barChart` — `title?`, `source`, `xKey`, `series[]` of `{ key, label?, color? }`
  (source → an array of row objects; `xKey` is the category field, each `series.key` a value field)
- `dataTable` — `title?`, `source`, `columns[]` of `{ key, label? }` (source → array of rows)
- `image` — `url` (absolute `https://` only; other schemes are rejected at the trust
  boundary), `alt?` (short description; rendered as escaped text, never markup)

**Present mode:** every viewer page (`/dashboard/<id>` and public `/p/<publicId>`)
has a **Present** button that takes the live dashboard fullscreen — a display mode
of the same live stream, not a deck builder. Pair it with a `slides` deck to run a
walkthrough: advance the deck by pushing a new index with `update_data`. Viewers can
also browse a deck on their own with the on-screen arrows or the ← / → keys; manual
navigation detaches them from the presenter, and a **Live** control snaps them back
to the publisher's synced position. `data[source]` remains the synced default that
`update_data` drives.

Minimal `slides` deck:
```jsonc
publish_dashboard {
  "dashboardId": "review",
  "layout": {
    "title": "Weekly review",
    "root": { "type": "slides", "source": "activeSlide", "children": [
      { "type": "kpiCard", "title": "MRR", "source": "mrr", "unit": "USD" },
      { "type": "lineChart", "title": "MRR trend", "source": "trend",
        "xKey": "month", "series": [{ "key": "mrr", "label": "MRR" }] }
    ] }
  }
}
```
```jsonc
// advance to the second slide (0-based)
update_data { "dashboardId": "review", "values": { "activeSlide": 1 } }
```

**Any node** — container or widget — may also carry `note?`: a short annotation
(1–500 chars, plain text) that viewers reveal from a small ⓘ badge on hover or
keyboard focus. Charts keep their normal data tooltips; the note is a separate
affordance for context the data can't carry:

```jsonc
{ "type": "kpiCard", "title": "MRR", "source": "mrr",
  "note": "Monthly recurring revenue, net of refunds" }
```

## 7. Designing a good dashboard

A valid layout is not automatically a readable one. Defaults that work:

- **Compose top-down by altitude**: a `grid` of 3–4 `kpiCard`s first (the
  headlines), charts in the middle (the trends behind them), a `dataTable` last
  (the detail). One screen; if it doesn't fit, make a second dashboard instead of
  scrolling.
- **Pick the form by the data's job**: a single current number → `kpiCard` (with
  `unit`, and `delta` when you have a comparison); change over time → `lineChart`;
  comparison across categories → `barChart`; lookup/detail → `dataTable`. Never
  render one number as a chart.
- **Keep charts sparse**: 2–4 `series` per chart, and always give each a human
  `label` — it becomes the legend. More than 4 series → split into two charts.
- **Let the theme color the series.** Set one `chartPalette` (3–4 distinguishable
  hex colors) and let series pick it up in order; per-series `color` is for when a
  color *means* something (brand, status), not decoration.
- **Trim time series** to a readable window (roughly the last 12–30 points) —
  `update_data` replaces the array, so send the window you want seen.
- **Use `text` sparingly**: `h2`/`h3` variants to head a section of the grid,
  `muted` for captions such as "updated hourly". The layout `title` is the page
  heading — don't repeat it as a text node.
- **Put caveats in `note`, not in extra widgets**: methodology, data-freshness,
  and "why does this look odd" context belongs in a node's `note` (revealed on
  hover), keeping the visible layout clean. A `note` is a sentence or two — not
  a paragraph.

## Rules & tips

- The layout is **data, not code** — no HTML/JS, no components outside the catalog.
- Every data widget's `source` must have a matching entry in `update_data` `values`.
- `patch_dashboard` replaces the layout but **keeps** live data; `remove_dashboard`
  deletes it; `list_dashboards` shows what exists for the tenant.
- If a tool returns an error (e.g. invalid layout, unknown dashboard), read the
  message and correct the call — errors are designed to be self-correcting.
- Prefer one `publish_dashboard` to define structure, then frequent `update_data`
  calls for live values.
