Skip to content

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):

bash
uv run cambridge-open-data-ma-mcp
# or: uv run python -m cambridge_data.mcp

The 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.

SubcommandTool / resourceFlags
refreshrefresh_catalog—
search KEYWORDsearch_datasets--limit (20)
describe DATASET_IDdescribe_dataset--probe, --top (10)
fetch DATASET_IDfetch_dataset--where, --select
coverage DATASET_IDfield_coverage--field (required), --date-field
audit DATASET_IDcoverage_audit--threshold (99.0), --top (10)
cachedlist_cached_datasets—
query SQLquery--limit (1000)
catalogcambridge://catalog--limit (50)
reportscambridge://reports—
report SLUGcambridge://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 ​

bash
cambridge-data refresh

Refresh 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.

bash
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_datasets tool
  • Example: cambridge-data search budget --json

describe ​

bash
cambridge-data describe ixg8-tyau --probe

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). 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_dataset tool and the cambridge://datasets/{id} resource
  • Example: cambridge-data describe ixg8-tyau --probe --top 5

fetch ​

bash
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 $where filter (server-side); --select — a SoQL $select projection (server-side)
  • Mirrors: the fetch_dataset tool
  • Example: cambridge-data fetch ixg8-tyau --where "fiscal_year='2027'" --select fiscal_year,total_salary

coverage ​

bash
cambridge-data coverage ixg8-tyau --field total_salary

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. 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, from columns_field_name); --date-field — the time-axis column that brackets the populated rows (auto-detected from the column names when omitted)
  • Mirrors: the field_coverage tool
  • Example: cambridge-data coverage ixg8-tyau --field total_salary

audit ​

bash
cambridge-data audit ixg8-tyau --threshold 99 --top 10

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.

  • 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_audit tool
  • Example: cambridge-data audit ixg8-tyau --threshold 99 --top 10

cached ​

bash
cambridge-data cached

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 to pull one that is not yet cached. No positional arguments or command-specific flags. Mirrors the list_cached_datasets tool.

query ​

bash
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 DuckDB SELECT referencing ds_<id> views
  • Flags: --limit — max rows to return (default 1000)
  • Mirrors: the query tool
  • 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 ​

bash
cambridge-data catalog --limit 100

Dump 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://catalog resource
  • Example: cambridge-data catalog --limit 100

reports ​

bash
cambridge-data reports

List 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 ​

bash
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, from cambridge-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 has id, 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 warning string 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 a warning.

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, so int64/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 ​

text
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