@cliwant/mcp-sam-gov 1.1.0 → 1.3.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.
package/src/server.ts CHANGED
@@ -56,6 +56,10 @@ import * as clinicaltrials from "./clinicaltrials.js";
56
56
  import * as census from "./census.js";
57
57
  import * as censusEconomic from "./census-economic.js";
58
58
  import * as fred from "./fred.js";
59
+ import * as bea from "./bea.js";
60
+ import * as gsaPerdiem from "./gsa-perdiem.js";
61
+ import * as dol from "./dol.js";
62
+ import * as lda from "./lda.js";
59
63
  import * as fema from "./fema.js";
60
64
  import * as fdic from "./fdic.js";
61
65
  import * as bls from "./bls.js";
@@ -80,7 +84,7 @@ import { realpathSync } from "node:fs";
80
84
  const SERVER_NAME = "mcp-sam-gov";
81
85
  // Kept in lockstep with package.json / manifest.json / server.json.
82
86
  // Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
83
- const SERVER_VERSION = "1.1.0";
87
+ const SERVER_VERSION = "1.3.0";
84
88
 
85
89
  // ─── Tool input schemas (Zod) ────────────────────────────────────
86
90
 
@@ -3485,6 +3489,223 @@ const FredSeriesObservationsInput = z.object({
3485
3489
  .describe("Observation date order: 'asc' (oldest first, FRED default) or 'desc' (newest first)."),
3486
3490
  });
3487
3491
 
3492
+ // ─── BEA Regional Economic Accounts (apps.bea.gov) — the THIRD key-required source ──
3493
+ // ADR-0051. County/state/MSA GDP-by-industry (CAGDP2/SAGDP2N) + personal income
3494
+ // (CAINC1/SAINC1) — the regional/sub-national place-of-performance lane. REQUIRES a
3495
+ // free BEA_API_KEY; without it the tool throws an honest config error (the other 116
3496
+ // tools stay keyless). ★A missing/invalid key returns HTTP 200 with a
3497
+ // BEAAPI.Results.Error carrier (NOT an HTTP error), detected pre-drift. The key rides
3498
+ // UserID= ONLY. DataValue is a comma string; suppression codes ((NA)/(D)/…) → null.
3499
+ const BeaRegionalDataInput = z.object({
3500
+ tableName: z
3501
+ .string()
3502
+ .regex(/^[A-Za-z0-9]{2,20}$/)
3503
+ .describe(
3504
+ "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.",
3505
+ ),
3506
+ geoFips: z
3507
+ .string()
3508
+ .regex(/^[A-Za-z0-9]{2,10}$/)
3509
+ .describe(
3510
+ "The BEA GeoFips selector: 'STATE' (all states), a county FIPS like '06075', or an MSA code. Validated ^[A-Za-z0-9]{2,10}$. Required.",
3511
+ ),
3512
+ lineCode: z
3513
+ .string()
3514
+ .regex(/^([0-9]{1,4}|ALL)$/)
3515
+ .describe(
3516
+ "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.",
3517
+ ),
3518
+ year: z
3519
+ .string()
3520
+ .regex(/^(\d{4}|LAST5|ALL)$/)
3521
+ .optional()
3522
+ .describe(
3523
+ "The data year: a 4-digit year (e.g. '2022'), 'LAST5' (the latest 5 years, default), or 'ALL'. Validated ^(\\d{4}|LAST5|ALL)$.",
3524
+ ),
3525
+ frequency: z
3526
+ .enum(["A", "Q"])
3527
+ .optional()
3528
+ .describe("Data frequency: 'A' (annual, default) or 'Q' (quarterly)."),
3529
+ });
3530
+
3531
+ // ─── GSA Federal Travel Per-Diem (api.gsa.gov) — travel-cost lane ──
3532
+ // ADR-0050. Lodging + M&IE reimbursement ceilings by city/state OR zip for a year.
3533
+ // KEYLESS by default via the shared DEMO_KEY (datagovKey.ts seam); DATA_GOV_API_KEY
3534
+ // lifts the rate. EITHER (city+state) OR zip — both/neither ⇒ invalid_input, 0 fetch.
3535
+ const GsaPerdiemRatesInput = z
3536
+ .object({
3537
+ city: z
3538
+ .string()
3539
+ .regex(/^[A-Za-z .'\-]{1,60}$/)
3540
+ .optional()
3541
+ .describe(
3542
+ "The city name (e.g. 'Washington', 'San Francisco'). Requires `state`. Validated ^[A-Za-z .'\\-]{1,60}$. Use EITHER (city + state) OR zip — not both.",
3543
+ ),
3544
+ state: z
3545
+ .string()
3546
+ .regex(/^[A-Za-z]{2}$/)
3547
+ .optional()
3548
+ .describe(
3549
+ "The 2-letter state/territory code (e.g. 'DC', 'CA'). Required with `city`. Validated ^[A-Za-z]{2}$.",
3550
+ ),
3551
+ zip: z
3552
+ .string()
3553
+ .regex(/^\d{5}$/)
3554
+ .optional()
3555
+ .describe(
3556
+ "A 5-digit ZIP code (e.g. '20001'). The alternative lookup mode to city+state. Validated ^\\d{5}$. Use EITHER zip OR (city + state) — not both.",
3557
+ ),
3558
+ year: z
3559
+ .string()
3560
+ .regex(/^\d{4}$/)
3561
+ .optional()
3562
+ .describe(
3563
+ "The per-diem fiscal year (default '2025'). Validated ^\\d{4}$ (it rides in the request path).",
3564
+ ),
3565
+ })
3566
+ .describe(
3567
+ "Look up GSA per-diem rates by EITHER (city + state) OR zip. Supplying both, or neither, ⇒ invalid_input.",
3568
+ );
3569
+
3570
+ // ─── US DOL Data API v4 (apiprod.dol.gov) — the labor-enforcement lane ──
3571
+ // ADR-0053. A DELIBERATE key split: dol_list_datasets (the CATALOG) is KEYLESS;
3572
+ // dol_get_dataset (the DATA endpoint) is the 4th REQUIRED key (DOL_API_KEY, no keyless
3573
+ // tier — throws pre-fetch without it). The key rides the X-API-KEY HEADER ONLY. The
3574
+ // data envelope is key-gated/unverified ⇒ records are surfaced verbatim + totalAvailable
3575
+ // defaults null (never `returned` faked as the total). agency/query filter is CLIENT-SIDE.
3576
+ const DolListDatasetsInput = z.object({
3577
+ agency: z
3578
+ .string()
3579
+ .min(1)
3580
+ .max(100)
3581
+ .optional()
3582
+ .describe(
3583
+ "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.",
3584
+ ),
3585
+ query: z
3586
+ .string()
3587
+ .min(1)
3588
+ .max(200)
3589
+ .optional()
3590
+ .describe(
3591
+ "CLIENT-SIDE free-text filter (substring over dataset name / description / category / table / endpoint), e.g. 'child labor', 'wage', 'inspection'.",
3592
+ ),
3593
+ limit: z
3594
+ .number()
3595
+ .int()
3596
+ .min(1)
3597
+ .max(200)
3598
+ .optional()
3599
+ .describe("Datasets to return per page (default 25, max 200). Offset-paginated over the (filtered) catalog."),
3600
+ offset: z
3601
+ .number()
3602
+ .int()
3603
+ .min(0)
3604
+ .optional()
3605
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3606
+ });
3607
+
3608
+ const DolGetDatasetInput = z.object({
3609
+ agency: z
3610
+ .string()
3611
+ .regex(/^[A-Za-z0-9_]+$/)
3612
+ .describe(
3613
+ "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.",
3614
+ ),
3615
+ table: z
3616
+ .string()
3617
+ .regex(/^[A-Za-z0-9_]+$/)
3618
+ .describe(
3619
+ "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.",
3620
+ ),
3621
+ limit: z
3622
+ .number()
3623
+ .int()
3624
+ .min(1)
3625
+ .max(100)
3626
+ .optional()
3627
+ .describe("Max records to return (default 10, max 100). Offset-paginated."),
3628
+ offset: z
3629
+ .number()
3630
+ .int()
3631
+ .min(0)
3632
+ .optional()
3633
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3634
+ filterField: z
3635
+ .string()
3636
+ .min(1)
3637
+ .max(100)
3638
+ .optional()
3639
+ .describe("Optional: a dataset field name to filter on (paired with filterValue → a DOL filter_object equality filter). Supply BOTH or NEITHER."),
3640
+ filterValue: z
3641
+ .string()
3642
+ .min(1)
3643
+ .max(200)
3644
+ .optional()
3645
+ .describe("Optional: the value the filterField must equal. Supply BOTH filterField and filterValue, or NEITHER."),
3646
+ fields: z
3647
+ .array(z.string().min(1))
3648
+ .optional()
3649
+ .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)."),
3650
+ });
3651
+
3652
+ // ─── US Senate LDA lobbying filings (lda.senate.gov) — the lobbying/B2G lane ──
3653
+ // ADR-0052. Who is paid HOW MUCH to lobby WHICH federal agency on WHICH issue.
3654
+ // KEYLESS (anonymous 200); an optional free LDA_API_KEY only raises the rate limit
3655
+ // and rides the Authorization: Token … header ONLY. `count` is the REAL total
3656
+ // (~1.95M) — never results.length; page-based pagination (page/pageSize ≤25). All
3657
+ // filter VALUES ride URLSearchParams; filingYear/page/pageSize charclass/range-guarded.
3658
+ const LdaSearchFilingsInput = z.object({
3659
+ registrantName: z
3660
+ .string()
3661
+ .min(1)
3662
+ .optional()
3663
+ .describe("Filter by the registrant (the lobbying firm / in-house filer) name, e.g. 'Akin Gump'. Substring match, upstream-validated."),
3664
+ clientName: z
3665
+ .string()
3666
+ .min(1)
3667
+ .optional()
3668
+ .describe("Filter by the client name (who the lobbying is FOR), e.g. 'Google'. Substring match, upstream-validated."),
3669
+ lobbyistName: z
3670
+ .string()
3671
+ .min(1)
3672
+ .optional()
3673
+ .describe("Filter by an individual lobbyist's name. Substring match, upstream-validated."),
3674
+ filingYear: z
3675
+ .string()
3676
+ .regex(/^\d{4}$/)
3677
+ .optional()
3678
+ .describe("Filter by filing year, a 4-digit year (e.g. '2024'). Validated ^\\d{4}$."),
3679
+ filingType: z
3680
+ .string()
3681
+ .min(1)
3682
+ .optional()
3683
+ .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)."),
3684
+ agency: z
3685
+ .string()
3686
+ .min(1)
3687
+ .optional()
3688
+ .describe("Filter by the federal government entity lobbied (maps to government_entity — the B2G signal), e.g. 'DEPARTMENT OF DEFENSE'."),
3689
+ issue: z
3690
+ .string()
3691
+ .min(1)
3692
+ .optional()
3693
+ .describe("Filter by the specific lobbying issues text (maps to filing_specific_lobbying_issues), e.g. 'appropriations'."),
3694
+ page: z
3695
+ .number()
3696
+ .int()
3697
+ .min(1)
3698
+ .default(1)
3699
+ .describe("1-based page number (default 1). Page with the next page number from _meta.notes / when _meta.pagination.hasMore."),
3700
+ pageSize: z
3701
+ .number()
3702
+ .int()
3703
+ .min(1)
3704
+ .max(25)
3705
+ .default(25)
3706
+ .describe("Filings per page, 1..25 (the LDA API caps at 25), default 25."),
3707
+ });
3708
+
3488
3709
  // api_key_status takes no input — it is a pure status query over process.env.
3489
3710
  const ApiKeyStatusInput = z.object({});
3490
3711
 
@@ -4983,15 +5204,79 @@ export const TOOLS: ToolDef[] = [
4983
5204
  inputSchema: FredSeriesObservationsInput,
4984
5205
  handler: (input) => fred.seriesObservations(input),
4985
5206
  }),
5207
+ // ━━━ BEA Regional Economic Accounts (apps.bea.gov) — regional GDP/income (1) ━━━ ADR-0051
5208
+ // ★The server's THIRD KEY-REQUIRED source: the BEA Data API has NO keyless tier, so
5209
+ // WITHOUT a BEA_API_KEY this tool throws an honest invalid_input config error (the
5210
+ // other 116 tools stay keyless). County/state/MSA GDP-by-industry + personal income —
5211
+ // the regional place-of-performance lane. ★The P2 crux: a missing/invalid key returns
5212
+ // HTTP 200 carrying BEAAPI.Results.Error (NOT an HTTP error status), which is detected
5213
+ // BEFORE the Data-array drift check and surfaced as invalid_input (never a fake empty).
5214
+ // DataValue is a comma-formatted string; suppression codes ((NA)/(D)/(NM)/(L)/*) → null.
5215
+ defineTool({
5216
+ name: "bea_regional_data",
5217
+ description:
5218
+ "Regional (county / state / MSA) economic data — GDP by industry and personal income — from the US Bureau of Economic Analysis (BEA) Regional Economic Accounts (apps.bea.gov/api/data, dataset 'Regional'). ★REQUIRES a free BEA_API_KEY: the BEA Data API has NO keyless tier, so without the key this tool THROWS an honest config error (get one at https://apps.bea.gov/API/signup/; Census, FRED, and BEA are the only key-required sources — every other tool is keyless). Input: `tableName` (required, e.g. 'CAGDP2' county GDP by industry, 'SAGDP2N' state GDP, 'CAINC1'/'SAINC1' personal income), `geoFips` (required — 'STATE' for all states, a county FIPS like '06075', or an MSA code), `lineCode` (required — an integer industry line like '1', or 'ALL'), optional `year` ('LAST5' default, a 4-digit year, or 'ALL'), `frequency` ('A' annual default, or 'Q'). Returns { rows:[{ geoFips, geoName, timePeriod, lineCode, dataValue, unitOfMeasure, unitMult, noteRef }], notes:[{ noteRef, noteText }] } + honest _meta. ★HONESTY (the crux): a missing/invalid key — or ANY bad parameter — returns HTTP 200 carrying an Error object (NOT an HTTP error status); this is detected and surfaced as invalid_input carrying BEA's APIErrorDescription — NEVER a fake empty. dataValue is parsed from BEA's comma-formatted string ('1,234,567'→1234567); BEA suppression/not-available codes ((NA)/(D)/(NM)/(L)/*) map to null — NEVER 0 (a genuine 0 stays 0). unitMult (a power-of-10 multiplier) and unitOfMeasure are reported ALONGSIDE the raw dataValue — the value is NOT multiplied in (apply unitMult yourself). BEA returns the COMPLETE set for the filter (no pagination) ⇒ totalAvailable = the row count, complete:true; a genuine empty Data:[] ⇒ honest empty (returned:0); a 5xx ⇒ THROWS; a 200 non-JSON ⇒ schema_drift. The key rides ONLY in the UserID= query param — never logged or echoed.",
5219
+ inputSchema: BeaRegionalDataInput,
5220
+ handler: (input) => bea.regionalData(input),
5221
+ }),
5222
+ // ━━━ GSA Federal Travel Per-Diem (api.gsa.gov) — travel-cost lane (1) ━━━ ADR-0050
5223
+ // The lodging + M&IE reimbursement ceilings the federal government pays for official
5224
+ // travel, by city/state OR zip for a year. SAME host (api.gsa.gov) + SAME api.data.gov
5225
+ // key seam (datagovKey.ts, X-Api-Key header) as datagov_search_datasets — KEYLESS by
5226
+ // default via the shared DEMO_KEY, keylessMode:false. EITHER (city+state) OR zip; both
5227
+ // or neither ⇒ invalid_input, 0 fetch. `value` (monthly lodging) / `meals` are null-
5228
+ // never-0; standardRate/isOconus are STRING booleans coerced to real booleans.
5229
+ defineTool({
5230
+ name: "gsa_perdiem_rates",
5231
+ description:
5232
+ "Look up GSA Federal Travel PER-DIEM rates — the max lodging + Meals & Incidental Expenses (M&IE) reimbursement ceilings for official U.S. government travel (api.gsa.gov /travel/perdiem/v2, keyed — DATA_GOV_API_KEY or the shared DEMO_KEY). Input: EITHER `city` (e.g. 'Washington') + `state` (2-letter, e.g. 'DC') OR `zip` (5-digit) — supplying BOTH, or NEITHER, ⇒ invalid_input with 0 fetch; optional `year` (default '2025'). Returns { rates:[{ city, county, state, zip, year, isOconus, standardRate, mealsUsd, monthlyLodgingUsd:[{ month (1-12), monthName, lodgingUsd }] }] } + honest _meta. HONESTY: lodgingUsd (the API's monthly `value`) is the MAX nightly lodging ceiling for that month — it VARIES SEASONALLY (hence a per-month array), and mealsUsd is the daily M&IE ceiling; both are integer US dollars, null-when-withheld (NEVER 0 — a genuine 0 is preserved). standardRate/isOconus are booleans coerced from the API's string 'true'/'false' (an unrecognized value ⇒ null, never a fabricated false); the months array is preserved AS-IS (never padded to 12). The API returns the COMPLETE rate set (no pagination) ⇒ totalAvailable = the row count, complete:true. A genuine no-match (rates:[]/rate:[]) ⇒ honest empty (returned:0); the API's `errors` field non-null ⇒ invalid_input carrying the message (never a fake empty); a 429 (DEMO_KEY ~10 req/hr, hit quickly) ⇒ rate_limited THROWS; a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON ⇒ schema_drift. DEMO_KEY ~10 req/hr shared ceiling — set DATA_GOV_API_KEY (free at api.data.gov/signup) for 1000/hr. The key rides ONLY in the X-Api-Key header (never the URL/_meta).",
5233
+ inputSchema: GsaPerdiemRatesInput,
5234
+ handler: (input) => gsaPerdiem.perdiemRates(input),
5235
+ }),
5236
+ // ━━━ US DOL Data API v4 (apiprod.dol.gov) — the labor-enforcement lane (2) ━━━ ADR-0053
5237
+ // A DELIBERATE key split: dol_list_datasets (the dataset CATALOG) is KEYLESS;
5238
+ // dol_get_dataset (the DATA endpoint) is the server's 4th REQUIRED key (DOL_API_KEY —
5239
+ // the data endpoint has NO keyless tier, so without the key it THROWS pre-fetch). The
5240
+ // key rides the X-API-KEY HEADER ONLY. The data-record envelope is key-gated/unverified
5241
+ // ⇒ records are surfaced VERBATIM + totalAvailable defaults null (never `returned` faked
5242
+ // as the total). agency/query filtering on the catalog is CLIENT-SIDE.
5243
+ defineTool({
5244
+ name: "dol_list_datasets",
5245
+ description:
5246
+ "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.",
5247
+ inputSchema: DolListDatasetsInput,
5248
+ handler: (input) => dol.listDatasets(input),
5249
+ }),
5250
+ defineTool({
5251
+ name: "dol_get_dataset",
5252
+ description:
5253
+ "Fetch records from ONE US DOL dataset (apiprod.dol.gov /v4/get/{agency}/{endpoint}/json). ★REQUIRES a free DOL_API_KEY: the DOL DATA endpoint has NO keyless tier, so without the key this tool THROWS an honest config error (get one at https://dol.gov/developer; the dataset CATALOG — dol_list_datasets — and agency list stay keyless). Input: `agency` (required — the `agencyAbbr` from dol_list_datasets, e.g. 'WHD', 'OSHA', 'ILAB'; rides the PATH, ^[A-Za-z0-9_]+$), `table` (required — the dataset's `apiUrl` endpoint from dol_list_datasets, e.g. 'Child_Labor_Report__2016_to_2022'; rides the PATH, ^[A-Za-z0-9_]+$), optional `limit` (default 10, max 100), `offset`, `filterField`+`filterValue` (a paired equality filter → a DOL filter_object), `fields` (best-effort column selection). Returns { records:[…verbatim dataset rows…] } + honest _meta. HONESTY: records are surfaced VERBATIM (the data-record envelope is key-gated and unverified, so field names/values are preserved as-is — a genuine 0 stays 0, a missing field stays null; the tool never coerces or fabricates). totalAvailable is a real count field ONLY when the response carries one, else null (an honest unknown — `returned` is NEVER passed off as the total); offset pagination (a full page ⇒ hasMore, page forward to confirm). A missing/invalid key (401/403) ⇒ invalid_input carrying the DOL_API_KEY guidance (never empty); a 400 ⇒ invalid_input; a genuine empty ⇒ honest empty (returned:0); a 429 ⇒ rate_limited THROWS (Retry-After honored); a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON / no row array ⇒ schema_drift. The key rides ONLY in the X-API-KEY request header — never the URL / _meta / a log.",
5254
+ inputSchema: DolGetDatasetInput,
5255
+ handler: (input) => dol.getDataset(input),
5256
+ }),
5257
+ // ━━━ US Senate LDA lobbying filings (lda.senate.gov) — the lobbying/B2G lane (1) ━━━ ADR-0052
5258
+ // Who is paid HOW MUCH to lobby WHICH federal agency on WHICH issue — the
5259
+ // registrant→client→government-entity signal no contract/spending source carries.
5260
+ // KEYLESS (anonymous 200); the OPTIONAL free LDA_API_KEY only raises the rate limit
5261
+ // and rides the Authorization: Token … header ONLY (the socrata app-token lineage —
5262
+ // NOT key-required). ★count is the API's REAL total (~1.95M) — never results.length;
5263
+ // page-based pagination. income/expenses are null-or-decimal-string ⇒ null-never-0.
5264
+ defineTool({
5265
+ name: "lda_search_filings",
5266
+ description:
5267
+ "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).",
5268
+ inputSchema: LdaSearchFilingsInput,
5269
+ handler: (input) => lda.searchFilings(input),
5270
+ }),
4986
5271
  // ━━━ Self-service key discovery (1) ━━━
4987
5272
  // KEYLESS. A local status query — reads process.env (+ any .env auto-loaded at
4988
5273
  // startup) and reports, per key, whether it is set (a BOOLEAN — the key VALUE is
4989
- // NEVER read into the output). Makes the 2-required + 5-optional key situation
5274
+ // NEVER read into the output). Makes the 4-required + 6-optional key situation
4990
5275
  // discoverable without reading source or docs.
4991
5276
  defineTool({
4992
5277
  name: "api_key_status",
4993
5278
  description:
4994
- "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.",
5279
+ "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.",
4995
5280
  inputSchema: ApiKeyStatusInput,
4996
5281
  handler: async () => keys.apiKeyStatus(),
4997
5282
  }),