freshcontext-mcp 0.3.2 → 0.3.3
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 +196 -0
- package/dist/server.js +19 -0
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Government Contracts adapter — fetches awarded contract data from USASpending.gov
|
|
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.
|
|
9
|
+
*
|
|
10
|
+
* 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
|
|
29
|
+
*/
|
|
30
|
+
function sanitize(s) {
|
|
31
|
+
return s.replace(/[^\x20-\x7E]/g, "").trim();
|
|
32
|
+
}
|
|
33
|
+
function formatUSD(amount) {
|
|
34
|
+
if (amount === null || isNaN(amount))
|
|
35
|
+
return "N/A";
|
|
36
|
+
if (Math.abs(amount) >= 1_000_000)
|
|
37
|
+
return `$${(amount / 1_000_000).toFixed(2)}M`;
|
|
38
|
+
if (Math.abs(amount) >= 1_000)
|
|
39
|
+
return `$${(amount / 1_000).toFixed(1)}K`;
|
|
40
|
+
return `$${amount.toFixed(0)}`;
|
|
41
|
+
}
|
|
42
|
+
// ─── Search by recipient (company name) ──────────────────────────────────────
|
|
43
|
+
async function searchByRecipient(query, maxLength) {
|
|
44
|
+
const body = {
|
|
45
|
+
filters: {
|
|
46
|
+
recipient_search_text: [query],
|
|
47
|
+
time_period: [
|
|
48
|
+
{
|
|
49
|
+
start_date: new Date(Date.now() - 365 * 2 * 86400000).toISOString().slice(0, 10),
|
|
50
|
+
end_date: new Date().toISOString().slice(0, 10),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
award_type_codes: ["A", "B", "C", "D"], // contracts only
|
|
54
|
+
},
|
|
55
|
+
fields: [
|
|
56
|
+
"Award_ID", "Recipient_Name", "Award_Amount", "Description",
|
|
57
|
+
"Award_Date", "Start_Date", "End_Date",
|
|
58
|
+
"Awarding_Agency_Name", "Awarding_Sub_Agency_Name",
|
|
59
|
+
"recipient_location_state_name", "recipient_location_city_name",
|
|
60
|
+
"naics_code", "naics_description",
|
|
61
|
+
],
|
|
62
|
+
page: 1,
|
|
63
|
+
limit: 10,
|
|
64
|
+
sort: "Award_Amount",
|
|
65
|
+
order: "desc",
|
|
66
|
+
subawards: false,
|
|
67
|
+
};
|
|
68
|
+
const res = await fetch("https://api.usaspending.gov/api/v2/search/spending_by_award/", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json", "User-Agent": "freshcontext-mcp" },
|
|
71
|
+
body: JSON.stringify(body),
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok)
|
|
74
|
+
throw new Error(`USASpending API error: ${res.status}`);
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
if (!data.results?.length) {
|
|
77
|
+
return {
|
|
78
|
+
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.`,
|
|
79
|
+
content_date: null,
|
|
80
|
+
freshness_confidence: "high",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return formatResults(data.results, `Federal contracts — ${query}`, maxLength);
|
|
84
|
+
}
|
|
85
|
+
// ─── Search by keyword ────────────────────────────────────────────────────────
|
|
86
|
+
async function searchByKeyword(keyword, maxLength) {
|
|
87
|
+
const body = {
|
|
88
|
+
filters: {
|
|
89
|
+
keywords: [keyword],
|
|
90
|
+
time_period: [
|
|
91
|
+
{
|
|
92
|
+
start_date: new Date(Date.now() - 365 * 86400000).toISOString().slice(0, 10),
|
|
93
|
+
end_date: new Date().toISOString().slice(0, 10),
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
award_type_codes: ["A", "B", "C", "D"],
|
|
97
|
+
},
|
|
98
|
+
fields: [
|
|
99
|
+
"Award_ID", "Recipient_Name", "Award_Amount", "Description",
|
|
100
|
+
"Award_Date", "Start_Date", "End_Date",
|
|
101
|
+
"Awarding_Agency_Name", "Awarding_Sub_Agency_Name",
|
|
102
|
+
"recipient_location_state_name", "naics_code", "naics_description",
|
|
103
|
+
],
|
|
104
|
+
page: 1,
|
|
105
|
+
limit: 10,
|
|
106
|
+
sort: "Award_Amount",
|
|
107
|
+
order: "desc",
|
|
108
|
+
subawards: false,
|
|
109
|
+
};
|
|
110
|
+
const res = await fetch("https://api.usaspending.gov/api/v2/search/spending_by_award/", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "Content-Type": "application/json", "User-Agent": "freshcontext-mcp" },
|
|
113
|
+
body: JSON.stringify(body),
|
|
114
|
+
});
|
|
115
|
+
if (!res.ok)
|
|
116
|
+
throw new Error(`USASpending keyword search error: ${res.status}`);
|
|
117
|
+
const data = await res.json();
|
|
118
|
+
if (!data.results?.length) {
|
|
119
|
+
return {
|
|
120
|
+
raw: `No federal contracts found matching keyword "${keyword}" in the last year.`,
|
|
121
|
+
content_date: null,
|
|
122
|
+
freshness_confidence: "high",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return formatResults(data.results, `Federal contracts matching "${keyword}"`, maxLength);
|
|
126
|
+
}
|
|
127
|
+
// ─── Format results ───────────────────────────────────────────────────────────
|
|
128
|
+
function formatResults(results, title, maxLength) {
|
|
129
|
+
const lines = [title, ""];
|
|
130
|
+
results.forEach((award, i) => {
|
|
131
|
+
const desc = sanitize(award.Description ?? "No description");
|
|
132
|
+
const location = [award.recipient_location_city_name, award.recipient_location_state_name]
|
|
133
|
+
.filter(Boolean).join(", ") || "N/A";
|
|
134
|
+
lines.push(`[${i + 1}] ${sanitize(award.Recipient_Name ?? "Unknown")}`);
|
|
135
|
+
lines.push(` Amount: ${formatUSD(award.Award_Amount)}`);
|
|
136
|
+
lines.push(` Awarded: ${award.Award_Date?.slice(0, 10) ?? "unknown"}`);
|
|
137
|
+
lines.push(` Period: ${award.Start_Date?.slice(0, 10) ?? "?"} → ${award.End_Date?.slice(0, 10) ?? "?"}`);
|
|
138
|
+
lines.push(` Agency: ${sanitize(award.Awarding_Agency_Name ?? "N/A")}`);
|
|
139
|
+
if (award.Awarding_Sub_Agency_Name && award.Awarding_Sub_Agency_Name !== award.Awarding_Agency_Name) {
|
|
140
|
+
lines.push(` Sub-agency: ${sanitize(award.Awarding_Sub_Agency_Name)}`);
|
|
141
|
+
}
|
|
142
|
+
if (award.naics_code) {
|
|
143
|
+
lines.push(` NAICS: ${award.naics_code} — ${sanitize(award.naics_description ?? "")}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push(` Location: ${location}`);
|
|
146
|
+
lines.push(` Description: ${desc.slice(0, 200)}`);
|
|
147
|
+
lines.push("");
|
|
148
|
+
});
|
|
149
|
+
const raw = lines.join("\n").slice(0, maxLength);
|
|
150
|
+
// Newest award date for freshness
|
|
151
|
+
const dates = results
|
|
152
|
+
.map((r) => r.Award_Date)
|
|
153
|
+
.filter(Boolean)
|
|
154
|
+
.sort()
|
|
155
|
+
.reverse();
|
|
156
|
+
return {
|
|
157
|
+
raw,
|
|
158
|
+
content_date: dates[0] ?? null,
|
|
159
|
+
freshness_confidence: "high", // USASpending dates are structured API fields
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// ─── Main export ──────────────────────────────────────────────────────────────
|
|
163
|
+
export async function govContractsAdapter(options) {
|
|
164
|
+
const input = (options.url ?? "").trim();
|
|
165
|
+
const maxLength = options.maxLength ?? 6000;
|
|
166
|
+
if (!input)
|
|
167
|
+
throw new Error("Query required: company name, keyword, or NAICS code");
|
|
168
|
+
// Direct API URL
|
|
169
|
+
if (input.startsWith("https://api.usaspending.gov")) {
|
|
170
|
+
const res = await fetch(input, { headers: { "User-Agent": "freshcontext-mcp" } });
|
|
171
|
+
if (!res.ok)
|
|
172
|
+
throw new Error(`USASpending direct fetch error: ${res.status}`);
|
|
173
|
+
const data = await res.json();
|
|
174
|
+
const raw = JSON.stringify(data, null, 2).slice(0, maxLength);
|
|
175
|
+
return { raw, content_date: new Date().toISOString(), freshness_confidence: "high" };
|
|
176
|
+
}
|
|
177
|
+
// NAICS code (6 digits)
|
|
178
|
+
if (/^\d{6}$/.test(input)) {
|
|
179
|
+
return searchByKeyword(input, maxLength);
|
|
180
|
+
}
|
|
181
|
+
// Default: try as recipient name first, fall back to keyword
|
|
182
|
+
try {
|
|
183
|
+
const result = await searchByRecipient(input, maxLength);
|
|
184
|
+
// If no results found, try keyword search
|
|
185
|
+
if (result.raw.includes("No federal contracts found")) {
|
|
186
|
+
const kwResult = await searchByKeyword(input, maxLength);
|
|
187
|
+
if (!kwResult.raw.includes("No federal contracts found")) {
|
|
188
|
+
return kwResult;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return searchByKeyword(input, maxLength);
|
|
195
|
+
}
|
|
196
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { repoSearchAdapter } from "./adapters/repoSearch.js";
|
|
|
10
10
|
import { packageTrendsAdapter } from "./adapters/packageTrends.js";
|
|
11
11
|
import { jobsAdapter } from "./adapters/jobs.js";
|
|
12
12
|
import { changelogAdapter } from "./adapters/changelog.js";
|
|
13
|
+
import { govContractsAdapter } from "./adapters/govcontracts.js";
|
|
13
14
|
import { stampFreshness, formatForLLM } from "./tools/freshnessStamp.js";
|
|
14
15
|
import { formatSecurityError } from "./security.js";
|
|
15
16
|
const server = new McpServer({
|
|
@@ -201,6 +202,24 @@ server.registerTool("extract_changelog", {
|
|
|
201
202
|
return { content: [{ type: "text", text: formatSecurityError(err) }] };
|
|
202
203
|
}
|
|
203
204
|
});
|
|
205
|
+
// ─── Tool: extract_govcontracts ────────────────────────────────────────────
|
|
206
|
+
server.registerTool("extract_govcontracts", {
|
|
207
|
+
description: "Fetch US federal government contract awards from USASpending.gov. No API key required. Search by company name (e.g. 'Palantir'), keyword (e.g. 'AI infrastructure'), or NAICS code (e.g. '541511'). Returns award amounts, dates, awarding agency, NAICS code, and contract descriptions — all timestamped. Use this to find buying intent signals (a company that just won a $5M DoD contract is actively hiring and spending), competitive intelligence, or GTM targeting.",
|
|
208
|
+
inputSchema: z.object({
|
|
209
|
+
url: z.string().describe("Company name (e.g. 'Cloudflare'), keyword (e.g. 'machine learning'), NAICS code (e.g. '541511'), or direct USASpending API URL."),
|
|
210
|
+
max_length: z.number().optional().default(6000).describe("Max content length"),
|
|
211
|
+
}),
|
|
212
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
213
|
+
}, async ({ url, max_length }) => {
|
|
214
|
+
try {
|
|
215
|
+
const result = await govContractsAdapter({ url, maxLength: max_length });
|
|
216
|
+
const ctx = stampFreshness(result, { url, maxLength: max_length }, "govcontracts");
|
|
217
|
+
return { content: [{ type: "text", text: formatForLLM(ctx) }] };
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
return { content: [{ type: "text", text: formatSecurityError(err) }] };
|
|
221
|
+
}
|
|
222
|
+
});
|
|
204
223
|
// ─── Start ───────────────────────────────────────────────────────────────────
|
|
205
224
|
async function main() {
|
|
206
225
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED