@cliwant/mcp-sam-gov 1.2.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,7 +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";
59
60
  import * as gsaPerdiem from "./gsa-perdiem.js";
61
+ import * as dol from "./dol.js";
62
+ import * as lda from "./lda.js";
60
63
  import * as fema from "./fema.js";
61
64
  import * as fdic from "./fdic.js";
62
65
  import * as bls from "./bls.js";
@@ -81,7 +84,7 @@ import { realpathSync } from "node:fs";
81
84
  const SERVER_NAME = "mcp-sam-gov";
82
85
  // Kept in lockstep with package.json / manifest.json / server.json.
83
86
  // Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
84
- const SERVER_VERSION = "1.2.0";
87
+ const SERVER_VERSION = "1.3.0";
85
88
 
86
89
  // ─── Tool input schemas (Zod) ────────────────────────────────────
87
90
 
@@ -3486,6 +3489,45 @@ const FredSeriesObservationsInput = z.object({
3486
3489
  .describe("Observation date order: 'asc' (oldest first, FRED default) or 'desc' (newest first)."),
3487
3490
  });
3488
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
+
3489
3531
  // ─── GSA Federal Travel Per-Diem (api.gsa.gov) — travel-cost lane ──
3490
3532
  // ADR-0050. Lodging + M&IE reimbursement ceilings by city/state OR zip for a year.
3491
3533
  // KEYLESS by default via the shared DEMO_KEY (datagovKey.ts seam); DATA_GOV_API_KEY
@@ -3525,6 +3567,145 @@ const GsaPerdiemRatesInput = z
3525
3567
  "Look up GSA per-diem rates by EITHER (city + state) OR zip. Supplying both, or neither, ⇒ invalid_input.",
3526
3568
  );
3527
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
+
3528
3709
  // api_key_status takes no input — it is a pure status query over process.env.
3529
3710
  const ApiKeyStatusInput = z.object({});
3530
3711
 
@@ -5023,6 +5204,21 @@ export const TOOLS: ToolDef[] = [
5023
5204
  inputSchema: FredSeriesObservationsInput,
5024
5205
  handler: (input) => fred.seriesObservations(input),
5025
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
+ }),
5026
5222
  // ━━━ GSA Federal Travel Per-Diem (api.gsa.gov) — travel-cost lane (1) ━━━ ADR-0050
5027
5223
  // The lodging + M&IE reimbursement ceilings the federal government pays for official
5028
5224
  // travel, by city/state OR zip for a year. SAME host (api.gsa.gov) + SAME api.data.gov
@@ -5037,15 +5233,50 @@ export const TOOLS: ToolDef[] = [
5037
5233
  inputSchema: GsaPerdiemRatesInput,
5038
5234
  handler: (input) => gsaPerdiem.perdiemRates(input),
5039
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
+ }),
5040
5271
  // ━━━ Self-service key discovery (1) ━━━
5041
5272
  // KEYLESS. A local status query — reads process.env (+ any .env auto-loaded at
5042
5273
  // 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
5274
+ // NEVER read into the output). Makes the 4-required + 6-optional key situation
5044
5275
  // discoverable without reading source or docs.
5045
5276
  defineTool({
5046
5277
  name: "api_key_status",
5047
5278
  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.",
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.",
5049
5280
  inputSchema: ApiKeyStatusInput,
5050
5281
  handler: async () => keys.apiKeyStatus(),
5051
5282
  }),