surplus-auctions-mcp
# surplus-auctions-mcp
**GovDeals as agent tools.** Every week, thousands of U.S. cities, counties, school districts, police departments and state agencies auction off pickup trucks, police cruisers, school buses, excavators, laptops and office furniture on **GovDeals**, often far below market value. GovDeals has no public API, so agents can't use it directly.
This MCP server gives any MCP-compatible assistant five tools for searching, inspecting and comparing GovDeals auctions. [TinyFish](https://tinyfish.ai) does the page reading underneath.
```
You: Find pickup trucks under $8k ending this week in Texas, then compare mileage on the best three.
Agent: search_auctions → get_auction_details ×3 → compare_auctions
"The 2015 Silverado (San Antonio) is cheapest at $1,100 + 12.5% premium, but it has no engine.
The 2009 F-350 has the lowest mileage (81,938) at $2,640 all-in and closes tomorrow at 1:10 PM CT…"
```
---
## Quick start
You need Node 20+ and a TinyFish API key ([create one](https://agent.tinyfish.ai/api-keys)). The normal path uses only TinyFish Fetch, which is free up to 1,000 URLs a day.
```bash
git clone <this repo> surplus-auctions-mcp && cd surplus-auctions-mcp
npm install # also builds dist/
export TINYFISH_API_KEY=sk-tinyfish-...
npm run health # live check: should print 3× PASS
```
Then connect it to your assistant (below).
## Connect it to your assistant
Every client runs the same command: `node /absolute/path/to/surplus-auctions-mcp/dist/src/index.js`, with `TINYFISH_API_KEY` in its environment.
### Claude Desktop
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows), then restart the app:
```json
{
"mcpServers": {
"surplus-auctions": {
"command": "node",
"args": ["/absolute/path/to/surplus-auctions-mcp/dist/src/index.js"],
"env": { "TINYFISH_API_KEY": "sk-tinyfish-..." }
}
}
}
```
### Claude Code
```bash
claude mcp add surplus-auctions -e TINYFISH_API_KEY=sk-tinyfish-... -- node /absolute/path/to/surplus-auctions-mcp/dist/src/index.js
```
### Cursor, Windsurf and other stdio clients
Add a server with the same command and environment variable as above.
### ChatGPT and other remote clients (Streamable HTTP)
```bash
MCP_HTTP_TOKEN=choose-a-secret npm run start:http # serves http://localhost:3333/mcp
```
Expose it over HTTPS (for example `cloudflared tunnel --url http://localhost:3333` or any reverse proxy), then add `https://<host>/mcp` as a connector in ChatGPT (Settings → Connectors, developer mode). If `MCP_HTTP_TOKEN` is set, clients must send `Authorization: Bearer <token>`.
### Settings
| Env var | Default | Meaning |
|---|---|---|
| `TINYFISH_API_KEY` | (required) | Your TinyFish key |
| `SURPLUS_AGENT_FALLBACK` | `on` | When GovDeals changes its pages, retry with TinyFish Agent (uses credits, slower). `off` returns a `SITE_CHANGED` error instead |
| `PORT` | `3333` | HTTP mode only |
| `MCP_HTTP_TOKEN` | unset | HTTP mode only: require this bearer token |
See `.env.example` for the same list with comments. The server does not read `.env` files: set these with `export` in your shell or in your MCP client's `env` block, as shown above.
---
## Using it
Ask your assistant in plain language. It picks the tools. Some prompts to try:
1. *"Find pickup trucks under $8,000 in Texas ending this week, then compare mileage and total cost on the three best."*
2. *"What police cars or SUVs close in the next 24 hours anywhere in the U.S.? Show me the five cheapest."*
3. *"I need an excavator under $50k in Florida. Show me what's available, then give me full details on the cheapest one, including the buyer's premium and where I'd pick it up."*
4. *"Is the surplus auction tool working right now?"*
A typical session goes: **search → pick a few → get details → compare → open the listing and bid on GovDeals.** The server never bids for you.
### The five tools
| Tool | Use it to | Key inputs | Returns |
|---|---|---|---|
| `search_auctions` | Find items | `keyword`, `state`, `category`, `min_price`, `max_price`, `ending_within_hours` (max 336), `limit` | `auctions[]` (id, title, url, current_bid, ends_at, hours_left, closes_in, state, city), `warnings` |
| `list_ending_soon` | See what closes in the next N hours | `hours` (max 336), `state`, `category`, `keyword`, `max_price` | Same as search, soonest first |
| `get_auction_details` | Decide whether to bid | `auction_id` (from search, or a GovDeals asset URL) | Bid and bid count, exact end time, agency, pickup location, buyer's premium, reserve status, condition, description, and for vehicles year / make / model / mileage / hours / VIN |
| `compare_auctions` | Choose between 2–5 lots | `auction_ids[]` | Side-by-side rows with estimated total (bid + premium) and highlights: cheapest, lowest mileage, newest, ending first |
| `health_check` | Check whether GovDeals still works | none | pass / degraded / fail per tool, with the reason |
**Ids** look like `govdeals:27018-390` (GovDeals' own Lot#). Full GovDeals asset URLs also work.
**Categories:** `vehicles`, `heavy_equipment`, `trailers`, `boats_marine`, `computers_electronics`, `furniture`, `office_equipment`, `industrial_tools`, `medical`, `real_estate`, `other`. Vehicles and trailers are filtered by GovDeals itself. The rest are matched by keyword, and the response says so in `warnings`.
**Reading the numbers**
- Prices are current bids in USD, and they change constantly. `ends_at` is UTC.
- `estimated_total` is the bid plus the buyer's premium. It does not include sales tax, title fees or transport.
- Mileage and hours come from the seller's listing and are sometimes wrong or missing. Check the listing before you bid.
### A real session
Recorded 2026-11-03 in Claude Code against live GovDeals data (bids and times have since moved on):
```
You: Find pickup trucks under $8,000 in Texas ending this week, then compare mileage and total cost on the three best.
search_auctions {keyword: "pickup truck", category: "vehicles", state: "TX", max_price: 8000, ending_within_hours: 144, limit: 20}
→ 20 auctions, e.g. govdeals:24637-96 2018 Ford F-150 $3,150 ends 2026-11-05T20:00Z Corpus Christi
compare_auctions {auction_ids: [govdeals:24637-96, govdeals:24637-90, govdeals:31894-8, govdeals:4052-1806, govdeals:4052-1805]}
→ govdeals:24637-96 2018 Ford F-150 72,679 mi bid $3,150 total $3,543.75 (12.5% premium)
govdeals:24637-90 2016 Ford F-150 86,653 mi bid $2,815 total $3,166.88
govdeals:31894-8 2019 Ford F-250 SD 159,880 mi bid $1,000 total $1,100.00 (10% premium)
govdeals:4052-1806 2016 Silverado 2500HD 197,605 mi bid $3,250 total $3,656.25
govdeals:4052-1805 2016 Ford F-350 SD 142,919 mi bid $3,000 total $3,375.00
highlights: lowest_mileage = govdeals:24637-96, lowest_estimated_total = govdeals:31894-8
Agent: The 2018 F-150 has the lowest mileage (72,679 mi, ~$3,544 all-in). The 2019 F-250 is the cheapest
(~$1,100) but has 159,880 mi. The 2016 F-150 sits between them at ~$3,167 and needs a jump to start.
```
The same session also covered a Florida excavator search (`search_auctions` → `get_auction_details`) and a `health_check` (3× pass).
---
## How TinyFish Fetch and Agent are used
```
tool call → build the GovDeals URL from typed inputs
→ TinyFish Fetch renders the page to markdown
→ strict parser + field validation → structured JSON (extraction: "fetch")
↳ page changed or blocked → TinyFish Agent reads it with an output_schema
→ structured JSON (extraction: "agent_fallback" + FETCH_PARSER_DRIFT warning)
↳ Agent fails too → typed error (SITE_CHANGED / SITE_BLOCKED)
```
**Fetch does the everyday work.** GovDeals is a JavaScript app, so plain HTTP sees an empty shell. Every search and detail page is rendered by Fetch in a real browser and returned as markdown, then parsed. A search is one Fetch call. Result cards whose title didn't render are filled from their detail pages in one extra Fetch batch.
**Agent found the site's search parameters, and is the safety net.**
- *Discovery.* GovDeals hides its filters behind forms and JavaScript. Calibration runs gave Agent goals like "search GovDeals for 'pickup truck', filter to Texas, sort by closing soon, report the address-bar URL". That revealed the real query parameters (`stateName=Texas`, `category=6`, price in cents as `price=c-0:800000`, `sf=auctionclose`), now encoded and commented in [`govdeals.ts`](src/sources/govdeals.ts).
- *Fallback.* If a parser stops recognizing a page, the tool re-runs the task with Agent (`stealth` profile). The data still comes back, tagged `extraction: "agent_fallback"` with a `FETCH_PARSER_DRIFT` warning so the drift stays visible.
**Why Fetch first:** an Agent search took 3–4.5 minutes (19 browser steps) in testing. The same search via Fetch takes 15–50 seconds and uses no Agent credits.
## Breaking loudly, not silently
An agent can always tell "nothing matched" apart from "the site broke":
- If GovDeals can't be read, the tool returns a typed error, never an empty list. An empty `auctions` list means the site really had no matches.
- Parsers are strict. Required fields (price line, end time, the `Lot#` marker) must be present or the parser raises drift. A results page that shows listings but yields no parseable rows counts as drift, not as "empty".
- TinyFish reports `COMPLETED` even when the goal failed, so the result payload is inspected as well.
- The health check disables the Agent fallback, so a redesign shows up as a failure instead of being hidden. The test suite simulates redesigns against recorded pages (for example GovDeals dropping its `Lot#:` marker) and asserts the right tool reports `fail` / `SITE_CHANGED`.
---
## Troubleshooting
Errors come back with a `code`, a `message`, a `retryable` flag and a `hint`.
| Code | What it means and what to do |
|---|---|
| `TINYFISH_AUTH` | The API key is missing or wrong. Set `TINYFISH_API_KEY` |
| `RATE_LIMITED` | TinyFish rate or credit limit. Wait a minute and check your balance |
| `UPSTREAM_TIMEOUT` | Retry once, or narrow the search with a state or category |
| `SITE_BLOCKED` | GovDeals blocked the browser. Retry in a few minutes |
| `SITE_CHANGED` | GovDeals changed its layout. Run `health_check` and report it |
| `NOT_FOUND` | The auction closed, sold or was withdrawn |
| `INVALID_INPUT` | Bad id, state, or contradictory filters (for example min price above max) |
| `TINYFISH_ERROR` | Other TinyFish failure. Retry once |
An empty `auctions` list means GovDeals really had no matches. A broken site returns an error instead.
**Slow responses.** Each GovDeals page takes about 15–50 seconds to load, so a multi-step conversation can take a few minutes. If your client has a tool timeout, raise it. Searches are cached for 60 seconds and details for 30 seconds.
**Health check.**
```bash
npm run health # exit 0 healthy · 1 broken · 2 degraded
npm run health -- --with-agent # also test the Agent fallback
npm run health -- --json
```
The same check is available to your assistant as the `health_check` tool.
---
## Development
```bash
npm test # offline, against real GovDeals pages recorded in test/fixtures
npm run typecheck
npm run build # rebuild dist/ after changing the source
npm run smoke # live end-to-end over MCP stdio
npm run edge # live edge cases: other states, filters, bad ids, empty results
npm run calibrate # re-capture live pages into test/fixtures/live/ after a site change
```
```
src/
index.ts stdio + Streamable HTTP entry point
server.ts MCP tool definitions (zod input/output schemas)
service.ts search, caching, compare
health.ts live checks
sources/ GovDeals URL builder, page parsers, Fetch → Agent fallback
tinyfish/client.ts TinyFish Agent + Fetch client
test/
fixtures/ recorded GovDeals pages (contact details redacted)
helpers/fixture.ts test stand-in for TinyFish
```
`.github/workflows/health.yml` runs the tests on every push and the live health check daily. Add `TINYFISH_API_KEY` as a repo secret.
## Limitations
- **Read-only.** No bidding, no login, no watchlists.
- Covers GovDeals only.
- Use within GovDeals' terms of service. Results link back to the original listing, where all bidding happens.
MIT licensed.
TDQS
Scored across 5 tools
Most tools have clearly distinct roles: details, comparison, and health checks are unambiguous. The main overlap is between search_auctions and list_ending_soon, since both return filtered auction listings, though the latter is explicitly time-scoped and bargain-oriented. Descriptions provide enough guidance to choose correctly in most cases.
Names mostly follow a verb_noun or verb_phrase pattern: search_auctions, get_auction_details, compare_auctions. list_ending_soon and health_check deviate slightly by omitting an explicit resource noun, but remain readable and consistent in snake_case.
Five tools is well-scoped for a read-only auction research server. Each tool earns its place: discovery, time-filtered discovery, detail retrieval, comparison, and diagnostics.
The surface covers search, time-based discovery, details, comparison, and health checking, which are the core workflows for auction research. Gaps exist around bidding, watchlists, saved searches, or alerts, but those may be intentionally out of scope for a read-only integration.