@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
package/src/server.ts CHANGED
@@ -55,20 +55,36 @@ import * as nsf from "./nsf.js";
55
55
  import * as clinicaltrials from "./clinicaltrials.js";
56
56
  import * as census from "./census.js";
57
57
  import * as censusEconomic from "./census-economic.js";
58
+ import * as epaEnvirofacts from "./epa-envirofacts.js";
59
+ import * as cmsUtilization from "./cms-utilization.js";
60
+ import * as cmsHospital from "./cms-hospital.js";
61
+ import * as cmsFacility from "./cms-facility.js";
62
+ import * as cmsSupplier from "./cms-supplier.js";
58
63
  import * as fred from "./fred.js";
59
64
  import * as bea from "./bea.js";
60
65
  import * as gsaPerdiem from "./gsa-perdiem.js";
61
66
  import * as dol from "./dol.js";
62
67
  import * as lda from "./lda.js";
68
+ import * as courtlistener from "./courtlistener.js";
69
+ import * as nonprofit from "./nonprofit.js";
63
70
  import * as fema from "./fema.js";
71
+ import * as nws from "./nws-weather.js";
72
+ import * as govDomains from "./gov-domains.js";
64
73
  import * as fdic from "./fdic.js";
65
74
  import * as bls from "./bls.js";
66
75
  import * as ofac from "./ofac.js";
67
76
  import * as nvd from "./nvd.js";
77
+ import * as nistControls from "./nist-controls.js";
68
78
  import * as nppes from "./nppes.js";
69
79
  import * as cms from "./cms.js";
70
80
  import * as fac from "./fac.js";
71
81
  import * as usitc from "./usitc.js";
82
+ import * as openfda from "./openfda.js";
83
+ import * as openfdaDevice from "./openfda-device.js";
84
+ import * as openfdaDrugsfda from "./openfda-drugsfda.js";
85
+ import * as nhtsa from "./nhtsa.js";
86
+ import * as cpsc from "./cpsc.js";
87
+ import * as cbpBorder from "./cbp-border.js";
72
88
  import { fetchAttachmentText } from "./attachments.js";
73
89
  import * as keys from "./keys.js";
74
90
  import { toToolError, ToolErrorCarrier, errorFromResponse } from "./errors.js";
@@ -84,7 +100,7 @@ import { realpathSync } from "node:fs";
84
100
  const SERVER_NAME = "mcp-sam-gov";
85
101
  // Kept in lockstep with package.json / manifest.json / server.json.
86
102
  // Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
87
- const SERVER_VERSION = "1.3.0";
103
+ const SERVER_VERSION = "1.5.0";
88
104
 
89
105
  // ─── Tool input schemas (Zod) ────────────────────────────────────
90
106
 
@@ -105,6 +121,7 @@ const SamSearchInput = z.object({
105
121
  "Set-aside codes: SBA, 8A, HZS, SDVOSBC, WOSB, EDWOSB, VSA, VSS",
106
122
  ),
107
123
  limit: z.number().min(1).max(50).optional(),
124
+ offset: z.number().min(0).optional().describe("Page offset into the result set (default 0)."),
108
125
  });
109
126
 
110
127
  // Pre-solicitation shaping radar (doc 06 §3.1). Surfaces Sources Sought /
@@ -218,7 +235,11 @@ const UsasIndividualAwardsInput = UsasFiltersBase.extend({
218
235
  });
219
236
 
220
237
  const UsasSubAgencyInput = z.object({
221
- agency: z.string(),
238
+ agency: z
239
+ .string()
240
+ .describe(
241
+ "Canonical agency NAME (e.g. 'Department of Veterans Affairs'), NOT a toptier code — this filter matches by name; a numeric code silently matches nothing. Resolve via usas_lookup_agency / usas_list_toptier_agencies.",
242
+ ),
222
243
  fiscalYear: z.number().int().min(2007).optional(),
223
244
  });
224
245
 
@@ -235,7 +256,14 @@ const UsasRecipientAwardsInput = z.object({
235
256
  });
236
257
 
237
258
  const UsasSubawardsInput = z.object({
238
- primeRecipientName: z.string().optional(),
259
+ // DRIFT/SEMANTICS FIX (dogfooding 2026-07-16): this filters the SUBAWARDEE name,
260
+ // NOT the prime. On spending_by_award{subawards:true} the only keyless recipient
261
+ // filter is `recipient_search_text`, which USAspending matches against the
262
+ // SUB-recipient (live-verified: recipient_search_text:["Leidos"] returns rows
263
+ // whose Sub-Awardee Name IS Leidos, under OTHER primes). The old name
264
+ // `primeRecipientName` promised the opposite. Renamed to `subRecipientName`; the
265
+ // #182 unknown-key guard makes the old name fail loud with the valid-key list.
266
+ subRecipientName: z.string().optional(),
239
267
  agency: z.string().optional(),
240
268
  naics: z.string().optional(),
241
269
  fiscalYear: z.number().int().min(2007).optional(),
@@ -358,14 +386,24 @@ const UsasSpendingOverTimeInput = z.object({
358
386
  });
359
387
 
360
388
  const UsasCategorySpendingInput = z.object({
361
- agency: z.string().optional(),
389
+ agency: z
390
+ .string()
391
+ .optional()
392
+ .describe(
393
+ "Canonical agency NAME (e.g. 'Department of Veterans Affairs'), NOT a toptier code — this filter matches by name; a numeric code silently matches nothing. Resolve via usas_lookup_agency.",
394
+ ),
362
395
  naics: z.string().optional(),
363
396
  fiscalYear: z.number().int().min(2007).optional(),
364
397
  limit: z.number().min(1).max(50).optional(),
365
398
  });
366
399
 
367
400
  const UsasCfdaInput = z.object({
368
- agency: z.string().optional(),
401
+ agency: z
402
+ .string()
403
+ .optional()
404
+ .describe(
405
+ "Canonical agency NAME (e.g. 'Department of Veterans Affairs'), NOT a toptier code — this filter matches by name; a numeric code silently matches nothing. Resolve via usas_lookup_agency.",
406
+ ),
369
407
  fiscalYear: z.number().int().min(2007).optional(),
370
408
  limit: z.number().min(1).max(50).optional(),
371
409
  });
@@ -430,6 +468,25 @@ const UsasListAgenciesInput = z.object({
430
468
  limit: z.number().min(1).max(150).optional(),
431
469
  });
432
470
 
471
+ const UsasListDisasterCodesInput = z.object({});
472
+
473
+ const UsasDisasterSpendingInput = z.object({
474
+ defCodes: z
475
+ .array(z.string().min(1))
476
+ .min(1)
477
+ .describe(
478
+ "Disaster Emergency Fund Codes (DEFC) to include — REQUIRED. e.g. ['L','M'] (COVID-19 relief) or ['1'] (IIJA / infrastructure). Discover the full code set via usas_list_disaster_codes.",
479
+ ),
480
+ spendingType: z
481
+ .enum(["obligation", "outlay"])
482
+ .optional()
483
+ .describe("obligation (default) or outlay. Some DEFCs report $0 obligations but real outlays — try both."),
484
+ geoLayer: z
485
+ .enum(["state", "county", "district"])
486
+ .optional()
487
+ .describe("Geographic breakout: state (default), county, or congressional district."),
488
+ });
489
+
433
490
  // Federal Register
434
491
  const FedRegSearchInput = z.object({
435
492
  query: z.string().optional(),
@@ -984,6 +1041,41 @@ const CisaKevLookupInput = z.object({
984
1041
  offset: z.number().min(0).optional().describe("Zero-based page offset (default 0)."),
985
1042
  });
986
1043
 
1044
+ const NistControlsInput = z.object({
1045
+ controlId: z
1046
+ .string()
1047
+ .min(1)
1048
+ .optional()
1049
+ .describe("Exact control identifier, e.g. 'AC-2', 'SC-7', 'AC-2(1)' (case-insensitive; zero-padding is normalized)."),
1050
+ family: z
1051
+ .string()
1052
+ .min(1)
1053
+ .optional()
1054
+ .describe("Control family — the 2-letter code ('AC', 'SC', 'IA') OR a substring of the family name ('Access Control', 'Audit'). Case-insensitive."),
1055
+ keyword: z
1056
+ .string()
1057
+ .min(1)
1058
+ .optional()
1059
+ .describe("Case-insensitive substring searched over the control title + requirement statement."),
1060
+ limit: z.number().int().min(1).max(200).optional().describe("Max controls returned (default 25, max 200)."),
1061
+ offset: z.number().int().min(0).optional().describe("Zero-based page offset (default 0)."),
1062
+ });
1063
+
1064
+ const CbpBorderWaitInput = z.object({
1065
+ border: z
1066
+ .string()
1067
+ .min(1)
1068
+ .optional()
1069
+ .describe("Filter by border — case-insensitive substring, e.g. 'Canadian' or 'Mexican' (the feed labels ports 'Canadian Border' / 'Mexican Border')."),
1070
+ portName: z
1071
+ .string()
1072
+ .min(1)
1073
+ .optional()
1074
+ .describe("Filter by port name — case-insensitive substring, e.g. 'Laredo', 'Detroit'."),
1075
+ limit: z.number().int().min(1).max(200).optional().describe("Max ports returned (default 100, max 200)."),
1076
+ offset: z.number().int().min(0).optional().describe("Zero-based page offset (default 0)."),
1077
+ });
1078
+
987
1079
  // ━━━ NPPES NPI Registry — the healthcare-provider identity/credentialing lane (1) ━━━ ADR-0036
988
1080
  // nppes_lookup_provider: exact NPI detail OR search over CMS/HHS's keyless public
989
1081
  // registry of every US healthcare provider (npiregistry.cms.hhs.gov/api, version=2.1
@@ -1315,9 +1407,11 @@ const TreasuryDatasetEnum = z
1315
1407
  "mts_table_1",
1316
1408
  "rates_of_exchange",
1317
1409
  "debt_outstanding",
1410
+ "interest_expense",
1411
+ "tror",
1318
1412
  ])
1319
1413
  .describe(
1320
- "Which confirmed Treasury Fiscal Data dataset to query: debt_to_penny (daily total public debt), avg_interest_rates (avg rate by security type), mts_table_1 (Monthly Treasury Statement receipts/outlays/deficit), rates_of_exchange (quarterly FX by currency), debt_outstanding (historical fiscal-year-end debt).",
1414
+ "Which confirmed Treasury Fiscal Data dataset to query: debt_to_penny (daily total public debt), avg_interest_rates (avg rate by security type), mts_table_1 (Monthly Treasury Statement receipts/outlays/deficit), rates_of_exchange (quarterly FX by currency), debt_outstanding (historical fiscal-year-end debt), interest_expense (ACTUAL interest PAID / debt-service cost by security type — distinct from the rate), tror (Treasury Report on Receivables: federal receivables + delinquent-debt collections BY AGENCY).",
1321
1415
  );
1322
1416
 
1323
1417
  const TreasuryQueryDatasetInput = z.object({
@@ -2459,6 +2553,127 @@ const FemaDisasterDeclarationsInput = z.object({
2459
2553
  .describe("0-based row offset ($skip) for pagination, default 0."),
2460
2554
  });
2461
2555
 
2556
+ const FemaSearchHazardMitigationInput = z.object({
2557
+ state: z
2558
+ .string()
2559
+ .min(1)
2560
+ .optional()
2561
+ .describe("Filter by state (→ state eq '...'). Accepts EITHER a 2-letter code ('AL', like the other FEMA tools) OR the full name ('Alabama'); the module maps a 2-letter code to the full name this dataset requires."),
2562
+ programArea: z
2563
+ .string()
2564
+ .min(1)
2565
+ .optional()
2566
+ .describe("Filter by mitigation program (→ programArea eq '...'): HMGP (Hazard Mitigation Grant Program), FMA (Flood Mitigation Assistance), PDM (Pre-Disaster Mitigation), BRIC (Building Resilient Infrastructure and Communities), LPDM, FMA-SL."),
2567
+ disasterNumber: z
2568
+ .number()
2569
+ .int()
2570
+ .positive()
2571
+ .optional()
2572
+ .describe("Filter by FEMA disaster number (→ disasterNumber eq N)."),
2573
+ status: z
2574
+ .string()
2575
+ .min(1)
2576
+ .optional()
2577
+ .describe("Filter by project status (→ status eq '...'). e.g. 'Closed', 'Open'."),
2578
+ programFy: z
2579
+ .number()
2580
+ .int()
2581
+ .optional()
2582
+ .describe("Filter by program fiscal year (→ programFy eq N). e.g. 2005."),
2583
+ region: z
2584
+ .number()
2585
+ .int()
2586
+ .min(1)
2587
+ .max(10)
2588
+ .optional()
2589
+ .describe("Filter by FEMA region number 1–10 (→ region eq N)."),
2590
+ minProjectAmount: z
2591
+ .number()
2592
+ .optional()
2593
+ .describe("Minimum project amount (→ projectAmount ge N)."),
2594
+ maxProjectAmount: z
2595
+ .number()
2596
+ .optional()
2597
+ .describe("Maximum project amount (→ projectAmount le N)."),
2598
+ limit: z
2599
+ .number()
2600
+ .int()
2601
+ .min(1)
2602
+ .max(1000)
2603
+ .default(100)
2604
+ .describe("Rows per page ($top), 1..1000, default 100."),
2605
+ offset: z
2606
+ .number()
2607
+ .int()
2608
+ .min(0)
2609
+ .default(0)
2610
+ .describe("0-based row offset ($skip) for pagination, default 0."),
2611
+ });
2612
+
2613
+ const NwsActiveAlertsInput = z.object({
2614
+ state: z
2615
+ .string()
2616
+ .regex(/^[A-Za-z]{2}$/)
2617
+ .optional()
2618
+ .describe("2-letter US state/territory code to scope alerts (→ NWS ?area=), e.g. 'CA'. Omit for all active US alerts."),
2619
+ event: z
2620
+ .string()
2621
+ .min(1)
2622
+ .optional()
2623
+ .describe("Filter by event type — case-insensitive substring, e.g. 'Flood', 'Wind', 'Winter Storm'."),
2624
+ severity: z
2625
+ .enum(["Extreme", "Severe", "Moderate", "Minor", "Unknown"])
2626
+ .optional()
2627
+ .describe("Filter by severity (exact): Extreme | Severe | Moderate | Minor | Unknown."),
2628
+ limit: z.number().int().min(1).max(500).optional().describe("Max alerts returned (default 50, max 500)."),
2629
+ offset: z.number().int().min(0).optional().describe("Zero-based page offset (default 0)."),
2630
+ });
2631
+
2632
+ const SearchGovDomainsInput = z.object({
2633
+ scope: z
2634
+ .enum(["all", "federal"])
2635
+ .optional()
2636
+ .describe("'all' (federal + SLED: state/county/city/school-district/special-district/tribal, ~16k rows, DEFAULT) or 'federal' (federal-only, ~1.3k rows)."),
2637
+ organization: z
2638
+ .string()
2639
+ .min(1)
2640
+ .optional()
2641
+ .describe("Organization name — case-insensitive SUBSTRING match (e.g. 'veterans', 'cybersecurity')."),
2642
+ domain: z
2643
+ .string()
2644
+ .min(1)
2645
+ .optional()
2646
+ .describe("Domain name — case-insensitive SUBSTRING match (e.g. 'cdc.gov', 'irs')."),
2647
+ domainType: z
2648
+ .string()
2649
+ .min(1)
2650
+ .optional()
2651
+ .describe("Domain type — case-insensitive match (e.g. 'Federal - Executive', 'County', 'Tribal', 'State or territory', 'School district')."),
2652
+ state: z
2653
+ .string()
2654
+ .min(1)
2655
+ .optional()
2656
+ .describe("2-letter state/territory code — case-insensitive exact match (e.g. 'CA')."),
2657
+ city: z
2658
+ .string()
2659
+ .min(1)
2660
+ .optional()
2661
+ .describe("City — case-insensitive SUBSTRING match."),
2662
+ limit: z
2663
+ .number()
2664
+ .int()
2665
+ .min(1)
2666
+ .max(500)
2667
+ .optional()
2668
+ .describe("Rows per page, 1..500, default 50."),
2669
+ offset: z
2670
+ .number()
2671
+ .int()
2672
+ .min(0)
2673
+ .optional()
2674
+ .describe("0-based row offset for pagination, default 0."),
2675
+ });
2676
+
2462
2677
  // ─── EPA ECHO REST (keyless facility compliance/enforcement) — input schemas ──
2463
2678
  // ADR-0009. KEYLESS, single fixed host (echodata.epa.gov) + three fixed service
2464
2679
  // paths (the SSRF core — no free host/path). `state` is a curated US state/
@@ -2719,7 +2934,7 @@ const DatagovSearchDatasetsInput = z.object({
2719
2934
  .min(1)
2720
2935
  .max(500)
2721
2936
  .optional()
2722
- .describe("Free-text search over the dataset catalog (→ _q), e.g. 'wildfire'. LIVE-CONFIRMED to narrow."),
2937
+ .describe("Free-text search over the dataset catalog (→ q), e.g. 'wildfire'. LIVE-CONFIRMED to narrow (2026-07-16: the v4 API param is `q`; the old `_q` is silently ignored)."),
2723
2938
  organization: z
2724
2939
  .string()
2725
2940
  .min(1)
@@ -3427,24 +3642,200 @@ const CensusBusinessPatternsInput = z.object({
3427
3642
  ),
3428
3643
  });
3429
3644
 
3430
- // ─── FRED (Federal Reserve Economic Data) — the SECOND key-required source ──
3431
- // ADR-0048. Macro context (GDP/CPI/rates/unemployment/PPI). REQUIRES a free
3432
- // FRED_API_KEY; without it both tools throw an honest config error (the other 112
3433
- // tools stay keyless). The key rides &api_key= ONLY. Missing observations ('.') → null.
3434
- const FredSearchSeriesInput = z.object({
3435
- query: z
3645
+ // ─── EPA Envirofacts TRI facilities (ADR-0059) — keyless, PATH-segment SSRF ──
3646
+ // data.epa.gov /efservice/tri_facility. Two requests: a count sub-query for the
3647
+ // EXACT total (P1) + the data slice. All user values ride as PATH SEGMENTS, so each
3648
+ // is charclass-validated + encodeURIComponent-encoded (the load-bearing SSRF guard).
3649
+ const EpaTriFacilitiesInput = z
3650
+ .object({
3651
+ state: z
3652
+ .string()
3653
+ .regex(/^[A-Za-z]{2}$/)
3654
+ .optional()
3655
+ .describe(
3656
+ "A 2-letter US state/territory code, e.g. 'VA', 'CA', 'PR' (→ state_abbr; case-insensitive). Provide at least this OR `facilityName`. Validated ^[A-Za-z]{2}$ (it rides in the request path).",
3657
+ ),
3658
+ facilityName: z
3659
+ .string()
3660
+ .min(1)
3661
+ .max(100)
3662
+ .regex(/^[A-Za-z0-9 &.\-]+$/)
3663
+ .optional()
3664
+ .describe(
3665
+ "A partial facility-name match (→ facility_name/CONTAINING/…; case-insensitive), e.g. 'chemical', 'boeing'. Provide at least this OR `state`. Allowed: letters/digits/space/& - . (≤100 chars); '/' and '..' rejected (path-injection guard).",
3666
+ ),
3667
+ county: z
3668
+ .string()
3669
+ .min(1)
3670
+ .max(100)
3671
+ .regex(/^[A-Za-z0-9 &.\-]+$/)
3672
+ .optional()
3673
+ .describe(
3674
+ "A partial county-name match (→ county_name/CONTAINING/…), e.g. 'FAIRFAX'. Optional additional filter; same charclass as facilityName.",
3675
+ ),
3676
+ limit: z
3677
+ .number()
3678
+ .int()
3679
+ .min(1)
3680
+ .max(100)
3681
+ .optional()
3682
+ .describe("Max facilities to return (1–100, default 25). Offset-paginated."),
3683
+ offset: z
3684
+ .number()
3685
+ .int()
3686
+ .min(0)
3687
+ .optional()
3688
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3689
+ })
3690
+ .refine((v) => v.state !== undefined || v.facilityName !== undefined, {
3691
+ message: "Provide at least `state` or `facilityName` (an all-empty query would scan the whole national TRI table and is refused).",
3692
+ path: ["state"],
3693
+ });
3694
+
3695
+ // ─── CMS Medicare provider-service utilization (ADR-0061) — keyless, two-request ──
3696
+ // data.cms.gov /data-api/v1/dataset/{uuid}. Two requests: a stats count sub-query
3697
+ // for the EXACT total (P1 — found_rows) + the data slice (a bare JSON array). Filter
3698
+ // VALUES ride via URLSearchParams (bracket key + value encoded). REQUIRE npi OR
3699
+ // state (the 9.78M-row table is never scanned unscoped).
3700
+ const CmsMedicareProviderServicesInput = z
3701
+ .object({
3702
+ npi: z
3703
+ .string()
3704
+ .regex(/^\d{10}$/)
3705
+ .optional()
3706
+ .describe(
3707
+ "A 10-digit National Provider Identifier (→ Rndrng_NPI), e.g. '1003000126'. Provide at least this OR `state`. Validated ^\\d{10}$.",
3708
+ ),
3709
+ state: z
3710
+ .string()
3711
+ .regex(/^[A-Za-z]{2}$/)
3712
+ .optional()
3713
+ .describe(
3714
+ "A 2-letter US state/territory code (→ Rndrng_Prvdr_State_Abrvtn), e.g. 'VA', 'CA'. Provide at least this OR `npi`. Validated ^[A-Za-z]{2}$.",
3715
+ ),
3716
+ providerType: z
3717
+ .string()
3718
+ .min(1)
3719
+ .max(100)
3720
+ .regex(/^[A-Za-z0-9 &.,()/'-]+$/)
3721
+ .optional()
3722
+ .describe(
3723
+ "An optional specialty filter matching the CMS provider type EXACTLY (→ Rndrng_Prvdr_Type), e.g. 'Family Practice', 'Physical Therapist in Private Practice'. Allowed: letters/digits/space/& . , ( ) / ' - (≤100 chars).",
3724
+ ),
3725
+ hcpcsCode: z
3726
+ .string()
3727
+ .regex(/^[A-Za-z0-9]{1,10}$/)
3728
+ .optional()
3729
+ .describe(
3730
+ "An optional HCPCS/CPT service code filter (→ HCPCS_Cd), e.g. '97110', 'G0463'. Validated ^[A-Za-z0-9]{1,10}$.",
3731
+ ),
3732
+ size: z
3733
+ .number()
3734
+ .int()
3735
+ .min(1)
3736
+ .max(100)
3737
+ .optional()
3738
+ .describe("Max provider-service rows to return (1–100, default 25). Offset-paginated."),
3739
+ offset: z
3740
+ .number()
3741
+ .int()
3742
+ .min(0)
3743
+ .optional()
3744
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3745
+ })
3746
+ .refine((v) => v.npi !== undefined || v.state !== undefined, {
3747
+ message: "Provide at least `npi` or `state` (an all-empty query would scan the entire 9.78M-row Medicare utilization table and is refused; providerType/hcpcsCode alone are not enough to scope).",
3748
+ path: ["npi"],
3749
+ });
3750
+
3751
+ // ─── CMS Hospital Compare "Hospital General Information" (ADR-0062) — keyless ──
3752
+ // data.cms.gov /provider-data/api/1/datastore/query/{datasetId}/0. A SINGLE request:
3753
+ // the response's top-level `count` is the EXACT per-filter total (P1). Filters ride
3754
+ // as DKAN conditions[] triples via URLSearchParams (bracket key + value encoded).
3755
+ // REQUIRE state OR facilityName (the ~5,432-hospital table is never scanned unscoped).
3756
+ const CmsHospitalCompareInput = z
3757
+ .object({
3758
+ state: z
3759
+ .string()
3760
+ .regex(/^[A-Za-z]{2}$/)
3761
+ .optional()
3762
+ .describe(
3763
+ "A 2-letter US state/territory code (→ state, EXACT match), e.g. 'VA', 'CA'. Provide at least this OR `facilityName`. Validated ^[A-Za-z]{2}$.",
3764
+ ),
3765
+ facilityName: z
3766
+ .string()
3767
+ .min(1)
3768
+ .max(100)
3769
+ .regex(/^[A-Za-z0-9 &.,()/'-]+$/)
3770
+ .optional()
3771
+ .describe(
3772
+ "A hospital-name fragment (→ facility_name, case-insensitive SUBSTRING/contains match), e.g. 'children'. Provide at least this OR `state`. Allowed: letters/digits/space/& . , ( ) / ' - (≤100 chars).",
3773
+ ),
3774
+ hospitalType: z
3775
+ .string()
3776
+ .min(1)
3777
+ .max(100)
3778
+ .regex(/^[A-Za-z0-9 &.,()/'-]+$/)
3779
+ .optional()
3780
+ .describe(
3781
+ "An optional hospital-type filter (→ hospital_type, case-insensitive SUBSTRING/contains match), e.g. 'Acute', 'Critical Access'. Allowed: letters/digits/space/& . , ( ) / ' - (≤100 chars).",
3782
+ ),
3783
+ size: z
3784
+ .number()
3785
+ .int()
3786
+ .min(1)
3787
+ .max(100)
3788
+ .optional()
3789
+ .describe("Max hospital rows to return (1–100, default 25). Offset-paginated."),
3790
+ offset: z
3791
+ .number()
3792
+ .int()
3793
+ .min(0)
3794
+ .optional()
3795
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3796
+ })
3797
+ .refine((v) => v.state !== undefined || v.facilityName !== undefined, {
3798
+ message: "Provide at least `state` or `facilityName` (an all-empty query would scan the entire ~5,432-hospital table and is refused; hospitalType alone is not enough to scope).",
3799
+ path: ["state"],
3800
+ });
3801
+
3802
+ // ─── CMS Facility Directory (data.cms.gov provider-data, ADR-0063) — KEYLESS ──
3803
+ // A four-dataset facility directory generalizing cms_hospital_compare beyond
3804
+ // hospitals. `facilityType` is a Zod ENUM that indexes a MODULE-CONSTANT map to a
3805
+ // VETTED dataset id (nursing_home → 4pq5-n9py, home_health → 6jpm-sxkc, hospice →
3806
+ // yc9t-dgbk, dialysis → 23ew-n7w9) — the user value never enters the URL path. A
3807
+ // SINGLE request: the response's top-level `count` is the EXACT per-filter total
3808
+ // (P1). Filters ride as DKAN conditions[] triples via URLSearchParams. name/address/
3809
+ // ownership columns vary per dataset → coalesced (null if none).
3810
+ const CmsFacilityDirectoryInput = z.object({
3811
+ facilityType: z
3812
+ .enum(["nursing_home", "home_health", "hospice", "dialysis"])
3813
+ .describe(
3814
+ "REQUIRED — which CMS provider-data dataset to search: 'nursing_home' (~14,695), 'home_health' (~12,460), 'hospice' (~6,852), or 'dialysis' (~7,490). Selects the dataset id via a constant map (the value never enters the URL path).",
3815
+ ),
3816
+ state: z
3817
+ .string()
3818
+ .regex(/^[A-Za-z]{2}$/)
3819
+ .optional()
3820
+ .describe(
3821
+ "An optional 2-letter US state/territory code (→ state, EXACT match), e.g. 'VA', 'TX'. Validated ^[A-Za-z]{2}$.",
3822
+ ),
3823
+ facilityName: z
3436
3824
  .string()
3437
3825
  .min(1)
3826
+ .max(100)
3827
+ .regex(/^[A-Za-z0-9 &.,()/'-]+$/)
3828
+ .optional()
3438
3829
  .describe(
3439
- "The FRED search_text free-text terms to discover economic series, e.g. 'unemployment rate', 'CPI', 'GDP', '10-year treasury'. Required.",
3830
+ "An optional facility-name fragment (case-insensitive SUBSTRING/contains match against the dataset's primary-name column). Allowed: letters/digits/space/& . , ( ) / ' - (≤100 chars).",
3440
3831
  ),
3441
- limit: z
3832
+ size: z
3442
3833
  .number()
3443
3834
  .int()
3444
3835
  .min(1)
3445
- .max(1000)
3836
+ .max(100)
3446
3837
  .optional()
3447
- .describe("Max series to return (default 25, max 1000). Offset-paginated."),
3838
+ .describe("Max facility rows to return (1–100, default 25). Offset-paginated."),
3448
3839
  offset: z
3449
3840
  .number()
3450
3841
  .int()
@@ -3453,7 +3844,115 @@ const FredSearchSeriesInput = z.object({
3453
3844
  .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3454
3845
  });
3455
3846
 
3456
- const FredSeriesObservationsInput = z.object({
3847
+ // ─── CMS DMEPOS by Supplier (data.cms.gov data-API, ADR-0064) — KEYLESS ──
3848
+ // SAME host/endpoint/two-request-stats-count pattern as cms_medicare_provider_services.
3849
+ // REQUIRE npi OR state (the supplier table is never scanned unscoped). Filter VALUES
3850
+ // ride as URLSearchParams filter[Col]=Val (bracket key + value encoded — the SSRF guard).
3851
+ const CmsDmeposSuppliersInput = z
3852
+ .object({
3853
+ npi: z
3854
+ .string()
3855
+ .regex(/^\d{10}$/)
3856
+ .optional()
3857
+ .describe(
3858
+ "A 10-digit supplier National Provider Identifier (→ Suplr_NPI), e.g. '1003000126'. Provide at least this OR `state`. Validated ^\\d{10}$.",
3859
+ ),
3860
+ state: z
3861
+ .string()
3862
+ .regex(/^[A-Za-z]{2}$/)
3863
+ .optional()
3864
+ .describe(
3865
+ "A 2-letter US state/territory code (→ Suplr_Prvdr_State_Abrvtn), e.g. 'VA', 'CA'. Provide at least this OR `npi`. Validated ^[A-Za-z]{2}$.",
3866
+ ),
3867
+ size: z
3868
+ .number()
3869
+ .int()
3870
+ .min(1)
3871
+ .max(100)
3872
+ .optional()
3873
+ .describe("Max supplier rows to return (1–100, default 25). Offset-paginated."),
3874
+ offset: z
3875
+ .number()
3876
+ .int()
3877
+ .min(0)
3878
+ .optional()
3879
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3880
+ })
3881
+ .refine((v) => v.npi !== undefined || v.state !== undefined, {
3882
+ message: "Provide at least `npi` or `state` (an all-empty query would scan the entire DMEPOS supplier table and is refused).",
3883
+ path: ["npi"],
3884
+ });
3885
+
3886
+ // ─── CMS Revoked Medicare Providers & Suppliers (data.cms.gov data-API, ADR-0064) ──
3887
+ // KEYLESS. A legally-published revocation/exclusion register (~7,059 rows) — the same
3888
+ // vetting class as the OFAC / SAM exclusion lists. ALL filters optional (small table —
3889
+ // pagination is fine unfiltered). SAME two-request stats-count P1 pattern.
3890
+ const CmsRevokedProvidersInput = z.object({
3891
+ npi: z
3892
+ .string()
3893
+ .regex(/^\d{10}$/)
3894
+ .optional()
3895
+ .describe(
3896
+ "An optional 10-digit National Provider Identifier (→ NPI), e.g. '1003000126'. Validated ^\\d{10}$.",
3897
+ ),
3898
+ state: z
3899
+ .string()
3900
+ .regex(/^[A-Za-z]{2}$/)
3901
+ .optional()
3902
+ .describe(
3903
+ "An optional 2-letter US state/territory code (→ STATE_CD, EXACT match), e.g. 'FL', 'CA'. Validated ^[A-Za-z]{2}$.",
3904
+ ),
3905
+ lastName: z
3906
+ .string()
3907
+ .min(1)
3908
+ .max(100)
3909
+ .regex(/^[A-Za-z0-9 .,'-]+$/)
3910
+ .optional()
3911
+ .describe(
3912
+ "An optional last-name filter (→ LAST_NAME, EXACT match). Allowed: letters/digits/space/. , ' - (≤100 chars).",
3913
+ ),
3914
+ size: z
3915
+ .number()
3916
+ .int()
3917
+ .min(1)
3918
+ .max(100)
3919
+ .optional()
3920
+ .describe("Max revocation rows to return (1–100, default 25). Offset-paginated."),
3921
+ offset: z
3922
+ .number()
3923
+ .int()
3924
+ .min(0)
3925
+ .optional()
3926
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3927
+ });
3928
+
3929
+ // ─── FRED (Federal Reserve Economic Data) — the SECOND key-required source ──
3930
+ // ADR-0048. Macro context (GDP/CPI/rates/unemployment/PPI). REQUIRES a free
3931
+ // FRED_API_KEY; without it both tools throw an honest config error (the other 112
3932
+ // tools stay keyless). The key rides &api_key= ONLY. Missing observations ('.') → null.
3933
+ const FredSearchSeriesInput = z.object({
3934
+ query: z
3935
+ .string()
3936
+ .min(1)
3937
+ .describe(
3938
+ "The FRED search_text — free-text terms to discover economic series, e.g. 'unemployment rate', 'CPI', 'GDP', '10-year treasury'. Required.",
3939
+ ),
3940
+ limit: z
3941
+ .number()
3942
+ .int()
3943
+ .min(1)
3944
+ .max(1000)
3945
+ .optional()
3946
+ .describe("Max series to return (default 25, max 1000). Offset-paginated."),
3947
+ offset: z
3948
+ .number()
3949
+ .int()
3950
+ .min(0)
3951
+ .optional()
3952
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3953
+ });
3954
+
3955
+ const FredSeriesObservationsInput = z.object({
3457
3956
  seriesId: z
3458
3957
  .string()
3459
3958
  .regex(/^[A-Za-z0-9._-]+$/)
@@ -3489,6 +3988,236 @@ const FredSeriesObservationsInput = z.object({
3489
3988
  .describe("Observation date order: 'asc' (oldest first, FRED default) or 'desc' (newest first)."),
3490
3989
  });
3491
3990
 
3991
+ // ─── openFDA recall/enforcement (api.fda.gov) — KEYLESS + OPTIONAL rate-limit key ──
3992
+ // ADR-0054. Drug/device/food recall enforcement records. Structured filters ONLY
3993
+ // (no raw Lucene passthrough — injection-safe); the tool assembles the openFDA
3994
+ // `search=` string with proper escaping. totalAvailable = meta.results.total (P1);
3995
+ // a no-match query (openFDA HTTP 404 NOT_FOUND) ⇒ an honest empty (P2). An OPTIONAL
3996
+ // OPENFDA_API_KEY only raises the rate limit (keyless works ~1000/day).
3997
+ const OpenfdaEnforcementInput = z.object({
3998
+ category: z
3999
+ .enum(["drug", "device", "food"])
4000
+ .optional()
4001
+ .describe(
4002
+ "The recall category (default 'drug'): 'drug', 'device', or 'food'. Selects the openFDA /{category}/enforcement endpoint.",
4003
+ ),
4004
+ firm: z
4005
+ .string()
4006
+ .min(1)
4007
+ .optional()
4008
+ .describe(
4009
+ "Recalling firm name filter (→ recalling_firm), e.g. 'pfizer'. Matched as an escaped Lucene phrase.",
4010
+ ),
4011
+ product: z
4012
+ .string()
4013
+ .min(1)
4014
+ .optional()
4015
+ .describe(
4016
+ "Product description filter (→ product_description), e.g. 'insulin'. Matched as an escaped Lucene phrase.",
4017
+ ),
4018
+ reason: z
4019
+ .string()
4020
+ .min(1)
4021
+ .optional()
4022
+ .describe(
4023
+ "Reason-for-recall filter (→ reason_for_recall), e.g. 'contamination'. Matched as an escaped Lucene phrase.",
4024
+ ),
4025
+ classification: z
4026
+ .enum(["Class I", "Class II", "Class III"])
4027
+ .optional()
4028
+ .describe(
4029
+ "FDA recall classification filter: 'Class I' (most serious), 'Class II', or 'Class III'.",
4030
+ ),
4031
+ status: z
4032
+ .string()
4033
+ .min(1)
4034
+ .optional()
4035
+ .describe(
4036
+ "Recall status filter (→ status), e.g. 'Ongoing', 'Terminated', 'Completed'.",
4037
+ ),
4038
+ state: z
4039
+ .string()
4040
+ .regex(/^[A-Za-z]{2}$/)
4041
+ .optional()
4042
+ .describe(
4043
+ "2-letter US state/territory postal code filter (→ state), e.g. 'CA'. Validated ^[A-Za-z]{2}$.",
4044
+ ),
4045
+ limit: z
4046
+ .number()
4047
+ .int()
4048
+ .min(1)
4049
+ .max(100)
4050
+ .optional()
4051
+ .describe("Max recall records to return (default 25, max 100). Offset-paginated via skip."),
4052
+ skip: z
4053
+ .number()
4054
+ .int()
4055
+ .min(0)
4056
+ .optional()
4057
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
4058
+ });
4059
+
4060
+ // ─── openFDA 510(k) device clearances (api.fda.gov) — KEYLESS + OPTIONAL rate-limit key ──
4061
+ // ADR-0056. FDA premarket-notification (510(k)) device clearances — SAME source/envelope/
4062
+ // crux as openfda_enforcement (structured filters ONLY — the tool assembles + escapes the
4063
+ // search= string, injection-safe). totalAvailable = meta.results.total (P1); a no-match
4064
+ // query (openFDA HTTP 404 NOT_FOUND) ⇒ an honest empty (P2). An OPTIONAL OPENFDA_API_KEY
4065
+ // only raises the rate limit (keyless works ~1000/day).
4066
+ const OpenfdaDeviceClearancesInput = z.object({
4067
+ applicant: z
4068
+ .string()
4069
+ .min(1)
4070
+ .optional()
4071
+ .describe(
4072
+ "Applicant / manufacturer name filter (→ applicant), e.g. 'medtronic'. Matched as an escaped Lucene phrase.",
4073
+ ),
4074
+ deviceName: z
4075
+ .string()
4076
+ .min(1)
4077
+ .optional()
4078
+ .describe(
4079
+ "Device name filter (→ device_name), e.g. 'catheter'. Matched as an escaped Lucene phrase.",
4080
+ ),
4081
+ productCode: z
4082
+ .string()
4083
+ .min(1)
4084
+ .optional()
4085
+ .describe(
4086
+ "FDA product code filter (→ product_code), e.g. 'DXN'. Matched as an escaped Lucene phrase.",
4087
+ ),
4088
+ clearanceType: z
4089
+ .string()
4090
+ .min(1)
4091
+ .optional()
4092
+ .describe(
4093
+ "510(k) clearance type filter (→ clearance_type), e.g. 'Traditional', 'Special', 'Abbreviated'. Matched as an escaped Lucene phrase.",
4094
+ ),
4095
+ kNumber: z
4096
+ .string()
4097
+ .min(1)
4098
+ .optional()
4099
+ .describe(
4100
+ "510(k) clearance number (K-number) filter (→ k_number), e.g. 'K123456'. Matched as an escaped Lucene phrase.",
4101
+ ),
4102
+ state: z
4103
+ .string()
4104
+ .regex(/^[A-Za-z]{2}$/)
4105
+ .optional()
4106
+ .describe(
4107
+ "2-letter US state/territory postal code filter (→ state), e.g. 'CA'. Validated ^[A-Za-z]{2}$.",
4108
+ ),
4109
+ limit: z
4110
+ .number()
4111
+ .int()
4112
+ .min(1)
4113
+ .max(100)
4114
+ .optional()
4115
+ .describe("Max clearance records to return (default 25, max 100). Offset-paginated via skip."),
4116
+ skip: z
4117
+ .number()
4118
+ .int()
4119
+ .min(0)
4120
+ .optional()
4121
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
4122
+ });
4123
+
4124
+ const OpenfdaDrugApprovalsInput = z.object({
4125
+ sponsorName: z
4126
+ .string()
4127
+ .min(1)
4128
+ .optional()
4129
+ .describe("Sponsor / applicant company name (→ sponsor_name), e.g. 'pfizer'. Matched as an escaped Lucene phrase."),
4130
+ brandName: z
4131
+ .string()
4132
+ .min(1)
4133
+ .optional()
4134
+ .describe("Product brand name (→ products.brand_name), e.g. 'lipitor'. Matched as an escaped Lucene phrase."),
4135
+ activeIngredient: z
4136
+ .string()
4137
+ .min(1)
4138
+ .optional()
4139
+ .describe("Active ingredient name (→ products.active_ingredients.name), e.g. 'atorvastatin calcium'. Matched as an escaped Lucene phrase."),
4140
+ applicationNumber: z
4141
+ .string()
4142
+ .min(1)
4143
+ .optional()
4144
+ .describe("FDA application number (→ application_number), e.g. 'NDA050347'. Matched as an escaped Lucene phrase."),
4145
+ limit: z
4146
+ .number()
4147
+ .int()
4148
+ .min(1)
4149
+ .max(100)
4150
+ .optional()
4151
+ .describe("Max application records to return (default 25, max 100). Offset-paginated via skip."),
4152
+ skip: z
4153
+ .number()
4154
+ .int()
4155
+ .min(0)
4156
+ .optional()
4157
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
4158
+ });
4159
+
4160
+ // ─── NHTSA vehicle safety (api.nhtsa.gov) — KEYLESS vehicle/parts supplier vetting ──
4161
+ // ADR-0057. Two tools (recalls + complaints) share make/model/modelYear inputs. NO
4162
+ // API key at all. ★The complaints VIN (PII) is excluded from the output. modelYear is
4163
+ // ^\d{4}$; make/model are letters/digits/space/hyphen only (SSRF/injection guard).
4164
+ const NhtsaVehicleInput = z.object({
4165
+ make: z
4166
+ .string()
4167
+ .regex(/^[A-Za-z0-9 -]+$/)
4168
+ .describe(
4169
+ "Vehicle make (required), e.g. 'honda', 'ford'. Letters/digits/space/hyphen only (^[A-Za-z0-9 -]+$).",
4170
+ ),
4171
+ model: z
4172
+ .string()
4173
+ .regex(/^[A-Za-z0-9 -]+$/)
4174
+ .describe(
4175
+ "Vehicle model (required), e.g. 'accord', 'f-150'. Letters/digits/space/hyphen only (^[A-Za-z0-9 -]+$).",
4176
+ ),
4177
+ modelYear: z
4178
+ .string()
4179
+ .regex(/^\d{4}$/)
4180
+ .describe("4-digit model year (required), e.g. '2020'. Validated ^\\d{4}$."),
4181
+ });
4182
+
4183
+ // ─── CPSC consumer-product recalls (www.saferproducts.gov) — KEYLESS goods/import vetting ──
4184
+ // ADR-0058. One tool. NO API key at all. The response is a bare JSON ARRAY with no
4185
+ // total-count field / no pagination (totalAvailable = the returned count). All filters
4186
+ // optional; with NO filter the tool defaults RecallDateStart to ~90 days ago (disclosed)
4187
+ // rather than fetch the whole dataset. dates are ^\d{4}-\d{2}-\d{2}$; recallNumber is
4188
+ // letters/digits/hyphen only (SSRF/injection guard).
4189
+ const CpscRecallsInput = z.object({
4190
+ dateStart: z
4191
+ .string()
4192
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
4193
+ .optional()
4194
+ .describe(
4195
+ "Recall date range START (optional), YYYY-MM-DD, e.g. '2025-01-01' (→ RecallDateStart). Validated ^\\d{4}-\\d{2}-\\d{2}$.",
4196
+ ),
4197
+ dateEnd: z
4198
+ .string()
4199
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
4200
+ .optional()
4201
+ .describe(
4202
+ "Recall date range END (optional), YYYY-MM-DD, e.g. '2025-01-31' (→ RecallDateEnd). Validated ^\\d{4}-\\d{2}-\\d{2}$.",
4203
+ ),
4204
+ productName: z
4205
+ .string()
4206
+ .optional()
4207
+ .describe("Product name substring filter (optional), e.g. 'helmet' (→ ProductName)."),
4208
+ manufacturer: z
4209
+ .string()
4210
+ .optional()
4211
+ .describe("Manufacturer name substring filter (optional) (→ Manufacturer)."),
4212
+ recallNumber: z
4213
+ .string()
4214
+ .regex(/^[A-Za-z0-9-]+$/)
4215
+ .optional()
4216
+ .describe(
4217
+ "A specific CPSC recall number (optional), e.g. '25088' (→ RecallNumber). Letters/digits/hyphen only (^[A-Za-z0-9-]+$).",
4218
+ ),
4219
+ });
4220
+
3492
4221
  // ─── BEA Regional Economic Accounts (apps.bea.gov) — the THIRD key-required source ──
3493
4222
  // ADR-0051. County/state/MSA GDP-by-industry (CAGDP2/SAGDP2N) + personal income
3494
4223
  // (CAINC1/SAINC1) — the regional/sub-national place-of-performance lane. REQUIRES a
@@ -3706,6 +4435,95 @@ const LdaSearchFilingsInput = z.object({
3706
4435
  .describe("Filings per page, 1..25 (the LDA API caps at 25), default 25."),
3707
4436
  });
3708
4437
 
4438
+ // ─── US federal court opinions (www.courtlistener.com) — the litigation lane ──
4439
+ // ADR-0055. Federal court decisions (opinions) — the judicial signal no contract/
4440
+ // spending/lobbying source carries (e.g. uscfc bid-protest / contract-claim opinions).
4441
+ // ★PROVENANCE: CourtListener (Free Law Project, a non-profit), NOT a .gov API —
4442
+ // PACER (the .gov source) is paywalled. KEYLESS (anonymous 200); an optional free
4443
+ // COURTLISTENER_API_TOKEN only raises the rate limit, riding the Authorization: Token
4444
+ // … header ONLY (the lda/socrata app-token lineage). `count` is the REAL total —
4445
+ // never results.length; CURSOR pagination (nextCursor extracted from `next`). court/
4446
+ // dates charclass-guarded; all filter VALUES ride URLSearchParams; type=o is FIXED.
4447
+ const CourtlistenerSearchOpinionsInput = z.object({
4448
+ query: z
4449
+ .string()
4450
+ .min(1)
4451
+ .optional()
4452
+ .describe("Full-text query (maps to q), e.g. 'bid protest' or a party name. Matches across the opinion text/metadata."),
4453
+ court: z
4454
+ .string()
4455
+ .regex(/^[a-z0-9]+$/)
4456
+ .optional()
4457
+ .describe("A CourtListener court id (lowercase alphanumerics ^[a-z0-9]+$), e.g. 'uscfc' (US Court of Federal Claims — contract claims/bid protests), 'cafc' (Federal Circuit — contract/patent appeals), 'scotus'."),
4458
+ dateFiledAfter: z
4459
+ .string()
4460
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
4461
+ .optional()
4462
+ .describe("Only opinions filed on/after this ISO date (→ filed_after), e.g. '2020-01-01'. Validated ^\\d{4}-\\d{2}-\\d{2}$."),
4463
+ dateFiledBefore: z
4464
+ .string()
4465
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
4466
+ .optional()
4467
+ .describe("Only opinions filed on/before this ISO date (→ filed_before), e.g. '2024-12-31'. Validated ^\\d{4}-\\d{2}-\\d{2}$."),
4468
+ natureOfSuit: z
4469
+ .string()
4470
+ .min(1)
4471
+ .optional()
4472
+ .describe("Nature-of-suit text — folded into the q full-text query (the v4 opinions search has no verified dedicated filter), so it matches the text anywhere in the document (disclosed in _meta.notes)."),
4473
+ cursor: z
4474
+ .string()
4475
+ .min(1)
4476
+ .optional()
4477
+ .describe("Opaque continuation token for the NEXT page — pass back the _meta.nextCursor from the previous response (CourtListener uses CURSOR pagination, not page/offset)."),
4478
+ order: z
4479
+ .string()
4480
+ .min(1)
4481
+ .default("dateFiled desc")
4482
+ .describe("Sort order (maps to order_by), default 'dateFiled desc' (most recent first). E.g. 'dateFiled asc', 'score desc'."),
4483
+ });
4484
+
4485
+ // ─── US tax-exempt nonprofits (projects.propublica.org) — the nonprofit lane ──
4486
+ // ADR-0060. IRS Form 990 public records republished KEYLESS by ProPublica Nonprofit
4487
+ // Explorer (a non-profit newsroom) — NOT a .gov API (the IRS has no clean query
4488
+ // API). ★PROVENANCE disclosed in _meta.source + a note. KEYLESS (no key of any
4489
+ // kind). search: q/state[id]/ntee[id]/page (0-based); total_results is the REAL
4490
+ // total — never organizations.length. All VALUES ride URLSearchParams (incl. the
4491
+ // bracket keys); state/ntee charclass/range-guarded.
4492
+ const NonprofitSearchInput = z.object({
4493
+ query: z
4494
+ .string()
4495
+ .min(1)
4496
+ .optional()
4497
+ .describe("Full-text query (maps to q) — an organization name or keyword, e.g. 'american red cross'. Matches across the org name/metadata."),
4498
+ state: z
4499
+ .string()
4500
+ .regex(/^[A-Za-z]{2}$/)
4501
+ .optional()
4502
+ .describe("Filter by a 2-letter US state/territory code (maps to state[id]), e.g. 'VA'. Validated ^[A-Za-z]{2}$."),
4503
+ ntee: z
4504
+ .number()
4505
+ .int()
4506
+ .min(1)
4507
+ .max(10)
4508
+ .optional()
4509
+ .describe("Filter by NTEE major category, an integer 1..10 (maps to ntee[id]) — the National Taxonomy of Exempt Entities top-level group (e.g. 1 Arts, 3 Environment, 8 Health)."),
4510
+ page: z
4511
+ .number()
4512
+ .int()
4513
+ .min(0)
4514
+ .default(0)
4515
+ .describe("0-BASED page number (default 0). Page with cur_page+1 from _meta.notes / when _meta.pagination.hasMore."),
4516
+ });
4517
+
4518
+ // nonprofit_financials — one org's Form 990 profile + financials by EIN. KEYLESS.
4519
+ // ein rides the URL PATH ⇒ digits-only ^\d{1,9}$. An unknown EIN (404) ⇒ not_found.
4520
+ const NonprofitFinancialsInput = z.object({
4521
+ ein: z
4522
+ .string()
4523
+ .regex(/^\d{1,9}$/)
4524
+ .describe("The organization's EIN (Employer Identification Number), 1..9 digits, e.g. '530196605' (American National Red Cross). Validated ^\\d{1,9}$; rides the URL path."),
4525
+ });
4526
+
3709
4527
  // api_key_status takes no input — it is a pure status query over process.env.
3710
4528
  const ApiKeyStatusInput = z.object({});
3711
4529
 
@@ -4340,7 +5158,7 @@ export const TOOLS: ToolDef[] = [
4340
5158
  defineTool({
4341
5159
  name: "usas_search_subawards",
4342
5160
  description:
4343
- "Enumerate subcontracts on prime awards. Use for 'who teams with Leidos at DISA' or 'show small-business subs on Accenture's DHS contracts' — surfaces the prime/sub network for teaming-map artifacts.",
5161
+ "Enumerate federal subawards (subcontracts), optionally filtered by SUBAWARDEE name. Use for 'where does Leidos appear as a SUBcontractor, and under which primes' — surfaces the prime/sub network for teaming-map artifacts. NOTE: subRecipientName matches the SUB-recipient, NOT the prime (the keyless spending_by_award subaward view has no prime-name filter); to see the subs UNDER a specific prime, resolve that prime's awards first (usas_search_awards → usas_get_award_detail) and read their sub network. Each row carries subRecipient (the subawardee), amount, actionDate, the prime award id, and the prime award's NAICS.",
4344
5162
  inputSchema: UsasSubawardsInput,
4345
5163
  handler: (input) => usas.searchSubawards(input),
4346
5164
  }),
@@ -4492,6 +5310,20 @@ export const TOOLS: ToolDef[] = [
4492
5310
  inputSchema: UsasListAgenciesInput,
4493
5311
  handler: (input) => usas.listToptierAgencies(input),
4494
5312
  }),
5313
+ defineTool({
5314
+ name: "usas_list_disaster_codes",
5315
+ description:
5316
+ "List the Disaster Emergency Fund Codes (DEFC) — the supplemental-appropriation tags (COVID-19 relief, IIJA/infrastructure, and other emergency laws) that usas_disaster_spending filters on. Keyless USAspending references/def_codes. Returns the COMPLETE code set (no pagination): each `code` with its `group` ('covid_19' | 'infrastructure' | null), `title`, and `publicLaw`. Use this to discover the codes to pass to usas_disaster_spending. HONESTY: group is null (never fabricated) when a code belongs to no named group; totalAvailable is the exact complete count.",
5317
+ inputSchema: UsasListDisasterCodesInput,
5318
+ handler: () => usas.listDisasterCodes(),
5319
+ }),
5320
+ defineTool({
5321
+ name: "usas_disaster_spending",
5322
+ description:
5323
+ "Disaster / emergency-fund spending BY GEOGRAPHY — obligations or outlays tagged to one or more Disaster Emergency Fund Codes (DEFC: COVID-19, IIJA, etc.), broken out per state / county / congressional district (keyless USAspending disaster/spending_by_geography). Answers 'which geographies captured COVID/IIJA relief money' — a distinct axis the standard award search does not expose. `defCodes` REQUIRED (discover via usas_list_disaster_codes); `spendingType` obligation (default) | outlay; `geoLayer` state (default) | county | district. Each row: name, code, amount, awardCount, population, perCapita. HONESTY: amount/perCapita are number|null (a real 0 stays 0 — some DEFCs like IIJA report $0 OBLIGATIONS with a nonzero awardCount, disclosed in a note; absent → null, never a fabricated 0); the endpoint returns the COMPLETE set of geo units (no pagination) so totalAvailable = returned; an outage/4xx THROWS (never a fake empty).",
5324
+ inputSchema: UsasDisasterSpendingInput,
5325
+ handler: (input) => usas.disasterSpending(input),
5326
+ }),
4495
5327
 
4496
5328
  // ━━━ Federal Register (4) ━━━
4497
5329
  defineTool({
@@ -4659,6 +5491,13 @@ export const TOOLS: ToolDef[] = [
4659
5491
  inputSchema: CisaKevLookupInput,
4660
5492
  handler: (input) => nvd.cisaKevLookup(input),
4661
5493
  }),
5494
+ defineTool({
5495
+ name: "nist_800_53_controls",
5496
+ description:
5497
+ "Look up NIST SP 800-53 Rev 5 security & privacy CONTROLS (keyless) — the requirement backbone for FedRAMP / CMMC / RMF compliance work. Retrieve a control by `controlId` (exact, e.g. 'AC-2', 'SC-7', 'AC-2(1)'), a `family` (2-letter code 'AC'/'SC'/'IA' or a name substring 'Access Control'), and/or a `keyword` (case-insensitive substring over title + statement); `limit`/`offset` pagination. Each row: { id (e.g. 'AC-2'), family (e.g. 'AC — Access Control'), title, statement (the labelled requirement prose), guidance (discussion), enhancements:[{id,title}] (e.g. AC-2(1)) }. Complements cve_lookup + cisa_kev_lookup (the vulnerability side) with the CONTROL/requirement side. HONESTY: source is NIST's OFFICIAL OSCAL catalog published at github.com/usnistgov/oscal-content (authoritative first-party data served from GitHub, not a .gov API host — provenance disclosed in _meta); the catalog has no query API so filtering is CLIENT-SIDE and totalAvailable is the EXACT match count; this is the REQUIREMENT text only — applicability depends on the system's FIPS-199 impact baseline (Low/Moderate/High), which the catalog does not encode (disclosed); a download failure or an implausibly-truncated catalog (< 15 families) THROWS (never a fake-empty 'control not found').",
5498
+ inputSchema: NistControlsInput,
5499
+ handler: (input) => nistControls.searchControls(input),
5500
+ }),
4662
5501
  // ━━━ NPPES NPI Registry — Healthcare-Provider Vetting (1) ━━━ ADR-0036
4663
5502
  defineTool({
4664
5503
  name: "nppes_lookup_provider",
@@ -4709,7 +5548,7 @@ export const TOOLS: ToolDef[] = [
4709
5548
  defineTool({
4710
5549
  name: "treasury_query_dataset",
4711
5550
  description:
4712
- "Escape-hatch query over 5 confirmed US Treasury Fiscal Data datasets (keyless): debt_to_penny, avg_interest_rates, mts_table_1 (Monthly Treasury Statement), rates_of_exchange, debt_outstanding. Choose `dataset` (enum — no free path), and optionally project `fields` (CSV), `filter` (CSV 'col:op:val', ops lt|lte|gt|gte|eq|in, AND-combined), and `sort` (CSV, '-' = desc), with page[size]/page[number] pagination. Returns raw rows plus a truthful `_meta` (totalAvailable = upstream total-count, offset pagination). Value/amount fields are raw upstream strings — the string \"null\"/empty means 'no value', never 0. Covers rates_of_exchange + debt_outstanding without a dedicated tool.",
5551
+ "Escape-hatch query over 7 confirmed US Treasury Fiscal Data datasets (keyless): debt_to_penny, avg_interest_rates, mts_table_1 (Monthly Treasury Statement), rates_of_exchange, debt_outstanding, interest_expense (actual interest PAID / debt-service cost), tror (Treasury Report on Receivables — federal receivables + delinquent-debt collections by agency). Choose `dataset` (enum — no free path), and optionally project `fields` (CSV), `filter` (CSV 'col:op:val', ops lt|lte|gt|gte|eq|in, AND-combined), and `sort` (CSV, '-' = desc), with page[size]/page[number] pagination. Returns raw rows plus a truthful `_meta` (totalAvailable = upstream total-count, offset pagination). Value/amount fields are raw upstream strings — the string \"null\"/empty means 'no value', never 0. Covers rates_of_exchange + debt_outstanding without a dedicated tool.",
4713
5552
  inputSchema: TreasuryQueryDatasetInput,
4714
5553
  handler: (input) => treasury.queryDataset(input),
4715
5554
  }),
@@ -4928,7 +5767,7 @@ export const TOOLS: ToolDef[] = [
4928
5767
  inputSchema: BlsQcewInput,
4929
5768
  handler: (input) => bls.qcew(input),
4930
5769
  }),
4931
- // ━━━ OpenFEMA — keyless disaster declarations + emergency-assistance spend (2) ━━━ ADR-0016
5770
+ // ━━━ OpenFEMA — keyless disaster declarations + emergency-assistance spend (3) ━━━ ADR-0016
4932
5771
  defineTool({
4933
5772
  name: "fema_search_public_assistance",
4934
5773
  description:
@@ -4943,6 +5782,29 @@ export const TOOLS: ToolDef[] = [
4943
5782
  inputSchema: FemaDisasterDeclarationsInput,
4944
5783
  handler: (input) => fema.disasterDeclarations(input),
4945
5784
  }),
5785
+ defineTool({
5786
+ name: "fema_search_hazard_mitigation",
5787
+ description:
5788
+ "Search FEMA Hazard Mitigation Assistance projects — the disaster-RESILIENCE grant axis (HMGP/FMA/PDM/BRIC mitigation grants to state/local/tribal subrecipients, distinct from the disaster-RECOVERY spend in fema_search_public_assistance). Keyless OpenFEMA, dataset HazardMitigationAssistanceProjects v4, ~56k rows. Structured filters (module-built into an OData $filter; each LIVE-VERIFIED to narrow): `state` (→ state — the FULL state NAME, e.g. 'Alabama', NOT the 2-letter code), `programArea` (HMGP/FMA/PDM/BRIC/LPDM/FMA-SL), `disasterNumber`, `status` (e.g. 'Closed'), `programFy`, `region` (FEMA region 1–10), `minProjectAmount`/`maxProjectAmount` (projectAmount ge/le). `limit` (≤1000, def 100 → $top), `offset` (→ $skip). HONESTY: the module ALWAYS sends $inlinecount=allpages so totalAvailable is the EXACT filtered total (metadata.count), never the page length; amount fields (projectAmount/federalShareObligated/initialObligationAmount/netValueBenefits) are number|null (a real 0 stays 0, absent → null); genuine-empty ⇒ complete:true/total:0; an outage/400/404 THROWS (never a fake empty). NOTE: 'state' here is the full name (this dataset 400s on a 2-letter code), whereas fema_search_public_assistance maps 'state' to the 2-letter 'stateAbbreviation'.",
5789
+ inputSchema: FemaSearchHazardMitigationInput,
5790
+ handler: (input) => fema.searchHazardMitigation(input),
5791
+ }),
5792
+ // ━━━ NWS — National Weather Service active alerts (keyless) (1) ━━━
5793
+ defineTool({
5794
+ name: "nws_active_alerts",
5795
+ description:
5796
+ "List CURRENTLY-ACTIVE National Weather Service alerts — watches, warnings, and advisories (keyless; api.weather.gov). The disaster/climate-readiness lane that pairs with the FEMA tools (declarations → public assistance → hazard mitigation → LIVE active weather): where severe-weather events are active NOW, ahead of the declarations/contracts that follow. Filters: `state` (2-letter code → server-side ?area=, e.g. 'CA'; omit for all US), `event` (case-insensitive substring, e.g. 'Flood', 'Wind'), `severity` (Extreme/Severe/Moderate/Minor/Unknown); `limit`/`offset` pagination. Each alert: { id, event, headline, severity, urgency, certainty, category, status, messageType, areaDesc, effective, onset, expires, ends, senderName, description, instruction, response }. HONESTY: this is REAL-TIME data (alerts active at request time — a live snapshot, NOT a historical archive; read effective/expires for each window, disclosed in _meta); every scalar is null-never-empty-string and dates are ISO strings; totalAvailable is the EXACT count of matched active alerts; a NO-active-alerts result is an HONEST EMPTY (returned:0), never an error; an outage/4xx/timeout THROWS and a non-FeatureCollection body ⇒ schema_drift. A descriptive User-Agent is sent per NWS policy (no key/token).",
5797
+ inputSchema: NwsActiveAlertsInput,
5798
+ handler: (input) => nws.activeAlerts(input),
5799
+ }),
5800
+ // ━━━ get.gov — CISA authoritative .gov domain registry (keyless) (1) ━━━
5801
+ defineTool({
5802
+ name: "search_gov_domains",
5803
+ description:
5804
+ "Search the authoritative US .gov domain registry (CISA get.gov) — resolve which ORGANIZATION owns a .gov domain, enumerate federal agencies, and MAP SLED entities (state/county/city/school-district/special-district/tribal) for market targeting. Keyless. scope 'all' (federal + SLED, ~16k rows, default) | 'federal'. Filters (client-side over the published CSV): organization/domain/city (case-insensitive SUBSTRING), domainType (e.g. 'Federal - Executive', 'County', 'Tribal'), state (2-letter). Each row: domain, domainType, organization, suborganization, city, state. HONESTY: source is CISA's OFFICIAL registry published at github.com/cisagov/dotgov-data (authoritative first-party data, not a .gov API host — provenance disclosed in _meta); the registry has no query API so filtering is CLIENT-SIDE and totalAvailable is the EXACT match count; the 'Security contact email' column is intentionally EXCLUDED (org mailbox — this tool resolves organizations, not contacts); an outage/4xx THROWS (never a fake empty); a header-column rename ⇒ schema_drift.",
5805
+ inputSchema: SearchGovDomainsInput,
5806
+ handler: (input) => govDomains.searchGovDomains(input),
5807
+ }),
4946
5808
  // ━━━ FPDS-NG — federal contract AWARD ACTIONS (keyless ATOM) (1) ━━━ ADR-0012
4947
5809
  // The FIRST XML/ATOM source (bounded, ReDoS-safe hand-parser — the far.ts/gao.ts
4948
5810
  // lineage; NOT the getJson port). FPDS is the system-of-record USAspending
@@ -5174,17 +6036,97 @@ export const TOOLS: ToolDef[] = [
5174
6036
  // ━━━ US Census County Business Patterns — market sizing (1) ━━━ ADR-0047
5175
6037
  // ★The server's FIRST KEY-REQUIRED source: the Census Data API removed its
5176
6038
  // keyless tier, so WITHOUT a CENSUS_API_KEY this tool throws an honest
5177
- // invalid_input config error (the other 111 tools stay keyless). NAICS×geography
6039
+ // invalid_input config error (most other tools are keyless — see api_key_status). NAICS×geography
5178
6040
  // establishments / employment / annual payroll — the demand-side market-sizing
5179
6041
  // lane. Census negative suppression sentinels (-999999999 …) map to null (never
5180
6042
  // a negative number / never 0). The 2D-array body is parsed by header name.
5181
6043
  defineTool({
5182
6044
  name: "census_business_patterns",
5183
6045
  description:
5184
- "Market sizing by NAICS × geography — establishments, employment, and annual payroll from the US Census County Business Patterns (CBP) API (api.census.gov/data/{year}/cbp). ★REQUIRES a free CENSUS_API_KEY: the Census Data API has NO keyless tier, so without the key this tool THROWS an honest config error (get one at https://api.census.gov/data/key_signup.html; Census and FRED are the only key-required sources — every other tool is keyless). Input: optional `naics` (2–6 digit NAICS-2017, e.g. '5415'; omit to aggregate all sectors), `geography` (us|state|county, default us; county REQUIRES `state`), `state` (2-digit FIPS, e.g. '06'), `year` (default '2022'), optional `limit` (client-side top-N; CBP has no server pagination). Returns { rows:[{ name, geoId, naicsCode, naicsLabel, establishments, employees, annualPayrollUsd, state }] } + honest _meta. HONESTY: establishments/employees are integer counts and annualPayrollUsd is annual US dollars (×1000 from the source's $1,000-unit PAYANN); Census SUPPRESSED/withheld cells (large negative sentinels like -999999999) map to null — NEVER a negative number and NEVER 0 (a genuine 0 stays 0); geoId/naicsCode/state are STRINGS (leading zeros survive). CBP returns the COMPLETE geography set for the filter (no pagination) ⇒ totalAvailable = the row count, complete:true. A missing/invalid key ⇒ invalid_input (a 302 to the Missing-Key page); a header-only body ⇒ honest empty (returned:0); a 5xx ⇒ THROWS; a 200 non-JSON ⇒ schema_drift. The key rides ONLY in the &key= query param — never logged or echoed.",
6046
+ "Market sizing by NAICS × geography — establishments, employment, and annual payroll from the US Census County Business Patterns (CBP) API (api.census.gov/data/{year}/cbp). ★REQUIRES a free CENSUS_API_KEY: the Census Data API has NO keyless tier, so without the key this tool THROWS an honest config error (get one at https://api.census.gov/data/key_signup.html; call api_key_status to see every source's key requirement). Input: optional `naics` (2–6 digit NAICS-2017, e.g. '5415'; omit to aggregate all sectors), `geography` (us|state|county, default us; county REQUIRES `state`), `state` (2-digit FIPS, e.g. '06'), `year` (default '2022'), optional `limit` (client-side top-N; CBP has no server pagination). Returns { rows:[{ name, geoId, naicsCode, naicsLabel, establishments, employees, annualPayrollUsd, state }] } + honest _meta. HONESTY: establishments/employees are integer counts and annualPayrollUsd is annual US dollars (×1000 from the source's $1,000-unit PAYANN); large-negative suppression sentinels map to null — NEVER a negative number and NEVER 0 (a genuine 0 stays 0; note CBP primarily uses noise-infusion + suppression flags, surfaced as reported — see the tool's suppression note); geoId/naicsCode/state are STRINGS (leading zeros survive). CBP returns the COMPLETE geography set for the filter (no pagination) ⇒ totalAvailable = the row count, complete:true. A missing/invalid key ⇒ invalid_input (a 302 to the Missing-Key page); a header-only body ⇒ honest empty (returned:0); a 5xx ⇒ THROWS; a 200 non-JSON ⇒ schema_drift. The key rides ONLY in the &key= query param — never logged or echoed.",
5185
6047
  inputSchema: CensusBusinessPatternsInput,
5186
6048
  handler: (input) => censusEconomic.businessPatterns(input),
5187
6049
  }),
6050
+ // ━━━ EPA Envirofacts TRI facilities — environmental footprint (1) ━━━ ADR-0059
6051
+ // KEYLESS (data.epa.gov /efservice/tri_facility). ★Two requests: a count
6052
+ // sub-query yields the EXACT total (P1 — TOTALQUERYRESULTS, e.g. VA=1247), then
6053
+ // the data slice. All user values ride as PATH SEGMENTS → each is
6054
+ // charclass-validated + encodeURIComponent-encoded (the load-bearing SSRF guard).
6055
+ defineTool({
6056
+ name: "epa_tri_facilities",
6057
+ description:
6058
+ "Look up EPA Toxics Release Inventory (TRI) reporting facilities by state / facility-name / county — an environmental-footprint / place-of-performance screen (EPA Envirofacts, keyless; data.epa.gov/efservice/tri_facility). Input: `state` (2-letter, e.g. 'VA'), `facilityName` (partial match, e.g. 'chemical'), `county` (partial match) — provide at least `state` OR `facilityName` (an all-empty query is refused); optional `limit` (1–100, default 25), `offset`. Returns { facilities:[{ triFacilityId, facilityName, streetAddress, city, county, state, zip, region, closed }] } + honest _meta. ★HONESTY: totalAvailable is the EXACT count from a SEPARATE count sub-query (…/count/JSON → TOTALQUERYRESULTS), NEVER the returned-rows length; if that count fails, totalAvailable is null + a disclosing note (never length-faked). offset/limit pagination (hasMore = offset+returned < total). `closed` normalizes fac_closed_ind ('0'/'N'→false, '1'/'Y'→true, unrecognized→null — never a fabricated false); addresses/names are null-never-empty-string. A genuine no-match ⇒ honest empty (returned:0); a 4xx ⇒ invalid_input/not_found; a 5xx ⇒ THROWS; a 200 non-array/non-JSON ⇒ schema_drift. These are nominal TRI reporters, NOT a compliance/enforcement determination. KEYLESS — no key is sent.",
6059
+ inputSchema: EpaTriFacilitiesInput,
6060
+ handler: (input) => epaEnvirofacts.triFacilities(input),
6061
+ }),
6062
+ // ━━━ CMS Medicare provider-service utilization — healthcare market (1) ━━━ ADR-0061
6063
+ // KEYLESS (data.cms.gov /data-api/v1/dataset/{uuid}). ★Two requests: a stats
6064
+ // count sub-query yields the EXACT per-filter total (P1 — found_rows, e.g.
6065
+ // VA=278254), then the data slice (a bare JSON array). All filter VALUES ride via
6066
+ // URLSearchParams (bracket key + value encoded — the SSRF guard). REQUIRE npi OR
6067
+ // state (the 9.78M-row table is never scanned unscoped). The dataset UUID is a
6068
+ // SPECIFIC ANNUAL VINTAGE (surfaced in a _meta note; update yearly).
6069
+ defineTool({
6070
+ name: "cms_medicare_provider_services",
6071
+ description:
6072
+ "Look up Medicare Part-B provider utilization — for a given provider (NPI) or state, the HCPCS services rendered, beneficiaries served, and submitted / Medicare-allowed / Medicare-paid amounts (CMS 'Medicare Physician & Other Practitioners — by Provider and Service', keyless; data.cms.gov data-API). The demand-side complement to nppes_lookup_provider (who providers ARE → what they BILL) for healthcare-market / competitor / teaming due-diligence. Input: `npi` (10-digit) OR `state` (2-letter) — at least ONE is REQUIRED (the table is 9.78M rows; an all-empty query is refused; providerType/hcpcsCode alone are NOT enough to scope); optional `providerType` (exact CMS specialty, e.g. 'Family Practice'), `hcpcsCode` (e.g. '97110', 'G0463'), `size` (1–100, default 25), `offset`. Returns { services:[{ npi, providerName, credentials, providerType, city, state, zip, hcpcsCode, hcpcsDescription, totalBeneficiaries, totalServices, avgSubmittedCharge, avgMedicareAllowed, avgMedicarePayment }] } + honest _meta. ★HONESTY: totalAvailable is the EXACT count from a SEPARATE stats sub-query (…/data-viewer/stats → found_rows, e.g. VA=278254), NEVER the returned-rows length; if that count fails, totalAvailable is null + a disclosing note (never length-faked). offset/size pagination (hasMore = offset+returned < total). Aggregate/payment values are numeric-string → number|null (a genuine 0 stays 0, absent → null, never 0-faked); NPI/HCPCS/names are null-never-empty-string. A genuine no-match ⇒ honest empty (returned:0); a 4xx ⇒ invalid_input/not_found; a 5xx ⇒ THROWS; a 200 non-array/non-JSON ⇒ schema_drift. These are public PROVIDER-level AGGREGATE figures (no patient identifiers) for ONE annual vintage (the dataset year is disclosed in _meta) — a utilization snapshot, NOT a fraud/quality/fitness determination. KEYLESS — no key is sent.",
6073
+ inputSchema: CmsMedicareProviderServicesInput,
6074
+ handler: (input) => cmsUtilization.providerServices(input),
6075
+ }),
6076
+ // ━━━ CMS Hospital Compare — Hospital General Information (1) ━━━ ADR-0062
6077
+ // KEYLESS (data.cms.gov /provider-data/api/1/datastore/query/{datasetId}). A
6078
+ // SINGLE request: the response's top-level `count` is the EXACT per-filter total
6079
+ // (P1 — VA=96), never the slice length. Filters ride as DKAN conditions[] triples
6080
+ // via URLSearchParams (bracket key + value encoded — the SSRF guard): state is an
6081
+ // EXACT match, facilityName/hospitalType are case-insensitive substring matches,
6082
+ // AND-combined server-side. REQUIRE state OR facilityName (never scanned unscoped).
6083
+ defineTool({
6084
+ name: "cms_hospital_compare",
6085
+ description:
6086
+ "Look up Medicare-certified hospitals by US state and/or facility-name fragment — location, type, ownership, emergency-services flag, and CMS star rating (CMS Hospital Compare 'Hospital General Information', keyless; data.cms.gov provider-data datastore-query API, ~5,432 hospitals). A healthcare-facility directory / market-map lane (WHERE hospitals are and HOW CMS rates them). Input: `state` (2-letter, EXACT) OR `facilityName` (a name fragment, case-insensitive substring/contains match) — at least ONE is REQUIRED (an all-empty query is refused; hospitalType alone is NOT enough to scope); optional `hospitalType` (substring, e.g. 'Acute', 'Critical Access'), `size` (1–100, default 25), `offset`. Returns { hospitals:[{ facilityId, facilityName, address, city, state, zip, county, phone, hospitalType, ownership, emergencyServices, overallRating }] } + honest _meta. ★HONESTY: totalAvailable is the response's EXACT top-level `count` for the filter set (VA=96), NEVER the returned-rows length; offset/size pagination (hasMore = offset+returned < count). overallRating is CMS's 1–5 star rating as a number; 'Not Available'/blank/non-numeric ⇒ null (NEVER 0). emergencyServices normalizes 'Yes'⇒true / 'No'⇒false / else null (never a fabricated false). IDs/names/addresses are null-never-empty-string. A genuine no-match ⇒ honest empty (returned:0); a 4xx ⇒ invalid_input/not_found; a 5xx ⇒ THROWS; a 200 non-array body or one missing count/results ⇒ schema_drift. Filters are applied SERVER-SIDE (AND-combined) — nothing is silently dropped. This is a summary star rating, NOT a clinical-quality or fitness determination. KEYLESS — no key is sent.",
6087
+ inputSchema: CmsHospitalCompareInput,
6088
+ handler: (input) => cmsHospital.hospitalCompare(input),
6089
+ }),
6090
+ // ━━━ CMS Facility Directory — 4 provider-data datasets (1) ━━━ ADR-0063
6091
+ // KEYLESS (data.cms.gov /provider-data/api/1/datastore/query/{datasetId}). A
6092
+ // generalization of cms_hospital_compare beyond hospitals: facilityType (a Zod
6093
+ // enum) indexes a CONSTANT map to a VETTED dataset id — the user value never enters
6094
+ // the path (the load-bearing SSRF guard). A SINGLE request: the response's top-level
6095
+ // `count` is the EXACT per-filter total (P1). name/address/ownership columns vary
6096
+ // per dataset → coalesced (null if none — never empty-string, never fabricated).
6097
+ defineTool({
6098
+ name: "cms_facility_directory",
6099
+ description:
6100
+ "Look up Medicare/Medicaid-certified healthcare FACILITIES by type — nursing homes, home health agencies, hospices, or dialysis facilities — with their name, address, city, state, zip, and ownership (CMS provider-data, keyless; data.cms.gov datastore-query API, four datasets). A healthcare-facility directory / market-map lane that generalizes cms_hospital_compare beyond hospitals. Input: `facilityType` (REQUIRED enum — 'nursing_home' ~14,695 / 'home_health' ~12,460 / 'hospice' ~6,852 / 'dialysis' ~7,490; selects the dataset id via a constant map, the value never enters the URL path), optional `state` (2-letter, EXACT), `facilityName` (a name fragment, case-insensitive substring/contains match against the dataset's primary-name column), `size` (1–100, default 25), `offset`. Returns { facilities:[{ name, address, city, state, zip, facilityType, ownership }] } + honest _meta. ★HONESTY: totalAvailable is the response's EXACT top-level `count` for the filter set, NEVER the returned-rows length; offset/size pagination (hasMore = offset+returned < count). name/address/ownership column names DIFFER across the four datasets, so each is COALESCED over per-dataset candidates (name: provider_name/facility_name/legal_business_name; address: address/provider_address/address_line_1; ownership: ownership_type/type_of_ownership/profit_or_nonprofit) — a field absent in the chosen dataset is null (unknown), NEVER an empty string and NEVER fabricated. facilityType is echoed on each row. A genuine no-match ⇒ honest empty (returned:0); an invalid facilityType ⇒ invalid_input (blocked by the enum); a 4xx ⇒ invalid_input/not_found; a 5xx ⇒ THROWS; a 200 non-array body or one missing count/results ⇒ schema_drift. Filters are applied SERVER-SIDE (AND-combined) — nothing is silently dropped. This is a facility directory, NOT a clinical-quality or fitness determination. KEYLESS — no key is sent.",
6101
+ inputSchema: CmsFacilityDirectoryInput,
6102
+ handler: (input) => cmsFacility.facilityDirectory(input),
6103
+ }),
6104
+ // ━━━ CMS DMEPOS by Supplier — supply-side utilization (1) ━━━ ADR-0064
6105
+ // KEYLESS (data.cms.gov /data-api/v1/dataset/{uuid}). ★Two requests: a stats count
6106
+ // sub-query yields the EXACT per-filter total (P1 — found_rows), then the data slice
6107
+ // (a bare JSON array). All filter VALUES ride via URLSearchParams (bracket key +
6108
+ // value encoded — the SSRF guard). REQUIRE npi OR state (the supplier table is never
6109
+ // scanned unscoped). The dataset UUID is a SPECIFIC ANNUAL VINTAGE (update yearly).
6110
+ defineTool({
6111
+ name: "cms_dmepos_suppliers",
6112
+ description:
6113
+ "Look up Medicare DMEPOS (Durable Medical Equipment, Devices & Supplies) SUPPLIERS — for a given supplier (NPI) or state, the supplier's identity plus aggregate Medicare figures: HCPCS codes billed, beneficiaries served, claims, services, and submitted / Medicare-allowed / Medicare-paid amounts (CMS 'Medicare DMEPOS — by Supplier', keyless; data.cms.gov data-API). The supply-side complement to cms_medicare_provider_services for healthcare-market / competitor / teaming due-diligence on equipment suppliers. Input: `npi` (10-digit) OR `state` (2-letter) — at least ONE is REQUIRED (an all-empty query is refused; the supplier table is never scanned unscoped); optional `size` (1–100, default 25), `offset`. Returns { suppliers:[{ npi, supplierName, credentials, entityType, city, state, zip, totalHcpcsCodes, totalBeneficiaries, totalClaims, totalServices, submittedCharges, medicareAllowed, medicarePayment }] } + honest _meta. ★HONESTY: totalAvailable is the EXACT count from a SEPARATE stats sub-query (…/data-viewer/stats → found_rows), NEVER the returned-rows length; if that count fails, totalAvailable is null + a disclosing note (never length-faked). offset/size pagination (hasMore = offset+returned < total). Aggregate/payment values are numeric-string → number|null (a genuine 0 stays 0, absent → null, never 0-faked); NPI/entityType/names are null-never-empty-string; supplierName joins Last_Name_Org + First_Name ('Last, First' for individuals, the org name alone for organizations). A genuine no-match ⇒ honest empty (returned:0); a 4xx ⇒ invalid_input/not_found; a 5xx ⇒ THROWS; a 200 non-array/non-JSON ⇒ schema_drift. These are public SUPPLIER-level AGGREGATE figures (no patient identifiers) for ONE annual vintage (disclosed in _meta) — a utilization snapshot, NOT a fraud/quality/fitness determination. KEYLESS — no key is sent.",
6114
+ inputSchema: CmsDmeposSuppliersInput,
6115
+ handler: (input) => cmsSupplier.dmeposSuppliers(input),
6116
+ }),
6117
+ // ━━━ CMS Revoked Medicare Providers & Suppliers — vetting/exclusion list (1) ━━━ ADR-0064
6118
+ // KEYLESS (data.cms.gov /data-api/v1/dataset/{uuid}). A legally-published
6119
+ // revocation/exclusion register (~7,059 rows) — the SAME vetting class as the OFAC /
6120
+ // SAM exclusion lists already shipped (surfacing the names IS the point). ALL filters
6121
+ // optional (small table — pagination is fine unfiltered). SAME two-request stats-count
6122
+ // P1 pattern; filter VALUES ride via URLSearchParams (bracket key + value encoded).
6123
+ defineTool({
6124
+ name: "cms_revoked_providers",
6125
+ description:
6126
+ "Search CMS's PUBLIC 'Revoked Medicare Providers & Suppliers' list — the legally-published register of Medicare enrollment revocations, with the revoked provider/supplier's identity, provider type, revocation reason, effective date, and re-enrollment-bar expiration (CMS 'Revoked Providers and Suppliers', keyless; data.cms.gov data-API, ~7,059 rows). A vetting / due-diligence lane in the SAME class as the OFAC / SAM-exclusions lists — for screening a counterparty before teaming or subcontracting. Input (ALL optional — the ~7K-row list is safe to page unfiltered): `npi` (10-digit → NPI), `state` (2-letter → STATE_CD, exact), `lastName` (→ LAST_NAME, exact), `size` (1–100, default 25), `offset`. Returns { revocations:[{ enrollmentId, npi, name, state, providerType, revocationReason, revocationEffectiveDate, reenrollmentBarExpiration }] } + honest _meta (which notes this is CMS's public revocation/exclusion list — a due-diligence signal, NOT a current-eligibility, guilt, or fitness determination). ★HONESTY: totalAvailable is the EXACT count from a SEPARATE stats sub-query (…/data-viewer/stats → found_rows), NEVER the returned-rows length; if that count fails, totalAvailable is null + a disclosing note (never length-faked). offset/size pagination (hasMore = offset+returned < total). name coalesces ORG_NAME (organizations) else FIRST_NAME + LAST_NAME (individuals) — null if none, never a fabricated empty; NPI/reasons/dates are strings (null-never-empty-string). A genuine no-match ⇒ honest empty (returned:0); a 4xx ⇒ invalid_input/not_found; a 5xx ⇒ THROWS; a 200 non-array/non-JSON ⇒ schema_drift. KEYLESS — no key is sent.",
6127
+ inputSchema: CmsRevokedProvidersInput,
6128
+ handler: (input) => cmsSupplier.revokedProviders(input),
6129
+ }),
5188
6130
  // ━━━ FRED (Federal Reserve Economic Data) — macro context (2) ━━━ ADR-0048
5189
6131
  // ★The server's SECOND KEY-REQUIRED source: FRED has NO keyless tier, so WITHOUT
5190
6132
  // a FRED_API_KEY both tools throw an honest invalid_input config error (the other
@@ -5193,7 +6135,7 @@ export const TOOLS: ToolDef[] = [
5193
6135
  defineTool({
5194
6136
  name: "fred_search_series",
5195
6137
  description:
5196
- "Discover FRED economic series (GDP, CPI, interest rates, unemployment, PPI…) by free-text search (FRED /fred/series/search; api.stlouisfed.org). ★REQUIRES a free FRED_API_KEY: FRED has NO keyless tier, so without the key this tool THROWS an honest config error (get one at https://fred.stlouisfed.org/docs/api/api_key.html; this and fred_series_observations are the key-required macro tools the other 112 tools stay keyless). Input: `query` (the search_text, required, e.g. 'unemployment rate' / 'CPI' / '10-year treasury'), optional `limit` (default 25, max 1000), `offset`. Returns { series:[{ id, title, frequency, frequencyShort, units, seasonalAdjustment, observationStart, observationEnd, lastUpdated, popularity }] } + honest _meta. Feed `id` into fred_series_observations for the time series. HONESTY: totalAvailable is FRED's EXACT reported `count` (offset pagination via hasMore/nextOffset — never fabricated); every scalar is null-never-empty-string; a genuine no-match ⇒ honest empty (returned:0); a 400 (bad/missing key) ⇒ invalid_input CARRYING FRED's error_message; a 5xx ⇒ THROWS; a 200 non-JSON / non-array `seriess` ⇒ schema_drift. The key rides ONLY in the &api_key= query param — never logged or echoed.",
6138
+ "Discover FRED economic series (GDP, CPI, interest rates, unemployment, PPI…) by free-text search (FRED /fred/series/search; api.stlouisfed.org). ★REQUIRES a free FRED_API_KEY: FRED has NO keyless tier, so without the key this tool THROWS an honest config error (get one at https://fred.stlouisfed.org/docs/api/api_key.html; fred_series_observations shares this key call api_key_status to see every source's key requirement). Input: `query` (the search_text, required, e.g. 'unemployment rate' / 'CPI' / '10-year treasury'), optional `limit` (default 25, max 1000), `offset`. Returns { series:[{ id, title, frequency, frequencyShort, units, seasonalAdjustment, observationStart, observationEnd, lastUpdated, popularity }] } + honest _meta. Feed `id` into fred_series_observations for the time series. HONESTY: totalAvailable is FRED's EXACT reported `count` (offset pagination via hasMore/nextOffset — never fabricated); every scalar is null-never-empty-string; a genuine no-match ⇒ honest empty (returned:0); a 400 (bad/missing key) ⇒ invalid_input CARRYING FRED's error_message; a 5xx ⇒ THROWS; a 200 non-JSON / non-array `seriess` ⇒ schema_drift. The key rides ONLY in the &api_key= query param — never logged or echoed.",
5197
6139
  inputSchema: FredSearchSeriesInput,
5198
6140
  handler: (input) => fred.searchSeries(input),
5199
6141
  }),
@@ -5204,10 +6146,88 @@ export const TOOLS: ToolDef[] = [
5204
6146
  inputSchema: FredSeriesObservationsInput,
5205
6147
  handler: (input) => fred.seriesObservations(input),
5206
6148
  }),
6149
+ // ━━━ openFDA recall/enforcement (api.fda.gov) — product-safety recalls (1) ━━━ ADR-0054
6150
+ // KEYLESS with an OPTIONAL OPENFDA_API_KEY (raises the rate limit; keyless works
6151
+ // ~1000/day — it NEVER throws for a missing key, unlike the key-REQUIRED sources).
6152
+ // Structured filters ONLY (no raw Lucene passthrough — the tool assembles + escapes
6153
+ // the search= string, injection-safe). ★P1: totalAvailable = meta.results.total
6154
+ // (EXACT). ★P2 crux: a no-match query returns openFDA HTTP 404 NOT_FOUND ⇒ an honest
6155
+ // empty, never a throw. The optional key rides &api_key= ONLY.
6156
+ defineTool({
6157
+ name: "openfda_enforcement",
6158
+ description:
6159
+ "Search openFDA recall/enforcement records — drug/device/food product recalls with the recalling firm, product, reason, FDA classification (Class I/II/III), status, and geography (openFDA /{category}/enforcement.json; api.fda.gov). KEYLESS (an OPTIONAL free OPENFDA_API_KEY only RAISES the rate limit — keyless works at ~1000 requests/day; it NEVER throws for a missing key; get one at https://open.fda.gov/apis/authentication/; call api_key_status to see every source's key requirement). Input: `category` (drug|device|food, default drug), and STRUCTURED filters — `firm` (→recalling_firm), `product` (→product_description), `reason` (→reason_for_recall), `classification` (Class I|II|III), `status` (e.g. Ongoing/Terminated/Completed), `state` (2-letter, e.g. 'CA') — the tool safely assembles + escapes these into the openFDA search= Lucene string (NO raw passthrough — injection-safe), plus `limit` (1..100, default 25) and `skip` (offset ≥0). Returns { recalls:[{ recallingFirm, productDescription, reasonForRecall, classification, status, state, city, recallInitiationDate, recallNumber, voluntaryMandated, distributionPattern }] } + honest _meta. HONESTY: totalAvailable is openFDA's EXACT meta.results.total (skip/limit pagination via hasMore/nextOffset — never results.length); every scalar (dates included, recall_initiation_date is a YYYYMMDD string) is null-never-empty-string. ★A no-match query returns openFDA HTTP 404 NOT_FOUND ⇒ an HONEST EMPTY (returned:0, totalAvailable:0), NOT an error; a 400 syntax error ⇒ invalid_input surfacing openFDA's message; a 5xx ⇒ THROWS; a 200 non-JSON ⇒ schema_drift. The optional key rides ONLY the &api_key= query param — never logged or echoed.",
6160
+ inputSchema: OpenfdaEnforcementInput,
6161
+ handler: (input) => openfda.enforcement(input),
6162
+ }),
6163
+ // ━━━ openFDA 510(k) device clearances (api.fda.gov) — medical-device regulatory (1) ━━━ ADR-0056
6164
+ // KEYLESS with an OPTIONAL OPENFDA_API_KEY (raises the rate limit; keyless works
6165
+ // ~1000/day — never throws for a missing key). SAME source/envelope/crux as
6166
+ // openfda_enforcement (reuses openfda.ts's fetchOpenfda/readOpenfdaError/luceneQuote).
6167
+ // Structured filters ONLY (no raw Lucene passthrough — the tool assembles + escapes the
6168
+ // search= string, injection-safe). ★P1: totalAvailable = meta.results.total (EXACT,
6169
+ // ~175507). ★P2 crux: a no-match query returns openFDA HTTP 404 NOT_FOUND ⇒ an honest
6170
+ // empty, never a throw. The optional key rides &api_key= ONLY.
6171
+ defineTool({
6172
+ name: "openfda_device_clearances",
6173
+ description:
6174
+ "Search openFDA 510(k) DEVICE CLEARANCES — the FDA's premarket-notification (510(k)) clearances for medical devices, with the applicant/manufacturer, device name, clearance number (K-number), decision (date + description), clearance type, product code, advisory committee, and geography (openFDA /device/510k.json; api.fda.gov). KEYLESS (an OPTIONAL free OPENFDA_API_KEY only RAISES the rate limit — keyless works at ~1000 requests/day; it NEVER throws for a missing key; get one at https://open.fda.gov/apis/authentication/; call api_key_status to see every source's key requirement). Input: STRUCTURED filters — `applicant` (→applicant), `deviceName` (→device_name), `productCode` (→product_code), `clearanceType` (→clearance_type, e.g. Traditional/Special/Abbreviated), `kNumber` (→k_number, e.g. 'K123456'), `state` (2-letter, e.g. 'CA') — the tool safely assembles + escapes these into the openFDA search= Lucene string (NO raw passthrough — injection-safe), plus `limit` (1..100, default 25) and `skip` (offset ≥0). Returns { clearances:[{ applicant, deviceName, kNumber, decisionDate, decisionDescription, clearanceType, productCode, advisoryCommittee, state }] } + honest _meta. HONESTY: totalAvailable is openFDA's EXACT meta.results.total (skip/limit pagination via hasMore/nextOffset — never results.length); every scalar (dates included, decision_date is a YYYY-MM-DD string) is null-never-empty-string. ★A no-match query returns openFDA HTTP 404 NOT_FOUND ⇒ an HONEST EMPTY (returned:0, totalAvailable:0), NOT an error; a 400 syntax error ⇒ invalid_input surfacing openFDA's message; a 5xx ⇒ THROWS; a 200 non-JSON ⇒ schema_drift. The optional key rides ONLY the &api_key= query param — never logged or echoed.",
6175
+ inputSchema: OpenfdaDeviceClearancesInput,
6176
+ handler: (input) => openfdaDevice.deviceClearances(input),
6177
+ }),
6178
+ defineTool({
6179
+ name: "openfda_drug_approvals",
6180
+ description:
6181
+ "Search openFDA Drugs@FDA DRUG APPROVALS — FDA-approved drug applications (NDA/ANDA/BLA) with the sponsor, application number, each approved product (brand + generic/active-ingredient name, dosage form, route, marketing status), and the submission/approval history (openFDA /drug/drugsfda.json; api.fda.gov). Answers 'what drugs did sponsor X get approved, and which are still marketed' — pharma vendor product/approval intelligence. KEYLESS (an OPTIONAL free OPENFDA_API_KEY only RAISES the rate limit — keyless works at ~1000 requests/day; NEVER throws for a missing key; api_key_status lists every source's key requirement). Input: STRUCTURED filters — `sponsorName` (→sponsor_name), `brandName` (→products.brand_name), `activeIngredient` (→products.active_ingredients.name), `applicationNumber` (→application_number) — safely escaped into the openFDA search= Lucene string (NO raw passthrough — injection-safe), plus `limit` (1..100, default 25) and `skip` (offset ≥0). Returns { applications:[{ applicationNumber, sponsorName, products:[{ brandName, genericIngredients:[{name,strength}], dosageForm, route, marketingStatus }], submissions:[{ submissionType, submissionNumber, submissionStatus, submissionStatusDate, submissionClass }] }] } + honest _meta. HONESTY: totalAvailable is openFDA's EXACT meta.results.total (skip/limit pagination — never results.length); every scalar is null-never-empty-string; a 'Discontinued' marketingStatus is NOT an approval revocation (disclosed in _meta). ★A no-match query returns openFDA HTTP 404 NOT_FOUND ⇒ an HONEST EMPTY (returned:0/total:0), NOT an error; a 400 ⇒ invalid_input surfacing openFDA's message; a 5xx ⇒ THROWS; a 200 non-JSON ⇒ schema_drift. The optional key rides ONLY the &api_key= query param — never logged or echoed.",
6182
+ inputSchema: OpenfdaDrugApprovalsInput,
6183
+ handler: (input) => openfdaDrugsfda.drugApprovals(input),
6184
+ }),
6185
+ // ━━━ NHTSA vehicle safety (api.nhtsa.gov) — vehicle/parts supplier vetting (2) ━━━ ADR-0057
6186
+ // ★KEYLESS — no API key at all (no parameter, no header). The cross-agency
6187
+ // product-safety family alongside openFDA (medical). Both tools share
6188
+ // make/model/modelYear inputs and return the COMPLETE matching set (no pagination
6189
+ // ⇒ totalAvailable = the upstream Count/count, complete:true). ★The complaints VIN
6190
+ // (an individual-vehicle PII identifier) is EXCLUDED from the output.
6191
+ defineTool({
6192
+ name: "nhtsa_recalls",
6193
+ description:
6194
+ "Look up NHTSA vehicle safety RECALLS for a specific vehicle — the manufacturer's recall campaigns with the affected component, the safety consequence, the remedy, and 'do not drive'/'park outside'/over-the-air-update flags (NHTSA /recalls/recallsByVehicle; api.nhtsa.gov). KEYLESS — no API key is required or accepted. Input: `make` (required, e.g. 'honda'), `model` (required, e.g. 'accord'), `modelYear` (required, 4-digit, e.g. '2020'). Returns { recalls:[{ campaignNumber, manufacturer, component, summary, consequence, remedy, reportReceivedDate, parkIt, parkOutside, overTheAirUpdate }] } + honest _meta. HONESTY: totalAvailable is NHTSA's EXACT Count and NHTSA returns the COMPLETE set for the vehicle (no pagination) ⇒ complete:true; a no-match (Count 0 / a bad make/model) ⇒ an HONEST EMPTY (returned:0), NOT an error; a 4xx ⇒ invalid_input; a 5xx/timeout ⇒ THROWS; a 200 non-JSON ⇒ schema_drift. The park-it/park-outside/over-the-air-update flags are preserved as booleans (never a fabricated false); dates are strings; every scalar is null-never-empty-string. Fixed host api.nhtsa.gov (SSRF-guarded); make/model are letters/digits/space/hyphen only and modelYear is ^\\d{4}$.",
6195
+ inputSchema: NhtsaVehicleInput,
6196
+ handler: (input) => nhtsa.recalls(input),
6197
+ }),
6198
+ defineTool({
6199
+ name: "nhtsa_complaints",
6200
+ description:
6201
+ "Look up NHTSA consumer COMPLAINTS for a specific vehicle — owner-filed safety complaints with the affected component, crash/fire flags, injury/death counts, and incident/filing dates (NHTSA /complaints/complaintsByVehicle; api.nhtsa.gov). KEYLESS — no API key is required or accepted. Input: `make` (required, e.g. 'honda'), `model` (required, e.g. 'accord'), `modelYear` (required, 4-digit, e.g. '2020'). Returns { complaints:[{ odiNumber, manufacturer, component, summary, crash, fire, numberOfInjuries, numberOfDeaths, dateOfIncident, dateComplaintFiled }] } + honest _meta. ★PRIVACY: the NHTSA complaint VIN (an individual-vehicle identifier) is INTENTIONALLY EXCLUDED from the output — the B2G signal is the manufacturer/component/crash/fire/injury/death safety history, not the VIN. HONESTY: totalAvailable is NHTSA's EXACT count and NHTSA returns the COMPLETE set for the vehicle (no pagination) ⇒ complete:true; a no-match ⇒ an HONEST EMPTY (returned:0), NOT an error; crash/fire preserved as booleans (never a fabricated false); numberOfInjuries/numberOfDeaths via numeric coercion (a genuine 0 stays 0, NEVER null-for-0); dates are strings; a 4xx ⇒ invalid_input; a 5xx/timeout ⇒ THROWS; a 200 non-JSON ⇒ schema_drift. Fixed host api.nhtsa.gov (SSRF-guarded); make/model are letters/digits/space/hyphen only and modelYear is ^\\d{4}$.",
6202
+ inputSchema: NhtsaVehicleInput,
6203
+ handler: (input) => nhtsa.complaints(input),
6204
+ }),
6205
+ // ━━━ CPSC consumer-product recalls (www.saferproducts.gov) — goods/import vetting (1) ━━━ ADR-0058
6206
+ // ★KEYLESS — no API key at all (no parameter, no header). The third leg of the
6207
+ // cross-agency product-safety family alongside NHTSA (vehicles) and openFDA
6208
+ // (medical). The response is a bare JSON ARRAY with NO total-count field and NO
6209
+ // pagination ⇒ totalAvailable = the returned count, complete:true. All filters are
6210
+ // optional; with NO filter the tool bounds results to a ~90-day default window
6211
+ // (disclosed) rather than silently fetch the entire dataset.
6212
+ defineTool({
6213
+ name: "cpsc_recalls",
6214
+ description:
6215
+ "Look up U.S. CPSC consumer-product RECALLS — the recall title, hazard description, remedy, affected products, manufacturers, retailers, injuries, and country of manufacture (CPSC SaferProducts /RestWebServices/Recall; www.saferproducts.gov). The consumer-goods / import product-safety lane alongside nhtsa_recalls (vehicles) and openfda (medical). KEYLESS — no API key is required or accepted. Inputs (ALL optional): `dateStart`/`dateEnd` (YYYY-MM-DD recall date range), `productName` (substring), `manufacturer` (substring), `recallNumber` (a specific CPSC recall number). Returns { recalls:[{ recallNumber, recallDate, title, description, url, products:[names], numberOfUnits, manufacturers:[names], retailers:[names], hazards:[descriptions], remedies:[descriptions], injuries:[names], manufacturerCountries:[names] }] } + honest _meta. HONESTY: the CPSC response is a bare array with NO count field and NO pagination — it returns the COMPLETE matching set, so totalAvailable = the number of returned recalls and complete:true (never a fabricated total). ★With NO filter given, results are bounded to a DEFAULT ~90-day recent window (RecallDateStart, disclosed in _meta.notes) rather than a silent whole-dataset fetch. An empty result ⇒ an HONEST EMPTY (returned:0), NOT an error; a 4xx ⇒ invalid_input; a 5xx/timeout ⇒ THROWS; a 200 non-JSON OR a non-array body ⇒ schema_drift. Nested arrays are flattened to name/description strings (an empty {} object is skipped, never fabricated); NumberOfUnits is free text kept as a string; dates are strings; every scalar is null-never-empty-string. Fixed host www.saferproducts.gov (SSRF-guarded); dates are ^\\d{4}-\\d{2}-\\d{2}$ and recallNumber is letters/digits/hyphen only.",
6216
+ inputSchema: CpscRecallsInput,
6217
+ handler: (input) => cpsc.recalls(input),
6218
+ }),
6219
+ // ━━━ CBP Border Wait Times (bwt.cbp.gov) — freight/logistics (1) ━━━
6220
+ defineTool({
6221
+ name: "cbp_border_wait_times",
6222
+ description:
6223
+ "Live CBP land-border-port wait times — current commercial-vehicle (and passenger) crossing delays at every US Canadian- and Mexican-border port (keyless; bwt.cbp.gov). The FREIGHT / LOGISTICS situational-awareness lane: per-port commercial-vehicle standard + FAST lane delay (minutes), operational status, open-lane count, and maximum lanes. Filters (optional): `border` (case-insensitive substring, 'Canadian'/'Mexican'), `portName` (substring, e.g. 'Laredo'); `limit`/`offset` pagination. Each row: { portNumber, portName, crossingName, border, portStatus (Open/Closed), asOf, commercialVehicle:{ maxLanes, standard:{operationalStatus, delayMinutes, lanesOpen, updateTime}, fast:{…} } }. HONESTY: this is REAL-TIME operational data — each lane carries its own updateTime (surfaced verbatim; freshness never implied live-to-the-second); delayMinutes/lanesOpen are number|null (a real 0 stays 0; an empty/N/A value — e.g. a closed lane — is null, NEVER a fabricated 0, because a closed lane's delay is UNKNOWN, not zero); the API returns the WHOLE port set so totalAvailable is the EXACT matched-port count; an outage/4xx/timeout THROWS and a non-array body ⇒ schema_drift (never a fake empty).",
6224
+ inputSchema: CbpBorderWaitInput,
6225
+ handler: (input) => cbpBorder.borderWaitTimes(input),
6226
+ }),
5207
6227
  // ━━━ BEA Regional Economic Accounts (apps.bea.gov) — regional GDP/income (1) ━━━ ADR-0051
5208
6228
  // ★The server's THIRD KEY-REQUIRED source: the BEA Data API has NO keyless tier, so
5209
6229
  // WITHOUT a BEA_API_KEY this tool throws an honest invalid_input config error (the
5210
- // other 116 tools stay keyless). County/state/MSA GDP-by-industry + personal income —
6230
+ // most other tools stay keyless — see api_key_status). County/state/MSA GDP-by-industry + personal income —
5211
6231
  // the regional place-of-performance lane. ★The P2 crux: a missing/invalid key returns
5212
6232
  // HTTP 200 carrying BEAAPI.Results.Error (NOT an HTTP error status), which is detected
5213
6233
  // BEFORE the Data-array drift check and surfaced as invalid_input (never a fake empty).
@@ -5215,7 +6235,7 @@ export const TOOLS: ToolDef[] = [
5215
6235
  defineTool({
5216
6236
  name: "bea_regional_data",
5217
6237
  description:
5218
- "Regional (county / state / MSA) economic data — GDP by industry and personal income — from the US Bureau of Economic Analysis (BEA) Regional Economic Accounts (apps.bea.gov/api/data, dataset 'Regional'). ★REQUIRES a free BEA_API_KEY: the BEA Data API has NO keyless tier, so without the key this tool THROWS an honest config error (get one at https://apps.bea.gov/API/signup/; Census, FRED, and BEA are the only key-required sources — every other tool is keyless). Input: `tableName` (required, e.g. 'CAGDP2' county GDP by industry, 'SAGDP2N' state GDP, 'CAINC1'/'SAINC1' personal income), `geoFips` (required — 'STATE' for all states, a county FIPS like '06075', or an MSA code), `lineCode` (required — an integer industry line like '1', or 'ALL'), optional `year` ('LAST5' default, a 4-digit year, or 'ALL'), `frequency` ('A' annual default, or 'Q'). Returns { rows:[{ geoFips, geoName, timePeriod, lineCode, dataValue, unitOfMeasure, unitMult, noteRef }], notes:[{ noteRef, noteText }] } + honest _meta. ★HONESTY (the crux): a missing/invalid key — or ANY bad parameter — returns HTTP 200 carrying an Error object (NOT an HTTP error status); this is detected and surfaced as invalid_input carrying BEA's APIErrorDescription — NEVER a fake empty. dataValue is parsed from BEA's comma-formatted string ('1,234,567'→1234567); BEA suppression/not-available codes ((NA)/(D)/(NM)/(L)/*) map to null — NEVER 0 (a genuine 0 stays 0). unitMult (a power-of-10 multiplier) and unitOfMeasure are reported ALONGSIDE the raw dataValue — the value is NOT multiplied in (apply unitMult yourself). BEA returns the COMPLETE set for the filter (no pagination) ⇒ totalAvailable = the row count, complete:true; a genuine empty Data:[] ⇒ honest empty (returned:0); a 5xx ⇒ THROWS; a 200 non-JSON ⇒ schema_drift. The key rides ONLY in the UserID= query param — never logged or echoed.",
6238
+ "Regional (county / state / MSA) economic data — GDP by industry and personal income — from the US Bureau of Economic Analysis (BEA) Regional Economic Accounts (apps.bea.gov/api/data, dataset 'Regional'). ★REQUIRES a free BEA_API_KEY: the BEA Data API has NO keyless tier, so without the key this tool THROWS an honest config error (get one at https://apps.bea.gov/API/signup/; call api_key_status to see every source's key requirement). Input: `tableName` (required, e.g. 'CAGDP2' county GDP by industry, 'SAGDP2N' state GDP, 'CAINC1'/'SAINC1' personal income), `geoFips` (required — 'STATE' for all states, a county FIPS like '06075', or an MSA code), `lineCode` (required — an integer industry line like '1', or 'ALL'), optional `year` ('LAST5' default, a 4-digit year, or 'ALL'), `frequency` ('A' annual default, or 'Q'). Returns { rows:[{ geoFips, geoName, timePeriod, lineCode, dataValue, unitOfMeasure, unitMult, noteRef }], notes:[{ noteRef, noteText }] } + honest _meta. ★HONESTY (the crux): a missing/invalid key — or ANY bad parameter — returns HTTP 200 carrying an Error object (NOT an HTTP error status); this is detected and surfaced as invalid_input carrying BEA's APIErrorDescription — NEVER a fake empty. dataValue is parsed from BEA's comma-formatted string ('1,234,567'→1234567); BEA suppression/not-available codes ((NA)/(D)/(NM)/(L)/*) map to null — NEVER 0 (a genuine 0 stays 0). unitMult (a power-of-10 multiplier) and unitOfMeasure are reported ALONGSIDE the raw dataValue — the value is NOT multiplied in (apply unitMult yourself). BEA returns the COMPLETE set for the filter (no pagination) ⇒ totalAvailable = the row count, complete:true; a genuine empty Data:[] ⇒ honest empty (returned:0); a 5xx ⇒ THROWS; a 200 non-JSON ⇒ schema_drift. The key rides ONLY in the UserID= query param — never logged or echoed.",
5219
6239
  inputSchema: BeaRegionalDataInput,
5220
6240
  handler: (input) => bea.regionalData(input),
5221
6241
  }),
@@ -5250,7 +6270,7 @@ export const TOOLS: ToolDef[] = [
5250
6270
  defineTool({
5251
6271
  name: "dol_get_dataset",
5252
6272
  description:
5253
- "Fetch records from ONE US DOL dataset (apiprod.dol.gov /v4/get/{agency}/{endpoint}/json). ★REQUIRES a free DOL_API_KEY: the DOL DATA endpoint has NO keyless tier, so without the key this tool THROWS an honest config error (get one at https://dol.gov/developer; the dataset CATALOG — dol_list_datasets — and agency list stay keyless). Input: `agency` (required — the `agencyAbbr` from dol_list_datasets, e.g. 'WHD', 'OSHA', 'ILAB'; rides the PATH, ^[A-Za-z0-9_]+$), `table` (required — the dataset's `apiUrl` endpoint from dol_list_datasets, e.g. 'Child_Labor_Report__2016_to_2022'; rides the PATH, ^[A-Za-z0-9_]+$), optional `limit` (default 10, max 100), `offset`, `filterField`+`filterValue` (a paired equality filter → a DOL filter_object), `fields` (best-effort column selection). Returns { records:[…verbatim dataset rows…] } + honest _meta. HONESTY: records are surfaced VERBATIM (the data-record envelope is key-gated and unverified, so field names/values are preserved as-is — a genuine 0 stays 0, a missing field stays null; the tool never coerces or fabricates). totalAvailable is a real count field ONLY when the response carries one, else null (an honest unknown — `returned` is NEVER passed off as the total); offset pagination (a full page ⇒ hasMore, page forward to confirm). A missing/invalid key (401/403) ⇒ invalid_input carrying the DOL_API_KEY guidance (never empty); a 400 ⇒ invalid_input; a genuine empty ⇒ honest empty (returned:0); a 429 ⇒ rate_limited THROWS (Retry-After honored); a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON / no row array ⇒ schema_drift. The key rides ONLY in the X-API-KEY request header — never the URL / _meta / a log.",
6273
+ "Fetch records from ONE US DOL dataset (apiprod.dol.gov /v4/get/{agency}/{endpoint}/json). ★REQUIRES a free DOL_API_KEY: the DOL DATA endpoint has NO keyless tier, so without the key this tool THROWS an honest config error (get one at https://dataportal.dol.gov/registration; the dataset CATALOG — dol_list_datasets — and agency list stay keyless). Input: `agency` (required — the `agencyAbbr` from dol_list_datasets, e.g. 'WHD', 'OSHA', 'ILAB'; rides the PATH, ^[A-Za-z0-9_]+$), `table` (required — the dataset's `apiUrl` endpoint from dol_list_datasets, e.g. 'Child_Labor_Report__2016_to_2022'; rides the PATH, ^[A-Za-z0-9_]+$), optional `limit` (default 10, max 100), `offset`, `filterField`+`filterValue` (a paired equality filter → a DOL filter_object), `fields` (best-effort column selection). Returns { records:[…verbatim dataset rows…] } + honest _meta. HONESTY: records are surfaced VERBATIM (the data-record envelope is key-gated and unverified, so field names/values are preserved as-is — a genuine 0 stays 0, a missing field stays null; the tool never coerces or fabricates). totalAvailable is a real count field ONLY when the response carries one, else null (an honest unknown — `returned` is NEVER passed off as the total); offset pagination (a full page ⇒ hasMore, page forward to confirm). A missing/invalid key (401/403) ⇒ invalid_input carrying the DOL_API_KEY guidance (never empty); a 400 ⇒ invalid_input; a genuine empty ⇒ honest empty (returned:0); a 429 ⇒ rate_limited THROWS (Retry-After honored); a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON / no row array ⇒ schema_drift. The key rides ONLY in the X-API-KEY request header — never the URL / _meta / a log.",
5254
6274
  inputSchema: DolGetDatasetInput,
5255
6275
  handler: (input) => dol.getDataset(input),
5256
6276
  }),
@@ -5268,6 +6288,43 @@ export const TOOLS: ToolDef[] = [
5268
6288
  inputSchema: LdaSearchFilingsInput,
5269
6289
  handler: (input) => lda.searchFilings(input),
5270
6290
  }),
6291
+ // ━━━ US federal court opinions (www.courtlistener.com) — the litigation lane (1) ━━━ ADR-0055
6292
+ // Federal court decisions — the judicial signal no contract/spending/lobbying source
6293
+ // carries (uscfc bid-protest/contract-claim opinions, cafc contract/patent appeals).
6294
+ // ★PROVENANCE: CourtListener (Free Law Project, a non-profit), NOT a .gov API — PACER
6295
+ // (the .gov source) is paywalled. KEYLESS (anonymous 200); the OPTIONAL free
6296
+ // COURTLISTENER_API_TOKEN only raises the rate limit, riding the Authorization: Token …
6297
+ // header ONLY. ★count is the API's REAL total — never results.length; CURSOR pagination
6298
+ // (nextCursor extracted from `next`, host re-asserted). type=o FIXED.
6299
+ defineTool({
6300
+ name: "courtlistener_search_opinions",
6301
+ description:
6302
+ "Search US FEDERAL COURT OPINIONS (case law / litigation) via CourtListener (www.courtlistener.com/api/rest/v4/search, type=o). ★PROVENANCE: the DATA is US federal court PUBLIC RECORDS, but the API is CourtListener, run by the Free Law Project (a NON-PROFIT) — this is NOT a .gov API; CourtListener republishes these records KEYLESS because the .gov primary source (PACER) is PAYWALLED. KEYLESS (anonymous access works; an optional free COURTLISTENER_API_TOKEN only raises the rate limit; get one at https://www.courtlistener.com/help/api/rest/; call api_key_status to see every source's key requirement). All inputs optional: `query` (full-text → q), `court` (a court id, ^[a-z0-9]+$ — e.g. 'uscfc' US Court of Federal Claims for contract claims/bid protests, 'cafc' Federal Circuit for contract/patent appeals, 'scotus'), `dateFiledAfter`/`dateFiledBefore` (ISO ^\\d{4}-\\d{2}-\\d{2}$ → filed_after/filed_before), `natureOfSuit` (folded into the q query — no verified dedicated filter, disclosed in notes), `cursor` (opaque continuation — pass back _meta.nextCursor), `order` (→ order_by, default 'dateFiled desc'). Returns { opinions:[{ caseName, court, courtId, dateFiled, docketNumber, natureOfSuit, status, judge, citation, absoluteUrl }] } + honest _meta. HONESTY: totalAvailable is the API's REAL `count` (the total match count for the filter) — NOT the rows on this page; pagination is an OPAQUE CURSOR (offset/nextOffset are null/meaningless — pass _meta.nextCursor back as `cursor`; nextCursor:null/hasMore:false = last page). CourtListener v4 stops counting on deep cursor pages (count:null) ⇒ totalAvailable:null is DISCLOSED, never faked as results.length. dateFiled is a date STRING; citation may be an array/object ⇒ flattened to a safe string/string[] (never fabricated); judge/natureOfSuit/docketNumber are null when absent (never ''); absoluteUrl is the full https://www.courtlistener.com link. A genuine no-match (results:[]) ⇒ honest empty (returned:0); a 400 (bad param) ⇒ invalid_input surfacing the API's message; a 429 (unauth throttle) ⇒ rate_limited THROWS (Retry-After honored, never routed around); a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON / non-array results / a count that is neither a number nor null ⇒ schema_drift; an off-host `next` is REFUSED (SSRF). The optional token rides ONLY in the Authorization: Token header (never the URL/_meta).",
6303
+ inputSchema: CourtlistenerSearchOpinionsInput,
6304
+ handler: (input) => courtlistener.searchOpinions(input),
6305
+ }),
6306
+ // ━━━ US tax-exempt nonprofits (projects.propublica.org) — the nonprofit lane (2) ━━━ ADR-0060
6307
+ // Who a 501(c) org IS (EIN, NTEE, subsection, ruling date, status) + its Form 990
6308
+ // FINANCIALS (revenue/expenses/assets/liabilities by year) — the nonprofit/grantee/
6309
+ // subcontractor vetting signal no contract/spending/grant/lobbying source carries.
6310
+ // ★PROVENANCE: IRS Form 990 public records republished KEYLESS by ProPublica Nonprofit
6311
+ // Explorer (a non-profit newsroom), NOT a .gov API (the IRS has no clean query API).
6312
+ // KEYLESS (no key). search total_results is the REAL total — never organizations.length;
6313
+ // financials totalAvailable = filings.length (the complete set). Money via num (null-never-0).
6314
+ defineTool({
6315
+ name: "nonprofit_search",
6316
+ description:
6317
+ "Search US TAX-EXEMPT NONPROFITS (501(c) organizations) by IRS Form 990 data via ProPublica Nonprofit Explorer (projects.propublica.org/nonprofits/api/v2/search). ★PROVENANCE: the DATA is IRS Form 990 filings — FEDERAL tax-exempt PUBLIC RECORDS — but the API is ProPublica Nonprofit Explorer, run by ProPublica (a NON-PROFIT newsroom) — this is NOT a .gov API; ProPublica republishes these records KEYLESS because the IRS itself has no clean query API (only bulk downloads / a web UI). KEYLESS (no key of any kind). All inputs optional: `query` (full-text org name/keyword → q), `state` (2-letter code → state[id], ^[A-Za-z]{2}$), `ntee` (NTEE major category, integer 1..10 → ntee[id]), `page` (0-BASED, default 0). Returns { organizations:[{ ein, name, city, state, nteeCode, subsectionCode }] } + honest _meta. HONESTY: totalAvailable is the API's REAL total_results (the total match count for the query) — NOT the organizations on this page; pagination is page-based and 0-INDEXED (pass page=cur_page+1 when hasMore). ein/nteeCode/subsectionCode are strings (never num-coerced). A genuine no-match (organizations:[]) ⇒ honest empty (returned:0, complete:true); a 4xx ⇒ invalid_input; a 429 ⇒ rate_limited THROWS (Retry-After honored, never routed around); a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON / non-array organizations / non-number total_results ⇒ schema_drift. Data is IRS Form 990 data via ProPublica Nonprofit Explorer, disclosed in _meta.source and a note.",
6318
+ inputSchema: NonprofitSearchInput,
6319
+ handler: (input) => nonprofit.search(input),
6320
+ }),
6321
+ defineTool({
6322
+ name: "nonprofit_financials",
6323
+ description:
6324
+ "Fetch ONE US tax-exempt nonprofit's IRS Form 990 profile + FINANCIALS by EIN via ProPublica Nonprofit Explorer (projects.propublica.org/nonprofits/api/v2/organizations/{ein}.json). ★PROVENANCE: the DATA is IRS Form 990 filings (federal tax-exempt public records) but the API is ProPublica Nonprofit Explorer, run by ProPublica (a NON-PROFIT newsroom) — NOT a .gov API; ProPublica republishes these records KEYLESS because the IRS has no clean query API. KEYLESS (no key). Input: `ein` (required — the Employer Identification Number, 1..9 digits ^\\d{1,9}$, e.g. '530196605' American National Red Cross; rides the URL path). Returns { organization:{ ein, name, address, city, state, zip, nteeCode, subsectionCode, rulingDate, statusCode }, filings:[{ taxYear, formType, revenueUsd, expensesUsd, assetsUsd, liabilitiesUsd, pdfUrl }] } + honest _meta. HONESTY: the four Form 990 figures (revenueUsd/expensesUsd/assetsUsd/liabilitiesUsd, from totrevenue/totfuncexpns/totassetsend/totliabend) ride null-never-0 coercion — a genuine reported 0 stays 0, an absent figure ⇒ null (NEVER 0-faked); ein/codes are strings; rulingDate is a date string. totalAvailable = filings.length (the COMPLETE Form 990 filing set from the one detail document — no pagination). An unknown EIN (HTTP 404) ⇒ not_found (NEVER a fabricated empty org); a 4xx ⇒ invalid_input; a 429 ⇒ rate_limited THROWS; a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON / non-object organization / non-array filings_with_data ⇒ schema_drift. Data is IRS Form 990 data via ProPublica Nonprofit Explorer, disclosed in _meta.source and a note.",
6325
+ inputSchema: NonprofitFinancialsInput,
6326
+ handler: (input) => nonprofit.financials(input),
6327
+ }),
5271
6328
  // ━━━ Self-service key discovery (1) ━━━
5272
6329
  // KEYLESS. A local status query — reads process.env (+ any .env auto-loaded at
5273
6330
  // startup) and reports, per key, whether it is set (a BOOLEAN — the key VALUE is
@@ -5421,6 +6478,28 @@ function synthesizeDefaultMeta(
5421
6478
  return buildMeta({ source, keylessMode, complete: true, truncated: false });
5422
6479
  }
5423
6480
 
6481
+ // Unwrap a tool's inputSchema down to its underlying ZodObject so we can read the
6482
+ // set of declared top-level keys. Tools wrap the object in .refine()/.superRefine()
6483
+ // (ZodEffects), or occasionally .optional()/.default()/.nullable(), so peel those
6484
+ // layers. Returns null if no ZodObject is reachable (then unknown-key rejection is
6485
+ // skipped for that tool — fail open, never fail closed on our own introspection).
6486
+ function objectSchemaOf(schema: z.ZodTypeAny): z.AnyZodObject | null {
6487
+ let s: unknown = schema;
6488
+ for (let i = 0; i < 20; i++) {
6489
+ const def = (s as { _def?: { typeName?: string } } | undefined)?._def;
6490
+ if (!def) break;
6491
+ const tn = def.typeName;
6492
+ if (tn === "ZodObject") return s as z.AnyZodObject;
6493
+ if (tn === "ZodEffects") { s = (def as { schema?: unknown }).schema; continue; }
6494
+ if (tn === "ZodOptional" || tn === "ZodDefault" || tn === "ZodNullable") {
6495
+ s = (def as { innerType?: unknown }).innerType;
6496
+ continue;
6497
+ }
6498
+ break;
6499
+ }
6500
+ return null;
6501
+ }
6502
+
5424
6503
  export async function runTool(
5425
6504
  name: string,
5426
6505
  args: Record<string, unknown>,
@@ -5434,6 +6513,27 @@ export async function runTool(
5434
6513
  // gone (all 52 tools migrated) — an unknown name has no entry and throws.
5435
6514
  const entry = TOOLS.find((t) => t.name === name);
5436
6515
  if (entry?.handler) {
6516
+ // HONESTY (dogfooding 2026-07-15): reject UNKNOWN top-level input keys LOUD
6517
+ // instead of Zod's default silent-strip. A misspelled filter (naicsCode↔naics,
6518
+ // keyword↔query) was otherwise dropped and the tool scanned the WHOLE corpus,
6519
+ // returning an authoritative-looking WRONG answer with no error. We name the
6520
+ // unknown key(s) and list the valid ones so the mistake self-corrects. Skip
6521
+ // when the schema intentionally accepts extras (unknownKeys==="passthrough").
6522
+ const obj = objectSchemaOf(entry.inputSchema);
6523
+ if (obj && obj._def.unknownKeys !== "passthrough" && args && typeof args === "object") {
6524
+ const known = new Set(Object.keys(obj.shape));
6525
+ const unknown = Object.keys(args).filter((k) => !known.has(k));
6526
+ if (unknown.length > 0) {
6527
+ throw new ToolErrorCarrier({
6528
+ kind: "invalid_input",
6529
+ message:
6530
+ `Unknown input ${unknown.length > 1 ? "keys" : "key"} for ${name}: ` +
6531
+ `${unknown.map((k) => `'${k}'`).join(", ")}. ` +
6532
+ `Valid keys: ${[...known].sort().join(", ")}.`,
6533
+ retryable: false,
6534
+ });
6535
+ }
6536
+ }
5437
6537
  const input = entry.inputSchema.parse(args);
5438
6538
  return await entry.handler(input, { sam });
5439
6539
  }