@cliwant/mcp-sam-gov 1.4.0 → 1.6.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 (140) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +240 -231
  3. package/README.ko.md +240 -231
  4. package/README.md +725 -706
  5. package/dist/cbp-border.d.ts +51 -0
  6. package/dist/cbp-border.d.ts.map +1 -0
  7. package/dist/cbp-border.js +123 -0
  8. package/dist/cbp-border.js.map +1 -0
  9. package/dist/datagov-catalog.d.ts.map +1 -1
  10. package/dist/datagov-catalog.js +16 -2
  11. package/dist/datagov-catalog.js.map +1 -1
  12. package/dist/ecfr.d.ts +2 -2
  13. package/dist/ecfr.d.ts.map +1 -1
  14. package/dist/ecfr.js +24 -10
  15. package/dist/ecfr.js.map +1 -1
  16. package/dist/edgar.d.ts.map +1 -1
  17. package/dist/edgar.js +26 -6
  18. package/dist/edgar.js.map +1 -1
  19. package/dist/epa-envirofacts.d.ts.map +1 -1
  20. package/dist/epa-envirofacts.js +14 -1
  21. package/dist/epa-envirofacts.js.map +1 -1
  22. package/dist/errors.d.ts +10 -0
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js +11 -0
  25. package/dist/errors.js.map +1 -1
  26. package/dist/far.d.ts.map +1 -1
  27. package/dist/far.js +3 -1
  28. package/dist/far.js.map +1 -1
  29. package/dist/federal-register.d.ts +2 -2
  30. package/dist/federal-register.d.ts.map +1 -1
  31. package/dist/federal-register.js +26 -10
  32. package/dist/federal-register.js.map +1 -1
  33. package/dist/feedback.d.ts +64 -0
  34. package/dist/feedback.d.ts.map +1 -0
  35. package/dist/feedback.js +131 -0
  36. package/dist/feedback.js.map +1 -0
  37. package/dist/fema.d.ts +36 -0
  38. package/dist/fema.d.ts.map +1 -1
  39. package/dist/fema.js +124 -0
  40. package/dist/fema.js.map +1 -1
  41. package/dist/gov-domains.d.ts +66 -0
  42. package/dist/gov-domains.d.ts.map +1 -0
  43. package/dist/gov-domains.js +211 -0
  44. package/dist/gov-domains.js.map +1 -0
  45. package/dist/nist-controls.d.ts +48 -0
  46. package/dist/nist-controls.d.ts.map +1 -0
  47. package/dist/nist-controls.js +174 -0
  48. package/dist/nist-controls.js.map +1 -0
  49. package/dist/nws-weather.d.ts +57 -0
  50. package/dist/nws-weather.d.ts.map +1 -0
  51. package/dist/nws-weather.js +131 -0
  52. package/dist/nws-weather.js.map +1 -0
  53. package/dist/openfda-drugsfda.d.ts +72 -0
  54. package/dist/openfda-drugsfda.d.ts.map +1 -0
  55. package/dist/openfda-drugsfda.js +230 -0
  56. package/dist/openfda-drugsfda.js.map +1 -0
  57. package/dist/openfda.d.ts.map +1 -1
  58. package/dist/openfda.js +31 -8
  59. package/dist/openfda.js.map +1 -1
  60. package/dist/server.d.ts.map +1 -1
  61. package/dist/server.js +374 -11
  62. package/dist/server.js.map +1 -1
  63. package/dist/treasury.d.ts +2 -0
  64. package/dist/treasury.d.ts.map +1 -1
  65. package/dist/treasury.js +7 -0
  66. package/dist/treasury.js.map +1 -1
  67. package/dist/usaspending.d.ts +32 -1
  68. package/dist/usaspending.d.ts.map +1 -1
  69. package/dist/usaspending.js +143 -16
  70. package/dist/usaspending.js.map +1 -1
  71. package/package.json +111 -111
  72. package/src/attachments.ts +652 -652
  73. package/src/bea.ts +372 -372
  74. package/src/bls.ts +1943 -1943
  75. package/src/cache.ts +73 -73
  76. package/src/cbp-border.ts +177 -0
  77. package/src/census-economic.ts +431 -431
  78. package/src/census.ts +735 -735
  79. package/src/ckan.ts +495 -495
  80. package/src/clinicaltrials.ts +923 -923
  81. package/src/cms-facility.ts +379 -379
  82. package/src/cms-hospital.ts +344 -344
  83. package/src/cms-supplier.ts +527 -527
  84. package/src/cms-utilization.ts +389 -389
  85. package/src/cms.ts +634 -634
  86. package/src/coerce.ts +47 -47
  87. package/src/courtlistener.ts +465 -465
  88. package/src/cpsc.ts +333 -333
  89. package/src/datagov-catalog.ts +312 -296
  90. package/src/datagov.ts +907 -907
  91. package/src/datagovKey.ts +68 -68
  92. package/src/datasource.ts +721 -721
  93. package/src/disclosure.ts +61 -61
  94. package/src/dol.ts +515 -515
  95. package/src/ecfr.ts +248 -231
  96. package/src/echo.ts +496 -496
  97. package/src/edgar.ts +3046 -3014
  98. package/src/epa-envirofacts.ts +358 -342
  99. package/src/errors.ts +324 -303
  100. package/src/fac.ts +529 -529
  101. package/src/far.ts +1009 -1007
  102. package/src/fdic.ts +2052 -2052
  103. package/src/federal-register.ts +725 -706
  104. package/src/feedback.ts +160 -0
  105. package/src/fema.ts +680 -541
  106. package/src/fpds.ts +620 -620
  107. package/src/fred.ts +464 -464
  108. package/src/gao.ts +744 -744
  109. package/src/gov-domains.ts +237 -0
  110. package/src/govinfo.ts +497 -497
  111. package/src/grants.ts +290 -290
  112. package/src/gsa-csv.ts +992 -992
  113. package/src/gsa-perdiem.ts +361 -361
  114. package/src/integrity.ts +928 -928
  115. package/src/keys.ts +268 -268
  116. package/src/lda.ts +385 -385
  117. package/src/meta.ts +292 -292
  118. package/src/nhtsa.ts +352 -352
  119. package/src/nih.ts +375 -375
  120. package/src/nist-controls.ts +219 -0
  121. package/src/nonprofit.ts +460 -460
  122. package/src/nppes.ts +834 -834
  123. package/src/nsf.ts +706 -706
  124. package/src/nvd.ts +1124 -1124
  125. package/src/nws-weather.ts +167 -0
  126. package/src/ofac.ts +1166 -1166
  127. package/src/openfda-device.ts +356 -356
  128. package/src/openfda-drugsfda.ts +313 -0
  129. package/src/openfda.ts +518 -495
  130. package/src/pricing.ts +1075 -1075
  131. package/src/sam-gov/client.ts +774 -774
  132. package/src/sam-gov/index.ts +32 -32
  133. package/src/sam-gov/types.ts +152 -152
  134. package/src/sba.ts +357 -357
  135. package/src/server.ts +6688 -6297
  136. package/src/snapshot.ts +223 -223
  137. package/src/socrata.ts +532 -532
  138. package/src/treasury.ts +582 -575
  139. package/src/usaspending.ts +2852 -2680
  140. package/src/usitc.ts +420 -420
package/dist/server.js CHANGED
@@ -58,28 +58,34 @@ import * as lda from "./lda.js";
58
58
  import * as courtlistener from "./courtlistener.js";
59
59
  import * as nonprofit from "./nonprofit.js";
60
60
  import * as fema from "./fema.js";
61
+ import * as nws from "./nws-weather.js";
62
+ import * as govDomains from "./gov-domains.js";
61
63
  import * as fdic from "./fdic.js";
62
64
  import * as bls from "./bls.js";
63
65
  import * as ofac from "./ofac.js";
64
66
  import * as nvd from "./nvd.js";
67
+ import * as nistControls from "./nist-controls.js";
65
68
  import * as nppes from "./nppes.js";
66
69
  import * as cms from "./cms.js";
67
70
  import * as fac from "./fac.js";
68
71
  import * as usitc from "./usitc.js";
69
72
  import * as openfda from "./openfda.js";
70
73
  import * as openfdaDevice from "./openfda-device.js";
74
+ import * as openfdaDrugsfda from "./openfda-drugsfda.js";
71
75
  import * as nhtsa from "./nhtsa.js";
72
76
  import * as cpsc from "./cpsc.js";
77
+ import * as cbpBorder from "./cbp-border.js";
73
78
  import { fetchAttachmentText } from "./attachments.js";
74
79
  import * as keys from "./keys.js";
75
80
  import { toToolError, ToolErrorCarrier, errorFromResponse } from "./errors.js";
81
+ import * as feedback from "./feedback.js";
76
82
  import { buildMeta, isMetaBundle, withMeta, } from "./meta.js";
77
83
  import { pathToFileURL, fileURLToPath } from "node:url";
78
84
  import { realpathSync } from "node:fs";
79
85
  const SERVER_NAME = "mcp-sam-gov";
80
86
  // Kept in lockstep with package.json / manifest.json / server.json.
81
87
  // Keep in sync with package.json "version" (asserted at release; see CHANGELOG).
82
- const SERVER_VERSION = "1.4.0";
88
+ const SERVER_VERSION = "1.6.0";
83
89
  // ─── Tool input schemas (Zod) ────────────────────────────────────
84
90
  const SamSearchInput = z.object({
85
91
  query: z.string().optional().describe("Free-text title query"),
@@ -94,6 +100,7 @@ const SamSearchInput = z.object({
94
100
  .describe("Place-of-performance state, 2-letter, e.g. 'MD'"),
95
101
  setAside: z.array(z.string()).optional().describe("Set-aside codes: SBA, 8A, HZS, SDVOSBC, WOSB, EDWOSB, VSA, VSS"),
96
102
  limit: z.number().min(1).max(50).optional(),
103
+ offset: z.number().min(0).optional().describe("Page offset into the result set (default 0)."),
97
104
  });
98
105
  // Pre-solicitation shaping radar (doc 06 §3.1). Surfaces Sources Sought /
99
106
  // Presolicitation / Special Notices BEFORE the RFP exists — the free, real-time
@@ -178,7 +185,9 @@ const UsasIndividualAwardsInput = UsasFiltersBase.extend({
178
185
  limit: z.number().min(1).max(50).optional(),
179
186
  });
180
187
  const UsasSubAgencyInput = z.object({
181
- agency: z.string(),
188
+ agency: z
189
+ .string()
190
+ .describe("Canonical agency NAME (e.g. 'Department of Veterans Affairs'), NOT a toptier code — this filter matches by name; a numeric code silently matches nothing. Resolve via usas_lookup_agency / usas_list_toptier_agencies."),
182
191
  fiscalYear: z.number().int().min(2007).optional(),
183
192
  });
184
193
  const UsasLookupAgencyInput = z.object({
@@ -192,7 +201,14 @@ const UsasRecipientAwardsInput = z.object({
192
201
  limit: z.number().min(1).max(50).optional(),
193
202
  });
194
203
  const UsasSubawardsInput = z.object({
195
- primeRecipientName: z.string().optional(),
204
+ // DRIFT/SEMANTICS FIX (dogfooding 2026-07-16): this filters the SUBAWARDEE name,
205
+ // NOT the prime. On spending_by_award{subawards:true} the only keyless recipient
206
+ // filter is `recipient_search_text`, which USAspending matches against the
207
+ // SUB-recipient (live-verified: recipient_search_text:["Leidos"] returns rows
208
+ // whose Sub-Awardee Name IS Leidos, under OTHER primes). The old name
209
+ // `primeRecipientName` promised the opposite. Renamed to `subRecipientName`; the
210
+ // #182 unknown-key guard makes the old name fail loud with the valid-key list.
211
+ subRecipientName: z.string().optional(),
196
212
  agency: z.string().optional(),
197
213
  naics: z.string().optional(),
198
214
  fiscalYear: z.number().int().min(2007).optional(),
@@ -297,13 +313,19 @@ const UsasSpendingOverTimeInput = z.object({
297
313
  .optional(),
298
314
  });
299
315
  const UsasCategorySpendingInput = z.object({
300
- agency: z.string().optional(),
316
+ agency: z
317
+ .string()
318
+ .optional()
319
+ .describe("Canonical agency NAME (e.g. 'Department of Veterans Affairs'), NOT a toptier code — this filter matches by name; a numeric code silently matches nothing. Resolve via usas_lookup_agency."),
301
320
  naics: z.string().optional(),
302
321
  fiscalYear: z.number().int().min(2007).optional(),
303
322
  limit: z.number().min(1).max(50).optional(),
304
323
  });
305
324
  const UsasCfdaInput = z.object({
306
- agency: z.string().optional(),
325
+ agency: z
326
+ .string()
327
+ .optional()
328
+ .describe("Canonical agency NAME (e.g. 'Department of Veterans Affairs'), NOT a toptier code — this filter matches by name; a numeric code silently matches nothing. Resolve via usas_lookup_agency."),
307
329
  fiscalYear: z.number().int().min(2007).optional(),
308
330
  limit: z.number().min(1).max(50).optional(),
309
331
  });
@@ -355,6 +377,21 @@ const UsasGlossaryInput = z.object({
355
377
  const UsasListAgenciesInput = z.object({
356
378
  limit: z.number().min(1).max(150).optional(),
357
379
  });
380
+ const UsasListDisasterCodesInput = z.object({});
381
+ const UsasDisasterSpendingInput = z.object({
382
+ defCodes: z
383
+ .array(z.string().min(1))
384
+ .min(1)
385
+ .describe("Disaster Emergency Fund Codes (DEFC) to include — REQUIRED. e.g. ['L','M'] (COVID-19 relief) or ['1'] (IIJA / infrastructure). Discover the full code set via usas_list_disaster_codes."),
386
+ spendingType: z
387
+ .enum(["obligation", "outlay"])
388
+ .optional()
389
+ .describe("obligation (default) or outlay. Some DEFCs report $0 obligations but real outlays — try both."),
390
+ geoLayer: z
391
+ .enum(["state", "county", "district"])
392
+ .optional()
393
+ .describe("Geographic breakout: state (default), county, or congressional district."),
394
+ });
358
395
  // Federal Register
359
396
  const FedRegSearchInput = z.object({
360
397
  query: z.string().optional(),
@@ -797,6 +834,39 @@ const CisaKevLookupInput = z.object({
797
834
  .describe("Max matches returned (default 100, max 1000)."),
798
835
  offset: z.number().min(0).optional().describe("Zero-based page offset (default 0)."),
799
836
  });
837
+ const NistControlsInput = z.object({
838
+ controlId: z
839
+ .string()
840
+ .min(1)
841
+ .optional()
842
+ .describe("Exact control identifier, e.g. 'AC-2', 'SC-7', 'AC-2(1)' (case-insensitive; zero-padding is normalized)."),
843
+ family: z
844
+ .string()
845
+ .min(1)
846
+ .optional()
847
+ .describe("Control family — the 2-letter code ('AC', 'SC', 'IA') OR a substring of the family name ('Access Control', 'Audit'). Case-insensitive."),
848
+ keyword: z
849
+ .string()
850
+ .min(1)
851
+ .optional()
852
+ .describe("Case-insensitive substring searched over the control title + requirement statement."),
853
+ limit: z.number().int().min(1).max(200).optional().describe("Max controls returned (default 25, max 200)."),
854
+ offset: z.number().int().min(0).optional().describe("Zero-based page offset (default 0)."),
855
+ });
856
+ const CbpBorderWaitInput = z.object({
857
+ border: z
858
+ .string()
859
+ .min(1)
860
+ .optional()
861
+ .describe("Filter by border — case-insensitive substring, e.g. 'Canadian' or 'Mexican' (the feed labels ports 'Canadian Border' / 'Mexican Border')."),
862
+ portName: z
863
+ .string()
864
+ .min(1)
865
+ .optional()
866
+ .describe("Filter by port name — case-insensitive substring, e.g. 'Laredo', 'Detroit'."),
867
+ limit: z.number().int().min(1).max(200).optional().describe("Max ports returned (default 100, max 200)."),
868
+ offset: z.number().int().min(0).optional().describe("Zero-based page offset (default 0)."),
869
+ });
800
870
  // ━━━ NPPES NPI Registry — the healthcare-provider identity/credentialing lane (1) ━━━ ADR-0036
801
871
  // nppes_lookup_provider: exact NPI detail OR search over CMS/HHS's keyless public
802
872
  // registry of every US healthcare provider (npiregistry.cms.hhs.gov/api, version=2.1
@@ -1094,8 +1164,10 @@ const TreasuryDatasetEnum = z
1094
1164
  "mts_table_1",
1095
1165
  "rates_of_exchange",
1096
1166
  "debt_outstanding",
1167
+ "interest_expense",
1168
+ "tror",
1097
1169
  ])
1098
- .describe("Which confirmed Treasury Fiscal Data dataset to query: debt_to_penny (daily total public debt), avg_interest_rates (avg rate by security type), mts_table_1 (Monthly Treasury Statement receipts/outlays/deficit), rates_of_exchange (quarterly FX by currency), debt_outstanding (historical fiscal-year-end debt).");
1170
+ .describe("Which confirmed Treasury Fiscal Data dataset to query: debt_to_penny (daily total public debt), avg_interest_rates (avg rate by security type), mts_table_1 (Monthly Treasury Statement receipts/outlays/deficit), rates_of_exchange (quarterly FX by currency), debt_outstanding (historical fiscal-year-end debt), interest_expense (ACTUAL interest PAID / debt-service cost by security type — distinct from the rate), tror (Treasury Report on Receivables: federal receivables + delinquent-debt collections BY AGENCY).");
1099
1171
  const TreasuryQueryDatasetInput = z.object({
1100
1172
  dataset: TreasuryDatasetEnum,
1101
1173
  fields: z
@@ -2037,6 +2109,124 @@ const FemaDisasterDeclarationsInput = z.object({
2037
2109
  .default(0)
2038
2110
  .describe("0-based row offset ($skip) for pagination, default 0."),
2039
2111
  });
2112
+ const FemaSearchHazardMitigationInput = z.object({
2113
+ state: z
2114
+ .string()
2115
+ .min(1)
2116
+ .optional()
2117
+ .describe("Filter by state (→ state eq '...'). Accepts EITHER a 2-letter code ('AL', like the other FEMA tools) OR the full name ('Alabama'); the module maps a 2-letter code to the full name this dataset requires."),
2118
+ programArea: z
2119
+ .string()
2120
+ .min(1)
2121
+ .optional()
2122
+ .describe("Filter by mitigation program (→ programArea eq '...'): HMGP (Hazard Mitigation Grant Program), FMA (Flood Mitigation Assistance), PDM (Pre-Disaster Mitigation), BRIC (Building Resilient Infrastructure and Communities), LPDM, FMA-SL."),
2123
+ disasterNumber: z
2124
+ .number()
2125
+ .int()
2126
+ .positive()
2127
+ .optional()
2128
+ .describe("Filter by FEMA disaster number (→ disasterNumber eq N)."),
2129
+ status: z
2130
+ .string()
2131
+ .min(1)
2132
+ .optional()
2133
+ .describe("Filter by project status (→ status eq '...'). e.g. 'Closed', 'Open'."),
2134
+ programFy: z
2135
+ .number()
2136
+ .int()
2137
+ .optional()
2138
+ .describe("Filter by program fiscal year (→ programFy eq N). e.g. 2005."),
2139
+ region: z
2140
+ .number()
2141
+ .int()
2142
+ .min(1)
2143
+ .max(10)
2144
+ .optional()
2145
+ .describe("Filter by FEMA region number 1–10 (→ region eq N)."),
2146
+ minProjectAmount: z
2147
+ .number()
2148
+ .optional()
2149
+ .describe("Minimum project amount (→ projectAmount ge N)."),
2150
+ maxProjectAmount: z
2151
+ .number()
2152
+ .optional()
2153
+ .describe("Maximum project amount (→ projectAmount le N)."),
2154
+ limit: z
2155
+ .number()
2156
+ .int()
2157
+ .min(1)
2158
+ .max(1000)
2159
+ .default(100)
2160
+ .describe("Rows per page ($top), 1..1000, default 100."),
2161
+ offset: z
2162
+ .number()
2163
+ .int()
2164
+ .min(0)
2165
+ .default(0)
2166
+ .describe("0-based row offset ($skip) for pagination, default 0."),
2167
+ });
2168
+ const NwsActiveAlertsInput = z.object({
2169
+ state: z
2170
+ .string()
2171
+ .regex(/^[A-Za-z]{2}$/)
2172
+ .optional()
2173
+ .describe("2-letter US state/territory code to scope alerts (→ NWS ?area=), e.g. 'CA'. Omit for all active US alerts."),
2174
+ event: z
2175
+ .string()
2176
+ .min(1)
2177
+ .optional()
2178
+ .describe("Filter by event type — case-insensitive substring, e.g. 'Flood', 'Wind', 'Winter Storm'."),
2179
+ severity: z
2180
+ .enum(["Extreme", "Severe", "Moderate", "Minor", "Unknown"])
2181
+ .optional()
2182
+ .describe("Filter by severity (exact): Extreme | Severe | Moderate | Minor | Unknown."),
2183
+ limit: z.number().int().min(1).max(500).optional().describe("Max alerts returned (default 50, max 500)."),
2184
+ offset: z.number().int().min(0).optional().describe("Zero-based page offset (default 0)."),
2185
+ });
2186
+ const SearchGovDomainsInput = z.object({
2187
+ scope: z
2188
+ .enum(["all", "federal"])
2189
+ .optional()
2190
+ .describe("'all' (federal + SLED: state/county/city/school-district/special-district/tribal, ~16k rows, DEFAULT) or 'federal' (federal-only, ~1.3k rows)."),
2191
+ organization: z
2192
+ .string()
2193
+ .min(1)
2194
+ .optional()
2195
+ .describe("Organization name — case-insensitive SUBSTRING match (e.g. 'veterans', 'cybersecurity')."),
2196
+ domain: z
2197
+ .string()
2198
+ .min(1)
2199
+ .optional()
2200
+ .describe("Domain name — case-insensitive SUBSTRING match (e.g. 'cdc.gov', 'irs')."),
2201
+ domainType: z
2202
+ .string()
2203
+ .min(1)
2204
+ .optional()
2205
+ .describe("Domain type — case-insensitive match (e.g. 'Federal - Executive', 'County', 'Tribal', 'State or territory', 'School district')."),
2206
+ state: z
2207
+ .string()
2208
+ .min(1)
2209
+ .optional()
2210
+ .describe("2-letter state/territory code — case-insensitive exact match (e.g. 'CA')."),
2211
+ city: z
2212
+ .string()
2213
+ .min(1)
2214
+ .optional()
2215
+ .describe("City — case-insensitive SUBSTRING match."),
2216
+ limit: z
2217
+ .number()
2218
+ .int()
2219
+ .min(1)
2220
+ .max(500)
2221
+ .optional()
2222
+ .describe("Rows per page, 1..500, default 50."),
2223
+ offset: z
2224
+ .number()
2225
+ .int()
2226
+ .min(0)
2227
+ .optional()
2228
+ .describe("0-based row offset for pagination, default 0."),
2229
+ });
2040
2230
  // ─── EPA ECHO REST (keyless facility compliance/enforcement) — input schemas ──
2041
2231
  // ADR-0009. KEYLESS, single fixed host (echodata.epa.gov) + three fixed service
2042
2232
  // paths (the SSRF core — no free host/path). `state` is a curated US state/
@@ -2280,7 +2470,7 @@ const DatagovSearchDatasetsInput = z.object({
2280
2470
  .min(1)
2281
2471
  .max(500)
2282
2472
  .optional()
2283
- .describe("Free-text search over the dataset catalog (→ _q), e.g. 'wildfire'. LIVE-CONFIRMED to narrow."),
2473
+ .describe("Free-text search over the dataset catalog (→ q), e.g. 'wildfire'. LIVE-CONFIRMED to narrow (2026-07-16: the v4 API param is `q`; the old `_q` is silently ignored)."),
2284
2474
  organization: z
2285
2475
  .string()
2286
2476
  .min(1)
@@ -3238,6 +3428,41 @@ const OpenfdaDeviceClearancesInput = z.object({
3238
3428
  .optional()
3239
3429
  .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3240
3430
  });
3431
+ const OpenfdaDrugApprovalsInput = z.object({
3432
+ sponsorName: z
3433
+ .string()
3434
+ .min(1)
3435
+ .optional()
3436
+ .describe("Sponsor / applicant company name (→ sponsor_name), e.g. 'pfizer'. Matched as an escaped Lucene phrase."),
3437
+ brandName: z
3438
+ .string()
3439
+ .min(1)
3440
+ .optional()
3441
+ .describe("Product brand name (→ products.brand_name), e.g. 'lipitor'. Matched as an escaped Lucene phrase."),
3442
+ activeIngredient: z
3443
+ .string()
3444
+ .min(1)
3445
+ .optional()
3446
+ .describe("Active ingredient name (→ products.active_ingredients.name), e.g. 'atorvastatin calcium'. Matched as an escaped Lucene phrase."),
3447
+ applicationNumber: z
3448
+ .string()
3449
+ .min(1)
3450
+ .optional()
3451
+ .describe("FDA application number (→ application_number), e.g. 'NDA050347'. Matched as an escaped Lucene phrase."),
3452
+ limit: z
3453
+ .number()
3454
+ .int()
3455
+ .min(1)
3456
+ .max(100)
3457
+ .optional()
3458
+ .describe("Max application records to return (default 25, max 100). Offset-paginated via skip."),
3459
+ skip: z
3460
+ .number()
3461
+ .int()
3462
+ .min(0)
3463
+ .optional()
3464
+ .describe("Row offset for pagination (default 0). Page with _meta.pagination.nextOffset."),
3465
+ });
3241
3466
  // ─── NHTSA vehicle safety (api.nhtsa.gov) — KEYLESS vehicle/parts supplier vetting ──
3242
3467
  // ADR-0057. Two tools (recalls + complaints) share make/model/modelYear inputs. NO
3243
3468
  // API key at all. ★The complaints VIN (PII) is excluded from the output. modelYear is
@@ -3561,6 +3786,25 @@ const NonprofitFinancialsInput = z.object({
3561
3786
  });
3562
3787
  // api_key_status takes no input — it is a pure status query over process.env.
3563
3788
  const ApiKeyStatusInput = z.object({});
3789
+ // `feedback` builds a PREFILLED GitHub issue URL for the human to submit (PULL —
3790
+ // the server never posts). All inputs optional; `summary` is a short, NON-sensitive
3791
+ // title line (the issue is public — the description + return value say so).
3792
+ const FeedbackInput = z.object({
3793
+ kind: z
3794
+ .enum(["bug", "feature", "wrong_output"])
3795
+ .optional()
3796
+ .describe("What kind of report: bug (default), feature (a capability this server lacks), or wrong_output (a tool returned a wrong/suspicious result)."),
3797
+ tool: z
3798
+ .string()
3799
+ .max(80)
3800
+ .optional()
3801
+ .describe("The tool name this is about, if any (e.g. 'sam_search_opportunities')."),
3802
+ summary: z
3803
+ .string()
3804
+ .max(500)
3805
+ .optional()
3806
+ .describe("A short one-line summary for the issue title/body. PUBLIC — never include API keys, personal data, or sensitive query values."),
3807
+ });
3564
3808
  // Build a ToolDef whose `handler` is type-checked against the schema's inferred
3565
3809
  // input `I` at the call site (e.g. `input.searchText` is known-present). The
3566
3810
  // `I` binding is erased to `any` in the ToolDef[] array, so entries without a
@@ -4097,7 +4341,7 @@ export const TOOLS = [
4097
4341
  }),
4098
4342
  defineTool({
4099
4343
  name: "usas_search_subawards",
4100
- description: "Enumerate subcontracts on prime awards. Use for 'who teams with Leidos at DISA' or 'show small-business subs on Accenture's DHS contracts' — surfaces the prime/sub network for teaming-map artifacts.",
4344
+ description: "Enumerate federal subawards (subcontracts), optionally filtered by SUBAWARDEE name. Use for 'where does Leidos appear as a SUBcontractor, and under which primes' — surfaces the prime/sub network for teaming-map artifacts. NOTE: subRecipientName matches the SUB-recipient, NOT the prime (the keyless spending_by_award subaward view has no prime-name filter); to see the subs UNDER a specific prime, resolve that prime's awards first (usas_search_awards → usas_get_award_detail) and read their sub network. Each row carries subRecipient (the subawardee), amount, actionDate, the prime award id, and the prime award's NAICS.",
4101
4345
  inputSchema: UsasSubawardsInput,
4102
4346
  handler: (input) => usas.searchSubawards(input),
4103
4347
  }),
@@ -4225,6 +4469,18 @@ export const TOOLS = [
4225
4469
  inputSchema: UsasListAgenciesInput,
4226
4470
  handler: (input) => usas.listToptierAgencies(input),
4227
4471
  }),
4472
+ defineTool({
4473
+ name: "usas_list_disaster_codes",
4474
+ description: "List the Disaster Emergency Fund Codes (DEFC) — the supplemental-appropriation tags (COVID-19 relief, IIJA/infrastructure, and other emergency laws) that usas_disaster_spending filters on. Keyless USAspending references/def_codes. Returns the COMPLETE code set (no pagination): each `code` with its `group` ('covid_19' | 'infrastructure' | null), `title`, and `publicLaw`. Use this to discover the codes to pass to usas_disaster_spending. HONESTY: group is null (never fabricated) when a code belongs to no named group; totalAvailable is the exact complete count.",
4475
+ inputSchema: UsasListDisasterCodesInput,
4476
+ handler: () => usas.listDisasterCodes(),
4477
+ }),
4478
+ defineTool({
4479
+ name: "usas_disaster_spending",
4480
+ description: "Disaster / emergency-fund spending BY GEOGRAPHY — obligations or outlays tagged to one or more Disaster Emergency Fund Codes (DEFC: COVID-19, IIJA, etc.), broken out per state / county / congressional district (keyless USAspending disaster/spending_by_geography). Answers 'which geographies captured COVID/IIJA relief money' — a distinct axis the standard award search does not expose. `defCodes` REQUIRED (discover via usas_list_disaster_codes); `spendingType` obligation (default) | outlay; `geoLayer` state (default) | county | district. Each row: name, code, amount, awardCount, population, perCapita. HONESTY: amount/perCapita are number|null (a real 0 stays 0 — some DEFCs like IIJA report $0 OBLIGATIONS with a nonzero awardCount, disclosed in a note; absent → null, never a fabricated 0); the endpoint returns the COMPLETE set of geo units (no pagination) so totalAvailable = returned; an outage/4xx THROWS (never a fake empty).",
4481
+ inputSchema: UsasDisasterSpendingInput,
4482
+ handler: (input) => usas.disasterSpending(input),
4483
+ }),
4228
4484
  // ━━━ Federal Register (4) ━━━
4229
4485
  defineTool({
4230
4486
  name: "fed_register_search_documents",
@@ -4365,6 +4621,12 @@ export const TOOLS = [
4365
4621
  inputSchema: CisaKevLookupInput,
4366
4622
  handler: (input) => nvd.cisaKevLookup(input),
4367
4623
  }),
4624
+ defineTool({
4625
+ name: "nist_800_53_controls",
4626
+ 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').",
4627
+ inputSchema: NistControlsInput,
4628
+ handler: (input) => nistControls.searchControls(input),
4629
+ }),
4368
4630
  // ━━━ NPPES NPI Registry — Healthcare-Provider Vetting (1) ━━━ ADR-0036
4369
4631
  defineTool({
4370
4632
  name: "nppes_lookup_provider",
@@ -4408,7 +4670,7 @@ export const TOOLS = [
4408
4670
  // ━━━ US Treasury — Fiscal Data (keyless) (4) ━━━ ADR-0002
4409
4671
  defineTool({
4410
4672
  name: "treasury_query_dataset",
4411
- description: "Escape-hatch query over 5 confirmed US Treasury Fiscal Data datasets (keyless): debt_to_penny, avg_interest_rates, mts_table_1 (Monthly Treasury Statement), rates_of_exchange, debt_outstanding. Choose `dataset` (enum — no free path), and optionally project `fields` (CSV), `filter` (CSV 'col:op:val', ops lt|lte|gt|gte|eq|in, AND-combined), and `sort` (CSV, '-' = desc), with page[size]/page[number] pagination. Returns raw rows plus a truthful `_meta` (totalAvailable = upstream total-count, offset pagination). Value/amount fields are raw upstream strings — the string \"null\"/empty means 'no value', never 0. Covers rates_of_exchange + debt_outstanding without a dedicated tool.",
4673
+ description: "Escape-hatch query over 7 confirmed US Treasury Fiscal Data datasets (keyless): debt_to_penny, avg_interest_rates, mts_table_1 (Monthly Treasury Statement), rates_of_exchange, debt_outstanding, interest_expense (actual interest PAID / debt-service cost), tror (Treasury Report on Receivables — federal receivables + delinquent-debt collections by agency). Choose `dataset` (enum — no free path), and optionally project `fields` (CSV), `filter` (CSV 'col:op:val', ops lt|lte|gt|gte|eq|in, AND-combined), and `sort` (CSV, '-' = desc), with page[size]/page[number] pagination. Returns raw rows plus a truthful `_meta` (totalAvailable = upstream total-count, offset pagination). Value/amount fields are raw upstream strings — the string \"null\"/empty means 'no value', never 0. Covers rates_of_exchange + debt_outstanding without a dedicated tool.",
4412
4674
  inputSchema: TreasuryQueryDatasetInput,
4413
4675
  handler: (input) => treasury.queryDataset(input),
4414
4676
  }),
@@ -4601,7 +4863,7 @@ export const TOOLS = [
4601
4863
  inputSchema: BlsQcewInput,
4602
4864
  handler: (input) => bls.qcew(input),
4603
4865
  }),
4604
- // ━━━ OpenFEMA — keyless disaster declarations + emergency-assistance spend (2) ━━━ ADR-0016
4866
+ // ━━━ OpenFEMA — keyless disaster declarations + emergency-assistance spend (3) ━━━ ADR-0016
4605
4867
  defineTool({
4606
4868
  name: "fema_search_public_assistance",
4607
4869
  description: "Search FEMA Public Assistance funded projects — federal emergency-assistance spend to state/local/tribal applicants (keyless OpenFEMA, dataset PublicAssistanceFundedProjectsDetails v2, ~800k rows). Structured filters (module-built into an OData $filter; each LIVE-VERIFIED to narrow): `state` (→ stateAbbreviation), `disasterNumber`, `applicantId`, `damageCategoryCode` (e.g. 'B' = Emergency Protective Measures), `incidentType`, `minProjectAmount`/`maxProjectAmount` (projectAmount ge/le), `declaredDateFrom`/`declaredDateTo` (declarationDate ge/le). `limit` (≤1000, def 100 → $top), `offset` (→ $skip). HONESTY: the module ALWAYS sends $inlinecount=allpages so totalAvailable is the EXACT filtered total (metadata.count), never the page length; amount fields are number|null (a real 0 stays 0, absent → null); genuine-empty ⇒ complete:true/total:0; an outage/400/404 THROWS (never a fake empty).",
@@ -4614,6 +4876,26 @@ export const TOOLS = [
4614
4876
  inputSchema: FemaDisasterDeclarationsInput,
4615
4877
  handler: (input) => fema.disasterDeclarations(input),
4616
4878
  }),
4879
+ defineTool({
4880
+ name: "fema_search_hazard_mitigation",
4881
+ description: "Search FEMA Hazard Mitigation Assistance projects — the disaster-RESILIENCE grant axis (HMGP/FMA/PDM/BRIC mitigation grants to state/local/tribal subrecipients, distinct from the disaster-RECOVERY spend in fema_search_public_assistance). Keyless OpenFEMA, dataset HazardMitigationAssistanceProjects v4, ~56k rows. Structured filters (module-built into an OData $filter; each LIVE-VERIFIED to narrow): `state` (→ state — the FULL state NAME, e.g. 'Alabama', NOT the 2-letter code), `programArea` (HMGP/FMA/PDM/BRIC/LPDM/FMA-SL), `disasterNumber`, `status` (e.g. 'Closed'), `programFy`, `region` (FEMA region 1–10), `minProjectAmount`/`maxProjectAmount` (projectAmount ge/le). `limit` (≤1000, def 100 → $top), `offset` (→ $skip). HONESTY: the module ALWAYS sends $inlinecount=allpages so totalAvailable is the EXACT filtered total (metadata.count), never the page length; amount fields (projectAmount/federalShareObligated/initialObligationAmount/netValueBenefits) are number|null (a real 0 stays 0, absent → null); genuine-empty ⇒ complete:true/total:0; an outage/400/404 THROWS (never a fake empty). NOTE: 'state' here is the full name (this dataset 400s on a 2-letter code), whereas fema_search_public_assistance maps 'state' to the 2-letter 'stateAbbreviation'.",
4882
+ inputSchema: FemaSearchHazardMitigationInput,
4883
+ handler: (input) => fema.searchHazardMitigation(input),
4884
+ }),
4885
+ // ━━━ NWS — National Weather Service active alerts (keyless) (1) ━━━
4886
+ defineTool({
4887
+ name: "nws_active_alerts",
4888
+ description: "List CURRENTLY-ACTIVE National Weather Service alerts — watches, warnings, and advisories (keyless; api.weather.gov). The disaster/climate-readiness lane that pairs with the FEMA tools (declarations → public assistance → hazard mitigation → LIVE active weather): where severe-weather events are active NOW, ahead of the declarations/contracts that follow. Filters: `state` (2-letter code → server-side ?area=, e.g. 'CA'; omit for all US), `event` (case-insensitive substring, e.g. 'Flood', 'Wind'), `severity` (Extreme/Severe/Moderate/Minor/Unknown); `limit`/`offset` pagination. Each alert: { id, event, headline, severity, urgency, certainty, category, status, messageType, areaDesc, effective, onset, expires, ends, senderName, description, instruction, response }. HONESTY: this is REAL-TIME data (alerts active at request time — a live snapshot, NOT a historical archive; read effective/expires for each window, disclosed in _meta); every scalar is null-never-empty-string and dates are ISO strings; totalAvailable is the EXACT count of matched active alerts; a NO-active-alerts result is an HONEST EMPTY (returned:0), never an error; an outage/4xx/timeout THROWS and a non-FeatureCollection body ⇒ schema_drift. A descriptive User-Agent is sent per NWS policy (no key/token).",
4889
+ inputSchema: NwsActiveAlertsInput,
4890
+ handler: (input) => nws.activeAlerts(input),
4891
+ }),
4892
+ // ━━━ get.gov — CISA authoritative .gov domain registry (keyless) (1) ━━━
4893
+ defineTool({
4894
+ name: "search_gov_domains",
4895
+ description: "Search the authoritative US .gov domain registry (CISA get.gov) — resolve which ORGANIZATION owns a .gov domain, enumerate federal agencies, and MAP SLED entities (state/county/city/school-district/special-district/tribal) for market targeting. Keyless. scope 'all' (federal + SLED, ~16k rows, default) | 'federal'. Filters (client-side over the published CSV): organization/domain/city (case-insensitive SUBSTRING), domainType (e.g. 'Federal - Executive', 'County', 'Tribal'), state (2-letter). Each row: domain, domainType, organization, suborganization, city, state. HONESTY: source is CISA's OFFICIAL registry published at github.com/cisagov/dotgov-data (authoritative first-party data, not a .gov API host — provenance disclosed in _meta); the registry has no query API so filtering is CLIENT-SIDE and totalAvailable is the EXACT match count; the 'Security contact email' column is intentionally EXCLUDED (org mailbox — this tool resolves organizations, not contacts); an outage/4xx THROWS (never a fake empty); a header-column rename ⇒ schema_drift.",
4896
+ inputSchema: SearchGovDomainsInput,
4897
+ handler: (input) => govDomains.searchGovDomains(input),
4898
+ }),
4617
4899
  // ━━━ FPDS-NG — federal contract AWARD ACTIONS (keyless ATOM) (1) ━━━ ADR-0012
4618
4900
  // The FIRST XML/ATOM source (bounded, ReDoS-safe hand-parser — the far.ts/gao.ts
4619
4901
  // lineage; NOT the getJson port). FPDS is the system-of-record USAspending
@@ -4952,6 +5234,12 @@ export const TOOLS = [
4952
5234
  inputSchema: OpenfdaDeviceClearancesInput,
4953
5235
  handler: (input) => openfdaDevice.deviceClearances(input),
4954
5236
  }),
5237
+ defineTool({
5238
+ name: "openfda_drug_approvals",
5239
+ description: "Search openFDA Drugs@FDA DRUG APPROVALS — FDA-approved drug applications (NDA/ANDA/BLA) with the sponsor, application number, each approved product (brand + generic/active-ingredient name, dosage form, route, marketing status), and the submission/approval history (openFDA /drug/drugsfda.json; api.fda.gov). Answers 'what drugs did sponsor X get approved, and which are still marketed' — pharma vendor product/approval intelligence. KEYLESS (an OPTIONAL free OPENFDA_API_KEY only RAISES the rate limit — keyless works at ~1000 requests/day; NEVER throws for a missing key; api_key_status lists every source's key requirement). Input: STRUCTURED filters — `sponsorName` (→sponsor_name), `brandName` (→products.brand_name), `activeIngredient` (→products.active_ingredients.name), `applicationNumber` (→application_number) — safely escaped into the openFDA search= Lucene string (NO raw passthrough — injection-safe), plus `limit` (1..100, default 25) and `skip` (offset ≥0). Returns { applications:[{ applicationNumber, sponsorName, products:[{ brandName, genericIngredients:[{name,strength}], dosageForm, route, marketingStatus }], submissions:[{ submissionType, submissionNumber, submissionStatus, submissionStatusDate, submissionClass }] }] } + honest _meta. HONESTY: totalAvailable is openFDA's EXACT meta.results.total (skip/limit pagination — never results.length); every scalar is null-never-empty-string; a 'Discontinued' marketingStatus is NOT an approval revocation (disclosed in _meta). ★A no-match query returns openFDA HTTP 404 NOT_FOUND ⇒ an HONEST EMPTY (returned:0/total:0), NOT an error; a 400 ⇒ invalid_input surfacing openFDA's message; a 5xx ⇒ THROWS; a 200 non-JSON ⇒ schema_drift. The optional key rides ONLY the &api_key= query param — never logged or echoed.",
5240
+ inputSchema: OpenfdaDrugApprovalsInput,
5241
+ handler: (input) => openfdaDrugsfda.drugApprovals(input),
5242
+ }),
4955
5243
  // ━━━ NHTSA vehicle safety (api.nhtsa.gov) — vehicle/parts supplier vetting (2) ━━━ ADR-0057
4956
5244
  // ★KEYLESS — no API key at all (no parameter, no header). The cross-agency
4957
5245
  // product-safety family alongside openFDA (medical). Both tools share
@@ -4983,6 +5271,13 @@ export const TOOLS = [
4983
5271
  inputSchema: CpscRecallsInput,
4984
5272
  handler: (input) => cpsc.recalls(input),
4985
5273
  }),
5274
+ // ━━━ CBP Border Wait Times (bwt.cbp.gov) — freight/logistics (1) ━━━
5275
+ defineTool({
5276
+ name: "cbp_border_wait_times",
5277
+ description: "Live CBP land-border-port wait times — current commercial-vehicle (and passenger) 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. Filters (optional): `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).",
5278
+ inputSchema: CbpBorderWaitInput,
5279
+ handler: (input) => cbpBorder.borderWaitTimes(input),
5280
+ }),
4986
5281
  // ━━━ BEA Regional Economic Accounts (apps.bea.gov) — regional GDP/income (1) ━━━ ADR-0051
4987
5282
  // ★The server's THIRD KEY-REQUIRED source: the BEA Data API has NO keyless tier, so
4988
5283
  // WITHOUT a BEA_API_KEY this tool throws an honest invalid_input config error (the
@@ -5087,6 +5382,17 @@ export const TOOLS = [
5087
5382
  inputSchema: ApiKeyStatusInput,
5088
5383
  handler: async () => keys.apiKeyStatus(),
5089
5384
  }),
5385
+ // ━━━ Meta: in-product feedback → GitHub issue (PULL-only, keyless) ━━━
5386
+ // The server's "user" is an AI agent, so we collect real-usage friction THROUGH
5387
+ // it: this returns a PREFILLED GitHub new-issue URL for the HUMAN to open and
5388
+ // submit. The server NEVER posts (no token/account/auto-submit) and embeds no
5389
+ // PII — only the caller's summary + tool name. See feedback.ts.
5390
+ defineTool({
5391
+ name: "feedback",
5392
+ description: "Report a problem, request a feature, or flag a wrong-looking result for THIS server — returns a PREFILLED GitHub issue link (public repo cliwant/mcp-sam-gov) for the USER to open and submit. KEYLESS. The server does NOT post anything: it only builds the link; the human submits it (nothing is sent automatically, no network call). Use when the user reports a bug, says a tool's output looks wrong, or wants a capability this server doesn't have. Input (all optional): `kind` (bug|feature|wrong_output, default bug), `tool` (the tool it's about), `summary` (a short, NON-SENSITIVE title line). Returns { reportUrl, repo, willPost:false, instructions, privacy }. PRIVACY: never put secrets, personal data, or sensitive query values in `summary` — the issue is PUBLIC and the link prefills only your summary + tool + server version.",
5393
+ inputSchema: FeedbackInput,
5394
+ handler: async (input) => feedback.feedbackTool(input),
5395
+ }),
5090
5396
  ];
5091
5397
  // ─── Server bootstrap ────────────────────────────────────────────
5092
5398
  async function main() {
@@ -5107,7 +5413,12 @@ async function main() {
5107
5413
  },
5108
5414
  },
5109
5415
  });
5110
- const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
5416
+ const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, {
5417
+ capabilities: { tools: {} },
5418
+ // Surfaced to the agent at initialize. Tells it how to route real-usage
5419
+ // friction back to the project WITHOUT the server ever posting anything.
5420
+ instructions: "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).",
5421
+ });
5111
5422
  server.setRequestHandler(ListToolsRequestSchema, async () => {
5112
5423
  return {
5113
5424
  tools: TOOLS.map((t) => ({
@@ -5146,6 +5457,12 @@ async function main() {
5146
5457
  // NON-retryable `invalid_input` with readable field-level issues — never a
5147
5458
  // generic `unknown` carrying Zod's raw JSON dump.
5148
5459
  const error = toToolError(err, name);
5460
+ // PULL-only feedback loop (feedback.ts): attach a PREFILLED GitHub issue URL
5461
+ // ONLY for the two "something may be broken" kinds — schema_drift (the
5462
+ // upstream changed shape) and upstream_unavailable (a possible moved/broken
5463
+ // endpoint). Carries only tool + kind + version (no args/PII). The 429 and
5464
+ // invalid_input/not_found envelopes stay byte-identical (no `report`).
5465
+ feedback.maybeAttachReport(error, name, SERVER_VERSION);
5149
5466
  const envelope = { ok: false, error };
5150
5467
  return {
5151
5468
  content: [
@@ -5221,6 +5538,32 @@ function synthesizeDefaultMeta(toolName, sam) {
5221
5538
  }
5222
5539
  return buildMeta({ source, keylessMode, complete: true, truncated: false });
5223
5540
  }
5541
+ // Unwrap a tool's inputSchema down to its underlying ZodObject so we can read the
5542
+ // set of declared top-level keys. Tools wrap the object in .refine()/.superRefine()
5543
+ // (ZodEffects), or occasionally .optional()/.default()/.nullable(), so peel those
5544
+ // layers. Returns null if no ZodObject is reachable (then unknown-key rejection is
5545
+ // skipped for that tool — fail open, never fail closed on our own introspection).
5546
+ function objectSchemaOf(schema) {
5547
+ let s = schema;
5548
+ for (let i = 0; i < 20; i++) {
5549
+ const def = s?._def;
5550
+ if (!def)
5551
+ break;
5552
+ const tn = def.typeName;
5553
+ if (tn === "ZodObject")
5554
+ return s;
5555
+ if (tn === "ZodEffects") {
5556
+ s = def.schema;
5557
+ continue;
5558
+ }
5559
+ if (tn === "ZodOptional" || tn === "ZodDefault" || tn === "ZodNullable") {
5560
+ s = def.innerType;
5561
+ continue;
5562
+ }
5563
+ break;
5564
+ }
5565
+ return null;
5566
+ }
5224
5567
  export async function runTool(name, args, sam) {
5225
5568
  // R1 (ADR-0001) — registry dispatch. Every tool's TOOLS[] entry carries a
5226
5569
  // co-located `handler`: route through it by parsing `args` with the entry's
@@ -5230,6 +5573,26 @@ export async function runTool(name, args, sam) {
5230
5573
  // gone (all 52 tools migrated) — an unknown name has no entry and throws.
5231
5574
  const entry = TOOLS.find((t) => t.name === name);
5232
5575
  if (entry?.handler) {
5576
+ // HONESTY (dogfooding 2026-07-15): reject UNKNOWN top-level input keys LOUD
5577
+ // instead of Zod's default silent-strip. A misspelled filter (naicsCode↔naics,
5578
+ // keyword↔query) was otherwise dropped and the tool scanned the WHOLE corpus,
5579
+ // returning an authoritative-looking WRONG answer with no error. We name the
5580
+ // unknown key(s) and list the valid ones so the mistake self-corrects. Skip
5581
+ // when the schema intentionally accepts extras (unknownKeys==="passthrough").
5582
+ const obj = objectSchemaOf(entry.inputSchema);
5583
+ if (obj && obj._def.unknownKeys !== "passthrough" && args && typeof args === "object") {
5584
+ const known = new Set(Object.keys(obj.shape));
5585
+ const unknown = Object.keys(args).filter((k) => !known.has(k));
5586
+ if (unknown.length > 0) {
5587
+ throw new ToolErrorCarrier({
5588
+ kind: "invalid_input",
5589
+ message: `Unknown input ${unknown.length > 1 ? "keys" : "key"} for ${name}: ` +
5590
+ `${unknown.map((k) => `'${k}'`).join(", ")}. ` +
5591
+ `Valid keys: ${[...known].sort().join(", ")}.`,
5592
+ retryable: false,
5593
+ });
5594
+ }
5595
+ }
5233
5596
  const input = entry.inputSchema.parse(args);
5234
5597
  return await entry.handler(input, { sam });
5235
5598
  }