Connect any publishing agent
Glance is an HTTP Streamable MCP server. Your agent publishes dashboards over MCP; your team watches them update live. The endpoint requires a Bearer publisher key, scoped to your organization (tenant).
Universal publisher connection
These wire-level values are the same for every compatible client. Only the command or config shape changes.
- Transport
- Streamable HTTP
- Endpoint
- https://glance.watch/api/mcp
- Request header
- Authorization: Bearer pk_...
https://glance.watch/api/mcp acts as tenant-scoped automation and uses a publisher key. The OAuth viewer and management server at https://glance.watch/api/viewer/mcp acts as the signed-in user. Do not combine the publisher key with the OAuth connection.Need notifications when a value crosses a limit or a source stops updating? Set up deterministic alerts.
Get a key
Sign in at /login — this creates your organization. Open /dashboard and click “Generate publisher API key”. The key (pk_...) is shown exactly once — only its hash is stored server-side, so copy it immediately. It is scoped to your organization; every MCP tool call and webhook POST authenticated with it is tenant-scoped end to end.
Configure your MCP client
The connection contract above is universal, but there is no portable config snippet. Clients spell the remote transport differently — type: "http", httpUrl, streamableHttp, serverUrl, or no key at all — and some do not read a config file for this at all. Use the entry for the client you actually run.
claude mcp add --transport http glance https://glance.watch/api/mcp \
--header "Authorization: Bearer pk_..."{
"mcpServers": {
"glance": {
"type": "http",
"url": "https://glance.watch/api/mcp",
"headers": {
"Authorization": "Bearer pk_..."
}
}
}
}type is mandatory in that file: an entry with a url and no type is read as a stdio command and fails. Claude Code also expands ${VAR} in url and headers, so the key can stay in your environment.
copilot mcp add --transport http --header "Authorization: Bearer pk_..." glance https://glance.watch/api/mcp{
"mcpServers": {
"glance": {
"url": "https://glance.watch/api/mcp",
"headers": {
"Authorization": "Bearer ${env:GLANCE_PUBLISHER_KEY}"
}
}
}
}{
"servers": {
"glance": {
"type": "http",
"url": "https://glance.watch/api/mcp",
"headers": {
"Authorization": "Bearer ${input:glance-publisher-key}"
}
}
}
}VS Code reads this workspace file and prompts for the publisher key through its password input variable. That keeps the key out of source control.
gemini mcp add --transport http --header "Authorization: Bearer pk_..." \
glance https://glance.watch/api/mcp
# In settings.json the URL key is httpUrl — Gemini reads "url" as the SSE endpoint.export GLANCE_PUBLISHER_KEY="pk_..."
codex mcp add glance --url https://glance.watch/api/mcp --bearer-token-env-var GLANCE_PUBLISHER_KEY[mcp_servers.glance]
url = "https://glance.watch/api/mcp"
# Store the key WITHOUT the "Bearer " prefix; Codex adds it.
bearer_token_env_var = "GLANCE_PUBLISHER_KEY"
# Or literally: http_headers = { "Authorization" = "Bearer pk_..." }{
"type": "mcp",
"server_label": "glance",
"server_url": "https://glance.watch/api/mcp",
"headers": {
"Authorization": "Bearer pk_..."
}
}Other clients that take a static header
| Client | The difference that decides whether it works |
|---|---|
| Cline | "type": "streamableHttp" is mandatory — omit it and Cline silently falls back to SSE |
| Continue.dev | Type streamable-http; the header goes under requestOptions.headers |
| Goose | YAML; type: streamable_http, and the URL key is uri |
| opencode | The block key is mcp, and the type is "remote" |
| Zed | The block is context_servers; supplying a header suppresses Zed's OAuth flow |
| Windsurf / Devin Desktop | The URL key is serverUrl, not url |
| Warp | ~/.warp/.mcp.json |
| Amp | amp.mcpServers; a workspace-level server also needs `amp mcp approve` |
| LM Studio (0.3.17+) | ~/.lmstudio/mcp.json |
| JetBrains Junie | .junie/mcp/mcp.json |
Hosted Claude connectors in Claude Desktop, claude.ai, and Cowork accept OAuth or no-auth remote servers, not a static publisher header. Connect the OAuth viewer server there instead. It signs in as the user and needs no publisher key.
ChatGPT web likewise connects to the OAuth viewer server; its connector UI does not accept an arbitrary static publisher header. The local ChatGPT desktop app shares Codex configuration, so use the Codex recipe above for publisher automation.
These snippets come from each client's own documentation; we have not connected every one of them to this endpoint ourselves. If a client rejects one, its error almost always names the field it wanted.
curl -X POST https://glance.watch/api/webhooks/update \
-H "Authorization: Bearer pk_..." -H "content-type: application/json" \
-d '{"dashboardId":"ops","values":{"mrr":51000}}'Tools
| Tool | Input | Purpose |
|---|---|---|
| get_dashboard_recipe | { kind: "executive_summary" | "operations_wallboard" | "investigation_workbench" } | Return a complete, clean dashboard layout with matching sample data and an adaptation workflow. |
| get_creative_direction | { intent, visualStyle?, dataVisualizationStyle?, customGuidance? } | Suggest optional art direction for a page or dashboard. Presets and custom directions are equally valid and advisory. |
| get_page_recipe | { kind: "proof_led_saas" | "technical_product" | "bold_launch" | "editorial_story" | "live_data_story" } | Return a complete typed page composition, sample data, required proof, and a rendered-review workflow. |
| publish_dashboard | { dashboardId?, title?, layout, visibility?, slug? } | Create or replace a dashboard layout and return advisory structural quality feedback. Omit dashboardId to create a new dashboard; provide it to replace an existing one. Pass slug (with layout.presentation: "page") to serve it as a page on your website. |
| patch_dashboard | { dashboardId, title?, layout, visibility?, slug? } | Replace an existing dashboard's layout/title, preserve its live data, and return advisory structural quality feedback. Same as publish_dashboard, but dashboardId is required. Pass slug to move the page's URL, or slug: null to take it off the site; omit it and the URL is unchanged. |
| get_dashboard | { dashboardId } | Read a dashboard back in full — layout, data, visibility, version, slug. Call it before patch_dashboard when editing something that already exists, and modify what it returns. |
| update_data | { dashboardId, values } | Merge new data-binding values into a dashboard. Shallow-merges values into data — no re-layout. |
| remove_dashboard | { dashboardId } | Delete a dashboard. Errors if it does not exist. |
| list_dashboards | {} | List all dashboards for the calling tenant. |
| list_sites | {} | Discover active sites this publisher key may target, including the default site. Site creation and management remain owner/admin OAuth operations. |
| check_layout | { layout } | Validate a layout before publishing it. Returns structured, self-correcting issues without changing a dashboard. |
| get_page_analytics | { siteId?, dashboardId?, days?, endingAt?, limit? } | Read built-in page-view analytics and daily visitor counts when the organization plan includes analytics. |
| get_visitor_geography | { siteId?, dashboardId?, days?, endingAt?, limit? } | Read privacy-safe country aggregates for visitorMap: countryCode, views, and visitorDays. No IP, city, coordinate, or individual event is returned. |
| get_funnel_report | { siteId?, from, to, breakdownLimit? } | Read observed aggregate landing, trusted-CTA, and native-lead stages for a bounded half-open UTC window. |
| create_campaign | { siteId?, name } | Create a site campaign and receive its opaque public glc code. |
| list_campaigns | { siteId? } | List campaigns for a site. Omit siteId to use the default site. |
| rename_campaign | { siteId?, campaignId, name } | Rename a campaign without changing its opaque public code. |
| set_campaign_status | { siteId?, campaignId, status: "active" | "paused" } | Pause or resume attribution for a campaign. |
| get_campaign_report | { siteId?, from, to } | Read aggregate trusted-CTA clicks by action and campaign for a bounded UTC window. |
| get_lead_summary | { siteId?, formId?, campaignId?, submittedFrom?, submittedTo? } | Read aggregate native lead counts by form. Publisher keys never receive field values, consent evidence, or delivery addresses. |
Build from a recipe, then check quality
For a new dashboard, call get_dashboard_recipe and choose executive_summary, operations_wallboard, or investigation_workbench. Each recipe returns a complete layout, matching sample data, and an adaptation workflow—not a fragment that leaves the agent to invent the hierarchy.
- Keep the layout title once and group distinct questions into sections or tabs.
- Use one accent KPI in the summary group, keep its siblings plain, and use four columns or fewer.
- Run
check_layout, then publish the corrected layout and matching data.
Every successful publish or patch includes quality.status: ready or revise. A revise result never blocks or rolls back the write; it identifies structural issues the agent should correct in a follow-up patch. It is not a subjective beauty score and does not validate the truth of the content.
Art-direct, preflight, and review the real render
Read glance://creative-directions and glance://page-recipes, or call get_creative_direction and get_page_recipe. These are optional starting points, not an allowlist: a custom direction is first-class. Pass the chosen or custom creativeDirection to check_layout for direction-aware advisories.
For a site page, saved site brand defaults are materialized into the layout during a publish or patch; explicit page values win. Changing the saved brand does not repaint existing stored or live pages. Give an existing page a reviewed republish or patch to adopt new defaults, preserving version history and preventing surprise public changes.
- Keep a new draft private before making it public.
- Open its authenticated
/dashboard/{id}/previewworkspace and inspect the actual trusted renderer at desktop 1440 and mobile 390 widths. - Check hierarchy, contrast, CTA, proof, overflow, and data-story clarity; write one critique and make one revision when it finds a material issue, then preflight again.
The review workspace is a browser-rendered, authenticated view of the stored draft. It does not expose a private page publicly or imply an automated image-review service.
Public vs. private
Dashboards are private by default — viewable only by signed-in members of the owning organization. Passing visibility: "public" to publish_dashboard (or toggling “Make public” on the dashboard page) mints an unguessable share token and makes the dashboard viewable with no login at /p/<token>. Setting it back to private immediately revokes that link.
Sites — pages on your own domain
A page is a dashboard with two extra things: a slug and layout.presentation: "page" (website framing — full-bleed sections, no dashboard chrome). A site is a first-class collection of pages, addresses, and analytics settings. Every organization starts with an immutable default site for compatibility. Business supports 5 active sites and Scale supports 25; Free and Pro support 1. Publish into another site by passing its siteId. Omitting it continues to target the default site.
https://<domain>/serves the slughome— the site root, not/home.https://<domain>/<slug>serves that page; a page slugged404becomes the site's custom not-found page.- Every page also keeps its
/p/<token>share link, which works whatever the slug does. Only public pages are served on the site. - Slugs are lowercase letters and digits joined by single hyphens, 3–64 characters, one path segment, unique within its site.
slug: nulltakes a dashboard off the site and frees the word.
Links inside a layout come in exactly three kinds: { kind: "external", href } (absolute https:// only), { kind: "internal", to: "<slug>" } — a reference to one of your own slugs, resolved into a URL at view time — and { kind: "tel", number }, a phone number written the way a visitor reads it, which we turn into a dialable tel: link. Never hardcode a URL to your own site, and never put a tel: string in an external href — that field takes https only and rejects the whole layout. An internal link to a page that isn't published yet renders inert and lights up on its own once you publish it, so publishing home first and linking forward is the normal workflow — never a publish error. A phone number resolves against nothing, so a tel link is never inert.
Site pages are cached, so a publish is not instant: a change you publish — a new page, an edit to an existing one, a forward link lighting up — takes at least about a minute to reach visitors on your site address. That is a floor, not a deadline: the stored copy is handed out one more time in each region while the new one renders, so the old page even then is normal rather than a failed publish. The dashboard surface and /p/<token> share links are not cached this way — they render fresh on every request, and update_data streams live to anyone already watching.
Three plan facts worth knowing up front: Free and Pro include one site, Business includes five, and Scale includes 25. Each site has its own platform handle, custom domains, page collection, and analytics configuration; omitting a site id targets the immutable default site. pages count against your dashboard limit (a page is a dashboard — there is no separate page allowance, and re-publishing an existing one is never blocked), and custom domains are a Pro-and-above feature added by an organization admin under Sites in the signed-in dashboard. Changing the slug of an already-public page is an admin action, like changing visibility; a publisher API key is the whole organization and is never gated that way.
Your own analytics — Plausible, GA4 or the Meta Pixel — can be pointed at these pages by an organization owner/admin in the dashboard or through an OAuth viewer connection with the explicit analytics:manage scope. A publisher key cannot administer analytics. See Analytics on your site for the identifiers we accept, the origins the scripts run on, and the consent banner.
Agent guide, with the design rules and the error table: https://glance.watch/site.md
Measurement and campaigns
Two different things measure a page, and they see different amounts. Ours is built-in page analytics: your agent calls get_page_analytics on the same publisher key it publishes with, and gets views, unique visitors, top pages and top referrer origins over a window, each already compared with the equal-length window before it. It counts views on your site addresses only — your custom domain and your free site handle — so a /p/<token> share link is never counted. It requires Business and above, and free public pages are additionally served noindex.
For the native landing → trusted CTA → lead view, call get_funnel_report. It reports events and visitor-days for each stage plus bounded breakdowns by page, action, form, campaign, and closed attribution source over the half-open [from,to) UTC window. The stages are observed independently, not joined into one person-level cohort, so they may be non-monotonic and must not be presented as a causal conversion rate. It returns no lead PII, visitor hash, UTM value, click ID, or other raw attribution.
The other is an analytics product you already own — Plausible (identified by a site domain), Google Analytics 4 (a G-… measurement ID) or the Meta Pixel (a 15–16 digit pixel ID). An owner or admin pastes the identifier under Site analytics on /dashboard, on Business and above. Setup, the origins those scripts run on and the consent banner are covered in the analytics guide.
First-party campaign attribution is part of built-in analytics on Business and above. Create a site campaign, append its opaque ?glc=… code to the page link you distribute, and give each trusted CTA link a stable actionId. Glance removes glc from the visible address immediately and carries only campaign, tenant and site IDs in a signed same-site token for 30 minutes. Reports contain aggregate clicks and visitor-days by action and campaign. Glance never stores UTM parameters, CTA destination URLs, form data, arbitrary event metadata, raw IP addresses or user agents.
In the EU and the UK, GA4 and the Meta Pixel only load once a visitor answers the consent banner; Plausible is cookieless and loads for everyone. Expect the consent-gated products to report lower numbers than Plausible and than our built-in analytics for European traffic. That difference is the banner doing its job, not a discrepancy to reconcile.
Native lead capture uses the site-only leadForm node on Business and above. Every valid submission is saved to the private inbox without an email destination; email delivery is optional and an owner or admin can configure it for future notifications. Definitions are versioned, submissions are encrypted and retained for 30 days, and publisher keys can read aggregate get_lead_summary results but never recipient addresses, submitted field values, raw UTMs, or click IDs. Its aggregate includes counts by attribution source and accepts only the closed source filter. The signed-in Lead inbox and OAuth viewer tools expose exact allowlisted attribution—including UTM fields and Google, Meta, Microsoft, and TikTok click IDs—only to an owner or admin with fresh leads:read consent. They keep that private data separate from dashboard data and publisher prompts. Form IDs are a site-wide namespace: use distinct formId values on different pages unless they share the same definition and version, with one shared optional delivery setting.
A successful native lead emits generic GA4 generate_lead and Meta Lead browser events only when that provider is configured and its runtime is loaded after any required consent. The calls contain no form or action identifier, PII, attribution, or other parameters; each provider can correlate the event only through its own browser session. Separately, a Business/Scale owner or admin can authorize Google Data Manager or Meta Conversions API through Vercel Connect and create outbound goals for submitted, qualified, or won leads. The click identifier is encrypted at rest and decrypted only for the outbound TLS request. Glance sends it with a stable event id, configured action, and optional value—never form fields or email—and imports no provider spend or campaign data. Tokens stay in Vercel Connect. Goal creation requires explicit human confirmation that every affected native form's consent and linked tenant privacy notice cover provider feedback; the analytics banner does not supply that confirmation. Publisher-key MCP remains aggregate-only. Only an eligible captured Meta touch plus an active deliverable Meta lead_submitted goal suppresses the duplicate browser Meta Lead. Organic or expired touches keep the browser event, and GA4 is unchanged.
Agents manage experiments through the signed-in OAuth MCP. Stage candidate content with stage_experiment_candidate so it is never briefly live, then create 2–5 weighted immutable variants totaling 10,000. Reconnect with all four required scopes:dashboards:read, dashboards:write, dashboards:manage, and experiments:manage. Reports are observed/descriptive only and never choose a winner; promotion always requires an explicit variant id. Public assignment is sticky, cache-safe, and frozen: refreshing keeps a visitor on the same variant, and experiment pages do not open a live SSE stream. Experiments run only at the canonical site URL; the generic /p/ share viewer does not participate. Owners and admins inspect each immutable variant from the signed-in experiment page's Preview links without changing assignment or recording exposure.
A third-party form is still an alternative. Link to its absolute HTTPS URL and give the CTA an actionId. In that pattern Glance measures the click-through to the form, not the submission; drop-off and completed-submission totals come from the form provider.
Layout (A2UI) reference
A layout is a JSON tree of containers wrapping widgets. It's declarative data — never executable code — validated against a fixed catalog before it's stored.
Any node — container or widget — may also carry note? (1–500 chars): a short plain-text annotation viewers reveal from a small ⓘ badge on hover or keyboard focus. Charts keep their normal data tooltips.
Every widget with a source reads live values from data[source] — push updates with update_data.
A layout may also carry a theme beside its title and root: mode? ("light" | "dark"), accent? and chartPalette? (hex only — a non-hex value is rejected at the trust boundary), and fonts? ({ display?, body? }). fonts picks the two typographic ROLES — display carries the labels, numbers and headlines (hero, section titles, KPI values, table headers), body carries the running prose — and each is one name from a closed allowlist of families we host: "geist" (the default), "outfit", "barlow", "space-grotesk", "manrope". It is never a URL and never a font stack: we load these families ourselves, so a layout can never add a third-party request to a viewer's browser. Pair a display face with a DIFFERENT body face for editorial contrast — the same family in both roles is the page you already have — and note that code blocks keep the monospace face whatever you choose.
// Start with: get_dashboard_recipe { "kind": "executive_summary" }
// Then adapt the recipe and publish it.
{ "dashboardId": "ops", "layout": { "title": "Ops",
"root": { "type": "stack", "direction": "column", "children": [
{ "type": "grid", "columns": 3, "children": [
{ "type": "kpiCard", "title": "MRR", "source": "mrr", "unit": "USD",
"emphasis": "accent" },
{ "type": "kpiCard", "title": "Customers", "source": "customers",
"emphasis": "plain" },
{ "type": "kpiCard", "title": "Retention", "source": "retention", "unit": "%",
"emphasis": "plain" }
] },
{ "type": "section", "title": "Trend", "children": [
{ "type": "lineChart", "source": "trend", "xKey": "month",
"series": [{ "key": "mrr", "label": "MRR" }] }
] }
] } } }
// then update_data
{ "dashboardId": "ops", "values": { "mrr": 48200, "customers": 118,
"retention": 108,
"trend": [{ "month": "Jan", "mrr": 41000 }, { "month": "Feb", "mrr": 43500 }] } }Viewer interactivity
Chart legends always toggle series. A dataTable opts into sorting, search, per-row detail and paging; a select node narrows every chart and table bound to the same source (KPI cards are publisher-computed aggregates, so they are never filtered). All of it is client-side view state: it never reaches the publishing agent and never changes data.
A calendar is that same filter in month form: clicking a day narrows the widgets on its source (ANDed with any selects), matching rows on the date part of dateKey, so 2026-07-29 and 2026-07-29T10:00:00Z both belong to the 29th. The grid keeps showing its own events either way, and rows whose dateKey doesn't read as a date are counted in a muted notice under it rather than silently dropped.
That state survives update_data, so a routine refresh leaves every viewer exactly where they were. publish_dashboard / patch_dashboard clears filter selections; a widget's own state — sort, page, search, open rows, hidden series, active tab, collapse — resets only where that widget's shape changed (its source, columns, series keys or tab labels). Publish the structure once, then push values.
One-step onboarding
Point any skill-aware agent at https://glance.watch/skill.md and it self-installs: gets a key, registers the MCP server, and learns the full tool/node/theme contract.
Ready to publish your first dashboard?
Sign in to get a key