
Robert Tidball
July 23, 2026
Add a Conversational Layer to a Dash Dashboard with Dash MCP
Author: Robert Tidball
Dash dashboards are great when a user knows what to click. They can choose a dropdown, change a time window, inspect a chart, and move through tabs at their own pace.
But many dashboard users start somewhere else: with a question.
They want to ask:
- "What changed in EUR/USD recently?"
- "How does this pair compare with GBP/USD?"
- "Is the current signal bullish, bearish, or neutral?"
- "Which chart should I look at first?"
Dash MCP gives those users another way into the same app. Instead of making an AI assistant guess from a screenshot, Dash MCP lets the assistant interact with the Dash app through approved callbacks and tools. The dashboard still does the data fetching, filtering, plotting, and business logic. The AI assistant simply becomes a new interface for using it.
In this article, we will add Dash MCP to a real Dash app: the FXMacroData Public Macro Monitor. It is an FX and macro dashboard with pair selectors, rolling correlations, strategy diagnostics, macro overlays, KPI cards, and Plotly charts. The same pattern applies to financial dashboards, operations dashboards, scientific dashboards, or any Dash app where users benefit from asking follow-up questions.
By the end, you will know how to:
- Keep API access and business logic in Python.
- Enable Dash MCP with one constructor argument.
- Choose which callbacks an AI assistant can call.
- Add a custom MCP tool for a short dashboard summary.
- Return clear empty states instead of confusing blank charts.
- Connect an MCP client to the running Dash app.
The Dashboard Example
The example app is a live Dash dashboard for exploring FX pairs and macro context:
https://fxmacrodata.com/app-gallery/dash/public-macro-monitor
It has three main areas:
Area
What users do there
Overview
Check the selected FX pair, KPI cards, pair correlation, and risk diagnostics.
Strategy Lab
Explore signal behavior, return distribution, seasonality, and scenario projections.
Macro Desk
Compare macro releases with spot moves and review recent returns.
This is the kind of app where a conversational layer is genuinely useful. A user can still click through the interface, but they can also ask an assistant to set up a view, summarize a chart, or return a compact snapshot of the current pair.
The goal is not to replace the dashboard. The goal is to make the dashboard easier to use.
Prerequisites
Dash MCP is available in Dash 4.3.0 or newer:
pip install "dash>=4.3.0" pandas plotly requests kaleido
kaleido is optional, but useful when an MCP client wants to render Plotly figures as images.
This example uses FXMacroData as the data source. If your app uses protected data, keep API keys on the server side:
FXMD_API_KEY=YOUR_API_KEY
Do not place production keys in browser-visible state, client-side JavaScript, or committed source files.
Step 1: Put Data Access Behind Python Functions
A conversational dashboard should use the same trusted data path as the visual dashboard. In other words, the AI assistant should not build its own URLs or scrape the page. It should call the same Python functions that your callbacks already use.
Here is a simplified API helper:
import osimport requestsAPI_BASE = (  os.getenv("PUBLIC_DASH_API_BASE_URL", "").strip()  or "https://api.fxmacrodata.com/v1")def build_api_params(params: dict) -> dict:  clean = {k: v for k, v in params.items() if v is not None}  api_key = os.getenv("FXMD_API_KEY", "").strip()  if api_key:    clean["api_key"] = api_key  return cleandef fetch_json(path: str, **params) -> dict:  response = requests.get(    f"{API_BASE}{path}",    params=build_api_params(params),    headers={"accept": "application/json"},    timeout=20,  )  response.raise_for_status()  payload = response.json()  return payload if isinstance(payload, dict) else {}
The dashboard can then load FX history through a normal Python function:
from datetime import datetime, timedelta, timezonedef load_forex_series(base: str, quote: str) -> list[dict]:  end_date = datetime.now(timezone.utc).date()  start_date = end_date - timedelta(days=365)  rows = []  for offset in range(0, 300, 100):    payload = fetch_json(      f"/forex/{base.lower()}/{quote.lower()}",      start_date=start_date.isoformat(),      end_date=end_date.isoformat(),      limit=100,      offset=offset,    )    data = payload.get("data", [])    if not data:      break    for row in data:      try:        rows.append({"date": row["date"], "val": float(row["val"])})      except (KeyError, TypeError, ValueError):        continue    if len(data) < 100:      break  return sorted(rows, key=lambda item: item["date"])
This keeps the dashboard predictable. Whether a person clicks a dropdown or an assistant calls a tool, the app uses the same data source, same timeout rules, and same parsing logic.
Step 2: Build the Dash App Normally
Dash MCP does not require you to design a separate AI-only app. Start with the dashboard your users already need.
For the Public Macro Monitor, the app includes controls such as:
- FX pair selector, for example EUR/USD or USD/JPY.
- Comparison pair selector.
- Window selector, such as 3M, 6M, or 1Y.
- Strategy model selector.
- Scenario and overlay controls.
- Plotly graphs and Dash tables.
The layout can stay familiar. In a production app, a small layout function keeps the page definition easy to test and extend:
from dash import dcc, htmldef serve_layout():  return html.Main(    [      dcc.Dropdown(id="pair-select", value="EUR_USD"),      dcc.Dropdown(id="compare-select", value=["GBP_USD"], multi=True),      dcc.RadioItems(id="window-select", value="3m"),      dcc.Graph(id="spot-graph"),      dcc.Graph(id="correlation-graph"),      dcc.Graph(id="risk-regime-graph"),      dcc.Store(id="monitor-state-store"),    ]  )app.layout = serve_layout
Good labels, clear component IDs, and predictable control values help both human users and AI assistants. If your dashboard is easy to reason about as a Dash app, it is easier to expose through MCP.
Step 3: Enable Dash MCP
The basic MCP setup is small:
from dash import Dashapp = Dash(  __name__,  title="FX Macro Pulse Monitor",  enable_mcp=True,  suppress_callback_exceptions=True,)server = app.server
For a local app running at http://127.0.0.1:8050, the MCP endpoint is:
http://127.0.0.1:8050/_mcp
If your Dash app is hosted under a URL prefix, the MCP endpoint uses that same app prefix. For example, the hosted Public Macro Monitor uses:
https://fxmacrodata.com/app-gallery/dash/public-macro-monitor/embed/_mcp
That endpoint is for MCP clients. The normal dashboard page remains the page people open in their browser.
Step 4: Choose What the Assistant Can Use
Dash MCP can expose callbacks. For a real dashboard, it is better to be deliberate than to expose everything.
Start with an opt-in policy:
from dash.mcp import configure_mcp_serverconfigure_mcp_server(  include_callbacks=False,  expose_callback_docstrings=False,)
Now callbacks are only available to the assistant when you explicitly mark them. This keeps the conversational interface focused on useful actions, such as changing the selected pair or returning a chart, instead of exposing every small UI helper.
Use this filter when deciding what to expose:
Ask
Why it matters
Would a user naturally ask for this?
Expose user-facing workflows, not tiny implementation steps.
Is the callback read-only?
Read-only tools are easier to share safely.
Does the callback return useful output?
Charts, tables, and summaries are good candidates.
Is the docstring safe to show?
Write docstrings for users, not maintainers.
Is the query expensive?
Add rate limits or expose a lighter custom tool.
Treat exposed docstrings as part of the app interface. The assistant reads them to decide when and how to call a callback or tool, so write them like short instructions for a new analyst using the dashboard.
Step 5: Expose a Useful Dashboard Callback
The Public Macro Monitor has a main callback that updates charts, KPI cards, table rows, and state metadata when the user changes the controls.
Here is a shortened version:
from dash import Input, Output@app.callback(  Output("spot-graph", "figure"),  Output("correlation-graph", "figure"),  Output("risk-regime-graph", "figure"),  Output("monitor-state-store", "data"),  Input("pair-select", "value"),  Input("compare-select", "value"),  Input("window-select", "value"),  mcp_enabled=True,  mcp_expose_docstring=True,)def update_figures(  pair: str,  compare_pairs: list[str] | None,  window_key: str,):  """Render the selected FX pair charts and dashboard state."""  ...
The important parts are:
- mcp_enabled=True makes this callback available to the assistant.
- mcp_expose_docstring=True lets the assistant see the plain-language description.
- Type annotations help the assistant call the callback with the right values.
Dash MCP also handles returned content in useful ways. When an exposed callback returns a Plotly figure, the MCP layer attempts to render the chart as a PNG for clients that support images; kaleido is used when that static image representation is available. Tabular outputs, such as pandas DataFrames, can be rendered as Markdown tables instead of unstructured text.
A user can now ask an assistant to change the dashboard to USD/JPY, compare it with another pair, or explain the returned figure.
Step 6: Add a Short Snapshot Tool
Callbacks are best when the assistant should interact with the visible app. Sometimes a user just wants a short answer.
For that, add a custom MCP tool:
import plotly.graph_objects as godef empty_figure(title: str, message: str) -> go.Figure:  fig = go.Figure()  fig.add_annotation(    text=message,    x=0.5,    y=0.5,    xref="paper",    yref="paper",    showarrow=False,    align="center",  )  fig.update_layout(    title=title,    xaxis={"visible": False},    yaxis={"visible": False},    height=420,  )  return fig
For example, a correlation matrix should not show a full grid of zeroes when there are no return observations:
if not any(return_maps.values()):  return empty_figure(    "Cross-Pair Correlation Matrix",    "No FX return data is available for the selected window.",  )
This makes the dashboard more honest for users and gives the assistant better information to work with.
Step 8: Connect an MCP Client
Run the app:
python app.py
Then connect an MCP client to the app. Some clients use a JSON shape like this:
{ "mcpServers": {  "fx-macro-monitor": {   "type": "http",   "url": "http://127.0.0.1:8050/_mcp"  } }}
For a hosted app, use the hosted MCP endpoint:
{ "mcpServers": {  "fx-macro-monitor": {   "type": "http",   "url": "https://fxmacrodata.com/app-gallery/dash/public-macro-monitor/embed/_mcp"  } }}
Some clients also provide a command-line way to add the same server. For example, Claude Code uses the same URL with an HTTP transport:
claude mcp add fx-macro-monitor --transport http --scope user http://127.0.0.1:8050/_mcp
For a hosted app, replace the local URL with the hosted mcp endpoint. Other clients use different configuration surfaces, but the important pieces are the server name, HTTP transport, and the mcp URL shown above.
Once connected, try prompts that refer to the dashboard:
- "Use the FX Macro Monitor to summarize EUR/USD over the 3M window."
- "Switch the dashboard to USD/JPY and explain the risk-regime chart."
- "Compare EUR/USD with GBP/USD and summarize the correlation view."
- "Call get_public_macro_monitor_snapshot for EUR_USD with a 6M window."
The assistant should answer from the app's callbacks and tools, not from general market memory.
Step 9: Deploy with the Same Care as Any Dashboard
Dash MCP gives an assistant a structured way to use your app. It does not replace normal app security.
For public demos:
- Keep credentials on the server.
- Expose read-only callbacks and tools.
- Use simple, user-facing docstrings.
- Return clear empty states.
- Avoid exposing values that are not meant for users.
For private dashboards:
- Put the app behind authentication.
- Review every exposed callback.
- Rate-limit expensive workflows.
- Keep logs for assistant-triggered actions.
- Separate read-only tools from anything that changes data.
The safest pattern is to expose the same tasks you would be comfortable letting a user perform in the browser.
Conclusion
Dash MCP works best when it is added to a well-designed Dash app. Start with a dashboard people can use visually. Keep the data path in Python. Expose a small number of useful callbacks. Add custom tools for short summaries. Make errors and empty states clear.
With those pieces in place, an AI assistant becomes another way to operate the dashboard:
app = Dash(__name__, enable_mcp=True)configure_mcp_server(include_callbacks=False)@app.callback(..., mcp_enabled=True, mcp_expose_docstring=True)def update_figures(...):Â Â ...@mcp_enabled(name="get_public_macro_monitor_snapshot", expose_docstring=True)def get_public_macro_monitor_snapshot(...):Â Â ...
That is the core pattern. The browser experience still matters. The charts still matter. Dash still owns the application logic. MCP simply lets users ask for the view, chart, or summary they need in natural language.
Further Reading
- Dash MCP documentation
- Dash MCP callbacks
- Dash MCP decorator
- FXMacroData Public Macro Monitor
- FXMacroData API documentation
Disclosure
This article uses FXMacroData as the macroeconomic and FX data source. The Dash and Dash MCP patterns are general and can be applied to other data providers and private datasets.