Skip to main content
Glama
wuqunfei

duckdb-mcp

by wuqunfei

๐Ÿฆ† 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.

CI Python DuckDB MCP Code style License

โšก ~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 src/ package, just two runtime dependencies (duckdb, mcp). No bloat, no magic.

๐Ÿงฐ Complete

12 tools covering query, write, CSV/Parquet loading, and full catalog/schema/table introspection.

๐Ÿ”’ Safe by default

Engine-enforced --read-only mode, plus a list_environments tool that masks every secret (****).

โ˜๏ธ Cloud-ready

Query s3:// Parquet directly, with ${VAR} interpolation to keep credentials out of config files.

๐Ÿงฉ 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 --transport flag.

๐Ÿ”‘ 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.csv and 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 install

CLI arguments

Flag

Description

--db, --database

Database path (:memory: or /path/to/db.duckdb). Default: :memory:

--schema

Default schema. Default: main

--init-sql

Path to a SQL file executed once on startup

--read-only

Open the database read-only (default: read-write)

--allow-unsigned-extensions

Allow unsigned/community extensions (default: off; env: ALLOW_UNSIGNED_EXTENSIONS)

--transport

stdio (default) or http (streamable HTTP)

--host

Bind host for the http transport (default: 127.0.0.1)

--port

Bind port for the http transport (default: 8000)

--log-level

Log level to stderr: DEBUG/INFO/WARNING/ERROR/CRITICAL (default: WARNING; env: LOG_LEVEL). DEBUG logs every tool request

๐Ÿ› Debugging: run with --log-level DEBUG (or LOG_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):

  • โœ… SELECT works normally

  • ๐Ÿšซ INSERT / UPDATE / DELETE / CREATE / DROP are 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)

--transport stdio

Local clients that launch the process, e.g. Claude Desktop

streamable HTTP

--transport http

Remote / networked clients โ€” the current MCP HTTP transport (single /mcp endpoint)

# 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-only if 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

--transport http

arg

Required to serve over HTTP

--host / --port

arg

Bind address (default 127.0.0.1:8000)

--db / --schema / --init-sql / --read-only

arg

Same as stdio mode

MCP_AUTH_TOKEN

env

Require Authorization: Bearer <token> (env-only, no flag)

ALLOW_UNSIGNED_EXTENSIONS

env

Community extensions (or the --allow-unsigned-extensions flag)

AWS_*, any ${VAR}

env

Cloud credentials for s3:// reads (see the Environment variables & cloud data section)

--log-level / LOG_LEVEL

both

DEBUG logs every tool request to stderr

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 of ps and 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-only and network controls; it is not per-user auth.

๐Ÿ”’ --host defaults to 127.0.0.1 (localhost only). Bind to 0.0.0.0 only on a trusted network, and always with MCP_AUTH_TOKEN set.

๐Ÿ”— 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.json

  • Linux: ~/.config/Claude/claude_desktop_config.json

  • Windows: %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 the list_environments tool 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

query

Run a SELECT and return the result as a text table

โœ๏ธ Core

execute

Run a write statement (INSERT/UPDATE/DELETE/CREATE/DROP); returns a status message

๐Ÿ“ฅ File I/O

read_csv

Load a CSV file into a table (table_name defaults to data)

๐Ÿ“ฅ File I/O

read_parquet

Load a Parquet file into a table (table_name defaults to data)

๐Ÿ“š Introspection

list_catalogs

List catalogs

๐Ÿ“š Introspection

list_databases

List databases

๐Ÿ“š Introspection

list_schemas

List schemas

๐Ÿ“š Introspection

list_tables

List tables in the current schema

๐Ÿ“š Introspection

list_columns

Describe a table's columns

๐Ÿ“š Introspection

list_extensions

List loaded extensions

๐Ÿ” Introspection

list_environments

List environment variables as key: value, with values masked (****, or empty when unset)

๐Ÿท๏ธ Introspection

check_version

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.py holds DuckDBSession with no MCP imports, so the connection/formatting logic is unit-testable with only duckdb.

  • ๐Ÿ—‚๏ธ server.py holds the _HANDLERS registry (single source of truth for the tool set) plus dispatch_tool() and the thin typed MCP registrations.

  • ๐ŸŽ›๏ธ cli.py parses 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 to main, and checks that duckdb-mcp --help works.

  • CD (.github/workflows/release.yml) builds the sdist/wheel on a v* tag and attaches them to the GitHub Release. PyPI publishing is intentionally disabled โ€” the publish-pypi job is gated behind if: 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