@cyanheads/cdc-health-mcp-server
Search and query CDC public health data — mortality, vaccinations, surveillance, behavioral risk (Socrata SODA API) via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://cdc.caseyjhand.com/mcp
Overview
CDC public health data from the CDC Open Data portal and CDC WONDER. Search 1,000+ datasets on vaccination, surveillance, and behavioral risk, inspect their schemas and query them with SoQL, or pull national mortality statistics by year, month, age, sex, race, Hispanic origin, or cause of death.
Tools
| Tool | Description |
|---|---|
cdc_discover_datasets | Search the CDC dataset catalog by keyword, category, tag, or asset type |
cdc_get_dataset_schema | Fetch column schema, row count, and metadata for a dataset |
cdc_list_catalog_vocabulary | List the catalog's category and tag values with their entry counts |
cdc_query_dataset | Execute SoQL queries — filter, aggregate, sort, full-text search, select fields |
cdc_query_wonder | Query CDC WONDER for national mortality, population, and death rates across five databases |
Resources
| Resource | Description |
|---|---|
cdc://datasets | Top 50 most-viewed catalog entries, for orientation |
cdc://datasets/{datasetId} | Dataset metadata and the first 100 columns for a specific dataset |
Both resources mirror data also reachable via cdc_discover_datasets and cdc_get_dataset_schema, for clients that surface resources but not tools.
Prompts
| Prompt | Description |
|---|---|
analyze_health_trend | Guided workflow for investigating a public health question across CDC data |
Skill
| Skill | Description |
|---|---|
cdc-health-guide | Gates for agents comparing or trending results: which WONDER rate to compare across groups, race beside Hispanic origin, the provisional database's partial year and withheld causes, finding a peak month, splicing WONDER databases, and reading stratified Socrata tables |
Ships in skills/ for plugin installs, the npm package, the Docker image, and the .mcpb bundle, and is served over MCP as skill://cdc-health-guide/SKILL.md.
Capability reference
cdc_discover_datasets tool
- Filters:
query,category,tags, andasset_types(dataset,filter,chart,map,story,file,href;["dataset", "filter"]keeps the entries with rows to query).tagsunion — a dataset matches when it carries any one of them, so each tag added widens the result set — whilequery,category, andasset_typesintersect with them.limitdefaults to 10;offset + limitis capped at 10,000 (page_out_of_rangepast it).orderdefaults torelevancewhenqueryis set anddataset_idotherwise; relevance order varies between calls, so onlydataset_idpages every match exactly once - Results carry
assetType,permalink,columnCount(0 marks a chart, map, or other non-tabular asset), an 8-namecolumnSample, and a description cut to 300 characters, plustotalCountandappliedFilters. A link (href) entry's destinations are inaccessPoints, and the datasets a chart, map, filter, or story is built on inparentIds - When a
categoryortagsvalue matches nothing, thenoticenames the closest catalog values with their entry counts, e.g.Vaccination→"Vaccinations" (89 datasets)
cdc_get_dataset_schema tool
- Takes a four-by-four
datasetId(e.g.bi63-dtpu).column_limit(default 100) andcolumn_offsetpage wide schemas, reportingtotalCount,truncated, andnextOffset - The dataset
descriptionis capped at 4,000 characters by default (descriptionTruncatedanddescriptionLengthare set when cut);full_description: truereturns the whole text - Columns carry
fieldName,dataType, anddescription.rowCountSourcesays whetherrowCountisliveor Socrata'scachedfigure - Where the publisher recorded them,
temporalCoverage,updateFrequency,geographicCoverage,citation,license, andattributioncome back as the publisher's own text, and a filter carriesparentId, the dataset it is a view of - A chart or map fails with
not_queryable, naming the dataset it is built on
cdc_list_catalog_vocabulary tool
- Every category comes back whole; the ~1,600 tags are ranked by entry count and paged with
tag_limit(default 50) andtag_offset.filter(up to 500 characters) narrows both by word match, not fuzzy matching; a word under 3 letters or digits matches whole words only - Each value carries
datasetCount; the response also reportsvocabularySize,categoryCount,tagCount,truncated,nextOffset, andtruncationCeiling(an upper bound on the counts of the tags after the page)
cdc_query_dataset tool
- Clauses:
select,where,group,having,order, and full-textsearch. Up to 1,000 rows per call (default 100; a largerlimitis clamped, and the notice names the clamp when rows remain),offsetup to 1,000,000 - For stable offset paging, set an
orderending in a unique tie-breaker (:idon ungrouped rows, the grouped columns on a grouped query); the notice says so when it is missing effectiveQueryechoes the SoQL clauses as sent;truncatedandnextOffsetreport paging, with nototalCount. A chart or map ID fails withnot_queryable, naming the dataset it is built on
cdc_query_wonder tool
database | CDC database | Years | Race groups | mcd_icd10 |
|---|---|---|---|---|
underlying_1999_2020 (default) | D76 — Underlying Cause of Death | 1999–2020 | 4 bridged | — |
provisional | D176 — Provisional Mortality Statistics | 2018 → current year (latest year partial) | 6 single-race | yes |
underlying_2018_2024 | D158 — Underlying Cause of Death, Single Race | 2018–2024 | 6 single-race | — |
multiple_1999_2020 | D77 — Multiple Cause of Death | 1999–2020 | 4 bridged | yes |
multiple_2018_2024 | D157 — Multiple Cause of Death, Single Race | 2018–2024 | 6 single-race | yes |
group_bytakes 1–4 ofyear,month,age_group,sex,race,hispanic_origin,icd_chapter,icd_subchapter,cause_113,leading_causes(alone only), andinjury_intent; results are always national. A grouping that could produce more than 10,000 rows is refused before any request.cause_icd10andmcd_icd10take an ICD-10 code, a range (X40-X49), or a list of up to 50.age_groupstakesNS(age not stated) alone or with all eleven known groups.racereports four bridged groups on the 1999–2020 databases and six single-race groups on the rest; the two families' series are not comparablelimitpages the table fetched from CDC; omitted, it returns the rest of the table fromoffset, up to 10,000 rows per call. A largerlimitis lowered to that ceiling, which equals the table-size bound, so one call always holds the whole table.offsetgoes up to 10,000- Rows carry
deaths,population,crude_rate, andage_adjusted_ratewhere age can be standardized, each rate followed by its 95% confidence interval (crude_rate_lowerandcrude_rate_upper,age_adjusted_rate_lowerandage_adjusted_rate_upper),nullwhere CDC sent none. Chapter and sub-chapter rows addcause_code, the ICD-10 rangecause_icd10takes;cause_113rows addcause_level;monthrows carry deaths only. Cells CDC returns asSuppressed,Unreliable, orNot Applicablereadnulland are listed incellNotes;messagesreports rows CDC hid, andtotalCountis exact - The
provisionaldatabase's latest year covers part of the year against a full-year population, and CDC withholds some recent causes; anoticesays so whenever a query reaches either, and on amonthgrouping names the month the data ends in as partial - CDC rejects requests less than 15 seconds apart, so calls are queued one at a time and spaced 16 seconds apart. A request WONDER has not finished answering within 20 seconds fails as a retryable
upstream_error
cdc://datasets resource
- Top 50 catalog entries by popularity, each with
id,name,assetType,category,columnCount, andupdatedAt, plus the catalogtotalCount columnCount: 0marks a non-tabular entry;cdc_discover_datasetssearches and pages the full catalog
cdc://datasets/{datasetId} resource
- Dataset metadata plus the first 100 columns as
application/json, withcolumnCountandtruncated; the description is capped at 4,000 characters. It carries the sameparentIdand publisher fields ascdc_get_dataset_schema, and a chart or map fails withnot_queryable, naming its parent dataset - For more columns or the full description, use
cdc_get_dataset_schemawithcolumn_offsetorfull_description: true
analyze_health_trend prompt
- Arguments:
topicrequired;timeRangeandgeographyoptional - Returns one user message that routes the question to
cdc_query_wonder(national mortality) or the Socrata tools (everything else), then walks discover → inspect → baseline → compare → synthesize
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
CDC-specific:
- The Socrata SODA API v2.1 over the CDC Open Data portal (~1,080 datasets); no auth required, and an optional app token raises rate limits
- CDC WONDER mortality data from five databases covering 1999 through the current year, reached over its XML request API
- The four Socrata tools take a
domaininput, limited todata.cdc.gov(default) andchronicdata.cdc.gov; both serve the same catalog categoryandtagsmust match the catalog's own values (matched ignoring case, otherwise exactly);cdc_list_catalog_vocabularylists them- Socrata returns values as strings (geo columns as GeoJSON objects), and a column like
yearis numeric in some datasets and text in others. Match WHERE literals to the column'sdataType: numbers bare, strings single-quoted
Agent-friendly output:
- Paging is reported explicitly:
totalCount,truncated,nextOffset, andshown/cap.limitsets the page:cdc_query_datasetclamps it to 1,000 rows, naming the clamp in thenoticewhen rows remain, andcdc_query_wonderlowers it to 10,000, its table-size bound; up to those ceilings a page ends short oflimitonly at the end of the result set - Typed errors with a recovery hint on every declared reason, such as
not_queryable,page_out_of_range, andinvalid_query - Upstream gaps stay visible: WONDER status tokens in
cellNotes, hidden-row notices inmessages, androwCountSourceon schema row counts effectiveQueryechoes the SoQL clauses or a WONDER query summary, so a result can be reproduced
Getting started
Connect to the hosted instance, no install:
claude mcp add --transport http cdc-health-mcp-server https://cdc.caseyjhand.com/mcp
Or run it locally:
claude mcp add cdc-health-mcp-server -- npx -y @cyanheads/cdc-health-mcp-server
For other clients, add this to the MCP config file:
{
"mcpServers": {
"cdc-health-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/cdc-health-mcp-server"]
}
}
}
Setting CDC_APP_TOKEN to a Socrata app token raises the Socrata rate limits. For Docker or a source build, see Running the server.
Configuration
| Variable | Description | Default |
|---|---|---|
CDC_APP_TOKEN | Socrata app token; raises rate limits. | none |
CDC_BASE_URL | SODA host for requests with no domain (the cdc://datasets/{datasetId} resource). | https://data.cdc.gov |
CDC_CATALOG_URL | Socrata Discovery API base URL. | https://api.us.socrata.com/api/catalog/v1 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
OTEL_ENABLED | Enable OpenTelemetry. | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+).
-
Clone and install:
git clone https://github.com/cyanheads/cdc-health-mcp-server.git cd cdc-health-mcp-server && bun install cp .env.example .env # optionally set CDC_APP_TOKEN -
Build and run the production version:
# One-time build bun run rebuild # Run the built server bun run start:http # or bun run start:stdio -
Run checks and tests:
bun run devcheck # Lints, formats, type-checks, and more bun run test # Runs the test suite
Docker
docker run --rm -p 3010:3010 ghcr.io/cyanheads/cdc-health-mcp-server:latest
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/cdc-health-mcp-server. To run it over stdio from an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/cdc-health-mcp-server:latest"]. To build locally, docker build -t cdc-health-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools/resources/prompts and inits services. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts). Four Socrata tools and one WONDER tool. |
src/mcp-server/resources | Resource definitions. Catalog overview and dataset detail. |
src/mcp-server/prompts | Prompt definitions. Health trend analysis workflow. |
src/services/socrata | Socrata service — catalog search, vocabularies, metadata, SoQL queries. |
src/services/wonder | CDC WONDER service — XML request builder, response parser, request spacing. |
src/utils | Shared helpers — HTML-to-text, table-cell escaping, vocabulary matching. |
skills/cdc-health-guide | Agent skill published to agents that install the server, and served over MCP as skill://cdc-health-guide/SKILL.md. |
tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor logging,ctx.statefor storage - Register new tools and resources in the
createApp()arrays insrc/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.