import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const API_KEY = process.env.CENSUS_API_KEY || "";
const BASE_URL = "https://api.census.gov/data";
// Helper to fetch from Census API
async function censusApiRequest(url: string): Promise<any> {
const response = await fetch(url);
if (!response.ok) {
const text = await response.text();
throw new Error(`Census API error: ${response.status} - ${text}`);
}
return response.json();
}
// Convert array response to objects with headers
function arrayToObjects(data: any[][]): Record<string, any>[] {
if (!data || data.length < 2) return [];
const headers = data[0];
return data.slice(1).map(row => {
const obj: Record<string, any> = {};
headers.forEach((header: string, i: number) => {
obj[header] = row[i];
});
return obj;
});
}
const server = new Server(
{ name: "census-api", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "query",
description: "Fetch Census data. Returns structured JSON with column headers. Common datasets: acs/acs1 (American Community Survey 1-year), acs/acs5 (5-year), dec/pl (Decennial Census).",
inputSchema: {
type: "object",
properties: {
year: { type: "string", description: "Year (e.g., '2022')" },
dataset: { type: "string", description: "Dataset path (e.g., 'acs/acs1', 'acs/acs5', 'dec/pl')" },
variables: { type: "string", description: "Comma-separated variable codes (e.g., 'NAME,B19013_001E,B01003_001E')" },
forClause: { type: "string", description: "Geography 'for' clause (e.g., 'state:*', 'county:*', 'state:06,48')" },
inClause: { type: "string", description: "Optional geography 'in' clause for nested geographies (e.g., 'state:06' when getting counties)" },
},
required: ["year", "dataset", "variables", "forClause"],
},
},
{
name: "list_datasets",
description: "List available Census datasets for a given year",
inputSchema: {
type: "object",
properties: {
year: { type: "string", description: "Year (e.g., '2022')" },
},
required: ["year"],
},
},
{
name: "list_variables",
description: "List variables available in a dataset. Returns variable codes, labels, and concepts.",
inputSchema: {
type: "object",
properties: {
year: { type: "string", description: "Year (e.g., '2022')" },
dataset: { type: "string", description: "Dataset path (e.g., 'acs/acs1')" },
search: { type: "string", description: "Optional: filter variables by keyword in label (e.g., 'income', 'population')" },
limit: { type: "number", description: "Max results to return (default 50)" },
},
required: ["year", "dataset"],
},
},
{
name: "list_geographies",
description: "List available geographic levels for a dataset",
inputSchema: {
type: "object",
properties: {
year: { type: "string", description: "Year (e.g., '2022')" },
dataset: { type: "string", description: "Dataset path (e.g., 'acs/acs1')" },
},
required: ["year", "dataset"],
},
},
{
name: "get_fips",
description: "Get FIPS codes for a geography level. Useful for finding state/county codes.",
inputSchema: {
type: "object",
properties: {
year: { type: "string", description: "Year (e.g., '2022')" },
dataset: { type: "string", description: "Dataset path (e.g., 'acs/acs1')" },
geography: { type: "string", description: "Geography level (e.g., 'state', 'county')" },
inClause: { type: "string", description: "Optional parent geography (e.g., 'state:06' for counties in California)" },
},
required: ["year", "dataset", "geography"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "query": {
const { year, dataset, variables, forClause, inClause } = args as any;
let url = `${BASE_URL}/${year}/${dataset}?get=${variables}&for=${forClause}&key=${API_KEY}`;
if (inClause) {
url += `&in=${inClause}`;
}
const data = await censusApiRequest(url);
const result = arrayToObjects(data);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "list_datasets": {
const { year } = args as any;
const url = `${BASE_URL}/${year}.json`;
const data = await censusApiRequest(url);
// Extract dataset info
const datasets = data.dataset?.map((d: any) => ({
identifier: d.c_dataset?.join("/") || d.identifier,
title: d.title,
description: d.description,
})) || [];
return {
content: [{ type: "text", text: JSON.stringify(datasets.slice(0, 30), null, 2) }],
};
}
case "list_variables": {
const { year, dataset, search, limit = 50 } = args as any;
const url = `${BASE_URL}/${year}/${dataset}/variables.json`;
const data = await censusApiRequest(url);
let variables = Object.entries(data.variables || {}).map(([code, info]: [string, any]) => ({
code,
label: info.label,
concept: info.concept,
}));
// Filter by search term if provided
if (search) {
const searchLower = search.toLowerCase();
variables = variables.filter(v =>
v.label?.toLowerCase().includes(searchLower) ||
v.concept?.toLowerCase().includes(searchLower)
);
}
// Sort by code and limit
variables = variables
.filter(v => !v.code.startsWith("_")) // Exclude internal variables
.sort((a, b) => a.code.localeCompare(b.code))
.slice(0, limit);
return {
content: [{ type: "text", text: JSON.stringify(variables, null, 2) }],
};
}
case "list_geographies": {
const { year, dataset } = args as any;
const url = `${BASE_URL}/${year}/${dataset}/geography.json`;
const data = await censusApiRequest(url);
const geos = data.fips?.map((g: any) => ({
name: g.name,
geoLevelDisplay: g.geoLevelDisplay,
requires: g.requires,
})) || [];
return {
content: [{ type: "text", text: JSON.stringify(geos, null, 2) }],
};
}
case "get_fips": {
const { year, dataset, geography, inClause } = args as any;
let url = `${BASE_URL}/${year}/${dataset}?get=NAME&for=${geography}:*&key=${API_KEY}`;
if (inClause) {
url += `&in=${inClause}`;
}
const data = await censusApiRequest(url);
const result = arrayToObjects(data);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
isError: true,
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Census API MCP server running");
}
main().catch(console.error);