mcp-server-motherduck 0.4.2__py3-none-any.whl → 0.5__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.
- mcp_server_motherduck/__init__.py +7 -0
- mcp_server_motherduck/server.py +65 -22
- {mcp_server_motherduck-0.4.2.dist-info → mcp_server_motherduck-0.5.dist-info}/METADATA +44 -9
- mcp_server_motherduck-0.5.dist-info/RECORD +8 -0
- mcp_server_motherduck-0.4.2.dist-info/RECORD +0 -8
- {mcp_server_motherduck-0.4.2.dist-info → mcp_server_motherduck-0.5.dist-info}/WHEEL +0 -0
- {mcp_server_motherduck-0.4.2.dist-info → mcp_server_motherduck-0.5.dist-info}/entry_points.txt +0 -0
- {mcp_server_motherduck-0.4.2.dist-info → mcp_server_motherduck-0.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -39,6 +39,12 @@ def main():
|
|
|
39
39
|
choices=["markdown", "duckbox", "text"],
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--read-only",
|
|
44
|
+
action="store_true",
|
|
45
|
+
help="Flag for connecting to DuckDB in read-only mode. Only supported for local DuckDB databases. Also makes use of short lived connections so multiple MCP clients or other systems can remain active (though each operation must be done sequentially).",
|
|
46
|
+
)
|
|
47
|
+
|
|
42
48
|
args = parser.parse_args()
|
|
43
49
|
logger.info("🦆 MotherDuck MCP Server v" + server.SERVER_VERSION)
|
|
44
50
|
logger.info("Ready to execute SQL queries via DuckDB/MotherDuck")
|
|
@@ -51,6 +57,7 @@ def main():
|
|
|
51
57
|
result_format=args.result_format,
|
|
52
58
|
home_dir=args.home_dir,
|
|
53
59
|
saas_mode=args.saas_mode,
|
|
60
|
+
read_only=args.read_only,
|
|
54
61
|
)
|
|
55
62
|
)
|
|
56
63
|
|
mcp_server_motherduck/server.py
CHANGED
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
import logging
|
|
3
3
|
import duckdb
|
|
4
4
|
from pydantic import AnyUrl
|
|
5
|
-
from typing import Literal
|
|
5
|
+
from typing import Literal, Optional
|
|
6
6
|
import io
|
|
7
7
|
from contextlib import redirect_stdout
|
|
8
8
|
import mcp.server.stdio
|
|
@@ -12,7 +12,7 @@ from mcp.server.models import InitializationOptions
|
|
|
12
12
|
from .prompt import PROMPT_TEMPLATE
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
SERVER_VERSION = "0.
|
|
15
|
+
SERVER_VERSION = "0.5"
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger("mcp_server_motherduck")
|
|
18
18
|
|
|
@@ -25,7 +25,9 @@ class DatabaseClient:
|
|
|
25
25
|
result_format: Literal["markdown", "duckbox", "text"] = "markdown",
|
|
26
26
|
home_dir: str | None = None,
|
|
27
27
|
saas_mode: bool = False,
|
|
28
|
+
read_only: bool = False,
|
|
28
29
|
):
|
|
30
|
+
self._read_only = read_only
|
|
29
31
|
self.db_path, self.db_type = self._resolve_db_path_type(
|
|
30
32
|
db_path, motherduck_token, saas_mode
|
|
31
33
|
)
|
|
@@ -38,11 +40,32 @@ class DatabaseClient:
|
|
|
38
40
|
self.conn = self._initialize_connection()
|
|
39
41
|
self.result_format = result_format
|
|
40
42
|
|
|
41
|
-
def _initialize_connection(self) -> duckdb.DuckDBPyConnection:
|
|
43
|
+
def _initialize_connection(self) -> Optional[duckdb.DuckDBPyConnection]:
|
|
42
44
|
"""Initialize connection to the MotherDuck or DuckDB database"""
|
|
43
45
|
|
|
44
46
|
logger.info(f"🔌 Connecting to {self.db_type} database")
|
|
45
47
|
|
|
48
|
+
if self.db_type == "duckdb" and self._read_only:
|
|
49
|
+
# check that we can connect, issue a `select 1` and then close + return None
|
|
50
|
+
try:
|
|
51
|
+
conn = duckdb.connect(
|
|
52
|
+
self.db_path,
|
|
53
|
+
config={
|
|
54
|
+
"custom_user_agent": f"mcp-server-motherduck/{SERVER_VERSION}"
|
|
55
|
+
},
|
|
56
|
+
read_only=self._read_only,
|
|
57
|
+
)
|
|
58
|
+
conn.execute("SELECT 1")
|
|
59
|
+
conn.close()
|
|
60
|
+
return None
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.error(f"❌ Read-only check failed: {e}")
|
|
63
|
+
raise
|
|
64
|
+
|
|
65
|
+
if self._read_only:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"Read-only mode is only supported for local DuckDB databases. See `saas_mode` for similar functionality with MotherDuck."
|
|
68
|
+
)
|
|
46
69
|
conn = duckdb.connect(
|
|
47
70
|
self.db_path,
|
|
48
71
|
config={"custom_user_agent": f"mcp-server-motherduck/{SERVER_VERSION}"},
|
|
@@ -62,9 +85,15 @@ class DatabaseClient:
|
|
|
62
85
|
logger.info("Using MotherDuck token to connect to database `md:`")
|
|
63
86
|
if saas_mode:
|
|
64
87
|
logger.info("Connecting to MotherDuck in SaaS mode")
|
|
65
|
-
return
|
|
88
|
+
return (
|
|
89
|
+
f"{db_path}?motherduck_token={motherduck_token}&saas_mode=true",
|
|
90
|
+
"motherduck",
|
|
91
|
+
)
|
|
66
92
|
else:
|
|
67
|
-
return
|
|
93
|
+
return (
|
|
94
|
+
f"{db_path}?motherduck_token={motherduck_token}",
|
|
95
|
+
"motherduck",
|
|
96
|
+
)
|
|
68
97
|
elif os.getenv("motherduck_token"):
|
|
69
98
|
logger.info(
|
|
70
99
|
"Using MotherDuck token from env to connect to database `md:`"
|
|
@@ -88,25 +117,37 @@ class DatabaseClient:
|
|
|
88
117
|
)
|
|
89
118
|
return db_path, "duckdb"
|
|
90
119
|
|
|
120
|
+
def _execute(self, query: str) -> str:
|
|
121
|
+
if self.conn is None:
|
|
122
|
+
# open short lived readonly connection, run query, close connection, return result
|
|
123
|
+
conn = duckdb.connect(
|
|
124
|
+
self.db_path,
|
|
125
|
+
config={"custom_user_agent": f"mcp-server-motherduck/{SERVER_VERSION}"},
|
|
126
|
+
read_only=self._read_only,
|
|
127
|
+
)
|
|
128
|
+
q = conn.execute(query)
|
|
129
|
+
else:
|
|
130
|
+
q = self.conn.execute(query)
|
|
131
|
+
|
|
132
|
+
if self.result_format == "markdown":
|
|
133
|
+
out = q.fetchdf().to_markdown()
|
|
134
|
+
elif self.result_format == "duckbox":
|
|
135
|
+
# Duckbox version of the output
|
|
136
|
+
buffer = io.StringIO()
|
|
137
|
+
with redirect_stdout(buffer):
|
|
138
|
+
q.show(max_rows=100, max_col_width=20)
|
|
139
|
+
out = buffer.getvalue()
|
|
140
|
+
else:
|
|
141
|
+
out = str(q.fetchall())
|
|
142
|
+
|
|
143
|
+
if self.conn is None:
|
|
144
|
+
conn.close()
|
|
145
|
+
|
|
146
|
+
return out
|
|
147
|
+
|
|
91
148
|
def query(self, query: str) -> str:
|
|
92
149
|
try:
|
|
93
|
-
|
|
94
|
-
# Markdown version of the output
|
|
95
|
-
logger.info(
|
|
96
|
-
f"🔍 Executing query: {query[:60]}{'...' if len(query) > 60 else ''}"
|
|
97
|
-
)
|
|
98
|
-
result = self.conn.execute(query).fetchdf().to_markdown()
|
|
99
|
-
logger.info("✅ Query executed successfully")
|
|
100
|
-
return result
|
|
101
|
-
elif self.result_format == "duckbox":
|
|
102
|
-
# Duckbox version of the output
|
|
103
|
-
buffer = io.StringIO()
|
|
104
|
-
with redirect_stdout(buffer):
|
|
105
|
-
self.conn.sql(query).show(max_rows=100, max_col_width=20)
|
|
106
|
-
return buffer.getvalue()
|
|
107
|
-
else:
|
|
108
|
-
# Text version of the output
|
|
109
|
-
return str(self.conn.execute(query).fetchall())
|
|
150
|
+
return self._execute(query)
|
|
110
151
|
|
|
111
152
|
except Exception as e:
|
|
112
153
|
raise ValueError(f"❌ Error executing query: {e}")
|
|
@@ -118,6 +159,7 @@ async def main(
|
|
|
118
159
|
result_format: Literal["markdown", "duckbox", "text"] = "markdown",
|
|
119
160
|
home_dir: str | None = None,
|
|
120
161
|
saas_mode: bool = False,
|
|
162
|
+
read_only: bool = False,
|
|
121
163
|
):
|
|
122
164
|
logger.info("Starting MotherDuck MCP Server")
|
|
123
165
|
server = Server("mcp-server-motherduck")
|
|
@@ -127,6 +169,7 @@ async def main(
|
|
|
127
169
|
motherduck_token=motherduck_token,
|
|
128
170
|
home_dir=home_dir,
|
|
129
171
|
saas_mode=saas_mode,
|
|
172
|
+
read_only=read_only,
|
|
130
173
|
)
|
|
131
174
|
|
|
132
175
|
logger.info("Registering handlers")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-server-motherduck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5
|
|
4
4
|
Summary: A MCP server for MotherDuck and local DuckDB
|
|
5
5
|
Author-email: tdoehmen <till@motherduck.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -15,6 +15,10 @@ Description-Content-Type: text/markdown
|
|
|
15
15
|
|
|
16
16
|
An MCP server implementation that interacts with DuckDB and MotherDuck databases, providing SQL analytics capabilities to AI Assistants and IDEs.
|
|
17
17
|
|
|
18
|
+
## Resources
|
|
19
|
+
- [Close the Loop: Faster Data Pipelines with MCP, DuckDB & AI (Blogpost)](https://motherduck.com/blog/faster-data-pipelines-with-mcp-duckdb-ai/)
|
|
20
|
+
- [Faster Data Pipelines development with MCP and DuckDB (YouTube)](https://www.youtube.com/watch?v=yG1mv8ZRxcU)
|
|
21
|
+
|
|
18
22
|
## Features
|
|
19
23
|
|
|
20
24
|
- **Hybrid execution**: query data from local DuckDB or/and cloud-based MotherDuck databases
|
|
@@ -44,13 +48,14 @@ All interactions with both DuckDB and MotherDuck are done through writing SQL qu
|
|
|
44
48
|
## Getting Started
|
|
45
49
|
|
|
46
50
|
### General Prerequisites
|
|
51
|
+
|
|
47
52
|
- `uv` installed, you can install it using `pip install uv` or `brew install uv`
|
|
48
53
|
|
|
49
|
-
If you plan to use the MCP with Claude Desktop or any other MCP comptabile client, the client need to be installed.
|
|
54
|
+
If you plan to use the MCP with Claude Desktop or any other MCP comptabile client, the client need to be installed.
|
|
50
55
|
|
|
51
56
|
### Prerequisites for DuckDB
|
|
52
57
|
|
|
53
|
-
- No prerequisites. The MCP server can create an in-memory database on-the-fly
|
|
58
|
+
- No prerequisites. The MCP server can create an in-memory database on-the-fly
|
|
54
59
|
- Or connect to an existing local DuckDB database file , or one stored on remote object storage (e.g., AWS S3).
|
|
55
60
|
|
|
56
61
|
See [Connect to local DuckDB](#connect-to-local-duckdb).
|
|
@@ -67,7 +72,7 @@ See [Connect to local DuckDB](#connect-to-local-duckdb).
|
|
|
67
72
|
|
|
68
73
|
2. Open Cursor:
|
|
69
74
|
|
|
70
|
-
- To set it up globally for the first time, go to Settings->MCP and click on "+ Add new global MCP server".
|
|
75
|
+
- To set it up globally for the first time, go to Settings->MCP and click on "+ Add new global MCP server".
|
|
71
76
|
- This will open a `mcp.json` file to which you add the following configuration:
|
|
72
77
|
|
|
73
78
|
```json
|
|
@@ -90,6 +95,7 @@ See [Connect to local DuckDB](#connect-to-local-duckdb).
|
|
|
90
95
|
### Usage with VS Code
|
|
91
96
|
|
|
92
97
|
[](https://insiders.vscode.dev/redirect/mcp/install?name=mcp-server-motherduck&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-motherduck%22%2C%22--db-path%22%2C%22md%3A%22%2C%22--motherduck-token%22%2C%22%24%7Binput%3Amotherduck_token%7D%22%5D%7D&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22motherduck_token%22%2C%22description%22%3A%22MotherDuck+Token%22%2C%22password%22%3Atrue%7D%5D) [](https://insiders.vscode.dev/redirect/mcp/install?name=mcp-server-motherduck&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-motherduck%22%2C%22--db-path%22%2C%22md%3A%22%2C%22--motherduck-token%22%2C%22%24%7Binput%3Amotherduck_token%7D%22%5D%7D&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22motherduck_token%22%2C%22description%22%3A%22MotherDuck+Token%22%2C%22password%22%3Atrue%7D%5D&quality=insiders)
|
|
98
|
+
|
|
93
99
|
1. For the quickest installation, click one of the "Install with UV" buttons at the top of this README.
|
|
94
100
|
|
|
95
101
|
### Manual Installation
|
|
@@ -186,12 +192,13 @@ Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace
|
|
|
186
192
|
|
|
187
193
|
If the MCP server is exposed to third parties and should only have read access to data, we recommend using a read scaling token and running the MCP server in SaaS mode.
|
|
188
194
|
|
|
189
|
-
**Read Scaling Tokens** are special access tokens that enable scalable read operations by allowing up to 4 concurrent read replicas, improving performance for multiple end users while *restricting write capabilities*.
|
|
195
|
+
**Read Scaling Tokens** are special access tokens that enable scalable read operations by allowing up to 4 concurrent read replicas, improving performance for multiple end users while *restricting write capabilities*.
|
|
190
196
|
Refer to the [Read Scaling documentation](https://motherduck.com/docs/key-tasks/authenticating-and-connecting-to-motherduck/read-scaling/#creating-a-read-scaling-token) to learn how to create a read-scaling token.
|
|
191
197
|
|
|
192
198
|
**SaaS Mode** in MotherDuck enhances security by restricting it's access to local files, databases, extensions, and configurations, making it ideal for third-party tools that require stricter environment protection. Learn more about it in the [SaaS Mode documentation](https://motherduck.com/docs/key-tasks/authenticating-and-connecting-to-motherduck/authenticating-to-motherduck/#authentication-using-saas-mode).
|
|
193
199
|
|
|
194
200
|
**Secure Configuration**
|
|
201
|
+
|
|
195
202
|
```json
|
|
196
203
|
{
|
|
197
204
|
"mcpServers": {
|
|
@@ -215,6 +222,7 @@ Refer to the [Read Scaling documentation](https://motherduck.com/docs/key-tasks/
|
|
|
215
222
|
To connect to a local DuckDB, instead of using the MotherDuck token, specify the path to your local DuckDB database file or use `:memory:` for an in-memory database.
|
|
216
223
|
|
|
217
224
|
In-memory database:
|
|
225
|
+
|
|
218
226
|
```json
|
|
219
227
|
{
|
|
220
228
|
"mcpServers": {
|
|
@@ -231,6 +239,7 @@ In-memory database:
|
|
|
231
239
|
```
|
|
232
240
|
|
|
233
241
|
Local DuckDB file:
|
|
242
|
+
|
|
234
243
|
```json
|
|
235
244
|
{
|
|
236
245
|
"mcpServers": {
|
|
@@ -246,6 +255,32 @@ Local DuckDB file:
|
|
|
246
255
|
}
|
|
247
256
|
```
|
|
248
257
|
|
|
258
|
+
Local DuckDB file in [readonly mode](https://duckdb.org/docs/stable/connect/concurrency.html):
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"mcpServers": {
|
|
263
|
+
"mcp-server-motherduck": {
|
|
264
|
+
"command": "uvx",
|
|
265
|
+
"args": [
|
|
266
|
+
"mcp-server-motherduck",
|
|
267
|
+
"--db-path",
|
|
268
|
+
"/path/to/your/local.db",
|
|
269
|
+
"--read-only"
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Note**: readonly mode for local file-backed DuckDB connections also makes use of
|
|
277
|
+
short lived connections. Each time the query MCP tool is used a temporary,
|
|
278
|
+
reaodnly connection is created + query is executed + connection is closed. This
|
|
279
|
+
feature was motivated by a workflow where [DBT](https://www.getdbt.com) was for
|
|
280
|
+
modeling data within duckdb and then an MCP client (Windsurf/Cline/Claude/Cursor)
|
|
281
|
+
was used for exploring the database. The short lived connections allow each tool
|
|
282
|
+
to run and then release their connection, allowing the next tool to connect.
|
|
283
|
+
|
|
249
284
|
## Example Queries
|
|
250
285
|
|
|
251
286
|
Once configured, you can e.g. ask Claude to run queries like:
|
|
@@ -307,10 +342,10 @@ To run the server from a local development environment, use the following config
|
|
|
307
342
|
"mcp-server-motherduck": {
|
|
308
343
|
"command": "uv",
|
|
309
344
|
"args": [
|
|
310
|
-
"--directory",
|
|
311
|
-
"/path/to/your/local/mcp-server-motherduck",
|
|
312
|
-
"run",
|
|
313
|
-
"mcp-server-motherduck",
|
|
345
|
+
"--directory",
|
|
346
|
+
"/path/to/your/local/mcp-server-motherduck",
|
|
347
|
+
"run",
|
|
348
|
+
"mcp-server-motherduck",
|
|
314
349
|
"--db-path",
|
|
315
350
|
"md:",
|
|
316
351
|
"--motherduck-token",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mcp_server_motherduck/__init__.py,sha256=Yd_ron4Fy7mV_p48z0IFNKNbqC7fYfjNnPv4pJ6dRCg,2208
|
|
2
|
+
mcp_server_motherduck/prompt.py,sha256=P7BrmhVXwDkPeSHQ3f25WMP6lpBpN2BxDzYPOQ3fxX8,56699
|
|
3
|
+
mcp_server_motherduck/server.py,sha256=hPZkx1WafvNx8LCLRdq0ftQV_C4qz7x18kqY5ACgZDk,10760
|
|
4
|
+
mcp_server_motherduck-0.5.dist-info/METADATA,sha256=Rg2d2t94aFpd1ArzkAoWY5su_J7n8SoXz9YBY4hGLws,12523
|
|
5
|
+
mcp_server_motherduck-0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
mcp_server_motherduck-0.5.dist-info/entry_points.txt,sha256=dRTgcvWJn40bz0PVuKPylK6w92cFN32lwunZOgo5j4s,69
|
|
7
|
+
mcp_server_motherduck-0.5.dist-info/licenses/LICENSE,sha256=Tj68w9jCiceFKTvZ3jET-008NjhozcQMXpm-fyL9WUI,1067
|
|
8
|
+
mcp_server_motherduck-0.5.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
mcp_server_motherduck/__init__.py,sha256=dugvMESmREMXUK0u7gRBLOQFFz29RhR7C3DemivAWo4,1826
|
|
2
|
-
mcp_server_motherduck/prompt.py,sha256=P7BrmhVXwDkPeSHQ3f25WMP6lpBpN2BxDzYPOQ3fxX8,56699
|
|
3
|
-
mcp_server_motherduck/server.py,sha256=Z_jDV_CgodFB8JpBYxB8H0iHRHulg-y86GvTC5Szkjc,9446
|
|
4
|
-
mcp_server_motherduck-0.4.2.dist-info/METADATA,sha256=HmnpB0LPxj2s-Nb-Sec_08VLH3K0mLAQZ3pv37xH7zw,11366
|
|
5
|
-
mcp_server_motherduck-0.4.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
-
mcp_server_motherduck-0.4.2.dist-info/entry_points.txt,sha256=dRTgcvWJn40bz0PVuKPylK6w92cFN32lwunZOgo5j4s,69
|
|
7
|
-
mcp_server_motherduck-0.4.2.dist-info/licenses/LICENSE,sha256=Tj68w9jCiceFKTvZ3jET-008NjhozcQMXpm-fyL9WUI,1067
|
|
8
|
-
mcp_server_motherduck-0.4.2.dist-info/RECORD,,
|
|
File without changes
|
{mcp_server_motherduck-0.4.2.dist-info → mcp_server_motherduck-0.5.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{mcp_server_motherduck-0.4.2.dist-info → mcp_server_motherduck-0.5.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|