Skip to main content

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-only ui_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_nearby take 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_equivalent and get_prescription_options are 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-only ui_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 keep ui://finerx/v2/app.html.
  • email_savings_card sends 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

  • next in 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); ask names 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_to on compare_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_drug draws 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_prices takes a drug name instead of an NDC (ndc + quantity still work), and a ZIP is optional.
  • The card travels with every answer: each result carries a card block (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, packageCount is gone, get_savings_card returns the finerx.view/2 envelope.

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) carries structuredContent.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_card adds 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_card sends one card-only message. It refuses without consent=true and 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> (chatgpt or mcp by 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

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)

Source distribution for finerx-mcp 2.2.0
File Size Uploaded
finerx_mcp-2.2.0.tar.gz 189.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for finerx-mcp 2.2.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

2.2.0 This release

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page