@pipeworx/mcp-medicare-pricing 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -4
- package/package.json +4 -4
- package/server.json +2 -2
- package/src/brand-codes.ts +113 -0
- package/src/index.ts +169 -23
- package/src/server.ts +5 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ What Medicare **pays** — physician fee schedule, lab rates, and per-day unit
|
|
|
4
4
|
limits. The companion to `medicare-coverage`, which answers whether something is
|
|
5
5
|
covered but not what it is worth.
|
|
6
6
|
|
|
7
|
-
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to
|
|
7
|
+
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1831+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
|
|
8
8
|
|
|
9
9
|
## Tools
|
|
10
10
|
|
|
@@ -17,6 +17,7 @@ Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents
|
|
|
17
17
|
| `medicare_code_units_limit` | "How many units of this code will they pay for in one day?" — NCCI MUE. |
|
|
18
18
|
| `check_code_pair` | "Can I bill 00142 and 64474 together?" — NCCI PTP edit + modifier indicator for a code pair. |
|
|
19
19
|
| `explain_ncci_edit` | "What is bundled into (or bundles into) this code?" — every recorded PTP edit for one code. |
|
|
20
|
+
| `medicare_fqhc_pps_rate` | "What is the FQHC PPS base rate for 2026?" — CMS's national Federally Qualified Health Center per-visit base rate, by year. |
|
|
20
21
|
| `medicare_pricing_coverage` | Which vintages are loaded, including the PTP baseline vintage. |
|
|
21
22
|
|
|
22
23
|
## Auth
|
|
@@ -32,6 +33,31 @@ All from CMS, all downloadable files with no API:
|
|
|
32
33
|
- **DMEPOS fee schedule** (durable medical equipment, prosthetics, orthotics, supplies) — quarterly (as necessary): A=Jan, B=Apr, C=Jul, D=Oct
|
|
33
34
|
- **NCCI Medically Unlikely Edits** — quarterly
|
|
34
35
|
- **NCCI PTP quarterly additions/deletions/revisions files** — quarterly (see below — a narrower source than the other three)
|
|
36
|
+
- **FQHC Prospective Payment System base payment rate** — annual (see below — a hand-maintained table, not a downloaded file)
|
|
37
|
+
|
|
38
|
+
## FQHC PPS base payment rate — a hand-maintained table, and why
|
|
39
|
+
|
|
40
|
+
`medicare_fqhc_pps_rate` is different from the rest of this pack: it is not
|
|
41
|
+
backed by Supabase or a downloaded bulk file, because there is neither. CMS
|
|
42
|
+
publishes the FQHC PPS national base rate exactly once a year, as a single new
|
|
43
|
+
dollar figure inside a CMS Manual System "Recurring Update Notification"
|
|
44
|
+
Change Request PDF — one number, one PDF, once a year. Standing up an ingest
|
|
45
|
+
pipeline for that is not proportionate, so the pack carries a small table of
|
|
46
|
+
`{year, base_rate_usd, effective_date, market_basket_update_pct,
|
|
47
|
+
prior_year_rate_usd, transmittal, change_request, publication}` in
|
|
48
|
+
`src/index.ts` (`FQHC_PPS_BASE_RATES`), five rows (CY2022–CY2026), each read
|
|
49
|
+
directly off its own primary CMS transmittal.
|
|
50
|
+
|
|
51
|
+
This is the national BASE rate only — before the per-FQHC Geographic
|
|
52
|
+
Adjustment Factor (GAF) CMS applies on top. The GAF table itself is not
|
|
53
|
+
loaded; an agent that needs the GAF-adjusted dollar amount for a specific FQHC
|
|
54
|
+
still needs that separately.
|
|
55
|
+
|
|
56
|
+
**Refresh**: CMS issues the following year's transmittal in Nov/Dec (the
|
|
57
|
+
CY2026 update was dated 2025-12-05 for a Jan 1, 2026 effective date). Add a
|
|
58
|
+
row to `FQHC_PPS_BASE_RATES` each year once it posts — search CMS's own
|
|
59
|
+
transmittals (Pub 100-04 Medicare Claims Processing, or Pub 100-02 Medicare
|
|
60
|
+
Benefit Policy) for "FQHC PPS Calendar Year".
|
|
35
61
|
|
|
36
62
|
## ADLT list — brand name to billing code
|
|
37
63
|
|
|
@@ -57,8 +83,26 @@ were checked and rejected:
|
|
|
57
83
|
- **NCBI GTR** — knows Signatera and Guardant360 but its `cptcode` field is
|
|
58
84
|
empty for them.
|
|
59
85
|
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
That made coverage ADLT-only, and fleet #2458 measured the cost: Oncotype DX
|
|
87
|
+
(81519, a Category I MAAA code, not an ADLT) routed here 5/5 and came back
|
|
88
|
+
`found: false`. Resolution now runs through three sources in order:
|
|
89
|
+
|
|
90
|
+
1. **CMS ADLT list** (`src/adlt.ts`, below).
|
|
91
|
+
2. **MAAA brand table** (`src/brand-codes.ts`) -- non-ADLT branded tests whose
|
|
92
|
+
proprietary name is tied to a code in CPT Appendix O and which each have a
|
|
93
|
+
brand-named or category MolDX billing article on the CMS MCD (Oncotype DX
|
|
94
|
+
breast/DCIS/colon/prostate, Prosigna, MammaPrint, EndoPredict, Breast
|
|
95
|
+
Cancer Index, Cologuard, Prolaris, Decipher, Afirma, ConfirmMDx, 4Kscore).
|
|
96
|
+
Only brand -> code -> lab FACTS are kept, never AMA descriptor text. Every
|
|
97
|
+
code was checked to carry a CLFS rate on 2026-09-26. To add a row: confirm
|
|
98
|
+
the pair in a public source, confirm `medicare_lab_rate` has a rate, and set
|
|
99
|
+
`coverage_topic` to a phrase from a live MolDX article title.
|
|
100
|
+
3. **NCBI GTR, live** -- where the performing lab registered a CPT code
|
|
101
|
+
(e.g. Afirma -> 81546, Decipher -> 81542). Labelled lab-reported. Sparse:
|
|
102
|
+
Oncotype DX and Signatera carry no code there, which is why it is last.
|
|
103
|
+
|
|
104
|
+
A test found in none returns `found: false, reason: brand_not_found` with
|
|
105
|
+
`sources_checked` naming all three, and a pointer to `medicare_lab_rate`.
|
|
62
106
|
|
|
63
107
|
**Copy, not proxy.** The list is a two-page PDF with no API, and the gateway
|
|
64
108
|
has no PDF parser, so the rows are baked into `src/adlt.ts`. The PDF's "Test
|
|
@@ -293,7 +337,7 @@ directly, instead of just this one's:
|
|
|
293
337
|
}
|
|
294
338
|
```
|
|
295
339
|
|
|
296
|
-
Both URLs reach the same gateway and the same
|
|
340
|
+
Both URLs reach the same gateway and the same 1831+ data sources. The
|
|
297
341
|
only difference is which pack's tools are listed **directly**; `ask_pipeworx`
|
|
298
342
|
reaches all of them from either one.
|
|
299
343
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipeworx/mcp-medicare-pricing",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "What does Medicare PAY — physician fee schedule, lab rates, and per-day unit",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "What does Medicare PAY — physician fee schedule, lab rates, and per-day unit limits.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@cloudflare/workers-types": "^4.20260405.1"
|
|
27
27
|
},
|
|
28
28
|
"pipeworx": {
|
|
29
|
-
"sourceHash": "v1-
|
|
30
|
-
"sourceCommit": "
|
|
29
|
+
"sourceHash": "v1-65f2f81a2d58d5024e9845e0238d0a24b75f7fa653f412494f5054bb6cfc84dd",
|
|
30
|
+
"sourceCommit": "514e3efb42b8d81082c07e01d3ae7654bbf91f52"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/server.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.pipeworx-io/medicare-pricing",
|
|
4
4
|
"title": "Medicare Pricing",
|
|
5
|
-
"description": "What does Medicare PAY — physician fee schedule, lab rates, and per-day unit",
|
|
6
|
-
"version": "0.1.
|
|
5
|
+
"description": "What does Medicare PAY — physician fee schedule, lab rates, and per-day unit limits.",
|
|
6
|
+
"version": "0.1.4",
|
|
7
7
|
"websiteUrl": "https://pipeworx.io/packs/medicare-pricing",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/pipeworx-io/mcp-medicare-pricing",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded lab tests that are NOT ADLTs but carry their own code (fleet #2458).
|
|
3
|
+
*
|
|
4
|
+
* #2426 resolved brand names only through the CMS ADLT list, so the most-asked
|
|
5
|
+
* branded test of all -- Oncotype DX, a Category I MAAA code (81519) -- came
|
|
6
|
+
* back found:false. Two more sources close the class:
|
|
7
|
+
*
|
|
8
|
+
* 1. THIS TABLE: the Category I MAAA tests whose proprietary name is tied to
|
|
9
|
+
* its code in CPT Appendix O, each also the subject of a brand-named MolDX
|
|
10
|
+
* billing-and-coding article on the CMS Medicare Coverage Database. What is
|
|
11
|
+
* kept is the FACT (brand -> code -> lab), never the AMA descriptor text,
|
|
12
|
+
* under the same rule as adlt.ts. Every code here was checked on
|
|
13
|
+
* 2026-09-26 to carry a rate on the loaded CLFS.
|
|
14
|
+
* 2. NCBI GTR (Genetic Testing Registry), fetched LIVE per request: labs
|
|
15
|
+
* register their tests there and many state the CPT code. US government
|
|
16
|
+
* work, keyless E-utilities. It is lab-reported, so the answer says so.
|
|
17
|
+
*
|
|
18
|
+
* `coverage_topic` is a phrase from the title of the test's MolDX policy (a
|
|
19
|
+
* brand-named billing article where one exists), matched live against the CMS
|
|
20
|
+
* Coverage API title list -- ours, not CMS's, checked 2026-09-26.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface BrandTest {
|
|
24
|
+
code: string;
|
|
25
|
+
lab: string;
|
|
26
|
+
test_name: string;
|
|
27
|
+
aliases: string[];
|
|
28
|
+
coverage_topic: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const BRAND_SOURCE = 'CPT Appendix O (MAAA) proprietary-name-to-code pairs, each with a brand-named or category MolDX billing article on the CMS Medicare Coverage Database';
|
|
32
|
+
export const BRAND_DATA_AS_OF = '2026-09-26';
|
|
33
|
+
|
|
34
|
+
export const BRAND_TESTS: BrandTest[] = [
|
|
35
|
+
{ code: '81519', lab: 'Genomic Health (Exact Sciences)', test_name: 'Oncotype DX Breast Recurrence Score', aliases: ['Oncotype DX', 'Oncotype DX Breast', 'Oncotype DX Breast Cancer Assay', 'Oncotype'], coverage_topic: 'Oncotype DX Breast Cancer Assay' },
|
|
36
|
+
{ code: '0045U', lab: 'Genomic Health (Exact Sciences)', test_name: 'Oncotype DX Breast DCIS Score', aliases: ['Oncotype DX DCIS', 'Oncotype DCIS'], coverage_topic: 'Oncotype DX Breast Cancer for DCIS' },
|
|
37
|
+
{ code: '81525', lab: 'Genomic Health (Exact Sciences)', test_name: 'Oncotype DX Colon Recurrence Score', aliases: ['Oncotype DX Colon', 'Oncotype DX Colon Cancer Assay'], coverage_topic: 'Oncotype DX Colon Cancer' },
|
|
38
|
+
{ code: '0047U', lab: 'Genomic Health (Exact Sciences)', test_name: 'Oncotype DX Genomic Prostate Score', aliases: ['Oncotype DX Prostate', 'Genomic Prostate Score', 'Oncotype GPS'], coverage_topic: 'Prostate Cancer Genomic Classifier Assay' },
|
|
39
|
+
{ code: '81518', lab: 'Biotheranostics (Hologic)', test_name: 'Breast Cancer Index', aliases: ['BCI'], coverage_topic: 'Breast Cancer Index' },
|
|
40
|
+
{ code: '81520', lab: 'NanoString Technologies (Veracyte)', test_name: 'Prosigna', aliases: ['Prosigna Breast Cancer Prognostic Gene Signature Assay', 'PAM50'], coverage_topic: 'Prosigna' },
|
|
41
|
+
{ code: '81521', lab: 'Agendia', test_name: 'MammaPrint', aliases: [], coverage_topic: 'MammaPrint' },
|
|
42
|
+
{ code: '81522', lab: 'Myriad Genetics', test_name: 'EndoPredict', aliases: [], coverage_topic: 'EndoPredict' },
|
|
43
|
+
{ code: '81528', lab: 'Exact Sciences', test_name: 'Cologuard', aliases: [], coverage_topic: null },
|
|
44
|
+
{ code: '81541', lab: 'Myriad Genetics', test_name: 'Prolaris', aliases: [], coverage_topic: 'Prostate Cancer Genomic Classifier Assay' },
|
|
45
|
+
{ code: '81542', lab: 'Veracyte (Decipher Biosciences)', test_name: 'Decipher Prostate', aliases: ['Decipher', 'Decipher Prostate Genomic Classifier'], coverage_topic: 'Prostate Cancer Genomic Classifier Assay' },
|
|
46
|
+
{ code: '81546', lab: 'Veracyte', test_name: 'Afirma Genomic Sequencing Classifier', aliases: ['Afirma', 'Afirma GSC'], coverage_topic: 'Molecular Testing for Risk Stratification of Thyroid Nodules' },
|
|
47
|
+
{ code: '81551', lab: 'MDxHealth', test_name: 'ConfirmMDx', aliases: ['ConfirmMDx for Prostate Cancer'], coverage_topic: 'Molecular Biomarkers to Risk-Stratify Patients at Increased Risk for Prostate Cancer' },
|
|
48
|
+
{ code: '81539', lab: 'OPKO Health (BioReference)', test_name: '4Kscore', aliases: ['4K score', '4Kscore Test'], coverage_topic: 'Molecular Biomarkers to Risk-Stratify Patients at Increased Risk for Prostate Cancer' },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export const norm = (s: string) => s.toLowerCase().replace(/[®™]/g, '').replace(/[^a-z0-9]/g, '');
|
|
52
|
+
|
|
53
|
+
type Named = { code: string; test_name: string; aliases: string[]; lab: string };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Match free text to rows: by code, then exact name/alias, then containment
|
|
57
|
+
* (either way, longest name wins so "Oncotype DX colon" is colon, not breast),
|
|
58
|
+
* then lab. A name hit beats a lab hit.
|
|
59
|
+
*/
|
|
60
|
+
export function matchTests<T extends Named>(rows: T[], query: string): { matched_by: 'code' | 'test_name' | 'lab' | null; tests: T[] } {
|
|
61
|
+
const q = norm(query);
|
|
62
|
+
if (q.length < 3) return { matched_by: null, tests: [] };
|
|
63
|
+
const byCode = rows.filter((t) => norm(t.code) === q);
|
|
64
|
+
if (byCode.length) return { matched_by: 'code', tests: byCode };
|
|
65
|
+
const names = (t: T) => [t.test_name, ...t.aliases].map(norm);
|
|
66
|
+
const exact = rows.filter((t) => names(t).includes(q));
|
|
67
|
+
if (exact.length) return { matched_by: 'test_name', tests: exact };
|
|
68
|
+
const byName = rows.filter((t) => names(t).some((n) => n.length >= 4 && (n.includes(q) || q.includes(n))));
|
|
69
|
+
if (byName.length) {
|
|
70
|
+
const longest = Math.max(...byName.map((t) => Math.max(...names(t).filter((n) => q.includes(n)).map((n) => n.length), 0)));
|
|
71
|
+
const narrowed = longest > 0 ? byName.filter((t) => names(t).some((n) => n.length === longest && q.includes(n))) : byName;
|
|
72
|
+
return { matched_by: 'test_name', tests: narrowed };
|
|
73
|
+
}
|
|
74
|
+
const byLab = rows.filter((t) => q.length >= 4 && norm(t.lab).includes(q));
|
|
75
|
+
if (byLab.length) return { matched_by: 'lab', tests: byLab };
|
|
76
|
+
return { matched_by: null, tests: [] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface GtrHit { code: string; test_name: string; lab: string; gtr_accession: string; url: string }
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* NCBI GTR, live: tests whose registered name matches the query AND whose lab
|
|
83
|
+
* stated a CPT code. null when NCBI did not answer (distinct from "no hit").
|
|
84
|
+
*/
|
|
85
|
+
export async function gtrLookup(query: string, fetcher: typeof fetch): Promise<GtrHit[] | null> {
|
|
86
|
+
const q = norm(query);
|
|
87
|
+
if (q.length < 4) return [];
|
|
88
|
+
const base = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
89
|
+
const tool = '&tool=pipeworx&email=support%40pipeworx.io';
|
|
90
|
+
try {
|
|
91
|
+
const s = await fetcher(`${base}/esearch.fcgi?db=gtr&retmode=json&retmax=40&term=${encodeURIComponent(query)}${tool}`);
|
|
92
|
+
if (!s.ok) return null;
|
|
93
|
+
const ids = ((await s.json()) as { esearchresult?: { idlist?: string[] } }).esearchresult?.idlist ?? [];
|
|
94
|
+
if (!ids.length) return [];
|
|
95
|
+
const r = await fetcher(`${base}/esummary.fcgi?db=gtr&retmode=json&id=${ids.join(',')}${tool}`);
|
|
96
|
+
if (!r.ok) return null;
|
|
97
|
+
const res = ((await r.json()) as { result?: Record<string, any> }).result ?? {};
|
|
98
|
+
const hits: GtrHit[] = [];
|
|
99
|
+
for (const id of ids) {
|
|
100
|
+
const x = res[id];
|
|
101
|
+
if (!x || typeof x.testname !== 'string') continue;
|
|
102
|
+
const n = norm(x.testname);
|
|
103
|
+
if (!(n.includes(q) || (n.length >= 4 && q.includes(n)))) continue;
|
|
104
|
+
const codes = String(x.cptcode ?? '').split(/[^0-9A-Za-z]+/).map((c: string) => c.toUpperCase()).filter((c: string) => /^(\d{5}|\d{4}U)$/.test(c));
|
|
105
|
+
for (const code of codes) {
|
|
106
|
+
hits.push({ code, test_name: x.testname, lab: String(x.offerer ?? ''), gtr_accession: String(x.accession ?? ''), url: `https://www.ncbi.nlm.nih.gov/gtr/tests/${id}/` });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return hits;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -740,6 +740,7 @@ function collapse(s: string): string {
|
|
|
740
740
|
*/
|
|
741
741
|
|
|
742
742
|
import { ADLT_DATA_AS_OF, ADLT_SOURCE_URL, SNAPSHOT_LAST_MODIFIED, matchAdlt, type AdltTest } from './adlt.js';
|
|
743
|
+
import { BRAND_DATA_AS_OF, BRAND_SOURCE, BRAND_TESTS, gtrLookup, matchTests, norm as normName } from './brand-codes.js';
|
|
743
744
|
|
|
744
745
|
// Bound every fetch() in this pack to a fixed timeout — an upstream that
|
|
745
746
|
// degrades without erroring would otherwise hold the Worker in `await fetch()`
|
|
@@ -863,6 +864,94 @@ const STATUS: Record<string, string> = {
|
|
|
863
864
|
X: 'Statutorily excluded from the fee schedule',
|
|
864
865
|
};
|
|
865
866
|
|
|
867
|
+
// ── FQHC PPS base payment rate (fleet #3241) ──────────────────────────────
|
|
868
|
+
//
|
|
869
|
+
// CMS publishes this ONE number a year, in a CMS Manual System TRANSMITTAL
|
|
870
|
+
// (a "Recurring Update Notification" Change Request PDF) — there is no API
|
|
871
|
+
// and no bulk file, just a yearly PDF with a single new figure in it. A
|
|
872
|
+
// hand-maintained table of a handful of values is the documented shape for
|
|
873
|
+
// that case (house rule for fleet #3241: "a PDF fact sheet with one number
|
|
874
|
+
// per year... a tiny dated table is acceptable"), not a Supabase ingest
|
|
875
|
+
// pipeline for five numbers. Every row below was read directly off its own
|
|
876
|
+
// primary CMS transmittal (not a summary site) — see transmittal/change
|
|
877
|
+
// request/pub per row.
|
|
878
|
+
//
|
|
879
|
+
// REFRESH: CMS issues the next year's transmittal in Nov/Dec for the
|
|
880
|
+
// following Jan 1 (the CY2026 one was dated 2025-12-05) — add a row here
|
|
881
|
+
// each year when it posts. Search "FQHC PPS Calendar Year" among CMS's own
|
|
882
|
+
// transmittals (Pub 100-04 Medicare Claims Processing or Pub 100-02 Medicare
|
|
883
|
+
// Benefit Policy) to find the next one.
|
|
884
|
+
interface FqhcPpsRow {
|
|
885
|
+
year: number;
|
|
886
|
+
base_rate_usd: number;
|
|
887
|
+
effective_date: string;
|
|
888
|
+
market_basket_update_pct: number;
|
|
889
|
+
prior_year_rate_usd: number;
|
|
890
|
+
transmittal: string;
|
|
891
|
+
change_request: string;
|
|
892
|
+
publication: string;
|
|
893
|
+
}
|
|
894
|
+
const FQHC_PPS_BASE_RATES: FqhcPpsRow[] = [
|
|
895
|
+
{ year: 2022, base_rate_usd: 180.16, effective_date: '2022-01-01', market_basket_update_pct: 2.1, prior_year_rate_usd: 176.45, transmittal: 'Transmittal 11057', change_request: 'CR 12490', publication: 'Pub 100-04 Medicare Claims Processing' },
|
|
896
|
+
{ year: 2023, base_rate_usd: 187.19, effective_date: '2023-01-01', market_basket_update_pct: 3.9, prior_year_rate_usd: 180.16, transmittal: 'Transmittal 11677', change_request: 'CR 12961', publication: 'Pub 100-04 Medicare Claims Processing' },
|
|
897
|
+
{ year: 2024, base_rate_usd: 195.99, effective_date: '2024-01-01', market_basket_update_pct: 4.7, prior_year_rate_usd: 187.19, transmittal: 'Transmittal 12267', change_request: 'CR 13398', publication: 'Pub 100-04 Medicare Claims Processing' },
|
|
898
|
+
{ year: 2025, base_rate_usd: 202.65, effective_date: '2025-01-01', market_basket_update_pct: 3.4, prior_year_rate_usd: 195.99, transmittal: 'Transmittal 12951', change_request: 'CR 13867', publication: 'Pub 100-04 Medicare Claims Processing' },
|
|
899
|
+
{ year: 2026, base_rate_usd: 207.72, effective_date: '2026-01-01', market_basket_update_pct: 2.5, prior_year_rate_usd: 202.65, transmittal: 'Transmittal 13506', change_request: 'CR 14309', publication: 'Pub 100-02 Medicare Benefit Policy' },
|
|
900
|
+
];
|
|
901
|
+
const FQHC_SOURCE = 'Centers for Medicare & Medicaid Services (CMS), Federally Qualified Health Center (FQHC) Prospective Payment System base payment rate';
|
|
902
|
+
|
|
903
|
+
function fqhcStatement(r: FqhcPpsRow): string {
|
|
904
|
+
return `CMS FQHC PPS base payment rate for CY ${r.year}: $${r.base_rate_usd.toFixed(2)}, effective ${r.effective_date} — a ${r.market_basket_update_pct}% FQHC market basket increase over CY ${r.year - 1}'s $${r.prior_year_rate_usd.toFixed(2)} (${r.transmittal}, ${r.change_request}, ${r.publication}). This is the national base rate before any per-FQHC Geographic Adjustment Factor (GAF).`;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function fqhcPpsRate(args: Record<string, unknown>) {
|
|
908
|
+
const askedYear = typeof args.year === 'number' ? Math.floor(args.year) : null;
|
|
909
|
+
const rows = FQHC_PPS_BASE_RATES;
|
|
910
|
+
if (askedYear == null) {
|
|
911
|
+
const latest = rows[rows.length - 1];
|
|
912
|
+
return {
|
|
913
|
+
found: true,
|
|
914
|
+
source: FQHC_SOURCE,
|
|
915
|
+
year: latest.year,
|
|
916
|
+
year_was_defaulted: true,
|
|
917
|
+
base_rate_usd: latest.base_rate_usd,
|
|
918
|
+
effective_date: latest.effective_date,
|
|
919
|
+
market_basket_update_pct: latest.market_basket_update_pct,
|
|
920
|
+
prior_year_rate_usd: latest.prior_year_rate_usd,
|
|
921
|
+
transmittal: latest.transmittal,
|
|
922
|
+
change_request: latest.change_request,
|
|
923
|
+
publication: latest.publication,
|
|
924
|
+
statement: fqhcStatement(latest),
|
|
925
|
+
per_visit_not_per_code: 'This is a per-DAY encounter rate (lesser of actual charges or this PPS rate), not a fee per HCPCS/CPT code.',
|
|
926
|
+
history: rows,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
const row = rows.find((r) => r.year === askedYear);
|
|
930
|
+
if (!row) {
|
|
931
|
+
return {
|
|
932
|
+
found: false,
|
|
933
|
+
reason: 'year_not_loaded',
|
|
934
|
+
loaded_years: rows.map((r) => r.year),
|
|
935
|
+
hint: `No FQHC PPS base payment rate loaded for CY ${askedYear}. Loaded years: ${rows.map((r) => r.year).join(', ')}.`,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
found: true,
|
|
940
|
+
source: FQHC_SOURCE,
|
|
941
|
+
year: row.year,
|
|
942
|
+
year_was_defaulted: false,
|
|
943
|
+
base_rate_usd: row.base_rate_usd,
|
|
944
|
+
effective_date: row.effective_date,
|
|
945
|
+
market_basket_update_pct: row.market_basket_update_pct,
|
|
946
|
+
prior_year_rate_usd: row.prior_year_rate_usd,
|
|
947
|
+
transmittal: row.transmittal,
|
|
948
|
+
change_request: row.change_request,
|
|
949
|
+
publication: row.publication,
|
|
950
|
+
statement: fqhcStatement(row),
|
|
951
|
+
per_visit_not_per_code: 'This is a per-DAY encounter rate (lesser of actual charges or this PPS rate), not a fee per HCPCS/CPT code.',
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
866
955
|
const tools: McpToolExport['tools'] = [
|
|
867
956
|
{
|
|
868
957
|
name: 'medicare_physician_payment',
|
|
@@ -896,7 +985,7 @@ const tools: McpToolExport['tools'] = [
|
|
|
896
985
|
{
|
|
897
986
|
name: 'medicare_lab_test_lookup',
|
|
898
987
|
description:
|
|
899
|
-
'Resolve a branded molecular or genomic lab test by its commercial name (e.g. Signatera, Guardant360 CDx, FoundationOne CDx, FoundationOne Liquid CDx, Tempus xT CDx, DecisionDx-Melanoma, Envisia, Shield) to its CPT PLA / HCPCS billing code, the lab that runs it, what Medicare pays for it now on the Clinical Laboratory Fee Schedule, and the MolDX local coverage determinations (LCDs) and billing-and-coding articles for its test category. Answers "what does Medicare pay for Signatera", "what is the PLA code for Guardant360", "which MolDX LCD covers this test". Also accepts a lab name (e.g. "Natera") or a code.
|
|
988
|
+
'Resolve a branded molecular or genomic lab test by its commercial name (e.g. Signatera, Guardant360 CDx, FoundationOne CDx, FoundationOne Liquid CDx, Tempus xT CDx, DecisionDx-Melanoma, Envisia, Shield) to its CPT PLA / HCPCS billing code, the lab that runs it, what Medicare pays for it now on the Clinical Laboratory Fee Schedule, and the MolDX local coverage determinations (LCDs) and billing-and-coding articles for its test category. Answers "what does Medicare pay for Signatera", "what is the PLA code for Guardant360", "which MolDX LCD covers this test". Also accepts a lab name (e.g. "Natera") or a code. Also covers non-ADLT branded MAAA tests (Oncotype DX, Prosigna, MammaPrint, EndoPredict, Breast Cancer Index, Cologuard, Prolaris, Decipher, Afirma, ConfirmMDx, 4Kscore) and any test whose lab registered its code in the NCBI Genetic Testing Registry. Resolves in order: the CMS list of Advanced Diagnostic Laboratory Tests (ADLTs), a brand-to-code table of MAAA tests with MolDX billing articles, then NCBI GTR live; a test found in none returns brand_not_found naming every source checked. Sourced from CMS (ADLT list, Clinical Laboratory Fee Schedule, Medicare Coverage Database) and NCBI GTR.',
|
|
900
989
|
inputSchema: {
|
|
901
990
|
type: 'object' as const,
|
|
902
991
|
properties: {
|
|
@@ -966,6 +1055,18 @@ const tools: McpToolExport['tools'] = [
|
|
|
966
1055
|
required: ['code'],
|
|
967
1056
|
},
|
|
968
1057
|
},
|
|
1058
|
+
{
|
|
1059
|
+
name: 'medicare_fqhc_pps_rate',
|
|
1060
|
+
description:
|
|
1061
|
+
'What does Medicare pay a Federally Qualified Health Center (FQHC) per visit? Returns the CMS FQHC Prospective Payment System (PPS) national BASE payment rate for a calendar year — the figure CMS publishes once a year, before any per-FQHC Geographic Adjustment Factor (GAF) is applied. Answers "what is the FQHC PPS base rate", "how much does Medicare pay an FQHC per encounter", "FQHC PPS rate for 2025 vs 2026". '
|
|
1062
|
+
+ 'Medicare pays an FQHC the LESSER of its actual charges or this PPS rate for all FQHC services furnished to a beneficiary on a day with a qualifying face-to-face visit — it is a per-DAY encounter rate, not a fee per procedure code (unlike medicare_physician_payment/medicare_lab_rate). This tool does NOT compute the GAF-adjusted, FQHC-specific amount — only the national base rate before that adjustment; the GAF table is not loaded here. Omit year for the newest loaded year plus the full known history (small — CMS publishes one number a year); pass year for one row.',
|
|
1063
|
+
inputSchema: {
|
|
1064
|
+
type: 'object' as const,
|
|
1065
|
+
properties: {
|
|
1066
|
+
year: { type: 'number', description: 'Calendar year, e.g. 2026. Omit to get the newest loaded year AND the full history table.' },
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
},
|
|
969
1070
|
{
|
|
970
1071
|
name: 'medicare_pricing_coverage',
|
|
971
1072
|
description:
|
|
@@ -1041,6 +1142,12 @@ async function resolveLocality(cfg: Cfg, q: unknown, year: number): Promise<{ ch
|
|
|
1041
1142
|
}
|
|
1042
1143
|
|
|
1043
1144
|
async function callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
|
|
1145
|
+
// FQHC PPS is a hand-maintained constant table (see FQHC_PPS_BASE_RATES
|
|
1146
|
+
// above) — not Supabase-backed like the rest of this pack — so it is
|
|
1147
|
+
// dispatched before the credential check below, which every OTHER tool
|
|
1148
|
+
// here needs.
|
|
1149
|
+
if (name === 'medicare_fqhc_pps_rate') return fqhcPpsRate(args);
|
|
1150
|
+
|
|
1044
1151
|
const url = (args._supabaseUrl as string | undefined)?.trim();
|
|
1045
1152
|
const key = (args._supabaseKey as string | undefined)?.trim();
|
|
1046
1153
|
if (!url || !key) {
|
|
@@ -1136,57 +1243,94 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
1136
1243
|
const test = typeof args.test === 'string' ? args.test.trim() : '';
|
|
1137
1244
|
if (!test) return { found: false, reason: 'no_test', hint: 'Pass test, e.g. "Signatera".' };
|
|
1138
1245
|
const includeRetired = args.include_retired === true;
|
|
1139
|
-
|
|
1140
|
-
|
|
1246
|
+
// Sources in order (fleet #2458): the CMS ADLT list, then the MAAA brand
|
|
1247
|
+
// table, then NCBI GTR live. The first that resolves answers; a miss
|
|
1248
|
+
// names every source checked.
|
|
1249
|
+
type Row = { code: string; test_name: string; lab: string; coverage_topic: string | null; code_source: string; adlt?: AdltTest; gtr?: { accession: string; url: string } };
|
|
1250
|
+
const sourcesChecked: string[] = ['CMS ADLT list'];
|
|
1251
|
+
let matched_by: string | null = null;
|
|
1252
|
+
let rows: Row[] = [];
|
|
1253
|
+
let sourceLabel = 'CMS list of Advanced Diagnostic Laboratory Tests (ADLTs) under the Medicare CLFS';
|
|
1254
|
+
const adltHit = matchAdlt(test);
|
|
1255
|
+
if (adltHit.tests.length) {
|
|
1256
|
+
matched_by = adltHit.matched_by;
|
|
1257
|
+
rows = adltHit.tests.map((t) => ({ code: t.code, test_name: t.test_name, lab: t.lab, coverage_topic: t.coverage_topic, code_source: 'CMS ADLT list', adlt: t }));
|
|
1258
|
+
} else {
|
|
1259
|
+
sourcesChecked.push('MAAA brand table (CPT Appendix O pairs backed by MolDX billing articles)');
|
|
1260
|
+
const b = matchTests(BRAND_TESTS, test);
|
|
1261
|
+
if (b.tests.length) {
|
|
1262
|
+
matched_by = b.matched_by;
|
|
1263
|
+
sourceLabel = BRAND_SOURCE;
|
|
1264
|
+
rows = b.tests.map((t) => ({ code: t.code, test_name: t.test_name, lab: t.lab, coverage_topic: t.coverage_topic, code_source: 'MAAA brand table' }));
|
|
1265
|
+
} else {
|
|
1266
|
+
sourcesChecked.push('NCBI Genetic Testing Registry (live)');
|
|
1267
|
+
const g = await gtrLookup(test, ((u: string) => pwFetch(u)) as unknown as typeof fetch);
|
|
1268
|
+
if (g === null) sourcesChecked[sourcesChecked.length - 1] += ' — did not answer';
|
|
1269
|
+
else if (g.length) {
|
|
1270
|
+
matched_by = 'test_name';
|
|
1271
|
+
sourceLabel = 'NCBI Genetic Testing Registry (GTR), code as registered by the performing lab';
|
|
1272
|
+
const seen = new Set<string>();
|
|
1273
|
+
rows = g.filter((h) => !seen.has(h.code + h.lab) && seen.add(h.code + h.lab)).slice(0, 10)
|
|
1274
|
+
.map((h) => ({ code: h.code, test_name: h.test_name, lab: h.lab, coverage_topic: null, code_source: 'NCBI GTR (lab-reported)', gtr: { accession: h.gtr_accession, url: h.url } }));
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const fromAdlt = rows.some((r) => r.adlt);
|
|
1279
|
+
const [snapshot, coverage] = await Promise.all([
|
|
1280
|
+
fromAdlt || !rows.length ? adltSnapshotCheck() : Promise.resolve({}),
|
|
1281
|
+
rows.some((t) => t.coverage_topic) ? coverageIndex() : Promise.resolve(null),
|
|
1282
|
+
]);
|
|
1141
1283
|
const provenance = {
|
|
1142
|
-
source:
|
|
1143
|
-
source_url: ADLT_SOURCE_URL,
|
|
1144
|
-
data_as_of: ADLT_DATA_AS_OF,
|
|
1284
|
+
source: `${sourceLabel}; rates from the CMS Clinical Laboratory Fee Schedule; policies from the CMS Medicare Coverage Database`,
|
|
1285
|
+
source_url: fromAdlt || !rows.length ? ADLT_SOURCE_URL : rows[0].gtr ? 'https://www.ncbi.nlm.nih.gov/gtr/' : 'https://www.cms.gov/medicare-coverage-database/',
|
|
1286
|
+
data_as_of: fromAdlt || !rows.length ? ADLT_DATA_AS_OF : rows[0].gtr ? new Date().toISOString().slice(0, 10) : BRAND_DATA_AS_OF,
|
|
1145
1287
|
...snapshot,
|
|
1146
1288
|
};
|
|
1147
|
-
if (!
|
|
1289
|
+
if (!rows.length) {
|
|
1148
1290
|
return {
|
|
1149
1291
|
found: false,
|
|
1150
|
-
reason: '
|
|
1292
|
+
reason: 'brand_not_found',
|
|
1151
1293
|
test,
|
|
1152
|
-
|
|
1294
|
+
sources_checked: sourcesChecked,
|
|
1295
|
+
hint: `No billing code found for "${test}" in: ${sourcesChecked.join('; ')}. The full PLA code list with proprietary names is published by the AMA under copyright and is not used here. If you have the code (PLA codes end in U, e.g. "0340U"), medicare_lab_rate gives its rate; medicare_lcd_search finds coverage policies by topic.`,
|
|
1153
1296
|
...provenance,
|
|
1154
1297
|
};
|
|
1155
1298
|
}
|
|
1156
|
-
const results = await Promise.all(
|
|
1299
|
+
const results = await Promise.all(rows.map(async (t) => {
|
|
1157
1300
|
const rates = await pg<{ year: number; quarter: string; rate: number }>(cfg,
|
|
1158
1301
|
`clfs_rates?hcpcs=eq.${t.code}&select=year,quarter,rate&order=year.desc,quarter.desc&limit=1`);
|
|
1159
1302
|
// "Response to Comments" articles are the comment period's record, not policy.
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1303
|
+
const topic = t.coverage_topic ? normName(t.coverage_topic) : '';
|
|
1304
|
+
const docs = topic && coverage
|
|
1305
|
+
? coverage.filter((d) => normName(d.title).includes(topic) && !/^response to comments/i.test(d.title))
|
|
1162
1306
|
: [];
|
|
1163
1307
|
const active = docs.filter((d) => !d.retired);
|
|
1164
1308
|
const retired = docs.filter((d) => d.retired);
|
|
1309
|
+
const a = t.adlt;
|
|
1165
1310
|
return {
|
|
1166
1311
|
code: t.code,
|
|
1167
|
-
...(
|
|
1312
|
+
...(a?.code_note ? { code_note: a.code_note } : {}),
|
|
1168
1313
|
test_name: t.test_name,
|
|
1169
1314
|
lab: t.lab,
|
|
1315
|
+
code_source: t.code_source,
|
|
1316
|
+
...(t.gtr ? { gtr: t.gtr } : {}),
|
|
1170
1317
|
current_clfs_rate: rates.length
|
|
1171
1318
|
? { rate_usd: rates[0].rate, fee_schedule_year: rates[0].year, quarter: rates[0].quarter, note: 'National rate; the CLFS is not adjusted by locality.' }
|
|
1172
1319
|
: { rate_usd: null, note: `${t.code} has no rate on the loaded Clinical Laboratory Fee Schedule (it may be contractor-priced).` },
|
|
1173
|
-
adlt:
|
|
1174
|
-
approval_date:
|
|
1175
|
-
|
|
1176
|
-
new_adlt_initial_period: t.new_adlt_initial_period,
|
|
1177
|
-
initial_period_payment_usd: t.initial_period_payment_usd,
|
|
1178
|
-
},
|
|
1320
|
+
adlt: a
|
|
1321
|
+
? { approval_date: a.adlt_approval_date, existing_adlt: a.existing_adlt, new_adlt_initial_period: a.new_adlt_initial_period, initial_period_payment_usd: a.initial_period_payment_usd }
|
|
1322
|
+
: { is_adlt: false },
|
|
1179
1323
|
coverage: t.coverage_topic
|
|
1180
1324
|
? {
|
|
1181
1325
|
policy_topic: t.coverage_topic,
|
|
1182
|
-
topic_note: 'Policies whose title matches this test
|
|
1326
|
+
topic_note: 'Policies whose title matches this test or its category (our topic mapping, not CMS\'s). Whether this specific test is listed is stated in the billing article\'s code table.',
|
|
1183
1327
|
active_documents: active.slice(0, 20),
|
|
1184
1328
|
active_count: active.length,
|
|
1185
1329
|
...(includeRetired ? { retired_documents: retired.slice(0, 20) } : {}),
|
|
1186
1330
|
retired_count: retired.length,
|
|
1187
1331
|
...(coverage === null ? { error: 'CMS Coverage API did not answer; retry, or use medicare_lcd_search with the policy_topic.' } : {}),
|
|
1188
1332
|
}
|
|
1189
|
-
: { policy_topic: null, note: 'No MolDX local policy title
|
|
1333
|
+
: { policy_topic: null, note: 'No MolDX local policy title matched to this test; it may be covered nationally (NCD) or case by case — medicare_lcd_search can look.' },
|
|
1190
1334
|
};
|
|
1191
1335
|
}));
|
|
1192
1336
|
return {
|
|
@@ -1194,7 +1338,8 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
1194
1338
|
query: test,
|
|
1195
1339
|
matched_by,
|
|
1196
1340
|
tests: results,
|
|
1197
|
-
|
|
1341
|
+
sources_checked: sourcesChecked,
|
|
1342
|
+
no_descriptors_note: 'The AMA\'s CPT descriptor text is not returned (AMA copyright); only the code, test name and lab.',
|
|
1198
1343
|
...provenance,
|
|
1199
1344
|
};
|
|
1200
1345
|
}
|
|
@@ -1391,6 +1536,7 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
1391
1536
|
geographic_localities: gpciN.length ? { year: gpciN[0].year } : null,
|
|
1392
1537
|
clinical_lab_fee_schedule: clfsN.length ? { year: clfsN[0].year, quarter: clfsN[0].quarter } : null,
|
|
1393
1538
|
dmepos_fee_schedule: dmeN.length ? { year: dmeN[0].year, release: dmeN[0].release, release_month: DME_RELEASE_MONTH[dmeN[0].release] ?? null, priced_by: 'state (not Medicare locality), with separate rural/non-rural amounts' } : null,
|
|
1539
|
+
fqhc_pps_base_rate: { newest_loaded_year: FQHC_PPS_BASE_RATES[FQHC_PPS_BASE_RATES.length - 1].year, loaded_years: FQHC_PPS_BASE_RATES.map((r) => r.year), source: FQHC_SOURCE },
|
|
1394
1540
|
medically_unlikely_edits: mueN.length ? { effective_from: mueN[0].effective_date } : null,
|
|
1395
1541
|
procedure_to_procedure_edits: ptpRange.length ? {
|
|
1396
1542
|
baseline_vintage: '2026q3 (v322r0, effective 2026-07-01) — CMS\'s FULL NCCI PTP edit baseline, AMA-CPT-licence-gated; licence accepted on the company\'s behalf by Bruce, 2026-09-07 (fleet #1293)',
|
|
@@ -1400,7 +1546,7 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
1400
1546
|
refresh_cadence: 'Physician fee schedule annually (with intra-year releases); lab fee schedule, DMEPOS fee schedule, MUE units edits, and PTP pair-edit changes all quarterly (as necessary).',
|
|
1401
1547
|
no_descriptions_note:
|
|
1402
1548
|
'CPT procedure descriptions (physician fee schedule, lab schedule) are not returned — American Medical Association copyright. DMEPOS is the exception: it uses HCPCS Level II codes, which CMS itself maintains, so medicare_dmepos_rate DOES return the code description.',
|
|
1403
|
-
source: 'CMS Physician Fee Schedule, Clinical Laboratory Fee Schedule, DMEPOS fee schedule, and National Correct Coding Initiative (MUE + PTP quarterly change files)',
|
|
1549
|
+
source: 'CMS Physician Fee Schedule, Clinical Laboratory Fee Schedule, DMEPOS fee schedule, FQHC PPS base payment rate, and National Correct Coding Initiative (MUE + PTP quarterly change files)',
|
|
1404
1550
|
};
|
|
1405
1551
|
}
|
|
1406
1552
|
|
package/src/server.ts
CHANGED
|
@@ -8,8 +8,9 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import pack from './index.js';
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
const server = new Server(
|
|
12
|
-
{ name: '@pipeworx/mcp-medicare-pricing', version: '0.1.
|
|
13
|
+
{ name: '@pipeworx/mcp-medicare-pricing', version: '0.1.4' },
|
|
13
14
|
{ capabilities: { tools: {} } },
|
|
14
15
|
);
|
|
15
16
|
|
|
@@ -24,7 +25,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
24
25
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
25
26
|
const { name, arguments: args } = request.params;
|
|
26
27
|
try {
|
|
27
|
-
const
|
|
28
|
+
const callArgs = { ...((args ?? {}) as Record<string, unknown>) };
|
|
29
|
+
|
|
30
|
+
const result = await pack.callTool(name, callArgs);
|
|
28
31
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
29
32
|
} catch (err) {
|
|
30
33
|
return {
|