roku-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/README.md +91 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/roku-config.d.ts +12 -0
- package/dist/roku-config.d.ts.map +1 -0
- package/dist/roku-config.js +19 -0
- package/dist/roku-config.js.map +1 -0
- package/dist/tools/console.d.ts +3 -0
- package/dist/tools/console.d.ts.map +1 -0
- package/dist/tools/console.js +154 -0
- package/dist/tools/console.js.map +1 -0
- package/dist/tools/deploy.d.ts +3 -0
- package/dist/tools/deploy.d.ts.map +1 -0
- package/dist/tools/deploy.js +66 -0
- package/dist/tools/deploy.js.map +1 -0
- package/dist/tools/ecp.d.ts +3 -0
- package/dist/tools/ecp.d.ts.map +1 -0
- package/dist/tools/ecp.js +196 -0
- package/dist/tools/ecp.js.map +1 -0
- package/dist/tools/screenshot.d.ts +3 -0
- package/dist/tools/screenshot.d.ts.map +1 -0
- package/dist/tools/screenshot.js +56 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# roku-mcp
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server for Roku device automation. Exposes tools for app deployment, ECP remote control, screenshot capture, SceneGraph node inspection, and BrightScript debug console access.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g roku-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly with npx:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx roku-mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Cursor Configuration
|
|
18
|
+
|
|
19
|
+
Add the following to your project's `.cursor/mcp.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"roku": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["-y", "roku-mcp"],
|
|
27
|
+
"env": {
|
|
28
|
+
"ROKU_DEVICE_HOST": "192.168.1.XXX",
|
|
29
|
+
"ROKU_DEVICE_PASSWORD": "your-password"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Replace `ROKU_DEVICE_HOST` with your Roku device's IP address and `ROKU_DEVICE_PASSWORD` with the developer password you set during dev mode setup.
|
|
37
|
+
|
|
38
|
+
## Environment Variables
|
|
39
|
+
|
|
40
|
+
| Variable | Description |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `ROKU_DEVICE_HOST` | IP address or hostname of the Roku device |
|
|
43
|
+
| `ROKU_DEVICE_PASSWORD` | Developer password for the Roku device |
|
|
44
|
+
|
|
45
|
+
Both can also be passed as parameters on each tool call, which override the environment variables.
|
|
46
|
+
|
|
47
|
+
## Available Tools
|
|
48
|
+
|
|
49
|
+
### Deploy
|
|
50
|
+
|
|
51
|
+
| Tool | Description |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `roku_deploy` | Sideload (deploy) a Roku app to the device |
|
|
54
|
+
| `roku_delete_dev_channel` | Delete the currently sideloaded developer channel |
|
|
55
|
+
|
|
56
|
+
### ECP (External Control Protocol)
|
|
57
|
+
|
|
58
|
+
| Tool | Description |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `roku_keypress` | Send a single key press (Home, Select, Up, Down, Left, Right, Back, etc.) |
|
|
61
|
+
| `roku_keypress_sequence` | Send multiple key presses in sequence with configurable delay |
|
|
62
|
+
| `roku_launch` | Launch or deep-link into a channel |
|
|
63
|
+
| `roku_query_device_info` | Get device model, firmware, serial number, network info |
|
|
64
|
+
| `roku_query_active_app` | Get the currently running app |
|
|
65
|
+
| `roku_query_app_ui` | Get the current app UI tree as XML |
|
|
66
|
+
| `roku_query_sg_nodes` | Query SceneGraph nodes (all, roots, or by node ID) |
|
|
67
|
+
|
|
68
|
+
### Screenshot
|
|
69
|
+
|
|
70
|
+
| Tool | Description |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `roku_screenshot` | Capture a screenshot (returns base64 image and file path) |
|
|
73
|
+
|
|
74
|
+
### Debug Console
|
|
75
|
+
|
|
76
|
+
| Tool | Description |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `roku_console_connect` | Open a TCP connection to the BrightScript debug console (port 8085) |
|
|
79
|
+
| `roku_console_read` | Read buffered console output since last read |
|
|
80
|
+
| `roku_console_send` | Send a command to the debug console (bt, var, cont, step, etc.) |
|
|
81
|
+
| `roku_console_disconnect` | Close the console connection |
|
|
82
|
+
|
|
83
|
+
## Requirements
|
|
84
|
+
|
|
85
|
+
- Node.js 18+
|
|
86
|
+
- A Roku device with [Developer Mode](https://developer.roku.com/docs/developer-program/getting-started/developer-setup.md) enabled on the same network
|
|
87
|
+
- For screenshots and deploy: a sideloaded dev channel must be running
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
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 { registerDeployTools } from './tools/deploy.js';
|
|
5
|
+
import { registerEcpTools } from './tools/ecp.js';
|
|
6
|
+
import { registerScreenshotTools } from './tools/screenshot.js';
|
|
7
|
+
import { registerConsoleTools } from './tools/console.js';
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: 'roku-mcp',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
});
|
|
12
|
+
registerDeployTools(server);
|
|
13
|
+
registerEcpTools(server);
|
|
14
|
+
registerScreenshotTools(server);
|
|
15
|
+
registerConsoleTools(server);
|
|
16
|
+
const transport = new StdioServerTransport();
|
|
17
|
+
await server.connect(transport);
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,mBAAmB,CAAC,MAAM,CAAC,CAAC;AAC5B,gBAAgB,CAAC,MAAM,CAAC,CAAC;AACzB,uBAAuB,CAAC,MAAM,CAAC,CAAC;AAChC,oBAAoB,CAAC,MAAM,CAAC,CAAC;AAE7B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface RokuDeviceConfig {
|
|
2
|
+
host: string;
|
|
3
|
+
password: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function resolveConfig(params: {
|
|
6
|
+
host?: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
}): RokuDeviceConfig;
|
|
9
|
+
export declare function resolveHost(params: {
|
|
10
|
+
host?: string;
|
|
11
|
+
}): string;
|
|
12
|
+
//# sourceMappingURL=roku-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roku-config.d.ts","sourceRoot":"","sources":["../src/roku-config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,gBAAgB,CAgB5F;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAQ7D"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function resolveConfig(params) {
|
|
2
|
+
const host = params.host || process.env.ROKU_DEVICE_HOST;
|
|
3
|
+
const password = params.password || process.env.ROKU_DEVICE_PASSWORD;
|
|
4
|
+
if (!host) {
|
|
5
|
+
throw new Error('Roku device host is required. Provide it as a tool parameter or set ROKU_DEVICE_HOST environment variable.');
|
|
6
|
+
}
|
|
7
|
+
if (!password) {
|
|
8
|
+
throw new Error('Roku device password is required. Provide it as a tool parameter or set ROKU_DEVICE_PASSWORD environment variable.');
|
|
9
|
+
}
|
|
10
|
+
return { host, password };
|
|
11
|
+
}
|
|
12
|
+
export function resolveHost(params) {
|
|
13
|
+
const host = params.host || process.env.ROKU_DEVICE_HOST;
|
|
14
|
+
if (!host) {
|
|
15
|
+
throw new Error('Roku device host is required. Provide it as a tool parameter or set ROKU_DEVICE_HOST environment variable.');
|
|
16
|
+
}
|
|
17
|
+
return host;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=roku-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roku-config.js","sourceRoot":"","sources":["../src/roku-config.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,aAAa,CAAC,MAA4C;IACxE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IACzD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IAErE,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACb,4GAA4G,CAC7G,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,oHAAoH,CACrH,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAyB;IACnD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IACzD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACb,4GAA4G,CAC7G,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../src/tools/console.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAcpE,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAoL5D"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import net from 'net';
|
|
3
|
+
import { resolveHost } from '../roku-config.js';
|
|
4
|
+
let session = null;
|
|
5
|
+
export function registerConsoleTools(server) {
|
|
6
|
+
server.registerTool('roku_console_connect', {
|
|
7
|
+
description: 'Open a persistent TCP connection to the BrightScript debug console on port 8085. Only one connection is active at a time.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
host: z.string().optional().describe('IP address or hostname of the Roku device'),
|
|
10
|
+
port: z.number().optional().default(8085).describe('Debug console port (default 8085)'),
|
|
11
|
+
},
|
|
12
|
+
}, async (params) => {
|
|
13
|
+
try {
|
|
14
|
+
if (session?.connected) {
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: 'text', text: `Already connected to ${session.host}. Disconnect first to connect to a different device.` }],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const host = resolveHost(params);
|
|
20
|
+
const port = params.port ?? 8085;
|
|
21
|
+
return await new Promise((resolve) => {
|
|
22
|
+
const socket = new net.Socket();
|
|
23
|
+
const newSession = {
|
|
24
|
+
socket,
|
|
25
|
+
buffer: '',
|
|
26
|
+
host,
|
|
27
|
+
connected: false,
|
|
28
|
+
};
|
|
29
|
+
const timeout = setTimeout(() => {
|
|
30
|
+
socket.destroy();
|
|
31
|
+
resolve({
|
|
32
|
+
content: [{ type: 'text', text: `Connection to ${host}:${port} timed out after 10 seconds` }],
|
|
33
|
+
isError: true,
|
|
34
|
+
});
|
|
35
|
+
}, 10_000);
|
|
36
|
+
socket.connect(port, host, () => {
|
|
37
|
+
clearTimeout(timeout);
|
|
38
|
+
newSession.connected = true;
|
|
39
|
+
session = newSession;
|
|
40
|
+
resolve({
|
|
41
|
+
content: [{ type: 'text', text: `Connected to BrightScript debug console at ${host}:${port}` }],
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
socket.on('data', (data) => {
|
|
45
|
+
if (session === newSession) {
|
|
46
|
+
session.buffer += data.toString();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
socket.on('close', () => {
|
|
50
|
+
if (session === newSession) {
|
|
51
|
+
session.connected = false;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
socket.on('error', (err) => {
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
if (session === newSession) {
|
|
57
|
+
session.connected = false;
|
|
58
|
+
}
|
|
59
|
+
if (!newSession.connected) {
|
|
60
|
+
resolve({
|
|
61
|
+
content: [{ type: 'text', text: `Connection failed: ${err.message}` }],
|
|
62
|
+
isError: true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: 'text', text: `Console connect failed: ${message}` }],
|
|
72
|
+
isError: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
server.registerTool('roku_console_read', {
|
|
77
|
+
description: 'Read buffered output from the BrightScript debug console since the last read. Clears the buffer after reading.',
|
|
78
|
+
}, async () => {
|
|
79
|
+
if (!session?.connected) {
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: 'text', text: 'No active console connection. Use roku_console_connect first.' }],
|
|
82
|
+
isError: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
86
|
+
const output = session.buffer;
|
|
87
|
+
session.buffer = '';
|
|
88
|
+
if (output.length === 0) {
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: 'text', text: '(no new output)' }],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: 'text', text: output }],
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
server.registerTool('roku_console_send', {
|
|
98
|
+
description: 'Send a command to the BrightScript debug console. Common commands: bt (backtrace), var (variables), cont (continue), step, over, out, exit, print <expr>.',
|
|
99
|
+
inputSchema: {
|
|
100
|
+
command: z.string().describe('Command to send to the debug console'),
|
|
101
|
+
},
|
|
102
|
+
}, async (params) => {
|
|
103
|
+
if (!session?.connected) {
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: 'text', text: 'No active console connection. Use roku_console_connect first.' }],
|
|
106
|
+
isError: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return await new Promise((resolve) => {
|
|
111
|
+
session.buffer = '';
|
|
112
|
+
session.socket.write(params.command + '\r\n', (err) => {
|
|
113
|
+
if (err) {
|
|
114
|
+
resolve({
|
|
115
|
+
content: [{ type: 'text', text: `Failed to send command: ${err.message}` }],
|
|
116
|
+
isError: true,
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
const output = session.buffer;
|
|
122
|
+
session.buffer = '';
|
|
123
|
+
resolve({
|
|
124
|
+
content: [{ type: 'text', text: output || '(command sent, no output received)' }],
|
|
125
|
+
});
|
|
126
|
+
}, 500);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: 'text', text: `Console send failed: ${message}` }],
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
server.registerTool('roku_console_disconnect', {
|
|
139
|
+
description: 'Close the active BrightScript debug console connection.',
|
|
140
|
+
}, async () => {
|
|
141
|
+
if (!session) {
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: 'text', text: 'No active console connection to disconnect.' }],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const host = session.host;
|
|
147
|
+
session.socket.destroy();
|
|
148
|
+
session = null;
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: 'text', text: `Disconnected from ${host}` }],
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=console.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console.js","sourceRoot":"","sources":["../../src/tools/console.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/tools/deploy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAsE3D"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { rokuDeploy } from 'roku-deploy';
|
|
3
|
+
import { resolveConfig } from '../roku-config.js';
|
|
4
|
+
export function registerDeployTools(server) {
|
|
5
|
+
server.registerTool('roku_deploy', {
|
|
6
|
+
description: 'Sideload (deploy) a Roku application to the device. A dev channel must not already be running, or it will be replaced.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
host: z.string().optional().describe('IP address or hostname of the Roku device'),
|
|
9
|
+
password: z.string().optional().describe('Developer password for the Roku device'),
|
|
10
|
+
rootDir: z.string().describe('Absolute path to the project root directory containing the manifest file'),
|
|
11
|
+
files: z
|
|
12
|
+
.array(z.string())
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Glob patterns for files to include. Defaults to source/**/*, components/**/*, images/**/*, manifest'),
|
|
15
|
+
},
|
|
16
|
+
}, async (params) => {
|
|
17
|
+
try {
|
|
18
|
+
const config = resolveConfig(params);
|
|
19
|
+
const deployOptions = {
|
|
20
|
+
host: config.host,
|
|
21
|
+
password: config.password,
|
|
22
|
+
rootDir: params.rootDir,
|
|
23
|
+
};
|
|
24
|
+
if (params.files && params.files.length > 0) {
|
|
25
|
+
deployOptions.files = params.files;
|
|
26
|
+
}
|
|
27
|
+
await rokuDeploy.deploy(deployOptions);
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: 'text', text: `Successfully deployed app from ${params.rootDir} to ${config.host}` }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: 'text', text: `Deploy failed: ${message}` }],
|
|
36
|
+
isError: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
server.registerTool('roku_delete_dev_channel', {
|
|
41
|
+
description: 'Delete the currently sideloaded developer channel from the Roku device.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
host: z.string().optional().describe('IP address or hostname of the Roku device'),
|
|
44
|
+
password: z.string().optional().describe('Developer password for the Roku device'),
|
|
45
|
+
},
|
|
46
|
+
}, async (params) => {
|
|
47
|
+
try {
|
|
48
|
+
const config = resolveConfig(params);
|
|
49
|
+
await rokuDeploy.deleteInstalledChannel({
|
|
50
|
+
host: config.host,
|
|
51
|
+
password: config.password,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: 'text', text: `Successfully deleted dev channel from ${config.host}` }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: 'text', text: `Delete dev channel failed: ${message}` }],
|
|
61
|
+
isError: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=deploy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy.js","sourceRoot":"","sources":["../../src/tools/deploy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ecp.d.ts","sourceRoot":"","sources":["../../src/tools/ecp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAYpE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAsNxD"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
4
|
+
import { resolveHost } from '../roku-config.js';
|
|
5
|
+
const xmlParser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' });
|
|
6
|
+
function ecpUrl(host, path) {
|
|
7
|
+
return `http://${host}:8060/${path}`;
|
|
8
|
+
}
|
|
9
|
+
export function registerEcpTools(server) {
|
|
10
|
+
server.registerTool('roku_keypress', {
|
|
11
|
+
description: 'Send a single key press to the Roku device via ECP. Common keys: Home, Rev, Fwd, Play, Select, Left, Right, Down, Up, Back, InstantReplay, Info, Backspace, Search, Enter, Lit_<character>',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
host: z.string().optional().describe('IP address or hostname of the Roku device'),
|
|
14
|
+
key: z.string().describe('The key to press (e.g. Home, Select, Up, Down, Left, Right, Back, Lit_a)'),
|
|
15
|
+
},
|
|
16
|
+
}, async (params) => {
|
|
17
|
+
try {
|
|
18
|
+
const host = resolveHost(params);
|
|
19
|
+
await axios.post(ecpUrl(host, `keypress/${encodeURIComponent(params.key)}`));
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: 'text', text: `Key press sent: ${params.key}` }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: `Key press failed: ${message}` }],
|
|
28
|
+
isError: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.registerTool('roku_keypress_sequence', {
|
|
33
|
+
description: 'Send a sequence of key presses to the Roku device with an optional delay between each press.',
|
|
34
|
+
inputSchema: {
|
|
35
|
+
host: z.string().optional().describe('IP address or hostname of the Roku device'),
|
|
36
|
+
keys: z.array(z.string()).describe('Array of keys to press in order'),
|
|
37
|
+
delayMs: z
|
|
38
|
+
.number()
|
|
39
|
+
.optional()
|
|
40
|
+
.default(100)
|
|
41
|
+
.describe('Delay in milliseconds between each key press (default 100)'),
|
|
42
|
+
},
|
|
43
|
+
}, async (params) => {
|
|
44
|
+
try {
|
|
45
|
+
const host = resolveHost(params);
|
|
46
|
+
const delay = params.delayMs ?? 100;
|
|
47
|
+
for (const key of params.keys) {
|
|
48
|
+
await axios.post(ecpUrl(host, `keypress/${encodeURIComponent(key)}`));
|
|
49
|
+
if (delay > 0) {
|
|
50
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: 'text', text: `Key sequence sent: ${params.keys.join(', ')}` }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: 'text', text: `Key sequence failed: ${message}` }],
|
|
61
|
+
isError: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
server.registerTool('roku_launch', {
|
|
66
|
+
description: 'Launch or deep-link into a channel on the Roku device. Use appId "dev" for the sideloaded developer channel.',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
host: z.string().optional().describe('IP address or hostname of the Roku device'),
|
|
69
|
+
appId: z.string().describe('Application ID to launch (use "dev" for the sideloaded channel)'),
|
|
70
|
+
params: z
|
|
71
|
+
.record(z.string(), z.string())
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Optional launch parameters for deep linking (key-value pairs)'),
|
|
74
|
+
},
|
|
75
|
+
}, async (toolParams) => {
|
|
76
|
+
try {
|
|
77
|
+
const host = resolveHost(toolParams);
|
|
78
|
+
const queryString = toolParams.params
|
|
79
|
+
? '?' + new URLSearchParams(toolParams.params).toString()
|
|
80
|
+
: '';
|
|
81
|
+
await axios.post(ecpUrl(host, `launch/${encodeURIComponent(toolParams.appId)}${queryString}`));
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text', text: `Launched app ${toolParams.appId} on ${host}` }],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: 'text', text: `Launch failed: ${message}` }],
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
server.registerTool('roku_query_device_info', {
|
|
95
|
+
description: 'Query the Roku device for its info: model, firmware version, serial number, network info, etc.',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
host: z.string().optional().describe('IP address or hostname of the Roku device'),
|
|
98
|
+
},
|
|
99
|
+
}, async (params) => {
|
|
100
|
+
try {
|
|
101
|
+
const host = resolveHost(params);
|
|
102
|
+
const response = await axios.get(ecpUrl(host, 'query/device-info'));
|
|
103
|
+
const parsed = xmlParser.parse(response.data);
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: 'text', text: `Query device info failed: ${message}` }],
|
|
112
|
+
isError: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
server.registerTool('roku_query_active_app', {
|
|
117
|
+
description: 'Query the currently active (foreground) app on the Roku device.',
|
|
118
|
+
inputSchema: {
|
|
119
|
+
host: z.string().optional().describe('IP address or hostname of the Roku device'),
|
|
120
|
+
},
|
|
121
|
+
}, async (params) => {
|
|
122
|
+
try {
|
|
123
|
+
const host = resolveHost(params);
|
|
124
|
+
const response = await axios.get(ecpUrl(host, 'query/active-app'));
|
|
125
|
+
const parsed = xmlParser.parse(response.data);
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: 'text', text: `Query active app failed: ${message}` }],
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
server.registerTool('roku_query_app_ui', {
|
|
139
|
+
description: 'Get the current app UI tree as XML. Only works when a sideloaded dev channel is running.',
|
|
140
|
+
inputSchema: {
|
|
141
|
+
host: z.string().optional().describe('IP address or hostname of the Roku device'),
|
|
142
|
+
},
|
|
143
|
+
}, async (params) => {
|
|
144
|
+
try {
|
|
145
|
+
const host = resolveHost(params);
|
|
146
|
+
const response = await axios.get(ecpUrl(host, 'query/app-ui'));
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: 'text', text: response.data }],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: 'text', text: `Query app UI failed: ${message}` }],
|
|
155
|
+
isError: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
server.registerTool('roku_query_sg_nodes', {
|
|
160
|
+
description: 'Query SceneGraph nodes on the Roku device. Use type "all" for all nodes, "roots" for root nodes, or "node" with a nodeId to inspect a specific node.',
|
|
161
|
+
inputSchema: {
|
|
162
|
+
host: z.string().optional().describe('IP address or hostname of the Roku device'),
|
|
163
|
+
type: z.enum(['all', 'roots', 'node']).describe('Type of query: "all", "roots", or "node"'),
|
|
164
|
+
nodeId: z.string().optional().describe('Node ID to inspect (required when type is "node")'),
|
|
165
|
+
},
|
|
166
|
+
}, async (params) => {
|
|
167
|
+
try {
|
|
168
|
+
const host = resolveHost(params);
|
|
169
|
+
let path;
|
|
170
|
+
if (params.type === 'node') {
|
|
171
|
+
if (!params.nodeId) {
|
|
172
|
+
return {
|
|
173
|
+
content: [{ type: 'text', text: 'nodeId is required when type is "node"' }],
|
|
174
|
+
isError: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
path = `query/sgnodes/nodes/${encodeURIComponent(params.nodeId)}`;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
path = `query/sgnodes/${params.type}`;
|
|
181
|
+
}
|
|
182
|
+
const response = await axios.get(ecpUrl(host, path));
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: 'text', text: response.data }],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: 'text', text: `Query SG nodes failed: ${message}` }],
|
|
191
|
+
isError: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=ecp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ecp.js","sourceRoot":"","sources":["../../src/tools/ecp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/tools/screenshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAOpE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAuD/D"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { rokuDeploy } from 'roku-deploy';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { resolveConfig } from '../roku-config.js';
|
|
6
|
+
export function registerScreenshotTools(server) {
|
|
7
|
+
server.registerTool('roku_screenshot', {
|
|
8
|
+
description: 'Capture a screenshot of the current screen on the Roku device. A sideloaded dev channel must be running. Returns the image as base64-encoded content and the file path where it was saved.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
host: z.string().optional().describe('IP address or hostname of the Roku device'),
|
|
11
|
+
password: z.string().optional().describe('Developer password for the Roku device'),
|
|
12
|
+
outDir: z.string().optional().describe('Directory to save the screenshot. Defaults to OS temp directory.'),
|
|
13
|
+
outFile: z.string().optional().describe('Base filename (without extension). Defaults to timestamped name.'),
|
|
14
|
+
},
|
|
15
|
+
}, async (params) => {
|
|
16
|
+
try {
|
|
17
|
+
const config = resolveConfig(params);
|
|
18
|
+
const options = {
|
|
19
|
+
host: config.host,
|
|
20
|
+
password: config.password,
|
|
21
|
+
};
|
|
22
|
+
if (params.outDir) {
|
|
23
|
+
options.outDir = params.outDir;
|
|
24
|
+
}
|
|
25
|
+
if (params.outFile) {
|
|
26
|
+
options.outFile = params.outFile;
|
|
27
|
+
}
|
|
28
|
+
const filePath = await rokuDeploy.takeScreenshot(options);
|
|
29
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
30
|
+
const mimeType = ext === '.png' ? 'image/png' : 'image/jpeg';
|
|
31
|
+
const imageBuffer = await fs.readFile(filePath);
|
|
32
|
+
const base64 = imageBuffer.toString('base64');
|
|
33
|
+
return {
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: 'image',
|
|
37
|
+
data: base64,
|
|
38
|
+
mimeType,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'text',
|
|
42
|
+
text: `Screenshot saved to: ${filePath}`,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: 'text', text: `Screenshot failed: ${message}` }],
|
|
51
|
+
isError: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=screenshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../src/tools/screenshot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,aAAa,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,MAAM,UAAU,uBAAuB,CAAC,MAAiB;IACvD,MAAM,CAAC,YAAY,CACjB,iBAAiB,EACjB;QACE,WAAW,EAAE,4LAA4L;QACzM,WAAW,EAAE;YACX,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2CAA2C,CAAC;YACjF,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wCAAwC,CAAC;YAClF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kEAAkE,CAAC;YAC1G,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kEAAkE,CAAC;SAC5G;KACF,EACD,KAAK,EAAE,MAAM,EAAE,EAAE;QACf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YACrC,MAAM,OAAO,GAA4B;gBACvC,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;aAC1B,CAAC;YAEF,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBAClB,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;YACjC,CAAC;YACD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;YACnC,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,cAAc,CAAC,OAAc,CAAC,CAAC;YACjE,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;YACjD,MAAM,QAAQ,GAAG,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC;YAC7D,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAChD,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAE9C,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,OAAgB;wBACtB,IAAI,EAAE,MAAM;wBACZ,QAAQ;qBACT;oBACD;wBACE,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,wBAAwB,QAAQ,EAAE;qBACzC;iBACF;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,sBAAsB,OAAO,EAAE,EAAE,CAAC;gBAClE,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "roku-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Roku device automation — deploy, ECP control, screenshots, and debug console",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"roku-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/jack1590/RokuMCP.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/jack1590/RokuMCP#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/jack1590/RokuMCP/issues"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"roku",
|
|
25
|
+
"brightscript",
|
|
26
|
+
"ecp",
|
|
27
|
+
"automation",
|
|
28
|
+
"modelcontextprotocol",
|
|
29
|
+
"scenegraph"
|
|
30
|
+
],
|
|
31
|
+
"author": "jack1590",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"watch": "tsc --watch",
|
|
35
|
+
"start": "node dist/index.js",
|
|
36
|
+
"prepare": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
41
|
+
"axios": "^1.13.6",
|
|
42
|
+
"fast-xml-parser": "^5.5.9",
|
|
43
|
+
"roku-deploy": "^3.16.3",
|
|
44
|
+
"zod": "^4.3.6"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^25.5.0",
|
|
48
|
+
"typescript": "^6.0.2"
|
|
49
|
+
}
|
|
50
|
+
}
|