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
claudeCLI) rather than an in-app chatbot. You can run it yourself today (Dockerfile.mcp, or themcpservice ininfra/docker-compose.yml). A hosted public endpoint on Measured Cloud's fleet (thedq-cloud-mcpservice) 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 — seeDQ_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.
| Resource | Tools |
|---|---|
| Identity | mc_whoami — who the bearer key resolves to (user, org, workspace scope, role). |
| Datasources | mc_datasource_list, mc_datasource_get, mc_datasource_create, mc_datasource_test (connection check), mc_datasource_discover_assets (enumerate tables/views). |
| Assets | mc_asset_list, mc_asset_get, mc_asset_create. |
| Profiling | mc_profile_run (kick off a profile run), mc_profile_get (fetch its result). |
| Rules | mc_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 suggestions | mc_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. |
| Validations | mc_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 runs | mc_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. |
| Results | mc_result_list, mc_result_get. |
| Anomalies | mc_anomaly_list, mc_anomaly_acknowledge. |
| Suggestions | mc_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 rules | mc_alert_rule_list, mc_alert_rule_create. |
| Schedules | mc_schedule_list, mc_schedule_create. |
| Reference | mc_rule_type_gallery — the catalogue of available rule types, so the model can pick a valid rule_type when it authors a rule. |
| Docs | mc_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:
- The MCP client sends
Authorization: Bearer dqk_…to the MCP server's/mcpendpoint. - 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). - 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 + path | Purpose |
|---|---|
POST /mcp | The MCP Streamable-HTTP endpoint. Clients open their session and send all JSON-RPC traffic here. Requires the Authorization: Bearer dqk_… header. |
GET /health | Unauthenticated 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 StarletteTestClient:GET /healthreturns200 {"status": "ok"}unauthenticated;POST /mcpwith no bearer, a non-dqk_bearer, or adqa_agent token is rejected with401before 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 silently404s 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 thesuggestion_id+ thepoll_with/accept_withpointer, 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_URLis advertised as the protectedresource_server_url, andDQ_MCP_DQ_API_URLdoubles as the OAuthissuer_url(the API is the authorization server that mintsdqk_keys). Until that flow ships, mint adqk_…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
- Connect an MCP client — the operator/user how-to.
- Ask your own LLM to suggest rules via MCP — using
mc_rule_suggestion_contextto have your model propose rules. - Ask your own LLM about Measured Cloud via MCP — using
mc_search_docsto answer "how do I…" questions from the docs. - Create an API token — minting the
dqk_…bearer. - Configuration reference → MCP server — the
DQ_MCP_*env vars. - Multi-tenancy — how org + workspace isolation (enforced upstream) keeps one key inside its lane.