US drug price benchmarks — what a pharmacy PAYS and what Medicare PAYS OUT.
- ✓Open-source license (MIT)
- ✓Actively maintained (<30d)
- ✓Clear description
- ✓Topics declared
- ✓Documented (README)
git clone https://github.com/pipeworx-io/mcp-drug-prices{
"mcpServers": {
"mcp-drug-prices": {
"command": "node",
"args": ["/path/to/mcp-drug-prices/dist/index.js"]
}
}
}MCP Servers overview
# drug-prices
US drug price benchmarks: what a pharmacy **pays** to acquire a drug, and what
Medicare Part B **pays out** for one administered by a provider. Keyless.
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1573+ live data sources.
These are two different numbers that are routinely confused for each other, and
keeping them apart is most of what this pack does.
| | NADAC | ASP payment limit |
|---|---|---|
| Answers | what a retail pharmacy pays a wholesaler | what Medicare pays a clinic |
| Cadence | weekly | quarterly |
| Keyed by | NDC | HCPCS (J-code) |
| Typical drug | generic tablets, retail products | oncology, biologics, infusions, vaccines |
| Source | CMS via data.medicaid.gov | CMS ASP Pricing Files (ZIP) |
A generic tablet has a NADAC and no ASP. An infused oncology drug has an ASP and
usually no retail NADAC. `drug_price_compare` reports whichever exist and says
why the other is missing, rather than returning an empty result.
## Tools
- `nadac_price(drug | ndc, as_of?, limit?)` — current acquisition cost per unit
- `nadac_history(ndc, weeks?)` — weekly price series and % change
- `asp_payment_limit(hcpcs | drug, quarter?, limit?)` — Part B limit per dosage unit
- `drug_price_compare(drug, ndc?)` — both benchmarks side by side, with vintages
## Plumbing
NADAC is read **live** — data.medicaid.gov exposes a real query API (filter,
sort, paginate) and the dataset holds ~20 months, so there is nothing to mirror.
ASP has **no API at all**: CMS publishes one ZIP per quarter, so it is ingested
into `asp_payment_limits` by `scripts/ingest-asp-payment-limits.mjs` and read
from Supabase.
```bash
node scripts/ingest-asp-payment-limits.mjs # current quarter
node scripts/ingest-asp-payment-limits.mjs --back=5 # backfill
```
## Traps
Every one of these was hit while building the pack, and each produced a
**confident wrong answer** rather than an error.
**The newest NADAC week is usually a correction file, not a week.** NADAC
publishes the full weekly file plus tiny off-cycle corrections sharing the same
`effective_date` column. Measured 2026-08-28: `2026-08-26` → 77 rows,
`2026-08-19` → 58,163 rows, `2026-08-12` → 6 rows. The obvious design — take
`max(effective_date)`, then filter — lands on the 77-row file and reports "no
price found" for almost every drug, which is indistinguishable from a drug NADAC
does not cover. Filter for the drug **first**, then take the newest date in the
matched set.
**The datastore API silently ignores unknown parameters.** `?sort=-effective_date`
returns HTTP 200 with *unsorted* rows — byte-identical to `?zzznonsense=x`. The
only spelling that sorts is `sorts[0][property]` + `sorts[0][order]`. A typo here
does not fail; it quietly answers with an arbitrary week's price.
**The NADAC dataset id rotates every January, and the year in the title is not
the range it covers.** The dataset called "NADAC … 2026" holds effective dates
from 2025-01-01 to 2026-08-26. The id is resolved by title at call time, with the
last known id as a fallback, so January does not break the pack.
**NADAC carries two dates.** `effective_date` is the week the price applies to;
`as_of_date` is when CMS published it. Prices key on the first; both are
returned.
**The ASP payment limit's vintage is not its quarter.** The CMS file states it:
the file effective 2026-07-01 is *"based on 1Q26 ASP data"*. The limit in force
is derived from ASP data roughly two quarters earlier, so comparing it against
this week's NADAC spans about six months. Both quarters are always returned.
**The ASP CSV is windows-1252, not UTF-8**, and carries a `0xA0` inside a real
value (`"1 million\xa0PFU"`). Reading it as UTF-8 throws — and worse, `grep`
silently *aborts* on the illegal byte, which is how `J9271` first appeared to be
missing from a file that contains it.
**The ASP CSV header is not row 1** (nine rows of notes precede it, and the count
varies by quarter), and the inner filename carries a per-quarter build date
(`…File 061626.csv`). Both are matched by pattern, never by offset or literal
name. CMS is also inconsistent about a `-final-file` suffix on the ZIP, so both
spellings are tried.
**NADAC names brand products by their BRAND, not their ingredient.** `APIXABAN`
matches 0 rows; `ELIQUIS` matches 204. A generic-name search for a
still-branded drug therefore returns a confident empty, and the tempting
explanation — "must be clinic-administered" — is simply wrong for that case. The
empty-result note offers both possibilities in order of likelihood rather than
asserting one.
**NDCs travel in three shapes** — dashed 5-4-2, 11-digit, and 10-digit variants
missing a leading zero — and they are not interchangeable as strings. All are
normalised to canonical 11-digit.
**`payment_limit` is per its `dosage` unit.** J9271 is $60.645 per **1 MG**, so a
200 MG administration is 200×. Returning the limit without the unit invites a
200-fold error, so the two always travel together.
**`drug_price_compare` deliberately does no arithmetic** between the two
benchmarks. NADAC is per pricing unit (EA/ML/GM) and ASP is per HCPCS dosage —
different denominators, measured months apart. Dividing one by the other would
produce a confident, meaningless "margin".
## 340B covered entities
`b340_covered_entities` and `b340_contract_pharmacies` read a mirror of HRSA
OPAIS: **94,363 covered entity registrations** (64,413 currently participating)
and **398,441 contract-pharmacy links**, as of the 2026-09-07 export.
A 340B ID is per SITE, not per organisation — one health system holds many, and
`parent_id340b` links a child site to its registered parent hospital. Roughly a
third of the roster is terminated registrations, so both tools exclude them
unless you ask; a "no match" answer says so explicitly rather than implying the
entity never existed.
`type` takes plain words — `"hospital"`, `"health center"`, `"Ryan White"`,
`"critical access"` — as well as HRSA's 23 codes, and `state` takes `"Texas"` as
happily as `"TX"`. An unrecognised value is REFUSED with the list of valid ones
rather than silently ignored, because an ignored filter returns a full unfiltered
result set with a clean 200, which reads as a real answer.
**No contact people.** The export carries `authorizingOfficial` and
`primaryContact` — 94,363 named individuals with direct phone numbers. Neither
is ingested and there is no column for them, so no query can return one. See the
migration header.
### How it stays fresh — a person clicks a button
The 340B data here is refreshed by a human, on purpose, roughly monthly:
1. `.github/workflows/340b-refresh-reminder.yml` runs on the 1st and files a
fleet task **only if** no load has landed in 25 days and no such ask is
already open.
2. Someone clicks *Covered Entity Daily Export (JSON)* at
<https://340bopais.hrsa.gov/Reports> and runs
`bash scripts/340b-drop.sh ~/Downloads/340B_CoveredEntityJson_Daily_*.zip`.
3. `.github/workflows/340b-ingest.yml` parses, loads and closes the task.
`as_of` comes from the export FILENAME, never from the load date, so a file that
sat in a Downloads folder for a week cannot overstate freshness. The loader
refuses to swap in a roster under 50,000 entities, so a truncated download fails
loudly instead of quietly shrinking the data. Every response carries `as_of`.
### Why it is not automated
Specified in fleet #618, probed in #633: the export cannot be fetched by
anything we run.
**Re-verified 2026-09-01 (fleet #804) and 2026-09-07 (fleet #633): still true.**
HRSA OPAIS (`340bopais.hrsa.gov`) is **Blazor Server**, not client-rendered as
first recorded here. `/Reports` ships `_framework/blazor.web.js` (a .NET 8+
Blazor Web App), and the SignalR circuit endpoint is live.
**The 411 this file used to cite as proof proves nothing — do not repeat that
probe.** A bodyless `POST` to *any* path on the host answers **411 Length
Required** from the edge, including a path that does not exist. Send an empty
body with `Content-Length: 0` and the paths separate cleanly (measured
2026-09-01):
| Path | Bodyless POST | `POST -d ''` |
|---|---|---|
| `/_blazor/negotiate?negotiateVersion=1` | 411 | **200** `{"negotiateVersion":1,"connectionId":"…","connectionToken":"…","availableTransports":[WebSockets, ServerSentEvents, LongPolling]}` |
| `/DXXRDV` | 411 | 404, 0 bytes |
| `/nonexistentpath123` | 411 | 404, 0 bytes |
`/DXXRDV` is worth naming because `/Reports` loads the DevExpress Web Document
Viewer scripts, and that control normally *does* mount an HTTP controller under
that path — an inviting-looking REST door that OPAIS has not opened.
**There is no request to reproduce.** Loading `/Reports` in a browser, clearing
the network log and clicking *Covered Entity Daily Export (JSON)* records **zero
HTTP requests** — filtered on everything, and on `_blazor` specifically. The file
comes back over the WebSocket that was already open from page load, and a
WebSocket frame is not an HTTP request. HRSA's own FAQ says the same in words: no
direct download URL, and "postbacks and button clicks must be done in the context
of a current session and view state".
**Correction, measured 2026-09-07 (fleet #633, Alaric): the blocker is the
STATEFUL CIRCUIT, not the WebSocket.** This file used to say a WebSocket transport
is "why curl, a Worker and a cron all have nothing to call." That reason is wrong,
and it is wrong in the same way as the 411 it replaced — it names a symptom the
next re-prober will disprove in one command. `negotiate` advertises **three**
transports, not one: `WebSockets`, `ServerSentEvents` **and `LongPolling`**.
LongPolling is ordinary HTTP POST/GET, so a Worker *can* drive this circuit. The
reason not to is what a caller would have to do once connected: replay the Blazor
component handshake, read the button's component id out of a binary render batch,
dispatch a synthetic UI event against it, and reassemble a streamed file from
circuit fWhat people ask about mcp-drug-prices
What is pipeworx-io/mcp-drug-prices?
+
pipeworx-io/mcp-drug-prices is mcp servers for the Claude AI ecosystem. US drug price benchmarks — what a pharmacy PAYS and what Medicare PAYS OUT. It has 0 GitHub stars and its last recorded update is dated 2026-09-14.
How do I install mcp-drug-prices?
+
You can install mcp-drug-prices by cloning the repository (https://github.com/pipeworx-io/mcp-drug-prices) or following the README instructions on GitHub. ClaudeWave also provides quick install blocks on this page.
Is pipeworx-io/mcp-drug-prices safe to use?
+
Our security agent has analyzed pipeworx-io/mcp-drug-prices and assigned a Trust Score of 95/100 (tier: Verified). See the full breakdown of passed checks and flags on this page.
Who maintains pipeworx-io/mcp-drug-prices?
+
pipeworx-io/mcp-drug-prices is maintained by pipeworx-io. The last recorded GitHub activity is dated 2026-09-14, with 0 open issues.
Are there alternatives to mcp-drug-prices?
+
Yes. On ClaudeWave you can browse similar mcp servers at /categories/mcp, sorted by popularity or recent activity.
Deploy mcp-drug-prices to your cloud
Ship this repo to production in minutes. Each platform spins up its own environment with editable env vars.
Maintain this repo? Add a badge to your README
Drop the badge into your GitHub README to show it's tracked on ClaudeWave. Each badge links back to this page and reflects the live Trust Score.
[](https://claudewave.com/repo/pipeworx-io-mcp-drug-prices)<a href="https://claudewave.com/repo/pipeworx-io-mcp-drug-prices"><img src="https://claudewave.com/api/badge/pipeworx-io-mcp-drug-prices" alt="Featured on ClaudeWave: pipeworx-io/mcp-drug-prices" width="320" height="64" /></a>More MCP Servers
Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.
User-friendly AI Interface (Supports Ollama, OpenAI API, ...)
An open-source AI agent that brings the power of Gemini directly into your terminal.
Real-time global intelligence dashboard. AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface
🕷️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl! Don't be shy, join here: https://discord.gg/EMgGbDceNQ
The fastest path to AI-powered full stack observability, even for lean teams.