duckdb-mcp
The server is a DuckDB-powered MCP tool for exploring, querying, and analyzing data files without importing them, supporting a wide range of formats and providing helpers for data quality checks. It supports local files, globs, compressed files, and remote data via HTTPS and S3.
query(sql, max_rows): Run read-only DuckDB SQL (SELECT, WITH, DESCRIBE, SUMMARIZE, SHOW, EXPLAIN) – write operations are rejected.describe_file(path, include_row_count): Get schema, column names, types, and optional row count for any supported file, glob, URL, or S3 URI.preview_file(path, rows): Preview the first N rows of a data file.list_files(path, pattern, recursive, data_files_only): List files in a directory or S3 prefix, with optional filtering to data formats.profile_columns(path, columns, top_k): Profile columns: null counts, approximate distinct counts, min/max, most-frequent values.Additional tools:
inspect_raw,compare_schemas,check_join,check_coverage,parquet_metadata,find_value,sample_rowsfor deeper data quality and inspection.Configuration: Adjustable row limits, query timeouts, result size caps, and memory ceilings.
Enables read-only SQL querying of data files (CSV, Parquet, JSON, Excel, etc.) via DuckDB, with tools for file preview, schema inspection, and column profiling.
Click on "Install 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., "@duckdb-mcpaggregate total sales by region from data/sales_*.parquet"
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.
duckdb-mcp
An MCP server that lets an AI assistant explore and query data files with
DuckDB — CSV, Parquet, JSON/NDJSON, Excel, compressed
variants, globs, https:// URLs and s3:// buckets.
Any statement DuckDB accepts runs, reads and writes alike — CREATE,
INSERT, COPY … TO, ATTACH and SET included. The connection is one
in-memory database that lives as long as the server, so tables and views
created in one call are still there in the next; nothing touches disk unless a
statement says so.
The server is therefore exactly as powerful as the user account running it. It
can read anything that account can read — SELECT * FROM read_text('~/.aws/credentials') is a legitimate query — write wherever it can
write, and fetch any URL. Run it as a user whose access you are happy to hand
over, and treat the files it reads as untrusted input to whatever model is
driving it: a CSV whose contents suggest running COPY … TO is now a CSV the
model can act on.
Install
The only thing to install is uv. Everything else is fetched and cached automatically.
winget install --id=astral-sh.uv -e # Windowscurl -LsSf https://astral.sh/uv/install.sh | sh # macOS / LinuxThere is no clone and no virtualenv to manage: uvx re-resolves the repository
each time it starts the server, so a new session already runs the current
main — there is no separate update step.
The same re-resolution means the server will not start without a network. A
warm cache does not save you, and neither does pinning the URL to a commit:
uvx contacts GitHub before it runs anything, and exits with a git error
instead of starting the server. If you need it to work offline, do a
development checkout — that runs from a local environment with
nothing to fetch — and point your MCP config at .venv/Scripts/duckdb-mcp
(.venv/bin/duckdb-mcp on macOS/Linux) instead of uvx.
Claude Code
claude mcp add -s user duckdb -- uvx --from git+https://github.com/lab1702/duck-mcp duckdb-mcp-s user registers the server for every project. Without it, claude mcp add
defaults to local scope and the server is only available in the directory you
ran the command in — it will not appear under /mcp anywhere else. Restart
Claude Code afterwards; /mcp reads the config at startup.
Claude Desktop / other MCP clients
Add to your client's MCP config (claude_desktop_config.json for Claude
Desktop):
{
"mcpServers": {
"duckdb": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/lab1702/duck-mcp",
"duckdb-mcp"
]
}
}
}Related MCP server: motherduck-mcp
Tools
Finding and reading data:
Tool | What it does |
| Run any DuckDB SQL statement, returns a markdown table. Defaults to the server's row cap (500). |
| List files in a directory or |
| Column names, types and row count for a file or glob. |
| First N rows, so the model sees real values. |
| A random sample of rows, for files whose head is not representative. |
| Null counts, approximate distinct counts, min/max and most-frequent values. |
| Which columns — and which files — contain a value. |
Checking that an answer is the right one. Most of what goes wrong with a data file is not an error — it is a plausible number that happens to be wrong. These four look for that:
Tool | What it does |
| Raw lines of a text file, before parsing, plus what the CSV sniffer detected. |
| Compare schemas across a glob and say what a plain read does about the differences. Reads up to 100 files. |
| What a join will do before you run it: fan-out, match rates, orphans. |
| Missing and repeated values in a column that should run in regular steps. |
| Parquet layout from the footer: sizes, compression, row-group pruning — no scan. |
Files are referenced by path directly in SQL — there is no import or registration step:
SELECT region, sum(amount) AS total
FROM 'data/sales_*.parquet'
WHERE order_date >= DATE '2026-01-01'
GROUP BY 1 ORDER BY total DESCJoining across formats works the same way:
SELECT c.name, sum(s.amount)
FROM 'data/sales.parquet' s
JOIN 'data/customers.csv' c ON c.id = s.customer_id
GROUP BY 1Writing
Statements that write are ordinary query calls. An expensive intermediate is
worth keeping, since the database outlives the call:
CREATE TABLE monthly AS
SELECT date_trunc('month', order_date) AS month, region, sum(amount) AS total
FROM 'data/sales_*.parquet' GROUP BY 1, 2Later calls query monthly directly, without rescanning the parquet. It lives
in memory and goes away with the server; COPY monthly TO 'monthly.parquet'
puts it on disk, and ATTACH 'warehouse.db' gives you somewhere durable to
create tables in the first place.
A statement that returns no rows reports itself rather than an empty table
(CREATE statement completed.), and one that changes rows returns DuckDB's own
count of them.
Sampling instead of the head
preview_file returns the first rows. For a file written in time or partition
order that is systematically unrepresentative — one date, one region, and often
the oldest and most schema-drifted records in the set. sample_rows draws a
uniform random sample instead:
sample_rows('data/events_2026.parquet', rows=20, seed=42)That costs a full scan, which the head does not, so it is the right tool for
understanding a file rather than for glancing at one. Passing seed fixes the
draw, so a follow-up call revisits the same rows.
When the parse looks wrong
Every other tool reads through DuckDB's CSV/JSON parser, so if auto-detection misreads a file there is nothing to check its answer against — the result is plausible-looking but wrong data, with no error. DuckDB's sniffer is good, but a report footer is enough to defeat it:
id,name,amount
1,alice,5
2,bob,6
-- end of report --describe_file reports one VARCHAR column named id,name,amount, three
rows, no error. inspect_raw shows the lines next to what the sniffer concluded:
1 | id,name,amount
2 | 1,alice,5
3 | 2,bob,6
4 | -- end of report --DuckDB's CSV sniffer reads this as: delimiter
',', skip 0 row(s), header row yes, 3 column(s).
Three columns against the one describe_file produced — the disagreement is
the diagnosis. inspect_raw also prints the sniffer's own read_csv call,
which here is already the fix, and can be pasted straight into query:
FROM read_csv('report.csv', auto_detect=false, delim=',', header=true,
columns={'id': 'BIGINT', 'name': 'VARCHAR', 'amount': 'BIGINT'},
ignore_errors=true); -- returns the 2 real rowsinspect_raw reads only as many lines as you ask for, so it is safe on a
multi-gigabyte file, and it tolerates input a real parse rejects — mixed line
endings, unterminated quotes, ragged rows. Tabs, carriage returns and other
invisible characters are escaped so they can be seen. It is text-only; parquet
and Excel are rejected with a pointer to describe_file.
Parquet layout
Everything else here reads data. parquet_metadata reads the footer, so it
answers layout questions on a file far too large to profile — and answers
"how many rows?" for free, since parquet records that in metadata:
**events/*.parquet** — 12 parquet file(s), 4,500,000 rows, 45 row group(s), 210.5MB on disk
| column | type | codec | compressed | ratio | nulls | row_group_order |
| ------ | ---- | ----- | ---------- | ----- | ----- | --------------- |
| id | INT64 | SNAPPY | 1.1MB | 2.0x | 0 | ascending |
| ts | INT64 | SNAPPY | 1.7MB | 1.3x | 0 | ascending |
| label | BYTE_ARRAY | SNAPPY | 1.4MB | 3.4x | 0 | ascending |
| bucket | INT64 | SNAPPY | 6.7KB | 16.7x | 0 | scattered (9/9) |
| notes | BYTE_ARRAY | SNAPPY | 290B | 0.9x | 300,000 | no stats |row_group_order is the useful part. A parquet reader skips a row group when
its recorded min/max cannot match the filter, so a range filter is cheap on a
column whose row-group ranges climb through the file (ascending) and reads
everything on one whose ranges overlap (scattered). Above, filtering on ts
prunes; filtering on bucket — which cycles through the same seven values in
every row group — cannot. no stats means the column records no min/max at
all, so nothing can be inferred either way.
Comparisons are numeric when both bounds parse as numbers and lexicographic otherwise, which is what parquet itself does for strings and matches the ISO-8601 text DuckDB gives timestamps.
The tool also flags layouts that make scans cost more than the data warrants — undersized row groups, or a glob of many small files:
Note: row groups average only 1,000 rows. Small row groups add per-group
overhead and give the reader less to parallelise over; 128MB-ish groups are
the usual target.
Note: 4 files averaging 4.3KB each. Many small files cost one open per file,
which dominates on object storage.row_groups=True adds a per-row-group table of row counts and sizes, for
finding uneven splits. Actual value ranges are not reported here — footer
statistics are approximate by design; use profile_columns for real min/max.
Schema drift across a glob
Reading data/*.parquet when the files disagree is not reliably an error.
DuckDB takes its schema from the first file and reconciles the rest against
it, so two of the four possible outcomes are silent and wrong:
difference | what a plain read does |
a later file adds a column | drops it — no error, the column simply is not there |
a later file widens a type | narrows the values — |
a later file drops a column | read fails |
the types cannot reconcile | read fails |
compare_schemas reports which of those you are in:
**data/*.parquet** — 3 files, 2 distinct schemas across 3 compared
A plain read takes its schema from the first file, `a.parquet`: id INTEGER, amount INTEGER
| column | files | types | a plain read |
| ------ | ----- | ----------------------- | ----------------------------------------- |
| id | 3/3 | INTEGER | ok |
| amount | 3/3 | INTEGER (2), DOUBLE (1) | narrows DOUBLE to INTEGER — values are lost |
| extra | 1/3 | VARCHAR | drops it — absent from a.parquet |Whether a difference is harmful depends on direction, so the tool asks DuckDB
rather than guessing: the common supertype of the two types is the one that
loses nothing, so a first file whose type is not that supertype must be
narrowing. DOUBLE first and INTEGER later is fine and reported as such;
INTEGER first and DOUBLE later loses your decimals.
The fix in every case is to reconcile by name, which the tool prints with the reader matching your format:
SELECT * FROM read_parquet('data/*.parquet', union_by_name=true)Schemas are read one file at a time, so cost is linear in the file count.
Above max_files (default 100) it compares a spread across the glob rather
than the first N — drift tends to track write order, so the first N files
would be the oldest and would miss exactly the recent change worth finding.
The first and last file are always included. Files that cannot be read at all
are reported rather than skipped silently.
Checking a join before trusting it
If the right side of a join holds more than one row per key, the join duplicates left rows and every aggregate over the result is inflated. There is no error, and the total still looks plausible:
sum(amount) alone = 10000.0
sum(amount) after join = 11000.0 <- 20 duplicate customer idscheck_join reports that from grouped key counts, without materialising the
join:
**orders.parquet** ⋈ **customers.parquet** on customer_id = id
| side | rows | distinct keys | max rows per key | matched | unmatched | null keys |
| ----- | ----- | ------------- | ---------------- | -------------- | --------- | --------- |
| left | 1,000 | 200 | 5 | 1,000 (100.0%) | 0 | 0 |
| right | 220 | 200 | 2 | 220 (100.0%) | 0 | 0 |
Relationship: many-to-many — up to 5 left rows and 2 right rows per key.
The join yields 1,100 rows from 1,000 on the left.Direction is what matters: five orders per customer is normal, two customer
rows per id is the bug. So many-to-one is reported as a safe lookup while
many-to-many gets a warning. The predicted row count is exact — a test
asserts it against the join it declined to run.
It also reports rows that match nothing, so you can see an inner join dropping half your data, and diagnoses a join returning nothing at all: keys that are entirely NULL, or key columns whose types cannot be compared.
Gaps in a series
A daily table missing three days still sums and averages perfectly happily — the total is just quietly short. One where a day was loaded twice sums too high. Neither shows up as an error, and neither changes anything a row count would reveal:
count(*) = 29, sum = 290 <- looks entirely finecheck_coverage infers the step from the data and reports what is absent:
29 rows, 28 distinct values from 2026-01-01 to 2026-01-31.
Step looks like 1 day (25 of 27 intervals), so 31 values were expected.
3 missing (9.7% of the expected range) across 2 gaps.
| after | before | missing |
| ---------- | ---------- | ------- |
| 2026-01-08 | 2026-01-11 | 2 |
| 2026-01-24 | 2026-01-26 | 1 |
1 repeated value. A sum over this column double-counts them.
| value | rows |
| ---------- | ---- |
| 2026-01-05 | 2 |The step is inferred rather than assumed, so the same tool covers dates, timestamps and plain integer sequences. Calendar steps are not constant — a month is 28 to 31 days — so an interval counts as a gap only once it is half again the usual step, which finds a missing month without reporting February as a hole every year.
For timestamps carrying a time of day that really describe a daily series,
pass granularity='day' (or hour, month, …) to bucket them first.
Finding where a value lives
find_value('data/*.parquet', 'NEEDLE')Searches every column as text — numbers, dates and nested values included —
and reports which columns match, with an example, plus which files they are
in. Useful for locating a join key in a dataset whose schema you do not know
yet, which is exactly when you cannot write the query yourself. % and _ in
the search value are matched literally rather than as wildcards.
Remote files
https:// and s3:// paths work through DuckDB's httpfs extension, which is
installed on first start. S3 authentication uses DuckDB's credential chain, so
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, ~/.aws/credentials, AWS_PROFILE
and instance roles are all picked up automatically. Public buckets and URLs
need no configuration.
Credentials are resolved once, at startup — if you add them afterwards, restart
the server. On a machine with no AWS credentials at all, that step is skipped
(DuckDB validates the chain when the secret is created) and public buckets still
work. Likewise, if the machine is offline at startup, extension setup is skipped
and local files still work. The server writes a line to stderr on startup
reporting which of httpfs, excel and the S3 credential chain came up.
Because all of that happens at startup, a read that fails later says nothing about the real cause. So when something set up at startup is missing, the error carries the reason with it:
HTTP Error: HTTP GET error reading 'https://bucket.s3.amazonaws.com/data.parquet'
in region '' (HTTP 403 Forbidden) AccessDenied: Access Denied
No AWS credentials were resolved when the server started (unavailable: Secret
Validation Failure: ... Credential Chain: 'config'), so this bucket is being read
anonymously — which is what a 403 here usually means. DuckDB resolves credentials
once at startup, so setting them now requires a restart.The same applies to an https:// or s3:// path when httpfs failed to
install, and to .xlsx when excel did. The hint is added only when the
capability is actually missing and the failure is the kind it would explain —
a 404 on S3 is a missing object, not a credentials problem, and gets no hint.
Limits
Defaults, all overridable:
Setting | Default | Env var | Flag |
Rows per result | 500 |
|
|
Query timeout | 120s |
|
|
Result text size | 200 KB |
|
|
Memory ceiling | DuckDB's own |
|
|
Truncated results say so explicitly, e.g. (showing first 500 rows (more available)), including when a schema or profile table is cut short by the
size cap. The timeout covers a whole tool call, not each statement in it, so
a tool that runs several queries still finishes within it; a call that
overruns is cancelled and reported.
No row cap goes above 10,000, however it is set. A timeout of 0 means no
time limit.
The memory ceiling is left to DuckDB by default, which sizes it against system
memory — the right answer for the single instance this server runs. Setting it
is for the case DuckDB cannot see: MCP starts one server process per client, so
several concurrent sessions mean several DuckDB instances on one machine, each
holding its own independent ceiling. --memory-limit takes a size with a unit
(4GB, 512MB); percentages are not accepted.
Unusable values are not silently accepted: a bad flag is a startup error, and a bad environment variable is ignored with a line on stderr.
{
"mcpServers": {
"duckdb": {
"command": "uvx",
"args": ["--from", "git+https://github.com/lab1702/duck-mcp", "duckdb-mcp",
"--max-rows", "1000", "--timeout", "300"]
}
}
}Development
git clone https://github.com/lab1702/duck-mcp
cd duck-mcp
uv sync --extra dev --upgrade
uv run pytestThere is no committed uv.lock. Dependencies are declared with lower bounds
only, so a fresh install — and uvx — resolves to the current duckdb and
mcp releases. --upgrade re-resolves an existing checkout to the latest
versions; run the tests after, since tracking upstream means meeting its
breaking changes early rather than at a pinned upgrade later.
Or without uv:
python -m venv .venv
.venv/Scripts/python -m pip install -e ".[dev]" # .venv/bin/python on macOS/Linux
.venv/Scripts/python -m pytestLicense
MIT
Available Tools
5 toolsdescribe_fileA
Return the column names and types of a data file, plus its row count.
Works for any path DuckDB can read -- csv, parquet, json/ndjson, xlsx, compressed variants, globs matching many files, http(s) URLs and s3 URIs.
Args: path: File path, glob or URL, e.g. 'data/sales_*.parquet'. include_row_count: Set False to skip counting rows on very large inputs.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| include_row_count | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the behavioral transparency burden. It discloses the row count behavior (can be skipped for large inputs) and the supported path types. It does not explicitly state side effects, but as a read-only describe operation, the behavior is sufficiently clear.
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 compact and well-structured: a clear opening statement, a brief list of supported formats, and an Args section. No unnecessary words or redundancy, and every sentence serves a purpose.
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 only has two parameters (one required) and an output schema exists, the description is complete. It covers the purpose, supported input types, parameter semantics, and the optional row count behavior. The output schema covers return values, so no further detail is needed.
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 Args section explicitly describes both parameters: path with an example ('data/sales_*.parquet') and include_row_count with its purpose. This fully compensates for the 0% schema description coverage and adds valuable meaning beyond the bare schema.
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 states the tool's function: 'Return the column names and types of a data file, plus its row count.' This uses a specific verb ('return') and resource ('data file') and distinguishes it from siblings like query, preview_file, and profile_columns, which have different purposes.
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 provides clear context on applicable inputs: 'Works for any path DuckDB can read -- csv, parquet, json/ndjson, xlsx, compressed variants, globs matching many files, http(s) URLs and s3 URIs.' It also explains the include_row_count option for large inputs. However, it does not explicitly mention when to use this tool instead of siblings like preview_file or profile_columns.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_filesA
List data files in a directory (local, or an s3:// prefix).
Args: path: Directory or bucket prefix to list. pattern: Glob pattern for the file name, e.g. '*.parquet'. recursive: Descend into subdirectories. data_files_only: Keep only extensions DuckDB can read; set False to see everything.
| Name | Required | Description | Default |
|---|---|---|---|
| path | No | . | |
| pattern | No | * | |
| recursive | No | ||
| data_files_only | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the burden of behavioral disclosure. It explains the filtering behavior (data_files_only limiting to DuckDB-readable extensions) and the recursive option, adding meaningful context beyond the bare 'list files'. However, it does not disclose edge cases like error handling or hidden files, preventing a perfect score.
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 purpose sentence followed by a concise bulleted list of arguments. Every sentence is informative, the structure is clear and front-loaded, and there is no unnecessary verbosity.
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 an output schema exists, the description doesn't need to explain return values. It covers the tool's purpose, parameters, and filtering behavior comprehensively for a simple listing tool. It could be slightly more explicit about how this fits into the overall workflow, but it is largely 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 input schema has no parameter descriptions (0% coverage), so the description fully compensates by explaining each parameter's meaning and providing an example glob pattern. This adds significant value beyond the schema's type/default information.
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 states the tool lists data files in a local directory or S3 prefix, using the specific verb 'list' and resource 'data files', which immediately distinguishes it from siblings like query, describe_file, preview_file, and profile_columns.
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 purpose is evident, but there is no explicit guidance on when to use this tool versus alternatives like describe_file or preview_file. Usage is implied from the description ('list data files'), but no exclusions or alternative scenarios are mentioned.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
preview_fileA
Show the first rows of a data file so you can see real values.
Args: path: File path, glob or URL. rows: How many rows to show (default 20).
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| rows | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the burden of disclosing behavior. It states the tool shows the first rows and supports glob/URL paths, which implies read-only behavior. However, it does not mention error handling, file size limits, or output format details. It is adequate but minimal.
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 exceptionally concise with two sentences, front-loaded with the primary purpose. The parameter list is minimal and each element adds value, with no redundant or extraneous 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?
Since an output schema exists, return values are already covered. The description successfully conveys the core purpose and parameters, and the tool is simple. However, it lacks explicit guidance on when to prefer this over sibling tools (e.g., describe_file, profile_columns), leaving a small gap in contextual guidance.
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 property descriptions are 0% covered, and the description fully compensates: 'path' is explained as 'File path, glob or URL' and 'rows' as 'How many rows to show (default 20)'. This adds meaningful semantics beyond the bare types and default value in the schema.
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 uses the specific verb 'show' with the resource 'data file' and scope 'first rows', clearly distinguishing it from siblings like describe_file (schema/metadata) and profile_columns (statistics). The phrase 'see real values' reinforces the data-inspection purpose.
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 'so you can see real values' implies a clear use case for inspecting actual data, and the tool's role is evident from its name and siblings. However, it does not explicitly state when not to use it or name alternatives, so it earns a 4 rather than a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
profile_columnsA
Profile a file's columns: null counts, approximate distinct counts, min/max and the most frequent values of low-cardinality columns.
This scans the whole file, so prefer describe_file when you only need types.
Args: path: File path, glob or URL. columns: Restrict to these columns (default: all). top_k: Number of most-frequent values to show; 0 to skip that pass.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| top_k | No | ||
| columns | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden. It discloses that the tool scans the whole file (cost), that distinct counts are approximate, and that most-frequent values are only for low-cardinality columns. These are useful behavioral traits. It does not discuss error conditions or permissions, but these are less critical for a read-only profiling tool with an output schema.
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 sentences plus a clear bulleted Args list. It front-loads the purpose, gives one usage guideline, and then details parameters. Every sentence adds value with no redundancy or fluff.
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 has an output schema, return values are covered. The description covers purpose, usage, parameters, and performance (whole-file scan). It is slightly incomplete in not addressing error handling or clarifying the exact output structure, but the output schema mitigates this. Overall it is sufficiently complete for a profiling tool.
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 0% description coverage, but the description's Args section provides meaning for all three parameters: path (file path/glob/URL), columns (restriction, default all), and top_k (number of values, 0 to skip). This fully compensates for the schema's lack of descriptions.
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 'Profile a file's columns' which is a specific verb and resource, and lists concrete outputs (null counts, approximate distinct counts, min/max, most frequent values). It also differentiates from the sibling tool describe_file by explicitly saying 'prefer describe_file when you only need types.'
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 provides clear when-to-use guidance: it states this tool 'scans the whole file' and directs users to 'prefer describe_file when you only need types.' This gives an explicit alternative and context for when this tool is appropriate.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
queryA
Run a read-only DuckDB SQL statement and return the rows as a markdown table.
Only a single SELECT (including WITH/DESCRIBE/SUMMARIZE/SHOW) or EXPLAIN
statement is accepted; anything that writes data, files or settings is
rejected. Read files by quoting their path in the FROM clause, e.g.
SELECT region, sum(amount) FROM 'data/sales.parquet' GROUP BY 1.
Args: sql: The statement to run. max_rows: Row cap for this call (defaults to the server's limit).
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | ||
| max_rows | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries full behavioral disclosure. It states the read-only nature, accepted and rejected statement types, the markdown output format, and the max_rows default. This goes beyond minimal requirements and helps the agent understand important constraints and return format.
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 compact and front-loaded with the purpose, followed by necessary constraints and an example. It includes an Args section that cleanly maps to parameters without unnecessary verbosity.
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 two-parameter tool with no annotations, the description covers purpose, accepted/rejected inputs, output format, and parameter semantics. It also clarifies the read-only nature and file-reading capability, making it self-sufficient for an agent to use 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?
Schema description coverage is 0%, so the description must compensate. It explains sql as 'the statement to run' and max_rows as a 'row cap' with a default to the server's limit, plus includes an example for reading files. This adds significant meaning beyond the raw schema.
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 states the tool runs a read-only DuckDB SQL statement and returns rows as a markdown table. It specifies the verb 'Run' and resource 'DuckDB SQL statement', and distinguishes itself from file-oriented siblings (describe_file, preview_file, etc.) by focusing on arbitrary SQL execution.
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?
It provides clear context: accepted statement types (SELECT, WITH, DESCRIBE, SUMMARIZE, SHOW, EXPLAIN) and explicitly rejects writes. However, it does not mention when to prefer this tool over sibling tools or provide exclusions beyond statement type, so it lacks explicit alternative guidance.
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. Dates show when Glama detected each change.
5 tool updates
v0.1.0- First observed
describe_file - First observed
list_files - First observed
preview_file - First observed
profile_columns - First observed
query
TDQS
Each tool has a distinct, non-overlapping purpose: query executes SQL, describe_file returns schema, preview_file shows row samples, list_files enumerates files, and profile_columns computes statistics. No two tools could be confused for the same task.
All tool names are lowercase snake_case and follow a clear verb-first pattern (query, describe_file, preview_file, list_files, profile_columns). The naming is uniform and predictable, aiding agent selection.
Five tools is a well-scoped set for a read-only DuckDB MCP server. Each tool covers a necessary aspect of data exploration without bloat or redundancy.
The tool set provides a complete read-only data exploration workflow: discover files, inspect schemas, preview data, profile columns, and run arbitrary SQL queries. There are no obvious gaps or dead ends for its stated purpose.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Query, join, profile, clean and convert CSV/JSON/Parquet with server-side DuckDB over MCP.
Explore, query, and inspect SQLite databases with ease. List tables, preview results, and view det…
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
- AlicenseBqualityDmaintenanceEnables AI assistants and IDEs to execute SQL queries on local DuckDB databases, in-memory databases, or cloud-stored databases with support for flexible connections and configurable result limits.11MIT
- 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
- AlicenseAqualityCmaintenanceEnables DuckDB database interaction through MCP, supporting SQL queries, table creation, and schema inspection with optional read-only mode.1MIT
- AlicenseNot gradedqualityAmaintenanceQuery local CSV, Parquet, JSON and TSV files with real SQL via DuckDB. Gives your AI coding tool ground-truth data access instead of hallucinated answers.4MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/lab1702/duck-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server