@cyanheads/openfda-mcp-server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@@cyanheads/openfda-mcp-serversearch for adverse events for aspirin"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
Tools
Tool | Description |
| One drug name → consolidated FDA profile: identity, label, adverse events, recalls, approval, shortage |
| Search adverse event reports across drugs, food, and devices |
| Search adverse event reports for veterinary drugs and devices |
| Search FDA drug shortage records by status, availability, therapeutic category, manufacturer |
| Search problem reports for tobacco products, e-cigarettes, and vaping devices |
| Search enforcement reports and recall actions across drugs, food, and devices |
| Aggregate and tally unique values for any field across any openFDA endpoint |
| Return searchable field paths for an openFDA endpoint, grouped by category |
| Look up FDA drug labeling (package inserts / SPL documents) |
| Search the Drugs@FDA database for NDA/ANDA application approvals |
| Search FDA device premarket notifications: 510(k) clearances and PMA approvals |
| Look up drugs in the NDC (National Drug Code) Directory |
| List tables and column schemas staged on a DataCanvas (opt-in) |
| Run read-only SQL over a result set staged on a DataCanvas (opt-in) |
| Delete one table staged on a DataCanvas (opt-in, off by default) |
Related MCP server: fda-approvals-mcp
Capability reference
openfda_drug_profile tool
One
drugname, brand or generic, resolved once to canonical identity (generic name, NDC, RxCUI, SPL set ID) that keys every sub-query;meta.resolvedVia(label,ndc,none) names the source, and a single-ingredient product is preferred over combinationsSections
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 absentAuth, 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 pageSortable date fields differ by category:
receivedate(drug),date_created(food),date_received(device); another category's field fails asquery_error
openfda_search_animal_events tool
Optional
searchover veterinary reports, e.g.animal.species,drug.brand_name,reaction.veddra_term_name,serious_ae; up to 1000 records per pageRecords carry animal species, breed, age, and weight, the drug and route, VeDDRA reaction terms, and outcome
openfda_search_drug_shortages tool
Optional
searchoverstatus(Current/Resolved),therapeutic_category,generic_name, orcompany_name; up to 1000 records per pageEach 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 pageReports 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 pageEnforcement 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 counttruncatedflags more distinct terms pastlimit, except at the 1000-term maximum, where a notice says completeness can't be known; 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_valuesacceptsField 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 1000A 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 pageRecords 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 page510(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 pageRecords 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 nullabilityFails as
canvas_disabledwhenCANVAS_PROVIDER_TYPEis unset, 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 columnsRows 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/listcanvas_idplus thetablename fromopenfda_dataframe_describe; deletes that one table or view and returnsremaining_tables, leaving the canvas and its other tables in placeFailures 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: 429 and 5xx responses retry with exponential backoff, a 404 is an empty result, and a 400 becomes an actionable
query_error. No API key is needed (1K requests/day); a freeOPENFDA_API_KEYraises that to 120K/dayQueries are
field:valueover dotted paths that differ per endpoint, joined by AND/OR, with phrases in double quotes;openfda_describe_fieldslists the paths. A query with an unbalanced quote, parenthesis, or range bracket, or a trailing backslash, fails asmalformed_searchbefore any requestPaging:
limitup to 1000 andskipup to openFDA's 25,000-record ceiling (pagination_limit_reachedpast it). Search pages are also held to a 24,000-byte serialized budget, so an oversized page returns fewer records than requested, but never zeroOptional DataCanvas staging (
CANVAS_PROVIDER_TYPE=duckdb):stage: trueor acanvas_idon anyopenfda_search_*tool oropenfda_lookup_ndcdrains the matched set into a DuckDB table, up to a size budget reported bystaged_rowsandtruncatedOptional local bulk mirror (
OPENFDA_MIRROR_ENABLED=true): a self-refreshing SQLite copy of four drug datasets that answers exact-key lookups without spending API budget, with live fallback
Agent-friendly output:
Disclosed bounds:
page_omitted/page_bytes, the labeloutline, andstaged_rowsreport every cut on bothcontent[]andstructuredContent, with the routes to the restTyped failure contracts: callers branch on
error.data.reason(rate_limited,query_error,not_aggregatable,pagination_limit_reached,canvas_disabled, ...) instead of parsing messagesUnknown vs. absent:
openfda_drug_profilereturnsnullfor a section with no FDA record and lists a section whose sub-query failed indegraded[]Empty-result guidance: a no-match search returns a notice pointing at
openfda_describe_fieldsand broader terms; a page past the end reports the real match count
Getting started
Public Hosted Instance
A 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"
}
}
}Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"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/mcpPrerequisites
Bun v1.4.0 or higher (or Node.js v24+).
Optional: an openFDA API key raises the limit from 1K to 120K requests per day.
Installation
Clone the repository:
git clone https://github.com/cyanheads/openfda-mcp-server.gitNavigate into the directory:
cd openfda-mcp-serverInstall dependencies:
bun installConfigure environment:
cp .env.example .env
# edit .env and set OPENFDA_API_KEY and the mirror or canvas options as neededConfiguration
Variable | Description | Default |
| Free API key from open.fda.gov; raises the daily limit from 1K to 120K requests. | none |
| API base URL override, for a proxy or mock. |
|
| Answer exact-key lookups from the local bulk mirror. |
|
| Directory holding one SQLite file per mirrored dataset. |
|
| Cron expression for the in-process mirror refresh (HTTP transport only). | none |
| Fall back to the live API when the mirror is cold, missing the record, or failing. |
|
| Wall-clock budget for one refresh before it is aborted, in ms. |
|
| Host serving the bulk download manifest ( |
|
| Set |
|
| Enable |
|
| Transport: |
|
| HTTP server port. |
|
| HTTP session mode: |
|
| Authentication: |
|
| Log level ( |
|
| Directory for log files (Node.js only). |
|
| Storage backend: |
|
| Enable OpenTelemetry. |
|
See .env.example for the full list of optional overrides.
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) and answers eligible lookups from it. openFDA ranks results in Elasticsearch, which a local corpus can't reproduce, so a query is answered locally only when all of these hold, and goes to the API otherwise:
the search is a single quoted
field:"value"term onid,set_id,product_id,product_ndc,recall_number,event_id, orapplication_number, with a whole identifier in its canonical spelling and casethere is no
countand nosort, andskipis 0the value matches exactly one record, so the mirrored answer is the API's answer
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
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 advancedopenFDA has no incremental feed for these datasets, so a refresh re-reads the whole dump, tombstones records the new export drops, and resumes where it stopped after an interruption. 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
Build and run the production version:
# One-time build bun run rebuild # Run the built server bun run start:http # or bun run start:stdioRun checks and tests:
bun run devcheck # Lints, formats, type-checks, and more bun run test # Runs the test suite
Docker
docker build -t openfda-mcp-server .
docker run --rm -p 3010:3010 openfda-mcp-serverThe Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/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 |
|
|
| Server-specific environment variable parsing and validation with Zod. |
| Tool definitions ( |
| Shared tool helpers: per-endpoint field catalog, input schemas and guards, formatters, SPL table rendering. |
| openFDA API client (retry, rate limits, error normalization), inline page budget, canvas staging. |
| Opt-in local bulk mirror: dataset registry, dump reader, harvester, mirror-vs-live query gate, refresh schedule. |
| DataCanvas accessor for staging and SQL. |
| Mirror lifecycle CLI behind the |
| Unit and integration tests, mirroring the |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor request-scoped loggingRegister new tools in the barrel at
src/mcp-server/tools/definitions/index.tsWrap 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 testLicense
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
This server cannot be deployed
Maintenance
Related MCP Connectors
Search FDA safety data: drug adverse events, recalls, and device events.
FDA drug data via openFDA: adverse-event reports and safety-signal tools (FAERS search, event…
Search openFDA drug, device, food and adverse-event datasets.
Scrape openFDA drug recalls, enforcement reports, labels and adverse events. Pay per row.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to query and analyze FDA adverse events, drug labels, medical device clearances, and other public health datasets through natural language commands.14-
- FlicenseNot gradedqualityDmaintenanceEnables querying FDA drug approvals, device clearances (510(k)), recalls, and adverse events via the openFDA API, providing tools for clinical and pharmaceutical research.1-
- AlicenseAqualityDmaintenanceEnables LLMs to search FDA drug labels and adverse event data via the OpenFDA API, supporting natural language queries for drug safety information.2MIT
- FlicenseNot gradedqualityDmaintenanceEnables searching FDA food recalls and adverse event reports through natural language, supporting filters and pagination.-