Skip to content

Reference

Shared rules

Paths, slugs, metadata semantics, connection resolution, scope, rate limits and retention. The rules every Dashies MCP tool shares.

These rules are enforced in one place in the server and therefore apply identically across tools. The per-tool pages name the rules that bite them and link back here rather than restating them.

Paths and slugs

You pass <slug>/<filename>. The workspace is added server-side from your authorization, as the HOST; never include it in the path. A bare <slug> means <slug>/index.html.

RuleValueError text
Slug charset/^[a-z0-9-]+$/slug "<x>" must match [a-z0-9-]
Slug length64 charactersslug exceeds 64-char limit (got <n>)
Slug not emptyslug must not be empty
Filename charset/^[a-z0-9._-]+$/filename "<x>" must match [a-z0-9._-]
Path shapeexactly 1 or 2 segmentspath "<p>": must be <slug>/<filename> (exactly two segments)
Path not emptypath must not be empty
Version ida UUIDversion_id must be a valid uuid
Upload cap5242880 bytes (5 MiB)413 payload too large: ...
Spec cap5242880 bytesspec is <n> bytes, over the 5242880-byte limit ...

Slugs are unique per account, and aliases left behind by earlier renames share that same namespace. A slug that would be shadowed by the app itself is reserved and refused by name.

Dashboard URLs

The canonical form is a subdomain, not a path:

https://<workspace-slug>.dashies.ai/<slug>

Every success message the server emits uses that form. Renaming leaves the old slug as an alias that 301-redirects to the new canonical URL.

Dashboard metadata

name, tags and chart follow one rule set, shared by publish_dashboard, update_dashboard and the spec publish. The distinction that matters is omitted versus empty:

You passEffect
The field omitted, or nullPreserved. On republish the prior value survives.
name: "" or whitespaceRejected: name must be a non-empty string
chart: ""Rejected: chart must be a non-empty string
tags: []Clears all tags.
tags not an arrayRejected: tags must be an array of strings
A non-string tag entryRejected: tags[<i>] must be a string (got <typeof>), never silently filtered
visibility present at allRejected: visibility is no longer accepted: public dashboards have been removed, so every dashboard is visible to its owner or, for a workspace dashboard, to that workspace's members. Omit the argument.

There is no way to clear a name: omitting it preserves, and an empty string is refused. That asymmetry is deliberate, because a nameless dashboard has nothing to show in a list.

There is no visibility setting. A dashboard opens for the members of the workspace it lives in, and a signed-out visitor is redirected to sign in with a return path. Nothing is viewable anonymously, and none of this is plan-gated.

Connections

A connection names where a refreshable dashboard's SQL runs. Two kinds:

  • self, the built-in no-PII Dashies metrics view. Always available, needs no connection id, and is the default whenever connection is omitted.
  • A warehouse connection you may use, named by its UUID. Warehouse connections are created in the Dashies web app, never through MCP, and require a paid plan.

Resolution runs in two steps, each with its own refusals.

Shape, before anything is looked up:

ConditionError text
Not a stringconnection must be a string
Neither self nor a UUIDconnection "<x>" is not a valid connection; pass "self" (Dashies' own no-PII metrics) or a connection id from list_connections - a warehouse, or this workspace's uploaded-file source, which also needs an upload id saying which uploaded file to read. Warehouses are connected in the Dashies web app (Data sources).

Usability, once it is looked up:

ConditionError text
Lookup failed in transportCould not verify the connection: <err>
Unknown, foreign, wrong-scope or not a warehouseConnection "<id>" was not found among your connections. Connect and manage warehouses in the Dashies web app (Data sources), then pass an active connection's id (from list_connections) here - or use "self" (Dashies' own no-PII metrics) with no connection id.
Found but not activeConnection "<label or id>" is not active (status: <status>). Test it in the Dashies web app (Data sources) and try again once it is active.

One refusal covers four causes, on purpose

Unknown, someone else's, wrong-scope and not-a-warehouse all produce the same sentence. That is not vagueness: a distinguishable "exists but is not yours" would let anyone probe for the existence of a connection id.

A connection from one workspace is not enough for another's dashboard

A connection's scope is the immutable (owner, workspace_id) pair, fixed when it was created, with no move path in either direction. A dashboard bound to a connection created in a different workspace publishes fine and can then never refresh, and dry_run does not catch it. Add the warehouse from inside the workspace you are publishing into.

This warning used to be about a connection you owned personally. There is no such connection: data_connections forbids the shape at the table, so every connection belongs to a workspace and the mismatch that remains is between two of them.

Scope: which workspace a call acts on

Your authorization is locked to one workspace when you grant it, and that lock, not an argument, decides the default. Every dashboard lives in a workspace, so a grant that names none cannot act on a dashboard at all. The grant is in one of three states:

Grant stateBehaviour
Workspace-lockedThat workspace is used. A workspace argument must match its slug or the call is refused.
Locked to no workspaceEvery dashboard tool is refused, and so is check_readiness. Re-authorize and choose a workspace.
Legacy (no lock)A workspace argument is honoured; without one, every dashboard tool is refused.

The refusals, verbatim:

Dashboards outside a workspace are retired, and this connection is authorized for
your personal account, so it cannot publish or act on anything. Re-authorize the
connection and choose the workspace it should use.
Dashboards outside a workspace are retired, and this connection names no workspace
to publish into. Pass `workspace` with a workspace slug you belong to, or
re-authorize the connection and choose the workspace it should use.
This connection is authorized for the "<locked-slug>" workspace and cannot act on
"<arg>". Re-authorize the connection and choose that workspace to use it there.

check_readiness sends none of those and has a sentence of its own, because it takes no arguments at all. Telling it to pass workspace names a fix its caller cannot perform, and telling a legacy grant it is authorized for a personal account would be false, so one clause covers both grant states:

Dashboards outside a workspace are retired, and this connection is not authorized
for any workspace, so it cannot publish a dashboard anywhere. Re-authorize the
connection and choose the workspace it should use.

On the legacy path a workspace argument can additionally fail with workspace must be a non-empty slug string, workspace "<slug>" not found, or not a member of workspace "<slug>".

Every dashboard-scoped tool accepts a workspace target. trigger_refresh used to be the one exception; it no longer is.

Two tools take no workspace argument because they are connection-scoped rather than dashboard-scoped (introspect_schema and validate_cube_sql), and list_connections derives its scope purely from the grant.

Six tools report a dashboard they cannot find with one shared sentence, No dashboard named "<slug>" in that workspace.: get_refresh_status, get_source_config, get_dashboard_spec, set_refresh_schedule, verify_dashboard and derive_dashboard_spec. That is an enumeration rather than a rule, and the list is the whole claim: five other tools word it their own way, and each one's own page prints its sentence. list_dashboard_versions and restore_dashboard_version answer dashboard not found, get_dashboard answers 404 not found: "<slug>/<file>" under the <workspace> workspace, trigger_refresh answers No dashboard found with slug "<slug>" in this workspace., and update_dashboard answers dashboard not found for slug "<slug>". The lookup is membership-gated, and a dashboard that genuinely does not exist produces the same sentence as one you cannot see, which is deliberate: a distinguishable refusal would be an existence oracle keyed by slug.

A SECOND CLASS PASSES THE DATABASE'S OWN REFUSAL THROUGH RATHER THAN WORDING ONE, and that is the boundary of the list of five: it enumerates tools whose own text names a missing dashboard. delete_dashboard wraps the delete RPC's refusal, so a slug that names nothing reads delete rejected: dashboard not found for slug "<slug>", and update_dashboard's RENAME path wraps its own rename RPCs' refusal inside rename rejected: <msg>. Neither is a sixth wording: dashboard not found for slug "<slug>" is one sentence the database raises on several paths, and it is what update_dashboard's metadata edit already prints without a wrapper.

update_dashboard is therefore in BOTH lists, and that is the reason these classes are about TEXT rather than about tools. Its metadata edit builds the sentence itself, so it belongs among the five; its rename passes the database's through, so it belongs here. A tool can do both, which is why "which tools report a missing dashboard" has no single answer and "which text does a caller see" does.

This page used to document a divergence here

Five of those tools said in your account while set_refresh_schedule alone said in that workspace, and this page recorded that as a known quirk. They share one sentence now.

list_connections has a refusal of its own, because it lists connections rather than acting on a dashboard and so cannot honestly claim it is unable to publish:

Data sources belong to a workspace, and this MCP connection is not authorized for
any workspace, so there is no space whose data sources it can list. Re-authorize it
and choose the workspace it should use.

Rate limits

Four independent counters, each a fixed ceiling over a 60-second window:

SurfaceCeiling per minuteKeyed on
publish_dashboard, both modes60your user id
validate_cube_sql120your user id
trigger_refresh20your user id
Client registration (POST /register)30client IP

A throttled tool returns:

Rate limit exceeded for <tool name>: too many requests. Wait about 60 seconds and try again.

A throttled registration is not a tool error but an HTTP 429 carrying retry-after: 60 and the body {"error":"rate_limited","error_description":"too many client registrations, please retry shortly"}.

trigger_refresh is limited twice: this 20-per-minute budget, plus an authoritative budget on the refresh service itself, which produces a different sentence (Too many refresh triggers right now. Wait about a minute and try again.).

That second budget is keyed on the WORKSPACE, so everyone with a seat in it draws on one shared allowance. Your own budget still applies as well, so moving between workspaces does not buy you extra capacity.

Rate limits fail open

If the limiter itself is unavailable, requests are allowed rather than refused. That is the deliberate inverse of the authorization gates, which fail closed: a throttle exists to control cost, so its outage must not look like a permission error.

Version retention

RuleValue
Automatic snapshots kept per dashboard20 (oldest unnamed ones pruned)
Named versions kept per dashboard30, exempt from the autosave prune
Versions returned by one list call50, with no pagination

Naming a version with update_dashboard_version is what protects it from the prune. The read limit of 50 is exactly the two caps added together, which is why the list needs no cursor.

Authorization

OAuth 2.1 with PKCE and Dynamic Client Registration. There are no tokens to copy, paste or rotate. Your client discovers the metadata off a 401, registers itself, opens your browser, and you sign in with Google once.

The steps behind that, and where each can fail:

  1. A tool call returns 401 with a WWW-Authenticate header.
  2. The client fetches /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource.
  3. The client registers at POST /register. Unauthenticated, so IP-rate-limited at 30 per minute.
  4. The client opens GET /authorize. The request is stored for 30 minutes and your browser is sent to the Dashies consent screen.
  5. The consent screen shows the real client name, the exact redirect URI in use, and the requested scope, so a non-loopback redirect URI is visible to you before you approve.
  6. You sign in, optionally pick a workspace, and click Allow access.
  7. The server verifies the token, mints a session dedicated to this grant, and only then consumes the request. A transient failure leaves it retryable, so clicking Allow access again is safe.
  8. The client exchanges the code at POST /token and stores the access token in your OS keychain.

Clicking Cancel returns error=access_denied with error_description=The user declined the authorization request.

Token lifetime. The MCP access token lives at most 50 minutes and is always clamped to expire before the underlying session it carries. On expiry the client refreshes silently; each grant owns its own rotating chain, so two AI clients and your browser tab cannot invalidate each other. That is why a working connection does not re-authorize every hour.

If a tool returns an auth error, the client re-triggers the browser flow. If it does not, retrying the call prompts it.

Check it worked

Ask your AI tool for your list of connections. Three outcomes, all informative: a list means a live authorization and a paid plan; You have no warehouse connections. means a live authorization with none added yet; a browser window means the handshake is running and you are about to be authorized.