@cliwant/mcp-sam-gov 1.9.0 → 1.11.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 +5 -5
- package/README.ko.md +5 -5
- package/README.md +19 -7
- package/dist/arcgis-feature.d.ts +68 -0
- package/dist/arcgis-feature.d.ts.map +1 -0
- package/dist/arcgis-feature.js +206 -0
- package/dist/arcgis-feature.js.map +1 -0
- package/dist/arcgis-hub.d.ts +90 -0
- package/dist/arcgis-hub.d.ts.map +1 -0
- package/dist/arcgis-hub.js +210 -0
- package/dist/arcgis-hub.js.map +1 -0
- package/dist/bonfire.d.ts +69 -0
- package/dist/bonfire.d.ts.map +1 -0
- package/dist/bonfire.js +212 -0
- package/dist/bonfire.js.map +1 -0
- package/dist/cbp-border.d.ts +9 -5
- package/dist/cbp-border.d.ts.map +1 -1
- package/dist/cbp-border.js +31 -11
- package/dist/cbp-border.js.map +1 -1
- package/dist/gao.d.ts.map +1 -1
- package/dist/gao.js +25 -0
- package/dist/gao.js.map +1 -1
- package/dist/grants.js +1 -1
- package/dist/grants.js.map +1 -1
- package/dist/gsa-perdiem.d.ts +16 -2
- package/dist/gsa-perdiem.d.ts.map +1 -1
- package/dist/gsa-perdiem.js +25 -4
- package/dist/gsa-perdiem.js.map +1 -1
- package/dist/integrity.d.ts.map +1 -1
- package/dist/integrity.js +18 -3
- package/dist/integrity.js.map +1 -1
- package/dist/lda.d.ts.map +1 -1
- package/dist/lda.js +17 -4
- package/dist/lda.js.map +1 -1
- package/dist/nhtsa.d.ts +9 -5
- package/dist/nhtsa.d.ts.map +1 -1
- package/dist/nhtsa.js +90 -26
- package/dist/nhtsa.js.map +1 -1
- package/dist/nist-controls.d.ts +3 -1
- package/dist/nist-controls.d.ts.map +1 -1
- package/dist/nist-controls.js +34 -12
- package/dist/nist-controls.js.map +1 -1
- package/dist/ofac.d.ts.map +1 -1
- package/dist/ofac.js +10 -1
- package/dist/ofac.js.map +1 -1
- package/dist/opengov.d.ts +96 -0
- package/dist/opengov.d.ts.map +1 -0
- package/dist/opengov.js +244 -0
- package/dist/opengov.js.map +1 -0
- package/dist/pricing.d.ts +1 -1
- package/dist/sam-gov/client.d.ts.map +1 -1
- package/dist/sam-gov/client.js +13 -4
- package/dist/sam-gov/client.js.map +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +199 -8
- package/dist/server.js.map +1 -1
- package/dist/socrata.d.ts +27 -16
- package/dist/socrata.d.ts.map +1 -1
- package/dist/socrata.js +149 -18
- package/dist/socrata.js.map +1 -1
- package/package.json +11 -3
- package/src/arcgis-feature.ts +232 -0
- package/src/arcgis-hub.ts +270 -0
- package/src/bonfire.ts +249 -0
- package/src/cbp-border.ts +30 -9
- package/src/gao.ts +27 -0
- package/src/grants.ts +1 -1
- package/src/gsa-perdiem.ts +28 -4
- package/src/integrity.ts +18 -3
- package/src/lda.ts +21 -5
- package/src/nhtsa.ts +92 -27
- package/src/nist-controls.ts +58 -9
- package/src/ofac.ts +12 -1
- package/src/opengov.ts +309 -0
- package/src/sam-gov/client.ts +13 -4
- package/src/server.ts +213 -8
- package/src/socrata.ts +153 -18
package/src/sam-gov/client.ts
CHANGED
|
@@ -353,11 +353,20 @@ export class SamGovClient {
|
|
|
353
353
|
filters: SamSearchFilters,
|
|
354
354
|
): Promise<SamSearchResult> {
|
|
355
355
|
const url = new URL(`${PUBLIC_BASE}/sgs/v1/search/`);
|
|
356
|
+
// Keyless HAL pagination is by PAGE (page × size), not a row offset — the
|
|
357
|
+
// list endpoint pages correctly (VERIFIED LIVE 2026-07: page=0 and page=1
|
|
358
|
+
// return disjoint result sets for the same query). Map the caller's row
|
|
359
|
+
// `offset` onto the page grid; a non-page-aligned offset snaps DOWN to its
|
|
360
|
+
// page boundary and we return the SERVED offset (page × size) so `_meta`
|
|
361
|
+
// never claims an offset the upstream didn't honor. (The authenticated path,
|
|
362
|
+
// buildAuthSearchUrl, honors an arbitrary `offset` directly.)
|
|
363
|
+
const size = filters.limit ?? 25;
|
|
364
|
+
const page = Math.max(0, Math.floor((filters.offset ?? 0) / size));
|
|
356
365
|
url.searchParams.set("index", "opp");
|
|
357
|
-
url.searchParams.set("page",
|
|
366
|
+
url.searchParams.set("page", String(page));
|
|
358
367
|
url.searchParams.set("mode", "search");
|
|
359
368
|
url.searchParams.set("sort", "-modifiedDate");
|
|
360
|
-
url.searchParams.set("size", String(
|
|
369
|
+
url.searchParams.set("size", String(size));
|
|
361
370
|
url.searchParams.set("is_active", "true");
|
|
362
371
|
// Keyless HAL facet params — VERIFIED LIVE (2026-07). The list endpoint
|
|
363
372
|
// honors these server-side: result counts drop correctly AND every returned
|
|
@@ -457,8 +466,8 @@ export class SamGovClient {
|
|
|
457
466
|
});
|
|
458
467
|
return {
|
|
459
468
|
totalRecords,
|
|
460
|
-
limit:
|
|
461
|
-
offset:
|
|
469
|
+
limit: size,
|
|
470
|
+
offset: page * size, // the SERVED offset (page-aligned), never the raw request
|
|
462
471
|
opportunitiesData: data,
|
|
463
472
|
};
|
|
464
473
|
}
|
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.
|
|
109
|
+
const SERVER_VERSION = "1.11.0";
|
|
106
110
|
|
|
107
111
|
// ─── Tool input schemas (Zod) ────────────────────────────────────
|
|
108
112
|
|
|
@@ -2975,6 +2979,113 @@ const DatagovSearchDatasetsInput = z.object({
|
|
|
2975
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."),
|
|
2976
2980
|
});
|
|
2977
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
|
+
|
|
2978
3089
|
// ─── GovInfo (api.govinfo.gov — the api.data.gov keyed trio's 3rd API) ─
|
|
2979
3090
|
// ADR-0010. Same DATA_GOV_API_KEY/DEMO_KEY/X-Api-Key discipline as the datagov
|
|
2980
3091
|
// trio (shared datagovKey.ts seam). `collection` is grammar-checked here AND
|
|
@@ -4307,7 +4418,7 @@ const GsaPerdiemRatesInput = z
|
|
|
4307
4418
|
.regex(/^\d{4}$/)
|
|
4308
4419
|
.optional()
|
|
4309
4420
|
.describe(
|
|
4310
|
-
"The per-diem fiscal year (default
|
|
4421
|
+
"The per-diem fiscal year (default: the current U.S. federal fiscal year, computed at call time — GSA sets rates per FY, Oct 1–Sep 30). Validated ^\\d{4}$ (it rides in the request path).",
|
|
4311
4422
|
),
|
|
4312
4423
|
})
|
|
4313
4424
|
.describe(
|
|
@@ -4432,7 +4543,7 @@ const LdaSearchFilingsInput = z.object({
|
|
|
4432
4543
|
.string()
|
|
4433
4544
|
.min(1)
|
|
4434
4545
|
.optional()
|
|
4435
|
-
.describe("
|
|
4546
|
+
.describe("NOTE: the keyless /filings/ endpoint has NO server-side government-entity filter — the LDA API silently ignores it, so this value is NOT applied (reported in _meta.filtersDropped, never as a narrowed total). Government entities are nested per lobbying activity (each filing's lobbyingActivities[].governmentEntities); to find who lobbied an agency, narrow by registrantName/clientName/issue and inspect those nested entities. Retained for discoverability of the limitation."),
|
|
4436
4547
|
issue: z
|
|
4437
4548
|
.string()
|
|
4438
4549
|
.min(1)
|
|
@@ -4784,6 +4895,21 @@ export const TOOLS: ToolDef[] = [
|
|
|
4784
4895
|
"The organization-name filter is NOT supported by the keyless endpoint and was ignored (results are unfiltered on organization). Set SAM_GOV_API_KEY to filter by organization, or filter client-side on the returned `agency` field.",
|
|
4785
4896
|
);
|
|
4786
4897
|
}
|
|
4898
|
+
// Pagination honesty: the keyless HAL endpoint pages by PAGE (page × size),
|
|
4899
|
+
// not an arbitrary row offset, so a requested offset that is not a multiple
|
|
4900
|
+
// of `limit` snaps DOWN to its page boundary. Disclose the snap so the AI
|
|
4901
|
+
// never believes it read rows [offset..offset+limit) when it actually got
|
|
4902
|
+
// the page-aligned window. (An aligned offset — the default paging pattern
|
|
4903
|
+
// of incrementing by `limit` — is served exactly, so no note.)
|
|
4904
|
+
{
|
|
4905
|
+
const reqOffset = input.offset ?? 0;
|
|
4906
|
+
const size = input.limit ?? 25;
|
|
4907
|
+
if (reqOffset % size !== 0) {
|
|
4908
|
+
notes.push(
|
|
4909
|
+
`Keyless SAM pagination is page-based (page × ${size}); the requested offset ${reqOffset} was snapped DOWN to the page boundary ${Math.floor(reqOffset / size) * size}. Page through by incrementing offset in multiples of limit (set SAM_GOV_API_KEY for exact row offsets).`,
|
|
4910
|
+
);
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4787
4913
|
notes.push(...enrichmentNotes);
|
|
4788
4914
|
|
|
4789
4915
|
// freshness is surfaced structurally in `data` (the ResponseMeta type
|
|
@@ -5543,7 +5669,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
5543
5669
|
defineTool({
|
|
5544
5670
|
name: "nist_800_53_controls",
|
|
5545
5671
|
description:
|
|
5546
|
-
"Look up NIST SP 800-53 Rev 5 security & privacy CONTROLS (keyless) — the requirement backbone for FedRAMP / CMMC / RMF compliance work. Retrieve a control by `controlId` (exact, e.g. 'AC-2', 'SC-7', 'AC-2(1)'), a `family` (2-letter code 'AC'/'SC'/'IA' or a name substring 'Access Control'), and/or a `keyword` (case-insensitive substring over title + statement); `limit`/`offset` pagination. Each row: { id (e.g. 'AC-2'), family (e.g. 'AC — Access Control'), title, statement (the labelled requirement prose), guidance (discussion), enhancements:[{id,title}] (e.g. AC-2(1)) }. Complements cve_lookup + cisa_kev_lookup (the vulnerability side) with the CONTROL/requirement side. HONESTY: source is NIST's OFFICIAL OSCAL catalog published at github.com/usnistgov/oscal-content (authoritative first-party data served from GitHub, not a .gov API host — provenance disclosed in _meta); the catalog has no query API so filtering is CLIENT-SIDE and totalAvailable is the EXACT match count; this is the REQUIREMENT text only — applicability depends on the system's FIPS-199 impact baseline (Low/Moderate/High), which the catalog does not encode (disclosed); a download failure or an implausibly-truncated catalog (< 15 families) THROWS (never a fake-empty 'control not found').",
|
|
5672
|
+
"Look up NIST SP 800-53 Rev 5 security & privacy CONTROLS (keyless) — the requirement backbone for FedRAMP / CMMC / RMF compliance work. Retrieve a control by `controlId` (exact, e.g. 'AC-2', 'SC-7', 'AC-2(1)'), a `family` (2-letter code 'AC'/'SC'/'IA' or a name substring 'Access Control'), and/or a `keyword` (case-insensitive substring over title + statement); `limit`/`offset` pagination. Each row: { id (e.g. 'AC-2'), family (e.g. 'AC — Access Control'), title, status ('withdrawn' | null), statement (the labelled requirement prose; NULL for a WITHDRAWN control, never ''), guidance (discussion), incorporatedInto:[control ids that superseded a withdrawn control, e.g. AC-13 → ['AC-2','AU-6']], enhancements:[{id,title}] (e.g. AC-2(1)) }. Complements cve_lookup + cisa_kev_lookup (the vulnerability side) with the CONTROL/requirement side. HONESTY: source is NIST's OFFICIAL OSCAL catalog published at github.com/usnistgov/oscal-content (authoritative first-party data served from GitHub, not a .gov API host — provenance disclosed in _meta); the exact OSCAL version + last-modified are surfaced in _meta (the catalog is fetched live from the MOVING 'main' branch, so control text can shift between point releases, e.g. 5.1.1 → 5.2.0 — cite the version, not just 'Rev 5'); a WITHDRAWN control (status:'withdrawn') has statement:null and is NOT an active requirement (see incorporatedInto for what replaced it); the catalog has no query API so filtering is CLIENT-SIDE and totalAvailable is the EXACT match count; this is the REQUIREMENT text only — applicability depends on the system's FIPS-199 impact baseline (Low/Moderate/High), which the catalog does not encode (disclosed); a download failure or an implausibly-truncated catalog (< 15 families) THROWS (never a fake-empty 'control not found').",
|
|
5547
5673
|
inputSchema: NistControlsInput,
|
|
5548
5674
|
handler: (input) => nistControls.searchControls(input),
|
|
5549
5675
|
}),
|
|
@@ -6030,6 +6156,72 @@ export const TOOLS: ToolDef[] = [
|
|
|
6030
6156
|
inputSchema: DatagovSearchDatasetsInput,
|
|
6031
6157
|
handler: (input) => datagovCatalog.searchDatasets(input),
|
|
6032
6158
|
}),
|
|
6159
|
+
// ━━━ ArcGIS Hub (hub.arcgis.com) — keyless SLED dataset DISCOVERY (loop cycle 9) ━━━
|
|
6160
|
+
// A NEW discovery platform: much US state/local/regional/tribal (SLED) open data
|
|
6161
|
+
// — GIS/infrastructure/permits/boundaries/procurement — lives on ArcGIS Hub, NOT
|
|
6162
|
+
// Socrata/CKAN. Fixed-host SSRF (no per-jurisdiction host vector). ★PROVENANCE: the
|
|
6163
|
+
// Hub is GLOBAL + OPEN (non-US/non-gov publishers), so this is a DISCOVERY aid, not
|
|
6164
|
+
// a curated official-source allowlist — per-row owner/orgName/source/region are
|
|
6165
|
+
// surfaced for vetting + the disclosure rides every response. P1: totalAvailable =
|
|
6166
|
+
// meta.total (exact Hub count). DISCOVERY ONLY (row-query on arbitrary ArcGIS hosts
|
|
6167
|
+
// is a planned, separately-guarded addition).
|
|
6168
|
+
defineTool({
|
|
6169
|
+
name: "arcgis_hub_discover_datasets",
|
|
6170
|
+
description:
|
|
6171
|
+
"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.",
|
|
6172
|
+
inputSchema: ArcgisHubDiscoverInput,
|
|
6173
|
+
handler: (input) => arcgisHub.discoverDatasets(input),
|
|
6174
|
+
}),
|
|
6175
|
+
// ━━━ OpenGov Procurement (api.procurement.opengov.com) — keyless SLED bids ━━━
|
|
6176
|
+
// SLED bid campaign (from the exhaustive US state+local bid-site research). 525+
|
|
6177
|
+
// state/local government portals' LIVE solicitations, via the public portal's OWN
|
|
6178
|
+
// anonymous backend REST API — the key-gated official api-key API is NOT touched
|
|
6179
|
+
// (genuinely keyless, live-verified). Fixed-host SSRF. Two tools: a directory
|
|
6180
|
+
// (one keyless GET returns all ~560 orgs) + per-org solicitations.
|
|
6181
|
+
defineTool({
|
|
6182
|
+
name: "opengov_list_governments",
|
|
6183
|
+
description:
|
|
6184
|
+
"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.",
|
|
6185
|
+
inputSchema: OpengovListGovernmentsInput,
|
|
6186
|
+
handler: (input) => opengov.listGovernments(input),
|
|
6187
|
+
}),
|
|
6188
|
+
defineTool({
|
|
6189
|
+
name: "opengov_search_solicitations",
|
|
6190
|
+
description:
|
|
6191
|
+
"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).",
|
|
6192
|
+
inputSchema: OpengovSearchSolicitationsInput,
|
|
6193
|
+
handler: (input) => opengov.searchSolicitations(input),
|
|
6194
|
+
}),
|
|
6195
|
+
// ━━━ Bonfire (Euna) — keyless per-org open-opportunity RSS (SLED bids) ━━━
|
|
6196
|
+
// SLED bid campaign. Thousands of US state/local govs on Bonfire expose a keyless
|
|
6197
|
+
// RSS of open opportunities. Ships a curated 187-org live-verified seed directory
|
|
6198
|
+
// (Bonfire's authoritative org API is auth-gated → out of bounds). Fixed-suffix
|
|
6199
|
+
// SSRF (.bonfirehub.com). RSS = the complete open set (totalAvailable honest).
|
|
6200
|
+
defineTool({
|
|
6201
|
+
name: "bonfire_list_organizations",
|
|
6202
|
+
description:
|
|
6203
|
+
"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.",
|
|
6204
|
+
inputSchema: BonfireListOrganizationsInput,
|
|
6205
|
+
handler: (input) => bonfire.listOrganizations(input),
|
|
6206
|
+
}),
|
|
6207
|
+
defineTool({
|
|
6208
|
+
name: "bonfire_search_opportunities",
|
|
6209
|
+
description:
|
|
6210
|
+
"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).",
|
|
6211
|
+
inputSchema: BonfireSearchOpportunitiesInput,
|
|
6212
|
+
handler: (input) => bonfire.searchOpportunities(input),
|
|
6213
|
+
}),
|
|
6214
|
+
// ━━━ ArcGIS REST feature query (curated allowlist) — SLED bids/GIS ━━━
|
|
6215
|
+
// Generic query companion to arcgis_hub_discover_datasets. First payload: the DC
|
|
6216
|
+
// OCP PASS procurement layers (live solicitations + contracts/PO/payments), a
|
|
6217
|
+
// keyless live-solicitation feed via ArcGIS REST. `service` enum = SSRF core.
|
|
6218
|
+
defineTool({
|
|
6219
|
+
name: "arcgis_feature_query",
|
|
6220
|
+
description:
|
|
6221
|
+
"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).",
|
|
6222
|
+
inputSchema: ArcgisFeatureQueryInput,
|
|
6223
|
+
handler: (input) => arcgisFeature.featureQuery(input),
|
|
6224
|
+
}),
|
|
6033
6225
|
// ━━━ GovInfo (api.govinfo.gov) — the api.data.gov keyed trio's 3rd API (3) ━━━ ADR-0010
|
|
6034
6226
|
// GPO-authoritative bulk publications (BILLS/PLAW/USCODE/CREC/CFR-FR editions/
|
|
6035
6227
|
// BUDGET/GAOREPORTS) with PDF/XML/MODS downloads + provenance. 2nd consumer of the
|
|
@@ -6269,7 +6461,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
6269
6461
|
defineTool({
|
|
6270
6462
|
name: "cbp_border_wait_times",
|
|
6271
6463
|
description:
|
|
6272
|
-
"Live CBP land-border-port wait times — current commercial-vehicle (
|
|
6464
|
+
"Live CBP land-border-port wait times — current commercial-vehicle (freight-truck) crossing delays at every US Canadian- and Mexican-border port (keyless; bwt.cbp.gov). The FREIGHT / LOGISTICS situational-awareness lane: per-port commercial-vehicle standard + FAST lane delay (minutes), operational status, open-lane count, and maximum lanes — passenger/pedestrian lanes are NOT surfaced (freight lane only). Filters (optional, applied CLIENT-SIDE over the full fetched port set — the feed has NO server-side filter; an empty-string value is reported in _meta.filtersDropped, not applied): `border` (case-insensitive substring, 'Canadian'/'Mexican'), `portName` (substring, e.g. 'Laredo'); `limit`/`offset` pagination. Each row: { portNumber, portName, crossingName, border, portStatus (Open/Closed), asOf, commercialVehicle:{ maxLanes, standard:{operationalStatus, delayMinutes, lanesOpen, updateTime}, fast:{…} } }. HONESTY: this is REAL-TIME operational data — each lane carries its own updateTime (surfaced verbatim; freshness never implied live-to-the-second); delayMinutes/lanesOpen are number|null (a real 0 stays 0; an empty/N/A value — e.g. a closed lane — is null, NEVER a fabricated 0, because a closed lane's delay is UNKNOWN, not zero); the API returns the WHOLE port set so totalAvailable is the EXACT matched-port count; an outage/4xx/timeout THROWS and a non-array body ⇒ schema_drift (never a fake empty).",
|
|
6273
6465
|
inputSchema: CbpBorderWaitInput,
|
|
6274
6466
|
handler: (input) => cbpBorder.borderWaitTimes(input),
|
|
6275
6467
|
}),
|
|
@@ -6298,7 +6490,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
6298
6490
|
defineTool({
|
|
6299
6491
|
name: "gsa_perdiem_rates",
|
|
6300
6492
|
description:
|
|
6301
|
-
"Look up GSA Federal Travel PER-DIEM rates — the max lodging + Meals & Incidental Expenses (M&IE) reimbursement ceilings for official U.S. government travel (api.gsa.gov /travel/perdiem/v2, keyed — DATA_GOV_API_KEY or the shared DEMO_KEY). Input: EITHER `city` (e.g. 'Washington') + `state` (2-letter, e.g. 'DC') OR `zip` (5-digit) — supplying BOTH, or NEITHER, ⇒ invalid_input with 0 fetch; optional `year` (default
|
|
6493
|
+
"Look up GSA Federal Travel PER-DIEM rates — the max lodging + Meals & Incidental Expenses (M&IE) reimbursement ceilings for official U.S. government travel (api.gsa.gov /travel/perdiem/v2, keyed — DATA_GOV_API_KEY or the shared DEMO_KEY). Input: EITHER `city` (e.g. 'Washington') + `state` (2-letter, e.g. 'DC') OR `zip` (5-digit) — supplying BOTH, or NEITHER, ⇒ invalid_input with 0 fetch; optional `year` (default: the current U.S. federal fiscal year). Returns { rates:[{ city, county, state, zip, year, isOconus, standardRate, mealsUsd, monthlyLodgingUsd:[{ month (1-12), monthName, lodgingUsd }] }] } + honest _meta. HONESTY: lodgingUsd (the API's monthly `value`) is the MAX nightly lodging ceiling for that month — it VARIES SEASONALLY (hence a per-month array), and mealsUsd is the daily M&IE ceiling; both are integer US dollars, null-when-withheld (NEVER 0 — a genuine 0 is preserved). standardRate/isOconus are booleans coerced from the API's string 'true'/'false' (an unrecognized value ⇒ null, never a fabricated false); the months array is preserved AS-IS (never padded to 12). The API returns the COMPLETE rate set (no pagination) ⇒ totalAvailable = the row count, complete:true. A genuine no-match (rates:[]/rate:[]) ⇒ honest empty (returned:0); the API's `errors` field non-null ⇒ invalid_input carrying the message (never a fake empty); a 429 (DEMO_KEY ~10 req/hr, hit quickly) ⇒ rate_limited THROWS; a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON ⇒ schema_drift. DEMO_KEY ~10 req/hr shared ceiling — set DATA_GOV_API_KEY (free at api.data.gov/signup) for 1000/hr. The key rides ONLY in the X-Api-Key header (never the URL/_meta).",
|
|
6302
6494
|
inputSchema: GsaPerdiemRatesInput,
|
|
6303
6495
|
handler: (input) => gsaPerdiem.perdiemRates(input),
|
|
6304
6496
|
}),
|
|
@@ -6333,7 +6525,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
6333
6525
|
defineTool({
|
|
6334
6526
|
name: "lda_search_filings",
|
|
6335
6527
|
description:
|
|
6336
|
-
"Search US Senate LDA (Lobbying Disclosure Act) filings — who is paid HOW MUCH to lobby WHICH federal agency on WHICH issue (lda.senate.gov/api/v1/filings, KEYLESS — anonymous access works; an optional free LDA_API_KEY only raises the rate limit). All inputs optional: `registrantName` (the lobbying firm/in-house filer), `clientName` (who it's for), `lobbyistName`, `filingYear` (4-digit), `filingType` (short code, e.g. 'Q1'/'RR'/'YE'), `agency` (
|
|
6528
|
+
"Search US Senate LDA (Lobbying Disclosure Act) filings — who is paid HOW MUCH to lobby WHICH federal agency on WHICH issue (lda.senate.gov/api/v1/filings, KEYLESS — anonymous access works; an optional free LDA_API_KEY only raises the rate limit). All inputs optional: `registrantName` (the lobbying firm/in-house filer), `clientName` (who it's for), `lobbyistName`, `filingYear` (4-digit), `filingType` (short code, e.g. 'Q1'/'RR'/'YE'), `agency` (NOTE: /filings/ has NO server-side agency filter — the LDA API silently ignores it, so it is reported in _meta.filtersDropped and NOT applied; government entities are nested per activity in lobbyingActivities[].governmentEntities), `issue` (specific lobbying issues text), `page` (1-based, default 1), `pageSize` (1..25, default 25). Returns { filings:[{ filingUuid, filingType, filingYear, filingPeriod, incomeUsd, expensesUsd, registrant, client, lobbyingActivities:[{ issueCode, description, governmentEntities:[names] }], documentUrl, postedDate, terminationDate }] } + honest _meta. HONESTY: totalAvailable is the API's REAL total match count (the corpus is ~1.95M filings) — NOT the rows on this page; pagination is page-based (pass the next page number when hasMore). incomeUsd/expensesUsd are parsed from the null-or-decimal-string income/expenses — null (not reported) ⇒ null, NEVER 0 (a genuine 0 stays 0); a filing reports EITHER income OR expenses, so the other is typically null. Missing lobbying_activities/government_entities ⇒ empty arrays (never fabricated). A genuine no-match (results:[]) ⇒ honest empty (returned:0); a 400 (bad filter) ⇒ invalid_input surfacing the API's message; a 429 ⇒ rate_limited THROWS (Retry-After honored, never routed around); a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON / non-array results / non-number count ⇒ schema_drift. The optional key rides ONLY in the Authorization: Token header (never the URL/_meta).",
|
|
6337
6529
|
inputSchema: LdaSearchFilingsInput,
|
|
6338
6530
|
handler: (input) => lda.searchFilings(input),
|
|
6339
6531
|
}),
|
|
@@ -6619,7 +6811,7 @@ export async function runTool(
|
|
|
6619
6811
|
/**
|
|
6620
6812
|
* Hand-rolled Zod → JSON Schema converter (subset we use).
|
|
6621
6813
|
*/
|
|
6622
|
-
function zodToJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> {
|
|
6814
|
+
export function zodToJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> {
|
|
6623
6815
|
const def = (schema as unknown as { _def: { typeName: string } })._def;
|
|
6624
6816
|
const tn = def.typeName;
|
|
6625
6817
|
const description = (schema as unknown as { description?: string }).description;
|
|
@@ -6671,6 +6863,19 @@ function zodToJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> {
|
|
|
6671
6863
|
const innerSchema = zodToJsonSchema(inner);
|
|
6672
6864
|
return description ? { ...innerSchema, description } : innerSchema;
|
|
6673
6865
|
}
|
|
6866
|
+
if (tn === "ZodEffects") {
|
|
6867
|
+
// .refine() / .superRefine() / .transform() wrap the REAL schema at
|
|
6868
|
+
// `_def.schema` (used for cross-field rules like "npi OR state required").
|
|
6869
|
+
// Without this branch these fall through to the {type:"string"} default,
|
|
6870
|
+
// publishing a degenerate object-less inputSchema that a schema-driven MCP
|
|
6871
|
+
// client cannot construct a call against — even though the runtime Zod still
|
|
6872
|
+
// demands the full object. Unwrap so the published schema keeps its real
|
|
6873
|
+
// properties / required / enums.
|
|
6874
|
+
const inner = (schema as unknown as { _def: { schema: z.ZodTypeAny } })._def
|
|
6875
|
+
.schema;
|
|
6876
|
+
const innerSchema = zodToJsonSchema(inner);
|
|
6877
|
+
return description ? { ...innerSchema, description } : innerSchema;
|
|
6878
|
+
}
|
|
6674
6879
|
return { type: "string", ...(description ? { description } : {}) };
|
|
6675
6880
|
}
|
|
6676
6881
|
|
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:
|
|
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 —
|
|
71
|
-
*
|
|
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
|
-
*
|
|
75
|
-
* (
|
|
76
|
-
* surface it
|
|
77
|
-
* (
|
|
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,85 @@ 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)
|
|
197
|
+
// ── County/city procurement sweep, wave 2 (loop cycle 19, 2026-07-20). Same
|
|
198
|
+
// federated-catalog mining + host-scoped count(*) + /resource 200 bare-array
|
|
199
|
+
// verification. All large real checkbook/PO/vendor-payment datasets. ──
|
|
200
|
+
"opendata.howardcountymd.gov", // Howard County MD (.gov) — e.g. mesh-jggc (Vendors Receiving Payments $30k+ ~35k)
|
|
201
|
+
"data.providenceri.gov", // Providence RI (.gov) — e.g. 425y-pm5m (City & School Dept Purchase Orders ~228k)
|
|
202
|
+
"fiscalfocus.pittsburghpa.gov", // Pittsburgh PA (.gov) — e.g. t8t2-4b5n (Checkbook Data ~1.01M)
|
|
203
|
+
"data.coloradosprings.gov", // Colorado Springs CO (.gov) — e.g. yn6y-xikx (Open Checkbook Vendors ~19k)
|
|
204
|
+
"data.framinghamma.gov", // Framingham MA (.gov) — e.g. cqve-ehkr (Checkbook ~324k)
|
|
205
|
+
"data.fultoncountyga.gov", // Fulton County GA (.gov) — e.g. mxhc-krcg (Vendor Payments/disbursements ~217k)
|
|
206
|
+
"atlanta.data.socrata.com", // City of Atlanta GA (Socrata-hosted official portal) — e.g. jmke-icfi (Open Checkbook Ledger ~1.78M)
|
|
207
|
+
"opendata.cityofmesquite.com", // Mesquite TX (.com, official) — e.g. 6tva-azs5 (Check Register ~144k)
|
|
208
|
+
// ── County/city procurement sweep, wave 3 (loop cycle 20, 2026-07-20). Expanded
|
|
209
|
+
// federated-catalog queries (rfp/rfq/disbursement/expenditure/commodity) +
|
|
210
|
+
// offset paging. Provenance confirmed via /api/views attribution or the gov
|
|
211
|
+
// domain itself; hosts with only an individual-name attribution on a generic
|
|
212
|
+
// *.data.socrata.com subdomain (washoe, newcastle) were DEFERRED. ──
|
|
213
|
+
"datahub.usac.org", // USAC E-Rate (.org, same org as opendata.usac.org) — e.g. 39tn-hjzv (E-Rate Open Competitive Bidding, FCC Form 470 — schools/libraries LIVE bids ~2.2M)
|
|
214
|
+
"performance.ci.janesville.wi.us", // City of Janesville WI (.us official municipal domain) — e.g. fd4q-2kma (Open Expenditures Ledger ~1.0M)
|
|
215
|
+
"datahub.austintexas.gov", // Austin TX secondary hub (.gov) — e.g. 3ebq-e9iz (Purchase Order Quantity/Price detail, commodity/goods procurements ~318k)
|
|
216
|
+
"cthru.data.socrata.com", // Commonwealth of Massachusetts, Office of the Comptroller — CTHRU statewide spending (attribution-confirmed) — e.g. kv7m-35wn (Budget/Actual + spending authorizations ~48k)
|
|
217
|
+
"data.macoupincountyil.gov", // Macoupin County IL (.gov) — e.g. wysn-7qcg (Open Expenditures Ledger ~156k)
|
|
218
|
+
"data.oaklandca.gov", // Oakland CA (.gov) — e.g. 4ewt-5m6f (budget expenditures ~66k)
|
|
219
|
+
"data.princegeorgescountymd.gov", // Prince George's County MD (.gov) — e.g. csi4-9jzc (Spending Information: payee_name/agency/amount ~62k)
|
|
220
|
+
"data.cstx.gov", // College Station TX (.gov) — e.g. i5cx-63zy (Open Budget Expenditures ~42k)
|
|
221
|
+
"datahub.transportation.gov", // US DOT secondary hub (.gov) — e.g. 255k-mnvp (Disbursements by States for Highways SF-2 ~15k)
|
|
222
|
+
// ── County/city procurement sweep, wave 4 / tail (loop cycle 21, 2026-07-20).
|
|
223
|
+
// Diminishing returns (most remaining catalog hits are Canada/AU, demo/test,
|
|
224
|
+
// duplicates, or off-theme); these two are the clean US wins. ──
|
|
225
|
+
"citydata.mesaaz.gov", // Mesa AZ secondary hub (.gov, attr "Office of Management and Budget") — e.g. vdg8-dx96 (City Expenditures ~15.4M)
|
|
226
|
+
"data.weho.org", // City of West Hollywood CA (.org, official portal) — e.g. atdr-sk64 (Active Contracts: contract_number/contractor_name/status/type — live/current ~1,030)
|
|
143
227
|
] as const;
|
|
144
228
|
|
|
145
229
|
export type SocrataDomain = (typeof SOCRATA_DOMAINS)[number];
|
|
@@ -262,6 +346,44 @@ async function getCatalog(params: URLSearchParams): Promise<unknown> {
|
|
|
262
346
|
});
|
|
263
347
|
}
|
|
264
348
|
|
|
349
|
+
/**
|
|
350
|
+
* GET a HOST-SCOPED discovery catalog (`https://{domain}/api/catalog/v1?
|
|
351
|
+
* search_context={domain}&…`). Unlike the FEDERATED `getCatalog`
|
|
352
|
+
* (api.us.socrata.com), each portal's OWN catalog COMPLETELY indexes its own
|
|
353
|
+
* datasets — the federated aggregator under-indexes many hosts (loop cycle 8:
|
|
354
|
+
* USAC → 0, DOT → 3 of 1,873). Used whenever a specific `domain` is requested;
|
|
355
|
+
* the all-host search (domain omitted) still needs the federated aggregator.
|
|
356
|
+
* SSRF: identical guard to getSocrataResource — domain ∈ allowlist + the
|
|
357
|
+
* CONSTRUCTED URL's hostname === domain (https); redirect:"error" (B1); the
|
|
358
|
+
* host-only label carries the domain (never the token, m7).
|
|
359
|
+
*/
|
|
360
|
+
async function getHostCatalog(
|
|
361
|
+
domain: string,
|
|
362
|
+
params: URLSearchParams,
|
|
363
|
+
): Promise<unknown> {
|
|
364
|
+
if (!SOCRATA_DOMAIN_SET.has(domain)) {
|
|
365
|
+
throw new ToolErrorCarrier({
|
|
366
|
+
kind: "invalid_input",
|
|
367
|
+
message: `Socrata domain ${JSON.stringify(domain)} is not on the curated allowlist. Allowed: ${SOCRATA_DOMAINS.join(", ")}.`,
|
|
368
|
+
retryable: false,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const url = `https://${domain}/api/catalog/v1?${params.toString()}`;
|
|
372
|
+
const built = new URL(url);
|
|
373
|
+
if (built.hostname !== domain || built.protocol !== "https:") {
|
|
374
|
+
throw new ToolErrorCarrier({
|
|
375
|
+
kind: "invalid_input",
|
|
376
|
+
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).`,
|
|
377
|
+
retryable: false,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return getJson(url, {
|
|
381
|
+
label: "socrata:catalog:" + domain,
|
|
382
|
+
headers: appTokenHeader(),
|
|
383
|
+
redirect: "error",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
265
387
|
// ─── map + meta helpers ───────────────────────────────────────────
|
|
266
388
|
const STRING_COERCION_NOTE =
|
|
267
389
|
"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 +586,21 @@ function mapCatalogRow(row: unknown): CatalogDataset {
|
|
|
464
586
|
}
|
|
465
587
|
|
|
466
588
|
/**
|
|
467
|
-
* Discover dataset 4x4 ids via the Socrata catalog (memoized ~10 min).
|
|
468
|
-
* `domain`
|
|
469
|
-
*
|
|
470
|
-
*
|
|
589
|
+
* Discover dataset 4x4 ids via the Socrata catalog (memoized ~10 min). Passing a
|
|
590
|
+
* `domain` queries that portal's OWN catalog (search_context) — a COMPLETE
|
|
591
|
+
* per-host index; omitting it searches the WHOLE allowlist via the federated
|
|
592
|
+
* api.us.socrata.com aggregator (repeated `domains=`). Returns `[{ id, name,
|
|
593
|
+
* description, domain, updatedAt, link }]` + `totalAvailable = resultSetSize`.
|
|
594
|
+
* Feeds `datasetId` to socrata_query.
|
|
471
595
|
*
|
|
472
596
|
* m3 — this is the catalog's PRIMARY response: a non-number `resultSetSize` is a
|
|
473
597
|
* hard schema_drift throw (nothing valid to return), never a fabricated total.
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
598
|
+
*
|
|
599
|
+
* UNDER-INDEX (loop cycle 8 fix): the FEDERATED aggregator under-indexes many
|
|
600
|
+
* hosts (USAC → 0; DOT → 3 of 1,873), so the all-host search (domain omitted) can
|
|
601
|
+
* miss datasets — its `_meta` note discloses this. A DOMAIN-scoped search now
|
|
602
|
+
* uses that host's OWN catalog, which indexes it completely (USAC/DOT/etc. fully
|
|
603
|
+
* discoverable). socrata_query works regardless with a known 4x4.
|
|
477
604
|
*/
|
|
478
605
|
export async function discoverDatasets(args: {
|
|
479
606
|
q: string;
|
|
@@ -485,8 +612,11 @@ export async function discoverDatasets(args: {
|
|
|
485
612
|
params.set("q", args.q);
|
|
486
613
|
params.set("only", "datasets");
|
|
487
614
|
params.set("limit", String(limit));
|
|
615
|
+
// A specific domain → that portal's OWN catalog (search_context), a COMPLETE
|
|
616
|
+
// per-host index. Omitting domain → the federated aggregator (repeated
|
|
617
|
+
// domains=), the only way to search across all hosts (loop cycle 8).
|
|
488
618
|
if (args.domain) {
|
|
489
|
-
params.
|
|
619
|
+
params.set("search_context", args.domain);
|
|
490
620
|
} else {
|
|
491
621
|
for (const d of SOCRATA_DOMAINS) params.append("domains", d);
|
|
492
622
|
}
|
|
@@ -495,7 +625,9 @@ export async function discoverDatasets(args: {
|
|
|
495
625
|
const { totalAvailable, results } = await memoize(
|
|
496
626
|
key,
|
|
497
627
|
async () => {
|
|
498
|
-
const body =
|
|
628
|
+
const body = args.domain
|
|
629
|
+
? await getHostCatalog(args.domain, params)
|
|
630
|
+
: await getCatalog(params);
|
|
499
631
|
const b = (body ?? {}) as { results?: unknown; resultSetSize?: unknown };
|
|
500
632
|
// m3 — hard drift on the PRIMARY response (contrast the best-effort count).
|
|
501
633
|
// The check stays INSIDE the memoize callback so a bad shape is never
|
|
@@ -517,6 +649,9 @@ export async function discoverDatasets(args: {
|
|
|
517
649
|
const scope = args.domain ? `domain ${args.domain}` : `the ${SOCRATA_DOMAINS.length}-host allowlist`;
|
|
518
650
|
const notes: string[] = [
|
|
519
651
|
`Catalog search over ${scope} (only=datasets). Feed a result's id to socrata_query as datasetId.`,
|
|
652
|
+
args.domain
|
|
653
|
+
? `Source: the ${args.domain} portal's OWN catalog (search_context) — a COMPLETE per-host index.`
|
|
654
|
+
: `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
655
|
`App token: ${appTokenPresent() ? "present (X-App-Token sent; value never logged)" : "absent (keyless)"}.`,
|
|
521
656
|
];
|
|
522
657
|
if (totalAvailable !== null && returned < totalAvailable) {
|
|
@@ -528,7 +663,7 @@ export async function discoverDatasets(args: {
|
|
|
528
663
|
return withMeta(
|
|
529
664
|
{ query: args.q, domain: args.domain ?? null, results },
|
|
530
665
|
{
|
|
531
|
-
source: `${CATALOG_HOST} catalog ${SOURCE_SUFFIX}`,
|
|
666
|
+
source: `${args.domain ?? CATALOG_HOST} catalog ${SOURCE_SUFFIX}`,
|
|
532
667
|
keylessMode: true,
|
|
533
668
|
returned,
|
|
534
669
|
totalAvailable,
|