@cliwant/mcp-sam-gov 1.0.0 → 1.1.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/dist/fred.d.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * fred.ts — FRED (Federal Reserve Economic Data, St. Louis Fed) — the MACRO
3
+ * CONTEXT lane (ADR-0048, Wave-4 source #2). GDP · CPI · interest rates ·
4
+ * unemployment · PPI … — the economy-wide backdrop for B2G bid escalation and
5
+ * market-timing that no contract/spending source carries.
6
+ *
7
+ * ★ THIS IS THE SERVER'S SECOND KEY-REQUIRED SOURCE (Census CBP was the first).
8
+ * FRED has NO keyless tier: every request needs `&api_key=`, and a missing/bad
9
+ * key returns HTTP 400 `{error_code, error_message}`. So, honestly: with NO
10
+ * `FRED_API_KEY` these two tools THROW an `invalid_input` config error BEFORE any
11
+ * fetch (never a fake-empty, never a keyless-pretend). The other 112 tools stay
12
+ * keyless — this key is scoped to this one source. (Contrast the OPTIONAL keys of
13
+ * datagov/bls/nvd, which lift a tier but are not required.)
14
+ *
15
+ * This module MIRRORS the census-economic.ts optional-key precedent: a `fredApiKey()`
16
+ * env seam, a pre-fetch `invalid_input` THROW when unset, the fixed-host SSRF assert
17
+ * + `redirect:"error"`, and the missing-sentinel→null idiom (Census's negative
18
+ * suppression sentinel there; FRED's `value === "."` here — the BLS `"-"` lineage).
19
+ * It writes ZERO coercion/meta code of its own: it REUSES `getJson` (the shared
20
+ * fetch envelope) / `driftError` / `num`·`str` (coerce.ts, null-never-0/empty) /
21
+ * `withMeta`·`buildMeta` (offset pagination via count-exact totals).
22
+ *
23
+ * GET https://api.stlouisfed.org/fred/series/search
24
+ * ?search_text=<q>&limit=&offset=&api_key=<KEY>&file_type=json
25
+ * → { seriess:[{ id,title,frequency,frequency_short,units,seasonal_adjustment,
26
+ * observation_start,observation_end,last_updated,popularity,notes }],
27
+ * count, limit, offset }
28
+ *
29
+ * GET https://api.stlouisfed.org/fred/series/observations
30
+ * ?series_id=<id>&observation_start=&observation_end=&limit=&offset=
31
+ * &sort_order=&api_key=<KEY>&file_type=json
32
+ * → { observations:[{ date, value }], count, … } ★value === "." ⇒ missing (null)
33
+ *
34
+ * ★ HONESTY (ADR-0048 P1–P5):
35
+ * [KEY] no key ⇒ invalid_input THROW pre-fetch (0 fetch); the message names
36
+ * FRED_API_KEY + the free-signup URL. A 400 carrying `{error_message}` (a
37
+ * bad series_id / expired key) ⇒ reclassified to invalid_input CARRYING the
38
+ * FRED error_message — honestly reported, never a fake empty.
39
+ * [P1] both endpoints report `count` (the total) ⇒ totalAvailable = num(count)
40
+ * EXACT; offset pagination (hasMore = offset+returned < count, nextOffset).
41
+ * NEVER fabricated (RED if totalAvailable = returned).
42
+ * [P3] ★the missing crux: an observation `value === "."` ⇒ **null** (FRED's
43
+ * missing sentinel — the BLS `"-"` lineage), NEVER 0. A genuine "0" ⇒ 0.
44
+ * [P2] a 400 ⇒ invalid_input (carrying error_message); a genuine no-match
45
+ * (seriess:[] / observations:[]) ⇒ honest empty; a 5xx ⇒ upstream_unavailable
46
+ * THROW; a 200 non-JSON ⇒ schema_drift.
47
+ * [P4] a body whose `seriess` / `observations` is absent or non-array ⇒ driftError
48
+ * (never a fabricated empty).
49
+ * [SSRF] fixed host `api.stlouisfed.org`; `series_id` charclass `^[A-Za-z0-9._-]+$`;
50
+ * dates `^\d{4}-\d{2}-\d{2}$`; sort_order enum {asc,desc}. All VALUES ride
51
+ * URLSearchParams; the key rides `&api_key=` ONLY — never a label/_meta/note
52
+ * (the K-test).
53
+ */
54
+ import { num } from "./coerce.js";
55
+ import { type MetaBundle } from "./meta.js";
56
+ export { num };
57
+ export declare const FRED_HOST = "api.stlouisfed.org";
58
+ /** Read FRED_API_KEY from env; trim; return the value or undefined (unset/blank). */
59
+ export declare function fredApiKey(): string | undefined;
60
+ /**
61
+ * num(), but map FRED's missing-observation sentinel `"."` → null (missing). A
62
+ * genuine "0" stays 0 (num("0") === 0); a real numeric string parses. This is the
63
+ * BLS `"-"` lineage — a data-absence marker, never a fabricated 0.
64
+ */
65
+ export declare function fredValue(v: unknown): number | null;
66
+ export type FredSearchSeriesArgs = {
67
+ query?: string;
68
+ limit?: number;
69
+ offset?: number;
70
+ };
71
+ export type FredSeries = {
72
+ id: string | null;
73
+ title: string | null;
74
+ frequency: string | null;
75
+ frequencyShort: string | null;
76
+ units: string | null;
77
+ seasonalAdjustment: string | null;
78
+ observationStart: string | null;
79
+ observationEnd: string | null;
80
+ lastUpdated: string | null;
81
+ popularity: number | null;
82
+ };
83
+ /**
84
+ * Search FRED series (`/fred/series/search`) by `search_text` → curated series
85
+ * rows + honest `_meta`. REQUIRES FRED_API_KEY (throws invalid_input pre-fetch when
86
+ * unset). totalAvailable = FRED's exact `count`; offset pagination.
87
+ */
88
+ export declare function searchSeries(args: FredSearchSeriesArgs): Promise<MetaBundle>;
89
+ export type FredSeriesObservationsArgs = {
90
+ seriesId?: string;
91
+ startDate?: string;
92
+ endDate?: string;
93
+ limit?: number;
94
+ offset?: number;
95
+ sortOrder?: string;
96
+ };
97
+ export type FredObservation = {
98
+ date: string | null;
99
+ value: number | null;
100
+ };
101
+ /**
102
+ * Fetch a FRED series' time series (`/fred/series/observations`) → date/value rows
103
+ * + honest `_meta`. REQUIRES FRED_API_KEY (throws invalid_input pre-fetch when
104
+ * unset). ★A missing observation (`value === "."`) maps to null, never 0.
105
+ * totalAvailable = FRED's exact `count`; offset pagination.
106
+ */
107
+ export declare function seriesObservations(args: FredSeriesObservationsArgs): Promise<MetaBundle>;
108
+ //# sourceMappingURL=fred.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fred.d.ts","sourceRoot":"","sources":["../src/fred.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAIH,OAAO,EAAE,GAAG,EAAO,MAAM,aAAa,CAAC;AACvC,OAAO,EAAY,KAAK,UAAU,EAAqB,MAAM,WAAW,CAAC;AAKzE,OAAO,EAAE,GAAG,EAAE,CAAC;AAGf,eAAO,MAAM,SAAS,uBAAuB,CAAC;AA2B9C,qFAAqF;AACrF,wBAAgB,UAAU,IAAI,MAAM,GAAG,SAAS,CAI/C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAGnD;AAgFD,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,UAAU,CAAC,CA4FrB;AAGD,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,0BAA0B,GAC/B,OAAO,CAAC,UAAU,CAAC,CAsHrB"}
package/dist/fred.js ADDED
@@ -0,0 +1,373 @@
1
+ /**
2
+ * fred.ts — FRED (Federal Reserve Economic Data, St. Louis Fed) — the MACRO
3
+ * CONTEXT lane (ADR-0048, Wave-4 source #2). GDP · CPI · interest rates ·
4
+ * unemployment · PPI … — the economy-wide backdrop for B2G bid escalation and
5
+ * market-timing that no contract/spending source carries.
6
+ *
7
+ * ★ THIS IS THE SERVER'S SECOND KEY-REQUIRED SOURCE (Census CBP was the first).
8
+ * FRED has NO keyless tier: every request needs `&api_key=`, and a missing/bad
9
+ * key returns HTTP 400 `{error_code, error_message}`. So, honestly: with NO
10
+ * `FRED_API_KEY` these two tools THROW an `invalid_input` config error BEFORE any
11
+ * fetch (never a fake-empty, never a keyless-pretend). The other 112 tools stay
12
+ * keyless — this key is scoped to this one source. (Contrast the OPTIONAL keys of
13
+ * datagov/bls/nvd, which lift a tier but are not required.)
14
+ *
15
+ * This module MIRRORS the census-economic.ts optional-key precedent: a `fredApiKey()`
16
+ * env seam, a pre-fetch `invalid_input` THROW when unset, the fixed-host SSRF assert
17
+ * + `redirect:"error"`, and the missing-sentinel→null idiom (Census's negative
18
+ * suppression sentinel there; FRED's `value === "."` here — the BLS `"-"` lineage).
19
+ * It writes ZERO coercion/meta code of its own: it REUSES `getJson` (the shared
20
+ * fetch envelope) / `driftError` / `num`·`str` (coerce.ts, null-never-0/empty) /
21
+ * `withMeta`·`buildMeta` (offset pagination via count-exact totals).
22
+ *
23
+ * GET https://api.stlouisfed.org/fred/series/search
24
+ * ?search_text=<q>&limit=&offset=&api_key=<KEY>&file_type=json
25
+ * → { seriess:[{ id,title,frequency,frequency_short,units,seasonal_adjustment,
26
+ * observation_start,observation_end,last_updated,popularity,notes }],
27
+ * count, limit, offset }
28
+ *
29
+ * GET https://api.stlouisfed.org/fred/series/observations
30
+ * ?series_id=<id>&observation_start=&observation_end=&limit=&offset=
31
+ * &sort_order=&api_key=<KEY>&file_type=json
32
+ * → { observations:[{ date, value }], count, … } ★value === "." ⇒ missing (null)
33
+ *
34
+ * ★ HONESTY (ADR-0048 P1–P5):
35
+ * [KEY] no key ⇒ invalid_input THROW pre-fetch (0 fetch); the message names
36
+ * FRED_API_KEY + the free-signup URL. A 400 carrying `{error_message}` (a
37
+ * bad series_id / expired key) ⇒ reclassified to invalid_input CARRYING the
38
+ * FRED error_message — honestly reported, never a fake empty.
39
+ * [P1] both endpoints report `count` (the total) ⇒ totalAvailable = num(count)
40
+ * EXACT; offset pagination (hasMore = offset+returned < count, nextOffset).
41
+ * NEVER fabricated (RED if totalAvailable = returned).
42
+ * [P3] ★the missing crux: an observation `value === "."` ⇒ **null** (FRED's
43
+ * missing sentinel — the BLS `"-"` lineage), NEVER 0. A genuine "0" ⇒ 0.
44
+ * [P2] a 400 ⇒ invalid_input (carrying error_message); a genuine no-match
45
+ * (seriess:[] / observations:[]) ⇒ honest empty; a 5xx ⇒ upstream_unavailable
46
+ * THROW; a 200 non-JSON ⇒ schema_drift.
47
+ * [P4] a body whose `seriess` / `observations` is absent or non-array ⇒ driftError
48
+ * (never a fabricated empty).
49
+ * [SSRF] fixed host `api.stlouisfed.org`; `series_id` charclass `^[A-Za-z0-9._-]+$`;
50
+ * dates `^\d{4}-\d{2}-\d{2}$`; sort_order enum {asc,desc}. All VALUES ride
51
+ * URLSearchParams; the key rides `&api_key=` ONLY — never a label/_meta/note
52
+ * (the K-test).
53
+ */
54
+ import { ToolErrorCarrier } from "./errors.js";
55
+ import { getJson, driftError } from "./datasource.js";
56
+ import { num, str } from "./coerce.js";
57
+ import { withMeta } from "./meta.js";
58
+ // Re-export the shared honesty coercion (single audited copy in ./coerce.js —
59
+ // ADR-0005 v2 FIX-C) so a `num` regression fails together across sources. NO local
60
+ // num/str; the `"."`→null map is a WRAPPER around num, not a fork.
61
+ export { num };
62
+ // ─── SSRF core: the single fixed host + base path ─────────────────
63
+ export const FRED_HOST = "api.stlouisfed.org";
64
+ const FRED_SEARCH_PATH = "/fred/series/search";
65
+ const FRED_OBS_PATH = "/fred/series/observations";
66
+ // HOST+path labels — surface in ToolError.upstreamEndpoint; the key rides ONLY in
67
+ // the &api_key= query param, so no token can ever appear here.
68
+ const FRED_SEARCH_LABEL = "fred:/fred/series/search";
69
+ const FRED_OBS_LABEL = "fred:/fred/series/observations";
70
+ // ─── Validation charclasses (SSRF + "verify the input" honesty) ───
71
+ const SERIES_ID_RE = /^[A-Za-z0-9._-]+$/; // FRED series ids: GDP, CPIAUCSL, DGS10 …
72
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; // YYYY-MM-DD observation bounds
73
+ const SORT_ORDERS = new Set(["asc", "desc"]);
74
+ const DEFAULT_SEARCH_LIMIT = 25;
75
+ const MAX_SEARCH_LIMIT = 1000;
76
+ const DEFAULT_OBS_LIMIT = 100;
77
+ const MAX_OBS_LIMIT = 100000;
78
+ // ─── Honesty notes (ADR-0048 required set) ────────────────────────
79
+ const KEY_REQUIRED_NOTE = "This source REQUIRES a free FRED_API_KEY (FRED has no keyless tier). The key is sent ONLY as the &api_key= query parameter to api.stlouisfed.org and is NEVER logged, echoed, or placed in this response.";
80
+ const MISSING_VALUE_NOTE = "FRED encodes a MISSING observation as the literal '.' — such values are mapped to null (missing), NEVER 0. A genuine reported 0 is preserved as 0.";
81
+ const COUNT_TOTAL_NOTE = "totalAvailable is FRED's exact reported `count` for the query; page with limit/offset (hasMore/nextOffset are derived from it, never fabricated).";
82
+ // ─── The key seam (REQUIRED; value NEVER leaked past the &api_key= param) ──
83
+ /** Read FRED_API_KEY from env; trim; return the value or undefined (unset/blank). */
84
+ export function fredApiKey() {
85
+ const raw = process.env.FRED_API_KEY;
86
+ const trimmed = typeof raw === "string" ? raw.trim() : "";
87
+ return trimmed ? trimmed : undefined;
88
+ }
89
+ /**
90
+ * num(), but map FRED's missing-observation sentinel `"."` → null (missing). A
91
+ * genuine "0" stays 0 (num("0") === 0); a real numeric string parses. This is the
92
+ * BLS `"-"` lineage — a data-absence marker, never a fabricated 0.
93
+ */
94
+ export function fredValue(v) {
95
+ if (v === ".")
96
+ return null;
97
+ return num(v);
98
+ }
99
+ // ─── Shared SSRF-guarded fetch (REQUIRED key; &api_key= ONLY carrier) ──
100
+ /**
101
+ * GET one FRED JSON resource. The REQUIRED key is checked BEFORE any fetch (an
102
+ * unset FRED_API_KEY ⇒ invalid_input THROW, 0 network call). The query is built on
103
+ * the FIXED host from `params` + `&api_key=` + `&file_type=json` via URLSearchParams
104
+ * (no host/path steer); a post-construction hostname/protocol assertion +
105
+ * `redirect:"error"` lock it (fail closed on any off-host 3xx — it could carry the
106
+ * key away). `label` is host+path only.
107
+ *
108
+ * A 400 carrying `{error_message}` (a bad series_id / expired key) is reclassified
109
+ * to invalid_input CARRYING the FRED message. `getJson`/`fetchWithRetry` discards a
110
+ * non-ok body (it throws before reading it), so to surface FRED's honest reason we
111
+ * re-read the 400 body via a single bare GET on the error path ONLY (the happy /
112
+ * 5xx / 429 / timeout paths keep the shared envelope's retry taxonomy untouched).
113
+ */
114
+ async function getFred(path, label, params, key) {
115
+ params.set("api_key", key);
116
+ params.set("file_type", "json");
117
+ const url = `https://${FRED_HOST}${path}?${params.toString()}`;
118
+ const built = new URL(url);
119
+ if (built.hostname !== FRED_HOST || built.protocol !== "https:") {
120
+ throw new ToolErrorCarrier({
121
+ kind: "invalid_input",
122
+ retryable: false,
123
+ message: `Constructed FRED URL host ${JSON.stringify(built.hostname)} (${built.protocol}) is not ${FRED_HOST} over https — refusing to fetch (SSRF safety).`,
124
+ upstreamEndpoint: label,
125
+ });
126
+ }
127
+ try {
128
+ // The key rides in &api_key= ONLY (never the label/_meta); redirect:"error"
129
+ // (fail closed on any off-host 3xx). A 200 non-JSON body ⇒ getJson's r.json()
130
+ // throws SyntaxError ⇒ the caller reclassifies to schema_drift.
131
+ return await getJson(url, { label, redirect: "error" });
132
+ }
133
+ catch (e) {
134
+ if (e instanceof ToolErrorCarrier) {
135
+ // A 400 (missing/bad key, or a bad series_id) carries FRED's honest
136
+ // `{error_message}`, but fetchWithRetry discarded the body. Re-read it once so
137
+ // the caller learns the REAL reason (never a fake-empty). A body that no longer
138
+ // 400s / is unreadable falls back to the generic 400 carrier unchanged.
139
+ if (e.toolError.upstreamStatus === 400) {
140
+ const fredMsg = await readFredErrorMessage(url);
141
+ throw new ToolErrorCarrier({
142
+ kind: "invalid_input",
143
+ retryable: false,
144
+ message: fredMsg
145
+ ? `FRED rejected the request (HTTP 400): ${fredMsg}. Check FRED_API_KEY and the series_id / parameters.`
146
+ : "FRED rejected the request (HTTP 400) — check FRED_API_KEY and the series_id / parameters.",
147
+ upstreamStatus: 400,
148
+ upstreamEndpoint: label,
149
+ });
150
+ }
151
+ throw e; // 5xx → upstream_unavailable, 404 → not_found, 429 → rate_limited …
152
+ }
153
+ throw e; // SyntaxError (200 non-JSON) → the caller maps it to driftError
154
+ }
155
+ }
156
+ /** Single bare GET to read a FRED 400's `error_message` (error path ONLY). null on any failure. */
157
+ async function readFredErrorMessage(url) {
158
+ try {
159
+ const r = await fetch(url, {
160
+ signal: AbortSignal.timeout(15_000),
161
+ redirect: "error",
162
+ });
163
+ const body = (await r.json());
164
+ return typeof body?.error_message === "string" ? body.error_message : null;
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ /**
171
+ * Search FRED series (`/fred/series/search`) by `search_text` → curated series
172
+ * rows + honest `_meta`. REQUIRES FRED_API_KEY (throws invalid_input pre-fetch when
173
+ * unset). totalAvailable = FRED's exact `count`; offset pagination.
174
+ */
175
+ export async function searchSeries(args) {
176
+ // ── [KEY] REQUIRED key — throw an honest config error BEFORE any fetch. ──
177
+ const key = fredApiKey();
178
+ if (key === undefined) {
179
+ throw new ToolErrorCarrier({
180
+ kind: "invalid_input",
181
+ retryable: false,
182
+ message: "FRED requires a free API key. Get one at https://fred.stlouisfed.org/docs/api/api_key.html and set FRED_API_KEY.",
183
+ upstreamEndpoint: FRED_SEARCH_LABEL,
184
+ });
185
+ }
186
+ // ── Validate + default (belt-and-suspenders behind the server Zod; a DIRECT
187
+ // handler call bypasses Zod). ──
188
+ const query = args.query ?? "";
189
+ if (query.trim() === "") {
190
+ throw new ToolErrorCarrier({
191
+ kind: "invalid_input",
192
+ retryable: false,
193
+ message: "fred_search_series requires a non-empty `query` (the FRED search_text), e.g. 'unemployment rate' or 'CPI'.",
194
+ upstreamEndpoint: FRED_SEARCH_LABEL,
195
+ });
196
+ }
197
+ const limit = clampLimit(args.limit, DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT);
198
+ const offset = clampOffset(args.offset);
199
+ const params = new URLSearchParams();
200
+ params.set("search_text", query);
201
+ params.set("limit", String(limit));
202
+ params.set("offset", String(offset));
203
+ let body;
204
+ try {
205
+ body = await getFred(FRED_SEARCH_PATH, FRED_SEARCH_LABEL, params, key);
206
+ }
207
+ catch (e) {
208
+ if (e instanceof SyntaxError) {
209
+ throw driftError(FRED_SEARCH_LABEL, "FRED /series/search returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).");
210
+ }
211
+ throw e;
212
+ }
213
+ // ── [P4] `seriess` MUST be an array (a missing/non-array is drift, never a
214
+ // fabricated empty). ──
215
+ const b = (body ?? {});
216
+ if (!Array.isArray(b.seriess)) {
217
+ throw driftError(FRED_SEARCH_LABEL, "FRED /series/search shape drift — `seriess` must be an array.");
218
+ }
219
+ const series = b.seriess.map((row) => {
220
+ const s = (row ?? {});
221
+ return {
222
+ id: str(s.id),
223
+ title: str(s.title),
224
+ frequency: str(s.frequency),
225
+ frequencyShort: str(s.frequency_short),
226
+ units: str(s.units),
227
+ seasonalAdjustment: str(s.seasonal_adjustment),
228
+ observationStart: str(s.observation_start),
229
+ observationEnd: str(s.observation_end),
230
+ lastUpdated: str(s.last_updated),
231
+ popularity: num(s.popularity),
232
+ };
233
+ });
234
+ const returned = series.length;
235
+ const totalAvailable = num(b.count); // [P1] EXACT — never returned
236
+ const hasMore = totalAvailable !== null && offset + returned < totalAvailable;
237
+ const nextOffset = hasMore ? offset + returned : null;
238
+ return withMeta({ series }, {
239
+ source: `${FRED_HOST} /fred/series/search (FRED; FRED_API_KEY)`,
240
+ keylessMode: false, // ★KEYED — the second key-required source
241
+ returned,
242
+ totalAvailable,
243
+ filtersApplied: [`query:${query}`],
244
+ filtersDropped: [],
245
+ fieldsUnavailable: [],
246
+ pagination: { offset, limit, hasMore, nextOffset },
247
+ notes: [KEY_REQUIRED_NOTE, COUNT_TOTAL_NOTE],
248
+ });
249
+ }
250
+ /**
251
+ * Fetch a FRED series' time series (`/fred/series/observations`) → date/value rows
252
+ * + honest `_meta`. REQUIRES FRED_API_KEY (throws invalid_input pre-fetch when
253
+ * unset). ★A missing observation (`value === "."`) maps to null, never 0.
254
+ * totalAvailable = FRED's exact `count`; offset pagination.
255
+ */
256
+ export async function seriesObservations(args) {
257
+ // ── [KEY] REQUIRED key — throw an honest config error BEFORE any fetch. ──
258
+ const key = fredApiKey();
259
+ if (key === undefined) {
260
+ throw new ToolErrorCarrier({
261
+ kind: "invalid_input",
262
+ retryable: false,
263
+ message: "FRED requires a free API key. Get one at https://fred.stlouisfed.org/docs/api/api_key.html and set FRED_API_KEY.",
264
+ upstreamEndpoint: FRED_OBS_LABEL,
265
+ });
266
+ }
267
+ // ── Validate (belt-and-suspenders behind the server Zod; a DIRECT handler call
268
+ // bypasses Zod — `series_id` rides the query, dates/sort_order too). ──
269
+ const seriesId = args.seriesId ?? "";
270
+ if (!SERIES_ID_RE.test(seriesId)) {
271
+ throw new ToolErrorCarrier({
272
+ kind: "invalid_input",
273
+ retryable: false,
274
+ message: `Invalid seriesId ${JSON.stringify(seriesId)} — expected a FRED series id (^[A-Za-z0-9._-]+$), e.g. "GDP", "CPIAUCSL", "UNRATE".`,
275
+ upstreamEndpoint: FRED_OBS_LABEL,
276
+ });
277
+ }
278
+ if (args.startDate !== undefined && !DATE_RE.test(args.startDate)) {
279
+ throw new ToolErrorCarrier({
280
+ kind: "invalid_input",
281
+ retryable: false,
282
+ message: `Invalid startDate ${JSON.stringify(args.startDate)} — expected YYYY-MM-DD (^\\d{4}-\\d{2}-\\d{2}$).`,
283
+ upstreamEndpoint: FRED_OBS_LABEL,
284
+ });
285
+ }
286
+ if (args.endDate !== undefined && !DATE_RE.test(args.endDate)) {
287
+ throw new ToolErrorCarrier({
288
+ kind: "invalid_input",
289
+ retryable: false,
290
+ message: `Invalid endDate ${JSON.stringify(args.endDate)} — expected YYYY-MM-DD (^\\d{4}-\\d{2}-\\d{2}$).`,
291
+ upstreamEndpoint: FRED_OBS_LABEL,
292
+ });
293
+ }
294
+ if (args.sortOrder !== undefined && !SORT_ORDERS.has(args.sortOrder)) {
295
+ throw new ToolErrorCarrier({
296
+ kind: "invalid_input",
297
+ retryable: false,
298
+ message: `Invalid sortOrder ${JSON.stringify(args.sortOrder)} — expected one of asc, desc.`,
299
+ upstreamEndpoint: FRED_OBS_LABEL,
300
+ });
301
+ }
302
+ const limit = clampLimit(args.limit, DEFAULT_OBS_LIMIT, MAX_OBS_LIMIT);
303
+ const offset = clampOffset(args.offset);
304
+ const params = new URLSearchParams();
305
+ params.set("series_id", seriesId);
306
+ params.set("limit", String(limit));
307
+ params.set("offset", String(offset));
308
+ const filtersApplied = [`series_id:${seriesId}`];
309
+ if (args.startDate !== undefined) {
310
+ params.set("observation_start", args.startDate);
311
+ filtersApplied.push(`observation_start:${args.startDate}`);
312
+ }
313
+ if (args.endDate !== undefined) {
314
+ params.set("observation_end", args.endDate);
315
+ filtersApplied.push(`observation_end:${args.endDate}`);
316
+ }
317
+ if (args.sortOrder !== undefined) {
318
+ params.set("sort_order", args.sortOrder);
319
+ filtersApplied.push(`sort_order:${args.sortOrder}`);
320
+ }
321
+ let body;
322
+ try {
323
+ body = await getFred(FRED_OBS_PATH, FRED_OBS_LABEL, params, key);
324
+ }
325
+ catch (e) {
326
+ if (e instanceof SyntaxError) {
327
+ throw driftError(FRED_OBS_LABEL, "FRED /series/observations returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).");
328
+ }
329
+ throw e;
330
+ }
331
+ // ── [P4] `observations` MUST be an array. ──
332
+ const b = (body ?? {});
333
+ if (!Array.isArray(b.observations)) {
334
+ throw driftError(FRED_OBS_LABEL, "FRED /series/observations shape drift — `observations` must be an array.");
335
+ }
336
+ const observations = b.observations.map((row) => {
337
+ const o = (row ?? {});
338
+ return { date: str(o.date), value: fredValue(o.value) }; // ★"." ⇒ null
339
+ });
340
+ const returned = observations.length;
341
+ const totalAvailable = num(b.count); // [P1] EXACT — never returned
342
+ const hasMore = totalAvailable !== null && offset + returned < totalAvailable;
343
+ const nextOffset = hasMore ? offset + returned : null;
344
+ return withMeta({ observations }, {
345
+ source: `${FRED_HOST} /fred/series/observations (FRED; FRED_API_KEY)`,
346
+ keylessMode: false, // ★KEYED
347
+ returned,
348
+ totalAvailable,
349
+ filtersApplied,
350
+ filtersDropped: [],
351
+ fieldsUnavailable: [],
352
+ pagination: { offset, limit, hasMore, nextOffset },
353
+ notes: [KEY_REQUIRED_NOTE, MISSING_VALUE_NOTE, COUNT_TOTAL_NOTE],
354
+ });
355
+ }
356
+ // ─── Small shared clamps (defensive, behind the server Zod bounds) ──
357
+ function clampLimit(v, def, max) {
358
+ if (typeof v !== "number" || !Number.isFinite(v))
359
+ return def;
360
+ const n = Math.floor(v);
361
+ if (n < 1)
362
+ return 1;
363
+ if (n > max)
364
+ return max;
365
+ return n;
366
+ }
367
+ function clampOffset(v) {
368
+ if (typeof v !== "number" || !Number.isFinite(v))
369
+ return 0;
370
+ const n = Math.floor(v);
371
+ return n < 0 ? 0 : n;
372
+ }
373
+ //# sourceMappingURL=fred.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fred.js","sourceRoot":"","sources":["../src/fred.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"}