@cliwant/mcp-sam-gov 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/server.ts CHANGED
@@ -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,
@@ -116,7 +119,7 @@ import {
116
119
  const SERVER_NAME = "mcp-sam-gov";
117
120
  // Kept in lockstep with package.json / manifest.json / server.json.
118
121
  // Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
119
- const SERVER_VERSION = "1.14.0";
122
+ const SERVER_VERSION = "1.15.0";
120
123
 
121
124
  // ─── Tool input schemas (Zod) ────────────────────────────────────
122
125
 
@@ -1940,7 +1943,8 @@ const EdgarCompanyConceptInput = z.object({
1940
1943
  const SocrataDomainEnum = z
1941
1944
  .enum(socrata.SOCRATA_DOMAINS)
1942
1945
  .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.",
1946
+ "Which allowlisted Socrata portal to query (the SSRF host allowlist — no free host). " +
1947
+ "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
1948
  );
1945
1949
 
1946
1950
  const SocrataQueryInput = z.object({
@@ -2001,7 +2005,8 @@ const SocrataDiscoverDatasetsInput = z.object({
2001
2005
  .min(1)
2002
2006
  .describe("Keyword(s) to find datasets, e.g. 'procurement', 'vendor payments', 'checkbook'."),
2003
2007
  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.",
2008
+ "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. " +
2009
+ "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
2010
  ),
2006
2011
  limit: z
2007
2012
  .number()
@@ -3106,7 +3111,7 @@ const TableauViewCsvInput = z.object({
3106
3111
  const ArcgisFeatureQueryInput = z.object({
3107
3112
  service: z
3108
3113
  .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)."),
3114
+ .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
3115
  where: z
3111
3116
  .string()
3112
3117
  .min(1)
@@ -5838,7 +5843,7 @@ export const TOOLS: ToolDef[] = [
5838
5843
  defineTool({
5839
5844
  name: "socrata_query",
5840
5845
  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.",
5846
+ "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). 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
5847
  inputSchema: SocrataQueryInput,
5843
5848
  handler: (input) => socrata.query(input),
5844
5849
  }),
@@ -5853,7 +5858,7 @@ export const TOOLS: ToolDef[] = [
5853
5858
  defineTool({
5854
5859
  name: "ckan_query",
5855
5860
  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.",
5861
+ "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
5862
  inputSchema: CkanQueryInput,
5858
5863
  handler: (input) => ckan.query(input),
5859
5864
  }),
@@ -6210,13 +6215,13 @@ export const TOOLS: ToolDef[] = [
6210
6215
  }),
6211
6216
  // ━━━ Bonfire (Euna) — keyless per-org open-opportunity RSS (SLED bids) ━━━
6212
6217
  // 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
6218
+ // RSS of open opportunities. Ships a curated 195-org live-verified seed directory
6214
6219
  // (Bonfire's authoritative org API is auth-gated → out of bounds). Fixed-suffix
6215
6220
  // SSRF (.bonfirehub.com). RSS = the complete open set (totalAvailable honest).
6216
6221
  defineTool({
6217
6222
  name: "bonfire_list_organizations",
6218
6223
  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.",
6224
+ "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 195 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.",
6220
6225
  inputSchema: BonfireListOrganizationsInput,
6221
6226
  handler: (input) => bonfire.listOrganizations(input),
6222
6227
  }),
@@ -6662,7 +6667,7 @@ async function main() {
6662
6667
  // B2: unknown toolset names are reported here (not only stderr) because
6663
6668
  // stderr is invisible in Claude Desktop.
6664
6669
  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).";
6670
+ "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
6671
 
6667
6672
  let serverInstructions: string;
6668
6673
  if (isAllTools && tsResult.unknown.length === 0) {
@@ -6694,7 +6699,7 @@ async function main() {
6694
6699
  const server = new Server(
6695
6700
  { name: SERVER_NAME, version: SERVER_VERSION },
6696
6701
  {
6697
- capabilities: { tools: {} },
6702
+ capabilities: { tools: {}, resources: {} },
6698
6703
  // Surfaced to the agent at initialize. Tells it how to route real-usage
6699
6704
  // friction back to the project WITHOUT the server ever posting anything.
6700
6705
  instructions: serverInstructions,
@@ -6712,6 +6717,42 @@ async function main() {
6712
6717
  };
6713
6718
  });
6714
6719
 
6720
+ // ── MCP Resources: state & local data map ──────────────────────────────
6721
+ const DATA_MAP_URI = "samgov://data-map/state-local";
6722
+ const DATA_MAP_NAME = "State & local data map";
6723
+ const DATA_MAP_DESCRIPTION =
6724
+ "Jurisdiction → verified tool call → row count for every allowlisted state/local Socrata, CKAN, Tableau and Open Checkbook dataset.";
6725
+
6726
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
6727
+ return {
6728
+ resources: [
6729
+ {
6730
+ uri: DATA_MAP_URI,
6731
+ name: DATA_MAP_NAME,
6732
+ description: DATA_MAP_DESCRIPTION,
6733
+ mimeType: "text/markdown",
6734
+ },
6735
+ ],
6736
+ };
6737
+ });
6738
+
6739
+ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
6740
+ const { uri } = req.params;
6741
+ if (uri !== DATA_MAP_URI) {
6742
+ throw new Error(`Unknown resource: ${uri}`);
6743
+ }
6744
+ return {
6745
+ contents: [
6746
+ {
6747
+ uri: DATA_MAP_URI,
6748
+ mimeType: "text/markdown",
6749
+ text: renderDataMapMarkdown(),
6750
+ },
6751
+ ],
6752
+ };
6753
+ });
6754
+ // ── end MCP Resources ──────────────────────────────────────────────────
6755
+
6715
6756
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
6716
6757
  const { name, arguments: args } = req.params;
6717
6758
  // Check if the tool exists but is not loaded in the current toolset profile.
package/src/socrata.ts CHANGED
@@ -179,7 +179,8 @@ export const SOCRATA_DOMAINS = [
179
179
  // allowlist, not the TLD; these five are the documented non-.gov entries. ──
180
180
  "data.cityofnewyork.us", // NYC OpenData (.us, official) — e.g. dg92-zbpx (City Record: procurement notices)
181
181
  "data.cityofchicago.org", // Chicago (.org, official) — e.g. rsxa-ify5 (Contracts)
182
- "data.sfgov.org", // San Francisco / DataSF (.org, official) — e.g. cqi5-hm2d (Supplier Contracts)
182
+ "data.sfgov.org", // San Francisco / DataSF — PERMANENTLY MIGRATED to data.sf.gov (301 all paths, 2026-09); kept allowlisted so requests reach the migration handler (see MIGRATED_SOCRATA_HOSTS below) instead of returning invalid_input. Use data.sf.gov for all new calls.
183
+ "data.sf.gov", // San Francisco / DataSF (canonical post-migration .gov domain) — e.g. cqi5-hm2d (Supplier Contracts 49,186 rows), hmh3-ff63 (Airport/SFO Contract Opportunities)
183
184
  "controllerdata.lacity.org", // LA City Controller (.org, official) — e.g. pggv-e4fn (Checkbook L.A.)
184
185
  "opendata.usac.org", // USAC E-rate (.org, m6) — e.g. avi8-svp9
185
186
  // ── County/city procurement sweep (loop cycle 17, 2026-07-20). Discovered via
@@ -230,6 +231,24 @@ export type SocrataDomain = (typeof SOCRATA_DOMAINS)[number];
230
231
 
231
232
  const SOCRATA_DOMAIN_SET: ReadonlySet<string> = new Set(SOCRATA_DOMAINS);
232
233
 
234
+ /**
235
+ * Known host migrations (permanent 301 redirects). A domain here has permanently
236
+ * moved; redirect:"error" means any fetch to the old host will fail immediately.
237
+ * Rather than returning a retryable upstream_unavailable (which implies the
238
+ * problem may resolve on its own), we pre-flight check this map and return
239
+ * invalid_input — non-retryable, naming the replacement — so the caller can
240
+ * update the `domain` parameter instead of spinning on a doomed retry loop.
241
+ *
242
+ * Keep the old host in SOCRATA_DOMAINS so it passes the allowlist gate (invalid
243
+ * domains are rejected before reaching this check). Only add an entry here when
244
+ * the migration is confirmed by a live 301 probe with a stable redirect_url.
245
+ *
246
+ * data.sfgov.org → data.sf.gov (confirmed 2026-09-21; all paths 301)
247
+ */
248
+ const MIGRATED_SOCRATA_HOSTS: ReadonlyMap<string, string> = new Map([
249
+ ["data.sfgov.org", "data.sf.gov"],
250
+ ]);
251
+
233
252
  const CATALOG_HOST = "api.us.socrata.com";
234
253
  const CATALOG_URL = `https://${CATALOG_HOST}/api/catalog/v1`;
235
254
 
@@ -297,6 +316,18 @@ async function getSocrataResource(
297
316
  retryable: false,
298
317
  });
299
318
  }
319
+ // Migration pre-flight (MIGRATED_SOCRATA_HOSTS): if the domain has permanently
320
+ // moved, redirect:"error" means any fetch will immediately fail with a 301.
321
+ // Return invalid_input (non-retryable) now — before wasting a network round-trip
322
+ // — naming the replacement so the caller can update the domain parameter.
323
+ const migratedTo = MIGRATED_SOCRATA_HOSTS.get(domain);
324
+ if (migratedTo !== undefined) {
325
+ throw new ToolErrorCarrier({
326
+ kind: "invalid_input",
327
+ message: `Socrata host ${JSON.stringify(domain)} has permanently moved to ${JSON.stringify(migratedTo)}. Update the \`domain\` parameter to ${JSON.stringify(migratedTo)} — retrying ${JSON.stringify(domain)} will not succeed.`,
328
+ retryable: false,
329
+ });
330
+ }
300
331
  if (datasetId.length !== 9 || !DATASET_ID_RE.test(datasetId)) {
301
332
  throw new ToolErrorCarrier({
302
333
  kind: "invalid_input",
@@ -375,6 +406,15 @@ async function getHostCatalog(
375
406
  retryable: false,
376
407
  });
377
408
  }
409
+ // Migration pre-flight — same policy as getSocrataResource (see comment there).
410
+ const migratedTo = MIGRATED_SOCRATA_HOSTS.get(domain);
411
+ if (migratedTo !== undefined) {
412
+ throw new ToolErrorCarrier({
413
+ kind: "invalid_input",
414
+ message: `Socrata host ${JSON.stringify(domain)} has permanently moved to ${JSON.stringify(migratedTo)}. Update the \`domain\` parameter to ${JSON.stringify(migratedTo)} — retrying ${JSON.stringify(domain)} will not succeed.`,
415
+ retryable: false,
416
+ });
417
+ }
378
418
  const url = `https://${domain}/api/catalog/v1?${params.toString()}`;
379
419
  const built = new URL(url);
380
420
  if (built.hostname !== domain || built.protocol !== "https:") {
@@ -419,6 +459,12 @@ async function fetchCount(
419
459
  const params = new URLSearchParams();
420
460
  if (where) params.set("$where", where);
421
461
  if (q) params.set("$q", q);
462
+ // ★ MUST stay count(*) — DO NOT change to count(1) or SELECT count(1).
463
+ // Cloudflare-fronted Socrata hosts (e.g. opendata.maryland.gov) deterministically
464
+ // 403 ("Just a moment...") on count(1) and `$query=SELECT count(1)` — their WAF
465
+ // treats the pattern as SQLi-like — while count(*) consistently returns 200.
466
+ // Verified 3/3 on opendata.maryland.gov (2026-09-21): count(1)→403, count(*)→200
467
+ // (342 rows). Maryland is not broken; the shape here is load-bearing.
422
468
  params.set("$select", "count(*)");
423
469
  let body: unknown;
424
470
  try {