mcp-remotetouch 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Signal Slot Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # mcp-remotetouch
2
+
3
+ An MCP server for remotely controlling a touchscreen on any Linux device over SSH.
4
+
5
+ Creates a virtual touch device via Linux `uinput` and injects tap, swipe, long press, and double tap events. **No installation required on the remote device** — the Python daemon is base64-encoded and sent as an SSH command argument, using only Python's standard library.
6
+
7
+ ## Architecture
8
+
9
+ ### Stdio mode (default)
10
+
11
+ ```
12
+ Dev Machine Remote Linux Device
13
+ ┌──────────────────┐ SSH (persistent) ┌──────────────────┐
14
+ │ MCP Server (TS) │ ──────────────────> │ Python daemon │
15
+ │ stdio transport │ JSON-line proto │ (stdlib only) │
16
+ │ │ <────────────────── │ │
17
+ │ touch_tap │ │ /dev/uinput │
18
+ │ touch_swipe │ │ ↓ │
19
+ │ touch_long_press │ │ Virtual touch │
20
+ │ touch_double_tap │ │ ↓ │
21
+ │ touch_disconnect │ │ Linux Input │
22
+ └──────────────────┘ └──────────────────┘
23
+ ```
24
+
25
+ ### HTTP server mode (`--server`)
26
+
27
+ ```
28
+ AI Agent (remote) ──HTTP/SSE──> Express + StreamableHTTPServerTransport
29
+
30
+ McpServer (per MCP session)
31
+
32
+ SshTouchSessionManager (shared)
33
+
34
+ SSH ──> Linux Device
35
+ ```
36
+
37
+ ## Prerequisites
38
+
39
+ ### Dev Machine
40
+
41
+ - Node.js 18+
42
+ - SSH client
43
+
44
+ ### Remote Device
45
+
46
+ - Any Linux device with a touchscreen (Raspberry Pi, SBC, embedded system, etc.)
47
+ - Python 3
48
+ - Access to `/dev/uinput`
49
+
50
+ Add the user to the `input` group on the remote device:
51
+
52
+ ```bash
53
+ sudo usermod -aG input $USER
54
+ ```
55
+
56
+ Re-login for the change to take effect.
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ git clone https://github.com/signal-slot/mcp-remotetouch.git
62
+ cd mcp-remotetouch
63
+ npm install
64
+ npm run build
65
+ ```
66
+
67
+ ## Registering as an MCP Server
68
+
69
+ Add to Claude Desktop's `claude_desktop_config.json`:
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "remotetouch": {
75
+ "command": "node",
76
+ "args": ["/path/to/mcp-remotetouch/build/index.js"],
77
+ "env": {
78
+ "REMOTETOUCH_SSH_HOST": "192.168.1.100",
79
+ "REMOTETOUCH_SSH_USER": "pi",
80
+ "REMOTETOUCH_SCREEN_WIDTH": "800",
81
+ "REMOTETOUCH_SCREEN_HEIGHT": "480"
82
+ }
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ ## Environment Variables
89
+
90
+ | Variable | Default | Description |
91
+ |---|---|---|
92
+ | `REMOTETOUCH_SSH_HOST` | (required) | SSH host of the remote device |
93
+ | `REMOTETOUCH_SSH_USER` | `pi` | SSH username |
94
+ | `REMOTETOUCH_SSH_PORT` | `22` | SSH port |
95
+ | `REMOTETOUCH_SSH_KEY` | (none) | Path to SSH private key |
96
+ | `REMOTETOUCH_SCREEN_WIDTH` | `800` | Screen width in pixels |
97
+ | `REMOTETOUCH_SCREEN_HEIGHT` | `480` | Screen height in pixels |
98
+ | `REMOTETOUCH_USE_SUDO` | `false` | Run daemon with sudo |
99
+
100
+ ## Tools
101
+
102
+ ### `touch_connect`
103
+
104
+ Connect to a remote Linux device via SSH and start the touch daemon. Returns a session ID.
105
+
106
+ | Parameter | Type | Description |
107
+ |---|---|---|
108
+ | `host` | string? | SSH host |
109
+ | `user` | string? | SSH username |
110
+ | `port` | number? | SSH port |
111
+ | `sshKey` | string? | Path to SSH private key |
112
+ | `screenWidth` | number? | Screen width in pixels |
113
+ | `screenHeight` | number? | Screen height in pixels |
114
+ | `useSudo` | boolean? | Run with sudo |
115
+
116
+ ### `touch_tap`
117
+
118
+ Tap at the given coordinates.
119
+
120
+ | Parameter | Type | Description |
121
+ |---|---|---|
122
+ | `sessionId` | string | Session ID |
123
+ | `x` | number | X coordinate |
124
+ | `y` | number | Y coordinate |
125
+ | `duration_ms` | number? | Tap duration (default: 50ms) |
126
+
127
+ ### `touch_swipe`
128
+
129
+ Swipe from (x1, y1) to (x2, y2).
130
+
131
+ | Parameter | Type | Description |
132
+ |---|---|---|
133
+ | `sessionId` | string | Session ID |
134
+ | `x1` | number | Start X coordinate |
135
+ | `y1` | number | Start Y coordinate |
136
+ | `x2` | number | End X coordinate |
137
+ | `y2` | number | End Y coordinate |
138
+ | `duration_ms` | number? | Swipe duration (default: 300ms) |
139
+ | `steps` | number? | Number of interpolation steps |
140
+
141
+ ### `touch_long_press`
142
+
143
+ Long press at the given coordinates.
144
+
145
+ | Parameter | Type | Description |
146
+ |---|---|---|
147
+ | `sessionId` | string | Session ID |
148
+ | `x` | number | X coordinate |
149
+ | `y` | number | Y coordinate |
150
+ | `duration_ms` | number? | Press duration (default: 800ms) |
151
+
152
+ ### `touch_double_tap`
153
+
154
+ Double tap at the given coordinates.
155
+
156
+ | Parameter | Type | Description |
157
+ |---|---|---|
158
+ | `sessionId` | string | Session ID |
159
+ | `x` | number | X coordinate |
160
+ | `y` | number | Y coordinate |
161
+
162
+ ### `touch_disconnect`
163
+
164
+ Disconnect a session and clean up the remote daemon.
165
+
166
+ | Parameter | Type | Description |
167
+ |---|---|---|
168
+ | `sessionId` | string | Session ID |
169
+
170
+ ### `touch_list_sessions`
171
+
172
+ List all active sessions. No parameters.
173
+
174
+ ## HTTP Server Mode
175
+
176
+ Instead of running as a stdio MCP server, you can run `mcp-remotetouch` as an HTTP server that AI agents can connect to remotely over HTTP.
177
+
178
+ ### Starting the server
179
+
180
+ ```bash
181
+ # Default: listen on 0.0.0.0:3000
182
+ node build/index.js --server
183
+
184
+ # Custom port and host
185
+ node build/index.js --server --port 8080 --host 127.0.0.1
186
+
187
+ # Or use the convenience script
188
+ npm run start:server
189
+ ```
190
+
191
+ ### CLI arguments
192
+
193
+ | Argument | Default | Description |
194
+ |---|---|---|
195
+ | `--server` | (off) | Enable HTTP server mode |
196
+ | `--port <N>` | `3000` (or `REMOTETOUCH_PORT` env) | HTTP listen port |
197
+ | `--host <addr>` | `0.0.0.0` | Bind address |
198
+
199
+ Without `--server`, the process runs in stdio mode (the default, backward-compatible behavior).
200
+
201
+ ### Connecting from an AI agent
202
+
203
+ The server exposes a single endpoint at `/mcp` that speaks the MCP Streamable HTTP transport protocol.
204
+
205
+ ```bash
206
+ # Initialize a session
207
+ curl -X POST http://localhost:3000/mcp \
208
+ -H "Content-Type: application/json" \
209
+ -H "Accept: application/json, text/event-stream" \
210
+ -d '{
211
+ "jsonrpc": "2.0",
212
+ "method": "initialize",
213
+ "params": {
214
+ "protocolVersion": "2025-03-26",
215
+ "capabilities": {},
216
+ "clientInfo": { "name": "test", "version": "1.0" }
217
+ },
218
+ "id": 1
219
+ }'
220
+ ```
221
+
222
+ The response includes an `mcp-session-id` header. Include it in all subsequent requests:
223
+
224
+ ```bash
225
+ curl -X POST http://localhost:3000/mcp \
226
+ -H "Content-Type: application/json" \
227
+ -H "Accept: application/json, text/event-stream" \
228
+ -H "mcp-session-id: <session-id>" \
229
+ -d '{"jsonrpc":"2.0","method":"tools/list","id":2}'
230
+ ```
231
+
232
+ ## Usage
233
+
234
+ From Claude Desktop:
235
+
236
+ 1. `touch_connect` to connect to the remote device
237
+ 2. `touch_tap` to tap a coordinate on the screen
238
+ 3. `touch_swipe` to scroll or swipe
239
+ 4. `touch_disconnect` to end the session
240
+
241
+ ## Troubleshooting
242
+
243
+ ### Permission denied accessing /dev/uinput
244
+
245
+ The user on the remote device is not in the `input` group:
246
+
247
+ ```bash
248
+ sudo usermod -aG input $USER
249
+ # Re-login for the change to take effect
250
+ ```
251
+
252
+ Alternatively, set `REMOTETOUCH_USE_SUDO=true`.
253
+
254
+ ### SSH connection fails
255
+
256
+ - Ensure SSH public key authentication is configured for the remote device (password authentication is not supported since the connection uses `BatchMode=yes`)
257
+ - Verify the hostname and port are correct
258
+
259
+ ## License
260
+
261
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/build/index.js ADDED
@@ -0,0 +1,310 @@
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 { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
6
+ import { z } from "zod";
7
+ import { SshTouchSessionManager } from "./ssh-touch-session.js";
8
+ import { randomUUID } from "node:crypto";
9
+ // Defaults from environment variables
10
+ function defaultConfig(overrides = {}) {
11
+ return {
12
+ host: overrides.host ?? process.env.REMOTETOUCH_SSH_HOST ?? "",
13
+ user: overrides.user ?? process.env.REMOTETOUCH_SSH_USER ?? "pi",
14
+ port: overrides.port ?? Number(process.env.REMOTETOUCH_SSH_PORT ?? "22"),
15
+ sshKey: overrides.sshKey ?? process.env.REMOTETOUCH_SSH_KEY ?? undefined,
16
+ screenWidth: overrides.screenWidth ?? Number(process.env.REMOTETOUCH_SCREEN_WIDTH ?? "800"),
17
+ screenHeight: overrides.screenHeight ?? Number(process.env.REMOTETOUCH_SCREEN_HEIGHT ?? "480"),
18
+ useSudo: overrides.useSudo ?? (process.env.REMOTETOUCH_USE_SUDO === "true"),
19
+ };
20
+ }
21
+ // --- Server factory ---
22
+ function createServer(manager) {
23
+ const server = new McpServer({
24
+ name: "mcp-remotetouch",
25
+ version: "0.1.0",
26
+ });
27
+ server.tool("touch_connect", "Connect to a remote Linux device via SSH and start the touch daemon. Returns a session ID for subsequent touch commands.", {
28
+ host: z.string().optional().describe("SSH host (default: env REMOTETOUCH_SSH_HOST)"),
29
+ user: z.string().optional().describe("SSH user (default: env REMOTETOUCH_SSH_USER or 'pi')"),
30
+ port: z.number().optional().describe("SSH port (default: env REMOTETOUCH_SSH_PORT or 22)"),
31
+ sshKey: z.string().optional().describe("Path to SSH private key"),
32
+ screenWidth: z.number().optional().describe("Screen width in pixels (default: env REMOTETOUCH_SCREEN_WIDTH or 800)"),
33
+ screenHeight: z.number().optional().describe("Screen height in pixels (default: env REMOTETOUCH_SCREEN_HEIGHT or 480)"),
34
+ useSudo: z.boolean().optional().describe("Run daemon with sudo (default: env REMOTETOUCH_USE_SUDO or false)"),
35
+ }, async (params) => {
36
+ const config = defaultConfig({
37
+ host: params.host,
38
+ user: params.user,
39
+ port: params.port,
40
+ sshKey: params.sshKey,
41
+ screenWidth: params.screenWidth,
42
+ screenHeight: params.screenHeight,
43
+ useSudo: params.useSudo,
44
+ });
45
+ if (!config.host) {
46
+ return {
47
+ content: [{ type: "text", text: "Error: host is required. Set REMOTETOUCH_SSH_HOST or pass host parameter." }],
48
+ isError: true,
49
+ };
50
+ }
51
+ try {
52
+ const sessionId = await manager.connect(config);
53
+ return {
54
+ content: [{ type: "text", text: `Connected. Session ID: ${sessionId}\nHost: ${config.user}@${config.host}:${config.port}\nScreen: ${config.screenWidth}x${config.screenHeight}` }],
55
+ };
56
+ }
57
+ catch (err) {
58
+ return {
59
+ content: [{ type: "text", text: `Connection failed: ${err instanceof Error ? err.message : String(err)}` }],
60
+ isError: true,
61
+ };
62
+ }
63
+ });
64
+ server.tool("touch_tap", "Perform a tap at the given coordinates on the remote touchscreen.", {
65
+ sessionId: z.string().describe("Session ID from touch_connect"),
66
+ x: z.number().describe("X coordinate"),
67
+ y: z.number().describe("Y coordinate"),
68
+ duration_ms: z.number().optional().describe("Tap duration in milliseconds (default: 50)"),
69
+ }, async (params) => {
70
+ const cmd = {
71
+ id: `tap-${Date.now()}`,
72
+ type: "tap",
73
+ x: params.x,
74
+ y: params.y,
75
+ duration_ms: params.duration_ms,
76
+ };
77
+ try {
78
+ const resp = await manager.sendCommand(params.sessionId, cmd);
79
+ if (resp.status === "error") {
80
+ return { content: [{ type: "text", text: `Tap failed: ${resp.message}` }], isError: true };
81
+ }
82
+ return { content: [{ type: "text", text: `Tapped at (${params.x}, ${params.y})` }] };
83
+ }
84
+ catch (err) {
85
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
86
+ }
87
+ });
88
+ server.tool("touch_swipe", "Perform a swipe gesture from (x1,y1) to (x2,y2) on the remote touchscreen.", {
89
+ sessionId: z.string().describe("Session ID from touch_connect"),
90
+ x1: z.number().describe("Start X coordinate"),
91
+ y1: z.number().describe("Start Y coordinate"),
92
+ x2: z.number().describe("End X coordinate"),
93
+ y2: z.number().describe("End Y coordinate"),
94
+ duration_ms: z.number().optional().describe("Swipe duration in milliseconds (default: 300)"),
95
+ steps: z.number().optional().describe("Number of interpolation steps"),
96
+ }, async (params) => {
97
+ const cmd = {
98
+ id: `swipe-${Date.now()}`,
99
+ type: "swipe",
100
+ x: params.x1,
101
+ y: params.y1,
102
+ x2: params.x2,
103
+ y2: params.y2,
104
+ duration_ms: params.duration_ms,
105
+ steps: params.steps,
106
+ };
107
+ try {
108
+ const resp = await manager.sendCommand(params.sessionId, cmd);
109
+ if (resp.status === "error") {
110
+ return { content: [{ type: "text", text: `Swipe failed: ${resp.message}` }], isError: true };
111
+ }
112
+ return { content: [{ type: "text", text: `Swiped from (${params.x1}, ${params.y1}) to (${params.x2}, ${params.y2})` }] };
113
+ }
114
+ catch (err) {
115
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
116
+ }
117
+ });
118
+ server.tool("touch_long_press", "Perform a long press at the given coordinates on the remote touchscreen.", {
119
+ sessionId: z.string().describe("Session ID from touch_connect"),
120
+ x: z.number().describe("X coordinate"),
121
+ y: z.number().describe("Y coordinate"),
122
+ duration_ms: z.number().optional().describe("Press duration in milliseconds (default: 800)"),
123
+ }, async (params) => {
124
+ const cmd = {
125
+ id: `longpress-${Date.now()}`,
126
+ type: "long_press",
127
+ x: params.x,
128
+ y: params.y,
129
+ duration_ms: params.duration_ms,
130
+ };
131
+ try {
132
+ const resp = await manager.sendCommand(params.sessionId, cmd);
133
+ if (resp.status === "error") {
134
+ return { content: [{ type: "text", text: `Long press failed: ${resp.message}` }], isError: true };
135
+ }
136
+ return { content: [{ type: "text", text: `Long pressed at (${params.x}, ${params.y}) for ${params.duration_ms ?? 800}ms` }] };
137
+ }
138
+ catch (err) {
139
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
140
+ }
141
+ });
142
+ server.tool("touch_double_tap", "Perform a double tap at the given coordinates on the remote touchscreen.", {
143
+ sessionId: z.string().describe("Session ID from touch_connect"),
144
+ x: z.number().describe("X coordinate"),
145
+ y: z.number().describe("Y coordinate"),
146
+ }, async (params) => {
147
+ const cmd = {
148
+ id: `doubletap-${Date.now()}`,
149
+ type: "double_tap",
150
+ x: params.x,
151
+ y: params.y,
152
+ };
153
+ try {
154
+ const resp = await manager.sendCommand(params.sessionId, cmd);
155
+ if (resp.status === "error") {
156
+ return { content: [{ type: "text", text: `Double tap failed: ${resp.message}` }], isError: true };
157
+ }
158
+ return { content: [{ type: "text", text: `Double tapped at (${params.x}, ${params.y})` }] };
159
+ }
160
+ catch (err) {
161
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
162
+ }
163
+ });
164
+ server.tool("touch_disconnect", "Disconnect a touch session and clean up the remote daemon.", {
165
+ sessionId: z.string().describe("Session ID to disconnect"),
166
+ }, async (params) => {
167
+ try {
168
+ await manager.disconnect(params.sessionId);
169
+ return { content: [{ type: "text", text: `Session ${params.sessionId} disconnected.` }] };
170
+ }
171
+ catch (err) {
172
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
173
+ }
174
+ });
175
+ server.tool("touch_list_sessions", "List all active touch sessions.", {}, async () => {
176
+ const sessions = manager.listSessions();
177
+ if (sessions.length === 0) {
178
+ return { content: [{ type: "text", text: "No active sessions." }] };
179
+ }
180
+ const lines = sessions.map((s) => `${s.id} - ${s.host} (${s.active ? "active" : "inactive"})`);
181
+ return { content: [{ type: "text", text: lines.join("\n") }] };
182
+ });
183
+ return server;
184
+ }
185
+ // --- CLI argument parsing ---
186
+ function parseArgs(argv) {
187
+ let server = false;
188
+ let port = Number(process.env.REMOTETOUCH_PORT ?? "3000");
189
+ let host = "0.0.0.0";
190
+ for (let i = 2; i < argv.length; i++) {
191
+ if (argv[i] === "--server") {
192
+ server = true;
193
+ }
194
+ else if (argv[i] === "--port" && i + 1 < argv.length) {
195
+ port = Number(argv[++i]);
196
+ }
197
+ else if (argv[i] === "--host" && i + 1 < argv.length) {
198
+ host = argv[++i];
199
+ }
200
+ }
201
+ return { server, port, host };
202
+ }
203
+ // --- Stdio mode ---
204
+ async function startStdioServer(manager) {
205
+ const server = createServer(manager);
206
+ const transport = new StdioServerTransport();
207
+ await server.connect(transport);
208
+ process.on("SIGINT", async () => {
209
+ await manager.disconnectAll();
210
+ process.exit(0);
211
+ });
212
+ process.on("SIGTERM", async () => {
213
+ await manager.disconnectAll();
214
+ process.exit(0);
215
+ });
216
+ }
217
+ // --- HTTP streaming mode ---
218
+ async function startHttpServer(manager, host, port) {
219
+ const app = createMcpExpressApp({ host });
220
+ const transports = new Map();
221
+ app.post("/mcp", async (req, res) => {
222
+ const sessionId = req.headers["mcp-session-id"];
223
+ if (sessionId && transports.has(sessionId)) {
224
+ const transport = transports.get(sessionId);
225
+ await transport.handleRequest(req, res, req.body);
226
+ return;
227
+ }
228
+ const body = req.body;
229
+ const isInitialize = Array.isArray(body)
230
+ ? body.some((msg) => msg.method === "initialize")
231
+ : body?.method === "initialize";
232
+ if (isInitialize) {
233
+ const transport = new StreamableHTTPServerTransport({
234
+ sessionIdGenerator: () => randomUUID(),
235
+ onsessioninitialized: (id) => {
236
+ transports.set(id, transport);
237
+ },
238
+ onsessionclosed: (id) => {
239
+ transports.delete(id);
240
+ },
241
+ });
242
+ const mcpServer = createServer(manager);
243
+ await mcpServer.connect(transport);
244
+ await transport.handleRequest(req, res, req.body);
245
+ return;
246
+ }
247
+ res.status(400).json({
248
+ jsonrpc: "2.0",
249
+ error: { code: -32000, message: "Bad Request: No valid session ID provided" },
250
+ id: null,
251
+ });
252
+ });
253
+ app.get("/mcp", async (req, res) => {
254
+ const sessionId = req.headers["mcp-session-id"];
255
+ if (!sessionId || !transports.has(sessionId)) {
256
+ res.status(400).json({
257
+ jsonrpc: "2.0",
258
+ error: { code: -32000, message: "Bad Request: No valid session ID provided" },
259
+ id: null,
260
+ });
261
+ return;
262
+ }
263
+ const transport = transports.get(sessionId);
264
+ await transport.handleRequest(req, res);
265
+ });
266
+ app.delete("/mcp", async (req, res) => {
267
+ const sessionId = req.headers["mcp-session-id"];
268
+ if (!sessionId || !transports.has(sessionId)) {
269
+ res.status(400).json({
270
+ jsonrpc: "2.0",
271
+ error: { code: -32000, message: "Bad Request: No valid session ID provided" },
272
+ id: null,
273
+ });
274
+ return;
275
+ }
276
+ const transport = transports.get(sessionId);
277
+ await transport.handleRequest(req, res);
278
+ });
279
+ const httpServer = app.listen(port, host, () => {
280
+ console.log(`MCP HTTP server listening on http://${host}:${port}/mcp`);
281
+ });
282
+ const shutdown = async () => {
283
+ console.log("\nShutting down...");
284
+ for (const transport of transports.values()) {
285
+ await transport.close();
286
+ }
287
+ transports.clear();
288
+ await manager.disconnectAll();
289
+ httpServer.close();
290
+ process.exit(0);
291
+ };
292
+ process.on("SIGINT", shutdown);
293
+ process.on("SIGTERM", shutdown);
294
+ }
295
+ // --- Entry point ---
296
+ async function main() {
297
+ const args = parseArgs(process.argv);
298
+ const manager = new SshTouchSessionManager();
299
+ if (args.server) {
300
+ await startHttpServer(manager, args.host, args.port);
301
+ }
302
+ else {
303
+ await startStdioServer(manager);
304
+ }
305
+ }
306
+ main().catch((err) => {
307
+ console.error("Fatal:", err);
308
+ process.exit(1);
309
+ });
310
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const PYTHON_DAEMON_SCRIPT = "\nimport sys\nimport os\nimport json\nimport struct\nimport fcntl\nimport time\n\n# ioctl constants\nUI_SET_EVBIT = 0x40045564\nUI_SET_ABSBIT = 0x40045567\nUI_SET_PROPBIT = 0x4004556e\nUI_DEV_SETUP = 0x405c5503\nUI_DEV_CREATE = 0x5501\nUI_DEV_DESTROY = 0x5502\n\n# Event types\nEV_SYN = 0x00\nEV_ABS = 0x03\n\n# Sync codes\nSYN_REPORT = 0x00\n\n# ABS codes\nABS_MT_SLOT = 0x2f\nABS_MT_TRACKING_ID = 0x39\nABS_MT_POSITION_X = 0x35\nABS_MT_POSITION_Y = 0x36\n\n# Input properties\nINPUT_PROP_DIRECT = 0x01\n\n# uinput_abs_setup struct: __u16 code, struct input_absinfo { __s32 value, min, max, fuzz, flat, resolution }\n# = HiiiiiI -> but let's use the raw struct\n# Actually uinput_abs_setup = __u16 + padding + input_absinfo(6 x __s32)\n# struct uinput_abs_setup { __u16 code; __u16 pad; struct input_absinfo { __s32 value, minimum, maximum, fuzz, flat, resolution; }; };\nUI_ABS_SETUP = 0x405c5504\n\nUINPUT_MAX_NAME_SIZE = 80\n\ndef make_input_event(tv_sec, tv_usec, ev_type, code, value):\n # struct input_event uses struct timeval (long, long) + __u16 + __u16 + __s32\n # native long size handles 32/64 bit automatically\n return struct.pack('llHHi', tv_sec, tv_usec, ev_type, code, value)\n\ndef write_event(fd, ev_type, code, value):\n now = time.time()\n sec = int(now)\n usec = int((now - sec) * 1000000)\n os.write(fd, make_input_event(sec, usec, ev_type, code, value))\n\ndef syn_report(fd):\n write_event(fd, EV_SYN, SYN_REPORT, 0)\n\ndef touch_down(fd, slot, tracking_id, x, y):\n write_event(fd, EV_ABS, ABS_MT_SLOT, slot)\n write_event(fd, EV_ABS, ABS_MT_TRACKING_ID, tracking_id)\n write_event(fd, EV_ABS, ABS_MT_POSITION_X, x)\n write_event(fd, EV_ABS, ABS_MT_POSITION_Y, y)\n syn_report(fd)\n\ndef touch_move(fd, slot, x, y):\n write_event(fd, EV_ABS, ABS_MT_SLOT, slot)\n write_event(fd, EV_ABS, ABS_MT_POSITION_X, x)\n write_event(fd, EV_ABS, ABS_MT_POSITION_Y, y)\n syn_report(fd)\n\ndef touch_up(fd, slot):\n write_event(fd, EV_ABS, ABS_MT_SLOT, slot)\n write_event(fd, EV_ABS, ABS_MT_TRACKING_ID, -1)\n syn_report(fd)\n\ndef setup_uinput(screen_width, screen_height):\n fd = os.open('/dev/uinput', os.O_WRONLY | os.O_NONBLOCK)\n\n # Enable EV_ABS\n fcntl.ioctl(fd, UI_SET_EVBIT, EV_ABS)\n\n # Enable ABS axes\n fcntl.ioctl(fd, UI_SET_ABSBIT, ABS_MT_SLOT)\n fcntl.ioctl(fd, UI_SET_ABSBIT, ABS_MT_TRACKING_ID)\n fcntl.ioctl(fd, UI_SET_ABSBIT, ABS_MT_POSITION_X)\n fcntl.ioctl(fd, UI_SET_ABSBIT, ABS_MT_POSITION_Y)\n\n # Set INPUT_PROP_DIRECT for touchscreen\n fcntl.ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT)\n\n # Setup ABS axes with uinput_abs_setup\n # struct uinput_abs_setup { __u16 code; __u16 pad; struct input_absinfo { __s32 value, minimum, maximum, fuzz, flat, resolution; }; };\n abs_setup_fmt = 'HH6i'\n\n # ABS_MT_SLOT: 0 to 9\n data = struct.pack(abs_setup_fmt, ABS_MT_SLOT, 0, 0, 0, 9, 0, 0, 0)\n fcntl.ioctl(fd, UI_ABS_SETUP, data)\n\n # ABS_MT_TRACKING_ID: 0 to 65535\n data = struct.pack(abs_setup_fmt, ABS_MT_TRACKING_ID, 0, 0, 0, 65535, 0, 0, 0)\n fcntl.ioctl(fd, UI_ABS_SETUP, data)\n\n # ABS_MT_POSITION_X: 0 to screen_width - 1\n data = struct.pack(abs_setup_fmt, ABS_MT_POSITION_X, 0, 0, 0, screen_width - 1, 0, 0, 0)\n fcntl.ioctl(fd, UI_ABS_SETUP, data)\n\n # ABS_MT_POSITION_Y: 0 to screen_height - 1\n data = struct.pack(abs_setup_fmt, ABS_MT_POSITION_Y, 0, 0, 0, screen_height - 1, 0, 0, 0)\n fcntl.ioctl(fd, UI_ABS_SETUP, data)\n\n # uinput_setup: char name[UINPUT_MAX_NAME_SIZE]; struct input_id { __u16 bustype, vendor, product, version; };\n # struct input_id: HHH H (bustype=0x03 USB, vendor=0x1234, product=0x5678, version=1)\n # struct uinput_setup: 80s HHHH I (ff_effects_max)\n name = b'mcp-remotetouch'\n setup_data = struct.pack('80sHHHHI', name.ljust(UINPUT_MAX_NAME_SIZE, b'\\x00'), 0x03, 0x1234, 0x5678, 1, 0)\n fcntl.ioctl(fd, UI_DEV_SETUP, setup_data)\n\n # Create the device\n fcntl.ioctl(fd, UI_DEV_CREATE)\n\n # Give udev time to create the device node\n time.sleep(0.2)\n\n return fd\n\ndef send_response(cmd_id, status, message=None):\n resp = {'id': cmd_id, 'status': status}\n if message is not None:\n resp['message'] = message\n sys.stdout.write(json.dumps(resp) + '\\n')\n sys.stdout.flush()\n\ntracking_id_counter = 0\n\ndef next_tracking_id():\n global tracking_id_counter\n tracking_id_counter = (tracking_id_counter + 1) % 65536\n return tracking_id_counter\n\ndef handle_tap(fd, cmd):\n x = int(cmd['x'])\n y = int(cmd['y'])\n duration_ms = cmd.get('duration_ms', 50)\n tid = next_tracking_id()\n touch_down(fd, 0, tid, x, y)\n time.sleep(duration_ms / 1000.0)\n touch_up(fd, 0)\n\ndef handle_swipe(fd, cmd):\n x1 = int(cmd['x'])\n y1 = int(cmd['y'])\n x2 = int(cmd['x2'])\n y2 = int(cmd['y2'])\n duration_ms = cmd.get('duration_ms', 300)\n steps = cmd.get('steps', max(int(duration_ms / 15), 2))\n tid = next_tracking_id()\n\n touch_down(fd, 0, tid, x1, y1)\n for i in range(1, steps + 1):\n t = i / steps\n cx = int(x1 + (x2 - x1) * t)\n cy = int(y1 + (y2 - y1) * t)\n time.sleep(duration_ms / 1000.0 / steps)\n touch_move(fd, 0, cx, cy)\n touch_up(fd, 0)\n\ndef handle_long_press(fd, cmd):\n x = int(cmd['x'])\n y = int(cmd['y'])\n duration_ms = cmd.get('duration_ms', 800)\n tid = next_tracking_id()\n touch_down(fd, 0, tid, x, y)\n time.sleep(duration_ms / 1000.0)\n touch_up(fd, 0)\n\ndef handle_double_tap(fd, cmd):\n x = int(cmd['x'])\n y = int(cmd['y'])\n tid = next_tracking_id()\n touch_down(fd, 0, tid, x, y)\n time.sleep(0.05)\n touch_up(fd, 0)\n time.sleep(0.1)\n tid = next_tracking_id()\n touch_down(fd, 0, tid, x, y)\n time.sleep(0.05)\n touch_up(fd, 0)\n\ndef main():\n fd = None\n try:\n for line in sys.stdin:\n line = line.strip()\n if not line:\n continue\n try:\n cmd = json.loads(line)\n except json.JSONDecodeError as e:\n sys.stderr.write('Invalid JSON: ' + str(e) + '\\n')\n continue\n\n cmd_id = cmd.get('id', '?')\n cmd_type = cmd.get('type', '')\n\n try:\n if cmd_type == 'init':\n screen_width = cmd.get('screen_width', 800)\n screen_height = cmd.get('screen_height', 480)\n fd = setup_uinput(screen_width, screen_height)\n send_response(cmd_id, 'ready', 'uinput device created')\n elif cmd_type == 'shutdown':\n send_response(cmd_id, 'ok', 'shutting down')\n break\n elif fd is None:\n send_response(cmd_id, 'error', 'device not initialized, send init first')\n elif cmd_type == 'tap':\n handle_tap(fd, cmd)\n send_response(cmd_id, 'ok')\n elif cmd_type == 'swipe':\n handle_swipe(fd, cmd)\n send_response(cmd_id, 'ok')\n elif cmd_type == 'long_press':\n handle_long_press(fd, cmd)\n send_response(cmd_id, 'ok')\n elif cmd_type == 'double_tap':\n handle_double_tap(fd, cmd)\n send_response(cmd_id, 'ok')\n else:\n send_response(cmd_id, 'error', 'unknown command: ' + cmd_type)\n except PermissionError:\n send_response(cmd_id, 'error', 'Permission denied accessing /dev/uinput. Ensure user is in the input group: sudo usermod -aG input $USER')\n except Exception as e:\n send_response(cmd_id, 'error', str(e))\n finally:\n if fd is not None:\n try:\n fcntl.ioctl(fd, UI_DEV_DESTROY)\n os.close(fd)\n except Exception:\n pass\n\nif __name__ == '__main__':\n main()\n";
@@ -0,0 +1,242 @@
1
+ export const PYTHON_DAEMON_SCRIPT = `
2
+ import sys
3
+ import os
4
+ import json
5
+ import struct
6
+ import fcntl
7
+ import time
8
+
9
+ # ioctl constants
10
+ UI_SET_EVBIT = 0x40045564
11
+ UI_SET_ABSBIT = 0x40045567
12
+ UI_SET_PROPBIT = 0x4004556e
13
+ UI_DEV_SETUP = 0x405c5503
14
+ UI_DEV_CREATE = 0x5501
15
+ UI_DEV_DESTROY = 0x5502
16
+
17
+ # Event types
18
+ EV_SYN = 0x00
19
+ EV_ABS = 0x03
20
+
21
+ # Sync codes
22
+ SYN_REPORT = 0x00
23
+
24
+ # ABS codes
25
+ ABS_MT_SLOT = 0x2f
26
+ ABS_MT_TRACKING_ID = 0x39
27
+ ABS_MT_POSITION_X = 0x35
28
+ ABS_MT_POSITION_Y = 0x36
29
+
30
+ # Input properties
31
+ INPUT_PROP_DIRECT = 0x01
32
+
33
+ # uinput_abs_setup struct: __u16 code, struct input_absinfo { __s32 value, min, max, fuzz, flat, resolution }
34
+ # = HiiiiiI -> but let's use the raw struct
35
+ # Actually uinput_abs_setup = __u16 + padding + input_absinfo(6 x __s32)
36
+ # struct uinput_abs_setup { __u16 code; __u16 pad; struct input_absinfo { __s32 value, minimum, maximum, fuzz, flat, resolution; }; };
37
+ UI_ABS_SETUP = 0x405c5504
38
+
39
+ UINPUT_MAX_NAME_SIZE = 80
40
+
41
+ def make_input_event(tv_sec, tv_usec, ev_type, code, value):
42
+ # struct input_event uses struct timeval (long, long) + __u16 + __u16 + __s32
43
+ # native long size handles 32/64 bit automatically
44
+ return struct.pack('llHHi', tv_sec, tv_usec, ev_type, code, value)
45
+
46
+ def write_event(fd, ev_type, code, value):
47
+ now = time.time()
48
+ sec = int(now)
49
+ usec = int((now - sec) * 1000000)
50
+ os.write(fd, make_input_event(sec, usec, ev_type, code, value))
51
+
52
+ def syn_report(fd):
53
+ write_event(fd, EV_SYN, SYN_REPORT, 0)
54
+
55
+ def touch_down(fd, slot, tracking_id, x, y):
56
+ write_event(fd, EV_ABS, ABS_MT_SLOT, slot)
57
+ write_event(fd, EV_ABS, ABS_MT_TRACKING_ID, tracking_id)
58
+ write_event(fd, EV_ABS, ABS_MT_POSITION_X, x)
59
+ write_event(fd, EV_ABS, ABS_MT_POSITION_Y, y)
60
+ syn_report(fd)
61
+
62
+ def touch_move(fd, slot, x, y):
63
+ write_event(fd, EV_ABS, ABS_MT_SLOT, slot)
64
+ write_event(fd, EV_ABS, ABS_MT_POSITION_X, x)
65
+ write_event(fd, EV_ABS, ABS_MT_POSITION_Y, y)
66
+ syn_report(fd)
67
+
68
+ def touch_up(fd, slot):
69
+ write_event(fd, EV_ABS, ABS_MT_SLOT, slot)
70
+ write_event(fd, EV_ABS, ABS_MT_TRACKING_ID, -1)
71
+ syn_report(fd)
72
+
73
+ def setup_uinput(screen_width, screen_height):
74
+ fd = os.open('/dev/uinput', os.O_WRONLY | os.O_NONBLOCK)
75
+
76
+ # Enable EV_ABS
77
+ fcntl.ioctl(fd, UI_SET_EVBIT, EV_ABS)
78
+
79
+ # Enable ABS axes
80
+ fcntl.ioctl(fd, UI_SET_ABSBIT, ABS_MT_SLOT)
81
+ fcntl.ioctl(fd, UI_SET_ABSBIT, ABS_MT_TRACKING_ID)
82
+ fcntl.ioctl(fd, UI_SET_ABSBIT, ABS_MT_POSITION_X)
83
+ fcntl.ioctl(fd, UI_SET_ABSBIT, ABS_MT_POSITION_Y)
84
+
85
+ # Set INPUT_PROP_DIRECT for touchscreen
86
+ fcntl.ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT)
87
+
88
+ # Setup ABS axes with uinput_abs_setup
89
+ # struct uinput_abs_setup { __u16 code; __u16 pad; struct input_absinfo { __s32 value, minimum, maximum, fuzz, flat, resolution; }; };
90
+ abs_setup_fmt = 'HH6i'
91
+
92
+ # ABS_MT_SLOT: 0 to 9
93
+ data = struct.pack(abs_setup_fmt, ABS_MT_SLOT, 0, 0, 0, 9, 0, 0, 0)
94
+ fcntl.ioctl(fd, UI_ABS_SETUP, data)
95
+
96
+ # ABS_MT_TRACKING_ID: 0 to 65535
97
+ data = struct.pack(abs_setup_fmt, ABS_MT_TRACKING_ID, 0, 0, 0, 65535, 0, 0, 0)
98
+ fcntl.ioctl(fd, UI_ABS_SETUP, data)
99
+
100
+ # ABS_MT_POSITION_X: 0 to screen_width - 1
101
+ data = struct.pack(abs_setup_fmt, ABS_MT_POSITION_X, 0, 0, 0, screen_width - 1, 0, 0, 0)
102
+ fcntl.ioctl(fd, UI_ABS_SETUP, data)
103
+
104
+ # ABS_MT_POSITION_Y: 0 to screen_height - 1
105
+ data = struct.pack(abs_setup_fmt, ABS_MT_POSITION_Y, 0, 0, 0, screen_height - 1, 0, 0, 0)
106
+ fcntl.ioctl(fd, UI_ABS_SETUP, data)
107
+
108
+ # uinput_setup: char name[UINPUT_MAX_NAME_SIZE]; struct input_id { __u16 bustype, vendor, product, version; };
109
+ # struct input_id: HHH H (bustype=0x03 USB, vendor=0x1234, product=0x5678, version=1)
110
+ # struct uinput_setup: 80s HHHH I (ff_effects_max)
111
+ name = b'mcp-remotetouch'
112
+ setup_data = struct.pack('80sHHHHI', name.ljust(UINPUT_MAX_NAME_SIZE, b'\\x00'), 0x03, 0x1234, 0x5678, 1, 0)
113
+ fcntl.ioctl(fd, UI_DEV_SETUP, setup_data)
114
+
115
+ # Create the device
116
+ fcntl.ioctl(fd, UI_DEV_CREATE)
117
+
118
+ # Give udev time to create the device node
119
+ time.sleep(0.2)
120
+
121
+ return fd
122
+
123
+ def send_response(cmd_id, status, message=None):
124
+ resp = {'id': cmd_id, 'status': status}
125
+ if message is not None:
126
+ resp['message'] = message
127
+ sys.stdout.write(json.dumps(resp) + '\\n')
128
+ sys.stdout.flush()
129
+
130
+ tracking_id_counter = 0
131
+
132
+ def next_tracking_id():
133
+ global tracking_id_counter
134
+ tracking_id_counter = (tracking_id_counter + 1) % 65536
135
+ return tracking_id_counter
136
+
137
+ def handle_tap(fd, cmd):
138
+ x = int(cmd['x'])
139
+ y = int(cmd['y'])
140
+ duration_ms = cmd.get('duration_ms', 50)
141
+ tid = next_tracking_id()
142
+ touch_down(fd, 0, tid, x, y)
143
+ time.sleep(duration_ms / 1000.0)
144
+ touch_up(fd, 0)
145
+
146
+ def handle_swipe(fd, cmd):
147
+ x1 = int(cmd['x'])
148
+ y1 = int(cmd['y'])
149
+ x2 = int(cmd['x2'])
150
+ y2 = int(cmd['y2'])
151
+ duration_ms = cmd.get('duration_ms', 300)
152
+ steps = cmd.get('steps', max(int(duration_ms / 15), 2))
153
+ tid = next_tracking_id()
154
+
155
+ touch_down(fd, 0, tid, x1, y1)
156
+ for i in range(1, steps + 1):
157
+ t = i / steps
158
+ cx = int(x1 + (x2 - x1) * t)
159
+ cy = int(y1 + (y2 - y1) * t)
160
+ time.sleep(duration_ms / 1000.0 / steps)
161
+ touch_move(fd, 0, cx, cy)
162
+ touch_up(fd, 0)
163
+
164
+ def handle_long_press(fd, cmd):
165
+ x = int(cmd['x'])
166
+ y = int(cmd['y'])
167
+ duration_ms = cmd.get('duration_ms', 800)
168
+ tid = next_tracking_id()
169
+ touch_down(fd, 0, tid, x, y)
170
+ time.sleep(duration_ms / 1000.0)
171
+ touch_up(fd, 0)
172
+
173
+ def handle_double_tap(fd, cmd):
174
+ x = int(cmd['x'])
175
+ y = int(cmd['y'])
176
+ tid = next_tracking_id()
177
+ touch_down(fd, 0, tid, x, y)
178
+ time.sleep(0.05)
179
+ touch_up(fd, 0)
180
+ time.sleep(0.1)
181
+ tid = next_tracking_id()
182
+ touch_down(fd, 0, tid, x, y)
183
+ time.sleep(0.05)
184
+ touch_up(fd, 0)
185
+
186
+ def main():
187
+ fd = None
188
+ try:
189
+ for line in sys.stdin:
190
+ line = line.strip()
191
+ if not line:
192
+ continue
193
+ try:
194
+ cmd = json.loads(line)
195
+ except json.JSONDecodeError as e:
196
+ sys.stderr.write('Invalid JSON: ' + str(e) + '\\n')
197
+ continue
198
+
199
+ cmd_id = cmd.get('id', '?')
200
+ cmd_type = cmd.get('type', '')
201
+
202
+ try:
203
+ if cmd_type == 'init':
204
+ screen_width = cmd.get('screen_width', 800)
205
+ screen_height = cmd.get('screen_height', 480)
206
+ fd = setup_uinput(screen_width, screen_height)
207
+ send_response(cmd_id, 'ready', 'uinput device created')
208
+ elif cmd_type == 'shutdown':
209
+ send_response(cmd_id, 'ok', 'shutting down')
210
+ break
211
+ elif fd is None:
212
+ send_response(cmd_id, 'error', 'device not initialized, send init first')
213
+ elif cmd_type == 'tap':
214
+ handle_tap(fd, cmd)
215
+ send_response(cmd_id, 'ok')
216
+ elif cmd_type == 'swipe':
217
+ handle_swipe(fd, cmd)
218
+ send_response(cmd_id, 'ok')
219
+ elif cmd_type == 'long_press':
220
+ handle_long_press(fd, cmd)
221
+ send_response(cmd_id, 'ok')
222
+ elif cmd_type == 'double_tap':
223
+ handle_double_tap(fd, cmd)
224
+ send_response(cmd_id, 'ok')
225
+ else:
226
+ send_response(cmd_id, 'error', 'unknown command: ' + cmd_type)
227
+ except PermissionError:
228
+ send_response(cmd_id, 'error', 'Permission denied accessing /dev/uinput. Ensure user is in the input group: sudo usermod -aG input $USER')
229
+ except Exception as e:
230
+ send_response(cmd_id, 'error', str(e))
231
+ finally:
232
+ if fd is not None:
233
+ try:
234
+ fcntl.ioctl(fd, UI_DEV_DESTROY)
235
+ os.close(fd)
236
+ except Exception:
237
+ pass
238
+
239
+ if __name__ == '__main__':
240
+ main()
241
+ `;
242
+ //# sourceMappingURL=python-daemon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-daemon.js","sourceRoot":"","sources":["../src/python-daemon.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgPnC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { SessionConfig, TouchSession, DaemonCommand, DaemonResponse } from "./types.js";
2
+ export declare class SshTouchSessionManager {
3
+ private sessions;
4
+ connect(config: SessionConfig): Promise<string>;
5
+ sendCommand(sessionId: string, cmd: DaemonCommand): Promise<DaemonResponse>;
6
+ private sendCommandRaw;
7
+ disconnect(sessionId: string): Promise<void>;
8
+ private cleanup;
9
+ getSession(sessionId: string): TouchSession | undefined;
10
+ listSessions(): Array<{
11
+ id: string;
12
+ host: string;
13
+ active: boolean;
14
+ }>;
15
+ disconnectAll(): Promise<void>;
16
+ }
@@ -0,0 +1,187 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createInterface } from "node:readline";
4
+ import { PYTHON_DAEMON_SCRIPT } from "./python-daemon.js";
5
+ const HANDSHAKE_TIMEOUT_MS = 15000;
6
+ const COMMAND_TIMEOUT_MS = 30000;
7
+ export class SshTouchSessionManager {
8
+ sessions = new Map();
9
+ async connect(config) {
10
+ const sessionId = randomUUID();
11
+ const session = {
12
+ id: sessionId,
13
+ config,
14
+ process: null,
15
+ active: false,
16
+ pending: null,
17
+ };
18
+ this.sessions.set(sessionId, session);
19
+ const scriptBase64 = Buffer.from(PYTHON_DAEMON_SCRIPT).toString("base64");
20
+ const pythonCmd = `python3 -u -c "import base64,sys;exec(base64.b64decode(sys.argv[1]))" "${scriptBase64}"`;
21
+ const remoteCmd = config.useSudo ? `sudo ${pythonCmd}` : pythonCmd;
22
+ const sshArgs = [
23
+ "-T",
24
+ "-o", "StrictHostKeyChecking=accept-new",
25
+ "-o", "BatchMode=yes",
26
+ "-o", "ServerAliveInterval=15",
27
+ "-o", "ServerAliveCountMax=3",
28
+ "-p", String(config.port),
29
+ ];
30
+ if (config.sshKey) {
31
+ sshArgs.push("-i", config.sshKey);
32
+ }
33
+ sshArgs.push(`${config.user}@${config.host}`, remoteCmd);
34
+ const proc = spawn("ssh", sshArgs, {
35
+ stdio: ["pipe", "pipe", "pipe"],
36
+ });
37
+ session.process = proc;
38
+ const rl = createInterface({ input: proc.stdout });
39
+ rl.on("line", (line) => {
40
+ let resp;
41
+ try {
42
+ resp = JSON.parse(line);
43
+ }
44
+ catch {
45
+ return;
46
+ }
47
+ if (session.pending) {
48
+ const p = session.pending;
49
+ session.pending = null;
50
+ p.resolve(resp);
51
+ }
52
+ });
53
+ let stderrBuf = "";
54
+ proc.stderr.on("data", (chunk) => {
55
+ stderrBuf += chunk.toString();
56
+ });
57
+ proc.on("close", (code) => {
58
+ session.active = false;
59
+ if (session.pending) {
60
+ const p = session.pending;
61
+ session.pending = null;
62
+ p.reject(new Error(`SSH process exited with code ${code}. stderr: ${stderrBuf.trim()}`));
63
+ }
64
+ });
65
+ proc.on("error", (err) => {
66
+ session.active = false;
67
+ if (session.pending) {
68
+ const p = session.pending;
69
+ session.pending = null;
70
+ p.reject(err);
71
+ }
72
+ });
73
+ // Send init command and wait for handshake
74
+ const initCmd = {
75
+ id: "init-" + sessionId,
76
+ type: "init",
77
+ screen_width: config.screenWidth,
78
+ screen_height: config.screenHeight,
79
+ };
80
+ try {
81
+ const resp = await this.sendCommandRaw(session, initCmd, HANDSHAKE_TIMEOUT_MS);
82
+ if (resp.status === "error") {
83
+ this.cleanup(session);
84
+ throw new Error(`Daemon init failed: ${resp.message}`);
85
+ }
86
+ session.active = true;
87
+ }
88
+ catch (err) {
89
+ this.cleanup(session);
90
+ this.sessions.delete(sessionId);
91
+ const msg = err instanceof Error ? err.message : String(err);
92
+ throw new Error(`Failed to connect to ${config.host}: ${msg}`);
93
+ }
94
+ return sessionId;
95
+ }
96
+ async sendCommand(sessionId, cmd) {
97
+ const session = this.sessions.get(sessionId);
98
+ if (!session) {
99
+ throw new Error(`Session not found: ${sessionId}`);
100
+ }
101
+ if (!session.active || !session.process) {
102
+ throw new Error(`Session is not active: ${sessionId}`);
103
+ }
104
+ return this.sendCommandRaw(session, cmd, COMMAND_TIMEOUT_MS);
105
+ }
106
+ sendCommandRaw(session, cmd, timeoutMs) {
107
+ return new Promise((resolve, reject) => {
108
+ if (!session.process || !session.process.stdin.writable) {
109
+ reject(new Error("SSH process stdin not writable"));
110
+ return;
111
+ }
112
+ const timer = setTimeout(() => {
113
+ if (session.pending) {
114
+ session.pending = null;
115
+ reject(new Error(`Command timed out after ${timeoutMs}ms`));
116
+ }
117
+ }, timeoutMs);
118
+ session.pending = {
119
+ resolve: (resp) => {
120
+ clearTimeout(timer);
121
+ resolve(resp);
122
+ },
123
+ reject: (err) => {
124
+ clearTimeout(timer);
125
+ reject(err);
126
+ },
127
+ };
128
+ const line = JSON.stringify(cmd) + "\n";
129
+ session.process.stdin.write(line);
130
+ });
131
+ }
132
+ async disconnect(sessionId) {
133
+ const session = this.sessions.get(sessionId);
134
+ if (!session) {
135
+ return;
136
+ }
137
+ if (session.active && session.process) {
138
+ try {
139
+ const shutdownCmd = {
140
+ id: "shutdown-" + sessionId,
141
+ type: "shutdown",
142
+ };
143
+ await this.sendCommandRaw(session, shutdownCmd, 5000);
144
+ }
145
+ catch {
146
+ // Ignore shutdown errors
147
+ }
148
+ }
149
+ this.cleanup(session);
150
+ this.sessions.delete(sessionId);
151
+ }
152
+ cleanup(session) {
153
+ session.active = false;
154
+ if (session.process) {
155
+ try {
156
+ session.process.stdin.end();
157
+ }
158
+ catch {
159
+ // ignore
160
+ }
161
+ try {
162
+ session.process.kill("SIGTERM");
163
+ }
164
+ catch {
165
+ // ignore
166
+ }
167
+ session.process = null;
168
+ }
169
+ }
170
+ getSession(sessionId) {
171
+ return this.sessions.get(sessionId);
172
+ }
173
+ listSessions() {
174
+ return Array.from(this.sessions.values()).map((s) => ({
175
+ id: s.id,
176
+ host: `${s.config.user}@${s.config.host}:${s.config.port}`,
177
+ active: s.active,
178
+ }));
179
+ }
180
+ async disconnectAll() {
181
+ const ids = Array.from(this.sessions.keys());
182
+ for (const id of ids) {
183
+ await this.disconnect(id);
184
+ }
185
+ }
186
+ }
187
+ //# sourceMappingURL=ssh-touch-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssh-touch-session.js","sourceRoot":"","sources":["../src/ssh-touch-session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { ChildProcessWithoutNullStreams } from "node:child_process";
2
+ export interface SessionConfig {
3
+ host: string;
4
+ user: string;
5
+ port: number;
6
+ sshKey?: string;
7
+ screenWidth: number;
8
+ screenHeight: number;
9
+ useSudo: boolean;
10
+ }
11
+ export interface TouchSession {
12
+ id: string;
13
+ config: SessionConfig;
14
+ process: ChildProcessWithoutNullStreams | null;
15
+ active: boolean;
16
+ pending: {
17
+ resolve: (value: DaemonResponse) => void;
18
+ reject: (reason: Error) => void;
19
+ } | null;
20
+ }
21
+ export type DaemonCommandType = "init" | "tap" | "swipe" | "long_press" | "double_tap" | "shutdown";
22
+ export interface DaemonCommand {
23
+ id: string;
24
+ type: DaemonCommandType;
25
+ x?: number;
26
+ y?: number;
27
+ x2?: number;
28
+ y2?: number;
29
+ duration_ms?: number;
30
+ steps?: number;
31
+ screen_width?: number;
32
+ screen_height?: number;
33
+ }
34
+ export interface DaemonResponse {
35
+ id: string;
36
+ status: "ok" | "error" | "ready";
37
+ message?: string;
38
+ }
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "mcp-remotetouch",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for remote touch input via SSH to any Linux device",
5
+ "type": "module",
6
+ "main": "build/index.js",
7
+ "types": "build/index.d.ts",
8
+ "bin": {
9
+ "mcp-remotetouch": "build/index.js"
10
+ },
11
+ "files": [
12
+ "build"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node build/index.js",
17
+ "start:server": "node build/index.js --server",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/signal-slot/mcp-remotetouch.git"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "touch",
27
+ "remote",
28
+ "ssh",
29
+ "linux",
30
+ "uinput"
31
+ ],
32
+ "license": "MIT",
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.12.1"
38
+ },
39
+ "devDependencies": {
40
+ "typescript": "^5.7.0",
41
+ "@types/node": "^22.0.0"
42
+ }
43
+ }