FineRx MCP server
Give an AI assistant what US pharmacy chains were seen charging with the free FineRx discount card — per chain, each price with the date it was observed, near a ZIP code or nationally — the pharmacies nearby, the card itself, and the reviewed map of foreign medicine brands to their US equivalent (pages and card text in 12 languages), through the Model Context Protocol.
Source: github.com/andrewfinerx/finerx-mcp (public mirror of this package, MIT; issues welcome).
finerx-mcp is a thin client over the FineRx public REST API
(/api/public/v1). It has no direct database access and inherits the public
API's authentication and rate limits, so it adds zero extra attack surface.
What changed in 2.1
- A search inside the card:
open_price_finder(query?)opens the FineRx search (fullscreen where the host allows it); the card suggests medicines as the person types (app-onlyui_suggest), each with where its card prices start and the date, and foreign brands that matched. - A place as text: inside the card the person can type a ZIP, a city or a
street address. Only the app-only
ui_prices/ui_nearbytake it (where); it goes once into the body of the API request that places it and is never logged, stored or echoed — the answer names only the ZIP area / city. find_us_equivalentandget_prescription_optionsare drawn as views too (equivalent,rx); every 2.0 field of their results is still there. A foreign brand picked in the search opens the same view (app-onlyui_equivalent). The three new views use their own template uri,ui://finerx/v2.1/app.html, so a host holding the 2.0 HTML never shows them as plain text; the 2.0 tools keepui://finerx/v2/app.html.email_savings_cardsends a per-person key (an HMAC of the host's anonymous user id), so the API caps card emails per person instead of per server.- UI strings for the new views in all 12 languages.
What changed in 2.2
nextin every answer — up to three calls that fit after it, with the arguments already filled in from the answer (the resolved slug, the priced package, a ZIP the person gave);asknames the one thing to get from the person first. An assistant takes the next call from there instead of composing it.compare_basket(drugs, zip?)— several medicines at once: one row per pharmacy chain with the sum of its card prices, only where every priced medicine was seen there. A sum adds observed prices; it is not a quote.compare_tooncompare_prices— an amount the person named (a copay, what they pay now, a price they want to hear about): the answer counts the chains seen below it. A card price replaces insurance for that fill.get_transfer_steps(chain?, drug?, zip?)— how to move a prescription to the chain the person picked: fixed steps, the chain's dated card price, its nearest stores.find_us_equivalents(brands, country?)— the list a person brought from another country, one reviewed entry per medicine.get_drugdraws a card (strengths × pack sizes with dated "from" prices), and the card view carries a QR of the card page for a person at a computer.- New views use the template
ui://finerx/v2.2/app.html.
What changed in 2.0
- Prices are card prices by pharmacy chain with their observation date
(Walmart per state), from one call per question; no other savings program's
prices are read or named.
compare_pricestakes a drug name instead of an NDC (ndc+quantitystill work), and a ZIP is optional. - The card travels with every answer: each result carries a
cardblock (codes, the dated price with the card, the card sentence, the small print) and ends its text with them. - One UI bundle,
ui://finerx/v2/app.html, draws prices, nearby pharmacies and the card; inside it the person changes dose, quantity or ZIP through two app-only tools the model does not see. fromPrice→fromCardPrice,packageCountis gone,get_savings_cardreturns thefinerx.view/2envelope.
Tools
| Tool | What it does |
|---|---|
compare_prices(drug, strength?, form?, quantity?, zip?, ndc?, locale?, compare_to?) |
Card price of one package at each pharmacy chain, with dates; nearest store of each chain; chains priced but without a store nearby; the card. Place: zip, else the host's approximate location (ChatGPT), else national |
find_nearby_pharmacies(zip?, family?, drug?, strength?, form?, quantity?) |
Stores within 30 miles per chain family; with drug, each with its chain's dated card price |
get_savings_card(locale?, channel?, drug?) |
The free card: codes, how to use it, what to say at the counter, links — plus a PNG for hosts that draw no UI |
email_savings_card(email, consent, locale?) |
Email the card to an address the person gave, after they said yes |
search_drugs(query, limit=10) |
Name → slug, with fromCardPrice (amount, package, date) and matching foreign brands |
get_drug(slug, locale?) |
Strengths × forms × pack sizes that have a card price, the default package and its prices by chain |
find_us_equivalent(brand, country?, locale?) |
A medicine from another country → what it is in the US, the vetted sentence to say, its card price |
foreign_brands_for_drug(slug) |
What a US drug is called abroad (the reverse lookup) |
get_prescription_options(locale?, drug?) |
What to do with no prescription yet, and where the drug's card prices start (for controlled / age-restricted medicines: card prices and the card only) |
open_price_finder(query?, locale?) |
Opens the search inside the card: matches for query with dated "from" card prices, foreign brands, often-searched medicines |
compare_basket(drugs, zip?, locale?) |
Two to six medicines at once: one row per chain with each card price and — only where every priced medicine was seen there — their sum with the dates |
get_transfer_steps(chain?, drug?, strength?, form?, quantity?, zip?) |
How to move a prescription to the chain the person picked: fixed steps, that chain's dated card price, its nearest stores (controlled medicines: the card only) |
find_us_equivalents(brands, country?, locale?) |
Two to six medicines from another country → one reviewed entry each; a US product only for the same active ingredient |
get_dataset_info() |
Card-price coverage: chain families, banners, newest observation, stores on the map |
ui_prices, ui_nearby, ui_suggest, ui_equivalent, ui_basket |
App-only (_meta.ui.visibility: ["app"]): called by the UI, hidden from the model. Only these take a place as text (where) |
Every price carries its observedAt date and is never scaled to another pack
size. The server also ships instructions (the card rule: answer with each
price and its date, then the card sentence and codes, then the small print; no
promises of a price or a saving; find_us_equivalent first for a medicine from
another country; no medical advice), the resources finerx://card,
finerx://how-it-works and the UI bundle, and three prompts,
price_and_card(drug), prescription_help(drug?) and
us_equivalent(brand, country?).
US equivalents (the thing nobody else does)
An immigrant does not search "atorvastatin" — they search Но-шпа, Nurofen,
Dolo-Neurobion, 999 Ganmaoling. FineRx holds a reviewed corpus mapping those
brands to their US status, and find_us_equivalent is how an assistant reaches
it instead of answering from memory.
find_us_equivalent(brand="Но-шпа")
# → usClass "rx_alternative", inn "drotaverine hydrochloride",
# guidance "No-Spa (drotaverine hydrochloride) is not sold in the US as the same
# product; the closest US options need a prescription. Ask a doctor or
# pharmacist which one fits."
# otherMatches [{"brand": "No-Spa", "countries": ["Poland"], ...}]
find_us_equivalent(brand="Nurofen", country="Turkey")
# → usClass "same_inn", usGeneric "ibuprofen", usBrands ["Advil", "Motrin"],
# usDrug {slug, fromCardPrice {amount, package, observedAt}}, the card, and the guidance sentence
# that names ibuprofen as the thing to ask the pharmacist for.
Three classes, and the difference between them is the whole honesty of the
feature: same_inn means the same active ingredient is sold here (not the
same product — strength, form and excipients differ, which is what
guidanceDisclaimer says); rx_alternative means it is not sold here and
the closest US options need a prescription, so the answer is "ask a clinician",
never a named swap; no_equivalent means nothing here matches, and the
components breakdown states each ingredient's own US status rather than
inventing a substitute.
guidance is written and reviewed server-side. Quote it; do not paraphrase,
translate or extend it — a sentence composed by the model is a medical claim
nobody reviewed. foreign_brands_for_drug(slug) is the reverse ("what is
lisinopril called in Mexico?"), and search_drugs carries a foreignBrands
list so a client that only calls search still finds the corpus.
Handing over the card
The card is free, is not insurance, needs no signup, and is credited at the pharmacy counter by its group code — so an assistant can hand it over completely, with no click-through.
- Every result (except
get_dataset_info) carriesstructuredContent.card—codes,priceWithCard(amount, chain,observedAt) or null,law(the approved card sentence in the person's language),fine(the small print),chainsCount,siteUrl,printUrl,actions— and its text ends with the codes, the sentence and the small print. If the API is unreachable, the codes still come back (last good answer, then built-in constants). get_savings_cardadds the steps, the sentence to say to the pharmacist and the links; hosts that draw no UI also get a PNG of the card (never ChatGPT, where an image counts against the Free plan's image quota).email_savings_cardsends one card-only message. It refuses withoutconsent=trueand never calls the API in that case. The result masks the address (j***@example.com); FineRx does not store it.- Every URL carries
src=<channel>(chatgptormcpby default), so the surface that sent someone is visible in FineRx's own analytics — nothing about the person is.
Renders as an app
On a host that supports UI components — ChatGPT (Apps SDK) and any host that
implements the standard MCP Apps extension (Claude) — compare_prices,
find_nearby_pharmacies, get_savings_card, open_price_finder,
find_us_equivalent and get_prescription_options are drawn by one bundle,
ui://finerx/v2/app.html (mime text/html;profile=mcp-app), picked by
structuredContent.view of the finerx.view/2 envelope. Tool _meta names it
under both ui.resourceUri and openai/outputTemplate; the resource _meta
carries ui.domain / openai/widgetDomain, an empty CSP and finerx/build.
What the person picks inside the card (a medicine, dose, pack size, ZIP or
city — never an address) is sent to the model as context by the bundle
(ui/update-model-context). UI strings arrive in _meta["finerx/labels"] in
the person's language. The
bundle is built from widget-src/ (Vite + Preact, single file, no external
resources) — see widget-src/README.md.
Prerequisites
- A FineRx developer API key (format
frx_live_...). Request one by emailing partners@finerxfinder.com — see https://finerxfinder.com/developers. uvinstalled (providesuvx).
Configuration
The server reads these environment variables:
| Variable | Required | Default | Notes |
|---|---|---|---|
FINERX_API_KEY |
yes | — | Your frx_live_... key |
FINERX_API_BASE |
no | https://finerxfinder.com/api/public/v1 |
Point at another host for local testing |
FINERX_CARD_IMAGE_URL |
no | https://www.finerxfinder.com/card.png |
PNG get_savings_card returns to hosts without UI, when the API names no delivery.imageUrl |
FINERX_MCP_TRANSPORT |
no | stdio |
streamable-http for the remote endpoint |
FINERX_MCP_HOST / FINERX_MCP_PORT |
no | 127.0.0.1 / 8000 |
Bind address for the HTTP transport |
FINERX_MCP_PATH |
no | /mcp |
HTTP path of the endpoint |
FINERX_MCP_STATELESS |
no | 1 (on) |
0 restores per-session HTTP transports (see below) |
FINERX_MCP_SUBJECT_SALT |
no | random per process | HMAC salt for the per-person rate limit (ChatGPT openai/subject) |
FINERX_MCP_COMPETITOR_PRICES |
no | false |
Keep off: other programs' prices are not served in 2.0 |
Install & run
Run directly with uvx (no manual install needed):
FINERX_API_KEY=frx_live_xxxxxxxx uvx finerx-mcp
The server speaks MCP over stdio by default.
Remote (HTTP) mode
The same tools can be served over Streamable HTTP — the transport that connector catalogs (ChatGPT Apps, Claude connectors, Gemini, Grok) consume, so a user adds FineRx by URL with no local install. Set the transport (and, for a hosted deployment, the bind host/port):
FINERX_API_KEY=frx_live_xxxx \
FINERX_MCP_TRANSPORT=streamable-http \
FINERX_MCP_HOST=0.0.0.0 FINERX_MCP_PORT=9000 \
uvx finerx-mcp
# → endpoint at http://<host>:9000/mcp
Defaults stay stdio, so existing Claude Desktop/Code/Cursor configs are
unaffected. The HTTP endpoint calls the same public REST API with the server's
FINERX_API_KEY, so hosting it exposes no data beyond the already-public API.
HTTP mode is stateless by default. Keyless one-shot connector calls — a
catalog probe, a single tool call from a chat — open a Streamable-HTTP session
and never DELETE it, so in session mode the transport map only grows (measured
on the hosted endpoint: 3568 sessions over 7 days, ~25 MB of swap a day, until
the container restarts). Stateless mode builds a transport per request and drops
it. It is safe here because no tool keeps per-session state: every call is a
fresh request against the public API, and the card-image cache is process-level,
not per-session. Set FINERX_MCP_STATELESS=0 if you need SSE resumability;
stdio ignores the setting entirely.
Claude Desktop
Add to your claude_desktop_config.json
(macOS: ~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"finerx": {
"command": "uvx",
"args": ["finerx-mcp"],
"env": {
"FINERX_API_KEY": "frx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}
Restart Claude Desktop; the FineRx tools appear in the tools menu.
Claude Code
claude mcp add finerx --env FINERX_API_KEY=frx_live_xxxx -- uvx finerx-mcp
Or add it to .mcp.json in your project:
{
"mcpServers": {
"finerx": {
"command": "uvx",
"args": ["finerx-mcp"],
"env": { "FINERX_API_KEY": "frx_live_xxxxxxxx" }
}
}
}
Local development
Run against a local FineRx stack (e.g. the dev proxy on :8080):
export FINERX_API_KEY=frx_live_... # a key you created via the CLI
export FINERX_API_BASE=http://localhost:8080/api/public/v1
uv run --project packages/finerx-mcp finerx-mcp
A smoke test that drives one tool call end-to-end lives at
scripts/smoke_mcp.py in the FineRx repo.
Run the unit tests (the API is mocked with respx — no key, no network):
cd packages/finerx-mcp && uv run --extra dev pytest -q
Terms
Data is provided under the FineRx public API terms: attribution required ("Prices via FineRx"), prices are observed estimates with their dates and may have changed — the pharmacy sets the final price — the card is not insurance, and nothing here is medical advice. Pharmacy location coordinates are © OpenStreetMap contributors (ODbL); place names (the city and state of a ZIP code) come from GeoNames (CC BY 4.0).
Metadata
Release files for finerx-mcp 2.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| finerx_mcp-2.2.0.tar.gz | 189.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| finerx_mcp-2.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 344.5 kB
Release files / finerx_mcp-2.2.0.tar.gz
| Download URL | finerx_mcp-2.2.0.tar.gz |
|---|---|
| Size | 189.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
70bbf5928d67f68392ab29fa0e50f1ff49776d870017ce8b153a9df684eb873e
|
|
BLAKE2b-256 checksum How to use checksums |
34680258ba765604b1fc915b1e431dfee4ea3d09558a8d48b78de65fade6bf2f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / finerx_mcp-2.2.0-py3-none-any.whl
| Download URL | finerx_mcp-2.2.0-py3-none-any.whl |
|---|---|
| Size | 154.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1dd5490ae76b7722c0b47a681ca29a7f1ec89ec83012422d11abd410b0aa2547
|
|
BLAKE2b-256 checksum How to use checksums |
9ab0c9e242872698c7801765858a8b3ecf33cf2a7b3990b83c77bd083f041255
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|