mcp-server-motherduck 0.6.4__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_server_motherduck/__init__.py +198 -133
- mcp_server_motherduck/assets/duck_feet_square.png +0 -0
- mcp_server_motherduck/configs.py +1 -1
- mcp_server_motherduck/database.py +342 -35
- mcp_server_motherduck/instructions.py +187 -0
- mcp_server_motherduck/server.py +208 -115
- mcp_server_motherduck/tools/__init__.py +19 -0
- mcp_server_motherduck/tools/execute_query.py +21 -0
- mcp_server_motherduck/tools/list_columns.py +99 -0
- mcp_server_motherduck/tools/list_databases.py +52 -0
- mcp_server_motherduck/tools/list_tables.py +91 -0
- mcp_server_motherduck/tools/switch_database_connection.py +130 -0
- mcp_server_motherduck-1.0.0.dist-info/METADATA +225 -0
- mcp_server_motherduck-1.0.0.dist-info/RECORD +17 -0
- {mcp_server_motherduck-0.6.4.dist-info → mcp_server_motherduck-1.0.0.dist-info}/WHEEL +1 -1
- mcp_server_motherduck/prompt.py +0 -195
- mcp_server_motherduck-0.6.4.dist-info/METADATA +0 -427
- mcp_server_motherduck-0.6.4.dist-info/RECORD +0 -10
- {mcp_server_motherduck-0.6.4.dist-info → mcp_server_motherduck-1.0.0.dist-info}/entry_points.txt +0 -0
- {mcp_server_motherduck-0.6.4.dist-info → mcp_server_motherduck-1.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,193 +1,258 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
MotherDuck MCP Server - A FastMCP server for DuckDB and MotherDuck.
|
|
3
|
+
|
|
4
|
+
This module provides the CLI entry point for the MCP server.
|
|
5
|
+
"""
|
|
6
|
+
|
|
2
7
|
import logging
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
3
10
|
import click
|
|
4
|
-
|
|
5
|
-
from .configs import
|
|
11
|
+
|
|
12
|
+
from .configs import SERVER_LOCALHOST, SERVER_VERSION
|
|
13
|
+
from .server import create_mcp_server
|
|
6
14
|
|
|
7
15
|
__version__ = SERVER_VERSION
|
|
8
16
|
|
|
9
17
|
logger = logging.getLogger("mcp_server_motherduck")
|
|
10
|
-
logging.basicConfig(
|
|
11
|
-
level=logging.INFO, format="[motherduck] %(levelname)s - %(message)s"
|
|
12
|
-
)
|
|
18
|
+
logging.basicConfig(level=logging.INFO, format="[motherduck] %(levelname)s - %(message)s")
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
@click.command()
|
|
16
|
-
@click.option(
|
|
22
|
+
@click.option(
|
|
23
|
+
"--port", default=8000, envvar="MCP_PORT", help="Port to listen on for HTTP transport"
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
"--host", default=SERVER_LOCALHOST, envvar="MCP_HOST", help="Host to bind the MCP server"
|
|
27
|
+
)
|
|
17
28
|
@click.option(
|
|
18
29
|
"--transport",
|
|
19
|
-
type=click.Choice(["stdio", "sse", "stream"]),
|
|
30
|
+
type=click.Choice(["stdio", "http", "sse", "stream"]),
|
|
20
31
|
default="stdio",
|
|
21
|
-
|
|
32
|
+
envvar="MCP_TRANSPORT",
|
|
33
|
+
help="(Default: `stdio`) Transport type. Use `http` for HTTP Streamable transport. `sse` and `stream` are deprecated aliases.",
|
|
22
34
|
)
|
|
23
35
|
@click.option(
|
|
24
36
|
"--db-path",
|
|
25
|
-
default="
|
|
26
|
-
|
|
37
|
+
default=":memory:",
|
|
38
|
+
envvar="MCP_DB_PATH",
|
|
39
|
+
help="(Default: `:memory:`) Path to local DuckDB database file or MotherDuck database",
|
|
27
40
|
)
|
|
28
41
|
@click.option(
|
|
29
42
|
"--motherduck-token",
|
|
30
43
|
default=None,
|
|
31
|
-
|
|
44
|
+
envvar=["motherduck_token", "MOTHERDUCK_TOKEN"],
|
|
45
|
+
help="(Default: env var `motherduck_token` or `MOTHERDUCK_TOKEN`) Access token to use for MotherDuck database connections",
|
|
32
46
|
)
|
|
33
47
|
@click.option(
|
|
34
48
|
"--home-dir",
|
|
35
49
|
default=None,
|
|
36
|
-
help="
|
|
50
|
+
help="Override the home directory for DuckDB (defaults to system HOME)",
|
|
37
51
|
)
|
|
38
52
|
@click.option(
|
|
39
|
-
"--saas-mode",
|
|
53
|
+
"--motherduck-saas-mode",
|
|
40
54
|
is_flag=True,
|
|
55
|
+
envvar="MCP_SAAS_MODE",
|
|
41
56
|
help="Flag for connecting to MotherDuck in SaaS mode",
|
|
42
57
|
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--read-write",
|
|
60
|
+
is_flag=True,
|
|
61
|
+
envvar="MCP_READ_WRITE",
|
|
62
|
+
help="Enable write access to the database. By default, the server runs in read-only mode for local DuckDB files and MotherDuck databases. Note: In-memory databases are always writable (DuckDB limitation).",
|
|
63
|
+
)
|
|
64
|
+
@click.option(
|
|
65
|
+
"--ephemeral-connections/--no-ephemeral-connections",
|
|
66
|
+
default=True,
|
|
67
|
+
envvar="MCP_EPHEMERAL_CONNECTIONS",
|
|
68
|
+
help="Use temporary connections for read-only local DuckDB files, creating a new connection for each query. This keeps the file unlocked so other processes can write to it.",
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--max-rows",
|
|
72
|
+
type=int,
|
|
73
|
+
default=1024,
|
|
74
|
+
envvar="MCP_MAX_ROWS",
|
|
75
|
+
help="(Default: `1024`) Maximum number of rows to return from queries. Use LIMIT in your SQL for specific row counts.",
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--max-chars",
|
|
79
|
+
type=int,
|
|
80
|
+
default=50000,
|
|
81
|
+
envvar="MCP_MAX_CHARS",
|
|
82
|
+
help="(Default: `50000`) Maximum number of characters in query results. Prevents issues with wide rows or large text columns.",
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--query-timeout",
|
|
86
|
+
type=int,
|
|
87
|
+
default=-1,
|
|
88
|
+
envvar="MCP_QUERY_TIMEOUT",
|
|
89
|
+
help="(Default: `-1`) Query execution timeout in seconds. Set to -1 to disable timeout.",
|
|
90
|
+
)
|
|
91
|
+
@click.option(
|
|
92
|
+
"--init-sql",
|
|
93
|
+
default=None,
|
|
94
|
+
envvar="MCP_INIT_SQL",
|
|
95
|
+
help="SQL file path or SQL string to execute on startup for database initialization.",
|
|
96
|
+
)
|
|
97
|
+
@click.option(
|
|
98
|
+
"--allow-switch-databases",
|
|
99
|
+
is_flag=True,
|
|
100
|
+
envvar="MCP_ALLOW_SWITCH_DATABASES",
|
|
101
|
+
help="Enable the switch_database_connection tool to change databases at runtime. Disabled by default.",
|
|
102
|
+
)
|
|
103
|
+
# Backwards compatibility aliases (deprecated)
|
|
104
|
+
@click.option(
|
|
105
|
+
"--saas-mode",
|
|
106
|
+
is_flag=True,
|
|
107
|
+
hidden=True,
|
|
108
|
+
help="[DEPRECATED] Use --motherduck-saas-mode instead.",
|
|
109
|
+
)
|
|
43
110
|
@click.option(
|
|
44
111
|
"--read-only",
|
|
45
112
|
is_flag=True,
|
|
46
|
-
|
|
113
|
+
hidden=True,
|
|
114
|
+
help="[DEPRECATED] Read-only is now the default. Use --read-write for write access.",
|
|
47
115
|
)
|
|
48
116
|
@click.option(
|
|
49
117
|
"--json-response",
|
|
50
118
|
is_flag=True,
|
|
51
|
-
|
|
52
|
-
help="
|
|
119
|
+
hidden=True,
|
|
120
|
+
help="[DEPRECATED] No longer needed, JSON responses are automatic.",
|
|
53
121
|
)
|
|
54
122
|
def main(
|
|
55
|
-
port,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
from starlette.routing import Mount, Route
|
|
82
|
-
|
|
83
|
-
logger.info("MCP server initialized in \033[32msse\033[0m mode")
|
|
84
|
-
|
|
85
|
-
sse = SseServerTransport("/messages/")
|
|
86
|
-
|
|
87
|
-
async def handle_sse(request):
|
|
88
|
-
async with sse.connect_sse(
|
|
89
|
-
request.scope, request.receive, request._send
|
|
90
|
-
) as (read_stream, write_stream):
|
|
91
|
-
await app.run(read_stream, write_stream, init_opts)
|
|
92
|
-
return Response()
|
|
93
|
-
|
|
94
|
-
logger.info(
|
|
95
|
-
f"🦆 Connect to MotherDuck MCP Server at \033[1m\033[36mhttp://{SERVER_LOCALHOST}:{port}/sse\033[0m"
|
|
123
|
+
port: int,
|
|
124
|
+
host: str,
|
|
125
|
+
transport: str,
|
|
126
|
+
db_path: str,
|
|
127
|
+
motherduck_token: str | None,
|
|
128
|
+
home_dir: str | None,
|
|
129
|
+
motherduck_saas_mode: bool,
|
|
130
|
+
read_write: bool,
|
|
131
|
+
ephemeral_connections: bool,
|
|
132
|
+
max_rows: int,
|
|
133
|
+
max_chars: int,
|
|
134
|
+
query_timeout: int,
|
|
135
|
+
init_sql: str | None,
|
|
136
|
+
allow_switch_databases: bool,
|
|
137
|
+
# Deprecated args
|
|
138
|
+
saas_mode: bool,
|
|
139
|
+
read_only: bool,
|
|
140
|
+
json_response: bool,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""MotherDuck MCP Server - Execute SQL queries via DuckDB/MotherDuck."""
|
|
143
|
+
# Handle deprecated flags with warnings
|
|
144
|
+
if saas_mode:
|
|
145
|
+
warnings.warn(
|
|
146
|
+
"The '--saas-mode' flag is deprecated. Use '--motherduck-saas-mode' instead.",
|
|
147
|
+
DeprecationWarning,
|
|
148
|
+
stacklevel=2,
|
|
96
149
|
)
|
|
150
|
+
logger.warning("⚠️ '--saas-mode' is deprecated. Use '--motherduck-saas-mode' instead.")
|
|
151
|
+
motherduck_saas_mode = True
|
|
97
152
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
153
|
+
if read_only:
|
|
154
|
+
warnings.warn(
|
|
155
|
+
"The '--read-only' flag is deprecated. Read-only is now the default. "
|
|
156
|
+
"Use '--read-write' for write access.",
|
|
157
|
+
DeprecationWarning,
|
|
158
|
+
stacklevel=2,
|
|
104
159
|
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
uvicorn.run(
|
|
109
|
-
starlette_app,
|
|
110
|
-
host=SERVER_LOCALHOST,
|
|
111
|
-
port=port,
|
|
112
|
-
log_config=UVICORN_LOGGING_CONFIG,
|
|
160
|
+
logger.warning(
|
|
161
|
+
"⚠️ '--read-only' is deprecated. Read-only is now the default. "
|
|
162
|
+
"Remove '--read-only' from your config."
|
|
113
163
|
)
|
|
164
|
+
# read_only flag is effectively a no-op now since default is read-only
|
|
114
165
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
from starlette.types import Receive, Scope, Send
|
|
121
|
-
import contextlib
|
|
122
|
-
|
|
123
|
-
logger.info("MCP server initialized in \033[32mhttp-streamable\033[0m mode")
|
|
124
|
-
|
|
125
|
-
# Create the session manager with true stateless mode
|
|
126
|
-
session_manager = StreamableHTTPSessionManager(
|
|
127
|
-
app=app,
|
|
128
|
-
event_store=None,
|
|
129
|
-
json_response=json_response,
|
|
130
|
-
stateless=True,
|
|
166
|
+
if json_response:
|
|
167
|
+
warnings.warn(
|
|
168
|
+
"The '--json-response' flag is deprecated and no longer needed.",
|
|
169
|
+
DeprecationWarning,
|
|
170
|
+
stacklevel=2,
|
|
131
171
|
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
scope: Scope, receive: Receive, send: Send
|
|
135
|
-
) -> None:
|
|
136
|
-
await session_manager.handle_request(scope, receive, send)
|
|
137
|
-
|
|
138
|
-
@contextlib.asynccontextmanager
|
|
139
|
-
async def lifespan(app: Starlette) -> AsyncIterator[None]:
|
|
140
|
-
"""Context manager for session manager."""
|
|
141
|
-
async with session_manager.run():
|
|
142
|
-
logger.info("MCP server started with StreamableHTTP session manager")
|
|
143
|
-
try:
|
|
144
|
-
yield
|
|
145
|
-
finally:
|
|
146
|
-
logger.info(
|
|
147
|
-
"🦆 MotherDuck MCP Server in \033[32mhttp-streamable\033[0m mode shutting down"
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
logger.info(
|
|
151
|
-
f"🦆 Connect to MotherDuck MCP Server at \033[1m\033[36mhttp://{SERVER_LOCALHOST}:{port}/mcp\033[0m"
|
|
172
|
+
logger.warning(
|
|
173
|
+
"⚠️ '--json-response' is deprecated and no longer needed. Remove it from your config."
|
|
152
174
|
)
|
|
153
175
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
debug=True,
|
|
157
|
-
routes=[
|
|
158
|
-
Mount("/mcp", app=handle_streamable_http),
|
|
159
|
-
],
|
|
160
|
-
lifespan=lifespan,
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
import uvicorn
|
|
176
|
+
# Convert read_write flag to read_only (inverted logic)
|
|
177
|
+
actual_read_only = not read_write
|
|
164
178
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
179
|
+
# In-memory databases require --read-write flag since read-only doesn't apply
|
|
180
|
+
if db_path == ":memory:" and actual_read_only:
|
|
181
|
+
raise click.UsageError(
|
|
182
|
+
"In-memory databases require the --read-write flag.\n"
|
|
183
|
+
"Options:\n"
|
|
184
|
+
" - Add --read-write to allow writes (data won't persist anyway)\n"
|
|
185
|
+
" - Use --db-path with a file path for read-only access to a DuckDB file\n"
|
|
186
|
+
" - Use --db-path md: with a MotherDuck token for cloud database access"
|
|
170
187
|
)
|
|
171
188
|
|
|
189
|
+
logger.info("🦆 MotherDuck MCP Server v" + SERVER_VERSION)
|
|
190
|
+
logger.info("Ready to execute SQL queries via DuckDB/MotherDuck")
|
|
191
|
+
if db_path == ":memory:":
|
|
192
|
+
logger.info("Database mode: in-memory (read-write)")
|
|
172
193
|
else:
|
|
173
|
-
|
|
194
|
+
mode_str = "read-write" if not actual_read_only else "read-only"
|
|
195
|
+
if actual_read_only and not ephemeral_connections:
|
|
196
|
+
mode_str += " (persistent connection)"
|
|
197
|
+
logger.info(f"Database mode: {mode_str}")
|
|
198
|
+
logger.info(f"Query result limits: {max_rows} rows, {max_chars:,} characters")
|
|
199
|
+
if query_timeout == -1:
|
|
200
|
+
logger.info("Query timeout: disabled")
|
|
201
|
+
else:
|
|
202
|
+
logger.info(f"Query timeout: {query_timeout}s")
|
|
203
|
+
if init_sql:
|
|
204
|
+
logger.info("Init SQL: configured")
|
|
205
|
+
if allow_switch_databases:
|
|
206
|
+
logger.info("Switch databases: enabled")
|
|
174
207
|
|
|
175
|
-
|
|
176
|
-
|
|
208
|
+
# Handle deprecated transport aliases
|
|
209
|
+
if transport == "stream":
|
|
210
|
+
warnings.warn(
|
|
211
|
+
"The 'stream' transport is deprecated. Use 'http' instead.",
|
|
212
|
+
DeprecationWarning,
|
|
213
|
+
stacklevel=2,
|
|
214
|
+
)
|
|
215
|
+
logger.warning("⚠️ '--transport stream' is deprecated. Use '--transport http' instead.")
|
|
216
|
+
transport = "http"
|
|
217
|
+
elif transport == "sse":
|
|
218
|
+
warnings.warn(
|
|
219
|
+
"The 'sse' transport is deprecated. Use 'http' instead.",
|
|
220
|
+
DeprecationWarning,
|
|
221
|
+
stacklevel=2,
|
|
222
|
+
)
|
|
223
|
+
logger.warning("⚠️ '--transport sse' is deprecated. Use '--transport http' instead.")
|
|
224
|
+
transport = "http"
|
|
177
225
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
226
|
+
# Create the FastMCP server
|
|
227
|
+
mcp = create_mcp_server(
|
|
228
|
+
db_path=db_path,
|
|
229
|
+
motherduck_token=motherduck_token,
|
|
230
|
+
home_dir=home_dir,
|
|
231
|
+
saas_mode=motherduck_saas_mode,
|
|
232
|
+
read_only=actual_read_only,
|
|
233
|
+
ephemeral_connections=ephemeral_connections,
|
|
234
|
+
max_rows=max_rows,
|
|
235
|
+
max_chars=max_chars,
|
|
236
|
+
query_timeout=query_timeout,
|
|
237
|
+
init_sql=init_sql,
|
|
238
|
+
allow_switch_databases=allow_switch_databases,
|
|
239
|
+
)
|
|
181
240
|
|
|
182
|
-
|
|
183
|
-
|
|
241
|
+
# Run the server with the appropriate transport
|
|
242
|
+
if transport == "http":
|
|
243
|
+
logger.info("MCP server initialized in \033[32mhttp\033[0m mode")
|
|
184
244
|
logger.info(
|
|
185
|
-
"🦆 MotherDuck MCP Server
|
|
245
|
+
f"🦆 Connect to MotherDuck MCP Server at \033[1m\033[36mhttp://{host}:{port}/mcp\033[0m"
|
|
186
246
|
)
|
|
247
|
+
mcp.run(transport="http", host=host, port=port)
|
|
248
|
+
else:
|
|
249
|
+
logger.info("MCP server initialized in \033[32mstdio\033[0m mode")
|
|
250
|
+
logger.info("Waiting for client connection")
|
|
251
|
+
mcp.run(transport="stdio")
|
|
187
252
|
|
|
188
253
|
|
|
189
254
|
# Optionally expose other important items at package level
|
|
190
|
-
__all__ = ["main"]
|
|
255
|
+
__all__ = ["main", "__version__"]
|
|
191
256
|
|
|
192
257
|
if __name__ == "__main__":
|
|
193
258
|
main()
|
|
Binary file
|