freshcontext-mcp 0.3.7 → 0.3.9
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 +93 -60
- package/package.json +1 -1
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* No other MCP server has this. USASpending.gov is the official US Treasury
|
|
5
5
|
* database of all federal contract awards. Updated daily.
|
|
6
6
|
*
|
|
7
|
+
* FIELD NAMING: USASpending API uses space-separated field names e.g. "Award ID",
|
|
8
|
+
* "Recipient Name", "Award Amount" — NOT underscores.
|
|
9
|
+
*
|
|
7
10
|
* Accepts:
|
|
8
11
|
* - Company name: "Palantir" → contracts awarded to that company
|
|
9
12
|
* - Keyword: "AI infrastructure" → contracts with that keyword in description
|
|
10
13
|
* - NAICS code: "541511" → all software publisher contracts
|
|
11
|
-
* - Direct URL: https://api.usaspending.gov/... → direct API call
|
|
12
14
|
*/
|
|
13
15
|
function sanitize(s) {
|
|
14
16
|
return s.replace(/[^\x20-\x7E]/g, "").trim();
|
|
@@ -28,21 +30,37 @@ function formatUSD(amount) {
|
|
|
28
30
|
const HEADERS = {
|
|
29
31
|
"Content-Type": "application/json",
|
|
30
32
|
"Accept": "application/json",
|
|
31
|
-
"User-Agent": "Mozilla/5.0 (compatible; freshcontext-mcp/1.0
|
|
33
|
+
"User-Agent": "Mozilla/5.0 (compatible; freshcontext-mcp/1.0)",
|
|
32
34
|
};
|
|
33
|
-
|
|
35
|
+
// USASpending API field names — space-separated, not underscores
|
|
36
|
+
const CONTRACT_FIELDS = [
|
|
37
|
+
"Award ID",
|
|
38
|
+
"Recipient Name",
|
|
39
|
+
"Award Amount",
|
|
40
|
+
"Award Date",
|
|
41
|
+
"Start Date",
|
|
42
|
+
"End Date",
|
|
43
|
+
"Awarding Agency",
|
|
44
|
+
"Awarding Sub Agency",
|
|
45
|
+
"Description",
|
|
46
|
+
"Place of Performance State Code",
|
|
47
|
+
"Place of Performance City Name",
|
|
48
|
+
"naics_code",
|
|
49
|
+
"naics_description",
|
|
50
|
+
];
|
|
51
|
+
async function postJSON(url, body) {
|
|
34
52
|
const controller = new AbortController();
|
|
35
53
|
const timeout = setTimeout(() => controller.abort(), 20000);
|
|
36
54
|
try {
|
|
37
55
|
const res = await fetch(url, {
|
|
38
|
-
method:
|
|
56
|
+
method: "POST",
|
|
39
57
|
headers: HEADERS,
|
|
40
|
-
body:
|
|
58
|
+
body: JSON.stringify(body),
|
|
41
59
|
signal: controller.signal,
|
|
42
60
|
});
|
|
43
61
|
if (!res.ok) {
|
|
44
62
|
const text = await res.text().catch(() => "");
|
|
45
|
-
throw new Error(`HTTP ${res.status}: ${text.slice(0,
|
|
63
|
+
throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
46
64
|
}
|
|
47
65
|
return await res.json();
|
|
48
66
|
}
|
|
@@ -50,46 +68,61 @@ async function fetchJSON(url, body) {
|
|
|
50
68
|
clearTimeout(timeout);
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let recipientName = name;
|
|
71
|
+
async function getJSON(url) {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const timeout = setTimeout(() => controller.abort(), 20000);
|
|
57
74
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
const res = await fetch(url, {
|
|
76
|
+
method: "GET",
|
|
77
|
+
headers: HEADERS,
|
|
78
|
+
signal: controller.signal,
|
|
79
|
+
});
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
const text = await res.text().catch(() => "");
|
|
82
|
+
throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
61
83
|
}
|
|
84
|
+
return await res.json();
|
|
62
85
|
}
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
finally {
|
|
87
|
+
clearTimeout(timeout);
|
|
65
88
|
}
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
}
|
|
90
|
+
// ─── Build award search body ──────────────────────────────────────────────────
|
|
91
|
+
function buildSearchBody(filters) {
|
|
92
|
+
return {
|
|
68
93
|
filters: {
|
|
69
|
-
|
|
94
|
+
...filters,
|
|
70
95
|
time_period: [{
|
|
71
96
|
start_date: new Date(Date.now() - 365 * 2 * 86400000).toISOString().slice(0, 10),
|
|
72
97
|
end_date: new Date().toISOString().slice(0, 10),
|
|
73
98
|
}],
|
|
74
99
|
award_type_codes: ["A", "B", "C", "D"],
|
|
75
100
|
},
|
|
76
|
-
fields:
|
|
77
|
-
"Award_ID", "Recipient_Name", "Award_Amount",
|
|
78
|
-
"Award_Date", "Start_Date", "End_Date",
|
|
79
|
-
"Awarding_Agency_Name", "Awarding_Sub_Agency_Name",
|
|
80
|
-
"Description", "recipient_location_state_name",
|
|
81
|
-
"recipient_location_city_name", "naics_code", "naics_description",
|
|
82
|
-
],
|
|
101
|
+
fields: CONTRACT_FIELDS,
|
|
83
102
|
page: 1,
|
|
84
103
|
limit: 10,
|
|
85
|
-
sort: "
|
|
104
|
+
sort: "Award Amount", // space-separated — matches field name exactly
|
|
86
105
|
order: "desc",
|
|
87
106
|
subawards: false,
|
|
88
107
|
};
|
|
89
|
-
|
|
108
|
+
}
|
|
109
|
+
// ─── Resolve company name via autocomplete ────────────────────────────────────
|
|
110
|
+
async function resolveRecipientName(name) {
|
|
111
|
+
try {
|
|
112
|
+
const data = await postJSON("https://api.usaspending.gov/api/v2/autocomplete/recipient/", { search_text: name, limit: 1 });
|
|
113
|
+
if (data.results?.length)
|
|
114
|
+
return data.results[0].recipient_name;
|
|
115
|
+
}
|
|
116
|
+
catch { /* fall through */ }
|
|
117
|
+
return name;
|
|
118
|
+
}
|
|
119
|
+
// ─── Search by recipient name ─────────────────────────────────────────────────
|
|
120
|
+
async function searchByRecipient(name, maxLength) {
|
|
121
|
+
const recipientName = await resolveRecipientName(name);
|
|
122
|
+
const data = await postJSON("https://api.usaspending.gov/api/v2/search/spending_by_award/", buildSearchBody({ recipient_search_text: [recipientName] }));
|
|
90
123
|
if (!data.results?.length) {
|
|
91
124
|
return {
|
|
92
|
-
raw: `No federal contracts found for "${name}" (
|
|
125
|
+
raw: `No federal contracts found for "${name}" (resolved to "${recipientName}") in the last 2 years.\n\nTips:\n- Try a keyword instead: "AI data analytics"\n- Try a NAICS code: "541511" (software)\n- Try the full legal name: "Palantir Technologies Inc"`,
|
|
93
126
|
content_date: null,
|
|
94
127
|
freshness_confidence: "high",
|
|
95
128
|
};
|
|
@@ -107,20 +140,14 @@ async function searchByKeyword(keyword, maxLength) {
|
|
|
107
140
|
}],
|
|
108
141
|
award_type_codes: ["A", "B", "C", "D"],
|
|
109
142
|
},
|
|
110
|
-
fields:
|
|
111
|
-
"Award_ID", "Recipient_Name", "Award_Amount",
|
|
112
|
-
"Award_Date", "Start_Date", "End_Date",
|
|
113
|
-
"Awarding_Agency_Name", "Awarding_Sub_Agency_Name",
|
|
114
|
-
"Description", "recipient_location_state_name",
|
|
115
|
-
"naics_code", "naics_description",
|
|
116
|
-
],
|
|
143
|
+
fields: CONTRACT_FIELDS,
|
|
117
144
|
page: 1,
|
|
118
145
|
limit: 10,
|
|
119
|
-
sort: "
|
|
146
|
+
sort: "Award Amount",
|
|
120
147
|
order: "desc",
|
|
121
148
|
subawards: false,
|
|
122
149
|
};
|
|
123
|
-
const data = await
|
|
150
|
+
const data = await postJSON("https://api.usaspending.gov/api/v2/search/spending_by_award/", body);
|
|
124
151
|
if (!data.results?.length) {
|
|
125
152
|
return {
|
|
126
153
|
raw: `No federal contracts found matching "${keyword}" in the last year.`,
|
|
@@ -134,26 +161,34 @@ async function searchByKeyword(keyword, maxLength) {
|
|
|
134
161
|
function formatResults(results, title, maxLength) {
|
|
135
162
|
const lines = [title, ""];
|
|
136
163
|
results.forEach((award, i) => {
|
|
137
|
-
const desc = sanitize(award
|
|
138
|
-
const location = [
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
lines.push(`
|
|
145
|
-
|
|
146
|
-
|
|
164
|
+
const desc = sanitize(award["Description"] ?? "No description").slice(0, 300);
|
|
165
|
+
const location = [
|
|
166
|
+
award["Place of Performance City Name"],
|
|
167
|
+
award["Place of Performance State Code"],
|
|
168
|
+
].filter(Boolean).join(", ") || "N/A";
|
|
169
|
+
const subAgency = award["Awarding Sub Agency"];
|
|
170
|
+
const agency = award["Awarding Agency"];
|
|
171
|
+
lines.push(`[${i + 1}] ${sanitize(award["Recipient Name"] ?? "Unknown")}`);
|
|
172
|
+
lines.push(` Amount: ${formatUSD(award["Award Amount"] ?? null)}`);
|
|
173
|
+
lines.push(` Awarded: ${award["Award Date"]?.slice(0, 10) ?? "unknown"}`);
|
|
174
|
+
lines.push(` Period: ${award["Start Date"]?.slice(0, 10) ?? "?"} → ${award["End Date"]?.slice(0, 10) ?? "?"}`);
|
|
175
|
+
lines.push(` Agency: ${sanitize(agency ?? "N/A")}`);
|
|
176
|
+
if (subAgency && subAgency !== agency) {
|
|
177
|
+
lines.push(` Sub: ${sanitize(subAgency)}`);
|
|
147
178
|
}
|
|
148
|
-
if (award
|
|
149
|
-
lines.push(` NAICS:
|
|
179
|
+
if (award["naics_code"]) {
|
|
180
|
+
lines.push(` NAICS: ${award["naics_code"]} — ${sanitize(award["naics_description"] ?? "")}`);
|
|
150
181
|
}
|
|
151
182
|
lines.push(` Location: ${location}`);
|
|
152
|
-
lines.push(`
|
|
183
|
+
lines.push(` Desc: ${desc}`);
|
|
153
184
|
lines.push("");
|
|
154
185
|
});
|
|
155
186
|
const raw = lines.join("\n").slice(0, maxLength);
|
|
156
|
-
const dates = results
|
|
187
|
+
const dates = results
|
|
188
|
+
.map(r => r["Award Date"])
|
|
189
|
+
.filter((d) => Boolean(d))
|
|
190
|
+
.sort()
|
|
191
|
+
.reverse();
|
|
157
192
|
return {
|
|
158
193
|
raw,
|
|
159
194
|
content_date: dates[0] ?? null,
|
|
@@ -166,32 +201,30 @@ export async function govContractsAdapter(options) {
|
|
|
166
201
|
const maxLength = options.maxLength ?? 6000;
|
|
167
202
|
if (!input)
|
|
168
203
|
throw new Error("Query required: company name, keyword, or NAICS code");
|
|
169
|
-
// Direct
|
|
170
|
-
if (input.startsWith("https://api.usaspending.gov")) {
|
|
171
|
-
const data = await
|
|
204
|
+
// Direct GET endpoint (non-search URLs)
|
|
205
|
+
if (input.startsWith("https://api.usaspending.gov") && !input.includes("spending_by_award")) {
|
|
206
|
+
const data = await getJSON(input);
|
|
172
207
|
return {
|
|
173
208
|
raw: JSON.stringify(data, null, 2).slice(0, maxLength),
|
|
174
209
|
content_date: new Date().toISOString(),
|
|
175
210
|
freshness_confidence: "high",
|
|
176
211
|
};
|
|
177
212
|
}
|
|
178
|
-
// NAICS code (6 digits)
|
|
213
|
+
// NAICS code (6 digits)
|
|
179
214
|
if (/^\d{6}$/.test(input)) {
|
|
180
215
|
return searchByKeyword(input, maxLength);
|
|
181
216
|
}
|
|
182
|
-
//
|
|
217
|
+
// Company name or keyword — try recipient first, fall back to keyword
|
|
183
218
|
try {
|
|
184
219
|
const result = await searchByRecipient(input, maxLength);
|
|
185
220
|
if (!result.raw.includes("No federal contracts found"))
|
|
186
221
|
return result;
|
|
187
|
-
// Fall back to keyword search
|
|
188
222
|
const kwResult = await searchByKeyword(input, maxLength);
|
|
189
223
|
if (!kwResult.raw.includes("No federal contracts found"))
|
|
190
224
|
return kwResult;
|
|
191
|
-
return result;
|
|
225
|
+
return result;
|
|
192
226
|
}
|
|
193
|
-
catch
|
|
194
|
-
// If recipient search fails entirely, try keyword
|
|
227
|
+
catch {
|
|
195
228
|
return searchByKeyword(input, maxLength);
|
|
196
229
|
}
|
|
197
230
|
}
|
package/package.json
CHANGED