@cliwant/mcp-sam-gov 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,244 @@
1
+ /**
2
+ * opengov.ts — OpenGov Procurement (formerly ProcureNow) public solicitations,
3
+ * a keyless-first SLED bid source (loop cycle — SLED bid campaign, 2026-07-19).
4
+ *
5
+ * WHAT IT ADDS: OpenGov Procurement hosts the live open-solicitation portals of
6
+ * **525+ US state/local governments** (cities, counties, school & special
7
+ * districts across 42 states + DC). The public portal SPA
8
+ * (`procurement.opengov.com/portal/{code}`) is served by an UNAUTHENTICATED
9
+ * backend REST API — the official key-gated `api-key` API is NOT used; this
10
+ * consumes ONLY the anonymous endpoints the public portal itself calls, so it is
11
+ * genuinely keyless (live-verified 2026-07-19: bare anonymous request, no api-key /
12
+ * auth header / cookie). It is the OpenGov sibling of the Socrata/CKAN/ArcGIS
13
+ * discovery tools and one of the two highest-reach keyless SLED bid feeds (with
14
+ * Bonfire RSS).
15
+ *
16
+ * The module writes ZERO fetch/coercion/error/meta code — it REUSES `getJson`
17
+ * (GET + POST/JSON via ADR-0014, redirect:"error") / `driftError` / `num`·`str`
18
+ * (coerce.ts, null-never-0 / null-never-empty) / `withMeta`·`buildMeta`,
19
+ * mirroring datagov-catalog.ts. KEYLESS: no key seam.
20
+ *
21
+ * ★ SSRF: the host is a compile-time literal (`OPENGOV_HOST`). All inputs ride in
22
+ * a MODULE-BUILT JSON body / URLSearchParams from validated typed args — NO
23
+ * raw-host/raw-path passthrough (this is a FIXED-host API). A post-construction
24
+ * hostname/protocol assertion + `redirect:"error"` lock every call.
25
+ *
26
+ * ★ TWO endpoints (both anonymous, live-verified):
27
+ * - `GET /api/v1/government` → a flat JSON array of ~560 org records (the whole
28
+ * directory in one call; `government.code` = the portal slug). We keep the
29
+ * ACTIVE, non-internal ones (~525 real public portals).
30
+ * - `POST /api/v1/project/list` body `{governmentCode, publicView:true, page,
31
+ * limit}` → `{projects:[…], count}`. `publicView:true` is REQUIRED (the public
32
+ * gate). `count` is the TRUE total of public projects for that org.
33
+ *
34
+ * ★ HONESTY PILLARS (captured live 2026-07-19):
35
+ * P1: totalAvailable = the real total — the filtered directory length (govt
36
+ * list) or the API's `count` (solicitations) — NEVER the page length.
37
+ * P2: getJson→fetchWithRetry THROWS on 429 / 5xx / timeout — NEVER a fake empty.
38
+ * A genuine no-match (projects:[], count:0) ⇒ honest empty.
39
+ * P3: every scalar via `str` (null-never-empty) / `num` (null-never-0). The
40
+ * `status` enum is surfaced VERBATIM (open = currently accepting; the response
41
+ * also carries pending/evaluation/closed — the CONSUMER filters; we do not
42
+ * hide non-open rows or fabricate an "open count").
43
+ * P4: `/government` non-array or `/project/list` `.projects` non-array ⇒
44
+ * driftError; a 200 non-JSON body ⇒ schema_drift via the catch-ladder
45
+ * (ToolErrorCarrier rethrow FIRST so a 429/5xx keeps its taxonomy).
46
+ */
47
+ import { ToolErrorCarrier } from "./errors.js";
48
+ import { getJson, driftError } from "./datasource.js";
49
+ import { num, str } from "./coerce.js";
50
+ import { withMeta } from "./meta.js";
51
+ // Re-export the shared honesty coercion (num-parity guard).
52
+ export { num };
53
+ // ─── Fixed endpoint (SSRF core — compile-time CONSTANTS) ──────────
54
+ export const OPENGOV_HOST = "api.procurement.opengov.com";
55
+ const OPENGOV_GOVERNMENT_PATH = "/api/v1/government";
56
+ const OPENGOV_PROJECT_LIST_PATH = "/api/v1/project/list";
57
+ const OPENGOV_GOV_LABEL = "opengov:/api/v1/government";
58
+ const OPENGOV_PROJ_LABEL = "opengov:/api/v1/project/list";
59
+ const OPENGOV_SOURCE = "procurement.opengov.com via OpenGov Procurement public API (keyless)";
60
+ const OPENGOV_PORTAL = "https://procurement.opengov.com/portal";
61
+ // The org slug grammar (SSRF + injection guard). Real slugs are lowercase
62
+ // alnum + hyphen (e.g. "santacruzca", "u-46", "twc-texas-gov"). Validated
63
+ // BEFORE it rides the JSON body — a "../"/space/uppercase ⇒ invalid_input, 0 fetch.
64
+ export const OPENGOV_CODE_RE = /^[a-z0-9-]{1,64}$/;
65
+ const OPENGOV_STATUS_NOTE = "`status` is surfaced verbatim: open = currently ACCEPTING responses; pending/evaluation/closed are also returned (publicView shows all public projects). Filter status==='open' for live bids. totalAvailable is the org's TOTAL public-project count (all statuses), not an open-only count.";
66
+ /** null-preserving boolean (a real true/false survives; else null). */
67
+ function boolOrNull(x) {
68
+ return typeof x === "boolean" ? x : null;
69
+ }
70
+ // ─── SSRF-guarded fetch (fixed host + hostname assertion + redirect) ──
71
+ async function getOpengov(path, label, init = {}) {
72
+ const url = `https://${OPENGOV_HOST}${path}`;
73
+ const built = new URL(url);
74
+ if (built.hostname !== OPENGOV_HOST || built.protocol !== "https:") {
75
+ throw new ToolErrorCarrier({
76
+ kind: "invalid_input",
77
+ message: `Constructed OpenGov URL host ${JSON.stringify(built.hostname)} (${built.protocol}) does not match the fixed host ${JSON.stringify(OPENGOV_HOST)} over https — refusing to fetch (SSRF safety).`,
78
+ retryable: false,
79
+ upstreamEndpoint: label,
80
+ });
81
+ }
82
+ const headers = init.method === "POST" ? { "Content-Type": "application/json" } : {};
83
+ return getJson(url, { label, headers, redirect: "error", ...init });
84
+ }
85
+ /**
86
+ * List the OpenGov Procurement government portals (the whole directory arrives in
87
+ * ONE keyless GET). Client-side filters: `state` (2-letter), `query` (name
88
+ * substring). Only ACTIVE, non-internal portals are kept. Feeds `code` to
89
+ * opengov_search_solicitations. totalAvailable = the filtered directory size (P1).
90
+ */
91
+ export async function listGovernments(args) {
92
+ const limit = args.limit ?? 50;
93
+ const offset = args.offset ?? 0;
94
+ let body;
95
+ try {
96
+ body = await getOpengov(OPENGOV_GOVERNMENT_PATH, OPENGOV_GOV_LABEL);
97
+ }
98
+ catch (e) {
99
+ if (e instanceof ToolErrorCarrier)
100
+ throw e;
101
+ if (e instanceof SyntaxError)
102
+ throw driftError(OPENGOV_GOV_LABEL, "OpenGov /government returned a non-JSON body at HTTP 200 — schema drift.");
103
+ throw e;
104
+ }
105
+ // P4: the response MUST be an array (a missing/object body is drift).
106
+ if (!Array.isArray(body)) {
107
+ throw driftError(OPENGOV_GOV_LABEL, "OpenGov /government shape drift — response must be a JSON array of governments.");
108
+ }
109
+ const stateFilter = args.state ? args.state.trim().toUpperCase() : null;
110
+ const queryFilter = args.query ? args.query.trim().toLowerCase() : null;
111
+ const filtersApplied = [];
112
+ if (stateFilter)
113
+ filtersApplied.push("state");
114
+ if (queryFilter)
115
+ filtersApplied.push("query");
116
+ const all = [];
117
+ for (const row of body) {
118
+ const r = (row ?? {});
119
+ // Keep only active, non-internal, non-vendor real public portals.
120
+ if (boolOrNull(r.isActive) === false)
121
+ continue;
122
+ if (boolOrNull(r.isInternal) === true)
123
+ continue;
124
+ const gov = (r.government ?? {});
125
+ const g = {
126
+ code: str(gov.code),
127
+ name: str(r.name),
128
+ city: str(r.city),
129
+ state: str(r.state),
130
+ website: str(r.website),
131
+ };
132
+ if (g.code === null)
133
+ continue; // no portal slug ⇒ not queryable
134
+ if (stateFilter && (g.state ?? "").toUpperCase() !== stateFilter)
135
+ continue;
136
+ if (queryFilter && !(g.name ?? "").toLowerCase().includes(queryFilter))
137
+ continue;
138
+ all.push(g);
139
+ }
140
+ const totalAvailable = all.length;
141
+ const page = all.slice(offset, offset + limit);
142
+ const returned = page.length;
143
+ const hasMore = offset + returned < totalAvailable;
144
+ const nextOffset = hasMore ? offset + returned : null;
145
+ const notes = [
146
+ "The full OpenGov Procurement directory is returned in one keyless call and filtered client-side; totalAvailable is the exact filtered portal count. Feed a result's `code` to opengov_search_solicitations.",
147
+ ];
148
+ if (filtersApplied.length === 0) {
149
+ notes.push("No filters applied — this lists ALL active OpenGov portals. Add `state` and/or `query` to scope.");
150
+ }
151
+ return withMeta({ governments: page }, {
152
+ source: OPENGOV_SOURCE,
153
+ keylessMode: true,
154
+ returned,
155
+ totalAvailable,
156
+ filtersApplied,
157
+ filtersDropped: [],
158
+ fieldsUnavailable: [],
159
+ pagination: { offset, limit, hasMore, nextOffset },
160
+ notes,
161
+ });
162
+ }
163
+ /**
164
+ * List an OpenGov government's public solicitations (POST /project/list with the
165
+ * REQUIRED publicView gate). Returns all public projects with `status` surfaced
166
+ * (filter status==='open' for live bids). totalAvailable = the API's `count` (the
167
+ * org's total public-project count — P1, never the page length).
168
+ */
169
+ export async function searchSolicitations(args) {
170
+ const limit = args.limit ?? 50;
171
+ const offset = args.offset ?? 0;
172
+ const code = args.governmentCode ?? "";
173
+ // Belt-and-suspenders slug grammar (behind the server's Zod). A bad slug would
174
+ // ride the JSON body and either error or mis-scope — reject pre-fetch (0 fetch).
175
+ if (!OPENGOV_CODE_RE.test(code)) {
176
+ throw new ToolErrorCarrier({
177
+ kind: "invalid_input",
178
+ message: `Invalid OpenGov governmentCode ${JSON.stringify(code)} — expected a lowercase-alnum/hyphen portal slug (≤64 chars) from opengov_list_governments (e.g. 'santacruzca', 'u-46').`,
179
+ retryable: false,
180
+ upstreamEndpoint: OPENGOV_PROJ_LABEL,
181
+ });
182
+ }
183
+ // The API paginates by 0-based `page` + `limit`. Map our offset → page.
184
+ const page = Math.floor(offset / limit);
185
+ const servedOffset = page * limit;
186
+ const payload = JSON.stringify({ governmentCode: code, publicView: true, page, limit });
187
+ let body;
188
+ try {
189
+ body = await getOpengov(OPENGOV_PROJECT_LIST_PATH, OPENGOV_PROJ_LABEL, { method: "POST", body: payload });
190
+ }
191
+ catch (e) {
192
+ if (e instanceof ToolErrorCarrier)
193
+ throw e;
194
+ if (e instanceof SyntaxError)
195
+ throw driftError(OPENGOV_PROJ_LABEL, "OpenGov /project/list returned a non-JSON body at HTTP 200 — schema drift.");
196
+ throw e;
197
+ }
198
+ const b = (body ?? {});
199
+ // P4: `projects` MUST be an array (a missing/object projects is drift).
200
+ if (!Array.isArray(b.projects)) {
201
+ throw driftError(OPENGOV_PROJ_LABEL, "OpenGov /project/list shape drift — response.projects must be an array.");
202
+ }
203
+ const solicitations = b.projects.map((row) => {
204
+ const r = (row ?? {});
205
+ const id = num(r.id);
206
+ const codeForLink = code;
207
+ return {
208
+ id,
209
+ title: str(r.title),
210
+ solicitationNumber: str(r.financialId),
211
+ status: str(r.status),
212
+ type: str(r.type),
213
+ department: str(r.departmentName),
214
+ releaseDate: str(r.releaseProjectDate ?? r.postedAt),
215
+ proposalDeadline: str(r.proposalDeadline),
216
+ contactName: [str(r.contactFirstName), str(r.contactLastName)].filter((v) => v !== null).join(" ") || null,
217
+ link: id !== null ? `${OPENGOV_PORTAL}/${codeForLink}/projects/${id}` : null,
218
+ };
219
+ });
220
+ const returned = solicitations.length;
221
+ // P1: totalAvailable = the API `count` (total public projects), NEVER page length.
222
+ const totalAvailable = num(b.count);
223
+ const hasMore = totalAvailable !== null ? servedOffset + returned < totalAvailable : returned >= limit;
224
+ const nextOffset = hasMore ? servedOffset + returned : null;
225
+ const notes = [OPENGOV_STATUS_NOTE];
226
+ if (servedOffset !== offset) {
227
+ notes.push(`offset was snapped to the page boundary ${servedOffset} (OpenGov paginates by fixed pages of ${limit}).`);
228
+ }
229
+ if (totalAvailable === null) {
230
+ notes.push("totalAvailable is unknown (the API omitted `count`); completeness inferred from page fullness.");
231
+ }
232
+ return withMeta({ governmentCode: code, solicitations }, {
233
+ source: OPENGOV_SOURCE,
234
+ keylessMode: true,
235
+ returned,
236
+ totalAvailable,
237
+ filtersApplied: ["governmentCode", "publicView"],
238
+ filtersDropped: [],
239
+ fieldsUnavailable: [],
240
+ pagination: { offset: servedOffset, limit, hasMore, nextOffset },
241
+ notes,
242
+ });
243
+ }
244
+ //# sourceMappingURL=opengov.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opengov.js","sourceRoot":"","sources":["../src/opengov.ts"],"names":[],"mappings":"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"}
package/dist/pricing.d.ts CHANGED
@@ -73,7 +73,7 @@ export declare function benchmarkLaborRates(args: {
73
73
  maxSamplePages?: number;
74
74
  }): Promise<import("./meta.js").MetaBundle<{
75
75
  laborCategory: string;
76
- matcher: "search" | "q";
76
+ matcher: "q" | "search";
77
77
  fuzzy: boolean;
78
78
  matchCount: number;
79
79
  matchCountSaturated: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;GAgBG;AAQH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,YAAY,EAKb,MAAM,oBAAoB,CAAC;AA67I5B,KAAK,OAAO,GAAG;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,CAAC,CAAC,UAAU,CAAC;IAO1B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE;QAAE,GAAG,EAAE,YAAY,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxE,CAAC;AAoBF,eAAO,MAAM,KAAK,EAAE,OAAO,EAowD1B,CAAC;AAkLF,wBAAsB,OAAO,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,YAAY,GAChB,OAAO,CAAC,OAAO,CAAC,CAkClB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;GAgBG;AAQH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,YAAY,EAKb,MAAM,oBAAoB,CAAC;AA4iJ5B,KAAK,OAAO,GAAG;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,CAAC,CAAC,UAAU,CAAC;IAO1B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE;QAAE,GAAG,EAAE,YAAY,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxE,CAAC;AAoBF,eAAO,MAAM,KAAK,EAAE,OAAO,EAs0D1B,CAAC;AAkLF,wBAAsB,OAAO,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,YAAY,GAChB,OAAO,CAAC,OAAO,CAAC,CAkClB"}
package/dist/server.js CHANGED
@@ -38,6 +38,10 @@ import * as ckan from "./ckan.js";
38
38
  import * as echo from "./echo.js";
39
39
  import * as datagov from "./datagov.js";
40
40
  import * as datagovCatalog from "./datagov-catalog.js";
41
+ import * as arcgisHub from "./arcgis-hub.js";
42
+ import * as opengov from "./opengov.js";
43
+ import * as bonfire from "./bonfire.js";
44
+ import * as arcgisFeature from "./arcgis-feature.js";
41
45
  import * as govinfo from "./govinfo.js";
42
46
  import * as fpds from "./fpds.js";
43
47
  import * as nih from "./nih.js";
@@ -86,7 +90,7 @@ import { realpathSync } from "node:fs";
86
90
  const SERVER_NAME = "mcp-sam-gov";
87
91
  // Kept in lockstep with package.json / manifest.json / server.json.
88
92
  // Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
89
- const SERVER_VERSION = "1.9.0";
93
+ const SERVER_VERSION = "1.10.0";
90
94
  // ─── Tool input schemas (Zod) ────────────────────────────────────
91
95
  const SamSearchInput = z.object({
92
96
  query: z.string().optional().describe("Free-text title query"),
@@ -2508,6 +2512,107 @@ const DatagovSearchDatasetsInput = z.object({
2508
2512
  .optional()
2509
2513
  .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."),
2510
2514
  });
2515
+ // ─── ArcGIS Hub (hub.arcgis.com — keyless SLED dataset discovery) ─
2516
+ // Loop cycle 9. A GLOBAL, OPEN publishing platform (non-US / non-gov publishers
2517
+ // included) — DISCOVERY ONLY, provenance surfaced per-row + disclosed every
2518
+ // response. Fixed-host SSRF (no per-jurisdiction host vector). q ≥ 2 chars.
2519
+ const ArcgisHubDiscoverInput = z.object({
2520
+ query: z
2521
+ .string()
2522
+ .min(2)
2523
+ .max(500)
2524
+ .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)."),
2525
+ openDataOnly: z
2526
+ .boolean()
2527
+ .default(true)
2528
+ .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)."),
2529
+ limit: z
2530
+ .number()
2531
+ .int()
2532
+ .min(1)
2533
+ .max(100)
2534
+ .default(20)
2535
+ .describe("Datasets per page (→ page[size]), 1..100, default 20."),
2536
+ offset: z
2537
+ .number()
2538
+ .int()
2539
+ .min(0)
2540
+ .default(0)
2541
+ .describe("0-based record offset (→ page[start]=offset+1). Page with _meta.pagination.nextOffset; totalAvailable is the exact Hub match count."),
2542
+ });
2543
+ // ─── OpenGov Procurement (api.procurement.opengov.com — keyless SLED bids) ─
2544
+ // SLED bid campaign. 525+ US state/local portals' live solicitations via the
2545
+ // public portal's own anonymous backend API (the key-gated api-key API is NOT
2546
+ // used). Fixed-host SSRF. governmentCode = the portal slug from list_governments.
2547
+ const OpengovListGovernmentsInput = z.object({
2548
+ state: z
2549
+ .string()
2550
+ .length(2)
2551
+ .optional()
2552
+ .describe("2-letter US state filter (client-side), e.g. 'CA', 'FL'. Optional."),
2553
+ query: z
2554
+ .string()
2555
+ .min(1)
2556
+ .max(120)
2557
+ .optional()
2558
+ .describe("Case-insensitive name substring filter (client-side), e.g. 'county', 'school'. Optional."),
2559
+ limit: z.number().int().min(1).max(200).default(50).describe("Portals per page, 1..200, default 50."),
2560
+ offset: z.number().int().min(0).default(0).describe("0-based offset; page with _meta.pagination.nextOffset. totalAvailable = exact filtered portal count."),
2561
+ });
2562
+ const OpengovSearchSolicitationsInput = z.object({
2563
+ governmentCode: z
2564
+ .string()
2565
+ .min(1)
2566
+ .max(64)
2567
+ .regex(opengov.OPENGOV_CODE_RE)
2568
+ .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."),
2569
+ limit: z.number().int().min(1).max(100).default(50).describe("Solicitations per page, 1..100, default 50 (→ API page size)."),
2570
+ 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."),
2571
+ });
2572
+ // ─── Bonfire (Euna) — keyless per-org open-opportunity RSS (SLED bids) ─
2573
+ // SLED bid campaign. Thousands of US state/local govs on Bonfire expose a keyless
2574
+ // RSS of open opportunities at {org}.bonfirehub.com/opportunities/rss. Ships a
2575
+ // curated 187-org seed directory. Fixed-suffix SSRF. org = charclass slug.
2576
+ const BonfireListOrganizationsInput = z.object({
2577
+ state: z.string().length(2).optional().describe("2-letter US state filter (client-side), e.g. 'TX', 'CA'. Optional."),
2578
+ query: z.string().min(1).max(120).optional().describe("Case-insensitive name substring filter (client-side), e.g. 'county', 'ISD'. Optional."),
2579
+ limit: z.number().int().min(1).max(200).default(50).describe("Orgs per page, 1..200, default 50."),
2580
+ offset: z.number().int().min(0).default(0).describe("0-based offset; page with _meta.pagination.nextOffset."),
2581
+ });
2582
+ const BonfireSearchOpportunitiesInput = z.object({
2583
+ org: z
2584
+ .string()
2585
+ .min(1)
2586
+ .max(64)
2587
+ .regex(bonfire.BONFIRE_ORG_RE)
2588
+ .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."),
2589
+ 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."),
2590
+ offset: z.number().int().min(0).default(0).describe("0-based offset; page with _meta.pagination.nextOffset. totalAvailable = the exact open-opportunity count."),
2591
+ });
2592
+ // ─── ArcGIS REST feature query (curated service allowlist — SLED bids/GIS) ─
2593
+ // Generic ArcGIS FeatureServer/MapServer layer query over a curated allowlist.
2594
+ // First payload: DC OCP PASS procurement layers (live solicitations/contracts/PO/
2595
+ // payments). `service` is an enum (SSRF core); where/outFields filter the layer.
2596
+ const ArcgisFeatureQueryInput = z.object({
2597
+ service: z
2598
+ .enum(arcgisFeature.ARCGIS_SERVICES.map((s) => s.key))
2599
+ .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)."),
2600
+ where: z
2601
+ .string()
2602
+ .min(1)
2603
+ .max(500)
2604
+ .optional()
2605
+ .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)."),
2606
+ outFields: z
2607
+ .string()
2608
+ .min(1)
2609
+ .max(500)
2610
+ .optional()
2611
+ .describe("Comma-separated fields to return (default '*' = all). e.g. 'SOLICITATIONNUMBER,SOLICITATIONTITLE,DUE_DATE,NIGPCODE'."),
2612
+ orderByFields: z.string().min(1).max(200).optional().describe("ArcGIS orderByFields, e.g. 'DUE_DATE DESC'. Optional."),
2613
+ limit: z.number().int().min(1).max(1000).default(50).describe("Records per page (→ resultRecordCount), 1..1000, default 50."),
2614
+ 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."),
2615
+ });
2511
2616
  // ─── GovInfo (api.govinfo.gov — the api.data.gov keyed trio's 3rd API) ─
2512
2617
  // ADR-0010. Same DATA_GOV_API_KEY/DEMO_KEY/X-Api-Key discipline as the datagov
2513
2618
  // trio (shared datagovKey.ts seam). `collection` is grammar-checked here AND
@@ -5078,6 +5183,66 @@ export const TOOLS = [
5078
5183
  inputSchema: DatagovSearchDatasetsInput,
5079
5184
  handler: (input) => datagovCatalog.searchDatasets(input),
5080
5185
  }),
5186
+ // ━━━ ArcGIS Hub (hub.arcgis.com) — keyless SLED dataset DISCOVERY (loop cycle 9) ━━━
5187
+ // A NEW discovery platform: much US state/local/regional/tribal (SLED) open data
5188
+ // — GIS/infrastructure/permits/boundaries/procurement — lives on ArcGIS Hub, NOT
5189
+ // Socrata/CKAN. Fixed-host SSRF (no per-jurisdiction host vector). ★PROVENANCE: the
5190
+ // Hub is GLOBAL + OPEN (non-US/non-gov publishers), so this is a DISCOVERY aid, not
5191
+ // a curated official-source allowlist — per-row owner/orgName/source/region are
5192
+ // surfaced for vetting + the disclosure rides every response. P1: totalAvailable =
5193
+ // meta.total (exact Hub count). DISCOVERY ONLY (row-query on arbitrary ArcGIS hosts
5194
+ // is a planned, separately-guarded addition).
5195
+ defineTool({
5196
+ name: "arcgis_hub_discover_datasets",
5197
+ description: "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.",
5198
+ inputSchema: ArcgisHubDiscoverInput,
5199
+ handler: (input) => arcgisHub.discoverDatasets(input),
5200
+ }),
5201
+ // ━━━ OpenGov Procurement (api.procurement.opengov.com) — keyless SLED bids ━━━
5202
+ // SLED bid campaign (from the exhaustive US state+local bid-site research). 525+
5203
+ // state/local government portals' LIVE solicitations, via the public portal's OWN
5204
+ // anonymous backend REST API — the key-gated official api-key API is NOT touched
5205
+ // (genuinely keyless, live-verified). Fixed-host SSRF. Two tools: a directory
5206
+ // (one keyless GET returns all ~560 orgs) + per-org solicitations.
5207
+ defineTool({
5208
+ name: "opengov_list_governments",
5209
+ description: "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.",
5210
+ inputSchema: OpengovListGovernmentsInput,
5211
+ handler: (input) => opengov.listGovernments(input),
5212
+ }),
5213
+ defineTool({
5214
+ name: "opengov_search_solicitations",
5215
+ description: "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).",
5216
+ inputSchema: OpengovSearchSolicitationsInput,
5217
+ handler: (input) => opengov.searchSolicitations(input),
5218
+ }),
5219
+ // ━━━ Bonfire (Euna) — keyless per-org open-opportunity RSS (SLED bids) ━━━
5220
+ // SLED bid campaign. Thousands of US state/local govs on Bonfire expose a keyless
5221
+ // RSS of open opportunities. Ships a curated 187-org live-verified seed directory
5222
+ // (Bonfire's authoritative org API is auth-gated → out of bounds). Fixed-suffix
5223
+ // SSRF (.bonfirehub.com). RSS = the complete open set (totalAvailable honest).
5224
+ defineTool({
5225
+ name: "bonfire_list_organizations",
5226
+ description: "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.",
5227
+ inputSchema: BonfireListOrganizationsInput,
5228
+ handler: (input) => bonfire.listOrganizations(input),
5229
+ }),
5230
+ defineTool({
5231
+ name: "bonfire_search_opportunities",
5232
+ description: "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).",
5233
+ inputSchema: BonfireSearchOpportunitiesInput,
5234
+ handler: (input) => bonfire.searchOpportunities(input),
5235
+ }),
5236
+ // ━━━ ArcGIS REST feature query (curated allowlist) — SLED bids/GIS ━━━
5237
+ // Generic query companion to arcgis_hub_discover_datasets. First payload: the DC
5238
+ // OCP PASS procurement layers (live solicitations + contracts/PO/payments), a
5239
+ // keyless live-solicitation feed via ArcGIS REST. `service` enum = SSRF core.
5240
+ defineTool({
5241
+ name: "arcgis_feature_query",
5242
+ description: "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).",
5243
+ inputSchema: ArcgisFeatureQueryInput,
5244
+ handler: (input) => arcgisFeature.featureQuery(input),
5245
+ }),
5081
5246
  // ━━━ GovInfo (api.govinfo.gov) — the api.data.gov keyed trio's 3rd API (3) ━━━ ADR-0010
5082
5247
  // GPO-authoritative bulk publications (BILLS/PLAW/USCODE/CREC/CFR-FR editions/
5083
5248
  // BUDGET/GAOREPORTS) with PDF/XML/MODS downloads + provenance. 2nd consumer of the