Query FDA data on drugs, food, devices, and recalls via openFDA. STDIO or Streamable HTTP.
Query FDA data on drugs, food, devices, and recalls via openFDA. STDIO or Streamable HTTP.
Public Hosted Server: https://openfda.caseyjhand.com/mcp
Fourteen tools for querying FDA data across drugs, food, devices, animal/veterinary products, and recalls — plus an optional DataCanvas SQL surface for large result sets:
| Tool | Description |
|---|---|
openfda_drug_profile | One drug name → consolidated FDA profile: identity, label, adverse events, recalls, approval, shortage |
openfda_search_adverse_events | Search adverse event reports across drugs, food, and devices |
openfda_search_animal_events | Search adverse event reports for veterinary drugs and devices |
openfda_search_drug_shortages | Search FDA drug shortage records — status, availability, therapeutic category, manufacturer |
openfda_search_tobacco_reports | Search problem reports for tobacco products, e-cigarettes, and vaping devices |
openfda_search_recalls | Search enforcement reports and recall actions across drugs, food, and devices |
openfda_count_values | Aggregate and tally unique values for any field across any openFDA endpoint |
openfda_describe_fields | Return searchable field paths for an openFDA endpoint, grouped by category |
openfda_get_drug_label | Look up FDA drug labeling (package inserts / SPL documents) |
openfda_search_drug_approvals | Search the Drugs@FDA database for NDA/ANDA application approvals |
openfda_search_device_clearances | Search FDA device premarket notifications — 510(k) clearances and PMA approvals |
openfda_lookup_ndc | Look up drugs in the NDC (National Drug Code) Directory |
openfda_dataframe_query | Run read-only SQL over a result set staged on a DataCanvas (opt-in) |
openfda_dataframe_describe | List tables and column schemas staged on a DataCanvas (opt-in) |
The nine record-returning tools bound their page at ~24 KB. openFDA record size is not something a caller can see in advance — a drug/event report averages ~34 KB against ~440 bytes for a food/event report, so limit: 10 means very different things per endpoint. The eight multi-row search tools return as many records of the requested window as fit, then report the withheld count on page_omitted alongside the byte figure and the routes to the rest (the exact skip to continue from, a lower limit, stage: true for SQL, or openfda_count_values for a distribution). Whenever anything matched, at least one record comes back, whatever it measures. openfda_get_drug_label returns a section outline instead, since its payload is one document rather than many rows.
openfda_drug_profileResolve one drug name to its FDA identity, then return a consolidated profile in a single call — replacing four or five chained lookups.
null rather than failing the whole call; use the dedicated tool for a deep dive into any areaopenfda_search_adverse_eventsSearch adverse event reports across drugs, food, and devices. Use to investigate safety signals, find reports for a specific product, or explore reactions by demographics.
drug, food, or device — each returns different field schemaslimit (up to 1000) and skip (openFDA's ceiling is 25000 — past it the call returns a typed pagination_limit_reached error with recovery guidance)drug/event is the largest-record endpoint openFDA serves, so this is where the ~24 KB page budget bites hardest: a default limit: 10 on drug reports typically returns one to three of them with the rest disclosed on page_omitted, where the same call on food reports returns all tenopenfda_count_valuesAggregate and tally unique values for any field across any openFDA endpoint. Returns ranked term-count pairs sorted by count descending.
.exact suffix on field names for whole-phrase countingsearch filter to scope the aggregationopenfda_search_recallsSearch enforcement reports and recall actions across drugs, food, and devices.
enforcement (all categories) and recall (devices only) endpointsopenfda_search_device_clearancesSearch FDA device premarket notifications — 510(k) clearances and PMA approvals.
510k (174K+ records, most common) and pma (higher-risk devices)openfda_get_drug_labelLook up FDA drug labeling (package inserts / SPL documents). Check indications, warnings, dosage, contraindications, active ingredients, or any structured label section.
content[] and structuredContent carry the same textkind: "outline" — the section names and their sizes — instead of the label text; re-call with sections: [...] for the ones you needlimit — the re-call guidance names a section measured to fit the budget at the requested limit and quotes its byte sizesections narrows each record to the requested keys plus metadata (openfda, set_id, id, effective_time, version); a selection over the budget is returned whole, never trimmed, with its serialized size reportedopenfda_search_drug_approvalsSearch the Drugs@FDA database for drug application approvals (NDAs and ANDAs). Returns application details, sponsor info, and full submission history.
limit (up to 1000) and skip (openFDA's ceiling is 25000 — past it the call returns a typed pagination_limit_reached error with recovery guidance)openfda_lookup_ndcLook up drugs in the NDC (National Drug Code) Directory. Identify drug products by NDC code, find active ingredients, packaging details, or manufacturer info.
openfda_search_animal_eventsSearch adverse event reports for veterinary drugs and devices submitted to the FDA Center for Veterinary Medicine (1.3M+ records).
openfda_search_tobacco_reportsSearch problem reports submitted to the FDA for tobacco products, including e-cigarettes, vaping products, cigarettes, and smokeless tobacco.
openfda_search_drug_shortagesSearch FDA drug shortage records (1,700+ entries, refreshed daily). Returns shortage status, availability notes, therapeutic category, dosage form, manufacturer, and timeline.
Current, Resolved), therapeutic category, generic name, or manufactureropenfda block carries brand_name, product_ndc, and rxcui for chaining into openfda_get_drug_label or openfda_lookup_ndclimit (up to 1000) and skip (openFDA's ceiling is 25000 — past it the call returns a typed pagination_limit_reached error with recovery guidance)openfda_describe_fieldsReturn the searchable field paths for an openFDA endpoint, grouped by category with type and description. Use before constructing a search query to discover the correct dotted field paths.
drug/event, drug/label, drug/shortages, drug/drugsfda, drug/ndc, drug/enforcement, food/event, food/enforcement, device/event, device/510k, device/pma, device/recall, device/enforcement, animalandveterinary/event, tobacco/problemnotice enrichment when a search returns emptyopenfda_dataframe_query · openfda_dataframe_describeA DataCanvas SQL surface over staged result sets — opt-in, enabled with CANVAS_PROVIDER_TYPE=duckdb and requested per call with stage: true.
stage: true to drain its matched set into a DuckDB table alongside the normal page of results; the response adds canvas_id, canvas_table, and staged_rows. openfda_dataframe_query runs read-only SELECT (GROUP BY, SUM/COUNT, joins) across the staged rows; openfda_dataframe_describe lists the table and column schemas needed to write valid SQL.drug/event. staged_rows against the match total says how much reached the table; truncated flags the cut and points at openfda_count_values, which aggregates over the whole matched set server-side rather than over the staged slice.CAST for numeric math); nested openFDA blocks (openfda, patient, products, …) are JSON columns. Pass a canvas_id back into a search tool to accumulate result sets on one canvas for cross-table joins.CANVAS_PROVIDER_TYPE=duckdb and an explicit staging request, a search costs one upstream request and the two dataframe tools report that canvas is disabled. Requires the optional @duckdb/node-api dependency; unsupported on Cloudflare Workers.Built on @cyanheads/mcp-ts-core:
none, jwt, oauth)in-memory, filesystem, Supabase, Cloudflare KV/R2/D1openFDA-specific:
content[] and structuredContent, never silently truncated and never emptiedCANVAS_PROVIDER_TYPE=duckdb, per call with stage: true) — stage large result sets as DuckDB tables and run SQL via openfda_dataframe_queryOPENFDA_MIRROR_ENABLED=true) — a self-refreshing SQLite copy of the four drug bulk downloads that answers exact-key lookups without spending API budget, with live fallbackA public instance is available at https://openfda.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"openfda-mcp-server": {
"type": "streamable-http",
"url": "https://openfda.caseyjhand.com/mcp"
}
}
}
Add to your MCP client config:
{
"mcpServers": {
"openfda-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/openfda-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"OPENFDA_API_KEY": "your-key-here"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"openfda-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/openfda-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"OPENFDA_API_KEY": "your-key-here"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"openfda-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/openfda-mcp-server:latest"]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
git clone https://github.com/cyanheads/openfda-mcp-server.git
cd openfda-mcp-server
bun install
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
| Variable | Description | Default |
|---|---|---|
MCP_TRANSPORT_TYPE | Transport: stdio or http | stdio |
MCP_HTTP_PORT | HTTP server port | 3010 |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.) | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1 | in-memory |
OPENFDA_API_KEY | Free API key from open.fda.gov. Increases daily limit from 1K to 120K requests. | none |
OPENFDA_BASE_URL | Base URL override for testing against a proxy or mock. | https://api.fda.gov |
OPENFDA_MIRROR_ENABLED | Answer exact-key lookups from a local copy of the openFDA bulk downloads instead of the API. See Local bulk mirror. | false |
OPENFDA_MIRROR_PATH | Directory holding one SQLite file per mirrored dataset. | ./data/openfda-mirror |
OPENFDA_MIRROR_REFRESH_CRON | Cron expression for the in-process mirror refresh (HTTP transport only). Unset means no scheduled refresh. | none |
OPENFDA_MIRROR_FALLBACK_LIVE | Fall back to the live API when the mirror is cold, missing the record, or failing. | true |
OPENFDA_MIRROR_REFRESH_TIMEOUT_MS | Wall-clock budget for one refresh before it is aborted. | 21600000 (6h) |
OPENFDA_MIRROR_BASE_URL | Host serving the bulk download manifest (download.json). | https://api.fda.gov |
CANVAS_PROVIDER_TYPE | Set to duckdb to enable DataCanvas staging — analytical SQL over result sets staged with stage: true and queried via openfda_dataframe_query. Requires the optional @duckdb/node-api dependency; unsupported on Cloudflare Workers. | none (disabled) |
OTEL_ENABLED | Enable OpenTelemetry | false |
openFDA publishes whole-dataset JSON dumps alongside the API. With OPENFDA_MIRROR_ENABLED=true the server keeps a local SQLite copy of four of them — drug/label, drug/ndc, drug/enforcement, drug/drugsfda — and answers eligible lookups from it, leaving the API budget for everything else.
The mirror is deliberately narrow. openFDA's search runs server-side in Elasticsearch, which tokenises and ranks; a local corpus cannot reproduce that. A query is answered locally only when all of the following hold, and is sent to the API otherwise:
field:"value" term — no boolean operators, wildcards, or ranges;id, set_id, product_id, product_ndc, recall_number, event_id, application_number, and the value is a whole identifier in its canonical spelling and case;count and no sort, and skip is 0;The last condition is what keeps a mirrored answer identical to the API's rather than merely equivalent. Four of the seven lookup fields are primary keys and always match one record. The other three — set_id, product_ndc, event_id — can address several, and openFDA returns those in relevance order, which a local corpus cannot recompute; such a lookup routes to the API whatever the requested page size.
openfda_count_values therefore always runs against the API — a partial mirror would return plausible but incomplete aggregates.
The initial harvest runs out-of-band, never at startup:
bun run mirror:init # all four datasets
bun run mirror:init drug/enforcement # one dataset (~3.8 MB compressed)
bun run mirror:status # sync state per dataset
bun run mirror:verify # integrity check + row counts
bun run mirror:refresh # re-harvest datasets whose dump has advanced
openFDA publishes no incremental API for these endpoints, so a refresh re-reads the whole dump and tombstones records the new export no longer carries. It is idempotent and resumable — re-running after an interrupt continues from the persisted cursor. Set OPENFDA_MIRROR_REFRESH_CRON to run it in-process on the HTTP transport; on stdio, run bun run mirror:refresh from the host.
meta.lastUpdated on a mirrored response reports the last_updated stamp of the dump being served, which can differ from the live API's — the API index and the published dumps advance on separate schedules.
On Node, install the optional better-sqlite3 peer dependency; Bun uses its built-in bun:sqlite. OPENFDA_MIRROR_REFRESH_CRON additionally needs the optional node-cron peer dependency — without it the server refuses to start rather than run with a schedule it cannot honour. The mirror is unavailable on Cloudflare Workers (no SQLite, no persistent filesystem) and stays off there.
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
| Directory | Purpose |
|---|---|
src/index.ts | Entry point — createApp() with tool registration and service setup. |
src/config/ | Server-specific env var parsing and validation with Zod. |
src/services/openfda/ | openFDA API client with retry, rate-limit handling, and error normalization. |
src/services/openfda/mirror/ | Opt-in local bulk mirror — dataset registry, dump reader, sync ingester, and the query gate that decides mirror vs live. |
src/mcp-server/tools/definitions/ | Tool definitions (*.tool.ts). Fourteen openFDA tools. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped loggingsrc/mcp-server/tools/definitions/index.tsIssues and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Data is served from openFDA, a U.S. Food and Drug Administration service. Under the openFDA license the data is dedicated to the public domain under CC0 1.0, with one exception: GMDN® device-classification content — Term Code, Term Name, and Term Definition — is licensed from The GMDN Agency, and redistributing it or using it to train AI requires a separate licence from the Agency.
The local mirror therefore covers drug datasets only. device/classification and every other device endpoint are excluded from it, and the ingester rejects any record carrying a GMDN-bearing field rather than writing it to disk. Extending the mirror to device data requires clearing that licence first.
FDA does not endorse this project. Do not rely on openFDA to make decisions regarding medical care.
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
cyanheads/openstreetmap-mcp-server-c6896a9c
cyanheads/pubchem-mcp-server-495646fa
cyanheads/socrata-mcp-server-9996f211
cyanheads/sports-mcp-server
cyanheads/usaspending-mcp-server-83b0720b
cyanheads/usgs-water-mcp-server