@cliwant/mcp-sam-gov 1.15.0 → 1.17.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 (63) hide show
  1. package/README.ja.md +11 -11
  2. package/README.ko.md +11 -11
  3. package/README.md +18 -17
  4. package/dist/bonfire.d.ts +1 -1
  5. package/dist/bonfire.d.ts.map +1 -1
  6. package/dist/bonfire.js +7 -9
  7. package/dist/bonfire.js.map +1 -1
  8. package/dist/ckan.d.ts +6 -4
  9. package/dist/ckan.d.ts.map +1 -1
  10. package/dist/ckan.js +17 -4
  11. package/dist/ckan.js.map +1 -1
  12. package/dist/courtlistener.d.ts +32 -0
  13. package/dist/courtlistener.d.ts.map +1 -1
  14. package/dist/courtlistener.js +180 -0
  15. package/dist/courtlistener.js.map +1 -1
  16. package/dist/data-map.d.ts.map +1 -1
  17. package/dist/data-map.js +56 -1
  18. package/dist/data-map.js.map +1 -1
  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/keys.d.ts.map +1 -1
  30. package/dist/keys.js +1 -0
  31. package/dist/keys.js.map +1 -1
  32. package/dist/open-checkbook.d.ts +8 -0
  33. package/dist/open-checkbook.d.ts.map +1 -1
  34. package/dist/open-checkbook.js +9 -1
  35. package/dist/open-checkbook.js.map +1 -1
  36. package/dist/pricing.d.ts +1 -0
  37. package/dist/pricing.d.ts.map +1 -1
  38. package/dist/pricing.js +135 -50
  39. package/dist/pricing.js.map +1 -1
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +93 -19
  42. package/dist/server.js.map +1 -1
  43. package/dist/socrata.d.ts +16 -1
  44. package/dist/socrata.d.ts.map +1 -1
  45. package/dist/socrata.js +31 -1
  46. package/dist/socrata.js.map +1 -1
  47. package/dist/toolsets.d.ts.map +1 -1
  48. package/dist/toolsets.js +1 -0
  49. package/dist/toolsets.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/bonfire.ts +7 -9
  52. package/src/ckan.ts +17 -4
  53. package/src/courtlistener.ts +244 -1
  54. package/src/data-map.ts +58 -1
  55. package/src/fpds.ts +3 -2
  56. package/src/gao.ts +20 -6
  57. package/src/gsa-perdiem.ts +12 -1
  58. package/src/keys.ts +1 -0
  59. package/src/open-checkbook.ts +26 -3
  60. package/src/pricing.ts +142 -48
  61. package/src/server.ts +101 -19
  62. package/src/socrata.ts +37 -1
  63. package/src/toolsets.ts +1 -0
package/src/server.ts CHANGED
@@ -116,10 +116,21 @@ import {
116
116
  toolNotLoadedEnvelope,
117
117
  } from "./toolsets.js";
118
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
+
119
130
  const SERVER_NAME = "mcp-sam-gov";
120
131
  // Kept in lockstep with package.json / manifest.json / server.json.
121
132
  // Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
122
- const SERVER_VERSION = "1.15.0";
133
+ const SERVER_VERSION = "1.17.0";
123
134
 
124
135
  // ─── Tool input schemas (Zod) ────────────────────────────────────
125
136
 
@@ -751,13 +762,19 @@ const WageSearchInput = z.object({
751
762
  .string()
752
763
  .optional()
753
764
  .describe(
754
- "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.",
755
766
  ),
756
767
  county: z
757
768
  .string()
758
769
  .optional()
759
770
  .describe(
760
- "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.",
761
778
  ),
762
779
  query: z
763
780
  .string()
@@ -770,8 +787,8 @@ const WageSearchInput = z.object({
770
787
  .boolean()
771
788
  .optional()
772
789
  .describe("Only standard (non-non-standard) WDs (default true)."),
773
- limit: z.number().min(1).max(50).optional().describe("Page size (default 20, max 50)."),
774
- 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."),
775
792
  });
776
793
 
777
794
  const WageRatesInput = z.object({
@@ -1965,7 +1982,14 @@ const SocrataQueryInput = z.object({
1965
1982
  select: z
1966
1983
  .string()
1967
1984
  .optional()
1968
- .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
+ ),
1969
1993
  where: z
1970
1994
  .string()
1971
1995
  .optional()
@@ -2003,7 +2027,7 @@ const SocrataDiscoverDatasetsInput = z.object({
2003
2027
  q: z
2004
2028
  .string()
2005
2029
  .min(1)
2006
- .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."),
2007
2031
  domain: SocrataDomainEnum.optional().describe(
2008
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. " +
2009
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.",
@@ -3082,7 +3106,7 @@ const BonfireSearchOpportunitiesInput = z.object({
3082
3106
  const OpenCheckbookSearchInput = z.object({
3083
3107
  portal: z
3084
3108
  .enum(openCheckbook.OPEN_CHECKBOOK_PORTALS.map((p) => p.key) as [string, ...string[]])
3085
- .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)."),
3086
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)."),
3087
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."),
3088
3112
  org: z.string().min(1).max(200).optional().describe("Department filter (org1, EXACT match, e.g. 'TRANSPORTATION' → 109,887)."),
@@ -4461,7 +4485,7 @@ const GsaPerdiemRatesInput = z
4461
4485
  .regex(/^\d{4}$/)
4462
4486
  .optional()
4463
4487
  .describe(
4464
- "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).",
4465
4489
  ),
4466
4490
  })
4467
4491
  .describe(
@@ -4617,11 +4641,16 @@ const LdaSearchFilingsInput = z.object({
4617
4641
  // never results.length; CURSOR pagination (nextCursor extracted from `next`). court/
4618
4642
  // dates charclass-guarded; all filter VALUES ride URLSearchParams; type=o is FIXED.
4619
4643
  const CourtlistenerSearchOpinionsInput = z.object({
4644
+ party: z
4645
+ .string()
4646
+ .min(1)
4647
+ .optional()
4648
+ .describe("Company or person name to match as an ACTUAL PARTY — builds caseName:\"…\" fielded query. A bare company name in `query` matches text mentions (~5,954 for 'Lockheed Martin'); `party` finds cases where the company is actually named as a party (~327). Use `party` for vendor/contractor vetting; use `query` for full-text topic search."),
4620
4649
  query: z
4621
4650
  .string()
4622
4651
  .min(1)
4623
4652
  .optional()
4624
- .describe("Full-text query (maps to q), e.g. 'bid protest' or a party name. Matches across the opinion text/metadata."),
4653
+ .describe("Additional full-text query (maps to q) — topic keywords such as 'False Claims Act' or 'bid protest'. Combined with caseName:\"…\" when `party` is also given. A bare company name here matches text mentions, not actual-party cases — use `party` for that."),
4625
4654
  court: z
4626
4655
  .string()
4627
4656
  .regex(/^[a-z0-9]+$/)
@@ -4654,6 +4683,53 @@ const CourtlistenerSearchOpinionsInput = z.object({
4654
4683
  .describe("Sort order (maps to order_by), default 'dateFiled desc' (most recent first). E.g. 'dateFiled asc', 'score desc'."),
4655
4684
  });
4656
4685
 
4686
+ // ─── CourtListener RECAP dockets (type=r) ────────────────────────
4687
+ // ADR-0064. FCA/qui tam matters are DOCKETS — they rarely produce published opinions
4688
+ // so they are invisible to type=o. suitNature:"…" is a REAL fielded filter on dockets.
4689
+ // Same SSRF / auth / cursor / provenance guards as opinions.
4690
+ const CourtlistenerSearchDocketsInput = z.object({
4691
+ party: z
4692
+ .string()
4693
+ .min(1)
4694
+ .optional()
4695
+ .describe("Company or person name to match as an ACTUAL PARTY — builds caseName:\"…\" fielded query. Use for vendor/contractor due-diligence (FCA, False Claims Act, qui tam). E.g. 'Lockheed Martin' → ~9 FCA dockets."),
4696
+ query: z
4697
+ .string()
4698
+ .min(1)
4699
+ .optional()
4700
+ .describe("Additional free-text keywords (combined with caseName:\"…\" when `party` is given). E.g. 'qui tam' or 'whistleblower'."),
4701
+ natureOfSuit: z
4702
+ .string()
4703
+ .min(1)
4704
+ .optional()
4705
+ .describe("Nature-of-suit filter — builds suitNature:\"…\" fielded query (THIS IS A REAL DOCKET FIELD, not folded into q). E.g. 'False Claims' to find FCA/qui tam dockets. Disclosed in _meta.notes."),
4706
+ court: z
4707
+ .string()
4708
+ .regex(/^[a-z0-9]+$/)
4709
+ .optional()
4710
+ .describe("CourtListener court id (lowercase alphanumerics ^[a-z0-9]+$), e.g. 'gand' (N.D. Ga.), 'flmd' (M.D. Fla.), 'cafc'. Filters to one court."),
4711
+ dateFiledAfter: z
4712
+ .string()
4713
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
4714
+ .optional()
4715
+ .describe("Only dockets filed on/after this ISO date (→ filed_after), e.g. '2015-01-01'."),
4716
+ dateFiledBefore: z
4717
+ .string()
4718
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
4719
+ .optional()
4720
+ .describe("Only dockets filed on/before this ISO date (→ filed_before), e.g. '2024-12-31'."),
4721
+ order: z
4722
+ .string()
4723
+ .min(1)
4724
+ .default("dateFiled desc")
4725
+ .describe("Sort order (maps to order_by), default 'dateFiled desc'. E.g. 'dateFiled asc'."),
4726
+ cursor: z
4727
+ .string()
4728
+ .min(1)
4729
+ .optional()
4730
+ .describe("Opaque continuation token — pass back _meta.nextCursor from the previous page."),
4731
+ });
4732
+
4657
4733
  // ─── US tax-exempt nonprofits (projects.propublica.org) — the nonprofit lane ──
4658
4734
  // ADR-0060. IRS Form 990 public records republished KEYLESS by ProPublica Nonprofit
4659
4735
  // Explorer (a non-profit newsroom) — NOT a .gov API (the IRS has no clean query
@@ -5639,7 +5715,7 @@ export const TOOLS: ToolDef[] = [
5639
5715
  defineTool({
5640
5716
  name: "sam_search_wage_determinations",
5641
5717
  description:
5642
- "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.",
5718
+ "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.",
5643
5719
  inputSchema: WageSearchInput,
5644
5720
  handler: (input) => pricing.searchWageDeterminations(input),
5645
5721
  }),
@@ -5843,7 +5919,7 @@ export const TOOLS: ToolDef[] = [
5843
5919
  defineTool({
5844
5920
  name: "socrata_query",
5845
5921
  description:
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.",
5922
+ "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.",
5847
5923
  inputSchema: SocrataQueryInput,
5848
5924
  handler: (input) => socrata.query(input),
5849
5925
  }),
@@ -6202,33 +6278,33 @@ export const TOOLS: ToolDef[] = [
6202
6278
  defineTool({
6203
6279
  name: "opengov_list_governments",
6204
6280
  description:
6205
- "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.",
6281
+ "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,
6206
6282
  inputSchema: OpengovListGovernmentsInput,
6207
6283
  handler: (input) => opengov.listGovernments(input),
6208
6284
  }),
6209
6285
  defineTool({
6210
6286
  name: "opengov_search_solicitations",
6211
6287
  description:
6212
- "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).",
6288
+ "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,
6213
6289
  inputSchema: OpengovSearchSolicitationsInput,
6214
6290
  handler: (input) => opengov.searchSolicitations(input),
6215
6291
  }),
6216
6292
  // ━━━ Bonfire (Euna) — keyless per-org open-opportunity RSS (SLED bids) ━━━
6217
6293
  // SLED bid campaign. Thousands of US state/local govs on Bonfire expose a keyless
6218
- // RSS of open opportunities. Ships a curated 195-org live-verified seed directory
6294
+ // RSS of open opportunities. Ships a curated 187-org live-verified seed directory
6219
6295
  // (Bonfire's authoritative org API is auth-gated → out of bounds). Fixed-suffix
6220
6296
  // SSRF (.bonfirehub.com). RSS = the complete open set (totalAvailable honest).
6221
6297
  defineTool({
6222
6298
  name: "bonfire_list_organizations",
6223
6299
  description:
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.",
6300
+ "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,
6225
6301
  inputSchema: BonfireListOrganizationsInput,
6226
6302
  handler: (input) => bonfire.listOrganizations(input),
6227
6303
  }),
6228
6304
  defineTool({
6229
6305
  name: "bonfire_search_opportunities",
6230
6306
  description:
6231
- "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).",
6307
+ "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,
6232
6308
  inputSchema: BonfireSearchOpportunitiesInput,
6233
6309
  handler: (input) => bonfire.searchOpportunities(input),
6234
6310
  }),
@@ -6520,7 +6596,7 @@ export const TOOLS: ToolDef[] = [
6520
6596
  // never-0; standardRate/isOconus are STRING booleans coerced to real booleans.
6521
6597
  defineTool({
6522
6598
  name: "gsa_perdiem_rates",
6523
- 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.",
6599
+ 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.",
6524
6600
  inputSchema: GsaPerdiemRatesInput,
6525
6601
  handler: (input) => gsaPerdiem.perdiemRates(input),
6526
6602
  }),
@@ -6567,10 +6643,16 @@ export const TOOLS: ToolDef[] = [
6567
6643
  // (nextCursor extracted from `next`, host re-asserted). type=o FIXED.
6568
6644
  defineTool({
6569
6645
  name: "courtlistener_search_opinions",
6570
- description: "Search US federal court opinions via CourtListener (www.courtlistener.com/api/rest/v4/search, type=o). ★PROVENANCE: DATA is US federal court PUBLIC RECORDS; the API is CourtListener (Free Law Project, NON-PROFIT) — NOT a .gov API; the .gov primary source (PACER) is PAYWALLED. KEYLESS (optional free COURTLISTENER_API_TOKEN only raises the rate limit). Filters (all optional): `query` (full-text → q), `court` (^[a-z0-9]+$ — e.g. 'uscfc' US Court of Federal Claims, 'cafc' Federal Circuit, 'scotus'), `dateFiledAfter`/`dateFiledBefore` (ISO YYYY-MM-DD), `natureOfSuit` (folded into q — no verified dedicated filter, disclosed in notes), `cursor` (opaque continuation), `order` (default 'dateFiled desc'). Returns { opinions:[{ caseName, court, courtId, dateFiled, docketNumber, natureOfSuit, status, judge, citation, absoluteUrl }] } + honest _meta. HONESTY: totalAvailable is the API's REAL `count` (total match count) — NOT rows on this page. Pagination is OPAQUE CURSOR (pass _meta.nextCursor back as `cursor`; nextCursor:null/hasMore:false = last page). CourtListener v4 stops counting on deep cursor pages (count:null) → totalAvailable:null DISCLOSED, never faked as results.length. dateFiled is a date STRING; citation → flattened to string/string[]; judge/natureOfSuit/docketNumber null when absent; absoluteUrl is the full CL link. Genuine no-match → honest empty; 400 → invalid_input; 429 → rate_limited (Retry-After honored); 5xx/timeout THROWS; 200 non-JSON/count not number or null → schema_drift; off-host `next` REFUSED (SSRF). Token rides ONLY in the Authorization: Token header.",
6646
+ description: "Search US federal court opinions via CourtListener (www.courtlistener.com/api/rest/v4/search, type=o). ★PROVENANCE: DATA is US federal court PUBLIC RECORDS; the API is CourtListener (Free Law Project, NON-PROFIT) — NOT a .gov API; the .gov primary source (PACER) is PAYWALLED. KEYLESS (optional COURTLISTENER_API_TOKEN only raises the rate limit). Filters (all optional): `party` → caseName:\"…\" FIELDED QUERY — finds actual-party cases (~327 for 'Lockheed Martin'); a bare company name in `query` matches text mentions (~5,954). `query` (free-text → q, AND-ed with caseName when party given). `court` (^[a-z0-9]+$ — e.g. 'uscfc','cafc','scotus'). `dateFiledAfter`/`dateFiledBefore` (ISO). `natureOfSuit` (folded into q — no dedicated filter, disclosed). `cursor`. `order` (default 'dateFiled desc'). Returns { opinions:[{ caseName, court, courtId, dateFiled, docketNumber, natureOfSuit, status, judge, citation, absoluteUrl }] } + honest _meta. HONESTY: totalAvailable is the API's REAL count; CURSOR pagination (pass _meta.nextCursor as cursor); count:null on deep pages → totalAvailable:null DISCLOSED. For FCA/qui tam DOCKETS (rarely produce opinions) use courtlistener_search_dockets instead. Token rides ONLY the Authorization header.",
6571
6647
  inputSchema: CourtlistenerSearchOpinionsInput,
6572
6648
  handler: (input) => courtlistener.searchOpinions(input),
6573
6649
  }),
6650
+ defineTool({
6651
+ name: "courtlistener_search_dockets",
6652
+ description: "Search US federal court RECAP DOCKETS via CourtListener (www.courtlistener.com/api/rest/v4/search, type=r). ★USE THIS for FCA / False Claims Act / qui tam matters — these are DOCKETS, not opinions; settlements rarely produce published opinions. ★PROVENANCE: DATA is US federal court PUBLIC RECORDS; the API is CourtListener (Free Law Project, NON-PROFIT) — NOT a .gov API; the .gov primary source (PACER) is PAYWALLED. KEYLESS. Key filters: `party` → caseName:\"…\" FIELDED (actual-party cases, e.g. 'Lockheed Martin' → ~9 FCA dockets). `natureOfSuit` → suitNature:\"…\" REAL DOCKET FIELD (e.g. 'False Claims' to find FCA/qui tam). `query` (free-text AND-ed with caseName when party also given). `court` (^[a-z0-9]+$). `dateFiledAfter`/`dateFiledBefore` (ISO). `cursor`. `order` (default 'dateFiled desc'). Returns { dockets:[{ caseName, caseNameFull, court, courtId, dateFiled, dateTerminated, docketNumber, natureOfSuit, cause, assignedTo, jurisdictionType, url }] } + honest _meta. dateTerminated is null when case still open (NEVER \"\"). url is the full https://www.courtlistener.com/... docket page URL. Docket = case record; outcome/settlement NOT in it — read docket page or DOJ for that. totalAvailable is the API REAL count; CURSOR pagination. Token rides ONLY Authorization header.",
6653
+ inputSchema: CourtlistenerSearchDocketsInput,
6654
+ handler: (input) => courtlistener.searchDockets(input),
6655
+ }),
6574
6656
  // ━━━ US tax-exempt nonprofits (projects.propublica.org) — the nonprofit lane (2) ━━━ ADR-0060
6575
6657
  // Who a 501(c) org IS (EIN, NTEE, subsection, ruling date, status) + its Form 990
6576
6658
  // FINANCIALS (revenue/expenses/assets/liabilities by year) — the nonprofit/grantee/
package/src/socrata.ts CHANGED
@@ -204,6 +204,7 @@ export const SOCRATA_DOMAINS = [
204
204
  "data.coloradosprings.gov", // Colorado Springs CO (.gov) — e.g. yn6y-xikx (Open Checkbook Vendors ~19k)
205
205
  "data.framinghamma.gov", // Framingham MA (.gov) — e.g. cqve-ehkr (Checkbook ~324k)
206
206
  "data.fultoncountyga.gov", // Fulton County GA (.gov) — e.g. mxhc-krcg (Vendor Payments/disbursements ~217k)
207
+ "sharefulton.fultoncountyga.gov", // Fulton County GA (.gov, second portal) — e.g. kp4p-scak (Vendor Payments 226,797 rows 2014–present, updated 2026-09-14, attribution: "Fulton County Government (GA)")
207
208
  "atlanta.data.socrata.com", // City of Atlanta GA (Socrata-hosted official portal) — e.g. jmke-icfi (Open Checkbook Ledger ~1.78M)
208
209
  "opendata.cityofmesquite.com", // Mesquite TX (.com, official) — e.g. 6tva-azs5 (Check Register ~144k)
209
210
  // ── County/city procurement sweep, wave 3 (loop cycle 20, 2026-07-20). Expanded
@@ -245,7 +246,7 @@ const SOCRATA_DOMAIN_SET: ReadonlySet<string> = new Set(SOCRATA_DOMAINS);
245
246
  *
246
247
  * data.sfgov.org → data.sf.gov (confirmed 2026-09-21; all paths 301)
247
248
  */
248
- const MIGRATED_SOCRATA_HOSTS: ReadonlyMap<string, string> = new Map([
249
+ export const MIGRATED_SOCRATA_HOSTS: ReadonlyMap<string, string> = new Map([
249
250
  ["data.sfgov.org", "data.sf.gov"],
250
251
  ]);
251
252
 
@@ -593,6 +594,22 @@ export async function query(args: {
593
594
  );
594
595
  }
595
596
 
597
+ // ── Response-side aggregate nudge (§A-nudge) ──
598
+ // When the result is truncated (hasMore) and the caller used no aggregate select,
599
+ // the agent only sees a PAGE of rows — NOT the total. Nudge toward an aggregate
600
+ // query so the agent does not try to sum one page manually.
601
+ if (!isAggregateSelect && hasMore) {
602
+ // Find the most likely amount-like column from the first row's keys.
603
+ const firstRow = rows[0];
604
+ const amountKey =
605
+ firstRow !== undefined
606
+ ? (Object.keys(firstRow).find((k) => /amount|amt|total|paid|payment/i.test(k)) ?? "<amount column>")
607
+ : "<amount column>";
608
+ notes.push(
609
+ `This is a PAGE of raw rows (truncated — more rows exist). Do NOT sum this page to get a total. For a grand total: re-query with select='sum(${amountKey})' plus a where filter for vendor/fiscal year. For a top-N ranking: select='vendor_name, sum(${amountKey}) as total' with order='total DESC'. These are aggregate queries — the result is the final answer, not another page.`,
610
+ );
611
+ }
612
+
596
613
  return withMeta(
597
614
  { domain: args.domain, datasetId: args.datasetId, rows },
598
615
  {
@@ -712,6 +729,25 @@ export async function discoverDatasets(args: {
712
729
  `Showing ${returned} of ${totalAvailable} matches; raise limit (≤100) or narrow q for the rest.`,
713
730
  );
714
731
  }
732
+ // HONESTY (false-zero guard). An empty catalog result is easy to read as "this
733
+ // portal has no such data", and the per-host note above even calls the index
734
+ // COMPLETE — but the catalog behaves as if EVERY q term must match, and datasets
735
+ // rarely repeat their own jurisdiction's name. Measured 2026-09-21 on
736
+ // data.illinois.gov: q="Illinois state solicitations" -> 0 and
737
+ // q="Illinois procurement" -> 0, while q="solicitations" -> 1 (6rb8-ntpm). An eval
738
+ // agent took that zero at face value and told the user no Illinois procurement
739
+ // datasets exist. So a zero is reported as possibly false, with the fix.
740
+ if (returned === 0) {
741
+ // Only "more than one word?" matters here, so test for it directly rather than
742
+ // splitting: a raw whitespace split is reserved for disclosure tokenizing
743
+ // (tokenizeForDisclosure, ADR-0022) and this is not a disclosure.
744
+ const multiWord = /\S\s+\S/.test(args.q);
745
+ notes.push(
746
+ multiWord
747
+ ? `0 matches for a multi-word q — likely a FALSE zero, not proof the data is absent: the catalog behaves as if every term must match, and datasets rarely repeat their jurisdiction's name in their title. Put the place in \`domain\` (not in q) and retry with ONE topical term, e.g. 'solicitations', 'bids', 'contract', 'vendor', 'payments', 'purchase'.`
748
+ : `0 matches for q=${JSON.stringify(args.q)} — this is not proof the data is absent: publishers title the same thing differently. Retry with a synonym ('solicitations', 'bids', 'contract', 'vendor', 'payments', 'purchase') before concluding the portal has none.`,
749
+ );
750
+ }
715
751
 
716
752
  return withMeta(
717
753
  { query: args.q, domain: args.domain ?? null, results },
package/src/toolsets.ts CHANGED
@@ -195,6 +195,7 @@ export const TOOL_TOOLSET_MAP: Readonly<Record<string, ToolsetName>> = {
195
195
  echo_facility_report: "vetting",
196
196
  epa_tri_facilities: "vetting",
197
197
  courtlistener_search_opinions: "vetting",
198
+ courtlistener_search_dockets: "vetting",
198
199
  nonprofit_search: "vetting",
199
200
  nonprofit_financials: "vetting",
200
201
  lda_search_filings: "vetting",