Skip to content

Commit 3a5ec0c

Browse files
committed
Allow SHOW/DESCRIBE in read-only mode, add sql.assistant.role prompt
- Policy: allow SHOW, DESCRIBE, DESC as read-only statements (MySQL/PG support) - Add sql.assistant.role MCP prompt with rules for tool usage and dialect detection - Update sql.query description to list all allowed statement types - Add pymysql to requirements.txt for MySQL support - README: add ChatGPT custom instructions, update tool/prompt tables
1 parent a09b71f commit 3a5ec0c

6 files changed

Lines changed: 82 additions & 4 deletions

File tree

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ SSE endpoint: http://localhost:8123/mcp (GET for Server-Sent Events)
6363

6464
| Tool | Description |
6565
|---|---|
66-
| `sql.query` | Execute read-only SQL queries (SELECT, WITH, EXPLAIN) |
66+
| `sql.query` | Execute read-only SQL queries (SELECT, WITH, EXPLAIN, SHOW, DESCRIBE) |
6767
| `sql.schema` | Introspect tables, columns, types, and indexes |
6868
| `sql.explain` | Get EXPLAIN plan for a query |
6969
| `db.design` | Generate a desired schema template |
@@ -84,9 +84,33 @@ SSE endpoint: http://localhost:8123/mcp (GET for Server-Sent Events)
8484

8585
| Prompt | Description |
8686
|---|---|
87+
| `sql.assistant.role` | System prompt with rules for correct tool usage, dialect detection, and schema discovery |
8788
| `sql.query.plan` | Generate a safe SQL query plan from a natural language question |
8889
| `db.design.schema` | Propose a SQL schema from a domain description |
8990

91+
> **Note:** ChatGPT's MCP connector supports only tools, not prompts or resources.
92+
> If you use ChatGPT, paste the following into your Custom Instructions:
93+
94+
<details>
95+
<summary>Recommended system prompt for ChatGPT</summary>
96+
97+
```
98+
You are a database assistant connected to a live SQL database via MCP
99+
(Model Context Protocol).
100+
101+
Rules:
102+
1. ALWAYS call sql.schema first at the start of each conversation to get
103+
the current schema. Never assume or cache table names from previous messages.
104+
2. Read the sql.query tool description — it tells you the database engine
105+
(SQLite, MySQL, PostgreSQL). Use the correct SQL dialect.
106+
3. For read queries use sql.query. For writes use db.apply. Never mix them.
107+
4. If a query fails with a syntax error, check the dialect and retry
108+
with correct syntax.
109+
5. Prefer sql.schema over SHOW TABLES / information_schema for discovering tables.
110+
6. When presenting results, format them as clean tables. Keep explanations concise.
111+
```
112+
</details>
113+
90114
## Admin UI
91115

92116
The built-in web UI provides:

app/mcp/prompts.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
from app.config import Config
44

5+
_ASSISTANT_ROLE = (
6+
"You are a database assistant connected to a live SQL database via MCP "
7+
"(Model Context Protocol).\n\n"
8+
"Rules:\n"
9+
"1. ALWAYS call sql.schema first at the start of each conversation to get "
10+
"the current schema. Never assume or cache table names from previous messages.\n"
11+
"2. Read the sql.query tool description — it tells you the database engine "
12+
"(SQLite, MySQL, PostgreSQL). Use the correct SQL dialect.\n"
13+
"3. For read queries use sql.query. For writes use db.apply. Never mix them.\n"
14+
"4. If a query fails with a syntax error, check the dialect and retry "
15+
"with correct syntax.\n"
16+
"5. Prefer sql.schema over SHOW TABLES / information_schema for discovering tables.\n"
17+
"6. When presenting results, format them as clean tables. Keep explanations concise."
18+
)
19+
520

621
class PromptRegistry:
722
def __init__(self, config: Config) -> None:
@@ -10,6 +25,15 @@ def __init__(self, config: Config) -> None:
1025
def list_prompts(self) -> Dict[str, Any]:
1126
return {
1227
"prompts": [
28+
{
29+
"name": "sql.assistant.role",
30+
"description": (
31+
"System prompt for an AI assistant working with this database. "
32+
"Provides rules for correct tool usage, dialect detection, "
33+
"and schema discovery."
34+
),
35+
"arguments": [],
36+
},
1337
{
1438
"name": "sql.query.plan",
1539
"description": "Generate a safe SQL query plan before execution.",
@@ -40,7 +64,17 @@ def list_prompts(self) -> Dict[str, Any]:
4064
]
4165
}
4266

43-
def get_prompt(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
67+
def get_prompt(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any] | None:
68+
if name == "sql.assistant.role":
69+
return {
70+
"messages": [
71+
{
72+
"role": "user",
73+
"content": {"type": "text", "text": _ASSISTANT_ROLE},
74+
},
75+
]
76+
}
77+
4478
if name == "sql.query.plan":
4579
question = arguments.get("question", "")
4680
content = (

app/mcp/tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ def db_migrate_plan_apply(payload: Dict[str, Any]) -> Dict[str, Any]:
387387
description=(
388388
f"Execute a read-only SQL query against {config.db_type}. "
389389
f"Use {config.db_type}-compatible syntax. "
390-
"Only SELECT, WITH, and EXPLAIN statements are allowed. "
390+
"SELECT, WITH, EXPLAIN, SHOW, and DESCRIBE statements are allowed. "
391391
"For INSERT, UPDATE, DELETE, or DDL statements use db.apply."
392392
),
393393
input_schema={

app/sql/policy.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import re
22

3-
READ_ONLY_PREFIXES = ("select", "with", "explain")
3+
READ_ONLY_PREFIXES = ("select", "with", "explain", "show", "describe", "desc")
44
READ_QUERY_PREFIXES = ("select", "with")
55

66

@@ -119,6 +119,10 @@ def _is_read_only_core(normalized_sql: str) -> bool:
119119
if normalized_sql.startswith("explain"):
120120
return _is_explain_readonly(normalized_sql)
121121

122+
# MySQL/PostgreSQL metadata commands are always read-only
123+
if normalized_sql.startswith(("show", "describe", "desc ")):
124+
return True
125+
122126
if not normalized_sql.startswith(("select", "with")):
123127
return False
124128

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ sqlalchemy
44
jinja2
55
python-dotenv
66
psycopg2-binary
7+
pymysql
78
python-multipart
89
httpx
910
cryptography

tests/test_sql_policy.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ def test_ignores_keywords_inside_literals(self) -> None:
3838
is_allowed("SELECT '-- drop table users' AS note FROM users", mode="read-only")
3939
)
4040

41+
def test_allows_show_tables_in_read_only(self) -> None:
42+
self.assertTrue(is_allowed("SHOW TABLES", mode="read-only"))
43+
44+
def test_allows_show_databases_in_read_only(self) -> None:
45+
self.assertTrue(is_allowed("SHOW DATABASES", mode="read-only"))
46+
47+
def test_allows_show_create_table_in_read_only(self) -> None:
48+
self.assertTrue(is_allowed("SHOW CREATE TABLE users", mode="read-only"))
49+
50+
def test_allows_describe_in_read_only(self) -> None:
51+
self.assertTrue(is_allowed("DESCRIBE users", mode="read-only"))
52+
53+
def test_allows_desc_in_read_only(self) -> None:
54+
self.assertTrue(is_allowed("DESC users", mode="read-only"))
55+
4156
def test_execute_mode_allows_single_write_statement(self) -> None:
4257
self.assertTrue(is_allowed("DELETE FROM users WHERE id = 1", mode="execute"))
4358

0 commit comments

Comments
 (0)