@pipeworx/mcp-nhtsa 0.2.1 → 0.2.3
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.md +24 -5
- package/package.json +4 -4
- package/server.json +2 -2
- package/src/fars-store.ts +170 -0
- package/src/fuel-economy.ts +1057 -0
- package/src/index.ts +607 -3
- package/src/server.ts +5 -2
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# NHTSA — National Highway Traffic Safety Administration
|
|
2
2
|
|
|
3
|
-
The U.S. Department of Transportation's NHTSA. Vehicle recalls, complaints, investigations, crash test ratings (NCAP), safety standards, VIN decoder. The authoritative source for "is this car safe?" Free, no auth.
|
|
3
|
+
The U.S. Department of Transportation's NHTSA. Vehicle recalls, complaints, investigations, crash test ratings (NCAP), safety standards, VIN decoder, fatal-crash statistics (FARS), and fuel-economy ratings. The authoritative source for "is this car safe?" and "how much gas does it use?" Free, no auth.
|
|
4
4
|
|
|
5
|
-
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to
|
|
5
|
+
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1877+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
|
|
6
6
|
|
|
7
7
|
## Why this matters for AI agents
|
|
8
8
|
|
|
9
|
-
For automotive research — buying a used car, evaluating recall exposure, analyzing fleet safety, doing IP / patent due diligence on vehicle technology — NHTSA is the canonical source. Pair with [USPTO patents](/docs/reference/patents) for vehicle-tech IP and [Federal Register](/docs/reference/federal-register) for proposed automotive rules.
|
|
9
|
+
For automotive research — buying a used car, evaluating recall exposure, analyzing fleet safety, doing IP / patent due diligence on vehicle technology, or researching traffic-safety policy — NHTSA (plus DOE/EPA's fueleconomy.gov) is the canonical source. Pair with [USPTO patents](/docs/reference/patents) for vehicle-tech IP and [Federal Register](/docs/reference/federal-register) for proposed automotive rules.
|
|
10
10
|
|
|
11
11
|
Common flows:
|
|
12
12
|
|
|
@@ -15,10 +15,12 @@ Common flows:
|
|
|
15
15
|
- **Consumer complaints.** "Complaints about brake issues on Honda Civic?" → complaint search by make/model.
|
|
16
16
|
- **VIN decoder.** "What does VIN 1HGBH41JXMN109186 tell me?" → year, make, model, engine, manufacturer details.
|
|
17
17
|
- **Crash test ratings.** NCAP star ratings for vehicles.
|
|
18
|
+
- **Fatal-crash statistics.** "How many traffic fatalities did Texas have in 2023?" → state-level FARS aggregates.
|
|
19
|
+
- **Fuel economy.** "What's the combined MPG for a 2024 Toyota Camry?" → fueleconomy.gov lookup.
|
|
18
20
|
|
|
19
21
|
## Auth
|
|
20
22
|
|
|
21
|
-
None. NHTSA's APIs are fully public, free.
|
|
23
|
+
None. NHTSA's and fueleconomy.gov's APIs are fully public, free, keyless.
|
|
22
24
|
|
|
23
25
|
## Datasets
|
|
24
26
|
|
|
@@ -30,6 +32,20 @@ None. NHTSA's APIs are fully public, free.
|
|
|
30
32
|
| **NCAP Crash Test** | Star ratings, frontal/side/rollover scores |
|
|
31
33
|
| **VIN Decoder** | Decode VIN to make/model/year + manufacturer specs |
|
|
32
34
|
| **TSBs** (Technical Service Bulletins) | Manufacturer service guidance |
|
|
35
|
+
| **FARS** (Fatality Analysis Reporting System) | Fatal motor-vehicle crash counts, fatalities, persons/vehicles/pedestrians involved, by state/month; vehicle makes involved in fatal crashes, by year |
|
|
36
|
+
| **Fuel Economy** (fueleconomy.gov, DOE/EPA) | City/highway/combined MPG, CO2 g/mile, fuel type, by model year + make + model |
|
|
37
|
+
|
|
38
|
+
## FARS data (fleet #2972)
|
|
39
|
+
|
|
40
|
+
NHTSA's live crash-query API returned an Akamai 403 to every probe tried on 2026-10-12, including with browser-like headers. NHTSA separately publishes FARS as annual bulk files, US federal government work and public domain, so `fars_state_fatalities`, `fars_vehicle_makes`, and `fars_coverage` are backed by those files loaded ahead of time rather than a live proxy, per CLAUDE.md "BUILD THE COPY".
|
|
41
|
+
|
|
42
|
+
- **What's loaded:** `scripts/nhtsa-fars/load-data.mjs` aggregates each year's crash-level file into `fars_state_month` (state × year × month: crashes, fatalities, persons/vehicles/pedestrians involved) and the vehicle-level file into `fars_vehicle_make_year` (make × year, national: count of vehicles of that make involved in a fatal crash — an involvement count, not a causation claim). Raw crash/vehicle rows are never stored. Call `fars_coverage` for the exact years loaded.
|
|
43
|
+
- **Refresh:** re-run `node scripts/nhtsa-fars/load-data.mjs --years 2022,2023,2024,2025` (add the newest published year; `--db`/`--config` override the defaults) once NHTSA publishes a new annual file — usually with roughly a one-year lag. Needs `wrangler` authenticated to the Pipeworx Cloudflare account (`CLOUDFLARE_ACCOUNT_ID` env var if the account has more than one in non-interactive mode).
|
|
44
|
+
- **Known gap:** no county/city granularity and no driver/vehicle-level detail (BAC, injury severity, road conditions) — those live in FARS's other per-record files, not aggregated here. File a follow-up if a caller needs them.
|
|
45
|
+
|
|
46
|
+
## fueleconomy.gov (live proxy)
|
|
47
|
+
|
|
48
|
+
`fuel_economy_mpg` and `fuel_economy_models` call the DOE/EPA fueleconomy.gov vehicle data service live, per request — it's a keyless REST API with no bulk download, so there is nothing to copy. A model year + make can have several engine/transmission/drivetrain *variants* under one model string (e.g. several Camry trims); `fuel_economy_mpg` returns up to 5. Pass the model string exactly as fueleconomy.gov lists it — `fuel_economy_models` shows the valid strings for a year + make when unsure.
|
|
33
49
|
|
|
34
50
|
## Common pitfalls
|
|
35
51
|
|
|
@@ -39,6 +55,9 @@ None. NHTSA's APIs are fully public, free.
|
|
|
39
55
|
- **Complaint counts ≠ defect rates.** Some defects produce many complaints; others produce few because they're catastrophic (drivers can't complain post-mortem). Cross-reference with crash data when relevant.
|
|
40
56
|
- **VIN decoder limitations.** The free VIN decoder gives factory-spec data but doesn't reflect aftermarket modifications, accident history, or current ownership. For those, you need commercial sources (Carfax, AutoCheck).
|
|
41
57
|
- **NCAP scoring evolved.** The 5-star scale changed in 2011 — earlier ratings aren't directly comparable to newer ones. Don't compare a 2008 5-star to a 2024 5-star.
|
|
58
|
+
- **FARS counts FATAL crashes only.** It is not a general crash database — non-fatal crashes (the overwhelming majority) aren't in here. For general crash/injury data, FARS isn't the right source.
|
|
59
|
+
- **`fars_vehicle_makes` is an involvement count, not a risk rate.** More Ford/Chevrolet/Toyota vehicles show up because those makes have far more vehicles on the road, not because they're less safe. Normalize by registrations/miles-driven before drawing a safety conclusion.
|
|
60
|
+
- **`fuel_economy_mpg`'s model string must match fueleconomy.gov's own listing.** "F-150" alone won't match; it's "F150 Pickup 2WD" / "F150 Pickup 4WD" etc. Call `fuel_economy_models` first if unsure.
|
|
42
61
|
|
|
43
62
|
## Quick Start
|
|
44
63
|
|
|
@@ -84,7 +103,7 @@ directly, instead of just this one's:
|
|
|
84
103
|
}
|
|
85
104
|
```
|
|
86
105
|
|
|
87
|
-
Both URLs reach the same gateway and the same
|
|
106
|
+
Both URLs reach the same gateway and the same 1877+ data sources. The
|
|
88
107
|
only difference is which pack's tools are listed **directly**; `ask_pipeworx`
|
|
89
108
|
reaches all of them from either one.
|
|
90
109
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipeworx/mcp-nhtsa",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "NHTSA MCP — wraps the NHTSA vPIC (Vehicle Product Information Catalog) API (free, no auth)",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "NHTSA MCP — wraps the NHTSA vPIC (Vehicle Product Information Catalog) API (free, no auth), NHTSA's Fatality Analysis Reporting System (FARS) fatal-crash data, and fueleconomy.gov vehicle MPG (fleet #2972).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@cloudflare/workers-types": "^4.20260405.1"
|
|
27
27
|
},
|
|
28
28
|
"pipeworx": {
|
|
29
|
-
"sourceHash": "v1-
|
|
30
|
-
"sourceCommit": "
|
|
29
|
+
"sourceHash": "v1-14bb9ef82ee44667f1660eb48a3483a7bd1242f9ff8d01450171f012220af4f8",
|
|
30
|
+
"sourceCommit": "774de7f685a75e02d0b828d24a7b94794102152c"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/server.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.pipeworx-io/nhtsa",
|
|
4
4
|
"title": "Nhtsa",
|
|
5
|
-
"description": "NHTSA MCP — wraps the NHTSA vPIC (Vehicle Product Information Catalog) API (free, no auth)",
|
|
6
|
-
"version": "0.2.
|
|
5
|
+
"description": "NHTSA MCP — wraps the NHTSA vPIC (Vehicle Product Information Catalog) API (free, no auth)…",
|
|
6
|
+
"version": "0.2.3",
|
|
7
7
|
"websiteUrl": "https://pipeworx.io/packs/nhtsa",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/pipeworx-io/mcp-nhtsa",
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D1 reads for the FARS fatal-crash tables added to the nhtsa pack (fleet #2972).
|
|
3
|
+
*
|
|
4
|
+
* Data: NHTSA Fatality Analysis Reporting System (FARS), annual National CSV
|
|
5
|
+
* bulk files (static.nhtsa.gov/nhtsa/downloads/FARS/<YYYY>/National/). US
|
|
6
|
+
* federal government work -- public domain (17 U.S.C. §105). FARS's live
|
|
7
|
+
* CrashAPI (crashviewer.nhtsa.dot.gov) returned an Akamai 403 to every probe
|
|
8
|
+
* on 2026-10-12, so this pack loads the annual bulk files ahead of time (CLAUDE.md "BUILD THE COPY")
|
|
9
|
+
* into D1 by scripts/nhtsa-fars/load-data.mjs -- never by a deploy. See
|
|
10
|
+
* mcps/nhtsa/README.md "FARS data".
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface D1Like {
|
|
14
|
+
prepare(sql: string): D1StmtLike;
|
|
15
|
+
}
|
|
16
|
+
export interface D1StmtLike {
|
|
17
|
+
bind(...values: unknown[]): D1StmtLike;
|
|
18
|
+
all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;
|
|
19
|
+
first<T = Record<string, unknown>>(): Promise<T | null>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class FarsStoreUnavailableError extends Error {
|
|
23
|
+
constructor() {
|
|
24
|
+
super(
|
|
25
|
+
'nhtsa: the FARS fatal-crash store is not bound in this runtime (no _d1). This tool only answers through the Pipeworx gateway; a standalone install has no data to read.',
|
|
26
|
+
);
|
|
27
|
+
this.name = 'FarsStoreUnavailableError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function requireFarsDb(d1: unknown): D1Like {
|
|
31
|
+
if (!d1 || typeof (d1 as D1Like).prepare !== 'function') throw new FarsStoreUnavailableError();
|
|
32
|
+
return d1 as D1Like;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const FARS_SOURCE = {
|
|
36
|
+
source: 'NHTSA Fatality Analysis Reporting System (FARS), annual National CSV files',
|
|
37
|
+
source_url: 'https://www.nhtsa.gov/research-data/fatality-analysis-reporting-system-fars',
|
|
38
|
+
license: 'US federal government work -- public domain (17 U.S.C. §105)',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// 2-letter postal code -> full state name used by this pack's loaded data
|
|
42
|
+
// (FARS's own STATENAME values, e.g. "Texas", "District of Columbia").
|
|
43
|
+
const STATE_ABBR: Record<string, string> = {
|
|
44
|
+
AL: 'Alabama', AK: 'Alaska', AZ: 'Arizona', AR: 'Arkansas', CA: 'California',
|
|
45
|
+
CO: 'Colorado', CT: 'Connecticut', DE: 'Delaware', DC: 'District of Columbia',
|
|
46
|
+
FL: 'Florida', GA: 'Georgia', HI: 'Hawaii', ID: 'Idaho', IL: 'Illinois',
|
|
47
|
+
IN: 'Indiana', IA: 'Iowa', KS: 'Kansas', KY: 'Kentucky', LA: 'Louisiana',
|
|
48
|
+
ME: 'Maine', MD: 'Maryland', MA: 'Massachusetts', MI: 'Michigan', MN: 'Minnesota',
|
|
49
|
+
MS: 'Mississippi', MO: 'Missouri', MT: 'Montana', NE: 'Nebraska', NV: 'Nevada',
|
|
50
|
+
NH: 'New Hampshire', NJ: 'New Jersey', NM: 'New Mexico', NY: 'New York',
|
|
51
|
+
NC: 'North Carolina', ND: 'North Dakota', OH: 'Ohio', OK: 'Oklahoma', OR: 'Oregon',
|
|
52
|
+
PA: 'Pennsylvania', RI: 'Rhode Island', SC: 'South Carolina', SD: 'South Dakota',
|
|
53
|
+
TN: 'Tennessee', TX: 'Texas', UT: 'Utah', VT: 'Vermont', VA: 'Virginia',
|
|
54
|
+
WA: 'Washington', WV: 'West Virginia', WI: 'Wisconsin', WY: 'Wyoming',
|
|
55
|
+
PR: 'Puerto Rico',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Resolve caller input (full name, 2-letter abbreviation, or FIPS code) to a
|
|
59
|
+
* candidate state name/FIPS pair. Does NOT guarantee the state is actually
|
|
60
|
+
* loaded -- call validState() for that. Returns null only on unparseable input. */
|
|
61
|
+
export function resolveState(input: unknown): { byName: string | null; byFips: number | null } | null {
|
|
62
|
+
if (input == null) return null;
|
|
63
|
+
const raw = String(input).trim();
|
|
64
|
+
if (!raw) return null;
|
|
65
|
+
if (/^\d{1,2}$/.test(raw)) return { byName: null, byFips: Number(raw) };
|
|
66
|
+
const upper = raw.toUpperCase();
|
|
67
|
+
if (STATE_ABBR[upper]) return { byName: STATE_ABBR[upper], byFips: null };
|
|
68
|
+
return { byName: raw, byFips: null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function lookupState(db: D1Like, resolved: { byName: string | null; byFips: number | null }): Promise<{ state_fips: number; state_name: string } | null> {
|
|
72
|
+
if (resolved.byFips != null) {
|
|
73
|
+
const r = await db.prepare('SELECT state_fips, state_name FROM fars_state_month WHERE state_fips = ? LIMIT 1').bind(resolved.byFips).first<{ state_fips: number; state_name: string }>();
|
|
74
|
+
return r ?? null;
|
|
75
|
+
}
|
|
76
|
+
if (resolved.byName) {
|
|
77
|
+
const r = await db
|
|
78
|
+
.prepare('SELECT state_fips, state_name FROM fars_state_month WHERE state_name = ? COLLATE NOCASE LIMIT 1')
|
|
79
|
+
.bind(resolved.byName)
|
|
80
|
+
.first<{ state_fips: number; state_name: string }>();
|
|
81
|
+
return r ?? null;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface FarsMeta {
|
|
87
|
+
loaded_at: string;
|
|
88
|
+
earliest_year: number;
|
|
89
|
+
latest_year: number;
|
|
90
|
+
years_loaded: number[];
|
|
91
|
+
}
|
|
92
|
+
let cachedMeta: FarsMeta | null = null;
|
|
93
|
+
export async function getFarsMeta(db: D1Like): Promise<FarsMeta> {
|
|
94
|
+
if (cachedMeta) return cachedMeta;
|
|
95
|
+
const r = await db.prepare('SELECT key, value FROM fars_meta').all<{ key: string; value: string }>();
|
|
96
|
+
const m = Object.fromEntries(r.results.map((row) => [row.key, row.value]));
|
|
97
|
+
cachedMeta = {
|
|
98
|
+
loaded_at: m.loaded_at ?? '',
|
|
99
|
+
earliest_year: Number(m.earliest_year ?? 0),
|
|
100
|
+
latest_year: Number(m.latest_year ?? 0),
|
|
101
|
+
years_loaded: (m.years_loaded ?? '').split(',').filter(Boolean).map(Number),
|
|
102
|
+
};
|
|
103
|
+
return cachedMeta;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface StateMonthRow {
|
|
107
|
+
year: number;
|
|
108
|
+
month: number;
|
|
109
|
+
state_fips: number;
|
|
110
|
+
state_name: string;
|
|
111
|
+
crashes: number;
|
|
112
|
+
fatalities: number;
|
|
113
|
+
persons_involved: number;
|
|
114
|
+
vehicles_involved: number;
|
|
115
|
+
pedestrians_involved: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Monthly rows for one state and (optionally) one year. Omitting year
|
|
119
|
+
* returns every loaded year/month for that state, most recent first. */
|
|
120
|
+
export async function stateMonths(db: D1Like, stateFips: number, year: number | null): Promise<StateMonthRow[]> {
|
|
121
|
+
const where = ['state_fips = ?'];
|
|
122
|
+
const params: unknown[] = [stateFips];
|
|
123
|
+
if (year != null) {
|
|
124
|
+
where.push('year = ?');
|
|
125
|
+
params.push(year);
|
|
126
|
+
}
|
|
127
|
+
const sql = `SELECT year, month, state_fips, state_name, crashes, fatalities, persons_involved, vehicles_involved, pedestrians_involved
|
|
128
|
+
FROM fars_state_month WHERE ${where.join(' AND ')}
|
|
129
|
+
ORDER BY year DESC, month DESC`;
|
|
130
|
+
const r = await db.prepare(sql).bind(...params).all<StateMonthRow>();
|
|
131
|
+
return r.results;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** All states for one year, summed across months -- for cross-state comparison. */
|
|
135
|
+
export async function allStatesForYear(db: D1Like, year: number, limit: number): Promise<Array<Omit<StateMonthRow, 'month'>>> {
|
|
136
|
+
const sql = `SELECT year, state_fips, state_name,
|
|
137
|
+
SUM(crashes) AS crashes, SUM(fatalities) AS fatalities,
|
|
138
|
+
SUM(persons_involved) AS persons_involved, SUM(vehicles_involved) AS vehicles_involved,
|
|
139
|
+
SUM(pedestrians_involved) AS pedestrians_involved
|
|
140
|
+
FROM fars_state_month WHERE year = ?
|
|
141
|
+
GROUP BY state_fips, state_name
|
|
142
|
+
ORDER BY fatalities DESC
|
|
143
|
+
LIMIT ?`;
|
|
144
|
+
const r = await db.prepare(sql).bind(year, limit).all<Omit<StateMonthRow, 'month'>>();
|
|
145
|
+
return r.results;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface MakeYearRow {
|
|
149
|
+
year: number;
|
|
150
|
+
make_name: string;
|
|
151
|
+
vehicles_involved: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function vehicleMakesForYear(db: D1Like, year: number, limit: number): Promise<MakeYearRow[]> {
|
|
155
|
+
const sql = `SELECT year, make_name, vehicles_involved FROM fars_vehicle_make_year WHERE year = ? ORDER BY vehicles_involved DESC LIMIT ?`;
|
|
156
|
+
const r = await db.prepare(sql).bind(year, limit).all<MakeYearRow>();
|
|
157
|
+
return r.results;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function vehicleMakeYears(db: D1Like, makeName: string): Promise<MakeYearRow[]> {
|
|
161
|
+
const sql = `SELECT year, make_name, vehicles_involved FROM fars_vehicle_make_year WHERE make_name = ? COLLATE NOCASE ORDER BY year DESC`;
|
|
162
|
+
const r = await db.prepare(sql).bind(makeName).all<MakeYearRow>();
|
|
163
|
+
return r.results;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Distinct make names actually loaded (for a "did you mean" / discovery hint). */
|
|
167
|
+
export async function listMakes(db: D1Like, limit = 80): Promise<string[]> {
|
|
168
|
+
const r = await db.prepare('SELECT DISTINCT make_name FROM fars_vehicle_make_year ORDER BY make_name LIMIT ?').bind(limit).all<{ make_name: string }>();
|
|
169
|
+
return r.results.map((row) => row.make_name);
|
|
170
|
+
}
|