---
name: query-doctor
description: Use when setting up Query Doctor for a repository, or when working with a repository that has it. Covers connecting the MCP server, creating a project, wiring CI, and reading what CI found. Query Doctor costs SQL against a real Postgres planner, so it answers "is this query slow" and "will this index help" with plans rather than guesses.
---

# Query Doctor, from nothing to a first CI run

Query Doctor runs your repository's queries through a real Postgres planner on every pull request, and tells you when one gets more expensive. This guide takes a repository from unconfigured to a run you can read.

You do most of it. The user opens a browser once to sign in, and runs one command to store a secret. Everything else is a tool call.

## 1. Register the server

```bash
claude mcp add --transport http query-doctor https://api.querydoctor.com/mcp
```

Any MCP client with HTTP transport works. For a client configured by JSON:

```json
{
  "mcpServers": {
    "query-doctor": { "type": "http", "url": "https://api.querydoctor.com/mcp" }
  }
}
```

Restart the client afterwards, then confirm with `claude mcp get query-doctor`.

## 2. Sign in

The first tool call that touches the user's projects opens their browser. They sign in there, or create an account if they have none, and approve the permissions.

Ask for **provisioning** among them. Creating a project returns a CI token, so it needs a permission the general one doesn't imply. Without it you can still read, but you cannot set anything up.

If a tool answers `403` with `insufficient_scope`, the connection lacks a permission. Tell the user to reconnect and grant it; you cannot fix this from inside the session.

## 3. Ask where things stand

Call `doctor` with the repository as `owner/repo`, read from the git origin remote. The server has no working directory and cannot derive it.

```
doctor({ repo: "acme/store" })
```

It returns the linked project, its database, CI state, the latest run, and an `attention` list. Each item that a tool can fix carries a `suggestedCall` — the tool and the arguments. Follow it rather than asking the user what to do.

Two answers mean the setup hasn't started:

- `sign-in` — nobody is authenticated. Go back to step 2.
- `no-connection` — the repo has no project. Continue below.

## 4. Create the project

```
create_project({ name: "store", repo: "acme/store" })
```

This creates the project, links the repo, and returns the project's CI token in one call.

The token authorizes writing CI runs for the project. Keep it out of your reply, out of the repo, and out of any file you write. Step 6 is the only place it is used, and it goes there by substitution, not by being shown.

If the response carries `incomplete`, part of the run didn't finish. Read it and do what it says. The project exists either way — never create a second one to recover.

## 5. Build the workflow

Before calling anything, look in `.github/workflows` for a file that already runs `query-doctor/analyzer`. If one exists, pass its path so no duplicate gets written.

**Detection is Node-only today.** On any other stack, skip to the note below the call.

Read the `package.json` nearest the code that talks to the database, and send its contents:

```
setup_ci({
  repo: "acme/store",
  packageJson: "<contents of package.json>",
  hasLockfile: true,
  hasDrizzleConfig: true,
  dbUrlEnvVar: "POSTGRES_URL",           // DATABASE_URL on most non-Node stacks
  workingDirectory: "apps/api",       // omit at the repo root
  existingAnalyzerWorkflow: undefined // the path, if you found one
})
```

The server has no filesystem, so it reads nothing itself. What you send is what it can detect. Send nothing and the migrate and test commands come back as `exit 1 # TODO`, which fails the CI job on purpose rather than passing on a guess.

**On Python, Ruby, Go, or anything else**, the tool has no input that fits your project, so it returns those TODOs and adds no runtime setup. That is a gap in Query Doctor, not a problem with the repository. Take the workflow it returns and finish it yourself: add the language's setup action, the dependency install, and the real migrate and test commands. The Postgres service, the extension, the analyzer step and the secret are correct for any stack. Pass `dbUrlEnvVar` so the rest is too: the default is `POSTGRES_URL`, a Node convention, and Django, Rails and most Python ORMs read `DATABASE_URL`. Get it wrong and the migrate step cannot connect.

That names the variable, not its format. The value is always a `postgres://` URL, which Python, Ruby and Go accept as-is. Java and .NET do not — Spring wants a JDBC URL, .NET an ADO.NET connection string — so on those stacks rewrite the value yourself and tell the user you did. Tell the user which parts you filled in.

Write each entry in `files` to disk. Then read `resolved`, where every field carries a `source`:

- `detected` — found in what you sent. It means the script exists, not that it runs. A `db:migrate` script calling a tool the project doesn't depend on is still `detected`, and the CI job will fail on it.
- `provided` — your own argument, echoed back. Nothing was read and nothing was checked. `dbUrlEnvVar` comes back this way when you pass it, so it is right only if you read the project's database config before sending it.
- `placeholder` — could not be worked out. Renders as `exit 1 # TODO`. Name each one to the user before they push.
- `default` — a Query Doctor fallback the user may want to change. Check `installCmd`, which falls back to `npm install` when you did not say whether a lockfile exists.
- `canonical` — fixed by Query Doctor and not yours to change, like the secret name. `apiEndpoint` is canonical too: it is the address of the server that answered you, so it is already correct for a self-hosted instance and you do not need to second-guess it.

The workflow installs dependencies before it migrates, so send `hasLockfile` too — `npm ci` needs a lockfile and fails without one, so the answer decides between `npm ci` and `npm install`.

Then read the workflow before the user commits it. It runs their project's own commands, and it can only know what you sent.

An `already_configured` action means a workflow is already wired. Write nothing.

## 6. The one command the user runs

Take `secretSetup.command`, substitute the token from step 4, and run it if `gh` is authenticated for that repository. If it is not, hand the command to the user with the token in place — that is the one time the token is shown, and it goes to the person who owns it.

Then commit the workflow and push, or ask the user to. The first push to the default branch establishes the baseline that later runs compare against.

## 7. Check that CI actually ran

Pushing is not finishing. Watch the run and read the failure if there is one:

```bash
gh run watch
gh run view --log-failed
```

A failed job means no run reached Query Doctor, and the read tools below will be empty. That is a build problem in the repo, not a Query Doctor problem — the workflow runs the project's own migrate and test commands, and it can only report on queries those commands actually execute.

Do not tell the user setup is done until a run has landed.

## 8. Read the run

Once CI has run:

- `get_latest_ci_run({ repo })` — the most recent run for a branch.
- `review_ci_run({ runId })` — the flagged queries with their index recommendations, ranked. Start here when something is wrong.
- `find_regressions({ branch })` — what got more expensive against the comparison branch.
- `get_ci_query({ runId, hash })` — one query's full plan.

`get_ci_run` returns every query and can overflow your output limit on a large repository. Prefer `review_ci_run` when you want the problems.

If `doctor` reports `baseline-unset`, follow its suggested call. Without a comparison branch there is nothing to diff against, and runs look clean because nothing is being compared.

`doctor` returning an empty `attention` list does not mean the setup works. It reports on what has been recorded, so a project with no runs has little to report. Read the fields: `ci.configured` false and `latestRun` null with a workflow already pushed means CI has not landed a run yet, and step 7 is where you find out why. When `ci.comparisonBranch` is null, call `set_comparison_branch` whether or not an attention item asked you to.

## 9. Triage what it found

`triage_query` records a decision about a flagged query: acknowledged, ignored, with a reason. Use it when the user has decided, not to quiet a gate you disagree with.

Acknowledging does not re-run a failed check. CI has to run again.

## What is not available yet

`optimize_query` relays a query to an analyzer running against the user's own database. That path is being rebuilt and currently returns `needs_setup` for every project. Do not put it on the path to a first result, and do not report its absence as a problem with the user's setup.

## Tools

Read without signing in: `find_regressions`, `list_ci_runs`, `get_ci_run`, `get_ci_query`, `review_ci_run`, `get_latest_ci_run`, `get_query_history`, `get_query_status`, `get_repo_config`.

Signed in: `doctor`, `triage_query`, `set_comparison_branch`, `optimize_query`.

Provisioning permission: `create_project`, `setup_ci`.

Team permission: `list_teams`, `create_team`, `transfer_project`.

## Standing habit

Once a repository is wired, use the tools before you claim anything about a query's cost. When you add an index, change a migration, or rewrite a query and say it is faster, that claim should come from a plan Query Doctor returned. Say so plainly when no tool could run and you are reasoning by hand instead.
