@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
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gsa-perdiem.ts — GSA Federal Travel Per-Diem lookup (`api.gsa.gov`, base
|
|
3
|
+
* `/travel/perdiem/v2`) — ADR-0050. The lodging + M&IE rate ceilings the federal
|
|
4
|
+
* government reimburses for official travel, by city/state or ZIP for a given year.
|
|
5
|
+
*
|
|
6
|
+
* WHAT IT ADDS: a NEW travel-cost lane (the per-diem authority) on the SAME
|
|
7
|
+
* `api.gsa.gov` host as datagov-catalog.ts, so it REUSES the audited `datagovKey.ts`
|
|
8
|
+
* key seam VERBATIM (keyHeader / keyModeLabel / pushKeyNote) — keyless by default via
|
|
9
|
+
* the shared DEMO_KEY, upgraded by DATA_GOV_API_KEY. The key rides ONLY in the
|
|
10
|
+
* X-Api-Key header — NEVER the URL / label / _meta / a log (the K-test). This module
|
|
11
|
+
* writes ZERO fetch/coercion/error/meta code: it REUSES `getJson` (redirect:"error",
|
|
12
|
+
* the X-Api-Key header) / `driftError` / `num`·`str` (coerce.ts) / `withMeta`·
|
|
13
|
+
* `buildMeta`, and MIRRORS the datagov-catalog schema_drift catch-ladder verbatim.
|
|
14
|
+
*
|
|
15
|
+
* ★ SSRF: the host is a compile-time literal (`GSA_PERDIEM_HOST`). The two lookup
|
|
16
|
+
* modes ride FIXED path templates; every caller value (city/state/zip/year) is
|
|
17
|
+
* charclass-validated THEN `encodeURIComponent`-escaped into a single path segment
|
|
18
|
+
* (no raw passthrough, no query steer). A post-construction hostname/protocol
|
|
19
|
+
* assertion + `redirect:"error"` lock it (fail closed on any off-host 3xx — a 3xx
|
|
20
|
+
* off api.gsa.gov could carry the X-Api-Key header away).
|
|
21
|
+
*
|
|
22
|
+
* ★ HONESTY (ADR-0050 P1–P5, live-verified 2026-07-15):
|
|
23
|
+
* [INPUT] EITHER (city + state) OR zip — supplying BOTH, or NEITHER, ⇒ invalid_input
|
|
24
|
+
* with 0 fetch (an ambiguous/empty lookup is a caller error, never a guess).
|
|
25
|
+
* [P1] the API returns the COMPLETE rate set for the lookup (no pagination) ⇒
|
|
26
|
+
* totalAvailable = the flattened row count, complete:true. NEVER fabricated.
|
|
27
|
+
* [P2] `errors` non-null ⇒ invalid_input surfacing the message (never a fake
|
|
28
|
+
* empty); a genuine no-match (rates:[] / rate:[]) ⇒ honest empty (returned:0,
|
|
29
|
+
* complete:true); a 429 (DEMO_KEY ~10/hr) ⇒ rate_limited THROW honoring
|
|
30
|
+
* Retry-After; a 5xx ⇒ upstream_unavailable THROW; a 200 non-JSON ⇒
|
|
31
|
+
* schema_drift. A DOWN service is NEVER a returned:0.
|
|
32
|
+
* [P3] `value` (monthly max lodging $) / `meals` (M&IE cap $) via `num` (null-
|
|
33
|
+
* never-0 — a genuine 0 stays 0). `standardRate` / `isOconus` are STRING
|
|
34
|
+
* booleans "true"/"false" ⇒ coerced to a real boolean (an unrecognized value
|
|
35
|
+
* ⇒ null, never a fabricated false). The months array is preserved AS-IS
|
|
36
|
+
* (never padded/fabricated to 12).
|
|
37
|
+
* [P4] `rates` / a group's `rate` / `months.month` absent or non-array ⇒
|
|
38
|
+
* driftError (never a fabricated empty).
|
|
39
|
+
*/
|
|
40
|
+
import { ToolErrorCarrier } from "./errors.js";
|
|
41
|
+
import { getJson, driftError } from "./datasource.js";
|
|
42
|
+
import { num, str } from "./coerce.js";
|
|
43
|
+
import { withMeta } from "./meta.js";
|
|
44
|
+
// The SHARED api.data.gov key seam (ADR-0010 §2). api.gsa.gov accepts the SAME
|
|
45
|
+
// DATA_GOV_API_KEY / DEMO_KEY via the X-Api-Key header — this is another consumer
|
|
46
|
+
// of the audited key discipline (a key-leak regression now fails this suite too).
|
|
47
|
+
import { keyHeader, keyModeLabel, pushKeyNote } from "./datagovKey.js";
|
|
48
|
+
// ─── Fixed endpoint (SSRF core — compile-time CONSTANTS) ──────────
|
|
49
|
+
export const GSA_PERDIEM_HOST = "api.gsa.gov";
|
|
50
|
+
const GSA_PERDIEM_BASE = "/travel/perdiem/v2";
|
|
51
|
+
// HOST+path label — surfaces in ToolError.upstreamEndpoint; the key rides ONLY in
|
|
52
|
+
// the X-Api-Key header, so no token can ever appear here.
|
|
53
|
+
const GSA_PERDIEM_LABEL = "gsa-perdiem:/travel/perdiem/v2/rates";
|
|
54
|
+
const GSA_PERDIEM_SOURCE = (mode) => `${GSA_PERDIEM_HOST} via GSA Federal Travel Per-Diem API (${mode})`;
|
|
55
|
+
// The default per-diem fiscal year (ADR-0050 — the current confirmed vintage).
|
|
56
|
+
export const DEFAULT_PERDIEM_YEAR = "2025";
|
|
57
|
+
// ─── Validation charclasses (SSRF + "verify the input" honesty) ───
|
|
58
|
+
// Each rides in a single PATH segment (encodeURIComponent-escaped), so these are
|
|
59
|
+
// belt-and-suspenders against a Zod-bypassing direct handler call.
|
|
60
|
+
const CITY_RE = /^[A-Za-z .'\-]{1,60}$/;
|
|
61
|
+
const STATE_RE = /^[A-Za-z]{2}$/;
|
|
62
|
+
const ZIP_RE = /^\d{5}$/;
|
|
63
|
+
const YEAR_RE = /^\d{4}$/;
|
|
64
|
+
// ─── Honesty notes (ADR-0050 required set) ────────────────────────
|
|
65
|
+
const RATE_MEANING_NOTE = "lodgingUsd (from the API's monthly `value`) is the MAX nightly lodging reimbursement ceiling for that month — it VARIES SEASONALLY, hence a per-month array; mealsUsd (from `meals`) is the daily Meals & Incidental Expenses (M&IE) ceiling. Both are integer US dollars. A withheld/absent figure is null, NEVER 0 (a genuine 0 is preserved).";
|
|
66
|
+
const STANDARD_RATE_NOTE = "standardRate:true means this location falls under the CONUS STANDARD rate (not an individually-set non-standard rate). standardRate/isOconus are booleans coerced from the API's string 'true'/'false'.";
|
|
67
|
+
const NO_PAGINATION_NOTE = "The per-diem API returns the COMPLETE rate set for the lookup (no pagination); totalAvailable equals the number of rows returned.";
|
|
68
|
+
// ─── STRING-boolean coercion (null-never-fabricate) ───────────────
|
|
69
|
+
/** Coerce the API's string 'true'/'false' → a real boolean; anything else ⇒ null. */
|
|
70
|
+
function strBool(v) {
|
|
71
|
+
if (typeof v === "boolean")
|
|
72
|
+
return v;
|
|
73
|
+
if (typeof v === "string") {
|
|
74
|
+
const s = v.trim().toLowerCase();
|
|
75
|
+
if (s === "true")
|
|
76
|
+
return true;
|
|
77
|
+
if (s === "false")
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Map one `months.month[]` entry → the curated per-month shape. `value` and the
|
|
84
|
+
* month `number` via `num` (null-never-0); `long` (month name) via `str`.
|
|
85
|
+
*/
|
|
86
|
+
function mapMonth(m) {
|
|
87
|
+
const it = (m ?? {});
|
|
88
|
+
return {
|
|
89
|
+
month: num(it.number),
|
|
90
|
+
monthName: str(it.long),
|
|
91
|
+
lodgingUsd: num(it.value),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// ─── SSRF-guarded fetch (fixed host + hostname assertion + redirect) ──
|
|
95
|
+
/**
|
|
96
|
+
* GET one GSA per-diem JSON resource. `path` is a fully-assembled, pre-escaped
|
|
97
|
+
* path (NO query params — the key rides in the X-Api-Key header only). Builds
|
|
98
|
+
* `https://${GSA_PERDIEM_HOST}${path}` on the FIXED host, asserts the CONSTRUCTED
|
|
99
|
+
* URL's hostname === the host over https (belt-and-suspenders), sets
|
|
100
|
+
* `redirect:"error"` (an off-host 3xx must NOT be followed — it could carry the
|
|
101
|
+
* X-Api-Key header to a foreign host), and attaches the key ONLY in the header.
|
|
102
|
+
*/
|
|
103
|
+
async function getGsaPerdiem(path) {
|
|
104
|
+
const url = `https://${GSA_PERDIEM_HOST}${path}`;
|
|
105
|
+
const built = new URL(url);
|
|
106
|
+
if (built.hostname !== GSA_PERDIEM_HOST || built.protocol !== "https:") {
|
|
107
|
+
throw new ToolErrorCarrier({
|
|
108
|
+
kind: "invalid_input",
|
|
109
|
+
message: `Constructed GSA per-diem URL host ${JSON.stringify(built.hostname)} (${built.protocol}) does not match the fixed host ${JSON.stringify(GSA_PERDIEM_HOST)} over https — refusing to fetch (SSRF safety).`,
|
|
110
|
+
retryable: false,
|
|
111
|
+
upstreamEndpoint: GSA_PERDIEM_LABEL,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// The key rides in the X-Api-Key header ONLY (never the URL/label/_meta);
|
|
115
|
+
// redirect:"error" (fail closed on any off-host 3xx).
|
|
116
|
+
return getJson(url, {
|
|
117
|
+
label: GSA_PERDIEM_LABEL,
|
|
118
|
+
headers: keyHeader(),
|
|
119
|
+
redirect: "error",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Look up GSA Federal Travel per-diem rates by EITHER (city + state) OR zip, for a
|
|
124
|
+
* given `year` (default 2025). Returns flattened rate rows (each outer state/year
|
|
125
|
+
* group × inner city/rate) + honest `_meta`: totalAvailable = the row count (no
|
|
126
|
+
* pagination — P1), lodging/meals as null-never-0 dollars (P3), standardRate/isOconus
|
|
127
|
+
* as real booleans, the months array preserved as-is. The DEMO_KEY rate disclosure
|
|
128
|
+
* rides in the notes.
|
|
129
|
+
*/
|
|
130
|
+
export async function perdiemRates(args) {
|
|
131
|
+
const label = GSA_PERDIEM_LABEL;
|
|
132
|
+
const year = args.year ?? DEFAULT_PERDIEM_YEAR;
|
|
133
|
+
// ── [INPUT] EITHER (city + state) OR zip — never both, never neither. This is a
|
|
134
|
+
// caller-shape check (0 fetch): an ambiguous or empty lookup is invalid_input,
|
|
135
|
+
// never a silent guess. ──
|
|
136
|
+
const hasCityState = args.city !== undefined || args.state !== undefined;
|
|
137
|
+
const hasZip = args.zip !== undefined;
|
|
138
|
+
if (hasCityState && hasZip) {
|
|
139
|
+
throw new ToolErrorCarrier({
|
|
140
|
+
kind: "invalid_input",
|
|
141
|
+
retryable: false,
|
|
142
|
+
message: "Provide EITHER (city + state) OR zip — not both. City/state and ZIP are two distinct lookup modes; supplying both is ambiguous.",
|
|
143
|
+
upstreamEndpoint: label,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (!hasCityState && !hasZip) {
|
|
147
|
+
throw new ToolErrorCarrier({
|
|
148
|
+
kind: "invalid_input",
|
|
149
|
+
retryable: false,
|
|
150
|
+
message: "Provide a lookup key: EITHER (city + state, e.g. city:'Washington', state:'DC') OR zip (e.g. zip:'20001').",
|
|
151
|
+
upstreamEndpoint: label,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// ── Validate + default the inputs (belt-and-suspenders behind the server Zod;
|
|
155
|
+
// a DIRECT handler call bypasses Zod). year rides in the PATH regardless. ──
|
|
156
|
+
if (!YEAR_RE.test(year)) {
|
|
157
|
+
throw new ToolErrorCarrier({
|
|
158
|
+
kind: "invalid_input",
|
|
159
|
+
retryable: false,
|
|
160
|
+
message: `Invalid year ${JSON.stringify(year)} — expected a 4-digit year (^\\d{4}$), e.g. "2025". (year rides in the request PATH; it is strictly validated.)`,
|
|
161
|
+
upstreamEndpoint: label,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
let path;
|
|
165
|
+
const filtersApplied = [`year:${year}`];
|
|
166
|
+
let lookupMode;
|
|
167
|
+
if (hasZip) {
|
|
168
|
+
const zip = args.zip;
|
|
169
|
+
if (!ZIP_RE.test(zip)) {
|
|
170
|
+
throw new ToolErrorCarrier({
|
|
171
|
+
kind: "invalid_input",
|
|
172
|
+
retryable: false,
|
|
173
|
+
message: `Invalid zip ${JSON.stringify(zip)} — expected a 5-digit ZIP code (^\\d{5}$), e.g. "20001".`,
|
|
174
|
+
upstreamEndpoint: label,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
lookupMode = `zip:${zip}`;
|
|
178
|
+
filtersApplied.push(lookupMode);
|
|
179
|
+
// Fixed template; each segment encodeURIComponent-escaped (belt-and-suspenders
|
|
180
|
+
// behind the charclass — no path injection, no query steer).
|
|
181
|
+
path = `${GSA_PERDIEM_BASE}/rates/zip/${encodeURIComponent(zip)}/year/${encodeURIComponent(year)}`;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// city + state — BOTH are required together for this mode.
|
|
185
|
+
if (args.city === undefined || args.state === undefined) {
|
|
186
|
+
throw new ToolErrorCarrier({
|
|
187
|
+
kind: "invalid_input",
|
|
188
|
+
retryable: false,
|
|
189
|
+
message: "The city lookup mode requires BOTH city AND state (e.g. city:'Washington', state:'DC'). Provide both, or use zip instead.",
|
|
190
|
+
upstreamEndpoint: label,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const city = args.city;
|
|
194
|
+
const state = args.state;
|
|
195
|
+
if (!CITY_RE.test(city)) {
|
|
196
|
+
throw new ToolErrorCarrier({
|
|
197
|
+
kind: "invalid_input",
|
|
198
|
+
retryable: false,
|
|
199
|
+
message: `Invalid city ${JSON.stringify(city)} — expected 1–60 letters/spaces/.'- (^[A-Za-z .'\\-]{1,60}$), e.g. "Washington".`,
|
|
200
|
+
upstreamEndpoint: label,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (!STATE_RE.test(state)) {
|
|
204
|
+
throw new ToolErrorCarrier({
|
|
205
|
+
kind: "invalid_input",
|
|
206
|
+
retryable: false,
|
|
207
|
+
message: `Invalid state ${JSON.stringify(state)} — expected a 2-letter state code (^[A-Za-z]{2}$), e.g. "DC", "CA".`,
|
|
208
|
+
upstreamEndpoint: label,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
lookupMode = `city:${city}, state:${state}`;
|
|
212
|
+
filtersApplied.push(`city:${city}`, `state:${state}`);
|
|
213
|
+
path = `${GSA_PERDIEM_BASE}/rates/city/${encodeURIComponent(city)}/state/${encodeURIComponent(state)}/year/${encodeURIComponent(year)}`;
|
|
214
|
+
}
|
|
215
|
+
// ── The typed catch-ladder (datagov-catalog searchDatasets shape, VERBATIM).
|
|
216
|
+
// Preserve the 429/404/5xx/400/timeout ToolErrorCarrier taxonomy FIRST
|
|
217
|
+
// (LOAD-BEARING: the DEMO_KEY-~10/hr 429→rate_limited frontier would regress to
|
|
218
|
+
// schema_drift under a broader catch); reclassify a 200 non-JSON `.json()`
|
|
219
|
+
// SyntaxError to schema_drift SECOND; bare-rethrow LAST. The host-assert
|
|
220
|
+
// ToolErrorCarrier is also rethrown first. ──
|
|
221
|
+
let body;
|
|
222
|
+
try {
|
|
223
|
+
body = await getGsaPerdiem(path);
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
if (e instanceof ToolErrorCarrier)
|
|
227
|
+
throw e;
|
|
228
|
+
if (e instanceof SyntaxError)
|
|
229
|
+
throw driftError(label, "GSA per-diem returned a non-JSON body at HTTP 200 — schema drift.");
|
|
230
|
+
throw e;
|
|
231
|
+
}
|
|
232
|
+
const b = (body ?? {});
|
|
233
|
+
// ── [P2] `errors` non-null ⇒ a lookup problem ⇒ invalid_input surfacing the
|
|
234
|
+
// message (NEVER a fake empty — swallowing this as empty ⇒ RED). ──
|
|
235
|
+
if (b.errors !== null && b.errors !== undefined) {
|
|
236
|
+
const msg = typeof b.errors === "string" ? b.errors : JSON.stringify(b.errors);
|
|
237
|
+
throw new ToolErrorCarrier({
|
|
238
|
+
kind: "invalid_input",
|
|
239
|
+
retryable: false,
|
|
240
|
+
message: `GSA per-diem reported a lookup error for ${lookupMode} (year ${year}): ${msg}`,
|
|
241
|
+
upstreamEndpoint: label,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// ── [P4] `rates` MUST be an array (a missing/object/null rates is drift, never a
|
|
245
|
+
// fabricated empty — a TypeError must never mask drift as upstream_unavailable). ──
|
|
246
|
+
if (!Array.isArray(b.rates)) {
|
|
247
|
+
throw driftError(label, "GSA per-diem shape drift — response.rates must be an array.");
|
|
248
|
+
}
|
|
249
|
+
// ── Flatten: each outer state/year group × its inner rate[]. ──
|
|
250
|
+
const rows = [];
|
|
251
|
+
for (const group of b.rates) {
|
|
252
|
+
const g = (group ?? {});
|
|
253
|
+
// [P4] a group's `rate` MUST be an array (never a fabricated empty).
|
|
254
|
+
if (!Array.isArray(g.rate)) {
|
|
255
|
+
throw driftError(label, "GSA per-diem shape drift — a rates[].rate must be an array.");
|
|
256
|
+
}
|
|
257
|
+
const gState = str(g.state);
|
|
258
|
+
const gYear = num(g.year);
|
|
259
|
+
const gOconus = strBool(g.isOconus);
|
|
260
|
+
for (const rate of g.rate) {
|
|
261
|
+
const r = (rate ?? {});
|
|
262
|
+
const monthsObj = (r.months ?? {});
|
|
263
|
+
// [P4] months.month MUST be an array (never padded/fabricated to 12).
|
|
264
|
+
if (!Array.isArray(monthsObj.month)) {
|
|
265
|
+
throw driftError(label, "GSA per-diem shape drift — a rate's months.month must be an array.");
|
|
266
|
+
}
|
|
267
|
+
rows.push({
|
|
268
|
+
city: str(r.city),
|
|
269
|
+
county: str(r.county),
|
|
270
|
+
state: gState,
|
|
271
|
+
zip: str(r.zip),
|
|
272
|
+
year: gYear,
|
|
273
|
+
isOconus: gOconus,
|
|
274
|
+
standardRate: strBool(r.standardRate),
|
|
275
|
+
mealsUsd: num(r.meals),
|
|
276
|
+
monthlyLodgingUsd: monthsObj.month.map(mapMonth),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const returned = rows.length;
|
|
281
|
+
const notes = [RATE_MEANING_NOTE, STANDARD_RATE_NOTE, NO_PAGINATION_NOTE];
|
|
282
|
+
pushKeyNote(notes);
|
|
283
|
+
return withMeta({ rates: rows }, {
|
|
284
|
+
source: GSA_PERDIEM_SOURCE(keyModeLabel()),
|
|
285
|
+
keylessMode: false, // keyed via the api.data.gov X-Api-Key (DEMO_KEY default)
|
|
286
|
+
returned,
|
|
287
|
+
// [P1] the COMPLETE set for the lookup (no pagination) ⇒ totalAvailable = the
|
|
288
|
+
// row count; complete is DERIVED true by buildMeta (returned === total).
|
|
289
|
+
totalAvailable: returned,
|
|
290
|
+
filtersApplied,
|
|
291
|
+
filtersDropped: [],
|
|
292
|
+
fieldsUnavailable: [],
|
|
293
|
+
notes,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=gsa-perdiem.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gsa-perdiem.js","sourceRoot":"","sources":["../src/gsa-perdiem.ts"],"names":[],"mappings":"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"}
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cliwant/mcp-sam-gov/keys — API-key discovery + `.env` auto-loading.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists
|
|
5
|
+
* ----------------
|
|
6
|
+
* The server rides 31 federal sources. MOST are fully keyless. But the set of
|
|
7
|
+
* *optional* keys (raise a rate limit, unlock one filter) plus the two *required*
|
|
8
|
+
* keys (Census business-patterns, FRED) has grown to the point where a user — or
|
|
9
|
+
* the AI driving the server — cannot tell, without reading source code:
|
|
10
|
+
* - which env var each source reads,
|
|
11
|
+
* - whether a key is REQUIRED or merely OPTIONAL,
|
|
12
|
+
* - where to get one (free), and
|
|
13
|
+
* - whether it is currently configured.
|
|
14
|
+
*
|
|
15
|
+
* `apiKeyStatus()` answers all four, truthfully, WITHOUT ever revealing a key's
|
|
16
|
+
* value (only a `currentlySet` boolean). `loadDotEnv()` lets a user configure
|
|
17
|
+
* keys ONCE in a `.env` file instead of the host's env block.
|
|
18
|
+
*
|
|
19
|
+
* Grounding: every `envVar` below is the exact string the code reads via
|
|
20
|
+
* `process.env.<NAME>` — DATA_GOV_API_KEY (datagovKey.ts), SAM_GOV_API_KEY
|
|
21
|
+
* (server.ts), BLS_API_KEY (bls.ts), NVD_API_KEY (nvd.ts), SOCRATA_APP_TOKEN
|
|
22
|
+
* (socrata.ts), CENSUS_API_KEY (census-economic.ts), FRED_API_KEY (fred.ts).
|
|
23
|
+
* No invented keys, sources, or signup URLs.
|
|
24
|
+
*/
|
|
25
|
+
/** One registry entry describing a single API key the server can use. */
|
|
26
|
+
export type KeyRegistryEntry = {
|
|
27
|
+
/** The exact `process.env.<NAME>` the code reads. */
|
|
28
|
+
envVar: string;
|
|
29
|
+
/** Human-readable source(s) this key affects. */
|
|
30
|
+
sources: string[];
|
|
31
|
+
/** true ⇒ the source has NO keyless tier (the tool throws without it). */
|
|
32
|
+
required: boolean;
|
|
33
|
+
/** Free signup URL (the user creates the account — this is their step). */
|
|
34
|
+
signupUrl: string;
|
|
35
|
+
/** What setting the key unlocks (higher limit / a filter / a whole tool). */
|
|
36
|
+
unlocks: string;
|
|
37
|
+
/** Extra honesty note (keyless fallback, precedence, scope). */
|
|
38
|
+
note: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The 7 keys the server reads — code-grounded, no inventions.
|
|
42
|
+
*
|
|
43
|
+
* REQUIRED (2): CENSUS_API_KEY, FRED_API_KEY — those sources have no keyless
|
|
44
|
+
* tier, so the tool throws without them. OPTIONAL (5): everything else works
|
|
45
|
+
* keyless; a key only raises a rate limit or unlocks a single filter.
|
|
46
|
+
*/
|
|
47
|
+
export declare const KEY_REGISTRY: readonly KeyRegistryEntry[];
|
|
48
|
+
/** Per-key status: the registry entry + a `currentlySet` boolean. NEVER the value. */
|
|
49
|
+
export type KeyStatus = KeyRegistryEntry & {
|
|
50
|
+
currentlySet: boolean;
|
|
51
|
+
};
|
|
52
|
+
/** The `apiKeyStatus()` result shape. */
|
|
53
|
+
export type ApiKeyStatusResult = {
|
|
54
|
+
keys: KeyStatus[];
|
|
55
|
+
/** envVars of REQUIRED keys not currently set (empty ⇒ all required keys present). */
|
|
56
|
+
requiredMissing: string[];
|
|
57
|
+
/** envVars of OPTIONAL keys not currently set. */
|
|
58
|
+
optionalMissing: string[];
|
|
59
|
+
/** Every key here is free to obtain. */
|
|
60
|
+
allKeysFree: boolean;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Report which API keys the server can use and whether each is configured.
|
|
64
|
+
*
|
|
65
|
+
* SECURITY: the returned object carries ONLY a `currentlySet` boolean per key —
|
|
66
|
+
* the key's VALUE is NEVER read into the output. (`isSet` inspects the value to
|
|
67
|
+
* compute the boolean, but the value itself never leaves this function.)
|
|
68
|
+
*/
|
|
69
|
+
export declare function apiKeyStatus(): ApiKeyStatusResult;
|
|
70
|
+
/**
|
|
71
|
+
* MINIMAL, dependency-free `.env` loader.
|
|
72
|
+
*
|
|
73
|
+
* Reads `${cwd||process.cwd()}/.env` if present and sets `process.env[KEY]` for
|
|
74
|
+
* each `KEY=VALUE` line — but ONLY if that key is not already set, so a real
|
|
75
|
+
* environment variable always wins over `.env` (standard precedence). Supports
|
|
76
|
+
* `export KEY=VALUE`, `#` comments, blank lines, and surrounding single/double
|
|
77
|
+
* quotes on the value. NEVER throws: a missing file returns 0 (⇒ byte-identical
|
|
78
|
+
* startup), and a malformed line is skipped rather than fatal.
|
|
79
|
+
*
|
|
80
|
+
* @returns the number of vars newly set into process.env.
|
|
81
|
+
*/
|
|
82
|
+
export declare function loadDotEnv(cwd?: string): number;
|
|
83
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,yEAAyE;AACzE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,SAAS,gBAAgB,EAiE1C,CAAC;AAQX,sFAAsF;AACtF,MAAM,MAAM,SAAS,GAAG,gBAAgB,GAAG;IAAE,YAAY,EAAE,OAAO,CAAA;CAAE,CAAC;AAErE,yCAAyC;AACzC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,sFAAsF;IACtF,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,kDAAkD;IAClD,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,wCAAwC;IACxC,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,kBAAkB,CAYjD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CA4C/C"}
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cliwant/mcp-sam-gov/keys — API-key discovery + `.env` auto-loading.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists
|
|
5
|
+
* ----------------
|
|
6
|
+
* The server rides 31 federal sources. MOST are fully keyless. But the set of
|
|
7
|
+
* *optional* keys (raise a rate limit, unlock one filter) plus the two *required*
|
|
8
|
+
* keys (Census business-patterns, FRED) has grown to the point where a user — or
|
|
9
|
+
* the AI driving the server — cannot tell, without reading source code:
|
|
10
|
+
* - which env var each source reads,
|
|
11
|
+
* - whether a key is REQUIRED or merely OPTIONAL,
|
|
12
|
+
* - where to get one (free), and
|
|
13
|
+
* - whether it is currently configured.
|
|
14
|
+
*
|
|
15
|
+
* `apiKeyStatus()` answers all four, truthfully, WITHOUT ever revealing a key's
|
|
16
|
+
* value (only a `currentlySet` boolean). `loadDotEnv()` lets a user configure
|
|
17
|
+
* keys ONCE in a `.env` file instead of the host's env block.
|
|
18
|
+
*
|
|
19
|
+
* Grounding: every `envVar` below is the exact string the code reads via
|
|
20
|
+
* `process.env.<NAME>` — DATA_GOV_API_KEY (datagovKey.ts), SAM_GOV_API_KEY
|
|
21
|
+
* (server.ts), BLS_API_KEY (bls.ts), NVD_API_KEY (nvd.ts), SOCRATA_APP_TOKEN
|
|
22
|
+
* (socrata.ts), CENSUS_API_KEY (census-economic.ts), FRED_API_KEY (fred.ts).
|
|
23
|
+
* No invented keys, sources, or signup URLs.
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
/**
|
|
28
|
+
* The 7 keys the server reads — code-grounded, no inventions.
|
|
29
|
+
*
|
|
30
|
+
* REQUIRED (2): CENSUS_API_KEY, FRED_API_KEY — those sources have no keyless
|
|
31
|
+
* tier, so the tool throws without them. OPTIONAL (5): everything else works
|
|
32
|
+
* keyless; a key only raises a rate limit or unlocks a single filter.
|
|
33
|
+
*/
|
|
34
|
+
export const KEY_REGISTRY = [
|
|
35
|
+
{
|
|
36
|
+
envVar: "DATA_GOV_API_KEY",
|
|
37
|
+
sources: [
|
|
38
|
+
"api.data.gov keyed sources: Regulations.gov, Congress.gov, GovInfo, Federal Audit Clearinghouse (FAC), data.gov catalog, GSA per-diem",
|
|
39
|
+
],
|
|
40
|
+
required: false,
|
|
41
|
+
signupUrl: "https://api.data.gov/signup/",
|
|
42
|
+
unlocks: "higher rate limits on the api.data.gov keyed sources (lifts the shared DEMO_KEY ~30/hr cap to ~1,000/hr)",
|
|
43
|
+
note: "Keyless by default via the public DEMO_KEY; a key only raises the shared hourly quota. (NPPES, CMS, and Federal Register are keyless on their own hosts and do NOT use this key.)",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
envVar: "SAM_GOV_API_KEY",
|
|
47
|
+
sources: ["SAM.gov opportunities"],
|
|
48
|
+
required: false,
|
|
49
|
+
signupUrl: "https://open.gsa.gov/api/get-opportunities-public-api/",
|
|
50
|
+
unlocks: "the authenticated v2 opportunity search + the organization-name filter",
|
|
51
|
+
note: "Keyless HAL endpoint works without it; a key enables the keyed v2 path and org-name filtering. Register at sam.gov / api.sam.gov.",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
envVar: "BLS_API_KEY",
|
|
55
|
+
sources: ["Bureau of Labor Statistics (BLS)"],
|
|
56
|
+
required: false,
|
|
57
|
+
signupUrl: "https://data.bls.gov/registrationEngine/",
|
|
58
|
+
unlocks: "the BLS v2 tier (~500 queries/day, 50 series/query, ~20-year span) vs keyless v1 (~25 queries/day)",
|
|
59
|
+
note: "Keyless v1 works out of the box; a key upgrades to the higher v2 limits.",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
envVar: "NVD_API_KEY",
|
|
63
|
+
sources: ["NIST NVD (cve_lookup)"],
|
|
64
|
+
required: false,
|
|
65
|
+
signupUrl: "https://nvd.nist.gov/developers/request-an-api-key",
|
|
66
|
+
unlocks: "a higher NVD rate limit",
|
|
67
|
+
note: "Keyless by default; a key lifts the request rate limit.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
envVar: "SOCRATA_APP_TOKEN",
|
|
71
|
+
sources: ["Socrata (state/city open-data portals)"],
|
|
72
|
+
required: false,
|
|
73
|
+
signupUrl: "https://evergreen.data.socrata.com/signup",
|
|
74
|
+
unlocks: "higher Socrata throttling limits",
|
|
75
|
+
note: "Keyless by default; a token raises the per-host throttle. Any Socrata portal's developer settings issues one.",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
envVar: "CENSUS_API_KEY",
|
|
79
|
+
sources: ["US Census (census_business_patterns)"],
|
|
80
|
+
required: true,
|
|
81
|
+
signupUrl: "https://api.census.gov/data/key_signup.html",
|
|
82
|
+
unlocks: "the census_business_patterns tool (there is no keyless tier — it throws without a key)",
|
|
83
|
+
note: "REQUIRED: the Census economic API has no keyless access.",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
envVar: "FRED_API_KEY",
|
|
87
|
+
sources: ["FRED (fred_search_series, fred_series_observations)"],
|
|
88
|
+
required: true,
|
|
89
|
+
signupUrl: "https://fred.stlouisfed.org/docs/api/api_key.html",
|
|
90
|
+
unlocks: "the 2 FRED tools (there is no keyless tier — they throw without a key)",
|
|
91
|
+
note: "REQUIRED: the FRED API has no keyless access.",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
/** true iff the env var is set to a non-empty (after-trim) string. */
|
|
95
|
+
function isSet(envVar) {
|
|
96
|
+
const v = process.env[envVar];
|
|
97
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Report which API keys the server can use and whether each is configured.
|
|
101
|
+
*
|
|
102
|
+
* SECURITY: the returned object carries ONLY a `currentlySet` boolean per key —
|
|
103
|
+
* the key's VALUE is NEVER read into the output. (`isSet` inspects the value to
|
|
104
|
+
* compute the boolean, but the value itself never leaves this function.)
|
|
105
|
+
*/
|
|
106
|
+
export function apiKeyStatus() {
|
|
107
|
+
const keys = KEY_REGISTRY.map((k) => ({
|
|
108
|
+
...k,
|
|
109
|
+
currentlySet: isSet(k.envVar),
|
|
110
|
+
}));
|
|
111
|
+
const requiredMissing = keys
|
|
112
|
+
.filter((k) => k.required && !k.currentlySet)
|
|
113
|
+
.map((k) => k.envVar);
|
|
114
|
+
const optionalMissing = keys
|
|
115
|
+
.filter((k) => !k.required && !k.currentlySet)
|
|
116
|
+
.map((k) => k.envVar);
|
|
117
|
+
return { keys, requiredMissing, optionalMissing, allKeysFree: true };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* MINIMAL, dependency-free `.env` loader.
|
|
121
|
+
*
|
|
122
|
+
* Reads `${cwd||process.cwd()}/.env` if present and sets `process.env[KEY]` for
|
|
123
|
+
* each `KEY=VALUE` line — but ONLY if that key is not already set, so a real
|
|
124
|
+
* environment variable always wins over `.env` (standard precedence). Supports
|
|
125
|
+
* `export KEY=VALUE`, `#` comments, blank lines, and surrounding single/double
|
|
126
|
+
* quotes on the value. NEVER throws: a missing file returns 0 (⇒ byte-identical
|
|
127
|
+
* startup), and a malformed line is skipped rather than fatal.
|
|
128
|
+
*
|
|
129
|
+
* @returns the number of vars newly set into process.env.
|
|
130
|
+
*/
|
|
131
|
+
export function loadDotEnv(cwd) {
|
|
132
|
+
const path = join(cwd ?? process.cwd(), ".env");
|
|
133
|
+
let text;
|
|
134
|
+
try {
|
|
135
|
+
text = readFileSync(path, "utf8");
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Missing / unreadable .env ⇒ zero change. This is the common case and
|
|
139
|
+
// MUST be a no-op so startup is byte-identical when no .env exists.
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
let loaded = 0;
|
|
143
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
144
|
+
const line = rawLine.trim();
|
|
145
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
146
|
+
continue;
|
|
147
|
+
// Optional `export ` prefix.
|
|
148
|
+
const body = line.startsWith("export ")
|
|
149
|
+
? line.slice("export ".length).trim()
|
|
150
|
+
: line;
|
|
151
|
+
const eq = body.indexOf("=");
|
|
152
|
+
if (eq <= 0)
|
|
153
|
+
continue; // no `=`, or empty key ⇒ skip (malformed).
|
|
154
|
+
const key = body.slice(0, eq).trim();
|
|
155
|
+
if (!key)
|
|
156
|
+
continue;
|
|
157
|
+
let value = body.slice(eq + 1).trim();
|
|
158
|
+
// Strip a single matching pair of surrounding quotes.
|
|
159
|
+
if (value.length >= 2 &&
|
|
160
|
+
((value.startsWith('"') && value.endsWith('"')) ||
|
|
161
|
+
(value.startsWith("'") && value.endsWith("'")))) {
|
|
162
|
+
value = value.slice(1, -1);
|
|
163
|
+
}
|
|
164
|
+
// Precedence: if the key is already set in the real environment, it wins —
|
|
165
|
+
// we set `process.env[key]` ONLY when it is not already present.
|
|
166
|
+
if (process.env[key] !== undefined)
|
|
167
|
+
continue;
|
|
168
|
+
process.env[key] = value;
|
|
169
|
+
loaded++;
|
|
170
|
+
}
|
|
171
|
+
return loaded;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=keys.js.map
|
package/dist/keys.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;GAgBG;AAQH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,YAAY,EAKb,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;GAgBG;AAQH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,YAAY,EAKb,MAAM,oBAAoB,CAAC;AA46G5B,KAAK,OAAO,GAAG;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,CAAC,CAAC,UAAU,CAAC;IAO1B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE;QAAE,GAAG,EAAE,YAAY,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxE,CAAC;AAoBF,eAAO,MAAM,KAAK,EAAE,OAAO,EAg9C1B,CAAC;AA6IF,wBAAsB,OAAO,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,YAAY,GAChB,OAAO,CAAC,OAAO,CAAC,CAalB"}
|