@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/README.ja.md +2 -1
- package/README.ko.md +2 -1
- package/README.md +4 -2
- package/dist/arcgis-feature.d.ts.map +1 -1
- package/dist/arcgis-feature.js +8 -0
- package/dist/arcgis-feature.js.map +1 -1
- package/dist/bonfire.d.ts +1 -1
- package/dist/bonfire.d.ts.map +1 -1
- package/dist/bonfire.js +15 -11
- package/dist/bonfire.js.map +1 -1
- package/dist/data-map.d.ts +43 -0
- package/dist/data-map.d.ts.map +1 -0
- package/dist/data-map.js +289 -0
- package/dist/data-map.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +46 -11
- package/dist/server.js.map +1 -1
- package/dist/socrata.d.ts +1 -1
- package/dist/socrata.d.ts.map +1 -1
- package/dist/socrata.js +46 -1
- package/dist/socrata.js.map +1 -1
- package/package.json +1 -1
- package/src/arcgis-feature.ts +8 -0
- package/src/bonfire.ts +15 -11
- package/src/data-map.ts +329 -0
- package/src/server.ts +51 -10
- package/src/socrata.ts +47 -1
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.
|
|
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 (
|
|
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
|
|
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;
|
|
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;
|
|
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
|
|
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
|
|
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 (
|
|
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 {
|