stackdoctor
Claude finds that the worker shut down and 40 jobs are waiting, in one diagnose() call.
Strictly read-only. stackdoctor never writes, deletes, restarts, retries, revokes or sends anything. Postgres sessions are forced read-only by the server, Redis commands go through an allowlist, and Celery is only inspected. Secrets are redacted from every response.
stackdoctor is an MCP server that lets Claude, Cursor or any MCP client diagnose a Python backend stack (Postgres, Celery, Redis and logs) from a single tool call.
Ask "why are my jobs stuck?" and the assistant calls diagnose(symptom). It runs every relevant check
in parallel and returns one timestamped snapshot with:
- findings: blocked queries, missing workers, queue backlogs, log error spikes, Redis memory pressure, …
- a merged timeline of events from Postgres, Celery, Redis and your logs
- possible cause → effect chains when events line up in time, each with its evidence timestamps
possible cause (confidence: medium): Database lock → blocked queries → stuck or failing tasks
cause: 21:58:01 postgres.lock_held pid 7959 holds a lock blocking 2 sessions: LOCK TABLE orders …
effect: 21:58:02 logs.db_lock_wait [postgres] process 7962 still waiting for RowExclusiveLock …
effect: 21:58:21 celery.task_failed tasks.process_order[…] failed: LockNotAvailable: lock timeout
effect: 21:58:21 celery.task_long_running tasks.process_order[…] running 13.2s on worker1
effect: 21:58:34 celery.workers_saturated worker1: all 2 slots busy, 2 reserved
caveat: Inferred from timing only. Verify before acting; this is not a confirmed root cause.
Install
You need uv:
curl -LsSf https://astral.sh/uv/install.sh | sh # macOS / Linux
powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Windows
Then either run it straight from PyPI or install it once:
uvx stackdoctor # runs the latest release, nothing to install
uv tool install stackdoctor # or install it; the command is then `stackdoctor`
You don't normally run it by hand: your MCP client starts it (see Configure).
To check that it works, run uvx stackdoctor --help, which prints usage and the configuration
variables. To upgrade, use uvx stackdoctor@latest or uv tool upgrade stackdoctor.
macOS 12 (Monterey):
uvx stackdoctorfails withrealpath: command not found, because uv's launcher script needsrealpath, which only ships with macOS 13+. Useuvx --from stackdoctor python -m stackdoctorinstead. In client configs that means"args": ["--from", "stackdoctor", "python", "-m", "stackdoctor"]. Alternatively, useuv tool install stackdoctorand set the command to the full path of the installed tool,/Users/<username>/.local/bin/stackdoctor. Its launcher doesn't needrealpath.
Configure
Set environment variables in your MCP client config, or in a .env file. stackdoctor looks for .env in
the working directory, or uses the file named by STACKDOCTOR_ENV_FILE.
Any source you don't configure is skipped, so it's fine to start with only DATABASE_URL.
| Variable | Example | Used for |
|---|---|---|
DATABASE_URL |
postgresql://stackdoctor_ro:…@localhost:5432/app |
Postgres checks |
REDIS_URL |
redis://localhost:6379/0 |
Redis checks |
CELERY_BROKER_URL |
redis://localhost:6379/0 (RabbitMQ amqp://… is experimental) |
Celery inspect + queue lengths |
CELERY_RESULT_BACKEND |
redis://localhost:6379/1 |
failed tasks / task details |
CELERY_APP |
myproject.celery:app |
optional: use your app's queues/routes/config (run from your project dir) |
CELERY_QUEUES |
default,emails |
extra queue names to measure |
LOG_SOURCES |
./logs/worker.log,docker:api,docker:worker |
log files and/or docker containers |
LOG_SOURCES entries are file paths, docker:<container>, or a bare container name.
Only configured sources can be read.
Celery workers: point
LOG_SOURCESat the worker's stdout. Celery printsworker: Warm shutdown (MainProcess)straight to stdout, not through logging, so it never reaches a--logfile. Docker containers already capture stdout. For a file, redirect stdout to it (celery … worker >> worker.log 2>&1, or supervisor/systemd stdout capture) so stackdoctor can see when a worker stopped.
Thresholds and limits
| Variable | Default | Meaning |
|---|---|---|
QUEUE_THRESHOLD |
100 | messages waiting before a queue counts as a backlog |
EXPECTED_WORKERS |
0 (off) | report missing workers if fewer reply |
LONG_TASK_S / LONG_QUERY_S |
60 / 30 | when a task / query counts as long-running |
REDIS_MEM_WARN_PCT |
85 | % of maxmemory that counts as "near the limit" |
LOG_ERROR_SPIKE |
10 | problem lines in 5 minutes that count as a spike |
CHAIN_WINDOW_MIN |
30 | how far apart cause and effect may be |
CHECK_TIMEOUT_S |
8 | per-check timeout inside diagnose |
CELERY_INSPECT_TIMEOUT_S |
1.0 | how long to wait for worker replies |
MAX_OUTPUT_CHARS |
20000 | per-tool response cap (diagnose gets 2×) |
Claude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or
%APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"stackdoctor": {
"command": "uvx",
"args": ["stackdoctor"],
"env": {
"DATABASE_URL": "postgresql://stackdoctor_ro:password@localhost:5432/app",
"REDIS_URL": "redis://localhost:6379/0",
"CELERY_BROKER_URL": "redis://localhost:6379/0",
"CELERY_RESULT_BACKEND": "redis://localhost:6379/1",
"LOG_SOURCES": "docker:api,docker:worker"
}
}
}
}
GUI apps often don't see your shell PATH. If Claude Desktop can't find uvx, use the full path:
/Users/<username>/.local/bin/uvx on macOS, or C:\\Users\\<username>\\.local\\bin\\uvx.exe on Windows.
Claude Code
claude mcp add stackdoctor \
-e DATABASE_URL=postgresql://stackdoctor_ro:password@localhost:5432/app \
-e REDIS_URL=redis://localhost:6379/0 \
-e CELERY_BROKER_URL=redis://localhost:6379/0 \
-e LOG_SOURCES=docker:api,docker:worker \
-- uvx stackdoctor
Or, from a project directory with a .env file, simply claude mcp add stackdoctor -- uvx stackdoctor.
Cursor
.cursor/mcp.json in your project, or ~/.cursor/mcp.json globally. This uses the same
mcpServers format as Claude Desktop:
{
"mcpServers": {
"stackdoctor": {
"command": "uvx",
"args": ["stackdoctor"],
"env": { "STACKDOCTOR_ENV_FILE": "${workspaceFolder}/.env" }
}
}
}
VS Code (Copilot agent mode)
.vscode/mcp.json:
{
"servers": {
"stackdoctor": {
"type": "stdio",
"command": "uvx",
"args": ["stackdoctor"],
"env": { "STACKDOCTOR_ENV_FILE": "${workspaceFolder}/.env" }
}
}
}
Tools
| Tool | What it returns |
|---|---|
diagnose(symptom) |
Start here. Runs the relevant checks concurrently, each with its own timeout, and returns findings, a merged timeline and possible causes |
active_queries(min_duration_s) |
non-idle sessions, incl. idle in transaction |
blocking_locks() |
waiting sessions and the sessions blocking them |
slow_queries(limit) |
top statements from pg_stat_statements (explains how to enable it if missing) and seq-scan-heavy tables |
run_select(sql, limit) |
one validated SELECT / WITH / EXPLAIN with a row limit |
workers() |
live workers, their queues, concurrency, active/reserved tasks |
queue_lengths(queues) |
messages waiting per queue, read from the broker (Redis incl. priority queues; RabbitMQ experimental) |
failed_tasks(limit) |
recent failures from a Redis result backend, or why they aren't visible |
task_details(task_id) |
stored result/traceback, and whether a worker holds the task right now |
memory_and_clients() |
Redis memory vs maxmemory, evictions, clients, slowlog |
scan_keys(pattern, limit) |
key names via SCAN |
key_info(key) |
type, TTL, size, encoding, short redacted preview |
tail_logs(source, lines) |
last lines of a configured log source |
search_logs(pattern, since_minutes, source) |
regex search over recent log lines |
Postgres checks are intentionally light. For deep Postgres health (index advice, vacuum, bloat), run Postgres MCP Pro next to stackdoctor.
Safety
stackdoctor is built so that a confused or prompt-injected assistant still can't change your systems.
Postgres
- Every session is opened with
default_transaction_read_only=on,statement_timeout=5sandlock_timeout=2s, passed as connection options so they override your URL. stackdoctor also checks the setting and refuses to run if the session isn't read-only. run_selectparses SQL with sqlglot, not regexes. It accepts exactly oneSELECT/WITH … SELECT/EXPLAINstatement and rejects:- writable CTEs,
SELECT … INTOandFOR UPDATE/SHARE EXPLAIN ANALYZE, because it executes the query- functions with side effects, even inside a read-only transaction:
pg_terminate_backend,pg_cancel_backend,pg_reload_conf,pg_read_file,pg_read_binary_file,pg_ls_dir,lo_*,dblink*,set_config,pg_advisory_*,query_to_xml(which runs SQL from a string),pg_sleep, …
- writable CTEs,
- Results are always limited: queries are wrapped as
SELECT * FROM (<q>) AS sd_sub LIMIT n(default 100, max 1000).
Recommended: a dedicated read-only role. Defense in depth, so the database enforces the rules too:
CREATE ROLE stackdoctor_ro LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE app TO stackdoctor_ro;
GRANT USAGE ON SCHEMA public TO stackdoctor_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO stackdoctor_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO stackdoctor_ro;
GRANT pg_monitor TO stackdoctor_ro; -- see other sessions' queries and pg_stat_statements
ALTER ROLE stackdoctor_ro SET default_transaction_read_only = on;
Anything the role can SELECT, the assistant can read. If some tables hold data you don't want to
share with an AI, leave them out of the GRANT SELECT.
Redis. Every command is checked against an allowlist that is aware of subcommands (INFO, SCAN,
TYPE, PTTL, LLEN, GET, …). CLIENT LIST is allowed; CLIENT KILL / PAUSE are not. The same goes
for OBJECT, MEMORY and CONFIG (only CONFIG GET). KEYS is never used; listing always goes through SCAN.
Celery. Only inspect (ping, active, reserved, active_queues, stats, query_task) and
broker reads are used: LLEN on Redis, and passive queue_declare on RabbitMQ.
RabbitMQ support is experimental. Queue lengths via passive
queue_declareand theinspectcalls are implemented but not yet covered by CI. The Redis broker is the tested path. Reports welcome. stackdoctor never revokes, retries, shuts down or sends tasks. To be precise:inspectworks by publishing a broadcast message on Celery's control channel (celery.pidbox) and reading replies from a temporary reply queue. That is how Celery's owncelery inspectcommand works. It doesn't touch your task queues or results, but it isn't zero traffic. Everything else stackdoctor does is pure reads.
Redaction. Every response is redacted before it leaves the server. This covers:
- passwords in URLs (
postgres://user:***@host) Authorizationheaders and bearer tokensPASSWORD=,SECRET_KEY=,api_key:and other secret-looking keys- AWS, GitHub, Slack, Stripe, OpenAI/Anthropic-style and Google keys, JWTs, and private keys
Task args/kwargs and Redis value previews are truncated and redacted too. Redaction is pattern-based, so treat it as a safety net, not a guarantee.
Output caps. Every tool caps its response (MAX_OUTPUT_CHARS) by trimming lists and long strings,
so a huge table or log can't flood the assistant's context.
Logs. Only sources listed in LOG_SOURCES can be read, so the assistant can't ask for /etc/passwd.
Docker logs are read with docker logs, called without a shell.
Try the demo
The demo is a small FastAPI + Celery + Redis + Postgres app with scripts that break it in realistic ways.
CI runs every scenario on each push (demo.yml): it breaks the stack,
calls diagnose() through a real MCP stdio client and checks the findings and chains.
To try it without Docker (local Postgres, Redis and worker), see DEMO.md.
cd demo
docker compose up -d --build # first run builds the app and loads 1M rows (~1 min)
cp .env.example .env # stackdoctor config for the demo (uses the read-only role)
Point your MCP client at the demo by setting STACKDOCTOR_ENV_FILE to the absolute path of demo/.env.
Then break something and ask:
| Script (macOS/Linux · Windows) | What it does | Ask |
|---|---|---|
./break_worker.sh · .\break_worker.ps1 |
stops the Celery worker and enqueues 50 tasks | "why are my jobs stuck?" |
./lock_table.sh · .\lock_table.ps1 |
holds an ACCESS EXCLUSIVE lock on orders for 3 min, then enqueues tasks that block on it |
"why are my jobs stuck?" |
./slow_query.sh · .\slow_query.ps1 |
runs three ~1-minute sequential scans | "why is the API slow?" |
./reset.sh · .\reset.ps1 |
restarts the worker and ends the demo's locks and slow queries |
Colima (macOS). docker compose works unchanged once Colima is running:
colima start --cpu 2 --memory 4. On macOS 13+ you can add --vm-type vz. On macOS 12 Colima uses
QEMU, which must be installed first (brew install qemu). Windows: use Docker Desktop or Docker
in WSL2 and run the .ps1 scripts from PowerShell.
Why stackdoctor instead of separate Postgres, Celery and log MCPs?
| Separate MCP servers | stackdoctor | |
|---|---|---|
| Install | one server per system, each with its own config | one uvx stackdoctor |
| Celery | usually needs Flower running | inspect API + broker, no Flower |
| Answering "why is it stuck?" | the AI calls 5–10 tools one after another, each a different moment in time | one diagnose() call: all checks run in parallel and share one timestamp |
| Correlation | left to the AI, across separate outputs | merged timeline + timing-based cause → effect hypotheses, with evidence |
| Safety | varies per server | one read-only policy for every system, redaction and output caps everywhere |
| Postgres depth | Postgres MCP Pro is deeper | intentionally light; use both |
Development
git clone https://github.com/lepri89/stackdoctor && cd stackdoctor
uv sync
uv run stackdoctor --help # run from the checkout
uv run pytest # unit tests (safety layer, diagnose, logs)
STACKDOCTOR_TEST_DATABASE_URL=postgresql://shop:shop@localhost:55432/shop \
STACKDOCTOR_TEST_REDIS_URL=redis://localhost:56379/0 \
uv run pytest # plus live tests against the demo
Releasing
Releases go to PyPI from GitHub Actions with trusted publishing, so no API token is stored anywhere.
- One-time: on PyPI, add a pending publisher (Account → Publishing) with project
stackdoctor, ownerlepri89, repositorystackdoctor, workflowrelease.ymland environmentpypi. In GitHub, create an environment namedpypi(Settings → Environments); adding yourself as a required reviewer is a good idea. - Bump
versioninpyproject.toml, commit, then tag and push:git tag v0.1.0 && git push origin v0.1.0. The workflow checks that the tag matches the version, runs the tests, builds, and publishes.
License
MIT
Metadata
Release files for stackdoctor 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| stackdoctor-0.1.1.tar.gz | 480.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| stackdoctor-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 517.2 kB
Release files / stackdoctor-0.1.1.tar.gz
| Download URL | stackdoctor-0.1.1.tar.gz |
|---|---|
| Size | 480.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fe54474056887fbe0cab7e54d02211e0400cf95312caaefd5bbf318499f8ee8d
|
|
BLAKE2b-256 checksum How to use checksums |
978a056c963e06282ba43882d95f3be24c55a84c5d9844dd49daa0176c4cee47
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / stackdoctor-0.1.1-py3-none-any.whl
| Download URL | stackdoctor-0.1.1-py3-none-any.whl |
|---|---|
| Size | 37.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3dcfeb771241b496e3aaebcd31ee14a38273263c1c7f8384ea5b3c12d05f45e5
|
|
BLAKE2b-256 checksum How to use checksums |
4b77c047abd17ab3c6ebac5ae59326a6a33492d3ab394cf730b6b03e3aeae386
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log