Skip to main content
Glama
Destiny-Enterprises

Dashboard Builder MCP server

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 that

Credential

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

DASHBOARD_API_URL

https://dashboard.yourcompany.com

MCP_API_KEYS

alice:<secret>,bob:<secret> — one per person, min 24 chars

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

DASHBOARD_MCP_USERNAME

a service account

DASHBOARD_MCP_PASSWORD

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

DASHBOARD_MCP_ALLOW_WRITES

false

start read-only until identities are bound

MCP_ALLOWED_HOSTS

mcp.yourcompany.com

enables DNS-rebinding protection

MCP_ALLOWED_ORIGINS

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

PORT

3001

HOST

127.0.0.1 — keeps the port off the public interface; the reverse proxy reaches it locally

MCP_RATE_LIMIT

120 requests per key per window

MCP_RATE_LIMIT_WINDOW_MS

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 mcp-server directory

Application startup file

server.js

Application mode

production

Environment variables

the tables above, in the Node.js panel

Before starting

npm install, then npm run build

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

list_datasources

read

Data source ids, labels, kinds (http/excel) and targets

describe_datasource

read

One source's shape: params, response path, columns — never credentials

preview_datasource

read

Live rows from a source; lists candidate response paths when one fails

list_datasets

read

Dataset ids, labels and scopes

describe_dataset

read

Exact field names, inferred types, one sample value each

sample_dataset

read

A capped sample of real rows

get_dataset

read

Full dataset definition: source, materialisation, workflow summary or raw envelope

validate_dataset_workflow

read

Run a workflow draft against real data without saving; per-step diagnostics

list_dashboards

read

Dashboard ids, labels and scopes

get_dashboard

read

Dashboard details plus one line per widget; one config on request

list_widget_kinds

read

The authorable widget kinds

describe_widget_kind

read

Config contract for one kind, plus a real example from your workspace

test_datasource

write

Dry-run an HTTP endpoint config; nothing saved, columns and paths inferred

create_datasource

write

Create an HTTP data source; the server probes the URL and seeds a default dataset

update_datasource

write

Change parts of an HTTP source; merges with the stored config

create_dataset

write

Create a dataset; the server builds the default workflow when none is given

update_dataset

write

Partial update: label, source, limits, materialisation, or replace the workflow

refresh_dataset_snapshot

write

Re-run a snapshot-materialised dataset, or report its last refresh

create_dashboard

write

Create a dashboard and attach its datasets

set_dashboard_datasets

write

Replace the dashboard's dataset list

add_widget

write

Add one widget, auto-placed on the grid

update_widget

write

Change title, dataset or config keys

delete_widget

write

Remove a widget

arrange_dashboard

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.ts mirrors src/features/dashboard/widgets/registry.ts — including each kind's defaultConfig — 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 a defaultConfig value 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 by describe_widget_kind is 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 ApiKey wired into src/lib/api-guard.ts in 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-drafts instead 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_datasource and preview_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_widget is the only destructive tool exposed.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables 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.
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables 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.
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables natural language querying and management of Apache Superset dashboard metadata, including user activity, data lineage, and change tracking.
    4
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI applications to interact with Tableau for querying data, exploring content, and retrieving views through natural language.
    4,263 npm
    Apache 2.0