@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.
Files changed (91) hide show
  1. package/README.ja.md +22 -9
  2. package/README.ko.md +22 -9
  3. package/README.md +70 -12
  4. package/dist/bea.d.ts +105 -0
  5. package/dist/bea.d.ts.map +1 -0
  6. package/dist/bea.js +303 -0
  7. package/dist/bea.js.map +1 -0
  8. package/dist/census-economic.d.ts +1 -1
  9. package/dist/census-economic.d.ts.map +1 -1
  10. package/dist/census-economic.js +12 -6
  11. package/dist/census-economic.js.map +1 -1
  12. package/dist/cms-facility.d.ts +112 -0
  13. package/dist/cms-facility.d.ts.map +1 -0
  14. package/dist/cms-facility.js +311 -0
  15. package/dist/cms-facility.js.map +1 -0
  16. package/dist/cms-hospital.d.ts +105 -0
  17. package/dist/cms-hospital.d.ts.map +1 -0
  18. package/dist/cms-hospital.js +290 -0
  19. package/dist/cms-hospital.js.map +1 -0
  20. package/dist/cms-supplier.d.ts +133 -0
  21. package/dist/cms-supplier.d.ts.map +1 -0
  22. package/dist/cms-supplier.js +414 -0
  23. package/dist/cms-supplier.js.map +1 -0
  24. package/dist/cms-utilization.d.ts +113 -0
  25. package/dist/cms-utilization.d.ts.map +1 -0
  26. package/dist/cms-utilization.js +328 -0
  27. package/dist/cms-utilization.js.map +1 -0
  28. package/dist/courtlistener.d.ts +115 -0
  29. package/dist/courtlistener.d.ts.map +1 -0
  30. package/dist/courtlistener.js +398 -0
  31. package/dist/courtlistener.js.map +1 -0
  32. package/dist/cpsc.d.ts +81 -0
  33. package/dist/cpsc.d.ts.map +1 -0
  34. package/dist/cpsc.js +283 -0
  35. package/dist/cpsc.js.map +1 -0
  36. package/dist/dol.d.ts +118 -0
  37. package/dist/dol.d.ts.map +1 -0
  38. package/dist/dol.js +421 -0
  39. package/dist/dol.js.map +1 -0
  40. package/dist/epa-envirofacts.d.ts +97 -0
  41. package/dist/epa-envirofacts.d.ts.map +1 -0
  42. package/dist/epa-envirofacts.js +292 -0
  43. package/dist/epa-envirofacts.js.map +1 -0
  44. package/dist/fred.d.ts +1 -1
  45. package/dist/fred.js +1 -1
  46. package/dist/keys.d.ts +11 -8
  47. package/dist/keys.d.ts.map +1 -1
  48. package/dist/keys.js +55 -8
  49. package/dist/keys.js.map +1 -1
  50. package/dist/lda.d.ts +105 -0
  51. package/dist/lda.d.ts.map +1 -0
  52. package/dist/lda.js +317 -0
  53. package/dist/lda.js.map +1 -0
  54. package/dist/nhtsa.d.ts +91 -0
  55. package/dist/nhtsa.d.ts.map +1 -0
  56. package/dist/nhtsa.js +263 -0
  57. package/dist/nhtsa.js.map +1 -0
  58. package/dist/nonprofit.d.ts +116 -0
  59. package/dist/nonprofit.d.ts.map +1 -0
  60. package/dist/nonprofit.js +342 -0
  61. package/dist/nonprofit.js.map +1 -0
  62. package/dist/openfda-device.d.ts +85 -0
  63. package/dist/openfda-device.d.ts.map +1 -0
  64. package/dist/openfda-device.js +277 -0
  65. package/dist/openfda-device.js.map +1 -0
  66. package/dist/openfda.d.ts +133 -0
  67. package/dist/openfda.d.ts.map +1 -0
  68. package/dist/openfda.js +402 -0
  69. package/dist/openfda.js.map +1 -0
  70. package/dist/server.d.ts.map +1 -1
  71. package/dist/server.js +872 -6
  72. package/dist/server.js.map +1 -1
  73. package/package.json +2 -1
  74. package/src/bea.ts +372 -0
  75. package/src/census-economic.ts +12 -6
  76. package/src/cms-facility.ts +379 -0
  77. package/src/cms-hospital.ts +344 -0
  78. package/src/cms-supplier.ts +527 -0
  79. package/src/cms-utilization.ts +389 -0
  80. package/src/courtlistener.ts +465 -0
  81. package/src/cpsc.ts +333 -0
  82. package/src/dol.ts +515 -0
  83. package/src/epa-envirofacts.ts +342 -0
  84. package/src/fred.ts +1 -1
  85. package/src/keys.ts +60 -8
  86. package/src/lda.ts +385 -0
  87. package/src/nhtsa.ts +352 -0
  88. package/src/nonprofit.ts +460 -0
  89. package/src/openfda-device.ts +356 -0
  90. package/src/openfda.ts +495 -0
  91. 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"}