@cliwant/mcp-sam-gov 1.10.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/bonfire.d.ts.map +1 -1
- package/dist/bonfire.js +5 -1
- package/dist/bonfire.js.map +1 -1
- 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/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 +34 -8
- package/dist/server.js.map +1 -1
- package/dist/socrata.d.ts +1 -1
- package/dist/socrata.d.ts.map +1 -1
- package/dist/socrata.js +30 -0
- package/dist/socrata.js.map +1 -1
- package/package.json +11 -3
- package/src/bonfire.ts +5 -1
- 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/sam-gov/client.ts +13 -4
- package/src/server.ts +36 -8
- package/src/socrata.ts +30 -0
package/src/nhtsa.ts
CHANGED
|
@@ -19,16 +19,20 @@
|
|
|
19
19
|
* entirely — never surfaced, logged, or stored. The B2G value is the
|
|
20
20
|
* manufacturer / component / safety signal, NOT the VIN.
|
|
21
21
|
*
|
|
22
|
-
* ★ THE HONESTY PILLARS (P1
|
|
22
|
+
* ★ THE HONESTY PILLARS (P1/P3/P4 live-verified 2026-07-15; P2 corrected +
|
|
23
|
+
* re-verified 2026-07-20):
|
|
23
24
|
* P1: totalAvailable = `Count` (recalls) / `count` (complaints) — the REAL total.
|
|
24
25
|
* NHTSA returns the COMPLETE filtered set (no pagination), so in the normal
|
|
25
26
|
* case Count === results.length ⇒ complete:true. totalAvailable is NEVER
|
|
26
27
|
* fabricated: a PRESENT numeric Count is trusted verbatim; a MISSING Count
|
|
27
28
|
* falls back to results.length WITH an honest note (never invented).
|
|
28
|
-
* P2:
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* P2: ★NHTSA returns HTTP 400 (NOT 200) + {Count/count:0, results:[]} (Message
|
|
30
|
+
* "Results returned successfully") for a VALID make/model/year that simply
|
|
31
|
+
* has ZERO records (live-verified 2026-07-20). getNhtsa reads the body and
|
|
32
|
+
* reclassifies THAT idiom as an HONEST EMPTY (returned:0, totalAvailable:0,
|
|
33
|
+
* complete:true) with a note; any OTHER 400 ⇒ invalid_input, a 404 ⇒
|
|
34
|
+
* not_found, a 5xx/timeout ⇒ THROW (never a fake empty), a 200 non-JSON
|
|
35
|
+
* body ⇒ schema_drift.
|
|
32
36
|
* P3: booleans (crash/fire/parkIt/parkOutSide/overTheAirUpdate) preserved AS
|
|
33
37
|
* booleans (a non-boolean ⇒ null, never a fabricated false); counts
|
|
34
38
|
* (numberOfInjuries/numberOfDeaths) via `num` (a genuine 0 stays 0, NEVER
|
|
@@ -42,8 +46,8 @@
|
|
|
42
46
|
* so a `../` or `%` can never reach the fixed path).
|
|
43
47
|
*/
|
|
44
48
|
|
|
45
|
-
import { ToolErrorCarrier } from "./errors.js";
|
|
46
|
-
import {
|
|
49
|
+
import { ToolErrorCarrier, errorFromResponse } from "./errors.js";
|
|
50
|
+
import { driftError, isRedirectError } from "./datasource.js";
|
|
47
51
|
import { num, str } from "./coerce.js";
|
|
48
52
|
import { withMeta, type MetaBundle, type ResponseMeta } from "./meta.js";
|
|
49
53
|
|
|
@@ -109,10 +113,31 @@ function validateVehicleArgs(args: NhtsaVehicleArgs, label: string): void {
|
|
|
109
113
|
|
|
110
114
|
// ─── SSRF-guarded fetch (fixed host + hostname assertion + redirect) ──
|
|
111
115
|
/**
|
|
112
|
-
*
|
|
113
|
-
* `
|
|
114
|
-
*
|
|
115
|
-
* `
|
|
116
|
+
* ★ NHTSA's "400-for-empty" idiom. A VALID make/model/year that simply has ZERO
|
|
117
|
+
* records returns HTTP 400 with a body `{Count/count:0, ..., results:[]}` and
|
|
118
|
+
* `Message/message:"Results returned successfully"` (live-verified 2026-07-20:
|
|
119
|
+
* `make=tesla&model=model 3&modelYear=2015` → HTTP 400, Count 0). Recalls use
|
|
120
|
+
* `Count`/`Message`, complaints use lowercase `count`/`message` — accept either.
|
|
121
|
+
* This is a genuine no-match, NOT a bad request; the caller reclassifies it to an
|
|
122
|
+
* honest empty. (A 400 that is NOT this idiom stays a real invalid_input.)
|
|
123
|
+
*/
|
|
124
|
+
function isNhtsaEmptyIdiom(body: unknown): boolean {
|
|
125
|
+
if (body === null || typeof body !== "object") return false;
|
|
126
|
+
const b = body as Record<string, unknown>;
|
|
127
|
+
const rawCount = b.Count ?? b.count;
|
|
128
|
+
return num(rawCount) === 0 && Array.isArray(b.results) && b.results.length === 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* GET one NHTSA JSON resource on the FIXED host. Bespoke `fetch` (NOT the shared
|
|
133
|
+
* getJson) because we must READ a non-2xx body: NHTSA returns HTTP 400 for a valid
|
|
134
|
+
* query with zero records (the ★400-for-empty idiom above), and getJson would
|
|
135
|
+
* throw invalid_input and DISCARD the body — turning "zero recalls" (a valid,
|
|
136
|
+
* high-value answer) into a phantom "Bad request". Asserts the constructed URL's
|
|
137
|
+
* hostname === the fixed host over https (belt-and-suspenders) + `redirect:"error"`
|
|
138
|
+
* (fail closed on any off-host 3xx). Keyless — no header/token. Non-2xx that is NOT
|
|
139
|
+
* the empty idiom keeps the standard taxonomy (400→invalid_input, 404→not_found,
|
|
140
|
+
* 429→rate_limited, 5xx→upstream_unavailable); a 200 non-JSON body ⇒ schema_drift.
|
|
116
141
|
*/
|
|
117
142
|
async function getNhtsa(
|
|
118
143
|
path: string,
|
|
@@ -129,7 +154,57 @@ async function getNhtsa(
|
|
|
129
154
|
upstreamEndpoint: label,
|
|
130
155
|
});
|
|
131
156
|
}
|
|
132
|
-
|
|
157
|
+
let res: Response;
|
|
158
|
+
try {
|
|
159
|
+
res = await fetch(built.toString(), {
|
|
160
|
+
redirect: "error",
|
|
161
|
+
signal: AbortSignal.timeout(15_000),
|
|
162
|
+
});
|
|
163
|
+
} catch (e) {
|
|
164
|
+
if (isRedirectError(e)) {
|
|
165
|
+
throw driftError(
|
|
166
|
+
label,
|
|
167
|
+
`NHTSA returned an off-host redirect (redirect:"error") while fetching ${label} — refusing to follow it (SSRF safety).`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (e instanceof Error && (e.name === "TimeoutError" || e.name === "AbortError")) {
|
|
171
|
+
throw new ToolErrorCarrier({
|
|
172
|
+
kind: "upstream_unavailable",
|
|
173
|
+
retryable: false,
|
|
174
|
+
message: `Request to ${label} timed out.`,
|
|
175
|
+
upstreamEndpoint: label,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
throw new ToolErrorCarrier({
|
|
179
|
+
kind: "upstream_unavailable",
|
|
180
|
+
retryable: true,
|
|
181
|
+
retryAfterSeconds: 30,
|
|
182
|
+
message: `Network error reaching ${label}: ${e instanceof Error ? e.message : String(e)}`,
|
|
183
|
+
upstreamEndpoint: label,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
// ★P2 CRUX: read the body — a 400 that is NHTSA's empty idiom is an HONEST
|
|
188
|
+
// no-match (returned via resolveTotal as Count 0 → total 0), NOT an error.
|
|
189
|
+
let body: unknown = null;
|
|
190
|
+
try {
|
|
191
|
+
body = await res.json();
|
|
192
|
+
} catch {
|
|
193
|
+
body = null;
|
|
194
|
+
}
|
|
195
|
+
if (res.status === 400 && isNhtsaEmptyIdiom(body)) return body;
|
|
196
|
+
// Any other non-2xx keeps the standard taxonomy (never a fabricated empty).
|
|
197
|
+
// errorFromResponse returns a plain ToolError → wrap it in the carrier to throw.
|
|
198
|
+
throw new ToolErrorCarrier(errorFromResponse(res, label));
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
return await res.json();
|
|
202
|
+
} catch {
|
|
203
|
+
throw driftError(
|
|
204
|
+
label,
|
|
205
|
+
`NHTSA ${label} returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
133
208
|
}
|
|
134
209
|
|
|
135
210
|
/** Build the shared make/model/modelYear query (module-built; no raw passthrough). */
|
|
@@ -142,27 +217,17 @@ function vehicleParams(args: NhtsaVehicleArgs): URLSearchParams {
|
|
|
142
217
|
}
|
|
143
218
|
|
|
144
219
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* error
|
|
220
|
+
* Build the vehicle query and fetch through getNhtsa, which fully classifies the
|
|
221
|
+
* response: the ★400-for-empty idiom → an honest empty; every other non-2xx → the
|
|
222
|
+
* standard taxonomy; a 200 non-JSON body → schema_drift. A thin wrapper — getNhtsa
|
|
223
|
+
* owns the error contract.
|
|
149
224
|
*/
|
|
150
225
|
async function fetchNhtsa(
|
|
151
226
|
path: string,
|
|
152
227
|
label: string,
|
|
153
228
|
args: NhtsaVehicleArgs,
|
|
154
229
|
): Promise<unknown> {
|
|
155
|
-
|
|
156
|
-
return await getNhtsa(path, label, vehicleParams(args));
|
|
157
|
-
} catch (e) {
|
|
158
|
-
if (e instanceof ToolErrorCarrier) throw e;
|
|
159
|
-
if (e instanceof SyntaxError)
|
|
160
|
-
throw driftError(
|
|
161
|
-
label,
|
|
162
|
-
`NHTSA ${label} returned a non-JSON body at HTTP 200 — schema drift (never read as an empty result).`,
|
|
163
|
-
);
|
|
164
|
-
throw e;
|
|
165
|
-
}
|
|
230
|
+
return await getNhtsa(path, label, vehicleParams(args));
|
|
166
231
|
}
|
|
167
232
|
|
|
168
233
|
/**
|
package/src/nist-controls.ts
CHANGED
|
@@ -46,8 +46,10 @@ export type NistControl = {
|
|
|
46
46
|
id: string; // display form, e.g. "AC-2"
|
|
47
47
|
family: string; // e.g. "AC — Access Control"
|
|
48
48
|
title: string;
|
|
49
|
-
|
|
49
|
+
status: string | null; // OSCAL status prop, e.g. "withdrawn"; null when active
|
|
50
|
+
statement: string | null; // requirement prose; NULL when absent (a WITHDRAWN control has none) — never ""
|
|
50
51
|
guidance: string | null; // discussion prose
|
|
52
|
+
incorporatedInto: string[]; // withdrawn control ⇒ the control id(s) it was folded into (e.g. ["AC-2","AU-6"])
|
|
51
53
|
enhancements: { id: string; title: string }[]; // e.g. AC-2(1)
|
|
52
54
|
};
|
|
53
55
|
|
|
@@ -60,6 +62,8 @@ type OscalPart = {
|
|
|
60
62
|
type OscalControl = {
|
|
61
63
|
id?: string;
|
|
62
64
|
title?: string;
|
|
65
|
+
props?: { name?: string; value?: string }[];
|
|
66
|
+
links?: { href?: string; rel?: string }[];
|
|
63
67
|
parts?: OscalPart[];
|
|
64
68
|
controls?: OscalControl[];
|
|
65
69
|
};
|
|
@@ -100,12 +104,26 @@ function partProse(control: OscalControl, name: string): string | null {
|
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
function mapControl(control: OscalControl, family: string): NistControl {
|
|
107
|
+
const status =
|
|
108
|
+
(control.props ?? []).find((p) => p.name === "status")?.value?.trim() || null;
|
|
109
|
+
// A withdrawn control carries `links[] rel="incorporated-into"` → the control(s)
|
|
110
|
+
// it was folded into (e.g. AC-13 → AC-2, AU-6). Surface them so a withdrawn
|
|
111
|
+
// control is never mistaken for an active requirement.
|
|
112
|
+
const incorporatedInto = (control.links ?? [])
|
|
113
|
+
.filter((l) => l.rel === "incorporated-into" && typeof l.href === "string")
|
|
114
|
+
.map((l) => displayId(String(l.href).replace(/^#/, "")))
|
|
115
|
+
.filter((x) => x.length > 0);
|
|
103
116
|
return {
|
|
104
117
|
id: displayId(control.id ?? ""),
|
|
105
118
|
family,
|
|
106
119
|
title: (control.title ?? "").trim(),
|
|
107
|
-
|
|
120
|
+
status,
|
|
121
|
+
// [P3] absent statement ⇒ null, NEVER "" — a withdrawn control has no
|
|
122
|
+
// requirement text; "" would misread as "an active control with a blank
|
|
123
|
+
// requirement".
|
|
124
|
+
statement: partProse(control, "statement"),
|
|
108
125
|
guidance: partProse(control, "guidance"),
|
|
126
|
+
incorporatedInto,
|
|
109
127
|
enhancements: (control.controls ?? []).map((e) => ({
|
|
110
128
|
id: displayId(e.id ?? ""),
|
|
111
129
|
title: (e.title ?? "").trim(),
|
|
@@ -119,7 +137,12 @@ function mapControl(control: OscalControl, family: string): NistControl {
|
|
|
119
137
|
* families (else driftError — a truncated catalog is NEVER a fake empty). Enhancements
|
|
120
138
|
* are carried on each control (not indexed as top-level entries).
|
|
121
139
|
*/
|
|
122
|
-
|
|
140
|
+
type OscalMetadata = { version: string | null; lastModified: string | null };
|
|
141
|
+
|
|
142
|
+
async function loadControls(): Promise<{
|
|
143
|
+
controls: NistControl[];
|
|
144
|
+
metadata: OscalMetadata;
|
|
145
|
+
}> {
|
|
123
146
|
return memoize(
|
|
124
147
|
"nist:sp800-53r5",
|
|
125
148
|
async () => {
|
|
@@ -131,7 +154,12 @@ async function loadControls(): Promise<NistControl[]> {
|
|
|
131
154
|
label: OSCAL_LABEL,
|
|
132
155
|
redirect: "error",
|
|
133
156
|
timeoutMs: OSCAL_TIMEOUT_MS,
|
|
134
|
-
})) as {
|
|
157
|
+
})) as {
|
|
158
|
+
catalog?: {
|
|
159
|
+
groups?: unknown;
|
|
160
|
+
metadata?: { version?: unknown; "last-modified"?: unknown };
|
|
161
|
+
};
|
|
162
|
+
};
|
|
135
163
|
const groups = body.catalog?.groups;
|
|
136
164
|
if (!Array.isArray(groups) || groups.length < FAMILY_FLOOR) {
|
|
137
165
|
throw driftError(
|
|
@@ -139,12 +167,21 @@ async function loadControls(): Promise<NistControl[]> {
|
|
|
139
167
|
`OSCAL catalog.groups missing or implausibly small (${Array.isArray(groups) ? groups.length : "not-an-array"} < ${FAMILY_FLOOR} families) — treating as schema drift / truncation, never a fake-empty catalog.`,
|
|
140
168
|
);
|
|
141
169
|
}
|
|
170
|
+
// [P5 freshness] The OSCAL catalog is a moving static file on the `main`
|
|
171
|
+
// branch — surface its exact version + last-modified so a compliance caller
|
|
172
|
+
// knows WHICH point-release (5.1.1 / 5.2.0 …) they are reading.
|
|
173
|
+
const md = body.catalog?.metadata ?? {};
|
|
174
|
+
const metadata: OscalMetadata = {
|
|
175
|
+
version: typeof md.version === "string" ? md.version : null,
|
|
176
|
+
lastModified:
|
|
177
|
+
typeof md["last-modified"] === "string" ? md["last-modified"] : null,
|
|
178
|
+
};
|
|
142
179
|
const controls: NistControl[] = [];
|
|
143
180
|
for (const g of groups as OscalGroup[]) {
|
|
144
181
|
const family = `${(g.id ?? "").toUpperCase()} — ${(g.title ?? "").trim()}`;
|
|
145
182
|
for (const c of g.controls ?? []) controls.push(mapControl(c, family));
|
|
146
183
|
}
|
|
147
|
-
return controls;
|
|
184
|
+
return { controls, metadata };
|
|
148
185
|
},
|
|
149
186
|
OSCAL_CACHE_TTL_MS,
|
|
150
187
|
);
|
|
@@ -166,7 +203,7 @@ export async function searchControls(args: {
|
|
|
166
203
|
}): Promise<MetaBundle> {
|
|
167
204
|
const limit = args.limit ?? 25;
|
|
168
205
|
const offset = args.offset ?? 0;
|
|
169
|
-
const all = await loadControls();
|
|
206
|
+
const { controls: all, metadata } = await loadControls();
|
|
170
207
|
|
|
171
208
|
const filtersApplied: string[] = [];
|
|
172
209
|
const idQ = args.controlId !== undefined ? displayId(args.controlId) : undefined;
|
|
@@ -189,7 +226,7 @@ export async function searchControls(args: {
|
|
|
189
226
|
// Search the title + requirement statement AND each enhancement's title, so a
|
|
190
227
|
// term that lives only in an enhancement (e.g. "multi-factor" → IA-2(1)) still
|
|
191
228
|
// surfaces the parent control. filtersApplied still lists 'keyword'.
|
|
192
|
-
const hay = `${c.title}\n${c.statement}\n${c.enhancements.map((e) => e.title).join("\n")}`.toLowerCase();
|
|
229
|
+
const hay = `${c.title}\n${c.statement ?? ""}\n${c.enhancements.map((e) => e.title).join("\n")}`.toLowerCase();
|
|
193
230
|
if (!hay.includes(kwQ)) return false;
|
|
194
231
|
}
|
|
195
232
|
return true;
|
|
@@ -201,10 +238,22 @@ export async function searchControls(args: {
|
|
|
201
238
|
const hasMore = offset + returned < totalAvailable;
|
|
202
239
|
const nextOffset = hasMore ? offset + returned : null;
|
|
203
240
|
|
|
241
|
+
const versionLabel = metadata.version ? `Rev ${metadata.version}` : "Rev 5";
|
|
242
|
+
const notes: string[] = [PROVENANCE_NOTE, CLIENT_FILTER_NOTE, REFERENCE_NOTE];
|
|
243
|
+
notes.push(
|
|
244
|
+
`OSCAL catalog ${metadata.version ? `version ${metadata.version}` : "revision 5"}${metadata.lastModified ? `, last-modified ${metadata.lastModified.slice(0, 10)}` : ""} — fetched live from the usnistgov/oscal-content \`main\` branch (a MOVING target; control text can change between point releases, e.g. 5.1.1 → 5.2.0). Cite the version above, not just "Rev 5".`,
|
|
245
|
+
);
|
|
246
|
+
const withdrawnOnPage = page.filter((c) => c.status === "withdrawn").length;
|
|
247
|
+
if (withdrawnOnPage > 0) {
|
|
248
|
+
notes.push(
|
|
249
|
+
`${withdrawnOnPage} of the returned control(s) are WITHDRAWN (status:"withdrawn") — a withdrawn control has NO requirement text (statement:null) and is NOT an active requirement; see its incorporatedInto for the control(s) that superseded it.`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
204
253
|
return withMeta(
|
|
205
254
|
{ controls: page },
|
|
206
255
|
{
|
|
207
|
-
source:
|
|
256
|
+
source: `NIST SP 800-53 ${versionLabel} (OSCAL catalog, keyless)`,
|
|
208
257
|
keylessMode: true,
|
|
209
258
|
returned,
|
|
210
259
|
totalAvailable,
|
|
@@ -213,7 +262,7 @@ export async function searchControls(args: {
|
|
|
213
262
|
filtersDropped: [],
|
|
214
263
|
fieldsUnavailable: [],
|
|
215
264
|
pagination: { offset, limit, hasMore, nextOffset },
|
|
216
|
-
notes
|
|
265
|
+
notes,
|
|
217
266
|
} satisfies Partial<ResponseMeta>,
|
|
218
267
|
);
|
|
219
268
|
}
|
package/src/ofac.ts
CHANGED
|
@@ -206,7 +206,18 @@ export function classifyMatch(
|
|
|
206
206
|
const allQinE = qt.length > 0 && qt.every((t) => eSet.has(t));
|
|
207
207
|
const allEinQ = et.length > 0 && et.every((t) => qSet.has(t));
|
|
208
208
|
if (allQinE || allEinQ) return "strong";
|
|
209
|
-
|
|
209
|
+
// Substring containment is "strong" ONLY when the CONTAINED (shorter) string is
|
|
210
|
+
// a substantial fragment. Without a floor, a tiny sub-word substring graded
|
|
211
|
+
// "strong" ("TS" ⊂ "WIDGETS", "IBB" ⊂ "QUIBBLEFARB", "D" ⊂ "WIDGETS") —
|
|
212
|
+
// overstating confidence and flooding nearly every input with false "strong"
|
|
213
|
+
// matches (alert fatigue). Whole-token containment is already handled above by
|
|
214
|
+
// allQinE/allEinQ; this mid-word tier is for the space-stripped joined form and
|
|
215
|
+
// must clear the same ≥3 bar the token "weak" tier uses — here a slightly higher
|
|
216
|
+
// floor since a contiguous substring is a weaker signal than a shared whole token.
|
|
217
|
+
if (
|
|
218
|
+
Math.min(queryNorm.length, entryNorm.length) >= 5 &&
|
|
219
|
+
(entryNorm.includes(queryNorm) || queryNorm.includes(entryNorm))
|
|
220
|
+
) {
|
|
210
221
|
return "strong";
|
|
211
222
|
}
|
|
212
223
|
for (const t of qt) {
|
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
|
@@ -106,7 +106,7 @@ import { realpathSync } from "node:fs";
|
|
|
106
106
|
const SERVER_NAME = "mcp-sam-gov";
|
|
107
107
|
// Kept in lockstep with package.json / manifest.json / server.json.
|
|
108
108
|
// Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
|
|
109
|
-
const SERVER_VERSION = "1.
|
|
109
|
+
const SERVER_VERSION = "1.11.0";
|
|
110
110
|
|
|
111
111
|
// ─── Tool input schemas (Zod) ────────────────────────────────────
|
|
112
112
|
|
|
@@ -4418,7 +4418,7 @@ const GsaPerdiemRatesInput = z
|
|
|
4418
4418
|
.regex(/^\d{4}$/)
|
|
4419
4419
|
.optional()
|
|
4420
4420
|
.describe(
|
|
4421
|
-
"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).",
|
|
4422
4422
|
),
|
|
4423
4423
|
})
|
|
4424
4424
|
.describe(
|
|
@@ -4543,7 +4543,7 @@ const LdaSearchFilingsInput = z.object({
|
|
|
4543
4543
|
.string()
|
|
4544
4544
|
.min(1)
|
|
4545
4545
|
.optional()
|
|
4546
|
-
.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."),
|
|
4547
4547
|
issue: z
|
|
4548
4548
|
.string()
|
|
4549
4549
|
.min(1)
|
|
@@ -4895,6 +4895,21 @@ export const TOOLS: ToolDef[] = [
|
|
|
4895
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.",
|
|
4896
4896
|
);
|
|
4897
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
|
+
}
|
|
4898
4913
|
notes.push(...enrichmentNotes);
|
|
4899
4914
|
|
|
4900
4915
|
// freshness is surfaced structurally in `data` (the ResponseMeta type
|
|
@@ -5654,7 +5669,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
5654
5669
|
defineTool({
|
|
5655
5670
|
name: "nist_800_53_controls",
|
|
5656
5671
|
description:
|
|
5657
|
-
"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').",
|
|
5658
5673
|
inputSchema: NistControlsInput,
|
|
5659
5674
|
handler: (input) => nistControls.searchControls(input),
|
|
5660
5675
|
}),
|
|
@@ -6446,7 +6461,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
6446
6461
|
defineTool({
|
|
6447
6462
|
name: "cbp_border_wait_times",
|
|
6448
6463
|
description:
|
|
6449
|
-
"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).",
|
|
6450
6465
|
inputSchema: CbpBorderWaitInput,
|
|
6451
6466
|
handler: (input) => cbpBorder.borderWaitTimes(input),
|
|
6452
6467
|
}),
|
|
@@ -6475,7 +6490,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
6475
6490
|
defineTool({
|
|
6476
6491
|
name: "gsa_perdiem_rates",
|
|
6477
6492
|
description:
|
|
6478
|
-
"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).",
|
|
6479
6494
|
inputSchema: GsaPerdiemRatesInput,
|
|
6480
6495
|
handler: (input) => gsaPerdiem.perdiemRates(input),
|
|
6481
6496
|
}),
|
|
@@ -6510,7 +6525,7 @@ export const TOOLS: ToolDef[] = [
|
|
|
6510
6525
|
defineTool({
|
|
6511
6526
|
name: "lda_search_filings",
|
|
6512
6527
|
description:
|
|
6513
|
-
"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).",
|
|
6514
6529
|
inputSchema: LdaSearchFilingsInput,
|
|
6515
6530
|
handler: (input) => lda.searchFilings(input),
|
|
6516
6531
|
}),
|
|
@@ -6796,7 +6811,7 @@ export async function runTool(
|
|
|
6796
6811
|
/**
|
|
6797
6812
|
* Hand-rolled Zod → JSON Schema converter (subset we use).
|
|
6798
6813
|
*/
|
|
6799
|
-
function zodToJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> {
|
|
6814
|
+
export function zodToJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> {
|
|
6800
6815
|
const def = (schema as unknown as { _def: { typeName: string } })._def;
|
|
6801
6816
|
const tn = def.typeName;
|
|
6802
6817
|
const description = (schema as unknown as { description?: string }).description;
|
|
@@ -6848,6 +6863,19 @@ function zodToJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> {
|
|
|
6848
6863
|
const innerSchema = zodToJsonSchema(inner);
|
|
6849
6864
|
return description ? { ...innerSchema, description } : innerSchema;
|
|
6850
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
|
+
}
|
|
6851
6879
|
return { type: "string", ...(description ? { description } : {}) };
|
|
6852
6880
|
}
|
|
6853
6881
|
|
package/src/socrata.ts
CHANGED
|
@@ -194,6 +194,36 @@ export const SOCRATA_DOMAINS = [
|
|
|
194
194
|
"data.lacity.org", // Los Angeles CA (.org, official) — e.g. hf3r-utnq (RAMP Open Bid Opportunities — live bids)
|
|
195
195
|
"data.ramseycountymn.gov", // Ramsey County MN (.gov) — e.g. iu7r-dzmj (Solicitations & Addenda, with due_date/download_url ~516)
|
|
196
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)
|
|
197
227
|
] as const;
|
|
198
228
|
|
|
199
229
|
export type SocrataDomain = (typeof SOCRATA_DOMAINS)[number];
|