sqlbuddy-nwi 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,705 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlbuddy-nwi
3
+ Version: 0.3.1
4
+ Summary: MCP server for read-only database access (MySQL, ClickHouse)
5
+ Author: ying.yuxiang
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yyx462/sql-buddy
8
+ Project-URL: Repository, https://github.com/yyx462/sql-buddy
9
+ Project-URL: Issues, https://github.com/yyx462/sql-buddy/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: mcp<2,>=1.0.0
24
+ Requires-Dist: pymysql>=1.1.0
25
+ Requires-Dist: clickhouse-driver>=0.2.6
26
+ Requires-Dist: pandas>=2.0.0
27
+ Requires-Dist: platformdirs>=4.0.0
28
+ Dynamic: license-file
29
+
30
+ # SQL Buddy
31
+
32
+ A read-only **MCP server** that lets your AI agent (Claude Code, and any
33
+ MCP-compatible client) safely query your **MySQL** and **ClickHouse** databases.
34
+
35
+ Your agent asks questions in plain language; SQL Buddy runs the SQL, enforces
36
+ **read-only** (no `INSERT`/`UPDATE`/`DROP` ever reaches your data), adds safety
37
+ `LIMIT`s, and caches results. You stay in control - the agent can look, never touch.
38
+
39
+ ---
40
+
41
+ ## How it works (30-second mental model)
42
+
43
+ ```
44
+ Your AI agent ──MCP──► SQL Buddy (runs on your laptop) ──► your databases
45
+ • opens its own SSH tunnel (behind a bastion)
46
+ • read-only guard
47
+ • auto LIMIT
48
+ • schema / table lookup
49
+ ```
50
+
51
+ - **SQL Buddy runs locally** on your machine (or a server you control). It is a small
52
+ Python program, not a cloud service.
53
+ - **It opens the SSH tunnel itself.** You give it the bastion, your key file, and a
54
+ per-database port mapping in `db.ini` - no separate `ssh -N` terminal to babysit.
55
+ Your database credentials never leave your machine.
56
+ - **It is read-only by design.** A guard layer rejects every write keyword before it
57
+ ever touches the network.
58
+
59
+ ---
60
+
61
+ ## Quickstart (60 seconds)
62
+
63
+ Already have [`uv`](https://docs.astral.sh/uv/) and SSH access? The whole install
64
+ is one command — `uv run` provisions an isolated `.venv` and installs deps for you,
65
+ so there is no separate `venv`/`pip install` step:
66
+
67
+ ```bash
68
+ git clone <this-repo-url> sql-buddy && cd sql-buddy
69
+ uv run server.py # installs deps, then starts the server (Ctrl+C to stop)
70
+ ```
71
+
72
+ Then configure your Connections and validate everything in one shot:
73
+
74
+ ```bash
75
+ uv run sql-buddy init # interactive wizard → writes connections/config/db.ini
76
+ uv run sql-buddy doctor # green-checks the venv, db.ini, and MCP client config
77
+ uv run sql-buddy mcp add # registers sql-buddy with Claude Code (writes .mcp.json)
78
+ # Windows + Trae IDE:
79
+ # uv run sql-buddy mcp add --client trae --scope user
80
+ ```
81
+
82
+ `uv run sql-buddy <command>` runs from anywhere inside the checkout (from
83
+ outside it, use `uv --directory /path/to/sql-buddy run sql-buddy …`, or call
84
+ the venv's `sql-buddy` directly). Subcommands: `version`, `doctor [--connect]`,
85
+ `mcp print|add`, `update-check`, `init`. **Bare `sql-buddy` (no command) runs the
86
+ MCP server** — that's what your AI client invokes, so MCP client configs hold no
87
+ fragile absolute path to `.venv/bin/python`.
88
+
89
+ On **Windows**, use PowerShell (or cmd) the same way — `uv` and `sql-buddy`
90
+ are `.exe` shims. Prefer [Trae](#windows--trae-ide) / Claude with
91
+ `mcp add --client …`. See [Windows & Trae IDE](#windows--trae-ide) below.
92
+
93
+ New here? Walk through the full [Setup](#setup) below.
94
+
95
+ ---
96
+
97
+ ## Prerequisites
98
+
99
+ You will need:
100
+
101
+ 1. **Python 3.11+** - `python3 --version`
102
+ 2. **[uv](https://docs.astral.sh/uv/)** (fast Python installer) - or plain `pip` works too.
103
+ 3. **SSH access** to a bastion/jump-host that can reach your databases:
104
+ - the bastion `user@host` and ssh `port` (ask your ops team — **don't assume 22**;
105
+ many orgs run sshd on a non-standard port like `22222`),
106
+ - a **private key file** on your machine (issued by your admin — key-based auth only),
107
+ - for each database, the DB `host:port` **as seen from the bastion** (private internal
108
+ IPs like `10.0.0.x`, not your laptop's view).
109
+ 4. **Read-only database credentials** - ask your DBA for a user that can `SELECT` but not write.
110
+ 5. An **MCP-compatible client** to connect it to (e.g. Claude Code, **Trae**).
111
+ 6. **Windows only (Auto tunnels):** [OpenSSH Client](https://learn.microsoft.com/windows-server/administration/openssh/openssh_install_firstuse)
112
+ — Settings → Apps → Optional features → OpenSSH Client. Skip if you use
113
+ **External** mode (`ssh -N` in another terminal).
114
+
115
+ ---
116
+
117
+ ## Setup
118
+
119
+ ### Step 1: Install SQL Buddy
120
+
121
+ ```bash
122
+ git clone <this-repo-url> sql-buddy
123
+ cd sql-buddy
124
+
125
+ # `uv run` creates an isolated .venv and installs dependencies for you.
126
+ # Smoke-test the server (Ctrl+C to stop):
127
+ uv run server.py
128
+ ```
129
+
130
+ If it prints no errors, the install worked. Stop it for now - we configure databases next.
131
+
132
+ This also gives you the `sql-buddy` command (run via `uv run sql-buddy …` or
133
+ `.venv/bin/sql-buddy …` once the env exists):
134
+
135
+ | Command | What it does |
136
+ |---------|--------------|
137
+ | `sql-buddy version` | Print the installed version. |
138
+ | `sql-buddy doctor` | Validate the install: venv, `db.ini`, and MCP client config. Add `--connect` to also ping each Connection (`SELECT 1`). |
139
+ | `sql-buddy init` | Interactive setup wizard (writes `db.ini`). Agents/CI: `--non-interactive --json-stdin` or `--json-file`. |
140
+ | `sql-buddy mcp print` | Print the MCP client config block (paths filled in). `--python` for a `sys.executable`-based config. `--client claude\|trae`. |
141
+ | `sql-buddy mcp add` | Write that block into a client config. `--client claude` (default) or `trae`; `--scope project\|user`. |
142
+ | `sql-buddy update-check` | Compare your checkout to the git remote; tells you when to `git pull`. |
143
+
144
+ > Prefer plain pip, or need the explicit venv?
145
+ > ```bash
146
+ > python3.11 -m venv .venv && source .venv/bin/activate && pip install -e .
147
+ > # or with uv: uv venv .venv --python 3.11 && uv pip install -e .
148
+ > .venv/bin/sql-buddy doctor # then use .venv/bin/sql-buddy / .venv/bin/python server.py
149
+ > # Windows: .venv\Scripts\sql-buddy.exe doctor
150
+ > ```
151
+
152
+ ---
153
+
154
+ ### Step 2: Reach your databases (SSH tunnel - managed by SQL Buddy)
155
+
156
+ Most databases are **not exposed to the public internet** - they sit behind a firewall
157
+ and are only reachable from inside the network. SQL Buddy reaches them by opening an
158
+ **SSH tunnel** itself: a secure pipe that maps a **port on your laptop** to the
159
+ **database port on the remote server**, through a **bastion** (a.k.a. jump-host) you
160
+ can SSH into.
161
+
162
+ > **What is a "bastion"?** A bastion is a single public-facing server your company
163
+ > exposes so you can SSH *into* the internal network. You log in to the bastion from
164
+ > the internet; the bastion then has line-of-sight to private internal hosts (your
165
+ > databases, internal services, etc.). Think of it as the only door into the walled
166
+ > garden - everything else is firewalled off.
167
+ >
168
+ > **Concrete example (shapes only — replace with your own values):**
169
+ >
170
+ > - bastion host: `192.0.2.10` (a public IP your ops team publishes)
171
+ > - bastion ssh port: `22222` ← **not 22!** Many orgs run sshd on a high, non-standard
172
+ > port to dodge drive-by scanners. Always confirm with ops; assuming 22 will hang.
173
+ > - ssh user: `zhang.san` (companies usually issue one in `first.last` form,
174
+ > tied to your identity)
175
+ > - private key: `~/.ssh/id_ed25519` (issued by your admin when they granted access;
176
+ > the *public* half is registered to your username on the bastion)
177
+ >
178
+ > Then, *inside* the network the bastion can see, your databases live at private IPs:
179
+ >
180
+ > | DB label | DBMS | Bastion's view (`remote_host:remote_port`) | Pick a `local_port` on your Mac |
181
+ > |------------|------------|---------------------------------------------|----------------------------------|
182
+ > | `ck` | ClickHouse | `10.0.0.21:9000` | `10021` |
183
+ > | `mysql` | MySQL | `10.0.0.11:3306` | `10011` |
184
+ >
185
+ > The **local_port** is any free port between 1024 and 65535 on *your* laptop - pick
186
+ > numbers you can remember and that nothing else on your machine is using. SQL Buddy
187
+ > binds `127.0.0.1:local_port` and the DB driver connects there - it never knows SSH
188
+ > is in the middle.
189
+
190
+ You do **not** run a separate `ssh -N` terminal. SQL Buddy starts the tunnel lazily on
191
+ the first query that needs it, keeps it alive for the life of the server, restarts it
192
+ if it drops, and tears it down when the server exits. One bastion -> one tunnel, even
193
+ with many databases behind it.
194
+
195
+ > **Manual smoke test first** (recommended): confirm your bastion + key + port work
196
+ > before writing `db.ini`. Equivalent to what SQL Buddy will run for you:
197
+ >
198
+ > ```bash
199
+ > ssh -N -o ExitOnForwardFailure=yes -o StrictHostKeyChecking=accept-new \
200
+ > -p 22222 -i ~/.ssh/id_ed25519 \
201
+ > -L 127.0.0.1:10021:10.0.0.21:9000 \
202
+ > zhang.san@192.0.2.10
203
+ > ```
204
+ >
205
+ > If this hangs at "Connecting to … port 22" *but you specified `-p 22222`* → sshd
206
+ > isn't reachable on that port from your network (firewall, wrong port, ops changed
207
+ > it). If it says `Permission denied (publickey)` → the key isn't accepted (wrong
208
+ > user, key not registered, key file too open — `chmod 600 ~/.ssh/id_ed25519`).
209
+ > If it works → Ctrl+C and let SQL Buddy manage it instead.
210
+
211
+ You describe the tunnel in `db.ini` (next step) with:
212
+
213
+ - an **`[ssh:<name>]`** section for the bastion (host, port, user, key file), and
214
+ - a **`tunnel = ssh:<name>`** line on each database connection, plus its
215
+ `remote_host` / `remote_port` (the DB as seen from the bastion) and `local_port`
216
+ (the port SQL Buddy binds on localhost).
217
+
218
+ The remote port must match the real database port (ClickHouse default **9000**,
219
+ MySQL default **3306**).
220
+
221
+ > **First connection only:** SQL Buddy uses `StrictHostKeyChecking=accept-new`, so the
222
+ > bastion's host key is auto-trusted on first run. If you prefer to vet it manually,
223
+ > `ssh you@bastion -p <port>` once first to add it to `~/.ssh/known_hosts`.
224
+
225
+ ---
226
+
227
+ ### Step 3: Add your credentials (`db.ini`)
228
+
229
+ Easiest path - run the interactive wizard. It asks whether sql-buddy should open
230
+ the SSH tunnel itself (**Auto**) or connect to a tunnel you already run elsewhere
231
+ (**External**, e.g. an `ssh -N` terminal on Windows), then writes `db.ini` for you:
232
+
233
+ ```bash
234
+ .venv/bin/python init.py
235
+ ```
236
+
237
+ Run it once when setting up, or again to switch modes - sql-buddy never prompts
238
+ during normal use (the choice persists in `db.ini`).
239
+
240
+ Or do it by hand - copy the template, then edit it:
241
+
242
+ ```bash
243
+ cp connections/config/db.ini.example connections/config/db.ini
244
+ $EDITOR connections/config/db.ini
245
+ ```
246
+
247
+ Each database is one **section** in this file. A section's name (the `[bracketed]` word)
248
+ becomes the **connection name** your agent uses (e.g. `ck`, `mysql`, `prod-read`).
249
+
250
+ ```ini
251
+ # db.ini - NEVER commit this file. It is gitignored for a reason.
252
+
253
+ # --- bastion ---
254
+ [ssh:prod]
255
+ host = bastion.example.com
256
+ port = 22
257
+ user = your_ssh_user
258
+ key = ~/.ssh/id_ed25519 # path to your private key (~ expanded)
259
+ server_alive_interval = 30 # optional keepalive (default 30)
260
+ server_alive_count_max = 3 # optional (default 3)
261
+
262
+ # --- a tunneled ClickHouse ---
263
+ [ck]
264
+ type = clickhouse
265
+ tunnel = ssh:prod # opt into the self-managed tunnel
266
+ remote_host = 10.0.0.21 # DB host AS SEEN FROM the bastion
267
+ remote_port = 9000 # DB port AS SEEN FROM the bastion
268
+ local_port = 10021 # localhost port == what the driver connects to
269
+ user = your_readonly_user
270
+ password = your_password
271
+ database = default
272
+
273
+ # --- a tunneled MySQL ---
274
+ [mysql]
275
+ type = mysql
276
+ tunnel = ssh:prod
277
+ remote_host = 10.0.0.95
278
+ remote_port = 3306
279
+ local_port = 10095
280
+ user = your_readonly_user
281
+ password = your_password
282
+ database = myapp
283
+
284
+ # --- a direct (non-tunneled) connection still works ---
285
+ [local-db]
286
+ type = mysql
287
+ host = localhost
288
+ port = 3306
289
+ user = your_readonly_user
290
+ password = your_password
291
+ database = myapp
292
+ ```
293
+
294
+ **Field reference:**
295
+
296
+ | Field | Where | Required | Notes |
297
+ |-------|-------|----------|-------|
298
+ | `type` | connection | yes | `clickhouse` or `mysql`. (If omitted, guessed from the section name: `ck*`->clickhouse, `db*`/`mysql*`->mysql. Explicit is safer.) |
299
+ | `tunnel` | connection | no | `ssh:<name>` to route through the matching `[ssh:<name>]` bastion. Omit for a direct connection. |
300
+ | `remote_host` | connection | if `tunnel` set | DB host as seen **from the bastion** (inside the network). |
301
+ | `remote_port` | connection | if `tunnel` set | DB port as seen from the bastion. |
302
+ | `local_port` | connection | if `tunnel` set | Port bound on localhost; the DB driver connects to `localhost:local_port`. |
303
+ | `host` | connection | direct only | Connect target for non-tunneled connections (almost always `localhost`). Ignored when `tunnel` is set. |
304
+ | `port` | connection | direct only | Connect target port for non-tunneled connections. Ignored when `tunnel` is set. |
305
+ | `user` | connection | yes | A read-only database user. Ask your DBA. |
306
+ | `password` | connection | yes | That user's password. |
307
+ | `database` | connection | ClickHouse | The default database/schema to query. |
308
+ | `host` | `[ssh:*]` | yes | The bastion/jump-host you SSH into. |
309
+ | `port` | `[ssh:*]` | no | SSH port (default 22). |
310
+ | `user` | `[ssh:*]` | yes | SSH login user on the bastion. |
311
+ | `key` | `[ssh:*]` | yes | Path to your private key file (`~` is expanded). Key-based auth only. |
312
+ | `server_alive_interval` | `[ssh:*]` | no | SSH keepalive ping seconds (default 30). |
313
+ | `server_alive_count_max` | `[ssh:*]` | no | Drop the tunnel after this many missed pings (default 3). |
314
+
315
+ > **Naming your connections:** both `my_db` and `my-db` work - underscores and dashes are
316
+ > treated interchangeably. So `connection="ck_amazon"` and `connection="ck-amazon"` are the same.
317
+
318
+ 🔒 **Security:** `db.ini` is listed in `.gitignore` and must never be committed. Double-check
319
+ with `git status` before pushing - it should never appear as a tracked file.
320
+
321
+ ---
322
+
323
+ ### Step 4: Test it works
324
+
325
+ One command validates the install and (optionally) pings each Connection:
326
+
327
+ ```bash
328
+ uv run sql-buddy doctor # offline: venv + db.ini shape + MCP client config
329
+ uv run sql-buddy doctor --connect # also opens each tunnel and runs SELECT 1
330
+ ```
331
+
332
+ `--connect` opens the SSH tunnel on first use (it can take a few seconds per
333
+ connection, and longer if a bastion is unreachable). If every line is `OK`, the
334
+ tunnel came up and you're ready to wire it into your AI agent.
335
+
336
+ ```bash
337
+ # (optional) Set which connection is used when the agent doesn't specify one
338
+ export SQL_BUDDY_DEFAULT_CONNECTION=ck
339
+ ```
340
+
341
+ ---
342
+
343
+ ### Step 5: Connect your AI agent (MCP config)
344
+
345
+ Easiest: let sql-buddy write the config for you.
346
+
347
+ ```bash
348
+ uv run sql-buddy mcp add # Claude Code project → ./.mcp.json
349
+ uv run sql-buddy mcp add --scope user # Claude Code user → ~/.claude.json
350
+ uv run sql-buddy mcp add --client trae # Trae project → ./.trae/mcp.json
351
+ uv run sql-buddy mcp add --client trae --scope user # Trae user-global
352
+ ```
353
+
354
+ Restart your client and you're done. Preview the exact block first with
355
+ `uv run sql-buddy mcp print` (add `--client trae` for Trae-shaped env/timeouts).
356
+ It produces this (paths filled in for you) — the **`uv --directory … run`** form
357
+ auto-provisions the venv and carries no absolute `.venv/bin/python` path to go
358
+ stale:
359
+
360
+ ```json
361
+ {
362
+ "mcpServers": {
363
+ "sql-buddy": {
364
+ "command": "uv",
365
+ "args": ["--directory", "/absolute/path/to/sql-buddy", "run", "server.py"]
366
+ }
367
+ }
368
+ }
369
+ ```
370
+
371
+ > No `uv` on the machine that runs your MCP client? Use the Python form instead:
372
+ > `sql-buddy mcp print --python` (or `mcp add --python`) — it sets `command` to the
373
+ > interpreter running the CLI (`sys.executable`), the most robust absolute path.
374
+
375
+ Ask your agent something like *"list the tables in the `ck` connection"* - if it
376
+ can answer, you're live. 🎉
377
+
378
+ ---
379
+
380
+ ## Windows & Trae IDE
381
+
382
+ Coworkers on **Windows + [Trae](https://www.trae.ai/)** (or Trae CN) should use
383
+ this path — Trae’s MCP host is pickier than a shell about PATH, spaces in the
384
+ `command` field, and cold-start timeouts.
385
+
386
+ ### One-time machine setup
387
+
388
+ 1. **Install [uv](https://docs.astral.sh/uv/)** (recommended) in a path **without
389
+ spaces** (default user install is fine):
390
+ ```powershell
391
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
392
+ ```
393
+ Close and reopen the terminal so `uv` is on PATH. Confirm: `uv --version`.
394
+ 2. **Python 3.11+** if you prefer `--python` mode (`py --version` / python.org).
395
+ 3. **OpenSSH Client** only if you want **Auto** tunnels:
396
+ Settings → Apps → Optional features → **OpenSSH Client**.
397
+ Many Windows users keep an `ssh -N …` window open instead (**External** mode).
398
+ 4. Clone and smoke-test:
399
+ ```powershell
400
+ git clone <this-repo-url> sql-buddy
401
+ cd sql-buddy
402
+ uv run server.py # Ctrl+C once it stays quiet
403
+ uv run sql-buddy init
404
+ uv run sql-buddy doctor
405
+ ```
406
+
407
+ ### Wire Trae
408
+
409
+ ```powershell
410
+ # User-global (available in every Trae window):
411
+ uv run sql-buddy mcp add --client trae --scope user
412
+
413
+ # Or project-only (writes .trae/mcp.json next to the workspace you have open):
414
+ uv run sql-buddy mcp add --client trae
415
+ # Then in Trae: Settings → MCP → enable "project-level MCP"
416
+ ```
417
+
418
+ What gets written (paths filled in; Trae also gets longer start/run timeouts and
419
+ UTF-8 env so JSON-RPC survives Chinese Windows code pages):
420
+
421
+ ```json
422
+ {
423
+ "mcpServers": {
424
+ "sql-buddy": {
425
+ "command": "C:\\Users\\you\\.local\\bin\\uv.exe",
426
+ "args": ["--directory", "D:\\src\\sql-buddy", "run", "server.py"],
427
+ "env": {
428
+ "PYTHONUTF8": "1",
429
+ "PYTHONIOENCODING": "utf-8",
430
+ "START_MCP_TIMEOUT_MS": "120000",
431
+ "RUN_MCP_TIMEOUT_MS": "120000"
432
+ }
433
+ }
434
+ }
435
+ }
436
+ ```
437
+
438
+ Fully restart Trae (not just reload window). Check **Settings → MCP** — sql-buddy
439
+ should show green. If it fails, open **Output → MCP Server Host**
440
+ (`Ctrl+Shift+U`) for the spawn log.
441
+
442
+ ### Windows gotchas we already handle
443
+
444
+ | Symptom | Fix / what sql-buddy does |
445
+ |--------|---------------------------|
446
+ | Trae can’t find `uv` | `mcp add` resolves `uv` to an **absolute** `.exe` path; falls back to `sys.executable` if `uv` is missing |
447
+ | `command` contains spaces (Trae rejects it) | Prefers the `py` launcher or refuses to write a broken Trae config |
448
+ | First start times out | Trae entries set `START_MCP_TIMEOUT_MS=120000` (venv provision is slow once) |
449
+ | Garbled errors / JSON parse fails on CN Windows | Sets `PYTHONUTF8=1` + `PYTHONIOENCODING=utf-8` |
450
+ | Flashing black console when a tunnel opens | SSH subprocess uses `CREATE_NO_WINDOW` |
451
+ | `ssh` missing in Auto mode | `doctor` tells you to install OpenSSH Client |
452
+ | Key path `~\.ssh\id_ed25519` | Expanded + normalized on write and at tunnel start |
453
+ | `db.ini` encoding (GBK editors) | Reader accepts UTF-8 BOM / UTF-8 / GBK / mbcs |
454
+
455
+ ### External tunnel on Windows (common)
456
+
457
+ If Auto SSH is awkward, run the tunnel yourself and point sql-buddy at localhost:
458
+
459
+ ```powershell
460
+ # keep this window open
461
+ ssh -N -L 127.0.0.1:10021:10.0.0.21:9000 -p 22222 user@bastion -i $HOME\.ssh\id_ed25519
462
+ ```
463
+
464
+ ```ini
465
+ # connections/config/db.ini (External — no tunnel= key)
466
+ [ck]
467
+ type = clickhouse
468
+ host = localhost
469
+ port = 10021
470
+ user = readonly
471
+ password = ...
472
+ database = default
473
+ ```
474
+
475
+ Then `uv run sql-buddy doctor --connect`.
476
+
477
+ ---
478
+
479
+ ## Updating
480
+
481
+ Check whether a newer version is available, then pull:
482
+
483
+ ```bash
484
+ uv run sql-buddy update-check # compares your checkout to the git remote
485
+ git pull # apply the update
486
+ uv run server.py # `uv run` re-syncs deps if they changed
487
+ ```
488
+
489
+ `sql-buddy version` prints what you're running. If you installed via a vendored
490
+ bundle instead of git, re-download the latest release to update. Your `db.ini` is
491
+ never touched by an update — it's gitignored and lives outside the tracked code.
492
+
493
+ ---
494
+
495
+ ## 👉 For AI agents: setting this up for a user
496
+
497
+ If you (the agent) are installing SQL Buddy on a user's machine, **do not guess** any
498
+ value below. Ask the user explicitly, one at a time, in plain language.
499
+
500
+ > **Drive the setup straight after `git clone`** — do not stall at "what next?". The
501
+ > whole point is for the user to be querying databases 5 minutes after cloning. Once
502
+ > step 1's `uv run server.py` returns, immediately proceed to steps 2-5 to collect
503
+ > values and write `db.ini`. The only blocker between you and a finished install is
504
+ > the user-supplied values in steps 2-4 — get to those questions now.
505
+
506
+ > Prefer the structured path to `db.ini`: collect the values below, then write
507
+ > them with **`uv run sql-buddy init --non-interactive --json-stdin`** (pipe the
508
+ > spec JSON on stdin; schema in `init.py`). It validates and writes `db.ini` for
509
+ > you — no hand-formatting INI. **Credentials must travel on stdin** (or a
510
+ > 0600 temp file you delete immediately via `--json-file /tmp/...` and `rm`).
511
+ > Never leave a password-bearing JSON file in a git checkout. You can also write
512
+ > `db.ini` directly with your file-editing tool if you prefer.
513
+
514
+ 1. **The repository** - clone it, then `uv run server.py` (Ctrl+C once it starts —
515
+ that one command created the venv and installed deps). As soon as it returns,
516
+ **move to step 2 — do not stop to ask "what next?"**.
517
+ 2. **Ask the user which mode** they want:
518
+ - **Auto** - sql-buddy opens the SSH tunnel itself. Continue to step 3.
519
+ - **External** - the user already runs a tunnel/connection (e.g. `ssh -N` on Windows).
520
+ Skip to step 4 (External branch).
521
+ 3. **Auto mode - SSH bastion** they reach their databases through. Many users have
522
+ never heard the word "bastion" — explain it in one line ("the public server your
523
+ company lets you SSH *into*, from which you can reach private internal hosts").
524
+ Collect:
525
+
526
+ | Field | What it is | Where the user finds it |
527
+ |--------------|-----------------------------------------------------------|------------------------------------------------|
528
+ | `host` | Public IP / hostname of the bastion | From their ops / infra team |
529
+ | `port` | SSH port the bastion listens on | From ops. **Do not assume 22** — many orgs run sshd on a high port like `22222`; assuming 22 will hang silently on `Connecting to host port 22` |
530
+ | `user` | SSH login user, usually `first.last` form (e.g. `zhang.san`) | The username IT registered for them |
531
+ | `key` | Absolute path to their **private** key file on this Mac | Issued by their admin when access was granted. The matching *public* key is registered on the bastion under their username, so the private key + username are tied together |
532
+ | keepalive | (optional) `server_alive_interval` / `server_alive_count_max` | Defaults 30s / 3 are fine — only set if ops says so |
533
+
534
+ Ask the user to **paste the equivalent ssh command they would run manually**, if
535
+ they have one — that's the single most reliable way to extract host/port/user/key
536
+ without transposition errors:
537
+
538
+ ```
539
+ ssh -N -o ExitOnForwardFailure=yes -o StrictHostKeyChecking=accept-new \
540
+ -p 22222 -i /Users/<them>/.ssh/id_ed25519 \
541
+ -L 127.0.0.1:10021:10.0.0.21:9000 \
542
+ zhang.san@192.0.2.10
543
+ ```
544
+
545
+ From that line you can read off `host=192.0.2.10`, `port=22222`,
546
+ `user=zhang.san`, `key=/Users/<them>/.ssh/id_ed25519`. The `-L` line also gives
547
+ you one database's `local_port:remote_host:remote_port` triple immediately.
548
+
549
+ 4. **For each database** the user wants to query, collect (in plain language, then
550
+ translate to `db.ini` keys):
551
+
552
+ | `db.ini` key | Plain-language question | Example value |
553
+ |------------------|--------------------------------------------------------------------------------|--------------------------|
554
+ | (section name) | "What short label do you want for this connection?" (use `_` or `-`) | `ck`, `mysql`, `prod` |
555
+ | `type` | "Is it ClickHouse or MySQL?" | `clickhouse` / `mysql` |
556
+ | `remote_host` | "From *on the bastion*, what IP/hostname is the DB at?" (NOT their laptop's) | `10.0.0.21` |
557
+ | `remote_port` | "What port does the DB listen on there?" (CH default 9000, MySQL 3306) | `9000` |
558
+ | `local_port` | "Pick a free port on your Mac between 1024-65535" (offer to pick one for them)| `10021` |
559
+ | `user` | "The read-only DB username (ask your DBA — must be SELECT-only)" | `api_asia` |
560
+ | `password` | "That user's password" | *(don't echo it back)* |
561
+ | `database` | "Default database name?" (required for ClickHouse, optional for MySQL) | `default` / `myapp` |
562
+
563
+ - **Auto mode:** needs all of `remote_host` / `remote_port` / `local_port`. No
564
+ `host` / `port` keys on this connection (those are direct-only).
565
+ - **External mode:** the user already runs the tunnel — collect `host` (usually
566
+ `localhost`) and `port` (the local tunnel port). No `tunnel`/`remote_*`/`local_port`
567
+ keys.
568
+ 5. **Write `connections/config/db.ini`** (see `db.ini.example` for the shape):
569
+ - Auto: one `[ssh:*]` section for the bastion + each connection with
570
+ `tunnel = ssh:*` and its `remote_host`/`remote_port`/`local_port` (no `host`/`port`).
571
+ - External: each connection with `host`/`port`/`user`/`password` (and `database` for
572
+ ClickHouse). No `[ssh:*]` block.
573
+ 6. **Verify** with `uv run sql-buddy doctor --connect`. Expect every line to read
574
+ `OK` (it runs `SELECT 1` over each connection and opens the tunnel on first use).
575
+ Any `FAIL` line is self-explanatory; read the tool-error `hint` and follow it (in
576
+ External mode a transport error usually means the user's tunnel is down).
577
+ **Common Auto-mode failures and fixes:**
578
+ - hangs at "Connecting to host port 22" but you specified another `-p` → sshd not
579
+ reachable on that port. Confirm the port with ops.
580
+ - `Permission denied (publickey)` → wrong user, key not registered on bastion, or
581
+ key file too permissive (`chmod 600 <key>` fixes the third one).
582
+ - tunnel comes up but DB call times out → wrong `remote_host`/`remote_port`
583
+ (those are the DB *as seen from the bastion*, not from the laptop).
584
+ 7. **Confirm `db.ini` is not tracked** by git (`git status` should not list it).
585
+ 8. **Register the MCP server** with one command — it writes the `uv --directory … run`
586
+ config straight into the client (no absolute `.venv/bin/python` path to verify):
587
+ ```bash
588
+ uv run sql-buddy mcp add --scope user # ~/.claude.json (user-global)
589
+ # or, for a single project: uv run sql-buddy mcp add # → ./.mcp.json
590
+ # Windows + Trae:
591
+ # uv run sql-buddy mcp add --client trae --scope user
592
+ uv run sql-buddy mcp print # preview the exact block first if you like
593
+ ```
594
+ Then tell the user to restart their MCP client.
595
+
596
+ Never write credentials to anywhere except `db.ini`. Never commit `db.ini`. When using
597
+ the running server, every tool error JSON carries a `hint` - follow it rather than
598
+ guessing.
599
+
600
+ ---
601
+
602
+ ## MCP Tools
603
+
604
+ | Tool | Description |
605
+ |------|-------------|
606
+ | `query(sql, connection)` | Execute read-only SQL. `LIMIT` auto-added if missing (default 10, max 2000); pass an explicit `LIMIT N` for more rows. Returns JSON. |
607
+ | `find_table(search, limit?)` | **Locate a table across ALL connections by name closeness** when you're unsure which connection holds it, or after a wrong-connection error. Returns `connection` + `database` + `qualified_name` to query next. |
608
+ | `get_table_definition(table_name, connection)` | Get `CREATE TABLE` with parsed columns, engine, partition. Cached 3 days. |
609
+ | `get_info(table_name, connection)` | Get row count, indexes (MySQL) / partitions (ClickHouse). Cached 3 days. |
610
+ | `list_tables(connection, database?, like?)` | List tables; always pass `like` to search server-side (fast). |
611
+ | `list_connections()` | Discover available connections from `db.ini`. |
612
+ | `check_connection(connection)` | **Ping one connection** (`SELECT 1` round-trip). Returns `{status:'ok', latency_ms}` or `{status:'error', hint}`. Use to diagnose a dead tunnel/conn before running real queries. |
613
+ | `get_history(connection, table_name?)` | Past query execution history, optionally filtered by table. |
614
+ | `explain(sql, connection)` | Run `EXPLAIN`; returns plan + cost estimates. |
615
+
616
+ All tools are **read-only**. `connection` names a `db.ini` section; `database` (in
617
+ `list_tables`) names a table namespace *inside* that server.
618
+
619
+ **Errors are agent-actionable.** Every error JSON includes the `connection` that
620
+ failed (when relevant) and a `hint` field with the suggested next action - an unknown
621
+ connection hints at `list_connections`; a transport error hints at `check_connection`
622
+ (and, in External tunnel mode, that the user's SSH tunnel may be down); a missing
623
+ table hints at `find_table`. Agents should read and follow the `hint`.
624
+
625
+ ---
626
+
627
+ ## Configuration reference
628
+
629
+ ### Environment variables
630
+
631
+ | Variable | Default | Purpose |
632
+ |----------|---------|---------|
633
+ | `SQL_BUDDY_DEFAULT_CONNECTION` | `ck` | Connection used when a tool's `connection` arg is omitted. |
634
+ | `SQL_BUDDY_RESOURCES` | *(unset)* | Optional JSON mapping `{connection: [table, ...]}` exposing selected table schemas as MCP resources. Advanced. |
635
+ | `DB_QUERY_HISTORY_DIR` | `./database` | Where query history + caches are stored. |
636
+
637
+ ### File layout
638
+
639
+ ```
640
+ .mcp.json # project-scope MCP client config (uv run server.py)
641
+ connections/config/db.ini # your credentials + ssh blocks (gitignored, never commit)
642
+ connections/config/db.ini.example # template (committed)
643
+ database/ # caches + query history (gitignored)
644
+ server.py # MCP server entrypoint (bare run = serve over stdio)
645
+ cli.py # `sql-buddy` CLI: doctor / mcp / version / update-check
646
+ db_query.py # query logic
647
+ db_security.py # secret-redaction + .env loader (installed at startup)
648
+ tunnel.py # self-managed SSH tunnels (one ssh -N per [ssh:*] block)
649
+ init.py # setup wizard — interactive, or --non-interactive --json-stdin
650
+ _version.py # single source of truth for the version
651
+ ```
652
+
653
+ ---
654
+
655
+ ## Troubleshooting
656
+
657
+ **"SSH tunnel did not come up" / "ssh stderr: ..."**
658
+ -> SQL Buddy could not establish the tunnel. Common causes: wrong `key` path (or
659
+ unreadable key), wrong bastion `host`/`port`/`user`, the bastion host key changed, or a
660
+ `local_port` already in use by something else. The error message includes the ssh stderr
661
+ tail. Test the SSH leg manually with the same details:
662
+ `ssh -N -p <port> -i <key> <user>@<host> -L 127.0.0.1:<local_port>:<remote_host>:<remote_port>`.
663
+
664
+ **"Connection refused" / "timed out" when the agent queries anything**
665
+ -> The tunnel came up but the DB target is wrong. Check `remote_host`/`remote_port` -
666
+ they must be the DB's address **as seen from the bastion**, not from your laptop.
667
+
668
+ **"Unknown connection" / wrong connection used**
669
+ -> The `connection` name must match a `[section]` in `db.ini`. Run
670
+ `list_connections()` to see what's configured. Check `SQL_BUDDY_DEFAULT_CONNECTION`.
671
+
672
+ **"Write operation blocked by sql-buddy's read-only guard"**
673
+ -> This is **working as intended.** SQL Buddy rejected a mutating statement. Use a
674
+ different tool if you genuinely need to change data - SQL Buddy cannot do it.
675
+
676
+ **Agent keeps querying the wrong database for a table**
677
+ -> Have it call `find_table("table_name")` first - it searches every connection and reports
678
+ where the table actually lives.
679
+
680
+ **An `ssh -N` process is still running after SQL Buddy exited**
681
+ -> On a graceful exit SQL Buddy kills its tunnels. A hard kill (`SIGKILL` / power loss)
682
+ can orphan the `ssh` process; it is harmless (idle) but will hold the local ports. Reclaim
683
+ them with `pkill -f 'ssh -N .*127.0.0.1:<local_port>'` or `pkill -af 'ssh -N'`.
684
+
685
+ **`db.ini` shows up in `git status` as a new/changed file**
686
+ -> Stop. Do **not** commit it. It should be gitignored. If it's already tracked, remove it
687
+ from git history (it contains live credentials):
688
+ ```bash
689
+ git rm --cached connections/config/db.ini
690
+ ```
691
+
692
+ ---
693
+
694
+ ## Security model
695
+
696
+ - **Read-only enforced in code**, before any SQL reaches the network. Write keywords
697
+ (`INSERT`, `UPDATE`, `DELETE`, `DROP`, `ALTER`, `TRUNCATE`, …) are rejected by a guard
698
+ layer wrapping every connection.
699
+ - **Credentials stay local** - in `db.ini` on your machine, never sent to any third party.
700
+ - **SSH tunnels bind to localhost only** (`127.0.0.1`) - the forwarded DB ports are not
701
+ exposed on your LAN. Key-based auth is used for the SSH leg; the key never leaves your
702
+ machine.
703
+ - **`LIMIT` is always applied** to `SELECT`/`WITH` (default 10, hard cap 2000) to prevent
704
+ accidental huge scans.
705
+ - **`db.ini` is gitignored.** Verify with `git status` before every push.