@cliwant/mcp-sam-gov 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +5 -5
- package/README.ko.md +5 -5
- package/README.md +19 -7
- package/dist/bonfire.d.ts.map +1 -1
- package/dist/bonfire.js +5 -1
- package/dist/bonfire.js.map +1 -1
- package/dist/cbp-border.d.ts +9 -5
- package/dist/cbp-border.d.ts.map +1 -1
- package/dist/cbp-border.js +31 -11
- package/dist/cbp-border.js.map +1 -1
- package/dist/gao.d.ts.map +1 -1
- package/dist/gao.js +25 -0
- package/dist/gao.js.map +1 -1
- package/dist/grants.js +1 -1
- package/dist/grants.js.map +1 -1
- package/dist/gsa-perdiem.d.ts +16 -2
- package/dist/gsa-perdiem.d.ts.map +1 -1
- package/dist/gsa-perdiem.js +25 -4
- package/dist/gsa-perdiem.js.map +1 -1
- package/dist/integrity.d.ts.map +1 -1
- package/dist/integrity.js +18 -3
- package/dist/integrity.js.map +1 -1
- package/dist/lda.d.ts.map +1 -1
- package/dist/lda.js +17 -4
- package/dist/lda.js.map +1 -1
- package/dist/nhtsa.d.ts +9 -5
- package/dist/nhtsa.d.ts.map +1 -1
- package/dist/nhtsa.js +90 -26
- package/dist/nhtsa.js.map +1 -1
- package/dist/nist-controls.d.ts +3 -1
- package/dist/nist-controls.d.ts.map +1 -1
- package/dist/nist-controls.js +34 -12
- package/dist/nist-controls.js.map +1 -1
- package/dist/ofac.d.ts.map +1 -1
- package/dist/ofac.js +10 -1
- package/dist/ofac.js.map +1 -1
- package/dist/sam-gov/client.d.ts.map +1 -1
- package/dist/sam-gov/client.js +13 -4
- package/dist/sam-gov/client.js.map +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +34 -8
- package/dist/server.js.map +1 -1
- package/dist/socrata.d.ts +1 -1
- package/dist/socrata.d.ts.map +1 -1
- package/dist/socrata.js +30 -0
- package/dist/socrata.js.map +1 -1
- package/package.json +11 -3
- package/src/bonfire.ts +5 -1
- package/src/cbp-border.ts +30 -9
- package/src/gao.ts +27 -0
- package/src/grants.ts +1 -1
- package/src/gsa-perdiem.ts +28 -4
- package/src/integrity.ts +18 -3
- package/src/lda.ts +21 -5
- package/src/nhtsa.ts +92 -27
- package/src/nist-controls.ts +58 -9
- package/src/ofac.ts +12 -1
- package/src/sam-gov/client.ts +13 -4
- package/src/server.ts +36 -8
- package/src/socrata.ts +30 -0
package/dist/server.js
CHANGED
|
@@ -90,7 +90,7 @@ import { realpathSync } from "node:fs";
|
|
|
90
90
|
const SERVER_NAME = "mcp-sam-gov";
|
|
91
91
|
// Kept in lockstep with package.json / manifest.json / server.json.
|
|
92
92
|
// Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
|
|
93
|
-
const SERVER_VERSION = "1.
|
|
93
|
+
const SERVER_VERSION = "1.11.0";
|
|
94
94
|
// ─── Tool input schemas (Zod) ────────────────────────────────────
|
|
95
95
|
const SamSearchInput = z.object({
|
|
96
96
|
query: z.string().optional().describe("Free-text title query"),
|
|
@@ -3688,7 +3688,7 @@ const GsaPerdiemRatesInput = z
|
|
|
3688
3688
|
.string()
|
|
3689
3689
|
.regex(/^\d{4}$/)
|
|
3690
3690
|
.optional()
|
|
3691
|
-
.describe("The per-diem fiscal year (default
|
|
3691
|
+
.describe("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)."),
|
|
3692
3692
|
})
|
|
3693
3693
|
.describe("Look up GSA per-diem rates by EITHER (city + state) OR zip. Supplying both, or neither, ⇒ invalid_input.");
|
|
3694
3694
|
// ─── US DOL Data API v4 (apiprod.dol.gov) — the labor-enforcement lane ──
|
|
@@ -3799,7 +3799,7 @@ const LdaSearchFilingsInput = z.object({
|
|
|
3799
3799
|
.string()
|
|
3800
3800
|
.min(1)
|
|
3801
3801
|
.optional()
|
|
3802
|
-
.describe("
|
|
3802
|
+
.describe("NOTE: the keyless /filings/ endpoint has NO server-side government-entity filter — the LDA API silently ignores it, so this value is NOT applied (reported in _meta.filtersDropped, never as a narrowed total). Government entities are nested per lobbying activity (each filing's lobbyingActivities[].governmentEntities); to find who lobbied an agency, narrow by registrantName/clientName/issue and inspect those nested entities. Retained for discoverability of the limitation."),
|
|
3803
3803
|
issue: z
|
|
3804
3804
|
.string()
|
|
3805
3805
|
.min(1)
|
|
@@ -4092,6 +4092,19 @@ export const TOOLS = [
|
|
|
4092
4092
|
if (filtersDropped.length > 0) {
|
|
4093
4093
|
notes.push("The organization-name filter is NOT supported by the keyless endpoint and was ignored (results are unfiltered on organization). Set SAM_GOV_API_KEY to filter by organization, or filter client-side on the returned `agency` field.");
|
|
4094
4094
|
}
|
|
4095
|
+
// Pagination honesty: the keyless HAL endpoint pages by PAGE (page × size),
|
|
4096
|
+
// not an arbitrary row offset, so a requested offset that is not a multiple
|
|
4097
|
+
// of `limit` snaps DOWN to its page boundary. Disclose the snap so the AI
|
|
4098
|
+
// never believes it read rows [offset..offset+limit) when it actually got
|
|
4099
|
+
// the page-aligned window. (An aligned offset — the default paging pattern
|
|
4100
|
+
// of incrementing by `limit` — is served exactly, so no note.)
|
|
4101
|
+
{
|
|
4102
|
+
const reqOffset = input.offset ?? 0;
|
|
4103
|
+
const size = input.limit ?? 25;
|
|
4104
|
+
if (reqOffset % size !== 0) {
|
|
4105
|
+
notes.push(`Keyless SAM pagination is page-based (page × ${size}); the requested offset ${reqOffset} was snapped DOWN to the page boundary ${Math.floor(reqOffset / size) * size}. Page through by incrementing offset in multiples of limit (set SAM_GOV_API_KEY for exact row offsets).`);
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4095
4108
|
notes.push(...enrichmentNotes);
|
|
4096
4109
|
// freshness is surfaced structurally in `data` (the ResponseMeta type
|
|
4097
4110
|
// has no typed freshness field, mirroring sam_lookup_notice_fields) —
|
|
@@ -4750,7 +4763,7 @@ export const TOOLS = [
|
|
|
4750
4763
|
}),
|
|
4751
4764
|
defineTool({
|
|
4752
4765
|
name: "nist_800_53_controls",
|
|
4753
|
-
description: "Look up NIST SP 800-53 Rev 5 security & privacy CONTROLS (keyless) — the requirement backbone for FedRAMP / CMMC / RMF compliance work. Retrieve a control by `controlId` (exact, e.g. 'AC-2', 'SC-7', 'AC-2(1)'), a `family` (2-letter code 'AC'/'SC'/'IA' or a name substring 'Access Control'), and/or a `keyword` (case-insensitive substring over title + statement); `limit`/`offset` pagination. Each row: { id (e.g. 'AC-2'), family (e.g. 'AC — Access Control'), title, statement (the labelled requirement prose), guidance (discussion), enhancements:[{id,title}] (e.g. AC-2(1)) }. Complements cve_lookup + cisa_kev_lookup (the vulnerability side) with the CONTROL/requirement side. HONESTY: source is NIST's OFFICIAL OSCAL catalog published at github.com/usnistgov/oscal-content (authoritative first-party data served from GitHub, not a .gov API host — provenance disclosed in _meta); the catalog has no query API so filtering is CLIENT-SIDE and totalAvailable is the EXACT match count; this is the REQUIREMENT text only — applicability depends on the system's FIPS-199 impact baseline (Low/Moderate/High), which the catalog does not encode (disclosed); a download failure or an implausibly-truncated catalog (< 15 families) THROWS (never a fake-empty 'control not found').",
|
|
4766
|
+
description: "Look up NIST SP 800-53 Rev 5 security & privacy CONTROLS (keyless) — the requirement backbone for FedRAMP / CMMC / RMF compliance work. Retrieve a control by `controlId` (exact, e.g. 'AC-2', 'SC-7', 'AC-2(1)'), a `family` (2-letter code 'AC'/'SC'/'IA' or a name substring 'Access Control'), and/or a `keyword` (case-insensitive substring over title + statement); `limit`/`offset` pagination. Each row: { id (e.g. 'AC-2'), family (e.g. 'AC — Access Control'), title, status ('withdrawn' | null), statement (the labelled requirement prose; NULL for a WITHDRAWN control, never ''), guidance (discussion), incorporatedInto:[control ids that superseded a withdrawn control, e.g. AC-13 → ['AC-2','AU-6']], enhancements:[{id,title}] (e.g. AC-2(1)) }. Complements cve_lookup + cisa_kev_lookup (the vulnerability side) with the CONTROL/requirement side. HONESTY: source is NIST's OFFICIAL OSCAL catalog published at github.com/usnistgov/oscal-content (authoritative first-party data served from GitHub, not a .gov API host — provenance disclosed in _meta); the exact OSCAL version + last-modified are surfaced in _meta (the catalog is fetched live from the MOVING 'main' branch, so control text can shift between point releases, e.g. 5.1.1 → 5.2.0 — cite the version, not just 'Rev 5'); a WITHDRAWN control (status:'withdrawn') has statement:null and is NOT an active requirement (see incorporatedInto for what replaced it); the catalog has no query API so filtering is CLIENT-SIDE and totalAvailable is the EXACT match count; this is the REQUIREMENT text only — applicability depends on the system's FIPS-199 impact baseline (Low/Moderate/High), which the catalog does not encode (disclosed); a download failure or an implausibly-truncated catalog (< 15 families) THROWS (never a fake-empty 'control not found').",
|
|
4754
4767
|
inputSchema: NistControlsInput,
|
|
4755
4768
|
handler: (input) => nistControls.searchControls(input),
|
|
4756
4769
|
}),
|
|
@@ -5461,7 +5474,7 @@ export const TOOLS = [
|
|
|
5461
5474
|
// ━━━ CBP Border Wait Times (bwt.cbp.gov) — freight/logistics (1) ━━━
|
|
5462
5475
|
defineTool({
|
|
5463
5476
|
name: "cbp_border_wait_times",
|
|
5464
|
-
description: "Live CBP land-border-port wait times — current commercial-vehicle (
|
|
5477
|
+
description: "Live CBP land-border-port wait times — current commercial-vehicle (freight-truck) crossing delays at every US Canadian- and Mexican-border port (keyless; bwt.cbp.gov). The FREIGHT / LOGISTICS situational-awareness lane: per-port commercial-vehicle standard + FAST lane delay (minutes), operational status, open-lane count, and maximum lanes — passenger/pedestrian lanes are NOT surfaced (freight lane only). Filters (optional, applied CLIENT-SIDE over the full fetched port set — the feed has NO server-side filter; an empty-string value is reported in _meta.filtersDropped, not applied): `border` (case-insensitive substring, 'Canadian'/'Mexican'), `portName` (substring, e.g. 'Laredo'); `limit`/`offset` pagination. Each row: { portNumber, portName, crossingName, border, portStatus (Open/Closed), asOf, commercialVehicle:{ maxLanes, standard:{operationalStatus, delayMinutes, lanesOpen, updateTime}, fast:{…} } }. HONESTY: this is REAL-TIME operational data — each lane carries its own updateTime (surfaced verbatim; freshness never implied live-to-the-second); delayMinutes/lanesOpen are number|null (a real 0 stays 0; an empty/N/A value — e.g. a closed lane — is null, NEVER a fabricated 0, because a closed lane's delay is UNKNOWN, not zero); the API returns the WHOLE port set so totalAvailable is the EXACT matched-port count; an outage/4xx/timeout THROWS and a non-array body ⇒ schema_drift (never a fake empty).",
|
|
5465
5478
|
inputSchema: CbpBorderWaitInput,
|
|
5466
5479
|
handler: (input) => cbpBorder.borderWaitTimes(input),
|
|
5467
5480
|
}),
|
|
@@ -5488,7 +5501,7 @@ export const TOOLS = [
|
|
|
5488
5501
|
// never-0; standardRate/isOconus are STRING booleans coerced to real booleans.
|
|
5489
5502
|
defineTool({
|
|
5490
5503
|
name: "gsa_perdiem_rates",
|
|
5491
|
-
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` (e.g. 'Washington') + `state` (2-letter, e.g. 'DC') OR `zip` (5-digit) — supplying BOTH, or NEITHER, ⇒ invalid_input with 0 fetch; optional `year` (default
|
|
5504
|
+
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` (e.g. 'Washington') + `state` (2-letter, e.g. 'DC') OR `zip` (5-digit) — supplying BOTH, or NEITHER, ⇒ invalid_input with 0 fetch; optional `year` (default: the current U.S. federal fiscal year). Returns { rates:[{ city, county, state, zip, year, isOconus, standardRate, mealsUsd, monthlyLodgingUsd:[{ month (1-12), monthName, lodgingUsd }] }] } + honest _meta. HONESTY: lodgingUsd (the API's monthly `value`) is the MAX nightly lodging ceiling for that month — it VARIES SEASONALLY (hence a per-month array), and mealsUsd is the daily M&IE ceiling; both are integer US dollars, null-when-withheld (NEVER 0 — a genuine 0 is preserved). standardRate/isOconus are booleans coerced from the API's string 'true'/'false' (an unrecognized value ⇒ null, never a fabricated false); the months array is preserved AS-IS (never padded to 12). The API returns the COMPLETE rate set (no pagination) ⇒ totalAvailable = the row count, complete:true. A genuine no-match (rates:[]/rate:[]) ⇒ honest empty (returned:0); the API's `errors` field non-null ⇒ invalid_input carrying the message (never a fake empty); a 429 (DEMO_KEY ~10 req/hr, hit quickly) ⇒ rate_limited THROWS; a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON ⇒ schema_drift. DEMO_KEY ~10 req/hr shared ceiling — set DATA_GOV_API_KEY (free at api.data.gov/signup) for 1000/hr. The key rides ONLY in the X-Api-Key header (never the URL/_meta).",
|
|
5492
5505
|
inputSchema: GsaPerdiemRatesInput,
|
|
5493
5506
|
handler: (input) => gsaPerdiem.perdiemRates(input),
|
|
5494
5507
|
}),
|
|
@@ -5520,7 +5533,7 @@ export const TOOLS = [
|
|
|
5520
5533
|
// page-based pagination. income/expenses are null-or-decimal-string ⇒ null-never-0.
|
|
5521
5534
|
defineTool({
|
|
5522
5535
|
name: "lda_search_filings",
|
|
5523
|
-
description: "Search US Senate LDA (Lobbying Disclosure Act) filings — who is paid HOW MUCH to lobby WHICH federal agency on WHICH issue (lda.senate.gov/api/v1/filings, KEYLESS — anonymous access works; an optional free LDA_API_KEY only raises the rate limit). All inputs optional: `registrantName` (the lobbying firm/in-house filer), `clientName` (who it's for), `lobbyistName`, `filingYear` (4-digit), `filingType` (short code, e.g. 'Q1'/'RR'/'YE'), `agency` (
|
|
5536
|
+
description: "Search US Senate LDA (Lobbying Disclosure Act) filings — who is paid HOW MUCH to lobby WHICH federal agency on WHICH issue (lda.senate.gov/api/v1/filings, KEYLESS — anonymous access works; an optional free LDA_API_KEY only raises the rate limit). All inputs optional: `registrantName` (the lobbying firm/in-house filer), `clientName` (who it's for), `lobbyistName`, `filingYear` (4-digit), `filingType` (short code, e.g. 'Q1'/'RR'/'YE'), `agency` (NOTE: /filings/ has NO server-side agency filter — the LDA API silently ignores it, so it is reported in _meta.filtersDropped and NOT applied; government entities are nested per activity in lobbyingActivities[].governmentEntities), `issue` (specific lobbying issues text), `page` (1-based, default 1), `pageSize` (1..25, default 25). Returns { filings:[{ filingUuid, filingType, filingYear, filingPeriod, incomeUsd, expensesUsd, registrant, client, lobbyingActivities:[{ issueCode, description, governmentEntities:[names] }], documentUrl, postedDate, terminationDate }] } + honest _meta. HONESTY: totalAvailable is the API's REAL total match count (the corpus is ~1.95M filings) — NOT the rows on this page; pagination is page-based (pass the next page number when hasMore). incomeUsd/expensesUsd are parsed from the null-or-decimal-string income/expenses — null (not reported) ⇒ null, NEVER 0 (a genuine 0 stays 0); a filing reports EITHER income OR expenses, so the other is typically null. Missing lobbying_activities/government_entities ⇒ empty arrays (never fabricated). A genuine no-match (results:[]) ⇒ honest empty (returned:0); a 400 (bad filter) ⇒ invalid_input surfacing the API's message; a 429 ⇒ rate_limited THROWS (Retry-After honored, never routed around); a 5xx/timeout ⇒ upstream_unavailable THROWS; a 200 non-JSON / non-array results / non-number count ⇒ schema_drift. The optional key rides ONLY in the Authorization: Token header (never the URL/_meta).",
|
|
5524
5537
|
inputSchema: LdaSearchFilingsInput,
|
|
5525
5538
|
handler: (input) => lda.searchFilings(input),
|
|
5526
5539
|
}),
|
|
@@ -5791,7 +5804,7 @@ export async function runTool(name, args, sam) {
|
|
|
5791
5804
|
/**
|
|
5792
5805
|
* Hand-rolled Zod → JSON Schema converter (subset we use).
|
|
5793
5806
|
*/
|
|
5794
|
-
function zodToJsonSchema(schema) {
|
|
5807
|
+
export function zodToJsonSchema(schema) {
|
|
5795
5808
|
const def = schema._def;
|
|
5796
5809
|
const tn = def.typeName;
|
|
5797
5810
|
const description = schema.description;
|
|
@@ -5843,6 +5856,19 @@ function zodToJsonSchema(schema) {
|
|
|
5843
5856
|
const innerSchema = zodToJsonSchema(inner);
|
|
5844
5857
|
return description ? { ...innerSchema, description } : innerSchema;
|
|
5845
5858
|
}
|
|
5859
|
+
if (tn === "ZodEffects") {
|
|
5860
|
+
// .refine() / .superRefine() / .transform() wrap the REAL schema at
|
|
5861
|
+
// `_def.schema` (used for cross-field rules like "npi OR state required").
|
|
5862
|
+
// Without this branch these fall through to the {type:"string"} default,
|
|
5863
|
+
// publishing a degenerate object-less inputSchema that a schema-driven MCP
|
|
5864
|
+
// client cannot construct a call against — even though the runtime Zod still
|
|
5865
|
+
// demands the full object. Unwrap so the published schema keeps its real
|
|
5866
|
+
// properties / required / enums.
|
|
5867
|
+
const inner = schema._def
|
|
5868
|
+
.schema;
|
|
5869
|
+
const innerSchema = zodToJsonSchema(inner);
|
|
5870
|
+
return description ? { ...innerSchema, description } : innerSchema;
|
|
5871
|
+
}
|
|
5846
5872
|
return { type: "string", ...(description ? { description } : {}) };
|
|
5847
5873
|
}
|
|
5848
5874
|
// Start the stdio server ONLY when run directly (node dist/server.js / the
|