geoparquet-mcp
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., "@geoparquet-mcpHow many restaurants are in Paris, France?"
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.
geoparquet-mcp
An MCP server that answers spatial questions about multi-gigabyte GeoParquet files sitting on public object storage, without downloading or importing them first.
The problem
Cloud-native geospatial data has largely settled on Parquet on object storage. The tools that query
it have not: most of them still want a database. So the path from "there is a 10 GB places file in
a bucket" to "how many restaurants are in this arrondissement" runs through an import — provision
PostGIS, load 73 million rows, index them, keep the copy in step with the next monthly release.
For a person that import is a chore. For an agent it is a wall: it cannot provision a database mid-conversation, so it falls back on downloading the file and filtering it locally — moving ten gigabytes to answer a question whose answer is four kilobytes.
DuckDB already reads remote Parquet over HTTP range requests and pushes filters down into the file. This project is that capability wrapped in the protocol an agent already speaks, with the perimeter of what it may read resolved once, at startup, by the application rather than by the tool.
Related MCP server: duckdb-iceberg-mcp
Demo
./scripts/demo.shOne command from a fresh clone. The script prepares its own environment into .venv/ — uv
when it is on PATH, otherwise python3 -m venv plus pip on a local Python 3.12+ — so there is
nothing to install first. Expect about a minute and ~200 MB of traffic, most of it the deliberately
unoptimised comparison in section 5.
Real output, against Overture Maps release 2026-08-19.0:
geoparquet-mcp demo — spatial analysis on a remote file, no import step
1. The dataset, described from its footers
────────────────────────────────────────────────────────────────────────
source Overture Maps — places
licence CDLA-Permissive-2.0 (data); ODbL applies to OpenStreetMap-derived records
release 2026-08-19.0
location https://overturemaps-us-west-2.s3.us-west-2.amazonaws.com/release/2026-08-19.0/theme=places/type=place/
size 73,631,092 rows, 16 files, 10.48 GB, 4,096 row groups
read to learn all of that: 26.5 MB
2. What is in Paris, France?
────────────────────────────────────────────────────────────────────────
5,045 french_restaurant
5,019 (uncategorised)
4,869 professional_services
4,216 community_services_non_profits
3,336 restaurant
3,306 parking
3,192 hotel
2,700 grocery_store
166,966 features, 1,022 distinct categories.primary values
read: 4.0 MB in 6,674 ms
3. Bakeries within 400 m of Notre-Dame
────────────────────────────────────────────────────────────────────────
152 m A. Lacroix Pâtissier
197 m Aux Fontaines de Chocolat
205 m Hure, Createur de Plaisir
232 m Boulangerie Pâtisserie Uré Île De La Cité
254 m Cookies By Moon's
read: 1.1 MB in 2,874 ms
4. Where is it densest? H3 cells, computed remotely
────────────────────────────────────────────────────────────────────────
3,906 881fb475b5fffff (48.8710, 2.3024)
3,518 881fb4662dfffff (48.8622, 2.3477)
3,207 881fb46667fffff (48.8701, 2.3440)
3,050 881fb46665fffff (48.8723, 2.3327)
2,660 881fb46629fffff (48.8679, 2.3553)
5 cells at resolution 8
read: nothing — already in the session cache (594 ms)
5. The point of all this: bytes moved
────────────────────────────────────────────────────────────────────────
matching features 166,966
whole dataset, if downloaded 10.48 GB (16 files)
one part file, if downloaded 667.9 MB
same query, pushdown OFF 128.8 MB in 25,033 ms
same query, pushdown ON 6.9 MB in 4,626 ms
18.8× fewer bytes than the same query without pushdown
97.5× fewer bytes than downloading that one file
1,530× fewer bytes than downloading the dataset
attribution: © Overture Maps Foundation — CDLA-Permissive-2.0 (data); ODbL applies to OpenStreetMap-derived recordsThe only thing that landed on disk is the project's own .venv/. There is no database, no import
step and no copy of the data: the 10.48 GB stayed in us-west-2, and the five sections above moved
about 200 MB of it, nearly all in the last one.
Running the server
The demo is a command, not a server: it answers five questions and exits. Connecting an agent to the same engine is a different script, with the same bootstrap — nothing to install first.
./scripts/serve.sh # MCP over stdio, the transport a desktop client launches
./scripts/serve.sh http # MCP at POST /mcp plus the REST routes, on :8000
./scripts/serve.sh config # the claude_desktop_config.json block for this clone
./scripts/serve.sh helpstdio sits silent, waiting for JSON-RPC on stdin; that is what a working server looks like, and
Ctrl+C ends it. Bootstrap messages go to stderr precisely so stdout stays a clean protocol channel.
For Claude Desktop you never run it yourself — the app launches the server as a subprocess. The
awkward part is that the configuration has to name the executable by absolute path, because the app
does not read your shell profile, so ./scripts/serve.sh config prints the block with that path
already filled in:
{
"mcpServers": {
"geoparquet": {
"command": "/absolute/path/to/geoparquet-mcp/.venv/bin/geoparquet-mcp-server",
"args": ["--transport", "stdio"]
}
}
}Paste it into claude_desktop_config.json, restart, and the eight tools appear under the connector.
docs/claude-desktop.md has the rest: the environment variables that narrow
what the process may read, what the first query costs, and what to check when the connector does not
appear.
The script is a wrapper over the entry points, which are what you would run in a deployment:
uv run geoparquet-mcp-server --transport stdio # or: geoparquet-mcp serve
uv run uvicorn geoparquet_mcp.app:create_app --factory --port 8000Putting the HTTP one somewhere an agent can reach it is a container and one gcloud command:
./scripts/deploy-gcp.sh deploy --dry-run # read the command before running it
./scripts/deploy-gcp.sh deploy # Cloud Build, then Cloud Rundocs/deployment-gcp.md is the rest of it: why the DuckDB extensions
belong in the image rather than in a cold start, why scaling to zero costs ten seconds and keeping an
instance warm costs hundreds of dollars, what the distance from Belgium to us-west-2 does to a query
that is mostly round trips — and the 421 Misdirected Request that answers every MCP call until the
service is told its own hostname, while /health keeps returning 200.
The measurement
Section 5 above is the whole argument, and it has its own command:
uv run geoparquet-mcp benchmark --only pushdownIt runs the same aggregate twice against the same remote Parquet part — once normally, once with
DuckDB's filter_pushdown optimiser disabled — each on its own cold DuckDB session so neither warms
the other:
scan target s3://overturemaps-us-west-2/release/2026-08-19.0/theme=places/type=place/part-00007-…-c000.zstd.parquet
matching features 166,966
whole dataset, if downloaded 10,480,684,059 bytes (16 files)
one part file, if downloaded 667,940,013 bytes
that part's Parquet footer 1,644,542 bytes (read by both runs)
same query, pushdown OFF 128,806,432 bytes in 24,989 ms
same query, pushdown ON 6,850,570 bytes in 4,349 ms
18.8× fewer bytes than the same query without pushdown6,850,570 bytes against 128,806,432 — 18.8×. The filter went down into the remote file. The dataset was not fetched and then filtered; the row groups that could not match were never requested, because their footer statistics ruled them out before a single data page was asked for.
Two things about that number are worth stating before a careful reader finds them.
The ~26 MB floor is footers, not data. Every first query in a fresh process pays about 26.5 MB before it reads anything useful: the Parquet footers of all 16 parts, carrying 4,096 row groups of statistics. That looks like a terrible fixed cost until you see what it buys — it is the pruning mechanism. DuckDB reads those statistics to decide which row groups to skip, and then fetches only the surviving column chunks. Per-operation, measured with:
uv run geoparquet-mcp benchmark --only operationsthat command prints a table with the two costs in separate columns. The +footer MB column is
26.49 MB on every row that reads places — the same 26.5 MB the demo reports in section 1 above. The
query MB column is what the operation itself costs once those footers are cached: 0.80 MB to 5.62 MB
across the ten operations. The bbox_query (GeoJSON) row — 50 features returned with true geometry —
is 2,972 ms and 4.33 MB, as a median over five runs, each starting from a fresh DuckDB session.
Because the session is a process-wide singleton with its HTTP metadata cache on, that toll is paid
once and every later query is the small number. Run the command yourself and the byte columns should
match; the millisecond columns will not, because they are mostly your link to us-west-2.
The predicates are on the four bbox struct members, not on the geometry. Overture stores a
STRUCT(xmin, xmax, ymin, ymax) alongside each feature, and the filter is written as four
independent comparisons on those four columns:
bbox.xmin <= 2.47 AND bbox.xmax >= 2.20 AND bbox.ymin <= 48.91 AND bbox.ymax >= 48.80An ST_Intersects over the geometry column would be equally correct and would destroy the result.
Parquet keeps min/max statistics per column chunk, so a plain comparison on bbox.xmin is something
the reader can evaluate against a footer; a spatial function is an opaque call it must materialise
rows to run. No pruning, no argument. Where a caller passes an arbitrary WKT geometry, the envelope
prunes the read and the exact shape then filters the survivors — so the answer stays exact and the
read stays cheap.
Architecture decision: MCP as an ASGI sub-application
Context. The same operations need two audiences: agents over MCP, and ordinary HTTP clients over REST. One implementation, two protocols. The question is how the MCP endpoint gets into the process.
Options.
(a) MCP mounted as an ASGI sub-application inside the FastAPI app. One process, one DuckDB session and its warm footer cache, one lifespan. The cost is real: the two protocols cannot be scaled or restarted independently, and a mounted ASGI app gets no startup event from the parent router — its session manager has to be chained into the host's lifespan by hand, and forgetting to is a failure that shows up only when the first MCP request arrives.
(b) A separate MCP service proxying the REST API. The clean separation: each protocol scales on its own, and a crash in one does not take the other with it. It buys that with a second deployable to run and version, a network hop on every tool call, and two caches instead of one — the MCP process holds no DuckDB session, so the footer warmth that makes the second query fast lives entirely in the API tier and the proxy pays for its own serialisation on both legs. It is the right answer for a service with independent traffic profiles for the two protocols.
(c) A sidecar process. Keeps the two protocols isolated without a network round-trip to a separately deployed service, and lets the MCP side crash and restart alone. It needs an IPC channel, a serialisation format across it, and a supervisor that starts both and knows what to do when one dies. That is a small distributed system, and it inherits the failure modes of one.
Decision: (a). The constraint that settled it is deployment. This server has to run as a single
command on someone's laptop — Claude Desktop launches it as a subprocess over stdio, with no
orchestrator, no supervisor and no service mesh. A proxy means a second process the user has to start
and keep in step. A sidecar means IPC and a supervisor for a workload that fits in one process. A
sub-application means one process, and the wiring is one file: src/geoparquet_mcp/app.py.
docs/architecture-c4.md draws that decision, and the rest of the
structure, as a C4 model — context, containers, components, code.
That same file also has to attach the ASGI app twice — a Route at /mcp and a Mount for
anything below it — because Starlette compiles a mount to a pattern requiring a segment after the
prefix, so a bare POST /mcp would only ever be answered by a 307 to /mcp/. Streamable HTTP is a
single endpoint, not a tree; that redirect would be friction on every request a client makes.
Consequences, including the bad ones.
The two protocols share a lifecycle. Restarting to pick up a REST change restarts every MCP session with it. They cannot be scaled apart: if MCP traffic grows and REST does not, the only lever is more copies of both.
They share a DuckDB session, which is the point — the warm footer cache is what makes the second query fast — and also a shared failure domain. A query that exhausts memory takes down both façades.
MCP is switched off with
GEOPARQUET_ENABLE_MCP=0, and then the sub-application is never built and the route is never registered./mcpreturns FastAPI's own 404 because nothing is there, not because a handler decided to refuse. That is the cheap version of independent deployment: a REST-only process is one environment variable away. The reverse — MCP without REST — is the stdio entry point, which is a different process shape entirely.The chained lifespan is load-bearing and easy to break. It is covered by a test rather than a comment.
The tools implement nothing
The engine — src/geoparquet_mcp/engine/ — is the entire capability. It opens the DuckDB session,
resolves dataset names to paths, builds the SQL, runs it, and reports the bytes that crossed the
network. It does not import mcp. It can be used from a notebook and tested without a protocol, and
most of the test suite does exactly that.
The tool layer is handlers. Here is one, complete:
def describe_source(source: str = engine.DEFAULT_SOURCE) -> dict[str, Any]:
"""Return one dataset's schema, CRS, extent and physical footprint."""
return engine.dataset_schema(source=source, **dependencies.engine_kwargs())That is the whole function. All eight are this shape, and the shape is enforced: a test walks the AST
of every registered handler and fails if the body is anything other than a single return. Not as a
style rule — the day a handler grows a second statement, the reason is always that logic has drifted
out of the engine and into the protocol layer, where it can no longer be used or tested without MCP.
Sibling tests assert that no file under tools/ contains SQL or a DuckDB reference, and that only
server.py imports mcp.
Why it matters: the REST routes in app.py call the same engine functions with the same arguments.
Adding a third protocol would duplicate no logic — it would be another file of handlers next to the
two that exist. The 400-word tool descriptions that teach a model when not to call a tool live in
the handler modules, because they are protocol surface; the behaviour they describe does not.
Isolation
A DatasetScope is the only thing in the engine that turns a dataset name into a readable path,
and no operation accepts a path. It is resolved once, when the application starts, and injected into
handlers through dependencies.py. A handler has no argument through which a different scope could
arrive and no constructor to call, so the strongest thing it can do is narrow the perimeter for
itself. Widening is not refused at runtime — it is unreachable. Set GEOPARQUET_SOURCES=overture_places
and the other two datasets do not exist as far as any tool is concerned.
The general point, and the reason this is in the README rather than in a docstring: when an agent
calls a tool, the authorisation decision has to come down the same path it does for any other
request. A tool layer that resolves its own perimeter is a second authorisation implementation, and
a second one is one that will drift from the first. dependencies.py imports neither mcp nor
fastapi; it is the adapter between the two, and both protocols end up holding the identical object.
The one code path that handles a path rather than a name — the benchmark, which pins a single Parquet
part to keep its unpushed comparison affordable — goes through scope.assert_within(), which checks
it against prefixes the scope built itself. Eighteen tests in tests/test_scope_isolation.py try to
get past it: a sibling dataset under the same root, a parent-directory escape, an escape dressed up
as a deeper path, a prefix that merely starts the same, a non-Parquet object inside the perimeter, a
symlink planted inside it pointing out.
Tests
uv run pytest -m "not network" # 229 tests, 3 s — what CI runs
uv run pytest -m network # 19 tests against the live Overture dataset248 tests, split by a network marker, because the two halves fail for different reasons.
The default half is hermetic. It generates a small GeoParquet corpus on disk laid out exactly like
Overture's — same nested column names, same bbox struct, same directory shape — and runs the real
engine against it, asserting exact results. It needs no network, finishes in three seconds, and is
what runs on every push, on Python 3.12 and 3.13. ./scripts/make_fixtures.py writes the corpus out if
you want to look at it.
The network half reads Overture's public bucket. It is the only place the byte-level pushdown claim
can be measured, and it breaks whenever a release expires — a failure that says nothing about the
change under review. It runs on a weekly cron and on manual dispatch, never on a pull request.
The demo above is held in place by the same kind of test. It broke once — cli.py went on calling a
tool-layer function that had been renamed, through a layer that needs a perimeter the CLI never
installs — and nothing caught it, because nothing imported cli at all. tests/test_cli.py now
reads the CLI's syntax tree and fails if it names something the engine does not have, if it reaches
through the tool layer, or if the demo stops passing an explicit scope. It runs in 0.24 seconds and
would have caught both halves of that failure before the command ever touched the network.
A syntax tree has a blind spot, though, and geoparquet-mcp serve sat in it. The subcommand built
the server and ran it without installing a perimeter first, so it started, announced its eight tools,
and failed every call that followed. It named nothing that did not exist and imported nothing it
should not have: the bug was a line that was not there. So the same file now launches both stdio
entry points as subprocesses and reads a resource through each, over real pipes — the only way to
find out that a server serves. That costs about a second, and it is the second the rest of the suite
was missing.
Among the hermetic tests is a snapshot of the complete JSON of the MCP surface — all eight tools, one resource and two resource templates, with every description and input schema. It fails on any unintended change to what a model sees, which is the part of this project a refactor is most likely to alter silently. Alongside it: a test that every declared parameter is documented in its tool's description, and one that no tool requires an argument a model cannot guess.
What this is not
Not multi-tenant. One perimeter per process, resolved at startup. The machinery for a per-request scope exists (
dependencies.using()) and is used by tests, but nothing calls it in production and no request carries a tenant.Not writable. Every operation is a read. The SQL escape hatch refuses anything that is not exactly one
SELECT, then walks the parsed tree and refuses any table reference that is not a dataset in scope — so it is bounded by the same perimeter as every other tool, not by a keyword blocklist.Not authenticated. There is no auth layer at all. Do not put this on a public interface.
Not for sensitive data. It reads anonymous public buckets. There is no credential handling, no encryption story, and no audit log.
Tested against one dataset family. Overture Maps
places,divisionsandbuildings. The engine assumes a GeoParquet-conventionalbboxstruct with row-group statistics; a file without one will be read correctly and pruned not at all, which turns the central claim off without announcing it.A demonstration of architecture, not a product. Version 0.1.0, one author, no users. It exists to be read and to have its numbers re-run, not to be deployed.
Keeping the demo alive
Overture publishes a release roughly monthly and keeps only about two on the public bucket; objects
carry a 60-day retention rule. A hard-coded release path in a README is therefore a demo with a
two-month shelf life, and a 404 from a third-party bucket is a bad first impression to hand someone
who cloned your repository in good faith.
resolve_release() pins a release that was verified working (2026-08-19.0, on 2026-09-05 with
DuckDB 1.5.5) and falls back to listing the bucket and taking the newest when the pin is gone.
Discovery failures fall back to the pin rather than raising, because a stale pin produces a clearer
error later than a network error at startup. The weekly network CI job exists to notice the
rotation before a reader does.
What would come next
Iceberg. The obvious one, and the one that would change the shape of the argument rather than extend it. Today pruning is a property of how Overture happened to lay its files out: 16 parts, 4,096 row groups, and a bbox column whose statistics happen to be well-clustered geographically. An Iceberg table moves that decision into the table format — snapshots and hidden partitioning mean the planner prunes from the manifest before touching a data file at all, and the ~26 MB footer toll becomes a manifest read instead. What I have read but not built is how a spatial predicate pushes through the manifest: partition transforms are defined over scalar columns, and a bounding box is four of them plus a claim about their relationship. Whether that is expressible as partition statistics, or needs the geometry-type support that is still landing in the spec, is the part I would have to find out by doing it.
Persisting the footer statistics. 26.5 MB per cold process is fine for a long-lived server and
poor for a desktop client that starts, answers three questions and exits. The same toll is why
overture_buildings — 513 parts, ~277 GB — is a minute-scale query rather than an interactive one,
even though pushdown cuts a city query on it to well under a gigabyte. Caching the row-group
statistics to disk would make a cold start nearly free. The open question is invalidation: the cache
is keyed on a release path that expires, so the cache and resolve_release() have to agree about
what "current" means, and getting that wrong means silently querying statistics for files that are no
longer there.
Measuring how much of the win is Overture's file layout. The 18.8× is real and reproducible, but it is a measurement of this engine against this dataset. A Parquet file whose rows are in ingestion order rather than spatially clustered has row-group bboxes that all span the planet, and prunes nothing. Quantifying that — the same query against the same data written with and without a Hilbert sort — would turn a number that is currently a demonstration into one that predicts something.
Licence
The project is MIT — see LICENSE.
The data is not mine and is not covered by that. Overture Maps places is
CDLA-Permissive-2.0; divisions and buildings are ODbL or CDLA-Permissive-2.0 depending on the
contributing source. Attribution: © Overture Maps Foundation. Records derived from OpenStreetMap
carry ODbL obligations of their own. Every catalogue entry and every tool response reports the licence
of the data it read.
Available Tools
8 toolsgeoparquet_aggregate_attributeA
Group the rows of a dataset by one column and aggregate them, optionally inside a lon/lat rectangle. The grouping runs inside the remote file.
WHEN TO USE IT. For "how many of each", "what is the average", "which is the most common" — any question whose answer is a table of groups rather than a set of features. This is the tool that makes a 10 GB dataset answerable in kilobytes: the grouping happens remotely and only the group rows cross the network, however many rows went into them. Reach for it before geoparquet_filter_spatial whenever counting would do.
PARAMETERS. source: dataset name. group_by: the column whose distinct values become the groups, for example 'categories.primary'. aggregate: one of count, sum, avg, min, max. Default count. measure: the column to aggregate. Required for sum, avg, min and max, and rejected for count, which counts rows. It must be numeric; a non-numeric one is refused before any byte is fetched. min_lon, min_lat, max_lon, max_lat: restrict the aggregate to a rectangle. Pass all four or none. Omitting them aggregates the entire dataset, which reads the grouped and measured columns in full — slow and expensive on a multi-gigabyte source. Pass a box unless you truly mean the whole world. limit: maximum groups returned, ordered by the aggregate descending.
WHAT COMES BACK. groups, each with group_value, value (the aggregate) and row_count (rows in the group); rows_aggregated; truncated; the sql that ran; and the scan block.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| source | No | overture_places | |
| max_lat | No | ||
| max_lon | No | ||
| measure | No | ||
| min_lat | No | ||
| min_lon | No | ||
| group_by | Yes | ||
| aggregate | No | count |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden and excels. It discloses that grouping executes server-side ('only the group rows cross the network'), that a non-numeric measure is 'refused before any byte is fetched', that omitting the rectangle reads the full dataset, and that results are 'ordered by the aggregate descending'. It even lists the complete return payload (groups, rows_aggregated, truncated, sql, scan), which is behavioral disclosure beyond the structured 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 long but densely organized into four labeled sections (overview, WHEN TO USE IT, PARAMETERS, WHAT COMES BACK) with one semantic fact per parameter line. No sentence is filler — even asides like 'which counts rows' clarify a conditional rule. The length is proportionate to the tool's genuine complexity (9 params, 5 aggregate modes, spatial constraints).
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 complex 9-parameter tool with zero schema descriptions and zero annotations, the description covers everything needed to invoke it correctly: operation, selection criteria, per-parameter semantics, relational constraints between parameters, performance trade-offs, and return shape. The output schema exists and is also described in prose, which is belt-and-suspenders completeness.
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 fully compensate, and it does: every one of the 9 parameters gets semantic meaning beyond type/default. It explains the conditional relationship between aggregate and measure ('Required for sum, avg, min and max, and rejected for count'), the 'Pass all four or none' invariant for the lon/lat box, and the ordering semantics of limit.
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 opening sentence states a specific verb and resource: 'Group the rows of a dataset by one column and aggregate them', with the distinctive detail that grouping runs remotely. It is explicitly differentiated from the sibling geoparquet_filter_spatial ('Reach for it before geoparquet_filter_spatial whenever counting would do'), so an agent can tell them apart without opening schemas.
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?
A dedicated 'WHEN TO USE IT' section gives concrete query shapes ('how many of each', 'what is the average', 'which is the most common') and states the decision rule against a named alternative: 'Reach for it before geoparquet_filter_spatial whenever counting would do.' It also warns about the expensive full-dataset path ('slow and expensive on a multi-gigabyte source'), which implicitly tells the agent when to add a bounding box.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
geoparquet_count_in_polygonsA
Count how many features of one dataset fall inside each polygon of another: a point-in-polygon join between two remote datasets, restricted to a rectangle.
WHEN TO USE IT. For "how many of these are in each district", "which neighbourhood has the most of them", "break this down by administrative area" — any question whose answer is a table of areas with a number against each. It is the only tool that reads two datasets at once, and the only way to group by something that is not a column but a shape.
Use geoparquet_aggregate_attribute instead when you can group by a column the dataset already carries; it is much cheaper. Use this one when the grouping is geographic and the boundaries live in a different file.
COST. This is the most expensive tool here, and knowingly so: the rectangle prunes both datasets before the join, but the containment test still has to decode real geometry on both sides. Expect tens of megabytes and tens of seconds on a city-sized box, against single-digit megabytes for the other tools. Keep the rectangle tight, and prefer a narrower polygon_subtype.
PARAMETERS. min_lon, min_lat, max_lon, max_lat: the rectangle, in WGS 84 degrees. All four are required — this tool has no whole-world mode. point_source: the dataset being counted. polygon_source: the dataset providing the containing areas. It must be a polygonal dataset; a point dataset is refused before anything is read. polygon_subtype: narrows the polygon side to one administrative level, for example 'locality' or 'county'. Without it a country-sized polygon is returned alongside a neighbourhood one, because both overlap the rectangle, and the counts are then not comparable to each other. limit: maximum polygons returned, ordered by count descending.
WHAT COMES BACK. polygons, each with polygon_name, polygon_subtype and feature_count; the sql that ran; and the scan block. The count is the number of features whose geometry is contained by that polygon, not merely overlapping its bounding box.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| max_lat | Yes | ||
| max_lon | Yes | ||
| min_lat | Yes | ||
| min_lon | Yes | ||
| point_source | No | overture_places | |
| polygon_source | No | overture_divisions | |
| polygon_subtype | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full behavioral disclosure burden and does so thoroughly. It discloses that this is the most expensive tool, explains the rectangle pruning and geometry decode costs, refuses point datasets for polygon_source, and clarifies that counts are based on true containment rather than bounding-box overlap.
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 long but tightly organized into labeled sections: purpose, when-to-use, cost, parameters, and return value. Each section adds decision-relevant information that is not available in the schema, and the main purpose is front-loaded. No sentence feels redundant or filler.
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 complexity, zero annotations, and 0% schema description coverage, the description is remarkably complete. It covers selection criteria, cost/performance expectations, parameter behavior, output shape, and a key semantic caveat about containment versus bounding boxes, leaving the agent with everything needed 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?
Schema description coverage is 0%, so the description must compensate, and it does. Every parameter is explained with practical guidance: the rectangle is required and WGS 84, polygon_source must be polygonal, polygon_subtype prevents incomparable mixed granularity, and limit controls count-descending output.
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 specific verb-plus-resource statement: counting features from one dataset inside polygons of another via a point-in-polygon join. It further distinguishes itself from siblings by noting it is the only tool that reads two datasets at once and the only way to group by a shape rather than a column.
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 'WHEN TO USE IT' section gives concrete natural-language queries, explicitly names the cheaper alternative geoparquet_aggregate_attribute, and states the condition for choosing this tool instead: geographic grouping where boundaries live in a separate file. This gives an agent clear selection criteria rather than leaving the decision to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
geoparquet_describe_sourceA
Describe one remote dataset without reading any of its data: column names and types, the geometry and bbox columns, the coordinate reference system, the exact row count, the number of Parquet parts and row groups, the total remote size, and the geographic extent the dataset covers.
WHEN TO USE IT. Call this before your first filter against a dataset. It is how you learn the real column names — Overture nests many of them, so a category is categories.primary and a label is names.primary, not category and name — and how you check that the region you care about is inside extent before spending a query on it.
PARAMETERS.
source: dataset name, from the geoparquet://sources resource or the default. Every other tool takes the same name.
WHAT COMES BACK. columns is a list of {name, type, role}, where role marks the geometry, bbox, name, category and confidence columns. crs is the coordinate reference system (OGC:CRS84 means plain longitude/latitude degrees, which is what every tool here expects) and crs_is_default says whether the file stated it or inherited the GeoParquet default. extent is the dataset's bounding box, computed from row-group statistics, or null when the file carries no statistics to compute it from. row_count, remote_files, row_groups and remote_bytes describe the physical file. scan reports the bytes this call itself pulled: Parquet footers only, never a data page. On Overture places that is about 26 MB the first time — the footers of 16 parts carrying 4096 row groups of statistics — and zero afterwards, because the session caches them. Either way it is metadata about a 10.5 GB file, not the file.
| Name | Required | Description | Default |
|---|---|---|---|
| source | No | overture_places |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so thoroughly. It discloses that only Parquet footers are scanned, never data pages, that the call may cost about 26 MB of remote traffic on first use, that results are cached afterward, and that extent may be null when statistics are missing. These are exactly the behavioral traits an agent needs.
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 long but tightly organized with clear section labels: overview, when to use, parameters, and return shape. Every sentence adds operational value, and the key claim ('without reading any of its data') is front-loaded. The concrete scan-cost example earns its place by setting accurate expectations.
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 description covers not only what the tool returns but also edge cases like null extent, CRS meanings, default vs stated CRS, and the physical file characteristics. Even though an output schema exists, the description enriches it with operational semantics and a clear usage narrative, leaving no significant gap for an agent deciding to call it.
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 for the single `source` parameter. It explains that `source` is the dataset name, where to obtain it (`geoparquet://sources` resource or the default), and that every other tool takes the same name — valuable cross-tool context beyond the bare schema declaration.
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 names a specific verb and resource — 'Describe one remote dataset without reading any data' — and enumerates the exact metadata returned: columns, geometry/bbox, CRS, row count, parts, row groups, size, extent. It is clearly distinguishable from sibling tools like geoparquet_preview_rows and geoparquet_filter_spatial.
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 'WHEN TO USE IT' section explicitly recommends calling this before the first filter against a dataset, explains it is how to learn real column names, and advises checking whether the region is inside extent before spending a query. This gives direct guidance with a concrete alternative behavior, satisfying both when and why to use it.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
geoparquet_filter_spatialA
Return the features of a dataset that fall inside an area, as a GeoJSON FeatureCollection. The area is either a lon/lat rectangle or an arbitrary WKT geometry.
WHEN TO USE IT. When the answer is the features themselves — "which cafes are in this neighbourhood", "give me the buildings along this street" — and you intend to look at them individually. When you only need a count, a ranking or a distribution, use geoparquet_aggregate_attribute or geoparquet_summarize_h3 instead: they answer from the remote file and transfer kilobytes instead of features.
COST. The rectangle is what makes the read cheap. It is pushed into the remote Parquet file and prunes whole row groups from their footer statistics before any byte of data is fetched, so a tight box costs far less than a wide one — this is the difference between megabytes and gigabytes, not a micro-optimisation. Always pass the tightest area the question allows.
PARAMETERS.
source: dataset name.
min_lon, min_lat, max_lon, max_lat: the rectangle, in WGS 84 degrees. Pass all four, or none if you are using wkt.
wkt: an arbitrary geometry instead of a rectangle, for example 'POLYGON ((2.33 48.85, 2.36 48.85, 2.36 48.87, 2.33 48.87, 2.33 48.85))'. Its envelope prunes the read and the exact shape then filters the survivors, so the answer is exact. Give either a rectangle or a wkt, never both.
category: exact match on the dataset's category column, for example 'restaurant'. Preview the column first — the vocabulary is not obvious.
name_contains: case-insensitive substring of the feature name.
min_confidence: 0 to 1, Overture's own confidence in the record. 0.8 drops most questionable entries.
columns: column expressions to return. A narrow projection is worth as much as a tight box, because Parquet is columnar and unread columns are unfetched.
include_geometry: false skips the geometry column — the widest in the file — and approximates each feature by its bounding-box corner, which is exact for points. Ignored when wkt is used, since the exact test needs the geometry.
limit: maximum features, capped at 1000.
WHAT COMES BACK. geojson as a FeatureCollection; feature_count and truncated, which tells you the limit was reached and there is more; geometry_is_exact; the sql that ran; and scan with bytes_scanned — read it, and tighten the area if it looks large.
| Name | Required | Description | Default |
|---|---|---|---|
| wkt | No | ||
| limit | No | ||
| source | No | overture_places | |
| columns | No | ||
| max_lat | No | ||
| max_lon | No | ||
| min_lat | No | ||
| min_lon | No | ||
| category | No | ||
| name_contains | No | ||
| min_confidence | No | ||
| include_geometry | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden of behavioral disclosure, and it exceeds expectations. It explains cost behavior, how rectangle pruning works in Parquet, how WKT envelope pruning plus exact filtering works, the effect of include_geometry, and the exact contents of the response including bytes_scanned as a diagnostic signal.
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 long but every section earns its place: purpose, when-to-use, cost rationale, parameter semantics, and return value explanation. Clear section headers and front-loaded main behavior make the length navigable, and 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 12-parameter spatial filter tool with no annotations and no schema-level descriptions, the description is remarkably complete. It covers all inputs, output shape, behavioral nuances, cost implications, and alternatives, leaving no meaningful gap an agent would need to guess about when selecting or invoking the 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?
Schema description coverage is 0%, so the description must compensate entirely for the parameter semantics. It does so thoroughly: every one of the 12 parameters is explained, including constraints like 'Pass all four, or none', the mutual exclusivity of rectangle vs wkt, the meaning of min_confidence, the columnar cost benefit of columns, and the limit cap of 1000.
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 specific verb and resource: 'Return the features of a dataset that fall inside an area, as a GeoJSON FeatureCollection.' It also clarifies the two possible area forms (rectangle or WKT), and the WHEN TO USE IT section names the exact sibling tools that cover other use cases, distinguishing this tool clearly.
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 explicitly states when to use this tool ('when the answer is the features themselves') and when not to use it ('when you only need a count, a ranking or a distribution, use geoparquet_aggregate_attribute or geoparquet_summarize_h3 instead'). It also gives practical guidance on passing the tightest area, which directly informs tool invocation.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
geoparquet_find_nearestA
Return the features closest to a point, nearest first, each with its great-circle distance in kilometres.
WHEN TO USE IT. For "what is near here" and "which is the closest" — the questions where the ranking and the distance are the answer. Use geoparquet_filter_spatial instead when you want everything in an area rather than the closest few.
HOW IT STAYS CHEAP. A radius is not something Parquet statistics can prune on, so the search circle is first widened to its bounding rectangle, which is prunable; the exact distance is then computed only over the rows that survive, and used both to filter and to order. A large radius therefore costs a large read: prefer the smallest radius that can contain the answer, and widen it only if you come back empty.
PARAMETERS.
source: dataset name.
lon, lat: the centre point, in WGS 84 degrees. Longitude first.
radius_km: how far to look, up to 500. Results outside it are excluded, so this is a filter, not just a hint.
category, name_contains: the same narrowing as geoparquet_filter_spatial.
columns: column expressions to return.
limit: how many neighbours, capped at 1000.
WHAT COMES BACK. rows, ordered nearest first, each carrying distance_km; the search_bbox actually used for pruning; and the scan block.
| Name | Required | Description | Default |
|---|---|---|---|
| lat | Yes | ||
| lon | Yes | ||
| limit | No | ||
| source | No | overture_places | |
| columns | No | ||
| category | No | ||
| radius_km | No | ||
| name_contains | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full behavioral burden and does so richly: it explains the radius acts as a filter, not a hint; distances are great-circle in kilometres; results are ordered nearest first; the search_bbox is used for pruning; and cost characteristics are disclosed ('A large radius therefore costs a large read'). This goes far beyond basic operation.
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?
Though lengthy, the description is organized into clear labeled sections (purpose, when to use, cost behavior, parameters, return values) with no filler. Each sentence conveys essential operational information, such as the bounding-rectangle pruning strategy and the exact response shape.
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 8 parameters, no annotations, and the presence of an output schema, this description is complete. It explains what to pass, how the tool behaves, what the output contains (rows with distance_km, search_bbox, scan block), and how to choose between this and sibling tools. Nothing critical for correct invocation 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?
Schema description coverage is 0%, so the description must compensate entirely. It does: every parameter is explained with added meaning — 'lon, lat' are WGS 84 degrees with longitude first, radius_km is capped at 500 and is a hard filter, limit is capped at 1000, and category/name_contains are stated to behave like the sibling filter tool.
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: 'Return the features closest to a point, nearest first, each with its great-circle distance in kilometres.' This clearly distinguishes it from sibling tools like geoparquet_filter_spatial, which returns everything in an area rather than the closest few.
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?
An explicit 'WHEN TO USE IT' section states the suitable question types ('what is near here' and 'which is the closest') and names the exact alternative: 'Use geoparquet_filter_spatial instead when you want everything in an area rather than the closest few.' This gives the agent direct routing guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
geoparquet_preview_rowsA
Return the first few rows of a dataset, so you can see what the values actually look like.
WHEN TO USE IT. After geoparquet_describe_source tells you a column exists and before you filter on it, to learn how it is populated: what a category string looks like in practice, whether a field is mostly null, how an address is spelled. Guessing a filter value and getting zero features back costs more than one preview.
This is NOT a spatial question. The rows are whatever the file stores first, in no geographic order and in no ranking — do not read them as "the most important places" or "places near anywhere". To ask where things are, use geoparquet_filter_spatial or geoparquet_find_nearest.
PARAMETERS. source: dataset name. columns: column expressions to return, for example ["id", "names.primary", "confidence"]. Omit for the dataset's default projection. limit: how many rows, 1 to 100. Ten is usually enough to see the shape.
WHAT COMES BACK. rows as plain records, columns_returned naming the keys, and the scan block. The read stops at the first row group of the first part file, so the cost does not grow with the dataset.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| source | No | overture_places | |
| columns | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so thoroughly. It discloses that rows come in file order, are not ranked or spatially significant, that reads stop at the first row group for cost predictability, and what the response contains. This goes well beyond a basic one-line definition.
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 organized into clear labeled sections, front-loaded with the core purpose, and every sentence adds practical value. Even the cost rationale and non-spatial warning serve important decision-making purposes.
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 modest complexity, the description covers when to use it, how each parameter behaves, what the return payload includes, and the operational cost characteristics. Nothing an agent needs to correctly call and interpret this tool 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?
Schema description coverage is 0%, but the description's PARAMETERS section explains each parameter meaning, provides a concrete example for columns, and advises on the limit range and typical values. It 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?
States a specific verb and resource: 'Return the first few rows of a dataset'. It clearly differentiates itself from spatial sibling tools, explicitly saying this is NOT a spatial question and directing to geoparquet_filter_spatial or geoparquet_find_nearest for spatial queries.
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?
Provides an explicit 'WHEN TO USE IT' section: after geoparquet_describe_source and before filtering, to inspect real values. It also states when NOT to use it and names the alternative tools, leaving no ambiguity about its intended role.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
geoparquet_run_sqlA
Run one read-only SELECT against the datasets in scope, for questions the other tools do not have a shape for.
WHEN TO USE IT. Last, not first. The typed tools push their filters into the remote Parquet file by construction; an ad-hoc query pushes down only what its WHERE clause happens to express, so a query that forgets a bbox predicate can read gigabytes to answer something geoparquet_aggregate_attribute would have answered in kilobytes. Reach for it for genuine gaps: a join between two datasets, a HAVING clause, a window function, a self-join.
WHAT YOU CAN QUERY. Each dataset in scope is a table named exactly as the dataset is — overture_places, overture_divisions, overture_buildings — and those are the only tables that exist. There is no way to name a file: table functions such as read_parquet are refused, and so is anything that is not a single SELECT. That is a perimeter, not a lint rule.
WRITING A FAST ONE. Constrain bbox explicitly, as four comparisons on its members, because that is the form Parquet statistics can prune on:
SELECT categories.primary AS category, count(*) AS n FROM overture_places WHERE bbox.xmin <= 2.40 AND bbox.xmax >= 2.30 AND bbox.ymin <= 48.88 AND bbox.ymax >= 48.85 GROUP BY 1 ORDER BY n DESC
Writing that filter with a geometry function instead would be correct and would read the entire file, because the Parquet reader cannot see through it. Select named columns rather than *, for the same reason: unread columns are unfetched.
PARAMETERS. sql: one SELECT statement. max_rows: row ceiling, applied as an outer LIMIT. Hard, and capped at 1000. max_bytes: byte ceiling. Reported, not pre-emptive — see below.
WHAT COMES BACK. rows, tables_read, the executed_sql actually run, the scan block, and byte_budget_exceeded. That last one is a verdict after the fact, not a brake: DuckDB cannot abort a scan on bytes already transferred, so the rows are returned — they have been paid for — and the flag tells you the query was too expensive and the next one should be narrower. The row ceiling, by contrast, is enforced.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | ||
| max_rows | No | ||
| max_bytes | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden, and it delivers extensively. It discloses that only a single SELECT is permitted, table functions are refused, read_parquet is blocked, and the 'perimeter' is intentional. It also explains that byte_budget_exceeded is a post-hoc verdict rather than a pre-emptive brake, and that max_rows is hard-enforced while max_bytes is not.
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 long but every section earns its place, and clear headers make it scannable: WHEN TO USE IT, WHAT YOU CAN QUERY, WRITING A FAST ONE, PARAMETERS, WHAT COMES BACK. It front-loads the most important guidance first and uses a concrete query example to illustrate the bbox pushdown point efficiently.
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 complex, flexible SQL tool with no annotations, the description is exceptionally complete. It covers query scope, performance optimization, filter pushdown mechanics, parameter semantics, and result fields including executed_sql, scan, and byte_budget_exceeded. Even though an output schema exists, the descriptive explanation of post-hoc byte budget behavior is necessary context that the schema alone would not convey.
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%, but the dedicated PARAMETERS section fully compensates. It explains sql as 'one SELECT statement,' max_rows as an outer LIMIT that is hard and capped at 1000, and max_bytes as a reported ceiling rather than pre-emptive. This goes far beyond the raw schema and gives an agent the operational meaning of each parameter.
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 SELECT against the datasets in scope.' It explicitly frames the tool as covering questions 'the other tools do not have a shape for,' distinguishing it from the typed sibling tools. It also clarifies that datasets are the only queryable tables, removing ambiguity about the tool's scope.
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 'WHEN TO USE IT' section is explicit and actionable: 'Last, not first,' with concrete examples of genuine gaps such as joins, HAVING, window functions, and self-joins. It warns against using ad-hoc queries when a typed tool would push filters down efficiently, naming the tradeoff explicitly and thereby guiding selection among siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
geoparquet_summarize_h3A
Bin the features inside a rectangle into H3 hexagonal cells and return the count per cell. A density map, computed remotely.
WHEN TO USE IT. For "where are these densest", "how is this spread across the city", and anything you would answer with a heatmap. The binning and counting happen inside the remote file, so the answer is a few hundred cells whether they cover a thousand features or ten million — you never transfer the features to find out where they cluster.
PARAMETERS. source: dataset name. min_lon, min_lat, max_lon, max_lat: the rectangle to bin, in WGS 84 degrees. All four are required; this tool has no whole-world mode by design. resolution: the H3 level, 0 to 15. 0 is continent-sized, 6 is a city, 8 is roughly a neighbourhood, 9 a block, 11 a building. Choosing too fine a resolution for a wide box returns thousands of near-empty cells; start at 8 for a city and adjust. limit: maximum cells, ordered by count descending, so the limit keeps the hotspots. include_cell_centre: adds the latitude and longitude of each cell's centre, which is what you need to plot the result.
WHAT COMES BACK. cells, each with h3_cell (the canonical hexadecimal id), feature_count and optionally the centre; plus features_binned, truncated, and the scan block.
Features are binned on their bounding-box centre rather than their true geometry, which avoids fetching the widest column in the file: exact for point datasets, the envelope's centre for polygonal ones. Requires DuckDB's H3 extension; if it cannot be loaded the tool says so rather than falling back to something slower.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| source | No | overture_places | |
| max_lat | Yes | ||
| max_lon | Yes | ||
| min_lat | Yes | ||
| min_lon | Yes | ||
| resolution | No | ||
| include_cell_centre | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full behavioral burden and exceeds it: it discloses remote computation, bounded result size, bounding-box-center binning versus true geometry, DuckDB H3 extension dependency, and the explicit failure behavior if the extension cannot be loaded. This gives an agent strong insight into side effects and limitations.
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 structured into clear sections - usage, parameters, return values, and caveats - and every sentence adds information. Even the H3 extension note earns its place by preventing the agent from assuming a silent fallback exists.
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?
Despite the absence of annotations and schema-level parameter descriptions, the description fully equips an agent to select and invoke this tool correctly. It covers input semantics, output structure (cells, feature_count, centre, features_binned, truncated, scan), and edge-case behavior around resolution and extension loading.
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 fully compensate, and it does. Every parameter is explained: source, the four rectangle coordinates in WGS84 with requiredness, resolution with real-world scale examples and tuning guidance, limit with ordering semantics, and include_cell_centre with its plotting purpose.
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 specific verb and resource: "Bin the features inside a rectangle into H3 hexagonal cells and return the count per cell." It further distinguishes the tool by labeling it "A density map, computed remotely," separating it from sibling aggregation and filtering tools.
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 an explicit "WHEN TO USE IT" section with concrete queries like "where are these densest" and "how is this spread across the city," plus the heatmap heuristic. It does not explicitly name alternative sibling tools or state when not to use this tool, but the use cases and efficiency rationale are clear.
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.
8 tool updates
v0.1.0- First observed
geoparquet_aggregate_attribute - First observed
geoparquet_count_in_polygons - First observed
geoparquet_describe_source - First observed
geoparquet_filter_spatial - First observed
geoparquet_find_nearest - First observed
geoparquet_preview_rows - First observed
geoparquet_run_sql - First observed
geoparquet_summarize_h3
TDQS
Each tool targets a distinct analytical task: metadata inspection, row preview, spatial feature extraction, nearest-neighbor search, grouped aggregation, H3 density binning, polygon containment counting, and ad-hoc SQL. The spatial/counting tools overlap thematically, but their usage guidance clearly separates feature retrieval from counting, ranking, and binning.
All tools share the geoparquet_ prefix and lowercase snake_case convention, and nearly all follow a verb-first pattern. A few names like filter_spatial, find_nearest, and count_in_polygons use adjectival or prepositional objects rather than strict verb_noun, which is a minor deviation from the otherwise consistent style.
Eight tools is well within the ideal range and each tool earns its place by covering a distinct query shape or analysis workflow. There is no bloat or redundancy; the set feels deliberately scoped for efficient remote GeoParquet querying.
The tool surface covers the full read-only analytical lifecycle: inspect schema, preview values, filter spatially, find nearest features, aggregate attributes, summarize by H3 cell, count points in polygons, and run arbitrary read-only SQL for anything outside the typed tools. The run_sql escape hatch and polygon-count join close the obvious gaps an agent might encounter.
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
Generate and run high performance queries on open and private spatial data at-scale in the cloud
Query, join, profile, clean and convert CSV/JSON/Parquet with server-side DuckDB over MCP.
A collaborative substrate over your data: vector, knowledge graph, SQL, geospatial, streaming.
Query BigQuery, Snowflake, Redshift & Azure Synapse with natural language
Related MCP Servers
- FlicenseNot gradedqualityNot gradedmaintenanceAn Iceberg-native geospatial MCP server powered by DuckDB that provides tools for spatial SQL queries, catalog discovery, and data management. It enables LLM agents to interact with Apache Iceberg lakehouses to perform complex spatial analysis, joins, and aggregations.1-
- AlicenseAqualityDmaintenanceEnables AI assistants to query Apache Iceberg tables on S3 via AWS Glue Data Catalog using DuckDB as the embedded query engine, supporting columnar Arrow reads with no data movement.4MIT
- AlicenseNot gradedqualityAmaintenanceAn MCP server that connects AI agents to cloud-native geospatial data via STAC metadata and DuckDB with H3 spatial indexing, enabling zero-configuration SQL queries on terabyte-scale datasets over S3.23BSD 3-Clause
- AlicenseNot gradedqualityCmaintenanceProvides geospatial data intelligence tools for inspecting, querying, and converting geospatial data using DuckDB Spatial.1MIT
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/rteina/geoparquet-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server