mcp-server-motherduck 0.6.4__py3-none-any.whl → 0.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-server-motherduck might be problematic. Click here for more details.
- mcp_server_motherduck/configs.py +1 -1
- mcp_server_motherduck/database.py +70 -2
- {mcp_server_motherduck-0.6.4.dist-info → mcp_server_motherduck-0.7.1.dist-info}/METADATA +35 -4
- mcp_server_motherduck-0.7.1.dist-info/RECORD +10 -0
- mcp_server_motherduck-0.6.4.dist-info/RECORD +0 -10
- {mcp_server_motherduck-0.6.4.dist-info → mcp_server_motherduck-0.7.1.dist-info}/WHEEL +0 -0
- {mcp_server_motherduck-0.6.4.dist-info → mcp_server_motherduck-0.7.1.dist-info}/entry_points.txt +0 -0
- {mcp_server_motherduck-0.6.4.dist-info → mcp_server_motherduck-0.7.1.dist-info}/licenses/LICENSE +0 -0
mcp_server_motherduck/configs.py
CHANGED
|
@@ -36,6 +36,10 @@ class DatabaseClient:
|
|
|
36
36
|
|
|
37
37
|
logger.info(f"🔌 Connecting to {self.db_type} database")
|
|
38
38
|
|
|
39
|
+
# S3 databases don't support read-only mode
|
|
40
|
+
if self.db_type == "s3" and self._read_only:
|
|
41
|
+
raise ValueError("Read-only mode is not supported for S3 databases")
|
|
42
|
+
|
|
39
43
|
if self.db_type == "duckdb" and self._read_only:
|
|
40
44
|
# check that we can connect, issue a `select 1` and then close + return None
|
|
41
45
|
try:
|
|
@@ -53,6 +57,66 @@ class DatabaseClient:
|
|
|
53
57
|
logger.error(f"❌ Read-only check failed: {e}")
|
|
54
58
|
raise
|
|
55
59
|
|
|
60
|
+
# Check if this is an S3 path
|
|
61
|
+
if self.db_type == "s3":
|
|
62
|
+
# For S3, we need to create an in-memory connection and attach the S3 database
|
|
63
|
+
conn = duckdb.connect(':memory:')
|
|
64
|
+
|
|
65
|
+
# Install and load the httpfs extension for S3 support
|
|
66
|
+
import io
|
|
67
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
68
|
+
|
|
69
|
+
null_file = io.StringIO()
|
|
70
|
+
with redirect_stdout(null_file), redirect_stderr(null_file):
|
|
71
|
+
try:
|
|
72
|
+
conn.execute("INSTALL httpfs;")
|
|
73
|
+
except:
|
|
74
|
+
pass # Extension might already be installed
|
|
75
|
+
conn.execute("LOAD httpfs;")
|
|
76
|
+
|
|
77
|
+
# Configure S3 credentials from environment variables using CREATE SECRET
|
|
78
|
+
aws_access_key = os.environ.get('AWS_ACCESS_KEY_ID')
|
|
79
|
+
aws_secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
|
|
80
|
+
aws_region = os.environ.get('AWS_DEFAULT_REGION', 'us-east-1')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if aws_access_key and aws_secret_key:
|
|
84
|
+
# Use CREATE SECRET for better credential management
|
|
85
|
+
conn.execute(f"""
|
|
86
|
+
CREATE SECRET IF NOT EXISTS s3_secret (
|
|
87
|
+
TYPE S3,
|
|
88
|
+
KEY_ID '{aws_access_key}',
|
|
89
|
+
SECRET '{aws_secret_key}',
|
|
90
|
+
REGION '{aws_region}'
|
|
91
|
+
);
|
|
92
|
+
""")
|
|
93
|
+
|
|
94
|
+
# Attach the S3 database
|
|
95
|
+
try:
|
|
96
|
+
# For S3, we always attach as READ_ONLY since S3 storage is typically read-only
|
|
97
|
+
# Even when not in read_only mode, we attach as READ_ONLY for S3
|
|
98
|
+
conn.execute(f"ATTACH '{self.db_path}' AS s3db (READ_ONLY);")
|
|
99
|
+
# Use the attached database
|
|
100
|
+
conn.execute("USE s3db;")
|
|
101
|
+
logger.info(f"✅ Successfully connected to {self.db_type} database (attached as read-only)")
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"Failed to attach S3 database: {e}")
|
|
104
|
+
# If the database doesn't exist and we're not in read-only mode, try to create it
|
|
105
|
+
if "database does not exist" in str(e) and not self._read_only:
|
|
106
|
+
logger.info("S3 database doesn't exist, attempting to create it...")
|
|
107
|
+
try:
|
|
108
|
+
# Create a new database at the S3 location
|
|
109
|
+
conn.execute(f"ATTACH '{self.db_path}' AS s3db;")
|
|
110
|
+
conn.execute("USE s3db;")
|
|
111
|
+
logger.info(f"✅ Created new S3 database at {self.db_path}")
|
|
112
|
+
except Exception as create_error:
|
|
113
|
+
logger.error(f"Failed to create S3 database: {create_error}")
|
|
114
|
+
raise
|
|
115
|
+
else:
|
|
116
|
+
raise
|
|
117
|
+
|
|
118
|
+
return conn
|
|
119
|
+
|
|
56
120
|
conn = duckdb.connect(
|
|
57
121
|
self.db_path,
|
|
58
122
|
config={"custom_user_agent": f"mcp-server-motherduck/{SERVER_VERSION}"},
|
|
@@ -65,8 +129,12 @@ class DatabaseClient:
|
|
|
65
129
|
|
|
66
130
|
def _resolve_db_path_type(
|
|
67
131
|
self, db_path: str, motherduck_token: str | None = None, saas_mode: bool = False
|
|
68
|
-
) -> tuple[str, Literal["duckdb", "motherduck"]]:
|
|
132
|
+
) -> tuple[str, Literal["duckdb", "motherduck", "s3"]]:
|
|
69
133
|
"""Resolve and validate the database path"""
|
|
134
|
+
# Handle S3 paths
|
|
135
|
+
if db_path.startswith("s3://"):
|
|
136
|
+
return db_path, "s3"
|
|
137
|
+
|
|
70
138
|
# Handle MotherDuck paths
|
|
71
139
|
if db_path.startswith("md:"):
|
|
72
140
|
if motherduck_token:
|
|
@@ -102,7 +170,7 @@ class DatabaseClient:
|
|
|
102
170
|
|
|
103
171
|
def _execute(self, query: str) -> str:
|
|
104
172
|
if self.conn is None:
|
|
105
|
-
# open short lived readonly connection, run query, close connection, return result
|
|
173
|
+
# open short lived readonly connection for local DuckDB, run query, close connection, return result
|
|
106
174
|
conn = duckdb.connect(
|
|
107
175
|
self.db_path,
|
|
108
176
|
config={"custom_user_agent": f"mcp-server-motherduck/{SERVER_VERSION}"},
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-server-motherduck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: A MCP server for MotherDuck and local DuckDB
|
|
5
5
|
Author-email: tdoehmen <till@motherduck.com>
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Requires-Dist: anyio>=4.8.0
|
|
9
9
|
Requires-Dist: click>=8.1.8
|
|
10
|
-
Requires-Dist: duckdb==1.
|
|
10
|
+
Requires-Dist: duckdb==1.4.1
|
|
11
11
|
Requires-Dist: mcp>=1.9.4
|
|
12
12
|
Requires-Dist: pytz>=2025.2
|
|
13
13
|
Requires-Dist: starlette>=0.46.1
|
|
@@ -19,7 +19,7 @@ Description-Content-Type: text/markdown
|
|
|
19
19
|
|
|
20
20
|
An MCP server implementation that interacts with DuckDB and MotherDuck databases, providing SQL analytics capabilities to AI Assistants and IDEs.
|
|
21
21
|
|
|
22
|
-
[
|
|
22
|
+
[<img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Install in Cursor">](https://cursor.com/en/install-mcp?name=DuckDB&config=eyJjb21tYW5kIjoidXZ4IG1jcC1zZXJ2ZXItbW90aGVyZHVjayAtLWRiLXBhdGggOm1lbW9yeToiLCJlbnYiOnsibW90aGVyZHVja190b2tlbiI6IiJ9fQ%3D%3D)
|
|
23
23
|
|
|
24
24
|
## Resources
|
|
25
25
|
- [Close the Loop: Faster Data Pipelines with MCP, DuckDB & AI (Blogpost)](https://motherduck.com/blog/faster-data-pipelines-with-mcp-duckdb-ai/)
|
|
@@ -59,7 +59,7 @@ The MCP server supports the following parameters:
|
|
|
59
59
|
|-----------|------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
60
60
|
| `--transport` | Choice | `stdio` | Transport type. Options: `stdio`, `sse`, `stream` |
|
|
61
61
|
| `--port` | Integer | `8000` | Port to listen on for sse and stream transport mode |
|
|
62
|
-
| `--db-path` | String | `md:` | Path to local DuckDB database file or
|
|
62
|
+
| `--db-path` | String | `md:` | Path to local DuckDB database file, MotherDuck database, or S3 URL (e.g., `s3://bucket/path/to/db.duckdb`) |
|
|
63
63
|
| `--motherduck-token` | String | `None` | Access token to use for MotherDuck database connections (uses `motherduck_token` env var by default) |
|
|
64
64
|
| `--read-only` | Flag | `False` | Flag for connecting to DuckDB or MotherDuck in read-only mode. For DuckDB it uses short-lived connections to enable concurrent access |
|
|
65
65
|
| `--home-dir` | String | `None` | Home directory for DuckDB (uses `HOME` env var by default) |
|
|
@@ -354,6 +354,37 @@ modeling data within duckdb and then an MCP client (Windsurf/Cline/Claude/Cursor
|
|
|
354
354
|
was used for exploring the database. The short lived connections allow each tool
|
|
355
355
|
to run and then release their connection, allowing the next tool to connect.
|
|
356
356
|
|
|
357
|
+
## Connect to DuckDB on S3
|
|
358
|
+
|
|
359
|
+
You can connect to DuckDB databases stored on Amazon S3 by providing an S3 URL as the database path. The server will automatically configure the necessary S3 credentials from your environment variables.
|
|
360
|
+
|
|
361
|
+
```json
|
|
362
|
+
{
|
|
363
|
+
"mcpServers": {
|
|
364
|
+
"mcp-server-motherduck": {
|
|
365
|
+
"command": "uvx",
|
|
366
|
+
"args": [
|
|
367
|
+
"mcp-server-motherduck",
|
|
368
|
+
"--db-path",
|
|
369
|
+
"s3://your-bucket/path/to/database.duckdb"
|
|
370
|
+
],
|
|
371
|
+
"env": {
|
|
372
|
+
"AWS_ACCESS_KEY_ID": "<your_key>",
|
|
373
|
+
"AWS_SECRET_ACCESS_KEY": "<your_secret>",
|
|
374
|
+
"AWS_DEFAULT_REGION": "<your_region>"
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
**Note**: For S3 connections:
|
|
383
|
+
- AWS credentials must be provided via environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and optionally `AWS_DEFAULT_REGION`)
|
|
384
|
+
- The S3 database is attached to an in-memory DuckDB instance
|
|
385
|
+
- The httpfs extension is automatically installed and configured for S3 access
|
|
386
|
+
- Both read and write operations are supported
|
|
387
|
+
|
|
357
388
|
## Example Queries
|
|
358
389
|
|
|
359
390
|
Once configured, you can e.g. ask Claude to run queries like:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
mcp_server_motherduck/__init__.py,sha256=vogjO-bwHYZFut85eeiVmRepamcWHk8bImri_kBKZMU,6026
|
|
2
|
+
mcp_server_motherduck/configs.py,sha256=uhfR1Tt6jaB5cG4XEHkxcbEJ6dmpBaWNL6a1GXoFCSE,771
|
|
3
|
+
mcp_server_motherduck/database.py,sha256=-n1Z0UXsi5Y3nO1_df1CeHiSEe644gJNkJ7r_pdWytw,7761
|
|
4
|
+
mcp_server_motherduck/prompt.py,sha256=P7BrmhVXwDkPeSHQ3f25WMP6lpBpN2BxDzYPOQ3fxX8,56699
|
|
5
|
+
mcp_server_motherduck/server.py,sha256=U1LM2oQ36gO_pAZuez9HV_u8YDWdER8tQIdDbiXfzx0,5232
|
|
6
|
+
mcp_server_motherduck-0.7.1.dist-info/METADATA,sha256=fBp9w2oJ-C4Qj2v8aAYiHHFHHx59w1Pm0fqUe_9EQAM,17820
|
|
7
|
+
mcp_server_motherduck-0.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
mcp_server_motherduck-0.7.1.dist-info/entry_points.txt,sha256=dRTgcvWJn40bz0PVuKPylK6w92cFN32lwunZOgo5j4s,69
|
|
9
|
+
mcp_server_motherduck-0.7.1.dist-info/licenses/LICENSE,sha256=Tj68w9jCiceFKTvZ3jET-008NjhozcQMXpm-fyL9WUI,1067
|
|
10
|
+
mcp_server_motherduck-0.7.1.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
mcp_server_motherduck/__init__.py,sha256=vogjO-bwHYZFut85eeiVmRepamcWHk8bImri_kBKZMU,6026
|
|
2
|
-
mcp_server_motherduck/configs.py,sha256=-9cNtMBsNn0OK-0pxRWxMoTy4VbfUdCMKQG_BPMmIu4,771
|
|
3
|
-
mcp_server_motherduck/database.py,sha256=3qkeBIpf08WV6pz9rf52b_MipzaHALurFugMx-XARI8,4513
|
|
4
|
-
mcp_server_motherduck/prompt.py,sha256=P7BrmhVXwDkPeSHQ3f25WMP6lpBpN2BxDzYPOQ3fxX8,56699
|
|
5
|
-
mcp_server_motherduck/server.py,sha256=U1LM2oQ36gO_pAZuez9HV_u8YDWdER8tQIdDbiXfzx0,5232
|
|
6
|
-
mcp_server_motherduck-0.6.4.dist-info/METADATA,sha256=McZsr6b8gFcH4ju85Ck2eC7-Z0cS06V_fOyecihfVIA,16831
|
|
7
|
-
mcp_server_motherduck-0.6.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
-
mcp_server_motherduck-0.6.4.dist-info/entry_points.txt,sha256=dRTgcvWJn40bz0PVuKPylK6w92cFN32lwunZOgo5j4s,69
|
|
9
|
-
mcp_server_motherduck-0.6.4.dist-info/licenses/LICENSE,sha256=Tj68w9jCiceFKTvZ3jET-008NjhozcQMXpm-fyL9WUI,1067
|
|
10
|
-
mcp_server_motherduck-0.6.4.dist-info/RECORD,,
|
|
File without changes
|
{mcp_server_motherduck-0.6.4.dist-info → mcp_server_motherduck-0.7.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{mcp_server_motherduck-0.6.4.dist-info → mcp_server_motherduck-0.7.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|