Skip to content

Commit 9c4520a

Browse files
authored
Merge pull request #4 from databendlabs/databend-python
feat: add local databend mode
2 parents 20cb20d + 6f98c85 commit 9c4520a

File tree

6 files changed

+427
-186
lines changed

6 files changed

+427
-186
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# databend dir
2+
.databend
3+
14
# Byte-compiled / optimized / DLL files
25
__pycache__/
36
*.py[cod]

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ Get your connection string from [Databend documentation](https://docs.databend.c
6161
| **Databend Cloud** | `databend://user:pwd@host:443/database?warehouse=wh` |
6262
| **Self-hosted** | `databend://user:pwd@localhost:8000/database?sslmode=disable` |
6363

64+
Or use local Databend by setting `LOCAL_MODE=true`, the metadata is stored in `.databend` directory:
65+
66+
6467
### Step 2: Install
6568

6669
```bash
@@ -71,10 +74,16 @@ uv tool install mcp-databend
7174

7275
#### Option A: Claude Code (CLI)
7376

77+
- For Databend server:
7478
```bash
7579
claude mcp add mcp-databend --env DATABEND_DSN='your-connection-string-here' -- uv tool run mcp-databend
7680
```
7781

82+
- For local Databend:
83+
```bash
84+
claude mcp add mcp-databend --env -- uv tool run mcp-databend
85+
```
86+
7887
#### Option B: MCP Configuration (JSON)
7988

8089
Add to your MCP client configuration (e.g., Claude Desktop, Windsurf):
@@ -105,7 +114,7 @@ Once configured, you can ask your AI assistant to:
105114

106115
**Database Operations:**
107116
- "Show me all databases"
108-
- "List tables in the sales database"
117+
- "List tables in the sales database"
109118
- "Describe the users table structure"
110119
- "Run this SQL query: SELECT * FROM products LIMIT 10"
111120

@@ -127,4 +136,7 @@ uv sync
127136

128137
# Run locally
129138
uv run python -m mcp_databend.main
139+
140+
# Use modelcontextprotocol/inspector to debug
141+
npx @modelcontextprotocol/inspector -e LOCAL_MODE=1 uv run python -m mcp_databend.main
130142
```

mcp_databend/env.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class DatabendConfig:
1111
Required environment variables:
1212
DATABEND_DSN: The dsn connect string (defaults to: "databend://default:@127.0.0.1:8000/?sslmode=disable")
1313
SAFE_MODE: Enable safe mode to restrict dangerous SQL operations (defaults to: "true")
14+
LOCAL_MODE: Enable local mode to use in-memory Databend (defaults to: "false")
1415
"""
1516

1617
def __init__(self):
@@ -29,6 +30,16 @@ def safe_mode(self) -> bool:
2930
"""Get the safe mode setting."""
3031
return os.environ.get("SAFE_MODE", "true").lower() in ("true", "1", "yes", "on")
3132

33+
@property
34+
def local_mode(self) -> bool:
35+
"""Get the local mode setting."""
36+
return os.environ.get("LOCAL_MODE", "false").lower() in (
37+
"true",
38+
"1",
39+
"yes",
40+
"on",
41+
)
42+
3243

3344
# Global instance placeholder for the singleton pattern
3445
_CONFIG_INSTANCE = None

mcp_databend/server.py

Lines changed: 73 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -32,43 +32,62 @@
3232
# Initialize MCP server
3333
mcp = FastMCP(SERVER_NAME)
3434

35+
# Global Databend client singleton
36+
_databend_client = None
37+
38+
39+
def get_global_databend_client():
40+
"""Get global Databend client instance (deprecated, use create_databend_client)."""
41+
global _databend_client
42+
if _databend_client is None:
43+
_databend_client = create_databend_client()
44+
return _databend_client
45+
3546

3647
def is_sql_safe(sql: str) -> tuple[bool, str]:
3748
"""
3849
Check if SQL query is safe to execute in safe mode.
39-
50+
4051
Args:
4152
sql: SQL query string to check
42-
53+
4354
Returns:
4455
Tuple of (is_safe, reason) where is_safe is boolean and reason is error message if unsafe
4556
"""
4657
sql_upper = sql.upper().strip()
47-
58+
4859
# List of dangerous operations to block in safe mode
4960
dangerous_patterns = [
50-
(r'\bDROP\s+', "DROP operations are not allowed in MCP safe mode"),
51-
(r'\bDELETE\s+', "DELETE operations are not allowed in MCP safe mode"),
52-
(r'\bTRUNCATE\s+', "TRUNCATE operations are not allowed in MCP safe mode"),
53-
(r'\bALTER\s+', "ALTER operations are not allowed in MCP safe mode"),
54-
(r'\bUPDATE\s+', "UPDATE operations are not allowed in MCP safe mode"),
55-
(r'\bREVOKE\s+', "REVOKE operations are not allowed in MCP safe mode"),
61+
(r"\bDROP\s+", "DROP operations are not allowed in MCP safe mode"),
62+
(r"\bDELETE\s+", "DELETE operations are not allowed in MCP safe mode"),
63+
(r"\bTRUNCATE\s+", "TRUNCATE operations are not allowed in MCP safe mode"),
64+
(r"\bALTER\s+", "ALTER operations are not allowed in MCP safe mode"),
65+
(r"\bUPDATE\s+", "UPDATE operations are not allowed in MCP safe mode"),
66+
(r"\bREVOKE\s+", "REVOKE operations are not allowed in MCP safe mode"),
5667
]
57-
68+
5869
# Check each dangerous pattern
5970
for pattern, reason in dangerous_patterns:
6071
if re.search(pattern, sql_upper, re.IGNORECASE | re.DOTALL):
6172
return False, reason
62-
73+
6374
return True, ""
6475

6576

6677
def create_databend_client():
6778
"""Create and return a Databend client instance."""
6879
config = get_config()
69-
from databend_driver import BlockingDatabendClient
7080

71-
return BlockingDatabendClient(config.dsn)
81+
if config.local_mode:
82+
# Use local in-memory Databend
83+
import databend
84+
85+
return databend.SessionContext()
86+
else:
87+
# Use remote Databend server
88+
from databend_driver import BlockingDatabendClient
89+
90+
return BlockingDatabendClient(config.dsn)
7291

7392

7493
def execute_databend_query(sql: str) -> list[dict] | dict:
@@ -81,20 +100,28 @@ def execute_databend_query(sql: str) -> list[dict] | dict:
81100
Returns:
82101
List of dictionaries containing query results or error dictionary
83102
"""
84-
client = create_databend_client()
85-
conn = client.get_conn()
103+
client = get_global_databend_client()
104+
config = get_config()
86105

87106
try:
88-
cursor = conn.query_iter(sql)
89-
column_names = [field.name for field in cursor.schema().fields()]
90-
results = []
91-
92-
for row in cursor:
93-
row_data = dict(zip(column_names, list(row.values())))
94-
results.append(row_data)
95-
96-
logger.info(f"Query executed successfully, returned {len(results)} rows")
97-
return results
107+
if config.local_mode:
108+
# Handle local in-memory Databend
109+
result = client.sql(sql)
110+
df = result.to_pandas()
111+
return df.to_dict("records")
112+
else:
113+
# Handle remote Databend server
114+
conn = client.get_conn()
115+
cursor = conn.query_iter(sql)
116+
column_names = [field.name for field in cursor.schema().fields()]
117+
results = []
118+
119+
for row in cursor:
120+
row_data = dict(zip(column_names, list(row.values())))
121+
results.append(row_data)
122+
123+
logger.info(f"Query executed successfully, returned {len(results)} rows")
124+
return results
98125

99126
except Exception as err:
100127
error_msg = f"Error executing query: {str(err)}"
@@ -104,7 +131,7 @@ def execute_databend_query(sql: str) -> list[dict] | dict:
104131

105132
def _execute_sql(sql: str) -> dict:
106133
logger.info(f"Executing SQL query: {sql}")
107-
134+
108135
# Check safe mode configuration
109136
config = get_config()
110137
if config.safe_mode:
@@ -141,12 +168,13 @@ def _execute_sql(sql: str) -> dict:
141168
logger.error(error_msg)
142169
return {"status": "error", "message": error_msg}
143170

171+
144172
@mcp.tool()
145173
async def execute_sql(sql: str) -> dict:
146174
"""
147175
Execute SQL query against Databend database with MCP safe mode protection.
148-
149-
Safe mode (enabled by default) blocks dangerous operations like DROP, DELETE,
176+
177+
Safe mode (enabled by default) blocks dangerous operations like DROP, DELETE,
150178
TRUNCATE, ALTER, UPDATE, and REVOKE. Set SAFE_MODE=false to disable.
151179
152180
Args:
@@ -159,14 +187,14 @@ async def execute_sql(sql: str) -> dict:
159187

160188

161189
@mcp.tool()
162-
def show_databases():
190+
async def show_databases():
163191
"""List available Databend databases (safe operation, not affected by MCP safe mode)"""
164192
logger.info("Listing all databases")
165193
return _execute_sql("SHOW DATABASES")
166194

167195

168196
@mcp.tool()
169-
def show_tables(database: Optional[str] = None, filter: Optional[str] = None):
197+
async def show_tables(database: Optional[str] = None, filter: Optional[str] = None):
170198
"""
171199
List available Databend tables in a database (safe operation, not affected by MCP safe mode)
172200
Args:
@@ -186,7 +214,7 @@ def show_tables(database: Optional[str] = None, filter: Optional[str] = None):
186214

187215

188216
@mcp.tool()
189-
def describe_table(table: str, database: Optional[str] = None):
217+
async def describe_table(table: str, database: Optional[str] = None):
190218
"""
191219
Describe a Databend table (safe operation, not affected by MCP safe mode)
192220
Args:
@@ -201,7 +229,7 @@ def describe_table(table: str, database: Optional[str] = None):
201229
table = f"{database}.{table}"
202230
logger.info(f"Describing table '{table}'")
203231
sql = f"DESCRIBE TABLE {table}"
204-
return execute_sql(sql)
232+
return _execute_sql(sql)
205233

206234

207235
@mcp.tool()
@@ -212,7 +240,7 @@ def show_stages():
212240

213241

214242
@mcp.tool()
215-
def list_stage_files(stage_name: str, path: Optional[str] = None):
243+
async def list_stage_files(stage_name: str, path: Optional[str] = None):
216244
"""
217245
List files in a Databend stage (safe operation, not affected by MCP safe mode)
218246
Args:
@@ -222,28 +250,30 @@ def list_stage_files(stage_name: str, path: Optional[str] = None):
222250
Returns:
223251
Dictionary containing either query results or error information
224252
"""
225-
if not stage_name.startswith('@'):
253+
if not stage_name.startswith("@"):
226254
stage_name = f"@{stage_name}"
227-
255+
228256
if path:
229257
stage_path = f"{stage_name}/{path.strip('/')}"
230258
else:
231259
stage_path = stage_name
232-
260+
233261
logger.info(f"Listing files in stage '{stage_path}'")
234262
sql = f"LIST {stage_path}"
235263
return _execute_sql(sql)
236264

237265

238266
@mcp.tool()
239-
def show_connections():
267+
async def show_connections():
240268
"""List available Databend connections (safe operation, not affected by MCP safe mode)"""
241269
logger.info("Listing all connections")
242270
return _execute_sql("SHOW CONNECTIONS")
243271

244272

245273
@mcp.tool()
246-
async def create_stage(name: str, url: str, connection_name: Optional[str] = None, **kwargs) -> dict:
274+
async def create_stage(
275+
name: str, url: str, connection_name: Optional[str] = None, **kwargs
276+
) -> dict:
247277
"""
248278
Create a Databend stage with connection
249279
Args:
@@ -256,17 +286,17 @@ async def create_stage(name: str, url: str, connection_name: Optional[str] = Non
256286
Dictionary containing either query results or error information
257287
"""
258288
logger.info(f"Creating stage '{name}' with URL '{url}'")
259-
289+
260290
sql_parts = [f"CREATE STAGE {name}", f"URL = '{url}'"]
261-
291+
262292
if connection_name:
263293
sql_parts.append(f"CONNECTION = (CONNECTION_NAME = '{connection_name}')")
264-
294+
265295
# Add any additional options from kwargs
266296
for key, value in kwargs.items():
267-
if key not in ['name', 'url', 'connection_name']:
297+
if key not in ["name", "url", "connection_name"]:
268298
sql_parts.append(f"{key.upper()} = '{value}'")
269-
299+
270300
sql = " ".join(sql_parts)
271301
return _execute_sql(sql)
272302

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ classifiers = [
2121
"Topic :: Software Development :: Libraries :: Python Modules",
2222
]
2323
dependencies = [
24+
"databend>=1.2.810",
2425
"databend-driver>=0.27.3",
2526
"mcp>=1.9.0",
27+
"pandas>=2.3.2",
28+
"pyarrow>=21.0.0",
2629
"python-dotenv>=1.1.0",
2730
]
2831

0 commit comments

Comments
 (0)