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 PLACEHOLDER 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 PLACEHOLDER Cloud as a set of Model Context Protocol tools, so an MCP-capable LLM client (Claude Desktop, the claude CLI, Cursor, or anything that speaks MCP) can drive your data-quality workflow directly: list datasources, profile an asset, author a rule suite, run a checkpoint, triage anomalies — in natural language, without you wiring up the REST API by hand.
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 PLACEHOLDER Cloud REST API. The MCP server holds no database, no credentials, and no AI of its own.
The primitives philosophy
The MCP server ships primitives, not workflows. Each of its 31 tools maps to one REST operation — datasources_list, assets_create, checkpoints_run, and so on. 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.
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 datasources_list → assets_list → profiling_run → profiling_get → rule_suites_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.
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 31 tools, grouped by the resource they act on. Names match the function the client sees.
| Resource | Tools |
|---|---|
| Identity | whoami — who the bearer key resolves to (user, org, workspace scope, role). |
| Datasources | datasources_list, datasources_get, datasources_create, datasources_test (connection check), datasources_discover_assets (enumerate tables/views). |
| Assets | assets_list, assets_get, assets_create. |
| Profiling | profiling_run (kick off a profile run), profiling_get (fetch its result). |
| Rule suites | rule_suites_list, rule_suites_get, rule_suites_create, rule_suites_update. |
| Checkpoints | checkpoints_list, checkpoints_get, checkpoints_create, checkpoints_run (trigger a run), checkpoints_run_get (fetch a run's outcome). |
| Results | results_list, results_get. |
| Anomalies | anomalies_list, anomalies_acknowledge. |
| Suggestions | suggestions_list, suggestions_review (accept / reject an AI rule suggestion). |
| Alert rules | alert_rules_list, alert_rules_create. |
| Schedules | schedules_list, schedules_create. |
| Reference | expectation_gallery — the catalogue of available expectation types, so the model can pick a valid expectation_type when it authors a rule. |
Because these are primitives, a single user request usually fans out into several tool calls. "Set up nightly validation on public.orders" might become datasources_discover_assets → assets_create → expectation_gallery → rule_suites_create → checkpoints_create → schedules_create.
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 PLACEHOLDER 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 (owner / admin / editor / viewer) are applied exactly as if you'd called the API directly with that key. The MCP server adds no privileges and removes none — it is the same credential acting as you.
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 11 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, and schedule-create send their model bare.
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.
- 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.