@cliwant/mcp-sam-gov 1.4.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 (82) hide show
  1. package/README.ja.md +11 -11
  2. package/README.ko.md +11 -11
  3. package/README.md +21 -13
  4. package/dist/cbp-border.d.ts +51 -0
  5. package/dist/cbp-border.d.ts.map +1 -0
  6. package/dist/cbp-border.js +123 -0
  7. package/dist/cbp-border.js.map +1 -0
  8. package/dist/datagov-catalog.d.ts.map +1 -1
  9. package/dist/datagov-catalog.js +16 -2
  10. package/dist/datagov-catalog.js.map +1 -1
  11. package/dist/ecfr.d.ts +2 -2
  12. package/dist/ecfr.d.ts.map +1 -1
  13. package/dist/ecfr.js +24 -10
  14. package/dist/ecfr.js.map +1 -1
  15. package/dist/edgar.d.ts.map +1 -1
  16. package/dist/edgar.js +26 -6
  17. package/dist/edgar.js.map +1 -1
  18. package/dist/epa-envirofacts.d.ts.map +1 -1
  19. package/dist/epa-envirofacts.js +14 -1
  20. package/dist/epa-envirofacts.js.map +1 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +11 -0
  23. package/dist/errors.js.map +1 -1
  24. package/dist/far.d.ts.map +1 -1
  25. package/dist/far.js +3 -1
  26. package/dist/far.js.map +1 -1
  27. package/dist/federal-register.d.ts +2 -2
  28. package/dist/federal-register.d.ts.map +1 -1
  29. package/dist/federal-register.js +26 -10
  30. package/dist/federal-register.js.map +1 -1
  31. package/dist/fema.d.ts +36 -0
  32. package/dist/fema.d.ts.map +1 -1
  33. package/dist/fema.js +124 -0
  34. package/dist/fema.js.map +1 -1
  35. package/dist/gov-domains.d.ts +66 -0
  36. package/dist/gov-domains.d.ts.map +1 -0
  37. package/dist/gov-domains.js +211 -0
  38. package/dist/gov-domains.js.map +1 -0
  39. package/dist/nist-controls.d.ts +48 -0
  40. package/dist/nist-controls.d.ts.map +1 -0
  41. package/dist/nist-controls.js +174 -0
  42. package/dist/nist-controls.js.map +1 -0
  43. package/dist/nws-weather.d.ts +57 -0
  44. package/dist/nws-weather.d.ts.map +1 -0
  45. package/dist/nws-weather.js +131 -0
  46. package/dist/nws-weather.js.map +1 -0
  47. package/dist/openfda-drugsfda.d.ts +72 -0
  48. package/dist/openfda-drugsfda.d.ts.map +1 -0
  49. package/dist/openfda-drugsfda.js +230 -0
  50. package/dist/openfda-drugsfda.js.map +1 -0
  51. package/dist/openfda.d.ts.map +1 -1
  52. package/dist/openfda.js +31 -8
  53. package/dist/openfda.js.map +1 -1
  54. package/dist/server.d.ts.map +1 -1
  55. package/dist/server.js +331 -10
  56. package/dist/server.js.map +1 -1
  57. package/dist/treasury.d.ts +2 -0
  58. package/dist/treasury.d.ts.map +1 -1
  59. package/dist/treasury.js +7 -0
  60. package/dist/treasury.js.map +1 -1
  61. package/dist/usaspending.d.ts +32 -1
  62. package/dist/usaspending.d.ts.map +1 -1
  63. package/dist/usaspending.js +143 -16
  64. package/dist/usaspending.js.map +1 -1
  65. package/package.json +2 -2
  66. package/src/cbp-border.ts +177 -0
  67. package/src/datagov-catalog.ts +18 -2
  68. package/src/ecfr.ts +27 -10
  69. package/src/edgar.ts +39 -7
  70. package/src/epa-envirofacts.ts +17 -1
  71. package/src/errors.ts +11 -0
  72. package/src/far.ts +3 -1
  73. package/src/federal-register.ts +29 -10
  74. package/src/fema.ts +139 -0
  75. package/src/gov-domains.ts +237 -0
  76. package/src/nist-controls.ts +219 -0
  77. package/src/nws-weather.ts +167 -0
  78. package/src/openfda-drugsfda.ts +313 -0
  79. package/src/openfda.ts +30 -7
  80. package/src/server.ts +352 -10
  81. package/src/treasury.ts +7 -0
  82. package/src/usaspending.ts +189 -17
package/src/server.ts CHANGED
@@ -68,18 +68,23 @@ import * as lda from "./lda.js";
68
68
  import * as courtlistener from "./courtlistener.js";
69
69
  import * as nonprofit from "./nonprofit.js";
70
70
  import * as fema from "./fema.js";
71
+ import * as nws from "./nws-weather.js";
72
+ import * as govDomains from "./gov-domains.js";
71
73
  import * as fdic from "./fdic.js";
72
74
  import * as bls from "./bls.js";
73
75
  import * as ofac from "./ofac.js";
74
76
  import * as nvd from "./nvd.js";
77
+ import * as nistControls from "./nist-controls.js";
75
78
  import * as nppes from "./nppes.js";
76
79
  import * as cms from "./cms.js";
77
80
  import * as fac from "./fac.js";
78
81
  import * as usitc from "./usitc.js";
79
82
  import * as openfda from "./openfda.js";
80
83
  import * as openfdaDevice from "./openfda-device.js";
84
+ import * as openfdaDrugsfda from "./openfda-drugsfda.js";
81
85
  import * as nhtsa from "./nhtsa.js";
82
86
  import * as cpsc from "./cpsc.js";
87
+ import * as cbpBorder from "./cbp-border.js";
83
88
  import { fetchAttachmentText } from "./attachments.js";
84
89
  import * as keys from "./keys.js";
85
90
  import { toToolError, ToolErrorCarrier, errorFromResponse } from "./errors.js";
@@ -95,7 +100,7 @@ import { realpathSync } from "node:fs";
95
100
  const SERVER_NAME = "mcp-sam-gov";
96
101
  // Kept in lockstep with package.json / manifest.json / server.json.
97
102
  // Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
98
- const SERVER_VERSION = "1.4.0";
103
+ const SERVER_VERSION = "1.5.0";
99
104
 
100
105
  // ─── Tool input schemas (Zod) ────────────────────────────────────
101
106
 
@@ -116,6 +121,7 @@ const SamSearchInput = z.object({
116
121
  "Set-aside codes: SBA, 8A, HZS, SDVOSBC, WOSB, EDWOSB, VSA, VSS",
117
122
  ),
118
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)."),
119
125
  });
120
126
 
121
127
  // Pre-solicitation shaping radar (doc 06 §3.1). Surfaces Sources Sought /
@@ -229,7 +235,11 @@ const UsasIndividualAwardsInput = UsasFiltersBase.extend({
229
235
  });
230
236
 
231
237
  const UsasSubAgencyInput = z.object({
232
- 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
+ ),
233
243
  fiscalYear: z.number().int().min(2007).optional(),
234
244
  });
235
245
 
@@ -246,7 +256,14 @@ const UsasRecipientAwardsInput = z.object({
246
256
  });
247
257
 
248
258
  const UsasSubawardsInput = z.object({
249
- 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(),
250
267
  agency: z.string().optional(),
251
268
  naics: z.string().optional(),
252
269
  fiscalYear: z.number().int().min(2007).optional(),
@@ -369,14 +386,24 @@ const UsasSpendingOverTimeInput = z.object({
369
386
  });
370
387
 
371
388
  const UsasCategorySpendingInput = z.object({
372
- 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
+ ),
373
395
  naics: z.string().optional(),
374
396
  fiscalYear: z.number().int().min(2007).optional(),
375
397
  limit: z.number().min(1).max(50).optional(),
376
398
  });
377
399
 
378
400
  const UsasCfdaInput = z.object({
379
- 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
+ ),
380
407
  fiscalYear: z.number().int().min(2007).optional(),
381
408
  limit: z.number().min(1).max(50).optional(),
382
409
  });
@@ -441,6 +468,25 @@ const UsasListAgenciesInput = z.object({
441
468
  limit: z.number().min(1).max(150).optional(),
442
469
  });
443
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
+
444
490
  // Federal Register
445
491
  const FedRegSearchInput = z.object({
446
492
  query: z.string().optional(),
@@ -995,6 +1041,41 @@ const CisaKevLookupInput = z.object({
995
1041
  offset: z.number().min(0).optional().describe("Zero-based page offset (default 0)."),
996
1042
  });
997
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
+
998
1079
  // ━━━ NPPES NPI Registry — the healthcare-provider identity/credentialing lane (1) ━━━ ADR-0036
999
1080
  // nppes_lookup_provider: exact NPI detail OR search over CMS/HHS's keyless public
1000
1081
  // registry of every US healthcare provider (npiregistry.cms.hhs.gov/api, version=2.1
@@ -1326,9 +1407,11 @@ const TreasuryDatasetEnum = z
1326
1407
  "mts_table_1",
1327
1408
  "rates_of_exchange",
1328
1409
  "debt_outstanding",
1410
+ "interest_expense",
1411
+ "tror",
1329
1412
  ])
1330
1413
  .describe(
1331
- "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).",
1332
1415
  );
1333
1416
 
1334
1417
  const TreasuryQueryDatasetInput = z.object({
@@ -2470,6 +2553,127 @@ const FemaDisasterDeclarationsInput = z.object({
2470
2553
  .describe("0-based row offset ($skip) for pagination, default 0."),
2471
2554
  });
2472
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
+
2473
2677
  // ─── EPA ECHO REST (keyless facility compliance/enforcement) — input schemas ──
2474
2678
  // ADR-0009. KEYLESS, single fixed host (echodata.epa.gov) + three fixed service
2475
2679
  // paths (the SSRF core — no free host/path). `state` is a curated US state/
@@ -2730,7 +2934,7 @@ const DatagovSearchDatasetsInput = z.object({
2730
2934
  .min(1)
2731
2935
  .max(500)
2732
2936
  .optional()
2733
- .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)."),
2734
2938
  organization: z
2735
2939
  .string()
2736
2940
  .min(1)
@@ -3917,6 +4121,42 @@ const OpenfdaDeviceClearancesInput = z.object({
3917
4121
  .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3918
4122
  });
3919
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
+
3920
4160
  // ─── NHTSA vehicle safety (api.nhtsa.gov) — KEYLESS vehicle/parts supplier vetting ──
3921
4161
  // ADR-0057. Two tools (recalls + complaints) share make/model/modelYear inputs. NO
3922
4162
  // API key at all. ★The complaints VIN (PII) is excluded from the output. modelYear is
@@ -4918,7 +5158,7 @@ export const TOOLS: ToolDef[] = [
4918
5158
  defineTool({
4919
5159
  name: "usas_search_subawards",
4920
5160
  description:
4921
- "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.",
4922
5162
  inputSchema: UsasSubawardsInput,
4923
5163
  handler: (input) => usas.searchSubawards(input),
4924
5164
  }),
@@ -5070,6 +5310,20 @@ export const TOOLS: ToolDef[] = [
5070
5310
  inputSchema: UsasListAgenciesInput,
5071
5311
  handler: (input) => usas.listToptierAgencies(input),
5072
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
+ }),
5073
5327
 
5074
5328
  // ━━━ Federal Register (4) ━━━
5075
5329
  defineTool({
@@ -5237,6 +5491,13 @@ export const TOOLS: ToolDef[] = [
5237
5491
  inputSchema: CisaKevLookupInput,
5238
5492
  handler: (input) => nvd.cisaKevLookup(input),
5239
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
+ }),
5240
5501
  // ━━━ NPPES NPI Registry — Healthcare-Provider Vetting (1) ━━━ ADR-0036
5241
5502
  defineTool({
5242
5503
  name: "nppes_lookup_provider",
@@ -5287,7 +5548,7 @@ export const TOOLS: ToolDef[] = [
5287
5548
  defineTool({
5288
5549
  name: "treasury_query_dataset",
5289
5550
  description:
5290
- "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.",
5291
5552
  inputSchema: TreasuryQueryDatasetInput,
5292
5553
  handler: (input) => treasury.queryDataset(input),
5293
5554
  }),
@@ -5506,7 +5767,7 @@ export const TOOLS: ToolDef[] = [
5506
5767
  inputSchema: BlsQcewInput,
5507
5768
  handler: (input) => bls.qcew(input),
5508
5769
  }),
5509
- // ━━━ OpenFEMA — keyless disaster declarations + emergency-assistance spend (2) ━━━ ADR-0016
5770
+ // ━━━ OpenFEMA — keyless disaster declarations + emergency-assistance spend (3) ━━━ ADR-0016
5510
5771
  defineTool({
5511
5772
  name: "fema_search_public_assistance",
5512
5773
  description:
@@ -5521,6 +5782,29 @@ export const TOOLS: ToolDef[] = [
5521
5782
  inputSchema: FemaDisasterDeclarationsInput,
5522
5783
  handler: (input) => fema.disasterDeclarations(input),
5523
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
+ }),
5524
5808
  // ━━━ FPDS-NG — federal contract AWARD ACTIONS (keyless ATOM) (1) ━━━ ADR-0012
5525
5809
  // The FIRST XML/ATOM source (bounded, ReDoS-safe hand-parser — the far.ts/gao.ts
5526
5810
  // lineage; NOT the getJson port). FPDS is the system-of-record USAspending
@@ -5891,6 +6175,13 @@ export const TOOLS: ToolDef[] = [
5891
6175
  inputSchema: OpenfdaDeviceClearancesInput,
5892
6176
  handler: (input) => openfdaDevice.deviceClearances(input),
5893
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
+ }),
5894
6185
  // ━━━ NHTSA vehicle safety (api.nhtsa.gov) — vehicle/parts supplier vetting (2) ━━━ ADR-0057
5895
6186
  // ★KEYLESS — no API key at all (no parameter, no header). The cross-agency
5896
6187
  // product-safety family alongside openFDA (medical). Both tools share
@@ -5925,6 +6216,14 @@ export const TOOLS: ToolDef[] = [
5925
6216
  inputSchema: CpscRecallsInput,
5926
6217
  handler: (input) => cpsc.recalls(input),
5927
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
+ }),
5928
6227
  // ━━━ BEA Regional Economic Accounts (apps.bea.gov) — regional GDP/income (1) ━━━ ADR-0051
5929
6228
  // ★The server's THIRD KEY-REQUIRED source: the BEA Data API has NO keyless tier, so
5930
6229
  // WITHOUT a BEA_API_KEY this tool throws an honest invalid_input config error (the
@@ -6179,6 +6478,28 @@ function synthesizeDefaultMeta(
6179
6478
  return buildMeta({ source, keylessMode, complete: true, truncated: false });
6180
6479
  }
6181
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
+
6182
6503
  export async function runTool(
6183
6504
  name: string,
6184
6505
  args: Record<string, unknown>,
@@ -6192,6 +6513,27 @@ export async function runTool(
6192
6513
  // gone (all 52 tools migrated) — an unknown name has no entry and throws.
6193
6514
  const entry = TOOLS.find((t) => t.name === name);
6194
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
+ }
6195
6537
  const input = entry.inputSchema.parse(args);
6196
6538
  return await entry.handler(input, { sam });
6197
6539
  }
package/src/treasury.ts CHANGED
@@ -83,6 +83,13 @@ export const TREASURY_DATASETS = {
83
83
  mts_table_1: "/v1/accounting/mts/mts_table_1",
84
84
  rates_of_exchange: "/v1/accounting/od/rates_of_exchange",
85
85
  debt_outstanding: "/v2/accounting/od/debt_outstanding",
86
+ // interest_expense = ACTUAL interest PAID on the debt (debt-service cost by
87
+ // security type), distinct from avg_interest_rates (rates only). LIVE-VERIFIED
88
+ // 2026-07-16 (total-count 7245, truthful pagination). tror = the Treasury Report
89
+ // on Receivables — federal receivables + delinquent-debt collections BY AGENCY
90
+ // (debt-collection contracting / agency financial-management signal; total 3953).
91
+ interest_expense: "/v2/accounting/od/interest_expense",
92
+ tror: "/v2/debt/tror",
86
93
  } as const;
87
94
 
88
95
  export type TreasuryDatasetKey = keyof typeof TREASURY_DATASETS;