@cliwant/mcp-sam-gov 1.2.0 → 1.4.0
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.ja.md +22 -9
- package/README.ko.md +22 -9
- package/README.md +70 -12
- package/dist/bea.d.ts +105 -0
- package/dist/bea.d.ts.map +1 -0
- package/dist/bea.js +303 -0
- package/dist/bea.js.map +1 -0
- package/dist/census-economic.d.ts +1 -1
- package/dist/census-economic.d.ts.map +1 -1
- package/dist/census-economic.js +12 -6
- package/dist/census-economic.js.map +1 -1
- package/dist/cms-facility.d.ts +112 -0
- package/dist/cms-facility.d.ts.map +1 -0
- package/dist/cms-facility.js +311 -0
- package/dist/cms-facility.js.map +1 -0
- package/dist/cms-hospital.d.ts +105 -0
- package/dist/cms-hospital.d.ts.map +1 -0
- package/dist/cms-hospital.js +290 -0
- package/dist/cms-hospital.js.map +1 -0
- package/dist/cms-supplier.d.ts +133 -0
- package/dist/cms-supplier.d.ts.map +1 -0
- package/dist/cms-supplier.js +414 -0
- package/dist/cms-supplier.js.map +1 -0
- package/dist/cms-utilization.d.ts +113 -0
- package/dist/cms-utilization.d.ts.map +1 -0
- package/dist/cms-utilization.js +328 -0
- package/dist/cms-utilization.js.map +1 -0
- package/dist/courtlistener.d.ts +115 -0
- package/dist/courtlistener.d.ts.map +1 -0
- package/dist/courtlistener.js +398 -0
- package/dist/courtlistener.js.map +1 -0
- package/dist/cpsc.d.ts +81 -0
- package/dist/cpsc.d.ts.map +1 -0
- package/dist/cpsc.js +283 -0
- package/dist/cpsc.js.map +1 -0
- package/dist/dol.d.ts +118 -0
- package/dist/dol.d.ts.map +1 -0
- package/dist/dol.js +421 -0
- package/dist/dol.js.map +1 -0
- package/dist/epa-envirofacts.d.ts +97 -0
- package/dist/epa-envirofacts.d.ts.map +1 -0
- package/dist/epa-envirofacts.js +292 -0
- package/dist/epa-envirofacts.js.map +1 -0
- package/dist/fred.d.ts +1 -1
- package/dist/fred.js +1 -1
- package/dist/keys.d.ts +11 -8
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +55 -8
- package/dist/keys.js.map +1 -1
- package/dist/lda.d.ts +105 -0
- package/dist/lda.d.ts.map +1 -0
- package/dist/lda.js +317 -0
- package/dist/lda.js.map +1 -0
- package/dist/nhtsa.d.ts +91 -0
- package/dist/nhtsa.d.ts.map +1 -0
- package/dist/nhtsa.js +263 -0
- package/dist/nhtsa.js.map +1 -0
- package/dist/nonprofit.d.ts +116 -0
- package/dist/nonprofit.d.ts.map +1 -0
- package/dist/nonprofit.js +342 -0
- package/dist/nonprofit.js.map +1 -0
- package/dist/openfda-device.d.ts +85 -0
- package/dist/openfda-device.d.ts.map +1 -0
- package/dist/openfda-device.js +277 -0
- package/dist/openfda-device.js.map +1 -0
- package/dist/openfda.d.ts +133 -0
- package/dist/openfda.d.ts.map +1 -0
- package/dist/openfda.js +402 -0
- package/dist/openfda.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +872 -6
- package/dist/server.js.map +1 -1
- package/package.json +2 -1
- package/src/bea.ts +372 -0
- package/src/census-economic.ts +12 -6
- package/src/cms-facility.ts +379 -0
- package/src/cms-hospital.ts +344 -0
- package/src/cms-supplier.ts +527 -0
- package/src/cms-utilization.ts +389 -0
- package/src/courtlistener.ts +465 -0
- package/src/cpsc.ts +333 -0
- package/src/dol.ts +515 -0
- package/src/epa-envirofacts.ts +342 -0
- package/src/fred.ts +1 -1
- package/src/keys.ts +60 -8
- package/src/lda.ts +385 -0
- package/src/nhtsa.ts +352 -0
- package/src/nonprofit.ts +460 -0
- package/src/openfda-device.ts +356 -0
- package/src/openfda.ts +495 -0
- package/src/server.ts +995 -6
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cms-supplier.ts — two CMS supplier/vetting lanes on `data.cms.gov` (the SAME
|
|
3
|
+
* data-API v1 dataset endpoint + the SAME two-request stats-count pattern as
|
|
4
|
+
* cms-utilization.ts / ADR-0061; ADR-0064). KEYLESS.
|
|
5
|
+
*
|
|
6
|
+
* WHAT IT ADDS
|
|
7
|
+
* 1. `cms_dmepos_suppliers` — CMS "Medicare Durable Medical Equipment,
|
|
8
|
+
* Devices & Supplies by Supplier": for a given supplier (NPI) or state, the
|
|
9
|
+
* DMEPOS supplier's identity + aggregate Medicare figures (HCPCS codes,
|
|
10
|
+
* beneficiaries, claims, services, submitted / Medicare-allowed / -paid
|
|
11
|
+
* amounts). A supplier-market / competitor-utilization lane on the SUPPLY
|
|
12
|
+
* side (who bills Medicare for equipment).
|
|
13
|
+
* 2. `cms_revoked_providers` — CMS "Revoked Medicare Providers & Suppliers":
|
|
14
|
+
* the legally-published debarment / revocation list (7,059 rows), with the
|
|
15
|
+
* revoked provider's identity, provider type, revocation reason, effective
|
|
16
|
+
* date, and re-enrollment-bar expiration. A vetting / due-diligence lane in
|
|
17
|
+
* the SAME class as the OFAC / SAM-exclusions lists already shipped —
|
|
18
|
+
* surfacing the names IS the point (this is a public exclusion list).
|
|
19
|
+
*
|
|
20
|
+
* ★THE TWO-REQUEST PATTERN (the load-bearing P1 honesty — MIRRORS cms-utilization):
|
|
21
|
+
* the data-API's `/data` slice is a bare JSON array that reports NO total. So the
|
|
22
|
+
* EXACT total for a filter comes from a SEPARATE count sub-query — the identical
|
|
23
|
+
* `filter[...]` on the `/data-viewer/stats` endpoint returns
|
|
24
|
+
* `{ "data": { "found_rows": N, "total_rows": M } }`. Each tool runs the stats
|
|
25
|
+
* count FIRST (best-effort) then the data slice: totalAvailable = found_rows (P1,
|
|
26
|
+
* the per-filter EXACT total), NEVER the returned rows' length. If the stats
|
|
27
|
+
* sub-query fails or is absent, totalAvailable falls to null + a disclosing note
|
|
28
|
+
* (never a length-faked total) and the data slice still returns.
|
|
29
|
+
*
|
|
30
|
+
* ★THE FILTER-REQUIRED INPUT GUARD (dmepos only): the supplier table is large; an
|
|
31
|
+
* all-empty query (no npi, no state) is REFUSED with invalid_input (0 fetch) — a
|
|
32
|
+
* caller MUST pin npi OR state. The revocation list is only ~7K rows, so it is
|
|
33
|
+
* safe to page unfiltered (all its filters are optional).
|
|
34
|
+
*
|
|
35
|
+
* The module writes ZERO fetch/coercion/error/meta code — it REUSES `getJson`
|
|
36
|
+
* (redirect:"error") / `driftError` (datasource.ts), `str`/`num` (coerce.ts,
|
|
37
|
+
* null-never-empty-string / null-never-0), and `withMeta`·`buildMeta` (meta.ts).
|
|
38
|
+
* It MIRRORS cms-utilization.ts's fixed-host SSRF idiom (a single host const + a
|
|
39
|
+
* post-construction hostname/protocol assertion + redirect:"error") and its
|
|
40
|
+
* count-first two-request pattern + schema_drift catch-ladder.
|
|
41
|
+
*
|
|
42
|
+
* ★ SSRF: the host is a compile-time literal (`CMS_HOST`); the dataset UUIDs + the
|
|
43
|
+
* endpoint paths are MODULE literals. Every USER filter value rides as a
|
|
44
|
+
* URLSearchParams VALUE (`filter[Col]=Val`) — URLSearchParams encodes the bracket
|
|
45
|
+
* key AND the value, so a value can never break out into the path or inject a
|
|
46
|
+
* parameter. npi is `^\d{10}$`; state `^[A-Za-z]{2}$`; lastName is a bounded
|
|
47
|
+
* free-text charclass; size/offset are coerced to integers. A post-construction
|
|
48
|
+
* hostname/protocol assertion + `redirect:"error"` fail closed on any off-host 3xx.
|
|
49
|
+
*
|
|
50
|
+
* ★ HONESTY (ADR-0064 P1–P5, live-verified 2026-07-15 on data.cms.gov):
|
|
51
|
+
* [P1] totalAvailable = the stats sub-query's found_rows (EXACT), NOT the slice
|
|
52
|
+
* length. hasMore = offset+returned < total. Stats fails/absent ⇒
|
|
53
|
+
* totalAvailable:null + a disclosing note (never length-faked).
|
|
54
|
+
* [P2] empty array ⇒ honest empty (returned:0). dmepos all-empty input ⇒
|
|
55
|
+
* invalid_input (0 fetch). getJson maps a 4xx (⇒ invalid_input / not_found)
|
|
56
|
+
* / 5xx (⇒ upstream_unavailable) and THROWS; a 200 non-array/non-JSON body ⇒
|
|
57
|
+
* schema_drift (NEVER a fabricated empty).
|
|
58
|
+
* [P3] aggregates via num() (numeric strings → numbers; a real 0 stays 0; absent
|
|
59
|
+
* ⇒ null, never 0-faked); NPI / codes / reasons / dates as strings
|
|
60
|
+
* (null-never-empty-string); a coalesced name ⇒ null if none.
|
|
61
|
+
* [P4] a data body that is not an array ⇒ driftError; a stats body missing
|
|
62
|
+
* found_rows ⇒ totalAvailable:null (handled, not a crash).
|
|
63
|
+
*/
|
|
64
|
+
import { ToolErrorCarrier } from "./errors.js";
|
|
65
|
+
import { getJson, driftError } from "./datasource.js";
|
|
66
|
+
import { str, num } from "./coerce.js";
|
|
67
|
+
import { withMeta } from "./meta.js";
|
|
68
|
+
// Re-export the shared honesty coercions (single audited copy in ./coerce.js) so a
|
|
69
|
+
// regression fails together across sources. NO local num/str.
|
|
70
|
+
export { num, str };
|
|
71
|
+
// ─── SSRF core: the single fixed host + module-literal path pieces ──
|
|
72
|
+
const CMS_HOST = "data.cms.gov";
|
|
73
|
+
// HOST-only label — surfaces in ToolError.upstreamEndpoint; keyless, so no token
|
|
74
|
+
// can ever appear here.
|
|
75
|
+
const CMS_LABEL = "cms-supplier:data.cms.gov";
|
|
76
|
+
// ★THE DATASET UUIDs — SPECIFIC ANNUAL VINTAGES on data.cms.gov (live-verified,
|
|
77
|
+
// keyless). ★UPDATE YEARLY for the DMEPOS set (CMS publishes a new uuid per
|
|
78
|
+
// calendar year of supplier data); the revocation list is a rolling published
|
|
79
|
+
// register. Each vintage is surfaced to the caller in a _meta note so a consumer
|
|
80
|
+
// never mistakes it for "current" or an unspecified year.
|
|
81
|
+
const DMEPOS_DATASET_UUID = "a2d56d3f-3531-4315-9d87-e29986516b41"; // DMEPOS by Supplier (annual vintage)
|
|
82
|
+
const REVOKED_DATASET_UUID = "a6496a7d-4e19-479a-a9ad-d4c0a49e07c3"; // Revoked Medicare Providers & Suppliers (~7,059 rows)
|
|
83
|
+
// ─── Validation charclasses (SSRF + "verify the input" honesty) ───
|
|
84
|
+
const NPI_RE = /^\d{10}$/; // a 10-digit National Provider Identifier
|
|
85
|
+
const STATE_RE = /^[A-Za-z]{2}$/; // 2-letter state/territory abbreviation
|
|
86
|
+
// lastName rides as a URLSearchParams VALUE (encoded), so this bound is a sanity
|
|
87
|
+
// guard, not an SSRF necessity: letters/digits/space and common name punctuation.
|
|
88
|
+
const LAST_NAME_RE = /^[A-Za-z0-9 .,'-]{1,100}$/;
|
|
89
|
+
const SIZE_MIN = 1;
|
|
90
|
+
const SIZE_MAX = 100;
|
|
91
|
+
const SIZE_DEFAULT = 25;
|
|
92
|
+
// ─── Honesty notes (ADR-0064 required set) ────────────────────────
|
|
93
|
+
const DMEPOS_VINTAGE_NOTE = `Source dataset: CMS "Medicare Durable Medical Equipment, Devices & Supplies — by Supplier" (data.cms.gov dataset ${DMEPOS_DATASET_UUID}) — a SPECIFIC ANNUAL VINTAGE (the most recent published year at build time), NOT a live/current or a multi-year figure. Amounts and counts are as-of that reference year. CMS publishes a new dataset id each year.`;
|
|
94
|
+
const DMEPOS_AGGREGATE_NOTE = "These are public SUPPLIER-level AGGREGATE Medicare DMEPOS figures (no patient identifiers). totalBeneficiaries is CMS-rounded and suppressed below 11 in the source. This is a utilization snapshot, NOT a fraud, quality, or fitness determination.";
|
|
95
|
+
const REVOKED_LIST_NOTE = `Source: CMS's PUBLIC "Revoked Medicare Providers & Suppliers" list (data.cms.gov dataset ${REVOKED_DATASET_UUID}) — a legally-published revocation/exclusion register (the same vetting class as the OFAC / SAM exclusion lists). A listing reflects a past Medicare enrollment revocation with its stated reason; it is a due-diligence signal, NOT a current-eligibility, guilt, or fitness determination. Verify against the primary source before acting.`;
|
|
96
|
+
const COUNT_FALLBACK_NOTE = "The count sub-query (…/data-viewer/stats) failed or did not report found_rows, so totalAvailable is null (unknown) — it was NOT faked from the returned row count. hasMore is a heuristic (a full page ⇒ likely more); re-page with offset to confirm.";
|
|
97
|
+
// ─── SSRF-guarded fetch (fixed host + hostname assertion + redirect:"error") ──
|
|
98
|
+
/**
|
|
99
|
+
* GET one data.cms.gov JSON resource at a MODULE-BUILT URL (the dataset UUID + the
|
|
100
|
+
* endpoint path are literals; all user filter VALUES are already carried in the
|
|
101
|
+
* URLSearchParams `query`). Asserts the CONSTRUCTED URL's hostname === the fixed
|
|
102
|
+
* host over https, and sets `redirect:"error"` (an off-host 3xx must NOT be
|
|
103
|
+
* followed). Keyless — no headers.
|
|
104
|
+
*/
|
|
105
|
+
async function getCms(path, query) {
|
|
106
|
+
const url = `https://${CMS_HOST}${path}?${query.toString()}`;
|
|
107
|
+
const built = new URL(url);
|
|
108
|
+
if (built.hostname !== CMS_HOST || built.protocol !== "https:") {
|
|
109
|
+
throw new ToolErrorCarrier({
|
|
110
|
+
kind: "invalid_input",
|
|
111
|
+
message: `Constructed CMS data-API URL host ${JSON.stringify(built.hostname)} (${built.protocol}) does not match the fixed host ${JSON.stringify(CMS_HOST)} over https — refusing to fetch (SSRF safety).`,
|
|
112
|
+
retryable: false,
|
|
113
|
+
upstreamEndpoint: CMS_LABEL,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return getJson(built.toString(), { label: CMS_LABEL, redirect: "error" });
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The shared two-request slice: run the stats count sub-query FIRST (best-effort —
|
|
120
|
+
* the EXACT total, P1), then the authoritative data slice. A count failure degrades
|
|
121
|
+
* to totalAvailable:null + countFailed (never throws, never length-fakes). The data
|
|
122
|
+
* slice preserves the 4xx/5xx/timeout ToolErrorCarrier taxonomy, reclassifies a 200
|
|
123
|
+
* non-JSON SyntaxError to schema_drift, and asserts the body is an array (P4).
|
|
124
|
+
*/
|
|
125
|
+
async function fetchDatasetSlice(uuid, filters, size, offset) {
|
|
126
|
+
const dataBase = `/data-api/v1/dataset/${uuid}/data`;
|
|
127
|
+
const statsPath = `/data-api/v1/dataset/${uuid}/data-viewer/stats`;
|
|
128
|
+
// ── (1) The COUNT sub-query FIRST. Body is `{ data: { found_rows, total_rows } }`.
|
|
129
|
+
// Any failure (network/5xx/drift/missing field) degrades to totalAvailable:null
|
|
130
|
+
// + a note; it NEVER throws and NEVER fakes the total from the slice length. ──
|
|
131
|
+
let totalAvailable = null;
|
|
132
|
+
let countFailed = false;
|
|
133
|
+
try {
|
|
134
|
+
const statsBody = await getCms(statsPath, filters);
|
|
135
|
+
const dataObj = statsBody !== null &&
|
|
136
|
+
typeof statsBody === "object" &&
|
|
137
|
+
typeof statsBody.data === "object" &&
|
|
138
|
+
statsBody.data !== null
|
|
139
|
+
? statsBody.data
|
|
140
|
+
: undefined;
|
|
141
|
+
if (dataObj !== undefined) {
|
|
142
|
+
const t = num(dataObj.found_rows);
|
|
143
|
+
if (t !== null && t >= 0) {
|
|
144
|
+
totalAvailable = t;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
countFailed = true; // present body but no usable found_rows (P4)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
countFailed = true; // stats body not the expected { data: {…} } shape
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
countFailed = true; // any count error ⇒ degrade, never propagate (P1)
|
|
156
|
+
}
|
|
157
|
+
// ── (2) The DATA slice — the authoritative request (a bare JSON array). ──
|
|
158
|
+
const dataQuery = new URLSearchParams(filters);
|
|
159
|
+
dataQuery.set("size", String(size));
|
|
160
|
+
dataQuery.set("offset", String(offset));
|
|
161
|
+
// Catch-ladder (cms-utilization shape): preserve the 4xx/5xx/timeout
|
|
162
|
+
// ToolErrorCarrier taxonomy FIRST; reclassify a 200 non-JSON SyntaxError to
|
|
163
|
+
// schema_drift SECOND; bare-rethrow LAST.
|
|
164
|
+
let body;
|
|
165
|
+
try {
|
|
166
|
+
body = await getCms(dataBase, dataQuery);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
if (e instanceof ToolErrorCarrier)
|
|
170
|
+
throw e;
|
|
171
|
+
if (e instanceof SyntaxError)
|
|
172
|
+
throw driftError(CMS_LABEL, "CMS data-API returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).");
|
|
173
|
+
throw e;
|
|
174
|
+
}
|
|
175
|
+
// [P4] the data body MUST be an array (a non-array 200 is drift, never a
|
|
176
|
+
// fabricated empty).
|
|
177
|
+
if (!Array.isArray(body)) {
|
|
178
|
+
throw driftError(CMS_LABEL, "CMS data-API shape drift — the /data response must be a JSON array of rows.");
|
|
179
|
+
}
|
|
180
|
+
return { rows: body, totalAvailable, countFailed };
|
|
181
|
+
}
|
|
182
|
+
/** Coerce args.size / args.offset to bounded integers (belt-and-suspenders behind
|
|
183
|
+
* the server Zod; a DIRECT handler call bypasses Zod). */
|
|
184
|
+
function boundSize(raw) {
|
|
185
|
+
let size = SIZE_DEFAULT;
|
|
186
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
187
|
+
size = Math.trunc(raw);
|
|
188
|
+
if (size < SIZE_MIN)
|
|
189
|
+
size = SIZE_MIN;
|
|
190
|
+
if (size > SIZE_MAX)
|
|
191
|
+
size = SIZE_MAX;
|
|
192
|
+
}
|
|
193
|
+
return size;
|
|
194
|
+
}
|
|
195
|
+
function boundOffset(raw) {
|
|
196
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
|
|
197
|
+
return Math.trunc(raw);
|
|
198
|
+
}
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
// ══════════════════════════════════════════════════════════════════
|
|
202
|
+
// (1) cms_dmepos_suppliers
|
|
203
|
+
// ══════════════════════════════════════════════════════════════════
|
|
204
|
+
/**
|
|
205
|
+
* Join the CMS Last_Name_Org + First_Name into one display name. An ORGANIZATION
|
|
206
|
+
* supplier (entity code "O") carries the org name in Last_Name_Org with an empty
|
|
207
|
+
* First_Name ⇒ just the org name. An INDIVIDUAL carries both ⇒ "Last, First".
|
|
208
|
+
* Either absent ⇒ the present one; both absent ⇒ null (never a fabricated "").
|
|
209
|
+
*/
|
|
210
|
+
export function joinSupplierName(lastOrg, first) {
|
|
211
|
+
const last = str(lastOrg);
|
|
212
|
+
const firstName = str(first);
|
|
213
|
+
if (last !== null && firstName !== null)
|
|
214
|
+
return `${last}, ${firstName}`;
|
|
215
|
+
return last ?? firstName ?? null;
|
|
216
|
+
}
|
|
217
|
+
/** Map ONE DMEPOS data-API row → the curated supplier shape. */
|
|
218
|
+
function mapSupplier(row) {
|
|
219
|
+
const r = (row ?? {});
|
|
220
|
+
return {
|
|
221
|
+
npi: str(r.Suplr_NPI),
|
|
222
|
+
supplierName: joinSupplierName(r.Suplr_Prvdr_Last_Name_Org, r.Suplr_Prvdr_First_Name),
|
|
223
|
+
credentials: str(r.Suplr_Prvdr_Crdntls),
|
|
224
|
+
entityType: str(r.Suplr_Prvdr_Ent_Cd),
|
|
225
|
+
city: str(r.Suplr_Prvdr_City),
|
|
226
|
+
state: str(r.Suplr_Prvdr_State_Abrvtn),
|
|
227
|
+
zip: str(r.Suplr_Prvdr_Zip5),
|
|
228
|
+
totalHcpcsCodes: num(r.Tot_Suplr_HCPCS_Cds),
|
|
229
|
+
totalBeneficiaries: num(r.Tot_Suplr_Benes),
|
|
230
|
+
totalClaims: num(r.Tot_Suplr_Clms),
|
|
231
|
+
totalServices: num(r.Tot_Suplr_Srvcs),
|
|
232
|
+
submittedCharges: num(r.Suplr_Sbmtd_Chrgs),
|
|
233
|
+
medicareAllowed: num(r.Suplr_Mdcr_Alowd_Amt),
|
|
234
|
+
medicarePayment: num(r.Suplr_Mdcr_Pymt_Amt),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Fetch DMEPOS supplier rows for an NPI / state → normalized supplier rows +
|
|
239
|
+
* honest `_meta`. REQUIRES npi OR state (an all-empty query is refused). Runs a
|
|
240
|
+
* stats count sub-query FIRST for the EXACT total (P1), then the data slice; a count
|
|
241
|
+
* failure degrades to totalAvailable:null + a note (never a length-faked total).
|
|
242
|
+
*/
|
|
243
|
+
export async function dmeposSuppliers(args) {
|
|
244
|
+
// ── [input guard] require npi OR state (never scan the whole supplier table). ──
|
|
245
|
+
const hasNpi = args.npi !== undefined && args.npi !== "";
|
|
246
|
+
const hasState = args.state !== undefined && args.state !== "";
|
|
247
|
+
if (!hasNpi && !hasState) {
|
|
248
|
+
throw new ToolErrorCarrier({
|
|
249
|
+
kind: "invalid_input",
|
|
250
|
+
retryable: false,
|
|
251
|
+
message: "cms_dmepos_suppliers requires at least `npi` (10-digit) OR `state` (2-letter) — an all-empty query would scan the entire DMEPOS supplier table and is refused. Add npi or state and retry.",
|
|
252
|
+
upstreamEndpoint: CMS_LABEL,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
// ── Validate + build the filter params (SSRF: charclass + URLSearchParams value). ──
|
|
256
|
+
const filters = new URLSearchParams();
|
|
257
|
+
const filtersApplied = [];
|
|
258
|
+
if (hasNpi) {
|
|
259
|
+
const npi = args.npi;
|
|
260
|
+
if (!NPI_RE.test(npi)) {
|
|
261
|
+
throw new ToolErrorCarrier({
|
|
262
|
+
kind: "invalid_input",
|
|
263
|
+
retryable: false,
|
|
264
|
+
message: `Invalid npi ${JSON.stringify(npi)} — expected a 10-digit National Provider Identifier (^\\d{10}$).`,
|
|
265
|
+
upstreamEndpoint: CMS_LABEL,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
filters.set("filter[Suplr_NPI]", npi);
|
|
269
|
+
filtersApplied.push(`npi:${npi}`);
|
|
270
|
+
}
|
|
271
|
+
if (hasState) {
|
|
272
|
+
const state = args.state;
|
|
273
|
+
if (!STATE_RE.test(state)) {
|
|
274
|
+
throw new ToolErrorCarrier({
|
|
275
|
+
kind: "invalid_input",
|
|
276
|
+
retryable: false,
|
|
277
|
+
message: `Invalid state ${JSON.stringify(state)} — expected a 2-letter state/territory code (^[A-Za-z]{2}$), e.g. "VA".`,
|
|
278
|
+
upstreamEndpoint: CMS_LABEL,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
filters.set("filter[Suplr_Prvdr_State_Abrvtn]", state.toUpperCase());
|
|
282
|
+
filtersApplied.push(`state:${state.toUpperCase()}`);
|
|
283
|
+
}
|
|
284
|
+
const size = boundSize(args.size);
|
|
285
|
+
const offset = boundOffset(args.offset);
|
|
286
|
+
const { rows, totalAvailable, countFailed } = await fetchDatasetSlice(DMEPOS_DATASET_UUID, filters, size, offset);
|
|
287
|
+
const suppliers = rows.map(mapSupplier);
|
|
288
|
+
const returned = suppliers.length;
|
|
289
|
+
const hasMore = totalAvailable !== null
|
|
290
|
+
? offset + returned < totalAvailable
|
|
291
|
+
: returned === size;
|
|
292
|
+
const nextOffset = hasMore ? offset + returned : null;
|
|
293
|
+
const notes = [DMEPOS_VINTAGE_NOTE, DMEPOS_AGGREGATE_NOTE];
|
|
294
|
+
if (countFailed)
|
|
295
|
+
notes.push(COUNT_FALLBACK_NOTE);
|
|
296
|
+
const meta = {
|
|
297
|
+
source: `${CMS_HOST} CMS Medicare DMEPOS — by Supplier (keyless)`,
|
|
298
|
+
keylessMode: true,
|
|
299
|
+
returned,
|
|
300
|
+
totalAvailable,
|
|
301
|
+
filtersApplied,
|
|
302
|
+
filtersDropped: [],
|
|
303
|
+
fieldsUnavailable: [],
|
|
304
|
+
pagination: { offset, limit: size, hasMore, nextOffset },
|
|
305
|
+
notes,
|
|
306
|
+
};
|
|
307
|
+
return withMeta({ suppliers }, meta);
|
|
308
|
+
}
|
|
309
|
+
// ══════════════════════════════════════════════════════════════════
|
|
310
|
+
// (2) cms_revoked_providers
|
|
311
|
+
// ══════════════════════════════════════════════════════════════════
|
|
312
|
+
/**
|
|
313
|
+
* Coalesce the revoked-provider display name: an ORGANIZATION carries ORG_NAME ⇒
|
|
314
|
+
* use it. An INDIVIDUAL carries FIRST_NAME + LAST_NAME ⇒ "First Last" (either
|
|
315
|
+
* present alone ⇒ that one). None present ⇒ null (never a fabricated "").
|
|
316
|
+
*/
|
|
317
|
+
export function coalesceRevokedName(org, first, last) {
|
|
318
|
+
const o = str(org);
|
|
319
|
+
if (o !== null)
|
|
320
|
+
return o;
|
|
321
|
+
const f = str(first);
|
|
322
|
+
const l = str(last);
|
|
323
|
+
if (f !== null && l !== null)
|
|
324
|
+
return `${f} ${l}`;
|
|
325
|
+
return f ?? l ?? null;
|
|
326
|
+
}
|
|
327
|
+
/** Map ONE revocation data-API row → the curated revocation shape. */
|
|
328
|
+
function mapRevocation(row) {
|
|
329
|
+
const r = (row ?? {});
|
|
330
|
+
return {
|
|
331
|
+
enrollmentId: str(r.ENRLMT_ID),
|
|
332
|
+
npi: str(r.NPI),
|
|
333
|
+
name: coalesceRevokedName(r.ORG_NAME, r.FIRST_NAME, r.LAST_NAME),
|
|
334
|
+
state: str(r.STATE_CD),
|
|
335
|
+
providerType: str(r.PROVIDER_TYPE_DESC),
|
|
336
|
+
revocationReason: str(r.REVOCATION_RSN),
|
|
337
|
+
revocationEffectiveDate: str(r.REVOCATION_EFCTV_DT),
|
|
338
|
+
reenrollmentBarExpiration: str(r.REENROLLMENT_BAR_EXPRTN_DT),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Fetch CMS revocation-list rows (all filters optional — the ~7K-row list is safe
|
|
343
|
+
* to page unfiltered) → normalized revocation rows + honest `_meta`. Runs a stats
|
|
344
|
+
* count sub-query FIRST for the EXACT total (P1), then the data slice; a count
|
|
345
|
+
* failure degrades to totalAvailable:null + a note (never a length-faked total).
|
|
346
|
+
*/
|
|
347
|
+
export async function revokedProviders(args) {
|
|
348
|
+
const filters = new URLSearchParams();
|
|
349
|
+
const filtersApplied = [];
|
|
350
|
+
if (args.npi !== undefined && args.npi !== "") {
|
|
351
|
+
const npi = args.npi;
|
|
352
|
+
if (!NPI_RE.test(npi)) {
|
|
353
|
+
throw new ToolErrorCarrier({
|
|
354
|
+
kind: "invalid_input",
|
|
355
|
+
retryable: false,
|
|
356
|
+
message: `Invalid npi ${JSON.stringify(npi)} — expected a 10-digit National Provider Identifier (^\\d{10}$).`,
|
|
357
|
+
upstreamEndpoint: CMS_LABEL,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
filters.set("filter[NPI]", npi);
|
|
361
|
+
filtersApplied.push(`npi:${npi}`);
|
|
362
|
+
}
|
|
363
|
+
if (args.state !== undefined && args.state !== "") {
|
|
364
|
+
const state = args.state;
|
|
365
|
+
if (!STATE_RE.test(state)) {
|
|
366
|
+
throw new ToolErrorCarrier({
|
|
367
|
+
kind: "invalid_input",
|
|
368
|
+
retryable: false,
|
|
369
|
+
message: `Invalid state ${JSON.stringify(state)} — expected a 2-letter state/territory code (^[A-Za-z]{2}$), e.g. "VA".`,
|
|
370
|
+
upstreamEndpoint: CMS_LABEL,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
filters.set("filter[STATE_CD]", state.toUpperCase());
|
|
374
|
+
filtersApplied.push(`state:${state.toUpperCase()}`);
|
|
375
|
+
}
|
|
376
|
+
if (args.lastName !== undefined && args.lastName !== "") {
|
|
377
|
+
const lastName = args.lastName;
|
|
378
|
+
if (!LAST_NAME_RE.test(lastName)) {
|
|
379
|
+
throw new ToolErrorCarrier({
|
|
380
|
+
kind: "invalid_input",
|
|
381
|
+
retryable: false,
|
|
382
|
+
message: `Invalid lastName ${JSON.stringify(lastName)} — allowed: letters, digits, space, . , ' - (≤100 chars).`,
|
|
383
|
+
upstreamEndpoint: CMS_LABEL,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
filters.set("filter[LAST_NAME]", lastName);
|
|
387
|
+
filtersApplied.push(`lastName:${lastName}`);
|
|
388
|
+
}
|
|
389
|
+
const size = boundSize(args.size);
|
|
390
|
+
const offset = boundOffset(args.offset);
|
|
391
|
+
const { rows, totalAvailable, countFailed } = await fetchDatasetSlice(REVOKED_DATASET_UUID, filters, size, offset);
|
|
392
|
+
const revocations = rows.map(mapRevocation);
|
|
393
|
+
const returned = revocations.length;
|
|
394
|
+
const hasMore = totalAvailable !== null
|
|
395
|
+
? offset + returned < totalAvailable
|
|
396
|
+
: returned === size;
|
|
397
|
+
const nextOffset = hasMore ? offset + returned : null;
|
|
398
|
+
const notes = [REVOKED_LIST_NOTE];
|
|
399
|
+
if (countFailed)
|
|
400
|
+
notes.push(COUNT_FALLBACK_NOTE);
|
|
401
|
+
const meta = {
|
|
402
|
+
source: `${CMS_HOST} CMS Revoked Medicare Providers & Suppliers (public revocation list, keyless)`,
|
|
403
|
+
keylessMode: true,
|
|
404
|
+
returned,
|
|
405
|
+
totalAvailable,
|
|
406
|
+
filtersApplied,
|
|
407
|
+
filtersDropped: [],
|
|
408
|
+
fieldsUnavailable: [],
|
|
409
|
+
pagination: { offset, limit: size, hasMore, nextOffset },
|
|
410
|
+
notes,
|
|
411
|
+
};
|
|
412
|
+
return withMeta({ revocations }, meta);
|
|
413
|
+
}
|
|
414
|
+
//# sourceMappingURL=cms-supplier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cms-supplier.js","sourceRoot":"","sources":["../src/cms-supplier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cms-utilization.ts — CMS Medicare Physician & Other Practitioners "by Provider
|
|
3
|
+
* and Service" utilization (`data.cms.gov`, the data-API v1 dataset endpoint;
|
|
4
|
+
* ADR-0061). KEYLESS.
|
|
5
|
+
*
|
|
6
|
+
* WHAT IT ADDS: `cms_medicare_provider_services` — a healthcare-market /
|
|
7
|
+
* competitor-utilization lane: for a given provider (NPI) or state, what Medicare
|
|
8
|
+
* Part-B services (HCPCS) did providers render, to how many beneficiaries, at what
|
|
9
|
+
* submitted / Medicare-allowed / Medicare-paid amounts. The demand-side complement
|
|
10
|
+
* to NPPES (who the providers ARE) — this is what they actually BILL.
|
|
11
|
+
*
|
|
12
|
+
* ★THE TWO-REQUEST PATTERN (the load-bearing P1 honesty — MIRRORS epa-envirofacts):
|
|
13
|
+
* the data-API's `/data` slice is a bare JSON array that reports NO total. So the
|
|
14
|
+
* EXACT total for a filter comes from a SEPARATE count sub-query — the identical
|
|
15
|
+
* `filter[...]` on the `/data-viewer/stats` endpoint returns
|
|
16
|
+
* `{ "data": { "found_rows": N, "total_rows": M } }`. This tool runs the stats
|
|
17
|
+
* count FIRST (best-effort) then the data slice: totalAvailable = found_rows (P1,
|
|
18
|
+
* the per-filter EXACT total), NEVER the returned rows' length. If the stats
|
|
19
|
+
* sub-query fails or is absent, totalAvailable falls to null + a disclosing note
|
|
20
|
+
* (never a length-faked total) and the data slice still returns.
|
|
21
|
+
*
|
|
22
|
+
* ★THE FILTER-REQUIRED INPUT GUARD: the table is 9.78M rows. An all-empty query
|
|
23
|
+
* (no npi, no state) is REFUSED with invalid_input (0 fetch) — providerType /
|
|
24
|
+
* hcpcsCode alone are NOT enough to scope; a caller MUST pin npi OR state.
|
|
25
|
+
*
|
|
26
|
+
* The module writes ZERO fetch/coercion/error/meta code — it REUSES `getJson`
|
|
27
|
+
* (redirect:"error") / `driftError` (datasource.ts), `str`/`num` (coerce.ts,
|
|
28
|
+
* null-never-empty-string / null-never-0), and `withMeta`·`buildMeta` (meta.ts,
|
|
29
|
+
* offset pagination + totalAvailable). It MIRRORS census-economic.ts's fixed-host
|
|
30
|
+
* SSRF idiom (a single host const + a post-construction hostname/protocol assertion
|
|
31
|
+
* + redirect:"error") and epa-envirofacts.ts's count-first two-request pattern +
|
|
32
|
+
* schema_drift catch-ladder (ToolErrorCarrier rethrow FIRST so a 5xx keeps its
|
|
33
|
+
* taxonomy → SyntaxError→driftError → bare rethrow).
|
|
34
|
+
*
|
|
35
|
+
* GET https://data.cms.gov/data-api/v1/dataset/{uuid}/data-viewer/stats
|
|
36
|
+
* ?filter[Rndrng_Prvdr_State_Abrvtn]=VA (the COUNT sub-query)
|
|
37
|
+
* → { "data": { "found_rows": 278254, "total_rows": 9781673 } }
|
|
38
|
+
* GET https://data.cms.gov/data-api/v1/dataset/{uuid}/data
|
|
39
|
+
* ?size=&offset=&filter[Rndrng_Prvdr_State_Abrvtn]=VA (the DATA slice)
|
|
40
|
+
* → [ { Rndrng_NPI, Rndrng_Prvdr_Last_Org_Name, …, Avg_Mdcr_Pymt_Amt }, … ]
|
|
41
|
+
*
|
|
42
|
+
* ★ SSRF: the host is a compile-time literal (`CMS_HOST`); the dataset UUID + the
|
|
43
|
+
* endpoint paths are MODULE literals. Every USER filter value rides as a
|
|
44
|
+
* URLSearchParams VALUE (`filter[Col]=Val`) — URLSearchParams encodes the bracket
|
|
45
|
+
* key AND the value, so a value can never break out into the path or inject a
|
|
46
|
+
* parameter. npi is `^\d{10}$`; state `^[A-Za-z]{2}$`; hcpcsCode `^[A-Za-z0-9]{1,10}$`;
|
|
47
|
+
* providerType is a bounded free-text charclass; size/offset are coerced to
|
|
48
|
+
* integers. A post-construction hostname/protocol assertion + `redirect:"error"`
|
|
49
|
+
* fail closed on any off-host 3xx.
|
|
50
|
+
*
|
|
51
|
+
* ★ PII NOTE: this is public PROVIDER-level AGGREGATE data — no patient identifiers.
|
|
52
|
+
* Provider name / practice address / NPI is public professional information (the
|
|
53
|
+
* same public surface as NPPES), so it is fine to surface.
|
|
54
|
+
*
|
|
55
|
+
* ★ HONESTY (ADR-0061 P1–P5, live-verified 2026-07-15 on data.cms.gov):
|
|
56
|
+
* [input] require npi OR state — an all-empty query is REFUSED (0 fetch) so the
|
|
57
|
+
* whole 9.78M-row table is never scanned.
|
|
58
|
+
* [P1] totalAvailable = the stats sub-query's found_rows (EXACT — e.g. VA =
|
|
59
|
+
* 278254), NOT the slice length. hasMore = offset+returned < total. Stats
|
|
60
|
+
* fails/absent ⇒ totalAvailable:null + a disclosing note.
|
|
61
|
+
* [P2] an empty array ⇒ honest empty (returned:0). getJson maps a 4xx/5xx via
|
|
62
|
+
* errorFromResponse and THROWS (503 ⇒ upstream_unavailable, 400 ⇒
|
|
63
|
+
* invalid_input, 404 ⇒ not_found); a 200 non-array/non-JSON body ⇒
|
|
64
|
+
* schema_drift (NEVER a fabricated empty).
|
|
65
|
+
* [P3] Tot_ / Avg_ fields via num() (numeric strings → numbers; a real 0 stays 0;
|
|
66
|
+
* absent ⇒ null, never 0-faked); NPI/codes/HCPCS/names as strings
|
|
67
|
+
* (null-never-empty-string).
|
|
68
|
+
* [P4] a data body that is not an array ⇒ driftError; a stats body missing
|
|
69
|
+
* found_rows ⇒ totalAvailable:null (handled, not a crash).
|
|
70
|
+
*/
|
|
71
|
+
import { str, num } from "./coerce.js";
|
|
72
|
+
import { type MetaBundle } from "./meta.js";
|
|
73
|
+
export { num, str };
|
|
74
|
+
export type ProviderService = {
|
|
75
|
+
npi: string | null;
|
|
76
|
+
providerName: string | null;
|
|
77
|
+
credentials: string | null;
|
|
78
|
+
providerType: string | null;
|
|
79
|
+
city: string | null;
|
|
80
|
+
state: string | null;
|
|
81
|
+
zip: string | null;
|
|
82
|
+
hcpcsCode: string | null;
|
|
83
|
+
hcpcsDescription: string | null;
|
|
84
|
+
totalBeneficiaries: number | null;
|
|
85
|
+
totalServices: number | null;
|
|
86
|
+
avgSubmittedCharge: number | null;
|
|
87
|
+
avgMedicareAllowed: number | null;
|
|
88
|
+
avgMedicarePayment: number | null;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Join the CMS Last_Org_Name + First_Name into one display name. An ORGANIZATION
|
|
92
|
+
* row (entity code "O") carries the org name in Last_Org_Name with an empty
|
|
93
|
+
* First_Name ⇒ just the org name. An INDIVIDUAL carries both ⇒ "Last, First".
|
|
94
|
+
* Either absent ⇒ the present one; both absent ⇒ null (never a fabricated "").
|
|
95
|
+
*/
|
|
96
|
+
export declare function joinProviderName(lastOrg: unknown, first: unknown): string | null;
|
|
97
|
+
export type CmsMedicareProviderServicesArgs = {
|
|
98
|
+
npi?: string;
|
|
99
|
+
state?: string;
|
|
100
|
+
providerType?: string;
|
|
101
|
+
hcpcsCode?: string;
|
|
102
|
+
size?: number;
|
|
103
|
+
offset?: number;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Fetch Medicare Part-B provider-service utilization rows for an NPI / state (+
|
|
107
|
+
* optional providerType / hcpcsCode) → normalized service rows + honest `_meta`.
|
|
108
|
+
* REQUIRES npi OR state (an all-empty query is refused). Runs a stats count
|
|
109
|
+
* sub-query FIRST for the EXACT total (P1), then the data slice; a count failure
|
|
110
|
+
* degrades to totalAvailable:null + a note (never a length-faked total).
|
|
111
|
+
*/
|
|
112
|
+
export declare function providerServices(args: CmsMedicareProviderServicesArgs): Promise<MetaBundle>;
|
|
113
|
+
//# sourceMappingURL=cms-utilization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cms-utilization.d.ts","sourceRoot":"","sources":["../src/cms-utilization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AAIH,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AAIzE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAsCpB,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKhF;AAgDD,MAAM,MAAM,+BAA+B,GAAG;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,+BAA+B,GACpC,OAAO,CAAC,UAAU,CAAC,CAkLrB"}
|