alexa-mcp 0.2.2 → 0.3.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/README.md CHANGED
@@ -1,138 +1,149 @@
1
- # alexa-mcp
1
+ # Alexa MCP
2
2
 
3
- MCP server and CLI for Alexa devices and smart home control via the unofficial Alexa API.
3
+ [![npm version](https://badge.fury.io/js/alexa-mcp.svg)](https://www.npmjs.com/package/alexa-mcp)
4
+ [![CI](https://github.com/m0nkmaster/alexa-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/m0nkmaster/alexa-mcp/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
6
 
5
- ## Requirements
6
-
7
- - Node.js 18+
8
- - Amazon Alexa account (amazon.com, amazon.co.uk, or amazon.de)
7
+ **Control your Alexa devices and smart home from the command line or AI assistants.**
9
8
 
10
- ## Setup
9
+ MCP server and CLI for Alexa/Echo devices and smart home control via the unofficial Alexa API. Works with Claude Desktop, Cursor, VS Code, and other MCP-compatible tools.
11
10
 
12
- 1. Install:
13
- ```bash
14
- npm install alexa-mcp # local install
15
- npm install -g alexa-mcp # global install (adds alexa-mcp to PATH)
16
- npx alexa-mcp auth # run without installing
17
- ```
11
+ ## Features
18
12
 
19
- 2. Authenticate:
20
- ```bash
21
- alexa-mcp auth
22
- ```
23
- Opens a URL (tunnel or localhost) for you to log in to Amazon. Works locally or headless (remote server) — same behaviour either way. Uses cloudflared or localtunnel automatically; no account required.
13
+ - 🎙️ **Voice & Media Control** - TTS, announcements, playback control
14
+ - 💡 **Smart Home** - Control lights, plugs, and devices by name, pattern, or room group
15
+ - ✅ **State Verification** - Every control command returns live device state (power, brightness, colour temp)
16
+ - 🔍 **Device Status** - Query current state of any device by name without issuing a command
17
+ - 🏠 **Group Membership** - List all devices in a room group for post-action verification
18
+ - 🤖 **Routines** - List and run routines by name, partial name, or automation ID
19
+ - 🌍 **Multi-Region** - Supports US (amazon.com), UK (amazon.co.uk), and DE (amazon.de)
20
+ - 🔌 **MCP Integration** - Use with Claude, Cursor, and other AI assistants
21
+ - 🛠️ **CLI & Programmatic** - Command-line tool or Node.js library
24
22
 
25
- Or headless:
26
- ```bash
27
- alexa-mcp auth --token "Atnr|..."
28
- alexa-mcp auth --token-file /path/to/token.txt
29
- alexa-mcp auth --domain amazon.com # US account (default: amazon.co.uk)
30
- alexa-mcp auth --no-save # validate token without saving
31
- ```
23
+ ## Quick Start
32
24
 
33
- 3. Config stored in `~/.alexa-mcp/config.json`. Also reads `~/.alexa-cli/config.json` or `ALEXA_REFRESH_TOKEN`.
25
+ ### Installation
34
26
 
35
- ### Environment variables
27
+ ```bash
28
+ # Global install (recommended for CLI usage)
29
+ npm install -g alexa-mcp
36
30
 
37
- | Variable | Description |
38
- |----------|-------------|
39
- | `ALEXA_REFRESH_TOKEN` | Refresh token; skips config file lookup |
40
- | `ALEXA_DOMAIN` | Amazon domain when using env token (default: `amazon.co.uk`; options: `amazon.com`, `amazon.de`) |
41
- | `ALEXA_DEBUG` | Set to any value to log API request/response details to stderr |
31
+ # Or use without installing
32
+ npx alexa-mcp auth
33
+ ```
42
34
 
43
- ## CLI
35
+ ### Authentication
36
+ **Interactive (browser-based):**
37
+ ```bash
38
+ alexa-mcp auth
39
+ ```
40
+ Opens a URL for you to log in to Amazon. Works locally or on remote servers using automatic tunneling (cloudflared or localtunnel).
44
41
 
42
+ **Headless (token-based):**
45
43
  ```bash
46
- alexa-mcp auth # Interactive auth (browser / tunnel URL)
47
- alexa-mcp auth --token <token> # Save token (headless)
48
- alexa-mcp auth --token-file <path> # Read token from file
49
- alexa-mcp auth --domain amazon.com # Specify Amazon domain (default: amazon.co.uk)
50
- alexa-mcp auth --no-save # Validate token without saving
51
- alexa-mcp auth status [--verify] # Show auth status (--verify calls API)
52
- alexa-mcp auth logout # Remove credentials
53
- alexa-mcp devices # List Echo devices
54
- alexa-mcp devices --owners # Show device names and owner customer IDs (profile matching)
55
- alexa-mcp speak "Hello" -d Office # Speak text on a specific device
56
- alexa-mcp announce "Dinner ready" # Announce to all devices
57
- alexa-mcp command -d Office "play jazz" # Voice command (no response returned)
58
- alexa-mcp groups # List room/space groups (Kitchen, Living room, etc.)
59
- alexa-mcp switch-group Kitchen off # Turn off all lights in a group
60
- alexa-mcp switch-group Kitchen off --all # Turn off ALL appliances in group (not just lights)
61
- alexa-mcp switch-room "kitchen lights" off # Turn off all devices matching name pattern
62
- alexa-mcp switch "Lounge light 2" off # Turn off single device by name (direct control; -d for voice fallback)
63
- alexa-mcp appliances # List smart home devices (endpointId + friendlyName when available)
64
- alexa-mcp control <entityId> turnOn|turnOff|setBrightness [--brightness 50]
65
- alexa-mcp routines # List routines
66
- alexa-mcp run <automationId> # Run a routine
67
- alexa-mcp now-playing -d Office # Now-playing state (EU/UK)
68
- alexa-mcp media play|pause|resume|stop|next|previous -d Office # Transport control (EU/UK)
44
+ alexa-mcp auth --token "Atnr|..."
45
+ alexa-mcp auth --token-file /path/to/token.txt
46
+ alexa-mcp auth --domain amazon.com # US account (default: amazon.co.uk)
69
47
  ```
70
48
 
71
- **Smart home:** For "all lights in group Kitchen", use `switch-group Kitchen off` (use `groups` to list group names). For pattern matching (e.g. "kitchen lights"), use `switch-room`. Both use direct control—avoids voice profile issues. `switch` is for a single device. Voice commands (`command`) do not return Alexa's response. `switch-group` targets only lights by default; add `--all` to include all appliances. `media` commands require active playback (`resume` re-starts paused playback). See [docs/API.md](docs/API.md).
49
+ Configuration is stored in `~/.alexa-mcp/config.json`.
72
50
 
73
- ### "Can't control – may need to switch user accounts"
51
+ ## Usage
74
52
 
75
- If the Echo says it can't control the device and suggests switching user accounts:
53
+ ### CLI Commands
76
54
 
77
- - **Who the CLI uses:** The CLI always acts as the **Amazon account you signed in with** when you last ran `alexa-mcp auth`. It does not use the Echo’s current profile. Changing the Echo’s profile in the Alexa app does **not** change which account the CLI uses.
78
- - **When you use profiles (e.g. Rob vs Emma):** You must run the CLI as the **same account that owns the smart home device**. So:
79
- 1. Run `alexa-mcp auth logout`.
80
- 2. Run `alexa-mcp auth` and sign in as the **household member who can say “Alexa, turn off Lounge Lamp”** on that Echo and have it work (the account that “owns” the lamp in the Alexa app).
81
- 3. Then run `alexa-mcp switch "Lounge Lamp" off -d "Lounge Echo"` again.
82
- - **Single account:** If there’s only one account, ensure the lamp is linked to that account in the Alexa app (Devices → Lights/Plugs).
55
+ **Authentication:**
56
+ ```bash
57
+ alexa-mcp auth # Interactive auth
58
+ alexa-mcp auth status [--verify] # Check auth status
59
+ alexa-mcp auth logout # Remove credentials
60
+ ```
83
61
 
84
- ### Seeing which profile owns devices
62
+ **Devices & Voice:**
63
+ ```bash
64
+ alexa-mcp devices # List Echo devices
65
+ alexa-mcp speak "Hello" -d Office # Text-to-speech on device
66
+ alexa-mcp announce "Dinner ready" # Announce to all devices
67
+ alexa-mcp command -d Office "play jazz" # Voice command
68
+ ```
85
69
 
86
- Each Echo and smart home device has a **deviceOwnerCustomerId** (Amazon’s internal account ID). The CLI uses the account you signed in with; that account has one or more such IDs. To see who owns what:
70
+ **Smart Home:**
71
+ ```bash
72
+ alexa-mcp groups # List room groups
73
+ alexa-mcp group-members Kitchen # List all devices in a group
74
+ alexa-mcp appliances # List smart home devices (with …suffix for disambiguation)
75
+ alexa-mcp appliances --type light # Filter by type: light, switch, plug, sensor, camera
76
+ alexa-mcp status "Lounge lamp" # Get current state (power, brightness, colour temp)
77
+ alexa-mcp switch-group Kitchen off # Turn off lights in room group → returns JSON result
78
+ alexa-mcp switch-room "kitchen lights" off # Turn off devices by pattern → returns JSON result
79
+ alexa-mcp switch "Lounge light 2" off # Turn off single device → returns live state JSON
80
+ alexa-mcp control <entityId> turnOn # Direct device control → returns live state JSON
81
+ alexa-mcp batch-control turnOff e1 e2 e3 # Batch control → returns per-device result map
82
+ ```
87
83
 
88
- - **Echo devices:**
89
- `alexa-mcp devices --owners`
90
- Prints each device name and its `deviceOwnerCustomerId`. Use the same account for `alexa-mcp auth` as the one that owns the Echo you’re targeting.
84
+ **Routines & Media:**
85
+ ```bash
86
+ alexa-mcp routines # List routines (includes name field)
87
+ alexa-mcp run <automationId> # Run a routine by ID
88
+ alexa-mcp run --name "our bedtime" # Run a routine by exact name
89
+ alexa-mcp run --partial "bedtime" # Run a routine by partial name match
90
+ alexa-mcp now-playing -d Office # Show now-playing
91
+ alexa-mcp media play|pause|next -d Office # Media control
92
+ ```
91
93
 
92
- - **Smart home appliances:**
93
- `alexa-mcp appliances`
94
- The JSON includes `deviceOwnerCustomerId` per device (when the API provides it). Match this to the account you use for auth.
94
+ **Tips:**
95
+ - Use `switch-group` for "all lights in [room]" (e.g., `Kitchen`)
96
+ - Use `switch-room` for pattern matching tries all-words first, falls back to any-word
97
+ - Use `switch` for single devices by exact name
98
+ - Use `status` to verify device state without issuing a command
99
+ - Use `group-members` after a `switch-group` to see which devices were targeted
100
+ - All control commands now return JSON state — no second API call needed to verify
101
+ - `appliances` output includes `displayName` with a 4-char endpoint suffix to disambiguate duplicates
102
+ - Direct control methods avoid voice profile issues
103
+ - See [docs/API.md](docs/API.md) for full API reference
95
104
 
96
- - **Which account the CLI is using:**
97
- `alexa-mcp auth status --verify`
98
- Shows “Account (deviceOwnerCustomerId): …” for the current session. Control will work when this matches the owner of the Echo and the smart home device.
105
+ ### MCP Server Setup
99
106
 
100
- ## MCP Server
107
+ ### Claude Desktop
101
108
 
102
- **Cursor** (`~/.cursor/mcp.json`) or **VS Code** (`.vscode/mcp.json`):
109
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
103
110
 
104
111
  ```json
105
112
  {
106
113
  "mcpServers": {
107
114
  "alexa": {
108
- "command": "node",
109
- "args": ["/path/to/alexa-mcp/dist/index.js"]
115
+ "command": "npx",
116
+ "args": ["alexa-mcp"]
110
117
  }
111
118
  }
112
119
  }
113
120
  ```
114
121
 
115
- **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
122
+ ### Cursor / VS Code
123
+
124
+ Edit `~/.cursor/mcp.json` or `.vscode/mcp.json`:
116
125
 
117
126
  ```json
118
127
  {
119
128
  "mcpServers": {
120
129
  "alexa": {
121
- "command": "node",
122
- "args": ["/path/to/alexa-mcp/dist/index.js"]
130
+ "command": "npx",
131
+ "args": ["alexa-mcp"]
123
132
  }
124
133
  }
125
134
  }
126
135
  ```
127
136
 
128
- **With environment variable token** (no file-based config needed):
137
+ ### Using Environment Variables
138
+
139
+ Pass token directly (no config file needed):
129
140
 
130
141
  ```json
131
142
  {
132
143
  "mcpServers": {
133
144
  "alexa": {
134
- "command": "node",
135
- "args": ["/path/to/alexa-mcp/dist/index.js"],
145
+ "command": "npx",
146
+ "args": ["alexa-mcp"],
136
147
  "env": {
137
148
  "ALEXA_REFRESH_TOKEN": "Atnr|...",
138
149
  "ALEXA_DOMAIN": "amazon.co.uk"
@@ -142,52 +153,105 @@ Each Echo and smart home device has a **deviceOwnerCustomerId** (Amazon’s inte
142
153
  }
143
154
  ```
144
155
 
145
- When installed locally, use the path to `node_modules/alexa-mcp/dist/index.js`. When installed globally (`npm install -g alexa-mcp`), use `npx alexa-mcp` as the command instead of `node`:
156
+ ### Local Installation
157
+
158
+ If installed locally, use the full path:
146
159
 
147
160
  ```json
148
161
  {
149
162
  "mcpServers": {
150
163
  "alexa": {
151
- "command": "npx",
152
- "args": ["alexa-mcp"]
164
+ "command": "node",
165
+ "args": ["/path/to/node_modules/alexa-mcp/dist/index.js"]
153
166
  }
154
167
  }
155
168
  }
156
169
  ```
157
170
 
158
- ### MCP Tools
159
-
160
- | Tool | Description |
161
- |------|-------------|
162
- | `alexa_list_devices` | List Echo devices |
163
- | `alexa_speak` | TTS on a device |
164
- | `alexa_announce` | Announce to all |
165
- | `alexa_command` | Voice command (no response returned; prefer direct control for smart home) |
166
- | `alexa_list_appliances` | List smart home devices (endpointId + friendlyName when available) |
167
- | `alexa_control_appliance` | turnOn/turnOff/setBrightness by entity/endpoint ID |
168
- | `alexa_control_by_group` | Turn on/off lights in a room group (e.g. "Kitchen") **for "all lights in group X"** |
169
- | `alexa_control_group` | Alias for `alexa_control_by_group`; also supports `lightsOnly` toggle |
170
- | `alexa_control_by_pattern` | Turn on/off devices matching name pattern (e.g. "kitchen lights") |
171
- | `alexa_switch_by_name` | Turn single device on/off by friendly name |
172
- | `alexa_list_device_groups` | List room groups (Living room, Kitchen, etc.) |
173
- | `alexa_list_audio_groups` | List multi-room audio groups |
174
- | `alexa_list_routines` | List routines |
175
- | `alexa_run_routine` | Run a routine by automation ID |
176
- | `alexa_now_playing` | Now-playing state for a device (includes `taskSessionId`) |
177
- | `alexa_media_control` | play, pause, resume, stop, next, previous (EU/UK) |
178
- | `alexa_auth_status` | Check auth status (configured/valid/deviceCount) |
171
+ ### Available MCP Tools
172
+
173
+ **Devices & Voice:**
174
+ - `alexa_list_devices` - List Echo devices
175
+ - `alexa_speak` - Text-to-speech on a device
176
+ - `alexa_announce` - Announce to all devices
177
+ - `alexa_command` - Send voice command
178
+
179
+ **Smart Home:**
180
+ - `alexa_list_appliances` - List smart home devices; optional `type` filter (light/switch/plug/sensor/camera); includes `displayName` with 4-char endpoint suffix
181
+ - `alexa_device_status` - Get live state of a device by name (power, brightness, colour temp, reachability)
182
+ - `alexa_list_device_groups` - List room groups with member counts
183
+ - `alexa_group_members` - List all appliances in a named room group
184
+ - `alexa_control_by_group` - Control all lights in a room group
185
+ - `alexa_control_by_pattern` - Control devices by name pattern (fuzzy fallback: any-word match if all-word fails)
186
+ - `alexa_switch_by_name` - Control single device by name
187
+ - `alexa_control_appliance` - Direct control by entity/endpoint ID
188
+ - `alexa_batch_control_appliances` - Batch control with same action; returns per-device `{friendlyName, success, error}` map
189
+ - `alexa_batch_control_appliances_custom` - Batch control with per-device actions; returns per-device results
190
+ - `alexa_get_brightness_by_name` - Get device brightness and power state
191
+ - `alexa_set_brightness_by_name` - Set device brightness
192
+ - `alexa_get_color_temperature_by_name` - Get device colour temperature
193
+ - `alexa_set_color_temperature_by_name` - Set device colour temperature
194
+
195
+ **Routines & Media:**
196
+ - `alexa_list_routines` - List Alexa routines with names and automation IDs
197
+ - `alexa_run_routine` - Execute a routine by `automationId`, exact `name`, or `partial` name match
198
+ - `alexa_list_audio_groups` - List multi-room audio groups
199
+ - `alexa_now_playing` - Get now-playing state
200
+ - `alexa_media_control` - Control playback (play/pause/next/etc.)
201
+ - `alexa_get_volume` / `alexa_set_volume` - Volume control
202
+
203
+ **Authentication:**
204
+ - `alexa_auth_status` - Check authentication status
205
+
206
+ ## Troubleshooting
207
+
208
+ ### "Can't control – may need to switch user accounts"
209
+
210
+ This error occurs when the Amazon account you authenticated with doesn't own the device.
211
+
212
+ **Solution:**
213
+ 1. Run `alexa-mcp auth logout`
214
+ 2. Run `alexa-mcp auth` and sign in with the account that owns the device
215
+ 3. Verify with `alexa-mcp auth status --verify`
216
+
217
+ **Check device ownership:**
218
+ ```bash
219
+ alexa-mcp devices --owners # Show Echo device owners
220
+ alexa-mcp appliances # Show smart home device owners
221
+ ```
222
+
223
+ The `deviceOwnerCustomerId` must match between your authenticated account and the device.
179
224
 
180
225
  ## Development
181
226
 
182
227
  ```bash
183
- npm install
184
- npm run build
185
- npm test
186
- npm run test:integration # Requires ALEXA_REFRESH_TOKEN
228
+ npm install # Install dependencies
229
+ npm run build # Compile TypeScript
230
+ npm test # Run unit tests
231
+ npm run test:integration # Run integration tests (requires auth)
232
+ npm run lint # Check code style
233
+ npm run lint:fix # Fix code style issues
187
234
  ```
188
235
 
189
- ## API Reference
236
+ ## Documentation
237
+
238
+ - **[API Reference](docs/API.md)** - Complete unofficial Alexa API documentation
239
+ - **[Device Capabilities](docs/DEVICE-CAPABILITIES.md)** - Device capability reference
240
+ - **[User Stories](docs/USER-STORIES.md)** - Usage examples and patterns
241
+
242
+ ## Requirements
243
+
244
+ - Node.js 18+
245
+ - Amazon Alexa account (amazon.com, amazon.co.uk, or amazon.de)
246
+
247
+ ## License
248
+
249
+ MIT - See LICENSE file for details
250
+
251
+ ## Contributing
252
+
253
+ Contributions welcome! Please follow [conventional commits](https://www.conventionalcommits.org/) format.
190
254
 
191
- The single authoritative API reference is **[docs/API.md](docs/API.md)** — region base URLs, authentication, all endpoints (devices, routines, smart home, behaviors, alarms, media), request/response bodies, and headers.
255
+ ## Acknowledgments
192
256
 
193
- **API usage:** All supported regions use the **app API** (eu-api-alexa for UK/EU, na-api-alexa for US): devices-v2, routinesandgroups, behaviors/preview, smarthome/v2/endpoints, layouts, and GraphQL for smart home control.
257
+ Built on the unofficial Alexa API. Uses [alexa-cookie2](https://github.com/Apollon77/alexa-cookie) for authentication.
package/dist/cli.js CHANGED
@@ -191,7 +191,7 @@ program
191
191
  });
192
192
  program
193
193
  .command("switch-group <group> <state>")
194
- .description("Turn on/off all lights in a room group (e.g. Kitchen, Living room). Uses group membership from Alexa app.")
194
+ .description("Turn on/off all lights in a room group (e.g. Kitchen, Living room). Returns JSON {action, group, controlled, errors}.")
195
195
  .option("--all", "Control all appliances in group, not just lights", false)
196
196
  .action(async (group, state, opts) => {
197
197
  const s = state.toLowerCase();
@@ -210,12 +210,7 @@ program
210
210
  const { controlled, errors } = await client.controlAppliancesByGroup(group, action, {
211
211
  lightsOnly: !opts.all,
212
212
  });
213
- if (controlled.length > 0) {
214
- console.log(`${action}: ${controlled.join(", ")}`);
215
- }
216
- if (errors.length > 0) {
217
- console.error(errors.join("\n"));
218
- }
213
+ console.log(JSON.stringify({ action, group, controlled, errors }));
219
214
  if (controlled.length === 0 && errors.length === 0) {
220
215
  console.error(`No lights in group "${group}". Try 'alexa-mcp groups' to see groups.`);
221
216
  process.exit(1);
@@ -228,7 +223,7 @@ program
228
223
  });
229
224
  program
230
225
  .command("switch-room <pattern> <state>")
231
- .description("Turn on/off all smart home devices matching a pattern (e.g. 'kitchen lights', 'living room'). Uses direct control—avoids profile issues.")
226
+ .description("Turn on/off all smart home devices matching a pattern (e.g. 'kitchen lights', 'living room'). Tries all-word match first; falls back to any-word. Returns JSON {action, pattern, controlled, errors}.")
232
227
  .action(async (pattern, state) => {
233
228
  const s = state.toLowerCase();
234
229
  if (s !== "on" && s !== "off") {
@@ -243,12 +238,7 @@ program
243
238
  const client = new AlexaClient({ refreshToken: cfg.refreshToken, domain: cfg.domain });
244
239
  const action = s === "on" ? "turnOn" : "turnOff";
245
240
  const { controlled, errors } = await client.controlAppliancesByPattern(pattern, action);
246
- if (controlled.length > 0) {
247
- console.log(`${action}: ${controlled.join(", ")}`);
248
- }
249
- if (errors.length > 0) {
250
- console.error(errors.join("\n"));
251
- }
241
+ console.log(JSON.stringify({ action, pattern, controlled, errors }));
252
242
  if (controlled.length === 0 && errors.length === 0) {
253
243
  console.error(`No devices matched "${pattern}". Try 'alexa-mcp appliances' to see names.`);
254
244
  process.exit(1);
@@ -256,7 +246,7 @@ program
256
246
  });
257
247
  program
258
248
  .command("switch <name> <state>")
259
- .description("Turn single smart home device on/off by name. For room/pattern (e.g. 'kitchen lights'), use switch-room instead.")
249
+ .description("Turn single smart home device on/off by name. Returns live device state JSON after applying. For room/pattern (e.g. 'kitchen lights'), use switch-room instead.")
260
250
  .option("-d, --device <echo>", "Echo for voice fallback when direct control fails", "")
261
251
  .action(async (name, state, opts) => {
262
252
  const s = state.toLowerCase();
@@ -274,7 +264,8 @@ program
274
264
  const app = await client.resolveApplianceByName(name);
275
265
  if (app?.endpointId) {
276
266
  await client.controlAppliance(app.endpointId, action);
277
- console.log(`Done: ${action} ${app.friendlyName} (direct control)`);
267
+ const state = await client.getBrightnessState(app.endpointId);
268
+ console.log(JSON.stringify({ friendlyName: app.friendlyName, endpointId: app.endpointId, ...state }, null, 2));
278
269
  return;
279
270
  }
280
271
  if (!opts.device) {
@@ -288,7 +279,7 @@ program
288
279
  }
289
280
  const text = s === "on" ? `turn on ${name}` : `turn off ${name}`;
290
281
  await client.command(d.serialNumber, d.deviceType, d.deviceOwnerCustomerId, text);
291
- console.log(`Sent "${text}" via ${d.accountName} (voice fallback)`);
282
+ console.log(JSON.stringify({ friendlyName: name, action, method: "voice", device: d.accountName }));
292
283
  });
293
284
  program
294
285
  .command("groups")
@@ -303,22 +294,80 @@ program
303
294
  const groups = await client.listDeviceGroups();
304
295
  console.log(JSON.stringify(groups, null, 2));
305
296
  });
297
+ function filterAppliancesByType(appliances, type) {
298
+ return appliances.filter((a) => {
299
+ const caps = (a.capabilities ?? []).join(" ").toLowerCase();
300
+ const name = (a.friendlyName ?? "").toLowerCase();
301
+ switch (type) {
302
+ case "light": return caps.includes("brightness") || caps.includes("colortemperature") || /light|lamp|bulb/.test(name);
303
+ case "switch": return caps.includes("power") && !caps.includes("brightness");
304
+ case "plug": return /plug/.test(name);
305
+ case "sensor": return /sensor|motion|contact|temperature/.test(caps);
306
+ case "camera": return /camera|doorbell/.test(name);
307
+ default: return true;
308
+ }
309
+ });
310
+ }
306
311
  program
307
312
  .command("appliances")
308
313
  .description("List smart home devices")
309
- .action(async () => {
314
+ .option("--type <type>", "Filter by type: light, switch, plug, sensor, camera")
315
+ .action(async (opts) => {
310
316
  const cfg = getAuthConfig();
311
317
  if (!cfg) {
312
318
  console.error("No refresh token.");
313
319
  process.exit(1);
314
320
  }
315
321
  const client = new AlexaClient({ refreshToken: cfg.refreshToken, domain: cfg.domain });
316
- const appliances = await client.listAppliances();
317
- console.log(JSON.stringify(appliances, null, 2));
322
+ let appliances = await client.listAppliances();
323
+ if (opts.type) {
324
+ appliances = filterAppliancesByType(appliances, opts.type.toLowerCase());
325
+ }
326
+ const output = appliances.map((a) => ({
327
+ ...a,
328
+ displayName: `${a.friendlyName}${a.endpointId ? ` \u2026${a.endpointId.slice(-4)}` : ""}`,
329
+ }));
330
+ console.log(JSON.stringify(output, null, 2));
331
+ });
332
+ program
333
+ .command("status <name>")
334
+ .description("Get current state of a smart home device by name")
335
+ .action(async (name) => {
336
+ const cfg = getAuthConfig();
337
+ if (!cfg) {
338
+ console.error("No refresh token.");
339
+ process.exit(1);
340
+ }
341
+ const client = new AlexaClient({ refreshToken: cfg.refreshToken, domain: cfg.domain });
342
+ const app = await client.resolveApplianceByName(name);
343
+ if (!app) {
344
+ console.error(JSON.stringify({ error: `Device not found: "${name}". Try 'alexa-mcp appliances' to see names.` }));
345
+ process.exit(1);
346
+ }
347
+ const eid = app.endpointId ?? app.entityId;
348
+ const state = eid ? await client.getBrightnessState(eid) : {};
349
+ console.log(JSON.stringify({ friendlyName: app.friendlyName, endpointId: eid, isReachable: app.isReachable, ...state }, null, 2));
350
+ });
351
+ program
352
+ .command("group-members <group>")
353
+ .description("List all devices in a named room group")
354
+ .action(async (group) => {
355
+ const cfg = getAuthConfig();
356
+ if (!cfg) {
357
+ console.error("No refresh token.");
358
+ process.exit(1);
359
+ }
360
+ const client = new AlexaClient({ refreshToken: cfg.refreshToken, domain: cfg.domain });
361
+ const members = await client.listGroupMembers(group);
362
+ if (members.length === 0) {
363
+ console.error(`No group found matching "${group}". Try 'alexa-mcp groups' to see groups.`);
364
+ process.exit(1);
365
+ }
366
+ console.log(JSON.stringify(members, null, 2));
318
367
  });
319
368
  program
320
369
  .command("control <entityId> <action>")
321
- .description("Control smart home device (turnOn, turnOff, setBrightness, setColorTemperature)")
370
+ .description("Control smart home device (turnOn, turnOff, setBrightness, setColorTemperature). Returns live state JSON after applying.")
322
371
  .option("-b, --brightness <0-100>", "Brightness for setBrightness", (v) => parseInt(v, 10))
323
372
  .option("-k, --kelvin <2000-6500>", "Color temperature in Kelvin for setColorTemperature", (v) => parseInt(v, 10))
324
373
  .action(async (entityId, action, opts) => {
@@ -342,7 +391,8 @@ program
342
391
  }
343
392
  const client = new AlexaClient({ refreshToken: cfg.refreshToken, domain: cfg.domain });
344
393
  await client.controlAppliance(entityId, action, opts.brightness, opts.kelvin);
345
- console.log(`Done: ${action} ${entityId}`);
394
+ const state = entityId.startsWith("amzn1.alexa.endpoint.") ? await client.getBrightnessState(entityId) : {};
395
+ console.log(JSON.stringify({ entityId, action, ...state }, null, 2));
346
396
  });
347
397
  program
348
398
  .command("routines")
@@ -358,24 +408,55 @@ program
358
408
  console.log(JSON.stringify(routines, null, 2));
359
409
  });
360
410
  program
361
- .command("run <automationId>")
362
- .description("Run a routine by automation ID")
363
- .action(async (automationId) => {
411
+ .command("run [automationId]")
412
+ .description("Run a routine by ID, exact name (--name), or partial name (--partial)")
413
+ .option("--name <name>", "Run routine by exact name (case-insensitive)")
414
+ .option("--partial <text>", "Run routine by partial name match")
415
+ .action(async (automationId, opts) => {
364
416
  const cfg = getAuthConfig();
365
417
  if (!cfg) {
366
418
  console.error("No refresh token.");
367
419
  process.exit(1);
368
420
  }
421
+ if (!automationId && !opts.name && !opts.partial) {
422
+ console.error("Provide automationId, --name <name>, or --partial <text>");
423
+ process.exit(1);
424
+ }
369
425
  const client = new AlexaClient({ refreshToken: cfg.refreshToken, domain: cfg.domain });
370
426
  const routines = await client.listRoutines();
371
- const r = routines.find((x) => x.automationId === automationId);
372
- if (!r) {
373
- console.error(`Routine not found: ${automationId}`);
374
- process.exit(1);
427
+ let r;
428
+ if (automationId) {
429
+ r = routines.find((x) => x.automationId === automationId);
430
+ if (!r) {
431
+ console.error(`Routine not found: ${automationId}`);
432
+ process.exit(1);
433
+ }
434
+ }
435
+ else if (opts.name) {
436
+ const q = opts.name.toLowerCase();
437
+ r = routines.find((x) => x.name.toLowerCase() === q);
438
+ if (!r) {
439
+ console.error(`Routine not found with name: "${opts.name}"`);
440
+ process.exit(1);
441
+ }
442
+ }
443
+ else if (opts.partial) {
444
+ const q = opts.partial.toLowerCase();
445
+ const matches = routines.filter((x) => x.name.toLowerCase().includes(q));
446
+ if (matches.length === 0) {
447
+ console.error(`No routines matched: "${opts.partial}"`);
448
+ process.exit(1);
449
+ }
450
+ if (matches.length > 1) {
451
+ console.log(JSON.stringify({ matches: matches.map((m) => ({ automationId: m.automationId, name: m.name })) }, null, 2));
452
+ console.error(`Multiple matches. Provide --name or a more specific --partial.`);
453
+ process.exit(1);
454
+ }
455
+ r = matches[0];
375
456
  }
376
457
  const sequenceJson = r.sequence != null ? JSON.stringify(r.sequence) : undefined;
377
458
  await client.runRoutine(r.automationId, sequenceJson);
378
- console.log(`Ran routine: ${r.name}`);
459
+ console.log(JSON.stringify({ ran: r.name, automationId: r.automationId }));
379
460
  });
380
461
  program
381
462
  .command("now-playing")
@@ -514,7 +595,7 @@ program
514
595
  });
515
596
  program
516
597
  .command("batch-control <action>")
517
- .description("Batch control multiple smart home devices with same action/value. Much faster than individual calls.")
598
+ .description("Batch control multiple smart home devices with same action/value. Returns per-device result map {friendlyName, success, error?}.")
518
599
  .argument("[entityIds...]", "Entity IDs or endpoint IDs (omit to read from stdin)")
519
600
  .option("-b, --brightness <0-100>", "Brightness for setBrightness", (v) => parseInt(v, 10))
520
601
  .option("-k, --kelvin <2000-6500>", "Color temperature in Kelvin for setColorTemperature", (v) => parseInt(v, 10))
@@ -552,10 +633,20 @@ program
552
633
  process.exit(1);
553
634
  }
554
635
  const client = new AlexaClient({ refreshToken: cfg.refreshToken, domain: cfg.domain });
555
- const startTime = Date.now();
556
- await client.batchControlAppliances(entityIds, action, opts.brightness, opts.kelvin);
557
- const duration = Date.now() - startTime;
558
- console.log(`Batch done: ${action} on ${entityIds.length} devices in ${duration}ms`);
636
+ const [results, appliances] = await Promise.all([
637
+ client.batchControlAppliances(entityIds, action, opts.brightness, opts.kelvin),
638
+ client.listAppliances(),
639
+ ]);
640
+ const nameMap = new Map(appliances.map((a) => [a.entityId, a.friendlyName]));
641
+ const output = Object.fromEntries(results.map((r) => [
642
+ r.entityId,
643
+ {
644
+ friendlyName: nameMap.get(r.entityId) ?? r.entityId,
645
+ success: r.success,
646
+ ...(r.error ? { error: r.error } : {}),
647
+ },
648
+ ]));
649
+ console.log(JSON.stringify(output, null, 2));
559
650
  });
560
651
  const mediaCmd = program
561
652
  .command("media <command>")