# Live Data Artifacts

> Build artifacts that read live data from the viewer's claude.ai connectors - capability declaration, watchTool versus callTool, cache and freshness UI, error-code handling and retry policy, the observe-a-real-response rule, and graceful degradation to a baked snapshot when the capability is absent. Use when a page must show current data rather than a build-time snapshot. Trigger on "live data", "live artifact", "connector", "real-time dashboard", "pull from BigQuery in the page", "auto-refresh", "mcp capability", "make it live".

- Skill: `lukehle/live-data-artifacts` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lukehle/live-data-artifacts`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lukehle/live-data-artifacts/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: Lukehle (https://skillmd.com/u/lukehle)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lukehle/live-data-artifacts

---


# Live data artifacts

A published page can call **the viewer's claude.ai connectors** and render current data. Powerful,
and narrower than it first appears — read the constraints before designing around it.

**Load the host's `artifact-capabilities` skill when it is available before writing a connector
call.** It carries the live capability roster and authoritative type definitions. If it is absent,
do not guess: build the baked snapshot path and state that live mode is unavailable.

---

## Decide whether you should

Four questions. A "no" on any of the first three means build a snapshot instead
(`artifact-architecture`, Tier 1).

1. **Is the source a connector on the organization's claude.ai account?**
   A locally-configured MCP server is *not* reachable from a published page. Local setup is
   irrelevant here — this is about what the org exposes.
2. **Does this page need to avoid being shared publicly?**
   A page declaring connector access **cannot be shared publicly**. Each viewer authenticates as
   themselves. For financial data that is the right posture; it is also a distribution constraint. A
   board pack that only opens for people with warehouse credentials is not a board pack.
3. **Does every intended viewer have access to the connector?**
   If half the audience gets `not_granted`, you have built a page that works for you and is empty
   for them.
4. **Would a changing number be a defect?**
   For anything reported, sent, or presented — yes. Liveness suits an operational monitor, not a
   deliverable. See the as-of discussion in `artifact-architecture`.

Good fits: cash position, collections status, pipeline, daily bookings, job/queue monitors.
Bad fits: board packs, investor updates, month-end reporting, anything with an as-of in its title.

---

## Getting the namespace

```js
const mcp = await claude.use('mcp');
if (!mcp) {
  renderSnapshot();          // ALWAYS have this path
  showBanner('Showing the built-in snapshot — live data is unavailable here.');
  return;
}
```

`claude.use()` resolves `null` when the capability cannot run in this view — not served, not granted,
or failed to load, **indistinguishable by design**. Branch on `null` and design for absence. Never
probe `window.claude` members to guess which case you are in.

Permission lives on the *calls*, not on `use()`. A consent prompt, rate limit, or policy refusal
arrives on the first call.

---

## Two arms: display versus act

**Displaying data → `watchTool`.** Replays cache immediately, refreshes when stale, polls only if you
ask. Returns a synchronous unsubscribe.

```js
const stop = mcp.watchTool(server, tool, input, (result, error) => {
  if (error) { showStale(error); return; }
  render(result.payload);
  showFreshness(result.cache?.storedAt);
}, { refetchInterval: 60_000 });
```

**Taking an action → `callTool`.** Fires once; read `result.payload`. Tool failures **reject** with a
`tool_error`, so wrap it.

The distinction matters: `watchTool` is idempotent-by-assumption and may re-run. Never put a
side-effecting tool behind it.

---

## The rule that prevents shipping broken pages

> **Observe one real request/response pair per tool, in this session, before publishing.**

The type definitions cover the call envelope. They do **not** tell you a connector tool's argument
names or how its results are encoded. Guessing produces a page that looks right and returns nothing.

If you cannot safely observe a pair — the connector is unauthenticated here, or calling it would have
side effects — **say so explicitly at publish time, in your reply to the user**, not as a note buried
in the page. Do not ship a guessed shape.

And when you do observe: **learn the shape, discard the values.** Observed data is the user's real
data. It never becomes sample or placeholder content in the published page.

---

## Error handling

Branch on the error code; do not treat all failures alike.

| Class | Behaviour |
|---|---|
| Retryable (transient, rate limit) | Retry with backoff, cap the attempts, show a retrying state |
| Authorization denied | **Drop the data.** Show an access-required state. Never fall back to stale data the viewer may no longer be entitled to see |
| Not granted / unavailable | Degrade to the snapshot, say so plainly |
| Tool error (bad args, upstream failure) | Not retryable. Surface it; retrying identical bad input just burns quota |

Retry **only** what is marked retryable. A retry loop on a permission error is indistinguishable from
an attack and will get the page throttled.

---

## Freshness UI

If a page claims to be live, it must be able to say how live. Drive this from the result's cache
timestamp:

```
● Live · updated 14:22:07        (under a minute)
◐ Updated 6 min ago              (stale but usable)
◌ Reconnecting…                  (retrying)
▲ Snapshot · as of 2026-08-19    (degraded — say it plainly)
```

Rules:
- **Never show a bare number with no freshness indicator.** A stale figure presented as current is
  worse than no figure.
- **Never silently swap live for cached.** State the degradation.
- Timestamps in the viewer's locale, with a timezone.

---

## Degradation is a first-class path, not an afterthought

Build the snapshot first, then layer live on top. This ordering is deliberate: it guarantees the
page works for every viewer, and it means "live unavailable" is a state you designed rather than a
blank screen you discover.

```js
render(SNAPSHOT);                       // always renders something real
const mcp = await claude.use('mcp');
if (mcp) upgradeToLive(mcp);            // enhancement, not the foundation
```

The page footer should always name which tier is actually in use.

---

## Manifest hygiene

Declare the minimum:

```js
capabilities: { mcp: { servers: [{ server: 'connector_name', tools: ['search', 'query'] }] } }
```

- `server` is the connector segment from the tool name (`mcp__<connector>__<tool>`), copied exactly,
  case included.
- `tools` takes the connector's **upstream** tool names, which can differ from the normalized segment
  when the upstream name contains a dot or a space.
- It is a viewer-consented grant. A large manifest asks for more trust and buys nothing.

On redeploy, **omitting `capabilities` carries the stored declaration forward**; `{}` clears it; a
non-empty object is a full-set declaration and revokes anything not restated.

---

## Related skills

- Host `artifact-capabilities` — conditional contract; use the baked snapshot when absent
- `artifact-architecture` — the tier decision this sits inside
- `stateful-artifacts` — when the page needs to *write*, not just read
- `app-interaction-patterns` — loading, stale, empty, and error states
- `artifact-testing` — verifying the degraded path actually works

