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.
@@ -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
 
@@ -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.4.2"
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 f"{db_path}?motherduck_token={motherduck_token}&saas_mode=true", "motherduck"
88
+ return (
89
+ f"{db_path}?motherduck_token={motherduck_token}&saas_mode=true",
90
+ "motherduck",
91
+ )
66
92
  else:
67
- return f"{db_path}?motherduck_token={motherduck_token}", "motherduck"
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
- if self.result_format == "markdown":
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.4.2
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
  [![Install with UV in VS Code](https://img.shields.io/badge/VS_Code-UV-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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) [![Install with UV in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-UV-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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,,