db-conn-mcp 0.1.0__tar.gz
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.
- db_conn_mcp-0.1.0/.cursorrules +57 -0
- db_conn_mcp-0.1.0/.github/workflows/publish.yml +48 -0
- db_conn_mcp-0.1.0/.gitignore +43 -0
- db_conn_mcp-0.1.0/AGENT_RULES.md +57 -0
- db_conn_mcp-0.1.0/ARCHITECTURE.md +312 -0
- db_conn_mcp-0.1.0/CLAUDE.md +57 -0
- db_conn_mcp-0.1.0/LICENSE +21 -0
- db_conn_mcp-0.1.0/PKG-INFO +214 -0
- db_conn_mcp-0.1.0/PLAN.md +69 -0
- db_conn_mcp-0.1.0/PRD.md +79 -0
- db_conn_mcp-0.1.0/README.md +185 -0
- db_conn_mcp-0.1.0/docs/superpowers/plans/2026-06-03-db-conn-mcp-v1.md +1367 -0
- db_conn_mcp-0.1.0/docs/superpowers/specs/2026-06-03-db-conn-mcp-design.md +246 -0
- db_conn_mcp-0.1.0/pyproject.toml +72 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/__init__.py +7 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/__main__.py +10 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/cli.py +571 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/config.py +124 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/diagnostics.py +130 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/dialects/__init__.py +5 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/dialects/base.py +39 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/dialects/postgres.py +148 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/dialects/registry.py +33 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/handlers.py +134 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/models.py +33 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/safety.py +40 -0
- db_conn_mcp-0.1.0/src/db_conn_mcp/server.py +109 -0
- db_conn_mcp-0.1.0/tests/__init__.py +0 -0
- db_conn_mcp-0.1.0/tests/test_cli.py +557 -0
- db_conn_mcp-0.1.0/tests/test_config.py +157 -0
- db_conn_mcp-0.1.0/tests/test_diagnostics.py +73 -0
- db_conn_mcp-0.1.0/tests/test_postgres.py +143 -0
- db_conn_mcp-0.1.0/tests/test_safety.py +46 -0
- db_conn_mcp-0.1.0/tests/test_server.py +203 -0
- db_conn_mcp-0.1.0/tests/test_smoke.py +33 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# AI Agent Rules (CLAUDE.md / .cursorrules)
|
|
2
|
+
|
|
3
|
+
Welcome, AI Agent (Claude, Agy, Cursor, or otherwise)!
|
|
4
|
+
When assisting the user with the `db-conn-mcp` project, please strictly adhere to the following rules to maintain the project's core philosophy.
|
|
5
|
+
|
|
6
|
+
> **Note:** This file is canonical. `CLAUDE.md` and `.cursorrules` are exact copies — when you change a rule, update all three so they stay byte-identical.
|
|
7
|
+
|
|
8
|
+
## 1. Simplicity First (No Over-engineering)
|
|
9
|
+
- **DO NOT** introduce complex architectures, custom OAuth servers, or JWT minting.
|
|
10
|
+
- **DO NOT** use complex AST parsers to validate read vs. write queries.
|
|
11
|
+
- **DO** rely on native database features. Read-only enforcement lives inside each dialect (for Postgres v1: set the session read-only with `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;`).
|
|
12
|
+
|
|
13
|
+
## 2. Code Quality & Standards (Production Grade)
|
|
14
|
+
- **Ruff:** Use `ruff` as the strict standard for all linting and formatting. Ensure all code passes `ruff check` and `ruff format`.
|
|
15
|
+
- **SOLID & KISS:** Code must be highly modular but radically simple. Follow SOLID principles to keep concerns separated (e.g., config parsing vs. database connection vs. MCP transport), but never at the expense of KISS (Keep It Simple, Stupid).
|
|
16
|
+
- **DevX & AgentX:** The codebase must be highly readable and easy to modify for *both* human developers and AI agents. Keep files reasonably small, avoid deep nesting, and use explicit typing (`typing` module) everywhere.
|
|
17
|
+
- **Documentation:** Well-documented from day one. Every class, module, and non-trivial function must have a clear Python docstring.
|
|
18
|
+
|
|
19
|
+
## 3. Configuration via JSON
|
|
20
|
+
- The single source of truth for database connections is `connections.json`.
|
|
21
|
+
- The schema is a top-level object `{ "connections": [ ... ] }`; each connection has `name`, `dsn`, `mode` (`"read"` or `"write"`), and an optional `yolo` (boolean, default `false`).
|
|
22
|
+
- Feel free to programmatically read, update, or create `connections.json` for the user if they ask you to add a database.
|
|
23
|
+
|
|
24
|
+
## 4. Technology Stack
|
|
25
|
+
- Keep the project in Python.
|
|
26
|
+
- Use the official `mcp` SDK (`pip install mcp`).
|
|
27
|
+
- Use standard, modern async libraries where applicable (e.g., `asyncpg`).
|
|
28
|
+
- **`pyproject.toml` is the single source of truth for dependencies** — declare all packages there. Do NOT maintain a separate `requirements.txt`.
|
|
29
|
+
- **v1 ships PostgreSQL only**, but all DB-specific code lives behind a `Dialect` abstraction (`dialects/`) so adding MySQL/SQLite later is a single new file. Never leak a specific database's SQL above the dialect layer.
|
|
30
|
+
|
|
31
|
+
## 5. MCP Transport Modes
|
|
32
|
+
- The application must support both `stdio` and `http` (SSE) transports.
|
|
33
|
+
- Ensure command-line arguments (using `argparse`) dictate which transport is launched.
|
|
34
|
+
|
|
35
|
+
## 6. Security & Safety
|
|
36
|
+
- **Never log DSNs** or passwords to the console. Connection diagnostics must be **sanitized** — describe the *category* of failure and how to fix it, never echo the DSN, host, user, or password.
|
|
37
|
+
- Treat the `mode` attribute in `connections.json` as an absolute security boundary. If `mode` is `read`, the AI must not be able to bypass it.
|
|
38
|
+
- Writes are gated server-side in this order: `mode` (hard, native) → `yolo` (per-database persisted trust) → `user_consent` (explicit per-operation approval). `yolo` and `user_consent` only relax the *prompt* on a DB that is already `write`; they can NEVER make a `read` DB writable.
|
|
39
|
+
|
|
40
|
+
## 7. Living Documentation (Always Keep Docs Updated)
|
|
41
|
+
- **ALWAYS, ALWAYS, ALWAYS** keep the docs in sync with decisions. The moment a design, scope, or architecture decision is made or accepted, update the relevant docs in the SAME change — never defer it:
|
|
42
|
+
- `PRD.md` — product scope, features, tools.
|
|
43
|
+
- `PLAN.md` — implementation roadmap / phases.
|
|
44
|
+
- `ARCHITECTURE.md` — components, flows, diagrams.
|
|
45
|
+
- These rule files (`AGENT_RULES.md` → copied to `CLAUDE.md` and `.cursorrules`) when a rule itself changes.
|
|
46
|
+
- Docs and code must never drift. A change that alters behavior or scope without updating the docs is **incomplete**.
|
|
47
|
+
|
|
48
|
+
## 8. Commit Hygiene (No Agent Watermarks)
|
|
49
|
+
- **NEVER** add agent/AI watermarks, attributions, or trailers to commits, PRs, or any other output — e.g. no `Co-Authored-By: Claude …`, no "Generated with Claude Code", no 🤖 lines. Commits and messages must read like a normal human developer's work.
|
|
50
|
+
|
|
51
|
+
## 9. Environment & Sanitation
|
|
52
|
+
- **Always work inside a project-local virtual environment** (`.venv`): `python -m venv .venv`, activate it, and install there. NEVER install dependencies into the global/system Python. `.venv` is git-ignored.
|
|
53
|
+
- **Install from `pyproject.toml`** (the single source of truth — see Rule 4): `pip install -e .`. Do not hand-maintain a `requirements.txt`.
|
|
54
|
+
- **Input sanitation:** validate and safely quote any identifier (database/table/column names) before placing it into catalog/introspection queries — never naive string concatenation. Route user-supplied *values* through driver parameterization. (Raw SQL in `execute_read_query`/`execute_write_query` is intentional and governed by the `mode` → `yolo` → `user_consent` gate.)
|
|
55
|
+
- **Secrets sanitation:** never commit `connections.json` or `.env` (both git-ignored); never print or log DSNs, hosts, or credentials; keep all error/diagnostic output sanitized (see Rule 6).
|
|
56
|
+
|
|
57
|
+
Follow these rules rigidly to ensure `db-conn-mcp` remains the cleanest, most straightforward open-source database MCP server available.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI via Trusted Publishing (OIDC) — no API token stored.
|
|
4
|
+
# Triggers when a GitHub Release is published (create one from a vX.Y.Z tag).
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
workflow_dispatch: {}
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
name: Build distributions
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
- name: Build wheel and sdist
|
|
23
|
+
run: |
|
|
24
|
+
python -m pip install --upgrade build
|
|
25
|
+
python -m build
|
|
26
|
+
- name: Check metadata
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade twine
|
|
29
|
+
python -m twine check dist/*
|
|
30
|
+
- uses: actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
|
|
35
|
+
publish:
|
|
36
|
+
name: Publish to PyPI
|
|
37
|
+
needs: build
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
environment: pypi
|
|
40
|
+
permissions:
|
|
41
|
+
id-token: write # required for trusted publishing (OIDC)
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/download-artifact@v4
|
|
44
|
+
with:
|
|
45
|
+
name: dist
|
|
46
|
+
path: dist/
|
|
47
|
+
- name: Publish
|
|
48
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# ---- Local-only files ----
|
|
2
|
+
progress.txt
|
|
3
|
+
|
|
4
|
+
# ---- Secrets / local config (NEVER commit real DSNs or credentials) ----
|
|
5
|
+
connections.json
|
|
6
|
+
.env
|
|
7
|
+
.env.*
|
|
8
|
+
|
|
9
|
+
# ---- Python bytecode / caches ----
|
|
10
|
+
__pycache__/
|
|
11
|
+
*.py[cod]
|
|
12
|
+
*$py.class
|
|
13
|
+
*.so
|
|
14
|
+
|
|
15
|
+
# ---- Virtual environments ----
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
env/
|
|
19
|
+
ENV/
|
|
20
|
+
|
|
21
|
+
# ---- Packaging / build artifacts ----
|
|
22
|
+
build/
|
|
23
|
+
dist/
|
|
24
|
+
*.egg
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.eggs/
|
|
27
|
+
wheels/
|
|
28
|
+
|
|
29
|
+
# ---- Tooling caches ----
|
|
30
|
+
.ruff_cache/
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
.mypy_cache/
|
|
33
|
+
.coverage
|
|
34
|
+
.coverage.*
|
|
35
|
+
htmlcov/
|
|
36
|
+
.tox/
|
|
37
|
+
|
|
38
|
+
# ---- IDE / OS noise ----
|
|
39
|
+
.idea/
|
|
40
|
+
.vscode/
|
|
41
|
+
*.swp
|
|
42
|
+
.DS_Store
|
|
43
|
+
Thumbs.db
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# AI Agent Rules (CLAUDE.md / .cursorrules)
|
|
2
|
+
|
|
3
|
+
Welcome, AI Agent (Claude, Agy, Cursor, or otherwise)!
|
|
4
|
+
When assisting the user with the `db-conn-mcp` project, please strictly adhere to the following rules to maintain the project's core philosophy.
|
|
5
|
+
|
|
6
|
+
> **Note:** This file is canonical. `CLAUDE.md` and `.cursorrules` are exact copies — when you change a rule, update all three so they stay byte-identical.
|
|
7
|
+
|
|
8
|
+
## 1. Simplicity First (No Over-engineering)
|
|
9
|
+
- **DO NOT** introduce complex architectures, custom OAuth servers, or JWT minting.
|
|
10
|
+
- **DO NOT** use complex AST parsers to validate read vs. write queries.
|
|
11
|
+
- **DO** rely on native database features. Read-only enforcement lives inside each dialect (for Postgres v1: set the session read-only with `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;`).
|
|
12
|
+
|
|
13
|
+
## 2. Code Quality & Standards (Production Grade)
|
|
14
|
+
- **Ruff:** Use `ruff` as the strict standard for all linting and formatting. Ensure all code passes `ruff check` and `ruff format`.
|
|
15
|
+
- **SOLID & KISS:** Code must be highly modular but radically simple. Follow SOLID principles to keep concerns separated (e.g., config parsing vs. database connection vs. MCP transport), but never at the expense of KISS (Keep It Simple, Stupid).
|
|
16
|
+
- **DevX & AgentX:** The codebase must be highly readable and easy to modify for *both* human developers and AI agents. Keep files reasonably small, avoid deep nesting, and use explicit typing (`typing` module) everywhere.
|
|
17
|
+
- **Documentation:** Well-documented from day one. Every class, module, and non-trivial function must have a clear Python docstring.
|
|
18
|
+
|
|
19
|
+
## 3. Configuration via JSON
|
|
20
|
+
- The single source of truth for database connections is `connections.json`.
|
|
21
|
+
- The schema is a top-level object `{ "connections": [ ... ] }`; each connection has `name`, `dsn`, `mode` (`"read"` or `"write"`), and an optional `yolo` (boolean, default `false`).
|
|
22
|
+
- Feel free to programmatically read, update, or create `connections.json` for the user if they ask you to add a database.
|
|
23
|
+
|
|
24
|
+
## 4. Technology Stack
|
|
25
|
+
- Keep the project in Python.
|
|
26
|
+
- Use the official `mcp` SDK (`pip install mcp`).
|
|
27
|
+
- Use standard, modern async libraries where applicable (e.g., `asyncpg`).
|
|
28
|
+
- **`pyproject.toml` is the single source of truth for dependencies** — declare all packages there. Do NOT maintain a separate `requirements.txt`.
|
|
29
|
+
- **v1 ships PostgreSQL only**, but all DB-specific code lives behind a `Dialect` abstraction (`dialects/`) so adding MySQL/SQLite later is a single new file. Never leak a specific database's SQL above the dialect layer.
|
|
30
|
+
|
|
31
|
+
## 5. MCP Transport Modes
|
|
32
|
+
- The application must support both `stdio` and `http` (SSE) transports.
|
|
33
|
+
- Ensure command-line arguments (using `argparse`) dictate which transport is launched.
|
|
34
|
+
|
|
35
|
+
## 6. Security & Safety
|
|
36
|
+
- **Never log DSNs** or passwords to the console. Connection diagnostics must be **sanitized** — describe the *category* of failure and how to fix it, never echo the DSN, host, user, or password.
|
|
37
|
+
- Treat the `mode` attribute in `connections.json` as an absolute security boundary. If `mode` is `read`, the AI must not be able to bypass it.
|
|
38
|
+
- Writes are gated server-side in this order: `mode` (hard, native) → `yolo` (per-database persisted trust) → `user_consent` (explicit per-operation approval). `yolo` and `user_consent` only relax the *prompt* on a DB that is already `write`; they can NEVER make a `read` DB writable.
|
|
39
|
+
|
|
40
|
+
## 7. Living Documentation (Always Keep Docs Updated)
|
|
41
|
+
- **ALWAYS, ALWAYS, ALWAYS** keep the docs in sync with decisions. The moment a design, scope, or architecture decision is made or accepted, update the relevant docs in the SAME change — never defer it:
|
|
42
|
+
- `PRD.md` — product scope, features, tools.
|
|
43
|
+
- `PLAN.md` — implementation roadmap / phases.
|
|
44
|
+
- `ARCHITECTURE.md` — components, flows, diagrams.
|
|
45
|
+
- These rule files (`AGENT_RULES.md` → copied to `CLAUDE.md` and `.cursorrules`) when a rule itself changes.
|
|
46
|
+
- Docs and code must never drift. A change that alters behavior or scope without updating the docs is **incomplete**.
|
|
47
|
+
|
|
48
|
+
## 8. Commit Hygiene (No Agent Watermarks)
|
|
49
|
+
- **NEVER** add agent/AI watermarks, attributions, or trailers to commits, PRs, or any other output — e.g. no `Co-Authored-By: Claude …`, no "Generated with Claude Code", no 🤖 lines. Commits and messages must read like a normal human developer's work.
|
|
50
|
+
|
|
51
|
+
## 9. Environment & Sanitation
|
|
52
|
+
- **Always work inside a project-local virtual environment** (`.venv`): `python -m venv .venv`, activate it, and install there. NEVER install dependencies into the global/system Python. `.venv` is git-ignored.
|
|
53
|
+
- **Install from `pyproject.toml`** (the single source of truth — see Rule 4): `pip install -e .`. Do not hand-maintain a `requirements.txt`.
|
|
54
|
+
- **Input sanitation:** validate and safely quote any identifier (database/table/column names) before placing it into catalog/introspection queries — never naive string concatenation. Route user-supplied *values* through driver parameterization. (Raw SQL in `execute_read_query`/`execute_write_query` is intentional and governed by the `mode` → `yolo` → `user_consent` gate.)
|
|
55
|
+
- **Secrets sanitation:** never commit `connections.json` or `.env` (both git-ignored); never print or log DSNs, hosts, or credentials; keep all error/diagnostic output sanitized (see Rule 6).
|
|
56
|
+
|
|
57
|
+
Follow these rules rigidly to ensure `db-conn-mcp` remains the cleanest, most straightforward open-source database MCP server available.
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# Architecture: `db-conn-mcp`
|
|
2
|
+
|
|
3
|
+
This document shows how `db-conn-mcp` is structured and traces the **end-to-end flow**, from a user setting up a connection on the CLI, through an AI agent exploring and querying the database, to the write-safety gate that protects their data.
|
|
4
|
+
|
|
5
|
+
> **v1 scope:** PostgreSQL only. The code is built around a **dialect seam** so adding MySQL / SQLite later is a *one-file* job. See [Extensibility](#extensibility-adding-a-database).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Component Overview
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌──────────────────────────────────────────┐
|
|
13
|
+
┌──────────────┐ │ db-conn-mcp │
|
|
14
|
+
│ AI Agent │ MCP │ server.py ── 8 tools + 1 prompt │
|
|
15
|
+
│ (Claude, │◄───────►│ │ │
|
|
16
|
+
│ Cursor, …) │ stdio/ │ ├─► safety.py ── write-gate │
|
|
17
|
+
└──────────────┘ http │ │ │
|
|
18
|
+
│ ├─► diagnostics.py ── doctor / │
|
|
19
|
+
┌──────────────┐ │ │ sanitized error → cause + fix │
|
|
20
|
+
│ Human user │ CLI │ ▼ │
|
|
21
|
+
│ (terminal) │────────►│ dialects/ ── Postgres SQL + RO guard │
|
|
22
|
+
└──────────────┘ setup │ (registry) │ config.py ◄─ conns │
|
|
23
|
+
└──────┼───────────────────────│──────────┘
|
|
24
|
+
▼ ▼
|
|
25
|
+
┌─────────────┐ ┌────────────────┐
|
|
26
|
+
│ PostgreSQL │ │connections.json│
|
|
27
|
+
└─────────────┘ └────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Module responsibilities (single-purpose layers)
|
|
31
|
+
|
|
32
|
+
| Module | Responsibility | Knows about Postgres? |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `cli.py` | Parse args (`--config`, `--transport`); run server or a management subcommand (`setup`/`status`/`add`/`clients`/`remove`/`yolo`); detect & inject MCP clients | No |
|
|
35
|
+
| `config.py` | Resolve, load, validate, **and save** `connections.json` | No |
|
|
36
|
+
| `models.py` | Pydantic types: `Connection{name, dsn, mode, yolo}`, `Config` | No |
|
|
37
|
+
| `dialects/base.py` | The `Dialect` ABC — the extensibility contract | No |
|
|
38
|
+
| `dialects/postgres.py` | `asyncpg` impl + native read-only enforcement | **Yes (only here)** |
|
|
39
|
+
| `dialects/registry.py` | Map DSN scheme → `Dialect`; clear error on unknown scheme | No |
|
|
40
|
+
| `safety.py` | Pure write-gate decision (`mode` + `yolo` + `consent`) | No |
|
|
41
|
+
| `diagnostics.py` | Classify driver errors → **sanitized** cause + fix; the doctor | No |
|
|
42
|
+
| `handlers.py` | The 8 tool handlers as plain async methods (transport-free, unit-testable) | No |
|
|
43
|
+
| `server.py` | `FastMCP` app: registers the 8 tools + the prompt onto `handlers`, transport wiring | No |
|
|
44
|
+
|
|
45
|
+
The **dialect layer is the only place that knows a database is PostgreSQL.** Everything above it speaks the abstract `Dialect` contract.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. The Tools an Agent Sees
|
|
50
|
+
|
|
51
|
+
| # | Tool | Kind | Safety |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| 1 | `list_databases` | Explore | safe — names + mode + yolo |
|
|
54
|
+
| 2 | `list_tables` | Explore | safe |
|
|
55
|
+
| 3 | `get_table_schema` | Explore | safe |
|
|
56
|
+
| 4 | `sample_table_rows` | Explore | safe (first N rows) |
|
|
57
|
+
| 5 | `execute_read_query` | Execute | runs inside a **read-only transaction** |
|
|
58
|
+
| 6 | `execute_write_query` | Execute | **gated** (mode → yolo → consent) |
|
|
59
|
+
| 7 | `set_yolo_mode` | Config | persists `yolo` flag for one named DB |
|
|
60
|
+
| 8 | `check_database` | Doctor | tests one DB (or all) → `OK` or sanitized cause + fix |
|
|
61
|
+
|
|
62
|
+
Plus **one MCP prompt** — `troubleshoot_connection` — a discoverable, full connection-gotchas checklist the agent can pull when a DB won't connect (see [§7](#7-flow-e--self-diagnosing-connections-the-doctor)).
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 3. Flow A — Setup (human, on the CLI)
|
|
67
|
+
|
|
68
|
+
A human runs the wizard once to register a database. This is the *only* step that handles a raw DSN.
|
|
69
|
+
|
|
70
|
+
```mermaid
|
|
71
|
+
sequenceDiagram
|
|
72
|
+
actor User
|
|
73
|
+
participant CLI as setup wizard (cli.py)
|
|
74
|
+
participant Cfg as config.py
|
|
75
|
+
participant FS as connections.json
|
|
76
|
+
|
|
77
|
+
User->>CLI: python -m db_conn_mcp setup
|
|
78
|
+
CLI->>User: Scope? (global ~/.db-conn-mcp | repo ./)
|
|
79
|
+
User-->>CLI: global
|
|
80
|
+
CLI->>User: Name? DSN? Mode (read/write)?
|
|
81
|
+
User-->>CLI: "prod", "postgresql://…", "read"
|
|
82
|
+
CLI->>Cfg: add_connection(Connection)
|
|
83
|
+
Cfg->>Cfg: validate (pydantic + scheme is known)
|
|
84
|
+
Cfg->>FS: write connections.json (yolo defaults to false)
|
|
85
|
+
CLI->>User: ✅ registered. Inject into Claude/Cursor config? (y/n)
|
|
86
|
+
Note over CLI,FS: DSN is never logged — only the name is echoed.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Resulting `connections.json`:
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"connections": [
|
|
93
|
+
{ "name": "prod", "dsn": "postgresql://…", "mode": "read" },
|
|
94
|
+
{ "name": "dev", "dsn": "postgresql://…", "mode": "write", "yolo": false }
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Config resolution order** (first match wins): `--config <path>` → `./connections.json` → `~/.db-conn-mcp/connections.json`.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 4. Flow B — Agent explores, then runs a READ query
|
|
104
|
+
|
|
105
|
+
The everyday safe path. The agent discovers what exists, learns the schema, then queries.
|
|
106
|
+
|
|
107
|
+
```mermaid
|
|
108
|
+
sequenceDiagram
|
|
109
|
+
actor Agent
|
|
110
|
+
participant Server as server.py
|
|
111
|
+
participant Reg as dialects/registry
|
|
112
|
+
participant PG as PostgresDialect
|
|
113
|
+
participant DB as PostgreSQL
|
|
114
|
+
|
|
115
|
+
Agent->>Server: list_databases()
|
|
116
|
+
Server-->>Agent: [{prod, read}, {dev, write, yolo:false}]
|
|
117
|
+
|
|
118
|
+
Agent->>Server: list_tables(db="prod")
|
|
119
|
+
Server->>Reg: dialect_for("postgresql://…")
|
|
120
|
+
Reg-->>Server: PostgresDialect
|
|
121
|
+
Server->>PG: connect(dsn, read_only=true)
|
|
122
|
+
PG->>DB: SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY
|
|
123
|
+
PG->>DB: query information_schema
|
|
124
|
+
DB-->>Agent: [users, orders, …]
|
|
125
|
+
|
|
126
|
+
Agent->>Server: get_table_schema(db="prod", table="orders")
|
|
127
|
+
Server->>PG: get_schema(conn, "orders")
|
|
128
|
+
PG-->>Agent: columns, types, PK/FK
|
|
129
|
+
|
|
130
|
+
Agent->>Server: execute_read_query(db="prod", sql="SELECT …")
|
|
131
|
+
Server->>PG: connect(read_only=true) → execute(sql)
|
|
132
|
+
Note over PG,DB: A write here would be rejected by Postgres itself.
|
|
133
|
+
DB-->>Agent: rows
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Read-only is enforced **natively by the database** — even a `read`-mode connection physically cannot mutate data, regardless of what SQL is sent.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 5. Flow C — Agent runs a WRITE query (the safety gate)
|
|
141
|
+
|
|
142
|
+
This is the heart of the safety model. The decision lives **in the server**, not in the agent's good intentions.
|
|
143
|
+
|
|
144
|
+
```mermaid
|
|
145
|
+
flowchart TD
|
|
146
|
+
A[execute_write_query db, sql, user_consent] --> B{db.mode == write?}
|
|
147
|
+
B -- no --> R1[❌ REJECT: db is read-only]
|
|
148
|
+
B -- yes --> C{db.yolo == true?}
|
|
149
|
+
C -- yes --> RUN[✅ run write SQL]
|
|
150
|
+
C -- no --> D{user_consent == true?}
|
|
151
|
+
D -- yes --> RUN
|
|
152
|
+
D -- no --> R2[❌ REJECT:<br/>'Read the table & schema, show the<br/>exact SQL to the user, get a yes,<br/>then call again with user_consent=true']
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The intended agent choreography for a non-yolo write:
|
|
156
|
+
|
|
157
|
+
```mermaid
|
|
158
|
+
sequenceDiagram
|
|
159
|
+
actor User
|
|
160
|
+
actor Agent
|
|
161
|
+
participant Server as server.py
|
|
162
|
+
participant PG as PostgresDialect
|
|
163
|
+
participant DB as PostgreSQL
|
|
164
|
+
|
|
165
|
+
Note over Agent: 1. Understand before changing
|
|
166
|
+
Agent->>Server: get_table_schema(db="dev", table="users")
|
|
167
|
+
Server->>PG: get_schema → columns/types
|
|
168
|
+
PG-->>Agent: schema
|
|
169
|
+
Agent->>Server: sample_table_rows(db="dev", table="users")
|
|
170
|
+
PG-->>Agent: example rows (learn the data shape)
|
|
171
|
+
|
|
172
|
+
Note over Agent,User: 2. First call WITHOUT consent — server forces the ask
|
|
173
|
+
Agent->>Server: execute_write_query(db="dev", sql="UPDATE users SET …", user_consent=false)
|
|
174
|
+
Server-->>Agent: ❌ REJECT — show SQL to user, get explicit yes
|
|
175
|
+
|
|
176
|
+
Agent->>User: "I will run: UPDATE users SET … (affects ~3 rows). Proceed?"
|
|
177
|
+
User-->>Agent: "yes"
|
|
178
|
+
|
|
179
|
+
Note over Agent,DB: 3. Re-call WITH consent
|
|
180
|
+
Agent->>Server: execute_write_query(db="dev", sql="UPDATE users SET …", user_consent=true)
|
|
181
|
+
Server->>PG: connect(read_only=false) → execute(sql)
|
|
182
|
+
DB-->>Agent: rows affected
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`mode` is the **hard boundary** (native, unbypassable). `yolo` and `user_consent` only relax the *prompt* on a DB that is *already* `write` — they can never make a `read` DB writable.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 6. Flow D — Enabling YOLO mode (persisted)
|
|
190
|
+
|
|
191
|
+
When the user is tired of confirming every write on a trusted DB, they ask the agent to enable yolo. The server writes it back to disk so it survives restarts.
|
|
192
|
+
|
|
193
|
+
```mermaid
|
|
194
|
+
sequenceDiagram
|
|
195
|
+
actor User
|
|
196
|
+
actor Agent
|
|
197
|
+
participant Server as server.py
|
|
198
|
+
participant Cfg as config.py
|
|
199
|
+
participant FS as connections.json
|
|
200
|
+
|
|
201
|
+
User->>Agent: "Enable yolo mode for dev — stop asking me."
|
|
202
|
+
Agent->>Server: set_yolo_mode(database="dev", enabled=true)
|
|
203
|
+
Server->>Cfg: set_yolo("dev", true)
|
|
204
|
+
Cfg->>FS: rewrite connections.json (dev.yolo = true)
|
|
205
|
+
Server-->>Agent: ✅ yolo enabled for "dev" (persisted)
|
|
206
|
+
|
|
207
|
+
Note over Agent,FS: Future writes to "dev" skip the consent gate —<br/>across this and all future sessions.
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`set_yolo_mode("dev", false)` reverts it. YOLO is **per-database**: enabling it on `dev` never affects `prod`.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 7. Flow E — Self-diagnosing connections (the doctor)
|
|
215
|
+
|
|
216
|
+
Connections are validated **lazily**: the server boots even if a database is down, and *any* tool that hits an unreachable DB gets a sanitized, actionable diagnostic instead of a raw stack trace. The agent (or user) can also probe proactively with `check_database`.
|
|
217
|
+
|
|
218
|
+
```mermaid
|
|
219
|
+
sequenceDiagram
|
|
220
|
+
actor Agent
|
|
221
|
+
participant Server as server.py
|
|
222
|
+
participant Diag as diagnostics.py
|
|
223
|
+
participant PG as PostgresDialect
|
|
224
|
+
participant DB as PostgreSQL
|
|
225
|
+
|
|
226
|
+
Agent->>Server: check_database(name="prod")
|
|
227
|
+
Server->>PG: connect(dsn, read_only=true)
|
|
228
|
+
PG->>DB: TCP connect / auth
|
|
229
|
+
DB--xPG: error (refused / auth / no-db / SSL / timeout)
|
|
230
|
+
PG-->>Server: raises driver exception
|
|
231
|
+
Server->>Diag: explain(error) ⟵ DSN/password NEVER passed in
|
|
232
|
+
Diag-->>Server: {category, cause, fixes[]}
|
|
233
|
+
Server-->>Agent: "❌ prod unreachable — AUTH FAILED.<br/>Likely: wrong user/password or role missing.<br/>Fix: verify credentials; confirm the role exists.<br/>(full checklist: prompt 'troubleshoot_connection')"
|
|
234
|
+
Note over Agent: For the complete gotchas list:
|
|
235
|
+
Agent->>Server: getPrompt("troubleshoot_connection")
|
|
236
|
+
Server-->>Agent: full host/port/firewall/sslmode/<br/>container-localhost/db-name/pool checklist
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Error classification** (`diagnostics.py` maps driver exception → sanitized advice — credentials never appear in the output):
|
|
240
|
+
|
|
241
|
+
| Category | Triggered by (examples) | Sanitized cause + fix shown to agent |
|
|
242
|
+
|---|---|---|
|
|
243
|
+
| `AUTH_FAILED` | invalid password / role does not exist | "Wrong user/password, or the role doesn't exist. Verify credentials and that the role exists." |
|
|
244
|
+
| `HOST_UNREACHABLE` | connection refused / timeout | "DB not accepting connections. Is it running? Check host/port, firewall. In Docker, `localhost` ≠ the DB container." |
|
|
245
|
+
| `DB_NOT_FOUND` | database "x" does not exist | "Database name is wrong or not created yet. Check spelling/case; create it if needed." |
|
|
246
|
+
| `DNS_FAILURE` | could not translate host name | "Hostname can't be resolved — likely a typo in the host or a missing VPN/network." |
|
|
247
|
+
| `SSL_REQUIRED` | server requires SSL | "Server demands SSL. Add `?sslmode=require` (or appropriate mode) to the DSN." |
|
|
248
|
+
| `POOL_EXHAUSTED` | too many connections | "Connection limit hit. Close idle sessions or raise the server's `max_connections`." |
|
|
249
|
+
| `UNKNOWN` | anything unmatched | "Unrecognized connection error. See the `troubleshoot_connection` prompt for the full checklist." |
|
|
250
|
+
|
|
251
|
+
The **`troubleshoot_connection` MCP prompt** is the standalone, full gotchas checklist — discoverable by the agent at any time, not just on error. Targeted fixes appear inline per error; the prompt is the complete reference.
|
|
252
|
+
|
|
253
|
+
> **Security:** `diagnostics.explain()` receives only the *driver exception type/message-class* — never the `Connection` object or DSN — so no host, user, or password can leak into a tool response or log.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 8. Extensibility — Adding a Database
|
|
258
|
+
|
|
259
|
+
Adding MySQL or SQLite later touches **exactly one new file** plus a one-line registration. Nothing above the dialect layer changes.
|
|
260
|
+
|
|
261
|
+
```mermaid
|
|
262
|
+
flowchart LR
|
|
263
|
+
subgraph Today [v1 — today]
|
|
264
|
+
REG[registry] --> PGD[postgres.py]
|
|
265
|
+
end
|
|
266
|
+
subgraph Later [adding MySQL]
|
|
267
|
+
REG2[registry<br/>+1 line] --> PGD2[postgres.py]
|
|
268
|
+
REG2 -.new file.-> MYD[mysql.py]
|
|
269
|
+
end
|
|
270
|
+
Today ==> Later
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The contract a new dialect must satisfy:
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
class Dialect(ABC):
|
|
277
|
+
scheme: str # e.g. "mysql"
|
|
278
|
+
async def connect(self, dsn, *, read_only): ... # native read-only enforcement lives here
|
|
279
|
+
async def list_tables(self, conn): ...
|
|
280
|
+
async def get_schema(self, conn, table): ...
|
|
281
|
+
async def sample_rows(self, conn, table, n=10): ...
|
|
282
|
+
async def execute(self, conn, sql): ...
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
| DB | Read-only mechanism (inside `connect`) | Introspection source |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| PostgreSQL (v1) | `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY` | `information_schema` |
|
|
288
|
+
| MySQL (future) | `START TRANSACTION READ ONLY` | `information_schema` |
|
|
289
|
+
| SQLite (future) | open file with `?mode=ro` | `sqlite_master` / `PRAGMA` |
|
|
290
|
+
|
|
291
|
+
Because each dialect owns its own read-only enforcement and catalog queries, those per-database differences **never leak** into `safety.py` or `server.py`.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 9. End-to-End at a Glance
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
Human ──setup──► connections.json ──read──► config.py
|
|
299
|
+
│
|
|
300
|
+
AI Agent ──MCP (stdio/http)──► server.py ─────┼─► safety.py (write gate)
|
|
301
|
+
│ ├─► diagnostics.py (doctor, sanitized)
|
|
302
|
+
▼ ▼
|
|
303
|
+
dialects/registry ──► PostgresDialect ──► PostgreSQL
|
|
304
|
+
(scheme → impl) (RO guard + SQL)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
1. A human registers a DB once via the CLI wizard → `connections.json`.
|
|
308
|
+
2. An agent connects over MCP and **explores** (`list_*`, `get_table_schema`, `sample_table_rows`) — always read-only.
|
|
309
|
+
3. **Reads** run inside a native read-only transaction.
|
|
310
|
+
4. **Writes** pass through the server-side gate: `mode` (hard) → `yolo` (persisted trust) → `user_consent` (per-op ask).
|
|
311
|
+
5. `set_yolo_mode` lets a trusted DB skip the per-op ask, persisted to disk.
|
|
312
|
+
6. Any unreachable DB yields a **sanitized diagnostic** (cause + fix), never a raw error or leaked DSN; `check_database` probes proactively and `troubleshoot_connection` is the full gotchas checklist.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# AI Agent Rules (CLAUDE.md / .cursorrules)
|
|
2
|
+
|
|
3
|
+
Welcome, AI Agent (Claude, Agy, Cursor, or otherwise)!
|
|
4
|
+
When assisting the user with the `db-conn-mcp` project, please strictly adhere to the following rules to maintain the project's core philosophy.
|
|
5
|
+
|
|
6
|
+
> **Note:** This file is canonical. `CLAUDE.md` and `.cursorrules` are exact copies — when you change a rule, update all three so they stay byte-identical.
|
|
7
|
+
|
|
8
|
+
## 1. Simplicity First (No Over-engineering)
|
|
9
|
+
- **DO NOT** introduce complex architectures, custom OAuth servers, or JWT minting.
|
|
10
|
+
- **DO NOT** use complex AST parsers to validate read vs. write queries.
|
|
11
|
+
- **DO** rely on native database features. Read-only enforcement lives inside each dialect (for Postgres v1: set the session read-only with `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;`).
|
|
12
|
+
|
|
13
|
+
## 2. Code Quality & Standards (Production Grade)
|
|
14
|
+
- **Ruff:** Use `ruff` as the strict standard for all linting and formatting. Ensure all code passes `ruff check` and `ruff format`.
|
|
15
|
+
- **SOLID & KISS:** Code must be highly modular but radically simple. Follow SOLID principles to keep concerns separated (e.g., config parsing vs. database connection vs. MCP transport), but never at the expense of KISS (Keep It Simple, Stupid).
|
|
16
|
+
- **DevX & AgentX:** The codebase must be highly readable and easy to modify for *both* human developers and AI agents. Keep files reasonably small, avoid deep nesting, and use explicit typing (`typing` module) everywhere.
|
|
17
|
+
- **Documentation:** Well-documented from day one. Every class, module, and non-trivial function must have a clear Python docstring.
|
|
18
|
+
|
|
19
|
+
## 3. Configuration via JSON
|
|
20
|
+
- The single source of truth for database connections is `connections.json`.
|
|
21
|
+
- The schema is a top-level object `{ "connections": [ ... ] }`; each connection has `name`, `dsn`, `mode` (`"read"` or `"write"`), and an optional `yolo` (boolean, default `false`).
|
|
22
|
+
- Feel free to programmatically read, update, or create `connections.json` for the user if they ask you to add a database.
|
|
23
|
+
|
|
24
|
+
## 4. Technology Stack
|
|
25
|
+
- Keep the project in Python.
|
|
26
|
+
- Use the official `mcp` SDK (`pip install mcp`).
|
|
27
|
+
- Use standard, modern async libraries where applicable (e.g., `asyncpg`).
|
|
28
|
+
- **`pyproject.toml` is the single source of truth for dependencies** — declare all packages there. Do NOT maintain a separate `requirements.txt`.
|
|
29
|
+
- **v1 ships PostgreSQL only**, but all DB-specific code lives behind a `Dialect` abstraction (`dialects/`) so adding MySQL/SQLite later is a single new file. Never leak a specific database's SQL above the dialect layer.
|
|
30
|
+
|
|
31
|
+
## 5. MCP Transport Modes
|
|
32
|
+
- The application must support both `stdio` and `http` (SSE) transports.
|
|
33
|
+
- Ensure command-line arguments (using `argparse`) dictate which transport is launched.
|
|
34
|
+
|
|
35
|
+
## 6. Security & Safety
|
|
36
|
+
- **Never log DSNs** or passwords to the console. Connection diagnostics must be **sanitized** — describe the *category* of failure and how to fix it, never echo the DSN, host, user, or password.
|
|
37
|
+
- Treat the `mode` attribute in `connections.json` as an absolute security boundary. If `mode` is `read`, the AI must not be able to bypass it.
|
|
38
|
+
- Writes are gated server-side in this order: `mode` (hard, native) → `yolo` (per-database persisted trust) → `user_consent` (explicit per-operation approval). `yolo` and `user_consent` only relax the *prompt* on a DB that is already `write`; they can NEVER make a `read` DB writable.
|
|
39
|
+
|
|
40
|
+
## 7. Living Documentation (Always Keep Docs Updated)
|
|
41
|
+
- **ALWAYS, ALWAYS, ALWAYS** keep the docs in sync with decisions. The moment a design, scope, or architecture decision is made or accepted, update the relevant docs in the SAME change — never defer it:
|
|
42
|
+
- `PRD.md` — product scope, features, tools.
|
|
43
|
+
- `PLAN.md` — implementation roadmap / phases.
|
|
44
|
+
- `ARCHITECTURE.md` — components, flows, diagrams.
|
|
45
|
+
- These rule files (`AGENT_RULES.md` → copied to `CLAUDE.md` and `.cursorrules`) when a rule itself changes.
|
|
46
|
+
- Docs and code must never drift. A change that alters behavior or scope without updating the docs is **incomplete**.
|
|
47
|
+
|
|
48
|
+
## 8. Commit Hygiene (No Agent Watermarks)
|
|
49
|
+
- **NEVER** add agent/AI watermarks, attributions, or trailers to commits, PRs, or any other output — e.g. no `Co-Authored-By: Claude …`, no "Generated with Claude Code", no 🤖 lines. Commits and messages must read like a normal human developer's work.
|
|
50
|
+
|
|
51
|
+
## 9. Environment & Sanitation
|
|
52
|
+
- **Always work inside a project-local virtual environment** (`.venv`): `python -m venv .venv`, activate it, and install there. NEVER install dependencies into the global/system Python. `.venv` is git-ignored.
|
|
53
|
+
- **Install from `pyproject.toml`** (the single source of truth — see Rule 4): `pip install -e .`. Do not hand-maintain a `requirements.txt`.
|
|
54
|
+
- **Input sanitation:** validate and safely quote any identifier (database/table/column names) before placing it into catalog/introspection queries — never naive string concatenation. Route user-supplied *values* through driver parameterization. (Raw SQL in `execute_read_query`/`execute_write_query` is intentional and governed by the `mode` → `yolo` → `user_consent` gate.)
|
|
55
|
+
- **Secrets sanitation:** never commit `connections.json` or `.env` (both git-ignored); never print or log DSNs, hosts, or credentials; keep all error/diagnostic output sanitized (see Rule 6).
|
|
56
|
+
|
|
57
|
+
Follow these rules rigidly to ensure `db-conn-mcp` remains the cleanest, most straightforward open-source database MCP server available.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 db-conn-mcp contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|