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.
- remote_debug_mcp/__init__.py +3 -0
- remote_debug_mcp/__main__.py +5 -0
- remote_debug_mcp/config.example.yaml +30 -0
- remote_debug_mcp/config_loader.py +216 -0
- remote_debug_mcp/server.py +741 -0
- remote_debug_mcp/sessions.py +1173 -0
- remote_debug_mcp-0.2.0.dist-info/METADATA +315 -0
- remote_debug_mcp-0.2.0.dist-info/RECORD +11 -0
- remote_debug_mcp-0.2.0.dist-info/WHEEL +4 -0
- remote_debug_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- remote_debug_mcp-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 远程调试连接配置文件(示例)
|
|
2
|
+
# 复制为 config.yaml 并填入真实参数
|
|
3
|
+
#
|
|
4
|
+
# save_config 工具是配置唯一入库入口:
|
|
5
|
+
# - 无参调用:保存当前内存配置到文件
|
|
6
|
+
# - 带 connections 参数:合并条目后写入文件
|
|
7
|
+
# - 无配置文件时:LLM 询问用户参数,调用 save_config 创建
|
|
8
|
+
# - setup_com2tcp 完成后:LLM 调用 save_config 持久化 com2tcp 配置
|
|
9
|
+
|
|
10
|
+
connections:
|
|
11
|
+
- name: "my-pc"
|
|
12
|
+
type: ssh
|
|
13
|
+
host: "192.168.1.100"
|
|
14
|
+
port: 22
|
|
15
|
+
username: "your-username"
|
|
16
|
+
password: "your-password"
|
|
17
|
+
# key_file: "/path/to/key" # 可选,密钥认证
|
|
18
|
+
|
|
19
|
+
- name: "com2tcp_COM4_5200"
|
|
20
|
+
type: com2tcp
|
|
21
|
+
ssh: "my-pc" # 关联的 SSH 配置名(用于解析 host)
|
|
22
|
+
com_port: "COM4"
|
|
23
|
+
telnet_port: 5200
|
|
24
|
+
baud: 115200
|
|
25
|
+
# 以下为可选参数(均有默认值)
|
|
26
|
+
# username: "" # Telnet 登录用户名
|
|
27
|
+
# password: "" # Telnet 登录密码
|
|
28
|
+
# connect_timeout: 15 # 连接超时(秒)
|
|
29
|
+
# buffer_max_size: 65536 # 缓冲区大小(字节,默认 64KB)
|
|
30
|
+
# max_retries: 3 # 自动重连次数
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
配置文件加载器,读取 config.yaml 中的连接参数。
|
|
3
|
+
支持 SSH 连接和 com2tcp 串口映射配置。
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SSHConfig:
|
|
12
|
+
name: str
|
|
13
|
+
host: str
|
|
14
|
+
port: int = 22
|
|
15
|
+
username: str = ""
|
|
16
|
+
password: str = ""
|
|
17
|
+
key_file: str = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Com2TcpConfig:
|
|
22
|
+
name: str
|
|
23
|
+
ssh: str
|
|
24
|
+
com_port: str
|
|
25
|
+
telnet_port: int = 5200
|
|
26
|
+
baud: int = 115200
|
|
27
|
+
username: str = ""
|
|
28
|
+
password: str = ""
|
|
29
|
+
connect_timeout: int = 15
|
|
30
|
+
buffer_max_size: int = 65536
|
|
31
|
+
max_retries: int = 3
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class AppConfig:
|
|
36
|
+
ssh_connections: list[SSHConfig] = field(default_factory=list)
|
|
37
|
+
com2tcp_connections: list[Com2TcpConfig] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
def get_ssh(self, name: str) -> Optional[SSHConfig]:
|
|
40
|
+
for c in self.ssh_connections:
|
|
41
|
+
if c.name == name:
|
|
42
|
+
return c
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def get_com2tcp(self, name: str) -> Optional[Com2TcpConfig]:
|
|
46
|
+
for c in self.com2tcp_connections:
|
|
47
|
+
if c.name == name:
|
|
48
|
+
return c
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _parse_yaml_simple(content: str) -> AppConfig:
|
|
53
|
+
"""简版 YAML 解析器,无需 PyYAML 依赖。"""
|
|
54
|
+
config = AppConfig()
|
|
55
|
+
current_section = None
|
|
56
|
+
current_entry = {}
|
|
57
|
+
in_connections = False
|
|
58
|
+
|
|
59
|
+
for raw_line in content.split("\n"):
|
|
60
|
+
line = raw_line.rstrip()
|
|
61
|
+
if not line or line.strip().startswith("#"):
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
stripped = line.strip()
|
|
65
|
+
|
|
66
|
+
if stripped == "connections:":
|
|
67
|
+
in_connections = True
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if not in_connections:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if stripped.startswith("- name:"):
|
|
74
|
+
if current_entry:
|
|
75
|
+
_add_entry(config, current_entry)
|
|
76
|
+
current_entry = {}
|
|
77
|
+
name_part = stripped.split(":", 1)[1].strip()
|
|
78
|
+
if "#" in name_part:
|
|
79
|
+
name_part = name_part[:name_part.index("#")].strip()
|
|
80
|
+
current_entry["name"] = name_part.strip().strip('"')
|
|
81
|
+
|
|
82
|
+
elif ":" in stripped and not stripped.startswith("-"):
|
|
83
|
+
key, _, val = stripped.partition(":")
|
|
84
|
+
key = key.strip()
|
|
85
|
+
val = val.strip()
|
|
86
|
+
if "#" in val:
|
|
87
|
+
val = val[:val.index("#")].strip()
|
|
88
|
+
val = val.strip('"').strip("'")
|
|
89
|
+
if key in ("port", "telnet_port", "baud", "connect_timeout", "buffer_max_size", "max_retries"):
|
|
90
|
+
current_entry[key] = int(val) if val else 0
|
|
91
|
+
else:
|
|
92
|
+
current_entry[key] = val
|
|
93
|
+
|
|
94
|
+
if current_entry:
|
|
95
|
+
_add_entry(config, current_entry)
|
|
96
|
+
|
|
97
|
+
return config
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _add_entry(config: AppConfig, entry: dict):
|
|
101
|
+
name = entry.get("name", "")
|
|
102
|
+
etype = entry.get("type", "ssh")
|
|
103
|
+
if etype == "ssh":
|
|
104
|
+
config.ssh_connections.append(SSHConfig(
|
|
105
|
+
name=name,
|
|
106
|
+
host=entry.get("host", ""),
|
|
107
|
+
port=entry.get("port", 22),
|
|
108
|
+
username=entry.get("username", ""),
|
|
109
|
+
password=entry.get("password", ""),
|
|
110
|
+
key_file=entry.get("key_file", ""),
|
|
111
|
+
))
|
|
112
|
+
elif etype == "com2tcp":
|
|
113
|
+
config.com2tcp_connections.append(Com2TcpConfig(
|
|
114
|
+
name=name,
|
|
115
|
+
ssh=entry.get("ssh", ""),
|
|
116
|
+
com_port=entry.get("com_port", ""),
|
|
117
|
+
telnet_port=entry.get("telnet_port", 5200),
|
|
118
|
+
baud=entry.get("baud", 115200),
|
|
119
|
+
username=entry.get("username", ""),
|
|
120
|
+
password=entry.get("password", ""),
|
|
121
|
+
connect_timeout=entry.get("connect_timeout", 15),
|
|
122
|
+
buffer_max_size=entry.get("buffer_max_size", 65536),
|
|
123
|
+
max_retries=entry.get("max_retries", 3),
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _find_config_path(path: str) -> str:
|
|
128
|
+
"""搜索配置文件路径,返回第一个存在的文件路径。"""
|
|
129
|
+
source_dir = os.path.dirname(os.path.abspath(__file__))
|
|
130
|
+
repo_root = os.path.join(source_dir, "..", "..", "..")
|
|
131
|
+
search_paths = [
|
|
132
|
+
path,
|
|
133
|
+
os.path.join(source_dir, path),
|
|
134
|
+
os.path.join(source_dir, "..", path),
|
|
135
|
+
os.path.join(repo_root, path),
|
|
136
|
+
os.path.join(repo_root, "src", "remote_debug_mcp", path),
|
|
137
|
+
os.path.join(os.path.expanduser("~"), ".config", "remote-debug-mcp", path),
|
|
138
|
+
]
|
|
139
|
+
for p in search_paths:
|
|
140
|
+
if os.path.exists(p):
|
|
141
|
+
return p
|
|
142
|
+
return search_paths[0] # default to cwd
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def load_config(path: str = "config.yaml") -> AppConfig:
|
|
146
|
+
"""加载配置文件,自动搜索多个路径。"""
|
|
147
|
+
found = _find_config_path(path)
|
|
148
|
+
if os.path.exists(found):
|
|
149
|
+
with open(found, "r", encoding="utf-8") as f:
|
|
150
|
+
return _parse_yaml_simple(f.read())
|
|
151
|
+
raise FileNotFoundError(f"Config file not found: {path}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _build_yaml(config: AppConfig) -> str:
|
|
155
|
+
"""将 AppConfig 序列化为 YAML 字符串。"""
|
|
156
|
+
lines = ["# 远程调试连接配置文件",
|
|
157
|
+
"# 支持 SSH 连接和 com2tcp 串口映射配置",
|
|
158
|
+
"",
|
|
159
|
+
"connections:"]
|
|
160
|
+
for c in config.ssh_connections:
|
|
161
|
+
lines.append(f" - name: {c.name}")
|
|
162
|
+
lines.append(" type: ssh")
|
|
163
|
+
lines.append(f" host: \"{c.host}\"")
|
|
164
|
+
lines.append(f" port: {c.port}")
|
|
165
|
+
lines.append(f" username: \"{c.username}\"")
|
|
166
|
+
lines.append(f" password: \"{c.password}\"")
|
|
167
|
+
if c.key_file:
|
|
168
|
+
lines.append(f" key_file: \"{c.key_file}\"")
|
|
169
|
+
lines.append("")
|
|
170
|
+
for c in config.com2tcp_connections:
|
|
171
|
+
lines.append(f" - name: {c.name}")
|
|
172
|
+
lines.append(" type: com2tcp")
|
|
173
|
+
lines.append(f" ssh: \"{c.ssh}\"")
|
|
174
|
+
lines.append(f" com_port: \"{c.com_port}\"")
|
|
175
|
+
lines.append(f" telnet_port: {c.telnet_port}")
|
|
176
|
+
lines.append(f" baud: {c.baud}")
|
|
177
|
+
if c.username:
|
|
178
|
+
lines.append(f" username: \"{c.username}\"")
|
|
179
|
+
if c.password:
|
|
180
|
+
lines.append(f" password: \"{c.password}\"")
|
|
181
|
+
if c.connect_timeout != 15:
|
|
182
|
+
lines.append(f" connect_timeout: {c.connect_timeout}")
|
|
183
|
+
if c.buffer_max_size != 65536:
|
|
184
|
+
lines.append(f" buffer_max_size: {c.buffer_max_size}")
|
|
185
|
+
if c.max_retries != 3:
|
|
186
|
+
lines.append(f" max_retries: {c.max_retries}")
|
|
187
|
+
lines.append("")
|
|
188
|
+
return "\n".join(lines) + "\n"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def save_config(config: AppConfig, path: str = "config.yaml") -> str:
|
|
192
|
+
"""保存当前配置到 YAML 文件。"""
|
|
193
|
+
output_path = os.path.join(os.getcwd(), path)
|
|
194
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
195
|
+
f.write(_build_yaml(config))
|
|
196
|
+
ssh_count = len(config.ssh_connections)
|
|
197
|
+
c2t_count = len(config.com2tcp_connections)
|
|
198
|
+
return (f"Config saved: {output_path}\n"
|
|
199
|
+
f" SSH connections: {ssh_count}\n"
|
|
200
|
+
f" com2tcp entries: {c2t_count}")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
_config: Optional[AppConfig] = None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_config(path: str = "config.yaml") -> AppConfig:
|
|
207
|
+
global _config
|
|
208
|
+
if _config is None:
|
|
209
|
+
_config = load_config(path)
|
|
210
|
+
return _config
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def reload_config(path: str = "config.yaml") -> AppConfig:
|
|
214
|
+
global _config
|
|
215
|
+
_config = load_config(path)
|
|
216
|
+
return _config
|