@cliwant/mcp-sam-gov 1.8.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/server.ts CHANGED
@@ -48,6 +48,10 @@ import * as ckan from "./ckan.js";
48
48
  import * as echo from "./echo.js";
49
49
  import * as datagov from "./datagov.js";
50
50
  import * as datagovCatalog from "./datagov-catalog.js";
51
+ import * as arcgisHub from "./arcgis-hub.js";
52
+ import * as opengov from "./opengov.js";
53
+ import * as bonfire from "./bonfire.js";
54
+ import * as arcgisFeature from "./arcgis-feature.js";
51
55
  import * as govinfo from "./govinfo.js";
52
56
  import * as fpds from "./fpds.js";
53
57
  import * as nih from "./nih.js";
@@ -102,7 +106,7 @@ import { realpathSync } from "node:fs";
102
106
  const SERVER_NAME = "mcp-sam-gov";
103
107
  // Kept in lockstep with package.json / manifest.json / server.json.
104
108
  // Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
105
- const SERVER_VERSION = "1.8.0";
109
+ const SERVER_VERSION = "1.10.0";
106
110
 
107
111
  // ─── Tool input schemas (Zod) ────────────────────────────────────
108
112
 
@@ -572,6 +576,22 @@ const EcfrSearchInput = z.object({
572
576
 
573
577
  const EcfrListTitlesInput = z.object({});
574
578
 
579
+ const EcfrGetSectionInput = z.object({
580
+ titleNumber: z
581
+ .number()
582
+ .int()
583
+ .min(1)
584
+ .max(50)
585
+ .describe("CFR title (1–50). e.g. 2 = federal financial assistance (grants), 29 = Labor, 26 = IRS. For FAR/DFARS (title 48) prefer far_clause_lookup."),
586
+ section: z
587
+ .string()
588
+ .describe("The section citation, e.g. '200.1' (2 CFR 200.1) or '52.204-21' — the part number is the pre-dot integer. Charclass-validated (^[0-9]{1,3}\\.[0-9]{1,4}(-[0-9]{1,4})?$)."),
589
+ date: z
590
+ .string()
591
+ .optional()
592
+ .describe("Optional eCFR issue date YYYY-MM-DD; omit to use the title's LATEST issue (disclosed in _meta)."),
593
+ });
594
+
575
595
  // FAR / DFARS clause lookup (eCFR versioner full endpoint)
576
596
  const FarClauseLookupInput = z.object({
577
597
  clauseNumber: z
@@ -2959,6 +2979,113 @@ const DatagovSearchDatasetsInput = z.object({
2959
2979
  .describe("Opaque continuation cursor (→ after) — pass back the _meta.nextCursor from the previous page. Pagination is a cursor, NOT a numeric offset (offset/nextOffset are null); nextCursor:null means the last page. A bad token (spaces/'../'/'%') ⇒ invalid_input pre-fetch."),
2960
2980
  });
2961
2981
 
2982
+ // ─── ArcGIS Hub (hub.arcgis.com — keyless SLED dataset discovery) ─
2983
+ // Loop cycle 9. A GLOBAL, OPEN publishing platform (non-US / non-gov publishers
2984
+ // included) — DISCOVERY ONLY, provenance surfaced per-row + disclosed every
2985
+ // response. Fixed-host SSRF (no per-jurisdiction host vector). q ≥ 2 chars.
2986
+ const ArcgisHubDiscoverInput = z.object({
2987
+ query: z
2988
+ .string()
2989
+ .min(2)
2990
+ .max(500)
2991
+ .describe("Keyword search over ArcGIS Hub datasets (→ q), e.g. 'procurement contract', 'zoning permits'. REQUIRED, ≥2 non-whitespace chars (a broad scan of the whole global Hub is refused)."),
2992
+ openDataOnly: z
2993
+ .boolean()
2994
+ .default(true)
2995
+ .describe("When true (default), filter to items the publisher designated as open data (→ filter[openData]=true) — the B2G-relevant subset. Set false to broaden to ALL shared items (vet the publisher even more)."),
2996
+ limit: z
2997
+ .number()
2998
+ .int()
2999
+ .min(1)
3000
+ .max(100)
3001
+ .default(20)
3002
+ .describe("Datasets per page (→ page[size]), 1..100, default 20."),
3003
+ offset: z
3004
+ .number()
3005
+ .int()
3006
+ .min(0)
3007
+ .default(0)
3008
+ .describe("0-based record offset (→ page[start]=offset+1). Page with _meta.pagination.nextOffset; totalAvailable is the exact Hub match count."),
3009
+ });
3010
+
3011
+ // ─── OpenGov Procurement (api.procurement.opengov.com — keyless SLED bids) ─
3012
+ // SLED bid campaign. 525+ US state/local portals' live solicitations via the
3013
+ // public portal's own anonymous backend API (the key-gated api-key API is NOT
3014
+ // used). Fixed-host SSRF. governmentCode = the portal slug from list_governments.
3015
+ const OpengovListGovernmentsInput = z.object({
3016
+ state: z
3017
+ .string()
3018
+ .length(2)
3019
+ .optional()
3020
+ .describe("2-letter US state filter (client-side), e.g. 'CA', 'FL'. Optional."),
3021
+ query: z
3022
+ .string()
3023
+ .min(1)
3024
+ .max(120)
3025
+ .optional()
3026
+ .describe("Case-insensitive name substring filter (client-side), e.g. 'county', 'school'. Optional."),
3027
+ limit: z.number().int().min(1).max(200).default(50).describe("Portals per page, 1..200, default 50."),
3028
+ offset: z.number().int().min(0).default(0).describe("0-based offset; page with _meta.pagination.nextOffset. totalAvailable = exact filtered portal count."),
3029
+ });
3030
+
3031
+ const OpengovSearchSolicitationsInput = z.object({
3032
+ governmentCode: z
3033
+ .string()
3034
+ .min(1)
3035
+ .max(64)
3036
+ .regex(opengov.OPENGOV_CODE_RE)
3037
+ .describe("The OpenGov portal slug (from opengov_list_governments `code`), e.g. 'santacruzca', 'orlando', 'u-46'. REQUIRED. Lowercase alnum/hyphen; a bad slug ⇒ invalid_input pre-fetch."),
3038
+ limit: z.number().int().min(1).max(100).default(50).describe("Solicitations per page, 1..100, default 50 (→ API page size)."),
3039
+ offset: z.number().int().min(0).default(0).describe("0-based offset (snapped to the API's fixed page boundary). Page with _meta.pagination.nextOffset."),
3040
+ });
3041
+
3042
+ // ─── Bonfire (Euna) — keyless per-org open-opportunity RSS (SLED bids) ─
3043
+ // SLED bid campaign. Thousands of US state/local govs on Bonfire expose a keyless
3044
+ // RSS of open opportunities at {org}.bonfirehub.com/opportunities/rss. Ships a
3045
+ // curated 187-org seed directory. Fixed-suffix SSRF. org = charclass slug.
3046
+ const BonfireListOrganizationsInput = z.object({
3047
+ state: z.string().length(2).optional().describe("2-letter US state filter (client-side), e.g. 'TX', 'CA'. Optional."),
3048
+ query: z.string().min(1).max(120).optional().describe("Case-insensitive name substring filter (client-side), e.g. 'county', 'ISD'. Optional."),
3049
+ limit: z.number().int().min(1).max(200).default(50).describe("Orgs per page, 1..200, default 50."),
3050
+ offset: z.number().int().min(0).default(0).describe("0-based offset; page with _meta.pagination.nextOffset."),
3051
+ });
3052
+
3053
+ const BonfireSearchOpportunitiesInput = z.object({
3054
+ org: z
3055
+ .string()
3056
+ .min(1)
3057
+ .max(64)
3058
+ .regex(bonfire.BONFIRE_ORG_RE)
3059
+ .describe("The Bonfire org subdomain slug (from bonfire_list_organizations `org`), e.g. 'harriscountytx', 'broward', 'u-46'. REQUIRED. Lowercase alnum/hyphen; a bad slug ⇒ invalid_input pre-fetch."),
3060
+ limit: z.number().int().min(1).max(200).default(50).describe("Opportunities per page, 1..200, default 50. The RSS is the complete open set; this pages over it."),
3061
+ offset: z.number().int().min(0).default(0).describe("0-based offset; page with _meta.pagination.nextOffset. totalAvailable = the exact open-opportunity count."),
3062
+ });
3063
+
3064
+ // ─── ArcGIS REST feature query (curated service allowlist — SLED bids/GIS) ─
3065
+ // Generic ArcGIS FeatureServer/MapServer layer query over a curated allowlist.
3066
+ // First payload: DC OCP PASS procurement layers (live solicitations/contracts/PO/
3067
+ // payments). `service` is an enum (SSRF core); where/outFields filter the layer.
3068
+ const ArcgisFeatureQueryInput = z.object({
3069
+ service: z
3070
+ .enum(arcgisFeature.ARCGIS_SERVICES.map((s) => s.key) as [string, ...string[]])
3071
+ .describe("The curated ArcGIS layer (SSRF allowlist enum). DC OCP PASS: 'dc_pass_solicitations' (live solicitations ~25k), 'dc_pass_contracts', 'dc_pass_purchase_orders', 'dc_pass_payments'. Other US local govs: 'asheville_purchase_orders'/'asheville_po_summary' (Asheville NC), 'bellevue_vendor_payments'/'bellevue_awarded_contracts' (Bellevue WA), 'miamidade_purchase_orders_2025'/'miamidade_purchase_orders_2017' (Miami-Dade FL, current/2017), 'suffolk_county_ny_contracts_2018' (Suffolk County NY), 'matsu_borough_ak_checkbook' (Matanuska-Susitna Borough AK), 'lasvegas_checkbook' (Las Vegas NV ~373k), 'baltimore_checkbook' (Baltimore City MD ~367k), 'naperville_vendor_payments' (Naperville IL ~127k), 'worcester_ma_checkbook_fy25' (Worcester MA FY25), 'lasvegas_purchasing_contracts' (Las Vegas NV contract register), 'txdot_construction_projects' (Texas DOT, awarded construction company ~85k), 'akdot_construction_awards'/'akdot_aashtoware_proposals' (Alaska DOT&PF bid awards/proposals), 'iowadot_public_bid_awards' (Iowa DOT public bid), 'okdot_cirb_contract_status' (Oklahoma DOT CIRB contract status), 'topeka_checkbook_aggregate' (Topeka KS checkbook FY2015–2023 ~332k). 23 curated services (state DOT bid/award registers: TX/AK/IA/OK + municipal checkbooks/contracts)."),
3072
+ where: z
3073
+ .string()
3074
+ .min(1)
3075
+ .max(500)
3076
+ .optional()
3077
+ .describe("ArcGIS SQL-ish filter (default '1=1'), e.g. \"SOLICITATIONTITLE LIKE '%security%'\" or \"DUE_DATE > 1750000000000\". Filters the read-only layer; a malformed clause ⇒ invalid_input (surfaced)."),
3078
+ outFields: z
3079
+ .string()
3080
+ .min(1)
3081
+ .max(500)
3082
+ .optional()
3083
+ .describe("Comma-separated fields to return (default '*' = all). e.g. 'SOLICITATIONNUMBER,SOLICITATIONTITLE,DUE_DATE,NIGPCODE'."),
3084
+ orderByFields: z.string().min(1).max(200).optional().describe("ArcGIS orderByFields, e.g. 'DUE_DATE DESC'. Optional."),
3085
+ limit: z.number().int().min(1).max(1000).default(50).describe("Records per page (→ resultRecordCount), 1..1000, default 50."),
3086
+ offset: z.number().int().min(0).default(0).describe("0-based offset (→ resultOffset). Page with _meta.pagination.nextOffset; totalAvailable = the layer's exact match count."),
3087
+ });
3088
+
2962
3089
  // ─── GovInfo (api.govinfo.gov — the api.data.gov keyed trio's 3rd API) ─
2963
3090
  // ADR-0010. Same DATA_GOV_API_KEY/DEMO_KEY/X-Api-Key discipline as the datagov
2964
3091
  // trio (shared datagovKey.ts seam). `collection` is grammar-checked here AND
@@ -5381,14 +5508,21 @@ export const TOOLS: ToolDef[] = [
5381
5508
  handler: (input) => fedreg.publicInspection(input),
5382
5509
  }),
5383
5510
 
5384
- // ━━━ eCFR (5) ━━━
5511
+ // ━━━ eCFR (6) ━━━
5385
5512
  defineTool({
5386
5513
  name: "ecfr_search",
5387
5514
  description:
5388
- "Full-text search across the entire CFR (Code of Federal Regulations). Use for compliance questions — pass titleNumber=48 for FAR (Federal Acquisition Regulation), titleNumber=2 for federal financial assistance, etc. Returns excerpt + section path + ecfrUrl.",
5515
+ "Full-text search across the entire CFR (Code of Federal Regulations). Use for DISCOVERY — pass titleNumber=48 for FAR (Federal Acquisition Regulation), titleNumber=2 for federal financial assistance, etc. Returns a ranked EXCERPT (snippet, not full text) + section path + ecfrUrl per hit. To then read the COMPLETE text of a hit: for a FAR/DFARS clause (title 48) use far_clause_lookup (adds prescription + revision); for any other title's section use ecfr_get_section; or open the ecfrUrl.",
5389
5516
  inputSchema: EcfrSearchInput,
5390
5517
  handler: (input) => ecfr.search(input),
5391
5518
  }),
5519
+ defineTool({
5520
+ name: "ecfr_get_section",
5521
+ description:
5522
+ "Get the FULL in-force text of ONE CFR section by citation (the companion to ecfr_search, which returns only snippets). Input titleNumber (1–50) + section (e.g. '200.1' → 2 CFR 200.1, uniform grants guidance; '1601.1' → 29 CFR labor) + optional issue date (default = the title's latest). Returns { citation, alternateReference, heading, fullText, issueDate, ecfrUrl }. ★For a FAR/DFARS clause (title 48) prefer far_clause_lookup — it adds the prescription, revision, and FAR-overhaul-risk this generic tool does not; use ecfr_get_section for the OTHER 49 titles (grants/labor/IRS/SBA/…). HONESTY: text is the eCFR's own, de-XMLed (no fabrication); a nonexistent section ⇒ not_found (never a fake/wrong section); the resolved issue date is disclosed (the title's latest is a moving target); a bad section format ⇒ invalid_input (SSRF charclass); an outage ⇒ throws. Keyless.",
5523
+ inputSchema: EcfrGetSectionInput,
5524
+ handler: (input) => ecfr.getSection(input),
5525
+ }),
5392
5526
  defineTool({
5393
5527
  name: "ecfr_list_titles",
5394
5528
  description:
@@ -6007,6 +6141,72 @@ export const TOOLS: ToolDef[] = [
6007
6141
  inputSchema: DatagovSearchDatasetsInput,
6008
6142
  handler: (input) => datagovCatalog.searchDatasets(input),
6009
6143
  }),
6144
+ // ━━━ ArcGIS Hub (hub.arcgis.com) — keyless SLED dataset DISCOVERY (loop cycle 9) ━━━
6145
+ // A NEW discovery platform: much US state/local/regional/tribal (SLED) open data
6146
+ // — GIS/infrastructure/permits/boundaries/procurement — lives on ArcGIS Hub, NOT
6147
+ // Socrata/CKAN. Fixed-host SSRF (no per-jurisdiction host vector). ★PROVENANCE: the
6148
+ // Hub is GLOBAL + OPEN (non-US/non-gov publishers), so this is a DISCOVERY aid, not
6149
+ // a curated official-source allowlist — per-row owner/orgName/source/region are
6150
+ // surfaced for vetting + the disclosure rides every response. P1: totalAvailable =
6151
+ // meta.total (exact Hub count). DISCOVERY ONLY (row-query on arbitrary ArcGIS hosts
6152
+ // is a planned, separately-guarded addition).
6153
+ defineTool({
6154
+ name: "arcgis_hub_discover_datasets",
6155
+ description:
6156
+ "Discover ArcGIS Hub datasets by keyword — the SLED/GIS open-data layer that Socrata and CKAN do NOT cover (keyless; hub.arcgis.com/api/v3/datasets). Much US state/local/regional/tribal open data (GIS, infrastructure, permits, zoning, boundaries, procurement) is published on ArcGIS Hub. Input `query` (→q, REQUIRED, ≥2 non-whitespace chars — a broad whole-Hub scan is refused), `openDataOnly` (default TRUE → filter[openData]=true, the B2G-relevant designated-open-data subset; false broadens to all shared items), `limit` (1..100, def 20 → page[size]), `offset` (0-based → page[start]=offset+1). Returns { query, openDataOnly, datasets:[{ id, name, description, owner, orgName, source, region, type, sector, keywords, downloadable, hasApi, created, modified, landingPage, itemId }] } + honest _meta. ★PROVENANCE (the crux — a DIFFERENT trust posture from our other sources): ArcGIS Hub is a GLOBAL, OPEN publishing platform — results include NON-US and NON-GOVERNMENTAL publishers. This is a DISCOVERY aid, NOT a curated official-source allowlist (unlike socrata_query): the per-row owner/orgName/source/region are surfaced VERBATIM so you can VET the publisher before relying on the data, and the global-platform caveat rides EVERY response. DISCOVERY ONLY — metadata + links; to read rows follow the dataset on its own ArcGIS endpoint (a guarded row-query tool is a planned addition). HONESTY: totalAvailable = the EXACT Hub match count (meta.total, NEVER data.length — P1); pagination is a 0-based offset (nextOffset when more remain); every scalar null-never-empty, booleans null-preserving, counts null-never-0; a genuine no-match ⇒ complete:true/returned:0; a 429 ⇒ rate_limited / 5xx/timeout ⇒ upstream_unavailable THROWS (never a fake empty); a 200 non-JSON / non-array data ⇒ schema_drift.",
6157
+ inputSchema: ArcgisHubDiscoverInput,
6158
+ handler: (input) => arcgisHub.discoverDatasets(input),
6159
+ }),
6160
+ // ━━━ OpenGov Procurement (api.procurement.opengov.com) — keyless SLED bids ━━━
6161
+ // SLED bid campaign (from the exhaustive US state+local bid-site research). 525+
6162
+ // state/local government portals' LIVE solicitations, via the public portal's OWN
6163
+ // anonymous backend REST API — the key-gated official api-key API is NOT touched
6164
+ // (genuinely keyless, live-verified). Fixed-host SSRF. Two tools: a directory
6165
+ // (one keyless GET returns all ~560 orgs) + per-org solicitations.
6166
+ defineTool({
6167
+ name: "opengov_list_governments",
6168
+ description:
6169
+ "List the government portals on OpenGov Procurement (formerly ProcureNow) — the directory for opengov_search_solicitations (keyless; api.procurement.opengov.com). OpenGov Procurement hosts the live open-solicitation portals of 525+ US state/local governments (cities, counties, school & special districts across 42 states + DC). The WHOLE directory arrives in ONE keyless GET and is filtered client-side: `state` (2-letter), `query` (case-insensitive name substring); `limit`(1..200)/`offset`. Only ACTIVE, non-internal portals are returned. Output: { governments:[{ code, name, city, state, website }] } + honest _meta. Feed a result's `code` to opengov_search_solicitations. HONESTY: this consumes ONLY the anonymous endpoints the public portal itself calls (the official key-gated api-key API is NOT used) — genuinely keyless; totalAvailable is the EXACT filtered portal count (never the page length); a 429/5xx/timeout THROWS (never a fake empty); a non-array body ⇒ schema_drift.",
6170
+ inputSchema: OpengovListGovernmentsInput,
6171
+ handler: (input) => opengov.listGovernments(input),
6172
+ }),
6173
+ defineTool({
6174
+ name: "opengov_search_solicitations",
6175
+ description:
6176
+ "List a government's public solicitations on OpenGov Procurement (keyless; api.procurement.opengov.com, POST /project/list with the required publicView gate). Input `governmentCode` (the portal slug from opengov_list_governments, e.g. 'santacruzca', 'orlando', 'u-46'; REQUIRED), `limit`(1..100)/`offset`. Returns { governmentCode, solicitations:[{ id, title, solicitationNumber, status, type, department, releaseDate, proposalDeadline, contactName, link }] } + honest _meta. ★STATUS: `status` is surfaced VERBATIM — **open = currently ACCEPTING responses**; pending/evaluation/closed are ALSO returned (publicView shows all public projects), so filter status==='open' for live bids. `link` is the public portal page. HONESTY: totalAvailable = the API's `count` = the org's TOTAL public-project count (all statuses), NEVER the page length and NOT an open-only count (a note discloses this); pagination is the API's fixed page (offset is snapped to the page boundary, disclosed); a genuine no-match ⇒ complete:true/returned:0; a 429/5xx/timeout THROWS (never a fake empty); a non-array `projects` ⇒ schema_drift; a bad `governmentCode` ⇒ invalid_input pre-fetch. Genuinely keyless (the key-gated official API is NOT used).",
6177
+ inputSchema: OpengovSearchSolicitationsInput,
6178
+ handler: (input) => opengov.searchSolicitations(input),
6179
+ }),
6180
+ // ━━━ Bonfire (Euna) — keyless per-org open-opportunity RSS (SLED bids) ━━━
6181
+ // SLED bid campaign. Thousands of US state/local govs on Bonfire expose a keyless
6182
+ // RSS of open opportunities. Ships a curated 187-org live-verified seed directory
6183
+ // (Bonfire's authoritative org API is auth-gated → out of bounds). Fixed-suffix
6184
+ // SSRF (.bonfirehub.com). RSS = the complete open set (totalAvailable honest).
6185
+ defineTool({
6186
+ name: "bonfire_list_organizations",
6187
+ description:
6188
+ "List US governments on the Bonfire (Euna) eProcurement platform — the directory for bonfire_search_opportunities (keyless). Bonfire hosts thousands of US state/local governments' open-bid portals, each with a keyless RSS feed. Filter the curated seed by `state` (2-letter) / `query` (case-insensitive name substring); `limit`(1..200)/`offset`. Output: { organizations:[{ org, name, state }] }. Feed a result's `org` to bonfire_search_opportunities. ★HONESTY: this is a CURATED, live-verified SEED of 187 US orgs — Bonfire has NO keyless org-list API (its authoritative directory is auth-gated, out of bounds), and Euna markets up to ~900 US orgs, so the seed is PARTIAL (disclosed in _meta); probe `{slug}.bonfirehub.com/opportunities/rss` to extend. totalAvailable = the exact filtered seed count.",
6189
+ inputSchema: BonfireListOrganizationsInput,
6190
+ handler: (input) => bonfire.listOrganizations(input),
6191
+ }),
6192
+ defineTool({
6193
+ name: "bonfire_search_opportunities",
6194
+ description:
6195
+ "List a government's currently-OPEN solicitations on Bonfire (keyless; {org}.bonfirehub.com/opportunities/rss, RSS 2.0). Input `org` (the subdomain slug from bonfire_list_organizations, e.g. 'harriscountytx', 'broward', 'u-46'; REQUIRED), `limit`(1..200)/`offset`. Returns { org, opportunities:[{ referenceNumber, name, description, closeDate, link, pubDate }] } + honest _meta. HONESTY: the RSS is the COMPLETE set of the org's currently-open opportunities (no server pagination), so totalAvailable = the exact open-opportunity count (never a page length) and this tool pages over it client-side; an empty feed (returned 0) means no open opportunities right now (honest empty, complete:true); `closeDate` is parsed best-effort from the description; a 429/5xx/404/timeout THROWS (never a fake empty); a 200 non-RSS body ⇒ schema_drift; a bad `org` ⇒ invalid_input pre-fetch. Fixed-suffix SSRF (.bonfirehub.com) + redirect:error. Keyless (Bonfire's auth-gated directory API is NOT used).",
6196
+ inputSchema: BonfireSearchOpportunitiesInput,
6197
+ handler: (input) => bonfire.searchOpportunities(input),
6198
+ }),
6199
+ // ━━━ ArcGIS REST feature query (curated allowlist) — SLED bids/GIS ━━━
6200
+ // Generic query companion to arcgis_hub_discover_datasets. First payload: the DC
6201
+ // OCP PASS procurement layers (live solicitations + contracts/PO/payments), a
6202
+ // keyless live-solicitation feed via ArcGIS REST. `service` enum = SSRF core.
6203
+ defineTool({
6204
+ name: "arcgis_feature_query",
6205
+ description:
6206
+ "Query rows from a curated US-government ArcGIS REST feature layer (keyless) — the QUERY companion to arcgis_hub_discover_datasets (which discovers Hub datasets). A large amount of SLED procurement/GIS data lives on ArcGIS. First payload: the **DC Office of Contracting & Procurement 'PASS'** layers — `dc_pass_solicitations` (DC's LIVE open solicitations, ~25k: SOLICITATIONNUMBER, SOLICITATIONTITLE, DUE_DATE, OPENDATE, CLOSEDATE, NIGPCODE, CONTRACTINGOFFICER, AWARD_TO, 46 fields), `dc_pass_contracts` (~50k), `dc_pass_purchase_orders` (~275k), `dc_pass_payments` (~1.55M). Inputs: `service` (the allowlist ENUM — the SSRF core, never a free host), `where` (ArcGIS SQL-ish filter, default '1=1', e.g. \"SOLICITATIONTITLE LIKE '%security%'\"), `outFields` (default '*'), `orderByFields`, `limit`(1..1000)/`offset`. Returns { service, records:[{…attributes verbatim…}] } + honest _meta. HONESTY: totalAvailable = the layer's EXACT match count (a returnCountOnly companion query, never the page length; a count failure ⇒ null + note, rows still returned); ★ArcGIS date fields are epoch MILLISECONDS and a negative/sentinel (≈1900) is a placeholder — surfaced verbatim, never coerced; a genuine no-match ⇒ complete:true/returned:0; a 429/5xx/timeout THROWS; an ArcGIS {error} body (e.g. a bad where) ⇒ invalid_input/upstream (surfaced, never a fake empty); a non-array features ⇒ schema_drift. SSRF: fixed allowlist base + hostname assertion + redirect:error (where/outFields cannot alter the host).",
6207
+ inputSchema: ArcgisFeatureQueryInput,
6208
+ handler: (input) => arcgisFeature.featureQuery(input),
6209
+ }),
6010
6210
  // ━━━ GovInfo (api.govinfo.gov) — the api.data.gov keyed trio's 3rd API (3) ━━━ ADR-0010
6011
6211
  // GPO-authoritative bulk publications (BILLS/PLAW/USCODE/CREC/CFR-FR editions/
6012
6212
  // BUDGET/GAOREPORTS) with PDF/XML/MODS downloads + provenance. 2nd consumer of the
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
@@ -130,7 +136,64 @@ export const SOCRATA_DOMAINS = [
130
136
  "data.pa.gov", // PA — e.g. mcba-yywm
131
137
  "data.mo.gov", // MO — e.g. gfq7-aa86
132
138
  "data.delaware.gov", // DE — e.g. 5zy2-grhr
139
+ // ── SLED city/county tier (loop cycle 1, 2026-07-18; host-scoped catalog +
140
+ // /resource/<4x4>.json 200 bare-array live-verified; all .gov; B2G
141
+ // procurement/vendor/contract data — the fragmented local layer above the
142
+ // state portals). ─────────────────────────────────────────────────────
143
+ "data.austintexas.gov", // Austin TX (city) — e.g. 3ebq-e9iz (Purchase Orders)
144
+ "data.kingcounty.gov", // King County WA — e.g. dqit-zt74 (Procurement Contracts)
145
+ "data.montgomerycountymd.gov", // Montgomery County MD — e.g. vmu2-pnrc (Contracts)
146
+ "data.mesaaz.gov", // Mesa AZ (city) — e.g. j7s9-qiuq (Vendor Payments)
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.)
133
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)
134
197
  ] as const;
135
198
 
136
199
  export type SocrataDomain = (typeof SOCRATA_DOMAINS)[number];
@@ -253,6 +316,44 @@ async function getCatalog(params: URLSearchParams): Promise<unknown> {
253
316
  });
254
317
  }
255
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
+
256
357
  // ─── map + meta helpers ───────────────────────────────────────────
257
358
  const STRING_COERCION_NOTE =
258
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.";
@@ -455,16 +556,21 @@ function mapCatalogRow(row: unknown): CatalogDataset {
455
556
  }
456
557
 
457
558
  /**
458
- * Discover dataset 4x4 ids via the Socrata catalog (memoized ~10 min). Omitting
459
- * `domain` searches the WHOLE allowlist (repeated `domains=`); passing one scopes
460
- * to it. Returns `[{ id, name, description, domain, updatedAt, link }]` +
461
- * `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.
462
565
  *
463
566
  * m3 — this is the catalog's PRIMARY response: a non-number `resultSetSize` is a
464
567
  * hard schema_drift throw (nothing valid to return), never a fabricated total.
465
- * (Note: the catalog does not index every allowlisted host — e.g. USAC — so a
466
- * host may return 0 here yet still be queryable via socrata_query with a known
467
- * 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.
468
574
  */
469
575
  export async function discoverDatasets(args: {
470
576
  q: string;
@@ -476,8 +582,11 @@ export async function discoverDatasets(args: {
476
582
  params.set("q", args.q);
477
583
  params.set("only", "datasets");
478
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).
479
588
  if (args.domain) {
480
- params.append("domains", args.domain);
589
+ params.set("search_context", args.domain);
481
590
  } else {
482
591
  for (const d of SOCRATA_DOMAINS) params.append("domains", d);
483
592
  }
@@ -486,7 +595,9 @@ export async function discoverDatasets(args: {
486
595
  const { totalAvailable, results } = await memoize(
487
596
  key,
488
597
  async () => {
489
- const body = await getCatalog(params);
598
+ const body = args.domain
599
+ ? await getHostCatalog(args.domain, params)
600
+ : await getCatalog(params);
490
601
  const b = (body ?? {}) as { results?: unknown; resultSetSize?: unknown };
491
602
  // m3 — hard drift on the PRIMARY response (contrast the best-effort count).
492
603
  // The check stays INSIDE the memoize callback so a bad shape is never
@@ -508,6 +619,9 @@ export async function discoverDatasets(args: {
508
619
  const scope = args.domain ? `domain ${args.domain}` : `the ${SOCRATA_DOMAINS.length}-host allowlist`;
509
620
  const notes: string[] = [
510
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.`,
511
625
  `App token: ${appTokenPresent() ? "present (X-App-Token sent; value never logged)" : "absent (keyless)"}.`,
512
626
  ];
513
627
  if (totalAvailable !== null && returned < totalAvailable) {
@@ -519,7 +633,7 @@ export async function discoverDatasets(args: {
519
633
  return withMeta(
520
634
  { query: args.q, domain: args.domain ?? null, results },
521
635
  {
522
- source: `${CATALOG_HOST} catalog ${SOURCE_SUFFIX}`,
636
+ source: `${args.domain ?? CATALOG_HOST} catalog ${SOURCE_SUFFIX}`,
523
637
  keylessMode: true,
524
638
  returned,
525
639
  totalAvailable,