← back to Govauctions Mcp
govauctions-mcp.mjs
153 lines
#!/usr/bin/env node
// govauctions-mcp — MCP server exposing live US government auction listings via
// the free official GSA Auctions API (https://gsa.github.io/auctions_api/).
//
// Tools: search_auctions, get_auction, auction_summary.
// The GSA endpoint returns ALL active auctions in one JSON payload, so we fetch
// once and cache ~5 min, then filter in-memory — near-zero API calls.
//
// Env: GSA_API_KEY (defaults to public DEMO_KEY). GOVAUCTIONS_API_KEY is accepted
// as an alias, but GSA is free and DEMO_KEY works out of the box.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const GSA_URL = "https://api.gsa.gov/assets/gsaauctions/v2/auctions";
const API_KEY = process.env.GSA_API_KEY || process.env.GOVAUCTIONS_API_KEY || "DEMO_KEY";
const CACHE_MS = 5 * 60 * 1000;
let cache = { at: 0, rows: [] };
const num = (v) => {
const n = Number(String(v ?? "").replace(/[$,]/g, ""));
return Number.isFinite(n) ? n : 0;
};
function normalize(r) {
const sale = (r.saleNo || "").trim();
const lot = (r.lotNo || "").trim();
return {
id: [sale, lot].filter(Boolean).join("-"),
saleNo: sale,
lotNo: lot,
title: (r.itemName || "").trim(),
description: r.lotInfo || "",
status: r.auctionStatus || "",
startDate: r.aucStartDt || null,
endDate: r.aucEndDt || null,
currentBid: num(r.highBidAmount),
reserve: num(r.reserve),
bidders: num(r.biddersCount),
city: r.propertyCity || "",
state: r.propertyState || "",
zip: r.propertyZip || "",
agency: r.agencyName || "",
bureau: r.bureauName || "",
url: r.itemDescURL || "https://gsaauctions.gov",
imageUrl: r.imageURL || null,
};
}
async function getFeed() {
const now = Date.now();
if (cache.rows.length && now - cache.at < CACHE_MS) return cache.rows;
const res = await fetch(GSA_URL, { headers: { "X-API-KEY": API_KEY, Accept: "application/json" } });
if (!res.ok) throw new Error(`GSA Auctions API ${res.status}: ${(await res.text()).slice(0, 160)}`);
const data = await res.json();
const rows = (data.Results || []).map(normalize).filter((r) => r.title);
cache = { at: now, rows };
return rows;
}
function ok(obj) {
return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] };
}
function fail(msg) {
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
}
const server = new McpServer({ name: "govauctions", version: "0.1.0" });
server.registerTool(
"search_auctions",
{
title: "Search government auctions",
description:
"Search live active US federal-surplus auctions (GSA Auctions API). Filter by keyword (matches item name/description), 2-letter state, and/or agency. Returns normalized listings.",
inputSchema: {
keyword: z.string().optional().describe("term to match in item name/description, e.g. 'forklift'"),
state: z.string().optional().describe("2-letter state code, e.g. 'CA'"),
agency: z.string().optional().describe("agency name fragment"),
openOnly: z.boolean().optional().describe("only auctions currently accepting bids"),
limit: z.number().int().min(1).max(200).optional(),
},
},
async ({ keyword, state, agency, openOnly, limit = 25 }) => {
try {
let rows = await getFeed();
if (keyword) {
const k = keyword.toLowerCase();
rows = rows.filter((r) => `${r.title} ${r.description}`.toLowerCase().includes(k));
}
if (state) rows = rows.filter((r) => r.state.toUpperCase() === state.toUpperCase());
if (agency) rows = rows.filter((r) => r.agency.toLowerCase().includes(agency.toLowerCase()));
if (openOnly) rows = rows.filter((r) => /active|open|bidding/i.test(r.status));
return ok({ count: rows.length, results: rows.slice(0, limit) });
} catch (e) {
return fail(e.message);
}
},
);
server.registerTool(
"get_auction",
{
title: "Get a specific auction",
description: "Fetch a single auction lot by sale number (and optional lot number).",
inputSchema: {
saleNo: z.string().describe("GSA sale number, e.g. '4-1-QSC-I-26-385'"),
lotNo: z.string().optional(),
},
},
async ({ saleNo, lotNo }) => {
try {
const rows = await getFeed();
const matches = rows.filter((r) => r.saleNo === saleNo && (!lotNo || r.lotNo === lotNo));
if (!matches.length) return fail(`No auction found for sale ${saleNo}${lotNo ? ` lot ${lotNo}` : ""}`);
return ok(matches.length === 1 ? matches[0] : { count: matches.length, lots: matches });
} catch (e) {
return fail(e.message);
}
},
);
server.registerTool(
"auction_summary",
{
title: "Summary of active auctions",
description: "Totals for currently-listed federal auctions: overall count, breakdown by state and by status.",
inputSchema: {},
},
async () => {
try {
const rows = await getFeed();
const by = (key) =>
Object.fromEntries(
Object.entries(
rows.reduce((m, r) => ((m[r[key] || "?"] = (m[r[key] || "?"] || 0) + 1), m), {}),
)
.sort((a, b) => b[1] - a[1])
.slice(0, 12),
);
return ok({ total: rows.length, byState: by("state"), byStatus: by("status") });
} catch (e) {
return fail(e.message);
}
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`govauctions MCP ready (GSA Auctions API, key ${API_KEY === "DEMO_KEY" ? "DEMO_KEY" : "custom"})`);