Skip to content

SQLDesk 0.6.0 — MCP

Choose a tag to compare

@bot-netizen bot-netizen released this 03 Oct 10:24
· 150 commits to main since this release

A tool server over your warehouse, a catalog built from the queries people have already written, and an audit of everything it was asked.

MCP

  • One endpoint, JSON-RPC over streamable HTTP, authenticated with the API key on your profile page.
  • No service account, and no way to configure one. Every call runs as that user and sees only the data sources that user can read — a tool server with more access than its user is a way to launder permissions.
  • Eight tools, in the order they are meant to be used. find_queries and find_dashboards to look for work that already exists; find_context, expand_table and list_data_sources to understand the data; check_sql for the shape and explain_query for the cost; then run_query.
  • run_query returns at most 1000 rows — the editor's own ceiling, reused rather than invented — and runs on a worker like any other query, so it appears in Admin under the name of whoever's key it was and can be cancelled there.
  • Every request leaves an audit row, including the refused ones. Never the question somebody put to their client, and never the SQL that came back.

MCP gives no more than the editor gives

  • run_query and explain_query check what the editor checks, paused sources included.
  • Reads only. One statement — SELECT, WITH, SHOW, DESCRIBE — refusing a write anywhere in it, a DELETE inside a WITH and SELECT ... INTO included. That stops a model doing damage by accident; the database account's grants are still what stop it on purpose, so give each data source a read-only user.
  • Bounded. One request waits at most ten seconds less than gunicorn's timeout, across all its calls. Batches hold at most ten messages, SQL at most 100,000 characters.

A catalog, built from what you already have

You have no data hub and most installs never will. But the warehouse states its own structure, several engines carry COMMENT ON text nobody was reading, and every dashboard is built on SQL somebody wrote and saved.

  • Harvesting reads all three: what exists, what it is called, which tables anyone uses, which columns anyone selects, which joins anyone writes.
  • It learns only from queries that have run in the last week — measured by when a query last ran rather than when it was last edited, because a dashboard refreshed every morning and untouched for a year is the most important thing in the warehouse.
  • On a schedule, and on demand. Admin → Catalog has a Harvest button per data source, and a Harvest new data sources that fills in what a source added this morning without re-reading every other one.

Measures are proposed, never assumed

  • SUM(amount) AS gross_revenue in a saved query is a person naming a metric, and the alias they chose becomes its name.
  • Admin → Catalog shows each proposal with how many distinct queries define it that way.
  • Agree, or Deny. It reaches a model only once somebody agrees it — and a proposal nobody can reject comes back every night until the list stops being read.
  • Aggregates across a join and arithmetic around an aggregate are refused, not guessed. A metric definition that is merely plausible is worse than none.

The semantic layer lives in git

  • manage ai export writes it as cube-shaped YAML, one file per table, into a directory you can commit from; manage ai import reads it back on deploy.
  • A Download button too, because the person writing the descriptions is often not the person with access to a container.
  • SQLDesk never speaks to git itself. Importing never creates anything the warehouse has not stated, nor lets a file redefine what a measure computes.

Core and add-ons

  • Core: server, worker, scheduler, Postgres, Redis.
  • Add-ons: MCP & Catalog, and Rendering. Off until asked for — compose profiles mcp and rendering, or mcp.enabled and rendering.enabled in the chart.
  • The published image is the same size either way. The renderer is its own image and runs as a non-root user.
  • A Helm chart for Kubernetes, with NodePort for minikube.

Also in this release

  • File uploads are confined to their own folder. The DuckDB behind uploads let SQL read any file the worker could — another organization's uploads, /etc/passwd — write over the application, and install extensions. Each upload source's SQL now reads its own uploads and nothing else, with the setting locked so SQL cannot switch it back.
  • Dashboard export is a one-page report. Past twelve widgets or one page tall, Export says so straight away. What fits exports in under a second.
  • A panel shows its chart's description, not its query's — as text under the title, rather than an "i" mark nobody on a phone or a wall screen could read.
  • A security review of everything reachable from outside: five passes over authentication, the REST API, the query runners, MCP and the browser. Most of what it found needed a signed-in account or a foothold on the cluster network; none of it was reachable anonymously from the internet. Details under 0.6.0-rc.4 in the changelog.
  • Twelve guide pages, with the in-app help links pointing into them.

Removed

  • The model-provider layer. SQLDesk calls no model; MCP serves tools to the client's. manage ai configure/status/test/disable/forget are gone with it.

Images

ghcr.io/bot-netizen/sqldesk:0.6.0
ghcr.io/bot-netizen/sqldesk-screenshots:0.6.0

linux/amd64 and linux/arm64. The screenshots image is the optional renderer for pictures in alert emails.

Upgrading from 0.5

  • Pull, then manage db upgrade.
  • Nothing here changes an existing dashboard, query or data source.
  • MCP and the catalog are off until you turn them on — mcp.enabled=true in the chart, or the mcp compose profile. With them off, /mcp answers 404 and the Catalog page is not there.

Verified before tagging

  • CI green on every job, including the end-to-end suite.
  • The published image smoke-tested through compose.prod.yaml: /ping and /setup answering, the scheduler registering its periodic jobs, and refresh_queries and cleanup_query_results both running clean on the worker.