freshcontext-mcp 0.3.4 → 0.3.6
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/adapters/govcontracts.js +104 -133
- package/package.json +1 -1
|
@@ -1,192 +1,163 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Government Contracts adapter — fetches awarded contract data from USASpending.gov
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* For GTM teams, VC investors, and competitive researchers, knowing when a
|
|
7
|
-
* company wins a government contract is a high-signal buying intent indicator.
|
|
8
|
-
* A company that just won a $2M DoD contract is hiring, spending, and building.
|
|
4
|
+
* No other MCP server has this. USASpending.gov is the official US Treasury
|
|
5
|
+
* database of all federal contract awards. Updated daily.
|
|
9
6
|
*
|
|
10
7
|
* Accepts:
|
|
11
|
-
* - Company name: "
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* - A URL: https://api.usaspending.gov/... → direct API call
|
|
16
|
-
*
|
|
17
|
-
* Data source: USASpending.gov public API (no API key required)
|
|
18
|
-
* Coverage: All US federal contracts, grants, and awards
|
|
19
|
-
* Freshness: Updated daily by the US Treasury
|
|
20
|
-
*
|
|
21
|
-
* What it returns:
|
|
22
|
-
* - Award recipient name and location
|
|
23
|
-
* - Contract amount (obligated)
|
|
24
|
-
* - Award date (high confidence timestamp)
|
|
25
|
-
* - Awarding agency and sub-agency
|
|
26
|
-
* - Contract description / award title
|
|
27
|
-
* - NAICS code and description
|
|
28
|
-
* - Period of performance dates
|
|
8
|
+
* - Company name: "Palantir" → contracts awarded to that company
|
|
9
|
+
* - Keyword: "AI infrastructure" → contracts with that keyword in description
|
|
10
|
+
* - NAICS code: "541511" → all software publisher contracts
|
|
11
|
+
* - Direct URL: https://api.usaspending.gov/... → direct API call
|
|
29
12
|
*/
|
|
30
13
|
function sanitize(s) {
|
|
31
14
|
return s.replace(/[^\x20-\x7E]/g, "").trim();
|
|
32
15
|
}
|
|
33
16
|
function formatUSD(amount) {
|
|
34
|
-
if (amount === null || isNaN(amount))
|
|
17
|
+
if (amount === null || amount === undefined || isNaN(amount))
|
|
35
18
|
return "N/A";
|
|
36
|
-
|
|
19
|
+
const abs = Math.abs(amount);
|
|
20
|
+
if (abs >= 1_000_000_000)
|
|
21
|
+
return `$${(amount / 1_000_000_000).toFixed(2)}B`;
|
|
22
|
+
if (abs >= 1_000_000)
|
|
37
23
|
return `$${(amount / 1_000_000).toFixed(2)}M`;
|
|
38
|
-
if (
|
|
24
|
+
if (abs >= 1_000)
|
|
39
25
|
return `$${(amount / 1_000).toFixed(1)}K`;
|
|
40
26
|
return `$${amount.toFixed(0)}`;
|
|
41
27
|
}
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
const HEADERS = {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"Accept": "application/json",
|
|
31
|
+
"User-Agent": "Mozilla/5.0 (compatible; freshcontext-mcp/1.0; +https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
32
|
+
};
|
|
33
|
+
async function fetchJSON(url, body) {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timeout = setTimeout(() => controller.abort(), 20000);
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(url, {
|
|
38
|
+
method: body ? "POST" : "GET",
|
|
39
|
+
headers: HEADERS,
|
|
40
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const text = await res.text().catch(() => "");
|
|
45
|
+
throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
46
|
+
}
|
|
47
|
+
return await res.json();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ─── Search by recipient name using autocomplete then awards ─────────────────
|
|
54
|
+
async function searchByRecipient(name, maxLength) {
|
|
55
|
+
// Step 1: Use autocomplete to get the exact recipient name USASpending knows
|
|
56
|
+
let recipientName = name;
|
|
57
|
+
try {
|
|
58
|
+
const autoRes = await fetchJSON("https://api.usaspending.gov/api/v2/autocomplete/recipient/", { search_text: name, limit: 1 });
|
|
59
|
+
if (autoRes.results?.length) {
|
|
60
|
+
recipientName = autoRes.results[0].recipient_name;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Use original name if autocomplete fails
|
|
65
|
+
}
|
|
66
|
+
// Step 2: Search awards with the resolved recipient name
|
|
44
67
|
const body = {
|
|
45
68
|
filters: {
|
|
46
|
-
recipient_search_text: [
|
|
47
|
-
time_period: [
|
|
48
|
-
{
|
|
69
|
+
recipient_search_text: [recipientName],
|
|
70
|
+
time_period: [{
|
|
49
71
|
start_date: new Date(Date.now() - 365 * 2 * 86400000).toISOString().slice(0, 10),
|
|
50
72
|
end_date: new Date().toISOString().slice(0, 10),
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
award_type_codes: ["A", "B", "C", "D"], // contracts only
|
|
73
|
+
}],
|
|
74
|
+
award_type_codes: ["A", "B", "C", "D"],
|
|
54
75
|
},
|
|
55
76
|
fields: [
|
|
56
|
-
"Award_ID", "Recipient_Name", "Award_Amount",
|
|
77
|
+
"Award_ID", "Recipient_Name", "Award_Amount",
|
|
57
78
|
"Award_Date", "Start_Date", "End_Date",
|
|
58
79
|
"Awarding_Agency_Name", "Awarding_Sub_Agency_Name",
|
|
59
|
-
"
|
|
60
|
-
"naics_code", "naics_description",
|
|
80
|
+
"Description", "recipient_location_state_name",
|
|
81
|
+
"recipient_location_city_name", "naics_code", "naics_description",
|
|
61
82
|
],
|
|
62
83
|
page: 1,
|
|
63
84
|
limit: 10,
|
|
64
|
-
sort: "
|
|
85
|
+
sort: "Award ID",
|
|
65
86
|
order: "desc",
|
|
66
87
|
subawards: false,
|
|
67
88
|
};
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"Accept": "application/json",
|
|
76
|
-
"User-Agent": "Mozilla/5.0 (compatible; freshcontext-mcp/1.0)",
|
|
77
|
-
"Origin": "https://www.usaspending.gov",
|
|
78
|
-
"Referer": "https://www.usaspending.gov/",
|
|
79
|
-
},
|
|
80
|
-
body: JSON.stringify(body),
|
|
81
|
-
signal: controller.signal,
|
|
82
|
-
});
|
|
83
|
-
clearTimeout(timeout);
|
|
84
|
-
if (!res.ok)
|
|
85
|
-
throw new Error(`USASpending API error: ${res.status} ${res.statusText}`);
|
|
86
|
-
const data = await res.json();
|
|
87
|
-
if (!data.results?.length) {
|
|
88
|
-
return {
|
|
89
|
-
raw: `No federal contracts found for "${query}" in the last 2 years.\n\nThis could mean:\n- The company name differs from the registered recipient name\n- The company operates under a subsidiary name\n- No contracts awarded in this period\n\nTry searching by parent company name or NAICS code.`,
|
|
90
|
-
content_date: null,
|
|
91
|
-
freshness_confidence: "high",
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
return formatResults(data.results, `Federal contracts — ${query}`, maxLength);
|
|
95
|
-
}
|
|
96
|
-
finally {
|
|
97
|
-
clearTimeout(timeout);
|
|
89
|
+
const data = await fetchJSON("https://api.usaspending.gov/api/v2/search/spending_by_award/", body);
|
|
90
|
+
if (!data.results?.length) {
|
|
91
|
+
return {
|
|
92
|
+
raw: `No federal contracts found for "${name}" (searched as "${recipientName}") in the last 2 years.\n\nTips:\n- Try the full legal company name (e.g. "Palantir Technologies Inc")\n- Try a keyword search instead (e.g. "AI data analytics")\n- Try a NAICS code (e.g. 541511 for software)`,
|
|
93
|
+
content_date: null,
|
|
94
|
+
freshness_confidence: "high",
|
|
95
|
+
};
|
|
98
96
|
}
|
|
97
|
+
return formatResults(data.results, `Federal contracts — ${recipientName}`, maxLength);
|
|
99
98
|
}
|
|
100
99
|
// ─── Search by keyword ────────────────────────────────────────────────────────
|
|
101
100
|
async function searchByKeyword(keyword, maxLength) {
|
|
102
101
|
const body = {
|
|
103
102
|
filters: {
|
|
104
103
|
keywords: [keyword],
|
|
105
|
-
time_period: [
|
|
106
|
-
{
|
|
104
|
+
time_period: [{
|
|
107
105
|
start_date: new Date(Date.now() - 365 * 86400000).toISOString().slice(0, 10),
|
|
108
106
|
end_date: new Date().toISOString().slice(0, 10),
|
|
109
|
-
},
|
|
110
|
-
],
|
|
107
|
+
}],
|
|
111
108
|
award_type_codes: ["A", "B", "C", "D"],
|
|
112
109
|
},
|
|
113
110
|
fields: [
|
|
114
|
-
"Award_ID", "Recipient_Name", "Award_Amount",
|
|
111
|
+
"Award_ID", "Recipient_Name", "Award_Amount",
|
|
115
112
|
"Award_Date", "Start_Date", "End_Date",
|
|
116
113
|
"Awarding_Agency_Name", "Awarding_Sub_Agency_Name",
|
|
117
|
-
"
|
|
114
|
+
"Description", "recipient_location_state_name",
|
|
115
|
+
"naics_code", "naics_description",
|
|
118
116
|
],
|
|
119
117
|
page: 1,
|
|
120
118
|
limit: 10,
|
|
121
|
-
sort: "
|
|
119
|
+
sort: "Award ID",
|
|
122
120
|
order: "desc",
|
|
123
121
|
subawards: false,
|
|
124
122
|
};
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"Accept": "application/json",
|
|
133
|
-
"User-Agent": "Mozilla/5.0 (compatible; freshcontext-mcp/1.0)",
|
|
134
|
-
"Origin": "https://www.usaspending.gov",
|
|
135
|
-
"Referer": "https://www.usaspending.gov/",
|
|
136
|
-
},
|
|
137
|
-
body: JSON.stringify(body),
|
|
138
|
-
signal: controller.signal,
|
|
139
|
-
});
|
|
140
|
-
clearTimeout(timeout);
|
|
141
|
-
if (!res.ok)
|
|
142
|
-
throw new Error(`USASpending keyword search error: ${res.status} ${res.statusText}`);
|
|
143
|
-
const data = await res.json();
|
|
144
|
-
if (!data.results?.length) {
|
|
145
|
-
return {
|
|
146
|
-
raw: `No federal contracts found matching keyword "${keyword}" in the last year.`,
|
|
147
|
-
content_date: null,
|
|
148
|
-
freshness_confidence: "high",
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
return formatResults(data.results, `Federal contracts matching "${keyword}"`, maxLength);
|
|
152
|
-
}
|
|
153
|
-
finally {
|
|
154
|
-
clearTimeout(timeout);
|
|
123
|
+
const data = await fetchJSON("https://api.usaspending.gov/api/v2/search/spending_by_award/", body);
|
|
124
|
+
if (!data.results?.length) {
|
|
125
|
+
return {
|
|
126
|
+
raw: `No federal contracts found matching "${keyword}" in the last year.`,
|
|
127
|
+
content_date: null,
|
|
128
|
+
freshness_confidence: "high",
|
|
129
|
+
};
|
|
155
130
|
}
|
|
131
|
+
return formatResults(data.results, `Federal contracts matching "${keyword}"`, maxLength);
|
|
156
132
|
}
|
|
157
133
|
// ─── Format results ───────────────────────────────────────────────────────────
|
|
158
134
|
function formatResults(results, title, maxLength) {
|
|
159
135
|
const lines = [title, ""];
|
|
160
136
|
results.forEach((award, i) => {
|
|
161
|
-
const desc = sanitize(award.Description ?? "No description");
|
|
137
|
+
const desc = sanitize(award.Description ?? "No description").slice(0, 300);
|
|
162
138
|
const location = [award.recipient_location_city_name, award.recipient_location_state_name]
|
|
163
139
|
.filter(Boolean).join(", ") || "N/A";
|
|
164
140
|
lines.push(`[${i + 1}] ${sanitize(award.Recipient_Name ?? "Unknown")}`);
|
|
165
|
-
lines.push(` Amount: ${formatUSD(award.Award_Amount)}`);
|
|
141
|
+
lines.push(` Amount: ${formatUSD(award.Award_Amount ?? null)}`);
|
|
166
142
|
lines.push(` Awarded: ${award.Award_Date?.slice(0, 10) ?? "unknown"}`);
|
|
167
143
|
lines.push(` Period: ${award.Start_Date?.slice(0, 10) ?? "?"} → ${award.End_Date?.slice(0, 10) ?? "?"}`);
|
|
168
144
|
lines.push(` Agency: ${sanitize(award.Awarding_Agency_Name ?? "N/A")}`);
|
|
169
|
-
if (award.Awarding_Sub_Agency_Name
|
|
145
|
+
if (award.Awarding_Sub_Agency_Name !== award.Awarding_Agency_Name && award.Awarding_Sub_Agency_Name) {
|
|
170
146
|
lines.push(` Sub-agency: ${sanitize(award.Awarding_Sub_Agency_Name)}`);
|
|
171
147
|
}
|
|
172
148
|
if (award.naics_code) {
|
|
173
149
|
lines.push(` NAICS: ${award.naics_code} — ${sanitize(award.naics_description ?? "")}`);
|
|
174
150
|
}
|
|
175
151
|
lines.push(` Location: ${location}`);
|
|
176
|
-
lines.push(` Description: ${desc
|
|
152
|
+
lines.push(` Description: ${desc}`);
|
|
177
153
|
lines.push("");
|
|
178
154
|
});
|
|
179
155
|
const raw = lines.join("\n").slice(0, maxLength);
|
|
180
|
-
|
|
181
|
-
const dates = results
|
|
182
|
-
.map((r) => r.Award_Date)
|
|
183
|
-
.filter(Boolean)
|
|
184
|
-
.sort()
|
|
185
|
-
.reverse();
|
|
156
|
+
const dates = results.map(r => r.Award_Date).filter(Boolean).sort().reverse();
|
|
186
157
|
return {
|
|
187
158
|
raw,
|
|
188
159
|
content_date: dates[0] ?? null,
|
|
189
|
-
freshness_confidence: "high",
|
|
160
|
+
freshness_confidence: "high",
|
|
190
161
|
};
|
|
191
162
|
}
|
|
192
163
|
// ─── Main export ──────────────────────────────────────────────────────────────
|
|
@@ -197,30 +168,30 @@ export async function govContractsAdapter(options) {
|
|
|
197
168
|
throw new Error("Query required: company name, keyword, or NAICS code");
|
|
198
169
|
// Direct API URL
|
|
199
170
|
if (input.startsWith("https://api.usaspending.gov")) {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
171
|
+
const data = await fetchJSON(input);
|
|
172
|
+
return {
|
|
173
|
+
raw: JSON.stringify(data, null, 2).slice(0, maxLength),
|
|
174
|
+
content_date: new Date().toISOString(),
|
|
175
|
+
freshness_confidence: "high",
|
|
176
|
+
};
|
|
206
177
|
}
|
|
207
|
-
// NAICS code (6 digits)
|
|
178
|
+
// NAICS code (6 digits) — treat as keyword
|
|
208
179
|
if (/^\d{6}$/.test(input)) {
|
|
209
180
|
return searchByKeyword(input, maxLength);
|
|
210
181
|
}
|
|
211
|
-
//
|
|
182
|
+
// Multi-word input or known company name → try recipient first, fall back to keyword
|
|
212
183
|
try {
|
|
213
184
|
const result = await searchByRecipient(input, maxLength);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return result;
|
|
185
|
+
if (!result.raw.includes("No federal contracts found"))
|
|
186
|
+
return result;
|
|
187
|
+
// Fall back to keyword search
|
|
188
|
+
const kwResult = await searchByKeyword(input, maxLength);
|
|
189
|
+
if (!kwResult.raw.includes("No federal contracts found"))
|
|
190
|
+
return kwResult;
|
|
191
|
+
return result; // Return the "not found" message from recipient search
|
|
222
192
|
}
|
|
223
|
-
catch {
|
|
193
|
+
catch (err) {
|
|
194
|
+
// If recipient search fails entirely, try keyword
|
|
224
195
|
return searchByKeyword(input, maxLength);
|
|
225
196
|
}
|
|
226
197
|
}
|
package/package.json
CHANGED