@cliwant/mcp-sam-gov 0.2.1 → 1.0.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/LICENSE +21 -21
- package/README.ja.md +48 -16
- package/README.ko.md +48 -16
- package/README.md +279 -67
- package/dist/attachments.d.ts +63 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +576 -0
- package/dist/attachments.js.map +1 -0
- package/dist/bls.d.ts +267 -0
- package/dist/bls.d.ts.map +1 -0
- package/dist/bls.js +1549 -0
- package/dist/bls.js.map +1 -0
- package/dist/cache.d.ts +42 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +64 -0
- package/dist/cache.js.map +1 -0
- package/dist/census.d.ts +182 -0
- package/dist/census.d.ts.map +1 -0
- package/dist/census.js +576 -0
- package/dist/census.js.map +1 -0
- package/dist/ckan.d.ts +141 -0
- package/dist/ckan.d.ts.map +1 -0
- package/dist/ckan.js +393 -0
- package/dist/ckan.js.map +1 -0
- package/dist/clinicaltrials.d.ts +180 -0
- package/dist/clinicaltrials.d.ts.map +1 -0
- package/dist/clinicaltrials.js +730 -0
- package/dist/clinicaltrials.js.map +1 -0
- package/dist/cms.d.ts +140 -0
- package/dist/cms.d.ts.map +1 -0
- package/dist/cms.js +482 -0
- package/dist/cms.js.map +1 -0
- package/dist/coerce.d.ts +32 -0
- package/dist/coerce.d.ts.map +1 -0
- package/dist/coerce.js +50 -0
- package/dist/coerce.js.map +1 -0
- package/dist/datagov-catalog.d.ts +84 -0
- package/dist/datagov-catalog.d.ts.map +1 -0
- package/dist/datagov-catalog.js +233 -0
- package/dist/datagov-catalog.js.map +1 -0
- package/dist/datagov.d.ts +146 -0
- package/dist/datagov.d.ts.map +1 -0
- package/dist/datagov.js +689 -0
- package/dist/datagov.js.map +1 -0
- package/dist/datagovKey.d.ts +36 -0
- package/dist/datagovKey.d.ts.map +1 -0
- package/dist/datagovKey.js +60 -0
- package/dist/datagovKey.js.map +1 -0
- package/dist/datasource.d.ts +313 -0
- package/dist/datasource.d.ts.map +1 -0
- package/dist/datasource.js +551 -0
- package/dist/datasource.js.map +1 -0
- package/dist/disclosure.d.ts +55 -0
- package/dist/disclosure.d.ts.map +1 -0
- package/dist/disclosure.js +57 -0
- package/dist/disclosure.js.map +1 -0
- package/dist/ecfr.d.ts +4 -2
- package/dist/ecfr.d.ts.map +1 -1
- package/dist/ecfr.js +92 -17
- package/dist/ecfr.js.map +1 -1
- package/dist/echo.d.ts +143 -0
- package/dist/echo.d.ts.map +1 -0
- package/dist/echo.js +424 -0
- package/dist/echo.js.map +1 -0
- package/dist/edgar.d.ts +377 -0
- package/dist/edgar.d.ts.map +1 -0
- package/dist/edgar.js +2418 -0
- package/dist/edgar.js.map +1 -0
- package/dist/errors.d.ts +102 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +247 -0
- package/dist/errors.js.map +1 -0
- package/dist/fac.d.ts +180 -0
- package/dist/fac.d.ts.map +1 -0
- package/dist/fac.js +416 -0
- package/dist/fac.js.map +1 -0
- package/dist/far.d.ts +170 -0
- package/dist/far.d.ts.map +1 -0
- package/dist/far.js +804 -0
- package/dist/far.js.map +1 -0
- package/dist/fdic.d.ts +599 -0
- package/dist/fdic.d.ts.map +1 -0
- package/dist/fdic.js +1624 -0
- package/dist/fdic.js.map +1 -0
- package/dist/federal-register.d.ts +139 -2
- package/dist/federal-register.d.ts.map +1 -1
- package/dist/federal-register.js +432 -15
- package/dist/federal-register.js.map +1 -1
- package/dist/fema.d.ts +181 -0
- package/dist/fema.d.ts.map +1 -0
- package/dist/fema.js +436 -0
- package/dist/fema.js.map +1 -0
- package/dist/fpds.d.ts +108 -0
- package/dist/fpds.d.ts.map +1 -0
- package/dist/fpds.js +519 -0
- package/dist/fpds.js.map +1 -0
- package/dist/gao.d.ts +64 -0
- package/dist/gao.d.ts.map +1 -0
- package/dist/gao.js +640 -0
- package/dist/gao.js.map +1 -0
- package/dist/govinfo.d.ts +111 -0
- package/dist/govinfo.d.ts.map +1 -0
- package/dist/govinfo.js +422 -0
- package/dist/govinfo.js.map +1 -0
- package/dist/grants.d.ts +27 -4
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +114 -11
- package/dist/grants.js.map +1 -1
- package/dist/gsa-csv.d.ts +249 -0
- package/dist/gsa-csv.d.ts.map +1 -0
- package/dist/gsa-csv.js +784 -0
- package/dist/gsa-csv.js.map +1 -0
- package/dist/integrity.d.ts +212 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +707 -0
- package/dist/integrity.js.map +1 -0
- package/dist/meta.d.ts +165 -0
- package/dist/meta.d.ts.map +1 -0
- package/dist/meta.js +162 -0
- package/dist/meta.js.map +1 -0
- package/dist/nih.d.ts +117 -0
- package/dist/nih.d.ts.map +1 -0
- package/dist/nih.js +291 -0
- package/dist/nih.js.map +1 -0
- package/dist/nppes.d.ts +157 -0
- package/dist/nppes.d.ts.map +1 -0
- package/dist/nppes.js +648 -0
- package/dist/nppes.js.map +1 -0
- package/dist/nsf.d.ts +176 -0
- package/dist/nsf.d.ts.map +1 -0
- package/dist/nsf.js +554 -0
- package/dist/nsf.js.map +1 -0
- package/dist/nvd.d.ts +176 -0
- package/dist/nvd.d.ts.map +1 -0
- package/dist/nvd.js +912 -0
- package/dist/nvd.js.map +1 -0
- package/dist/ofac.d.ts +205 -0
- package/dist/ofac.d.ts.map +1 -0
- package/dist/ofac.js +919 -0
- package/dist/ofac.js.map +1 -0
- package/dist/pricing.d.ts +110 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +843 -0
- package/dist/pricing.js.map +1 -0
- package/dist/sam-gov/client.d.ts +60 -2
- package/dist/sam-gov/client.d.ts.map +1 -1
- package/dist/sam-gov/client.js +320 -54
- package/dist/sam-gov/client.js.map +1 -1
- package/dist/sam-gov/index.d.ts +1 -1
- package/dist/sam-gov/index.d.ts.map +1 -1
- package/dist/sam-gov/index.js +1 -1
- package/dist/sam-gov/index.js.map +1 -1
- package/dist/sam-gov/types.d.ts +24 -0
- package/dist/sam-gov/types.d.ts.map +1 -1
- package/dist/sba.d.ts +72 -0
- package/dist/sba.d.ts.map +1 -0
- package/dist/sba.js +281 -0
- package/dist/sba.js.map +1 -0
- package/dist/server.d.ts +14 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3897 -295
- package/dist/server.js.map +1 -1
- package/dist/snapshot.d.ts +98 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +146 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/socrata.d.ts +157 -0
- package/dist/socrata.d.ts.map +1 -0
- package/dist/socrata.js +448 -0
- package/dist/socrata.js.map +1 -0
- package/dist/treasury.d.ts +143 -0
- package/dist/treasury.d.ts.map +1 -0
- package/dist/treasury.js +436 -0
- package/dist/treasury.js.map +1 -0
- package/dist/usaspending.d.ts +260 -65
- package/dist/usaspending.d.ts.map +1 -1
- package/dist/usaspending.js +1664 -228
- package/dist/usaspending.js.map +1 -1
- package/dist/usitc.d.ts +142 -0
- package/dist/usitc.d.ts.map +1 -0
- package/dist/usitc.js +339 -0
- package/dist/usitc.js.map +1 -0
- package/package.json +24 -2
- package/src/attachments.ts +652 -0
- package/src/bls.ts +1943 -0
- package/src/cache.ts +73 -0
- package/src/census.ts +735 -0
- package/src/ckan.ts +495 -0
- package/src/clinicaltrials.ts +923 -0
- package/src/cms.ts +634 -0
- package/src/coerce.ts +47 -0
- package/src/datagov-catalog.ts +296 -0
- package/src/datagov.ts +907 -0
- package/src/datagovKey.ts +68 -0
- package/src/datasource.ts +721 -0
- package/src/disclosure.ts +61 -0
- package/src/ecfr.ts +231 -127
- package/src/echo.ts +496 -0
- package/src/edgar.ts +3014 -0
- package/src/errors.ts +303 -0
- package/src/fac.ts +529 -0
- package/src/far.ts +1007 -0
- package/src/fdic.ts +2052 -0
- package/src/federal-register.ts +706 -191
- package/src/fema.ts +541 -0
- package/src/fpds.ts +620 -0
- package/src/gao.ts +744 -0
- package/src/govinfo.ts +497 -0
- package/src/grants.ts +290 -155
- package/src/gsa-csv.ts +992 -0
- package/src/integrity.ts +928 -0
- package/src/meta.ts +292 -0
- package/src/nih.ts +375 -0
- package/src/nppes.ts +834 -0
- package/src/nsf.ts +706 -0
- package/src/nvd.ts +1124 -0
- package/src/ofac.ts +1166 -0
- package/src/pricing.ts +1075 -0
- package/src/sam-gov/client.ts +345 -63
- package/src/sam-gov/index.ts +5 -1
- package/src/sam-gov/types.ts +22 -0
- package/src/sba.ts +357 -0
- package/src/server.ts +4559 -327
- package/src/snapshot.ts +192 -0
- package/src/socrata.ts +532 -0
- package/src/treasury.ts +575 -0
- package/src/usaspending.ts +2680 -925
- package/src/usitc.ts +420 -0
package/dist/grants.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare function searchGrants(args: {
|
|
|
19
19
|
oppNum?: string;
|
|
20
20
|
oppStatuses?: GrantStatus[];
|
|
21
21
|
rows?: number;
|
|
22
|
-
}): Promise<{
|
|
22
|
+
}): Promise<import("./meta.js").MetaBundle<{
|
|
23
23
|
totalRecords: number;
|
|
24
24
|
grants: {
|
|
25
25
|
id: string;
|
|
@@ -31,18 +31,40 @@ export declare function searchGrants(args: {
|
|
|
31
31
|
closeDate: string | undefined;
|
|
32
32
|
status: string | undefined;
|
|
33
33
|
docType: string | undefined;
|
|
34
|
-
cfdaList: string
|
|
34
|
+
cfdaList: string[];
|
|
35
35
|
}[];
|
|
36
|
-
}
|
|
36
|
+
}>>;
|
|
37
37
|
export declare function getGrant(args: {
|
|
38
38
|
opportunityId: string;
|
|
39
39
|
}): Promise<{
|
|
40
|
+
found: false;
|
|
41
|
+
opportunityId: string;
|
|
42
|
+
id?: undefined;
|
|
43
|
+
opportunityNumber?: undefined;
|
|
44
|
+
title?: undefined;
|
|
45
|
+
agency?: undefined;
|
|
46
|
+
description?: undefined;
|
|
47
|
+
postingDate?: undefined;
|
|
48
|
+
responseDate?: undefined;
|
|
49
|
+
archiveDate?: undefined;
|
|
50
|
+
awardCeiling?: undefined;
|
|
51
|
+
awardFloor?: undefined;
|
|
52
|
+
estimatedFunding?: undefined;
|
|
53
|
+
expectedNumberOfAwards?: undefined;
|
|
54
|
+
applicantTypes?: undefined;
|
|
55
|
+
fundingInstruments?: undefined;
|
|
56
|
+
fundingCategories?: undefined;
|
|
57
|
+
cfdaPrograms?: undefined;
|
|
58
|
+
} | {
|
|
59
|
+
found: true;
|
|
40
60
|
id: number;
|
|
41
61
|
opportunityNumber: string;
|
|
42
62
|
title: string;
|
|
43
63
|
agency: {
|
|
44
64
|
code: string | undefined;
|
|
45
|
-
name: string |
|
|
65
|
+
name: string | null;
|
|
66
|
+
department: string | null;
|
|
67
|
+
contactName: string | null;
|
|
46
68
|
};
|
|
47
69
|
description: string;
|
|
48
70
|
postingDate: string | undefined;
|
|
@@ -59,5 +81,6 @@ export declare function getGrant(args: {
|
|
|
59
81
|
number: string;
|
|
60
82
|
title: string;
|
|
61
83
|
}[];
|
|
84
|
+
opportunityId?: undefined;
|
|
62
85
|
}>;
|
|
63
86
|
//# sourceMappingURL=grants.d.ts.map
|
package/dist/grants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAyBH,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE1E,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;;;;;;;;;;;;;;IAwIA;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBA6FpC,MAAM,EAAE;wBAGR,MAAM,EAAE;uBAGR,MAAM,EAAE;;;;;;GAMjC"}
|
package/dist/grants.js
CHANGED
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Documented at https://grants.gov/web/grants/s2s/grantor/schemas/grants-search-2-soap.html
|
|
13
13
|
*/
|
|
14
|
+
import { fetchWithRetry } from "./errors.js";
|
|
15
|
+
import { driftError } from "./datasource.js";
|
|
16
|
+
import { withMeta } from "./meta.js";
|
|
14
17
|
const GRANTS = "https://api.grants.gov/v1/api";
|
|
15
18
|
async function postJson(endpoint, body) {
|
|
16
|
-
const r = await
|
|
19
|
+
const r = await fetchWithRetry(`${GRANTS}/${endpoint}`, {
|
|
17
20
|
method: "POST",
|
|
18
21
|
headers: { "Content-Type": "application/json" },
|
|
19
22
|
body: JSON.stringify(body),
|
|
20
23
|
signal: AbortSignal.timeout(15_000),
|
|
21
|
-
});
|
|
22
|
-
if (!r.ok) {
|
|
23
|
-
throw new Error(`Grants.gov ${endpoint} returned ${r.status}`);
|
|
24
|
-
}
|
|
24
|
+
}, `grants.gov:${endpoint}`);
|
|
25
25
|
return (await r.json());
|
|
26
26
|
}
|
|
27
27
|
export async function searchGrants(args) {
|
|
@@ -37,21 +37,98 @@ export async function searchGrants(args) {
|
|
|
37
37
|
if (json.errorcode && json.errorcode !== 0) {
|
|
38
38
|
throw new Error(`Grants.gov error: ${json.msg ?? "unknown"}`);
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// F4 (P2 empty-vs-outage): a 200 that is NEITHER a recognized error envelope
|
|
41
|
+
// (handled above) NOR a `{data:{hitCount:number,…}}` shape is drift — a
|
|
42
|
+
// malformed / outage / interstitial body — and must NOT coalesce to
|
|
43
|
+
// `totalRecords:0` (an AUTHORITATIVE "no grants"). A GENUINE empty carries a
|
|
44
|
+
// numeric `data.hitCount:0` and stays an honest empty below. (Mirrors the
|
|
45
|
+
// govinfo/nih driftError precedent — a drift is a throw, never a fake-empty.)
|
|
46
|
+
const hitCount = json.data?.hitCount;
|
|
47
|
+
if (typeof hitCount !== "number") {
|
|
48
|
+
throw driftError("grants.gov", "Grants.gov search2 returned HTTP 200 but the body carries neither an error code nor a numeric data.hitCount — treating it as schema drift / an outage interstitial, NOT an empty result set.");
|
|
49
|
+
}
|
|
50
|
+
const totalRecords = hitCount;
|
|
51
|
+
const data = {
|
|
52
|
+
totalRecords,
|
|
42
53
|
grants: (json.data?.oppHits ?? []).map((g) => ({
|
|
43
54
|
id: g.id ?? "",
|
|
44
55
|
opportunityNumber: g.number ?? "",
|
|
45
56
|
title: g.title ?? "",
|
|
46
57
|
agencyCode: g.agencyCode ?? "",
|
|
47
|
-
|
|
58
|
+
// GRANT-2: prefer `agency` (the real agency name) over the empty `agencyName`.
|
|
59
|
+
agencyName: g.agency ?? g.agencyName ?? "",
|
|
48
60
|
openDate: g.openDate,
|
|
49
61
|
closeDate: g.closeDate,
|
|
50
62
|
status: g.oppStatus,
|
|
51
63
|
docType: g.docType,
|
|
52
|
-
|
|
64
|
+
// E-2: always an array — normalize absent/legacy-string to []/[str]
|
|
65
|
+
// so consumers can rely on `cfdaList` being string[] unconditionally.
|
|
66
|
+
cfdaList: Array.isArray(g.cfdaList)
|
|
67
|
+
? g.cfdaList
|
|
68
|
+
: g.cfdaList
|
|
69
|
+
? [g.cfdaList]
|
|
70
|
+
: [],
|
|
53
71
|
})),
|
|
54
72
|
};
|
|
73
|
+
// Truthful `_meta` (spec §1.2 A4, §2.3). Grants.gov reports a real match
|
|
74
|
+
// total (`hitCount`), so `totalAvailable` is trustworthy. A4: Grants.gov
|
|
75
|
+
// silently IGNORES unknown agency codes / CFDA numbers (it returns the
|
|
76
|
+
// unfiltered set rather than erroring), and it does not echo back which
|
|
77
|
+
// filters it honored — so the AI cannot verify a filter took. We list the
|
|
78
|
+
// filters we SENT in `filtersApplied` and warn that unknown values are
|
|
79
|
+
// dropped silently, so the AI treats a suspiciously large result set with
|
|
80
|
+
// caution instead of asserting it is filtered.
|
|
81
|
+
const returned = data.grants.length;
|
|
82
|
+
const sent = [];
|
|
83
|
+
if (args.keyword)
|
|
84
|
+
sent.push("keyword");
|
|
85
|
+
if (args.cfda)
|
|
86
|
+
sent.push("cfda");
|
|
87
|
+
if (args.agency)
|
|
88
|
+
sent.push("agency");
|
|
89
|
+
if (args.oppNum)
|
|
90
|
+
sent.push("oppNum");
|
|
91
|
+
if (args.oppStatuses?.length)
|
|
92
|
+
sent.push("oppStatuses");
|
|
93
|
+
const notes = [];
|
|
94
|
+
if (args.agency || args.cfda) {
|
|
95
|
+
notes.push("Grants.gov silently ignores an unknown agency code or CFDA number (it returns the UNFILTERED result set instead of an error) and does not confirm which filters were honored — if the result count looks too broad, verify the agency/CFDA value.");
|
|
96
|
+
}
|
|
97
|
+
// VQ-1 (C82 dogfooding): Grants.gov OR-tokenizes multi-word keywords (matches ANY
|
|
98
|
+
// word) and does NOT support phrase quoting (a quoted "..." keyword returns 0). Live:
|
|
99
|
+
// "cybersecurity information technology" → 926 broad hits (State-Dept program
|
|
100
|
+
// statements top) vs 280 focused for "cybersecurity". The note is RESULT-AWARE
|
|
101
|
+
// (adversarial review F1/F2): "broad set ≠ no grants" only holds when there ARE
|
|
102
|
+
// results; a 0-result multi-word query needs the opposite advice. Quotes are stripped
|
|
103
|
+
// for the word count so a quote-wrapped phrase is still detected as multi-word.
|
|
104
|
+
const rawKw = args.keyword ?? "";
|
|
105
|
+
const looksQuoted = /^\s*["'][\s\S]*["']\s*$/.test(rawKw);
|
|
106
|
+
const kwWords = rawKw
|
|
107
|
+
.trim()
|
|
108
|
+
.replace(/^["']+|["']+$/g, "")
|
|
109
|
+
.trim()
|
|
110
|
+
// disclosure-split-ok: grants.gov OR-splits ONLY whitespace (and '+'), NOT punctuation — live-verified 2026-07; the shared DISCLOSURE_SPLIT_RE punctuation class must NOT be applied here (it would over-disclose). See ADR-0022.
|
|
111
|
+
.split(/\s+/)
|
|
112
|
+
.filter(Boolean);
|
|
113
|
+
if (kwWords.length > 1) {
|
|
114
|
+
if (totalRecords > 0) {
|
|
115
|
+
notes.push(`Grants.gov OR-matches multi-word keywords: it returns opportunities containing ANY of these ${kwWords.length} words, so this keyword may BROADEN results if any word is a common term ("information"/"technology"/"program" match many unrelated grants) — a broad, poorly-ranked set here does NOT mean "no relevant grants". For focused results pass ONE specific term (e.g. "cybersecurity"); Grants.gov does NOT support phrase quoting (a quoted "..." keyword returns 0). Narrow with cfda / agency / oppStatuses instead.`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
notes.push(`This ${kwWords.length}-word keyword returned 0 results. Grants.gov OR-matches keywords and does NOT support phrase quoting${looksQuoted ? ' — your keyword appears quote-wrapped, so REMOVE the quotes (a quoted "..." keyword always returns 0)' : ' (a quoted "..." keyword returns 0)'}. A 0 here means even the OR of these words has no match — try each word separately, or narrow with cfda/agency/oppStatuses, to isolate.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return withMeta(data, {
|
|
122
|
+
source: "grants.gov/api (search2)",
|
|
123
|
+
keylessMode: true,
|
|
124
|
+
returned,
|
|
125
|
+
totalAvailable: totalRecords,
|
|
126
|
+
truncated: returned < totalRecords,
|
|
127
|
+
filtersApplied: sent,
|
|
128
|
+
filtersDropped: [],
|
|
129
|
+
fieldsUnavailable: [],
|
|
130
|
+
notes,
|
|
131
|
+
});
|
|
55
132
|
}
|
|
56
133
|
export async function getGrant(args) {
|
|
57
134
|
const json = await postJson("fetchOpportunity", {
|
|
@@ -61,12 +138,38 @@ export async function getGrant(args) {
|
|
|
61
138
|
throw new Error(`Grants.gov error: ${json.msg ?? "unknown"}`);
|
|
62
139
|
}
|
|
63
140
|
const d = json.data ?? {};
|
|
141
|
+
// NOT-FOUND: Grants.gov's fetchOpportunity returns errorcode:0 ("Webservice
|
|
142
|
+
// Succeeds") + a HOLLOW `data` object even for a NONEXISTENT opportunityId
|
|
143
|
+
// (LIVE-VERIFIED 2026-07-06: id 999999999 → errorcode 0 but data carries NO
|
|
144
|
+
// id / opportunityNumber / opportunityTitle / synopsis — only skeleton fields).
|
|
145
|
+
// Mapping that shell would fabricate a grant (id:0, title:"") for an opportunity
|
|
146
|
+
// that does NOT exist — an absence-as-present lie. `id` is the reliable signal:
|
|
147
|
+
// a real grant ALWAYS carries a numeric `id`, the hollow shell never does.
|
|
148
|
+
if (d.id === undefined || d.id === null) {
|
|
149
|
+
return { found: false, opportunityId: args.opportunityId };
|
|
150
|
+
}
|
|
64
151
|
const s = d.synopsis ?? {};
|
|
152
|
+
const rawContact = s.agencyContactName ?? s.agencyName ?? null;
|
|
65
153
|
return {
|
|
66
|
-
|
|
154
|
+
found: true,
|
|
155
|
+
id: d.id, // guaranteed present past the not-found guard above
|
|
67
156
|
opportunityNumber: d.opportunityNumber ?? "",
|
|
68
157
|
title: d.opportunityTitle ?? "",
|
|
69
|
-
|
|
158
|
+
// GRANT-2: `name` is the REAL agency (subtier preferred, else the department),
|
|
159
|
+
// NOT synopsis.agencyName (which is the contact person). Fall back through both
|
|
160
|
+
// the synopsis and top-level `data` locations. `department` is the top-tier
|
|
161
|
+
// agency; `contactName` preserves the person the mislabeled old `name` held
|
|
162
|
+
// (newlines collapsed so it renders on one line).
|
|
163
|
+
agency: {
|
|
164
|
+
code: s.agencyCode ?? d.owningAgencyCode,
|
|
165
|
+
name: s.agencyDetails?.agencyName ??
|
|
166
|
+
d.agencyDetails?.agencyName ??
|
|
167
|
+
s.topAgencyDetails?.agencyName ??
|
|
168
|
+
d.topAgencyDetails?.agencyName ??
|
|
169
|
+
null,
|
|
170
|
+
department: s.topAgencyDetails?.agencyName ?? d.topAgencyDetails?.agencyName ?? null,
|
|
171
|
+
contactName: rawContact ? rawContact.replace(/\s*\n\s*/g, " — ") : null,
|
|
172
|
+
},
|
|
70
173
|
description: s.synopsisDesc ?? d.synopsisDesc ?? "",
|
|
71
174
|
postingDate: s.postingDate,
|
|
72
175
|
responseDate: s.responseDate,
|
package/dist/grants.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grants.js","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,GAAG,+BAA+B,CAAC;AAE/C,KAAK,UAAU,QAAQ,CACrB,QAAgB,EAChB,IAA6B;IAE7B,MAAM,CAAC,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"grants.js","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSA daily-CSV keyless backbone (opt-in) — page-completing enrichment for the
|
|
3
|
+
* fields the keyless SAM HAL list endpoint nulls.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists
|
|
6
|
+
* ----------------
|
|
7
|
+
* `sam_search_opportunities` (keyless HAL) returns a page of notices but nulls
|
|
8
|
+
* each row's naics / setAside / place-of-performance / responseDeadline / type
|
|
9
|
+
* (the list payload omits those VALUES even though the server-side filter
|
|
10
|
+
* honored them). Today the only fill path is `sam_get_opportunity` PER notice —
|
|
11
|
+
* N detail calls to complete an N-row page.
|
|
12
|
+
*
|
|
13
|
+
* The GSA "Contract Opportunities" daily bulk CSV carries all of those fields
|
|
14
|
+
* keyed by NoticeId (the 32-hex id that equals the HAL `_id`). So ONE cached
|
|
15
|
+
* index lookup completes a whole page, and it's a durability hedge if the
|
|
16
|
+
* undocumented HAL list drifts.
|
|
17
|
+
*
|
|
18
|
+
* Source — LIVE-VERIFIED 2026-07-03
|
|
19
|
+
* ---------------------------------
|
|
20
|
+
* https://falextracts.s3.amazonaws.com/Contract Opportunities/datagov/ContractOpportunitiesFullCSV.csv
|
|
21
|
+
* HEAD 200, 225,722,960 bytes (225.7 MB), `last-modified` daily,
|
|
22
|
+
* `accept-ranges: bytes`, 47 columns. Key + HAL-nulled fields (0-indexed):
|
|
23
|
+
* NoticeId@0 (32-hex == HAL _id), Title@1, Type@10, SetASideCode@14,
|
|
24
|
+
* SetASide@15, ResponseDeadLine@16, NaicsCode@17, PopCity@20, PopState@21,
|
|
25
|
+
* PopZip@22, PopCountry@23, Active@24, Description@46.
|
|
26
|
+
*
|
|
27
|
+
* Gold-standard verification (2026-07-03): for a live notice
|
|
28
|
+
* (a4be592da0304872a252980925b9458f) the CSV index's naics/PoP EQUAL what
|
|
29
|
+
* `sam_get_opportunity` (detail) returns — naics 238990, PoP WA / Hoodsport /
|
|
30
|
+
* 98548 all match; setAsideCode 'SBA' equals detail's mapped setAside 'SBA'.
|
|
31
|
+
*
|
|
32
|
+
* The non-negotiable design constraints
|
|
33
|
+
* -------------------------------------
|
|
34
|
+
* 1. OFF BY DEFAULT. No forced 225 MB download. Enabled only via env
|
|
35
|
+
* (`SAM_GOV_CSV_CACHE` = a cache dir, or `SAM_GOV_ENABLE_CSV=1` to use a
|
|
36
|
+
* default dir under the OS temp). Every EXISTING keyless HAL tool is
|
|
37
|
+
* UNCHANGED.
|
|
38
|
+
* 2. BOUNDED RAM. Never hold the 225 MB in memory. Stream the download to a
|
|
39
|
+
* cache FILE on disk. Build a COMPACT index of only the ~11 needed columns
|
|
40
|
+
* (NOT Description@46), keyed by NoticeId (~24 MB for this snapshot's
|
|
41
|
+
* ~78k rows, verified). Persist the compact index to the cache dir and
|
|
42
|
+
* load THAT into a Map; refresh when > 24 h old or the CSV `last-modified`
|
|
43
|
+
* changed.
|
|
44
|
+
* 3. STREAMING RFC-4180 parser, NO new npm dependency. Fields are quoted and
|
|
45
|
+
* some (Title, addresses) embed commas; a state machine handles the
|
|
46
|
+
* in-quote toggle, `""`→`"` escape, and newlines inside quotes. Since we
|
|
47
|
+
* skip Description@46 (the worst offender) we only parse through column 24.
|
|
48
|
+
* 4. TRUTHFUL `_meta` (the product): honest source, freshness (CSV
|
|
49
|
+
* last-modified + index build time), degraded on download/parse failure
|
|
50
|
+
* (never a silent empty), and per-noticeId a notice ABSENT from the
|
|
51
|
+
* current snapshot → `found:false` + nulls + an explicit "not in current
|
|
52
|
+
* CSV snapshot" disclosure, NEVER faked. DISABLED → a structured note
|
|
53
|
+
* explaining how to enable it (not an error, not fake data). A cold-cache
|
|
54
|
+
* first call that triggers the download is slow → disclosed as "warming".
|
|
55
|
+
*/
|
|
56
|
+
/** The GSA daily bulk CSV (keyless). Space in the path is URL-encoded. */
|
|
57
|
+
export declare const GSA_CSV_URL = "https://falextracts.s3.amazonaws.com/Contract%20Opportunities/datagov/ContractOpportunitiesFullCSV.csv";
|
|
58
|
+
export declare const GSA_CSV_SOURCE = "gsa.gov daily bulk CSV (keyless)";
|
|
59
|
+
/** Max noticeIds accepted per batch (completes a `sam_search_opportunities` page). */
|
|
60
|
+
export declare const MAX_NOTICE_IDS = 100;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the opt-in config from the environment.
|
|
63
|
+
*
|
|
64
|
+
* Enablement (in priority order):
|
|
65
|
+
* - `SAM_GOV_CSV_FIXTURE` = path to a LOCAL CSV file → enabled, no network
|
|
66
|
+
* (used by tests and for pinning a pre-downloaded file). The cache dir
|
|
67
|
+
* defaults next to the fixture unless `SAM_GOV_CSV_CACHE` is also set.
|
|
68
|
+
* - `SAM_GOV_CSV_CACHE` = a cache DIRECTORY path → enabled; downloads live.
|
|
69
|
+
* - `SAM_GOV_ENABLE_CSV` truthy (`1`/`true`/`yes`) → enabled with a default
|
|
70
|
+
* cache dir under the OS temp (`<tmp>/mcp-sam-gov-csv`).
|
|
71
|
+
* - otherwise DISABLED (default) — the backbone never touches the network.
|
|
72
|
+
*/
|
|
73
|
+
export type CsvConfig = {
|
|
74
|
+
enabled: boolean;
|
|
75
|
+
cacheDir: string;
|
|
76
|
+
/** A local CSV to index instead of downloading (tests / pinned file). */
|
|
77
|
+
fixturePath: string | null;
|
|
78
|
+
};
|
|
79
|
+
export declare function resolveCsvConfig(env?: NodeJS.ProcessEnv): CsvConfig;
|
|
80
|
+
/**
|
|
81
|
+
* Parse one LOGICAL CSV record's text into fields 0..maxCol (inclusive).
|
|
82
|
+
*
|
|
83
|
+
* A proper state machine (NOT `split(",")`): tracks an in-quote toggle, turns
|
|
84
|
+
* `""` into a single `"`, and treats commas/CR/quotes literally while inside a
|
|
85
|
+
* quoted field. It also reports whether parsing ended INSIDE an open quote —
|
|
86
|
+
* the record-assembler uses that to know a physical newline fell inside a
|
|
87
|
+
* quoted field and the logical record continues on the next line.
|
|
88
|
+
*
|
|
89
|
+
* CRITICAL — we must scan the WHOLE text to get quote parity right, even though
|
|
90
|
+
* we only KEEP fields up to `maxCol`. Once we're past `maxCol` we stop STORING
|
|
91
|
+
* fields (so the giant Description@46 is never materialized — bounded memory),
|
|
92
|
+
* but we KEEP walking the characters to maintain the in-quote toggle. Early-
|
|
93
|
+
* returning at `maxCol` would misjudge a newline that falls inside a LATER
|
|
94
|
+
* quoted field (e.g. an embedded newline in Description@46): the record would
|
|
95
|
+
* be wrongly treated as complete and the field's tail would corrupt the next
|
|
96
|
+
* record. (This was a real bug — the fix is: cap storage, never cap scanning.)
|
|
97
|
+
*/
|
|
98
|
+
export declare function parseRecordFields(text: string, maxCol?: number): {
|
|
99
|
+
fields: string[];
|
|
100
|
+
inQuotes: boolean;
|
|
101
|
+
};
|
|
102
|
+
/** The compact per-notice record we keep in the index (NOT Description). */
|
|
103
|
+
export type NoticeFields = {
|
|
104
|
+
title: string;
|
|
105
|
+
type: string;
|
|
106
|
+
setAsideCode: string;
|
|
107
|
+
setAside: string;
|
|
108
|
+
responseDeadline: string;
|
|
109
|
+
naicsCode: string;
|
|
110
|
+
popCity: string;
|
|
111
|
+
popState: string;
|
|
112
|
+
popZip: string;
|
|
113
|
+
popCountry: string;
|
|
114
|
+
active: string;
|
|
115
|
+
};
|
|
116
|
+
/** TEST-ONLY: drop the process-lifetime memo so a test can re-point the env. */
|
|
117
|
+
export declare function _resetIndexForTests(): void;
|
|
118
|
+
/** A read-only view of a loaded index for enrichment consumers. */
|
|
119
|
+
export type ReadyIndex = {
|
|
120
|
+
get(noticeId: string): NoticeFields | undefined;
|
|
121
|
+
csvLastModified: string | null;
|
|
122
|
+
indexBuiltAt: string;
|
|
123
|
+
rowCount: number;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* NON-BLOCKING: return a ready CSV index if one is loaded + fresh, else null.
|
|
127
|
+
*
|
|
128
|
+
* Guarantees for the search hot-path:
|
|
129
|
+
* - Disabled config → null (no work, no network) — caller skips enrichment.
|
|
130
|
+
* - An in-memory index within its TTL → returned synchronously (no HEAD, no
|
|
131
|
+
* download).
|
|
132
|
+
* - No in-memory index (cold) → returns null IMMEDIATELY and, unless a
|
|
133
|
+
* refresh is already in flight, kicks off a background `ensureIndex` (its
|
|
134
|
+
* result is memoized into `loaded` for a subsequent call). The promise is
|
|
135
|
+
* deliberately NOT awaited here and its rejection is swallowed so a failed
|
|
136
|
+
* warm never surfaces on the search path.
|
|
137
|
+
* - An in-memory index PAST its TTL → still returned (stale-but-usable) while
|
|
138
|
+
* a background refresh is kicked off; the caller discloses the age via
|
|
139
|
+
* freshness so a slightly-stale snapshot is honest, never a stall.
|
|
140
|
+
*
|
|
141
|
+
* This never throws — any misconfiguration or I/O error degrades to null.
|
|
142
|
+
*/
|
|
143
|
+
export declare function tryGetReadyIndex(cfg?: CsvConfig): ReadyIndex | null;
|
|
144
|
+
/** One opportunity row as emitted by the keyless sam_search handler. */
|
|
145
|
+
export type SearchOppRow = {
|
|
146
|
+
noticeId: string;
|
|
147
|
+
title?: string | null;
|
|
148
|
+
agency?: string | null;
|
|
149
|
+
solicitationNumber?: string | null;
|
|
150
|
+
responseDeadline?: string | null;
|
|
151
|
+
naics?: string | null;
|
|
152
|
+
setAside?: string | null;
|
|
153
|
+
uiLink?: string | null;
|
|
154
|
+
type?: string | null;
|
|
155
|
+
placeOfPerformance?: SamCsvPlaceOfPerformance | null;
|
|
156
|
+
[k: string]: unknown;
|
|
157
|
+
};
|
|
158
|
+
/** Place-of-performance shape composed from the CSV Pop* columns. */
|
|
159
|
+
export type SamCsvPlaceOfPerformance = {
|
|
160
|
+
city: string | null;
|
|
161
|
+
state: string | null;
|
|
162
|
+
zip: string | null;
|
|
163
|
+
country: string | null;
|
|
164
|
+
};
|
|
165
|
+
export type EnrichmentOutcome = {
|
|
166
|
+
/** The page with null fields filled from the CSV where the notice was found. */
|
|
167
|
+
opportunities: SearchOppRow[];
|
|
168
|
+
/** Notices in the page that were present in the CSV snapshot. */
|
|
169
|
+
foundCount: number;
|
|
170
|
+
/** Notices in the page absent from the CSV snapshot (left un-enriched). */
|
|
171
|
+
missingCount: number;
|
|
172
|
+
/** Union of field names filled on ≥1 row (subset of naics/setAside/… ). */
|
|
173
|
+
fieldsFilled: Set<string>;
|
|
174
|
+
/** Snapshot freshness, mirrored into data + _meta by the caller. */
|
|
175
|
+
freshness: {
|
|
176
|
+
csvLastModified: string | null;
|
|
177
|
+
indexBuiltAt: string;
|
|
178
|
+
indexAgeHours: number | null;
|
|
179
|
+
rowCount: number;
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Fill the keyless search page's null naics/setAside/placeOfPerformance (and
|
|
184
|
+
* responseDeadline/type when currently null) from the ready CSV index.
|
|
185
|
+
*
|
|
186
|
+
* Rules (honesty):
|
|
187
|
+
* - Only a null field is filled — a value already present (e.g. keyed mode,
|
|
188
|
+
* or a HAL row that carried the value) is NEVER overwritten.
|
|
189
|
+
* - A field is filled only when the CSV cell is a real non-empty value (an
|
|
190
|
+
* empty CSV cell stays null — absence ≠ empty).
|
|
191
|
+
* - `type`/`placeOfPerformance` keys are ADDED only when a value is actually
|
|
192
|
+
* filled from the CSV, so a not-in-snapshot row keeps the exact original
|
|
193
|
+
* shape (no spurious null keys).
|
|
194
|
+
* - A noticeId absent from the snapshot is left byte-identical + counted.
|
|
195
|
+
*/
|
|
196
|
+
export declare function enrichSearchOpportunities(opportunities: SearchOppRow[], index: ReadyIndex): EnrichmentOutcome;
|
|
197
|
+
export type LookupResult = {
|
|
198
|
+
noticeId: string;
|
|
199
|
+
found: boolean;
|
|
200
|
+
naicsCode: string | null;
|
|
201
|
+
setAside: string | null;
|
|
202
|
+
setAsideCode: string | null;
|
|
203
|
+
popState: string | null;
|
|
204
|
+
popCity: string | null;
|
|
205
|
+
popZip: string | null;
|
|
206
|
+
popCountry: string | null;
|
|
207
|
+
responseDeadline: string | null;
|
|
208
|
+
type: string | null;
|
|
209
|
+
active: boolean | null;
|
|
210
|
+
title: string | null;
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* BATCH keyless enrichment: for each 32-hex noticeId return the HAL-nulled
|
|
214
|
+
* fields (naics / setAside / place-of-performance / responseDeadline / type /
|
|
215
|
+
* active / title) from the cached GSA CSV index — completing a whole
|
|
216
|
+
* `sam_search_opportunities` page in ONE call.
|
|
217
|
+
*
|
|
218
|
+
* Honesty contract (the product):
|
|
219
|
+
* - DISABLED (default) → `data.enabled:false`, every row `found:false`+nulls,
|
|
220
|
+
* and `_meta.notes` explains how to enable the backbone. NOT an error, NOT
|
|
221
|
+
* fake data.
|
|
222
|
+
* - a noticeId ABSENT from the current snapshot → `found:false` + nulls + a
|
|
223
|
+
* disclosure that it is "not in the current CSV snapshot", NEVER faked.
|
|
224
|
+
* - download/parse failure → `degraded` (a structured error is thrown and
|
|
225
|
+
* surfaced by the server as a retryable envelope) — never a silent empty.
|
|
226
|
+
* - `_meta.freshness` carries the CSV `last-modified` + the index build time
|
|
227
|
+
* so the AI knows how stale the snapshot is; a cold-cache first call that
|
|
228
|
+
* triggered the download is disclosed as "warming".
|
|
229
|
+
*/
|
|
230
|
+
export declare function lookupNoticeFields(args: {
|
|
231
|
+
noticeIds: string[];
|
|
232
|
+
}, env?: NodeJS.ProcessEnv): Promise<import("./meta.js").MetaBundle<{
|
|
233
|
+
results: LookupResult[];
|
|
234
|
+
enabled: boolean;
|
|
235
|
+
freshness: null;
|
|
236
|
+
}> | import("./meta.js").MetaBundle<{
|
|
237
|
+
results: LookupResult[];
|
|
238
|
+
enabled: boolean;
|
|
239
|
+
freshness: {
|
|
240
|
+
csvLastModified: string | null;
|
|
241
|
+
indexBuiltAt: string;
|
|
242
|
+
indexAgeHours: number | null;
|
|
243
|
+
rowCount: number;
|
|
244
|
+
warming: boolean;
|
|
245
|
+
};
|
|
246
|
+
foundCount: number;
|
|
247
|
+
missingCount: number;
|
|
248
|
+
}>>;
|
|
249
|
+
//# sourceMappingURL=gsa-csv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gsa-csv.d.ts","sourceRoot":"","sources":["../src/gsa-csv.ts"],"names":[],"mappings":"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"}
|