pgsafe-mcp 1.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Johann Schiffer / NEXUS
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.
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: pgsafe-mcp
3
+ Version: 1.0.0
4
+ Summary: Zero-Trust Database Inspection and Safe Query Guard MCP Server for AI Agents
5
+ Author: Johann Schiffer
6
+ License: MIT
7
+ Project-URL: Homepage, https://johannschiffer.github.io
8
+ Project-URL: Repository, https://github.com/johannschiffer/pgsafe-mcp
9
+ Project-URL: Starter Kit, https://johanld.gumroad.com/l/genesis-starter-kit
10
+ Project-URL: Enterprise Suite, https://johanld.gumroad.com/l/genesis-enterprise-suite
11
+ Keywords: mcp,model-context-protocol,postgres,sqlite,database,claude,cursor,security,ai-agent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=7; extra == "test"
17
+ Provides-Extra: postgres
18
+ Requires-Dist: psycopg[binary]>=3.1.0; extra == "postgres"
19
+ Dynamic: license-file
20
+
21
+ # PgSafe MCP (Lite Edition)
22
+
23
+ <a href="https://glama.ai/mcp/servers/@johannschiffer/pgsafe-mcp">
24
+ <img width="380" height="200" src="https://glama.ai/mcp/servers/@johannschiffer/pgsafe-mcp/badge" alt="pgsafe-mcp MCP server" />
25
+ </a>
26
+
27
+
28
+ > **Free:** [Genesis Starter Kit](https://johanld.gumroad.com/l/genesis-starter-kit) (25 `.cursorrules`/`CLAUDE.md` rulesets + MCP configs). Try the [live config builder](https://johannschiffer.github.io).
29
+ > **Full stack:** all six servers in [Genesis Enterprise Suite ($19)](https://johanld.gumroad.com/l/genesis-enterprise-suite).
30
+
31
+
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
33
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10+-brightgreen.svg)](https://python.org)
34
+ [![MCP Compliant](https://img.shields.io/badge/MCP-2024--11--05-purple.svg)](https://modelcontextprotocol.io)
35
+
36
+ **PgSafe MCP** is a Zero-Trust Model Context Protocol server that enables AI agents in **Claude Desktop**, **Cursor**, **Windsurf**, and **Antigravity** to inspect schemas and run analytical SQL queries without risking accidental data loss, destructive mutations (`DROP`, `TRUNCATE`, `DELETE`), or runaway table scans.
37
+
38
+ ---
39
+
40
+ ## Run without cloning
41
+
42
+ ```bash
43
+ uvx --from git+https://github.com/johannschiffer/pgsafe-mcp pgsafe-mcp
44
+ ```
45
+
46
+ Claude Desktop / Cursor config entry:
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "pgsafe-mcp": {
52
+ "command": "uvx",
53
+ "args": [
54
+ "--from",
55
+ "git+https://github.com/johannschiffer/pgsafe-mcp",
56
+ "pgsafe-mcp"
57
+ ]
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ [![CI](https://github.com/johannschiffer/pgsafe-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/johannschiffer/pgsafe-mcp/actions/workflows/ci.yml)
64
+
65
+ ## 🛡️ Why PgSafe MCP?
66
+
67
+ Giving an AI assistant raw access to your database is terrifying:
68
+ 1. **Accidental Deletions:** One ambiguous prompt can lead an LLM to generate `DROP TABLE` or `DELETE FROM users;`.
69
+ 2. **Production Table Locks:** An unindexed query with a multi-table Cartesian product can consume 100% CPU and lock production tables.
70
+ 3. **Multi-Statement Injection:** LLMs can be tricked via prompt injection into chaining malicious SQL commands (`SELECT 1; TRUNCATE audit_logs;`).
71
+
72
+ **PgSafe** acts as a strict guardrail sitting between the AI agent and your database engine.
73
+
74
+ ---
75
+
76
+ ## ⚡ Features (Lite Edition)
77
+
78
+ - 🔒 **Enforced Read-Only Guard**: Automatically detects and blocks all destructive statements (`DROP`, `TRUNCATE`, `ALTER`, `UPDATE`, `INSERT`, `DELETE`).
79
+ - 🛑 **Anti-Chaining Protection**: Disallows multi-statement execution via semicolons.
80
+ - ⏱️ **Automatic LIMIT Injection**: Ensures every `SELECT` query has a capped `LIMIT` clause (default: 100 rows, max: 1,000) to protect database memory.
81
+ - 📊 **Zero-Cost Schema Reflection**: Tools to explore public tables and columns without executing custom introspection queries.
82
+ - 🪶 **Zero Dependency Core**: Native support for SQLite out of the box; optional PostgreSQL adapter.
83
+
84
+ ---
85
+
86
+ ## 🚀 Quick Setup in 2 Minutes
87
+
88
+ ### 1. Installation
89
+
90
+ ```bash
91
+ git clone https://github.com/johannschiffer/pgsafe-mcp.git
92
+ cd pgsafe-mcp
93
+ pip install .
94
+ ```
95
+
96
+ For PostgreSQL support:
97
+ ```bash
98
+ pip install "psycopg[binary]>=3.1.0"
99
+ ```
100
+
101
+ ### 2. Configure Claude Desktop (`claude_desktop_config.json`)
102
+
103
+ On Linux: `~/.config/Claude/claude_desktop_config.json`
104
+ On macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
105
+ On Windows: `%APPDATA%\Claude\claude_desktop_config.json`
106
+
107
+ ```json
108
+ {
109
+ "mcpServers": {
110
+ "pgsafe": {
111
+ "command": "python",
112
+ "args": ["-m", "pgsafe.server"],
113
+ "cwd": "/path/to/pgsafe-mcp",
114
+ "env": {
115
+ "DATABASE_URL": "postgresql://user:password@localhost:5432/production_db"
116
+ }
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ For SQLite, simply point to your `.db` file:
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "pgsafe": {
127
+ "command": "python",
128
+ "args": ["-m", "pgsafe.server"],
129
+ "cwd": "/path/to/pgsafe-mcp",
130
+ "env": {
131
+ "DATABASE_URL": "sqlite:///data/nexus.db"
132
+ }
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ## 🛠️ MCP Tools Exposed to AI Agents
141
+
142
+ | Tool | Parameters | Description |
143
+ |---|---|---|
144
+ | `list_tables` | *(none)* | Returns list of all user tables in the database |
145
+ | `describe_table` | `table_name` | Returns column names, data types, nullability, and primary keys |
146
+ | `safe_query` | `query`, `limit` | Executes strictly read-only SQL with forced LIMIT enforcement |
147
+ | `analyze_query` | `query` | Inspects SQL for unindexed table scan and Cartesian join risks |
148
+
149
+ ---
150
+
151
+ ## 💎 Need Enterprise Data Protection? Upgrade to PgSafe Pro
152
+
153
+ Running against live production workloads with sensitive customer data?
154
+
155
+ | Feature | Lite (Open Source) | Enterprise Suite ($19) |
156
+ |---|:---:|:---:|
157
+ | **Read-Only SQL Validation** | ✅ | ✅ |
158
+ | **Forced LIMIT Injection** | ✅ | ✅ |
159
+ | **PostgreSQL & SQLite** | ✅ | ✅ |
160
+ | **MySQL, MariaDB & ClickHouse** | ❌ | ✅ |
161
+ | **Automatic PII Redaction & Masking** *(Emails, Cards, Hashes)* | ❌ | ✅ |
162
+ | **Dry-Run Mutation Simulation** *(Auto-Rollback Test Harness)* | ❌ | ✅ |
163
+ | **Index Recommendation Advisor** | ❌ | ✅ |
164
+ | **Production Docker & Compose** | ❌ | ✅ |
165
+ | **Full Pytest Suite & Architecture Guide** | ❌ | ✅ |
166
+
167
+ 👉 **[Get the Genesis Enterprise Suite ($19)](https://johanld.gumroad.com/l/genesis-enterprise-suite)**
168
+
169
+ ---
170
+
171
+ ## 📄 License
172
+
173
+ MIT License. See [LICENSE](LICENSE) for details.
174
+
175
+ ## Genesis MCP Ecosystem
176
+
177
+ | Module | What it does | Repo |
178
+ |---|---|---|
179
+ | **CrawlEngine MCP** | Anti-bot web scraping, clean Markdown, proxy rotation and TLS evasion | [GitHub](https://github.com/johannschiffer/crawlengine-mcp) |
180
+ | **BrowserBridge MCP** | Deterministic headless browser automation with cryptographic action receipts | [GitHub](https://github.com/johannschiffer/browserbridge-mcp) |
181
+ | **PgSafe MCP** (this repo) | Zero-trust PostgreSQL/SQLite guard, live PII masking, zero-downtime migration checks | [GitHub](https://github.com/johannschiffer/pgsafe-mcp) |
182
+ | **OmniBridge MCP** | Multi-channel notification and webhook bus with a persistent SQLite WAL queue | [GitHub](https://github.com/johannschiffer/omnibridge-mcp) |
183
+ | **SysFlow MCP** | Safe Linux administration plane: systemd, port audit, isolated execution | [GitHub](https://github.com/johannschiffer/sysflow-mcp) |
184
+ | **FastMCP Forge** | Production MCP server scaffolder: stdio, SSE and auth middleware | [GitHub](https://github.com/johannschiffer/fastmcp-forge) |
185
+
186
+ [Live config builder](https://johannschiffer.github.io) | [Starter Kit, free](https://johanld.gumroad.com/l/genesis-starter-kit) | [Enterprise Suite, $19](https://johanld.gumroad.com/l/genesis-enterprise-suite)
@@ -0,0 +1,166 @@
1
+ # PgSafe MCP (Lite Edition)
2
+
3
+ <a href="https://glama.ai/mcp/servers/@johannschiffer/pgsafe-mcp">
4
+ <img width="380" height="200" src="https://glama.ai/mcp/servers/@johannschiffer/pgsafe-mcp/badge" alt="pgsafe-mcp MCP server" />
5
+ </a>
6
+
7
+
8
+ > **Free:** [Genesis Starter Kit](https://johanld.gumroad.com/l/genesis-starter-kit) (25 `.cursorrules`/`CLAUDE.md` rulesets + MCP configs). Try the [live config builder](https://johannschiffer.github.io).
9
+ > **Full stack:** all six servers in [Genesis Enterprise Suite ($19)](https://johanld.gumroad.com/l/genesis-enterprise-suite).
10
+
11
+
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
13
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10+-brightgreen.svg)](https://python.org)
14
+ [![MCP Compliant](https://img.shields.io/badge/MCP-2024--11--05-purple.svg)](https://modelcontextprotocol.io)
15
+
16
+ **PgSafe MCP** is a Zero-Trust Model Context Protocol server that enables AI agents in **Claude Desktop**, **Cursor**, **Windsurf**, and **Antigravity** to inspect schemas and run analytical SQL queries without risking accidental data loss, destructive mutations (`DROP`, `TRUNCATE`, `DELETE`), or runaway table scans.
17
+
18
+ ---
19
+
20
+ ## Run without cloning
21
+
22
+ ```bash
23
+ uvx --from git+https://github.com/johannschiffer/pgsafe-mcp pgsafe-mcp
24
+ ```
25
+
26
+ Claude Desktop / Cursor config entry:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "pgsafe-mcp": {
32
+ "command": "uvx",
33
+ "args": [
34
+ "--from",
35
+ "git+https://github.com/johannschiffer/pgsafe-mcp",
36
+ "pgsafe-mcp"
37
+ ]
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ [![CI](https://github.com/johannschiffer/pgsafe-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/johannschiffer/pgsafe-mcp/actions/workflows/ci.yml)
44
+
45
+ ## 🛡️ Why PgSafe MCP?
46
+
47
+ Giving an AI assistant raw access to your database is terrifying:
48
+ 1. **Accidental Deletions:** One ambiguous prompt can lead an LLM to generate `DROP TABLE` or `DELETE FROM users;`.
49
+ 2. **Production Table Locks:** An unindexed query with a multi-table Cartesian product can consume 100% CPU and lock production tables.
50
+ 3. **Multi-Statement Injection:** LLMs can be tricked via prompt injection into chaining malicious SQL commands (`SELECT 1; TRUNCATE audit_logs;`).
51
+
52
+ **PgSafe** acts as a strict guardrail sitting between the AI agent and your database engine.
53
+
54
+ ---
55
+
56
+ ## ⚡ Features (Lite Edition)
57
+
58
+ - 🔒 **Enforced Read-Only Guard**: Automatically detects and blocks all destructive statements (`DROP`, `TRUNCATE`, `ALTER`, `UPDATE`, `INSERT`, `DELETE`).
59
+ - 🛑 **Anti-Chaining Protection**: Disallows multi-statement execution via semicolons.
60
+ - ⏱️ **Automatic LIMIT Injection**: Ensures every `SELECT` query has a capped `LIMIT` clause (default: 100 rows, max: 1,000) to protect database memory.
61
+ - 📊 **Zero-Cost Schema Reflection**: Tools to explore public tables and columns without executing custom introspection queries.
62
+ - 🪶 **Zero Dependency Core**: Native support for SQLite out of the box; optional PostgreSQL adapter.
63
+
64
+ ---
65
+
66
+ ## 🚀 Quick Setup in 2 Minutes
67
+
68
+ ### 1. Installation
69
+
70
+ ```bash
71
+ git clone https://github.com/johannschiffer/pgsafe-mcp.git
72
+ cd pgsafe-mcp
73
+ pip install .
74
+ ```
75
+
76
+ For PostgreSQL support:
77
+ ```bash
78
+ pip install "psycopg[binary]>=3.1.0"
79
+ ```
80
+
81
+ ### 2. Configure Claude Desktop (`claude_desktop_config.json`)
82
+
83
+ On Linux: `~/.config/Claude/claude_desktop_config.json`
84
+ On macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
85
+ On Windows: `%APPDATA%\Claude\claude_desktop_config.json`
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "pgsafe": {
91
+ "command": "python",
92
+ "args": ["-m", "pgsafe.server"],
93
+ "cwd": "/path/to/pgsafe-mcp",
94
+ "env": {
95
+ "DATABASE_URL": "postgresql://user:password@localhost:5432/production_db"
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ For SQLite, simply point to your `.db` file:
103
+ ```json
104
+ {
105
+ "mcpServers": {
106
+ "pgsafe": {
107
+ "command": "python",
108
+ "args": ["-m", "pgsafe.server"],
109
+ "cwd": "/path/to/pgsafe-mcp",
110
+ "env": {
111
+ "DATABASE_URL": "sqlite:///data/nexus.db"
112
+ }
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
118
+ ---
119
+
120
+ ## 🛠️ MCP Tools Exposed to AI Agents
121
+
122
+ | Tool | Parameters | Description |
123
+ |---|---|---|
124
+ | `list_tables` | *(none)* | Returns list of all user tables in the database |
125
+ | `describe_table` | `table_name` | Returns column names, data types, nullability, and primary keys |
126
+ | `safe_query` | `query`, `limit` | Executes strictly read-only SQL with forced LIMIT enforcement |
127
+ | `analyze_query` | `query` | Inspects SQL for unindexed table scan and Cartesian join risks |
128
+
129
+ ---
130
+
131
+ ## 💎 Need Enterprise Data Protection? Upgrade to PgSafe Pro
132
+
133
+ Running against live production workloads with sensitive customer data?
134
+
135
+ | Feature | Lite (Open Source) | Enterprise Suite ($19) |
136
+ |---|:---:|:---:|
137
+ | **Read-Only SQL Validation** | ✅ | ✅ |
138
+ | **Forced LIMIT Injection** | ✅ | ✅ |
139
+ | **PostgreSQL & SQLite** | ✅ | ✅ |
140
+ | **MySQL, MariaDB & ClickHouse** | ❌ | ✅ |
141
+ | **Automatic PII Redaction & Masking** *(Emails, Cards, Hashes)* | ❌ | ✅ |
142
+ | **Dry-Run Mutation Simulation** *(Auto-Rollback Test Harness)* | ❌ | ✅ |
143
+ | **Index Recommendation Advisor** | ❌ | ✅ |
144
+ | **Production Docker & Compose** | ❌ | ✅ |
145
+ | **Full Pytest Suite & Architecture Guide** | ❌ | ✅ |
146
+
147
+ 👉 **[Get the Genesis Enterprise Suite ($19)](https://johanld.gumroad.com/l/genesis-enterprise-suite)**
148
+
149
+ ---
150
+
151
+ ## 📄 License
152
+
153
+ MIT License. See [LICENSE](LICENSE) for details.
154
+
155
+ ## Genesis MCP Ecosystem
156
+
157
+ | Module | What it does | Repo |
158
+ |---|---|---|
159
+ | **CrawlEngine MCP** | Anti-bot web scraping, clean Markdown, proxy rotation and TLS evasion | [GitHub](https://github.com/johannschiffer/crawlengine-mcp) |
160
+ | **BrowserBridge MCP** | Deterministic headless browser automation with cryptographic action receipts | [GitHub](https://github.com/johannschiffer/browserbridge-mcp) |
161
+ | **PgSafe MCP** (this repo) | Zero-trust PostgreSQL/SQLite guard, live PII masking, zero-downtime migration checks | [GitHub](https://github.com/johannschiffer/pgsafe-mcp) |
162
+ | **OmniBridge MCP** | Multi-channel notification and webhook bus with a persistent SQLite WAL queue | [GitHub](https://github.com/johannschiffer/omnibridge-mcp) |
163
+ | **SysFlow MCP** | Safe Linux administration plane: systemd, port audit, isolated execution | [GitHub](https://github.com/johannschiffer/sysflow-mcp) |
164
+ | **FastMCP Forge** | Production MCP server scaffolder: stdio, SSE and auth middleware | [GitHub](https://github.com/johannschiffer/fastmcp-forge) |
165
+
166
+ [Live config builder](https://johannschiffer.github.io) | [Starter Kit, free](https://johanld.gumroad.com/l/genesis-starter-kit) | [Enterprise Suite, $19](https://johanld.gumroad.com/l/genesis-enterprise-suite)
@@ -0,0 +1,8 @@
1
+ """
2
+ PgSafe MCP (Lite Edition)
3
+ Zero-Trust Database Inspection and Safe Query Guard MCP Server for AI Agents.
4
+ """
5
+
6
+ __version__ = "1.0.0"
7
+ __author__ = "Johann Schiffer / NEXUS"
8
+ __license__ = "MIT"
@@ -0,0 +1,11 @@
1
+ """Upgrade notice. Printed to stderr so MCP stdio traffic on stdout is never affected."""
2
+ import os
3
+ import sys
4
+
5
+ NOTICE = 'PgSafe MCP (open edition). Starter Kit, free: https://johanld.gumroad.com/l/khffw | Enterprise Suite ($19): https://johanld.gumroad.com/l/tzuvz'
6
+
7
+
8
+ def print_upgrade_notice():
9
+ if os.environ.get("GENESIS_NO_NOTICE") == "1":
10
+ return
11
+ print(NOTICE, file=sys.stderr)
@@ -0,0 +1,26 @@
1
+ """
2
+ Base Database Adapter Interface for PgSafe.
3
+ """
4
+ from abc import ABC, abstractmethod
5
+ from typing import List, Dict, Any, Tuple
6
+
7
+ class BaseAdapter(ABC):
8
+ @abstractmethod
9
+ def connect(self):
10
+ """Establishes database connection."""
11
+ pass
12
+
13
+ @abstractmethod
14
+ def list_tables(self) -> List[str]:
15
+ """Returns list of user tables in the database."""
16
+ pass
17
+
18
+ @abstractmethod
19
+ def describe_table(self, table_name: str) -> List[Dict[str, Any]]:
20
+ """Returns column names, types, and constraints for a table."""
21
+ pass
22
+
23
+ @abstractmethod
24
+ def execute_read_only(self, sql: str) -> Tuple[List[str], List[List[Any]]]:
25
+ """Executes a read-only query and returns (columns, rows)."""
26
+ pass
@@ -0,0 +1,72 @@
1
+ """
2
+ PostgreSQL Database Adapter with READ ONLY transaction guarantees.
3
+ """
4
+ from typing import List, Dict, Any, Tuple, Optional
5
+ from .base import BaseAdapter
6
+
7
+ class PostgresAdapter(BaseAdapter):
8
+ def __init__(self, connection_url: str):
9
+ self.connection_url = connection_url
10
+ self._conn = None
11
+
12
+ def connect(self):
13
+ if self._conn:
14
+ return self._conn
15
+
16
+ try:
17
+ # Attempt psycopg2 or psycopg or asyncpg
18
+ import psycopg
19
+ self._conn = psycopg.connect(self.connection_url)
20
+ # Guarantee read-only session at PostgreSQL engine level
21
+ with self._conn.cursor() as cur:
22
+ cur.execute("SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;")
23
+ return self._conn
24
+ except ImportError:
25
+ try:
26
+ import psycopg2
27
+ self._conn = psycopg2.connect(self.connection_url)
28
+ self._conn.set_session(readonly=True)
29
+ return self._conn
30
+ except ImportError:
31
+ raise RuntimeError(
32
+ "PostgreSQL driver not installed. Install psycopg via: pip install psycopg[binary] "
33
+ "or use SQLite adapter."
34
+ )
35
+
36
+ def list_tables(self) -> List[str]:
37
+ conn = self.connect()
38
+ with conn.cursor() as cur:
39
+ cur.execute("""
40
+ SELECT table_name
41
+ FROM information_schema.tables
42
+ WHERE table_schema = 'public'
43
+ ORDER BY table_name;
44
+ """)
45
+ return [r[0] for r in cur.fetchall()]
46
+
47
+ def describe_table(self, table_name: str) -> List[Dict[str, Any]]:
48
+ conn = self.connect()
49
+ with conn.cursor() as cur:
50
+ cur.execute("""
51
+ SELECT column_name, data_type, is_nullable, column_default
52
+ FROM information_schema.columns
53
+ WHERE table_schema = 'public' AND table_name = %s
54
+ ORDER BY ordinal_position;
55
+ """, (table_name,))
56
+ return [
57
+ {
58
+ "name": r[0],
59
+ "type": r[1],
60
+ "nullable": r[2] == "YES",
61
+ "default": r[3]
62
+ }
63
+ for r in cur.fetchall()
64
+ ]
65
+
66
+ def execute_read_only(self, sql: str) -> Tuple[List[str], List[List[Any]]]:
67
+ conn = self.connect()
68
+ with conn.cursor() as cur:
69
+ cur.execute(sql)
70
+ columns = [desc[0] for desc in cur.description] if cur.description else []
71
+ rows = [list(r) for r in cur.fetchall()]
72
+ return columns, rows
@@ -0,0 +1,57 @@
1
+ """
2
+ SQLite Database Adapter with Read-Only URI enforcement.
3
+ """
4
+ import sqlite3
5
+ from typing import List, Dict, Any, Tuple
6
+ from .base import BaseAdapter
7
+
8
+ class SQLiteAdapter(BaseAdapter):
9
+ def __init__(self, db_path: str):
10
+ self.db_path = db_path
11
+ self._conn = None
12
+
13
+ def connect(self):
14
+ if not self._conn:
15
+ # Enforce read-only mode via SQLite URI if file exists
16
+ uri = f"file:{self.db_path}?mode=ro"
17
+ try:
18
+ self._conn = sqlite3.connect(uri, uri=True, timeout=10.0)
19
+ except sqlite3.OperationalError:
20
+ # Fallback to standard connect with PRAGMA query_only
21
+ self._conn = sqlite3.connect(self.db_path, timeout=10.0)
22
+ self._conn.execute("PRAGMA query_only = ON;")
23
+ self._conn.row_factory = sqlite3.Row
24
+ return self._conn
25
+
26
+ def list_tables(self) -> List[str]:
27
+ conn = self.connect()
28
+ cursor = conn.cursor()
29
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name;")
30
+ return [row[0] for row in cursor.fetchall()]
31
+
32
+ def describe_table(self, table_name: str) -> List[Dict[str, Any]]:
33
+ conn = self.connect()
34
+ cursor = conn.cursor()
35
+ # Sanitize table_name to identifier
36
+ safe_name = "".join(c for c in table_name if c.isalnum() or c == "_")
37
+ cursor.execute(f"PRAGMA table_info({safe_name});")
38
+ rows = cursor.fetchall()
39
+ return [
40
+ {
41
+ "cid": r[0],
42
+ "name": r[1],
43
+ "type": r[2],
44
+ "notnull": bool(r[3]),
45
+ "dflt_value": r[4],
46
+ "pk": bool(r[5])
47
+ }
48
+ for r in rows
49
+ ]
50
+
51
+ def execute_read_only(self, sql: str) -> Tuple[List[str], List[List[Any]]]:
52
+ conn = self.connect()
53
+ cursor = conn.cursor()
54
+ cursor.execute(sql)
55
+ columns = [desc[0] for desc in cursor.description] if cursor.description else []
56
+ rows = [list(r) for r in cursor.fetchall()]
57
+ return columns, rows
@@ -0,0 +1,74 @@
1
+ """
2
+ PgSafe CLI inspection tool.
3
+ Usage:
4
+ python -m pgsafe.cli tables --db sqlite:///demo.db
5
+ python -m pgsafe.cli describe users --db sqlite:///demo.db
6
+ python -m pgsafe.cli query "SELECT * FROM users" --db sqlite:///demo.db
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from .adapters.sqlite_adapter import SQLiteAdapter
13
+ from .adapters.postgres_adapter import PostgresAdapter
14
+ from .guards.readonly import ReadOnlyGuard
15
+ from .guards.cost_analyzer import CostAnalyzer
16
+
17
+ def main():
18
+ parser = argparse.ArgumentParser(description="PgSafe CLI database utility")
19
+ parser.add_argument("--db", default="sqlite:///data/nexus.db", help="Database connection URL")
20
+ subparsers = parser.add_subparsers(dest="command", help="Sub-commands")
21
+
22
+ subparsers.add_parser("tables", help="List database tables")
23
+
24
+ desc_parser = subparsers.add_parser("describe", help="Describe table structure")
25
+ desc_parser.add_argument("table", help="Table name")
26
+
27
+ query_parser = subparsers.add_parser("query", help="Execute safe read-only query")
28
+ query_parser.add_argument("sql", help="SQL query")
29
+ query_parser.add_argument("--limit", type=int, default=100, help="Row limit")
30
+
31
+ args = parser.parse_args()
32
+
33
+ url = args.db
34
+ if url.startswith("postgres://") or url.startswith("postgresql://"):
35
+ adapter = PostgresAdapter(url)
36
+ else:
37
+ clean_path = url.replace("sqlite:///", "").replace("sqlite://", "")
38
+ adapter = SQLiteAdapter(clean_path)
39
+
40
+ if args.command == "tables":
41
+ tables = adapter.list_tables()
42
+ print(json.dumps({"tables": tables, "count": len(tables)}, indent=2))
43
+
44
+ elif args.command == "describe":
45
+ cols = adapter.describe_table(args.table)
46
+ print(json.dumps({"table": args.table, "columns": cols}, indent=2))
47
+
48
+ elif args.command == "query":
49
+ is_safe, reason = ReadOnlyGuard.validate(args.sql)
50
+ if not is_safe:
51
+ print(f"[SECURITY BLOCKED] {reason}", file=sys.stderr)
52
+ sys.exit(1)
53
+
54
+ safe_sql = CostAnalyzer.enforce_limit(args.sql, default_limit=args.limit)
55
+ cols, rows = adapter.execute_read_only(safe_sql)
56
+ print(json.dumps({"columns": cols, "rows": rows, "count": len(rows)}, indent=2))
57
+
58
+ else:
59
+ parser.print_help()
60
+
61
+
62
+
63
+ _main_impl = main
64
+
65
+
66
+ def main():
67
+ from ._genesis import print_upgrade_notice
68
+ try:
69
+ return _main_impl()
70
+ finally:
71
+ print_upgrade_notice()
72
+
73
+ if __name__ == "__main__":
74
+ main()
@@ -0,0 +1,41 @@
1
+ """
2
+ Query Cost and Performance Safety Guard.
3
+ Enforces query limits and guards against unconstrained table scans.
4
+ """
5
+ import re
6
+
7
+ class CostAnalyzer:
8
+ @staticmethod
9
+ def enforce_limit(sql: str, default_limit: int = 100, max_limit: int = 1000) -> str:
10
+ """
11
+ Inspects SQL and ensures a LIMIT clause is applied.
12
+ If a LIMIT is present and exceeds max_limit, caps it.
13
+ If no LIMIT is present, appends default_limit.
14
+ """
15
+ cleaned = sql.strip().rstrip(";")
16
+ limit_match = re.search(r"\bLIMIT\s+(\d+)\b", cleaned, re.IGNORECASE)
17
+
18
+ if limit_match:
19
+ current_limit = int(limit_match.group(1))
20
+ if current_limit > max_limit:
21
+ return re.sub(r"\bLIMIT\s+\d+\b", f"LIMIT {max_limit}", cleaned, flags=re.IGNORECASE)
22
+ return cleaned
23
+ else:
24
+ return f"{cleaned} LIMIT {default_limit}"
25
+
26
+ @staticmethod
27
+ def evaluate_risk(sql: str) -> dict:
28
+ """Evaluates heuristic risk metrics."""
29
+ warnings = []
30
+ sql_upper = sql.upper()
31
+
32
+ if "WHERE" not in sql_upper and "SELECT" in sql_upper:
33
+ warnings.append("Query lacks a WHERE clause; full table scan may occur.")
34
+
35
+ if "JOIN" in sql_upper and "ON" not in sql_upper and "USING" not in sql_upper:
36
+ warnings.append("JOIN statement without explicit ON/USING condition (possible Cartesian product).")
37
+
38
+ return {
39
+ "risk_level": "medium" if warnings else "low",
40
+ "warnings": warnings
41
+ }
@@ -0,0 +1,57 @@
1
+ """
2
+ SQL Read-Only Security Guard.
3
+ Inspects SQL statements to guarantee no mutations or destructive statements are executed.
4
+ """
5
+ import re
6
+ from typing import Tuple
7
+
8
+ BLOCKED_PATTERNS = [
9
+ r"\bDROP\b",
10
+ r"\bTRUNCATE\b",
11
+ r"\bDELETE\b",
12
+ r"\bUPDATE\b",
13
+ r"\bINSERT\b",
14
+ r"\bALTER\b",
15
+ r"\bCREATE\b",
16
+ r"\bGRANT\b",
17
+ r"\bREVOKE\b",
18
+ r"\bEXEC\b",
19
+ r"\bEXECUTE\b",
20
+ r"\bCALL\b",
21
+ r"\bRENAME\b",
22
+ r"\bREPLACE\b"
23
+ ]
24
+
25
+ COMPILED_BLOCKED = [re.compile(p, re.IGNORECASE) for p in BLOCKED_PATTERNS]
26
+
27
+ class ReadOnlyGuard:
28
+ @staticmethod
29
+ def validate(sql: str) -> Tuple[bool, str]:
30
+ """
31
+ Validates whether SQL is strictly read-only.
32
+ Returns (is_safe: bool, reason: str).
33
+ """
34
+ cleaned = sql.strip()
35
+ if not cleaned:
36
+ return False, "Empty query provided."
37
+
38
+ # Remove SQL comments (-- comment and /* comment */)
39
+ cleaned_no_comments = re.sub(r"--.*?$", "", cleaned, flags=re.MULTILINE)
40
+ cleaned_no_comments = re.sub(r"/\*.*?\*/", "", cleaned_no_comments, flags=re.DOTALL).strip()
41
+
42
+ # Check semicolon chaining (disallow multi-statement injection)
43
+ statements = [s.strip() for s in cleaned_no_comments.split(";") if s.strip()]
44
+ if len(statements) > 1:
45
+ return False, "Multiple SQL statements detected (semicolon chaining disallowed)."
46
+
47
+ first_word = statements[0].split()[0].upper() if statements else ""
48
+ allowed_starts = {"SELECT", "EXPLAIN", "WITH", "PRAGMA", "SHOW", "DESCRIBE"}
49
+ if first_word not in allowed_starts:
50
+ return False, f"Statement type '{first_word}' is disallowed. Only read-only queries are permitted."
51
+
52
+ for pattern in COMPILED_BLOCKED:
53
+ if pattern.search(cleaned_no_comments):
54
+ match = pattern.pattern.replace(r"\b", "")
55
+ return False, f"Blocked keyword detected in query: '{match}'"
56
+
57
+ return True, "Query verified as read-only."
@@ -0,0 +1,248 @@
1
+ """
2
+ PgSafe MCP: Model Context Protocol Server (Lite Edition)
3
+ Implements standard JSON-RPC 2.0 stdio transport with Zero-Trust database access.
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ import json
9
+ import logging
10
+ from typing import Dict, Any, List, Optional
11
+ from .guards.readonly import ReadOnlyGuard
12
+ from .guards.cost_analyzer import CostAnalyzer
13
+ from .adapters.sqlite_adapter import SQLiteAdapter
14
+ from .adapters.postgres_adapter import PostgresAdapter
15
+
16
+ logging.basicConfig(
17
+ level=logging.INFO,
18
+ format="%(asctime)s [%(levelname)s] [PgSafe] %(message)s",
19
+ stream=sys.stderr
20
+ )
21
+ logger = logging.getLogger("PgSafeServer")
22
+
23
+ class PgSafeMCPServer:
24
+ def __init__(self, db_url: Optional[str] = None):
25
+ self.db_url = db_url or os.getenv("DATABASE_URL", "sqlite:///demo.db")
26
+ self.server_name = "pgsafe-mcp"
27
+ self.server_version = "1.0.0"
28
+ self.adapter = self._init_adapter(self.db_url)
29
+
30
+ def _init_adapter(self, url: str):
31
+ if url.startswith("postgres://") or url.startswith("postgresql://"):
32
+ return PostgresAdapter(url)
33
+ else:
34
+ # Assume SQLite path or sqlite:///
35
+ clean_path = url.replace("sqlite:///", "").replace("sqlite://", "")
36
+ return SQLiteAdapter(clean_path)
37
+
38
+ def get_tools_definition(self) -> List[Dict[str, Any]]:
39
+ return [
40
+ {
41
+ "name": "list_tables",
42
+ "description": "List all public database tables available for inspection and querying.",
43
+ "inputSchema": {
44
+ "type": "object",
45
+ "properties": {}
46
+ }
47
+ },
48
+ {
49
+ "name": "describe_table",
50
+ "description": "Retrieve column definitions, data types, nullability, and keys for a specific table.",
51
+ "inputSchema": {
52
+ "type": "object",
53
+ "properties": {
54
+ "table_name": {
55
+ "type": "string",
56
+ "description": "Name of the table to inspect"
57
+ }
58
+ },
59
+ "required": ["table_name"]
60
+ }
61
+ },
62
+ {
63
+ "name": "safe_query",
64
+ "description": "Execute a strictly read-only SQL query with automatic LIMIT enforcement and mutation blocking.",
65
+ "inputSchema": {
66
+ "type": "object",
67
+ "properties": {
68
+ "query": {
69
+ "type": "string",
70
+ "description": "SQL query to execute (must be SELECT/EXPLAIN)"
71
+ },
72
+ "limit": {
73
+ "type": "integer",
74
+ "description": "Maximum rows to return (default: 100, max: 1000)"
75
+ }
76
+ },
77
+ "required": ["query"]
78
+ }
79
+ },
80
+ {
81
+ "name": "analyze_query",
82
+ "description": "Analyze a SQL query for dangerous operations, unindexed scans, and Cartesian join risks without executing it.",
83
+ "inputSchema": {
84
+ "type": "object",
85
+ "properties": {
86
+ "query": {
87
+ "type": "string",
88
+ "description": "SQL query to inspect"
89
+ }
90
+ },
91
+ "required": ["query"]
92
+ }
93
+ }
94
+ ]
95
+
96
+ def handle_tool_call(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
97
+ if name == "list_tables":
98
+ tables = self.adapter.list_tables()
99
+ return {"success": True, "tables": tables, "count": len(tables)}
100
+
101
+ elif name == "describe_table":
102
+ table_name = arguments.get("table_name", "")
103
+ columns = self.adapter.describe_table(table_name)
104
+ return {"success": True, "table": table_name, "columns": columns}
105
+
106
+ elif name == "safe_query":
107
+ raw_sql = arguments.get("query", "")
108
+ limit = arguments.get("limit", 100)
109
+
110
+ # 1. Read-only validation
111
+ is_safe, reason = ReadOnlyGuard.validate(raw_sql)
112
+ if not is_safe:
113
+ return {
114
+ "success": False,
115
+ "error": f"Security Guard Blocked Query: {reason}",
116
+ "blocked": True
117
+ }
118
+
119
+ # 2. Limit enforcement & risk evaluation
120
+ safe_sql = CostAnalyzer.enforce_limit(raw_sql, default_limit=limit, max_limit=1000)
121
+ risk = CostAnalyzer.evaluate_risk(raw_sql)
122
+
123
+ # 3. Execution
124
+ try:
125
+ cols, rows = self.adapter.execute_read_only(safe_sql)
126
+ return {
127
+ "success": True,
128
+ "query_executed": safe_sql,
129
+ "columns": cols,
130
+ "rows": rows,
131
+ "row_count": len(rows),
132
+ "risk_analysis": risk
133
+ }
134
+ except Exception as e:
135
+ return {
136
+ "success": False,
137
+ "error": f"Database execution error: {str(e)}"
138
+ }
139
+
140
+ elif name == "analyze_query":
141
+ raw_sql = arguments.get("query", "")
142
+ is_safe, reason = ReadOnlyGuard.validate(raw_sql)
143
+ risk = CostAnalyzer.evaluate_risk(raw_sql)
144
+ return {
145
+ "success": True,
146
+ "is_read_only": is_safe,
147
+ "security_verdict": reason,
148
+ "performance_risk": risk
149
+ }
150
+
151
+ else:
152
+ raise ValueError(f"Unknown tool: {name}")
153
+
154
+ def handle_request(self, request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
155
+ req_id = request.get("id")
156
+ method = request.get("method")
157
+ params = request.get("params", {})
158
+
159
+ if req_id is None:
160
+ if method == "notifications/initialized":
161
+ logger.info("Client completed initialization handshake.")
162
+ return None
163
+
164
+ if method == "initialize":
165
+ return {
166
+ "jsonrpc": "2.0",
167
+ "id": req_id,
168
+ "result": {
169
+ "protocolVersion": "2024-11-05",
170
+ "capabilities": {"tools": {}},
171
+ "serverInfo": {
172
+ "name": self.server_name,
173
+ "version": self.server_version
174
+ }
175
+ }
176
+ }
177
+
178
+ elif method == "ping":
179
+ return {"jsonrpc": "2.0", "id": req_id, "result": {}}
180
+
181
+ elif method == "tools/list":
182
+ return {
183
+ "jsonrpc": "2.0",
184
+ "id": req_id,
185
+ "result": {"tools": self.get_tools_definition()}
186
+ }
187
+
188
+ elif method == "tools/call":
189
+ tool_name = params.get("name")
190
+ tool_args = params.get("arguments", {})
191
+ try:
192
+ output = self.handle_tool_call(tool_name, tool_args)
193
+ return {
194
+ "jsonrpc": "2.0",
195
+ "id": req_id,
196
+ "result": {
197
+ "content": [
198
+ {
199
+ "type": "text",
200
+ "text": json.dumps(output, indent=2, ensure_ascii=False)
201
+ }
202
+ ],
203
+ "isError": not output.get("success", True) if "success" in output else False
204
+ }
205
+ }
206
+ except Exception as e:
207
+ logger.error(f"Error calling tool {tool_name}: {e}")
208
+ return {
209
+ "jsonrpc": "2.0",
210
+ "id": req_id,
211
+ "error": {"code": -32000, "message": str(e)}
212
+ }
213
+ else:
214
+ return {
215
+ "jsonrpc": "2.0",
216
+ "id": req_id,
217
+ "error": {"code": -32601, "message": f"Method not found: {method}"}
218
+ }
219
+
220
+ def run_stdio(self):
221
+ logger.info(f"Starting {self.server_name} v{self.server_version} stdio listener...")
222
+ for line in sys.stdin:
223
+ line = line.strip()
224
+ if not line:
225
+ continue
226
+ try:
227
+ request = json.loads(line)
228
+ response = self.handle_request(request)
229
+ if response is not None:
230
+ sys.stdout.write(json.dumps(response) + "\n")
231
+ sys.stdout.flush()
232
+ except Exception as e:
233
+ logger.error(f"Error processing stdio line: {e}")
234
+
235
+ def main():
236
+ import argparse
237
+ parser = argparse.ArgumentParser(
238
+ prog="pgsafe-mcp",
239
+ description="PgSafe MCP: Zero-Trust Database Inspection & Safe Query Guard for AI Agents"
240
+ )
241
+ parser.add_argument("-v", "--version", action="version", version="%(prog)s 1.0.0")
242
+ parser.add_argument("--db-url", type=str, default=None, help="Database connection URL (Postgres or SQLite). Defaults to DATABASE_URL or sqlite:///demo.db")
243
+ args, unknown = parser.parse_known_args()
244
+ server = PgSafeMCPServer(db_url=args.db_url)
245
+ server.run_stdio()
246
+
247
+ if __name__ == "__main__":
248
+ main()
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: pgsafe-mcp
3
+ Version: 1.0.0
4
+ Summary: Zero-Trust Database Inspection and Safe Query Guard MCP Server for AI Agents
5
+ Author: Johann Schiffer
6
+ License: MIT
7
+ Project-URL: Homepage, https://johannschiffer.github.io
8
+ Project-URL: Repository, https://github.com/johannschiffer/pgsafe-mcp
9
+ Project-URL: Starter Kit, https://johanld.gumroad.com/l/genesis-starter-kit
10
+ Project-URL: Enterprise Suite, https://johanld.gumroad.com/l/genesis-enterprise-suite
11
+ Keywords: mcp,model-context-protocol,postgres,sqlite,database,claude,cursor,security,ai-agent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=7; extra == "test"
17
+ Provides-Extra: postgres
18
+ Requires-Dist: psycopg[binary]>=3.1.0; extra == "postgres"
19
+ Dynamic: license-file
20
+
21
+ # PgSafe MCP (Lite Edition)
22
+
23
+ <a href="https://glama.ai/mcp/servers/@johannschiffer/pgsafe-mcp">
24
+ <img width="380" height="200" src="https://glama.ai/mcp/servers/@johannschiffer/pgsafe-mcp/badge" alt="pgsafe-mcp MCP server" />
25
+ </a>
26
+
27
+
28
+ > **Free:** [Genesis Starter Kit](https://johanld.gumroad.com/l/genesis-starter-kit) (25 `.cursorrules`/`CLAUDE.md` rulesets + MCP configs). Try the [live config builder](https://johannschiffer.github.io).
29
+ > **Full stack:** all six servers in [Genesis Enterprise Suite ($19)](https://johanld.gumroad.com/l/genesis-enterprise-suite).
30
+
31
+
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
33
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10+-brightgreen.svg)](https://python.org)
34
+ [![MCP Compliant](https://img.shields.io/badge/MCP-2024--11--05-purple.svg)](https://modelcontextprotocol.io)
35
+
36
+ **PgSafe MCP** is a Zero-Trust Model Context Protocol server that enables AI agents in **Claude Desktop**, **Cursor**, **Windsurf**, and **Antigravity** to inspect schemas and run analytical SQL queries without risking accidental data loss, destructive mutations (`DROP`, `TRUNCATE`, `DELETE`), or runaway table scans.
37
+
38
+ ---
39
+
40
+ ## Run without cloning
41
+
42
+ ```bash
43
+ uvx --from git+https://github.com/johannschiffer/pgsafe-mcp pgsafe-mcp
44
+ ```
45
+
46
+ Claude Desktop / Cursor config entry:
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "pgsafe-mcp": {
52
+ "command": "uvx",
53
+ "args": [
54
+ "--from",
55
+ "git+https://github.com/johannschiffer/pgsafe-mcp",
56
+ "pgsafe-mcp"
57
+ ]
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ [![CI](https://github.com/johannschiffer/pgsafe-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/johannschiffer/pgsafe-mcp/actions/workflows/ci.yml)
64
+
65
+ ## 🛡️ Why PgSafe MCP?
66
+
67
+ Giving an AI assistant raw access to your database is terrifying:
68
+ 1. **Accidental Deletions:** One ambiguous prompt can lead an LLM to generate `DROP TABLE` or `DELETE FROM users;`.
69
+ 2. **Production Table Locks:** An unindexed query with a multi-table Cartesian product can consume 100% CPU and lock production tables.
70
+ 3. **Multi-Statement Injection:** LLMs can be tricked via prompt injection into chaining malicious SQL commands (`SELECT 1; TRUNCATE audit_logs;`).
71
+
72
+ **PgSafe** acts as a strict guardrail sitting between the AI agent and your database engine.
73
+
74
+ ---
75
+
76
+ ## ⚡ Features (Lite Edition)
77
+
78
+ - 🔒 **Enforced Read-Only Guard**: Automatically detects and blocks all destructive statements (`DROP`, `TRUNCATE`, `ALTER`, `UPDATE`, `INSERT`, `DELETE`).
79
+ - 🛑 **Anti-Chaining Protection**: Disallows multi-statement execution via semicolons.
80
+ - ⏱️ **Automatic LIMIT Injection**: Ensures every `SELECT` query has a capped `LIMIT` clause (default: 100 rows, max: 1,000) to protect database memory.
81
+ - 📊 **Zero-Cost Schema Reflection**: Tools to explore public tables and columns without executing custom introspection queries.
82
+ - 🪶 **Zero Dependency Core**: Native support for SQLite out of the box; optional PostgreSQL adapter.
83
+
84
+ ---
85
+
86
+ ## 🚀 Quick Setup in 2 Minutes
87
+
88
+ ### 1. Installation
89
+
90
+ ```bash
91
+ git clone https://github.com/johannschiffer/pgsafe-mcp.git
92
+ cd pgsafe-mcp
93
+ pip install .
94
+ ```
95
+
96
+ For PostgreSQL support:
97
+ ```bash
98
+ pip install "psycopg[binary]>=3.1.0"
99
+ ```
100
+
101
+ ### 2. Configure Claude Desktop (`claude_desktop_config.json`)
102
+
103
+ On Linux: `~/.config/Claude/claude_desktop_config.json`
104
+ On macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
105
+ On Windows: `%APPDATA%\Claude\claude_desktop_config.json`
106
+
107
+ ```json
108
+ {
109
+ "mcpServers": {
110
+ "pgsafe": {
111
+ "command": "python",
112
+ "args": ["-m", "pgsafe.server"],
113
+ "cwd": "/path/to/pgsafe-mcp",
114
+ "env": {
115
+ "DATABASE_URL": "postgresql://user:password@localhost:5432/production_db"
116
+ }
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ For SQLite, simply point to your `.db` file:
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "pgsafe": {
127
+ "command": "python",
128
+ "args": ["-m", "pgsafe.server"],
129
+ "cwd": "/path/to/pgsafe-mcp",
130
+ "env": {
131
+ "DATABASE_URL": "sqlite:///data/nexus.db"
132
+ }
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ## 🛠️ MCP Tools Exposed to AI Agents
141
+
142
+ | Tool | Parameters | Description |
143
+ |---|---|---|
144
+ | `list_tables` | *(none)* | Returns list of all user tables in the database |
145
+ | `describe_table` | `table_name` | Returns column names, data types, nullability, and primary keys |
146
+ | `safe_query` | `query`, `limit` | Executes strictly read-only SQL with forced LIMIT enforcement |
147
+ | `analyze_query` | `query` | Inspects SQL for unindexed table scan and Cartesian join risks |
148
+
149
+ ---
150
+
151
+ ## 💎 Need Enterprise Data Protection? Upgrade to PgSafe Pro
152
+
153
+ Running against live production workloads with sensitive customer data?
154
+
155
+ | Feature | Lite (Open Source) | Enterprise Suite ($19) |
156
+ |---|:---:|:---:|
157
+ | **Read-Only SQL Validation** | ✅ | ✅ |
158
+ | **Forced LIMIT Injection** | ✅ | ✅ |
159
+ | **PostgreSQL & SQLite** | ✅ | ✅ |
160
+ | **MySQL, MariaDB & ClickHouse** | ❌ | ✅ |
161
+ | **Automatic PII Redaction & Masking** *(Emails, Cards, Hashes)* | ❌ | ✅ |
162
+ | **Dry-Run Mutation Simulation** *(Auto-Rollback Test Harness)* | ❌ | ✅ |
163
+ | **Index Recommendation Advisor** | ❌ | ✅ |
164
+ | **Production Docker & Compose** | ❌ | ✅ |
165
+ | **Full Pytest Suite & Architecture Guide** | ❌ | ✅ |
166
+
167
+ 👉 **[Get the Genesis Enterprise Suite ($19)](https://johanld.gumroad.com/l/genesis-enterprise-suite)**
168
+
169
+ ---
170
+
171
+ ## 📄 License
172
+
173
+ MIT License. See [LICENSE](LICENSE) for details.
174
+
175
+ ## Genesis MCP Ecosystem
176
+
177
+ | Module | What it does | Repo |
178
+ |---|---|---|
179
+ | **CrawlEngine MCP** | Anti-bot web scraping, clean Markdown, proxy rotation and TLS evasion | [GitHub](https://github.com/johannschiffer/crawlengine-mcp) |
180
+ | **BrowserBridge MCP** | Deterministic headless browser automation with cryptographic action receipts | [GitHub](https://github.com/johannschiffer/browserbridge-mcp) |
181
+ | **PgSafe MCP** (this repo) | Zero-trust PostgreSQL/SQLite guard, live PII masking, zero-downtime migration checks | [GitHub](https://github.com/johannschiffer/pgsafe-mcp) |
182
+ | **OmniBridge MCP** | Multi-channel notification and webhook bus with a persistent SQLite WAL queue | [GitHub](https://github.com/johannschiffer/omnibridge-mcp) |
183
+ | **SysFlow MCP** | Safe Linux administration plane: systemd, port audit, isolated execution | [GitHub](https://github.com/johannschiffer/sysflow-mcp) |
184
+ | **FastMCP Forge** | Production MCP server scaffolder: stdio, SSE and auth middleware | [GitHub](https://github.com/johannschiffer/fastmcp-forge) |
185
+
186
+ [Live config builder](https://johannschiffer.github.io) | [Starter Kit, free](https://johanld.gumroad.com/l/genesis-starter-kit) | [Enterprise Suite, $19](https://johanld.gumroad.com/l/genesis-enterprise-suite)
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ pgsafe/__init__.py
5
+ pgsafe/_genesis.py
6
+ pgsafe/cli.py
7
+ pgsafe/server.py
8
+ pgsafe/adapters/base.py
9
+ pgsafe/adapters/postgres_adapter.py
10
+ pgsafe/adapters/sqlite_adapter.py
11
+ pgsafe/guards/cost_analyzer.py
12
+ pgsafe/guards/readonly.py
13
+ pgsafe_mcp.egg-info/PKG-INFO
14
+ pgsafe_mcp.egg-info/SOURCES.txt
15
+ pgsafe_mcp.egg-info/dependency_links.txt
16
+ pgsafe_mcp.egg-info/entry_points.txt
17
+ pgsafe_mcp.egg-info/requires.txt
18
+ pgsafe_mcp.egg-info/top_level.txt
19
+ tests/test_genesis_entrypoints.py
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ pgsafe-cli = pgsafe.cli:main
3
+ pgsafe-mcp = pgsafe.server:main
@@ -0,0 +1,6 @@
1
+
2
+ [postgres]
3
+ psycopg[binary]>=3.1.0
4
+
5
+ [test]
6
+ pytest>=7
@@ -0,0 +1 @@
1
+ pgsafe
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pgsafe-mcp"
7
+ version = "1.0.0"
8
+ description = "Zero-Trust Database Inspection and Safe Query Guard MCP Server for AI Agents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Johann Schiffer"}
14
+ ]
15
+ keywords = ["mcp", "model-context-protocol", "postgres", "sqlite", "database", "claude", "cursor", "security", "ai-agent"]
16
+ dependencies = []
17
+
18
+ [project.optional-dependencies]
19
+ test = ["pytest>=7"]
20
+ postgres = ["psycopg[binary]>=3.1.0"]
21
+
22
+ [project.scripts]
23
+ pgsafe-mcp = "pgsafe.server:main"
24
+ pgsafe-cli = "pgsafe.cli:main"
25
+
26
+ [tool.setuptools.packages.find]
27
+ include = ["pgsafe*"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://johannschiffer.github.io"
31
+ Repository = "https://github.com/johannschiffer/pgsafe-mcp"
32
+ "Starter Kit" = "https://johanld.gumroad.com/l/genesis-starter-kit"
33
+ "Enterprise Suite" = "https://johanld.gumroad.com/l/genesis-enterprise-suite"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ import importlib
2
+ import io
3
+ import os
4
+ import sys
5
+
6
+
7
+ def test_script_target_is_callable():
8
+ mod = importlib.import_module('pgsafe.server')
9
+ assert callable(getattr(mod, 'main'))
10
+
11
+
12
+ def test_upgrade_notice_goes_to_stderr(capsys, monkeypatch):
13
+ monkeypatch.delenv('GENESIS_NO_NOTICE', raising=False)
14
+ from pgsafe._genesis import print_upgrade_notice
15
+ print_upgrade_notice()
16
+ out = capsys.readouterr()
17
+ assert out.out == ''
18
+ assert 'gumroad.com' in out.err
19
+
20
+
21
+ def test_upgrade_notice_can_be_silenced(capsys, monkeypatch):
22
+ monkeypatch.setenv('GENESIS_NO_NOTICE', '1')
23
+ from pgsafe._genesis import print_upgrade_notice
24
+ print_upgrade_notice()
25
+ assert capsys.readouterr().err == ''