Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion mcp-server/src/tools/fetch-aggregate-data.tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TableArgs> {
name = 'fetch-aggregate-data'
Expand Down Expand Up @@ -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 ` +
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mcp-server/src/tools/fetch-dataset-geography.tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export class FetchDatasetGeographyTool extends BaseTool<FetchDatasetGeographyArg
content: [
{
type: 'text',
text: `Available geographies for ${args.dataset}${args.year ? ` (${args.year})` : ''}:\n\n${JSON.stringify(parsedGeographyData, null, 2)}`,
text: `Available geographies for ${args.dataset}${args.year ? ` (${args.year})` : ''}:\n\n${JSON.stringify(parsedGeographyData)}`,
},
],
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,32 @@ describe('FetchAggregateDataTool', () => {
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) => {
Expand Down