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.
- mcp_server_motherduck-0.3.1/.github/workflows/python-publish.yml +33 -0
- mcp_server_motherduck-0.3.1/.gitignore +176 -0
- {mcp_server_motherduck-0.2.2 → mcp_server_motherduck-0.3.1}/PKG-INFO +13 -15
- {mcp_server_motherduck-0.2.2 → mcp_server_motherduck-0.3.1}/README.md +12 -14
- mcp_server_motherduck-0.3.1/makefile +6 -0
- {mcp_server_motherduck-0.2.2 → mcp_server_motherduck-0.3.1}/pyproject.toml +1 -1
- mcp_server_motherduck-0.3.1/src/mcp_server_motherduck/__init__.py +19 -0
- mcp_server_motherduck-0.3.1/src/mcp_server_motherduck/__main__.py +19 -0
- mcp_server_motherduck-0.2.2/src/mcp_server_motherduck/server.py → mcp_server_motherduck-0.3.1/src/mcp_server_motherduck/prompt.py +6 -180
- mcp_server_motherduck-0.3.1/src/mcp_server_motherduck/server.py +196 -0
- mcp_server_motherduck-0.2.2/.gitignore +0 -1
- mcp_server_motherduck-0.2.2/src/mcp_server_motherduck/__init__.py +0 -9
- {mcp_server_motherduck-0.2.2 → mcp_server_motherduck-0.3.1}/.python-version +0 -0
- {mcp_server_motherduck-0.2.2 → mcp_server_motherduck-0.3.1}/LICENSE +0 -0
- {mcp_server_motherduck-0.2.2 → mcp_server_motherduck-0.3.1}/uv.lock +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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,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
|
-
|
|
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
|
-
- "
|
|
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
|
|
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
|
|
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!
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|