@cliwant/mcp-sam-gov 1.14.0 → 1.16.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.
Files changed (55) hide show
  1. package/README.ja.md +3 -2
  2. package/README.ko.md +3 -2
  3. package/README.md +7 -5
  4. package/dist/arcgis-feature.d.ts.map +1 -1
  5. package/dist/arcgis-feature.js +8 -0
  6. package/dist/arcgis-feature.js.map +1 -1
  7. package/dist/bonfire.d.ts +1 -1
  8. package/dist/bonfire.d.ts.map +1 -1
  9. package/dist/bonfire.js +19 -17
  10. package/dist/bonfire.js.map +1 -1
  11. package/dist/ckan.d.ts +6 -4
  12. package/dist/ckan.d.ts.map +1 -1
  13. package/dist/ckan.js +17 -4
  14. package/dist/ckan.js.map +1 -1
  15. package/dist/data-map.d.ts +43 -0
  16. package/dist/data-map.d.ts.map +1 -0
  17. package/dist/data-map.js +344 -0
  18. package/dist/data-map.js.map +1 -0
  19. package/dist/fpds.d.ts.map +1 -1
  20. package/dist/fpds.js +3 -2
  21. package/dist/fpds.js.map +1 -1
  22. package/dist/gao.d.ts.map +1 -1
  23. package/dist/gao.js +17 -6
  24. package/dist/gao.js.map +1 -1
  25. package/dist/gsa-perdiem.d.ts +5 -0
  26. package/dist/gsa-perdiem.d.ts.map +1 -1
  27. package/dist/gsa-perdiem.js +6 -1
  28. package/dist/gsa-perdiem.js.map +1 -1
  29. package/dist/open-checkbook.d.ts +8 -0
  30. package/dist/open-checkbook.d.ts.map +1 -1
  31. package/dist/open-checkbook.js +9 -1
  32. package/dist/open-checkbook.js.map +1 -1
  33. package/dist/pricing.d.ts +1 -0
  34. package/dist/pricing.d.ts.map +1 -1
  35. package/dist/pricing.js +135 -50
  36. package/dist/pricing.js.map +1 -1
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +76 -24
  39. package/dist/server.js.map +1 -1
  40. package/dist/socrata.d.ts +16 -1
  41. package/dist/socrata.d.ts.map +1 -1
  42. package/dist/socrata.js +76 -1
  43. package/dist/socrata.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/arcgis-feature.ts +8 -0
  46. package/src/bonfire.ts +19 -17
  47. package/src/ckan.ts +17 -4
  48. package/src/data-map.ts +386 -0
  49. package/src/fpds.ts +3 -2
  50. package/src/gao.ts +20 -6
  51. package/src/gsa-perdiem.ts +12 -1
  52. package/src/open-checkbook.ts +26 -3
  53. package/src/pricing.ts +142 -48
  54. package/src/server.ts +88 -23
  55. package/src/socrata.ts +83 -1
package/src/pricing.ts CHANGED
@@ -164,10 +164,26 @@ function epochToIso(v: number | string | undefined): string | null {
164
164
  return null;
165
165
  }
166
166
 
167
+ // Maximum pages to scan when a client-side filter (county / constructionType)
168
+ // requires a full-state sweep. 10 pages × 50 rows = 500 WDs — well above any
169
+ // real state's active DBA stock (IL has 70 as of 2026-09-22).
170
+ const WD_SCAN_PAGE_CAP = 10;
171
+ const WD_SCAN_SIZE = 50; // max the SGS API accepts per request
172
+
173
+ /** Extract constructionTypes as a lowercase string array (handles string[] or unknown). */
174
+ function resultConstructionTypes(r: SgsResult): string[] {
175
+ const ct = r.constructionTypes;
176
+ if (!ct) return [];
177
+ if (Array.isArray(ct)) return ct.map((v) => String(v).toLowerCase());
178
+ if (typeof ct === "string") return [ct.toLowerCase()];
179
+ return [];
180
+ }
181
+
167
182
  export async function searchWageDeterminations(args: {
168
183
  coverage: string;
169
184
  state?: string;
170
185
  county?: string;
186
+ constructionType?: string;
171
187
  query?: string;
172
188
  activeOnly?: boolean;
173
189
  standardOnly?: boolean;
@@ -181,35 +197,47 @@ export async function searchWageDeterminations(args: {
181
197
  const page = Math.max(0, Math.floor(args.page ?? 0));
182
198
  const stateFilter = args.state?.trim().toUpperCase() || undefined;
183
199
  const countyFilter = args.county?.trim().toLowerCase() || undefined;
200
+ const ctFilter = args.constructionType?.trim().toLowerCase() || undefined;
201
+
202
+ // When county or constructionType is given, we must scan ALL pages for the
203
+ // state to avoid missing matching WDs (e.g. IL has 70 DBA WDs — the target
204
+ // Cook County Building WD is on page 1). We use max size=50 per fetch and
205
+ // cap at WD_SCAN_PAGE_CAP pages to keep the request count bounded.
206
+ const needsFullScan = Boolean(countyFilter || ctFilter);
184
207
 
185
- const params = new URLSearchParams({
186
- index,
187
- size: String(limit),
188
- page: String(page),
189
- mode: "search",
190
- sort: "-modifiedDate",
191
- });
192
- if (activeOnly) params.set("is_active", "true");
193
- if (standardOnly) params.set("is_standard", "true");
194
- // `q` matches WD number/title ONLY (NOT occupation) — verified q=guard→0.
195
- if (args.query) params.set("q", args.query);
196
- // `state` IS honored server-side (LIVE-VERIFIED 2026-07-03: index=sca&state=VA
197
- // → 30 vs 1028 unfiltered, and every returned WD's location contains VA;
198
- // state=ZZ → 0). It wants the 2-letter USPS code (a full name like "Virginia"
199
- // → 0). This CORRECTS the earlier brief which assumed state was ignored.
200
208
  const filtersApplied: string[] = [`coverage(${index})`];
201
209
  const filtersDropped: string[] = [];
202
210
  const notes: string[] = [];
203
211
  if (activeOnly) filtersApplied.push("activeOnly");
204
212
  if (standardOnly) filtersApplied.push("standardOnly");
205
213
  if (args.query) filtersApplied.push("query(WD number/title only)");
214
+
215
+ // Build the base URLSearchParams that are shared across every page fetch.
216
+ function buildParams(pg: number, sz: number): URLSearchParams {
217
+ const p = new URLSearchParams({
218
+ index,
219
+ size: String(sz),
220
+ page: String(pg),
221
+ mode: "search",
222
+ sort: "-modifiedDate",
223
+ });
224
+ if (activeOnly) p.set("is_active", "true");
225
+ if (standardOnly) p.set("is_standard", "true");
226
+ // `q` matches WD number/title ONLY (NOT occupation) — verified q=guard→0.
227
+ if (args.query) p.set("q", args.query);
228
+ return p;
229
+ }
230
+
231
+ // `state` IS honored server-side (LIVE-VERIFIED 2026-07-03: index=sca&state=VA
232
+ // → 30 vs 1028 unfiltered, and every returned WD's location contains VA;
233
+ // state=ZZ → 0). It wants the 2-letter USPS code (a full name like "Virginia"
234
+ // → 0). This CORRECTS the earlier brief which assumed state was ignored.
235
+ const needsClientState =
236
+ stateFilter !== undefined && !/^[A-Z]{2}$/.test(stateFilter);
206
237
  if (stateFilter) {
207
- if (/^[A-Z]{2}$/.test(stateFilter)) {
208
- params.set("state", stateFilter);
238
+ if (!needsClientState) {
209
239
  filtersApplied.push("state(server-side)");
210
240
  } else {
211
- // Not a 2-letter code → the server would return 0; apply client-side
212
- // instead so a full name still works, and disclose it.
213
241
  filtersDropped.push("state(server-side; not a 2-letter code)");
214
242
  notes.push(
215
243
  `The state value '${args.state}' is not a 2-letter USPS code; the SGS 'state' param only matches 2-letter codes (a full name returns 0), so it was applied CLIENT-SIDE over the fetched page instead. Pass a 2-letter code (e.g. 'VA') for a precise server-side filter.`,
@@ -217,30 +245,75 @@ export async function searchWageDeterminations(args: {
217
245
  }
218
246
  }
219
247
 
220
- const url = `${SGS_BASE}?${params.toString()}`;
221
- const json = await getJson<SgsSearchResp>(url, SAM_HAL_HEADERS, `sam:sgs:${index}`);
222
- const rawResults = json._embedded?.results ?? [];
223
- const serverTotal = json.page?.totalElements ?? null;
248
+ // ── Fetch: one page (no client filters) OR full-state scan ──────
249
+ let allRaw: SgsResult[];
250
+ let serverTotal: number | null;
251
+ let scanCapHit = false;
252
+
253
+ if (!needsFullScan) {
254
+ // Original single-page path (no county/constructionType filter).
255
+ const params = buildParams(page, limit);
256
+ if (!needsClientState && stateFilter) params.set("state", stateFilter);
257
+ const url = `${SGS_BASE}?${params.toString()}`;
258
+ const json = await getJson<SgsSearchResp>(url, SAM_HAL_HEADERS, `sam:sgs:${index}`);
259
+ allRaw = json._embedded?.results ?? [];
260
+ serverTotal = json.page?.totalElements ?? null;
261
+ } else {
262
+ // Full-state scan: fetch all pages with size=50 until exhausted or cap.
263
+ const scanParams = buildParams(0, WD_SCAN_SIZE);
264
+ if (!needsClientState && stateFilter) scanParams.set("state", stateFilter);
265
+ const firstUrl = `${SGS_BASE}?${scanParams.toString()}`;
266
+ const firstJson = await getJson<SgsSearchResp>(firstUrl, SAM_HAL_HEADERS, `sam:sgs:${index}`);
267
+ allRaw = firstJson._embedded?.results ?? [];
268
+ serverTotal = firstJson.page?.totalElements ?? null;
269
+ const totalPages = firstJson.page?.totalPages ?? 1;
270
+
271
+ const pagesToFetch = Math.min(totalPages, WD_SCAN_PAGE_CAP);
272
+ if (totalPages > WD_SCAN_PAGE_CAP) scanCapHit = true;
273
+
274
+ for (let pg = 1; pg < pagesToFetch; pg++) {
275
+ const p = buildParams(pg, WD_SCAN_SIZE);
276
+ if (!needsClientState && stateFilter) p.set("state", stateFilter);
277
+ const pgJson = await getJson<SgsSearchResp>(
278
+ `${SGS_BASE}?${p.toString()}`,
279
+ SAM_HAL_HEADERS,
280
+ `sam:sgs:${index}`,
281
+ );
282
+ allRaw = allRaw.concat(pgJson._embedded?.results ?? []);
283
+ }
284
+ }
224
285
 
225
- // Client-side filtering: county is NOT a documented SGS server param, so it
226
- // is applied here over the fetched page. A non-2-letter state also lands here.
227
- const needsClientState = filtersDropped.some((f) => f.startsWith("state"));
228
- let filtered = rawResults;
286
+ // ── Client-side filtering ────────────────────────────────────────
287
+ let filtered = allRaw;
229
288
  if (needsClientState && stateFilter) {
230
289
  filtered = filtered.filter((r) =>
231
- resultStateCodes(r).some((c) => c === stateFilter || r.location?.state?.name?.toUpperCase() === stateFilter),
290
+ resultStateCodes(r).some(
291
+ (c) => c === stateFilter || r.location?.state?.name?.toUpperCase() === stateFilter,
292
+ ),
232
293
  );
233
294
  }
234
295
  if (countyFilter) {
235
- filtersApplied.push("county(client-side)");
296
+ filtersApplied.push("county(client-side, full-state scan)");
236
297
  filtered = filtered.filter((r) =>
237
298
  resultCounties(r).some((c) => c.name.toLowerCase().includes(countyFilter)),
238
299
  );
239
- notes.push(
240
- "County is filtered CLIENT-SIDE over the fetched page only (the SGS API has no county filter). A county filter combined with a small limit can miss WDs on later pages — raise `limit` or narrow by `state` (server-side) to be sure.",
300
+ }
301
+ if (ctFilter) {
302
+ filtersApplied.push(`constructionType(client-side, value=${args.constructionType})`);
303
+ filtered = filtered.filter((r) =>
304
+ resultConstructionTypes(r).includes(ctFilter),
241
305
  );
242
306
  }
243
307
 
308
+ // ── Specificity ranking: single-county WDs before multi-county ──
309
+ if (countyFilter) {
310
+ filtered.sort((a, b) => {
311
+ const aCount = resultCounties(a).length;
312
+ const bCount = resultCounties(b).length;
313
+ return aCount - bCount; // 1-county first, then 2-county, etc.
314
+ });
315
+ }
316
+
244
317
  const determinations = filtered.map((r) => {
245
318
  const coverageCode = r.type?.code ?? (index === "sca" ? "SCA" : "DBA");
246
319
  return {
@@ -273,27 +346,48 @@ export async function searchWageDeterminations(args: {
273
346
  "The `q` parameter matches the WD number/title only — it does NOT search by occupation or job title (e.g. q=guard returns 0). To find rates for a specific occupation, open the WD and read its rate table.",
274
347
  );
275
348
 
276
- // totalAvailable: the server total is REAL for the coverage/active/standard/
277
- // query/state filters (state is server-side). But when we additionally filter
278
- // client-side (county, or a non-code state), the returned page count no longer
279
- // reflects a full server total for THAT combined filter → null it out and say so.
280
- const clientFiltered = Boolean(countyFilter) || needsClientState;
281
- const totalAvailable = clientFiltered ? null : serverTotal;
349
+ // ── Honest meta ──────────────────────────────────────────────────
350
+ const clientFiltered = Boolean(countyFilter) || Boolean(ctFilter) || needsClientState;
351
+ const scannedCount = allRaw.length;
282
352
  const returned = determinations.length;
283
- const truncated = clientFiltered
284
- ? true // page-bounded client filter → can't prove completeness
285
- : serverTotal !== null && page * limit + returned < serverTotal;
286
353
 
287
- if (clientFiltered) {
354
+ // When we did a full-state scan, totalAvailable is the REAL count of matching
355
+ // WDs across the scanned pages (not null). Only null it when the scan cap was
356
+ // hit (we didn't see all pages) or when there was no client filter.
357
+ let totalAvailable: number | null;
358
+ let truncated: boolean;
359
+
360
+ if (!clientFiltered) {
361
+ // No client filter: server total is accurate for this result set.
362
+ totalAvailable = serverTotal;
363
+ truncated = serverTotal !== null && page * limit + returned < serverTotal;
364
+ } else if (needsFullScan && !scanCapHit) {
365
+ // Full scan completed: real match count is known.
366
+ totalAvailable = returned;
367
+ truncated = false;
368
+ } else {
369
+ // Scan cap hit or no full scan (shouldn't happen but be safe).
370
+ totalAvailable = null;
371
+ truncated = true;
372
+ }
373
+
374
+ if (needsFullScan) {
375
+ const stateLabel = stateFilter ?? "all states";
376
+ const capNote = scanCapHit
377
+ ? ` (scan cap of ${WD_SCAN_PAGE_CAP * WD_SCAN_SIZE} WDs reached — some WDs may have been missed)`
378
+ : "";
288
379
  notes.push(
289
- "totalAvailable is null because a client-side filter (county and/or a non-code state) was applied over just the fetched page — the true match count for the combined filter is unknown. The server-side total for the coverage/state/active filters was " +
290
- (serverTotal ?? "unknown") +
291
- ".",
380
+ `Scanned ${scannedCount} of ${serverTotal ?? scannedCount} ${stateLabel} ${index.toUpperCase()} WDs across all pages${capNote}. ` +
381
+ (countyFilter ? `County filter applied across all scanned WDs. ` : "") +
382
+ (ctFilter ? `constructionType filter (${args.constructionType}) applied. ` : "") +
383
+ (countyFilter
384
+ ? "Single-county WDs ranked first — they are almost always the most specific match for a given locality. "
385
+ : ""),
292
386
  );
293
387
  }
294
388
 
295
389
  return withMeta(
296
- { determinations, coverageIndex: index, page, limit },
390
+ { determinations, coverageIndex: index, page: needsFullScan ? 0 : page, limit: needsFullScan ? returned : limit },
297
391
  {
298
392
  source: WD_SEARCH_SOURCE,
299
393
  keylessMode: true,
@@ -301,9 +395,9 @@ export async function searchWageDeterminations(args: {
301
395
  totalAvailable,
302
396
  truncated,
303
397
  pagination: {
304
- offset: page * limit,
305
- limit,
306
- nextOffset: truncated ? (page + 1) * limit : null,
398
+ offset: 0,
399
+ limit: needsFullScan ? returned : limit,
400
+ nextOffset: truncated && !needsFullScan ? (page + 1) * limit : null,
307
401
  hasMore: truncated,
308
402
  },
309
403
  filtersApplied,
package/src/server.ts CHANGED
@@ -21,7 +21,9 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
21
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
22
  import {
23
23
  CallToolRequestSchema,
24
+ ListResourcesRequestSchema,
24
25
  ListToolsRequestSchema,
26
+ ReadResourceRequestSchema,
25
27
  } from "@modelcontextprotocol/sdk/types.js";
26
28
  import { z } from "zod";
27
29
  import {
@@ -96,6 +98,7 @@ import * as keys from "./keys.js";
96
98
  import { toToolError, ToolErrorCarrier, errorFromResponse } from "./errors.js";
97
99
  import * as feedback from "./feedback.js";
98
100
  import { checkForUpdate } from "./update-check.js";
101
+ import { renderDataMapMarkdown } from "./data-map.js";
99
102
  import {
100
103
  buildMeta,
101
104
  isMetaBundle,
@@ -113,10 +116,21 @@ import {
113
116
  toolNotLoadedEnvelope,
114
117
  } from "./toolsets.js";
115
118
 
119
+ // Where a state-level bid feed lives when it is NOT on OpenGov or Bonfire. Measured,
120
+ // not cosmetic: asked for "Illinois state solicitations", an eval agent searched
121
+ // opengov_list_governments then bonfire_list_organizations, found no state-level IL
122
+ // portal in either, and never reached data.illinois.gov 6rb8-ntpm (in one run it answered
123
+ // with generic portal advice after a single call; in another it ran out of budget). These are
124
+ // the tools an agent reaches for when it thinks "solicitations", so the pointer goes
125
+ // here rather than only on socrata_query, which it never opened.
126
+ const STATE_BID_FEEDS_POINTER =
127
+ "STATE-LEVEL bid feeds on open-data portals are NOT in this directory: TX TxDOT lettings, advertised and taking bids = socrata_query data.texas.gov qh8x-rm8r; IL CDB capital bids that are ANTICIPATED and NOT YET POSTED = socrata_query data.illinois.gov 6rb8-ntpm (~48 rows, not IL's full register). Map: resource samgov://data-map/state-local.";
128
+
129
+
116
130
  const SERVER_NAME = "mcp-sam-gov";
117
131
  // Kept in lockstep with package.json / manifest.json / server.json.
118
132
  // Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
119
- const SERVER_VERSION = "1.14.0";
133
+ const SERVER_VERSION = "1.16.0";
120
134
 
121
135
  // ─── Tool input schemas (Zod) ────────────────────────────────────
122
136
 
@@ -748,13 +762,19 @@ const WageSearchInput = z.object({
748
762
  .string()
749
763
  .optional()
750
764
  .describe(
751
- "2-letter USPS state code (e.g. 'VA'), applied SERVER-SIDE. A full name is applied client-side instead.",
765
+ "2-letter USPS state code (e.g. 'IL'), applied SERVER-SIDE. A full name is applied client-side instead.",
752
766
  ),
753
767
  county: z
754
768
  .string()
755
769
  .optional()
756
770
  .describe(
757
- "County name (substring match), applied CLIENT-SIDE over the fetched page only (the API has no county filter).",
771
+ "County name (substring match, e.g. 'Cook'). When given, ALL pages for the state are scanned before filtering so no WDs are missed.",
772
+ ),
773
+ constructionType: z
774
+ .enum(["Building", "Residential", "Heavy", "Highway"])
775
+ .optional()
776
+ .describe(
777
+ "DBA construction type (Building | Residential | Heavy | Highway). This is the PRIMARY key for a Davis-Bacon lookup: pass state + county + constructionType to pinpoint the correct WD. E.g. Building = federal buildings, schools; Heavy = bridges, utilities; Highway = roads.",
758
778
  ),
759
779
  query: z
760
780
  .string()
@@ -767,8 +787,8 @@ const WageSearchInput = z.object({
767
787
  .boolean()
768
788
  .optional()
769
789
  .describe("Only standard (non-non-standard) WDs (default true)."),
770
- limit: z.number().min(1).max(50).optional().describe("Page size (default 20, max 50)."),
771
- page: z.number().min(0).optional().describe("0-based page index (default 0)."),
790
+ limit: z.number().min(1).max(50).optional().describe("Page size (default 20, max 50). Ignored when county or constructionType is given — a full-state scan is performed instead."),
791
+ page: z.number().min(0).optional().describe("0-based page index (default 0). Ignored when county or constructionType is given — a full-state scan is performed instead."),
772
792
  });
773
793
 
774
794
  const WageRatesInput = z.object({
@@ -1940,7 +1960,8 @@ const EdgarCompanyConceptInput = z.object({
1940
1960
  const SocrataDomainEnum = z
1941
1961
  .enum(socrata.SOCRATA_DOMAINS)
1942
1962
  .describe(
1943
- "Which allowlisted Socrata portal to query (curated .gov hosts + USAC E-rate .org; the SSRF host allowlist — no free host). e.g. data.ny.gov, data.texas.gov, data.wa.gov, opendata.usac.org.",
1963
+ "Which allowlisted Socrata portal to query (the SSRF host allowlist — no free host). " +
1964
+ "Jurisdiction of non-obvious hosts: cthru.data.socrata.com=MASSACHUSETTS statewide (CTHRU); atlanta.data.socrata.com=Atlanta GA; controllerdata.lacity.org+data.lacity.org=Los Angeles; www.dallasopendata.com=Dallas TX; data.brla.gov=Baton Rouge LA; data.kcmo.org=Kansas City MO; data.cstx.gov=College Station TX; data.weho.org=West Hollywood CA; opendata.usac.org+datahub.usac.org=federal USAC E-rate. data.colorado.gov's procurement data is CITY OF DENVER, not CO state.",
1944
1965
  );
1945
1966
 
1946
1967
  const SocrataQueryInput = z.object({
@@ -1961,7 +1982,14 @@ const SocrataQueryInput = z.object({
1961
1982
  select: z
1962
1983
  .string()
1963
1984
  .optional()
1964
- .describe("Optional SoQL $select (column projection / aggregate), e.g. 'agency,SUM(amount)'."),
1985
+ .describe(
1986
+ "Optional SoQL $select (column projection or aggregate). " +
1987
+ "For a TOTAL: 'sum(amount)' (add a where for vendor/fiscal-year filter). " +
1988
+ "For a TOP-N ranking: 'vendor_name, sum(amount) as total' — pair with order='total DESC'. " +
1989
+ "Any SoQL function call (sum/count/avg/min/max) or 'distinct' activates aggregate mode: " +
1990
+ "totalAvailable becomes null (no raw-row total for aggregates) and the count(*) companion is skipped. " +
1991
+ "NEVER sum rows from one page to get a total — always use an aggregate select.",
1992
+ ),
1965
1993
  where: z
1966
1994
  .string()
1967
1995
  .optional()
@@ -1999,9 +2027,10 @@ const SocrataDiscoverDatasetsInput = z.object({
1999
2027
  q: z
2000
2028
  .string()
2001
2029
  .min(1)
2002
- .describe("Keyword(s) to find datasets, e.g. 'procurement', 'vendor payments', 'checkbook'."),
2030
+ .describe("TOPIC words only, e.g. 'solicitations', 'contract', 'vendor payments', 'checkbook'. Put the JURISDICTION in `domain`, never in q: every q term must match and dataset titles rarely repeat their place name, so q='Illinois solicitations' returns 0 while domain=data.illinois.gov q='solicitations' finds the dataset."),
2003
2031
  domain: SocrataDomainEnum.optional().describe(
2004
- "Optional: scope discovery to ONE allowlisted portal. Omit to search the whole allowlist. NOTE: the federated catalog does not index every host (e.g. USAC E-rate returns 0) — those remain queryable via socrata_query with a known 4x4.",
2032
+ "Optional: scope discovery to ONE portal; omit to search all. The catalog does not index every host (USAC returns 0); those stay queryable via socrata_query with a known 4x4. " +
2033
+ "Jurisdiction of non-obvious hosts: cthru.data.socrata.com=MASSACHUSETTS statewide (CTHRU); atlanta.data.socrata.com=Atlanta GA; controllerdata.lacity.org+data.lacity.org=Los Angeles; www.dallasopendata.com=Dallas TX; data.brla.gov=Baton Rouge LA; data.kcmo.org=Kansas City MO; data.cstx.gov=College Station TX; data.weho.org=West Hollywood CA; opendata.usac.org+datahub.usac.org=federal USAC E-rate. data.colorado.gov's procurement data is CITY OF DENVER, not CO state.",
2005
2034
  ),
2006
2035
  limit: z
2007
2036
  .number()
@@ -3077,7 +3106,7 @@ const BonfireSearchOpportunitiesInput = z.object({
3077
3106
  const OpenCheckbookSearchInput = z.object({
3078
3107
  portal: z
3079
3108
  .enum(openCheckbook.OPEN_CHECKBOOK_PORTALS.map((p) => p.key) as [string, ...string[]])
3080
- .describe("The curated Open-Checkbook portal (SSRF allowlist enum). 'sd' = State of South Dakota Open Checkbook (~740,980 vendor payments, ~$8.41B, ~3 most-recent fiscal years)."),
3109
+ .describe("The curated Open-Checkbook portal (SSRF allowlist enum). 'sd' = State of South Dakota Open Checkbook (~740,980 vendor payments, ~$8.41B, ~3 most-recent fiscal years). 'ak' = State of Alaska Open Checkbook (41,751 payments, $1.18B — FY2026 ONLY; FY2019–2025 return count:0 meaning not published, NOT zero spend)."),
3081
3110
  year: z.string().min(1).max(40).optional().describe("Fiscal-year filter (EXACT match), e.g. '2025'. Default 'All Years' = the exposed ~3-year window (NOT full history)."),
3082
3111
  vendor: z.string().min(1).max(200).optional().describe("Vendor name filter (EXACT match, e.g. 'US BANK NA' → 917). A partial/misspelled value returns an honest count:0."),
3083
3112
  org: z.string().min(1).max(200).optional().describe("Department filter (org1, EXACT match, e.g. 'TRANSPORTATION' → 109,887)."),
@@ -3106,7 +3135,7 @@ const TableauViewCsvInput = z.object({
3106
3135
  const ArcgisFeatureQueryInput = z.object({
3107
3136
  service: z
3108
3137
  .enum(arcgisFeature.ARCGIS_SERVICES.map((s) => s.key) as [string, ...string[]])
3109
- .describe("Service key (SSRF allowlist; 27 services). DC OCP PASS (solicitations/contracts/purchase_orders/payments). US local govs: Asheville NC, Bellevue WA, Miami-Dade FL×2, Suffolk County NY, Mat-Su AK, Las Vegas NV×2, Baltimore MD, Naperville IL, Worcester MA, Topeka KS (FY2015–23); TX/AK/IA/OK DOT bid/award registers. ND DOT flex-funding to local agencies (nddot_flex×4 — NOT vendor contracts)."),
3138
+ .describe("Service key (SSRF allowlist; 29 services). DC OCP PASS (solicitations/contracts/purchase_orders/payments). US local govs: Asheville NC, Bellevue WA, Miami-Dade FL×2, Suffolk County NY, Mat-Su AK, Las Vegas NV×2, Baltimore MD, Naperville IL, Worcester MA, Topeka KS (FY2015–23), Hennepin County MN (CIP pipeline), Charlotte-Mecklenburg NC (CIP pipeline); TX/AK/IA/OK DOT bid/award registers. ND DOT flex-funding to local agencies (nddot_flex×4 — NOT vendor contracts)."),
3110
3139
  where: z
3111
3140
  .string()
3112
3141
  .min(1)
@@ -4456,7 +4485,7 @@ const GsaPerdiemRatesInput = z
4456
4485
  .regex(/^\d{4}$/)
4457
4486
  .optional()
4458
4487
  .describe(
4459
- "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).",
4488
+ "U.S. federal fiscal year number (Oct 1–Sep 30). Default: current FY at call time. IMPORTANT: October 2026 = FY2027; September 2026 = FY2026. To get October 2026 rates, pass year='2027'. Validated ^\d{4}$ (it rides in the request path).",
4460
4489
  ),
4461
4490
  })
4462
4491
  .describe(
@@ -5634,7 +5663,7 @@ export const TOOLS: ToolDef[] = [
5634
5663
  defineTool({
5635
5664
  name: "sam_search_wage_determinations",
5636
5665
  description:
5637
- "Find the Service Contract Act (SCA) or Davis-Bacon (DBA) wage determination(s) governing a locality (keyless SAM SGS). Filter by coverage (sca|dba), state (2-letter, server-side), county (client-side), or WD number/title. Returns the structured WD list; follow with sam_get_wage_rates to read the rate table. NOTE: `query` matches WD number/title only, NOT occupation.",
5666
+ "Find the Service Contract Act (SCA) or Davis-Bacon Act (DBA) wage determination(s) for a locality (keyless SAM SGS). For a Davis-Bacon lookup pass state + county + constructionType (e.g. 'IL', 'Cook', 'Building') — the tool scans ALL pages for the state so no WDs are missed, ranks single-county WDs first (the most specific match), and reports real match counts. Then call sam_get_wage_rates on the top result to read the rate table. SCA: pass state + county. constructionType is DBA-only: Building (federal buildings/schools), Residential, Heavy (bridges/utilities), Highway (roads). NOTE: `query` matches WD number/title only, NOT occupation.",
5638
5667
  inputSchema: WageSearchInput,
5639
5668
  handler: (input) => pricing.searchWageDeterminations(input),
5640
5669
  }),
@@ -5838,7 +5867,7 @@ export const TOOLS: ToolDef[] = [
5838
5867
  defineTool({
5839
5868
  name: "socrata_query",
5840
5869
  description:
5841
- "Query rows from an allowlisted Socrata/SODA open-data portal (keyless; ~a dozen US state portals + USAC E-rate on one identical API — state spend/checkbook/contract/vendor-payment datasets). Input `domain` (curated allowlist enum — the SSRF host guard), `datasetId` (4x4, from socrata_discover_datasets), optional SoQL `select`/`where`/`order`/`q`, `limit` (≤1000, def 100), `offset`, `withTotal` (def true). HONESTY: SODA's row response has no total, so a count(*) companion supplies an exact totalAvailable; if it fails the rows still return with totalAvailable:null + a note (hasMore is then inferred from page-fill, never a false complete). Genuine-empty ⇒ complete:true/total:0; an outage/400/404 THROWS (never a fake empty). Value fields are strings.",
5870
+ "Query rows from an allowlisted Socrata/SODA open-data portal (keyless; ~a dozen US state portals + USAC E-rate on one identical API — state spend/checkbook/contract/vendor-payment datasets). State procurement mirrors: NY ehig-g5x3, NJ ubnu-tqu7, WA s8d5-pj78, MA cthru.data.socrata.com pegc-naaa (~49M payment rows). Full map: read resource samgov://data-map/state-local. Input `domain` (curated allowlist enum — the SSRF host guard), `datasetId` (4x4, from socrata_discover_datasets), optional SoQL `select`/`where`/`order`/`q`, `limit` (≤1000, def 100), `offset`, `withTotal` (def true). AGGREGATES: for a grand total pass select='sum(amount)' with a where filter; for top-N vendors pass select='vendor_name, sum(amount) as total' with order='total DESC' — these return the final answer directly, NOT a page to manually sum. HONESTY: SODA's row response has no total, so a count(*) companion supplies an exact totalAvailable; if it fails the rows still return with totalAvailable:null + a note (hasMore is then inferred from page-fill, never a false complete). Genuine-empty ⇒ complete:true/total:0; an outage/400/404 THROWS (never a fake empty). Value fields are strings.",
5842
5871
  inputSchema: SocrataQueryInput,
5843
5872
  handler: (input) => socrata.query(input),
5844
5873
  }),
@@ -5853,7 +5882,7 @@ export const TOOLS: ToolDef[] = [
5853
5882
  defineTool({
5854
5883
  name: "ckan_query",
5855
5884
  description:
5856
- "Query rows from an allowlisted CKAN datastore resource (keyless; the FIRST source on the R2 DataSource port — state/city spend/checkbook/procurement/vendor tables on the identical CKAN Action API). Input `host` (curated allowlist enum — the SSRF host guard: data.ca.gov, data.virginia.gov, data.boston.gov), `resourceId` (36-char lowercase UUID, from ckan_discover_datasets), optional `q` (full-text), `filters` (constrained object {field:value} we JSON.stringify), `sort`, `limit` (≤1000, def 100), `offset`. HONESTY: CKAN's envelope carries a real result.total — the DEFAULT is an EXACT total (exact totalAvailable + hasMore); the rare estimated total (total_was_estimated:true) is disclosed via totalIsEstimated + a note and does NOT drive pagination (it can be above OR below the truth). Genuine-empty ⇒ complete:true/total:0; an outage/404/409 or success:false THROWS (never a fake empty). Values are typed per result.fields[].type.",
5885
+ "Query rows from an allowlisted CKAN datastore resource (keyless; state/city spend/checkbook/procurement/vendor tables on the CKAN Action API). VA eVA PO lines: host=data.virginia.gov resourceId=3c7f1bde-35b0-4fbf-b89c-978a19124d53. Full map: read resource samgov://data-map/state-local. Input `host` (curated allowlist enum — the SSRF host guard: data.ca.gov, data.virginia.gov, data.boston.gov), `resourceId` (36-char lowercase UUID, from ckan_discover_datasets), optional `q` (full-text), `filters` (constrained object {field:value} we JSON.stringify), `sort`, `limit` (≤1000, def 100), `offset`. HONESTY: CKAN's envelope carries a real result.total — the DEFAULT is an EXACT total (exact totalAvailable + hasMore); the rare estimated total (total_was_estimated:true) is disclosed via totalIsEstimated + a note and does NOT drive pagination (it can be above OR below the truth). Genuine-empty ⇒ complete:true/total:0; an outage/404/409 or success:false THROWS (never a fake empty). Values are typed per result.fields[].type.",
5857
5886
  inputSchema: CkanQueryInput,
5858
5887
  handler: (input) => ckan.query(input),
5859
5888
  }),
@@ -6197,33 +6226,33 @@ export const TOOLS: ToolDef[] = [
6197
6226
  defineTool({
6198
6227
  name: "opengov_list_governments",
6199
6228
  description:
6200
- "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.",
6229
+ "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." + " " + STATE_BID_FEEDS_POINTER,
6201
6230
  inputSchema: OpengovListGovernmentsInput,
6202
6231
  handler: (input) => opengov.listGovernments(input),
6203
6232
  }),
6204
6233
  defineTool({
6205
6234
  name: "opengov_search_solicitations",
6206
6235
  description:
6207
- "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).",
6236
+ "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)." + " " + STATE_BID_FEEDS_POINTER,
6208
6237
  inputSchema: OpengovSearchSolicitationsInput,
6209
6238
  handler: (input) => opengov.searchSolicitations(input),
6210
6239
  }),
6211
6240
  // ━━━ Bonfire (Euna) — keyless per-org open-opportunity RSS (SLED bids) ━━━
6212
6241
  // SLED bid campaign. Thousands of US state/local govs on Bonfire expose a keyless
6213
- // RSS of open opportunities. Ships a curated 186-org live-verified seed directory
6242
+ // RSS of open opportunities. Ships a curated 187-org live-verified seed directory
6214
6243
  // (Bonfire's authoritative org API is auth-gated → out of bounds). Fixed-suffix
6215
6244
  // SSRF (.bonfirehub.com). RSS = the complete open set (totalAvailable honest).
6216
6245
  defineTool({
6217
6246
  name: "bonfire_list_organizations",
6218
6247
  description:
6219
- "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 186 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.",
6248
+ "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." + " " + STATE_BID_FEEDS_POINTER,
6220
6249
  inputSchema: BonfireListOrganizationsInput,
6221
6250
  handler: (input) => bonfire.listOrganizations(input),
6222
6251
  }),
6223
6252
  defineTool({
6224
6253
  name: "bonfire_search_opportunities",
6225
6254
  description:
6226
- "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).",
6255
+ "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)." + " " + STATE_BID_FEEDS_POINTER,
6227
6256
  inputSchema: BonfireSearchOpportunitiesInput,
6228
6257
  handler: (input) => bonfire.searchOpportunities(input),
6229
6258
  }),
@@ -6515,7 +6544,7 @@ export const TOOLS: ToolDef[] = [
6515
6544
  // never-0; standardRate/isOconus are STRING booleans coerced to real booleans.
6516
6545
  defineTool({
6517
6546
  name: "gsa_perdiem_rates",
6518
- description: "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` + `state` (2-letter) OR `zip` (5-digit) — supplying BOTH or NEITHER → invalid_input with 0 fetch; optional `year` (default: current federal fiscal year). Returns { rates:[{ city, county, state, zip, year, isOconus, standardRate, mealsUsd, monthlyLodgingUsd:[{ month (1-12), monthName, lodgingUsd }] }] } + honest _meta. HONESTY: lodgingUsd is the MAX nightly lodging ceiling for that month — VARIES SEASONALLY (hence a per-month array); mealsUsd is the daily M&IE ceiling; both are integer US dollars, null-when-withheld (NEVER 0 — genuine 0 preserved). standardRate/isOconus are booleans coerced from the API's string 'true'/'false' (unrecognized → null, never fabricated false); months array preserved AS-IS (never padded to 12). API returns COMPLETE rate set (no pagination) → totalAvailable = row count, complete:true. Genuine no-match → honest empty; `errors` field non-null → invalid_input; 429 (DEMO_KEY ~10 req/hr) → rate_limited THROWS; set DATA_GOV_API_KEY (free, api.data.gov/signup) for 1000/hr. 5xx/timeout → upstream_unavailable THROWS; 200 non-JSON → schema_drift. Key rides ONLY in the X-Api-Key header.",
6547
+ description: "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` + `state` (2-letter) OR `zip` (5-digit) — supplying BOTH or NEITHER → invalid_input with 0 fetch; optional `year` (default: current federal fiscal year). Returns { rates:[{ city, county, state, zip, year, fiscalYear (= year, the U.S. FY: Oct 2026 = FY2027), isOconus, standardRate, mealsUsd, monthlyLodgingUsd:[{ month (1-12), monthName, lodgingUsd }] }] } + honest _meta. HONESTY: lodgingUsd is the MAX nightly lodging ceiling for that month — VARIES SEASONALLY (hence a per-month array); mealsUsd is the daily M&IE ceiling; both are integer US dollars, null-when-withheld (NEVER 0 — genuine 0 preserved). standardRate/isOconus are booleans coerced from the API's string 'true'/'false' (unrecognized → null, never fabricated false); months array preserved AS-IS (never padded to 12). API returns COMPLETE rate set (no pagination) → totalAvailable = row count, complete:true. Genuine no-match → honest empty; `errors` field non-null → invalid_input; 429 (DEMO_KEY ~10 req/hr) → rate_limited THROWS; set DATA_GOV_API_KEY (free, api.data.gov/signup) for 1000/hr. 5xx/timeout → upstream_unavailable THROWS; 200 non-JSON → schema_drift. Key rides ONLY in the X-Api-Key header.",
6519
6548
  inputSchema: GsaPerdiemRatesInput,
6520
6549
  handler: (input) => gsaPerdiem.perdiemRates(input),
6521
6550
  }),
@@ -6662,7 +6691,7 @@ async function main() {
6662
6691
  // B2: unknown toolset names are reported here (not only stderr) because
6663
6692
  // stderr is invisible in Claude Desktop.
6664
6693
  const BASE_INSTRUCTIONS =
6665
- "This server wraps US government open data (keyless-first). If a tool result looks wrong, a tool stays broken, or the user wants a capability this server lacks, help improve it: call the `feedback` tool — or use the `report` URL present on schema_drift / upstream_unavailable errors — to get a PREFILLED GitHub issue link, and offer it to the user to open and submit. Nothing is posted automatically; the user submits. Never include secrets or personal data in a report (the repo is public).";
6694
+ "This server wraps US government open data (keyless-first). State/local dataset IDs (Socrata, CKAN, etc.) are listed in the MCP resource samgov://data-map/state-local. If a tool result looks wrong, a tool stays broken, or the user wants a capability this server lacks, help improve it: call the `feedback` tool — or use the `report` URL present on schema_drift / upstream_unavailable errors — to get a PREFILLED GitHub issue link, and offer it to the user to open and submit. Nothing is posted automatically; the user submits. Never include secrets or personal data in a report (the repo is public).";
6666
6695
 
6667
6696
  let serverInstructions: string;
6668
6697
  if (isAllTools && tsResult.unknown.length === 0) {
@@ -6694,7 +6723,7 @@ async function main() {
6694
6723
  const server = new Server(
6695
6724
  { name: SERVER_NAME, version: SERVER_VERSION },
6696
6725
  {
6697
- capabilities: { tools: {} },
6726
+ capabilities: { tools: {}, resources: {} },
6698
6727
  // Surfaced to the agent at initialize. Tells it how to route real-usage
6699
6728
  // friction back to the project WITHOUT the server ever posting anything.
6700
6729
  instructions: serverInstructions,
@@ -6712,6 +6741,42 @@ async function main() {
6712
6741
  };
6713
6742
  });
6714
6743
 
6744
+ // ── MCP Resources: state & local data map ──────────────────────────────
6745
+ const DATA_MAP_URI = "samgov://data-map/state-local";
6746
+ const DATA_MAP_NAME = "State & local data map";
6747
+ const DATA_MAP_DESCRIPTION =
6748
+ "Jurisdiction → verified tool call → row count for every allowlisted state/local Socrata, CKAN, Tableau and Open Checkbook dataset.";
6749
+
6750
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
6751
+ return {
6752
+ resources: [
6753
+ {
6754
+ uri: DATA_MAP_URI,
6755
+ name: DATA_MAP_NAME,
6756
+ description: DATA_MAP_DESCRIPTION,
6757
+ mimeType: "text/markdown",
6758
+ },
6759
+ ],
6760
+ };
6761
+ });
6762
+
6763
+ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
6764
+ const { uri } = req.params;
6765
+ if (uri !== DATA_MAP_URI) {
6766
+ throw new Error(`Unknown resource: ${uri}`);
6767
+ }
6768
+ return {
6769
+ contents: [
6770
+ {
6771
+ uri: DATA_MAP_URI,
6772
+ mimeType: "text/markdown",
6773
+ text: renderDataMapMarkdown(),
6774
+ },
6775
+ ],
6776
+ };
6777
+ });
6778
+ // ── end MCP Resources ──────────────────────────────────────────────────
6779
+
6715
6780
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
6716
6781
  const { name, arguments: args } = req.params;
6717
6782
  // Check if the tool exists but is not loaded in the current toolset profile.