Semaphor
MCP

Making Changes

Preview and apply dashboard and semantic model changes through Semaphor MCP.

Agents can create a dashboard or add a relationship to a semantic model. Every change goes through two tools: semaphor_propose_change previews it and saves nothing, and semaphor_apply_change applies exactly what was previewed.

propose_changesaves nothingproposedexpires in 30 minapplyingone writer at a timeappliedresult recordedfailednothing writtenapplyretry: sameresulttarget changed: proposal_stale, propose again
A proposal saves nothing and expires after 30 minutes. A retry of an applied proposal returns the recorded result instead of writing twice.

Quick Start

Propose

The agent calls semaphor_propose_change and gets back a proposalId and a readable preview of what apply will do. Nothing is saved.

Propose: create a dashboard
{
  "target": { "kind": "dashboard" },
  "operations": [{
    "op": "create",
    "request": {
      "prompt": "Weekly revenue by region, with top customers",
      "domainId": "dom_abc",
      "datasetNames": ["orders"]
    }
  }],
  "rationale": "Sales leads asked for a weekly regional view."
}

Review

The agent shows you the preview. Your client may also ask before the apply runs: see Permission and Approval.

Apply

The agent calls semaphor_apply_change with the proposalId, and the change is applied exactly as previewed.

Apply
{ "proposalId": "chg_..." }

Who Can Make Changes

The change tools appear only for organization users with an Author role or higher whose session has the mcp:write scope. What each session can change also depends on how it connected:

KindOperationLogged in (OAuth)Automation tokenEnd-user token
dashboardcreateYesYesNo
semantic_modeladd_relationshipYesNoNo

Semantic model changes affect every query on a domain, so they need an interactive login. A token session that proposes one gets change_not_allowed_for_credential. End users never get change tools.

To get write scope, log in with a client that requests mcp:read mcp:write, or mint a token with "mcp": { "scopes": ["mcp:read", "mcp:write"] }. See Authentication and Security and Connect with a Token.

Proposals

semaphor_propose_change takes:

ArgumentRequiredDescription
targetYesWhat to change: { "kind": "dashboard" } or { "kind": "semantic_model", "domainId": "..." }
operationsYesExactly one operation for that kind
rationaleNoWhy the change is needed, in a sentence. It's added to the preview summary.
projectIdNoInteractive sessions pass the project; token sessions use the token's project

It returns:

Propose response
{
  "proposalId": "chg_...",
  "expiresAt": "2026-10-04T18:30:00.000Z",
  "preview": {
    "summary": "Create a private dashboard \"Weekly Revenue by Region\" with 3 cards on 1 sheet, created private like a dashboard you create in the app. Rationale: Sales leads asked for a weekly regional view.",
    "changes": [
      { "action": "create", "entity": "dashboard", "name": "Weekly Revenue by Region", "detail": { "datasets": ["orders"], "sheets": ["Overview"] } },
      { "action": "create", "entity": "card", "name": "Revenue by Week", "detail": { "type": "line", "sheet": "Overview" } }
    ]
  },
  "validation": { "errors": [], "warnings": [], "repairHints": [] },
  "risk": "low"
}
  • Proposals expire after 30 minutes. After that, propose again.
  • Only you can apply your proposal, in the same project. Anyone else gets proposal_not_found.
  • risk is low for a new private dashboard and medium for a relationship, because every query on the domain can use it.

Create a Dashboard

Kind dashboard, operation create. Describe the dashboard in a prompt over one semantic domain:

FieldRequiredDescription
request.promptYesWhat the dashboard should show
request.domainIdYesThe semantic domain to build on, from semaphor_list_semantic_domains
request.datasetNameNoOne dataset to use
request.datasetNamesNoSeveral datasets to use

Semaphor plans the dashboard once, when you propose, and stores that plan. Apply creates exactly that plan and never plans again, so the dashboard always matches the preview. The new dashboard is created private, like a dashboard you create in the app, and you share it from the app.

If the plan fails validation, propose returns invalid_change with a repair hint: name the measures and dimensions to show, or pick datasets with datasetNames, then propose again.

Apply response
{
  "applied": true,
  "replayed": false,
  "result": { "kind": "dashboard", "id": "d_...", "url": "https://semaphor.cloud/..." },
  "warnings": []
}

Add a Relationship

Kind semantic_model, operation add_relationship. Adds a relationship between two datasets of a domain, so queries can join them:

Propose: add a relationship
{
  "target": { "kind": "semantic_model", "domainId": "dom_abc" },
  "operations": [{
    "op": "add_relationship",
    "relationship": {
      "sourceDataset": "orders",
      "sourceFields": ["customer_id"],
      "targetDataset": "customers",
      "targetFields": ["id"],
      "cardinality": "many_to_one",
      "defaultJoinType": "LEFT"
    }
  }],
  "rationale": "Break revenue down by customer segment."
}
FieldValues
sourceDataset, targetDatasetDataset names in the domain
sourceFields, targetFieldsOne or more field names on each side
cardinalitymany_to_one, one_to_one or one_to_many
defaultJoinTypeLEFT or INNER
descriptionOptional

The relationship is validated when you propose. If a field doesn't exist, propose returns invalid_change and stores nothing.

If someone edits the domain between your preview and your apply, apply returns proposal_stale and changes nothing. Propose again against the current domain. When two proposals target the same domain, the first to apply wins and the other returns proposal_stale.

Retries

semaphor_apply_change is safe to retry with the same proposalId. A change is never applied twice.

You call apply again and the first apply...You get
SucceededThe recorded result again, with replayed: true
FailedThe recorded error. Propose again if you still want the change.
Is still runningapply_in_progress. Retry the same call shortly.
Started over 5 minutes ago and never recorded a resultapply_outcome_unknown, with a howToCheck message that says how to check whether the change landed

If your client loses the response to an apply, retry the same call. You get the same dashboard back, not a second one. After apply_outcome_unknown, follow howToCheck before proposing again, so you don't create a duplicate.

Errors

Change tools return errors as isError: true results with a code:

CodeMeaningWhat to do
write_scope_requiredThe session doesn't have mcp:writeLog in with write access, or mint a token with mcp:write
change_not_allowed_for_credentialA token session proposed a semantic model changeUse an interactive login
invalid_change_targetUnknown kind or operationUse a kind and operation from the error's list
invalid_changeThe change failed validation; nothing was storedFix the request and propose again
proposal_not_foundUnknown or expired proposalId, or another user's or project's proposalPropose again
proposal_staleThe target changed after the previewPropose again
apply_in_progressThe first apply is still runningRetry the same call
apply_outcome_unknownThe first apply never recorded a resultCheck with howToCheck before proposing again
permission_deniedYour permissions don't allow the change in this project, such as creating dashboardsAsk an admin for access

Permission and Approval

These are two different things.

  • Permission is enforced by Semaphor on every call: the mcp:write scope, your role, the per-kind rule above, and your project permissions.
  • Approval of a particular change happens in your client, before it calls semaphor_apply_change.

semaphor_apply_change is marked as destructive and semaphor_propose_change as read only. These MCP annotations are advisory hints: they don't force a confirmation dialog, and Semaphor can't prove that a person saw the preview. What happens before apply runs depends on the client:

ClientBefore semaphor_apply_change runs
Claude CodeAsks before an MCP tool runs, unless the tool is already allowed. You can choose "Yes, and don't ask again", or add the tool to permissions.allow (for a server you configured yourself, the rule is mcp__<server>__<tool>). Claude Code doesn't use the annotations to decide. Running with permissions bypassed skips these prompts. In non-interactive claude -p runs, a call that would ask is denied unless --allowedTools or an allow rule covers it.
claude.aiUses the annotations for default permissions: read-only tools can run without a per-call confirmation, and destructive tools prompt.
CodexFollows your Codex approval settings for MCP tool calls.
Headless tokenNo per-change approval. Minting a token with mcp:write is delegated authority: whoever minted it approved writes in advance.

For an unattended Claude Code run that should read but never write, allow only the read tools, for example --allowedTools "mcp__semaphor-token__semaphor_analyze,mcp__semaphor-token__semaphor_get_analysis_context", or mint the token without mcp:write. The second is the stronger control, because the server then refuses writes whatever the client does.

See Authentication and Security for more.

On this page