betterkey-mcp 0.0.0-stage → 0.1.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 +112 -2
- package/dist/http.d.ts +42 -0
- package/dist/http.js +209 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/_vehicle_command.d.ts +19 -0
- package/dist/tools/_vehicle_command.js +48 -0
- package/dist/tools/_vehicle_command.js.map +1 -0
- package/dist/tools/get_vehicle_capabilities.d.ts +3 -0
- package/dist/tools/get_vehicle_capabilities.js +72 -0
- package/dist/tools/get_vehicle_capabilities.js.map +1 -0
- package/dist/tools/get_vehicle_location.d.ts +3 -0
- package/dist/tools/get_vehicle_location.js +45 -0
- package/dist/tools/get_vehicle_location.js.map +1 -0
- package/dist/tools/get_vehicle_telemetry.d.ts +4 -0
- package/dist/tools/get_vehicle_telemetry.js +64 -0
- package/dist/tools/get_vehicle_telemetry.js.map +1 -0
- package/dist/tools/list_vehicles.d.ts +10 -0
- package/dist/tools/list_vehicles.js +66 -0
- package/dist/tools/list_vehicles.js.map +1 -0
- package/dist/tools/lock_vehicle.d.ts +3 -0
- package/dist/tools/lock_vehicle.js +15 -0
- package/dist/tools/lock_vehicle.js.map +1 -0
- package/dist/tools/unlock_vehicle.d.ts +3 -0
- package/dist/tools/unlock_vehicle.js +15 -0
- package/dist/tools/unlock_vehicle.js.map +1 -0
- package/package.json +51 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CMLabs LLC
|
|
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
CHANGED
|
@@ -1,3 +1,113 @@
|
|
|
1
|
-
#
|
|
1
|
+
# betterkey-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Model Context Protocol](https://modelcontextprotocol.io) server that lets AI agents inspect **and act on** your [BetterKey](https://betterkey.xyz)-connected vehicles.
|
|
4
|
+
|
|
5
|
+
Open-source bridge. You mint the key, you keep the control. The server never calls your vehicle's API directly; every request goes to BetterKey's existing `/v1/*` HTTP API over HTTPS, authenticated with an API key you mint in the BetterKey iOS app.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
Six tools are exposed over stdio:
|
|
10
|
+
|
|
11
|
+
| Tool | What it does | API-key scope required |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `list_vehicles` | Lists your connected vehicles (id, make, model, color). | `telemetry.read` |
|
|
14
|
+
| `get_vehicle_telemetry` | Returns the latest windows/odometer/fuel/battery snapshot with per-signal `oemUpdatedAt` and an `isStale` flag. | `telemetry.read` |
|
|
15
|
+
| `get_vehicle_capabilities` | Identity + which signals have fresh data right now + known upstream quirks the agent should respect. | `telemetry.read` |
|
|
16
|
+
| `lock_vehicle` | Lock the vehicle. **Destructive**. The agent must obtain explicit human confirmation. Returns `confirmation: "confirmed"` or `"unconfirmed"`. | `vehicle.command` |
|
|
17
|
+
| `unlock_vehicle` | Unlock the vehicle. **Destructive**, with the same confirmation and confirmation-status semantics as `lock_vehicle`. | `vehicle.command` |
|
|
18
|
+
| `get_vehicle_location` | Current OEM-reported location. Location-sensitive; coordinates are never logged by the server. | `vehicle.command` |
|
|
19
|
+
|
|
20
|
+
## What it does not do
|
|
21
|
+
|
|
22
|
+
- It does not read your Firestore directly.
|
|
23
|
+
- It does not hold a vehicle-manufacturer OAuth token.
|
|
24
|
+
- It does not call the vehicle manufacturer's API itself.
|
|
25
|
+
- It does not expose any way to bypass BetterKey's ownership, subscription, or rate-limit checks. Those run server-side on every call.
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
### 1. Mint a BetterKey API key
|
|
30
|
+
|
|
31
|
+
Open the BetterKey app on iOS → **Settings → API Keys → Create**. Copy the `bk_...` secret immediately; it is shown only once.
|
|
32
|
+
|
|
33
|
+
**Scopes.** A key minted from the iOS app today holds `telemetry.read + vehicle.command` by default, enough to drive every tool here. If you'd rather keep this bridge read-only (Homebridge-style), mint the key via a direct `POST /v1/account/api-keys` with `{"label":"...","scopes":["telemetry.read"]}`. A read-only key calling a command tool returns a tool-level `insufficient_scope` error naming the missing scope.
|
|
34
|
+
|
|
35
|
+
### 2. Register with your MCP host
|
|
36
|
+
|
|
37
|
+
**Claude Desktop.** Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
38
|
+
|
|
39
|
+
```jsonc
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"betterkey": {
|
|
43
|
+
"command": "npx",
|
|
44
|
+
"args": ["-y", "betterkey-mcp"],
|
|
45
|
+
"env": {
|
|
46
|
+
"BETTERKEY_API_KEY": "bk_...",
|
|
47
|
+
"BETTERKEY_API_BASE": "https://api.betterkey.xyz"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Restart Claude Desktop. The six tools should appear in the tool palette.
|
|
55
|
+
|
|
56
|
+
### 3. Try it
|
|
57
|
+
|
|
58
|
+
- "List my vehicles."
|
|
59
|
+
- "What's the latest telemetry for \<make>?"
|
|
60
|
+
- "What can BetterKey tell me about \<make> right now?"
|
|
61
|
+
- "Lock \<make>." The agent should confirm with you first; a `confirmation: "unconfirmed"` result means the lock command was accepted by BetterKey but the automaker's acknowledgement is unavailable (the lock may or may not have actuated).
|
|
62
|
+
- "Where is \<make>?" Returns latitude/longitude plus the OEM timestamp.
|
|
63
|
+
|
|
64
|
+
## Environment
|
|
65
|
+
|
|
66
|
+
| Variable | Required | Default | Description |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| `BETTERKEY_API_KEY` | yes | *(none)* | A `bk_...` secret from the BetterKey app. The server refuses to start without one. |
|
|
69
|
+
| `BETTERKEY_API_BASE` | no | `https://api.betterkey.xyz` | Point at `https://api-staging.betterkey.xyz` for staging, or `http://localhost:8080` when developing against a local BetterKey backend. |
|
|
70
|
+
|
|
71
|
+
## Revoking access
|
|
72
|
+
|
|
73
|
+
Agent-host access to your vehicle data lasts only as long as the API key is valid. To revoke:
|
|
74
|
+
|
|
75
|
+
1. In the BetterKey app → **Settings → API Keys** → delete the key.
|
|
76
|
+
2. All subsequent tool calls from the MCP server will fail with `unauthorized`.
|
|
77
|
+
|
|
78
|
+
To rotate: delete the old key, mint a new one, update `BETTERKEY_API_KEY` in your MCP host config, restart the host.
|
|
79
|
+
|
|
80
|
+
## Known upstream quirks the agent sees
|
|
81
|
+
|
|
82
|
+
The `get_vehicle_capabilities` tool returns a `knownQuirks` list so agents don't over-report:
|
|
83
|
+
|
|
84
|
+
- `isLockedWebhookUnreliable`. The vehicle's upstream lock-state webhook is defective at the provider level; any lock state reported here is last-known, not live.
|
|
85
|
+
- `statusEndpointReturns501`. There is no engine-run-state (on/off) tool for your vehicle.
|
|
86
|
+
|
|
87
|
+
If a tool call returns `401 unauthorized`, the API key was revoked or regenerated. If it returns `403` with `type: "insufficient_scope"` and `requiredScope: "vehicle.command"`, the key does not have the scope the called route requires; mint a new key with that scope and retry.
|
|
88
|
+
|
|
89
|
+
## Security notes
|
|
90
|
+
|
|
91
|
+
- The API key lives in your MCP host's env; it is never written to tool output or error messages.
|
|
92
|
+
- [Zod](https://zod.dev) validates every tool argument before it reaches the HTTP client.
|
|
93
|
+
- All path segments interpolated into URLs go through `encodeURIComponent`.
|
|
94
|
+
- Timeouts default to 20s with `Retry-After` honored on 429.
|
|
95
|
+
- `fetch` is called with `redirect: "manual"`; redirects fail closed.
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
git clone https://github.com/CMLabsLLC/betterkey-mcp.git
|
|
101
|
+
cd betterkey-mcp
|
|
102
|
+
npm install
|
|
103
|
+
npm run dev # run with tsx (no build step)
|
|
104
|
+
npm run typecheck # strict typecheck without emit
|
|
105
|
+
npm test # Node's built-in test runner
|
|
106
|
+
npm run build # emit dist/
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Point at a local backend by setting `BETTERKEY_API_BASE=http://localhost:8080` and running `make simulator-backend` in the main BetterKey repo.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
[MIT](./LICENSE)
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface BetterKeyClientOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
userAgent?: string;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BetterKeyErrorPayload {
|
|
8
|
+
error?: string;
|
|
9
|
+
type?: string;
|
|
10
|
+
message?: string;
|
|
11
|
+
retry?: {
|
|
12
|
+
recommended?: boolean;
|
|
13
|
+
after?: string;
|
|
14
|
+
reason?: string;
|
|
15
|
+
};
|
|
16
|
+
[k: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
export declare class BetterKeyApiError extends Error {
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly type: string;
|
|
21
|
+
readonly retryAfterSeconds?: number;
|
|
22
|
+
readonly payload: BetterKeyErrorPayload;
|
|
23
|
+
constructor(status: number, type: string, message: string, payload: BetterKeyErrorPayload, retryAfterSeconds?: number);
|
|
24
|
+
}
|
|
25
|
+
export declare class BetterKeyClient {
|
|
26
|
+
private readonly apiKey;
|
|
27
|
+
private readonly baseUrl;
|
|
28
|
+
private readonly userAgent;
|
|
29
|
+
private readonly timeoutMs;
|
|
30
|
+
constructor(opts: BetterKeyClientOptions);
|
|
31
|
+
get<T>(path: string, query?: Record<string, string | boolean | undefined>): Promise<T>;
|
|
32
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
33
|
+
postWithHeaders<T>(path: string, body?: unknown): Promise<{
|
|
34
|
+
body: T;
|
|
35
|
+
headers: Record<string, string>;
|
|
36
|
+
}>;
|
|
37
|
+
private buildUrl;
|
|
38
|
+
private request;
|
|
39
|
+
private requestWithHeaders;
|
|
40
|
+
}
|
|
41
|
+
export declare function httpErrorHint(type: string, requiredScope?: string): string | undefined;
|
|
42
|
+
export declare function redactApiKey(input: string, apiKey: string): string;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
export class BetterKeyApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
type;
|
|
4
|
+
retryAfterSeconds;
|
|
5
|
+
payload;
|
|
6
|
+
constructor(status, type, message, payload, retryAfterSeconds) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "BetterKeyApiError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.type = type;
|
|
11
|
+
this.payload = payload;
|
|
12
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 20_000;
|
|
16
|
+
export class BetterKeyClient {
|
|
17
|
+
apiKey;
|
|
18
|
+
baseUrl;
|
|
19
|
+
userAgent;
|
|
20
|
+
timeoutMs;
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
if (!opts.apiKey || opts.apiKey.trim() === "") {
|
|
23
|
+
throw new Error("BETTERKEY_API_KEY is required");
|
|
24
|
+
}
|
|
25
|
+
if (!opts.baseUrl || opts.baseUrl.trim() === "") {
|
|
26
|
+
throw new Error("BETTERKEY_API_BASE is required");
|
|
27
|
+
}
|
|
28
|
+
this.apiKey = opts.apiKey.trim();
|
|
29
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
30
|
+
this.userAgent = opts.userAgent ?? "betterkey-mcp/0.1.0";
|
|
31
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
32
|
+
}
|
|
33
|
+
async get(path, query) {
|
|
34
|
+
const url = this.buildUrl(path, query);
|
|
35
|
+
return this.request("GET", url);
|
|
36
|
+
}
|
|
37
|
+
async post(path, body) {
|
|
38
|
+
const url = this.buildUrl(path);
|
|
39
|
+
return this.request("POST", url, body);
|
|
40
|
+
}
|
|
41
|
+
// postWithHeaders exposes a few response headers alongside the parsed body so a tool
|
|
42
|
+
// that cares about X-Command-Confirmation ("confirmed" vs "unconfirmed") or
|
|
43
|
+
// X-Operation-Id (for idempotency debugging) can surface them without a second call.
|
|
44
|
+
async postWithHeaders(path, body) {
|
|
45
|
+
const url = this.buildUrl(path);
|
|
46
|
+
return this.requestWithHeaders("POST", url, body);
|
|
47
|
+
}
|
|
48
|
+
buildUrl(path, query) {
|
|
49
|
+
if (!path.startsWith("/")) {
|
|
50
|
+
throw new Error(`internal: path must start with /: ${path}`);
|
|
51
|
+
}
|
|
52
|
+
const url = new URL(this.baseUrl + path);
|
|
53
|
+
if (query) {
|
|
54
|
+
for (const [key, value] of Object.entries(query)) {
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
url.searchParams.set(key, String(value));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return url.toString();
|
|
61
|
+
}
|
|
62
|
+
async request(method, url, body) {
|
|
63
|
+
const result = await this.requestWithHeaders(method, url, body);
|
|
64
|
+
return result.body;
|
|
65
|
+
}
|
|
66
|
+
async requestWithHeaders(method, url, body) {
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
69
|
+
const headers = {
|
|
70
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
71
|
+
Accept: "application/json",
|
|
72
|
+
"User-Agent": this.userAgent,
|
|
73
|
+
};
|
|
74
|
+
let serializedBody;
|
|
75
|
+
if (body !== undefined) {
|
|
76
|
+
serializedBody = JSON.stringify(body);
|
|
77
|
+
headers["Content-Type"] = "application/json";
|
|
78
|
+
}
|
|
79
|
+
let res;
|
|
80
|
+
try {
|
|
81
|
+
res = await fetch(url, {
|
|
82
|
+
method,
|
|
83
|
+
headers,
|
|
84
|
+
body: serializedBody,
|
|
85
|
+
signal: controller.signal,
|
|
86
|
+
// Fail hard on any 3xx. WHATWG fetch would otherwise follow redirects
|
|
87
|
+
// and strip the Authorization header on cross-origin hops, surfacing
|
|
88
|
+
// as a confusing 401 somewhere downstream. The backend never redirects
|
|
89
|
+
// today, so flagging a redirect explicitly is the right failure mode.
|
|
90
|
+
redirect: "error",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
96
|
+
throw new BetterKeyApiError(0, "timeout", `Request timed out after ${this.timeoutMs}ms`, {});
|
|
97
|
+
}
|
|
98
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
99
|
+
throw new BetterKeyApiError(0, "network", redactApiKey(msg, this.apiKey), {});
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
if (res.ok) {
|
|
105
|
+
const parsed = (await res.json());
|
|
106
|
+
return { body: parsed, headers: pickExposedHeaders(res.headers) };
|
|
107
|
+
}
|
|
108
|
+
const retryAfterRaw = res.headers.get("Retry-After");
|
|
109
|
+
const retryAfterSeconds = parseRetryAfter(retryAfterRaw);
|
|
110
|
+
const payload = await safeParseErrorBody(res);
|
|
111
|
+
const type = typeof payload.type === "string" && payload.type.length > 0
|
|
112
|
+
? payload.type
|
|
113
|
+
: mapStatusToType(res.status);
|
|
114
|
+
const message = typeof payload.message === "string" && payload.message.length > 0
|
|
115
|
+
? payload.message
|
|
116
|
+
: typeof payload.error === "string" && payload.error.length > 0
|
|
117
|
+
? payload.error
|
|
118
|
+
: `HTTP ${res.status}`;
|
|
119
|
+
throw new BetterKeyApiError(res.status, type, redactApiKey(message, this.apiKey), payload, retryAfterSeconds);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function safeParseErrorBody(res) {
|
|
123
|
+
try {
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
if (!text)
|
|
126
|
+
return {};
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(text);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return { message: text };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function parseRetryAfter(value) {
|
|
139
|
+
if (!value)
|
|
140
|
+
return undefined;
|
|
141
|
+
const n = Number(value);
|
|
142
|
+
if (Number.isFinite(n) && n >= 0)
|
|
143
|
+
return n;
|
|
144
|
+
const d = Date.parse(value);
|
|
145
|
+
if (Number.isFinite(d)) {
|
|
146
|
+
const diff = Math.max(0, Math.round((d - Date.now()) / 1000));
|
|
147
|
+
return diff;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
function mapStatusToType(status) {
|
|
152
|
+
if (status === 401)
|
|
153
|
+
return "unauthorized";
|
|
154
|
+
if (status === 402)
|
|
155
|
+
return "subscription_required";
|
|
156
|
+
if (status === 403)
|
|
157
|
+
return "forbidden";
|
|
158
|
+
if (status === 404)
|
|
159
|
+
return "not_found";
|
|
160
|
+
if (status === 409)
|
|
161
|
+
return "conflict";
|
|
162
|
+
if (status === 429)
|
|
163
|
+
return "rate_limit";
|
|
164
|
+
if (status >= 500 && status < 600)
|
|
165
|
+
return "upstream";
|
|
166
|
+
return "unknown";
|
|
167
|
+
}
|
|
168
|
+
// httpErrorHint returns a short, agent-actionable sentence for well-known backend error
|
|
169
|
+
// types. Returning undefined means "no extra guidance" — the generic message is already
|
|
170
|
+
// clear enough. Centralized here so every tool error-path uses the same wording.
|
|
171
|
+
export function httpErrorHint(type, requiredScope) {
|
|
172
|
+
switch (type) {
|
|
173
|
+
case "unauthorized":
|
|
174
|
+
return "The BetterKey API key was rejected. Mint a new key in the BetterKey app and update the MCP server's BETTERKEY_API_KEY environment variable.";
|
|
175
|
+
case "insufficient_scope":
|
|
176
|
+
return requiredScope
|
|
177
|
+
? `This tool requires the ${requiredScope} scope on your BetterKey API key. Revoke the current key and mint a new one with that scope enabled.`
|
|
178
|
+
: "This tool requires a scope your current API key does not grant. Revoke the key and mint a new one with the needed scope.";
|
|
179
|
+
case "subscription_required":
|
|
180
|
+
return "An active BetterKey subscription is required for this action.";
|
|
181
|
+
case "conflict":
|
|
182
|
+
return "Another command is already in flight for this vehicle. Wait a few seconds and try again.";
|
|
183
|
+
case "rate_limit":
|
|
184
|
+
return "The backend is rate-limiting this key. Honor the retryAfterSeconds hint before trying again.";
|
|
185
|
+
default:
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export function redactApiKey(input, apiKey) {
|
|
190
|
+
if (!apiKey)
|
|
191
|
+
return input;
|
|
192
|
+
const prefix = apiKey.slice(0, 4);
|
|
193
|
+
return input.split(apiKey).join(`${prefix}…[redacted]`);
|
|
194
|
+
}
|
|
195
|
+
// EXPOSED_HEADERS is the allowlist of response headers that tools are permitted to echo
|
|
196
|
+
// back to the agent. Deliberately narrow: everything else (Set-Cookie, Server, Fly-Request-Id,
|
|
197
|
+
// anything we don't recognize) stays server-side so a header rename cannot accidentally
|
|
198
|
+
// become an agent-visible contract.
|
|
199
|
+
const EXPOSED_HEADERS = new Set(["x-command-confirmation", "x-operation-id", "x-smartcar-upstream-status"]);
|
|
200
|
+
function pickExposedHeaders(headers) {
|
|
201
|
+
const out = {};
|
|
202
|
+
headers.forEach((value, key) => {
|
|
203
|
+
if (EXPOSED_HEADERS.has(key.toLowerCase())) {
|
|
204
|
+
out[key.toLowerCase()] = value;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { BetterKeyClient } from "./http.js";
|
|
5
|
+
import { registerListVehicles } from "./tools/list_vehicles.js";
|
|
6
|
+
import { registerGetVehicleTelemetry } from "./tools/get_vehicle_telemetry.js";
|
|
7
|
+
import { registerGetVehicleCapabilities } from "./tools/get_vehicle_capabilities.js";
|
|
8
|
+
import { registerLockVehicle } from "./tools/lock_vehicle.js";
|
|
9
|
+
import { registerUnlockVehicle } from "./tools/unlock_vehicle.js";
|
|
10
|
+
import { registerGetVehicleLocation } from "./tools/get_vehicle_location.js";
|
|
11
|
+
const SERVER_NAME = "betterkey-mcp";
|
|
12
|
+
const SERVER_VERSION = "0.1.0";
|
|
13
|
+
const DEFAULT_API_BASE = "https://api.betterkey.xyz";
|
|
14
|
+
async function main() {
|
|
15
|
+
const apiKey = process.env.BETTERKEY_API_KEY;
|
|
16
|
+
const apiBase = process.env.BETTERKEY_API_BASE ?? DEFAULT_API_BASE;
|
|
17
|
+
if (!apiKey || apiKey.trim() === "") {
|
|
18
|
+
process.stderr.write("betterkey-mcp: BETTERKEY_API_KEY is required. Mint one in the BetterKey app's API Keys screen and set the env var in your MCP client's configuration.\n");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const client = new BetterKeyClient({
|
|
22
|
+
apiKey,
|
|
23
|
+
baseUrl: apiBase,
|
|
24
|
+
userAgent: `${SERVER_NAME}/${SERVER_VERSION}`,
|
|
25
|
+
});
|
|
26
|
+
const server = new McpServer({
|
|
27
|
+
name: SERVER_NAME,
|
|
28
|
+
version: SERVER_VERSION,
|
|
29
|
+
});
|
|
30
|
+
registerListVehicles(server, client);
|
|
31
|
+
registerGetVehicleTelemetry(server, client);
|
|
32
|
+
registerGetVehicleCapabilities(server, client);
|
|
33
|
+
registerLockVehicle(server, client);
|
|
34
|
+
registerUnlockVehicle(server, client);
|
|
35
|
+
registerGetVehicleLocation(server, client);
|
|
36
|
+
const transport = new StdioServerTransport();
|
|
37
|
+
await server.connect(transport);
|
|
38
|
+
}
|
|
39
|
+
main().catch((err) => {
|
|
40
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
41
|
+
process.stderr.write(`betterkey-mcp: fatal: ${msg}\n`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC;AAC/E,OAAO,EAAE,8BAA8B,EAAE,MAAM,qCAAqC,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAE7E,MAAM,WAAW,GAAG,eAAe,CAAC;AACpC,MAAM,cAAc,GAAG,OAAO,CAAC;AAC/B,MAAM,gBAAgB,GAAG,2BAA2B,CAAC;AAErD,KAAK,UAAU,IAAI;IACf,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;IAC7C,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,gBAAgB,CAAC;IAEnE,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAClC,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,yJAAyJ,CAC5J,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QAC/B,MAAM;QACN,OAAO,EAAE,OAAO;QAChB,SAAS,EAAE,GAAG,WAAW,IAAI,cAAc,EAAE;KAChD,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QACzB,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,cAAc;KAC1B,CAAC,CAAC;IAEH,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,2BAA2B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,8BAA8B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,0BAA0B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE3C,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AACpC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC1B,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAAC;IACvD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { z as ZodNamespace } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { type BetterKeyClient } from "../http.js";
|
|
4
|
+
export interface VehicleCommandOptions {
|
|
5
|
+
toolName: "lock_vehicle" | "unlock_vehicle";
|
|
6
|
+
title: string;
|
|
7
|
+
command: "lock" | "unlock";
|
|
8
|
+
path: (vehicleId: string) => string;
|
|
9
|
+
description: string;
|
|
10
|
+
toolErrorResult: (err: unknown) => {
|
|
11
|
+
isError: boolean;
|
|
12
|
+
content: Array<{
|
|
13
|
+
type: "text";
|
|
14
|
+
text: string;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
z: typeof ZodNamespace;
|
|
18
|
+
}
|
|
19
|
+
export declare function registerVehicleCommand(server: McpServer, client: BetterKeyClient, opts: VehicleCommandOptions): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// registerVehicleCommand factors the shared machinery for lock_vehicle and unlock_vehicle:
|
|
2
|
+
// identical input schema, destructiveHint, output shape (body + exposed headers), and
|
|
3
|
+
// error handling. Each command tool is a thin shell around this so the "confirmed vs
|
|
4
|
+
// unconfirmed" surfacing cannot drift between lock and unlock.
|
|
5
|
+
export function registerVehicleCommand(server, client, opts) {
|
|
6
|
+
const { toolName, title, command, path, description, toolErrorResult, z } = opts;
|
|
7
|
+
server.registerTool(toolName, {
|
|
8
|
+
title,
|
|
9
|
+
description,
|
|
10
|
+
annotations: {
|
|
11
|
+
destructiveHint: true,
|
|
12
|
+
openWorldHint: true,
|
|
13
|
+
idempotentHint: false,
|
|
14
|
+
},
|
|
15
|
+
inputSchema: {
|
|
16
|
+
vehicle_id: z.string().min(1).describe("The vehicle id from list_vehicles."),
|
|
17
|
+
},
|
|
18
|
+
}, async (args) => {
|
|
19
|
+
try {
|
|
20
|
+
const vehicleId = encodeURIComponent(args.vehicle_id);
|
|
21
|
+
const result = await client.postWithHeaders(path(vehicleId));
|
|
22
|
+
// Fail-safe: when the backend returns 2xx but gives us no explicit
|
|
23
|
+
// confirmation marker (neither the x-command-confirmation header nor
|
|
24
|
+
// the body.confirmation field), surface "unconfirmed" rather than
|
|
25
|
+
// defaulting to "confirmed". The product promise is that the agent
|
|
26
|
+
// reports uncertainty honestly; defaulting to confirmed on a missing
|
|
27
|
+
// marker would silently violate that if backend contract ever drifts.
|
|
28
|
+
const confirmation = result.headers["x-command-confirmation"] ??
|
|
29
|
+
(typeof result.body.confirmation === "string" ? result.body.confirmation : "unconfirmed");
|
|
30
|
+
const output = {
|
|
31
|
+
status: result.body.status ?? "success",
|
|
32
|
+
command,
|
|
33
|
+
vehicleId: result.body.vehicleId ?? args.vehicle_id,
|
|
34
|
+
confirmation,
|
|
35
|
+
message: result.body.message,
|
|
36
|
+
operationId: result.headers["x-operation-id"],
|
|
37
|
+
upstreamStatus: result.headers["x-smartcar-upstream-status"],
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return toolErrorResult(err);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=_vehicle_command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_vehicle_command.js","sourceRoot":"","sources":["../../src/tools/_vehicle_command.ts"],"names":[],"mappings":"AAiBA,2FAA2F;AAC3F,sFAAsF;AACtF,qFAAqF;AACrF,+DAA+D;AAC/D,MAAM,UAAU,sBAAsB,CAClC,MAAiB,EACjB,MAAuB,EACvB,IAA2B;IAE3B,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC;IAEjF,MAAM,CAAC,YAAY,CACf,QAAQ,EACR;QACI,KAAK;QACL,WAAW;QACX,WAAW,EAAE;YACT,eAAe,EAAE,IAAI;YACrB,aAAa,EAAE,IAAI;YACnB,cAAc,EAAE,KAAK;SACxB;QACD,WAAW,EAAE;YACT,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,oCAAoC,CAAC;SAC/E;KACJ,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;QACX,IAAI,CAAC;YACD,MAAM,SAAS,GAAG,kBAAkB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACtD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,eAAe,CAAuB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YACnF,mEAAmE;YACnE,qEAAqE;YACrE,kEAAkE;YAClE,mEAAmE;YACnE,qEAAqE;YACrE,sEAAsE;YACtE,MAAM,YAAY,GACd,MAAM,CAAC,OAAO,CAAC,wBAAwB,CAAC;gBACxC,CAAC,OAAO,MAAM,CAAC,IAAI,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;YAC9F,MAAM,MAAM,GAAG;gBACX,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,SAAS;gBACvC,OAAO;gBACP,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,UAAU;gBACnD,YAAY;gBACZ,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO;gBAC5B,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC;gBAC7C,cAAc,EAAE,MAAM,CAAC,OAAO,CAAC,4BAA4B,CAAC;aAC/D,CAAC;YACF,OAAO;gBACH,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;aAC9E,CAAC;QACN,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,OAAO,eAAe,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;IACL,CAAC,CACJ,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { toolErrorResult } from "./list_vehicles.js";
|
|
3
|
+
const VehicleSummary = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
displayName: z.string(),
|
|
6
|
+
make: z.string(),
|
|
7
|
+
model: z.string(),
|
|
8
|
+
color: z.string().optional().default(""),
|
|
9
|
+
});
|
|
10
|
+
const ListVehiclesResponse = z.object({
|
|
11
|
+
vehicles: z.array(VehicleSummary),
|
|
12
|
+
});
|
|
13
|
+
const TelemetryResponse = z.object({
|
|
14
|
+
vehicleId: z.string(),
|
|
15
|
+
capabilities: z.record(z.boolean()),
|
|
16
|
+
});
|
|
17
|
+
// Static Smartcar/BetterKey quirks the agent should know when reasoning about
|
|
18
|
+
// what the car can report. Not derived from the backend; these are
|
|
19
|
+
// well-documented upstream behaviors that will not become false between
|
|
20
|
+
// requests. Keep this list short — only quirks that would mislead an agent if
|
|
21
|
+
// omitted.
|
|
22
|
+
const KNOWN_QUIRKS = [
|
|
23
|
+
{
|
|
24
|
+
id: "isLockedWebhookUnreliable",
|
|
25
|
+
description: "Smartcar's Closure.IsLocked webhook is known upstream to be unreliable. Any lock-state value in the capabilities map or on past tool calls is last-known, not live. Do not report 'currently locked' with high confidence from a telemetry read; a lock command's response is more reliable than any pushed lock state.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "statusEndpointReturns501",
|
|
29
|
+
description: "BetterKey has no engine-run-state (on/off) tool for Smartcar vehicles; the underlying endpoint returns HTTP 501 Not Implemented for this provider. Do not promise to tell the user whether the engine is running.",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
export function registerGetVehicleCapabilities(server, client) {
|
|
33
|
+
server.registerTool("get_vehicle_capabilities", {
|
|
34
|
+
title: "Get vehicle capabilities",
|
|
35
|
+
description: "Describe what BetterKey can currently report for one vehicle: identity (make/model/color), the per-signal freshCapabilities map (true when a fresh reading is available right now), and a knownQuirks list of upstream Smartcar behaviors the agent should respect when reasoning about results.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
vehicle_id: z.string().min(1).describe("The vehicle id from list_vehicles."),
|
|
38
|
+
},
|
|
39
|
+
}, async (args) => {
|
|
40
|
+
try {
|
|
41
|
+
const vehicleId = encodeURIComponent(args.vehicle_id);
|
|
42
|
+
const [listRaw, telemetryRaw] = await Promise.all([
|
|
43
|
+
client.get("/v1/vehicles"),
|
|
44
|
+
client.get(`/v1/vehicles/${vehicleId}/telemetry`),
|
|
45
|
+
]);
|
|
46
|
+
const list = ListVehiclesResponse.parse(listRaw);
|
|
47
|
+
const telemetry = TelemetryResponse.parse(telemetryRaw);
|
|
48
|
+
const summary = list.vehicles.find((v) => v.id === args.vehicle_id);
|
|
49
|
+
const body = {
|
|
50
|
+
vehicleId: telemetry.vehicleId,
|
|
51
|
+
provider: "smartcar",
|
|
52
|
+
identity: summary
|
|
53
|
+
? {
|
|
54
|
+
displayName: summary.displayName,
|
|
55
|
+
make: summary.make,
|
|
56
|
+
model: summary.model,
|
|
57
|
+
color: summary.color,
|
|
58
|
+
}
|
|
59
|
+
: null,
|
|
60
|
+
freshCapabilities: telemetry.capabilities,
|
|
61
|
+
knownQuirks: KNOWN_QUIRKS,
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return toolErrorResult(err);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=get_vehicle_capabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get_vehicle_capabilities.js","sourceRoot":"","sources":["../../src/tools/get_vehicle_capabilities.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { toolErrorResult } from "./list_vehicles.js";
|
|
3
|
+
const LocationResponse = z.object({
|
|
4
|
+
location: z.object({
|
|
5
|
+
latitude: z.number(),
|
|
6
|
+
longitude: z.number(),
|
|
7
|
+
locationTimestamp: z.string().optional(),
|
|
8
|
+
}),
|
|
9
|
+
});
|
|
10
|
+
export function registerGetVehicleLocation(server, client) {
|
|
11
|
+
server.registerTool("get_vehicle_location", {
|
|
12
|
+
title: "Get vehicle location",
|
|
13
|
+
description: "Fetch the vehicle's current reported location (latitude, longitude, and the OEM timestamp when known). This is sensitive data — the agent host should treat the result accordingly and should not persist coordinates beyond what the user asked for. Returns 503 with a Retry-After hint when the automaker's location pipe is in-flight or temporarily unavailable; retry only when the user asks.",
|
|
14
|
+
annotations: {
|
|
15
|
+
destructiveHint: false,
|
|
16
|
+
readOnlyHint: true,
|
|
17
|
+
openWorldHint: true,
|
|
18
|
+
},
|
|
19
|
+
inputSchema: {
|
|
20
|
+
vehicle_id: z.string().min(1).describe("The vehicle id from list_vehicles."),
|
|
21
|
+
},
|
|
22
|
+
}, async (args) => {
|
|
23
|
+
try {
|
|
24
|
+
const vehicleId = encodeURIComponent(args.vehicle_id);
|
|
25
|
+
const raw = await client.get(`/v1/vehicles/${vehicleId}/location`);
|
|
26
|
+
const parsed = LocationResponse.parse(raw);
|
|
27
|
+
return {
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: JSON.stringify({
|
|
32
|
+
latitude: parsed.location.latitude,
|
|
33
|
+
longitude: parsed.location.longitude,
|
|
34
|
+
locationTimestamp: parsed.location.locationTimestamp,
|
|
35
|
+
}, null, 2),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return toolErrorResult(err);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=get_vehicle_location.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get_vehicle_location.js","sourceRoot":"","sources":["../../src/tools/get_vehicle_location.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;QACrB,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAC3C,CAAC;CACL,CAAC,CAAC;AAEH,MAAM,UAAU,0BAA0B,CAAC,MAAiB,EAAE,MAAuB;IACjF,MAAM,CAAC,YAAY,CACf,sBAAsB,EACtB;QACI,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EACP,sYAAsY;QAC1Y,WAAW,EAAE;YACT,eAAe,EAAE,KAAK;YACtB,YAAY,EAAE,IAAI;YAClB,aAAa,EAAE,IAAI;SACtB;QACD,WAAW,EAAE;YACT,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,oCAAoC,CAAC;SAC/E;KACJ,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;QACX,IAAI,CAAC;YACD,MAAM,SAAS,GAAG,kBAAkB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACtD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,GAAG,CAAU,gBAAgB,SAAS,WAAW,CAAC,CAAC;YAC5E,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC3C,OAAO;gBACH,OAAO,EAAE;oBACL;wBACI,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,IAAI,CAAC,SAAS,CAChB;4BACI,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ;4BAClC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,SAAS;4BACpC,iBAAiB,EAAE,MAAM,CAAC,QAAQ,CAAC,iBAAiB;yBACvD,EACD,IAAI,EACJ,CAAC,CACJ;qBACJ;iBACJ;aACJ,CAAC;QACN,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,OAAO,eAAe,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;IACL,CAAC,CACJ,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type BetterKeyClient } from "../http.js";
|
|
3
|
+
export declare function registerGetVehicleTelemetry(server: McpServer, client: BetterKeyClient): void;
|
|
4
|
+
export declare function isStale(oemUpdatedAt: string | undefined, now: number): boolean;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { toolErrorResult } from "./list_vehicles.js";
|
|
3
|
+
const SignalEnvelope = z.object({
|
|
4
|
+
oemUpdatedAt: z.string().optional(),
|
|
5
|
+
retrievedAt: z.string().optional(),
|
|
6
|
+
}).passthrough();
|
|
7
|
+
const TelemetryResponse = z.object({
|
|
8
|
+
vehicleId: z.string(),
|
|
9
|
+
capabilities: z.record(z.boolean()),
|
|
10
|
+
signals: z.record(SignalEnvelope).optional(),
|
|
11
|
+
});
|
|
12
|
+
const STALE_THRESHOLD_MS = 15 * 60 * 1000;
|
|
13
|
+
export function registerGetVehicleTelemetry(server, client) {
|
|
14
|
+
server.registerTool("get_vehicle_telemetry", {
|
|
15
|
+
title: "Get vehicle telemetry",
|
|
16
|
+
description: "Fetch the latest telemetry snapshot (windows, odometer, fuel, battery) for one vehicle. Each returned signal carries oemUpdatedAt from the automaker and an isStale flag (true when the reading is older than 15 minutes). Pass refresh=true to bypass the server's 15-minute cache; most calls should leave it false.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
vehicle_id: z.string().min(1).describe("The vehicle id from list_vehicles."),
|
|
19
|
+
refresh: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("If true, bypass the server-side 15 min signal cache. Default false."),
|
|
23
|
+
},
|
|
24
|
+
}, async (args) => {
|
|
25
|
+
try {
|
|
26
|
+
const vehicleId = encodeURIComponent(args.vehicle_id);
|
|
27
|
+
const query = args.refresh ? { refresh: "true" } : undefined;
|
|
28
|
+
const raw = await client.get(`/v1/vehicles/${vehicleId}/telemetry`, query);
|
|
29
|
+
const parsed = TelemetryResponse.parse(raw);
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const signalsWithStaleness = {};
|
|
32
|
+
for (const [name, signal] of Object.entries(parsed.signals ?? {})) {
|
|
33
|
+
signalsWithStaleness[name] = {
|
|
34
|
+
...signal,
|
|
35
|
+
isStale: isStale(signal.oemUpdatedAt, now),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: JSON.stringify({
|
|
43
|
+
vehicleId: parsed.vehicleId,
|
|
44
|
+
capabilities: parsed.capabilities,
|
|
45
|
+
signals: signalsWithStaleness,
|
|
46
|
+
}, null, 2),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return toolErrorResult(err);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function isStale(oemUpdatedAt, now) {
|
|
57
|
+
if (!oemUpdatedAt)
|
|
58
|
+
return true;
|
|
59
|
+
const parsed = Date.parse(oemUpdatedAt);
|
|
60
|
+
if (!Number.isFinite(parsed))
|
|
61
|
+
return true;
|
|
62
|
+
return now - parsed > STALE_THRESHOLD_MS;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=get_vehicle_telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get_vehicle_telemetry.js","sourceRoot":"","sources":["../../src/tools/get_vehicle_telemetry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type BetterKeyClient } from "../http.js";
|
|
3
|
+
export declare function registerListVehicles(server: McpServer, client: BetterKeyClient): void;
|
|
4
|
+
export declare function toolErrorResult(err: unknown): {
|
|
5
|
+
isError: boolean;
|
|
6
|
+
content: {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}[];
|
|
10
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BetterKeyApiError, httpErrorHint } from "../http.js";
|
|
3
|
+
const VehicleSummary = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
displayName: z.string(),
|
|
6
|
+
make: z.string(),
|
|
7
|
+
model: z.string(),
|
|
8
|
+
color: z.string().optional().default(""),
|
|
9
|
+
});
|
|
10
|
+
const ListVehiclesResponse = z.object({
|
|
11
|
+
vehicles: z.array(VehicleSummary),
|
|
12
|
+
});
|
|
13
|
+
export function registerListVehicles(server, client) {
|
|
14
|
+
server.registerTool("list_vehicles", {
|
|
15
|
+
title: "List vehicles",
|
|
16
|
+
description: "List the user's Smartcar-connected vehicles in BetterKey. Returns id, display name, make, model, and exterior color. Use the id as the vehicle_id argument to other tools.",
|
|
17
|
+
inputSchema: {},
|
|
18
|
+
}, async () => {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await client.get("/v1/vehicles");
|
|
21
|
+
const parsed = ListVehiclesResponse.parse(raw);
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: "text",
|
|
26
|
+
text: JSON.stringify({ vehicles: parsed.vehicles }, null, 2),
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
return toolErrorResult(err);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function toolErrorResult(err) {
|
|
37
|
+
if (err instanceof BetterKeyApiError) {
|
|
38
|
+
const body = {
|
|
39
|
+
error: true,
|
|
40
|
+
type: err.type,
|
|
41
|
+
status: err.status,
|
|
42
|
+
message: err.message,
|
|
43
|
+
};
|
|
44
|
+
if (err.retryAfterSeconds !== undefined)
|
|
45
|
+
body.retryAfterSeconds = err.retryAfterSeconds;
|
|
46
|
+
const requiredScope = typeof err.payload["required"] === "string" ? err.payload["required"] : undefined;
|
|
47
|
+
if (requiredScope)
|
|
48
|
+
body.requiredScope = requiredScope;
|
|
49
|
+
const granted = err.payload["granted"];
|
|
50
|
+
if (Array.isArray(granted))
|
|
51
|
+
body.grantedScopes = granted;
|
|
52
|
+
const hint = httpErrorHint(err.type, requiredScope);
|
|
53
|
+
if (hint)
|
|
54
|
+
body.hint = hint;
|
|
55
|
+
return {
|
|
56
|
+
isError: true,
|
|
57
|
+
content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
61
|
+
return {
|
|
62
|
+
isError: true,
|
|
63
|
+
content: [{ type: "text", text: JSON.stringify({ error: true, type: "internal", message }, null, 2) }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=list_vehicles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list_vehicles.js","sourceRoot":"","sources":["../../src/tools/list_vehicles.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { toolErrorResult } from "./list_vehicles.js";
|
|
3
|
+
import { registerVehicleCommand } from "./_vehicle_command.js";
|
|
4
|
+
export function registerLockVehicle(server, client) {
|
|
5
|
+
registerVehicleCommand(server, client, {
|
|
6
|
+
toolName: "lock_vehicle",
|
|
7
|
+
title: "Lock vehicle",
|
|
8
|
+
command: "lock",
|
|
9
|
+
path: (id) => `/v1/vehicles/${id}/lock`,
|
|
10
|
+
description: "Lock the vehicle. This physically actuates the car's door locks. The agent MUST obtain explicit human confirmation before invoking this tool. The response includes a confirmation field: 'confirmed' means the automaker acknowledged the command; 'unconfirmed' means BetterKey accepted it but the automaker's acknowledgement is unavailable (the lock may or may not have actuated) — report that honestly rather than assuming success.",
|
|
11
|
+
toolErrorResult,
|
|
12
|
+
z,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=lock_vehicle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock_vehicle.js","sourceRoot":"","sources":["../../src/tools/lock_vehicle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,MAAM,UAAU,mBAAmB,CAAC,MAAiB,EAAE,MAAuB;IAC1E,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE;QACnC,QAAQ,EAAE,cAAc;QACxB,KAAK,EAAE,cAAc;QACrB,OAAO,EAAE,MAAM;QACf,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,gBAAgB,EAAE,OAAO;QACvC,WAAW,EACP,+aAA+a;QACnb,eAAe;QACf,CAAC;KACJ,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { toolErrorResult } from "./list_vehicles.js";
|
|
3
|
+
import { registerVehicleCommand } from "./_vehicle_command.js";
|
|
4
|
+
export function registerUnlockVehicle(server, client) {
|
|
5
|
+
registerVehicleCommand(server, client, {
|
|
6
|
+
toolName: "unlock_vehicle",
|
|
7
|
+
title: "Unlock vehicle",
|
|
8
|
+
command: "unlock",
|
|
9
|
+
path: (id) => `/v1/vehicles/${id}/unlock`,
|
|
10
|
+
description: "Unlock the vehicle. This physically actuates the car's door locks and will leave the car unsecured until it is locked again. The agent MUST obtain explicit human confirmation before invoking this tool and should surface a clear reason to the user. The response includes a confirmation field: 'confirmed' means the automaker acknowledged the command; 'unconfirmed' means BetterKey accepted it but the automaker's acknowledgement is unavailable — report that honestly rather than assuming success.",
|
|
11
|
+
toolErrorResult,
|
|
12
|
+
z,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=unlock_vehicle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unlock_vehicle.js","sourceRoot":"","sources":["../../src/tools/unlock_vehicle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,MAAM,UAAU,qBAAqB,CAAC,MAAiB,EAAE,MAAuB;IAC5E,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE;QACnC,QAAQ,EAAE,gBAAgB;QAC1B,KAAK,EAAE,gBAAgB;QACvB,OAAO,EAAE,QAAQ;QACjB,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,gBAAgB,EAAE,SAAS;QACzC,WAAW,EACP,ifAAif;QACrf,eAAe;QACf,CAAC;KACJ,CAAC,CAAC;AACP,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "betterkey-mcp",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"author": "CMLabs LLC",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Model Context Protocol (MCP) server exposing BetterKey vehicle data and commands to AI agents.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"betterkey-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/**/*",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"test": "node --test --import tsx $(find src -name '*.test.ts')",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
26
|
+
"zod": "^3.23.8"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.14.0",
|
|
30
|
+
"tsx": "^4.19.0",
|
|
31
|
+
"typescript": "^5.5.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/CMLabsLLC/betterkey-mcp.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/CMLabsLLC/betterkey-mcp/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://betterkey.xyz",
|
|
44
|
+
"keywords": [
|
|
45
|
+
"mcp",
|
|
46
|
+
"model-context-protocol",
|
|
47
|
+
"betterkey",
|
|
48
|
+
"vehicle",
|
|
49
|
+
"smartcar",
|
|
50
|
+
"ai-agent"
|
|
51
|
+
],
|
|
52
|
+
"license": "MIT"
|
|
53
|
+
}
|