mcp-server-motherduck 0.2.1__tar.gz → 0.3__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
@@ -1,3 +1,5 @@
1
+ .DS_Store
2
+
1
3
  # Byte-compiled / optimized / DLL files
2
4
  __pycache__/
3
5
  *.py[cod]
@@ -94,6 +96,12 @@ ipython_config.py
94
96
  # install all needed dependencies.
95
97
  #Pipfile.lock
96
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
+
97
105
  # poetry
98
106
  # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
107
  # This is especially recommended for binary packages to ensure reproducibility, and is more
@@ -106,8 +114,10 @@ ipython_config.py
106
114
  #pdm.lock
107
115
  # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
116
  # in version control.
109
- # https://pdm.fming.dev/#use-with-ide
117
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110
118
  .pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
111
121
 
112
122
  # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
123
  __pypackages__/
@@ -159,34 +169,8 @@ cython_debug/
159
169
  # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
170
  #.idea/
161
171
 
162
- # General
163
- .DS_Store
164
- .AppleDouble
165
- .LSOverride
166
- .idea/
167
-
168
- # Icon must end with two \r
169
- Icon
170
-
171
- # Thumbnails
172
- ._*
173
-
174
- # Files that might appear in the root of a volume
175
- .DocumentRevisions-V100
176
- .fseventsd
177
- .Spotlight-V100
178
- .TemporaryItems
179
- .Trashes
180
- .VolumeIcon.icns
181
- .com.apple.timemachine.donotpresent
182
-
183
- # Directories potentially created on remote AFP share
184
- .AppleDB
185
- .AppleDesktop
186
- Network Trash Folder
187
- Temporary Items
188
- .apdisk
189
-
190
- # Node
191
- node_modules/
192
- dist/
172
+ # Ruff stuff:
173
+ .ruff_cache/
174
+
175
+ # PyPI configuration file
176
+ .pypirc
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 MotherDuck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-server-motherduck
3
+ Version: 0.3
4
+ Summary: A MCP server for MotherDuck and local DuckDB
5
+ Author-email: tdoehmen <till@motherduck.com>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: duckdb>=1.1.3
9
+ Requires-Dist: mcp>=1.0.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # mcp-server-motherduck MCP server
13
+
14
+ An [MCP server](https://modelcontextprotocol.io/introduction) for MotherDuck and local DuckDB.
15
+
16
+ ## Components
17
+
18
+ ### Prompts
19
+
20
+ The server provides one prompt:
21
+
22
+ - duckdb-motherduck-prompt: A prompt to initialize a connection to duckdb or motherduck and start working with it
23
+
24
+ ### Tools
25
+
26
+ The server offers one tool:
27
+
28
+ - query: Execute a query on the MotherDuck (DuckDB) database
29
+ - Takes "query" as required string arguments
30
+
31
+ This is because all the interactions with both the DuckDB and MotherDuck are done through writing SQL queries.
32
+
33
+ ## Usage with Claude Desktop
34
+
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).
36
+
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.
38
+
39
+ On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
40
+
41
+ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
42
+
43
+ ### Servers Configuration
44
+
45
+ ```
46
+ "mcpServers": {
47
+ "mcp-server-motherduck": {
48
+ "command": "uvx",
49
+ "args": [
50
+ "mcp-server-motherduck"
51
+ ],
52
+ "env": {
53
+ "motherduck_token": "<your-motherduck-token>",
54
+ "HOME": "<your-home-folder-for-project-files>"
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ ## License
61
+
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.
@@ -0,0 +1,51 @@
1
+ # mcp-server-motherduck MCP server
2
+
3
+ An [MCP server](https://modelcontextprotocol.io/introduction) for MotherDuck and local DuckDB.
4
+
5
+ ## Components
6
+
7
+ ### Prompts
8
+
9
+ The server provides one prompt:
10
+
11
+ - duckdb-motherduck-prompt: A prompt to initialize a connection to duckdb or motherduck and start working with it
12
+
13
+ ### Tools
14
+
15
+ The server offers one tool:
16
+
17
+ - query: Execute a query on the MotherDuck (DuckDB) database
18
+ - Takes "query" as required string arguments
19
+
20
+ This is because all the interactions with both the DuckDB and MotherDuck are done through writing SQL queries.
21
+
22
+ ## Usage with Claude Desktop
23
+
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).
25
+
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.
27
+
28
+ On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
29
+
30
+ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
31
+
32
+ ### Servers Configuration
33
+
34
+ ```
35
+ "mcpServers": {
36
+ "mcp-server-motherduck": {
37
+ "command": "uvx",
38
+ "args": [
39
+ "mcp-server-motherduck"
40
+ ],
41
+ "env": {
42
+ "motherduck_token": "<your-motherduck-token>",
43
+ "HOME": "<your-home-folder-for-project-files>"
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## License
50
+
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.
@@ -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.1"
3
+ version = "0.3"
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"]
@@ -1,32 +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
- PROMPT_TEMPLATE = """
12
- 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.
13
2
  Start by establishing the connection type preference and maintain a helpful, conversational tone throughout the interaction.
3
+
14
4
  <mcp>
15
5
  Tools:
16
- - "initialize-connection": Creates connection to DuckDB or MotherDuck
17
- - "read-schemas": Retrieves table schemas from specified database
18
- - "execute-query": Runs SQL queries and returns results
6
+ - "query": Runs SQL queries and returns results
19
7
  </mcp>
20
8
 
21
9
  <workflow>
22
10
  1. Connection Setup:
23
11
  - Ask whether user prefers MotherDuck or local DuckDB
24
- - Use initialize-connection with chosen type
12
+ - Use query with the chosen type
25
13
  - Store and display available databases if successful
26
14
 
27
15
  2. Database Exploration:
28
16
  - When user mentions data analysis needs, identify target database
29
- - Use read-schemas to fetch table information
17
+ - Use query to fetch table information
30
18
  - Present schema details in user-friendly format
31
19
 
32
20
  3. Query Execution:
@@ -49,7 +37,7 @@ Tools:
49
37
  </workflow>
50
38
 
51
39
  <conversation-flow>
52
- 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?"
53
41
 
54
42
  2. After connection:
55
43
  - Acknowledge success/failure
@@ -112,7 +100,7 @@ Here are some DuckDB SQL syntax specifics you should be aware of:
112
100
  - DuckDB has built-in functions for regex regexp_matches(column, regex), regexp_replace(column, regex), and regexp_extract(column, regex).
113
101
  - DuckDB has a way to quickly get a subset of your data with `SELECT * FROM large_table USING SAMPLE 10%;`
114
102
 
115
- DuckDB Functions:
103
+ Common DuckDB Functions:
116
104
  `count`: Calculates the total number of rows returned by a SQL query result. This function is commonly used to determine the row count of a SELECT operation., Parameters: ['result: The result object']
117
105
  `sum`: Calculates the total of all non-null values in a specified column or expression across rows., Parameters: ['arg: Values to be aggregated']
118
106
  `max`: Returns the largest value from all values in a specified column or expression., Parameters: ['arg: expression to evaluate maximum', "n: top 'n' value list size(optional)"]
@@ -143,7 +131,7 @@ DuckDB Functions:
143
131
  `now`: Obtains the current date and time at the start of the current transaction, using the system's time zone., Parameters: ['None: No parameters required(optional)']
144
132
  `group_concat`: Concatenates column string values using a specified separator, respecting the provided order., Parameters: ['arg: The column to concatenate', 'sep: Separator between concatenated values(optional)', 'ORDER BY: Specifies order of concatenation(optional)']
145
133
 
146
- DuckDB Statements:
134
+ Common DuckDB Statements:
147
135
  `FROM`: The FROM clause specifies the source of the data for the query. It can include a single table, multiple joined tables, or subqueries. The JOIN clause is used to combine rows from two or more tables based on a related column between them. There are several types of joins, including INNER, OUTER, CROSS, NATURAL, SEMI, ANTI, LATERAL, POSITIONAL, ASOF, and self-joins., Examples: ['SELECT * FROM table_name;', 'FROM table_name SELECT *;', 'FROM table_name;', 'SELECT tn.* FROM table_name tn;', 'SELECT * FROM schema_name.table_name;', 'SELECT t.i FROM range(100) AS t(i);', "SELECT * FROM 'test.csv';", 'SELECT * FROM (SELECT * FROM table_name);', 'SELECT t FROM t;', "SELECT t FROM (SELECT unnest(generate_series(41, 43)) AS x, 'hello' AS y) t;", 'SELECT * FROM table_name JOIN other_table ON table_name.key = other_table.key;', 'SELECT * FROM table_name TABLESAMPLE 10%;', 'SELECT * FROM table_name TABLESAMPLE 10 ROWS;', 'FROM range(100) AS t(i) SELECT sum(t.i) WHERE i % 2 = 0;', 'SELECT a.*, b.* FROM a CROSS JOIN b;', 'SELECT a.*, b.* FROM a, b;', 'SELECT n.*, r.* FROM l_nations n JOIN l_regions r ON (n_regionkey = r_regionkey);', 'SELECT * FROM city_airport NATURAL JOIN airport_names;', 'SELECT * FROM city_airport JOIN airport_names USING (iata);', 'SELECT * FROM city_airport SEMI JOIN airport_names USING (iata);', 'SELECT * FROM city_airport WHERE iata IN (SELECT iata FROM airport_names);', 'SELECT * FROM city_airport ANTI JOIN airport_names USING (iata);', 'SELECT * FROM city_airport WHERE iata NOT IN (SELECT iata FROM airport_names WHERE iata IS NOT NULL);', 'SELECT * FROM range(3) t(i), LATERAL (SELECT i + 1) t2(j);', 'SELECT * FROM generate_series(0, 1) t(i), LATERAL (SELECT i + 10 UNION ALL SELECT i + 100) t2(j);', 'SELECT * FROM trades t ASOF JOIN prices p ON t.symbol = p.symbol AND t.when >= p.when;', 'SELECT * FROM trades t ASOF LEFT JOIN prices p ON t.symbol = p.symbol AND t.when >= p.when;', 'SELECT * FROM trades t ASOF JOIN prices p USING (symbol, "when");', 'SELECT t.symbol, t.when AS trade_when, p.when AS price_when, price FROM trades t ASOF LEFT JOIN prices p USING (symbol, "when");', 'SELECT * FROM t AS t t1 JOIN t t2 USING(x);', 'FROM tbl SELECT i, s;', 'FROM tbl;']
148
136
  `SELECT`: The SELECT statement retrieves rows from the database. It is used to query the database and retrieve data according to specific requirements. The statement can include several clauses, such as FROM, WHERE, GROUP BY, ORDER BY, and LIMIT, to filter, organize, and limit the query results., Examples: ['SELECT * FROM tbl;', 'SELECT j FROM tbl WHERE i = 3;', 'SELECT i, sum(j) FROM tbl GROUP BY i;', 'SELECT * FROM tbl ORDER BY i DESC LIMIT 3;', 'SELECT * FROM t1 JOIN t2 USING (a, b);', 'SELECT #1, #3 FROM tbl;', 'SELECT DISTINCT city FROM addresses;', 'SELECT d FROM (SELECT 1 AS a, 2 AS b) d;', 'SELECT rowid, id, content FROM t;']
149
137
  `WHERE`: The WHERE clause specifies filters to apply to the data being queried, allowing selection of a specific subset of data. It is logically applied immediately after the FROM clause in a SQL query., Examples: ['SELECT * FROM table_name WHERE id = 3;', "SELECT * FROM table_name WHERE name ILIKE '%mark%';", 'SELECT * FROM table_name WHERE id = 3 OR id = 7;']
@@ -172,7 +160,7 @@ DuckDB Statements:
172
160
  `PIVOT`: The PIVOT statement in DuckDB allows distinct values within a column to be transformed into their own columns. The values within these new columns are calculated using an aggregate function on the subset of rows matching each distinct value. DuckDB supports both the SQL Standard PIVOT syntax, which requires explicit column names, and a simplified PIVOT syntax that can automatically detect which columns to create. The PIVOT statement is useful for reshaping data for analysis, similar to the way pivot tables work in spreadsheet software., Examples: ['PIVOT Cities ON Year USING sum(Population);', 'PIVOT Cities ON Year USING first(Population);', 'PIVOT Cities ON Year USING sum(Population) GROUP BY Country;', 'PIVOT Cities ON Year IN (2000, 2010) USING sum(Population) GROUP BY Country;', 'PIVOT Cities ON Country, Name USING sum(Population);', "PIVOT Cities ON Country || '_' || Name USING sum(Population);", 'PIVOT Cities ON Year USING sum(Population) AS total, max(Population) AS max GROUP BY Country;', 'PIVOT Cities ON Year USING sum(Population) GROUP BY Country, Name;', 'SELECT * FROM (PIVOT Cities ON Year USING sum(Population) GROUP BY Country) pivot_alias;']
173
161
  `FILTER`: The FILTER clause is used in conjunction with aggregate functions within a SELECT statement to filter the input data specifically for the aggregate function. This allows for conditions to be applied similar to a WHERE clause, but localized to the aggregate function. It is useful for scenarios like pivoting data and handling null values cleanly, particularly with functions like 'first', 'last', 'list', and 'array_agg'. FILTER cannot be used in window functions., Examples: ['SELECT count(*) FILTER (i <= 5) AS lte_five FROM generate_series(1, 10) tbl(i);', 'SELECT sum(i) FILTER (i <= 5) AS lte_five_sum FROM generate_series(1, 10) tbl(i);', 'SELECT count(i) FILTER (year = 2022) AS "2022" FROM stacked_data;', 'SELECT first(i) FILTER (year = 2022) AS "2022" FROM stacked_data;']
174
162
 
175
- DuckDB Types:
163
+ Common DuckDB Types:
176
164
  `VARCHAR`: `VARCHAR` is a versatile data type used to store variable-length character strings, accommodating a wide range of text and string data without enforcing a specific length., Examples: ['CREATE TABLE people (name VARCHAR, age INTEGER);', "INSERT INTO documents (text) VALUES ('This is a VARCHAR example text.');", "SELECT * FROM employees WHERE department = 'Engineering';", 'ALTER TABLE students ADD COLUMN email VARCHAR;', "UPDATE orders SET status = 'Shipped' WHERE order_id = 102;", "COPY products TO 'products.csv' DELIMITER ',' HEADER;"]
177
165
  `INTEGER`: The INTEGER data type, with aliases such as int, signed, int4, int32, integer, and integral, represents whole numbers and is commonly used to store numeric data without fractional components., Examples: ['-- Assigning integer values to columns in a CREATE TABLE statement\nCREATE TABLE my_table (id INTEGER, age INTEGER);', '-- Inserting integer values as literals within an INSERT statement\nINSERT INTO my_table VALUES (1, 25);', '-- Using integer operations in a SELECT statement\nSELECT id + 10 AS new_id FROM my_table;', '-- Casting a float to an integer\nSELECT CAST(3.7 AS INTEGER) AS whole_number;', '-- Defining a column to only accept non-negative integers using a CHECK constraint\nCREATE TABLE my_table (id INTEGER CHECK (id >= 0));', '-- Using the INTEGER type in a primary key definition\nCREATE TABLE users (user_id INTEGER PRIMARY KEY, username VARCHAR);', '-- Updating integer columns\nUPDATE my_table SET age = age + 1 WHERE id = 1;', '-- Comparing integer values in a WHERE clause\nSELECT * FROM my_table WHERE age > 20;']
178
166
  `NULL`: The `NULL` type in SQL represents a missing or unknown value, allowing for fields within a table to be uninitialized or absent in data., Examples: ['SELECT NULL = NULL;', 'SELECT NULL IS NULL;', "INSERT INTO table_name (column1, column2) VALUES (NULL, 'data');", "SELECT coalesce(NULL, 'default_value');", 'UPDATE table_name SET column1 = NULL WHERE condition;', "SELECT CASE WHEN column IS NULL THEN 'Value is NULL' ELSE column END FROM table_name;"]
@@ -197,194 +185,11 @@ DuckDB Types:
197
185
  `SMALLINT`: The SMALLINT type, with aliases such as short, int2, smallint, and int16, represents a signed two-byte integer that can store whole numbers ranging from -32768 to 32767., Examples: ['CREATE TABLE test_table (id SMALLINT);', 'INSERT INTO test_table (id) VALUES (100);', 'SELECT * FROM test_table WHERE id BETWEEN -100 AND 100;', 'ALTER TABLE test_table ADD COLUMN new_column SMALLINT;', 'UPDATE test_table SET id = id + 1 WHERE id < 32767;']
198
186
  `BLOB`: The BLOB (Binary Large Object) type represents a variable-length binary data object, used for storing arbitrary binary data in the database, such as images or files, without any interpretation of its contents., Examples: ["-- Create a BLOB with a single byte\\nSELECT '\\xAA'::BLOB;\\n-- Result: \\xAA\\n\\n-- Create a BLOB with multiple bytes\\nSELECT '\\xAA\\xAB\\xAC'::BLOB;\\n-- Result: \\xAA\\xAB\\xAC\\n\\n-- Concatenate two BLOB values\\nSELECT '\\xAA'::BLOB || '\\xBB'::BLOB;\\n-- Result: \\xAABB\\n\\n-- Convert a BLOB to a hexadecimal string\\nSELECT hex('\\xAA\\xBB'::BLOB);\\n-- Result: AABB\\n\\n-- Decode a BLOB to a string, ensuring it is valid UTF-8\\nSELECT decode('\\xC3\\xBC'::BLOB);\\n-- Result: ü\\n\\n-- Read a BLOB from a file\\nSELECT read_blob('myfile.bin');\\n-- Result: Contents of 'myfile.bin' as a BLOB"]
199
187
 
200
- DuckDB Other Keywords:
201
- `SELECT`: The `SELECT` statement is used to query and retrieve data from tables, allowing for specification of columns, conditions, and data transformations., Examples: ['SELECT * FROM table_name;', 'SELECT column1, column2 FROM table_name WHERE condition;', 'SELECT DISTINCT column1 FROM table_name;', 'SELECT column1, COUNT(*) FROM table_name GROUP BY column1;', 'SELECT column1, column2 FROM table_name ORDER BY column1 DESC;', 'SELECT column1 AS alias FROM table_name;', 'SELECT t1.column1, t2.column2 FROM table1 t1 JOIN table2 t2 ON t1.id = t2.id;', 'SELECT column1 FROM table_name WHERE column2 IN (SELECT column2 FROM other_table);']
202
- `FROM`: The FROM clause is used to specify the data source for a SQL query, which can be a single table, a combination of tables joined together, or a subquery, and can optionally use a FROM-first syntax allowing query formation without a SELECT clause., Examples: ['SELECT * FROM table_name;', 'FROM table_name SELECT *;', 'FROM table_name;', 'SELECT tn.* FROM table_name tn;', 'SELECT * FROM schema_name.table_name;', 'SELECT t.i FROM range(100) AS t(i);', "SELECT * FROM 'test.csv';", 'SELECT * FROM (SELECT * FROM table_name);', 'SELECT t FROM t;', "SELECT t FROM (SELECT unnest(generate_series(41, 43)) AS x, 'hello' AS y) t;", 'SELECT * FROM table_name JOIN other_table ON table_name.key = other_table.key;', 'SELECT * FROM table_name TABLESAMPLE 10%;', 'SELECT * FROM table_name TABLESAMPLE 10 ROWS;', 'FROM range(100) AS t(i) SELECT sum(t.i) WHERE i % 2 = 0;']
188
+ Common DuckDB Keywords:
203
189
  `AS`: The `AS` keyword in SQL is used to create an alias for columns or tables, helping to simplify query logic and improve readability., Examples: ['SELECT first_name AS name FROM employees;', 'SELECT department AS dept FROM company;', 'CREATE VIEW sales_report AS SELECT * FROM sales WHERE year = 2023;', 'SELECT product_name AS name, SUM(sales) AS total_sales FROM store GROUP BY product_name;', 'SELECT c.customer_id, c.name AS customer_name, o.order_id, o.total_amount AS amount FROM customers c INNER JOIN orders o ON c.customer_id = o.customer_id;']
204
- `BY`: "by" is used in SQL to specify sorting or grouping columns for organizing query results, such as in "ORDER BY" or "PARTITION BY" clauses., Examples: ['SELECT row_number() OVER (ORDER BY time) FROM sales;', 'SELECT amount - lag(amount) OVER (ORDER BY time) FROM sales;', 'SELECT amount / sum(amount) OVER (PARTITION BY region) FROM sales;', 'SELECT * FROM tbl ORDER BY column_name ASC;', 'SELECT MIN(salary) FROM employees GROUP BY department;']
205
- `AND`: "AND" is a logical operator used to combine multiple conditions in SQL queries, returning true only if all conditions are true., Examples: ["SELECT * FROM employees WHERE department = 'Sales' AND salary > 50000;", "SELECT * FROM orders WHERE order_date >= '2023-01-01' AND status = 'Pending';", "SELECT * FROM customers WHERE age > 21 AND city = 'New York';", "SELECT * FROM products WHERE category = 'Electronics' AND stock_quantity > 0;", "SELECT * FROM students WHERE grade = 'A' AND attendance_percentage > 90;"]
206
- `WHERE`: The `WHERE` clause in SQL is used to filter records that satisfy a specified boolean condition, determining which rows are returned in the result set., Examples: ["SELECT * FROM weather WHERE city = 'San Francisco' AND prcp > 0.0;", 'SELECT city, max(temp_lo) FROM weather GROUP BY city HAVING max(temp_lo) < 40;', "UPDATE weather SET temp_hi = temp_hi - 2, temp_lo = temp_lo - 2 WHERE date > '1994-11-28';", "DELETE FROM weather WHERE city = 'Hayward';"]
207
- `OR`: The `ORDER BY` clause sorts query results based on specified columns or expressions, with optional ascending or descending order and handling of NULL values., Examples: ['SELECT * FROM addresses ORDER BY city;', 'SELECT * FROM addresses ORDER BY city DESC NULLS LAST;', 'SELECT * FROM addresses ORDER BY city, zip;', 'SELECT * FROM addresses ORDER BY city COLLATE DE;', 'SELECT * FROM addresses ORDER BY ALL;', 'SELECT * FROM addresses ORDER BY ALL DESC;']
208
- `TABLE`: The `CREATE TABLE` statement in SQL is used to define a new table along with its columns and constraints, such as specifying data types, primary keys, unique constraints, and check constraints, among other options., Examples: ['CREATE TABLE t1 (i INTEGER, j INTEGER);', 'CREATE TABLE t1 (id INTEGER PRIMARY KEY, j VARCHAR);', 'CREATE TABLE t1 (id INTEGER, j VARCHAR, PRIMARY KEY (id, j));', 'CREATE TABLE t1 (i INTEGER NOT NULL, decimalnr DOUBLE CHECK (decimalnr < 10), date DATE UNIQUE, time TIMESTAMP);', 'CREATE TABLE t1 AS SELECT 42 AS i, 84 AS j;', "CREATE TABLE t1 AS FROM read_csv('path/file.csv');", 'CREATE TABLE t1 AS FROM t2 LIMIT 0;', "CREATE TEMP TABLE t1 AS SELECT * FROM read_csv('path/file.csv');", 'CREATE OR REPLACE TABLE t1 (i INTEGER, j INTEGER);', 'CREATE TABLE IF NOT EXISTS t1 (i INTEGER, j INTEGER);']
209
- `JOIN`: Joins are essential operations in SQL for combining tables based on related columns, helping to establish relationships and derive meaningful insights from separate datasets., Examples: ['SELECT * FROM table1 JOIN table2 ON table1.id = table2.id;', 'SELECT a.*, b.* FROM a LEFT JOIN b ON a.id = b.id;', 'SELECT * FROM a NATURAL JOIN b;', 'SELECT * FROM table1 CROSS JOIN table2;', 'SELECT * FROM table1 LEFT OUTER JOIN table2 ON table1.id = table2.id;', 'SELECT * FROM orders INNER JOIN customers ON orders.customer_id = customers.id;', 'SELECT t.*, s.value FROM table t LEFT LATERAL JOIN summary s ON t.id = s.id;', 'SELECT t.* FROM table1 t ANTI JOIN table2 s ON t.id = s.id;']
210
- `ON`: The `ON` keyword in SQL is used with various clauses and statements such as JOIN, UPDATE, DELETE, and ON CONFLICT to specify conditions for selecting, updating, or deleting rows that match specific criteria in relational databases., Examples: ['SELECT * FROM table1 INNER JOIN table2 ON table1.id = table2.id;', "UPDATE employees SET salary = salary * 1.1 ON department.id = employees.department_id WHERE department.name = 'Engineering';", "DELETE FROM orders WHERE orders.date < '2023-01-01' ON CONFLICT DO NOTHING;", "INSERT INTO products (id, name) VALUES (1, 'Widget') ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;"]
211
- `NOT`: The `NOT` keyword in SQL is used to negate a condition, meaning it reverses the truth value of the condition it's used with., Examples: ['SELECT * FROM Students WHERE NOT (grade > 80);', 'SELECT * FROM Employees WHERE NOT EXISTS (SELECT * FROM Promotions WHERE Promotions.employee_id = Employees.id);', "SELECT * FROM Products WHERE NOT (name LIKE 'A%');", 'SELECT * FROM Orders WHERE order_date IS NOT NULL;', 'SELECT * FROM Library WHERE NOT (publication_year BETWEEN 1980 AND 2000);']
212
- `THEN`: The `FILTER` clause refines the input to an aggregate function in a `SELECT` statement, allowing different conditions for separate aggregates., Examples: ['SELECT count(*) AS total_rows, count(*) FILTER (i <= 5) AS lte_five, count(*) FILTER (i % 2 = 1) AS odds FROM generate_series(1, 10) tbl(i);', 'SELECT sum(i) FILTER (i <= 5) AS lte_five_sum, median(i) FILTER (i % 2 = 1) AS odds_median, median(i) FILTER (i % 2 = 1 AND i <= 5) AS odds_lte_five_median FROM generate_series(1, 10) tbl(i);', 'SELECT count(i) FILTER (year = 2022) AS "2022", count(i) FILTER (year = 2023) AS "2023", count(i) FILTER (year = 2024) AS "2024", count(i) FILTER (year = 2025) AS "2025", count(i) FILTER (year IS NULL) AS "NULLs" FROM stacked_data;', 'SELECT first(i) FILTER (year = 2022) AS "2022", first(i) FILTER (year = 2023) AS "2023", first(i) FILTER (year = 2024) AS "2024", first(i) FILTER (year = 2025) AS "2025", first(i) FILTER (year IS NULL) AS "NULLs" FROM stacked_data;']
213
- `END`: The FILTER clause is used to apply conditions to aggregate functions, allowing selective aggregation based on specific criteria within a SELECT statement., Examples: ['SELECT count(*) FILTER (i <= 5) AS lte_five FROM generate_series(1, 10) tbl(i);', 'SELECT sum(i) FILTER (i <= 5) AS lte_five_sum FROM generate_series(1, 10) tbl(i);', 'SELECT count(i) FILTER (year = 2022) AS "2022" FROM stacked_data;']
214
- `WHEN`: The `WHEN` keyword is used in a `CASE` statement to specify a condition that, if true, results in the execution of the corresponding expression., Examples: ["SELECT i, CASE WHEN i > 2 THEN 'Greater than 2' ELSE '2 or less' END FROM integers;", "SELECT i, CASE WHEN i = 1 THEN 'One' WHEN i = 2 THEN 'Two' ELSE 'Other' END FROM integers;", 'SELECT pretty_print_integer(n) AS result FROM numbers WHERE WHEN n > 1000 THEN true ELSE false;']
215
- `CASE`: The `CASE` statement in SQL is used for conditional logic in queries, allowing you to perform a switch based on a condition, similar to a ternary operator in programming languages., Examples: ['SELECT i, CASE WHEN i > 2 THEN 1 ELSE 0 END AS test FROM integers;', 'SELECT i, CASE WHEN i = 1 THEN 10 WHEN i = 2 THEN 20 ELSE 0 END AS test FROM integers;', 'SELECT i, CASE WHEN i = 1 THEN 10 END AS test FROM integers;', 'SELECT i, CASE i WHEN 1 THEN 10 WHEN 2 THEN 20 WHEN 3 THEN 30 END AS test FROM integers;']
216
- `NULL`: The NULL keyword in SQL represents a missing or unknown value, often used to indicate the absence of a definite value in a column., Examples: ["CREATE TABLE users (id INTEGER, name VARCHAR, age INTEGER); INSERT INTO users VALUES (1, 'Alice', NULL);", 'SELECT * FROM users WHERE age IS NULL;', 'UPDATE users SET age = 30 WHERE age IS NULL;', 'DELETE FROM users WHERE name IS NULL;', 'SELECT COALESCE(age, 0) AS age_with_default FROM users;', 'SELECT NULL = NULL; -- Returns NULL as NULL is not equal to anything, including itself', "SELECT ifnull(name, 'Unknown') FROM users -- Uses 'Unknown' if name is NULL;"]
217
190
  `DISTINCT`: The `DISTINCT` keyword is used in the SQL `SELECT` statement to ensure that only unique values are returned for specified columns, effectively removing duplicate rows from the result set., Examples: ['SELECT DISTINCT city FROM addresses;', 'SELECT DISTINCT ON(country) city, population FROM cities ORDER BY population DESC;']
218
- `DESC`: The keyword `desc` is used in SQL to describe the DESCENDING order in which query results should be sorted, often associated with the `ORDER BY` clause., Examples: ['SELECT name FROM employees ORDER BY salary DESC;', 'CREATE TABLE example (id INTEGER, name VARCHAR);', 'DESCRIBE example;', 'DESCRIBE SELECT * FROM example WHERE id < 10 ORDER BY name DESC;']
219
- `CREATE`: The `CREATE` keyword is used to define new database structures, such as tables or macros, in the SQL catalog., Examples: ['CREATE TABLE t1 (i INTEGER, j INTEGER);', "CREATE TEMP TABLE t1 AS SELECT * FROM read_csv('path/file.csv');", 'CREATE OR REPLACE TABLE t1 (i INTEGER, j INTEGER);', 'CREATE TABLE IF NOT EXISTS t1 (i INTEGER, j INTEGER);', 'CREATE TABLE nums AS SELECT i FROM range(0, 3) t(i);', 'CREATE TABLE t1 (x FLOAT, two_x AS (2 * x));', 'CREATE MACRO add(a, b) AS a + b;']
220
- `ORDER`: The `ORDER BY` clause is used to sort the rows returned by a SQL query based on specified columns, allowing for ascending or descending order and optional null order modifiers., Examples: ['SELECT * FROM addresses ORDER BY city;', 'SELECT * FROM addresses ORDER BY city DESC NULLS LAST;', 'SELECT * FROM addresses ORDER BY city, zip;', 'SELECT * FROM addresses ORDER BY city COLLATE DE;', 'SELECT * FROM addresses ORDER BY ALL;', 'SELECT * FROM addresses ORDER BY ALL DESC;']
221
- `ELSE`: The "ELSE" keyword in a CASE statement provides a default result when none of the "WHEN" conditions are met., Examples: ['SELECT i, CASE WHEN i > 2 THEN 1 ELSE 0 END AS test FROM integers;', 'SELECT i, CASE WHEN i = 1 THEN 10 WHEN i = 2 THEN 20 ELSE 0 END AS test FROM integers;', 'SELECT i, CASE WHEN i = 1 THEN 10 END AS test FROM integers;']
222
- `GROUP`: Grouping in SQL using clauses like GROUPING SETS, ROLLUP, and CUBE allows for performing aggregations across multiple dimensions within a single query., Examples: ['SELECT city, street_name, avg(income) FROM addresses GROUP BY GROUPING SETS ((city, street_name), (city), (street_name), ());', 'SELECT city, street_name, avg(income) FROM addresses GROUP BY CUBE (city, street_name);', 'SELECT city, street_name, avg(income) FROM addresses GROUP BY ROLLUP (city, street_name);', 'SELECT course, type, count(*) FROM students GROUP BY GROUPING SETS ((course, type), course, type, ());', 'SELECT y, q, m, GROUPING_ID(y, q, m) AS "grouping_id()" FROM days GROUP BY GROUPING SETS ((y, q, m), (y, q), (y), ()) ORDER BY y, q, m;']
223
- `IS`: The "IS" keyword is used in SQL to perform tests on values to check for NULL values or to use as part of statements like IS DISTINCT FROM which can handle NULLs in equality comparisons., Examples: ['SELECT 4 IS DISTINCT FROM NULL;', 'SELECT 4 IS NOT DISTINCT FROM 4;', 'SELECT NULL IS NULL;', 'SELECT NULL IS NOT NULL;']
224
- `REPLACE`: The `REPLACE` clause is used in a `SELECT` statement to replace specific columns with different expressions, effectively transforming the output of the `REPLACE` columns with the new specified expressions while retaining the rest of the columns unchanged., Examples: ['SELECT * REPLACE (lower(city) AS city) FROM addresses;', 'SELECT * REPLACE (col / 1000 AS col) FROM tbl;']
225
- `WITH`: The `WITH` clause allows defining common table expressions (CTEs) for simplifying complex queries by creating temporary result sets that can be referenced within the main SQL query., Examples: ['WITH cte AS (SELECT 42 AS x) SELECT * FROM cte;', 'WITH cte1 AS (SELECT 42 AS i), cte2 AS (SELECT i * 100 AS x FROM cte1) SELECT * FROM cte2;', 'WITH t(x) AS MATERIALIZED (SELECT * FROM t), SELECT * FROM t AS t1, t AS t2, t AS t3;', 'WITH RECURSIVE FibonacciNumbers AS (... recursive logic ...) SELECT ... FROM FibonacciNumbers;']
226
191
  `IN`: The `IN` keyword is used in SQL to specify a list of discrete values for a column to match against, typically in a `WHERE` clause, allowing for multiple specific conditions to be evaluated at once., Examples: ["SELECT * FROM employees WHERE department IN ('HR', 'Engineering', 'Marketing');", 'SELECT id, name FROM students WHERE grade IN (10, 11, 12);', "DELETE FROM orders WHERE order_status IN ('Cancelled', 'Returned');", "UPDATE items SET status = 'Unavailable' WHERE item_id IN (1001, 1002, 1003);", "SELECT * FROM logs WHERE severity IN ('ERROR', 'CRITICAL') ORDER BY timestamp DESC;"]
227
192
  `OVER`: The `OVER` clause in SQL specifies a window for evaluating window functions, allowing computations over a defined group of rows in a result set., Examples: ['SELECT row_number() OVER () FROM sales;', 'SELECT row_number() OVER (ORDER BY time) FROM sales;', 'SELECT row_number() OVER (PARTITION BY region ORDER BY time) FROM sales;', 'SELECT amount - lag(amount) OVER (ORDER BY time) FROM sales;', 'SELECT amount / sum(amount) OVER (PARTITION BY region) FROM sales;']
228
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.']
229
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"]
230
- `LEFT`: The `LEFT JOIN` keyword in SQL is used to combine rows from two or more tables based on a related column between them, but it will return all records from the left table and the matched records from the right table, with null values in columns of the right table where there is no match., Examples: ['SELECT customers.customer_id, orders.order_id FROM customers LEFT JOIN orders ON customers.customer_id = orders.customer_id;', 'SELECT city.name, weather.temp_lo FROM city LEFT JOIN weather ON city.name = weather.city;', 'SELECT a.id, b.value FROM table_a a LEFT JOIN table_b b ON a.id = b.a_id;', 'SELECT * FROM employees LEFT JOIN departments ON employees.department_id = departments.id;', 'SELECT students.name, grades.score FROM students LEFT JOIN grades ON students.id = grades.student_id WHERE grades.score IS NULL;']
231
195
  """
232
-
233
- server = Server("mcp-server-motherduck")
234
-
235
- conn = duckdb.connect()
236
-
237
- @server.list_resources()
238
- async def handle_list_resources() -> list[types.Resource]:
239
- """
240
- List available note resources.
241
- Each note is exposed as a resource with a custom note:// URI scheme.
242
- """
243
- return []
244
-
245
- @server.read_resource()
246
- async def handle_read_resource(uri: AnyUrl) -> str:
247
- """
248
- Read a specific note's content by its URI.
249
- The note name is extracted from the URI host component.
250
- """
251
- raise ValueError(f"Unsupported URI scheme: {uri.scheme}")
252
-
253
- @server.list_prompts()
254
- async def handle_list_prompts() -> list[types.Prompt]:
255
- """
256
- List available prompts.
257
- Each prompt can have optional arguments to customize its behavior.
258
- """
259
- return [
260
- types.Prompt(
261
- name="duckdb-motherduck-initial-prompt",
262
- description="A prompt to initialize a connection to duckdb or motherduck and start working with it",
263
- )
264
- ]
265
-
266
- @server.get_prompt()
267
- async def handle_get_prompt(
268
- name: str, arguments: dict[str, str] | None
269
- ) -> types.GetPromptResult:
270
- """
271
- Generate a prompt by combining arguments with server state.
272
- The prompt includes all current notes and can be customized via arguments.
273
- """
274
- if name != "duckdb-motherduck-initial-prompt":
275
- raise ValueError(f"Unknown prompt: {name}")
276
-
277
- return types.GetPromptResult(
278
- description=f"Initial prompt when interacting with DuckDB/MotherDuck",
279
- messages=[
280
- types.PromptMessage(
281
- role="user",
282
- content=types.TextContent(type="text", text=PROMPT_TEMPLATE),
283
- )
284
- ],
285
- )
286
-
287
- @server.list_tools()
288
- async def handle_list_tools() -> list[types.Tool]:
289
- """
290
- List available tools.
291
- Each tool specifies its arguments using JSON Schema validation.
292
- """
293
- return [
294
- types.Tool(
295
- name="initialize-connection",
296
- description="Create a connection to either a local DuckDB or MotherDuck and retrieve available databases",
297
- inputSchema={
298
- "type": "object",
299
- "properties": {
300
- "type": {"type": "string", "description": "Type of the database, either 'DuckDB' or 'MotherDuck'"},
301
- },
302
- "required": ["type"],
303
- },
304
- ),
305
- types.Tool(
306
- name="read-schemas",
307
- description="Get table schemas from a specific DuckDB/MotherDuck database",
308
- inputSchema={
309
- "type": "object",
310
- "properties": {
311
- "type": {"database_name": "string", "description": "name of the database"},
312
- },
313
- "required": ["database_name"],
314
- },
315
- ),
316
- types.Tool(
317
- name="execute-query",
318
- description="Execute a query on the MotherDuck (DuckDB) database",
319
- inputSchema={
320
- "type": "object",
321
- "properties": {
322
- "query": {"type": "string", "description": "SQL query to execute"},
323
- },
324
- "required": ["query"],
325
- },
326
- )
327
- ]
328
-
329
- @server.call_tool()
330
- async def handle_call_tool(
331
- name: str, arguments: dict | None
332
- ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
333
- """
334
- Handle tool execution requests.
335
- Tools can modify server state and notify clients of changes.
336
- """
337
- global conn
338
- if name == "initialize-connection":
339
- type = arguments["type"].strip().upper()
340
- if not type in ['DUCKDB', 'MOTHERDUCK']:
341
- raise ValueError("Only 'DuckDB' or 'MotherDuck' are supported")
342
- if type == 'MOTHERDUCK' and not os.getenv('motherduck_token'):
343
- raise ValueError("Please set the `motherduck_token` environment variable.")
344
- if type == 'MOTHERDUCK':
345
- conn = duckdb.connect('md:')
346
- elif type == 'DUCKDB':
347
- conn = duckdb.connect()
348
- databases = conn.execute("""select string_agg(database_name, ',\n')
349
- from duckdb_databases() where database_name
350
- not in ('system', 'temp')""").fetchone()[0]
351
- response = f'Connection to {type} successfully established. Here are the available databases: \n{databases}'
352
- return [types.TextContent(type="text", text=response)]
353
- if name == "read-schemas":
354
- database = arguments["database_name"]
355
- tables = conn.execute(f"""
356
- SELECT string_agg(regexp_replace(sql, 'CREATE TABLE ', 'CREATE TABLE '||database_name||'.'), '\n\n') as sql
357
- FROM duckdb_tables()
358
- WHERE database_name = '{database}'""").fetchone()[0]
359
- views = conn.execute(f"""
360
- SELECT string_agg(regexp_replace(sql, 'CREATE TABLE ', 'CREATE TABLE '||database_name||'.'), '\n\n') as sql
361
- FROM duckdb_views()
362
- where schema_name not in ('information_schema', 'pg_catalog', 'localmemdb')
363
- 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')
364
- and database_name = '{database}'
365
- """).fetchone()[0]
366
- results = f"Here are all tables: \n{tables} \n\n Here are all views: {views}"
367
- return [types.TextContent(type="text", text=str(results))]
368
- if name == "execute-query":
369
- try:
370
- results = conn.execute(arguments["query"]).fetchall()
371
- except Exception as e:
372
- raise ValueError('Error querying the database:'+str(e))
373
- return [types.TextContent(type="text", text=str(results))]
374
-
375
-
376
- async def main():
377
- # Run the server using stdin/stdout streams
378
- async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
379
- await server.run(
380
- read_stream,
381
- write_stream,
382
- InitializationOptions(
383
- server_name="motherduck",
384
- server_version="0.1.0",
385
- capabilities=server.get_capabilities(
386
- notification_options=NotificationOptions(),
387
- experimental_capabilities={},
388
- ),
389
- ),
390
- )
@@ -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"
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
+ )
@@ -179,7 +179,7 @@ wheels = [
179
179
 
180
180
  [[package]]
181
181
  name = "mcp-server-motherduck"
182
- version = "0.2.1"
182
+ version = "0.2.2"
183
183
  source = { editable = "." }
184
184
  dependencies = [
185
185
  { name = "duckdb" },
@@ -1,115 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: mcp-server-motherduck
3
- Version: 0.2.1
4
- Summary: A MCP server for MotherDuck and local DuckDB
5
- Author-email: tdoehmen <till@motherduck.com>
6
- Requires-Python: >=3.10
7
- Requires-Dist: duckdb>=1.1.3
8
- Requires-Dist: mcp>=1.0.0
9
- Description-Content-Type: text/markdown
10
-
11
- # mcp-server-motherduck MCP server
12
-
13
- A MCP server for MotherDuck and local DuckDB
14
-
15
- ## Components
16
-
17
- ### Resources
18
-
19
- ### Prompts
20
-
21
- The server implements one prompt:
22
- - duckdb-motherduck-initial-prompt: A prompt to initialize a connection to duckdb or motherduck and start working with it
23
-
24
- ### Tools
25
-
26
- The server implements three tools:
27
- - initialize-connection: Create a connection to either a local DuckDB or MotherDuck and retrieve available databases
28
- - Takes "type" (DuckDB or MotherDuck) as input
29
- - read-schemas: Get table schemas from a specific DuckDB/MotherDuck database
30
- - Takes "database_name" as required string arguments
31
- - execute-query: Execute a query on the MotherDuck (DuckDB) database
32
- - Takes "query" as required string arguments
33
-
34
- ## Configuration Claude Desktop
35
-
36
- Add the snippet below to your Claude Desktop config and make sure to set the HOME var to your home folder (needed by DuckDB).
37
-
38
- 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.
39
-
40
- On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
41
- On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
42
-
43
- ### Servers Configuration
44
- ```
45
- "mcpServers": {
46
- "mcp-server-motherduck": {
47
- "command": "uvx",
48
- "args": [
49
- "mcp-server-motherduck"
50
- ],
51
- "env": {
52
- "motherduck_token": "",
53
- "HOME": ""
54
- }
55
- }
56
- }
57
- ```
58
-
59
- ## Development
60
-
61
- ### Building and Publishing
62
-
63
- To prepare the package for distribution:
64
-
65
- 1. Sync dependencies and update lockfile:
66
- ```bash
67
- uv sync
68
- ```
69
-
70
- 2. Build package distributions:
71
- ```bash
72
- uv build
73
- ```
74
-
75
- This will create source and wheel distributions in the `dist/` directory.
76
-
77
- 3. Publish to PyPI:
78
- ```bash
79
- uv publish
80
- ```
81
-
82
- Note: You'll need to set PyPI credentials via environment variables or command flags:
83
- - Token: `--token` or `UV_PUBLISH_TOKEN`
84
- - Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD`
85
-
86
- ### Debugging
87
-
88
- <details>
89
- <summary>Development/Unpublished Servers Configuration</summary>
90
- ```
91
- "mcpServers": {
92
- "mcp-server-motherduck": {
93
- "command": "uv",
94
- "args": [
95
- "--directory",
96
- "/Users/<username>/mcp-server/mcp-server-motherduck",
97
- "run",
98
- "mcp-server-motherduck"
99
- ]
100
- }
101
- }
102
- ```
103
- </details>
104
-
105
- Since MCP servers run over stdio, debugging can be challenging. For the best debugging
106
- experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
107
-
108
- You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
109
-
110
- ```bash
111
- npx @modelcontextprotocol/inspector uv --directory /Users/<username>/mcp-server/mcp-server-motherduck run mcp-server-motherduck
112
- ```
113
-
114
-
115
- Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
@@ -1,105 +0,0 @@
1
- # mcp-server-motherduck MCP server
2
-
3
- A MCP server for MotherDuck and local DuckDB
4
-
5
- ## Components
6
-
7
- ### Resources
8
-
9
- ### Prompts
10
-
11
- The server implements one prompt:
12
- - duckdb-motherduck-initial-prompt: A prompt to initialize a connection to duckdb or motherduck and start working with it
13
-
14
- ### Tools
15
-
16
- The server implements 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
22
- - Takes "query" as required string arguments
23
-
24
- ## Configuration Claude Desktop
25
-
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).
27
-
28
- 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
-
30
- On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
31
- On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
32
-
33
- ### Servers Configuration
34
- ```
35
- "mcpServers": {
36
- "mcp-server-motherduck": {
37
- "command": "uvx",
38
- "args": [
39
- "mcp-server-motherduck"
40
- ],
41
- "env": {
42
- "motherduck_token": "",
43
- "HOME": ""
44
- }
45
- }
46
- }
47
- ```
48
-
49
- ## Development
50
-
51
- ### Building and Publishing
52
-
53
- To prepare the package for distribution:
54
-
55
- 1. Sync dependencies and update lockfile:
56
- ```bash
57
- uv sync
58
- ```
59
-
60
- 2. Build package distributions:
61
- ```bash
62
- uv build
63
- ```
64
-
65
- This will create source and wheel distributions in the `dist/` directory.
66
-
67
- 3. Publish to PyPI:
68
- ```bash
69
- uv publish
70
- ```
71
-
72
- Note: You'll need to set PyPI credentials via environment variables or command flags:
73
- - Token: `--token` or `UV_PUBLISH_TOKEN`
74
- - Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD`
75
-
76
- ### Debugging
77
-
78
- <details>
79
- <summary>Development/Unpublished Servers Configuration</summary>
80
- ```
81
- "mcpServers": {
82
- "mcp-server-motherduck": {
83
- "command": "uv",
84
- "args": [
85
- "--directory",
86
- "/Users/<username>/mcp-server/mcp-server-motherduck",
87
- "run",
88
- "mcp-server-motherduck"
89
- ]
90
- }
91
- }
92
- ```
93
- </details>
94
-
95
- Since MCP servers run over stdio, debugging can be challenging. For the best debugging
96
- experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
97
-
98
- You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
99
-
100
- ```bash
101
- npx @modelcontextprotocol/inspector uv --directory /Users/<username>/mcp-server/mcp-server-motherduck run mcp-server-motherduck
102
- ```
103
-
104
-
105
- Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
@@ -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']