Semaphor
MCP

MCP Changes

Dated changes to Semaphor MCP's tools and their shapes.

Changes to the tools, their arguments and the outline, newest first. A change that alters a shape an agent may already read says what to check.

November 2026: dashboard calculated fields

  • Agents add a dashboard's own calculated fields. A dashboard proposal takes calculation.add (with the dataset's domainId and dataset), calculation.update and calculation.remove. A card query names the field by its name, as it names any field of its dataset. See Calculated fields.
  • Card queries name domain calculations. A domain's calculated facts, dimensions and metrics read back by name in the outline's queries and can be named in a new card's query.
  • The outline lists them under calculatedFields: each field's dataset, name, label, formula in field names, use, dataType and usedBy.
  • A domain proposal takes calculation.update for a domain calculation's label, description or formula. Removing a domain calculation stays on the domain page.
  • New refusals: calculated_field_name_taken, calculated_field_not_found, calculated_field_in_use and calculated_field_not_for_matrix.

Check in your code: the outline may carry calculatedFields, and a card query may name one of them.

November 2026: dataset names checked at save

  • A save checks the datasets a dashboard names. A proposal or save that adds a card or filter naming a dataset its semantic domain doesn't have, or a physical table more than one dataset reads, is refused with document_invalid. Its details.errorCode is unknown_dataset; details names the card or filter, the field, the datasetName and the candidates, and the fix lists the datasets to name instead.
  • Older names still work. A name that reaches one dataset by another spelling, such as the table of a renamed dataset, saves as it is, and a name the saved dashboard already has on the same card or filter is let through.

Check in your code: a document_invalid refusal may now carry details.errorCode: "unknown_dataset"; re-read the domain's datasets with semaphor_list_datasets and propose again with one of them.

November 2026: table column filters

  • Viewers filter a table's columns from their headers, on Tables, Detail tables and Matrix cards built from a dataset; it's on for every one. card.add and card.update take columnFilters: false to turn it off for a table, and true to turn it back on. See Card settings.
  • Viewers search a table, on Tables and Detail tables built from a dataset. card.add and card.update take tableSearch: false to remove the search box, and true to bring it back.
  • The outline reads it only when it's off: a table's columnFilters or tableSearch is false. A SQL table, a chart or a KPI refuses it with setting_not_for_card.
  • Agents pin filters on a card. filter.add takes a cardId (instead of sheetIds) and a position; filter.update and filter.remove take a pinned filter's id and its card's cardId (required when copies of one visual share the id), and filter.update takes position to reorder. The outline lists each card's pinnedFilters in order. See Pinned filters.

Check in your code: a card in the outline may carry pinnedFilters.

November 2026: number filters as ranges

  • Number filters show as a range or a list. filter.add and filter.update take showAs: "range" | "list". A new number filter is a range unless showAs says list or the change gives defaultValues. See Number filters.
  • A range opens with a condition. defaultCondition is { "operator", "values" } in the card filters' vocabulary (between, not_between, >=, <=, >, <, =, !=, is_null, is_not_null); null on filter.update clears it.
  • The outline reads both. Number filters carry showAs; a range carries defaultCondition, or defaultValues when it was saved earlier with values to tick (defaultValues: null clears them, a defaultCondition replaces them).
  • SQL cards apply every condition. A dashboard filter's not equal, is empty and is not empty now apply to SQL cards, and numbers reach them as numbers, on every warehouse.

Check in your code: a number filter added without showAs or defaultValues is now a range, and its sentence says "a range". Code that read a number filter's default from defaultValues should also read defaultCondition when showAs is "range".

November 2026: totals per measure

  • Agents set each measure's total. A table's totals takes measures, by column name: "overall" (the measure over the whole table, the default) or "sumOfRows" (its rows added together). row is optional, so a change can set a measure's total alone. See Totals.
  • The outline reads each total. While a table's total row shows, totals.measures lists what each measure totals to, including "none" for a table calculation with no single total.
  • Totals are Overall by default. A distinct count, an average or a median totals to its value over the whole table, where the total row was empty before; a comparison table totals its comparison columns.

Check in your code: code that read totals as exactly { "row": boolean } should allow measures. A total that changed from a sum to an Overall value can be put back with "sumOfRows" for that measure.

November 2026: semantic model editing

  • Agents improve a semantic model. semaphor_propose_change with a domainId takes domain.update, dataset.update, field.update and metric.update (labels, descriptions, Context, formats, the direction that's good, the field that names a row, hiding a field), calculation.add (a new calculation, placed by its formula) and relationship.update (turn a relationship off, change its join). See Improve a Semantic Model and Change a Semantic Model.
  • Context changes item by item. Synonyms and example questions take { "add": [...], "remove": [...] }; instructions are replaced, and null clears them.
  • One line per change, saying what changes, from what to what, and what stays; a definition's line says what in the domain reads it, and the proposal is risk: medium.
  • Nothing typed by hand is lost. If a value a change replaces was changed after the preview, apply returns proposal_stale with changed (each value as { what, now }) and saves nothing.
  • More of a field in the schema. semaphor_get_dataset_schema fields carry format and isDisplayField when set.

Check in your code: semaphor_propose_change lists more changes for organization Authors and higher, so its definition is larger. A domain proposal holds either relationship.add changes or the new changes, not both, and up to 20. A change that sends a field's or metric's name is refused with "Change the label instead."

October 2026: date windows

  • Agents state the editor's date windows. timeWindow on semaphor_analyze, semaphor_plan_analytics_recovery obligations and card queries, and defaultPeriod on a filter, take the windows a dashboard's date filter saves: periods (whole periods, such as "from": -3, "to": -1 for the last 3 complete months, with "toDate": true for so far), rolling (elapsed time; minutes and hours are rolling only), monthDay and absolute. See Time windows.
  • The anchor is required: "now", "latest_data" or { "lastComplete": unit }. Year to last completed month, once set only in the editor, is { "kind": "periods", "unit": "year", "from": 0, "to": 0, "toDate": true, "anchor": { "lastComplete": "month" } }.
  • Saved windows follow the editor's rules. A card's timeWindow and a filter's defaultPeriod count days and longer in whole periods, and take minutes and hours only on a date field with a time of day. A default counts from the run, never from the latest data, and has no fixed dates. A refused window's fix lists the windows to send instead.
  • Results report the window that ran. metadata.resolvedTimeWindow has the window as asked, its label, and the start, end, timezone and latestData it ran with. A period change reports the same fields in metadata.timeWindow; a comparison reports its window without bounds, and the dates it compared as comparison_window entries in executionResult.constraints.
  • An empty period change says why. metadata.noChanges gives a reason (row_limit, no_changes_in_direction, no_values_in_window or no_comparable_values), a message, and a fix when a different request helps. A period change no longer answers from a different period when its own window has no changes.
  • Readback is the stored window. The outline's defaultPeriod and a card query's timeWindow are what you'd send. A stored window that can't be saved again, such as a rolling 7-day default, carries defaultPeriodReadOnly or timeWindowReadOnly with the refusal and its replacements, and stays while the field is left out.

Check in your code: windows in the earlier form, { "unit", "value", "anchor"?, "completeness"? } and { "kind": "calendar", ... }, are refused with the form to send. anchor: "latest_available" is now "latest_data". metadata.timeWindowResolution is replaced by metadata.resolvedTimeWindow, and a period change's metadata.timeWindow takes the same fields: windowStart, windowEnd and latestAvailableAt are now start, end and latestData, and requestedAnchor, appliedAnchor, fallbackApplied, completeness and excludedPartialPeriod are gone (it keeps referenceTime, filteredChangeCount and unfilteredChangeCount). Code that treated an empty changes list as "no change" should read metadata.noChanges. The warning window_saved_as_fixed_dates is gone.

October 2026: first-run fixes

  • The watch link after a new dashboard. In logged-in sessions, semaphor_apply_change returns watchUrl in its target for a dashboard it created, as it already did for one it saved. The semaphor_get_dashboard_details description says what watchUrl and watching are. See Watch Changes Land in Your Browser.
  • A change on the watch page is no preview. A proposal that goes to your watch page comes back with delivery and no confirm, since there is nothing to apply, and its next names the cards it added or changed, so the agent corrects one with card.update instead of adding a second.
  • A new dashboard is laid out. After its changes, each sheet with cards is arranged on every screen size, unless a card.add states a width or position or the changes include layout.arrange. See Build a New Dashboard.
  • A width alone places a card beside the last one when the row has room, so two halves sit side by side; it used to start a new row.
  • Refusals name fields as a query does. A refusal that lists a card's groupings writes them as query.dimensions takes them: a plain name on the card's dataset, { "name", "datasetName" } for a joined one. Display labels were listed before and never accepted.
  • A row count keeps its label. { "name": "__count", "label": "…" } works in card.add and card.update.
  • A format change reads "formats Revenue like $12,480", so the sample doesn't read as a value.

Check in your code: percent_of_total with scope: "partition_total" and no resetBy (or an empty one) is now refused with transform_field_invalid, listing the card's groupings; it used to give a share of the grand total. A partition_total read from the outline and sent back unchanged is still accepted. In logged-in sessions, code that reads confirm on a proposal should allow it to be absent when delivery is present.

October 2026: report a problem

  • semaphor_report_problem. An agent sends Semaphor support its own account of what went wrong, with your request in your words and, when one is involved, the proposal. It asks you first, and returns a report number. Every session gets it. See Report a Problem.

Check in your code: tools/list now has one more tool for every session. Code that checks for an exact tool count should expect one more.

October 2026: building a report

  • A new dashboard in one proposal. semaphor_propose_change takes create: { title, description } in place of dashboardId, with up to 50 of the changes an edit takes. The proposal previews the cards with real numbers, and apply saves the dashboard private to you. Organization users and end users with the Power User role can create one. See Build a New Dashboard.
  • Sheets. sheet.add adds a sheet and sheet.update renames one. A $handle from sheet.add names the new sheet in later changes.
  • Cards as tabs. layout.tabs puts cards in one spot as tabs, the first named shown; layout.untab gives a card its own spot again. Cards keep their IDs, filter bindings and highlights.
  • Text cards. card.add with type: "text" and card.update take text as blocks: headings, paragraphs and one-level lists of runs, each run text or one of the card's variables, with bold, italic and link. Text is taken as written. The outline reads text cards in the same blocks. See Text cards.
  • confirm: a new dashboard, a new sheet, tabs and a text card's words need the user's yes; renaming a sheet doesn't.

Check in your code: dashboard.generate is gone; build the dashboard with create and the changes you want, and a proposal made with it before now returns proposal_stale. semaphor_apply_change may return created entries of kind sheet. An outline text card is now editable with text blocks in place of query; a text card whose words come from a query stays read only.

October 2026: what a number means

  • Comparisons and targets on a measure. A measure in query.measures takes comparison: inherit, off, previous_period, previous_year (with offsets, weekday alignment and to-date) or target. The outline shows each measure's own comparison. See What a Number Means.
  • Table calculations. A measure takes transform: running_total, moving_average, difference_from_previous or percent_of_total, with orderBy and resetBy. Cards with a table calculation can now be changed through the MCP.
  • Which way is good, and how the change shows. columns[name].polarity, comparisonDisplay and inheritComparison on card.add and card.update. A measure's direction from the semantic model reaches cards built through the MCP.
  • semaphor_analyze takes comparison and transform on its measures too.

Check in your code: an outline measure may now be an object with comparison or transform where it was a name. semaphor_analyze refuses a comparison with a key it doesn't take, naming it (invalid_comparison), where it used to ignore the key; offsets, alignment and to-date are refused there, since they are card settings.

October 2026: how cards read and show

  • Card settings in a change. card.add and card.update take columns (each column's label, format, hidden and width), totals, dataLabels, legend and axes, and a chart's variant as orientation, stack and donut. See How Cards Read and Show.
  • Highlights. highlight.add and highlight.remove color cells and rows where a condition holds, shade a column on a color scale, or add data bars.
  • The dashboard's title and description. dashboard.update. The outline shows the description.
  • The outline shows each editable card's settings, with inherits and editorOnly.
  • The server's instructions give one recipe: read the outline, change only what the user asked, propose, and apply when confirm is not_needed or the user agrees. The tool definition lists a format's stored fields, and a refused format's fix states them.
  • confirm on every proposal: not_needed when a proposal only changes how cards read and show, or the dashboard's title and description; needed otherwise. An apply checks again and refuses with confirm_changed when that has changed.
  • Horizontal bars, stacked bars, stacked lines and areas and doughnuts can be changed.

Check in your code: the outline's type for those five variants is now the base type with the variant as properties. A stacked bar that read "type": "stacked_bar" now reads "type": "bar", "orientation": "vertical", "stack": "stacked", and a doughnut reads "type": "pie", "donut": true. Bar, line, area and pie cards carry their properties too.

On this page