@cliwant/mcp-sam-gov 1.14.0 → 1.16.0
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/README.ja.md +3 -2
- package/README.ko.md +3 -2
- package/README.md +7 -5
- package/dist/arcgis-feature.d.ts.map +1 -1
- package/dist/arcgis-feature.js +8 -0
- package/dist/arcgis-feature.js.map +1 -1
- package/dist/bonfire.d.ts +1 -1
- package/dist/bonfire.d.ts.map +1 -1
- package/dist/bonfire.js +19 -17
- package/dist/bonfire.js.map +1 -1
- package/dist/ckan.d.ts +6 -4
- package/dist/ckan.d.ts.map +1 -1
- package/dist/ckan.js +17 -4
- package/dist/ckan.js.map +1 -1
- package/dist/data-map.d.ts +43 -0
- package/dist/data-map.d.ts.map +1 -0
- package/dist/data-map.js +344 -0
- package/dist/data-map.js.map +1 -0
- package/dist/fpds.d.ts.map +1 -1
- package/dist/fpds.js +3 -2
- package/dist/fpds.js.map +1 -1
- package/dist/gao.d.ts.map +1 -1
- package/dist/gao.js +17 -6
- package/dist/gao.js.map +1 -1
- package/dist/gsa-perdiem.d.ts +5 -0
- package/dist/gsa-perdiem.d.ts.map +1 -1
- package/dist/gsa-perdiem.js +6 -1
- package/dist/gsa-perdiem.js.map +1 -1
- package/dist/open-checkbook.d.ts +8 -0
- package/dist/open-checkbook.d.ts.map +1 -1
- package/dist/open-checkbook.js +9 -1
- package/dist/open-checkbook.js.map +1 -1
- package/dist/pricing.d.ts +1 -0
- package/dist/pricing.d.ts.map +1 -1
- package/dist/pricing.js +135 -50
- package/dist/pricing.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +76 -24
- package/dist/server.js.map +1 -1
- package/dist/socrata.d.ts +16 -1
- package/dist/socrata.d.ts.map +1 -1
- package/dist/socrata.js +76 -1
- package/dist/socrata.js.map +1 -1
- package/package.json +1 -1
- package/src/arcgis-feature.ts +8 -0
- package/src/bonfire.ts +19 -17
- package/src/ckan.ts +17 -4
- package/src/data-map.ts +386 -0
- package/src/fpds.ts +3 -2
- package/src/gao.ts +20 -6
- package/src/gsa-perdiem.ts +12 -1
- package/src/open-checkbook.ts +26 -3
- package/src/pricing.ts +142 -48
- package/src/server.ts +88 -23
- package/src/socrata.ts +83 -1
package/src/socrata.ts
CHANGED
|
@@ -179,7 +179,8 @@ export const SOCRATA_DOMAINS = [
|
|
|
179
179
|
// allowlist, not the TLD; these five are the documented non-.gov entries. ──
|
|
180
180
|
"data.cityofnewyork.us", // NYC OpenData (.us, official) — e.g. dg92-zbpx (City Record: procurement notices)
|
|
181
181
|
"data.cityofchicago.org", // Chicago (.org, official) — e.g. rsxa-ify5 (Contracts)
|
|
182
|
-
"data.sfgov.org", // San Francisco / DataSF (
|
|
182
|
+
"data.sfgov.org", // San Francisco / DataSF — PERMANENTLY MIGRATED to data.sf.gov (301 all paths, 2026-09); kept allowlisted so requests reach the migration handler (see MIGRATED_SOCRATA_HOSTS below) instead of returning invalid_input. Use data.sf.gov for all new calls.
|
|
183
|
+
"data.sf.gov", // San Francisco / DataSF (canonical post-migration .gov domain) — e.g. cqi5-hm2d (Supplier Contracts 49,186 rows), hmh3-ff63 (Airport/SFO Contract Opportunities)
|
|
183
184
|
"controllerdata.lacity.org", // LA City Controller (.org, official) — e.g. pggv-e4fn (Checkbook L.A.)
|
|
184
185
|
"opendata.usac.org", // USAC E-rate (.org, m6) — e.g. avi8-svp9
|
|
185
186
|
// ── County/city procurement sweep (loop cycle 17, 2026-07-20). Discovered via
|
|
@@ -203,6 +204,7 @@ export const SOCRATA_DOMAINS = [
|
|
|
203
204
|
"data.coloradosprings.gov", // Colorado Springs CO (.gov) — e.g. yn6y-xikx (Open Checkbook Vendors ~19k)
|
|
204
205
|
"data.framinghamma.gov", // Framingham MA (.gov) — e.g. cqve-ehkr (Checkbook ~324k)
|
|
205
206
|
"data.fultoncountyga.gov", // Fulton County GA (.gov) — e.g. mxhc-krcg (Vendor Payments/disbursements ~217k)
|
|
207
|
+
"sharefulton.fultoncountyga.gov", // Fulton County GA (.gov, second portal) — e.g. kp4p-scak (Vendor Payments 226,797 rows 2014–present, updated 2026-09-14, attribution: "Fulton County Government (GA)")
|
|
206
208
|
"atlanta.data.socrata.com", // City of Atlanta GA (Socrata-hosted official portal) — e.g. jmke-icfi (Open Checkbook Ledger ~1.78M)
|
|
207
209
|
"opendata.cityofmesquite.com", // Mesquite TX (.com, official) — e.g. 6tva-azs5 (Check Register ~144k)
|
|
208
210
|
// ── County/city procurement sweep, wave 3 (loop cycle 20, 2026-07-20). Expanded
|
|
@@ -230,6 +232,24 @@ export type SocrataDomain = (typeof SOCRATA_DOMAINS)[number];
|
|
|
230
232
|
|
|
231
233
|
const SOCRATA_DOMAIN_SET: ReadonlySet<string> = new Set(SOCRATA_DOMAINS);
|
|
232
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Known host migrations (permanent 301 redirects). A domain here has permanently
|
|
237
|
+
* moved; redirect:"error" means any fetch to the old host will fail immediately.
|
|
238
|
+
* Rather than returning a retryable upstream_unavailable (which implies the
|
|
239
|
+
* problem may resolve on its own), we pre-flight check this map and return
|
|
240
|
+
* invalid_input — non-retryable, naming the replacement — so the caller can
|
|
241
|
+
* update the `domain` parameter instead of spinning on a doomed retry loop.
|
|
242
|
+
*
|
|
243
|
+
* Keep the old host in SOCRATA_DOMAINS so it passes the allowlist gate (invalid
|
|
244
|
+
* domains are rejected before reaching this check). Only add an entry here when
|
|
245
|
+
* the migration is confirmed by a live 301 probe with a stable redirect_url.
|
|
246
|
+
*
|
|
247
|
+
* data.sfgov.org → data.sf.gov (confirmed 2026-09-21; all paths 301)
|
|
248
|
+
*/
|
|
249
|
+
export const MIGRATED_SOCRATA_HOSTS: ReadonlyMap<string, string> = new Map([
|
|
250
|
+
["data.sfgov.org", "data.sf.gov"],
|
|
251
|
+
]);
|
|
252
|
+
|
|
233
253
|
const CATALOG_HOST = "api.us.socrata.com";
|
|
234
254
|
const CATALOG_URL = `https://${CATALOG_HOST}/api/catalog/v1`;
|
|
235
255
|
|
|
@@ -297,6 +317,18 @@ async function getSocrataResource(
|
|
|
297
317
|
retryable: false,
|
|
298
318
|
});
|
|
299
319
|
}
|
|
320
|
+
// Migration pre-flight (MIGRATED_SOCRATA_HOSTS): if the domain has permanently
|
|
321
|
+
// moved, redirect:"error" means any fetch will immediately fail with a 301.
|
|
322
|
+
// Return invalid_input (non-retryable) now — before wasting a network round-trip
|
|
323
|
+
// — naming the replacement so the caller can update the domain parameter.
|
|
324
|
+
const migratedTo = MIGRATED_SOCRATA_HOSTS.get(domain);
|
|
325
|
+
if (migratedTo !== undefined) {
|
|
326
|
+
throw new ToolErrorCarrier({
|
|
327
|
+
kind: "invalid_input",
|
|
328
|
+
message: `Socrata host ${JSON.stringify(domain)} has permanently moved to ${JSON.stringify(migratedTo)}. Update the \`domain\` parameter to ${JSON.stringify(migratedTo)} — retrying ${JSON.stringify(domain)} will not succeed.`,
|
|
329
|
+
retryable: false,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
300
332
|
if (datasetId.length !== 9 || !DATASET_ID_RE.test(datasetId)) {
|
|
301
333
|
throw new ToolErrorCarrier({
|
|
302
334
|
kind: "invalid_input",
|
|
@@ -375,6 +407,15 @@ async function getHostCatalog(
|
|
|
375
407
|
retryable: false,
|
|
376
408
|
});
|
|
377
409
|
}
|
|
410
|
+
// Migration pre-flight — same policy as getSocrataResource (see comment there).
|
|
411
|
+
const migratedTo = MIGRATED_SOCRATA_HOSTS.get(domain);
|
|
412
|
+
if (migratedTo !== undefined) {
|
|
413
|
+
throw new ToolErrorCarrier({
|
|
414
|
+
kind: "invalid_input",
|
|
415
|
+
message: `Socrata host ${JSON.stringify(domain)} has permanently moved to ${JSON.stringify(migratedTo)}. Update the \`domain\` parameter to ${JSON.stringify(migratedTo)} — retrying ${JSON.stringify(domain)} will not succeed.`,
|
|
416
|
+
retryable: false,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
378
419
|
const url = `https://${domain}/api/catalog/v1?${params.toString()}`;
|
|
379
420
|
const built = new URL(url);
|
|
380
421
|
if (built.hostname !== domain || built.protocol !== "https:") {
|
|
@@ -419,6 +460,12 @@ async function fetchCount(
|
|
|
419
460
|
const params = new URLSearchParams();
|
|
420
461
|
if (where) params.set("$where", where);
|
|
421
462
|
if (q) params.set("$q", q);
|
|
463
|
+
// ★ MUST stay count(*) — DO NOT change to count(1) or SELECT count(1).
|
|
464
|
+
// Cloudflare-fronted Socrata hosts (e.g. opendata.maryland.gov) deterministically
|
|
465
|
+
// 403 ("Just a moment...") on count(1) and `$query=SELECT count(1)` — their WAF
|
|
466
|
+
// treats the pattern as SQLi-like — while count(*) consistently returns 200.
|
|
467
|
+
// Verified 3/3 on opendata.maryland.gov (2026-09-21): count(1)→403, count(*)→200
|
|
468
|
+
// (342 rows). Maryland is not broken; the shape here is load-bearing.
|
|
422
469
|
params.set("$select", "count(*)");
|
|
423
470
|
let body: unknown;
|
|
424
471
|
try {
|
|
@@ -547,6 +594,22 @@ export async function query(args: {
|
|
|
547
594
|
);
|
|
548
595
|
}
|
|
549
596
|
|
|
597
|
+
// ── Response-side aggregate nudge (§A-nudge) ──
|
|
598
|
+
// When the result is truncated (hasMore) and the caller used no aggregate select,
|
|
599
|
+
// the agent only sees a PAGE of rows — NOT the total. Nudge toward an aggregate
|
|
600
|
+
// query so the agent does not try to sum one page manually.
|
|
601
|
+
if (!isAggregateSelect && hasMore) {
|
|
602
|
+
// Find the most likely amount-like column from the first row's keys.
|
|
603
|
+
const firstRow = rows[0];
|
|
604
|
+
const amountKey =
|
|
605
|
+
firstRow !== undefined
|
|
606
|
+
? (Object.keys(firstRow).find((k) => /amount|amt|total|paid|payment/i.test(k)) ?? "<amount column>")
|
|
607
|
+
: "<amount column>";
|
|
608
|
+
notes.push(
|
|
609
|
+
`This is a PAGE of raw rows (truncated — more rows exist). Do NOT sum this page to get a total. For a grand total: re-query with select='sum(${amountKey})' plus a where filter for vendor/fiscal year. For a top-N ranking: select='vendor_name, sum(${amountKey}) as total' with order='total DESC'. These are aggregate queries — the result is the final answer, not another page.`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
550
613
|
return withMeta(
|
|
551
614
|
{ domain: args.domain, datasetId: args.datasetId, rows },
|
|
552
615
|
{
|
|
@@ -666,6 +729,25 @@ export async function discoverDatasets(args: {
|
|
|
666
729
|
`Showing ${returned} of ${totalAvailable} matches; raise limit (≤100) or narrow q for the rest.`,
|
|
667
730
|
);
|
|
668
731
|
}
|
|
732
|
+
// HONESTY (false-zero guard). An empty catalog result is easy to read as "this
|
|
733
|
+
// portal has no such data", and the per-host note above even calls the index
|
|
734
|
+
// COMPLETE — but the catalog behaves as if EVERY q term must match, and datasets
|
|
735
|
+
// rarely repeat their own jurisdiction's name. Measured 2026-09-21 on
|
|
736
|
+
// data.illinois.gov: q="Illinois state solicitations" -> 0 and
|
|
737
|
+
// q="Illinois procurement" -> 0, while q="solicitations" -> 1 (6rb8-ntpm). An eval
|
|
738
|
+
// agent took that zero at face value and told the user no Illinois procurement
|
|
739
|
+
// datasets exist. So a zero is reported as possibly false, with the fix.
|
|
740
|
+
if (returned === 0) {
|
|
741
|
+
// Only "more than one word?" matters here, so test for it directly rather than
|
|
742
|
+
// splitting: a raw whitespace split is reserved for disclosure tokenizing
|
|
743
|
+
// (tokenizeForDisclosure, ADR-0022) and this is not a disclosure.
|
|
744
|
+
const multiWord = /\S\s+\S/.test(args.q);
|
|
745
|
+
notes.push(
|
|
746
|
+
multiWord
|
|
747
|
+
? `0 matches for a multi-word q — likely a FALSE zero, not proof the data is absent: the catalog behaves as if every term must match, and datasets rarely repeat their jurisdiction's name in their title. Put the place in \`domain\` (not in q) and retry with ONE topical term, e.g. 'solicitations', 'bids', 'contract', 'vendor', 'payments', 'purchase'.`
|
|
748
|
+
: `0 matches for q=${JSON.stringify(args.q)} — this is not proof the data is absent: publishers title the same thing differently. Retry with a synonym ('solicitations', 'bids', 'contract', 'vendor', 'payments', 'purchase') before concluding the portal has none.`,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
669
751
|
|
|
670
752
|
return withMeta(
|
|
671
753
|
{ query: args.q, domain: args.domain ?? null, results },
|