mcp-duckdb-analyst
Provides read-only analytical access to DuckDB database files, including table discovery, schema inspection, column profiling, sampling, and guarded SQL queries with bounded results.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-duckdb-analystProfile the orders table and show me total revenue by month"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-duckdb-analyst
A read-only MCP server that lets Claude (Desktop or Code) or any other MCP client explore and query local CSV, Parquet and JSON files, or an existing DuckDB database, with guarded SQL.
Most useful analysis questions in a mid-sized company start with "what is actually in this export?" and end long before a data warehouse project is justified. Handing a language model raw SQL access to those files is quick, but the default trade-off is uncomfortable: the same connection that can read a CSV can also write files, attach databases, install extensions and read anything on disk. This server keeps the useful part (discover tables, profile columns, run analytical SQL, explain plans) and removes the rest by design: a SQL guard, DuckDB's own sandbox settings, row and time caps, and a data directory the model cannot leave.
Features
Zero-setup catalog: every
.csv,.parquet,.json,.jsonland.ndjsonfile below--data(recursively) becomes a DuckDB view named from its relative path (articles/2025.csvbecomesarticles_2025), with deterministic handling of name collisions. Optionally attach an existing DuckDB file read-only with--db.Nine analysis tools:
list_tables,describe_table,profile_table,sample_rows,query,explain_query,search_columns,table_relationships, plus opt-insave_query/list_saved_queries. All statistics are computed in SQL; no pandas.Resources and prompt:
duckdb://schemaandduckdb://schema/{table}give the model a Markdown schema overview; theanalyze_tableprompt encodes a repeatable first-pass analysis.SQL guard (
guard.py, unit-tested with an 84-case allow/deny matrix): singleSELECT/WITH/DESCRIBE/SHOW/SUMMARIZE/EXPLAINstatements only; every DDL/DML,COPY,ATTACH,INSTALL,PRAGMA,SETand friends is rejected wherever it appears, including inside CTEs and behind comments; file-reading functions may only reference files inside the data directory;LIMITis enforced by wrapping the statement.Second line of defence in DuckDB itself:
enable_external_access = false,allowed_directories = [data dir], extension autoloading off,lock_configuration = true,--dbattachedREAD_ONLY.Bounded results: row cap (
--max-rows), cell cap (--max-cells), per-query timeout viainterrupt()on a worker thread, atruncatedflag and notes that tell the model how to refine.Structured errors with hints: every rejection comes back as
[code] message Hint: ..., so the model can fix its call instead of guessing.Two transports: stdio (Claude Desktop, Claude Code) and streamable HTTP.
Synthetic sample data for a fictional publisher (subscribers, orders, articles, web events), generated deterministically and checked in CI.
Related MCP server: motherduck-mcp
Quickstart
Requirements: Python 3.11+ and uv.
git clone https://github.com/nkrimmel/mcp-duckdb-analyst.git
cd mcp-duckdb-analyst
uv sync
uv run mcp-duckdb-analyst --data examples/data --check--check registers the files, prints the catalog to stderr and exits:
mcp-duckdb-analyst 0.1.0 - data: .../mcp-duckdb-analyst/examples/data
table kind rows cols source
articles_2025 view 150 8 articles/2025.csv
articles_2026 view 110 8 articles/2026.csv
orders view 1,200 9 orders.parquet
subscribers view 300 13 subscribers.csv
web_events view 900 8 web_events.jsonl
max-rows=500 max-cells=20000 timeout=30s extensions=off saved-queries=off transport=stdioWithout --check the same command serves MCP over stdio. Run the tests with uv run pytest -q.
Usage
Claude Desktop
Add the server to claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\). uv run --directory makes the project's virtual environment available without activating it; use absolute paths, Claude Desktop does not expand ~.
{
"mcpServers": {
"duckdb-analyst": {
"command": "uv",
"args": [
"run", "--directory", "/ABSOLUTE/PATH/TO/mcp-duckdb-analyst",
"mcp-duckdb-analyst", "--data", "/ABSOLUTE/PATH/TO/your-data-folder"
]
}
}
}Restart Claude Desktop; the tools appear under the server name. To expose a DuckDB database as well, append "--db", "/ABSOLUTE/PATH/TO/warehouse.duckdb" to args.
Claude Code
claude mcp add duckdb-analyst -- \
uv run --directory /ABSOLUTE/PATH/TO/mcp-duckdb-analyst mcp-duckdb-analyst --data ./dataOr check a project-scoped .mcp.json into the repository whose data you want to analyse:
{
"mcpServers": {
"duckdb-analyst": {
"command": "uv",
"args": ["run", "--directory", "/ABSOLUTE/PATH/TO/mcp-duckdb-analyst",
"mcp-duckdb-analyst", "--data", "./data", "--max-rows", "200"]
}
}
}Streamable HTTP
uv run mcp-duckdb-analyst --data examples/data --transport streamable-http --port 8931The endpoint is http://127.0.0.1:8931/mcp. A raw initialize request answers with the server's capabilities and instructions:
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"prompts":{...},"resources":{...},"tools":{...}},
"instructions":"Read-only analytical access to local data files via DuckDB SQL.\nTables (5): ...",
"protocolVersion":"2025-06-18","serverInfo":{"name":"mcp-duckdb-analyst","version":"0.1.0"}}}There is no authentication layer; bind to localhost (the default) or put a reverse proxy with auth in front of it.
Example conversation
The sample data describes a fictional publisher, "Nordlicht Verlag". Tool calls and results below are real output from this server (abbreviated).
User: Which subscription plans lose the most customers?
Claude calls
list_tables():{"tables": [{"name": "articles_2025", "kind": "view", "source": "articles/2025.csv", "row_count": 150, "column_count": 8}, {"name": "orders", "kind": "view", "source": "orders.parquet", "row_count": 1200, "column_count": 9}, {"name": "subscribers", "kind": "view", "source": "subscribers.csv", "row_count": 300, "column_count": 13}, ...]}then
query(sql="SELECT plan, count(*) AS subscribers, round(100.0 * avg(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END), 1) AS cancelled_pct FROM subscribers GROUP BY plan ORDER BY cancelled_pct DESC"):
plan
subscribers
cancelled_pct
student
64
21.9
digital
80
20.0
print_digital
90
15.6
66
13.6
(
truncated: false,elapsed_ms: 9.72)Claude: Student and digital-only plans cancel most often (21.9 % and 20.0 %), print-only plans least (13.6 %). Shall I break this down by acquisition channel?
User: Yes. And export the subscriber table to CSV for me.
Claude calls
query(sql="COPY subscribers TO '/tmp/subscribers.csv'")and receives a tool error:[denied_keyword] The statement contains 'COPY', which is not allowed on this read-only server. Hint: Only single SELECT / WITH ... SELECT / DESCRIBE / SHOW / EXPLAIN / SUMMARIZE statements are accepted by this read-only server. If this is a column name, double-quote it.then
query(sql="SELECT acquisition_channel, count(*) AS n, round(100.0 * avg(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END), 1) AS cancelled_pct FROM subscribers GROUP BY 1 ORDER BY cancelled_pct DESC"):
acquisition_channel
n
cancelled_pct
web
48
25.0
partner
64
21.9
newsletter
53
18.9
phone
66
15.2
app
69
10.1
Claude: The server is read-only, so I cannot write a CSV; here is the query you can run yourself. By channel, web sign-ups cancel most (25.0 %) and app sign-ups least (10.1 %).
Python API
The MCP layer is thin; the same operations are available directly:
from pathlib import Path
from mcp_duckdb_analyst import Analyst, Engine, ServerConfig
engine = Engine(ServerConfig(data_dir=Path("examples/data"), max_rows=500))
analyst = Analyst(engine)
result = engine.query(
"SELECT plan, count(*) AS orders, round(sum(amount_eur), 2) AS revenue_eur "
"FROM orders WHERE status = 'paid' GROUP BY plan ORDER BY revenue_eur DESC"
)
print(result.markdown)
print(result.truncated, result.row_count, result.elapsed_ms)
profile = analyst.profile_table("orders", ["plan", "amount_eur"])
print(profile.model_dump_json(indent=2))Output (the profile JSON is condensed here; the real output has one key per line):
| plan | orders | revenue_eur |
| --- | --- | --- |
| print_digital | 239 | 31837.37 |
| print | 176 | 18229.87 |
| digital | 195 | 6855.85 |
| student | 178 | 2716.95 |
False 4 1.39
{
"name": "orders",
"row_count": 1200,
"columns": [
{"name": "plan", "type": "VARCHAR", "is_numeric": false, "count": 1200, "null_count": 0,
"null_pct": 0.0, "distinct_count": 4, "min": "digital", "max": "student", "mean": null, "stddev": null,
"top_values": [{"value": "print_digital", "count": 364}, {"value": "digital", "count": 323},
{"value": "print", "count": 257}, {"value": "student", "count": 256}]},
{"name": "amount_eur", "type": "DECIMAL(10,2)", "is_numeric": true, "count": 1200, "null_count": 0,
"null_pct": 0.0, "distinct_count": 36, "min": 3.68, "max": 418.8, "mean": 73.488592,
"stddev": 110.934603, "top_values": null}
],
"low_cardinality_threshold": 20
}table_relationships() on the sample data finds the two real foreign keys and nothing else:
orders.subscriber_id -> subscribers.subscriber_id (containment 1.0, 295 sampled values, target unique)
web_events.subscriber_id -> subscribers.subscriber_id (containment 1.0, 260 sampled values, target unique)How it works
flowchart LR
C[MCP client<br/>Claude Desktop / Code] -- stdio or HTTP --> S[MCPServer<br/>tools, resources, prompt]
S --> A[Analyst<br/>describe, profile, sample,<br/>search, relationships, explain]
A --> E[Engine<br/>timeout, row and cell caps,<br/>error translation]
S -- query --> E
E --> G[SqlGuard<br/>tokenizer scan, statement type,<br/>AST walk, path check, LIMIT wrap]
G --> D[(DuckDB in-memory<br/>views over files,<br/>optional read-only DB)]
D --> F[data directory<br/>csv parquet json jsonl]Catalog
catalog.py walks the data directory (hidden entries and saved_queries.json are skipped) and creates one view per file using the matching reader (read_csv, read_parquet, read_json with format = 'auto' or 'newline_delimited'). Names are lower-cased, non-alphanumeric runs become _, names starting with a digit get a t_ prefix and reserved keywords a trailing _. Collisions (sales/2026.parquet next to sales_2026.csv, or a file named like a table in --db) get _2, _3 ... in sorted path order, so the mapping is stable across restarts. Files DuckDB cannot read are reported in the startup summary and skipped.
The SQL guard
Every statement sent to query, explain_query or save_query passes four independent checks; any one of them can reject it:
Keyword scan on tokenizer output. DuckDB's own tokenizer (
duckdb.tokenize) classifies every token; only tokens of type keyword are compared against the deny list (INSERT UPDATE DELETE TRUNCATE MERGE CREATE DROP ALTER COPY EXPORT IMPORT ATTACH DETACH INSTALL LOAD PRAGMA SET RESET CALL VACUUM CHECKPOINT FORCE GRANT REVOKE BEGIN COMMIT ROLLBACK PREPARE EXECUTE DEALLOCATE USE). Comments and string literals are therefore never mistaken for SQL, and keywords hidden between comments or inside a CTE are still caught.Statement type.
duckdb.extract_statementsmust return exactly one statement (so;chaining fails) and its type must beSELECTorEXPLAIN; DuckDB representsDESCRIBE,SHOWandSUMMARIZEasSELECTstatements internally.PIVOT/UNPIVOTexpand into several statements and are rejected with a hint to use conditional aggregation.AST walk. The statement is serialised with
json_serialize_sql; every file-reading table function (read_csv*,read_parquet,read_json*,glob,read_text,read_blob,sqlite_scan, ...) and every path-like table reference (FROM 'x.csv') must use string literals whose resolved path lies inside the data directory. Computed paths,..escapes,~, hidden files and remote schemes (http://,s3://,hf://, ...) are rejected. Accepted literals are rewritten to their absolute form so that DuckDB'sallowed_directoriescheck (which sees raw paths) accepts them too.LIMIT enforcement. The statement is wrapped as
SELECT * FROM (...) AS _guarded LIMIT n + 1withn = min(limit, --max-rows); the extra row is how thetruncatedflag is detected without a second query.EXPLAINis executed as-is.
The engine then executes the statement on a worker thread and calls connection.interrupt() when --timeout-s elapses, lowers the row limit further if rows x columns would exceed --max-cells, converts values to JSON-safe types (dates to ISO strings, decimals to numbers) and renders a Markdown table alongside the row list.
Security model
Layer | What it guarantees | Where |
SQL guard | Only read statements; file access only inside the data directory; one statement per call; bounded rows |
|
DuckDB configuration |
|
|
Resource caps |
|
|
Transport | stdio by default: no network listener at all; HTTP binds to |
|
Errors | Rejections are returned as tool errors ( |
|
What the server does not do: it does not authenticate HTTP clients, it does not cap memory (DuckDB's default limit applies), and --allow-extensions deliberately re-enables external access at the DuckDB level so INSTALL/LOAD can work; the SQL guard still applies, but use that flag only with clients you trust. Anything inside the data directory that is not a hidden file is readable by design, so point --data at a folder that contains only what the model may see.
Configuration
All options are CLI flags of mcp-duckdb-analyst:
Flag | Default | Meaning |
| required | Directory scanned recursively for |
| none | Existing DuckDB database file, attached read-only as catalog |
|
| MCP transport |
|
| Bind address for streamable HTTP; the endpoint is |
|
| Hard cap on rows per result; |
|
| Cap on rows x columns per result |
|
| Per-query timeout; the query is interrupted |
| off | Accept |
| off | Enable |
|
| Server log level; logs go to stderr |
| Register the files, print the catalog and exit | |
|
Diagnostics always go to stderr because stdout carries the MCP protocol on stdio.
Tools at a glance
Tool | Returns |
| name, kind ( |
| columns with DuckDB type, declared nullability, observed null count, three sample values |
| per column: count, null %, distinct, min, max, mean and sample stddev for numerics, top-5 values with counts when distinct <= 20 |
| reproducible reservoir sample (fixed seed) |
| columns, JSON rows, Markdown table, |
| DuckDB's physical plan as text |
| columns across all tables matching a substring or |
|
|
| opt-in named queries, guard-validated before writing |
Project structure
mcp-duckdb-analyst/
├── .github/workflows/ci.yml lint + tests on Python 3.11 and 3.13, sample-data reproducibility
├── examples/data/ synthetic publisher data set (229 KB)
│ ├── articles/2025.csv, 2026.csv
│ ├── orders.parquet
│ ├── subscribers.csv
│ └── web_events.jsonl
├── scripts/make_sample_data.py deterministic generator (--check verifies the committed files)
├── src/mcp_duckdb_analyst/
│ ├── analysis.py describe / profile / sample / search / relationships / explain
│ ├── catalog.py file discovery, view naming, collisions, --db objects
│ ├── cli.py Typer entry point (mcp-duckdb-analyst)
│ ├── config.py ServerConfig (pydantic)
│ ├── engine.py DuckDB connection, sandbox settings, timeout, caps
│ ├── errors.py AnalystError hierarchy (code, message, hint)
│ ├── formatting.py JSON-safe values, Markdown tables, quoting
│ ├── guard.py the SQL guard
│ ├── models.py pydantic result models (= MCP output schemas)
│ ├── saved_queries.py opt-in saved_queries.json store
│ └── server.py MCPServer assembly: tools, resources, prompt
├── tests/ 186 tests, see Development
├── CHANGELOG.md
├── LICENSE
├── pyproject.toml
└── uv.lockDevelopment
uv sync # installs the package (editable) and the dev group
uv run pytest -q # 186 tests, about 3 seconds
uv run ruff check . # lint
uv run ruff format --check . # formatting
uv run python scripts/make_sample_data.py --out build/sample-data --check examples/dataThe test suite covers the guard allow/deny matrix (including comments hiding keywords, INSERT inside a CTE, ATTACH in odd casing and whitespace, read_csv inside vs. outside the data directory, LIMIT rewriting and path rewriting), view naming and collisions, profile statistics against hand-computed values, every tool called in-process, the CLI, and one end-to-end session in which a real mcp stdio client spawns the server, lists tools, runs allowed and denied queries and reads a resource. Tests need no network access. CI runs the same commands on ubuntu-latest for Python 3.11 and 3.13 and checks that the sample data regenerates byte-for-byte.
Limitations
The sample data is entirely synthetic: names, cities, plans and prices are generated from a fixed seed and do not describe any real publisher or person.
The guard is a deny-list over DuckDB's parser output plus DuckDB's own sandbox; it is designed to block writes and file escapes, not to hide data that lives inside the data directory. Do not point
--dataat a folder with files the model must not read.PIVOT/UNPIVOTare rejected because DuckDB expands them into several statements. Prefixed string literals (E'...') are not rewritten and fall through to DuckDB's sandbox, which rejects relative paths.Result sets are capped and truncated; the server is meant for aggregation and inspection, not for bulk export.
Memory is not capped by the server; a pathological join can still make DuckDB use a lot of RAM before the timeout hits.
Requires the
mcpPython SDK 2.x (MCPServer, formerlyFastMCP).
License
MIT, see LICENSE.
Built by Nicholas Krimmel · LinkedIn
Available Tools
8 toolsdescribe_tableBRead-onlyIdempotent
Columns of a table with DuckDB type, nullability, null count and sample values.
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| kind | Yes | |
| name | Yes | |
| source | Yes | |
| columns | Yes | |
| row_count | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare the tool read-only, idempotent, and non-destructive, so no safety disclaimer is needed. The description adds value by specifying the exact returned information, which aligns with the read-only behavior, but it does not disclose edge cases like behavior when the table does not exist.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single compact sentence, front-loaded with 'Columns of a table' and every phrase adds information about the returned data. There is no filler or repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple one-parameter read-only introspection tool, the description covers the main purpose and output, and the presence of an output schema fills in the return structure. However, it misses explicit parameter-to-role mapping and usage boundaries, so it is not fully complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema offers only a bare 'name' string with no description, and schema description coverage is 0%. The description mentions 'a table' but never explicitly states that the 'name' parameter is the table name or what format it should take, leaving the parameter role under-specified.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly identifies the resource (a table) and the specific column metadata returned (DuckDB type, nullability, null count, sample values), which distinguishes it from siblings like list_tables and sample_rows. However, it is a noun phrase rather than an explicit verb phrase, so it stops short of a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No guidance is given on when to use describe_table versus alternatives such as profile_table or sample_rows. The description only states what the tool outputs, leaving the agent to infer the appropriate invocation context.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
explain_queryARead-onlyIdempotent
Show DuckDB's physical query plan for a SELECT without running it.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| sql | Yes | |
| plan | Yes | |
| elapsed_ms | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description adds behavioral value beyond the annotations by stating the query is not executed. It also clarifies the output is a physical query plan. This aligns with the readOnly and idempotent hints without contradicting them.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single sentence that front-loads the tool's purpose and adds the key behavioral distinction ('without running it'). There is no filler or redundant information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple one-parameter tool with an output schema and complete safety annotations, the description covers the purpose, accepted input, and non-execution behavior. Nothing essential is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema provides no description for the 'sql' parameter (0% coverage), so the description carries some burden. It clarifies that the parameter should be a SELECT statement, but does not provide format, dialect, or constraint details. For a single self-named parameter, this is adequate but not rich.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('Show'), a specific resource ('DuckDB's physical query plan'), and a scope ('for a SELECT'). It clearly distinguishes this from running a query, especially in contrast to the sibling 'query' tool.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The phrase 'without running it' makes clear this is for plan inspection rather than execution, and limiting to SELECT gives a clear applicability boundary. It does not explicitly name alternatives or say when not to use it, but enough context is present.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_tablesARead-onlyIdempotent
List all queryable tables and views with source file, kind and row count.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| tables | Yes | |
| data_dir | Yes | |
| database | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare the tool to be read-only, idempotent, and non-destructive. The description adds the useful scope constraint 'queryable' and lists the return fields, but does not disclose additional behavioral details such as pagination or ordering. This is acceptable given the strong annotation coverage.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single, well-structured sentence that front-loads the action and resource, then appends the exact returned fields. No filler or redundant wording.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool is simple, has no parameters, strong annotations, and an output schema. The description fully conveys the purpose, scope, and returned information, so an agent can confidently invoke it for table discovery.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters, so the schema covers all parameter semantics by definition. The description still adds value by clarifying what 'all' means in the output scope.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('List'), a clear resource ('all queryable tables and views'), and the exact metadata returned ('source file, kind and row count'). It clearly distinguishes this discovery tool from siblings like describe_table or query.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The context is clearly implied: use this tool to enumerate available tables and views before drilling into details. However, it does not explicitly mention when not to use it or name alternatives such as search_columns or describe_table.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
profile_tableARead-onlyIdempotent
Per-column statistics: count, null %, distinct, min, max, mean/stddev (numeric), top-5 values for low-cardinality columns. Optionally restrict to some columns.
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | ||
| columns | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| name | Yes | |
| columns | Yes | |
| row_count | Yes | |
| low_cardinality_threshold | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, and non-destructive behavior, so the description need not repeat those. It adds useful behavioral detail: numeric-only mean/stddev, low-cardinality top-5 values, and optional column restriction. No contradiction with annotations exists.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, compact sentence with no filler. It front-loads the core output, then adds the optional restriction. Every word contributes information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the low parameter count, straightforward optional behavior, safe annotations, and presence of an output schema, the description provides enough for an agent to invoke the tool and understand the result shape. It lacks explicit sibling differentiation and table-name semantics, but these are minor for this simple read-only profiler.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate. It partially clarifies the columns parameter as an optional restriction, but it never explains that name refers to the table being profiled nor any expected naming/format details. This leaves a meaningful gap for one of only two parameters.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly identifies the tool as producing per-column statistics and enumerates specific outputs: count, null %, distinct, min, max, mean/stddev, and top-5 values. This distinguishes it from describe_table and sample_rows despite not using an explicit verb. It would be a 5 with a direct action verb like 'Compute'.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The use case is implied: use this when you need column-level statistical summaries and optionally want to restrict to certain columns. However, there is no explicit guidance on when to prefer this over describe_table, sample_rows, or query, and no alternatives are named.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
queryARead-onlyIdempotent
Run one read-only SQL statement (SELECT / WITH / DESCRIBE / SHOW / SUMMARIZE / EXPLAIN). Returns columns, JSON rows, a Markdown table, a truncated flag and the elapsed time. The row limit is min(limit, server max rows).
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | ||
| limit | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| sql | Yes | The statement as executed (after guard rewriting). |
| rows | Yes | Row values, JSON-safe (dates as ISO strings). |
| notes | No | |
| columns | Yes | |
| markdown | Yes | The same rows rendered as a Markdown table. |
| row_count | Yes | Number of rows returned (after truncation). |
| row_limit | Yes | Row cap that was applied, if any. |
| truncated | Yes | True if more rows existed than were returned. |
| elapsed_ms | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare the tool read-only, idempotent, and non-destructive, so the description's main value is its return contract: columns, JSON rows, Markdown table, truncated flag, elapsed time, and the row-limit rule. This meaningfully supplements the annotations, though failure/error behavior is not addressed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is three compact sentences with no filler. The core purpose is front-loaded, and the statement-type list, return payload, and row-limit behavior each earn their place without redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only query tool with a supporting output schema, the description covers the essential operational details: what statements can run, what the response includes, and how limits are applied. The only notable gaps are the null-limit behavior and explicit sibling routing, both minor for this tool's safety profile.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must carry the parameter semantics. It implicitly defines 'sql' by listing supported statement types and explicitly defines 'limit' with 'min(limit, server max rows)'. It does not clarify what a null limit means, but the schema supplies the default.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a precise verb and resource: 'Run one read-only SQL statement,' and enumerates exactly which statement types are accepted. It clearly differentiates the tool from table/schema helpers like list_tables or describe_table, though it does not explicitly disambiguate from the explain_query sibling even though EXPLAIN is allowed.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives clear context for when to use this tool: for a single, read-only SQL statement among the listed types. It explicitly excludes writes and multi-statement calls, but it does not name sibling tools or state when a specialized tool like explain_query or sample_rows would be preferable.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
sample_rowsARead-onlyIdempotent
A reproducible random sample of n rows from a table (n is capped at max rows).
| Name | Required | Description | Default |
|---|---|---|---|
| n | No | ||
| name | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| sql | Yes | The statement as executed (after guard rewriting). |
| rows | Yes | Row values, JSON-safe (dates as ISO strings). |
| notes | No | |
| columns | Yes | |
| markdown | Yes | The same rows rendered as a Markdown table. |
| row_count | Yes | Number of rows returned (after truncation). |
| row_limit | Yes | Row cap that was applied, if any. |
| truncated | Yes | True if more rows existed than were returned. |
| elapsed_ms | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already convey read-only, idempotent, and non-destructive behavior. The description adds that the sample is reproducible and that n is capped at max rows, which are useful behavioral details beyond the annotations. No contradiction with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single sentence with no filler. Every keyword—reproducible, random sample, n, capped—adds meaningful information, and the most important detail is front-loaded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple two-parameter read-only tool with a safety annotation set and an output schema, the description is mostly sufficient. The main gaps are the undefined 'max rows' limit and the implicit mapping of 'name' to a table, but these are minor and unlikely to prevent correct invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With 0% schema description coverage, the description must explain the parameters. It clarifies that n is the row count and references a table, but it never explicitly maps the required 'name' parameter to the table name, leaving that to inference. This is only partial compensation.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific action (sample), a resource (rows from a table), and the key modifier 'reproducible random'. This clearly distinguishes it from sibling tools like query, profile_table, and describe_table, even without naming them.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The phrase 'random sample' implies when to use the tool, but there is no explicit guidance about when to choose sample_rows over query or profile_table, and no exclusions or alternatives are mentioned. Usage is inferred, not stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_columnsARead-onlyIdempotent
Find columns across all tables by case-insensitive substring or * / ? glob.
| Name | Required | Description | Default |
|---|---|---|---|
| pattern | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| matches | Yes | |
| pattern | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already communicate read-only, idempotent, non-destructive behavior. The description adds valuable behavioral detail beyond annotations: case-insensitive matching, glob support, and the cross-table scope. It does not contradict any annotation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single, well-structured sentence with no filler. The core action, scope, and matching semantics are all front-loaded and immediately actionable.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's low complexity (one parameter), rich annotations, and presence of an output schema, the description covers the essential invocation details. It lacks explicit guidance on when to choose this over sibling tools, but the simple search nature makes this a minor gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema has zero description coverage for the 'pattern' parameter, and the description fully compensates by defining how the pattern is interpreted: case-insensitive substring or * / ? glob. This is exactly the semantic information an agent needs to call the tool correctly.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Description states a specific verb ('Find'), a specific resource ('columns'), and a precise scope ('across all tables'), plus matching semantics ('case-insensitive substring or * / ? glob'). This clearly distinguishes it from siblings like list_tables or describe_table.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The intended use is implied: search for column names across any table using a pattern. However, there is no explicit statement of when to use this tool versus alternatives (e.g., describe_table for a single table's columns, or list_tables for tables), and no exclusion criteria.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
table_relationshipsARead-onlyIdempotent
Heuristic foreign-key candidates: *_id columns whose sampled values are contained in a matching column of another table.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| candidates | Yes | |
| sample_size | Yes | |
| min_containment | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already mark the tool as read-only, idempotent, and non-destructive. The description adds important behavioral context beyond annotations: results are heuristic and based on sampled values, meaning they are not guaranteed exhaustive or exact. This is valuable for setting agent expectations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single sentence that states the core concept, defines the heuristic mechanism, and includes the key caveat about sampling. Every part earns its place, and the main idea is front-loaded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
This is a no-parameter, read-only introspection tool with an output schema available. The description adequately explains what the tool computes and how, and nothing else is needed for an agent to invoke it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters and the schema is already fully descriptive by being empty, so the baseline of 4 applies. There are no parameter semantics for the description to clarify.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly defines the tool's output as heuristic foreign-key candidates and specifies the exact heuristic: *_id columns whose sampled values appear in a matching column of another table. It is not a tautology and is readily distinguishable from sibling tools like describe_table or search_columns, though it lacks an explicit verb.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no guidance on when to use this tool versus alternatives such as search_columns or describe_table. The description explains what the tool computes but does not state when an agent should prefer it or when a different tool would be more appropriate.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
8 tool updates
v0.1.0- First observed
describe_table - First observed
explain_query - First observed
list_tables - First observed
profile_table - First observed
query - First observed
sample_rows - First observed
search_columns - First observed
table_relationships
TDQS
Scored across 8 tools
Most tools have clearly distinct purposes: table listing, column search, profiling, sampling, relationships, and query execution. The main ambiguity is that `query` supports DESCRIBE, SUMMARIZE, and EXPLAIN, which overlaps semantically with `describe_table`, `profile_table`, and `explain_query`, though the specialized tools return richer structures.
Tool names generally follow a clear snake_case verb_noun pattern like list_tables, describe_table, profile_table, and explain_query. Two deviations exist: `query` has no object and `table_relationships` is noun_noun rather than verb_noun, but the overall naming style remains coherent.
Eight tools is well within the ideal range for a read-only DuckDB analyst server. Each tool covers a meaningful part of the analytical workflow without unnecessary redundancy or bloat.
The tool surface covers the core read-only analysis lifecycle: discovering tables, exploring schemas, profiling columns, sampling rows, searching columns, understanding relationships, and running arbitrary queries. There are no obvious dead ends for a typical data-analysis workflow.
Maintenance
Related MCP Connectors
Query, join, profile, clean and convert CSV/JSON/Parquet with server-side DuckDB over MCP.
Query your warehouse or a CSV with Claude/ChatGPT over MCP, governed by table-level ACL + audit.
Query your org's data in natural language — read-only MCP access to SQL, NoSQL, files & warehouses.
Query 40 databases from Claude, ChatGPT, or Cursor — on any device. Read-only, encrypted, audited.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables natural language interaction with local SQLite databases through Claude Desktop, translating plain English queries into SQL for data analysis and exploration.3MIT
- AlicenseNot gradedqualityDmaintenanceEnables executing SQL queries on DuckDB databases locally or on MotherDuck cloud, with support for multiple databases, read-only mode, and Claude Desktop integration.MIT
- FlicenseAqualityBmaintenanceEnables querying JSON/JSONL social datasets in Claude Desktop via MCP tools, using DuckDB as the backend database.8-
- AlicenseBqualityCmaintenanceEnables SQL querying over CSV and Excel files using DuckDB, providing tools to load files, inspect schemas, and run read-only queries via MCP.5MIT