Skip to content

Commit 5e365f0

Browse files
committed
Initial commit
0 parents  commit 5e365f0

File tree

8 files changed

+727
-0
lines changed

8 files changed

+727
-0
lines changed

.gitignore

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
9+
# Virtual environments
10+
.venv
11+
12+
# mypy
13+
.mypy_cache/

.python-version

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.10

.vscode/settings.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"[python]": {
3+
"editor.defaultFormatter": "charliermarsh.ruff",
4+
"editor.formatOnSave": true,
5+
"editor.formatOnType": true,
6+
"editor.codeActionsOnSave": {
7+
"source.organizeImports": "explicit",
8+
"source.fixAll.ruff": "explicit"
9+
}
10+
}
11+
}

README.md

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# mcp-server-duckdb
2+
3+
A Model Context Protocol (MCP) server implementation for DuckDB, providing database interaction capabilities through MCP tools.
4+
It would be interesting to have LLM analyze it. DuckDB is suitable for local analysis.
5+
6+
## Overview
7+
8+
This server enables interaction with a DuckDB database through the Model Context Protocol, allowing for database operations like querying, table creation, and schema inspection.
9+
10+
## Components
11+
12+
### Resources
13+
14+
Currently, no custom resources are implemented.
15+
16+
### Prompts
17+
18+
Currently, no custom prompts are implemented.
19+
20+
### Tools
21+
22+
The server implements the following database interaction tools:
23+
24+
- **read-query**: Execute SELECT queries to read data from the database
25+
- Input: `query` (string) - Must be a SELECT statement
26+
- Output: Query results as text
27+
28+
- **write-query**: Execute INSERT, UPDATE, or DELETE queries to modify data
29+
- Input: `query` (string) - Must be a non-SELECT statement
30+
- Output: Query results as text
31+
32+
- **create-table**: Create new tables in the database
33+
- Input: `query` (string) - Must be a CREATE TABLE statement
34+
- Output: Success confirmation message
35+
36+
- **list-tables**: List all tables in the database
37+
- Input: None required
38+
- Output: List of tables from information_schema
39+
40+
- **describe-table**: Get schema information for a specific table
41+
- Input: `table_name` (string) - Name of the table to describe
42+
- Output: Table schema information
43+
44+
## Configuration
45+
46+
### Required Parameters
47+
48+
- **db-path** (string): Path to the DuckDB database file
49+
- The server will automatically create the database file and parent directories if they don't exist
50+
51+
## Installation
52+
53+
### Claude Desktop Integration
54+
55+
Configure the MCP server in Claude Desktop's configuration file:
56+
57+
#### MacOS
58+
Location: `~/Library/Application Support/Claude/claude_desktop_config.json`
59+
60+
#### Windows
61+
Location: `%APPDATA%/Claude/claude_desktop_config.json`
62+
63+
```json
64+
{
65+
"mcpServers": {
66+
"duckdb": {
67+
"command": "uv",
68+
"args": [
69+
"--directory",
70+
"~/mcp-server-duckdb",
71+
"run",
72+
"mcp-server-duckdb",
73+
"--db-path",
74+
"~/mcp-server-duckdb/data/data.db"
75+
]
76+
}
77+
}
78+
}
79+
```
80+
81+
## Development
82+
83+
### Prerequisites
84+
85+
- Python with `uv` package manager
86+
- DuckDB Python package
87+
- MCP server dependencies
88+
89+
### Debugging
90+
91+
Debugging MCP servers can be challenging due to their stdio-based communication. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) for the best debugging experience.
92+
93+
#### Using MCP Inspector
94+
95+
1. Install the inspector using npm:
96+
```bash
97+
npx @modelcontextprotocol/inspector uv --directory ~/mcp-server-duckdb run mcp-server-duckdb
98+
```
99+
100+
2. Open the provided URL in your browser to access the debugging interface
101+
102+
The inspector provides visibility into:
103+
- Request/response communication
104+
- Tool execution
105+
- Server state
106+
- Error messages

pyproject.toml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[project]
2+
name = "mcp-server-duckdb"
3+
version = "0.1.0"
4+
description = "A DuckDB MCP server"
5+
readme = "README.md"
6+
requires-python = ">=3.10"
7+
dependencies = ["duckdb>=1.1.3", "mcp>=1.0.0"]
8+
9+
[[project.authors]]
10+
name = "ktanaka101"
11+
12+
13+
[build-system]
14+
requires = ["hatchling"]
15+
build-backend = "hatchling.build"
16+
17+
[project.scripts]
18+
mcp-server-duckdb = "mcp_server_duckdb:main"
19+
20+
[tool.ruff]
21+
line-length = 120
22+
23+
[tool.ruff.format]
24+
docstring-code-format = true
25+
26+
[tool.ruff.lint]
27+
select = ["E", "F", "I"]

src/mcp_server_duckdb/__init__.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import argparse
2+
import asyncio
3+
4+
from . import server
5+
6+
7+
def main():
8+
"""Main entry point for the package."""
9+
10+
parser = argparse.ArgumentParser(description="DuckDB MCP Server")
11+
parser.add_argument(
12+
"--db-path",
13+
help="Path to DuckDB database file",
14+
required=True,
15+
)
16+
17+
args = parser.parse_args()
18+
asyncio.run(server.main(args.db_path))
19+
20+
21+
# Optionally expose other important items at package level
22+
__all__ = ["main", "server"]

src/mcp_server_duckdb/server.py

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import logging
2+
from contextlib import closing
3+
from pathlib import Path
4+
from typing import Any, List
5+
6+
import duckdb
7+
import mcp.server.stdio
8+
import mcp.types as types
9+
from mcp.server import NotificationOptions, Server
10+
from mcp.server.models import InitializationOptions
11+
from pydantic import AnyUrl
12+
13+
logger = logging.getLogger("mcp-server-duckdb")
14+
logger.info("Starting MCP DuckDB Server")
15+
16+
17+
class DuckDBDatabase:
18+
def __init__(self, db_path: str):
19+
path = Path(db_path).expanduser()
20+
dir_path = path.parent
21+
if not dir_path.exists():
22+
logger.info(f"Creating directory: {dir_path}")
23+
dir_path.mkdir(parents=True)
24+
25+
if not path.exists():
26+
logger.info(f"Creating DuckDB database: {path}")
27+
duckdb.connect(str(path)).close()
28+
29+
self.db_path = str(path)
30+
31+
def connect(self):
32+
return duckdb.connect(self.db_path)
33+
34+
def execute_query(self, query: object, parameters: object = None) -> List[Any]:
35+
with closing(self.connect()) as connection:
36+
return connection.execute(query, parameters).fetchall()
37+
38+
39+
async def main(db_path: str):
40+
logger.info(f"Starting SQLite MCP Server with DB path: {db_path}")
41+
42+
db = DuckDBDatabase(db_path)
43+
server = Server("mcp-duckdb-server")
44+
45+
logger.debug("Registering handlers")
46+
47+
@server.list_resources()
48+
async def handle_list_resources() -> list[types.Resource]:
49+
"""
50+
List available duckdb resources.
51+
"""
52+
return []
53+
54+
@server.read_resource()
55+
async def handle_read_resource(uri: AnyUrl) -> str:
56+
"""
57+
Read a specific note's content by its URI.
58+
"""
59+
return "No data"
60+
61+
@server.list_prompts()
62+
async def handle_list_prompts() -> list[types.Prompt]:
63+
"""
64+
List available prompts.
65+
"""
66+
return []
67+
68+
@server.get_prompt()
69+
async def handle_get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult:
70+
"""
71+
Generate a prompt by combining arguments with server state.
72+
"""
73+
74+
return types.GetPromptResult(
75+
description="No",
76+
messages=[],
77+
)
78+
79+
@server.list_tools()
80+
async def handle_list_tools() -> list[types.Tool]:
81+
"""List available tools"""
82+
return [
83+
types.Tool(
84+
name="read-query",
85+
description="Execute a SELECT query on the DuckDB database",
86+
inputSchema={
87+
"type": "object",
88+
"properties": {
89+
"query": {
90+
"type": "string",
91+
"description": "SELECT SQL query to execute",
92+
},
93+
},
94+
"required": ["query"],
95+
},
96+
),
97+
types.Tool(
98+
name="write-query",
99+
description="Execute an INSERT, UPDATE, or DELETE query on the DuckDB database",
100+
inputSchema={
101+
"type": "object",
102+
"properties": {
103+
"query": {
104+
"type": "string",
105+
"description": "SQL query to execute",
106+
},
107+
},
108+
"required": ["query"],
109+
},
110+
),
111+
types.Tool(
112+
name="create-table",
113+
description="Create a new table in the DuckDB database",
114+
inputSchema={
115+
"type": "object",
116+
"properties": {
117+
"query": {
118+
"type": "string",
119+
"description": "CREATE TABLE SQL statement",
120+
},
121+
},
122+
"required": ["query"],
123+
},
124+
),
125+
types.Tool(
126+
name="list-tables",
127+
description="List all tables in the DuckDB database",
128+
inputSchema={
129+
"type": "object",
130+
"properties": {},
131+
},
132+
),
133+
types.Tool(
134+
name="describe-table",
135+
description="Get the schema information for a specific table",
136+
inputSchema={
137+
"type": "object",
138+
"properties": {
139+
"table_name": {
140+
"type": "string",
141+
"description": "Name of the table to describe",
142+
},
143+
},
144+
"required": ["table_name"],
145+
},
146+
),
147+
]
148+
149+
@server.call_tool()
150+
async def handle_call_tool(
151+
name: str, arguments: dict[str, Any] | None
152+
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
153+
"""Handle tool execution requests"""
154+
try:
155+
if name == "list-tables":
156+
results = db.execute_query("SELECT * FROM information_schema.tables;")
157+
return [types.TextContent(type="text", text=str(results))]
158+
159+
elif name == "describe-table":
160+
if not arguments or "table_name" not in arguments:
161+
raise ValueError("Missing table_name argument")
162+
results = db.execute_query("PRAGMA table_info(?)", [arguments["table_name"]])
163+
return [types.TextContent(type="text", text=str(results))]
164+
165+
if not arguments:
166+
raise ValueError("Missing arguments")
167+
168+
if name == "read-query":
169+
if not arguments["query"].strip().upper().startswith("SELECT"):
170+
raise ValueError("Only SELECT queries are allowed for read-query")
171+
results = db.execute_query(arguments["query"])
172+
return [types.TextContent(type="text", text=str(results))]
173+
174+
elif name == "write-query":
175+
if arguments["query"].strip().upper().startswith("SELECT"):
176+
raise ValueError("SELECT queries are not allowed for write-query")
177+
results = db.execute_query(arguments["query"])
178+
return [types.TextContent(type="text", text=str(results))]
179+
180+
elif name == "create-table":
181+
if not arguments["query"].strip().upper().startswith("CREATE TABLE"):
182+
raise ValueError("Only CREATE TABLE statements are allowed")
183+
db.execute_query(arguments["query"])
184+
return [types.TextContent(type="text", text="Table created successfully")]
185+
186+
else:
187+
raise ValueError(f"Unknown tool: {name}")
188+
189+
except duckdb.Error as e:
190+
return [types.TextContent(type="text", text=f"Database error: {str(e)}")]
191+
except Exception as e:
192+
return [types.TextContent(type="text", text=f"Error: {str(e)}")]
193+
194+
# Run the server using stdin/stdout streams
195+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
196+
logger.info("DuckDB MCP Server running with stdio transport")
197+
await server.run(
198+
read_stream,
199+
write_stream,
200+
InitializationOptions(
201+
server_name="mcp-server-duckdb",
202+
server_version="0.1.0",
203+
capabilities=server.get_capabilities(
204+
notification_options=NotificationOptions(),
205+
experimental_capabilities={},
206+
),
207+
),
208+
)

0 commit comments

Comments
 (0)