@cliwant/mcp-sam-gov 1.9.0 → 1.10.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/src/socrata.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Socrata / SODA — keyless open data for state / local (SLED) + E-rate portals.
2
+ * Socrata / SODA — keyless open data for state / local (SLED) + federal + E-rate portals.
3
3
  *
4
4
  * First SLED source (ADR-0004); 3rd consumer of the fetch/map/meta shape after
5
5
  * treasury.ts / edgar.ts. Fully PUBLIC, KEYLESS. ONE connector reaches ~a dozen
@@ -7,7 +7,8 @@
7
7
  * 4x4 dataset id. Hosts are a CURATED allowlist (see SSRF below); the caller
8
8
  * never supplies a free host or a free path.
9
9
  * Row query: https://{domain}/resource/{4x4}.json?$select=…&$where=…&$limit=…
10
- * Catalog: https://api.us.socrata.com/api/catalog/v1?domains={domain}&q=…
10
+ * Catalog (host-scoped): https://{domain}/api/catalog/v1?search_context={domain}&q=…
11
+ * Catalog (all-host): https://api.us.socrata.com/api/catalog/v1?domains=…&q=…
11
12
  *
12
13
  * Three layers (mirror treasury.ts / edgar.ts):
13
14
  * fetch — `getSocrataResource` / `getCatalog`: SSRF-guard, build the URL,
@@ -67,14 +68,19 @@
67
68
  * wrong shape (possible upstream API change). A hard schema_drift throw is
68
69
  * reserved for a PRIMARY query (rows / catalog), NEVER the secondary count.
69
70
  *
70
- * ALLOWLIST — LIVE-VERIFIED 2026-07-10 (each carries a real sample 4x4). All
71
- * `.gov` except `opendata.usac.org`:
71
+ * ALLOWLIST — each entry carries a real sample 4x4, live-verified on its tier's
72
+ * date (base state slice 2026-07-10; local + major-city + federal 2026-07-18).
73
+ * All `.gov` except the documented non-.gov exceptions: `opendata.usac.org` and
74
+ * the four major-city portals (.us/.org — see the inline blocks below):
72
75
  * m6 — `opendata.usac.org` is a `.org` (USAC, a Congress-designated non-profit;
73
76
  * E-rate). It is on the periodic re-verification checklist. NOTE: the
74
- * federated discovery catalog (api.us.socrata.com) does NOT index USAC
75
- * (returns resultSetSize 0), so `socrata_discover_datasets` will not
76
- * surface it — but `socrata_query` works against it with a known 4x4
77
- * (live: opendata.usac.org/resource/avi8-svp9.json → 200 bare array).
77
+ * FEDERATED discovery catalog (api.us.socrata.com) does NOT index USAC
78
+ * (resultSetSize 0), so the ALL-HOST `socrata_discover_datasets` (domain
79
+ * omitted) won't surface it; as of loop cycle 8 a DOMAIN-scoped discover
80
+ * uses USAC's own catalog (search_context) and DOES surface it (live:
81
+ * opendata.usac.org/api/catalog/v1?search_context=… → resultSetSize 8).
82
+ * `socrata_query` works regardless with a known 4x4 (live:
83
+ * opendata.usac.org/resource/avi8-svp9.json → 200 bare array).
78
84
  * M1 — MA is DROPPED from slice 1: `cthru.data.socrata.com` is a commercial
79
85
  * vendor host (Tyler Technologies `.socrata.com`, not gov-controlled) and
80
86
  * no `.gov` MA Socrata host verifies (`data.mass.gov` → the catalog
@@ -139,7 +145,55 @@ export const SOCRATA_DOMAINS = [
139
145
  "data.montgomerycountymd.gov", // Montgomery County MD — e.g. vmu2-pnrc (Contracts)
140
146
  "data.mesaaz.gov", // Mesa AZ (city) — e.g. j7s9-qiuq (Vendor Payments)
141
147
  "data.cambridgema.gov", // Cambridge MA (city) — e.g. gp98-ja4f (Contracts bid list)
148
+ // ── Federal open-data portal tier (loop cycle 7, 2026-07-18). The FIRST
149
+ // federal Socrata hosts (prior tiers were state + local only); all .gov,
150
+ // host-scoped catalog + /resource/<4x4>.json 200 bare-array + count(*)
151
+ // companion live-verified. High-value B2G federal datasets (carrier/company
152
+ // census, transportation infrastructure & stats, public health).
153
+ // NOTE (m3/under-index): the FEDERATED aggregator (api.us.socrata.com)
154
+ // under-indexes these hosts (DOT: 3 federated vs 1,873 host-scoped). As of
155
+ // loop cycle 8, `socrata_discover_datasets` WITH a domain uses each host's
156
+ // OWN catalog (search_context) → complete per-host discovery; only the
157
+ // all-host search (domain omitted) still relies on the federated aggregator
158
+ // (its `_meta` note discloses the gap). ──────────────────────────────────
159
+ "data.transportation.gov", // US DOT — e.g. az4n-8mr2 (Company Census File, ~4.47M carriers)
160
+ "data.cdc.gov", // US CDC — e.g. 9bhg-hcku (Provisional COVID-19 Deaths)
161
+ "data.bts.gov", // US BTS (DOT) — e.g. keg4-3bc2 (Border Crossing Entry Data)
162
+ // ── SLED procurement bid-catalog tier (loop cycle 11, 2026-07-19; from the
163
+ // exhaustive US state+local bid-site research). All .gov; host-scoped catalog
164
+ // + /resource/<4x4>.json 200 bare-array live-verified. Distinctive value:
165
+ // these carry LIVE bid-cycle data (bid tabulations / anticipated
166
+ // solicitations), not only award/spend. (★4 other candidate hosts —
167
+ // data.iowa.gov, data.scottsdaleaz.gov, data.gilbertaz.gov, opendata.hawaii.gov
168
+ // — were REJECTED: their /api/catalog/v1 AND /resource endpoints 404 (Next.js/
169
+ // Express apps, not Socrata — a research-agent "Socrata" claim that live
170
+ // verification disproved). Never add a host without a 200 bare-array probe.) ─
171
+ "datacatalog.cookcountyil.gov", // Cook County IL — e.g. 32au-zaqn (Bid Tabulations ~5,607; +awards qh8j-6k63, intent-to-award bgq7-v7ms — 17 procurement datasets)
172
+ "data.illinois.gov", // IL — e.g. 6rb8-ntpm (Future Solicitations = anticipated construction bids); re-included (cycle-1 excluded on federated-discover 0, but host-scoped catalog + /resource verified 2026-07-19)
173
+ "data.cincinnati-oh.gov", // Cincinnati OH (city) — e.g. 2iq3-bugw (Certified Vendors MBE/WBE; +contracts 85xi-xdtw)
174
+ // ── Major-city OFFICIAL portals (loop cycle 5, 2026-07-18). NON-.gov but the
175
+ // unambiguously-official municipal open-data portals for the largest local
176
+ // procurement markets (NYC OpenData / Chicago / DataSF / LA Controller) —
177
+ // host-scoped catalog + /resource 200 bare-array verified. Trust-boundary
178
+ // (like the USAC .org exception): the SSRF guard is the CURATED, FROZEN
179
+ // allowlist, not the TLD; these five are the documented non-.gov entries. ──
180
+ "data.cityofnewyork.us", // NYC OpenData (.us, official) — e.g. dg92-zbpx (City Record: procurement notices)
181
+ "data.cityofchicago.org", // Chicago (.org, official) — e.g. rsxa-ify5 (Contracts)
182
+ "data.sfgov.org", // San Francisco / DataSF (.org, official) — e.g. cqi5-hm2d (Supplier Contracts)
183
+ "controllerdata.lacity.org", // LA City Controller (.org, official) — e.g. pggv-e4fn (Checkbook L.A.)
142
184
  "opendata.usac.org", // USAC E-rate (.org, m6) — e.g. avi8-svp9
185
+ // ── County/city procurement sweep (loop cycle 17, 2026-07-20). Discovered via
186
+ // the Socrata federated catalog (api.us.socrata.com) filtered to procurement/
187
+ // bid/contract datasets, then each host-scoped $select=count(*) + /resource
188
+ // 200 bare-array live-verified. US local govs only (Canada/AU + demo/test +
189
+ // off-theme aggregate hosts filtered out). Mix of official .gov/.org/.com
190
+ // municipal portals (same documented non-.gov trust-boundary as above). ──
191
+ "data.kcmo.org", // Kansas City MO (.org, official) — e.g. 4mdg-usvj (Vendor Payments ~144k; 22 procurement datasets)
192
+ "data.brla.gov", // Baton Rouge / East Baton Rouge Parish LA (.gov) — e.g. e5pk-us93 (Upcoming Procurement Opportunities; 19 procurement datasets)
193
+ "www.dallasopendata.com", // Dallas TX (.com, official) — e.g. x5ih-idh7 (Vendor Payments FY2019–present ~166k)
194
+ "data.lacity.org", // Los Angeles CA (.org, official) — e.g. hf3r-utnq (RAMP Open Bid Opportunities — live bids)
195
+ "data.ramseycountymn.gov", // Ramsey County MN (.gov) — e.g. iu7r-dzmj (Solicitations & Addenda, with due_date/download_url ~516)
196
+ "data.richmondgov.com", // Richmond VA (.com, official) — e.g. xqn7-jvv2 (City Contracts: contract_value/supplier/procurement_type ~1,387)
143
197
  ] as const;
144
198
 
145
199
  export type SocrataDomain = (typeof SOCRATA_DOMAINS)[number];
@@ -262,6 +316,44 @@ async function getCatalog(params: URLSearchParams): Promise<unknown> {
262
316
  });
263
317
  }
264
318
 
319
+ /**
320
+ * GET a HOST-SCOPED discovery catalog (`https://{domain}/api/catalog/v1?
321
+ * search_context={domain}&…`). Unlike the FEDERATED `getCatalog`
322
+ * (api.us.socrata.com), each portal's OWN catalog COMPLETELY indexes its own
323
+ * datasets — the federated aggregator under-indexes many hosts (loop cycle 8:
324
+ * USAC → 0, DOT → 3 of 1,873). Used whenever a specific `domain` is requested;
325
+ * the all-host search (domain omitted) still needs the federated aggregator.
326
+ * SSRF: identical guard to getSocrataResource — domain ∈ allowlist + the
327
+ * CONSTRUCTED URL's hostname === domain (https); redirect:"error" (B1); the
328
+ * host-only label carries the domain (never the token, m7).
329
+ */
330
+ async function getHostCatalog(
331
+ domain: string,
332
+ params: URLSearchParams,
333
+ ): Promise<unknown> {
334
+ if (!SOCRATA_DOMAIN_SET.has(domain)) {
335
+ throw new ToolErrorCarrier({
336
+ kind: "invalid_input",
337
+ message: `Socrata domain ${JSON.stringify(domain)} is not on the curated allowlist. Allowed: ${SOCRATA_DOMAINS.join(", ")}.`,
338
+ retryable: false,
339
+ });
340
+ }
341
+ const url = `https://${domain}/api/catalog/v1?${params.toString()}`;
342
+ const built = new URL(url);
343
+ if (built.hostname !== domain || built.protocol !== "https:") {
344
+ throw new ToolErrorCarrier({
345
+ kind: "invalid_input",
346
+ message: `Constructed Socrata host-catalog URL host ${JSON.stringify(built.hostname)} (${built.protocol}) does not match the allowlisted domain ${JSON.stringify(domain)} over https — refusing to fetch (SSRF safety).`,
347
+ retryable: false,
348
+ });
349
+ }
350
+ return getJson(url, {
351
+ label: "socrata:catalog:" + domain,
352
+ headers: appTokenHeader(),
353
+ redirect: "error",
354
+ });
355
+ }
356
+
265
357
  // ─── map + meta helpers ───────────────────────────────────────────
266
358
  const STRING_COERCION_NOTE =
267
359
  "Row value fields arrive as strings verbatim from SODA (e.g. \"13650.00\", \"1\") — parse client-side. A missing value is absent, never 0.";
@@ -464,16 +556,21 @@ function mapCatalogRow(row: unknown): CatalogDataset {
464
556
  }
465
557
 
466
558
  /**
467
- * Discover dataset 4x4 ids via the Socrata catalog (memoized ~10 min). Omitting
468
- * `domain` searches the WHOLE allowlist (repeated `domains=`); passing one scopes
469
- * to it. Returns `[{ id, name, description, domain, updatedAt, link }]` +
470
- * `totalAvailable = resultSetSize`. Feeds `datasetId` to socrata_query.
559
+ * Discover dataset 4x4 ids via the Socrata catalog (memoized ~10 min). Passing a
560
+ * `domain` queries that portal's OWN catalog (search_context) — a COMPLETE
561
+ * per-host index; omitting it searches the WHOLE allowlist via the federated
562
+ * api.us.socrata.com aggregator (repeated `domains=`). Returns `[{ id, name,
563
+ * description, domain, updatedAt, link }]` + `totalAvailable = resultSetSize`.
564
+ * Feeds `datasetId` to socrata_query.
471
565
  *
472
566
  * m3 — this is the catalog's PRIMARY response: a non-number `resultSetSize` is a
473
567
  * hard schema_drift throw (nothing valid to return), never a fabricated total.
474
- * (Note: the catalog does not index every allowlisted host — e.g. USAC — so a
475
- * host may return 0 here yet still be queryable via socrata_query with a known
476
- * 4x4.)
568
+ *
569
+ * UNDER-INDEX (loop cycle 8 fix): the FEDERATED aggregator under-indexes many
570
+ * hosts (USAC → 0; DOT → 3 of 1,873), so the all-host search (domain omitted) can
571
+ * miss datasets — its `_meta` note discloses this. A DOMAIN-scoped search now
572
+ * uses that host's OWN catalog, which indexes it completely (USAC/DOT/etc. fully
573
+ * discoverable). socrata_query works regardless with a known 4x4.
477
574
  */
478
575
  export async function discoverDatasets(args: {
479
576
  q: string;
@@ -485,8 +582,11 @@ export async function discoverDatasets(args: {
485
582
  params.set("q", args.q);
486
583
  params.set("only", "datasets");
487
584
  params.set("limit", String(limit));
585
+ // A specific domain → that portal's OWN catalog (search_context), a COMPLETE
586
+ // per-host index. Omitting domain → the federated aggregator (repeated
587
+ // domains=), the only way to search across all hosts (loop cycle 8).
488
588
  if (args.domain) {
489
- params.append("domains", args.domain);
589
+ params.set("search_context", args.domain);
490
590
  } else {
491
591
  for (const d of SOCRATA_DOMAINS) params.append("domains", d);
492
592
  }
@@ -495,7 +595,9 @@ export async function discoverDatasets(args: {
495
595
  const { totalAvailable, results } = await memoize(
496
596
  key,
497
597
  async () => {
498
- const body = await getCatalog(params);
598
+ const body = args.domain
599
+ ? await getHostCatalog(args.domain, params)
600
+ : await getCatalog(params);
499
601
  const b = (body ?? {}) as { results?: unknown; resultSetSize?: unknown };
500
602
  // m3 — hard drift on the PRIMARY response (contrast the best-effort count).
501
603
  // The check stays INSIDE the memoize callback so a bad shape is never
@@ -517,6 +619,9 @@ export async function discoverDatasets(args: {
517
619
  const scope = args.domain ? `domain ${args.domain}` : `the ${SOCRATA_DOMAINS.length}-host allowlist`;
518
620
  const notes: string[] = [
519
621
  `Catalog search over ${scope} (only=datasets). Feed a result's id to socrata_query as datasetId.`,
622
+ args.domain
623
+ ? `Source: the ${args.domain} portal's OWN catalog (search_context) — a COMPLETE per-host index.`
624
+ : `Source: the federated ${CATALOG_HOST} aggregator, which UNDER-INDEXES some hosts (e.g. USAC, DOT) — pass a specific domain to use that host's own complete catalog.`,
520
625
  `App token: ${appTokenPresent() ? "present (X-App-Token sent; value never logged)" : "absent (keyless)"}.`,
521
626
  ];
522
627
  if (totalAvailable !== null && returned < totalAvailable) {
@@ -528,7 +633,7 @@ export async function discoverDatasets(args: {
528
633
  return withMeta(
529
634
  { query: args.q, domain: args.domain ?? null, results },
530
635
  {
531
- source: `${CATALOG_HOST} catalog ${SOURCE_SUFFIX}`,
636
+ source: `${args.domain ?? CATALOG_HOST} catalog ${SOURCE_SUFFIX}`,
532
637
  keylessMode: true,
533
638
  returned,
534
639
  totalAvailable,