@cliwant/mcp-sam-gov 1.0.0 → 1.2.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/README.ja.md +11 -9
- package/README.ko.md +11 -9
- package/README.md +51 -12
- package/dist/census-economic.d.ts +93 -0
- package/dist/census-economic.d.ts.map +1 -0
- package/dist/census-economic.js +355 -0
- package/dist/census-economic.js.map +1 -0
- package/dist/fred.d.ts +108 -0
- package/dist/fred.d.ts.map +1 -0
- package/dist/fred.js +373 -0
- package/dist/fred.js.map +1 -0
- package/dist/gsa-perdiem.d.ts +74 -0
- package/dist/gsa-perdiem.d.ts.map +1 -0
- package/dist/gsa-perdiem.js +296 -0
- package/dist/gsa-perdiem.js.map +1 -0
- package/dist/keys.d.ts +83 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +173 -0
- package/dist/keys.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +185 -1
- package/dist/server.js.map +1 -1
- package/dist/snapshot.d.ts +33 -16
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +46 -17
- package/dist/snapshot.js.map +1 -1
- package/package.json +1 -1
- package/src/census-economic.ts +425 -0
- package/src/fred.ts +464 -0
- package/src/gsa-perdiem.ts +361 -0
- package/src/keys.ts +216 -0
- package/src/server.ts +221 -1
- package/src/snapshot.ts +51 -20
package/src/server.ts
CHANGED
|
@@ -54,6 +54,9 @@ import * as nih from "./nih.js";
|
|
|
54
54
|
import * as nsf from "./nsf.js";
|
|
55
55
|
import * as clinicaltrials from "./clinicaltrials.js";
|
|
56
56
|
import * as census from "./census.js";
|
|
57
|
+
import * as censusEconomic from "./census-economic.js";
|
|
58
|
+
import * as fred from "./fred.js";
|
|
59
|
+
import * as gsaPerdiem from "./gsa-perdiem.js";
|
|
57
60
|
import * as fema from "./fema.js";
|
|
58
61
|
import * as fdic from "./fdic.js";
|
|
59
62
|
import * as bls from "./bls.js";
|
|
@@ -64,6 +67,7 @@ import * as cms from "./cms.js";
|
|
|
64
67
|
import * as fac from "./fac.js";
|
|
65
68
|
import * as usitc from "./usitc.js";
|
|
66
69
|
import { fetchAttachmentText } from "./attachments.js";
|
|
70
|
+
import * as keys from "./keys.js";
|
|
67
71
|
import { toToolError, ToolErrorCarrier, errorFromResponse } from "./errors.js";
|
|
68
72
|
import {
|
|
69
73
|
buildMeta,
|
|
@@ -77,7 +81,7 @@ import { realpathSync } from "node:fs";
|
|
|
77
81
|
const SERVER_NAME = "mcp-sam-gov";
|
|
78
82
|
// Kept in lockstep with package.json / manifest.json / server.json.
|
|
79
83
|
// Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
|
|
80
|
-
const SERVER_VERSION = "1.
|
|
84
|
+
const SERVER_VERSION = "1.2.0";
|
|
81
85
|
|
|
82
86
|
// ─── Tool input schemas (Zod) ────────────────────────────────────
|
|
83
87
|
|
|
@@ -3381,6 +3385,149 @@ const CensusGeographiesByCoordinatesInput = z
|
|
|
3381
3385
|
path: ["latitude"],
|
|
3382
3386
|
});
|
|
3383
3387
|
|
|
3388
|
+
// ─── US Census County Business Patterns (CBP) — the FIRST key-required source ──
|
|
3389
|
+
const CensusBusinessPatternsInput = z.object({
|
|
3390
|
+
naics: z
|
|
3391
|
+
.string()
|
|
3392
|
+
.regex(/^\d{2,6}$/)
|
|
3393
|
+
.optional()
|
|
3394
|
+
.describe(
|
|
3395
|
+
"A NAICS-2017 code (2–6 digits), e.g. '5415' (Computer Systems Design & Related Services) or '54' (Professional/Scientific/Technical). Omit to aggregate across all sectors. Validated ^\\d{2,6}$.",
|
|
3396
|
+
),
|
|
3397
|
+
geography: z
|
|
3398
|
+
.enum(["us", "state", "county"])
|
|
3399
|
+
.optional()
|
|
3400
|
+
.describe(
|
|
3401
|
+
"The geography level (default 'us'). 'state' returns one row per state (or a single state when `state` is given); 'county' returns every county in a state and REQUIRES `state`.",
|
|
3402
|
+
),
|
|
3403
|
+
state: z
|
|
3404
|
+
.string()
|
|
3405
|
+
.regex(/^\d{2}$/)
|
|
3406
|
+
.optional()
|
|
3407
|
+
.describe(
|
|
3408
|
+
"A 2-digit state FIPS code, e.g. '06' (California), '48' (Texas). Optional filter for geography='state'; REQUIRED for geography='county' (the CBP `in=state:` predicate). Validated ^\\d{2}$.",
|
|
3409
|
+
),
|
|
3410
|
+
year: z
|
|
3411
|
+
.string()
|
|
3412
|
+
.regex(/^\d{4}$/)
|
|
3413
|
+
.optional()
|
|
3414
|
+
.describe(
|
|
3415
|
+
"The CBP data year (default '2022', the latest confirmed vintage). Validated ^\\d{4}$ (it rides in the request path).",
|
|
3416
|
+
),
|
|
3417
|
+
limit: z
|
|
3418
|
+
.number()
|
|
3419
|
+
.int()
|
|
3420
|
+
.min(0)
|
|
3421
|
+
.optional()
|
|
3422
|
+
.describe(
|
|
3423
|
+
"OPTIONAL client-side top-N cap on the returned rows. CBP has NO server-side pagination, so this slices AFTER the full set is fetched and DISCLOSES the omission (totalAvailable stays the full count). Omit to return every matching row.",
|
|
3424
|
+
),
|
|
3425
|
+
});
|
|
3426
|
+
|
|
3427
|
+
// ─── FRED (Federal Reserve Economic Data) — the SECOND key-required source ──
|
|
3428
|
+
// ADR-0048. Macro context (GDP/CPI/rates/unemployment/PPI). REQUIRES a free
|
|
3429
|
+
// FRED_API_KEY; without it both tools throw an honest config error (the other 112
|
|
3430
|
+
// tools stay keyless). The key rides &api_key= ONLY. Missing observations ('.') → null.
|
|
3431
|
+
const FredSearchSeriesInput = z.object({
|
|
3432
|
+
query: z
|
|
3433
|
+
.string()
|
|
3434
|
+
.min(1)
|
|
3435
|
+
.describe(
|
|
3436
|
+
"The FRED search_text — free-text terms to discover economic series, e.g. 'unemployment rate', 'CPI', 'GDP', '10-year treasury'. Required.",
|
|
3437
|
+
),
|
|
3438
|
+
limit: z
|
|
3439
|
+
.number()
|
|
3440
|
+
.int()
|
|
3441
|
+
.min(1)
|
|
3442
|
+
.max(1000)
|
|
3443
|
+
.optional()
|
|
3444
|
+
.describe("Max series to return (default 25, max 1000). Offset-paginated."),
|
|
3445
|
+
offset: z
|
|
3446
|
+
.number()
|
|
3447
|
+
.int()
|
|
3448
|
+
.min(0)
|
|
3449
|
+
.optional()
|
|
3450
|
+
.describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
|
|
3451
|
+
});
|
|
3452
|
+
|
|
3453
|
+
const FredSeriesObservationsInput = z.object({
|
|
3454
|
+
seriesId: z
|
|
3455
|
+
.string()
|
|
3456
|
+
.regex(/^[A-Za-z0-9._-]+$/)
|
|
3457
|
+
.describe(
|
|
3458
|
+
"A FRED series id, e.g. 'GDP', 'CPIAUCSL' (CPI), 'UNRATE' (unemployment), 'DGS10' (10-yr Treasury), 'PPIACO' (PPI). Discover ids with fred_search_series. Validated ^[A-Za-z0-9._-]+$. Required.",
|
|
3459
|
+
),
|
|
3460
|
+
startDate: z
|
|
3461
|
+
.string()
|
|
3462
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
3463
|
+
.optional()
|
|
3464
|
+
.describe("Earliest observation date (YYYY-MM-DD). Maps to FRED observation_start."),
|
|
3465
|
+
endDate: z
|
|
3466
|
+
.string()
|
|
3467
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
3468
|
+
.optional()
|
|
3469
|
+
.describe("Latest observation date (YYYY-MM-DD). Maps to FRED observation_end."),
|
|
3470
|
+
limit: z
|
|
3471
|
+
.number()
|
|
3472
|
+
.int()
|
|
3473
|
+
.min(1)
|
|
3474
|
+
.max(100000)
|
|
3475
|
+
.optional()
|
|
3476
|
+
.describe("Max observations to return (default 100, max 100000). Offset-paginated."),
|
|
3477
|
+
offset: z
|
|
3478
|
+
.number()
|
|
3479
|
+
.int()
|
|
3480
|
+
.min(0)
|
|
3481
|
+
.optional()
|
|
3482
|
+
.describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
|
|
3483
|
+
sortOrder: z
|
|
3484
|
+
.enum(["asc", "desc"])
|
|
3485
|
+
.optional()
|
|
3486
|
+
.describe("Observation date order: 'asc' (oldest first, FRED default) or 'desc' (newest first)."),
|
|
3487
|
+
});
|
|
3488
|
+
|
|
3489
|
+
// ─── GSA Federal Travel Per-Diem (api.gsa.gov) — travel-cost lane ──
|
|
3490
|
+
// ADR-0050. Lodging + M&IE reimbursement ceilings by city/state OR zip for a year.
|
|
3491
|
+
// KEYLESS by default via the shared DEMO_KEY (datagovKey.ts seam); DATA_GOV_API_KEY
|
|
3492
|
+
// lifts the rate. EITHER (city+state) OR zip — both/neither ⇒ invalid_input, 0 fetch.
|
|
3493
|
+
const GsaPerdiemRatesInput = z
|
|
3494
|
+
.object({
|
|
3495
|
+
city: z
|
|
3496
|
+
.string()
|
|
3497
|
+
.regex(/^[A-Za-z .'\-]{1,60}$/)
|
|
3498
|
+
.optional()
|
|
3499
|
+
.describe(
|
|
3500
|
+
"The city name (e.g. 'Washington', 'San Francisco'). Requires `state`. Validated ^[A-Za-z .'\\-]{1,60}$. Use EITHER (city + state) OR zip — not both.",
|
|
3501
|
+
),
|
|
3502
|
+
state: z
|
|
3503
|
+
.string()
|
|
3504
|
+
.regex(/^[A-Za-z]{2}$/)
|
|
3505
|
+
.optional()
|
|
3506
|
+
.describe(
|
|
3507
|
+
"The 2-letter state/territory code (e.g. 'DC', 'CA'). Required with `city`. Validated ^[A-Za-z]{2}$.",
|
|
3508
|
+
),
|
|
3509
|
+
zip: z
|
|
3510
|
+
.string()
|
|
3511
|
+
.regex(/^\d{5}$/)
|
|
3512
|
+
.optional()
|
|
3513
|
+
.describe(
|
|
3514
|
+
"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.",
|
|
3515
|
+
),
|
|
3516
|
+
year: z
|
|
3517
|
+
.string()
|
|
3518
|
+
.regex(/^\d{4}$/)
|
|
3519
|
+
.optional()
|
|
3520
|
+
.describe(
|
|
3521
|
+
"The per-diem fiscal year (default '2025'). Validated ^\\d{4}$ (it rides in the request path).",
|
|
3522
|
+
),
|
|
3523
|
+
})
|
|
3524
|
+
.describe(
|
|
3525
|
+
"Look up GSA per-diem rates by EITHER (city + state) OR zip. Supplying both, or neither, ⇒ invalid_input.",
|
|
3526
|
+
);
|
|
3527
|
+
|
|
3528
|
+
// api_key_status takes no input — it is a pure status query over process.env.
|
|
3529
|
+
const ApiKeyStatusInput = z.object({});
|
|
3530
|
+
|
|
3384
3531
|
// ─── Tool catalog ────────────────────────────────────────────────
|
|
3385
3532
|
|
|
3386
3533
|
type ToolDef = {
|
|
@@ -4843,11 +4990,82 @@ export const TOOLS: ToolDef[] = [
|
|
|
4843
4990
|
inputSchema: CensusGeographiesByCoordinatesInput,
|
|
4844
4991
|
handler: (input) => census.geographiesByCoordinates(input),
|
|
4845
4992
|
}),
|
|
4993
|
+
// ━━━ US Census County Business Patterns — market sizing (1) ━━━ ADR-0047
|
|
4994
|
+
// ★The server's FIRST KEY-REQUIRED source: the Census Data API removed its
|
|
4995
|
+
// 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
|
|
4997
|
+
// establishments / employment / annual payroll — the demand-side market-sizing
|
|
4998
|
+
// lane. Census negative suppression sentinels (-999999999 …) map to null (never
|
|
4999
|
+
// a negative number / never 0). The 2D-array body is parsed by header name.
|
|
5000
|
+
defineTool({
|
|
5001
|
+
name: "census_business_patterns",
|
|
5002
|
+
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.",
|
|
5004
|
+
inputSchema: CensusBusinessPatternsInput,
|
|
5005
|
+
handler: (input) => censusEconomic.businessPatterns(input),
|
|
5006
|
+
}),
|
|
5007
|
+
// ━━━ FRED (Federal Reserve Economic Data) — macro context (2) ━━━ ADR-0048
|
|
5008
|
+
// ★The server's SECOND KEY-REQUIRED source: FRED has NO keyless tier, so WITHOUT
|
|
5009
|
+
// a FRED_API_KEY both tools throw an honest invalid_input config error (the other
|
|
5010
|
+
// 112 tools stay keyless). GDP/CPI/rates/unemployment/PPI — the macro backdrop for
|
|
5011
|
+
// bid escalation / market timing. A missing observation ('.') maps to null (never 0).
|
|
5012
|
+
defineTool({
|
|
5013
|
+
name: "fred_search_series",
|
|
5014
|
+
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.",
|
|
5016
|
+
inputSchema: FredSearchSeriesInput,
|
|
5017
|
+
handler: (input) => fred.searchSeries(input),
|
|
5018
|
+
}),
|
|
5019
|
+
defineTool({
|
|
5020
|
+
name: "fred_series_observations",
|
|
5021
|
+
description:
|
|
5022
|
+
"Fetch a FRED series' time series of date/value observations (FRED /fred/series/observations; api.stlouisfed.org). ★REQUIRES a free FRED_API_KEY (FRED has NO keyless tier — without it this tool THROWS an honest config error; get one at https://fred.stlouisfed.org/docs/api/api_key.html). Input: `seriesId` (required, e.g. 'GDP', 'CPIAUCSL', 'UNRATE', 'DGS10', 'PPIACO'; discover with fred_search_series), optional `startDate`/`endDate` (YYYY-MM-DD), `limit` (default 100, max 100000), `offset`, `sortOrder` (asc|desc). Returns { observations:[{ date, value }] } + honest _meta. ★MISSING-VALUE HONESTY (the crux): FRED encodes a missing observation as the literal '.', which maps to value:null (missing) — NEVER 0; a genuine reported 0 is preserved as 0. HONESTY: totalAvailable is FRED's EXACT `count` (offset pagination via hasMore/nextOffset — never fabricated); a 400 (bad seriesId / missing key) ⇒ invalid_input CARRYING FRED's error_message (never a fake empty); a genuine empty ⇒ honest empty; a 5xx ⇒ THROWS; a 200 non-JSON / non-array `observations` ⇒ schema_drift. seriesId is charclass-validated (^[A-Za-z0-9._-]+$) and dates are YYYY-MM-DD; the key rides ONLY in the &api_key= query param.",
|
|
5023
|
+
inputSchema: FredSeriesObservationsInput,
|
|
5024
|
+
handler: (input) => fred.seriesObservations(input),
|
|
5025
|
+
}),
|
|
5026
|
+
// ━━━ GSA Federal Travel Per-Diem (api.gsa.gov) — travel-cost lane (1) ━━━ ADR-0050
|
|
5027
|
+
// The lodging + M&IE reimbursement ceilings the federal government pays for official
|
|
5028
|
+
// travel, by city/state OR zip for a year. SAME host (api.gsa.gov) + SAME api.data.gov
|
|
5029
|
+
// key seam (datagovKey.ts, X-Api-Key header) as datagov_search_datasets — KEYLESS by
|
|
5030
|
+
// default via the shared DEMO_KEY, keylessMode:false. EITHER (city+state) OR zip; both
|
|
5031
|
+
// or neither ⇒ invalid_input, 0 fetch. `value` (monthly lodging) / `meals` are null-
|
|
5032
|
+
// never-0; standardRate/isOconus are STRING booleans coerced to real booleans.
|
|
5033
|
+
defineTool({
|
|
5034
|
+
name: "gsa_perdiem_rates",
|
|
5035
|
+
description:
|
|
5036
|
+
"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).",
|
|
5037
|
+
inputSchema: GsaPerdiemRatesInput,
|
|
5038
|
+
handler: (input) => gsaPerdiem.perdiemRates(input),
|
|
5039
|
+
}),
|
|
5040
|
+
// ━━━ Self-service key discovery (1) ━━━
|
|
5041
|
+
// KEYLESS. A local status query — reads process.env (+ any .env auto-loaded at
|
|
5042
|
+
// 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
|
|
5044
|
+
// discoverable without reading source or docs.
|
|
5045
|
+
defineTool({
|
|
5046
|
+
name: "api_key_status",
|
|
5047
|
+
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.",
|
|
5049
|
+
inputSchema: ApiKeyStatusInput,
|
|
5050
|
+
handler: async () => keys.apiKeyStatus(),
|
|
5051
|
+
}),
|
|
4846
5052
|
];
|
|
4847
5053
|
|
|
4848
5054
|
// ─── Server bootstrap ────────────────────────────────────────────
|
|
4849
5055
|
|
|
4850
5056
|
async function main() {
|
|
5057
|
+
// Auto-load API keys from a `.env` in the working directory BEFORE anything
|
|
5058
|
+
// reads process.env (tools read env at call time; SamGovClient below reads
|
|
5059
|
+
// SAM_GOV_API_KEY immediately). Real env wins over .env (precedence); no .env
|
|
5060
|
+
// present ⇒ zero change ⇒ byte-identical startup. We log only the COUNT — never
|
|
5061
|
+
// which keys or their values.
|
|
5062
|
+
const loadedFromEnvFile = keys.loadDotEnv();
|
|
5063
|
+
if (loadedFromEnvFile > 0) {
|
|
5064
|
+
console.error(
|
|
5065
|
+
`[mcp-sam-gov] loaded ${loadedFromEnvFile} key(s) from .env`,
|
|
5066
|
+
);
|
|
5067
|
+
}
|
|
5068
|
+
|
|
4851
5069
|
const sam = new SamGovClient({
|
|
4852
5070
|
apiKey: process.env.SAM_GOV_API_KEY?.trim() || undefined,
|
|
4853
5071
|
logger: {
|
|
@@ -4964,6 +5182,8 @@ function synthesizeDefaultMeta(
|
|
|
4964
5182
|
source = "gao.gov Legal Products RSS + decision pages (keyless)";
|
|
4965
5183
|
} else if (toolName.startsWith("fpds_")) {
|
|
4966
5184
|
source = "www.fpds.gov ezSearch ATOM (FPDS-NG, keyless)";
|
|
5185
|
+
} else if (toolName === "api_key_status") {
|
|
5186
|
+
source = "local (process.env + .env)";
|
|
4967
5187
|
} else {
|
|
4968
5188
|
source = "unknown";
|
|
4969
5189
|
}
|
package/src/snapshot.ts
CHANGED
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
* egress (an edge/WAF IP-reputation block). This module is that reader; it slots
|
|
10
10
|
* into the Phase-1 `throughPathChain` as a LOWER-priority `ResiliencePath`.
|
|
11
11
|
*
|
|
12
|
-
* ★
|
|
13
|
-
* env var `SAMGOV_SNAPSHOT_BASE_URL
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* ★DEFAULT-ON (resilience active out of the box): the snapshot base URL comes
|
|
13
|
+
* from the env var `SAMGOV_SNAPSHOT_BASE_URL`. When it is UNSET, the reader now
|
|
14
|
+
* resolves to `DEFAULT_SNAPSHOT_BASE_URL` — the public, weekly-refreshed GitHub
|
|
15
|
+
* mirror — so every user gets offline fallback with zero configuration. An
|
|
16
|
+
* operator can point at their own mirror (any custom URL) or DISABLE the
|
|
17
|
+
* fallback entirely (pure live-only) with a disable sentinel
|
|
18
|
+
* (`SAMGOV_SNAPSHOT_BASE_URL=off`); when disabled, `snapshotPath()` returns
|
|
19
|
+
* `null` — the path is simply NOT added to a source's chain, so every source
|
|
20
|
+
* stays single-path (live-only) and its output is byte-identical to today.
|
|
18
21
|
*
|
|
19
22
|
* ★POLICY BOUNDARY (ADR-0045 §"정책 경계", invariant — mirrors datasource.ts):
|
|
20
23
|
* • PUBLIC-ONLY (M3/m2): the builder writes ONLY public + redistributable data
|
|
@@ -52,27 +55,54 @@ export type SnapshotEnvelope<T = unknown> = {
|
|
|
52
55
|
/** Env-driven resilience config. Read at CALL TIME so it is togglable per call. */
|
|
53
56
|
export type ResilienceConfig = {
|
|
54
57
|
/**
|
|
55
|
-
* The snapshot mirror base URL (no trailing slash)
|
|
56
|
-
* env var is unset
|
|
58
|
+
* The snapshot mirror base URL (no trailing slash) — the hosted default when
|
|
59
|
+
* the env var is unset, a custom mirror when set to a URL, or `undefined` when
|
|
60
|
+
* DISABLED via a sentinel (`off`) ⇒ every source stays live-only.
|
|
57
61
|
*/
|
|
58
62
|
snapshotBaseUrl: string | undefined;
|
|
59
63
|
};
|
|
60
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The public, weekly-refreshed snapshot mirror (see .github/workflows/snapshots.yml);
|
|
67
|
+
* read-only public reference data. This is the DEFAULT base URL when
|
|
68
|
+
* `SAMGOV_SNAPSHOT_BASE_URL` is unset. Disable the fallback with
|
|
69
|
+
* `SAMGOV_SNAPSHOT_BASE_URL=off`.
|
|
70
|
+
*/
|
|
71
|
+
export const DEFAULT_SNAPSHOT_BASE_URL =
|
|
72
|
+
"https://raw.githubusercontent.com/cliwant/mcp-sam-gov/snapshots";
|
|
73
|
+
|
|
74
|
+
/** Case-insensitive disable sentinels: any of these (or a blank value) means
|
|
75
|
+
* "snapshot DISABLED = live-only", resolving to `undefined`. */
|
|
76
|
+
const SNAPSHOT_DISABLE_SENTINELS = new Set([
|
|
77
|
+
"off",
|
|
78
|
+
"none",
|
|
79
|
+
"false",
|
|
80
|
+
"0",
|
|
81
|
+
"disabled",
|
|
82
|
+
]);
|
|
83
|
+
|
|
61
84
|
/**
|
|
62
85
|
* Resolve `SAMGOV_SNAPSHOT_BASE_URL` at CALL TIME (never cached at module load,
|
|
63
86
|
* so a test — or an operator flipping the env — takes effect immediately, and so
|
|
64
|
-
* importing this module has zero config side effects).
|
|
65
|
-
*
|
|
66
|
-
*
|
|
87
|
+
* importing this module has zero config side effects). The resolution is
|
|
88
|
+
* DEFAULT-ON:
|
|
89
|
+
* • env UNSET ⇒ `DEFAULT_SNAPSHOT_BASE_URL` (resilience ON by default).
|
|
90
|
+
* • env is a DISABLE sentinel — case-insensitive one of `off` / `none` /
|
|
91
|
+
* `false` / `0` / `disabled`, OR blank after trim ⇒ `undefined` (snapshot
|
|
92
|
+
* disabled = live-only, byte-identical to pre-ADR output).
|
|
93
|
+
* • any other value ⇒ that custom mirror URL, trailing slash stripped so
|
|
94
|
+
* `${base}/${key}.json` is well-formed.
|
|
67
95
|
*/
|
|
68
96
|
export function resolveSnapshotBaseUrl(): string | undefined {
|
|
69
97
|
const raw = process.env.SAMGOV_SNAPSHOT_BASE_URL;
|
|
70
|
-
if (raw === undefined) return
|
|
71
|
-
const trimmed = raw.trim()
|
|
72
|
-
|
|
98
|
+
if (raw === undefined) return DEFAULT_SNAPSHOT_BASE_URL;
|
|
99
|
+
const trimmed = raw.trim();
|
|
100
|
+
if (trimmed.length === 0) return undefined;
|
|
101
|
+
if (SNAPSHOT_DISABLE_SENTINELS.has(trimmed.toLowerCase())) return undefined;
|
|
102
|
+
return trimmed.replace(/\/+$/, "");
|
|
73
103
|
}
|
|
74
104
|
|
|
75
|
-
/** The env-driven resilience config (default = snapshot
|
|
105
|
+
/** The env-driven resilience config (default = hosted snapshot mirror ON). */
|
|
76
106
|
export function resilienceConfig(): ResilienceConfig {
|
|
77
107
|
return { snapshotBaseUrl: resolveSnapshotBaseUrl() };
|
|
78
108
|
}
|
|
@@ -139,7 +169,7 @@ function parseSnapshotEnvelope<T>(
|
|
|
139
169
|
|
|
140
170
|
/**
|
|
141
171
|
* Build a `ResiliencePath` that reads the snapshot for `key` — or `null` when
|
|
142
|
-
* the snapshot mirror is
|
|
172
|
+
* the snapshot mirror is DISABLED (`SAMGOV_SNAPSHOT_BASE_URL=off`).
|
|
143
173
|
*
|
|
144
174
|
* When configured, the path fetches `${base}/${key}.json` via the shipped
|
|
145
175
|
* `getJson` with `redirect:"error"` (off-host redirect ⇒ TypeError ⇒ honest
|
|
@@ -149,16 +179,17 @@ function parseSnapshotEnvelope<T>(
|
|
|
149
179
|
* `throughPathChain` reads `path.provenance` AFTER awaiting `run()`, so the
|
|
150
180
|
* per-fetch `asOf` is captured (mirrors the `{body,provenance}` contract).
|
|
151
181
|
*
|
|
152
|
-
* ★A NULL return is how
|
|
153
|
-
*
|
|
154
|
-
*
|
|
182
|
+
* ★A NULL return is how the DISABLED (live-only) path stays byte-identical
|
|
183
|
+
* structurally: the Treasury pilot builds
|
|
184
|
+
* `[livePath, snapshotPath(key)].filter(Boolean)`, so when this returns null the
|
|
185
|
+
* chain is single-entry (live only) ⇒ `throughPathChain` fast-paths ⇒
|
|
155
186
|
* byte-identical to today.
|
|
156
187
|
*/
|
|
157
188
|
export function snapshotPath<T = unknown>(
|
|
158
189
|
key: string,
|
|
159
190
|
): ResiliencePath<T> | null {
|
|
160
191
|
const base = resolveSnapshotBaseUrl();
|
|
161
|
-
if (base === undefined) return null; //
|
|
192
|
+
if (base === undefined) return null; // DISABLED (live-only) ⇒ no path.
|
|
162
193
|
if (!SNAPSHOT_KEY_RE.test(key)) {
|
|
163
194
|
// A bad key is a programming error, not a runtime data condition — refuse to
|
|
164
195
|
// construct a path rather than build a URL that could traverse.
|