mcp-server-motherduck 0.2.2__tar.gz → 0.3.1__tar.gz

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.

@@ -0,0 +1,33 @@
1
+ name: Upload Python Package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ pypi-publish:
12
+ name: python
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v5
20
+
21
+ - name: "Set up Python"
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version-file: "pyproject.toml"
25
+
26
+ - name: Install the project
27
+ run: uv sync
28
+
29
+ - name: Build package
30
+ run: uv build
31
+
32
+ - name: Publish package
33
+ run: uv publish
@@ -0,0 +1,176 @@
1
+ .DS_Store
2
+
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # PyInstaller
32
+ # Usually these files are written by a python script from a template
33
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
34
+ *.manifest
35
+ *.spec
36
+
37
+ # Installer logs
38
+ pip-log.txt
39
+ pip-delete-this-directory.txt
40
+
41
+ # Unit test / coverage reports
42
+ htmlcov/
43
+ .tox/
44
+ .nox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ *.py,cover
52
+ .hypothesis/
53
+ .pytest_cache/
54
+ cover/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # UV
100
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ #uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ #poetry.lock
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ #pdm.lock
115
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
116
+ # in version control.
117
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
118
+ .pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
123
+ __pypackages__/
124
+
125
+ # Celery stuff
126
+ celerybeat-schedule
127
+ celerybeat.pid
128
+
129
+ # SageMath parsed files
130
+ *.sage.py
131
+
132
+ # Environments
133
+ .env
134
+ .venv
135
+ env/
136
+ venv/
137
+ ENV/
138
+ env.bak/
139
+ venv.bak/
140
+
141
+ # Spyder project settings
142
+ .spyderproject
143
+ .spyproject
144
+
145
+ # Rope project settings
146
+ .ropeproject
147
+
148
+ # mkdocs documentation
149
+ /site
150
+
151
+ # mypy
152
+ .mypy_cache/
153
+ .dmypy.json
154
+ dmypy.json
155
+
156
+ # Pyre type checker
157
+ .pyre/
158
+
159
+ # pytype static type analyzer
160
+ .pytype/
161
+
162
+ # Cython debug symbols
163
+ cython_debug/
164
+
165
+ # PyCharm
166
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
167
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
168
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
169
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
170
+ #.idea/
171
+
172
+ # Ruff stuff:
173
+ .ruff_cache/
174
+
175
+ # PyPI configuration file
176
+ .pypirc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-server-motherduck
3
- Version: 0.2.2
3
+ Version: 0.3.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
@@ -11,30 +11,28 @@ Description-Content-Type: text/markdown
11
11
 
12
12
  # mcp-server-motherduck MCP server
13
13
 
14
- A MCP server for MotherDuck and local DuckDB
14
+ An [MCP server](https://modelcontextprotocol.io/introduction) for MotherDuck and local DuckDB.
15
15
 
16
16
  ## Components
17
17
 
18
- ### Resources
19
-
20
18
  ### Prompts
21
19
 
22
20
  The server provides one prompt:
23
- - duckdb-motherduck-initial-prompt: A prompt to initialize a connection to duckdb or motherduck and start working with it
21
+
22
+ - duckdb-motherduck-prompt: A prompt to initialize a connection to duckdb or motherduck and start working with it
24
23
 
25
24
  ### Tools
26
25
 
27
- The server offers three tools:
28
- - initialize-connection: Create a connection to either a local DuckDB or MotherDuck and retrieve available databases
29
- - Takes "type" (DuckDB or MotherDuck) as input
30
- - read-schemas: Get table schemas from a specific DuckDB/MotherDuck database
31
- - Takes "database_name" as required string arguments
32
- - execute-query: Execute a query on the MotherDuck (DuckDB) database
26
+ The server offers one tool:
27
+
28
+ - query: Execute a query on the MotherDuck (DuckDB) database
33
29
  - Takes "query" as required string arguments
34
30
 
31
+ This is because all the interactions with both the DuckDB and MotherDuck are done through writing SQL queries.
32
+
35
33
  ## Usage with Claude Desktop
36
34
 
37
- Add the snippet below to your Claude Desktop config and make sure to set the HOME var to your home folder (needed by DuckDB).
35
+ Add the snippet below to your Claude Desktop config and make sure to set the HOME var to your home folder (needed by DuckDB).
38
36
 
39
37
  When using MotherDuck, you also need to set a [MotherDuck token](https://motherduck.com/docs/key-tasks/authenticating-and-connecting-to-motherduck/authenticating-to-motherduck/#storing-the-access-token-as-an-environment-variable) env var.
40
38
 
@@ -43,6 +41,7 @@ On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
43
41
  On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
44
42
 
45
43
  ### Servers Configuration
44
+
46
45
  ```
47
46
  "mcpServers": {
48
47
  "mcp-server-motherduck": {
@@ -51,8 +50,8 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
51
50
  "mcp-server-motherduck"
52
51
  ],
53
52
  "env": {
54
- "motherduck_token": "",
55
- "HOME": ""
53
+ "motherduck_token": "<your-motherduck-token>",
54
+ "HOME": "<your-home-folder-for-project-files>"
56
55
  }
57
56
  }
58
57
  }
@@ -61,4 +60,3 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
61
60
  ## License
62
61
 
63
62
  This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
64
-
@@ -1,29 +1,27 @@
1
1
  # mcp-server-motherduck MCP server
2
2
 
3
- A MCP server for MotherDuck and local DuckDB
3
+ An [MCP server](https://modelcontextprotocol.io/introduction) for MotherDuck and local DuckDB.
4
4
 
5
5
  ## Components
6
6
 
7
- ### Resources
8
-
9
7
  ### Prompts
10
8
 
11
9
  The server provides one prompt:
12
- - duckdb-motherduck-initial-prompt: A prompt to initialize a connection to duckdb or motherduck and start working with it
10
+
11
+ - duckdb-motherduck-prompt: A prompt to initialize a connection to duckdb or motherduck and start working with it
13
12
 
14
13
  ### Tools
15
14
 
16
- The server offers three tools:
17
- - initialize-connection: Create a connection to either a local DuckDB or MotherDuck and retrieve available databases
18
- - Takes "type" (DuckDB or MotherDuck) as input
19
- - read-schemas: Get table schemas from a specific DuckDB/MotherDuck database
20
- - Takes "database_name" as required string arguments
21
- - execute-query: Execute a query on the MotherDuck (DuckDB) database
15
+ The server offers one tool:
16
+
17
+ - query: Execute a query on the MotherDuck (DuckDB) database
22
18
  - Takes "query" as required string arguments
23
19
 
20
+ This is because all the interactions with both the DuckDB and MotherDuck are done through writing SQL queries.
21
+
24
22
  ## Usage with Claude Desktop
25
23
 
26
- Add the snippet below to your Claude Desktop config and make sure to set the HOME var to your home folder (needed by DuckDB).
24
+ Add the snippet below to your Claude Desktop config and make sure to set the HOME var to your home folder (needed by DuckDB).
27
25
 
28
26
  When using MotherDuck, you also need to set a [MotherDuck token](https://motherduck.com/docs/key-tasks/authenticating-and-connecting-to-motherduck/authenticating-to-motherduck/#storing-the-access-token-as-an-environment-variable) env var.
29
27
 
@@ -32,6 +30,7 @@ On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
32
30
  On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
33
31
 
34
32
  ### Servers Configuration
33
+
35
34
  ```
36
35
  "mcpServers": {
37
36
  "mcp-server-motherduck": {
@@ -40,8 +39,8 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
40
39
  "mcp-server-motherduck"
41
40
  ],
42
41
  "env": {
43
- "motherduck_token": "",
44
- "HOME": ""
42
+ "motherduck_token": "<your-motherduck-token>",
43
+ "HOME": "<your-home-folder-for-project-files>"
45
44
  }
46
45
  }
47
46
  }
@@ -50,4 +49,3 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
50
49
  ## License
51
50
 
52
51
  This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
53
-
@@ -0,0 +1,6 @@
1
+ .ONESHELL:
2
+
3
+ .PHONY: publish
4
+
5
+ publish:
6
+ uv publish --token $(PYPI_KEY)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-server-motherduck"
3
- version = "0.2.2"
3
+ version = "0.3.1"
4
4
  description = "A MCP server for MotherDuck and local DuckDB"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -0,0 +1,19 @@
1
+ from . import server
2
+ import asyncio
3
+ import argparse
4
+
5
+
6
+ def main():
7
+ """Main entry point for the package."""
8
+ parser = argparse.ArgumentParser(description="MotherDuck MCP Server")
9
+ parser.add_argument(
10
+ "--db-path",
11
+ help="Path to local DuckDB database file",
12
+ )
13
+
14
+ args = parser.parse_args()
15
+ asyncio.run(server.main(db_path=args.db_path))
16
+
17
+
18
+ # Optionally expose other important items at package level
19
+ __all__ = ["main", "server"]
@@ -0,0 +1,19 @@
1
+ from . import server
2
+ import asyncio
3
+ import argparse
4
+
5
+
6
+ def main():
7
+ """Main entry point for the package."""
8
+ parser = argparse.ArgumentParser(description="MotherDuck MCP Server")
9
+ parser.add_argument(
10
+ "--db-path",
11
+ help="Path to local DuckDB database file",
12
+ )
13
+
14
+ args = parser.parse_args()
15
+ asyncio.run(server.main(db_path=args.db_path))
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
@@ -1,33 +1,20 @@
1
- import asyncio
2
-
3
- from mcp.server.models import InitializationOptions
4
- import mcp.types as types
5
- from mcp.server import NotificationOptions, Server
6
- from pydantic import AnyUrl
7
- import mcp.server.stdio
8
- import os
9
- import duckdb
10
-
11
- SERVER_VERSION = "0.2.2"
12
-
13
- PROMPT_TEMPLATE = """The assistant's goal is to help users interact with DuckDB/MotherDuck databases effectively.
1
+ PROMPT_TEMPLATE = """The assistant's goal is to help users interact with DuckDB or MotherDuck databases effectively.
14
2
  Start by establishing the connection type preference and maintain a helpful, conversational tone throughout the interaction.
3
+
15
4
  <mcp>
16
5
  Tools:
17
- - "initialize-connection": Creates connection to DuckDB or MotherDuck
18
- - "read-schemas": Retrieves table schemas from specified database
19
- - "execute-query": Runs SQL queries and returns results
6
+ - "query": Runs SQL queries and returns results
20
7
  </mcp>
21
8
 
22
9
  <workflow>
23
10
  1. Connection Setup:
24
11
  - Ask whether user prefers MotherDuck or local DuckDB
25
- - Use initialize-connection with chosen type
12
+ - Use query with the chosen type
26
13
  - Store and display available databases if successful
27
14
 
28
15
  2. Database Exploration:
29
16
  - When user mentions data analysis needs, identify target database
30
- - Use read-schemas to fetch table information
17
+ - Use query to fetch table information
31
18
  - Present schema details in user-friendly format
32
19
 
33
20
  3. Query Execution:
@@ -50,7 +37,7 @@ Tools:
50
37
  </workflow>
51
38
 
52
39
  <conversation-flow>
53
- 1. Start with: "Hi! Would you prefer to connect to MotherDuck or use a local DuckDB instance?"
40
+ 1. Start with: "Hi! What query would you like to run on your database?"
54
41
 
55
42
  2. After connection:
56
43
  - Acknowledge success/failure
@@ -206,164 +193,3 @@ Common DuckDB Keywords:
206
193
  `ALL`: The `ALL` keyword in SQL specifies that operations should retain all duplicate rows, as seen in commands like `UNION ALL`, `INTERSECT ALL`, and `EXCEPT ALL`, which follow bag semantics instead of eliminating duplicates., Examples: ['UNION ALL\n\n```sql\nSELECT * FROM range(2) t1(x)\nUNION ALL\nSELECT * FROM range(3) t2(x);\n```\nThis example demonstrates using `UNION ALL` to combine rows from two queries without eliminating duplicates.', 'INTERSECT ALL\n\n```sql\nSELECT unnest([5, 5, 6, 6, 6, 6, 7, 8]) AS x\nINTERSECT ALL\nSELECT unnest([5, 6, 6, 7, 7, 9]);\n```\nThis example shows using `INTERSECT ALL` to select rows that are present in both result sets, keeping duplicate values.', 'EXCEPT ALL\n\n```sql\nSELECT unnest([5, 5, 6, 6, 6, 6, 7, 8]) AS x\nEXCEPT ALL\nSELECT unnest([5, 6, 6, 7, 7, 9]);\n```\nThis example illustrates `EXCEPT ALL`, which selects all rows present in the first query but not in the second, without removing duplicates.', 'ORDER BY ALL\n\n```sql\nSELECT *\nFROM addresses\nORDER BY ALL;\n```\nThis SQL command uses `ORDER BY ALL` to sort the result set by all columns sequentially from left to right.']
207
194
  `LIKE`: The `LIKE` expression is used to determine if a string matches a specified pattern, allowing wildcard characters such as `_` to represent any single character and `%` to match any sequence of characters., Examples: ["SELECT 'abc' LIKE 'abc'; -- true", "SELECT 'abc' LIKE 'a%'; -- true", "SELECT 'abc' LIKE '_b_'; -- true", "SELECT 'abc' LIKE 'c'; -- false", "SELECT 'abc' LIKE 'c%'; -- false", "SELECT 'abc' LIKE '%c'; -- true", "SELECT 'abc' NOT LIKE '%c'; -- false", "SELECT 'abc' ILIKE '%C'; -- true"]
208
195
  """
209
-
210
- server = Server("mcp-server-motherduck")
211
-
212
- conn = duckdb.connect()
213
-
214
- @server.list_resources()
215
- async def handle_list_resources() -> list[types.Resource]:
216
- """
217
- List available note resources.
218
- Each note is exposed as a resource with a custom note:// URI scheme.
219
- """
220
- return []
221
-
222
- @server.read_resource()
223
- async def handle_read_resource(uri: AnyUrl) -> str:
224
- """
225
- Read a specific note's content by its URI.
226
- The note name is extracted from the URI host component.
227
- """
228
- raise ValueError(f"Unsupported URI scheme: {uri.scheme}")
229
-
230
- @server.list_prompts()
231
- async def handle_list_prompts() -> list[types.Prompt]:
232
- """
233
- List available prompts.
234
- Each prompt can have optional arguments to customize its behavior.
235
- """
236
- return [
237
- types.Prompt(
238
- name="duckdb-motherduck-initial-prompt",
239
- description="A prompt to initialize a connection to duckdb or motherduck and start working with it",
240
- )
241
- ]
242
-
243
- @server.get_prompt()
244
- async def handle_get_prompt(
245
- name: str, arguments: dict[str, str] | None
246
- ) -> types.GetPromptResult:
247
- """
248
- Generate a prompt by combining arguments with server state.
249
- The prompt includes all current notes and can be customized via arguments.
250
- """
251
- if name != "duckdb-motherduck-initial-prompt":
252
- raise ValueError(f"Unknown prompt: {name}")
253
-
254
- return types.GetPromptResult(
255
- description=f"Initial prompt for interacting with DuckDB/MotherDuck",
256
- messages=[
257
- types.PromptMessage(
258
- role="user",
259
- content=types.TextContent(type="text", text=PROMPT_TEMPLATE),
260
- )
261
- ],
262
- )
263
-
264
- @server.list_tools()
265
- async def handle_list_tools() -> list[types.Tool]:
266
- """
267
- List available tools.
268
- Each tool specifies its arguments using JSON Schema validation.
269
- """
270
- return [
271
- types.Tool(
272
- name="initialize-connection",
273
- description="Create a connection to either a local DuckDB or MotherDuck and retrieve available databases",
274
- inputSchema={
275
- "type": "object",
276
- "properties": {
277
- "type": {"type": "string", "description": "Type of the database, either 'DuckDB' or 'MotherDuck'"},
278
- },
279
- "required": ["type"],
280
- },
281
- ),
282
- types.Tool(
283
- name="read-schemas",
284
- description="Get table schemas from a specific DuckDB/MotherDuck database",
285
- inputSchema={
286
- "type": "object",
287
- "properties": {
288
- "type": {"database_name": "string", "description": "name of the database"},
289
- },
290
- "required": ["database_name"],
291
- },
292
- ),
293
- types.Tool(
294
- name="execute-query",
295
- description="Execute a query on the MotherDuck (DuckDB) database",
296
- inputSchema={
297
- "type": "object",
298
- "properties": {
299
- "query": {"type": "string", "description": "SQL query to execute"},
300
- },
301
- "required": ["query"],
302
- },
303
- )
304
- ]
305
-
306
- @server.call_tool()
307
- async def handle_call_tool(
308
- name: str, arguments: dict | None
309
- ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
310
- """
311
- Handle tool execution requests.
312
- Tools can modify server state and notify clients of changes.
313
- """
314
- global conn
315
- if name == "initialize-connection":
316
- type = arguments["type"].strip().upper()
317
- if not type in ['DUCKDB', 'MOTHERDUCK']:
318
- raise ValueError("Only 'DuckDB' or 'MotherDuck' are supported")
319
- if type == 'MOTHERDUCK' and not os.getenv('motherduck_token'):
320
- raise ValueError("Please set the `motherduck_token` environment variable.")
321
- if type == 'MOTHERDUCK':
322
- conn = duckdb.connect('md:', config={
323
- "custom_user_agent": f"mcp-server-motherduck/{SERVER_VERSION}"
324
- })
325
- elif type == 'DUCKDB':
326
- conn = duckdb.connect()
327
- databases = conn.execute("""select string_agg(database_name, ',\n')
328
- from duckdb_databases() where database_name
329
- not in ('system', 'temp')""").fetchone()[0]
330
- response = f'Connection to {type} successfully established. Here are the available databases: \n{databases}'
331
- return [types.TextContent(type="text", text=response)]
332
- if name == "read-schemas":
333
- database = arguments["database_name"]
334
- tables = conn.execute(f"""
335
- SELECT string_agg(regexp_replace(sql, 'CREATE TABLE ', 'CREATE TABLE '||database_name||'.'), '\n\n') as sql
336
- FROM duckdb_tables()
337
- WHERE database_name = '{database}'""").fetchone()[0]
338
- views = conn.execute(f"""
339
- SELECT string_agg(regexp_replace(sql, 'CREATE TABLE ', 'CREATE TABLE '||database_name||'.'), '\n\n') as sql
340
- FROM duckdb_views()
341
- where schema_name not in ('information_schema', 'pg_catalog', 'localmemdb')
342
- and view_name not in ('duckdb_columns','duckdb_constraints','duckdb_databases','duckdb_indexes','duckdb_schemas','duckdb_tables','duckdb_types','duckdb_views','pragma_database_list','sqlite_master','sqlite_schema','sqlite_temp_master','sqlite_temp_schema')
343
- and database_name = '{database}'
344
- """).fetchone()[0]
345
- results = f"Here are all tables: \n{tables} \n\n Here are all views: {views}"
346
- return [types.TextContent(type="text", text=str(results))]
347
- if name == "execute-query":
348
- try:
349
- results = conn.execute(arguments["query"]).fetchall()
350
- except Exception as e:
351
- raise ValueError('Error querying the database:'+str(e))
352
- return [types.TextContent(type="text", text=str(results))]
353
-
354
-
355
- async def main():
356
- # Run the server using stdin/stdout streams
357
- async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
358
- await server.run(
359
- read_stream,
360
- write_stream,
361
- InitializationOptions(
362
- server_name="motherduck",
363
- server_version=SERVER_VERSION,
364
- capabilities=server.get_capabilities(
365
- notification_options=NotificationOptions(),
366
- experimental_capabilities={},
367
- ),
368
- ),
369
- )
@@ -0,0 +1,196 @@
1
+ import os
2
+ import logging
3
+ import duckdb
4
+ from pydantic import AnyUrl
5
+ from typing import Literal
6
+ import mcp.server.stdio
7
+ import mcp.types as types
8
+ from mcp.server import NotificationOptions, Server
9
+ from mcp.server.models import InitializationOptions
10
+ from .prompt import PROMPT_TEMPLATE
11
+
12
+ SERVER_VERSION = "0.3.1"
13
+
14
+ logger = logging.getLogger("mcp_server_motherduck")
15
+
16
+
17
+ class DatabaseClient:
18
+ def __init__(self, db_path: str = None):
19
+ self.db_path, self.db_type = self._resolve_db_path_type(db_path)
20
+ self.conn = self._initialize_connection()
21
+
22
+ def _initialize_connection(self) -> duckdb.DuckDBPyConnection:
23
+ """Initialize connection to the MotherDuck or DuckDB database"""
24
+
25
+ logger.info(f"Connecting to {self.db_type} database: `{self.db_path}`")
26
+
27
+ return duckdb.connect(
28
+ self.db_path,
29
+ config={"custom_user_agent": f"mcp-server-motherduck/{SERVER_VERSION}"},
30
+ )
31
+
32
+ def _resolve_db_path_type(
33
+ self, db_path: str = None
34
+ ) -> tuple[str, Literal["duckdb", "motherduck"]]:
35
+ """Resolve and validate the database path"""
36
+ # Use MotherDuck if token is available and no path specified
37
+ if db_path is None and os.getenv("motherduck_token"):
38
+ logger.info("Using MotherDuck token to connect to database `md:`")
39
+ return "md:", "motherduck"
40
+
41
+ # Handle MotherDuck paths
42
+ if db_path and (db_path == "md:" or db_path.startswith("md:")):
43
+ if not os.getenv("motherduck_token"):
44
+ raise ValueError(
45
+ "Please set the `motherduck_token` environment variable when using `md:` as db_path."
46
+ )
47
+ return db_path, "motherduck"
48
+
49
+ # Handle local database paths
50
+ if db_path:
51
+ if not os.path.exists(db_path):
52
+ raise FileNotFoundError(
53
+ f"The database path `{db_path}` does not exist."
54
+ )
55
+ return db_path, "duckdb"
56
+
57
+ # Default to in-memory database
58
+ return ":memory:", "duckdb"
59
+
60
+ def query(self, query: str) -> str:
61
+ try:
62
+ return str(self.conn.execute(query).fetchall())
63
+ except Exception as e:
64
+ logger.error(f"Database error executing query: {e}")
65
+ raise ValueError(f"Error executing query: {e}")
66
+
67
+ def mcp_config(self) -> str:
68
+ """Used for debugging purposes to show the current MCP config"""
69
+ return {
70
+ "current_working_directory": os.getcwd(),
71
+ "database_type": self.db_type,
72
+ "database_path": self.db_path,
73
+ }
74
+
75
+
76
+ async def main(db_path: str):
77
+ logger.info(f"Starting MotherDuck MCP Server with DB path: {db_path}")
78
+ server = Server("mcp-server-motherduck")
79
+ db_client = DatabaseClient(db_path=db_path)
80
+
81
+ logger.info("Registering handlers")
82
+
83
+ @server.list_resources()
84
+ async def handle_list_resources() -> list[types.Resource]:
85
+ """
86
+ List available note resources.
87
+ Each note is exposed as a resource with a custom note:// URI scheme.
88
+ """
89
+ logger.info("No resources available to list")
90
+ return []
91
+
92
+ @server.read_resource()
93
+ async def handle_read_resource(uri: AnyUrl) -> str:
94
+ """
95
+ Read a specific note's content by its URI.
96
+ The note name is extracted from the URI host component.
97
+ """
98
+ logger.info(f"Reading resource: {uri}")
99
+ raise ValueError(f"Unsupported URI scheme: {uri.scheme}")
100
+
101
+ @server.list_prompts()
102
+ async def handle_list_prompts() -> list[types.Prompt]:
103
+ """
104
+ List available prompts.
105
+ Each prompt can have optional arguments to customize its behavior.
106
+ """
107
+ logger.info("Listing prompts")
108
+ # TODO: Check where and how this is used, and how to optimize this.
109
+ # Check postgres and sqlite servers.
110
+ return [
111
+ types.Prompt(
112
+ name="duckdb-motherduck-initial-prompt",
113
+ description="A prompt to initialize a connection to duckdb or motherduck and start working with it",
114
+ )
115
+ ]
116
+
117
+ @server.get_prompt()
118
+ async def handle_get_prompt(
119
+ name: str, arguments: dict[str, str] | None
120
+ ) -> types.GetPromptResult:
121
+ """
122
+ Generate a prompt by combining arguments with server state.
123
+ The prompt includes all current notes and can be customized via arguments.
124
+ """
125
+ logger.info(f"Getting prompt: {name}::{arguments}")
126
+ # TODO: Check where and how this is used, and how to optimize this.
127
+ # Check postgres and sqlite servers.
128
+ if name != "duckdb-motherduck-initial-prompt":
129
+ raise ValueError(f"Unknown prompt: {name}")
130
+
131
+ return types.GetPromptResult(
132
+ description="Initial prompt for interacting with DuckDB/MotherDuck",
133
+ messages=[
134
+ types.PromptMessage(
135
+ role="user",
136
+ content=types.TextContent(type="text", text=PROMPT_TEMPLATE),
137
+ )
138
+ ],
139
+ )
140
+
141
+ @server.list_tools()
142
+ async def handle_list_tools() -> list[types.Tool]:
143
+ """
144
+ List available tools.
145
+ Each tool specifies its arguments using JSON Schema validation.
146
+ """
147
+ logger.info("Listing tools")
148
+ return [
149
+ types.Tool(
150
+ name="query",
151
+ description="Use this to execute a query on the MotherDuck or DuckDB database",
152
+ inputSchema={
153
+ "type": "object",
154
+ "properties": {
155
+ "query": {
156
+ "type": "string",
157
+ "description": "SQL query to execute that is a dialect of DuckDB SQL",
158
+ },
159
+ },
160
+ "required": ["query"],
161
+ },
162
+ ),
163
+ ]
164
+
165
+ @server.call_tool()
166
+ async def handle_tool_call(
167
+ name: str, arguments: dict | None
168
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
169
+ """
170
+ Handle tool execution requests.
171
+ Tools can modify server state and notify clients of changes.
172
+ """
173
+ logger.info(f"Calling tool: {name}::{arguments}")
174
+ try:
175
+ if name == "query":
176
+ tool_response = db_client.query(arguments["query"])
177
+ return [types.TextContent(type="text", text=str(tool_response))]
178
+
179
+ except Exception as e:
180
+ logger.error(f"Error executing tool {name}: {e}")
181
+ raise ValueError(f"Error executing tool {name}: {str(e)}")
182
+
183
+ # Run the server using stdin/stdout streams
184
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
185
+ await server.run(
186
+ read_stream,
187
+ write_stream,
188
+ InitializationOptions(
189
+ server_name="motherduck",
190
+ server_version=SERVER_VERSION,
191
+ capabilities=server.get_capabilities(
192
+ notification_options=NotificationOptions(),
193
+ experimental_capabilities={},
194
+ ),
195
+ ),
196
+ )
@@ -1 +0,0 @@
1
- .DS_Store
@@ -1,9 +0,0 @@
1
- from . import server
2
- import asyncio
3
-
4
- def main():
5
- """Main entry point for the package."""
6
- asyncio.run(server.main())
7
-
8
- # Optionally expose other important items at package level
9
- __all__ = ['main', 'server']