remote-debug-mcp 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,741 @@
1
+ import os
2
+ import asyncio
3
+
4
+ from mcp.server import Server
5
+ from mcp.server.stdio import stdio_server
6
+ from mcp.types import Tool, TextContent
7
+
8
+ from remote_debug_mcp.sessions import get_manager
9
+ from remote_debug_mcp.config_loader import (
10
+ load_config, get_config, reload_config, save_config,
11
+ )
12
+
13
+ server = Server("remote-debug-mcp")
14
+
15
+ _PKG_DIR = os.path.dirname(os.path.abspath(__file__))
16
+ REMOTE_TOOLS_DIR = os.path.join(_PKG_DIR, "..", "remote")
17
+
18
+
19
+ TOOLS = [
20
+ # ── SSH ──────────────────────────────────────────
21
+ Tool(
22
+ name="ssh_connect",
23
+ description="SSH connect using a named configuration from config.yaml. "
24
+ "Auto-detects remote OS (Linux/Windows). "
25
+ "Supports auto-reconnect on connection loss. "
26
+ "Use list_connections to see available config entries.",
27
+ inputSchema={
28
+ "type": "object",
29
+ "properties": {
30
+ "session_id": {
31
+ "type": "string",
32
+ "description": "Unique session ID (reusing overwrites old)",
33
+ },
34
+ "config_name": {
35
+ "type": "string",
36
+ "description": "Name of the connection in config.yaml (e.g. 'windows-pc')",
37
+ },
38
+ "max_retries": {
39
+ "type": "integer",
40
+ "description": "Max auto-reconnect attempts (default 3)",
41
+ "default": 3,
42
+ },
43
+ },
44
+ "required": ["session_id", "config_name"],
45
+ },
46
+ ),
47
+ Tool(
48
+ name="ssh_execute",
49
+ description="Execute command on a connected SSH session. "
50
+ "Automatically adapts to Linux (bash) or Windows "
51
+ "(PowerShell) remote shell. Triggers auto-reconnect "
52
+ "if connection dropped.",
53
+ inputSchema={
54
+ "type": "object",
55
+ "properties": {
56
+ "session_id": {
57
+ "type": "string",
58
+ "description": "Session ID from ssh_connect",
59
+ },
60
+ "command": {
61
+ "type": "string",
62
+ "description": "Command to execute",
63
+ },
64
+ "timeout": {
65
+ "type": "integer",
66
+ "description": "Timeout seconds (default 30)",
67
+ "default": 30,
68
+ },
69
+ },
70
+ "required": ["session_id", "command"],
71
+ },
72
+ ),
73
+ Tool(
74
+ name="ssh_disconnect",
75
+ description="Close and remove an SSH session.",
76
+ inputSchema={
77
+ "type": "object",
78
+ "properties": {
79
+ "session_id": {"type": "string", "description": "Session ID"},
80
+ },
81
+ "required": ["session_id"],
82
+ },
83
+ ),
84
+ Tool(
85
+ name="ssh_upload",
86
+ description="Upload file via SCP or SFTP. Automatically adapts "
87
+ "path format for Linux/Windows targets.",
88
+ inputSchema={
89
+ "type": "object",
90
+ "properties": {
91
+ "session_id": {"type": "string", "description": "Session ID"},
92
+ "local_path": {"type": "string", "description": "Local file path"},
93
+ "remote_path": {"type": "string", "description": "Remote destination"},
94
+ },
95
+ "required": ["session_id", "local_path", "remote_path"],
96
+ },
97
+ ),
98
+ Tool(
99
+ name="ssh_download",
100
+ description="Download file via SCP or SFTP. Automatically adapts "
101
+ "path format for Linux/Windows targets.",
102
+ inputSchema={
103
+ "type": "object",
104
+ "properties": {
105
+ "session_id": {"type": "string", "description": "Session ID"},
106
+ "remote_path": {"type": "string", "description": "Remote file path"},
107
+ "local_path": {"type": "string", "description": "Local destination"},
108
+ },
109
+ "required": ["session_id", "remote_path", "local_path"],
110
+ },
111
+ ),
112
+ Tool(
113
+ name="ssh_list",
114
+ description="List all active SSH sessions with platform and status.",
115
+ inputSchema={"type": "object", "properties": {}},
116
+ ),
117
+ # ── Telnet ───────────────────────────────────────
118
+ Tool(
119
+ name="telnet_connect",
120
+ description="Connect to remote host via Telnet, persistent background "
121
+ "session. All connection parameters come from the com2tcp "
122
+ "config in config.yaml.\n\n"
123
+ "Look up com2tcp config by name, resolve host from linked "
124
+ "SSH config, and use configured telnet_port, timeout, "
125
+ "buffer settings, and auto-reconnect policy.\n\n"
126
+ "Use list_connections to see available configs. "
127
+ "Use save_config to create/update com2tcp entries.",
128
+ inputSchema={
129
+ "type": "object",
130
+ "properties": {
131
+ "session_id": {
132
+ "type": "string",
133
+ "description": "Unique session ID (reusing overwrites old)",
134
+ },
135
+ "config_name": {
136
+ "type": "string",
137
+ "description": "Name of the com2tcp config in config.yaml "
138
+ "(e.g. 'com2tcp_COM4_5200'). All connection "
139
+ "params are read from this config.",
140
+ },
141
+ },
142
+ "required": ["session_id", "config_name"],
143
+ },
144
+ ),
145
+ Tool(
146
+ name="telnet_send",
147
+ description="Send data to a Telnet session. timeout=0: send and "
148
+ "return immediately (no wait). timeout>0: send then "
149
+ "wait for response. Supports auto-reconnect. "
150
+ "Special values: __CTRL_C__, __CTRL_D__, __CTRL_Z__.",
151
+ inputSchema={
152
+ "type": "object",
153
+ "properties": {
154
+ "session_id": {"type": "string", "description": "Session ID"},
155
+ "data": {"type": "string", "description": "Data/command to send"},
156
+ "timeout": {
157
+ "type": "integer",
158
+ "description": "Wait timeout seconds. 0 = no wait (default), "
159
+ ">0 = wait for response",
160
+ "default": 0,
161
+ },
162
+ },
163
+ "required": ["session_id", "data"],
164
+ },
165
+ ),
166
+ Tool(
167
+ name="telnet_listen",
168
+ description="Listen on a Telnet session for a specified duration, "
169
+ "returning all newly received data (consumer pattern: "
170
+ "data is consumed after read). Supports multiple encodings "
171
+ "for binary data.",
172
+ inputSchema={
173
+ "type": "object",
174
+ "properties": {
175
+ "session_id": {"type": "string", "description": "Session ID"},
176
+ "duration": {
177
+ "type": "integer",
178
+ "description": "Listen duration seconds (default 10)",
179
+ "default": 10,
180
+ },
181
+ "encoding": {
182
+ "type": "string",
183
+ "enum": ["utf-8", "base64", "hex"],
184
+ "description": "Output encoding: utf-8 (text), "
185
+ "base64 (binary safe), hex (binary safe)",
186
+ "default": "utf-8",
187
+ },
188
+ },
189
+ "required": ["session_id"],
190
+ },
191
+ ),
192
+ Tool(
193
+ name="telnet_disconnect",
194
+ description="Close and remove a Telnet session.",
195
+ inputSchema={
196
+ "type": "object",
197
+ "properties": {
198
+ "session_id": {"type": "string", "description": "Session ID"},
199
+ },
200
+ "required": ["session_id"],
201
+ },
202
+ ),
203
+ Tool(
204
+ name="telnet_list",
205
+ description="List all active Telnet sessions with status and "
206
+ "buffer sizes.",
207
+ inputSchema={"type": "object", "properties": {}},
208
+ ),
209
+ # ── Workflow ─────────────────────────────────────
210
+ Tool(
211
+ name="setup_com2tcp",
212
+ description="Complete com2telnet deployment workflow:\n"
213
+ "1. SSH upload com2telnet.py + pyproject.toml to "
214
+ "D:\\remote-debug\\com2telnet\\ on Windows PC\n"
215
+ "2. Install com2telnet + dependencies (pyserial) via pip\n"
216
+ "3. Kill any previous com2telnet instance on the same port\n"
217
+ "4. Start com2telnet in background "
218
+ "(PowerShell Start-Process -WindowStyle Hidden)\n"
219
+ "5. Verify process is running\n\n"
220
+ "After setup, use telnet_connect to host:telnet_port "
221
+ "to access serial data from the COM port.\n\n"
222
+ "Example: setup_com2tcp with com_port='COM4', "
223
+ "telnet_port=5200 runs:\n"
224
+ " com2telnet --serial COM4:5200:115200",
225
+ inputSchema={
226
+ "type": "object",
227
+ "properties": {
228
+ "ssh_session_id": {
229
+ "type": "string",
230
+ "description": "SSH session ID (must already be connected "
231
+ "to the Windows PC with the COM port)",
232
+ },
233
+ "com_port": {
234
+ "type": "string",
235
+ "description": "COM port name (e.g. COM4)",
236
+ },
237
+ "telnet_port": {
238
+ "type": "integer",
239
+ "description": "Telnet port to expose (e.g. 5200)",
240
+ },
241
+ "baud": {
242
+ "type": "integer",
243
+ "description": "Baud rate (default 115200)",
244
+ "default": 115200,
245
+ },
246
+ },
247
+ "required": ["ssh_session_id", "com_port", "telnet_port"],
248
+ },
249
+ ),
250
+ # ── Config ──────────────────────────────────────
251
+ Tool(
252
+ name="list_connections",
253
+ description="List all configured connections from config.yaml. "
254
+ "Shows SSH and com2tcp entries.",
255
+ inputSchema={"type": "object", "properties": {}},
256
+ ),
257
+ Tool(
258
+ name="save_config",
259
+ description="Save configuration to config.yaml. "
260
+ "This is the unified entry for creating/updating connection configs. "
261
+ "When no config.yaml exists: ask user for connection details "
262
+ "(host, port, username, password for SSH; ssh_config_name, com_port, "
263
+ "telnet_port for com2tcp), then call this tool with 'connections' parameter. "
264
+ "When setup_com2tcp completes, call this tool with the com2tcp connection "
265
+ "details to persist them. "
266
+ "When called without arguments, saves current in-memory config.",
267
+ inputSchema={
268
+ "type": "object",
269
+ "properties": {
270
+ "connections": {
271
+ "type": "array",
272
+ "description": "Connections to save/merge. "
273
+ "type=ssh: {name, host, port, username, password, key_file?}. "
274
+ "type=com2tcp: {name, ssh, com_port, telnet_port, baud, "
275
+ "connect_timeout?, buffer_max_size?, max_retries?, username?, password?}. "
276
+ "Omit to save current in-memory config.",
277
+ "items": {
278
+ "type": "object",
279
+ "properties": {
280
+ "name": {"type": "string", "description": "Connection name"},
281
+ "type": {"type": "string", "enum": ["ssh", "com2tcp"]},
282
+ "host": {"type": "string"},
283
+ "port": {"type": "integer"},
284
+ "username": {"type": "string"},
285
+ "password": {"type": "string"},
286
+ "key_file": {"type": "string"},
287
+ "ssh": {"type": "string", "description": "SSH config name (com2tcp only)"},
288
+ "com_port": {"type": "string", "description": "COM port name (com2tcp only)"},
289
+ "telnet_port": {"type": "integer", "description": "Telnet port (com2tcp only)"},
290
+ "baud": {"type": "integer", "description": "Baud rate (com2tcp only)"},
291
+ "connect_timeout": {"type": "integer", "description": "Connect timeout seconds (com2tcp, default 15)"},
292
+ "buffer_max_size": {"type": "integer", "description": "Buffer max size bytes (com2tcp, default 65536)"},
293
+ "max_retries": {"type": "integer", "description": "Max auto-reconnect attempts (com2tcp, default 3)"},
294
+ },
295
+ "required": ["name", "type"],
296
+ },
297
+ },
298
+ },
299
+ },
300
+ ),
301
+ # ── Telnet Monitor ────────────────────────────────
302
+ Tool(
303
+ name="telnet_start_monitor",
304
+ description="Start background monitoring on a Telnet session. "
305
+ "Data is continuously read into a circular buffer "
306
+ "(max 900000 lines, oldest evicted when full). "
307
+ "Optionally append to a file continuously.",
308
+ inputSchema={
309
+ "type": "object",
310
+ "properties": {
311
+ "session_id": {
312
+ "type": "string",
313
+ "description": "Telnet session ID",
314
+ },
315
+ "output_file": {
316
+ "type": "string",
317
+ "description": "Optional local file path to continuously "
318
+ "append received data",
319
+ },
320
+ },
321
+ "required": ["session_id"],
322
+ },
323
+ ),
324
+ Tool(
325
+ name="telnet_stop_monitor",
326
+ description="Stop background monitoring on a Telnet session. "
327
+ "Returns the total line count captured.",
328
+ inputSchema={
329
+ "type": "object",
330
+ "properties": {
331
+ "session_id": {
332
+ "type": "string",
333
+ "description": "Telnet session ID",
334
+ },
335
+ },
336
+ "required": ["session_id"],
337
+ },
338
+ ),
339
+ # ── Utility ──────────────────────────────────────
340
+ Tool(
341
+ name="list_sessions",
342
+ description="List all active SSH and Telnet sessions.",
343
+ inputSchema={"type": "object", "properties": {}},
344
+ ),
345
+ ]
346
+
347
+
348
+ @server.list_tools()
349
+ async def list_tools():
350
+ return TOOLS
351
+
352
+
353
+ @server.call_tool()
354
+ async def call_tool(name: str, arguments: dict):
355
+ mgr = get_manager()
356
+ loop = asyncio.get_event_loop()
357
+
358
+ try:
359
+ if name == "ssh_connect":
360
+ config_name = arguments["config_name"]
361
+ session_id = arguments.get("session_id", config_name)
362
+ result = await _ssh_connect_from_config(
363
+ mgr, config_name, session_id,
364
+ arguments.get("max_retries", 3),
365
+ )
366
+
367
+ elif name == "ssh_execute":
368
+ result = await loop.run_in_executor(
369
+ None,
370
+ mgr.ssh_execute,
371
+ arguments["session_id"],
372
+ arguments["command"],
373
+ arguments.get("timeout", 30),
374
+ )
375
+
376
+ elif name == "ssh_disconnect":
377
+ result = mgr.ssh_disconnect(arguments["session_id"])
378
+
379
+ elif name == "ssh_upload":
380
+ result = await loop.run_in_executor(
381
+ None,
382
+ mgr.ssh_upload,
383
+ arguments["session_id"],
384
+ arguments["local_path"],
385
+ arguments["remote_path"],
386
+ )
387
+
388
+ elif name == "ssh_download":
389
+ result = await loop.run_in_executor(
390
+ None,
391
+ mgr.ssh_download,
392
+ arguments["session_id"],
393
+ arguments["remote_path"],
394
+ arguments["local_path"],
395
+ )
396
+
397
+ elif name == "ssh_list":
398
+ result = mgr.ssh_list()
399
+
400
+ elif name == "telnet_connect":
401
+ host, port, username, password, timeout, buf_size, retries = \
402
+ _resolve_telnet_config(arguments["config_name"])
403
+ result = await loop.run_in_executor(
404
+ None,
405
+ mgr.telnet_connect,
406
+ arguments["session_id"],
407
+ host,
408
+ port,
409
+ username,
410
+ password,
411
+ timeout,
412
+ buf_size,
413
+ retries,
414
+ )
415
+
416
+ elif name == "telnet_send":
417
+ timeout = arguments.get("timeout", 0)
418
+ if timeout > 0:
419
+ result = await loop.run_in_executor(
420
+ None,
421
+ mgr.telnet_send,
422
+ arguments["session_id"],
423
+ arguments["data"],
424
+ timeout,
425
+ )
426
+ else:
427
+ result = mgr.telnet_send(
428
+ arguments["session_id"],
429
+ arguments["data"],
430
+ timeout,
431
+ )
432
+
433
+ elif name == "telnet_listen":
434
+ result = await loop.run_in_executor(
435
+ None,
436
+ mgr.telnet_listen,
437
+ arguments["session_id"],
438
+ arguments.get("duration", 10),
439
+ arguments.get("encoding", "utf-8"),
440
+ )
441
+
442
+ elif name == "telnet_disconnect":
443
+ result = mgr.telnet_disconnect(arguments["session_id"])
444
+
445
+ elif name == "telnet_list":
446
+ result = mgr.telnet_list()
447
+
448
+ elif name == "setup_com2tcp":
449
+ result = await _setup_com2tcp(
450
+ mgr,
451
+ arguments["ssh_session_id"],
452
+ arguments["com_port"],
453
+ arguments["telnet_port"],
454
+ arguments.get("baud", 115200),
455
+ )
456
+
457
+ elif name == "telnet_start_monitor":
458
+ result = mgr.telnet_start_monitor(
459
+ arguments["session_id"],
460
+ arguments.get("output_file", ""),
461
+ )
462
+
463
+ elif name == "telnet_stop_monitor":
464
+ result = mgr.telnet_stop_monitor(arguments["session_id"])
465
+
466
+ elif name == "list_sessions":
467
+ result = mgr.list_all()
468
+
469
+ elif name == "list_connections":
470
+ result = await _list_connections()
471
+
472
+ elif name == "save_config":
473
+ result = await _save_config(arguments.get("connections"))
474
+
475
+ else:
476
+ result = f"Unknown tool: {name}"
477
+
478
+ return [TextContent(type="text", text=str(result))]
479
+
480
+ except Exception as e:
481
+ return [TextContent(type="text", text=f"Error in {name}: {e}")]
482
+
483
+
484
+ async def _ssh_connect_from_config(mgr, config_name: str,
485
+ session_id: str,
486
+ max_retries: int) -> str:
487
+ loop = asyncio.get_event_loop()
488
+ try:
489
+ config = get_config()
490
+ except Exception as e:
491
+ return f"Config load failed: {e}"
492
+
493
+ entry = config.get_ssh(config_name)
494
+ if not entry:
495
+ return f"SSH config '{config_name}' not found. Use list_connections."
496
+
497
+ if entry.key_file:
498
+ result = await loop.run_in_executor(
499
+ None, mgr.ssh_connect_key,
500
+ session_id, entry.host, entry.port,
501
+ entry.username, entry.key_file, max_retries,
502
+ )
503
+ else:
504
+ result = await loop.run_in_executor(
505
+ None, mgr.ssh_connect,
506
+ session_id, entry.host, entry.port,
507
+ entry.username, entry.password, max_retries,
508
+ )
509
+ return result
510
+
511
+
512
+ async def _list_connections() -> str:
513
+ try:
514
+ config = get_config()
515
+ except Exception as e:
516
+ return f"Config not loaded: {e}"
517
+
518
+ lines = ["Configured connections:"]
519
+ if config.ssh_connections:
520
+ lines.append("\n[SSH]")
521
+ for c in config.ssh_connections:
522
+ lines.append(
523
+ f" {c.name}: {c.username}@{c.host}:{c.port}"
524
+ + (" (key)" if c.key_file else "")
525
+ )
526
+ if config.com2tcp_connections:
527
+ lines.append("\n[com2tcp]")
528
+ for c in config.com2tcp_connections:
529
+ extra = []
530
+ if c.username:
531
+ extra.append(f"login={c.username}")
532
+ if c.connect_timeout != 15:
533
+ extra.append(f"timeout={c.connect_timeout}s")
534
+ if c.buffer_max_size != 65536:
535
+ extra.append(f"buf={c.buffer_max_size}")
536
+ if c.max_retries != 3:
537
+ extra.append(f"retries={c.max_retries}")
538
+ extra_str = " " + " ".join(extra) if extra else ""
539
+ lines.append(
540
+ f" {c.name}: SSH={c.ssh} COM={c.com_port} "
541
+ f"telnet=:{c.telnet_port} baud={c.baud}{extra_str}"
542
+ )
543
+ if not config.ssh_connections and not config.com2tcp_connections:
544
+ lines.append(" (none)")
545
+ return "\n".join(lines)
546
+
547
+
548
+ def _resolve_telnet_config(config_name: str):
549
+ """从 com2tcp 配置解析全部连接参数。
550
+ 返回 (host, port, username, password, timeout, buffer_max_size, max_retries)。"""
551
+ config = get_config()
552
+ c2t = config.get_com2tcp(config_name)
553
+ if not c2t:
554
+ raise ValueError(
555
+ f"com2tcp config '{config_name}' not found. "
556
+ f"Use list_connections to see available configs."
557
+ )
558
+ ssh_cfg = config.get_ssh(c2t.ssh)
559
+ if not ssh_cfg:
560
+ raise ValueError(
561
+ f"SSH config '{c2t.ssh}' (referenced by com2tcp '{config_name}') "
562
+ f"not found. Check config.yaml."
563
+ )
564
+ return (ssh_cfg.host, c2t.telnet_port, c2t.username, c2t.password,
565
+ c2t.connect_timeout, c2t.buffer_max_size, c2t.max_retries)
566
+
567
+
568
+ async def _save_config(connections=None) -> str:
569
+ from remote_debug_mcp.config_loader import AppConfig, SSHConfig, Com2TcpConfig
570
+
571
+ try:
572
+ try:
573
+ config = get_config()
574
+ except FileNotFoundError:
575
+ config = AppConfig()
576
+
577
+ if connections:
578
+ for c in connections:
579
+ entry_type = c.get("type", "ssh")
580
+ if entry_type == "ssh":
581
+ ssh_cfg = SSHConfig(
582
+ name=c.get("name", ""),
583
+ host=c.get("host", ""),
584
+ port=c.get("port", 22),
585
+ username=c.get("username", ""),
586
+ password=c.get("password", ""),
587
+ key_file=c.get("key_file", ""),
588
+ )
589
+ existing = config.get_ssh(ssh_cfg.name)
590
+ if existing:
591
+ existing.host = ssh_cfg.host
592
+ existing.port = ssh_cfg.port
593
+ existing.username = ssh_cfg.username
594
+ existing.password = ssh_cfg.password
595
+ existing.key_file = ssh_cfg.key_file
596
+ else:
597
+ config.ssh_connections.append(ssh_cfg)
598
+ elif entry_type == "com2tcp":
599
+ c2t_cfg = Com2TcpConfig(
600
+ name=c.get("name", ""),
601
+ ssh=c.get("ssh", ""),
602
+ com_port=c.get("com_port", ""),
603
+ telnet_port=c.get("telnet_port", 5200),
604
+ baud=c.get("baud", 115200),
605
+ username=c.get("username", ""),
606
+ password=c.get("password", ""),
607
+ connect_timeout=c.get("connect_timeout", 15),
608
+ buffer_max_size=c.get("buffer_max_size", 65536),
609
+ max_retries=c.get("max_retries", 3),
610
+ )
611
+ existing = config.get_com2tcp(c2t_cfg.name)
612
+ if existing:
613
+ existing.ssh = c2t_cfg.ssh
614
+ existing.com_port = c2t_cfg.com_port
615
+ existing.telnet_port = c2t_cfg.telnet_port
616
+ existing.baud = c2t_cfg.baud
617
+ existing.username = c2t_cfg.username
618
+ existing.password = c2t_cfg.password
619
+ existing.connect_timeout = c2t_cfg.connect_timeout
620
+ existing.buffer_max_size = c2t_cfg.buffer_max_size
621
+ existing.max_retries = c2t_cfg.max_retries
622
+ else:
623
+ config.com2tcp_connections.append(c2t_cfg)
624
+
625
+ result = save_config(config)
626
+ return result
627
+ except Exception as e:
628
+ return f"Save config failed: {e}"
629
+
630
+
631
+ async def _setup_com2tcp(mgr, ssh_session_id: str, com_port: str,
632
+ telnet_port: int, baud: int) -> str:
633
+ loop = asyncio.get_event_loop()
634
+
635
+ com2telnet_py = os.path.join(REMOTE_TOOLS_DIR, "com2telnet.py")
636
+ pyproject_toml = os.path.join(REMOTE_TOOLS_DIR, "pyproject.toml")
637
+
638
+ for f in [com2telnet_py, pyproject_toml]:
639
+ if not os.path.exists(f):
640
+ return f"Required file not found: {f}"
641
+
642
+ remote_dir = "D:\\remote-debug\\com2telnet"
643
+ remote_com2telnet = f"{remote_dir}\\com2telnet.py"
644
+ remote_pyproject = f"{remote_dir}\\pyproject.toml"
645
+
646
+ parts = [
647
+ f"=== com2telnet deployment ===",
648
+ f"SSH session : {ssh_session_id}",
649
+ f"COM port : {com_port}",
650
+ f"Telnet port : {telnet_port}",
651
+ f"Baud rate : {baud}",
652
+ "",
653
+ ]
654
+
655
+ mkdir_cmd = f"New-Item -ItemType Directory -Force -Path {remote_dir} | Out-Null"
656
+ await loop.run_in_executor(None, mgr.ssh_execute, ssh_session_id, mkdir_cmd, 5)
657
+ parts.append(f"[Mkdir] {remote_dir}")
658
+
659
+ upload1 = await loop.run_in_executor(
660
+ None, mgr.ssh_upload, ssh_session_id,
661
+ com2telnet_py, remote_com2telnet,
662
+ )
663
+ parts.append(f"[Upload com2telnet.py] {upload1}")
664
+
665
+ upload2 = await loop.run_in_executor(
666
+ None, mgr.ssh_upload, ssh_session_id,
667
+ pyproject_toml, remote_pyproject,
668
+ )
669
+ parts.append(f"[Upload pyproject.toml] {upload2}")
670
+
671
+ if "OK" not in upload1:
672
+ return "\n".join(parts)
673
+
674
+ parts.append("")
675
+
676
+ pip_cmd = f"python -m pip install pyserial 2>&1 | Select-Object -Last 3"
677
+ pip_output = await loop.run_in_executor(
678
+ None, mgr.ssh_execute, ssh_session_id,
679
+ pip_cmd, 30,
680
+ )
681
+ parts.append(f"[pip check] {pip_output.strip() or '(pyserial OK)'}")
682
+
683
+ kill_cmd = (
684
+ f"$pids = (netstat -ano | Select-String ':{telnet_port}.*LISTENING' | "
685
+ f"ForEach-Object {{ ($_ -split '\\s+')[-1] }} | "
686
+ f"Where-Object {{ $_ -match '^\\d+$' }}); "
687
+ f"if ($pids) {{ foreach ($p in $pids) {{ taskkill /PID $p /F 2>&1 }} }}"
688
+ )
689
+ kill_output = await loop.run_in_executor(
690
+ None, mgr.ssh_execute, ssh_session_id, kill_cmd, 10,
691
+ )
692
+ parts.append(f"[Kill previous] {kill_output.strip() if kill_output.strip() else '(none)'}")
693
+
694
+ launch_cmd = (
695
+ f"Start-Process -WindowStyle Hidden -FilePath python "
696
+ f"-ArgumentList "
697
+ f"'{remote_com2telnet}',"
698
+ f"'--serial','{com_port}:{telnet_port}:{baud}'"
699
+ )
700
+ parts.append(f"[Launch] {launch_cmd}")
701
+
702
+ launch_output = await loop.run_in_executor(
703
+ None, mgr.ssh_execute, ssh_session_id, launch_cmd, 5,
704
+ )
705
+ parts.append(f"[Launch output] {launch_output.strip() if launch_output else '(launched)'}")
706
+
707
+ await asyncio.sleep(2)
708
+
709
+ verify_cmd = (
710
+ f"netstat -ano | Select-String ':{telnet_port}.*LISTENING'"
711
+ )
712
+ pid_check = await loop.run_in_executor(
713
+ None, mgr.ssh_execute, ssh_session_id, verify_cmd, 5,
714
+ )
715
+ parts.append(f"[Process check] {pid_check.strip() if pid_check and pid_check.strip() else '(no output - may still be starting)'}")
716
+
717
+ session = mgr._ssh_sessions.get(ssh_session_id)
718
+ host = session.params.host if session else "unknown"
719
+
720
+ parts.append("")
721
+ parts.append("=== Setup complete ===")
722
+ parts.append(f"Connect: telnet_connect(session_id='serial', "
723
+ f"config_name='com2tcp_{com_port}_{telnet_port}')")
724
+ parts.append("")
725
+ parts.append("IMPORTANT: To persist this com2tcp config, call save_config with:")
726
+ s = (f" connections=[{{\"name\":\"com2tcp_{com_port}_{telnet_port}\","
727
+ f"\"type\":\"com2tcp\",\"ssh\":\"<your-ssh-config-name>\","
728
+ f"\"com_port\":\"{com_port}\",\"telnet_port\":{telnet_port},"
729
+ f"\"baud\":{baud}}}]")
730
+ parts.append(s)
731
+
732
+ return "\n".join(parts)
733
+
734
+
735
+ async def main():
736
+ async with stdio_server() as (read_stream, write_stream):
737
+ await server.run(
738
+ read_stream,
739
+ write_stream,
740
+ server.create_initialization_options(),
741
+ )