rivian-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/lib/rivian.js +379 -0
- package/mcp-server.js +540 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Heneise
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Rivian MCP
|
|
2
|
+
|
|
3
|
+
Read-only [MCP server](https://modelcontextprotocol.io) for the unofficial Rivian GraphQL API. Check your vehicle's battery, range, OTA updates, charging status, and more — right from Claude.
|
|
4
|
+
|
|
5
|
+
**Strictly read-only** — no vehicle commands, no settings changes.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
claude mcp add rivian \
|
|
11
|
+
-e RIVIAN_EMAIL=your@email.com \
|
|
12
|
+
-e RIVIAN_PASSWORD=your-password \
|
|
13
|
+
-- npx rivian-mcp
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
<details>
|
|
17
|
+
<summary>Manual config</summary>
|
|
18
|
+
|
|
19
|
+
Add to `~/.claude.json` or your project's `.mcp.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"rivian": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["rivian-mcp"],
|
|
27
|
+
"env": {
|
|
28
|
+
"RIVIAN_EMAIL": "your@email.com",
|
|
29
|
+
"RIVIAN_PASSWORD": "your-password"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
</details>
|
|
37
|
+
|
|
38
|
+
### First-time login
|
|
39
|
+
|
|
40
|
+
Rivian requires 2FA on every new login:
|
|
41
|
+
|
|
42
|
+
1. Ask Claude: **"Log in to Rivian"**
|
|
43
|
+
2. Rivian sends a verification code to your phone/email
|
|
44
|
+
3. Tell Claude the code: **"The code is 123456"**
|
|
45
|
+
|
|
46
|
+
Your session is saved to `~/.rivian-mcp/session.json` and reused automatically until it expires (7 days).
|
|
47
|
+
|
|
48
|
+
## What you can ask
|
|
49
|
+
|
|
50
|
+
- "What's my battery level?"
|
|
51
|
+
- "Is there a software update available?"
|
|
52
|
+
- "Are all the doors locked?"
|
|
53
|
+
- "Show me the full vehicle status"
|
|
54
|
+
- "Who has keys to my R1S?"
|
|
55
|
+
- "Am I currently charging?"
|
|
56
|
+
|
|
57
|
+
## Tools
|
|
58
|
+
|
|
59
|
+
| Tool | What it does |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `rivian_login` | Start sign-in (triggers verification code) |
|
|
62
|
+
| `rivian_submit_otp` | Complete sign-in with the verification code |
|
|
63
|
+
| `rivian_get_user_info` | Your account, vehicles, and software versions |
|
|
64
|
+
| `rivian_get_vehicle_state` | Live status — battery, doors, tires, location, climate, OTA |
|
|
65
|
+
| `rivian_get_ota_status` | Current and available software versions |
|
|
66
|
+
| `rivian_get_charging_session` | Active charging session details |
|
|
67
|
+
| `rivian_get_drivers_and_keys` | Drivers and their phone keys / key fobs |
|
|
68
|
+
|
|
69
|
+
## Requirements
|
|
70
|
+
|
|
71
|
+
- Node.js 24+
|
|
72
|
+
- A Rivian account with a vehicle
|
|
73
|
+
|
|
74
|
+
## References
|
|
75
|
+
|
|
76
|
+
- [Unofficial Rivian API Docs](https://rivian-api.kaedenb.org/app/)
|
|
77
|
+
- [rivian-python-client](https://github.com/bretterer/rivian-python-client) — Python client this is based on
|
|
78
|
+
- [home-assistant-rivian](https://github.com/bretterer/home-assistant-rivian) — Home Assistant integration
|
package/lib/rivian.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rivian API — READ ONLY
|
|
3
|
+
*
|
|
4
|
+
* Based on bretterer/rivian-python-client and rivian-api.kaedenb.org docs.
|
|
5
|
+
* No write/post/patch mutations — only auth and queries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const GRAPHQL_GATEWAY = 'https://rivian.com/api/gql/gateway/graphql';
|
|
9
|
+
const GRAPHQL_CHARGING = 'https://rivian.com/api/gql/chrg/user/graphql';
|
|
10
|
+
const APOLLO_CLIENT_NAME = 'com.rivian.ios.consumer-apollo-ios';
|
|
11
|
+
|
|
12
|
+
const BASE_HEADERS = {
|
|
13
|
+
'User-Agent': 'RivianApp/707 CFNetwork/1237 Darwin/20.4.0',
|
|
14
|
+
'Accept': 'application/json',
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'Apollographql-Client-Name': APOLLO_CLIENT_NAME,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// ── Session state ───────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
let csrfToken = '';
|
|
22
|
+
let appSessionToken = '';
|
|
23
|
+
let accessToken = '';
|
|
24
|
+
let refreshToken = '';
|
|
25
|
+
let userSessionToken = '';
|
|
26
|
+
let otpToken = '';
|
|
27
|
+
|
|
28
|
+
export const needsOtp = () => !!otpToken;
|
|
29
|
+
export const isAuthenticated = () => !!accessToken;
|
|
30
|
+
|
|
31
|
+
// ── Auth ────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export async function createCsrfToken() {
|
|
34
|
+
const data = await gql(GRAPHQL_GATEWAY, {
|
|
35
|
+
operationName: 'CreateCSRFToken',
|
|
36
|
+
query: `mutation CreateCSRFToken {
|
|
37
|
+
createCsrfToken {
|
|
38
|
+
__typename
|
|
39
|
+
csrfToken
|
|
40
|
+
appSessionToken
|
|
41
|
+
}
|
|
42
|
+
}`,
|
|
43
|
+
variables: null,
|
|
44
|
+
});
|
|
45
|
+
csrfToken = data.createCsrfToken.csrfToken;
|
|
46
|
+
appSessionToken = data.createCsrfToken.appSessionToken;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function login(email, password) {
|
|
50
|
+
const data = await gql(
|
|
51
|
+
GRAPHQL_GATEWAY,
|
|
52
|
+
{
|
|
53
|
+
operationName: 'Login',
|
|
54
|
+
query: `mutation Login($email: String!, $password: String!) {
|
|
55
|
+
login(email: $email, password: $password) {
|
|
56
|
+
__typename
|
|
57
|
+
... on MobileLoginResponse {
|
|
58
|
+
__typename
|
|
59
|
+
accessToken
|
|
60
|
+
refreshToken
|
|
61
|
+
userSessionToken
|
|
62
|
+
}
|
|
63
|
+
... on MobileMFALoginResponse {
|
|
64
|
+
__typename
|
|
65
|
+
otpToken
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}`,
|
|
69
|
+
variables: { email, password },
|
|
70
|
+
},
|
|
71
|
+
{ 'Csrf-Token': csrfToken, 'A-Sess': appSessionToken },
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (data.login.otpToken) {
|
|
75
|
+
otpToken = data.login.otpToken;
|
|
76
|
+
return { mfa: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
accessToken = data.login.accessToken;
|
|
80
|
+
refreshToken = data.login.refreshToken;
|
|
81
|
+
userSessionToken = data.login.userSessionToken;
|
|
82
|
+
return { mfa: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function validateOtp(email, otpCode) {
|
|
86
|
+
const data = await gql(
|
|
87
|
+
GRAPHQL_GATEWAY,
|
|
88
|
+
{
|
|
89
|
+
operationName: 'LoginWithOTP',
|
|
90
|
+
query: `mutation LoginWithOTP($email: String!, $otpCode: String!, $otpToken: String!) {
|
|
91
|
+
loginWithOTP(email: $email, otpCode: $otpCode, otpToken: $otpToken) {
|
|
92
|
+
__typename
|
|
93
|
+
... on MobileLoginResponse {
|
|
94
|
+
__typename
|
|
95
|
+
accessToken
|
|
96
|
+
refreshToken
|
|
97
|
+
userSessionToken
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}`,
|
|
101
|
+
variables: { email, otpCode, otpToken },
|
|
102
|
+
},
|
|
103
|
+
{ 'Csrf-Token': csrfToken, 'A-Sess': appSessionToken },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
accessToken = data.loginWithOTP.accessToken;
|
|
107
|
+
refreshToken = data.loginWithOTP.refreshToken;
|
|
108
|
+
userSessionToken = data.loginWithOTP.userSessionToken;
|
|
109
|
+
otpToken = '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Read-only queries ───────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
export async function getUserInfo() {
|
|
115
|
+
const body = {
|
|
116
|
+
operationName: 'getUserInfo',
|
|
117
|
+
query: `query getUserInfo {
|
|
118
|
+
currentUser {
|
|
119
|
+
__typename
|
|
120
|
+
id
|
|
121
|
+
firstName
|
|
122
|
+
lastName
|
|
123
|
+
email
|
|
124
|
+
vehicles {
|
|
125
|
+
id
|
|
126
|
+
vin
|
|
127
|
+
name
|
|
128
|
+
roles
|
|
129
|
+
state
|
|
130
|
+
createdAt
|
|
131
|
+
updatedAt
|
|
132
|
+
vas { __typename vasVehicleId vehiclePublicKey }
|
|
133
|
+
vehicle {
|
|
134
|
+
__typename
|
|
135
|
+
id
|
|
136
|
+
vin
|
|
137
|
+
modelYear
|
|
138
|
+
make
|
|
139
|
+
model
|
|
140
|
+
expectedBuildDate
|
|
141
|
+
plannedBuildDate
|
|
142
|
+
otaEarlyAccessStatus
|
|
143
|
+
currentOTAUpdateDetails { url version locale }
|
|
144
|
+
availableOTAUpdateDetails { url version locale }
|
|
145
|
+
vehicleState {
|
|
146
|
+
supportedFeatures { __typename name status }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
registrationChannels { type }
|
|
151
|
+
}
|
|
152
|
+
}`,
|
|
153
|
+
variables: null,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return (await gql(GRAPHQL_GATEWAY, body, authHeaders())).currentUser;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function getVehicleState(vehicleId, properties) {
|
|
160
|
+
const props = properties || DEFAULT_VEHICLE_STATE_PROPERTIES;
|
|
161
|
+
const fragment = [...props]
|
|
162
|
+
.map((p) => `${p} ${TEMPLATE_MAP[p] || VALUE_TEMPLATE}`)
|
|
163
|
+
.join('\n ');
|
|
164
|
+
|
|
165
|
+
const body = {
|
|
166
|
+
operationName: 'GetVehicleState',
|
|
167
|
+
query: `query GetVehicleState($vehicleID: String!) {
|
|
168
|
+
vehicleState(id: $vehicleID) {
|
|
169
|
+
${fragment}
|
|
170
|
+
}
|
|
171
|
+
}`,
|
|
172
|
+
variables: { vehicleID: vehicleId },
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return (await gql(GRAPHQL_GATEWAY, body, authHeaders())).vehicleState;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function getOTAUpdateDetails(vehicleId) {
|
|
179
|
+
const body = {
|
|
180
|
+
operationName: 'getOTAUpdateDetails',
|
|
181
|
+
query: `query getOTAUpdateDetails($vehicleId: String!) {
|
|
182
|
+
getVehicle(id: $vehicleId) {
|
|
183
|
+
availableOTAUpdateDetails { url version locale }
|
|
184
|
+
currentOTAUpdateDetails { url version locale }
|
|
185
|
+
}
|
|
186
|
+
}`,
|
|
187
|
+
variables: { vehicleId },
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return (await gql(GRAPHQL_GATEWAY, body, authHeaders())).getVehicle;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function getLiveChargingSession(vehicleId) {
|
|
194
|
+
const body = {
|
|
195
|
+
operationName: 'getLiveSessionData',
|
|
196
|
+
query: `query getLiveSessionData($vehicleId: ID!) {
|
|
197
|
+
getLiveSessionData(vehicleId: $vehicleId) {
|
|
198
|
+
__typename
|
|
199
|
+
chargerId
|
|
200
|
+
current { __typename value updatedAt }
|
|
201
|
+
currentCurrency
|
|
202
|
+
currentMiles { __typename value updatedAt }
|
|
203
|
+
currentPrice
|
|
204
|
+
isFreeSession
|
|
205
|
+
isRivianCharger
|
|
206
|
+
kilometersChargedPerHour { __typename value updatedAt }
|
|
207
|
+
locationId
|
|
208
|
+
power { __typename value updatedAt }
|
|
209
|
+
rangeAddedThisSession { __typename value updatedAt }
|
|
210
|
+
soc { __typename value updatedAt }
|
|
211
|
+
startTime
|
|
212
|
+
timeElapsed
|
|
213
|
+
timeRemaining { __typename value updatedAt }
|
|
214
|
+
totalChargedEnergy { __typename value updatedAt }
|
|
215
|
+
vehicleChargerState { __typename value updatedAt }
|
|
216
|
+
}
|
|
217
|
+
}`,
|
|
218
|
+
variables: { vehicleId },
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return (await gql(GRAPHQL_CHARGING, body, chargingHeaders())).getLiveSessionData;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function getDriversAndKeys(vehicleId) {
|
|
225
|
+
const body = {
|
|
226
|
+
operationName: 'DriversAndKeys',
|
|
227
|
+
query: `query DriversAndKeys($vehicleId: String) {
|
|
228
|
+
getVehicle(id: $vehicleId) {
|
|
229
|
+
__typename
|
|
230
|
+
id
|
|
231
|
+
vin
|
|
232
|
+
invitedUsers {
|
|
233
|
+
__typename
|
|
234
|
+
... on ProvisionedUser {
|
|
235
|
+
firstName lastName email roles userId
|
|
236
|
+
devices { type mappedIdentityId id hrid deviceName isPaired isEnabled }
|
|
237
|
+
}
|
|
238
|
+
... on UnprovisionedUser {
|
|
239
|
+
email inviteId status
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}`,
|
|
244
|
+
variables: { vehicleId },
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return (await gql(GRAPHQL_GATEWAY, body, authHeaders())).getVehicle;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Session persistence ──────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
export function exportSession() {
|
|
253
|
+
return {
|
|
254
|
+
csrfToken,
|
|
255
|
+
appSessionToken,
|
|
256
|
+
accessToken,
|
|
257
|
+
refreshToken,
|
|
258
|
+
userSessionToken,
|
|
259
|
+
otpToken,
|
|
260
|
+
needsOtp: !!otpToken,
|
|
261
|
+
authenticated: !!accessToken,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function restoreSession(session) {
|
|
266
|
+
csrfToken = session.csrfToken || '';
|
|
267
|
+
appSessionToken = session.appSessionToken || '';
|
|
268
|
+
accessToken = session.accessToken || '';
|
|
269
|
+
refreshToken = session.refreshToken || '';
|
|
270
|
+
userSessionToken = session.userSessionToken || '';
|
|
271
|
+
otpToken = session.otpToken || '';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Internals ───────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
function authHeaders() {
|
|
277
|
+
return { 'A-Sess': appSessionToken, 'U-Sess': userSessionToken };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function chargingHeaders() {
|
|
281
|
+
return { 'U-Sess': userSessionToken };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function gql(url, body, extraHeaders = {}) {
|
|
285
|
+
const res = await fetch(url, {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
headers: {
|
|
288
|
+
...BASE_HEADERS,
|
|
289
|
+
'dc-cid': `m-ios-${crypto.randomUUID()}`,
|
|
290
|
+
...extraHeaders,
|
|
291
|
+
},
|
|
292
|
+
body: JSON.stringify(body),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const json = await res.json();
|
|
296
|
+
|
|
297
|
+
if (json.errors?.length) {
|
|
298
|
+
const e = json.errors[0];
|
|
299
|
+
const msg = e.message || e.extensions?.code || 'Unknown GraphQL error';
|
|
300
|
+
const err = new Error(msg);
|
|
301
|
+
err.code = e.extensions?.code;
|
|
302
|
+
err.reason = e.extensions?.reason;
|
|
303
|
+
throw err;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!res.ok) {
|
|
307
|
+
throw new Error(`HTTP ${res.status}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return json.data;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── Vehicle state property templates ────────────────────────────────
|
|
314
|
+
|
|
315
|
+
const VALUE_TEMPLATE = '{ timeStamp value }';
|
|
316
|
+
|
|
317
|
+
const TEMPLATE_MAP = {
|
|
318
|
+
cloudConnection: '{ lastSync isOnline }',
|
|
319
|
+
gnssLocation: '{ latitude longitude timeStamp isAuthorized }',
|
|
320
|
+
gnssError: '{ timeStamp positionVertical positionHorizontal speed bearing }',
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const DEFAULT_VEHICLE_STATE_PROPERTIES = new Set([
|
|
324
|
+
'cloudConnection',
|
|
325
|
+
'gnssLocation',
|
|
326
|
+
'batteryLevel',
|
|
327
|
+
'batteryLimit',
|
|
328
|
+
'batteryCapacity',
|
|
329
|
+
'distanceToEmpty',
|
|
330
|
+
'vehicleMileage',
|
|
331
|
+
'powerState',
|
|
332
|
+
'chargerStatus',
|
|
333
|
+
'chargerState',
|
|
334
|
+
'chargePortState',
|
|
335
|
+
'otaAvailableVersion',
|
|
336
|
+
'otaAvailableVersionGitHash',
|
|
337
|
+
'otaCurrentVersion',
|
|
338
|
+
'otaCurrentVersionGitHash',
|
|
339
|
+
'otaStatus',
|
|
340
|
+
'otaInstallReady',
|
|
341
|
+
'otaInstallProgress',
|
|
342
|
+
'otaCurrentStatus',
|
|
343
|
+
'otaDownloadProgress',
|
|
344
|
+
'otaInstallType',
|
|
345
|
+
'driveMode',
|
|
346
|
+
'gearStatus',
|
|
347
|
+
'tirePressureStatusFrontLeft',
|
|
348
|
+
'tirePressureStatusFrontRight',
|
|
349
|
+
'tirePressureStatusRearLeft',
|
|
350
|
+
'tirePressureStatusRearRight',
|
|
351
|
+
'doorFrontLeftClosed',
|
|
352
|
+
'doorFrontRightClosed',
|
|
353
|
+
'doorRearLeftClosed',
|
|
354
|
+
'doorRearRightClosed',
|
|
355
|
+
'doorFrontLeftLocked',
|
|
356
|
+
'doorFrontRightLocked',
|
|
357
|
+
'doorRearLeftLocked',
|
|
358
|
+
'doorRearRightLocked',
|
|
359
|
+
'closureFrunkClosed',
|
|
360
|
+
'closureFrunkLocked',
|
|
361
|
+
'closureLiftgateClosed',
|
|
362
|
+
'closureLiftgateLocked',
|
|
363
|
+
'closureTailgateClosed',
|
|
364
|
+
'closureTailgateLocked',
|
|
365
|
+
'closureTonneauClosed',
|
|
366
|
+
'closureTonneauLocked',
|
|
367
|
+
'windowFrontLeftClosed',
|
|
368
|
+
'windowFrontRightClosed',
|
|
369
|
+
'windowRearLeftClosed',
|
|
370
|
+
'windowRearRightClosed',
|
|
371
|
+
'cabinClimateInteriorTemperature',
|
|
372
|
+
'cabinPreconditioningStatus',
|
|
373
|
+
'defrostDefogStatus',
|
|
374
|
+
'petModeStatus',
|
|
375
|
+
'gearGuardLocked',
|
|
376
|
+
'gearGuardVideoStatus',
|
|
377
|
+
'timeToEndOfCharge',
|
|
378
|
+
'remoteChargingAvailable',
|
|
379
|
+
]);
|
package/mcp-server.js
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, statSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import * as rivian from './lib/rivian.js';
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = join(homedir(), '.rivian-mcp');
|
|
12
|
+
const SESSION_FILE = join(CONFIG_DIR, 'session.json');
|
|
13
|
+
const SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
// ── Session persistence ───────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function loadSession() {
|
|
18
|
+
if (!existsSync(SESSION_FILE)) return false;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const st = statSync(SESSION_FILE);
|
|
22
|
+
if (st.mode & 0o077) {
|
|
23
|
+
console.error(
|
|
24
|
+
`[rivian-mcp] WARNING: ${SESSION_FILE} is readable by other users. Run: chmod 600 "${SESSION_FILE}"`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
|
|
29
|
+
const session = JSON.parse(readFileSync(SESSION_FILE, 'utf8'));
|
|
30
|
+
|
|
31
|
+
if (session.savedAt && Date.now() - session.savedAt > SESSION_MAX_AGE_MS) {
|
|
32
|
+
console.error('[rivian-mcp] Session expired. Please log in again.');
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (session.authenticated || session.needsOtp) {
|
|
37
|
+
rivian.restoreSession(session);
|
|
38
|
+
return session.authenticated ? true : 'needs_otp';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function saveSession() {
|
|
45
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
46
|
+
chmodSync(CONFIG_DIR, 0o700);
|
|
47
|
+
const session = { ...rivian.exportSession(), savedAt: Date.now() };
|
|
48
|
+
writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
49
|
+
chmodSync(SESSION_FILE, 0o600);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requireAuth() {
|
|
53
|
+
if (!rivian.isAuthenticated()) {
|
|
54
|
+
throw new Error('Not logged in. Ask the user to log in to Rivian first.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function text(str) {
|
|
59
|
+
return { content: [{ type: 'text', text: str }] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Formatting ────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function formatUserInfo(user) {
|
|
65
|
+
const lines = [`${user.firstName} ${user.lastName} (${user.email})`, ''];
|
|
66
|
+
|
|
67
|
+
if (!user.vehicles?.length) {
|
|
68
|
+
lines.push('No vehicles on this account.');
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const v of user.vehicles) {
|
|
73
|
+
const car = v.vehicle;
|
|
74
|
+
lines.push(v.name || car.model);
|
|
75
|
+
lines.push(` ${car.modelYear} ${car.make} ${car.model}`);
|
|
76
|
+
lines.push(` VIN: ${v.vin}`);
|
|
77
|
+
lines.push(` Vehicle ID: ${v.id}`);
|
|
78
|
+
|
|
79
|
+
if (car.otaEarlyAccessStatus) {
|
|
80
|
+
lines.push(` OTA early access: ${car.otaEarlyAccessStatus === 'OPTED_IN' ? 'Yes' : 'No'}`);
|
|
81
|
+
}
|
|
82
|
+
if (car.currentOTAUpdateDetails) {
|
|
83
|
+
lines.push(` Software: v${car.currentOTAUpdateDetails.version}`);
|
|
84
|
+
}
|
|
85
|
+
if (car.availableOTAUpdateDetails) {
|
|
86
|
+
lines.push(` Update available: v${car.availableOTAUpdateDetails.version}`);
|
|
87
|
+
lines.push(` Release notes: ${car.availableOTAUpdateDetails.url}`);
|
|
88
|
+
} else {
|
|
89
|
+
lines.push(' Software is up to date');
|
|
90
|
+
}
|
|
91
|
+
lines.push('');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return lines.join('\n').trim();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatVehicleState(state) {
|
|
98
|
+
const lines = [];
|
|
99
|
+
const printed = new Set();
|
|
100
|
+
|
|
101
|
+
function v(key) {
|
|
102
|
+
return state[key]?.value ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function print(label, key, suffix = '') {
|
|
106
|
+
if (!(key in state)) return;
|
|
107
|
+
printed.add(key);
|
|
108
|
+
const value = v(key);
|
|
109
|
+
if (value === null || value === undefined) return;
|
|
110
|
+
lines.push(` ${label}: ${value}${suffix}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Battery & Range
|
|
114
|
+
if ('batteryLevel' in state || 'distanceToEmpty' in state) {
|
|
115
|
+
lines.push('Battery & Range');
|
|
116
|
+
print('Battery', 'batteryLevel', '%');
|
|
117
|
+
print('Charge limit', 'batteryLimit', '%');
|
|
118
|
+
print('Capacity', 'batteryCapacity');
|
|
119
|
+
print('Range', 'distanceToEmpty', ' miles');
|
|
120
|
+
print('Odometer', 'vehicleMileage', ' miles');
|
|
121
|
+
print('Power', 'powerState');
|
|
122
|
+
print('Time to full', 'timeToEndOfCharge', ' min');
|
|
123
|
+
print('Remote charging', 'remoteChargingAvailable');
|
|
124
|
+
lines.push('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Charging
|
|
128
|
+
if ('chargerStatus' in state || 'chargerState' in state) {
|
|
129
|
+
lines.push('Charging');
|
|
130
|
+
print('Charger status', 'chargerStatus');
|
|
131
|
+
print('Charger state', 'chargerState');
|
|
132
|
+
print('Charge port', 'chargePortState');
|
|
133
|
+
lines.push('');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Doors — combine closed + locked per door
|
|
137
|
+
const doorPositions = ['FrontLeft', 'FrontRight', 'RearLeft', 'RearRight'];
|
|
138
|
+
const doorLines = [];
|
|
139
|
+
for (const pos of doorPositions) {
|
|
140
|
+
const closedKey = `door${pos}Closed`;
|
|
141
|
+
const lockedKey = `door${pos}Locked`;
|
|
142
|
+
if (closedKey in state || lockedKey in state) {
|
|
143
|
+
printed.add(closedKey);
|
|
144
|
+
printed.add(lockedKey);
|
|
145
|
+
const parts = [v(closedKey), v(lockedKey)].filter(Boolean);
|
|
146
|
+
const label = pos.replace(/([A-Z])/g, ' $1').trim().toLowerCase();
|
|
147
|
+
if (parts.length) doorLines.push(` ${label}: ${parts.join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (doorLines.length) {
|
|
151
|
+
lines.push('Doors');
|
|
152
|
+
lines.push(...doorLines);
|
|
153
|
+
lines.push('');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Closures — frunk, liftgate, tailgate, tonneau
|
|
157
|
+
const closures = ['Frunk', 'Liftgate', 'Tailgate', 'Tonneau'];
|
|
158
|
+
const closureLines = [];
|
|
159
|
+
for (const name of closures) {
|
|
160
|
+
const closedKey = `closure${name}Closed`;
|
|
161
|
+
const lockedKey = `closure${name}Locked`;
|
|
162
|
+
if (closedKey in state || lockedKey in state) {
|
|
163
|
+
printed.add(closedKey);
|
|
164
|
+
printed.add(lockedKey);
|
|
165
|
+
const parts = [v(closedKey), v(lockedKey)].filter(Boolean);
|
|
166
|
+
if (parts.length) closureLines.push(` ${name.toLowerCase()}: ${parts.join(', ')}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (closureLines.length) {
|
|
170
|
+
lines.push('Closures');
|
|
171
|
+
lines.push(...closureLines);
|
|
172
|
+
lines.push('');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Windows
|
|
176
|
+
const windowLines = [];
|
|
177
|
+
for (const pos of doorPositions) {
|
|
178
|
+
const key = `window${pos}Closed`;
|
|
179
|
+
if (key in state) {
|
|
180
|
+
printed.add(key);
|
|
181
|
+
const value = v(key);
|
|
182
|
+
if (value) {
|
|
183
|
+
const label = pos.replace(/([A-Z])/g, ' $1').trim().toLowerCase();
|
|
184
|
+
windowLines.push(` ${label}: ${value}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (windowLines.length) {
|
|
189
|
+
lines.push('Windows');
|
|
190
|
+
lines.push(...windowLines);
|
|
191
|
+
lines.push('');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Climate
|
|
195
|
+
if ('cabinClimateInteriorTemperature' in state || 'cabinPreconditioningStatus' in state) {
|
|
196
|
+
lines.push('Climate');
|
|
197
|
+
if ('cabinClimateInteriorTemperature' in state) {
|
|
198
|
+
printed.add('cabinClimateInteriorTemperature');
|
|
199
|
+
const temp = v('cabinClimateInteriorTemperature');
|
|
200
|
+
if (temp) lines.push(` Cabin temp: ${temp}°`);
|
|
201
|
+
}
|
|
202
|
+
print('Preconditioning', 'cabinPreconditioningStatus');
|
|
203
|
+
print('Defrost/defog', 'defrostDefogStatus');
|
|
204
|
+
print('Pet mode', 'petModeStatus');
|
|
205
|
+
lines.push('');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Tires
|
|
209
|
+
const tirePositions = { FrontLeft: 'front left', FrontRight: 'front right', RearLeft: 'rear left', RearRight: 'rear right' };
|
|
210
|
+
const tireLines = [];
|
|
211
|
+
for (const [pos, label] of Object.entries(tirePositions)) {
|
|
212
|
+
const key = `tirePressureStatus${pos}`;
|
|
213
|
+
if (key in state) {
|
|
214
|
+
printed.add(key);
|
|
215
|
+
const value = v(key);
|
|
216
|
+
if (value) tireLines.push(` ${label}: ${value}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (tireLines.length) {
|
|
220
|
+
lines.push('Tire Pressure');
|
|
221
|
+
lines.push(...tireLines);
|
|
222
|
+
lines.push('');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Software (OTA)
|
|
226
|
+
if ('otaCurrentVersion' in state || 'otaAvailableVersion' in state || 'otaStatus' in state) {
|
|
227
|
+
lines.push('Software');
|
|
228
|
+
print('Current version', 'otaCurrentVersion');
|
|
229
|
+
print('Available update', 'otaAvailableVersion');
|
|
230
|
+
print('Status', 'otaStatus');
|
|
231
|
+
print('Install status', 'otaCurrentStatus');
|
|
232
|
+
print('Install ready', 'otaInstallReady');
|
|
233
|
+
print('Install progress', 'otaInstallProgress', '%');
|
|
234
|
+
print('Download progress', 'otaDownloadProgress', '%');
|
|
235
|
+
print('Install type', 'otaInstallType');
|
|
236
|
+
print('Current hash', 'otaCurrentVersionGitHash');
|
|
237
|
+
print('Available hash', 'otaAvailableVersionGitHash');
|
|
238
|
+
lines.push('');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Security
|
|
242
|
+
if ('gearGuardLocked' in state) {
|
|
243
|
+
lines.push('Security');
|
|
244
|
+
print('Gear Guard', 'gearGuardLocked');
|
|
245
|
+
print('Gear Guard video', 'gearGuardVideoStatus');
|
|
246
|
+
lines.push('');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Drive
|
|
250
|
+
if ('driveMode' in state || 'gearStatus' in state) {
|
|
251
|
+
lines.push('Drive');
|
|
252
|
+
print('Drive mode', 'driveMode');
|
|
253
|
+
print('Gear', 'gearStatus');
|
|
254
|
+
lines.push('');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Connection
|
|
258
|
+
if ('cloudConnection' in state) {
|
|
259
|
+
printed.add('cloudConnection');
|
|
260
|
+
const cc = state.cloudConnection;
|
|
261
|
+
const online = cc?.isOnline ? 'Online' : 'Offline';
|
|
262
|
+
const sync = cc?.lastSync ? ` (last sync: ${cc.lastSync})` : '';
|
|
263
|
+
lines.push('Connection');
|
|
264
|
+
lines.push(` Status: ${online}${sync}`);
|
|
265
|
+
lines.push('');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Location
|
|
269
|
+
if ('gnssLocation' in state) {
|
|
270
|
+
printed.add('gnssLocation');
|
|
271
|
+
const loc = state.gnssLocation;
|
|
272
|
+
if (loc?.latitude && loc?.longitude) {
|
|
273
|
+
lines.push('Location');
|
|
274
|
+
lines.push(` ${loc.latitude}, ${loc.longitude}`);
|
|
275
|
+
lines.push('');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Anything not already printed
|
|
280
|
+
const remaining = [];
|
|
281
|
+
for (const [key, entry] of Object.entries(state)) {
|
|
282
|
+
if (printed.has(key)) continue;
|
|
283
|
+
const value = entry?.value ?? entry;
|
|
284
|
+
if (value !== null && value !== undefined) {
|
|
285
|
+
remaining.push(` ${key}: ${value}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (remaining.length) {
|
|
289
|
+
lines.push('Other');
|
|
290
|
+
lines.push(...remaining);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return lines.join('\n').trim();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function formatOTAStatus(ota) {
|
|
297
|
+
const lines = [];
|
|
298
|
+
|
|
299
|
+
if (ota.currentOTAUpdateDetails) {
|
|
300
|
+
lines.push(`Current software: v${ota.currentOTAUpdateDetails.version}`);
|
|
301
|
+
} else {
|
|
302
|
+
lines.push('Current software: unknown');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (ota.availableOTAUpdateDetails) {
|
|
306
|
+
lines.push(`Update available: v${ota.availableOTAUpdateDetails.version}`);
|
|
307
|
+
if (ota.availableOTAUpdateDetails.url) {
|
|
308
|
+
lines.push(`Release notes: ${ota.availableOTAUpdateDetails.url}`);
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
lines.push('No update available — software is up to date.');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return lines.join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function formatChargingSession(data) {
|
|
318
|
+
if (!data) return 'No active charging session.';
|
|
319
|
+
|
|
320
|
+
const lines = ['Charging Session'];
|
|
321
|
+
|
|
322
|
+
const add = (label, entry, suffix = '') => {
|
|
323
|
+
const value = entry?.value;
|
|
324
|
+
if (value !== undefined && value !== null) lines.push(` ${label}: ${value}${suffix}`);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
add('Battery', data.soc, '%');
|
|
328
|
+
add('Power', data.power, ' kW');
|
|
329
|
+
add('Range added', data.rangeAddedThisSession, ' miles');
|
|
330
|
+
add('Energy charged', data.totalChargedEnergy, ' kWh');
|
|
331
|
+
add('Current', data.current, ' A');
|
|
332
|
+
if (data.timeElapsed) lines.push(` Time elapsed: ${data.timeElapsed}`);
|
|
333
|
+
add('Time remaining', data.timeRemaining, ' min');
|
|
334
|
+
|
|
335
|
+
if (data.isRivianCharger) lines.push(' Network: Rivian Adventure Network');
|
|
336
|
+
if (data.isFreeSession) {
|
|
337
|
+
lines.push(' Cost: Free');
|
|
338
|
+
} else if (data.currentPrice) {
|
|
339
|
+
lines.push(` Cost so far: ${data.currentCurrency || '$'}${data.currentPrice}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
add('Charger state', data.vehicleChargerState);
|
|
343
|
+
|
|
344
|
+
return lines.join('\n');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function formatDriversAndKeys(data) {
|
|
348
|
+
const lines = [];
|
|
349
|
+
|
|
350
|
+
if (data.vin) lines.push(`Vehicle: ${data.vin}`);
|
|
351
|
+
|
|
352
|
+
if (!data.invitedUsers?.length) {
|
|
353
|
+
lines.push('No drivers or keys found.');
|
|
354
|
+
return lines.join('\n');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
lines.push('');
|
|
358
|
+
for (const user of data.invitedUsers) {
|
|
359
|
+
if (user.firstName) {
|
|
360
|
+
lines.push(`${user.firstName} ${user.lastName} (${user.email})`);
|
|
361
|
+
if (user.roles?.length) lines.push(` Roles: ${user.roles.join(', ')}`);
|
|
362
|
+
|
|
363
|
+
if (user.devices?.length) {
|
|
364
|
+
for (const d of user.devices) {
|
|
365
|
+
const name = d.deviceName || d.type;
|
|
366
|
+
const status = [
|
|
367
|
+
d.isPaired ? 'paired' : 'not paired',
|
|
368
|
+
d.isEnabled ? 'enabled' : 'disabled',
|
|
369
|
+
].join(', ');
|
|
370
|
+
lines.push(` ${name} — ${status}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
lines.push(`${user.email} (invited, ${user.status})`);
|
|
375
|
+
}
|
|
376
|
+
lines.push('');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return lines.join('\n').trim();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── Restore session on startup ────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
loadSession();
|
|
385
|
+
|
|
386
|
+
// ── MCP Server ────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
const server = new McpServer({
|
|
389
|
+
name: 'rivian',
|
|
390
|
+
version: '1.0.0',
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// ── Auth tools ────────────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
server.tool(
|
|
396
|
+
'rivian_login',
|
|
397
|
+
'Log in to your Rivian account. Rivian will send a verification code to your phone or email — use rivian_submit_otp to complete sign-in.',
|
|
398
|
+
{},
|
|
399
|
+
async () => {
|
|
400
|
+
const email = process.env.RIVIAN_EMAIL;
|
|
401
|
+
const password = process.env.RIVIAN_PASSWORD;
|
|
402
|
+
if (!email || !password) {
|
|
403
|
+
return text(
|
|
404
|
+
'Rivian credentials are not configured. Set RIVIAN_EMAIL and RIVIAN_PASSWORD in your MCP server settings.',
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
await rivian.createCsrfToken();
|
|
410
|
+
const { mfa } = await rivian.login(email, password);
|
|
411
|
+
saveSession();
|
|
412
|
+
|
|
413
|
+
if (mfa) {
|
|
414
|
+
return text(
|
|
415
|
+
"A verification code has been sent to your phone or email. Tell me the code and I'll complete the sign-in.",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return text('Signed in to Rivian successfully.');
|
|
420
|
+
} catch (err) {
|
|
421
|
+
return text(`Couldn't sign in: ${err.message}`);
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
server.tool(
|
|
427
|
+
'rivian_submit_otp',
|
|
428
|
+
'Complete Rivian sign-in with the verification code sent to your phone or email.',
|
|
429
|
+
{ otp_code: z.string().describe('The verification code') },
|
|
430
|
+
async ({ otp_code }) => {
|
|
431
|
+
const email = process.env.RIVIAN_EMAIL;
|
|
432
|
+
if (!email) {
|
|
433
|
+
return text('RIVIAN_EMAIL is not configured.');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!rivian.needsOtp()) {
|
|
437
|
+
return text('No pending verification. Start with rivian_login first.');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
await rivian.validateOtp(email, otp_code);
|
|
442
|
+
saveSession();
|
|
443
|
+
return text('Signed in to Rivian successfully.');
|
|
444
|
+
} catch (err) {
|
|
445
|
+
return text(
|
|
446
|
+
`Verification failed: ${err.message}. You may need to start over with rivian_login.`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// ── Read-only query tools ─────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
server.tool(
|
|
455
|
+
'rivian_get_user_info',
|
|
456
|
+
'Look up your Rivian account — your vehicles, software versions, and account details.',
|
|
457
|
+
{},
|
|
458
|
+
async () => {
|
|
459
|
+
try {
|
|
460
|
+
requireAuth();
|
|
461
|
+
return text(formatUserInfo(await rivian.getUserInfo()));
|
|
462
|
+
} catch (err) {
|
|
463
|
+
return text(err.message);
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
server.tool(
|
|
469
|
+
'rivian_get_vehicle_state',
|
|
470
|
+
"Check your vehicle's current status — battery, range, doors, tires, location, climate, software, and more.",
|
|
471
|
+
{
|
|
472
|
+
vehicle_id: z.string().describe('Vehicle ID from your account info'),
|
|
473
|
+
properties: z
|
|
474
|
+
.array(z.string())
|
|
475
|
+
.optional()
|
|
476
|
+
.describe('Specific properties to check. Leave empty for a full status report.'),
|
|
477
|
+
},
|
|
478
|
+
async ({ vehicle_id, properties }) => {
|
|
479
|
+
try {
|
|
480
|
+
requireAuth();
|
|
481
|
+
const props = properties ? new Set(properties) : undefined;
|
|
482
|
+
return text(formatVehicleState(await rivian.getVehicleState(vehicle_id, props)));
|
|
483
|
+
} catch (err) {
|
|
484
|
+
return text(err.message);
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
server.tool(
|
|
490
|
+
'rivian_get_ota_status',
|
|
491
|
+
"Check for software updates — what version you're running and whether a new one is available.",
|
|
492
|
+
{
|
|
493
|
+
vehicle_id: z.string().describe('Vehicle ID from your account info'),
|
|
494
|
+
},
|
|
495
|
+
async ({ vehicle_id }) => {
|
|
496
|
+
try {
|
|
497
|
+
requireAuth();
|
|
498
|
+
return text(formatOTAStatus(await rivian.getOTAUpdateDetails(vehicle_id)));
|
|
499
|
+
} catch (err) {
|
|
500
|
+
return text(err.message);
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
server.tool(
|
|
506
|
+
'rivian_get_charging_session',
|
|
507
|
+
'Check on an active charging session — power, battery level, time remaining, and cost.',
|
|
508
|
+
{
|
|
509
|
+
vehicle_id: z.string().describe('Vehicle ID from your account info'),
|
|
510
|
+
},
|
|
511
|
+
async ({ vehicle_id }) => {
|
|
512
|
+
try {
|
|
513
|
+
requireAuth();
|
|
514
|
+
return text(formatChargingSession(await rivian.getLiveChargingSession(vehicle_id)));
|
|
515
|
+
} catch (err) {
|
|
516
|
+
return text(err.message);
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
server.tool(
|
|
522
|
+
'rivian_get_drivers_and_keys',
|
|
523
|
+
'See who has access to your vehicle — drivers, phone keys, and key fobs.',
|
|
524
|
+
{
|
|
525
|
+
vehicle_id: z.string().describe('Vehicle ID from your account info'),
|
|
526
|
+
},
|
|
527
|
+
async ({ vehicle_id }) => {
|
|
528
|
+
try {
|
|
529
|
+
requireAuth();
|
|
530
|
+
return text(formatDriversAndKeys(await rivian.getDriversAndKeys(vehicle_id)));
|
|
531
|
+
} catch (err) {
|
|
532
|
+
return text(err.message);
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
// ── Start ─────────────────────────────────────────────────────────────
|
|
538
|
+
|
|
539
|
+
const transport = new StdioServerTransport();
|
|
540
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rivian-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Read-only MCP server for the Rivian API — vehicle state, OTA status, charging, and more",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rivian-mcp": "mcp-server.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "mcp-server.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"mcp-server.js",
|
|
12
|
+
"lib/"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node mcp-server.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"rivian",
|
|
19
|
+
"mcp",
|
|
20
|
+
"ev",
|
|
21
|
+
"vehicle",
|
|
22
|
+
"api"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/PatrickHeneise/rivian-mcp.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/PatrickHeneise/rivian-mcp#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/PatrickHeneise/rivian-mcp/issues"
|
|
31
|
+
},
|
|
32
|
+
"author": "Patrick Heneise",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"registry": "https://registry.npmjs.org/",
|
|
36
|
+
"tag": "latest",
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=24"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
44
|
+
"zod": "^3.24.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"semantic-release": "^25.0.3"
|
|
48
|
+
}
|
|
49
|
+
}
|