@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/README.ja.md +3 -2
- package/README.ko.md +3 -2
- package/README.md +32 -3
- 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/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 +143 -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/keys.ts +216 -0
- package/src/server.ts +167 -1
- package/src/snapshot.ts +51 -20
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",
|
|
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;AAo4G5B,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,EAk8C1B,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"}
|
package/dist/server.js
CHANGED
|
@@ -44,6 +44,8 @@ import * as nih from "./nih.js";
|
|
|
44
44
|
import * as nsf from "./nsf.js";
|
|
45
45
|
import * as clinicaltrials from "./clinicaltrials.js";
|
|
46
46
|
import * as census from "./census.js";
|
|
47
|
+
import * as censusEconomic from "./census-economic.js";
|
|
48
|
+
import * as fred from "./fred.js";
|
|
47
49
|
import * as fema from "./fema.js";
|
|
48
50
|
import * as fdic from "./fdic.js";
|
|
49
51
|
import * as bls from "./bls.js";
|
|
@@ -54,6 +56,7 @@ import * as cms from "./cms.js";
|
|
|
54
56
|
import * as fac from "./fac.js";
|
|
55
57
|
import * as usitc from "./usitc.js";
|
|
56
58
|
import { fetchAttachmentText } from "./attachments.js";
|
|
59
|
+
import * as keys from "./keys.js";
|
|
57
60
|
import { toToolError, ToolErrorCarrier, errorFromResponse } from "./errors.js";
|
|
58
61
|
import { buildMeta, isMetaBundle, withMeta, } from "./meta.js";
|
|
59
62
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
@@ -61,7 +64,7 @@ import { realpathSync } from "node:fs";
|
|
|
61
64
|
const SERVER_NAME = "mcp-sam-gov";
|
|
62
65
|
// Kept in lockstep with package.json / manifest.json / server.json.
|
|
63
66
|
// Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
|
|
64
|
-
const SERVER_VERSION = "1.
|
|
67
|
+
const SERVER_VERSION = "1.1.0";
|
|
65
68
|
// ─── Tool input schemas (Zod) ────────────────────────────────────
|
|
66
69
|
const SamSearchInput = z.object({
|
|
67
70
|
query: z.string().optional().describe("Free-text title query"),
|
|
@@ -2789,6 +2792,92 @@ const CensusGeographiesByCoordinatesInput = z
|
|
|
2789
2792
|
message: "latitude (or its alias y) is required.",
|
|
2790
2793
|
path: ["latitude"],
|
|
2791
2794
|
});
|
|
2795
|
+
// ─── US Census County Business Patterns (CBP) — the FIRST key-required source ──
|
|
2796
|
+
const CensusBusinessPatternsInput = z.object({
|
|
2797
|
+
naics: z
|
|
2798
|
+
.string()
|
|
2799
|
+
.regex(/^\d{2,6}$/)
|
|
2800
|
+
.optional()
|
|
2801
|
+
.describe("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}$."),
|
|
2802
|
+
geography: z
|
|
2803
|
+
.enum(["us", "state", "county"])
|
|
2804
|
+
.optional()
|
|
2805
|
+
.describe("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`."),
|
|
2806
|
+
state: z
|
|
2807
|
+
.string()
|
|
2808
|
+
.regex(/^\d{2}$/)
|
|
2809
|
+
.optional()
|
|
2810
|
+
.describe("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}$."),
|
|
2811
|
+
year: z
|
|
2812
|
+
.string()
|
|
2813
|
+
.regex(/^\d{4}$/)
|
|
2814
|
+
.optional()
|
|
2815
|
+
.describe("The CBP data year (default '2022', the latest confirmed vintage). Validated ^\\d{4}$ (it rides in the request path)."),
|
|
2816
|
+
limit: z
|
|
2817
|
+
.number()
|
|
2818
|
+
.int()
|
|
2819
|
+
.min(0)
|
|
2820
|
+
.optional()
|
|
2821
|
+
.describe("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."),
|
|
2822
|
+
});
|
|
2823
|
+
// ─── FRED (Federal Reserve Economic Data) — the SECOND key-required source ──
|
|
2824
|
+
// ADR-0048. Macro context (GDP/CPI/rates/unemployment/PPI). REQUIRES a free
|
|
2825
|
+
// FRED_API_KEY; without it both tools throw an honest config error (the other 112
|
|
2826
|
+
// tools stay keyless). The key rides &api_key= ONLY. Missing observations ('.') → null.
|
|
2827
|
+
const FredSearchSeriesInput = z.object({
|
|
2828
|
+
query: z
|
|
2829
|
+
.string()
|
|
2830
|
+
.min(1)
|
|
2831
|
+
.describe("The FRED search_text — free-text terms to discover economic series, e.g. 'unemployment rate', 'CPI', 'GDP', '10-year treasury'. Required."),
|
|
2832
|
+
limit: z
|
|
2833
|
+
.number()
|
|
2834
|
+
.int()
|
|
2835
|
+
.min(1)
|
|
2836
|
+
.max(1000)
|
|
2837
|
+
.optional()
|
|
2838
|
+
.describe("Max series to return (default 25, max 1000). Offset-paginated."),
|
|
2839
|
+
offset: z
|
|
2840
|
+
.number()
|
|
2841
|
+
.int()
|
|
2842
|
+
.min(0)
|
|
2843
|
+
.optional()
|
|
2844
|
+
.describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
|
|
2845
|
+
});
|
|
2846
|
+
const FredSeriesObservationsInput = z.object({
|
|
2847
|
+
seriesId: z
|
|
2848
|
+
.string()
|
|
2849
|
+
.regex(/^[A-Za-z0-9._-]+$/)
|
|
2850
|
+
.describe("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."),
|
|
2851
|
+
startDate: z
|
|
2852
|
+
.string()
|
|
2853
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
2854
|
+
.optional()
|
|
2855
|
+
.describe("Earliest observation date (YYYY-MM-DD). Maps to FRED observation_start."),
|
|
2856
|
+
endDate: z
|
|
2857
|
+
.string()
|
|
2858
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
2859
|
+
.optional()
|
|
2860
|
+
.describe("Latest observation date (YYYY-MM-DD). Maps to FRED observation_end."),
|
|
2861
|
+
limit: z
|
|
2862
|
+
.number()
|
|
2863
|
+
.int()
|
|
2864
|
+
.min(1)
|
|
2865
|
+
.max(100000)
|
|
2866
|
+
.optional()
|
|
2867
|
+
.describe("Max observations to return (default 100, max 100000). Offset-paginated."),
|
|
2868
|
+
offset: z
|
|
2869
|
+
.number()
|
|
2870
|
+
.int()
|
|
2871
|
+
.min(0)
|
|
2872
|
+
.optional()
|
|
2873
|
+
.describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
|
|
2874
|
+
sortOrder: z
|
|
2875
|
+
.enum(["asc", "desc"])
|
|
2876
|
+
.optional()
|
|
2877
|
+
.describe("Observation date order: 'asc' (oldest first, FRED default) or 'desc' (newest first)."),
|
|
2878
|
+
});
|
|
2879
|
+
// api_key_status takes no input — it is a pure status query over process.env.
|
|
2880
|
+
const ApiKeyStatusInput = z.object({});
|
|
2792
2881
|
// Build a ToolDef whose `handler` is type-checked against the schema's inferred
|
|
2793
2882
|
// input `I` at the call site (e.g. `input.searchText` is known-present). The
|
|
2794
2883
|
// `I` binding is erased to `any` in the ToolDef[] array, so entries without a
|
|
@@ -4049,9 +4138,59 @@ export const TOOLS = [
|
|
|
4049
4138
|
inputSchema: CensusGeographiesByCoordinatesInput,
|
|
4050
4139
|
handler: (input) => census.geographiesByCoordinates(input),
|
|
4051
4140
|
}),
|
|
4141
|
+
// ━━━ US Census County Business Patterns — market sizing (1) ━━━ ADR-0047
|
|
4142
|
+
// ★The server's FIRST KEY-REQUIRED source: the Census Data API removed its
|
|
4143
|
+
// keyless tier, so WITHOUT a CENSUS_API_KEY this tool throws an honest
|
|
4144
|
+
// invalid_input config error (the other 111 tools stay keyless). NAICS×geography
|
|
4145
|
+
// establishments / employment / annual payroll — the demand-side market-sizing
|
|
4146
|
+
// lane. Census negative suppression sentinels (-999999999 …) map to null (never
|
|
4147
|
+
// a negative number / never 0). The 2D-array body is parsed by header name.
|
|
4148
|
+
defineTool({
|
|
4149
|
+
name: "census_business_patterns",
|
|
4150
|
+
description: "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.",
|
|
4151
|
+
inputSchema: CensusBusinessPatternsInput,
|
|
4152
|
+
handler: (input) => censusEconomic.businessPatterns(input),
|
|
4153
|
+
}),
|
|
4154
|
+
// ━━━ FRED (Federal Reserve Economic Data) — macro context (2) ━━━ ADR-0048
|
|
4155
|
+
// ★The server's SECOND KEY-REQUIRED source: FRED has NO keyless tier, so WITHOUT
|
|
4156
|
+
// a FRED_API_KEY both tools throw an honest invalid_input config error (the other
|
|
4157
|
+
// 112 tools stay keyless). GDP/CPI/rates/unemployment/PPI — the macro backdrop for
|
|
4158
|
+
// bid escalation / market timing. A missing observation ('.') maps to null (never 0).
|
|
4159
|
+
defineTool({
|
|
4160
|
+
name: "fred_search_series",
|
|
4161
|
+
description: "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.",
|
|
4162
|
+
inputSchema: FredSearchSeriesInput,
|
|
4163
|
+
handler: (input) => fred.searchSeries(input),
|
|
4164
|
+
}),
|
|
4165
|
+
defineTool({
|
|
4166
|
+
name: "fred_series_observations",
|
|
4167
|
+
description: "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.",
|
|
4168
|
+
inputSchema: FredSeriesObservationsInput,
|
|
4169
|
+
handler: (input) => fred.seriesObservations(input),
|
|
4170
|
+
}),
|
|
4171
|
+
// ━━━ Self-service key discovery (1) ━━━
|
|
4172
|
+
// KEYLESS. A local status query — reads process.env (+ any .env auto-loaded at
|
|
4173
|
+
// startup) and reports, per key, whether it is set (a BOOLEAN — the key VALUE is
|
|
4174
|
+
// NEVER read into the output). Makes the 2-required + 5-optional key situation
|
|
4175
|
+
// discoverable without reading source or docs.
|
|
4176
|
+
defineTool({
|
|
4177
|
+
name: "api_key_status",
|
|
4178
|
+
description: "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.",
|
|
4179
|
+
inputSchema: ApiKeyStatusInput,
|
|
4180
|
+
handler: async () => keys.apiKeyStatus(),
|
|
4181
|
+
}),
|
|
4052
4182
|
];
|
|
4053
4183
|
// ─── Server bootstrap ────────────────────────────────────────────
|
|
4054
4184
|
async function main() {
|
|
4185
|
+
// Auto-load API keys from a `.env` in the working directory BEFORE anything
|
|
4186
|
+
// reads process.env (tools read env at call time; SamGovClient below reads
|
|
4187
|
+
// SAM_GOV_API_KEY immediately). Real env wins over .env (precedence); no .env
|
|
4188
|
+
// present ⇒ zero change ⇒ byte-identical startup. We log only the COUNT — never
|
|
4189
|
+
// which keys or their values.
|
|
4190
|
+
const loadedFromEnvFile = keys.loadDotEnv();
|
|
4191
|
+
if (loadedFromEnvFile > 0) {
|
|
4192
|
+
console.error(`[mcp-sam-gov] loaded ${loadedFromEnvFile} key(s) from .env`);
|
|
4193
|
+
}
|
|
4055
4194
|
const sam = new SamGovClient({
|
|
4056
4195
|
apiKey: process.env.SAM_GOV_API_KEY?.trim() || undefined,
|
|
4057
4196
|
logger: {
|
|
@@ -4166,6 +4305,9 @@ function synthesizeDefaultMeta(toolName, sam) {
|
|
|
4166
4305
|
else if (toolName.startsWith("fpds_")) {
|
|
4167
4306
|
source = "www.fpds.gov ezSearch ATOM (FPDS-NG, keyless)";
|
|
4168
4307
|
}
|
|
4308
|
+
else if (toolName === "api_key_status") {
|
|
4309
|
+
source = "local (process.env + .env)";
|
|
4310
|
+
}
|
|
4169
4311
|
else {
|
|
4170
4312
|
source = "unknown";
|
|
4171
4313
|
}
|