Reference
The MCP server (cambridge_data.mcp, console script cambridge-open-data-ma-mcp) exposes 8 tools and 4 resources. Each tool delegates to the same library function the cambridge-data CLI calls, so the server and the CLI never drift. The server holds no state of its own — each call opens a fresh DuckDB connection over data/raw/*.parquet, so it reflects the cache as of the call and honours the "no separate warehouse to keep in sync" design.
Run it over stdio (the transport opencode and the other harnesses launch it with):
uv run cambridge-open-data-ma-mcp
# or: uv run python -m cambridge_data.mcpThe CLI mirror
The cambridge-data console script (installed by uv sync, alongside the cambridge-open-data-ma-mcp server) is the same eight tools and four resources as a human-facing CLI — one subcommand per tool, plus catalog, reports, and report for the opencode-native resources, and a global --json (-j) on every subcommand that pipes the result as JSON-native rows instead of a Rich table.
| Subcommand | Tool / resource | Flags |
|---|---|---|
refresh | refresh_catalog | — |
search KEYWORD | search_datasets | --limit (20) |
describe DATASET_ID | describe_dataset | --probe, --top (10) |
fetch DATASET_ID | fetch_dataset | --where, --select |
coverage DATASET_ID | field_coverage | --field (required), --date-field |
audit DATASET_ID | coverage_audit | --threshold (99.0), --top (10) |
cached | list_cached_datasets | — |
query SQL | query | --limit (1000) |
catalog | cambridge://catalog | --limit (50) |
reports | cambridge://reports | — |
report SLUG | cambridge://reports/{slug} | — |
The eight tools map one-to-one onto the first eight rows; the four resources map onto catalog, describe, reports, and report — describe doubles as the cambridge://datasets/{id} reader. Every subcommand exits 0 on success, 1 on a handled error, 130 on Ctrl-C, and 2 on a usage error; append --json to machine-parse the same result.
CLI command reference
The table above is the quick index; this section is the per-subcommand reference. Every subcommand accepts -j / --json (emits JSON-native rows instead of a Rich table, so the output pipes into another tool), and long-running network work (refresh, fetch) shows a spinner. Each block gives the usage line, its positional argument(s), its command-specific flags with defaults, and a worked example drawn from the tutorials — the flag and field names are the machine names a query accepts.
refresh
cambridge-data refreshRefresh the local catalog cache from the live Socrata Discovery API — the full dataset listing (~280 datasets) into data/catalog.parquet. Run it first when search reports a missing cache, or when the portal may have new datasets. Needs no credentials; the app token only lifts the anonymous rate limit. No positional arguments or command-specific flags. Mirrors the refresh_catalog tool.
search
cambridge-data search "budget salaries"Find datasets by keyword — matching name, description, category, and tags in the cached catalog — and read columns_field_name, the machine field names a query accepts (not the display names; querying with a display name silently returns zero rows). Run refresh first if the cache looks stale.
- Positional:
keyword— a free-text search term - Flags:
--limit— max results (default 20) - Mirrors: the
search_datasetstool - Example:
cambridge-data search budget --json
describe
cambridge-data describe ixg8-tyau --probeFull schema and coverage profile for one dataset: the machine field names (query with these, not the display names), their types, display names, tags, update cadence, and — when cached — the row count and the dataset's own earliest/latest date (the era-gap signal). Set --probe for coverage_flags: fields only partly populated, the recent-window traps to check before trusting a total.
- Positional:
dataset_id— a Socrata four-by-four dataset id, e.g.ixg8-tyau - Flags:
--probe— include coverage flags (off by default);--top— max coverage flags (default 10) - Mirrors: the
describe_datasettool and thecambridge://datasets/{id}resource - Example:
cambridge-data describe ixg8-tyau --probe --top 5
fetch
cambridge-data fetch ixg8-tyau --where "fiscal_year='2027'"Pull every row of a dataset via the SODA API and cache it as Parquet (data/raw/<id>.parquet) so it can be cross-referenced in SQL. Supports a server-side $where filter and $select projection. Re-running overwrites the cache; large datasets take time, so prefer a $where when a subset suffices.
- Positional:
dataset_id— a Socrata four-by-four dataset id, e.g.ixg8-tyau - Flags:
--where— a SoQL$wherefilter (server-side);--select— a SoQL$selectprojection (server-side) - Mirrors: the
fetch_datasettool - Example:
cambridge-data fetch ixg8-tyau --where "fiscal_year='2027'" --select fiscal_year,total_salary
coverage
cambridge-data coverage ixg8-tyau --field total_salaryProbe how one field is actually populated across a cached dataset — the defence against the recent-window trap. Reports the populated fraction and, for populated rows, the date range they span, and states a warning when the pattern looks like a recent operational window rather than the full history. Always run this before summing or rate-computing over a field.
- Positional:
dataset_id— a Socrata four-by-four dataset id (must be cached) - Flags:
--field— the machine field name to probe (required, fromcolumns_field_name);--date-field— the time-axis column that brackets the populated rows (auto-detected from the column names when omitted) - Mirrors: the
field_coveragetool - Example:
cambridge-data coverage ixg8-tyau --field total_salary
audit
cambridge-data audit ixg8-tyau --threshold 99 --top 10Scan every field of a cached dataset and return the least-populated first — the automated version of "check field population before trusting a total." This is how you discover, up front, that a complete-looking column is really a recent window. Use it instead of hand-checking fields one at a time.
- Positional:
dataset_id— a Socrata four-by-four dataset id (must be cached) - Flags:
--threshold— flag fields below this percent populated (default 99.0);--top— max fields to return (default 10) - Mirrors: the
coverage_audittool - Example:
cambridge-data audit ixg8-tyau --threshold 99 --top 10
cached
cambridge-data cachedList the dataset ids whose rows are cached locally in data/raw/. Only cached datasets can be queried in SQL or audited for coverage; use fetch to pull one that is not yet cached. No positional arguments or command-specific flags. Mirrors the list_cached_datasets tool.
query
cambridge-data query "SELECT department, SUM(TRY_CAST(total_salary AS DOUBLE)) total \
FROM ds_ixg8_tyau GROUP BY 1 ORDER BY 2 DESC LIMIT 3"Run a DuckDB SELECT over the cached datasets, exposed as one SQL view per dataset named ds_<id_with_underscores> (ixg8-tyau → ds_ixg8_tyau). This is the cross-referencing step: join and aggregate across ds_* views. Every Socrata column is a string — use TRY_CAST(col AS DOUBLE) for numeric work, and expect join keys (e.g. department names) to need normalising before they line up.
- Positional:
sql— a DuckDBSELECTreferencingds_<id>views - Flags:
--limit— max rows to return (default 1000) - Mirrors: the
querytool - Example: see the command above;
cambridge-data query "SELECT COUNT(*) FROM ds_ixg8_tyau"
NOTE
query runs arbitrary SQL over local Parquet only — a trusted single-user tool, not sandboxed. Run describe + audit first so you know the fields exist and are fully populated (see the query entry under Tools for the full warning).
catalog
cambridge-data catalog --limit 100Dump the full compact dataset index — id, name, category, update frequency, and last-updated per dataset — the human form of the cambridge://catalog resource. Read it to discover datasets; use describe for one dataset's full schema and coverage. No positional arguments.
- Flags:
--limit— rows to show (0= all; default 50) - Mirrors: the
cambridge://catalogresource - Example:
cambridge-data catalog --limit 100
reports
cambridge-data reportsList the finished analysis report slugs in docs/reports/ — the human form of the cambridge://reports resource. The corpus lives in the upstreamlocal-data-experiments repo, so a fresh public install lists none until it populates its own docs/reports/. No positional arguments. Mirrors the cambridge://reports resource.
report
cambridge-data report <slug>Print one finished report's Markdown — self-contained, with a Data provenance section — the human form of the cambridge://reports/{slug} resource. Raises an actionable error listing the available slugs when the slug is unknown.
- Positional:
slug— a report directory name, fromcambridge-data reports - Mirrors: the
cambridge://reports/{slug}resource - Example:
cambridge-data report <slug>
Tools
refresh_catalog
Refresh the local catalog cache from the live Socrata Discovery API — the full dataset listing (~280 datasets: id, name, description, fields, tags, update cadence) into data/catalog.parquet. Run first when search_datasets reports a missing cache, or when the portal may have new datasets. Needs no credentials; the app token only lifts the anonymous rate limit.
- Args: none
- Returns:
{ datasets: int, path: str }— the count written and the cache path.
search_datasets(keyword, limit=20)
Search the cached catalog by keyword. Matches name, description, category, and tags. Returns each result's id, name, category, link, update_frequency, and — crucially — columns_field_name, the machine field names a query actually accepts (not columns_name, the display names; querying with a display name silently returns zero rows).
- Args:
keyword: str,limit: int = 20 - Returns:
{ keyword, count, results[] }— each result hasid,name,category,link,update_frequency,columns_field_name[].
describe_dataset(dataset_id, probe=False)
Full schema and coverage profile for one dataset: the machine field names (query with these, not the display names), their types, display names, tags, update cadence, and — when cached — the row count and the dataset's own earliest/latest date (the era-gap signal: a zero result for an old date may mean the dataset predates it, not that nothing happened). Set probe=true for coverage_flags: fields that are only partly populated — the recent-window traps to check before trusting any total or rate.
- Args:
dataset_id: str,probe: bool = false - Returns: the dataset profile as a dict.
fetch_dataset(dataset_id, where=None, select=None)
Pull every row of a dataset via the SODA API and cache it as Parquet (data/raw/<id>.parquet), so it can be cross-referenced in SQL. Supports a server-side $where filter and $select projection. Re-running overwrites the cache; check describe_dataset's cached/fetched_at first to avoid re-pulling time-sensitive data. Large datasets (millions of rows) take time; prefer a $where filter when a subset suffices.
- Args:
dataset_id: str,where: str | null,select: str | null - Returns:
{ dataset_id, rows: int, path, fetched_at }.
field_coverage(dataset_id, field, date_field=None)
Probe how one field is actually populated across a cached dataset — the defence against the recent-window trap. Reports the populated fraction and, for populated rows, the date range they span, and states a warning when the pattern looks like a recent operational window rather than the full history (e.g. a "balance" field only filled for the last year). Always run this before summing or rate-computing over a field. The field must be a machine name from columns_field_name; a display name raises an actionable error listing the real fields.
- Args:
dataset_id: str(must be cached),field: str,date_field: str | null = null(auto-detected from the column names when omitted) - Returns: the field-coverage record as a dict, including a
warningstring when applicable.
coverage_audit(dataset_id, threshold=99.0, top=10)
Automated field-population audit: scan every field of a cached dataset and return the least-populated first — the automated version of "check field population before trusting a total." This is how you discover, up front, that a "complete-looking" column is really a recent window. Use it instead of hand-checking fields one at a time.
- Args:
dataset_id: str(must be cached),threshold: float = 99.0(flag fields below this percent populated),top: int = 10 - Returns:
{ dataset_id, count, flags[] }— each flag is a field-coverage record with awarning.
list_cached_datasets
List the dataset ids whose rows are cached locally in data/raw/. Only cached datasets can be queried in SQL or audited for coverage; use fetch_dataset to pull one that is not yet cached.
- Args: none
- Returns:
{ cached: string[] }— the sorted list of cached dataset ids.
query(sql, limit=1000)
Run a DuckDB SELECT over the cached datasets, exposed as one SQL view per dataset named ds_<id_with_underscores> (ixg8-tyau → ds_ixg8_tyau). This is the cross-referencing step: join/aggregate across ds_* views. Every Socrata column is a string — use TRY_CAST(col AS DOUBLE) for numeric work, and expect join keys (e.g. department names) to need normalisation before they line up.
WARNING
query runs arbitrary SQL over local Parquet only. It is a trusted single-user tool, not sandboxed — a public multi-tenant deployment would need a boundary. Prefer describe_dataset + coverage_audit first, so you know the fields exist and are fully populated.
- Args:
sql: str,limit: int = 1000 - Returns:
{ columns: string[], row_count: int, rows: object[] }on success;{ error: str, sql: str }on failure. Rows are JSON-native (the result is round-tripped through JSON, soint64/datetime64/numpy arrays are coerced to plain types that cross the MCP boundary).
Resources
opencode reads these natively via list_mcp_resources / read_mcp_resource. The other harnesses reach the same content through the equivalent tools (search_datasets, describe_dataset, and the report corpus).
cambridge://catalog
The full dataset index for data.cambridgema.gov (~280 datasets), compact: id, name, category, update frequency, and last-updated per dataset. application/json. Read this to discover datasets; use describe_dataset(id) for a single dataset's full schema and coverage.
cambridge://datasets/{dataset_id}
The full schema-and-coverage profile for one dataset: machine field names, types, display names, tags, update cadence, and (when cached) row count and date range. application/json. Read this before writing a query.
cambridge://reports
The list of finished analysis reports in docs/reports/, by slug. application/json. The corpus lives in the upstream local-data-experiments repo; a fresh public install has none until it populates its own docs/reports/.
cambridge://reports/{slug}
A finished analysis report's Markdown (self-contained, with a Data provenance section). text/markdown. Raises an actionable error listing available slugs when the slug is unknown.
A typical call sequence
refresh_catalog # once, to get/refresh the index
search_datasets("budget salary") # find the dataset; read columns_field_name
describe_dataset("ixg8-tyau", probe=true) # full schema + coverage before touching data
fetch_dataset("ixg8-tyau") # cache it (idempotent)
coverage_audit("ixg8-tyau") # which fields are partial?
query("SELECT ... FROM ds_ixg8_tyau") # the cross-reference