@cliwant/mcp-sam-gov 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.ja.md +22 -9
  2. package/README.ko.md +22 -9
  3. package/README.md +70 -12
  4. package/dist/bea.d.ts +105 -0
  5. package/dist/bea.d.ts.map +1 -0
  6. package/dist/bea.js +303 -0
  7. package/dist/bea.js.map +1 -0
  8. package/dist/census-economic.d.ts +1 -1
  9. package/dist/census-economic.d.ts.map +1 -1
  10. package/dist/census-economic.js +12 -6
  11. package/dist/census-economic.js.map +1 -1
  12. package/dist/cms-facility.d.ts +112 -0
  13. package/dist/cms-facility.d.ts.map +1 -0
  14. package/dist/cms-facility.js +311 -0
  15. package/dist/cms-facility.js.map +1 -0
  16. package/dist/cms-hospital.d.ts +105 -0
  17. package/dist/cms-hospital.d.ts.map +1 -0
  18. package/dist/cms-hospital.js +290 -0
  19. package/dist/cms-hospital.js.map +1 -0
  20. package/dist/cms-supplier.d.ts +133 -0
  21. package/dist/cms-supplier.d.ts.map +1 -0
  22. package/dist/cms-supplier.js +414 -0
  23. package/dist/cms-supplier.js.map +1 -0
  24. package/dist/cms-utilization.d.ts +113 -0
  25. package/dist/cms-utilization.d.ts.map +1 -0
  26. package/dist/cms-utilization.js +328 -0
  27. package/dist/cms-utilization.js.map +1 -0
  28. package/dist/courtlistener.d.ts +115 -0
  29. package/dist/courtlistener.d.ts.map +1 -0
  30. package/dist/courtlistener.js +398 -0
  31. package/dist/courtlistener.js.map +1 -0
  32. package/dist/cpsc.d.ts +81 -0
  33. package/dist/cpsc.d.ts.map +1 -0
  34. package/dist/cpsc.js +283 -0
  35. package/dist/cpsc.js.map +1 -0
  36. package/dist/dol.d.ts +118 -0
  37. package/dist/dol.d.ts.map +1 -0
  38. package/dist/dol.js +421 -0
  39. package/dist/dol.js.map +1 -0
  40. package/dist/epa-envirofacts.d.ts +97 -0
  41. package/dist/epa-envirofacts.d.ts.map +1 -0
  42. package/dist/epa-envirofacts.js +292 -0
  43. package/dist/epa-envirofacts.js.map +1 -0
  44. package/dist/fred.d.ts +1 -1
  45. package/dist/fred.js +1 -1
  46. package/dist/keys.d.ts +11 -8
  47. package/dist/keys.d.ts.map +1 -1
  48. package/dist/keys.js +55 -8
  49. package/dist/keys.js.map +1 -1
  50. package/dist/lda.d.ts +105 -0
  51. package/dist/lda.d.ts.map +1 -0
  52. package/dist/lda.js +317 -0
  53. package/dist/lda.js.map +1 -0
  54. package/dist/nhtsa.d.ts +91 -0
  55. package/dist/nhtsa.d.ts.map +1 -0
  56. package/dist/nhtsa.js +263 -0
  57. package/dist/nhtsa.js.map +1 -0
  58. package/dist/nonprofit.d.ts +116 -0
  59. package/dist/nonprofit.d.ts.map +1 -0
  60. package/dist/nonprofit.js +342 -0
  61. package/dist/nonprofit.js.map +1 -0
  62. package/dist/openfda-device.d.ts +85 -0
  63. package/dist/openfda-device.d.ts.map +1 -0
  64. package/dist/openfda-device.js +277 -0
  65. package/dist/openfda-device.js.map +1 -0
  66. package/dist/openfda.d.ts +133 -0
  67. package/dist/openfda.d.ts.map +1 -0
  68. package/dist/openfda.js +402 -0
  69. package/dist/openfda.js.map +1 -0
  70. package/dist/server.d.ts.map +1 -1
  71. package/dist/server.js +872 -6
  72. package/dist/server.js.map +1 -1
  73. package/package.json +2 -1
  74. package/src/bea.ts +372 -0
  75. package/src/census-economic.ts +12 -6
  76. package/src/cms-facility.ts +379 -0
  77. package/src/cms-hospital.ts +344 -0
  78. package/src/cms-supplier.ts +527 -0
  79. package/src/cms-utilization.ts +389 -0
  80. package/src/courtlistener.ts +465 -0
  81. package/src/cpsc.ts +333 -0
  82. package/src/dol.ts +515 -0
  83. package/src/epa-envirofacts.ts +342 -0
  84. package/src/fred.ts +1 -1
  85. package/src/keys.ts +60 -8
  86. package/src/lda.ts +385 -0
  87. package/src/nhtsa.ts +352 -0
  88. package/src/nonprofit.ts +460 -0
  89. package/src/openfda-device.ts +356 -0
  90. package/src/openfda.ts +495 -0
  91. package/src/server.ts +995 -6
package/src/server.ts CHANGED
@@ -55,8 +55,18 @@ 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";
64
+ import * as bea from "./bea.js";
59
65
  import * as gsaPerdiem from "./gsa-perdiem.js";
66
+ import * as dol from "./dol.js";
67
+ import * as lda from "./lda.js";
68
+ import * as courtlistener from "./courtlistener.js";
69
+ import * as nonprofit from "./nonprofit.js";
60
70
  import * as fema from "./fema.js";
61
71
  import * as fdic from "./fdic.js";
62
72
  import * as bls from "./bls.js";
@@ -66,6 +76,10 @@ import * as nppes from "./nppes.js";
66
76
  import * as cms from "./cms.js";
67
77
  import * as fac from "./fac.js";
68
78
  import * as usitc from "./usitc.js";
79
+ import * as openfda from "./openfda.js";
80
+ import * as openfdaDevice from "./openfda-device.js";
81
+ import * as nhtsa from "./nhtsa.js";
82
+ import * as cpsc from "./cpsc.js";
69
83
  import { fetchAttachmentText } from "./attachments.js";
70
84
  import * as keys from "./keys.js";
71
85
  import { toToolError, ToolErrorCarrier, errorFromResponse } from "./errors.js";
@@ -81,7 +95,7 @@ import { realpathSync } from "node:fs";
81
95
  const SERVER_NAME = "mcp-sam-gov";
82
96
  // Kept in lockstep with package.json / manifest.json / server.json.
83
97
  // Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
84
- const SERVER_VERSION = "1.2.0";
98
+ const SERVER_VERSION = "1.4.0";
85
99
 
86
100
  // ─── Tool input schemas (Zod) ────────────────────────────────────
87
101
 
@@ -3424,6 +3438,290 @@ const CensusBusinessPatternsInput = z.object({
3424
3438
  ),
3425
3439
  });
3426
3440
 
3441
+ // ─── EPA Envirofacts TRI facilities (ADR-0059) — keyless, PATH-segment SSRF ──
3442
+ // data.epa.gov /efservice/tri_facility. Two requests: a count sub-query for the
3443
+ // EXACT total (P1) + the data slice. All user values ride as PATH SEGMENTS, so each
3444
+ // is charclass-validated + encodeURIComponent-encoded (the load-bearing SSRF guard).
3445
+ const EpaTriFacilitiesInput = z
3446
+ .object({
3447
+ state: z
3448
+ .string()
3449
+ .regex(/^[A-Za-z]{2}$/)
3450
+ .optional()
3451
+ .describe(
3452
+ "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).",
3453
+ ),
3454
+ facilityName: z
3455
+ .string()
3456
+ .min(1)
3457
+ .max(100)
3458
+ .regex(/^[A-Za-z0-9 &.\-]+$/)
3459
+ .optional()
3460
+ .describe(
3461
+ "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).",
3462
+ ),
3463
+ county: z
3464
+ .string()
3465
+ .min(1)
3466
+ .max(100)
3467
+ .regex(/^[A-Za-z0-9 &.\-]+$/)
3468
+ .optional()
3469
+ .describe(
3470
+ "A partial county-name match (→ county_name/CONTAINING/…), e.g. 'FAIRFAX'. Optional additional filter; same charclass as facilityName.",
3471
+ ),
3472
+ limit: z
3473
+ .number()
3474
+ .int()
3475
+ .min(1)
3476
+ .max(100)
3477
+ .optional()
3478
+ .describe("Max facilities to return (1–100, default 25). Offset-paginated."),
3479
+ offset: z
3480
+ .number()
3481
+ .int()
3482
+ .min(0)
3483
+ .optional()
3484
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3485
+ })
3486
+ .refine((v) => v.state !== undefined || v.facilityName !== undefined, {
3487
+ message: "Provide at least `state` or `facilityName` (an all-empty query would scan the whole national TRI table and is refused).",
3488
+ path: ["state"],
3489
+ });
3490
+
3491
+ // ─── CMS Medicare provider-service utilization (ADR-0061) — keyless, two-request ──
3492
+ // data.cms.gov /data-api/v1/dataset/{uuid}. Two requests: a stats count sub-query
3493
+ // for the EXACT total (P1 — found_rows) + the data slice (a bare JSON array). Filter
3494
+ // VALUES ride via URLSearchParams (bracket key + value encoded). REQUIRE npi OR
3495
+ // state (the 9.78M-row table is never scanned unscoped).
3496
+ const CmsMedicareProviderServicesInput = z
3497
+ .object({
3498
+ npi: z
3499
+ .string()
3500
+ .regex(/^\d{10}$/)
3501
+ .optional()
3502
+ .describe(
3503
+ "A 10-digit National Provider Identifier (→ Rndrng_NPI), e.g. '1003000126'. Provide at least this OR `state`. Validated ^\\d{10}$.",
3504
+ ),
3505
+ state: z
3506
+ .string()
3507
+ .regex(/^[A-Za-z]{2}$/)
3508
+ .optional()
3509
+ .describe(
3510
+ "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}$.",
3511
+ ),
3512
+ providerType: z
3513
+ .string()
3514
+ .min(1)
3515
+ .max(100)
3516
+ .regex(/^[A-Za-z0-9 &.,()/'-]+$/)
3517
+ .optional()
3518
+ .describe(
3519
+ "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).",
3520
+ ),
3521
+ hcpcsCode: z
3522
+ .string()
3523
+ .regex(/^[A-Za-z0-9]{1,10}$/)
3524
+ .optional()
3525
+ .describe(
3526
+ "An optional HCPCS/CPT service code filter (→ HCPCS_Cd), e.g. '97110', 'G0463'. Validated ^[A-Za-z0-9]{1,10}$.",
3527
+ ),
3528
+ size: z
3529
+ .number()
3530
+ .int()
3531
+ .min(1)
3532
+ .max(100)
3533
+ .optional()
3534
+ .describe("Max provider-service rows to return (1–100, default 25). Offset-paginated."),
3535
+ offset: z
3536
+ .number()
3537
+ .int()
3538
+ .min(0)
3539
+ .optional()
3540
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3541
+ })
3542
+ .refine((v) => v.npi !== undefined || v.state !== undefined, {
3543
+ 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).",
3544
+ path: ["npi"],
3545
+ });
3546
+
3547
+ // ─── CMS Hospital Compare "Hospital General Information" (ADR-0062) — keyless ──
3548
+ // data.cms.gov /provider-data/api/1/datastore/query/{datasetId}/0. A SINGLE request:
3549
+ // the response's top-level `count` is the EXACT per-filter total (P1). Filters ride
3550
+ // as DKAN conditions[] triples via URLSearchParams (bracket key + value encoded).
3551
+ // REQUIRE state OR facilityName (the ~5,432-hospital table is never scanned unscoped).
3552
+ const CmsHospitalCompareInput = z
3553
+ .object({
3554
+ state: z
3555
+ .string()
3556
+ .regex(/^[A-Za-z]{2}$/)
3557
+ .optional()
3558
+ .describe(
3559
+ "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}$.",
3560
+ ),
3561
+ facilityName: z
3562
+ .string()
3563
+ .min(1)
3564
+ .max(100)
3565
+ .regex(/^[A-Za-z0-9 &.,()/'-]+$/)
3566
+ .optional()
3567
+ .describe(
3568
+ "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).",
3569
+ ),
3570
+ hospitalType: z
3571
+ .string()
3572
+ .min(1)
3573
+ .max(100)
3574
+ .regex(/^[A-Za-z0-9 &.,()/'-]+$/)
3575
+ .optional()
3576
+ .describe(
3577
+ "An optional hospital-type filter (→ hospital_type, case-insensitive SUBSTRING/contains match), e.g. 'Acute', 'Critical Access'. Allowed: letters/digits/space/& . , ( ) / ' - (≤100 chars).",
3578
+ ),
3579
+ size: z
3580
+ .number()
3581
+ .int()
3582
+ .min(1)
3583
+ .max(100)
3584
+ .optional()
3585
+ .describe("Max hospital rows to return (1–100, default 25). Offset-paginated."),
3586
+ offset: z
3587
+ .number()
3588
+ .int()
3589
+ .min(0)
3590
+ .optional()
3591
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3592
+ })
3593
+ .refine((v) => v.state !== undefined || v.facilityName !== undefined, {
3594
+ 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).",
3595
+ path: ["state"],
3596
+ });
3597
+
3598
+ // ─── CMS Facility Directory (data.cms.gov provider-data, ADR-0063) — KEYLESS ──
3599
+ // A four-dataset facility directory generalizing cms_hospital_compare beyond
3600
+ // hospitals. `facilityType` is a Zod ENUM that indexes a MODULE-CONSTANT map to a
3601
+ // VETTED dataset id (nursing_home → 4pq5-n9py, home_health → 6jpm-sxkc, hospice →
3602
+ // yc9t-dgbk, dialysis → 23ew-n7w9) — the user value never enters the URL path. A
3603
+ // SINGLE request: the response's top-level `count` is the EXACT per-filter total
3604
+ // (P1). Filters ride as DKAN conditions[] triples via URLSearchParams. name/address/
3605
+ // ownership columns vary per dataset → coalesced (null if none).
3606
+ const CmsFacilityDirectoryInput = z.object({
3607
+ facilityType: z
3608
+ .enum(["nursing_home", "home_health", "hospice", "dialysis"])
3609
+ .describe(
3610
+ "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).",
3611
+ ),
3612
+ state: z
3613
+ .string()
3614
+ .regex(/^[A-Za-z]{2}$/)
3615
+ .optional()
3616
+ .describe(
3617
+ "An optional 2-letter US state/territory code (→ state, EXACT match), e.g. 'VA', 'TX'. Validated ^[A-Za-z]{2}$.",
3618
+ ),
3619
+ facilityName: z
3620
+ .string()
3621
+ .min(1)
3622
+ .max(100)
3623
+ .regex(/^[A-Za-z0-9 &.,()/'-]+$/)
3624
+ .optional()
3625
+ .describe(
3626
+ "An optional facility-name fragment (case-insensitive SUBSTRING/contains match against the dataset's primary-name column). Allowed: letters/digits/space/& . , ( ) / ' - (≤100 chars).",
3627
+ ),
3628
+ size: z
3629
+ .number()
3630
+ .int()
3631
+ .min(1)
3632
+ .max(100)
3633
+ .optional()
3634
+ .describe("Max facility rows to return (1–100, default 25). Offset-paginated."),
3635
+ offset: z
3636
+ .number()
3637
+ .int()
3638
+ .min(0)
3639
+ .optional()
3640
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3641
+ });
3642
+
3643
+ // ─── CMS DMEPOS by Supplier (data.cms.gov data-API, ADR-0064) — KEYLESS ──
3644
+ // SAME host/endpoint/two-request-stats-count pattern as cms_medicare_provider_services.
3645
+ // REQUIRE npi OR state (the supplier table is never scanned unscoped). Filter VALUES
3646
+ // ride as URLSearchParams filter[Col]=Val (bracket key + value encoded — the SSRF guard).
3647
+ const CmsDmeposSuppliersInput = z
3648
+ .object({
3649
+ npi: z
3650
+ .string()
3651
+ .regex(/^\d{10}$/)
3652
+ .optional()
3653
+ .describe(
3654
+ "A 10-digit supplier National Provider Identifier (→ Suplr_NPI), e.g. '1003000126'. Provide at least this OR `state`. Validated ^\\d{10}$.",
3655
+ ),
3656
+ state: z
3657
+ .string()
3658
+ .regex(/^[A-Za-z]{2}$/)
3659
+ .optional()
3660
+ .describe(
3661
+ "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}$.",
3662
+ ),
3663
+ size: z
3664
+ .number()
3665
+ .int()
3666
+ .min(1)
3667
+ .max(100)
3668
+ .optional()
3669
+ .describe("Max supplier rows to return (1–100, default 25). Offset-paginated."),
3670
+ offset: z
3671
+ .number()
3672
+ .int()
3673
+ .min(0)
3674
+ .optional()
3675
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3676
+ })
3677
+ .refine((v) => v.npi !== undefined || v.state !== undefined, {
3678
+ message: "Provide at least `npi` or `state` (an all-empty query would scan the entire DMEPOS supplier table and is refused).",
3679
+ path: ["npi"],
3680
+ });
3681
+
3682
+ // ─── CMS Revoked Medicare Providers & Suppliers (data.cms.gov data-API, ADR-0064) ──
3683
+ // KEYLESS. A legally-published revocation/exclusion register (~7,059 rows) — the same
3684
+ // vetting class as the OFAC / SAM exclusion lists. ALL filters optional (small table —
3685
+ // pagination is fine unfiltered). SAME two-request stats-count P1 pattern.
3686
+ const CmsRevokedProvidersInput = z.object({
3687
+ npi: z
3688
+ .string()
3689
+ .regex(/^\d{10}$/)
3690
+ .optional()
3691
+ .describe(
3692
+ "An optional 10-digit National Provider Identifier (→ NPI), e.g. '1003000126'. Validated ^\\d{10}$.",
3693
+ ),
3694
+ state: z
3695
+ .string()
3696
+ .regex(/^[A-Za-z]{2}$/)
3697
+ .optional()
3698
+ .describe(
3699
+ "An optional 2-letter US state/territory code (→ STATE_CD, EXACT match), e.g. 'FL', 'CA'. Validated ^[A-Za-z]{2}$.",
3700
+ ),
3701
+ lastName: z
3702
+ .string()
3703
+ .min(1)
3704
+ .max(100)
3705
+ .regex(/^[A-Za-z0-9 .,'-]+$/)
3706
+ .optional()
3707
+ .describe(
3708
+ "An optional last-name filter (→ LAST_NAME, EXACT match). Allowed: letters/digits/space/. , ' - (≤100 chars).",
3709
+ ),
3710
+ size: z
3711
+ .number()
3712
+ .int()
3713
+ .min(1)
3714
+ .max(100)
3715
+ .optional()
3716
+ .describe("Max revocation rows to return (1–100, default 25). Offset-paginated."),
3717
+ offset: z
3718
+ .number()
3719
+ .int()
3720
+ .min(0)
3721
+ .optional()
3722
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3723
+ });
3724
+
3427
3725
  // ─── FRED (Federal Reserve Economic Data) — the SECOND key-required source ──
3428
3726
  // ADR-0048. Macro context (GDP/CPI/rates/unemployment/PPI). REQUIRES a free
3429
3727
  // FRED_API_KEY; without it both tools throw an honest config error (the other 112
@@ -3486,6 +3784,239 @@ const FredSeriesObservationsInput = z.object({
3486
3784
  .describe("Observation date order: 'asc' (oldest first, FRED default) or 'desc' (newest first)."),
3487
3785
  });
3488
3786
 
3787
+ // ─── openFDA recall/enforcement (api.fda.gov) — KEYLESS + OPTIONAL rate-limit key ──
3788
+ // ADR-0054. Drug/device/food recall enforcement records. Structured filters ONLY
3789
+ // (no raw Lucene passthrough — injection-safe); the tool assembles the openFDA
3790
+ // `search=` string with proper escaping. totalAvailable = meta.results.total (P1);
3791
+ // a no-match query (openFDA HTTP 404 NOT_FOUND) ⇒ an honest empty (P2). An OPTIONAL
3792
+ // OPENFDA_API_KEY only raises the rate limit (keyless works ~1000/day).
3793
+ const OpenfdaEnforcementInput = z.object({
3794
+ category: z
3795
+ .enum(["drug", "device", "food"])
3796
+ .optional()
3797
+ .describe(
3798
+ "The recall category (default 'drug'): 'drug', 'device', or 'food'. Selects the openFDA /{category}/enforcement endpoint.",
3799
+ ),
3800
+ firm: z
3801
+ .string()
3802
+ .min(1)
3803
+ .optional()
3804
+ .describe(
3805
+ "Recalling firm name filter (→ recalling_firm), e.g. 'pfizer'. Matched as an escaped Lucene phrase.",
3806
+ ),
3807
+ product: z
3808
+ .string()
3809
+ .min(1)
3810
+ .optional()
3811
+ .describe(
3812
+ "Product description filter (→ product_description), e.g. 'insulin'. Matched as an escaped Lucene phrase.",
3813
+ ),
3814
+ reason: z
3815
+ .string()
3816
+ .min(1)
3817
+ .optional()
3818
+ .describe(
3819
+ "Reason-for-recall filter (→ reason_for_recall), e.g. 'contamination'. Matched as an escaped Lucene phrase.",
3820
+ ),
3821
+ classification: z
3822
+ .enum(["Class I", "Class II", "Class III"])
3823
+ .optional()
3824
+ .describe(
3825
+ "FDA recall classification filter: 'Class I' (most serious), 'Class II', or 'Class III'.",
3826
+ ),
3827
+ status: z
3828
+ .string()
3829
+ .min(1)
3830
+ .optional()
3831
+ .describe(
3832
+ "Recall status filter (→ status), e.g. 'Ongoing', 'Terminated', 'Completed'.",
3833
+ ),
3834
+ state: z
3835
+ .string()
3836
+ .regex(/^[A-Za-z]{2}$/)
3837
+ .optional()
3838
+ .describe(
3839
+ "2-letter US state/territory postal code filter (→ state), e.g. 'CA'. Validated ^[A-Za-z]{2}$.",
3840
+ ),
3841
+ limit: z
3842
+ .number()
3843
+ .int()
3844
+ .min(1)
3845
+ .max(100)
3846
+ .optional()
3847
+ .describe("Max recall records to return (default 25, max 100). Offset-paginated via skip."),
3848
+ skip: z
3849
+ .number()
3850
+ .int()
3851
+ .min(0)
3852
+ .optional()
3853
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3854
+ });
3855
+
3856
+ // ─── openFDA 510(k) device clearances (api.fda.gov) — KEYLESS + OPTIONAL rate-limit key ──
3857
+ // ADR-0056. FDA premarket-notification (510(k)) device clearances — SAME source/envelope/
3858
+ // crux as openfda_enforcement (structured filters ONLY — the tool assembles + escapes the
3859
+ // search= string, injection-safe). totalAvailable = meta.results.total (P1); a no-match
3860
+ // query (openFDA HTTP 404 NOT_FOUND) ⇒ an honest empty (P2). An OPTIONAL OPENFDA_API_KEY
3861
+ // only raises the rate limit (keyless works ~1000/day).
3862
+ const OpenfdaDeviceClearancesInput = z.object({
3863
+ applicant: z
3864
+ .string()
3865
+ .min(1)
3866
+ .optional()
3867
+ .describe(
3868
+ "Applicant / manufacturer name filter (→ applicant), e.g. 'medtronic'. Matched as an escaped Lucene phrase.",
3869
+ ),
3870
+ deviceName: z
3871
+ .string()
3872
+ .min(1)
3873
+ .optional()
3874
+ .describe(
3875
+ "Device name filter (→ device_name), e.g. 'catheter'. Matched as an escaped Lucene phrase.",
3876
+ ),
3877
+ productCode: z
3878
+ .string()
3879
+ .min(1)
3880
+ .optional()
3881
+ .describe(
3882
+ "FDA product code filter (→ product_code), e.g. 'DXN'. Matched as an escaped Lucene phrase.",
3883
+ ),
3884
+ clearanceType: z
3885
+ .string()
3886
+ .min(1)
3887
+ .optional()
3888
+ .describe(
3889
+ "510(k) clearance type filter (→ clearance_type), e.g. 'Traditional', 'Special', 'Abbreviated'. Matched as an escaped Lucene phrase.",
3890
+ ),
3891
+ kNumber: z
3892
+ .string()
3893
+ .min(1)
3894
+ .optional()
3895
+ .describe(
3896
+ "510(k) clearance number (K-number) filter (→ k_number), e.g. 'K123456'. Matched as an escaped Lucene phrase.",
3897
+ ),
3898
+ state: z
3899
+ .string()
3900
+ .regex(/^[A-Za-z]{2}$/)
3901
+ .optional()
3902
+ .describe(
3903
+ "2-letter US state/territory postal code filter (→ state), e.g. 'CA'. Validated ^[A-Za-z]{2}$.",
3904
+ ),
3905
+ limit: z
3906
+ .number()
3907
+ .int()
3908
+ .min(1)
3909
+ .max(100)
3910
+ .optional()
3911
+ .describe("Max clearance records to return (default 25, max 100). Offset-paginated via skip."),
3912
+ skip: z
3913
+ .number()
3914
+ .int()
3915
+ .min(0)
3916
+ .optional()
3917
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3918
+ });
3919
+
3920
+ // ─── NHTSA vehicle safety (api.nhtsa.gov) — KEYLESS vehicle/parts supplier vetting ──
3921
+ // ADR-0057. Two tools (recalls + complaints) share make/model/modelYear inputs. NO
3922
+ // API key at all. ★The complaints VIN (PII) is excluded from the output. modelYear is
3923
+ // ^\d{4}$; make/model are letters/digits/space/hyphen only (SSRF/injection guard).
3924
+ const NhtsaVehicleInput = z.object({
3925
+ make: z
3926
+ .string()
3927
+ .regex(/^[A-Za-z0-9 -]+$/)
3928
+ .describe(
3929
+ "Vehicle make (required), e.g. 'honda', 'ford'. Letters/digits/space/hyphen only (^[A-Za-z0-9 -]+$).",
3930
+ ),
3931
+ model: z
3932
+ .string()
3933
+ .regex(/^[A-Za-z0-9 -]+$/)
3934
+ .describe(
3935
+ "Vehicle model (required), e.g. 'accord', 'f-150'. Letters/digits/space/hyphen only (^[A-Za-z0-9 -]+$).",
3936
+ ),
3937
+ modelYear: z
3938
+ .string()
3939
+ .regex(/^\d{4}$/)
3940
+ .describe("4-digit model year (required), e.g. '2020'. Validated ^\\d{4}$."),
3941
+ });
3942
+
3943
+ // ─── CPSC consumer-product recalls (www.saferproducts.gov) — KEYLESS goods/import vetting ──
3944
+ // ADR-0058. One tool. NO API key at all. The response is a bare JSON ARRAY with no
3945
+ // total-count field / no pagination (totalAvailable = the returned count). All filters
3946
+ // optional; with NO filter the tool defaults RecallDateStart to ~90 days ago (disclosed)
3947
+ // rather than fetch the whole dataset. dates are ^\d{4}-\d{2}-\d{2}$; recallNumber is
3948
+ // letters/digits/hyphen only (SSRF/injection guard).
3949
+ const CpscRecallsInput = z.object({
3950
+ dateStart: z
3951
+ .string()
3952
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
3953
+ .optional()
3954
+ .describe(
3955
+ "Recall date range START (optional), YYYY-MM-DD, e.g. '2025-01-01' (→ RecallDateStart). Validated ^\\d{4}-\\d{2}-\\d{2}$.",
3956
+ ),
3957
+ dateEnd: z
3958
+ .string()
3959
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
3960
+ .optional()
3961
+ .describe(
3962
+ "Recall date range END (optional), YYYY-MM-DD, e.g. '2025-01-31' (→ RecallDateEnd). Validated ^\\d{4}-\\d{2}-\\d{2}$.",
3963
+ ),
3964
+ productName: z
3965
+ .string()
3966
+ .optional()
3967
+ .describe("Product name substring filter (optional), e.g. 'helmet' (→ ProductName)."),
3968
+ manufacturer: z
3969
+ .string()
3970
+ .optional()
3971
+ .describe("Manufacturer name substring filter (optional) (→ Manufacturer)."),
3972
+ recallNumber: z
3973
+ .string()
3974
+ .regex(/^[A-Za-z0-9-]+$/)
3975
+ .optional()
3976
+ .describe(
3977
+ "A specific CPSC recall number (optional), e.g. '25088' (→ RecallNumber). Letters/digits/hyphen only (^[A-Za-z0-9-]+$).",
3978
+ ),
3979
+ });
3980
+
3981
+ // ─── BEA Regional Economic Accounts (apps.bea.gov) — the THIRD key-required source ──
3982
+ // ADR-0051. County/state/MSA GDP-by-industry (CAGDP2/SAGDP2N) + personal income
3983
+ // (CAINC1/SAINC1) — the regional/sub-national place-of-performance lane. REQUIRES a
3984
+ // free BEA_API_KEY; without it the tool throws an honest config error (the other 116
3985
+ // tools stay keyless). ★A missing/invalid key returns HTTP 200 with a
3986
+ // BEAAPI.Results.Error carrier (NOT an HTTP error), detected pre-drift. The key rides
3987
+ // UserID= ONLY. DataValue is a comma string; suppression codes ((NA)/(D)/…) → null.
3988
+ const BeaRegionalDataInput = z.object({
3989
+ tableName: z
3990
+ .string()
3991
+ .regex(/^[A-Za-z0-9]{2,20}$/)
3992
+ .describe(
3993
+ "A BEA Regional table code (2–20 alphanumerics), e.g. 'CAGDP2' (county GDP by industry), 'SAGDP2N' (state GDP by industry), 'CAINC1'/'SAINC1' (personal income). Validated ^[A-Za-z0-9]{2,20}$. Required.",
3994
+ ),
3995
+ geoFips: z
3996
+ .string()
3997
+ .regex(/^[A-Za-z0-9]{2,10}$/)
3998
+ .describe(
3999
+ "The BEA GeoFips selector: 'STATE' (all states), a county FIPS like '06075', or an MSA code. Validated ^[A-Za-z0-9]{2,10}$. Required.",
4000
+ ),
4001
+ lineCode: z
4002
+ .string()
4003
+ .regex(/^([0-9]{1,4}|ALL)$/)
4004
+ .describe(
4005
+ "The industry/statistic line code — an integer (1–4 digits), e.g. '1', or 'ALL' for every line in the table. Validated ^([0-9]{1,4}|ALL)$. Required.",
4006
+ ),
4007
+ year: z
4008
+ .string()
4009
+ .regex(/^(\d{4}|LAST5|ALL)$/)
4010
+ .optional()
4011
+ .describe(
4012
+ "The data year: a 4-digit year (e.g. '2022'), 'LAST5' (the latest 5 years, default), or 'ALL'. Validated ^(\\d{4}|LAST5|ALL)$.",
4013
+ ),
4014
+ frequency: z
4015
+ .enum(["A", "Q"])
4016
+ .optional()
4017
+ .describe("Data frequency: 'A' (annual, default) or 'Q' (quarterly)."),
4018
+ });
4019
+
3489
4020
  // ─── GSA Federal Travel Per-Diem (api.gsa.gov) — travel-cost lane ──
3490
4021
  // ADR-0050. Lodging + M&IE reimbursement ceilings by city/state OR zip for a year.
3491
4022
  // KEYLESS by default via the shared DEMO_KEY (datagovKey.ts seam); DATA_GOV_API_KEY
@@ -3525,6 +4056,234 @@ const GsaPerdiemRatesInput = z
3525
4056
  "Look up GSA per-diem rates by EITHER (city + state) OR zip. Supplying both, or neither, ⇒ invalid_input.",
3526
4057
  );
3527
4058
 
4059
+ // ─── US DOL Data API v4 (apiprod.dol.gov) — the labor-enforcement lane ──
4060
+ // ADR-0053. A DELIBERATE key split: dol_list_datasets (the CATALOG) is KEYLESS;
4061
+ // dol_get_dataset (the DATA endpoint) is the 4th REQUIRED key (DOL_API_KEY, no keyless
4062
+ // tier — throws pre-fetch without it). The key rides the X-API-KEY HEADER ONLY. The
4063
+ // data envelope is key-gated/unverified ⇒ records are surfaced verbatim + totalAvailable
4064
+ // defaults null (never `returned` faked as the total). agency/query filter is CLIENT-SIDE.
4065
+ const DolListDatasetsInput = z.object({
4066
+ agency: z
4067
+ .string()
4068
+ .min(1)
4069
+ .max(100)
4070
+ .optional()
4071
+ .describe(
4072
+ "CLIENT-SIDE filter by agency abbreviation (e.g. 'WHD', 'OSHA', 'ILAB', 'ETA') or a substring of the agency name. The DOL catalog API does not filter server-side, so this is applied to the fetched catalog.",
4073
+ ),
4074
+ query: z
4075
+ .string()
4076
+ .min(1)
4077
+ .max(200)
4078
+ .optional()
4079
+ .describe(
4080
+ "CLIENT-SIDE free-text filter (substring over dataset name / description / category / table / endpoint), e.g. 'child labor', 'wage', 'inspection'.",
4081
+ ),
4082
+ limit: z
4083
+ .number()
4084
+ .int()
4085
+ .min(1)
4086
+ .max(200)
4087
+ .optional()
4088
+ .describe("Datasets to return per page (default 25, max 200). Offset-paginated over the (filtered) catalog."),
4089
+ offset: z
4090
+ .number()
4091
+ .int()
4092
+ .min(0)
4093
+ .optional()
4094
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
4095
+ });
4096
+
4097
+ const DolGetDatasetInput = z.object({
4098
+ agency: z
4099
+ .string()
4100
+ .regex(/^[A-Za-z0-9_]+$/)
4101
+ .describe(
4102
+ "The agency abbreviation (the `agencyAbbr` from dol_list_datasets), e.g. 'WHD', 'OSHA', 'ILAB'. Rides in the request PATH. Validated ^[A-Za-z0-9_]+$. Required.",
4103
+ ),
4104
+ table: z
4105
+ .string()
4106
+ .regex(/^[A-Za-z0-9_]+$/)
4107
+ .describe(
4108
+ "The dataset endpoint — the `apiUrl` field from dol_list_datasets (the DOL 'api_url', NOT the tablename), e.g. 'Child_Labor_Report__2016_to_2022'. Rides in the request PATH. Validated ^[A-Za-z0-9_]+$. Required.",
4109
+ ),
4110
+ limit: z
4111
+ .number()
4112
+ .int()
4113
+ .min(1)
4114
+ .max(100)
4115
+ .optional()
4116
+ .describe("Max records to return (default 10, max 100). Offset-paginated."),
4117
+ offset: z
4118
+ .number()
4119
+ .int()
4120
+ .min(0)
4121
+ .optional()
4122
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
4123
+ filterField: z
4124
+ .string()
4125
+ .min(1)
4126
+ .max(100)
4127
+ .optional()
4128
+ .describe("Optional: a dataset field name to filter on (paired with filterValue → a DOL filter_object equality filter). Supply BOTH or NEITHER."),
4129
+ filterValue: z
4130
+ .string()
4131
+ .min(1)
4132
+ .max(200)
4133
+ .optional()
4134
+ .describe("Optional: the value the filterField must equal. Supply BOTH filterField and filterValue, or NEITHER."),
4135
+ fields: z
4136
+ .array(z.string().min(1))
4137
+ .optional()
4138
+ .describe("Optional: best-effort column selection (a subset of field names to return). Not documented for v4; the API ignores or 400s an unsupported selection (surfaced honestly)."),
4139
+ });
4140
+
4141
+ // ─── US Senate LDA lobbying filings (lda.senate.gov) — the lobbying/B2G lane ──
4142
+ // ADR-0052. Who is paid HOW MUCH to lobby WHICH federal agency on WHICH issue.
4143
+ // KEYLESS (anonymous 200); an optional free LDA_API_KEY only raises the rate limit
4144
+ // and rides the Authorization: Token … header ONLY. `count` is the REAL total
4145
+ // (~1.95M) — never results.length; page-based pagination (page/pageSize ≤25). All
4146
+ // filter VALUES ride URLSearchParams; filingYear/page/pageSize charclass/range-guarded.
4147
+ const LdaSearchFilingsInput = z.object({
4148
+ registrantName: z
4149
+ .string()
4150
+ .min(1)
4151
+ .optional()
4152
+ .describe("Filter by the registrant (the lobbying firm / in-house filer) name, e.g. 'Akin Gump'. Substring match, upstream-validated."),
4153
+ clientName: z
4154
+ .string()
4155
+ .min(1)
4156
+ .optional()
4157
+ .describe("Filter by the client name (who the lobbying is FOR), e.g. 'Google'. Substring match, upstream-validated."),
4158
+ lobbyistName: z
4159
+ .string()
4160
+ .min(1)
4161
+ .optional()
4162
+ .describe("Filter by an individual lobbyist's name. Substring match, upstream-validated."),
4163
+ filingYear: z
4164
+ .string()
4165
+ .regex(/^\d{4}$/)
4166
+ .optional()
4167
+ .describe("Filter by filing year, a 4-digit year (e.g. '2024'). Validated ^\\d{4}$."),
4168
+ filingType: z
4169
+ .string()
4170
+ .min(1)
4171
+ .optional()
4172
+ .describe("Filter by the filing type short code (e.g. 'Q1' Q1 report, 'RR' registration, 'YE' year-end). A bad code ⇒ upstream HTTP 400 ⇒ invalid_input (surfaced)."),
4173
+ agency: z
4174
+ .string()
4175
+ .min(1)
4176
+ .optional()
4177
+ .describe("Filter by the federal government entity lobbied (maps to government_entity — the B2G signal), e.g. 'DEPARTMENT OF DEFENSE'."),
4178
+ issue: z
4179
+ .string()
4180
+ .min(1)
4181
+ .optional()
4182
+ .describe("Filter by the specific lobbying issues text (maps to filing_specific_lobbying_issues), e.g. 'appropriations'."),
4183
+ page: z
4184
+ .number()
4185
+ .int()
4186
+ .min(1)
4187
+ .default(1)
4188
+ .describe("1-based page number (default 1). Page with the next page number from _meta.notes / when _meta.pagination.hasMore."),
4189
+ pageSize: z
4190
+ .number()
4191
+ .int()
4192
+ .min(1)
4193
+ .max(25)
4194
+ .default(25)
4195
+ .describe("Filings per page, 1..25 (the LDA API caps at 25), default 25."),
4196
+ });
4197
+
4198
+ // ─── US federal court opinions (www.courtlistener.com) — the litigation lane ──
4199
+ // ADR-0055. Federal court decisions (opinions) — the judicial signal no contract/
4200
+ // spending/lobbying source carries (e.g. uscfc bid-protest / contract-claim opinions).
4201
+ // ★PROVENANCE: CourtListener (Free Law Project, a non-profit), NOT a .gov API —
4202
+ // PACER (the .gov source) is paywalled. KEYLESS (anonymous 200); an optional free
4203
+ // COURTLISTENER_API_TOKEN only raises the rate limit, riding the Authorization: Token
4204
+ // … header ONLY (the lda/socrata app-token lineage). `count` is the REAL total —
4205
+ // never results.length; CURSOR pagination (nextCursor extracted from `next`). court/
4206
+ // dates charclass-guarded; all filter VALUES ride URLSearchParams; type=o is FIXED.
4207
+ const CourtlistenerSearchOpinionsInput = z.object({
4208
+ query: z
4209
+ .string()
4210
+ .min(1)
4211
+ .optional()
4212
+ .describe("Full-text query (maps to q), e.g. 'bid protest' or a party name. Matches across the opinion text/metadata."),
4213
+ court: z
4214
+ .string()
4215
+ .regex(/^[a-z0-9]+$/)
4216
+ .optional()
4217
+ .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'."),
4218
+ dateFiledAfter: z
4219
+ .string()
4220
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
4221
+ .optional()
4222
+ .describe("Only opinions filed on/after this ISO date (→ filed_after), e.g. '2020-01-01'. Validated ^\\d{4}-\\d{2}-\\d{2}$."),
4223
+ dateFiledBefore: z
4224
+ .string()
4225
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
4226
+ .optional()
4227
+ .describe("Only opinions filed on/before this ISO date (→ filed_before), e.g. '2024-12-31'. Validated ^\\d{4}-\\d{2}-\\d{2}$."),
4228
+ natureOfSuit: z
4229
+ .string()
4230
+ .min(1)
4231
+ .optional()
4232
+ .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)."),
4233
+ cursor: z
4234
+ .string()
4235
+ .min(1)
4236
+ .optional()
4237
+ .describe("Opaque continuation token for the NEXT page — pass back the _meta.nextCursor from the previous response (CourtListener uses CURSOR pagination, not page/offset)."),
4238
+ order: z
4239
+ .string()
4240
+ .min(1)
4241
+ .default("dateFiled desc")
4242
+ .describe("Sort order (maps to order_by), default 'dateFiled desc' (most recent first). E.g. 'dateFiled asc', 'score desc'."),
4243
+ });
4244
+
4245
+ // ─── US tax-exempt nonprofits (projects.propublica.org) — the nonprofit lane ──
4246
+ // ADR-0060. IRS Form 990 public records republished KEYLESS by ProPublica Nonprofit
4247
+ // Explorer (a non-profit newsroom) — NOT a .gov API (the IRS has no clean query
4248
+ // API). ★PROVENANCE disclosed in _meta.source + a note. KEYLESS (no key of any
4249
+ // kind). search: q/state[id]/ntee[id]/page (0-based); total_results is the REAL
4250
+ // total — never organizations.length. All VALUES ride URLSearchParams (incl. the
4251
+ // bracket keys); state/ntee charclass/range-guarded.
4252
+ const NonprofitSearchInput = z.object({
4253
+ query: z
4254
+ .string()
4255
+ .min(1)
4256
+ .optional()
4257
+ .describe("Full-text query (maps to q) — an organization name or keyword, e.g. 'american red cross'. Matches across the org name/metadata."),
4258
+ state: z
4259
+ .string()
4260
+ .regex(/^[A-Za-z]{2}$/)
4261
+ .optional()
4262
+ .describe("Filter by a 2-letter US state/territory code (maps to state[id]), e.g. 'VA'. Validated ^[A-Za-z]{2}$."),
4263
+ ntee: z
4264
+ .number()
4265
+ .int()
4266
+ .min(1)
4267
+ .max(10)
4268
+ .optional()
4269
+ .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)."),
4270
+ page: z
4271
+ .number()
4272
+ .int()
4273
+ .min(0)
4274
+ .default(0)
4275
+ .describe("0-BASED page number (default 0). Page with cur_page+1 from _meta.notes / when _meta.pagination.hasMore."),
4276
+ });
4277
+
4278
+ // nonprofit_financials — one org's Form 990 profile + financials by EIN. KEYLESS.
4279
+ // ein rides the URL PATH ⇒ digits-only ^\d{1,9}$. An unknown EIN (404) ⇒ not_found.
4280
+ const NonprofitFinancialsInput = z.object({
4281
+ ein: z
4282
+ .string()
4283
+ .regex(/^\d{1,9}$/)
4284
+ .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."),
4285
+ });
4286
+
3528
4287
  // api_key_status takes no input — it is a pure status query over process.env.
3529
4288
  const ApiKeyStatusInput = z.object({});
3530
4289
 
@@ -4993,17 +5752,97 @@ export const TOOLS: ToolDef[] = [
4993
5752
  // ━━━ US Census County Business Patterns — market sizing (1) ━━━ ADR-0047
4994
5753
  // ★The server's FIRST KEY-REQUIRED source: the Census Data API removed its
4995
5754
  // keyless tier, so WITHOUT a CENSUS_API_KEY this tool throws an honest
4996
- // invalid_input config error (the other 111 tools stay keyless). NAICS×geography
5755
+ // invalid_input config error (most other tools are keyless — see api_key_status). NAICS×geography
4997
5756
  // establishments / employment / annual payroll — the demand-side market-sizing
4998
5757
  // lane. Census negative suppression sentinels (-999999999 …) map to null (never
4999
5758
  // a negative number / never 0). The 2D-array body is parsed by header name.
5000
5759
  defineTool({
5001
5760
  name: "census_business_patterns",
5002
5761
  description:
5003
- "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.",
5762
+ "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.",
5004
5763
  inputSchema: CensusBusinessPatternsInput,
5005
5764
  handler: (input) => censusEconomic.businessPatterns(input),
5006
5765
  }),
5766
+ // ━━━ EPA Envirofacts TRI facilities — environmental footprint (1) ━━━ ADR-0059
5767
+ // KEYLESS (data.epa.gov /efservice/tri_facility). ★Two requests: a count
5768
+ // sub-query yields the EXACT total (P1 — TOTALQUERYRESULTS, e.g. VA=1247), then
5769
+ // the data slice. All user values ride as PATH SEGMENTS → each is
5770
+ // charclass-validated + encodeURIComponent-encoded (the load-bearing SSRF guard).
5771
+ defineTool({
5772
+ name: "epa_tri_facilities",
5773
+ description:
5774
+ "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.",
5775
+ inputSchema: EpaTriFacilitiesInput,
5776
+ handler: (input) => epaEnvirofacts.triFacilities(input),
5777
+ }),
5778
+ // ━━━ CMS Medicare provider-service utilization — healthcare market (1) ━━━ ADR-0061
5779
+ // KEYLESS (data.cms.gov /data-api/v1/dataset/{uuid}). ★Two requests: a stats
5780
+ // count sub-query yields the EXACT per-filter total (P1 — found_rows, e.g.
5781
+ // VA=278254), then the data slice (a bare JSON array). All filter VALUES ride via
5782
+ // URLSearchParams (bracket key + value encoded — the SSRF guard). REQUIRE npi OR
5783
+ // state (the 9.78M-row table is never scanned unscoped). The dataset UUID is a
5784
+ // SPECIFIC ANNUAL VINTAGE (surfaced in a _meta note; update yearly).
5785
+ defineTool({
5786
+ name: "cms_medicare_provider_services",
5787
+ description:
5788
+ "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.",
5789
+ inputSchema: CmsMedicareProviderServicesInput,
5790
+ handler: (input) => cmsUtilization.providerServices(input),
5791
+ }),
5792
+ // ━━━ CMS Hospital Compare — Hospital General Information (1) ━━━ ADR-0062
5793
+ // KEYLESS (data.cms.gov /provider-data/api/1/datastore/query/{datasetId}). A
5794
+ // SINGLE request: the response's top-level `count` is the EXACT per-filter total
5795
+ // (P1 — VA=96), never the slice length. Filters ride as DKAN conditions[] triples
5796
+ // via URLSearchParams (bracket key + value encoded — the SSRF guard): state is an
5797
+ // EXACT match, facilityName/hospitalType are case-insensitive substring matches,
5798
+ // AND-combined server-side. REQUIRE state OR facilityName (never scanned unscoped).
5799
+ defineTool({
5800
+ name: "cms_hospital_compare",
5801
+ description:
5802
+ "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.",
5803
+ inputSchema: CmsHospitalCompareInput,
5804
+ handler: (input) => cmsHospital.hospitalCompare(input),
5805
+ }),
5806
+ // ━━━ CMS Facility Directory — 4 provider-data datasets (1) ━━━ ADR-0063
5807
+ // KEYLESS (data.cms.gov /provider-data/api/1/datastore/query/{datasetId}). A
5808
+ // generalization of cms_hospital_compare beyond hospitals: facilityType (a Zod
5809
+ // enum) indexes a CONSTANT map to a VETTED dataset id — the user value never enters
5810
+ // the path (the load-bearing SSRF guard). A SINGLE request: the response's top-level
5811
+ // `count` is the EXACT per-filter total (P1). name/address/ownership columns vary
5812
+ // per dataset → coalesced (null if none — never empty-string, never fabricated).
5813
+ defineTool({
5814
+ name: "cms_facility_directory",
5815
+ description:
5816
+ "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.",
5817
+ inputSchema: CmsFacilityDirectoryInput,
5818
+ handler: (input) => cmsFacility.facilityDirectory(input),
5819
+ }),
5820
+ // ━━━ CMS DMEPOS by Supplier — supply-side utilization (1) ━━━ ADR-0064
5821
+ // KEYLESS (data.cms.gov /data-api/v1/dataset/{uuid}). ★Two requests: a stats count
5822
+ // sub-query yields the EXACT per-filter total (P1 — found_rows), then the data slice
5823
+ // (a bare JSON array). All filter VALUES ride via URLSearchParams (bracket key +
5824
+ // value encoded — the SSRF guard). REQUIRE npi OR state (the supplier table is never
5825
+ // scanned unscoped). The dataset UUID is a SPECIFIC ANNUAL VINTAGE (update yearly).
5826
+ defineTool({
5827
+ name: "cms_dmepos_suppliers",
5828
+ description:
5829
+ "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.",
5830
+ inputSchema: CmsDmeposSuppliersInput,
5831
+ handler: (input) => cmsSupplier.dmeposSuppliers(input),
5832
+ }),
5833
+ // ━━━ CMS Revoked Medicare Providers & Suppliers — vetting/exclusion list (1) ━━━ ADR-0064
5834
+ // KEYLESS (data.cms.gov /data-api/v1/dataset/{uuid}). A legally-published
5835
+ // revocation/exclusion register (~7,059 rows) — the SAME vetting class as the OFAC /
5836
+ // SAM exclusion lists already shipped (surfacing the names IS the point). ALL filters
5837
+ // optional (small table — pagination is fine unfiltered). SAME two-request stats-count
5838
+ // P1 pattern; filter VALUES ride via URLSearchParams (bracket key + value encoded).
5839
+ defineTool({
5840
+ name: "cms_revoked_providers",
5841
+ description:
5842
+ "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.",
5843
+ inputSchema: CmsRevokedProvidersInput,
5844
+ handler: (input) => cmsSupplier.revokedProviders(input),
5845
+ }),
5007
5846
  // ━━━ FRED (Federal Reserve Economic Data) — macro context (2) ━━━ ADR-0048
5008
5847
  // ★The server's SECOND KEY-REQUIRED source: FRED has NO keyless tier, so WITHOUT
5009
5848
  // a FRED_API_KEY both tools throw an honest invalid_input config error (the other
@@ -5012,7 +5851,7 @@ export const TOOLS: ToolDef[] = [
5012
5851
  defineTool({
5013
5852
  name: "fred_search_series",
5014
5853
  description:
5015
- "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.",
5854
+ "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.",
5016
5855
  inputSchema: FredSearchSeriesInput,
5017
5856
  handler: (input) => fred.searchSeries(input),
5018
5857
  }),
@@ -5023,6 +5862,84 @@ export const TOOLS: ToolDef[] = [
5023
5862
  inputSchema: FredSeriesObservationsInput,
5024
5863
  handler: (input) => fred.seriesObservations(input),
5025
5864
  }),
5865
+ // ━━━ openFDA recall/enforcement (api.fda.gov) — product-safety recalls (1) ━━━ ADR-0054
5866
+ // KEYLESS with an OPTIONAL OPENFDA_API_KEY (raises the rate limit; keyless works
5867
+ // ~1000/day — it NEVER throws for a missing key, unlike the key-REQUIRED sources).
5868
+ // Structured filters ONLY (no raw Lucene passthrough — the tool assembles + escapes
5869
+ // the search= string, injection-safe). ★P1: totalAvailable = meta.results.total
5870
+ // (EXACT). ★P2 crux: a no-match query returns openFDA HTTP 404 NOT_FOUND ⇒ an honest
5871
+ // empty, never a throw. The optional key rides &api_key= ONLY.
5872
+ defineTool({
5873
+ name: "openfda_enforcement",
5874
+ description:
5875
+ "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.",
5876
+ inputSchema: OpenfdaEnforcementInput,
5877
+ handler: (input) => openfda.enforcement(input),
5878
+ }),
5879
+ // ━━━ openFDA 510(k) device clearances (api.fda.gov) — medical-device regulatory (1) ━━━ ADR-0056
5880
+ // KEYLESS with an OPTIONAL OPENFDA_API_KEY (raises the rate limit; keyless works
5881
+ // ~1000/day — never throws for a missing key). SAME source/envelope/crux as
5882
+ // openfda_enforcement (reuses openfda.ts's fetchOpenfda/readOpenfdaError/luceneQuote).
5883
+ // Structured filters ONLY (no raw Lucene passthrough — the tool assembles + escapes the
5884
+ // search= string, injection-safe). ★P1: totalAvailable = meta.results.total (EXACT,
5885
+ // ~175507). ★P2 crux: a no-match query returns openFDA HTTP 404 NOT_FOUND ⇒ an honest
5886
+ // empty, never a throw. The optional key rides &api_key= ONLY.
5887
+ defineTool({
5888
+ name: "openfda_device_clearances",
5889
+ description:
5890
+ "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.",
5891
+ inputSchema: OpenfdaDeviceClearancesInput,
5892
+ handler: (input) => openfdaDevice.deviceClearances(input),
5893
+ }),
5894
+ // ━━━ NHTSA vehicle safety (api.nhtsa.gov) — vehicle/parts supplier vetting (2) ━━━ ADR-0057
5895
+ // ★KEYLESS — no API key at all (no parameter, no header). The cross-agency
5896
+ // product-safety family alongside openFDA (medical). Both tools share
5897
+ // make/model/modelYear inputs and return the COMPLETE matching set (no pagination
5898
+ // ⇒ totalAvailable = the upstream Count/count, complete:true). ★The complaints VIN
5899
+ // (an individual-vehicle PII identifier) is EXCLUDED from the output.
5900
+ defineTool({
5901
+ name: "nhtsa_recalls",
5902
+ description:
5903
+ "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}$.",
5904
+ inputSchema: NhtsaVehicleInput,
5905
+ handler: (input) => nhtsa.recalls(input),
5906
+ }),
5907
+ defineTool({
5908
+ name: "nhtsa_complaints",
5909
+ description:
5910
+ "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}$.",
5911
+ inputSchema: NhtsaVehicleInput,
5912
+ handler: (input) => nhtsa.complaints(input),
5913
+ }),
5914
+ // ━━━ CPSC consumer-product recalls (www.saferproducts.gov) — goods/import vetting (1) ━━━ ADR-0058
5915
+ // ★KEYLESS — no API key at all (no parameter, no header). The third leg of the
5916
+ // cross-agency product-safety family alongside NHTSA (vehicles) and openFDA
5917
+ // (medical). The response is a bare JSON ARRAY with NO total-count field and NO
5918
+ // pagination ⇒ totalAvailable = the returned count, complete:true. All filters are
5919
+ // optional; with NO filter the tool bounds results to a ~90-day default window
5920
+ // (disclosed) rather than silently fetch the entire dataset.
5921
+ defineTool({
5922
+ name: "cpsc_recalls",
5923
+ description:
5924
+ "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.",
5925
+ inputSchema: CpscRecallsInput,
5926
+ handler: (input) => cpsc.recalls(input),
5927
+ }),
5928
+ // ━━━ BEA Regional Economic Accounts (apps.bea.gov) — regional GDP/income (1) ━━━ ADR-0051
5929
+ // ★The server's THIRD KEY-REQUIRED source: the BEA Data API has NO keyless tier, so
5930
+ // WITHOUT a BEA_API_KEY this tool throws an honest invalid_input config error (the
5931
+ // most other tools stay keyless — see api_key_status). County/state/MSA GDP-by-industry + personal income —
5932
+ // the regional place-of-performance lane. ★The P2 crux: a missing/invalid key returns
5933
+ // HTTP 200 carrying BEAAPI.Results.Error (NOT an HTTP error status), which is detected
5934
+ // BEFORE the Data-array drift check and surfaced as invalid_input (never a fake empty).
5935
+ // DataValue is a comma-formatted string; suppression codes ((NA)/(D)/(NM)/(L)/*) → null.
5936
+ defineTool({
5937
+ name: "bea_regional_data",
5938
+ description:
5939
+ "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.",
5940
+ inputSchema: BeaRegionalDataInput,
5941
+ handler: (input) => bea.regionalData(input),
5942
+ }),
5026
5943
  // ━━━ GSA Federal Travel Per-Diem (api.gsa.gov) — travel-cost lane (1) ━━━ ADR-0050
5027
5944
  // The lodging + M&IE reimbursement ceilings the federal government pays for official
5028
5945
  // travel, by city/state OR zip for a year. SAME host (api.gsa.gov) + SAME api.data.gov
@@ -5037,15 +5954,87 @@ export const TOOLS: ToolDef[] = [
5037
5954
  inputSchema: GsaPerdiemRatesInput,
5038
5955
  handler: (input) => gsaPerdiem.perdiemRates(input),
5039
5956
  }),
5957
+ // ━━━ US DOL Data API v4 (apiprod.dol.gov) — the labor-enforcement lane (2) ━━━ ADR-0053
5958
+ // A DELIBERATE key split: dol_list_datasets (the dataset CATALOG) is KEYLESS;
5959
+ // dol_get_dataset (the DATA endpoint) is the server's 4th REQUIRED key (DOL_API_KEY —
5960
+ // the data endpoint has NO keyless tier, so without the key it THROWS pre-fetch). The
5961
+ // key rides the X-API-KEY HEADER ONLY. The data-record envelope is key-gated/unverified
5962
+ // ⇒ records are surfaced VERBATIM + totalAvailable defaults null (never `returned` faked
5963
+ // as the total). agency/query filtering on the catalog is CLIENT-SIDE.
5964
+ defineTool({
5965
+ name: "dol_list_datasets",
5966
+ description:
5967
+ "List the US Department of Labor Data API v4 dataset catalog (apiprod.dol.gov /v4/datasets) — the machine inventory of DOL enforcement/statistics datasets (WHD wage & hour, OSHA inspections, ILAB child/forced-labor reports, MSHA mine safety, ETA …). KEYLESS: the catalog needs NO API key (only dol_get_dataset does). Input (all optional): `agency` (CLIENT-SIDE filter by agency abbreviation like 'WHD'/'OSHA'/'ILAB', or an agency-name substring), `query` (CLIENT-SIDE free-text substring over dataset name/description/category/table/endpoint), `limit` (default 25, max 200), `offset`. Returns { datasets:[{ name, tablename, apiUrl, agency, agencyAbbr, description, frequency, datasetType, category }] } + honest _meta. ★Feed a row's `apiUrl` (the DOL 'api_url' endpoint) + its `agencyAbbr` into dol_get_dataset to fetch that dataset's records. HONESTY: agency/query filtering is CLIENT-SIDE (the DOL catalog API does not filter server-side, verified live); totalAvailable is the catalog's REAL total (meta.total_count) for an unfiltered scan, or the exact filtered-set size (the whole catalog is fetched in one page); offset pagination. Every scalar is null-never-empty-string. A non-array `datasets` / 200 non-JSON ⇒ schema_drift; a 5xx ⇒ THROWS.",
5968
+ inputSchema: DolListDatasetsInput,
5969
+ handler: (input) => dol.listDatasets(input),
5970
+ }),
5971
+ defineTool({
5972
+ name: "dol_get_dataset",
5973
+ description:
5974
+ "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.",
5975
+ inputSchema: DolGetDatasetInput,
5976
+ handler: (input) => dol.getDataset(input),
5977
+ }),
5978
+ // ━━━ US Senate LDA lobbying filings (lda.senate.gov) — the lobbying/B2G lane (1) ━━━ ADR-0052
5979
+ // Who is paid HOW MUCH to lobby WHICH federal agency on WHICH issue — the
5980
+ // registrant→client→government-entity signal no contract/spending source carries.
5981
+ // KEYLESS (anonymous 200); the OPTIONAL free LDA_API_KEY only raises the rate limit
5982
+ // and rides the Authorization: Token … header ONLY (the socrata app-token lineage —
5983
+ // NOT key-required). ★count is the API's REAL total (~1.95M) — never results.length;
5984
+ // page-based pagination. income/expenses are null-or-decimal-string ⇒ null-never-0.
5985
+ defineTool({
5986
+ name: "lda_search_filings",
5987
+ description:
5988
+ "Search US Senate LDA (Lobbying Disclosure Act) filings — who is paid HOW MUCH to lobby WHICH federal agency on WHICH issue (lda.senate.gov/api/v1/filings, KEYLESS — anonymous access works; an optional free LDA_API_KEY only raises the rate limit). All inputs optional: `registrantName` (the lobbying firm/in-house filer), `clientName` (who it's for), `lobbyistName`, `filingYear` (4-digit), `filingType` (short code, e.g. 'Q1'/'RR'/'YE'), `agency` (the federal government_entity lobbied — the B2G signal), `issue` (specific lobbying issues text), `page` (1-based, default 1), `pageSize` (1..25, default 25). Returns { filings:[{ filingUuid, filingType, filingYear, filingPeriod, incomeUsd, expensesUsd, registrant, client, lobbyingActivities:[{ issueCode, description, governmentEntities:[names] }], documentUrl, postedDate, terminationDate }] } + honest _meta. HONESTY: totalAvailable is the API's REAL total match count (the corpus is ~1.95M filings) — NOT the rows on this page; pagination is page-based (pass the next page number when hasMore). incomeUsd/expensesUsd are parsed from the null-or-decimal-string income/expenses — null (not reported) ⇒ null, NEVER 0 (a genuine 0 stays 0); a filing reports EITHER income OR expenses, so the other is typically null. Missing lobbying_activities/government_entities ⇒ empty arrays (never fabricated). A genuine no-match (results:[]) ⇒ honest empty (returned:0); a 400 (bad filter) ⇒ invalid_input surfacing the API's message; a 429 ⇒ rate_limited THROWS (Retry-After honored, never routed around); a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON / non-array results / non-number count ⇒ schema_drift. The optional key rides ONLY in the Authorization: Token header (never the URL/_meta).",
5989
+ inputSchema: LdaSearchFilingsInput,
5990
+ handler: (input) => lda.searchFilings(input),
5991
+ }),
5992
+ // ━━━ US federal court opinions (www.courtlistener.com) — the litigation lane (1) ━━━ ADR-0055
5993
+ // Federal court decisions — the judicial signal no contract/spending/lobbying source
5994
+ // carries (uscfc bid-protest/contract-claim opinions, cafc contract/patent appeals).
5995
+ // ★PROVENANCE: CourtListener (Free Law Project, a non-profit), NOT a .gov API — PACER
5996
+ // (the .gov source) is paywalled. KEYLESS (anonymous 200); the OPTIONAL free
5997
+ // COURTLISTENER_API_TOKEN only raises the rate limit, riding the Authorization: Token …
5998
+ // header ONLY. ★count is the API's REAL total — never results.length; CURSOR pagination
5999
+ // (nextCursor extracted from `next`, host re-asserted). type=o FIXED.
6000
+ defineTool({
6001
+ name: "courtlistener_search_opinions",
6002
+ description:
6003
+ "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).",
6004
+ inputSchema: CourtlistenerSearchOpinionsInput,
6005
+ handler: (input) => courtlistener.searchOpinions(input),
6006
+ }),
6007
+ // ━━━ US tax-exempt nonprofits (projects.propublica.org) — the nonprofit lane (2) ━━━ ADR-0060
6008
+ // Who a 501(c) org IS (EIN, NTEE, subsection, ruling date, status) + its Form 990
6009
+ // FINANCIALS (revenue/expenses/assets/liabilities by year) — the nonprofit/grantee/
6010
+ // subcontractor vetting signal no contract/spending/grant/lobbying source carries.
6011
+ // ★PROVENANCE: IRS Form 990 public records republished KEYLESS by ProPublica Nonprofit
6012
+ // Explorer (a non-profit newsroom), NOT a .gov API (the IRS has no clean query API).
6013
+ // KEYLESS (no key). search total_results is the REAL total — never organizations.length;
6014
+ // financials totalAvailable = filings.length (the complete set). Money via num (null-never-0).
6015
+ defineTool({
6016
+ name: "nonprofit_search",
6017
+ description:
6018
+ "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.",
6019
+ inputSchema: NonprofitSearchInput,
6020
+ handler: (input) => nonprofit.search(input),
6021
+ }),
6022
+ defineTool({
6023
+ name: "nonprofit_financials",
6024
+ description:
6025
+ "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.",
6026
+ inputSchema: NonprofitFinancialsInput,
6027
+ handler: (input) => nonprofit.financials(input),
6028
+ }),
5040
6029
  // ━━━ Self-service key discovery (1) ━━━
5041
6030
  // KEYLESS. A local status query — reads process.env (+ any .env auto-loaded at
5042
6031
  // startup) and reports, per key, whether it is set (a BOOLEAN — the key VALUE is
5043
- // NEVER read into the output). Makes the 2-required + 5-optional key situation
6032
+ // NEVER read into the output). Makes the 4-required + 6-optional key situation
5044
6033
  // discoverable without reading source or docs.
5045
6034
  defineTool({
5046
6035
  name: "api_key_status",
5047
6036
  description:
5048
- "List every API key this server can use, whether each is REQUIRED or OPTIONAL, the free signup URL + what it unlocks, and whether it is CURRENTLY configured — a boolean only; the key VALUE is NEVER shown. KEYLESS (no input). Most sources are keyless; only Census (census_business_patterns) and FRED (2 tools) REQUIRE a key (they throw without one), the other 5 keys are OPTIONAL (raise a rate limit or unlock one filter). Keys can be set as host env vars OR in a `.env` file in the server's working directory (auto-loaded at startup; real env wins over .env). Returns { keys:[{ envVar, sources[], required, signupUrl, unlocks, note, currentlySet }], requiredMissing:[envVars], optionalMissing:[envVars], allKeysFree:true }. This tool tells you the CONFIG state; to verify a key actually WORKS, call that source's own tool. Getting a key (creating the account at the signup URL) is your step — the server automates discovery + configuration, not signup.",
6037
+ "List every API key this server can use, whether each is REQUIRED or OPTIONAL, the free signup URL + what it unlocks, and whether it is CURRENTLY configured — a boolean only; the key VALUE is NEVER shown. KEYLESS (no input). Most sources are keyless; four sources need a key — Census (census_business_patterns), FRED (2 tools), and BEA (bea_regional_data) require one outright, and DOL's DATA endpoint (dol_get_dataset) needs one too (its catalog, dol_list_datasets, stays keyless) — the other 6 keys are OPTIONAL (raise a rate limit or unlock one filter). Keys can be set as host env vars OR in a `.env` file in the server's working directory (auto-loaded at startup; real env wins over .env). Returns { keys:[{ envVar, sources[], required, signupUrl, unlocks, note, currentlySet }], requiredMissing:[envVars], optionalMissing:[envVars], allKeysFree:true }. This tool tells you the CONFIG state; to verify a key actually WORKS, call that source's own tool. Getting a key (creating the account at the signup URL) is your step — the server automates discovery + configuration, not signup.",
5049
6038
  inputSchema: ApiKeyStatusInput,
5050
6039
  handler: async () => keys.apiKeyStatus(),
5051
6040
  }),