About openfda-mcp-server
openfda-mcp-server gives MCP clients access to public FDA data on drugs, food, devices, and recalls. It also supports field aggregation and read-only SQL over staged results, and offers STDIO and Streamable HTTP transports.
Key features
- 01Search adverse events, recalls, and shortages
- 02Look up labels, approvals, and NDCs
- 03Find device clearances and tobacco reports
- 04Aggregate field values and inspect schemas
- 05Stage results and run read-only SQL
Use cases
- 01Review a drug's label, adverse events, and recalls
- 02Check drug shortages or FDA approvals
- 03Explore adverse events for food, devices, or drugs
- 04Run SQL over staged query results
MCP support
Connects MCP clients to openFDA data through 15 tools for searching, lookup, aggregation, and staged-result analysis. Supports STDIO and Streamable HTTP.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://openfda.caseyjhand.com/mcpclaude mcp add --transport http cyanheads-openfda https://openfda.caseyjhand.com/mcp{"mcpServers":{"cyanheads-openfda":{"type":"http","url":"https://openfda.caseyjhand.com/mcp"}}}Open in Cursor{"servers":{"cyanheads-openfda":{"type":"http","url":"https://openfda.caseyjhand.com/mcp"}}}Open in VS Code{"mcpServers":{"cyanheads-openfda":{"type":"http","url":"https://openfda.caseyjhand.com/mcp"}}}{"mcpServers":{"cyanheads-openfda":{"serverUrl":"https://openfda.caseyjhand.com/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
Tools
14| Parameter | Type | Required | Description |
|---|---|---|---|
| skip | number | No | Number of records to skip for pagination (default 0). openFDA caps pagination at 25000 records; a higher value returns a pagination_limit_reached error. |
| sort | string | No | Sort expression — a field path optionally suffixed with :asc or :desc (any letter case; :ascending and :descending are also accepted, and every direction is sent as asc or desc); comma-separate for multi-field sort. Field paths take only letters, digits, underscores, and dots; any other character or direction is rejected before the request. Sortable date fields are category-specific: drug → receivedate:desc (or receiptdate), food → date_created:desc (or date_started), device → date_received:desc (or date_of_event). A field from another category (e.g. receivedate on food or device) causes a query error — use the field for this category. |
| limit | number | No | Maximum number of records to return (1-1000, default 10). Serialized record size varies by three orders of magnitude across openFDA endpoints, so the page is also bounded by a 24000-byte serialized budget: a page that would overrun it returns fewer records than requested and reports the cut on page_omitted. Whenever any record matched, at least one comes back, however large it measures — page_bytes then reports its size and page_largest_fields names its largest fields, which fields can project away. Drug reports are by far the largest — a single drug/event report averages tens of kilobytes where a food/event report is a few hundred bytes. |
| stage | boolean | No | Stage the matched set on a DataCanvas for SQL analysis — openfda_dataframe_describe lists the staged columns, openfda_dataframe_query runs the SQL. Default false — the call returns one page for one upstream request. When true, records are also drained onto a canvas table up to a size budget (staged_rows reports how many reached it). Staging is for record-level SQL over a bounded slice; for a distribution over everything that matched, openfda_count_values aggregates server-side in one request. Requires CANVAS_PROVIDER_TYPE=duckdb. |
| fields | array | No | Dotted record paths to keep on each returned record, e.g. ["receivedate", "patient.drug.medicinalproduct"] — any key path on a returned record works, and openfda_describe_fields lists the searchable ones. Each record is projected to these paths plus its identity key (safetyreportid, report_number, mdr_report_key) before the inline byte budget is measured, identically on both response surfaces, and a path through an array applies to every element. Use it when records are large: a page_bytes over the budget comes with the largest fields of the oversized record. A path no returned record carries is named on the notice. Omit for whole records. Paths take only letters, digits, underscores, and dots; anything else is rejected before the request. With stage=true, only the inline page is projected — the staged table keeps its columns. |
| search | string | No | openFDA search query. Examples: patient.drug.medicinalproduct:"aspirin", patient.reaction.reactionmeddrapt:"nausea" AND serious:"1". Omit to browse recent. Double quotes, parentheses, and range brackets must balance, and the query must not end on a backslash — each is rejected before the request. |
| category | enum | Yes | Product category — each has different field schemas in the responsedrug | food | device |
| canvas_id | string | No | Canvas ID returned by a prior stage=true call to this tool or another openFDA search tool (openfda_search_* or openfda_lookup_ndc). Passing one stages this search onto that canvas (same effect as stage=true) so result sets accumulate for cross-table joins. Omit to stage onto a fresh canvas. |
Details
From the project README@cyanheads/openfda-mcp-server
Query FDA data on drugs, food, devices, and recalls via openFDA. STDIO or Streamable HTTP.
Public Hosted Server: https://openfda.caseyjhand.com/mcp
Overview
FDA data on drugs, food, devices, and recalls from the openFDA public API. Search adverse events, recalls, drug approvals, and device clearances; look up NDC codes and drug labels; aggregate field counts across any endpoint.
Tools
| 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 by 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_describe | List tables and column schemas staged on a DataCanvas |
openfda_dataframe_query | Run read-only SQL over a result set staged on a DataCanvas |
openfda_dataframe_drop | Delete one table staged on a DataCanvas (opt-in, off by default) |
Capability reference
openfda_drug_profile tool
- One
drugname, brand or generic, resolved once to an identity (generic name, NDC, RxCUI, SPL set ID); recalls, approval, and shortage key onmeta.fanOutKey— the typed name when it is an ingredient of the resolved generic, else that generic;meta.resolvedVia(label,ndc,none) names the source, and a single-ingredient product is preferred over combinations - Recalls and approval also search records openFDA never harmonized (
product_description,products.active_ingredients.name);recalls_totalgives the match count beside the newest 5 recalls - Sections
label,adverse_events,recalls,approval, andshortageare best-effort and come backnull(or an emptyrecalls) on a miss;degraded[]names each section whose sub-query failed upstream, so a listed section is unknown rather than absent - Auth, configuration, and cancellation failures fail the whole call instead of degrading a section
openfda_search_adverse_events tool
category(drug,food,device) selects the endpoint and its record schema; up to 1000 records per page- Sortable date fields differ by category:
receivedate(drug),date_created(food),date_received(device); another category's field fails asquery_error - Text output decodes drug administration route, reaction outcome, and patient age group codes beside the raw value (
via oral (048),fatal (5),elderly (6)); structured output keeps the codes
openfda_search_animal_events tool
- Optional
searchover veterinary reports, e.g.animal.species,drug.active_ingredients.name,reaction.veddra_term_name,serious_ae; up to 1000 records per page - Brand and manufacturer names are masked as
MSKon every record, so search by active ingredient;drug.manufacturer.registration_numbernarrows to one approval - Records carry animal species, breed, age, and weight, the drug and route, VeDDRA reaction terms, and outcome
openfda_search_drug_shortages tool
- Optional
searchoverstatus(Current/To Be Discontinued/Resolved),therapeutic_category,generic_name, orcompany_name; up to 1000 records per page - Each record's
openfdablock carriesbrand_name,product_ndc, andrxcuifor chaining intoopenfda_get_drug_labeloropenfda_lookup_ndc
openfda_search_tobacco_reports tool
- Optional
searchovertobacco_products,reported_health_problems,reported_product_problems, ornonuser_affected; up to 1000 records per page - Reports carry the problem arrays plus
number_tobacco_products,number_health_problems, andnumber_product_problemscounts
openfda_search_recalls tool
category(drug,food,device) plusendpoint:enforcement(default, every category) orrecall(devices only; other categories fail asrecall_endpoint_non_device); up to 1000 records per page- Enforcement records carry
recall_number,classification(Class I/II/III), andstatus; device recall records carryproduct_res_numberandrecall_statusinstead, with no hazard classification
openfda_count_values tool
endpoint(any of the 20 openFDA endpoints), acountfield expression, and an optionalsearch; returns up to 1000 top terms (default 100), ranked by count; a date field returns one term per distinctYYYYMMDDdate, ranked the same waytruncatedflags more distinct terms pastlimit, except at the 1000-term maximum, where a notice says completeness can't be known (a date count is exact at any limit, since openFDA sends every date); an expression openFDA can't aggregate fails asnot_aggregatable(take thecountAsform fromopenfda_describe_fields)- Always runs against the live API, even with the bulk mirror enabled
openfda_describe_fields tool
endpoint: the same 20 endpointsopenfda_count_valuesaccepts- Field paths grouped by category, each with
type, a description, andcountAs(the verified count expression, ornullwhen the field can't be aggregated), plusqueryTipson quoting, AND/OR,.exact, and date ranges
openfda_get_drug_label tool
- Required
searchon label fields (openfda.brand_name,openfda.generic_name,openfda.manufacturer_name,set_id);limitdefaults to 5, max 1000 - A page over the 24,000-byte inline budget returns
kind: "outline"(section names and sizes, largest first) instead of label text; re-call withsections: [...]to get those sections plus identity metadata (openfda,set_id,id,effective_time,version), returned whole even when over budget *_tablesections render as Markdown tables in the text output; structured results keep the raw SPL markup
openfda_search_drug_approvals tool
- Optional
searchoveropenfda.brand_name,sponsor_name(stored uppercase, so a lowercase quoted value matches nothing),submissions.submission_type, orsubmissions.review_priority; up to 1000 records per page - Records carry
application_number,sponsor_name,products[], and the application's fullsubmissions[]history
openfda_search_device_clearances tool
pathway(510korpma, one per call) plus an optionalsearchoverapplicant,product_code,advisory_committee_description, oropenfda.device_name; up to 1000 records per page- 510(k) records carry
k_number,device_name, anddecision_date; PMA records carrypma_number,trade_name, andsupplement_number
openfda_lookup_ndc tool
- Required
searchoverproduct_ndc,brand_name,generic_name,openfda.manufacturer_name, oractive_ingredients.name; up to 1000 records per page - Records carry
product_ndc,labeler_name,dosage_form,route,marketing_category,active_ingredients[], andpackaging[]
openfda_dataframe_describe tool
canvas_idfrom a staged search; lists each table'sname,kind, full stagedrow_count, and columns with DuckDB type and nullability- Fails as
canvas_disabledwhenCANVAS_PROVIDER_TYPE=none, orcanvas_not_foundwhen thecanvas_idhas expired or never existed
openfda_dataframe_query tool
canvas_idplus one read-onlySELECT; DDL, DML, COPY, and file-reading functions are rejected. Scalars are stored as text (CASTfor math) and nested openFDA blocks as JSON columns- Rows are capped at the canvas row limit, and
truncated: truemeans page on withORDER BYplusLIMIT/OFFSET; failures arecanvas_disabled,canvas_not_found,missing_table, orinvalid_query
openfda_dataframe_drop tool
- Off by default: set
OPENFDA_DATAFRAME_DROP_ENABLED=trueto make it callable; otherwise it is listed as disabled on the landing page and absent fromtools/list canvas_idplus thetablename fromopenfda_dataframe_describe; deletes that one table or view and returnsremaining_tables, leaving the canvas and its other tables in place- Failures are
canvas_disabled,canvas_not_found, ormissing_table(already dropped, expired, or mistyped)
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.
openFDA-specific:
- One client for all 20 openFDA endpoints, with retry on 429 and 5xx. No API key is needed (1K requests/day); a free
OPENFDA_API_KEYraises that to 120K/day - Queries are
field:valueover dotted paths joined by AND/OR (a URL-style+between clauses reads as a space);openfda_describe_fieldslists the paths, and a malformed query fails asmalformed_searchbefore any request - Paging:
limitup to 1000 andskipup to openFDA's 25,000-record ceiling (pagination_limit_reachedpast it); each page is held to a 24,000-byte budget fieldson anyopenfda_search_*tool oropenfda_lookup_ndctrims each record to the dotted paths you name, so large records fit inline- DataCanvas staging (on by default;
CANVAS_PROVIDER_TYPE=noneturns it off) withstage: trueor acanvas_id, and an optional local bulk mirror (OPENFDA_MIRROR_ENABLED=true) that answers exact-key lookups without spending API budget
Agent-friendly output:
- Disclosed bounds:
page_omitted,page_bytes,page_largest_fields,projected_fields, the labeloutline, andstaged_rowsreport every cut - Typed failures: callers branch on
error.data.reason(rate_limited,query_error,not_aggregatable, ...) instead of parsing messages - Unknown vs. absent:
openfda_drug_profilereturnsnullfor a section with no FDA record and lists failed sections indegraded[] - Empty-result guidance: a no-match search points at
openfda_describe_fieldsand broader terms
Getting started
Connect to the hosted instance, no install:
claude mcp add --transport http openfda-mcp-server https://openfda.caseyjhand.com/mcp
Or run it locally:
claude mcp add openfda-mcp-server -- npx -y @cyanheads/openfda-mcp-server
For other clients, add this to the MCP config file:
{
"mcpServers": {
"openfda-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/openfda-mcp-server"]
}
}
}
No API key is needed. A free openFDA API key set as OPENFDA_API_KEY raises the limit from 1K to 120K requests per day. For Docker or a source build, see Running the server.
Configuration
| Variable | Description | Default |
|---|---|---|
OPENFDA_API_KEY | Free API key from open.fda.gov; raises the daily limit from 1K to 120K requests. | none |
OPENFDA_BASE_URL | API base URL override, for a proxy or mock. | https://api.fda.gov |
OPENFDA_MIRROR_ENABLED | Answer exact-key lookups from the 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). | 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, in ms. | 21600000 (6h) |
OPENFDA_MIRROR_BASE_URL | Host serving the bulk download manifest (download.json). | https://api.fda.gov |
CANVAS_PROVIDER_TYPE | DataCanvas staging and SQL. none disables them; dataframe tools remain listed and return canvas_disabled. | duckdb |
OPENFDA_DATAFRAME_DROP_ENABLED | Enable openfda_dataframe_drop, which deletes a staged table. Needs CANVAS_PROVIDER_TYPE=duckdb. | false |
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 |
LOGS_DIR | Directory for log files (Node.js only). | <app-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry. | false |
See .env.example for the full list of optional overrides.
The Claude Desktop MCPB bundle disables DataCanvas because it excludes platform-specific DuckDB bindings. Use npm or Docker for staging and SQL.
Local bulk mirror
With OPENFDA_MIRROR_ENABLED=true the server keeps a local SQLite copy of four openFDA bulk dumps (drug/label, drug/ndc, drug/enforcement, drug/drugsfda). Only exact lookups are answered locally: a single quoted field:"value" term on id, set_id, product_id, product_ndc, recall_number, event_id, or application_number, with no count or sort, skip 0, and one matching record. Everything else goes to the API.
The initial harvest runs separately, never at startup:
bun run mirror:init # all four datasets
bun run mirror:init drug/enforcement # one dataset
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
A refresh re-reads the whole dump. Set OPENFDA_MIRROR_REFRESH_CRON to run it in-process on the HTTP transport; on stdio, run bun run mirror:refresh from the host. A mirrored response's meta.lastUpdated is the served dump's stamp, which can differ from the live API's.
On Node, install the optional better-sqlite3 peer dependency (Bun uses its built-in bun:sqlite). OPENFDA_MIRROR_REFRESH_CRON also needs the optional node-cron peer dependency; without it the server fails at startup.
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/openfda-mcp-server.git cd openfda-mcp-server && bun install cp .env.example .env # optionally set OPENFDA_API_KEY and the mirror or canvas options -
Build and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:http -
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker run --rm -p 3010:3010 ghcr.io/cyanheads/openfda-mcp-server:latest
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/openfda-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/openfda-mcp-server:latest"]. To build locally, docker build -t openfda-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them. To keep a mirror harvest across container replacement, mount a volume over /usr/src/app/data/openfda-mirror.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point: tool registration, service setup, mirror refresh wiring. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools/definitions | Tool definitions (*.tool.ts), fifteen in all. |
src/mcp-server/tools | Shared tool helpers: per-endpoint field catalog, input schemas and guards, formatters, SPL table rendering. |
src/services/openfda | openFDA API client (retry, rate limits, error normalization), inline page budget, fields record projection, canvas staging. |
src/services/openfda/mirror | Opt-in local bulk mirror: dataset registry, dump reader, harvester, mirror-vs-live query gate, refresh schedule. |
src/services/canvas | DataCanvas accessor for staging and SQL. |
scripts/openfda-mirror.ts | Mirror lifecycle CLI behind the mirror:* scripts. |
tests/ | Unit and integration tests, mirroring the src/ structure. |
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 request-scoped logging - Register new tools in the barrel at
src/mcp-server/tools/definitions/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate a field openFDA didn't return
Data attribution
Data comes 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.
That is why the local mirror 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.
FDA does not endorse this project. Do not rely on openFDA to make decisions regarding medical care.
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.