Skip to main content

MCP server

Status: available (self-host); hosted endpoint rolling out. The MCP server is the first-class way to drive the platform with AI — bring your own MCP client (Claude, Cursor, the claude CLI) rather than an in-app chatbot. You can run it yourself today (Dockerfile.mcp, or the mcp service in infra/docker-compose.yml). A hosted public endpoint on Measured Cloud's fleet (the dq-cloud-mcp service) is being finalized behind a public load balancer — once live, its URL appears in Settings → Connect AI. Authentication is by a personal API key (dqk_…) carried as a bearer; OAuth-2.1 + Dynamic Client Registration is on the roadmap (see Roadmap below).

The MCP server exposes Measured Cloud as a set of Model Context Protocol tools, so your own AI assistant (Claude Desktop, the claude CLI, Cursor, or anything that speaks MCP) can list data sources, profile an asset, author rules, run a validation and triage anomalies when you ask it to, without you wiring up the REST API by hand. In the app this is Settings → AI → Use your own AI assistant; it is separate from MeasuredCloud AI, the AI features built into the app.

It is a separate service from the API, worker, and UI. It runs as its own process (uvicorn dq_cloud_mcp.server:app), speaks MCP over Streamable HTTP, and is a thin proxy: every tool call is forwarded to the Measured Cloud REST API. The MCP server holds no database and no credentials, and runs no AI in its own process — even the two AI-backed tools below only kick off a job that the worker (not the MCP server) runs.

The primitives philosophy​

The MCP server ships mostly primitives, not workflows. Most of its 53 tools map to one REST operation — mc_datasource_list, mc_asset_create, mc_validation_run, and so on. For those there is no server-side intelligence: the MCP server does not summarise, plan, retry, or decide. It validates inputs, forwards the call, and returns the JSON. (Two reads are self-contained rather than 1:1 REST proxies: mc_rule_suggestion_context composes a few existing reads into one bundle for the calling model to reason over, and mc_search_docs searches a small in-package index of the product docs — neither makes a decision of its own or calls an LLM; see below.)

For those primitives the intelligence lives in the calling LLM. When you ask Claude to "find the table with the most null emails and write a rule that flags it," Claude is the one that decomposes that into mc_datasource_list → mc_asset_list → mc_profile_run → mc_profile_get → mc_rule_create. The server just answers each primitive call faithfully. This keeps the server small, auditable, and model-agnostic: any LLM that can call tools can use it, and a smarter model automatically gets a better experience without a server change.

Two AI-backed tools (opt-in, server-side AI in the worker)​

Two tools deliberately break the primitives rule: mc_ai_profile_summary (a narrative data-quality summary of an asset's profile) and mc_ai_suggest_rules (concrete, validated rule proposals for an asset). These are the MCP face of Measured Cloud's native-AI capability. They are opt-in — you call them by name (or ask the model to), and each call spends the org's LLM budget.

Crucially, the AI still does not run inside the MCP process. Each of these tools forwards your bearer to a REST endpoint that writes a pending suggestion and enqueues a worker job; the worker resolves the org's engine — native (Gemini) or the org's own bring-your-own-LLM config — and does the reasoning. The tool returns immediately with a suggestion_id and status: "pending". You then poll mc_suggestion_get until status is completed (or failed). For a suggest-rules run you then accept the validated proposals as rules on the asset with mc_suggestion_accept. So the MCP server stays a thin, stateless, credential-free proxy; the "server-side AI" lives one hop away, in the worker.

Because they spend budget and reach an external LLM provider, both tools are marked not read-only, not idempotent, and open-world in their MCP annotations, and the underlying endpoints require the can_create_rule permission (editor/admin/owner) plus a per-org AI rate limit.

Practically, this means:

  • Composable. The model chains primitives to accomplish whatever the user asked. We don't pre-bake "recommended" multi-step flows into the server.
  • No hidden state. Each tool call is stateless and idempotent where the underlying REST verb is. What the model sees is exactly what the API returned.
  • Bounded output. List tools page (limit / offset) so a single call can't flood the model's context — see DQ_MCP_DEFAULT_PAGE_LIMIT / DQ_MCP_MAX_PAGE_LIMIT.

Tool catalogue​

All 53 tools, grouped by the resource they act on. Every tool name is prefixed mc_ and matches the function the client sees.

ResourceTools
Identitymc_whoami — who the bearer key resolves to (user, org, workspace scope, role).
Datasourcesmc_datasource_list, mc_datasource_get, mc_datasource_create, mc_datasource_test (connection check), mc_datasource_discover_assets (enumerate tables/views).
Assetsmc_asset_list, mc_asset_get, mc_asset_create.
Profilingmc_profile_run (kick off a profile run), mc_profile_get (fetch its result).
Rulesmc_rule_list (the rules you can see; filter by data_asset_id or validation_id, cursor-paged), mc_rule_get (one rule, with the validation_ids that hold it), mc_rule_create (add one rule to an asset; it joins the asset's default validation plus any validation_ids), mc_rule_update (edit one rule in place), mc_rule_delete, mc_rule_run (run one rule now; returns an execution_run_id to poll).
Rule suites (deprecated)mc_rule_suite_list, mc_rule_suite_get, mc_rule_suite_create, mc_rule_suite_update — kept for one release. Rule suites were retired (see Rule suites (retired)): mc_rule_suite_list returns the rules grouped per asset, mc_rule_suite_get reads the API's read-only alias, mc_rule_suite_create adds the rules to the asset you name (it fails without a data_asset_id), and mc_rule_suite_update replaces the rule list of the asset the suite was attached to. Use the rule tools instead.
Rule suggestionsmc_rule_suggestion_context — a read-only bundle (asset + latest completed profile + existing rules + rule-type gallery) so your model can propose grounded rules for an asset. (This is the "bring your own model" path; for server-side AI, see the AI-backed row below.) See Ask your own LLM to suggest rules via MCP.
AI-backed (server-side AI, opt-in)mc_ai_profile_summary — kick off an LLM narrative summary + findings for an asset's latest profile (read-only). mc_ai_suggest_rules — kick off an LLM run that authors validated rule proposals for an asset. Both spend LLM budget and run in the worker (native Gemini or the org's BYOLLM), not in the MCP process; each returns a suggestion_id you poll with mc_suggestion_get. Accept a suggest-rules run's proposals as rules on the asset with mc_suggestion_accept. mc_validation_generate — kick off an LLM draft of a whole validation from a description, with or without target assets (poll with mc_suggestion_get); mc_validation_draft_accept — create the validation from the reviewed draft in one atomic call. See Generate a validation with AI.
Validationsmc_validation_list, mc_validation_get, mc_validation_run (start an execution run; optional rule_ids), mc_validation_cancel_run (stop a run, or one asset of it).
Execution runsmc_execution_run_list (a validation's runs), mc_execution_run_get (poll one run until it finishes), mc_execution_run_progress (live per-asset state), mc_execution_run_results (per-asset and per-rule results).
Checkpoints (deprecated)mc_checkpoint_list, mc_checkpoint_get, mc_checkpoint_run, mc_checkpoint_create, mc_run_get — kept for one release. Checkpoints were retired (see Checkpoints (retired)): list / get return each checkpoint's mapped validation_id, mc_checkpoint_run runs that validation and returns an execution_run_id to poll with mc_execution_run_get, mc_checkpoint_create always fails, and mc_run_get only resolves runs queued before the retirement. Use the validation tools instead.
Resultsmc_result_list, mc_result_get.
Anomaliesmc_anomaly_list, mc_anomaly_acknowledge.
Suggestionsmc_suggestion_list, mc_suggestion_get (fetch one by id — the poll target for the AI-backed kickoffs), mc_suggestion_review (record accept / reject), mc_suggestion_accept (add a completed rule suggestion's validated rules to its data asset; optional data_asset_id and validation_ids).
Alert rulesmc_alert_rule_list, mc_alert_rule_create.
Schedulesmc_schedule_list, mc_schedule_create.
Referencemc_rule_type_gallery — the catalogue of available rule types, so the model can pick a valid rule_type when it authors a rule.
Docsmc_search_docs — a read-only search over the product documentation. Returns the most relevant doc sections (title + snippet + docs.measured.cloud URL) so your model can answer a "how do I…" question in plain language and cite the doc. No LLM, no network — it fronts the docs as a knowledge base instead of an in-app chatbot. See Ask your own LLM about Measured Cloud via MCP.

Because these are primitives, a single user request usually fans out into several tool calls. "Set up nightly validation on public.orders" might become mc_datasource_discover_assets → mc_asset_create → mc_rule_type_gallery → mc_rule_create → mc_schedule_create (on the asset's default validation, found with mc_validation_list).

Authentication​

The MCP server authenticates one way today: an opaque personal API key (dqk_…), sent as a bearer token on the HTTP request that opens the MCP session.

Authorization: Bearer dqk_…

What happens to that token:

  1. The MCP client sends Authorization: Bearer dqk_… to the MCP server's /mcp endpoint.
  2. The MCP server forwards the same bearer to the Measured Cloud REST API on every proxied call (the API root is configured by DQ_MCP_DQ_API_URL).
  3. The REST API enforces all authorization. Org membership, workspace scoping, and your role (super admin, technical admin, workspace admin, team admin, editor, viewer or billing) are applied exactly as if you'd called the API directly with that key. The MCP server adds no privileges and removes none: the assistant can do exactly what that key can, nothing more.

So a dqk_… key scoped to a single workspace exposes only that workspace's datasources, assets, and results through the tools; a viewer-role key can *_list / *_get but gets the API's 403 on *_create calls, surfaced back to the model as an error. The MCP server never sees your password, never mints tokens, and stores nothing — revoking the key (Settings → API keys) cuts off the MCP session at the next call, subject to the API's usual TOKEN_CACHE_TTL_SECONDS window.

Mint the key per Create an API token and point your client at the server per Connect an MCP client.

The endpoint​

The server speaks the MCP Streamable HTTP transport. There are exactly two routes:

Method + pathPurpose
POST /mcpThe MCP Streamable-HTTP endpoint. Clients open their session and send all JSON-RPC traffic here. Requires the Authorization: Bearer dqk_… header.
GET /healthUnauthenticated liveness probe. Returns 200 when the process is up. Used by the container HEALTHCHECK and Cloud Run startup probe.

Point an MCP client at https://<mcp-host>/mcp. The default listen port is 8080 (Dockerfile.mcp; Cloud Run injects $PORT).

Testing​

The server's behaviour is pinned by tests/mcp/test_server.py and tests/mcp/test_transport.py — no database, no network, no real API:

  • Transport + security boundary (test_transport.py) drives the real ASGI app (dq_cloud_mcp.server.app) through a Starlette TestClient: GET /health returns 200 {"status": "ok"} unauthenticated; POST /mcp with no bearer, a non-dqk_ bearer, or a dqa_ agent token is rejected with 401 before any tool runs, and the rejected token never appears in the response body.
  • Tool handlers (test_server.py) exercise every one of the 13 tool groups by patching the shared API client and asserting the exact HTTP method + path each tool builds (a wrong path silently 404s against the live API, so the path is the load-bearing assertion), the three pagination regimes (client-side limit, offset/limit, and the opaque cursor threaded through verbatim), and the request-body envelope per group — CRUD creates send {"data": …}; profile-run, suggestion-review, suggestion-accept, schedule-create, and the two AI-backed kickoffs (ai/profile-summary, ai/suggest-rules) send their model bare. The AI-backed tests also assert each kickoff returns the suggestion_id + the poll_with / accept_with pointer, and that its annotations mark it not read-only, not idempotent, open-world.

Run them with uv run pytest tests/mcp.

Roadmap​

  • OAuth 2.1 + Dynamic Client Registration (DCR). MCP's authorization spec is moving toward OAuth: a client discovers the authorization server from the MCP server's protected-resource metadata, registers itself dynamically, and runs an authorization-code + PKCE flow instead of pasting a long-lived bearer. The server already carries the wiring for this — DQ_MCP_MCP_PUBLIC_URL is advertised as the protected resource_server_url, and DQ_MCP_DQ_API_URL doubles as the OAuth issuer_url (the API is the authorization server that mints dqk_ keys). Until that flow ships, mint a dqk_… key and use it as a bearer.
  • Production hosting. The hosted instance will live at its own subdomain with public ingress (it must be reachable by external LLM clients, so it cannot sit behind the same Google IAP gate as the UI / API). Until then, self-host it next to your API.

See also​