@cliwant/mcp-sam-gov 1.4.0 → 1.6.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 (140) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +240 -231
  3. package/README.ko.md +240 -231
  4. package/README.md +725 -706
  5. package/dist/cbp-border.d.ts +51 -0
  6. package/dist/cbp-border.d.ts.map +1 -0
  7. package/dist/cbp-border.js +123 -0
  8. package/dist/cbp-border.js.map +1 -0
  9. package/dist/datagov-catalog.d.ts.map +1 -1
  10. package/dist/datagov-catalog.js +16 -2
  11. package/dist/datagov-catalog.js.map +1 -1
  12. package/dist/ecfr.d.ts +2 -2
  13. package/dist/ecfr.d.ts.map +1 -1
  14. package/dist/ecfr.js +24 -10
  15. package/dist/ecfr.js.map +1 -1
  16. package/dist/edgar.d.ts.map +1 -1
  17. package/dist/edgar.js +26 -6
  18. package/dist/edgar.js.map +1 -1
  19. package/dist/epa-envirofacts.d.ts.map +1 -1
  20. package/dist/epa-envirofacts.js +14 -1
  21. package/dist/epa-envirofacts.js.map +1 -1
  22. package/dist/errors.d.ts +10 -0
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js +11 -0
  25. package/dist/errors.js.map +1 -1
  26. package/dist/far.d.ts.map +1 -1
  27. package/dist/far.js +3 -1
  28. package/dist/far.js.map +1 -1
  29. package/dist/federal-register.d.ts +2 -2
  30. package/dist/federal-register.d.ts.map +1 -1
  31. package/dist/federal-register.js +26 -10
  32. package/dist/federal-register.js.map +1 -1
  33. package/dist/feedback.d.ts +64 -0
  34. package/dist/feedback.d.ts.map +1 -0
  35. package/dist/feedback.js +131 -0
  36. package/dist/feedback.js.map +1 -0
  37. package/dist/fema.d.ts +36 -0
  38. package/dist/fema.d.ts.map +1 -1
  39. package/dist/fema.js +124 -0
  40. package/dist/fema.js.map +1 -1
  41. package/dist/gov-domains.d.ts +66 -0
  42. package/dist/gov-domains.d.ts.map +1 -0
  43. package/dist/gov-domains.js +211 -0
  44. package/dist/gov-domains.js.map +1 -0
  45. package/dist/nist-controls.d.ts +48 -0
  46. package/dist/nist-controls.d.ts.map +1 -0
  47. package/dist/nist-controls.js +174 -0
  48. package/dist/nist-controls.js.map +1 -0
  49. package/dist/nws-weather.d.ts +57 -0
  50. package/dist/nws-weather.d.ts.map +1 -0
  51. package/dist/nws-weather.js +131 -0
  52. package/dist/nws-weather.js.map +1 -0
  53. package/dist/openfda-drugsfda.d.ts +72 -0
  54. package/dist/openfda-drugsfda.d.ts.map +1 -0
  55. package/dist/openfda-drugsfda.js +230 -0
  56. package/dist/openfda-drugsfda.js.map +1 -0
  57. package/dist/openfda.d.ts.map +1 -1
  58. package/dist/openfda.js +31 -8
  59. package/dist/openfda.js.map +1 -1
  60. package/dist/server.d.ts.map +1 -1
  61. package/dist/server.js +374 -11
  62. package/dist/server.js.map +1 -1
  63. package/dist/treasury.d.ts +2 -0
  64. package/dist/treasury.d.ts.map +1 -1
  65. package/dist/treasury.js +7 -0
  66. package/dist/treasury.js.map +1 -1
  67. package/dist/usaspending.d.ts +32 -1
  68. package/dist/usaspending.d.ts.map +1 -1
  69. package/dist/usaspending.js +143 -16
  70. package/dist/usaspending.js.map +1 -1
  71. package/package.json +111 -111
  72. package/src/attachments.ts +652 -652
  73. package/src/bea.ts +372 -372
  74. package/src/bls.ts +1943 -1943
  75. package/src/cache.ts +73 -73
  76. package/src/cbp-border.ts +177 -0
  77. package/src/census-economic.ts +431 -431
  78. package/src/census.ts +735 -735
  79. package/src/ckan.ts +495 -495
  80. package/src/clinicaltrials.ts +923 -923
  81. package/src/cms-facility.ts +379 -379
  82. package/src/cms-hospital.ts +344 -344
  83. package/src/cms-supplier.ts +527 -527
  84. package/src/cms-utilization.ts +389 -389
  85. package/src/cms.ts +634 -634
  86. package/src/coerce.ts +47 -47
  87. package/src/courtlistener.ts +465 -465
  88. package/src/cpsc.ts +333 -333
  89. package/src/datagov-catalog.ts +312 -296
  90. package/src/datagov.ts +907 -907
  91. package/src/datagovKey.ts +68 -68
  92. package/src/datasource.ts +721 -721
  93. package/src/disclosure.ts +61 -61
  94. package/src/dol.ts +515 -515
  95. package/src/ecfr.ts +248 -231
  96. package/src/echo.ts +496 -496
  97. package/src/edgar.ts +3046 -3014
  98. package/src/epa-envirofacts.ts +358 -342
  99. package/src/errors.ts +324 -303
  100. package/src/fac.ts +529 -529
  101. package/src/far.ts +1009 -1007
  102. package/src/fdic.ts +2052 -2052
  103. package/src/federal-register.ts +725 -706
  104. package/src/feedback.ts +160 -0
  105. package/src/fema.ts +680 -541
  106. package/src/fpds.ts +620 -620
  107. package/src/fred.ts +464 -464
  108. package/src/gao.ts +744 -744
  109. package/src/gov-domains.ts +237 -0
  110. package/src/govinfo.ts +497 -497
  111. package/src/grants.ts +290 -290
  112. package/src/gsa-csv.ts +992 -992
  113. package/src/gsa-perdiem.ts +361 -361
  114. package/src/integrity.ts +928 -928
  115. package/src/keys.ts +268 -268
  116. package/src/lda.ts +385 -385
  117. package/src/meta.ts +292 -292
  118. package/src/nhtsa.ts +352 -352
  119. package/src/nih.ts +375 -375
  120. package/src/nist-controls.ts +219 -0
  121. package/src/nonprofit.ts +460 -460
  122. package/src/nppes.ts +834 -834
  123. package/src/nsf.ts +706 -706
  124. package/src/nvd.ts +1124 -1124
  125. package/src/nws-weather.ts +167 -0
  126. package/src/ofac.ts +1166 -1166
  127. package/src/openfda-device.ts +356 -356
  128. package/src/openfda-drugsfda.ts +313 -0
  129. package/src/openfda.ts +518 -495
  130. package/src/pricing.ts +1075 -1075
  131. package/src/sam-gov/client.ts +774 -774
  132. package/src/sam-gov/index.ts +32 -32
  133. package/src/sam-gov/types.ts +152 -152
  134. package/src/sba.ts +357 -357
  135. package/src/server.ts +6688 -6297
  136. package/src/snapshot.ts +223 -223
  137. package/src/socrata.ts +532 -532
  138. package/src/treasury.ts +582 -575
  139. package/src/usaspending.ts +2852 -2680
  140. package/src/usitc.ts +420 -420
@@ -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
+ }