@cliwant/mcp-sam-gov 1.3.0 → 1.5.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 (149) hide show
  1. package/README.ja.md +20 -12
  2. package/README.ko.md +20 -12
  3. package/README.md +62 -14
  4. package/dist/bea.d.ts +1 -1
  5. package/dist/bea.js +1 -1
  6. package/dist/cbp-border.d.ts +51 -0
  7. package/dist/cbp-border.d.ts.map +1 -0
  8. package/dist/cbp-border.js +123 -0
  9. package/dist/cbp-border.js.map +1 -0
  10. package/dist/census-economic.d.ts +1 -1
  11. package/dist/census-economic.d.ts.map +1 -1
  12. package/dist/census-economic.js +12 -6
  13. package/dist/census-economic.js.map +1 -1
  14. package/dist/cms-facility.d.ts +112 -0
  15. package/dist/cms-facility.d.ts.map +1 -0
  16. package/dist/cms-facility.js +311 -0
  17. package/dist/cms-facility.js.map +1 -0
  18. package/dist/cms-hospital.d.ts +105 -0
  19. package/dist/cms-hospital.d.ts.map +1 -0
  20. package/dist/cms-hospital.js +290 -0
  21. package/dist/cms-hospital.js.map +1 -0
  22. package/dist/cms-supplier.d.ts +133 -0
  23. package/dist/cms-supplier.d.ts.map +1 -0
  24. package/dist/cms-supplier.js +414 -0
  25. package/dist/cms-supplier.js.map +1 -0
  26. package/dist/cms-utilization.d.ts +113 -0
  27. package/dist/cms-utilization.d.ts.map +1 -0
  28. package/dist/cms-utilization.js +328 -0
  29. package/dist/cms-utilization.js.map +1 -0
  30. package/dist/courtlistener.d.ts +115 -0
  31. package/dist/courtlistener.d.ts.map +1 -0
  32. package/dist/courtlistener.js +398 -0
  33. package/dist/courtlistener.js.map +1 -0
  34. package/dist/cpsc.d.ts +81 -0
  35. package/dist/cpsc.d.ts.map +1 -0
  36. package/dist/cpsc.js +283 -0
  37. package/dist/cpsc.js.map +1 -0
  38. package/dist/datagov-catalog.d.ts.map +1 -1
  39. package/dist/datagov-catalog.js +16 -2
  40. package/dist/datagov-catalog.js.map +1 -1
  41. package/dist/dol.d.ts +2 -2
  42. package/dist/dol.js +5 -5
  43. package/dist/dol.js.map +1 -1
  44. package/dist/ecfr.d.ts +2 -2
  45. package/dist/ecfr.d.ts.map +1 -1
  46. package/dist/ecfr.js +24 -10
  47. package/dist/ecfr.js.map +1 -1
  48. package/dist/edgar.d.ts.map +1 -1
  49. package/dist/edgar.js +26 -6
  50. package/dist/edgar.js.map +1 -1
  51. package/dist/epa-envirofacts.d.ts +97 -0
  52. package/dist/epa-envirofacts.d.ts.map +1 -0
  53. package/dist/epa-envirofacts.js +305 -0
  54. package/dist/epa-envirofacts.js.map +1 -0
  55. package/dist/errors.d.ts.map +1 -1
  56. package/dist/errors.js +11 -0
  57. package/dist/errors.js.map +1 -1
  58. package/dist/far.d.ts.map +1 -1
  59. package/dist/far.js +3 -1
  60. package/dist/far.js.map +1 -1
  61. package/dist/federal-register.d.ts +2 -2
  62. package/dist/federal-register.d.ts.map +1 -1
  63. package/dist/federal-register.js +26 -10
  64. package/dist/federal-register.js.map +1 -1
  65. package/dist/fema.d.ts +36 -0
  66. package/dist/fema.d.ts.map +1 -1
  67. package/dist/fema.js +124 -0
  68. package/dist/fema.js.map +1 -1
  69. package/dist/fred.d.ts +1 -1
  70. package/dist/fred.js +1 -1
  71. package/dist/gov-domains.d.ts +66 -0
  72. package/dist/gov-domains.d.ts.map +1 -0
  73. package/dist/gov-domains.js +211 -0
  74. package/dist/gov-domains.js.map +1 -0
  75. package/dist/keys.d.ts +6 -5
  76. package/dist/keys.d.ts.map +1 -1
  77. package/dist/keys.js +25 -6
  78. package/dist/keys.js.map +1 -1
  79. package/dist/nhtsa.d.ts +91 -0
  80. package/dist/nhtsa.d.ts.map +1 -0
  81. package/dist/nhtsa.js +263 -0
  82. package/dist/nhtsa.js.map +1 -0
  83. package/dist/nist-controls.d.ts +48 -0
  84. package/dist/nist-controls.d.ts.map +1 -0
  85. package/dist/nist-controls.js +174 -0
  86. package/dist/nist-controls.js.map +1 -0
  87. package/dist/nonprofit.d.ts +116 -0
  88. package/dist/nonprofit.d.ts.map +1 -0
  89. package/dist/nonprofit.js +342 -0
  90. package/dist/nonprofit.js.map +1 -0
  91. package/dist/nws-weather.d.ts +57 -0
  92. package/dist/nws-weather.d.ts.map +1 -0
  93. package/dist/nws-weather.js +131 -0
  94. package/dist/nws-weather.js.map +1 -0
  95. package/dist/openfda-device.d.ts +85 -0
  96. package/dist/openfda-device.d.ts.map +1 -0
  97. package/dist/openfda-device.js +277 -0
  98. package/dist/openfda-device.js.map +1 -0
  99. package/dist/openfda-drugsfda.d.ts +72 -0
  100. package/dist/openfda-drugsfda.d.ts.map +1 -0
  101. package/dist/openfda-drugsfda.js +230 -0
  102. package/dist/openfda-drugsfda.js.map +1 -0
  103. package/dist/openfda.d.ts +133 -0
  104. package/dist/openfda.d.ts.map +1 -0
  105. package/dist/openfda.js +425 -0
  106. package/dist/openfda.js.map +1 -0
  107. package/dist/server.d.ts.map +1 -1
  108. package/dist/server.js +996 -16
  109. package/dist/server.js.map +1 -1
  110. package/dist/treasury.d.ts +2 -0
  111. package/dist/treasury.d.ts.map +1 -1
  112. package/dist/treasury.js +7 -0
  113. package/dist/treasury.js.map +1 -1
  114. package/dist/usaspending.d.ts +32 -1
  115. package/dist/usaspending.d.ts.map +1 -1
  116. package/dist/usaspending.js +143 -16
  117. package/dist/usaspending.js.map +1 -1
  118. package/package.json +3 -2
  119. package/src/bea.ts +1 -1
  120. package/src/cbp-border.ts +177 -0
  121. package/src/census-economic.ts +12 -6
  122. package/src/cms-facility.ts +379 -0
  123. package/src/cms-hospital.ts +344 -0
  124. package/src/cms-supplier.ts +527 -0
  125. package/src/cms-utilization.ts +389 -0
  126. package/src/courtlistener.ts +465 -0
  127. package/src/cpsc.ts +333 -0
  128. package/src/datagov-catalog.ts +18 -2
  129. package/src/dol.ts +5 -5
  130. package/src/ecfr.ts +27 -10
  131. package/src/edgar.ts +39 -7
  132. package/src/epa-envirofacts.ts +358 -0
  133. package/src/errors.ts +11 -0
  134. package/src/far.ts +3 -1
  135. package/src/federal-register.ts +29 -10
  136. package/src/fema.ts +139 -0
  137. package/src/fred.ts +1 -1
  138. package/src/gov-domains.ts +237 -0
  139. package/src/keys.ts +27 -6
  140. package/src/nhtsa.ts +352 -0
  141. package/src/nist-controls.ts +219 -0
  142. package/src/nonprofit.ts +460 -0
  143. package/src/nws-weather.ts +167 -0
  144. package/src/openfda-device.ts +356 -0
  145. package/src/openfda-drugsfda.ts +313 -0
  146. package/src/openfda.ts +518 -0
  147. package/src/server.ts +1127 -27
  148. package/src/treasury.ts +7 -0
  149. package/src/usaspending.ts +189 -17
@@ -0,0 +1,313 @@
1
+ /**
2
+ * openfda-drugsfda.ts — openFDA DRUG APPROVALS (Drugs@FDA, api.fda.gov) — the
3
+ * PHARMA REGULATORY-APPROVAL lane. FDA-approved drug applications (NDA/ANDA/BLA):
4
+ * the sponsor, the application number, each approved product (brand/generic name,
5
+ * dosage form, route, marketing status), and the submission/approval history.
6
+ * Answers "what drugs did sponsor X get approved, and which are still marketed" —
7
+ * pharma vendor product/approval intelligence.
8
+ *
9
+ * ★ SAME SOURCE + ENVELOPE + CRUX as openfda.ts / openfda-device.ts (ADR-0054/0056).
10
+ * Same host (api.fda.gov), same envelope `{ meta:{ results:{ skip,limit,total }},
11
+ * results:[…] }`, same ★no-match→HTTP-404-NOT_FOUND-as-honest-empty crux, same
12
+ * optional query-key K-test, same fixed-host SSRF idiom, same structured-only (no
13
+ * raw Lucene passthrough) search. REUSES openfda.ts's `fetchOpenfda`,
14
+ * `readOpenfdaError`, `luceneQuote`, `openfdaApiKey`, `OPENFDA_HOST` verbatim.
15
+ *
16
+ * GET https://api.fda.gov/drug/drugsfda.json?search=<lucene>&limit=&skip=[&api_key=]
17
+ * → { meta:{ results:{ skip,limit,total }}, results:[ { application_number,
18
+ * sponsor_name, products:[…], submissions:[…] } ] }.
19
+ *
20
+ * ★ HONESTY (mirrors the siblings exactly): [P1] totalAvailable = meta.results.total
21
+ * EXACT (never results.length); skip/limit offset pagination. [★P2] 404 NOT_FOUND
22
+ * ⇒ honest empty (never thrown); other 4xx ⇒ invalid_input surfacing openFDA's
23
+ * message; 5xx/429 ⇒ THROW. [P3] every scalar via `str` (null-never-""); nested
24
+ * products/submissions arrays default to [] and each field is str (never fabricated).
25
+ * [P4] meta.results / results absent-or-mis-shaped ⇒ driftError. [K-test] the
26
+ * OPTIONAL OPENFDA_API_KEY rides ONLY &api_key=; label/source name mode only.
27
+ * [SSRF] fixed host; all VALUES Lucene-escaped + phrase-quoted via URLSearchParams.
28
+ */
29
+
30
+ import { ToolErrorCarrier, errorFromResponse } from "./errors.js";
31
+ import { driftError } from "./datasource.js";
32
+ import { str } from "./coerce.js";
33
+ import { withMeta, type MetaBundle, type ResponseMeta } from "./meta.js";
34
+ import {
35
+ OPENFDA_HOST,
36
+ fetchOpenfda,
37
+ readOpenfdaError,
38
+ luceneQuote,
39
+ openfdaApiKey,
40
+ } from "./openfda.js";
41
+
42
+ const DEFAULT_LIMIT = 25;
43
+ const MAX_LIMIT = 100;
44
+
45
+ /** host+path-only label (→ ToolError.upstreamEndpoint); NEVER carries the key. */
46
+ const DRUGSFDA_LABEL = "openfda:/drug/drugsfda";
47
+
48
+ // ─── Honesty notes ────────────────────────────────────────────────
49
+ const NOT_DETERMINATION_NOTE =
50
+ "openFDA Drugs@FDA records are FDA-published drug-application data; treat approval/marketing status as of the source's last publication. This is reference data, not a live regulatory determination.";
51
+ const KEYLESS_NOTE =
52
+ "Keyless: no OPENFDA_API_KEY is set. openFDA allows ~1000 requests/day without a key; a free key raises the rate limit (get one at https://open.fda.gov/apis/authentication/).";
53
+ const KEYED_NOTE =
54
+ "OPENFDA_API_KEY is set — it rides ONLY the &api_key= query parameter to api.fda.gov (openFDA has no header option), raising the rate limit. Its value is NEVER logged, echoed, or placed in this response.";
55
+ const NO_FILTER_NOTE =
56
+ "No structured filters were applied — this is an unscoped scan of the WHOLE Drugs@FDA collection. Add sponsorName / brandName / activeIngredient / applicationNumber to scope the result set.";
57
+ const MARKETING_NOTE =
58
+ "products[].marketingStatus is the FDA marketing category (e.g. 'Prescription', 'Over-the-counter', 'Discontinued', 'None (Tentative Approval)') — a 'Discontinued' product is NOT an approval revocation. submissions[] is the application's action history (ORIG approval + subsequent supplements).";
59
+
60
+ // ─── Curated row shape ────────────────────────────────────────────
61
+ export type DrugsfdaProduct = {
62
+ brandName: string | null;
63
+ genericIngredients: { name: string | null; strength: string | null }[];
64
+ dosageForm: string | null;
65
+ route: string | null;
66
+ marketingStatus: string | null;
67
+ };
68
+ export type DrugsfdaSubmission = {
69
+ submissionType: string | null; // ORIG / SUPPL
70
+ submissionNumber: string | null;
71
+ submissionStatus: string | null; // AP (approved) etc.
72
+ submissionStatusDate: string | null; // YYYYMMDD string, preserved (P3)
73
+ submissionClass: string | null;
74
+ };
75
+ export type DrugsfdaApplication = {
76
+ applicationNumber: string | null;
77
+ sponsorName: string | null;
78
+ products: DrugsfdaProduct[];
79
+ submissions: DrugsfdaSubmission[];
80
+ };
81
+
82
+ /** Map ONE Drugs@FDA result row → the curated shape. Every scalar via `str`. */
83
+ function mapApplication(row: unknown): DrugsfdaApplication {
84
+ const r = (row ?? {}) as Record<string, unknown>;
85
+ const products = Array.isArray(r.products) ? r.products : [];
86
+ const submissions = Array.isArray(r.submissions) ? r.submissions : [];
87
+ return {
88
+ applicationNumber: str(r.application_number),
89
+ sponsorName: str(r.sponsor_name),
90
+ products: products.map((p) => {
91
+ const pr = (p ?? {}) as Record<string, unknown>;
92
+ const ings = Array.isArray(pr.active_ingredients) ? pr.active_ingredients : [];
93
+ return {
94
+ brandName: str(pr.brand_name),
95
+ genericIngredients: ings.map((a) => {
96
+ const ai = (a ?? {}) as Record<string, unknown>;
97
+ return { name: str(ai.name), strength: str(ai.strength) };
98
+ }),
99
+ dosageForm: str(pr.dosage_form),
100
+ route: str(pr.route),
101
+ marketingStatus: str(pr.marketing_status),
102
+ };
103
+ }),
104
+ submissions: submissions.map((s) => {
105
+ const su = (s ?? {}) as Record<string, unknown>;
106
+ return {
107
+ submissionType: str(su.submission_type),
108
+ submissionNumber: str(su.submission_number),
109
+ submissionStatus: str(su.submission_status),
110
+ submissionStatusDate: str(su.submission_status_date),
111
+ submissionClass: str(su.submission_class_code_description),
112
+ };
113
+ }),
114
+ };
115
+ }
116
+
117
+ // ─── Lucene search assembly (structured-only; injection-safe) ─────
118
+ export type DrugsfdaFilters = {
119
+ sponsorName?: string; // → sponsor_name
120
+ brandName?: string; // → products.brand_name
121
+ activeIngredient?: string; // → products.active_ingredients.name
122
+ applicationNumber?: string; // → application_number
123
+ };
124
+
125
+ export function buildDrugsfdaSearch(f: DrugsfdaFilters): string {
126
+ const clauses: string[] = [];
127
+ if (f.sponsorName !== undefined)
128
+ clauses.push(`sponsor_name:${luceneQuote(f.sponsorName)}`);
129
+ if (f.brandName !== undefined)
130
+ clauses.push(`products.brand_name:${luceneQuote(f.brandName)}`);
131
+ if (f.activeIngredient !== undefined)
132
+ clauses.push(`products.active_ingredients.name:${luceneQuote(f.activeIngredient)}`);
133
+ if (f.applicationNumber !== undefined)
134
+ clauses.push(`application_number:${luceneQuote(f.applicationNumber)}`);
135
+ return clauses.join(" AND ");
136
+ }
137
+
138
+ // ─── Tool: openfda_drug_approvals ─────────────────────────────────
139
+ export type DrugApprovalsArgs = DrugsfdaFilters & {
140
+ limit?: number;
141
+ skip?: number;
142
+ };
143
+
144
+ /**
145
+ * Search openFDA Drugs@FDA drug-approval applications with structured filters →
146
+ * curated application rows (sponsor, application number, approved products, submission
147
+ * history) + honest `_meta`. KEYLESS (OPTIONAL OPENFDA_API_KEY only raises the rate
148
+ * limit). totalAvailable = meta.results.total (EXACT); skip/limit pagination. A
149
+ * no-match query (openFDA HTTP 404 NOT_FOUND) ⇒ an honest empty, never a throw.
150
+ */
151
+ export async function drugApprovals(args: DrugApprovalsArgs): Promise<MetaBundle> {
152
+ const limit = clampLimit(args.limit);
153
+ const skip = clampSkip(args.skip);
154
+
155
+ const filters: DrugsfdaFilters = {
156
+ sponsorName: args.sponsorName,
157
+ brandName: args.brandName,
158
+ activeIngredient: args.activeIngredient,
159
+ applicationNumber: args.applicationNumber,
160
+ };
161
+ const search = buildDrugsfdaSearch(filters);
162
+ const filtersApplied: string[] = [];
163
+ if (args.sponsorName !== undefined) filtersApplied.push("sponsorName");
164
+ if (args.brandName !== undefined) filtersApplied.push("brandName");
165
+ if (args.activeIngredient !== undefined) filtersApplied.push("activeIngredient");
166
+ if (args.applicationNumber !== undefined) filtersApplied.push("applicationNumber");
167
+
168
+ const params = new URLSearchParams();
169
+ if (search !== "") params.set("search", search);
170
+ params.set("limit", String(limit));
171
+ params.set("skip", String(skip));
172
+ const key = openfdaApiKey();
173
+ if (key !== undefined) params.set("api_key", key); // OPTIONAL — &api_key= ONLY
174
+
175
+ const url = `https://${OPENFDA_HOST}/drug/drugsfda.json?${params.toString()}`;
176
+
177
+ const res = await fetchOpenfda(url, DRUGSFDA_LABEL);
178
+
179
+ if (res.status === 404) {
180
+ const { code, message } = await readOpenfdaError(res);
181
+ if (code === "NOT_FOUND") {
182
+ return emptyResult(limit, skip, filtersApplied, key !== undefined);
183
+ }
184
+ throw new ToolErrorCarrier({
185
+ ...errorFromResponse(res, DRUGSFDA_LABEL),
186
+ message: message
187
+ ? `openFDA returned HTTP 404 at ${DRUGSFDA_LABEL}: ${message}`
188
+ : `Resource not found at ${DRUGSFDA_LABEL} (HTTP 404).`,
189
+ });
190
+ }
191
+
192
+ if (res.status >= 400 && res.status < 500 && res.status !== 429) {
193
+ const { message } = await readOpenfdaError(res);
194
+ throw new ToolErrorCarrier({
195
+ kind: "invalid_input",
196
+ retryable: false,
197
+ message: message
198
+ ? `openFDA rejected the request (HTTP ${res.status}) at ${DRUGSFDA_LABEL}: ${message}`
199
+ : `Bad request (HTTP ${res.status}) at ${DRUGSFDA_LABEL}.`,
200
+ upstreamStatus: res.status,
201
+ upstreamEndpoint: DRUGSFDA_LABEL,
202
+ });
203
+ }
204
+
205
+ if (!res.ok) {
206
+ throw new ToolErrorCarrier(errorFromResponse(res, DRUGSFDA_LABEL));
207
+ }
208
+
209
+ let body: unknown;
210
+ try {
211
+ body = await res.json();
212
+ } catch (e) {
213
+ if (e instanceof SyntaxError) {
214
+ throw driftError(
215
+ DRUGSFDA_LABEL,
216
+ `openFDA ${DRUGSFDA_LABEL} returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).`,
217
+ );
218
+ }
219
+ throw e;
220
+ }
221
+
222
+ const b = (body ?? {}) as { meta?: unknown; results?: unknown };
223
+ const meta = (b.meta ?? {}) as { results?: unknown };
224
+ const metaResults = meta.results as { total?: unknown } | undefined;
225
+ if (metaResults === undefined || metaResults === null || typeof metaResults !== "object") {
226
+ throw driftError(
227
+ DRUGSFDA_LABEL,
228
+ `openFDA ${DRUGSFDA_LABEL} shape drift — meta.results (the skip/limit/total carrier) is missing.`,
229
+ );
230
+ }
231
+ if (!Array.isArray(b.results)) {
232
+ throw driftError(
233
+ DRUGSFDA_LABEL,
234
+ `openFDA ${DRUGSFDA_LABEL} shape drift — results must be an array.`,
235
+ );
236
+ }
237
+
238
+ const applications = (b.results as unknown[]).map(mapApplication);
239
+ const returned = applications.length;
240
+
241
+ const rawTotal = metaResults.total;
242
+ const totalAvailable =
243
+ typeof rawTotal === "number" && Number.isFinite(rawTotal) ? rawTotal : null;
244
+ const hasMore = totalAvailable !== null && skip + returned < totalAvailable;
245
+ const nextOffset = hasMore ? skip + returned : null;
246
+
247
+ const notes: string[] = [NOT_DETERMINATION_NOTE, MARKETING_NOTE, keyNote(key !== undefined)];
248
+ if (filtersApplied.length === 0) notes.push(NO_FILTER_NOTE);
249
+
250
+ return withMeta(
251
+ { applications },
252
+ {
253
+ source: `${OPENFDA_HOST} /drug/drugsfda (openFDA Drugs@FDA approvals; ${
254
+ key !== undefined ? "OPENFDA_API_KEY rate-limit key applied" : "keyless"
255
+ })`,
256
+ keylessMode: true,
257
+ returned,
258
+ totalAvailable,
259
+ filtersApplied,
260
+ filtersDropped: [],
261
+ fieldsUnavailable: [],
262
+ pagination: { offset: skip, limit, hasMore, nextOffset },
263
+ notes,
264
+ } satisfies Partial<ResponseMeta>,
265
+ );
266
+ }
267
+
268
+ // ─── Small helpers ────────────────────────────────────────────────
269
+ function keyNote(hasKey: boolean): string {
270
+ return hasKey ? KEYED_NOTE : KEYLESS_NOTE;
271
+ }
272
+
273
+ function emptyResult(
274
+ limit: number,
275
+ skip: number,
276
+ filtersApplied: string[],
277
+ hasKey: boolean,
278
+ ): MetaBundle {
279
+ return withMeta(
280
+ { applications: [] as DrugsfdaApplication[] },
281
+ {
282
+ source: `${OPENFDA_HOST} /drug/drugsfda (openFDA Drugs@FDA approvals; ${
283
+ hasKey ? "OPENFDA_API_KEY rate-limit key applied" : "keyless"
284
+ })`,
285
+ keylessMode: true,
286
+ returned: 0,
287
+ totalAvailable: 0,
288
+ filtersApplied,
289
+ filtersDropped: [],
290
+ fieldsUnavailable: [],
291
+ pagination: { offset: skip, limit, hasMore: false, nextOffset: null },
292
+ notes: [
293
+ "No Drugs@FDA applications matched this query (openFDA returned HTTP 404 NOT_FOUND — the source's honest no-match). This is an exact empty, not an error.",
294
+ NOT_DETERMINATION_NOTE,
295
+ keyNote(hasKey),
296
+ ],
297
+ } satisfies Partial<ResponseMeta>,
298
+ );
299
+ }
300
+
301
+ function clampLimit(v: unknown): number {
302
+ if (typeof v !== "number" || !Number.isFinite(v)) return DEFAULT_LIMIT;
303
+ const n = Math.floor(v);
304
+ if (n < 1) return 1;
305
+ if (n > MAX_LIMIT) return MAX_LIMIT;
306
+ return n;
307
+ }
308
+
309
+ function clampSkip(v: unknown): number {
310
+ if (typeof v !== "number" || !Number.isFinite(v)) return 0;
311
+ const n = Math.floor(v);
312
+ return n < 0 ? 0 : n;
313
+ }