# Set up Contrast and reach a first useful answer

Contrast is a data warehouse with a built-in data agent. Follow this guide when the user
asks you to set up Contrast, connect their sources, or get to their first answer. Complete the work
through the existing tools; ask the user only for missing information, sign-in,
access approval, or a decision you cannot safely infer.

## Start here

You can start without a Contrast account. Connect MCP first so the client's
authorization request carries the user through signup and back to the agent.
Do not stop at “Connect Contrast to continue”; follow the authentication steps below.

1. Use an already connected Contrast MCP server when its tools respond in the
   intended workspace. Do not reinstall it or ask the user to sign in again.
2. If your client supports remote MCP and OAuth, connect
   `https://contrastdb.com/api/mcp`. Let the client handle sign-in and scope approval.
   Use the client-specific instructions below; do not overwrite existing servers.
3. If the Contrast tools are missing, check the client's MCP status first
   (`codex mcp list` or `claude mcp list`). If an entry for
   `https://contrastdb.com/api/mcp` exists, follow Recovery below instead of
   installing a second connection. Only if no entry exists and you have a
   terminal, run:

   ```sh
   npx --yes @contrastlabs/cli@latest setup --json
   ```

   Setup waits up to 10 minutes for browser sign-in. If your command timeout is
   shorter, run it in the background: its first stderr line is JSON with
   `"event":"awaiting_sign_in"` and the `url` to give the user. If it stops with
   `nextAction.alternatives`, follow one of those instead of retrying.
   Reuse the saved workspace when it matches the user's request. If several
   workspaces are plausible, ask which one. Never guess another tenant or create
   a duplicate workspace. Rerun setup to resume after an interruption.
4. If neither MCP nor a terminal is available, explain how to enable a remote MCP
   connector in the current client. If the client cannot do that, direct the user
   to Claude Code, Codex, or Cursor with the same prompt. Opening this guide or
   sending the user back to the prompt page does not connect an agent.

The CLI's JSON result describes setup progress and follow-up work. A browser URL
is a handoff, not a completed connection. A timeout is unfinished work, not
success. Installing MCP may require the client to refresh its tools; do not claim
those tools are available until an authenticated call succeeds. Once setup is
query-ready, a terminal agent can continue in the same session without waiting
for MCP tools to reload:

```sh
npx --yes @contrastlabs/cli@latest ask "Find one useful fact from our data. Include query evidence, the period, and coverage limits." --workspace <workspace-id> --json
```

Replace the question with the user's original goal when provided. If the result
returns an unfinished run, inspect it:

```sh
npx --yes @contrastlabs/cli@latest investigate run <run-id> --workspace <workspace-id> --json
```

An accepted run is not an answer.

## Use a downloaded API key file

A workspace admin can download `contrast-api-key.txt` from **Sources** in
Contrast. It is a `.env`-compatible file with `CONTRAST_API_TOKEN`,
`CONTRAST_WORKSPACE_ID`, and `CONTRAST_API_BASE_URL`, plus the same three values as
`CONTRAST_*`, which the `contrast` CLI reads. The key has read, write, and admin access to
one workspace and expires after 30 days. Downloading a new file revokes the last one.
When the user says they downloaded it, use it instead of MCP sign-in:

1. Look in the user's Downloads folder. If the file is missing, ask where it is
   saved. If several match (such as `contrast-api-key (1).txt`), ask which to use.
2. Never print, `cat`, or summarize the file, and never ask the user to paste its
   values. Check names only, for example with `grep -c '^CONTRAST_API_TOKEN=' <file>`.
3. Move it into the working directory as `.env.contrast` with mode `600`, and add
   `.env.contrast` to `.gitignore`. Ask before replacing an existing
   `.env.contrast`, existing Contrast CLI configuration, or saved logins.
4. Load it only for Contrast commands, then verify the workspace:

   ```sh
   (set -a; . ./.env.contrast; set +a; npx --yes @contrastlabs/cli@latest status --json)
   ```

5. Run the other CLI commands the same way (`contrast` below stands for
   `npx --yes @contrastlabs/cli@latest`): `contrast source types`,
   `contrast source connect <kind>`,
   `contrast ingest plan <connection-id> --source-kind <kind>` (then `ingest start`
   with the same arguments), and `contrast query run`. Source credentials go through
   the browser handoff `source connect` returns. For Postgres and Supabase only,
   `--credential-env <NAME>` can read a connection string the user already set in
   their own environment. Never take credentials through chat.

If a command returns 401, the key was revoked or expired. Ask the user to choose
**Regenerate API key** on the Sources page and download a fresh file. A 403
with `sso_required` means the workspace only allows SSO sign-in; use the MCP
connection below instead of the key file.

## Create an account and authenticate MCP

1. Start the MCP client's authentication flow and give the user its exact browser
   authorization link. Keep the flow running while they complete it. Do not
   replace this link with a generic homepage or ask for their password in chat.
2. In the browser, an existing user signs in. A new user chooses **Create an
   account**, uses an available sign-in method, and completes email verification
   if requested. The user handles identity verification and provider consent.
3. If the account has no workspace, choose **Create workspace and continue**.
   Existing users select the intended workspace. Do not create another workspace
   when the user already has the right one.
4. Finish the requesting client's access approval. Signup and workspace creation
   return to that pending request. If account approval, SSO, or MFA is required,
   complete that requirement without starting another signup.
5. Back in the agent, call `warehouse_status` (on the six-tool endpoint, `warehouse`
   with `action: "status"`). Authentication is
   complete only when the call succeeds in the intended workspace. A warehouse
   that is not provisioned yet is expected; use `warehouse` plan/apply next.
6. If setup requires additional scope, use the client's authorization flow to
   approve it, then retry the blocked operation. Do not request another account
   or duplicate the source. If tools need reloading, use the supported client
   refresh or the authenticated local CLI continuation above.

Website login does not authorize the agent; finish MCP authorization even if the
user signed up separately. The client may also ask the user to approve individual
tool calls; never disable those protections or repeat signup. If the client cannot
authenticate or present approvals, name that limitation and use an interactive
session or the local CLI fallback, resuming from saved handles. Never collect tokens
or callback codes in chat. Expired links need a fresh authorization request; the
account and workspace already created remain.

## Choose the connection for the current client

Prefer existing working tools. Configure one connection, then make an authenticated
call to confirm the intended workspace. App sign-in, MCP authorization, and source
provider consent are separate steps; success in one does not prove the others.

- **Claude Code:** `claude mcp add --transport http --scope user contrast https://contrastdb.com/api/mcp`,
  then run `claude mcp login contrast` yourself to start OAuth. Keep it running
  while the user signs in through the browser it opens.
- **Codex:** `codex mcp add contrast --url https://contrastdb.com/api/mcp`,
  then `codex mcp login contrast`.
- **Cursor:** add an HTTP server with URL `https://contrastdb.com/api/mcp` in MCP
  settings, or merge an `contrast` entry with that `url` into `mcpServers` in
  `~/.cursor/mcp.json`. Use Authenticate in the client's MCP settings.
- **Claude with connector settings:** add the same remote MCP URL through the
  client's connector settings. Client plan or organization policy may require
  an administrator to enable custom connectors.
- **Local terminal fallback:** use `npx --yes @contrastlabs/cli@latest setup --client <claude|codex|cursor> --json`
  when the client is known. Replace the placeholder with its actual name. This
  reuses the CLI session for the installed local MCP bridge. A new tool list may
  require restarting the client, so continue with CLI commands in the meantime;
  when you need MCP-only tools such as BI, ask the user to restart the client or
  reconnect through `/mcp`, then continue in the same workspace.

A cloud sandbox, SSH host, container, or WSL terminal may not share the user's
browser or credential store, and browser sign-in must return to loopback on the
machine running the CLI or client (`--no-open` only prints the URL). Prefer hosted
MCP OAuth or run the CLI on the user's computer. Do not loop login attempts, copy
callback codes into chat, or weaken credential storage; name an unreachable
callback as the blocker and move sign-in to a supported local client.

Inspect installed tools before calling them and follow each tool's actual schema.
Contrast has two MCP endpoints over the same workspace and permissions:

| Endpoint | Tools | Use it when |
| --- | --- | --- |
| `https://contrastdb.com/api/mcp` | One tool per operation, such as `warehouse_status`, `sources_connect`, `ingestion_start`, `catalog_search`, `bi_create`, and `query_run`. Adds catalog context edits, ingestion column edits, and governed deletion. | Default for this guide. |
| `https://contrastdb.com/api/mcp/warehouse` | Six tools that take an `action`: `warehouse`, `sources`, `ingestion`, `catalog`, `bi`, and `query`. | The user already connected it from the **MCP** page in Contrast, or runs `contrast mcp serve` from `@contrastlabs/cli`. |

Don't add a second endpoint when one already works. A stale legacy connection
with different tools needs its configuration corrected, not invented tool names.
CLI JSON goes to stdout; browser handoffs and progress can appear on stderr. Keep
an interactive process alive while the user signs in, or use the remote connection.

## Plan the smallest set of user handoffs

Ask only for the source systems, the workspace if ambiguous, and missing access.
Use the user's question or known project context; skip questions already answered.
Batch missing information into one short request. Source consent still belongs to
the user, and credentials belong in the connection form or an authorized secret
store. Give the exact source settings link and permission name when credentials
are needed. Do not ask the user to find a token without explaining where.

List source capabilities first. Offer OAuth only when it is actually configured
and available; use the advertised credential form otherwise. Do not present a
retired or in-progress connector as generally supported. Typical handoffs:

| Source family | User handoff | Agent checks afterward |
| --- | --- | --- |
| OAuth SaaS and Supabase | Approve the intended account or project in the browser. | Correct tenant, required permissions, discovery, and current source status. |
| API-key SaaS | Enter the required key and account/project identifiers in the secure form. | Key permissions, region, selected project, and covered history. |
| Postgres | Supply a supported database connection through the form. | Reachability from ingestion infrastructure, TLS, database/schema permissions, and table eligibility. Local browser access does not prove server access. |
| Google Sheets | Approve Google access and select the intended file in Google Picker. | The chosen file is accessible and all supported tabs are discovered. Google consent alone does not select every file in Drive. |

“All tables” applies within the approved account, project, schema, or selected
file. It does not mean every account, every Google Drive file, or all historical
data. Inspect history windows and optional objects; report the actual coverage.
Do not enable paid modules, broaden grants, change replication settings, or expose
a private database merely to remove a setup blocker. Prepare the exact required
change for the user or administrator while continuing independent sources.

## Connect the sources

- Use the user's named sources. If none are named, ask one short question about
  which systems hold their data. Do not require an intake questionnaire.
- Inspect existing connections before creating new ones. Resume working
  connections and preserve saved selections and exclusions.
- For a new source, default to all accessible, supported tables and columns.
  Do not ask the user to pick tables or limit ingestion to the first question.
  A user's explicit narrower scope always wins. Keep automatic addition of new
  tables enabled when supported and within the approved source scope.
- Use the `sources` operations to list supported types, connect, and verify. Open the returned
  authorization URL for the selected source. Continue when authorization returns
  and the source's status verifies the connection. If a popup is blocked, present
  the same authorization link rather than starting another flow. Cancellation or
  a closed tab is pending access, not a failed password or a successful connection.
- Use the `warehouse` operations to inspect/apply the Contrast Warehouse setup,
  and `ingestion` to plan and start synchronization. Omit table overrides for the all-table
  default. Inspect the plan's coverage and blockers before starting.
- Keeping data synced is `ingestion_start` itself: batch syncs repeat on a schedule
  (the source's default, usually hourly; set `cadence_minutes` from 15 to 10080)
  and CDC sources stream continuously. `pipelines_*` tools only operate existing
  dbt pipelines; they do not create syncs.
- Warehouse setup and new source connections require admin approval. Remote OAuth
  clients should show the requested scope for approval. If you're using the CLI
  bridge and see `insufficient_scope` for `admin`, run
  `npx --yes @contrastlabs/cli@latest setup --workspace <workspace-id> --no-install --no-wait --json`
  and open the returned `warehouseSetupUrl`. Finish the approved setup there,
  then resume the same run. Use the intended workspace ID. Refreshing a read/write
  session won't add admin access; keep working on sources it already supports.
- Never silently discard tables. Explain inaccessible or unsupported tables and
  their exact recovery steps. An incomplete or truncated discovery is not proof
  that all tables were connected. Respect connector limits and source permissions;
  do not change source schemas, keys, replication, or access grants without the
  necessary authorization.
- Start approved sources as they become available. Let synchronization continue
  while another source is authorized; do not wait for a full backfill before
  doing independent setup work. After an uncertain start, inspect its operation
  before retrying so ingestion is not duplicated.

Do not ask the user to paste database, warehouse, or SaaS credentials into agent chat.
Use the secure browser handoff or a supported secret store. Never print tokens,
passwords, or connection strings in progress, commands, or your final response.

## Get to first value

1. Check `warehouse` and `ingestion` status. Distinguish connected, syncing,
   query-ready, and failed. Inspect each requested source; one ready source does
   not make every source ready.
   If synchronization is complete but query access is unverified, verify it with
   a small read-only query against a requested source table before proceeding.
2. As soon as sufficient data is query-ready, use `catalog` to find definitions
   and inspect actual tables and columns. Follow pagination when needed.
3. Answer the user's question. If none was supplied, choose one useful, bounded
   aggregate from the available data, such as a recent revenue trend or a change
   in activity. Ask for a metric definition only when ambiguity would materially
   change the answer; label any assumptions.
4. Run read-only SQL with `query`, wait for the query to succeed, and retrieve
   the results. Keep each statement's SQL and `query_id`. Report only facts
   supported by those results. An empty result is not evidence that the business
   has no activity; inspect source coverage and freshness before concluding.
5. Return one concise finding with its period, source/table references, the SQL
   and `query_id` behind each number, and material coverage or freshness limits.
   Keep raw records out of the summary. Continue tracking unfinished sources and
   clearly identify remaining blockers.
6. If the user asked for a dashboard, follow **Build a dashboard** below. For
   Slack, see **Optional Slack connection**. Otherwise answer in this conversation.

Setup readiness is not first value. Do not stop at an installed tool, an accepted
sync job, a sample answer, or a successful `SELECT 1`. Finish with a real answer
from the user's data, or the precise blocker and the next action needed.

## Build a dashboard

Dashboards need the MCP tools; no CLI command builds them. Names below are from
`https://contrastdb.com/api/mcp`; on the six-tool endpoint, call `bi` with the same
arguments plus `action`. If the BI tools aren't in this session yet (MCP was added
mid-session), finish the CLI steps, then ask the user to restart the client or
reconnect through `/mcp` and continue; the workspace and verified SQL carry over.
Writes need the write scope. Give each create a new `payload.uuid` and keep it:
repeating a create with that UUID returns the same object, so recover an uncertain
create that way. `params` and `position_json` are JSON-encoded strings, not objects.

1. Verify each metric's SQL with `query_run` first, using fully qualified
   `database.schema.table` names; for example, one row per `week` with `revenue`.
2. `bi_list` `{"resource": "database"}` and take the database `id`. If BI is
   unavailable or lists none, report that rather than building elsewhere.
3. For each metric, find or create a dataset. Dataset names are unique per BI
   database, so first `bi_list` `{"resource": "dataset", "search": "Weekly revenue"}`.
   Reuse an exact `table_name` match whose `sql` is the verified SQL; otherwise
   choose a name no dataset uses, such as `Weekly revenue 2`. To create
   one, `bi_create` it, then `bi_refresh` it to load columns:
   `{"resource": "dataset", "payload": {"uuid": "<new>", "database": <database id>, "table_name": "Weekly revenue", "sql": "<verified SQL>"}}`,
   then `{"resource": "dataset", "id": <dataset id>}`.
4. `bi_create` `{"resource": "dashboard", "payload": {"uuid": "<new>", "dashboard_title": "Revenue and active users", "published": true}}`.
5. For each metric, `bi_create` a chart with `resource: "chart"` and payload
   `uuid`, `slice_name`, `viz_type`, `datasource_id: <dataset id>`,
   `datasource_type: "table"`, `dashboards: [<dashboard id>]`, and `params`,
   where `M` is `{"expressionType": "SQL", "sqlExpression": "SUM(revenue)", "label": "Revenue"}`:

   ```text
   params: {"datasource": "<dataset id>__table", "viz_type": "echarts_timeseries_bar", "x_axis": "week",
            "groupby": [], "metrics": [M], "adhoc_filters": [], "time_range": "No filter", "row_limit": 1000}
   ```

   Use the same `viz_type` in the payload and in `params`. `echarts_timeseries_line`
   takes the same fields. For a single number use `big_number_total` with
   `"metric": M`; for a table use `table` with `"groupby": ["week"]`,
   `"metrics": [M]` and `"query_mode": "aggregate"`. Contrast saves each of these
   charts' `query_context` from `params`, matching what dashboard viewers request,
   so don't send one. Other chart types need a `query_context` that matches
   `params` exactly, or viewers' charts fail; prefer these four.
6. `bi_update` `{"resource": "dashboard", "id": <dashboard id>, "payload": {"position_json": "<string>"}}`,
   with one `CHART` per chart and row widths totalling at most 12:

   ```json
   {"DASHBOARD_VERSION_KEY": "v2",
    "ROOT_ID": {"id": "ROOT_ID", "type": "ROOT", "children": ["GRID_ID"]},
    "GRID_ID": {"id": "GRID_ID", "type": "GRID", "children": ["ROW-1"], "parents": ["ROOT_ID"]},
    "ROW-1": {"id": "ROW-1", "type": "ROW", "children": ["CHART-1", "CHART-2"], "parents": ["ROOT_ID", "GRID_ID"], "meta": {"background": "BACKGROUND_TRANSPARENT"}},
    "CHART-1": {"id": "CHART-1", "type": "CHART", "children": [], "parents": ["ROOT_ID", "GRID_ID", "ROW-1"], "meta": {"chartId": 11, "width": 6, "height": 50}},
    "CHART-2": {"id": "CHART-2", "type": "CHART", "children": [], "parents": ["ROOT_ID", "GRID_ID", "ROW-1"], "meta": {"chartId": 12, "width": 6, "height": 50}}}
   ```

7. Verify each chart with `bi_data` `{"resource": "chart", "id": <chart id>}`,
   which runs its saved query. `result[0]` must have no `error`, and
   `result[0].data` must match the `query_run` rows allowing for its format: rows
   are ordered by the first metric rather than by time, date and time values are
   epoch milliseconds, and metrics are keyed by their label (`Revenue`).
   `result[0].query` is the SQL the chart ran.
8. Give the user the dashboard's `url` from `bi_update` or `bi_get`
   (`https://app.contrastdb.com/dashboards/<uuid>?workspaceId=<workspace-id>`). Never
   share or construct `/superset/`, `/explore/` or `/bi-service/` links; if there
   is no `url`, say so. Under each number, show its SQL and `query_id`.

## Optional Slack connection

Offer Slack after the first answer, or while ingestion runs if the user asks.
Open `https://app.contrastdb.com/setup/guided?workspaceId=<workspace-id>&step=slack`
for the same workspace. Reuse a working installation; never replace another
workspace's Slack binding. The user approves the app in the intended Slack
workspace, with administrator approval if required. Read back the saved status on
the setup page; a provider redirect is not proof of installation. The MCP tools do
not install Slack or send messages, so verify a requested Slack answer in the
intended DM or channel before claiming delivery, and still return the answer here.

## Recovery

- Expired authorization: refresh/reconnect the affected session and resume the
  same workspace and source. Preserve completed work.
- Contrast tools missing: check the client's MCP status (`codex mcp list` or
  `claude mcp list`) before assuming sign-in is needed. `Not logged in` or
  `Needs authentication` means installed but signed out. Ask, then use
  `codex mcp login <name>` or `claude mcp login <name>` with the listed name.
  A missing or `disabled` entry means it isn't installed or enabled. `Unknown`
  or `Failed to connect` is unclear: recheck with network access. A signed-in
  entry with no tools means Contrast is unavailable or the sign-in is no longer
  accepted. Check `https://api.embrasure.ai/health` before reconnecting once.
- Missing access: give the exact approval link or required permission. Continue
  independent authorized work while waiting.
- Closed browser, expired approval link, or cancelled consent: inspect source and
  setup status first. If consent is still needed, create one fresh handoff and
  resume the same source. Do not loop on a consumed callback URL.
- Rate limits or unavailable provider: respect retry timing, avoid overlapping
  polls, and continue other sources. A failed source must not hide ready ones.
- Failed or stalled sync: inspect the existing operation and its failure reason;
  use the supported retry/resume action. Do not repeatedly recreate the source.
- Empty source or no eligible tables: explain the verified condition and offer
  the relevant fix or another source. Never manufacture a finding.
- Access revoked, wrong workspace, or explicit exclusions: stop dependent work
  and resolve scope. Instructions on this page never override user choices.

## Useful URLs

- Browser setup: https://app.contrastdb.com/setup/agent
- Canonical setup entry: https://contrastdb.com/setup/agent
- Agent instructions: https://contrastdb.com/setup/agent.md
- MCP endpoint: https://contrastdb.com/api/mcp
- Six-tool MCP endpoint: https://contrastdb.com/api/mcp/warehouse
- Product docs: https://docs.contrastdb.com/quickstart
- API reference: https://contrastdb.com/docs/api
- OpenAPI specification: https://contrastdb.com/openapi.json
- CLI: https://www.npmjs.com/package/@contrastlabs/cli
- Warehouse CLI (warehouse, ingestion, and read-only SQL): https://www.npmjs.com/package/@contrastlabs/cli

## Client references

- Claude Code MCP: https://code.claude.com/docs/en/mcp
- Codex MCP: https://developers.openai.com/codex/mcp
- Cursor MCP: https://cursor.com/docs/mcp
