freshcontext-mcp 0.3.4 → 0.3.5

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.
@@ -1,63 +1,84 @@
1
1
  /**
2
2
  * Government Contracts adapter — fetches awarded contract data from USASpending.gov
3
3
  *
4
- * Why this is unique:
5
- * No other MCP server exposes government contract data.
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: "Cloudflare" → finds contracts awarded to that company
12
- * - NAICS code: "541511" → software publishers contracts
13
- * - Agency name: "Department of Defense" → all DoD contracts
14
- * - Keyword: "AI infrastructure" contracts with that keyword
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
- if (Math.abs(amount) >= 1_000_000)
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 (Math.abs(amount) >= 1_000)
24
+ if (abs >= 1_000)
39
25
  return `$${(amount / 1_000).toFixed(1)}K`;
40
26
  return `$${amount.toFixed(0)}`;
41
27
  }
42
- // ─── Search by recipient (company name) ──────────────────────────────────────
43
- async function searchByRecipient(query, maxLength) {
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: [query],
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", "Description",
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
- "recipient_location_state_name", "recipient_location_city_name",
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,
@@ -65,56 +86,33 @@ async function searchByRecipient(query, maxLength) {
65
86
  order: "desc",
66
87
  subawards: false,
67
88
  };
68
- const controller = new AbortController();
69
- const timeout = setTimeout(() => controller.abort(), 15000);
70
- try {
71
- const res = await fetch("https://api.usaspending.gov/api/v2/search/spending_by_award/", {
72
- method: "POST",
73
- headers: {
74
- "Content-Type": "application/json",
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", "Description",
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
- "recipient_location_state_name", "naics_code", "naics_description",
114
+ "Description", "recipient_location_state_name",
115
+ "naics_code", "naics_description",
118
116
  ],
119
117
  page: 1,
120
118
  limit: 10,
@@ -122,71 +120,44 @@ async function searchByKeyword(keyword, maxLength) {
122
120
  order: "desc",
123
121
  subawards: false,
124
122
  };
125
- const controller = new AbortController();
126
- const timeout = setTimeout(() => controller.abort(), 15000);
127
- try {
128
- const res = await fetch("https://api.usaspending.gov/api/v2/search/spending_by_award/", {
129
- method: "POST",
130
- headers: {
131
- "Content-Type": "application/json",
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 && award.Awarding_Sub_Agency_Name !== award.Awarding_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.slice(0, 200)}`);
152
+ lines.push(` Description: ${desc}`);
177
153
  lines.push("");
178
154
  });
179
155
  const raw = lines.join("\n").slice(0, maxLength);
180
- // Newest award date for freshness
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", // USASpending dates are structured API fields
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 res = await fetch(input, { headers: { "User-Agent": "freshcontext-mcp" } });
201
- if (!res.ok)
202
- throw new Error(`USASpending direct fetch error: ${res.status}`);
203
- const data = await res.json();
204
- const raw = JSON.stringify(data, null, 2).slice(0, maxLength);
205
- return { raw, content_date: new Date().toISOString(), freshness_confidence: "high" };
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
- // Default: try as recipient name first, fall back to keyword
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
- // If no results found, try keyword search
215
- if (result.raw.includes("No federal contracts found")) {
216
- const kwResult = await searchByKeyword(input, maxLength);
217
- if (!kwResult.raw.includes("No federal contracts found")) {
218
- return kwResult;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "freshcontext-mcp",
3
3
  "mcpName": "io.github.PrinceGabriel-lgtm/freshcontext",
4
- "version": "0.3.4",
4
+ "version": "0.3.5",
5
5
  "description": "Real-time web extraction MCP server with freshness timestamps for AI agents",
6
6
  "keywords": [
7
7
  "mcp",