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.
@@ -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; +https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
33
+ "User-Agent": "Mozilla/5.0 (compatible; freshcontext-mcp/1.0)",
32
34
  };
33
- async function fetchJSON(url, body) {
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: body ? "POST" : "GET",
56
+ method: "POST",
39
57
  headers: HEADERS,
40
- body: body ? JSON.stringify(body) : undefined,
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, 200)}`);
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
- // ─── 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;
71
+ async function getJSON(url) {
72
+ const controller = new AbortController();
73
+ const timeout = setTimeout(() => controller.abort(), 20000);
57
74
  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;
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
- catch {
64
- // Use original name if autocomplete fails
86
+ finally {
87
+ clearTimeout(timeout);
65
88
  }
66
- // Step 2: Search awards with the resolved recipient name
67
- const body = {
89
+ }
90
+ // ─── Build award search body ──────────────────────────────────────────────────
91
+ function buildSearchBody(filters) {
92
+ return {
68
93
  filters: {
69
- recipient_search_text: [recipientName],
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: "Award_ID",
104
+ sort: "Award Amount", // space-separated — matches field name exactly
86
105
  order: "desc",
87
106
  subawards: false,
88
107
  };
89
- const data = await fetchJSON("https://api.usaspending.gov/api/v2/search/spending_by_award/", body);
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}" (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)`,
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: "Award_ID",
146
+ sort: "Award Amount",
120
147
  order: "desc",
121
148
  subawards: false,
122
149
  };
123
- const data = await fetchJSON("https://api.usaspending.gov/api/v2/search/spending_by_award/", body);
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.Description ?? "No description").slice(0, 300);
138
- const location = [award.recipient_location_city_name, award.recipient_location_state_name]
139
- .filter(Boolean).join(", ") || "N/A";
140
- lines.push(`[${i + 1}] ${sanitize(award.Recipient_Name ?? "Unknown")}`);
141
- lines.push(` Amount: ${formatUSD(award.Award_Amount ?? null)}`);
142
- lines.push(` Awarded: ${award.Award_Date?.slice(0, 10) ?? "unknown"}`);
143
- lines.push(` Period: ${award.Start_Date?.slice(0, 10) ?? "?"} → ${award.End_Date?.slice(0, 10) ?? "?"}`);
144
- lines.push(` Agency: ${sanitize(award.Awarding_Agency_Name ?? "N/A")}`);
145
- if (award.Awarding_Sub_Agency_Name !== award.Awarding_Agency_Name && award.Awarding_Sub_Agency_Name) {
146
- lines.push(` Sub-agency: ${sanitize(award.Awarding_Sub_Agency_Name)}`);
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.naics_code) {
149
- lines.push(` NAICS: ${award.naics_code} — ${sanitize(award.naics_description ?? "")}`);
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(` Description: ${desc}`);
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.map(r => r.Award_Date).filter(Boolean).sort().reverse();
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 API URL
170
- if (input.startsWith("https://api.usaspending.gov")) {
171
- const data = await fetchJSON(input);
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) — treat as keyword
213
+ // NAICS code (6 digits)
179
214
  if (/^\d{6}$/.test(input)) {
180
215
  return searchByKeyword(input, maxLength);
181
216
  }
182
- // Multi-word input or known company name → try recipient first, fall back to keyword
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; // Return the "not found" message from recipient search
225
+ return result;
192
226
  }
193
- catch (err) {
194
- // If recipient search fails entirely, try keyword
227
+ catch {
195
228
  return searchByKeyword(input, maxLength);
196
229
  }
197
230
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "freshcontext-mcp",
3
3
  "mcpName": "io.github.PrinceGabriel-lgtm/freshcontext",
4
- "version": "0.3.7",
4
+ "version": "0.3.9",
5
5
  "description": "Real-time web extraction MCP server with freshness timestamps for AI agents",
6
6
  "keywords": [
7
7
  "mcp",