duckdb-mcp
๐ฆ DuckDB MCP Server
Give your AI assistant a blazing-fast, persistent SQL engine.
A minimal Model Context Protocol server that plugs DuckDB straight into Claude โ query CSVs, Parquet, and cloud data in plain language, at in-process speed.
โก ~10 ms / query ยท ๐ชถ 2 runtime deps ยท ๐งฐ 12 tools ยท ๐ read-only & secret-safe
โจ Why this one?
โก Fast | One persistent connection for the whole session โ no subprocess spawn, no reconnect. ~10 ms per query. |
๐ชถ Tiny | A focused |
๐งฐ Complete | 12 tools covering query, write, CSV/Parquet loading, and full catalog/schema/table introspection. |
๐ Safe by default | Engine-enforced |
โ๏ธ Cloud-ready | Query |
๐งฉ Extensible | Opt-in unsigned/community extensions (e.g. TA-Lib) via a single flag or env var. |
๐ Local or remote | Run over stdio (Claude Desktop) or streamable HTTP โ one |
๐ Token auth | Protect the HTTP transport with a bearer token from a single env var. |
โ Trustworthy | Fully typed, linted, and tested โ CI runs black + ruff + mypy + pytest on Python 3.12 & 3.13. |
๐ Quick start
1. Run it โ no install needed (via uv):
uvx --from git+https://github.com/wuqunfei/duckdb-mcp-mini duckdb-mcp --db :memory:2. Point Claude Desktop at it โ add this to claude_desktop_config.json and restart Claude:
{
"mcpServers": {
"duckdb": {
"command": "uvx",
"args": ["--from", "git+https://github.com/wuqunfei/duckdb-mcp-mini", "duckdb-mcp", "--db", ":memory:"]
}
}
}3. Ask away ๐ฌ
"Load
~/data/sales.csvand show me total revenue by region."
That's it. Tools are available immediately. ๐
๐ฆ Install
pip install . # from a clone, into the current environment
pip install -e ".[dev]" # for development (editable + dev tools)Or run straight from Git without installing:
uvx --from git+https://github.com/wuqunfei/duckdb-mcp-mini duckdb-mcp --db :memory:โน๏ธ This package is not published to PyPI โ install from source or run from Git as shown above.
Requirements: ๐ Python 3.11โ3.14 ยท ๐ฆ DuckDB 1.5.2 (pinned in pyproject.toml, easy to change) ยท ๐ MCP SDK 2.0.0+
๐ Run
The installed console script is duckdb-mcp. It speaks MCP over stdio, so you'll normally launch it from an MCP client โ but you can start it directly too:
duckdb-mcp # in-memory, read-write
duckdb-mcp --db /path/to/analytics.duckdb --schema main
duckdb-mcp --db analytics.duckdb --init-sql init.sql
duckdb-mcp --db analytics.duckdb --read-only
python -m duckdb_mcp.cli --db :memory: # module form
uv run duckdb-mcp --db :memory: # via uv, no installCLI arguments
Flag | Description |
| Database path ( |
| Default schema. Default: |
| Path to a SQL file executed once on startup |
| Open the database read-only (default: read-write) |
| Allow unsigned/community extensions (default: off; env: |
|
|
| Bind host for the |
| Bind port for the |
| Log level to stderr: |
๐ Debugging: run with
--log-level DEBUG(orLOG_LEVEL=DEBUG) to log every incoming tool request โ name and arguments โ to stderr, so you can see exactly what a client sends:DEBUG duckdb_mcp: tool request: query args={'sql': 'SELECT 42 AS answer'}Logs always go to stderr (never stdout, which is the protocol channel in stdio mode).
๐ Read-only mode
--read-only opens the connection via DuckDB's own read-only flag, so writes are blocked at the engine level (not by inspecting SQL):
โ
SELECTworks normally๐ซ
INSERT/UPDATE/DELETE/CREATE/DROPare rejected by DuckDB๐ก Perfect for shared analytics/reporting databases where accidental writes must be impossible
๐ Transports
The server speaks two MCP transports โ pick with --transport:
Transport | Flag | Use it for |
stdio (default) |
| Local clients that launch the process, e.g. Claude Desktop |
streamable HTTP |
| Remote / networked clients โ the current MCP HTTP transport (single |
# stdio (default) โ the process talks over stdin/stdout
duckdb-mcp --db analytics.duckdb๐ Run an HTTP server
Configuration splits cleanly in two: args set the database and network binding, environment variables carry secrets and toggles (so they stay out of ps and shell history).
# 1) Environment โ secrets & toggles
export MCP_AUTH_TOKEN="a-long-random-secret" # require Bearer auth (recommended)
export ALLOW_UNSIGNED_EXTENSIONS=true # optional: allow community extensions
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID}" # optional: for s3:// queries
export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY}"
# 2) Args โ database + network binding
duckdb-mcp \
--transport http \
--host 0.0.0.0 \
--port 8000 \
--db /data/analytics.duckdb \
--init-sql /data/init.sql \
--read-only
# โ serving at http://0.0.0.0:8000/mcp--init-sql runs a SQL file once, when the connection opens. Combined with --read-only it must be read-only-safe (loading extensions or setting options is fine โ creating tables is not):
-- /data/init.sql
INSTALL httpfs; LOAD httpfs; -- read remote/S3 data
SET memory_limit = '4GB';Drop
--read-onlyif your init script needs to create tables or views.
No install? Run the same thing straight from Git:
MCP_AUTH_TOKEN="a-long-random-secret" \
uvx --from git+https://github.com/wuqunfei/duckdb-mcp-mini duckdb-mcp \
--transport http --host 0.0.0.0 --port 8000 --db :memory:Setting | Kind | Notes |
| arg | Required to serve over HTTP |
| arg | Bind address (default |
| arg | Same as stdio mode |
| env | Require |
| env | Community extensions (or the |
| env | Cloud credentials for |
| both |
|
Verify it's up (with MCP_AUTH_TOKEN set, a missing/wrong token returns 401):
curl -s -o /dev/null -w '%{http_code}\n' \
-X POST http://127.0.0.1:8000/mcp \
-H 'Authorization: Bearer a-long-random-secret' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"ping"}'
# 401 = missing/invalid token ยท 400 = reached the server (a real client does the full MCP handshake)๐ Bearer token
Setting MCP_AUTH_TOKEN turns on auth for the HTTP transport:
๐ฑ Environment only โ read from
MCP_AUTH_TOKEN, never a CLI flag, so it stays out ofpsand shell history.๐ HTTP only โ ignored for
stdio(the client owns the process); setting it there prints a warning.โ ๏ธ Access control, not identity โ every caller with the token gets full database access. Pair it with
--read-onlyand network controls; it is not per-user auth.
๐
--hostdefaults to127.0.0.1(localhost only). Bind to0.0.0.0only on a trusted network, and always withMCP_AUTH_TOKENset.
๐ Connect a client
MCP client config (Claude Desktop or any client that supports an HTTP URL) โ omit headers if you didn't set a token:
{
"mcpServers": {
"duckdb": {
"url": "http://127.0.0.1:8000/mcp",
"headers": { "Authorization": "Bearer a-long-random-secret" }
}
}
}From Python (the mcp client libraries ship httpx2):
import asyncio
import httpx2
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
URL = "http://127.0.0.1:8000/mcp"
HEADERS = {"Authorization": "Bearer a-long-random-secret"} # omit if no MCP_AUTH_TOKEN
async def main():
async with httpx2.AsyncClient(headers=HEADERS) as http:
async with streamable_http_client(URL, http_client=http) as streams:
read, write = streams[0], streams[1]
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("tools:", [t.name for t in tools.tools])
result = await session.call_tool("query", {"sql": "SELECT 42 AS answer"})
print(result.content[0].text)
asyncio.run(main())๐ฅ๏ธ Configure Claude Desktop
Add one of the following to your claude_desktop_config.json, then restart Claude.
Run from Git (no install):
{
"mcpServers": {
"duckdb": {
"command": "uvx",
"args": ["--from", "git+https://github.com/wuqunfei/duckdb-mcp-mini", "duckdb-mcp", "--db", ":memory:"]
}
}
}Run from a local clone:
{
"mcpServers": {
"duckdb": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/duckdb-mcp-mini", "duckdb-mcp", "--db", "/home/user/analytics.duckdb", "--schema", "main"]
}
}
}After pip install . (command on PATH):
{
"mcpServers": {
"duckdb": {
"command": "duckdb-mcp",
"args": ["--db", "/home/user/analytics.duckdb", "--read-only"]
}
}
}๐ Config file locations
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonLinux:
~/.config/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.json
๐ Multiple servers
Add more entries under mcpServers with distinct names (e.g. duckdb-dev, duckdb-prod), each with its own --db/--schema.
โ๏ธ Environment variables & cloud data
Any variables set in the client's env block are available to DuckDB during connection (handy for S3/cloud credentials). Values support ${VAR_NAME} interpolation, so you can reference the system environment instead of hardcoding secrets into the config file:
{
"mcpServers": {
"duckdb": {
"command": "uvx",
"args": ["--from", "git+https://github.com/wuqunfei/duckdb-mcp-mini", "duckdb-mcp", "--db", ":memory:"],
"env": {
"AWS_ACCESS_KEY_ID": "${AWS_ACCESS_KEY_ID}",
"AWS_SECRET_ACCESS_KEY": "${AWS_SECRET_ACCESS_KEY}",
"AWS_REGION": "${AWS_REGION}",
"S3_BUCKET": "my-data-bucket"
}
}
}
}Set the referenced variables in your shell first:
export AWS_ACCESS_KEY_ID="your-key"
export AWS_SECRET_ACCESS_KEY="your-secret"
export AWS_REGION="us-east-1"Then query cloud data directly:
SELECT * FROM read_parquet('s3://my-data-bucket/data.parquet')๐ Interpolation happens before the DuckDB connection is opened, so credentials are ready in time for connection setup and any
--init-sql. Use thelist_environmentstool to confirm what's set โ values are always masked.
๐งฉ Community & unsigned extensions
DuckDB only loads signed extensions by default. To use community or self-hosted extensions, enable unsigned extensions with the --allow-unsigned-extensions flag or the ALLOW_UNSIGNED_EXTENSIONS environment variable (default: off):
duckdb-mcp --db :memory: --allow-unsigned-extensions
# or
ALLOW_UNSIGNED_EXTENSIONS=true duckdb-mcp --db :memory:In a Claude Desktop config:
{
"mcpServers": {
"duckdb": {
"command": "uvx",
"args": ["--from", "git+https://github.com/wuqunfei/duckdb-mcp-mini", "duckdb-mcp", "--db", ":memory:", "--allow-unsigned-extensions"]
}
}
}The flag is applied as a connection-time setting, so it's already on โ just INSTALL and LOAD through the execute tool. For example, neuesql/atm_talib (TA-Lib for DuckDB):
INSTALL talib FROM 'https://neuesql.github.io/atm_talib';
LOAD talib;โ ๏ธ Unsigned extensions run native code with full trust. Only enable this for extensions from sources you trust.
๐งฐ Tools (12)
Category | Tool | Purpose |
๐ Core |
| Run a |
โ๏ธ Core |
| Run a write statement ( |
๐ฅ File I/O |
| Load a CSV file into a table ( |
๐ฅ File I/O |
| Load a Parquet file into a table ( |
๐ Introspection |
| List catalogs |
๐ Introspection |
| List databases |
๐ Introspection |
| List schemas |
๐ Introspection |
| List tables in the current schema |
๐ Introspection |
| Describe a table's columns |
๐ Introspection |
| List loaded extensions |
๐ Introspection |
| List environment variables as |
๐ท๏ธ Introspection |
| Report the DuckDB version |
query vs execute โ query is for reads (fetches rows, formats a table); execute is for writes/DDL (runs the statement, returns "Executed successfully").
๐๏ธ Architecture
launch (cli.main)
โ
DuckDBSession(...) # src/duckdb_mcp/session.py
โโ interpolate ${VAR} across os.environ โ runs BEFORE connecting
โโ duckdb.connect(db, read_only=...) โ one persistent connection
โโ run --init-sql (if provided) โ falls back to :memory: on error
โ
create_server(session) # src/duckdb_mcp/server.py
โโ registers 12 tools on an mcp MCPServer; each calls dispatch_tool()
โ
_serve(session, transport=...) # src/duckdb_mcp/cli.py
โโ run_stdio_async() | run_streamable_http_async()๐งฉ
session.pyholdsDuckDBSessionwith no MCP imports, so the connection/formatting logic is unit-testable with onlyduckdb.๐๏ธ
server.pyholds the_HANDLERSregistry (single source of truth for the tool set) plusdispatch_tool()and the thin typed MCP registrations.๐๏ธ
cli.pyparses args, builds the session, and serves.
Good to know:
๐ If connecting to
--db(or running--init-sql) fails, the session degrades to an in-memory read-write connection rather than crashing.โ ๏ธ SQL is f-string interpolated (table names, filepaths). This is intentional for a local single-user tool โ inputs are not sanitized.
๐ ๏ธ Development
pip install -e ".[dev]" # or: uv sync --extra dev
black --check src tests # format (drop --check to apply)
ruff check src tests # lint
mypy src # type check
pytest # tests
pytest tests/test_server.py::test_dispatch_query # run a single test๐ Project layout
duckdb-mcp-mini/
โโโ src/duckdb_mcp/
โ โโโ __init__.py # package version + DuckDBSession export
โ โโโ session.py # persistent DuckDB session (no MCP deps)
โ โโโ server.py # tool registry + dispatch + MCP server wiring
โ โโโ cli.py # argument parsing + entrypoint
โโโ tests/ # pytest suite
โโโ .github/workflows/
โ โโโ ci.yml # lint + type + test on py3.11โ3.14
โ โโโ release.yml # build on tag (PyPI publish disabled)
โโโ pyproject.toml
โโโ LICENSE
โโโ README.md๐ CI / CD
CI (
.github/workflows/ci.yml) runs black, ruff, mypy, and pytest on Python 3.11, 3.12, 3.13, and 3.14 for every push/PR tomain, and checks thatduckdb-mcp --helpworks.CD (
.github/workflows/release.yml) builds the sdist/wheel on av*tag and attaches them to the GitHub Release. PyPI publishing is intentionally disabled โ thepublish-pypijob is gated behindif: false; see the comments in that file to enable it later.
๐ค Contributing
Issues and PRs are welcome! Please keep the footprint small (the minimalism is a feature ๐ชถ), and make sure black / ruff / mypy / pytest all pass before opening a PR.
Built with ๐ฆ + ๐ ยท Licensed under MIT