Skip to main content
Glama
zenlyticgreg

census-mcp-server

by zenlyticgreg

census-mcp-server

Production-grade Model Context Protocol server for the U.S. Census Bureau Data API, backed by a PostgreSQL cache layer for FIPS geography mappings and variable metadata.

Tools

Tool

Purpose

get_census_data

Fetch ACS 5-Year / Decennial data with multi-variable chunked batching and geographic nesting (e.g. all tracts in a county).

search_census_variables

Full-text search over cached variable metadata (labels, concepts, table codes).

resolve_geography

Fuzzy-resolve place names → FIPS codes via a lazily hydrated pg_trgm cache.

Live deployment: https://census-mcp-server-production-4b30.up.railway.app/mcp (streamable HTTP, Railway, bearer-token protected — see Deployed on Railway below).

All tools support response_format: 'markdown' | 'json', return structured content alongside text, and are annotated read-only/idempotent.

Supported datasets

id

Dataset

Vintages

acs5

ACS 5-Year Detailed Tables

2009–2023

acs5_profile

ACS 5-Year Data Profiles

2010–2023

acs5_subject

ACS 5-Year Subject Tables

2010–2023

dec_pl

Decennial Redistricting (P.L. 94-171)

2000, 2010, 2020

dec_dhc

Decennial DHC

2020

dec_sf1

Decennial Summary File 1

2000, 2010

Architecture

src/
├── index.ts                  Bootstrap + transports (stdio / streamable HTTP)
├── server.ts                 McpServer factory (composition root)
├── config/env.ts             Zod-validated environment config (fail-fast)
├── constants.ts              Limits, sentinels, state abbreviations
├── errors/census-error.ts    CensusError taxonomy + remediation hints
├── http/census-http-client.ts  fetch wrapper: retries, backoff+jitter, timeouts
├── logging/logger.ts         pino → stderr (stdout reserved for MCP frames)
├── schemas/tool-schemas.ts   Zod input/output schemas for every tool
├── tools/                    Controllers: schema ↔ service ↔ response shaping
├── services/                 CensusApi / CensusData / Geography / VariableSearch / Database
├── repositories/             SQL access: geographies, variables, ingestions
├── db/                       Embedded migrations + advisory-locked runner + CLI
└── utils/                    Bounded concurrency, name normalization, Markdown

Design notes:

  • Resilience — upstream 429/5xx/timeouts retry with exponential backoff + full jitter, honoring Retry-After. Errors map onto a typed CensusError taxonomy (rate limits, invalid FIPS, invalid variables, missing API key, ...) with agent-actionable hints.

  • Chunked batching — variable lists split into API-sized chunks (48/request), fetched with bounded concurrency, and merged on geography keys. Cache hydration writes in 1,000-row batched unnest() upserts. Responses are row-capped (max_rows) and character-capped with explicit truncation flags.

  • Cache layer — geography names (~32k places) and variable catalogs (~28k entries) hydrate lazily on first use, then serve locally: trigram-indexed fuzzy search for names, GIN-indexed full-text search for variables. Migrations are embedded, idempotent, and serialized with a PostgreSQL advisory lock.

Quickstart (local, stdio)

Requires Node 20+ and a reachable PostgreSQL 14+ instance.

npm install                 # also generates package-lock.json (needed for Docker builds)
cp .env.example .env        # set CENSUS_API_KEY and DATABASE_URL
npm run build
npm run db:migrate          # optional; the server also migrates at startup
npm run start:local         # loads .env via node --env-file

(npm start runs without loading .env — it expects the environment to be provided externally, as in Docker or an MCP client config.)

Get a free API key at https://api.census.gov/data/key_signup.html.

Claude Desktop / Claude Code registration

{
  "mcpServers": {
    "census": {
      "command": "node",
      "args": ["/absolute/path/to/census-mcp-server/dist/index.js"],
      "env": {
        "CENSUS_API_KEY": "your-key",
        "DATABASE_URL": "postgres://census:change_me@localhost:5432/census_cache"
      }
    }
  }
}

Docker deployment (streamable HTTP)

npm install          # generate package-lock.json before the first image build
cp .env.example .env # set CENSUS_API_KEY and POSTGRES_PASSWORD
docker compose up --build -d
curl http://localhost:3000/healthz
  • Multi-stage build: TypeScript compiles in a build stage; the runtime stage ships only production node_modules + dist, running as a non-root mcp user.

  • Healthchecks: pg_isready for PostgreSQL; a Node probe against /healthz (which verifies database connectivity) for the server. The server only starts after PostgreSQL is healthy.

  • Network isolation: PostgreSQL lives on an internal-only network with no published ports; data persists in the census_pgdata volume.

  • MCP endpoint: POST http://localhost:3000/mcp (stateless JSON mode).

Scripts

Command

Description

npm run build

Compile TypeScript to dist/

npm start

Run the compiled server (env provided externally)

npm run start:local

Run the compiled server, loading .env

npm run dev

Watch-mode development server (tsx)

npm run lint

ESLint (strict: no any, explicit return types)

npm run typecheck

tsc --noEmit

npm run db:migrate

Apply pending schema migrations and exit

Example workflow

  1. resolve_geography{ "name": "San Francisco", "level": "county", "state": "CA" }state_fips: "06", geo_code: "075".

  2. search_census_variables{ "query": "median household income" }B19013_001E.

  3. get_census_data

    {
      "variables": ["B19013_001E", "B01001_001E"],
      "geography": {
        "level": "tract",
        "codes": ["*"],
        "within": [
          { "level": "state", "code": "06" },
          { "level": "county", "code": "075" }
        ]
      }
    }

Deployed on Railway

The server is deployed at project census-mcp-server in Railway (workspace: Greg Peters's Projects), built directly from this repo's Dockerfile on every push to master:

  • App service census-mcp-server — env vars TRANSPORT=http, PORT=3000, LOG_LEVEL=info, CENSUS_API_KEY, MCP_AUTH_TOKEN, and DATABASE_URL set as a Railway reference variable (${{Postgres.DATABASE_URL}}) pointing at the sibling Postgres service — so the cache survives redeploys and the connection string is never duplicated.

  • Postgres service — managed Railway PostgreSQL; migrations run automatically at container startup (see src/db/migration-runner.ts).

  • Public domain — a generated *.up.railway.app domain targets container port 3000. /mcp requires Authorization: Bearer <MCP_AUTH_TOKEN>; /healthz is open for Railway's health probes.

To redeploy: push to master (Railway auto-builds), or run railway redeploy --service census-mcp-server --yes --from-source to force a fresh pull. To rotate secrets: railway variable set CENSUS_API_KEY=<new-key> --service census-mcp-server (a set triggers a redeploy unless --skip-deploys is passed).

Security notes

  • The API key is only read from the environment, appended per-request, and redacted from all log output.

  • Every tool input is validated by Zod schemas (strict bounds and patterns) before any I/O.

  • Internal errors are logged server-side and never leaked to MCP clients verbatim.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zenlyticgreg/census-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server