Semaphor
MCP

Live Changes in Embedded Dashboards

Let your own agent change the dashboard your user is looking at, unsaved, with Undo, using one token for the iframe and the MCP.

Your product already shows a Semaphor dashboard in an iframe, and it has its own agent. With live changes, your user asks your agent for a change ("add revenue by region as a bar chart", "make that weekly", "rename the Region filter to Area") and it appears in the embedded dashboard a moment later, without a refresh.

The change lands unsaved. Your user sees it, can undo it, can keep editing by hand, and keeps everything with Save or discards it with Cancel, as with any edit they make themselves. Your agent reads what is on screen, unsaved cards and filters included, and is told what happened to each change. You add no front-end code: your agent keeps calling the Semaphor MCP from your server, and your page keeps showing the iframe.

Your backendmints one live tokenper user sessionYour pageshows the iframewith that tokenYour agentcalls the MCPwith the same tokenThe dashboardshows the change,unsaved, with Undo

Set It Up

Mint one live token per user session

Call POST https://semaphor.cloud/api/v1/token from your server with the mcp option and live: true. The same token opens the embedded dashboard and calls the MCP.

A live token for one end user
curl -X POST https://semaphor.cloud/api/v1/token \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "p_...",
    "projectSecret": "<project-secret>",
    "tenantId": "tenant_123",
    "endUserId": "maria",
    "endUserEmail": "maria@acme.com",
    "targetDashboardId": "d_8f2k",
    "parentOrigin": "https://app.acme.com",
    "mcp": { "live": true },
    "tokenExpiry": 28800
  }'
FieldWhat it does
mcp: { "live": true }Makes the token live: the iframe it opens becomes a live screen, and the MCP changes that screen instead of saving.
mcp.scopesRead scope (mcp:read), the default and the only scope a live token takes. Your agent changes the dashboard on screen, and your user's Save keeps the change; the token itself never saves. If your agent should save directly, use a separate write token kept on your server; live with mcp:write is refused with mcp_live_write_not_allowed.
mcp.sqlOn by default. false turns off the MCP's SQL tools and the SQL form of a card in changes. The dashboard's own cards, SQL cards included, still run as in any embed.
Identity fieldsorgUserId, or endUserId with tenantId, as for any MCP token. Changes reach the screen only when that user can edit the dashboard.
targetDashboardIdThe dashboard the iframe opens.
parentOriginYour page's exact origin, if you want the optional host events below.
tokenExpirySeconds, 60 to 86,400 (24 hours). Mint it for a working session: an open embed's token isn't refreshed.

Keep the project secret on your server

Mint tokens on your backend. Pass the accessToken to your page for the iframe and to your agent for the MCP; never send the project secret to a browser.

Show the iframe with the toolbar

Put the token in the iframe and show the toolbar, so your user has Save and Cancel:

<iframe src="https://semaphor.cloud/embed/<accessToken>?showControls=true" width="100%" height="100%"></iframe>

The iframe connects for live changes on its own while it shows a dashboard your user may edit, with the toolbar shown. Nothing else is needed on your page.

The connection is a long-lived streaming response, so a proxy or firewall that holds responses back until they finish will stop it; if your users are on a corporate network, check that it passes streaming responses through. On a self-hosted deployment with more than one API container, point them all at the same Redis, so your agent's call reaches the tab whichever container it lands on.

Give the same token to your agent

Your agent calls https://semaphor.cloud/api/mcp with Authorization: Bearer <accessToken>, as in Connect with a Token. With a live token, the dashboard tools read the screen and semaphor_propose_change sends its change there.

What Your Agent Does

A turn is two calls:

  1. Read the screen. semaphor_get_dashboard_details with fields: ["outline"] describes the dashboard as it is on screen right now, unsaved edits included. The cards, bindings, SQL and template fields, and semaphor_get_dashboard_analysis_context, read the screen the same way. A call that reads contents names one dashboard with dashboardId.
  2. Propose the change. semaphor_propose_change builds the change from what is on screen, sends it there, and waits a few seconds for the result. With a live token it changes the open dashboard in place: it takes no saveAs, preview or domainId, and it never saves.
Propose: add a bar chart to the dashboard on screen
{
  "dashboardId": "d_8f2k",
  "changes": [{
    "op": "card.add",
    "type": "bar",
    "title": "Revenue by region",
    "query": {
      "domainId": "dom_sales",
      "datasetName": "orders",
      "measures": ["revenue"],
      "dimensions": ["region"]
    }
  }]
}

The result carries delivery, which says what became of the change:

The change landed
{
  "proposalId": "chg_...",
  "summary": ["Adds the bar chart \"Revenue by region\"."],
  "delivery": { "deliveryId": "8a1f...", "outcome": { "status": "applied" } },
  "next": "The change is on the user's screen, unsaved; they keep it with Save or remove it with Undo. ..."
}
delivery.outcome.statusWhat it meansWhat the agent does
appliedThe change is on screen, unsaved.Tells the user what changed. They keep it with Save or remove it with Undo.
not_appliedThe screen didn't apply it. reason and message say why: dashboard_changed (the user changed the dashboard, or saved or cancelled, while the agent worked), not_editable (the editor is open, or Save isn't shown), refused (the change no longer fits) or expired (it arrived too late to land).For dashboard_changed and expired, reads the outline again and proposes again. Otherwise tells the user.
not_confirmedThe screen didn't confirm it within a few seconds: it closed or reconnected after receiving the change, or its answer was lost. The change may or may not be on screen.Reads the outline, which shows the screen as it is, and proposes only what is missing.

The deliveryId matches the one in the host events, so your page and your agent can refer to the same change.

When the screen can't be read

CodeSituation
screen_unavailableThe dashboard isn't open on a connected live screen with this token: the tab is closed or reconnecting, the user can't edit it, or the toolbar isn't shown. Propose returns it too when the screen closed between the read and the change; nothing was sent, so proposing again is safe.
multiple_screensThe dashboard is open in more than one tab or window with this token. Ask the user to close the others.
screen_copy_too_largeThe dashboard on screen is larger than 2 MiB.
one_dashboard_per_readA read of contents named several dashboards. Call again with dashboardId for each.
live_channel_unavailableLive changes couldn't be reached for a moment. Try again shortly.

semaphor_analyze with fromCard runs the card as saved. A card that exists only on screen is described in the outline; build its query from the outline's fields.

What Your User Sees

The change appears in place, and its data loads like any card's. The dashboard is in editing, with Save and Cancel in the toolbar, and a one-line note under the toolbar reads Changed by your assistant · Undo. The note stays until Save, Cancel or Undo. Your user can undo the change, keep editing by hand, ask for more, or save.

Mirror Changes in Your Page (optional)

With parentOrigin in the token, the iframe posts two more messages to your page beside dashboard:dirty-state (see Warn before leaving with unsaved changes), so your chat can show what landed:

{
  source: 'semaphor-embed',
  version: 1,
  type: 'dashboard:change-applied', // or 'dashboard:change-undone' after the note's Undo
  dashboardId: 'd_8f2k',
  payload: { deliveryId: '8a1f...', summary: ['Adds the bar chart "Revenue by region".'] },
}

Save and Cancel are reported by dashboard:dirty-state, as today.

Token Lifetime

A live token works for the iframe and the MCP until its tokenExpiry. When it expires while the dashboard is open, the live connection closes and the dashboard shows its session-expired state, as embeds do. Mint one token per working session, with a tokenExpiry that covers the session and no more, and mint a new one for the next session.

On this page