From 8e0edcad8ea6d2968e19a6259d1b78e441784f20 Mon Sep 17 00:00:00 2001 From: brendanbabb Date: Mon, 1 Jun 2026 12:57:16 -0800 Subject: [PATCH] Adapt upstream PR #145 (ACS 1-year hint) + #144 (token flattening) Incorporates two upstream enhancements from uscensusbureau/us-census-bureau-data-api-mcp, adapted to this fork's existing response-shape and error machinery rather than applied verbatim. #145 -- ACS 1-year population threshold: - buildApiErrorMessage now detects acs/acs1 datasets and, on a 400, leads with the 65,000-population-threshold explanation and an acs/acs5 suggestion before the generic discovery-tool guidance. Folded into our existing helper instead of upstream's standalone inline branch. - Tool description gains a concise threshold note, consistent with our caveats-led description style. #144 -- token management: - fetch-dataset-geography now emits compact JSON (drops the 2-space indent), the one tool still returning pretty-printed JSON. resolve-geography-fips and search-data-tables already exceed #144's goal via response-format.ts, so the upstream content-flattener helper is intentionally not vendored. Adds a unit test for the ACS-1yr 400 path. typecheck clean; affected unit suites (35 tests) pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/tools/fetch-aggregate-data.tool.ts | 17 +++++++++++- .../src/tools/fetch-dataset-geography.tool.ts | 2 +- .../fetch-aggregate-data.tool.test.ts | 26 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/mcp-server/src/tools/fetch-aggregate-data.tool.ts b/mcp-server/src/tools/fetch-aggregate-data.tool.ts index 65dc5d8..794d5f7 100644 --- a/mcp-server/src/tools/fetch-aggregate-data.tool.ts +++ b/mcp-server/src/tools/fetch-aggregate-data.tool.ts @@ -20,7 +20,7 @@ import { validateGeographyArgs, } from '../schema/validators.js' -export const toolDescription = `Fetches Census Bureau statistics for a dataset, vintage, and geography; never guess cell codes -- use search-data-tables first or this tool will reject unknown codes with a "did you mean" hint. Every ACS estimate is auto-paired with its margin of error and flagged LOW RELIABILITY when CV exceeds 30%; suppression sentinels are decoded to text rather than returned as numbers. Required: dataset, year, get (variables or group), and one of for/ucgid. Returns numbered Record blocks with caveats led at the top.` +export const toolDescription = `Fetches Census Bureau statistics for a dataset, vintage, and geography; never guess cell codes -- use search-data-tables first or this tool will reject unknown codes with a "did you mean" hint. Every ACS estimate is auto-paired with its margin of error and flagged LOW RELIABILITY when CV exceeds 30%; suppression sentinels are decoded to text rather than returned as numbers. ACS 1-year (acs/acs1) only covers areas with populations of 65,000+; use acs/acs5 for smaller geographies or the API returns a bare 400. Required: dataset, year, get (variables or group), and one of for/ucgid. Returns numbered Record blocks with caveats led at the top.` export class FetchAggregateDataTool extends BaseTool { name = 'fetch-aggregate-data' @@ -218,6 +218,14 @@ function buildApiErrorMessage( `Recover by calling list-datasets to confirm the dataset exists and which vintages are published.`, ) } else if (status === 400) { + if (isAcs1YearDataset(ctx.dataset)) { + lines.push( + `ACS 1-year estimates (acs/acs1) are only published for geographic areas with ` + + `populations of 65,000 or more, so a 400 here often means the requested area is too small. ` + + `Switch the dataset to acs/acs5 (5-year estimates) to cover smaller geographies. ` + + `See https://www.census.gov/programs-surveys/acs/guidance/estimates.html.`, + ) + } lines.push( `The Census API rejected the query as malformed.`, `Re-verify the geography (for/in/ucgid) via fetch-dataset-geography and ` + @@ -231,6 +239,13 @@ function buildApiErrorMessage( return lines.join(' ') } +// ACS 1-year estimates are only published for areas of 65,000+ people, a +// frequent cause of opaque 400s. Detect acs1 regardless of spacing/case so the +// error path can hand back the population-threshold hint above. +function isAcs1YearDataset(dataset: string): boolean { + return dataset.toLowerCase().replace(/\s+/g, '').includes('acs/acs1') +} + function buildQueryEcho(opts: { dataset: string year: number | string diff --git a/mcp-server/src/tools/fetch-dataset-geography.tool.ts b/mcp-server/src/tools/fetch-dataset-geography.tool.ts index 2637183..1056013 100644 --- a/mcp-server/src/tools/fetch-dataset-geography.tool.ts +++ b/mcp-server/src/tools/fetch-dataset-geography.tool.ts @@ -218,7 +218,7 @@ export class FetchDatasetGeographyTool extends BaseTool { expect(text).toContain('list-datasets') }) + it('explains the ACS 1-year 65,000 population threshold on a 400 and suggests acs/acs5', async () => { + mockFetch.mockImplementation((url: string) => { + if (url.includes('/variables.json')) { + return Promise.resolve(new Response('nope', { status: 404 })) + } + return Promise.resolve( + new Response('bad request', { + status: 400, + statusText: 'Bad Request', + }), + ) + }) + + const args = { + dataset: 'acs/acs1', + year: 2022, + get: { group: 'B01001' }, + for: 'place:00100', + } + + const response = await tool.toolHandler(args, process.env.CENSUS_API_KEY) + const text = response.content[0].text as string + expect(text).toContain('65,000') + expect(text).toContain('acs/acs5') + }) + it('should handle network errors', async () => { // Variables fetch fails silently; data fetch rejects. mockFetch.mockImplementation((url: string) => {