Dashboard Builder MCP server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Dashboard Builder MCP servershow me available datasets"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Dashboard Builder MCP server
Lets an AI client discover your datasets and author dashboards in Dashboard Builder.
It talks to the Next.js app over HTTP as an ordinary API client, so every permission guard, dependency policy and validation rule in the app still applies. Nothing in the main application changes.
Run it two ways:
Who runs it | Identity | Users need | |
Hosted | one server, whole org | each person's own account, bound to their key once | a URL and a key |
Local | each person, own machine | that person's own account | Node and a copy of this folder |
Hosted is the normal deployment and is what this document covers. Local mode is for developing the server itself, or for per-user identity, and lives in DEVELOPMENT.md.
For users: connecting to a hosted server
You need two things from whoever deployed it: the URL and your gate key. Nothing to clone, no
files to point at, no .env.
Add this to claude_desktop_config.json (Claude Desktop) or .mcp.json (Claude Code):
{
"mcpServers": {
"dashboard-builder": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://mcp.yourcompany.com/mcp",
"--header", "Authorization: Bearer YOUR_KEY_HERE",
"--header", "X-Dashboard-Username: you",
"--header", "X-Dashboard-Password: your-dashboard-password"
]
}
}
}mcpServers is a top-level key, a sibling of preferences — not nested inside it. Quit Claude
Desktop from the system tray and reopen; closing the window is not enough.
With the two X-Dashboard-* headers present, the server logs in as you automatically on first use
and again whenever the session expires — nothing else to do, and every call acts as you: your
permissions, your audit trail. The trade-off is that your dashboard password sits in this config file
and travels (over HTTPS) with each request. If the password contains characters outside ASCII, use
the curl bind below instead — HTTP headers do not carry them reliably.
Alternative: bind once with curl, keep the password out of the config
Omit the two X-Dashboard-* headers and instead bind your key once — the password is used for that
single login and never stored anywhere; the server keeps only the resulting session tokens, exactly
like a browser keeps cookies:
curl -X POST https://mcp.yourcompany.com/auth/bind \
-H "Authorization: Bearer YOUR_KEY_HERE" \
-H "content-type: application/json" \
-d '{"username":"you","password":"your-dashboard-password"}'The difference from the header route: when the session chain eventually expires you re-run this
command, whereas the headers re-bind automatically. DELETE /auth/bind with the same
Authorization header signs the key out in either case.
Alice's Claude ──[gate key]──> MCP server ──[Alice's session cookies]──> Dashboard API
^ ^
client config bound via credential headers or
POST /auth/bind; refreshed
automatically after thatCredential | Lives in | Answers |
Gate key | each user's client config | may this person use the MCP server? |
Session tokens | the server, one file per key | who does this key act as? |
If a key is never bound, tool calls fail with an error that explains the bind step — or, when the server is configured with a legacy service account, they fall back to that shared identity.
mcp-remote is a small bridge that runs locally and forwards to the server, so Node must be installed
on the user's machine. To avoid even that, Claude Desktop's Settings → Connectors → Add custom
connector takes a URL directly with nothing local — that path expects OAuth rather than a static
key, and availability varies by Desktop version.
Related MCP server: MCP SQL Server Data Warehouse Connector
Deploying the server
server.js is the startup file. It listens on PORT like a Next.js server.js, and puts an
API-key gate in front of every MCP request so unauthenticated callers are rejected before anything
reaches the dashboard system.
Endpoints: POST /mcp (gated), POST /auth/bind and DELETE /auth/bind (gated — bind or unbind the
calling key's dashboard identity), and GET /health (open, for the platform health check). Everything
else returns 404.
Environment variables
Required — the server will not start without these
Variable | Value |
|
|
|
|
Generate keys with openssl rand -hex 24. The label before the colon appears in logs and rate-limit
buckets; the secret itself is never logged. Revoke one person by removing their entry and restarting —
and delete their session file under ~/.dashboard-mcp/sessions/ to drop the bound identity too.
Each key is then bound to a dashboard account by its holder via POST /auth/bind — see the user
section above. No dashboard credential lives in the server's environment.
Optional legacy fallback — a shared service account
Variable | Value |
| a service account |
| that account's password |
When set, keys that have not been bound act as this shared account instead of failing — as does a key whose binding has expired, until it is re-bound. Useful during migration; skip it for new deployments so every caller has their own identity.
Strongly recommended
Variable | Value | Why |
|
| start read-only until identities are bound |
|
| enables DNS-rebinding protection |
| your client origin | same |
Leave DASHBOARD_MCP_PERSIST_SESSION at its default (true): bindings are stored one file per key
and survive restarts. Setting it to false keeps bindings in memory only, so every restart — and
every worker in a multi-worker host — needs its own re-bind.
MCP_ALLOWED_HOSTS and MCP_ALLOWED_ORIGINS are optional — the server runs without them and the
API-key gate still applies. Setting either switches on the transport's DNS-rebinding protection.
Leave both unset and the startup log says so explicitly.
Optional
Variable | Default |
| 3001 |
|
|
| 120 requests per key per window |
| 60000 |
Further tuning variables — session file path, request timeout, response caps and the dashboard kind
id override — are documented inline in .env.example, which is organised by mode and lists every
variable the server reads.
Multi-worker note
Bindings are one file per key, and a worker whose in-memory token has been rotated out by another worker recovers by re-reading that file, which the winning worker has already updated. The failure window is two workers refreshing the same token at the same instant; the loser recovers on its next attempt, and in the worst case the key must be re-bound. The MCP transport itself is stateless, so requests can land on any worker.
Plesk setup
Setting | Value |
Application root | the |
Application startup file |
|
Application mode | production |
Environment variables | the tables above, in the Node.js panel |
Before starting |
|
Add to the domain's Additional nginx directives:
proxy_buffering off;
proxy_read_timeout 300s;MCP replies as Server-Sent Events and nginx buffers proxied responses by default. Without
proxy_buffering off requests appear to hang rather than fail, which is a confusing way to lose an
afternoon.
Keep the Node port off the public firewall. Plesk's nginx proxies to it and sets X-Forwarded-For,
which is what makes the logged client IPs trustworthy.
Access versus identity
The gate key controls access; identity comes from the binding. The key gets a caller past the gate, and the session bound to that key decides who the dashboard sees — their permissions, their audit trail. The two are deliberately separate: rotating a key's secret drops its binding (the session is filed under the key's digest), and revoking a key removes access without touching the account.
The binding works the way a browser login does. POST /auth/bind runs the app's real
/api/auth/login once, the password is discarded after the exchange, and only the rotating
refresh-token session is kept — one file per key, mode 0600. Because the app rotates the refresh token
on every use, a leaked session file dies quickly; because the password is never stored, there is
nothing long-lived to leak. The trade-off: when a refresh chain expires or breaks, that key re-binds
with one curl.
A stable per-request credential (ApiKey in the main system, or OAuth) would remove even that
re-bind, but requires changes in the main application. This design deliberately needs none.
Developing or running it locally
Running the server on your own machine — for development, or for per-user identity without hosting — is documented separately in DEVELOPMENT.md.
Tools
The chain is: data source (HTTP API or Excel upload) → dataset (a shaped view with a transformation workflow) → dashboard widgets. The tools cover all three stages.
Tool | Mode | Purpose |
| read | Data source ids, labels, kinds (http/excel) and targets |
| read | One source's shape: params, response path, columns — never credentials |
| read | Live rows from a source; lists candidate response paths when one fails |
| read | Dataset ids, labels and scopes |
| read | Exact field names, inferred types, one sample value each |
| read | A capped sample of real rows |
| read | Full dataset definition: source, materialisation, workflow summary or raw envelope |
| read | Run a workflow draft against real data without saving; per-step diagnostics |
| read | Dashboard ids, labels and scopes |
| read | Dashboard details plus one line per widget; one config on request |
| read | The authorable widget kinds |
| read | Config contract for one kind, plus a real example from your workspace |
| write | Dry-run an HTTP endpoint config; nothing saved, columns and paths inferred |
| write | Create an HTTP data source; the server probes the URL and seeds a default dataset |
| write | Change parts of an HTTP source; merges with the stored config |
| write | Create a dataset; the server builds the default workflow when none is given |
| write | Partial update: label, source, limits, materialisation, or replace the workflow |
| write | Re-run a snapshot-materialised dataset, or report its last refresh |
| write | Create a dashboard and attach its datasets |
| write | Replace the dashboard's dataset list |
| write | Add one widget, auto-placed on the grid |
| write | Change title, dataset or config keys |
| write | Remove a widget |
| write | Repack the grid or apply explicit positions |
test_datasource sits in the write set because the app gates it behind the datasource
create/edit permission and it calls the upstream endpoint — but it persists nothing.
Design notes
Context discipline. The whole tool surface costs about 7 KB — 24 descriptions plus the server
instructions — so it stays cheap to keep loaded. Responses are compact text rather than raw JSON, and
every list is capped with an explicit note about what was omitted. get_dashboard deliberately omits
widget configs; you ask for one widget by id when you need its config.
Progressive disclosure. A chart config has roughly 59 fields. Putting that in a tool description
would dominate the client's context on every request, so describe_widget_kind serves the contract on
demand instead: field names, types, notes, a minimal working example, and — the useful part — a real
config harvested from an existing widget of that kind in your own workspace. Copying a shape that
already renders beats inventing one from field names.
The server does the geometry. Models are unreliable at 2D packing. add_widget takes a size
hint (small, medium, large, full) and finds the first free non-overlapping cell on the
12-column grid itself. arrange_dashboard in auto mode repacks an entire dashboard.
Fail before the API, not after. Widget configs are stored as opaque JSON by the app, so a
misspelled key produces a blank widget rather than an error. add_widget validates the config against
the kind's contract first — required keys, valid aggregation names, field present when the
aggregation needs one — and returns a specific list of what is missing.
Widgets match what the UI would create. The app's palette seeds every new widget with the kind's
defaultConfig from the registry (config === undefined ? def.defaultConfig : config). add_widget
mirrors that: the kind defaults are layered underneath whatever the caller supplies, so an
MCP-authored chart carries the same paginationMode and maxPoints baseline as a hand-built one
instead of a sparse config the renderer has to fall back on. The merged object is what gets validated.
Merge instead of resend. PATCH /widgets/:id replaces the config object wholesale. update_widget
defaults to merging your keys into the existing config, so changing one setting does not mean
resending all of it.
Fail closed on startup. The HTTP server refuses to start without at least one MCP_API_KEYS
entry, and rejects keys under 24 characters. An unauthenticated MCP endpoint should never be possible
by accident. Keys are compared as SHA-256 digests with timingSafeEqual, and only labels are logged.
Secrets stay server-side. describe_datasource reads the app's /schema endpoint, which never
includes credentials. update_datasource needs the full stored config to merge against — it fetches
it inside the tool run and sends the merged result back without ever rendering authConfig, stored
headers or body into the model's context.
Validate before saving. Workflow envelopes are easy to get subtly wrong, so the flow is: copy a
working envelope with get_dataset includeWorkflow, edit it, run validate_dataset_workflow (which
executes it server-side and reports per-step row counts, output fields and null counts without
persisting anything), then save with update_dataset.
Known limits
The widget kind catalog is a copy.
src/catalog/widget-kinds.tsmirrorssrc/features/dashboard/widgets/registry.ts— including each kind'sdefaultConfig— and the per-kind config interfaces. The app's registry is a client component and imports React, so it cannot be imported here. If a widget kind gains a field, or adefaultConfigvalue changes, update the catalog too, or MCP-created widgets will drift from UI-created ones.Catalog coverage is high but not total. Documented vs. actual config fields: table 18/21, stat 22/25, chart 39/59, select 9/12, text 16/17. What is left out is mostly cosmetic variants (pie/line/bar style options, right-hand axis overrides) and legacy interaction keys superseded by
highlightBindings. The live example returned bydescribe_widget_kindis the reference for those. Fields are grouped into core / display / interaction so the data contract reads first.Bindings expire with the refresh chain. A key's session lasts as long as the app keeps its rotating refresh token alive. When it lapses, calls fail with an error naming the fix and the key's holder re-binds with one curl. A never-expiring identity needs
ApiKeywired intosrc/lib/api-guard.tsin the main system, which is not done.Writes are direct. The app has a change-draft and approval workflow (
ChangeDraft,ApprovalRequest). These tools write straight through with the signed-in account's permissions. If AI-authored dashboards should be reviewed before going live, route the write tools at/api/change-draftsinstead and keep the account's grants read-only.Excel data sources are read-only here. Creating or refreshing one requires a multipart file upload, which does not fit the MCP text flow. They still appear in
list_datasources,describe_datasourceandpreview_datasource; create and refresh them in the UI.No delete tools. Deleting a datasource cascades a soft delete to every dataset built on it, so deletions stay a human action in the UI.
delete_widgetis the only destructive tool exposed.
This server cannot be deployed
Maintenance
Related MCP Connectors
Query your org's data in natural language — read-only MCP access to SQL, NoSQL, files & warehouses.
Query your warehouse or a CSV with Claude/ChatGPT over MCP, governed by table-level ACL + audit.
Security gateway for AI agents: policy, approval, and audited execution, no secrets shared.
Zero-setup MCP gateway securely connecting AI to your tools with authentication and workflows
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables secure querying of analytics data stored in AWS Athena/S3 through natural language, with multiple security layers including query validation, resource limits, and automatic PII redaction to prevent data breaches and destructive operations.-
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to interact with SQL Server Data Warehouses using natural language for automatic schema discovery and report generation. It ensures security by restricting operations to read-only SELECT queries through both code validation and database permissions.-
- AlicenseNot gradedqualityDmaintenanceEnables natural language querying and management of Apache Superset dashboard metadata, including user activity, data lineage, and change tracking.4MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI applications to interact with Tableau for querying data, exploring content, and retrieving views through natural language.4,263 npmApache 2.0