ida-pro-mcp-xjoker 1.0.1__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.
- ida_pro_mcp/__init__.py +0 -0
- ida_pro_mcp/__main__.py +6 -0
- ida_pro_mcp/ida_mcp/__init__.py +68 -0
- ida_pro_mcp/ida_mcp/api_analysis.py +1296 -0
- ida_pro_mcp/ida_mcp/api_core.py +337 -0
- ida_pro_mcp/ida_mcp/api_debug.py +617 -0
- ida_pro_mcp/ida_mcp/api_memory.py +304 -0
- ida_pro_mcp/ida_mcp/api_modify.py +406 -0
- ida_pro_mcp/ida_mcp/api_python.py +179 -0
- ida_pro_mcp/ida_mcp/api_resources.py +295 -0
- ida_pro_mcp/ida_mcp/api_stack.py +167 -0
- ida_pro_mcp/ida_mcp/api_types.py +480 -0
- ida_pro_mcp/ida_mcp/auth.py +166 -0
- ida_pro_mcp/ida_mcp/cache.py +232 -0
- ida_pro_mcp/ida_mcp/config.py +228 -0
- ida_pro_mcp/ida_mcp/framework.py +547 -0
- ida_pro_mcp/ida_mcp/http.py +859 -0
- ida_pro_mcp/ida_mcp/port_utils.py +104 -0
- ida_pro_mcp/ida_mcp/rpc.py +187 -0
- ida_pro_mcp/ida_mcp/server_manager.py +339 -0
- ida_pro_mcp/ida_mcp/sync.py +233 -0
- ida_pro_mcp/ida_mcp/tests/__init__.py +14 -0
- ida_pro_mcp/ida_mcp/tests/test_api_analysis.py +336 -0
- ida_pro_mcp/ida_mcp/tests/test_api_core.py +237 -0
- ida_pro_mcp/ida_mcp/tests/test_api_memory.py +207 -0
- ida_pro_mcp/ida_mcp/tests/test_api_modify.py +123 -0
- ida_pro_mcp/ida_mcp/tests/test_api_resources.py +199 -0
- ida_pro_mcp/ida_mcp/tests/test_api_stack.py +77 -0
- ida_pro_mcp/ida_mcp/tests/test_api_types.py +249 -0
- ida_pro_mcp/ida_mcp/ui.py +357 -0
- ida_pro_mcp/ida_mcp/utils.py +1186 -0
- ida_pro_mcp/ida_mcp/zeromcp/__init__.py +5 -0
- ida_pro_mcp/ida_mcp/zeromcp/jsonrpc.py +384 -0
- ida_pro_mcp/ida_mcp/zeromcp/mcp.py +883 -0
- ida_pro_mcp/ida_mcp.py +186 -0
- ida_pro_mcp/idalib_server.py +354 -0
- ida_pro_mcp/idalib_session_manager.py +259 -0
- ida_pro_mcp/server.py +1060 -0
- ida_pro_mcp/test.py +170 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/METADATA +405 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/RECORD +45 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/WHEEL +5 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/entry_points.txt +4 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/licenses/LICENSE +21 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import html
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import ida_netnode
|
|
5
|
+
from urllib.parse import urlparse, parse_qs
|
|
6
|
+
from typing import TypeVar, cast
|
|
7
|
+
from http.server import HTTPServer
|
|
8
|
+
|
|
9
|
+
from .sync import idasync
|
|
10
|
+
from .rpc import (
|
|
11
|
+
McpRpcRegistry,
|
|
12
|
+
McpHttpRequestHandler,
|
|
13
|
+
MCP_SERVER,
|
|
14
|
+
MCP_UNSAFE,
|
|
15
|
+
get_cached_output,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# 工具描述双语翻译 / Tool descriptions bilingual
|
|
23
|
+
TOOL_DESCRIPTIONS = {
|
|
24
|
+
# Core functions
|
|
25
|
+
"idb_meta": ("Get IDB metadata", "获取 IDB 元数据"),
|
|
26
|
+
"lookup_funcs": ("Get functions by address or name", "通过地址或名称获取函数"),
|
|
27
|
+
"cursor_addr": ("Get current cursor address", "获取当前光标地址"),
|
|
28
|
+
"cursor_func": ("Get current function at cursor", "获取光标所在函数"),
|
|
29
|
+
"int_convert": ("Convert numbers between formats", "数字格式转换"),
|
|
30
|
+
"list_funcs": ("List functions with filtering", "列出函数(支持过滤)"),
|
|
31
|
+
"list_globals": ("List global variables", "列出全局变量"),
|
|
32
|
+
"imports": ("List imported symbols", "列出导入符号"),
|
|
33
|
+
"strings": ("List strings in binary", "列出二进制中的字符串"),
|
|
34
|
+
"segments": ("List memory segments", "列出内存段"),
|
|
35
|
+
"local_types": ("List local types", "列出本地类型"),
|
|
36
|
+
"entrypoints": ("Get entry points", "获取入口点"),
|
|
37
|
+
|
|
38
|
+
# Analysis
|
|
39
|
+
"decompile": ("Decompile function to pseudocode", "反编译函数为伪代码"),
|
|
40
|
+
"disasm": ("Disassemble function", "反汇编函数"),
|
|
41
|
+
"xrefs_to": ("Get cross-references to address", "获取到地址的交叉引用"),
|
|
42
|
+
"xrefs_to_field": ("Get xrefs to struct field", "获取到结构体字段的交叉引用"),
|
|
43
|
+
"callees": ("Get functions called by function", "获取函数调用的其他函数"),
|
|
44
|
+
"callers": ("Get functions calling this function", "获取调用此函数的函数"),
|
|
45
|
+
"analyze_funcs": ("Comprehensive function analysis", "全面的函数分析"),
|
|
46
|
+
"find_bytes": ("Search for byte patterns", "搜索字节模式"),
|
|
47
|
+
"find_insns": ("Search for instruction sequences", "搜索指令序列"),
|
|
48
|
+
"basic_blocks": ("Get control flow basic blocks", "获取控制流基本块"),
|
|
49
|
+
"find_paths": ("Find execution paths", "查找执行路径"),
|
|
50
|
+
"search": ("Search for patterns in binary", "在二进制中搜索模式"),
|
|
51
|
+
"find_insn_operands": ("Find instructions with operands", "查找带特定操作数的指令"),
|
|
52
|
+
"callgraph": ("Build call graph", "构建调用图"),
|
|
53
|
+
"xref_matrix": ("Build cross-reference matrix", "构建交叉引用矩阵"),
|
|
54
|
+
"analyze_strings": ("Analyze and filter strings", "分析和过滤字符串"),
|
|
55
|
+
"export_funcs": ("Export function data", "导出函数数据"),
|
|
56
|
+
|
|
57
|
+
# Memory
|
|
58
|
+
"get_bytes": ("Read bytes from memory", "从内存读取字节"),
|
|
59
|
+
"get_u8": ("Read 8-bit unsigned integer", "读取 8 位无符号整数"),
|
|
60
|
+
"get_u16": ("Read 16-bit unsigned integer", "读取 16 位无符号整数"),
|
|
61
|
+
"get_u32": ("Read 32-bit unsigned integer", "读取 32 位无符号整数"),
|
|
62
|
+
"get_u64": ("Read 64-bit unsigned integer", "读取 64 位无符号整数"),
|
|
63
|
+
"get_string": ("Read string from memory", "从内存读取字符串"),
|
|
64
|
+
"get_global_value": ("Read global variable value", "读取全局变量值"),
|
|
65
|
+
"patch": ("Patch bytes at address", "在地址处补丁字节"),
|
|
66
|
+
|
|
67
|
+
# Types
|
|
68
|
+
"declare_type": ("Declare C types", "声明 C 类型"),
|
|
69
|
+
"structs": ("List all structures", "列出所有结构体"),
|
|
70
|
+
"struct_info": ("Get structure info", "获取结构体信息"),
|
|
71
|
+
"read_struct": ("Read struct fields at address", "读取地址处的结构体字段"),
|
|
72
|
+
"search_structs": ("Search structures by name", "按名称搜索结构体"),
|
|
73
|
+
"apply_types": ("Apply types to entities", "应用类型到实体"),
|
|
74
|
+
"infer_types": ("Infer types at address", "推断地址处的类型"),
|
|
75
|
+
|
|
76
|
+
# Modify
|
|
77
|
+
"set_comments": ("Set comments at address", "在地址处设置注释"),
|
|
78
|
+
"patch_asm": ("Patch assembly instructions", "补丁汇编指令"),
|
|
79
|
+
"rename": ("Rename functions/variables", "重命名函数/变量"),
|
|
80
|
+
|
|
81
|
+
# Stack
|
|
82
|
+
"stack_frame": ("Get stack frame variables", "获取栈帧变量"),
|
|
83
|
+
"declare_stack": ("Create stack variable", "创建栈变量"),
|
|
84
|
+
"delete_stack": ("Delete stack variable", "删除栈变量"),
|
|
85
|
+
|
|
86
|
+
# Debug (unsafe)
|
|
87
|
+
"dbg_start": ("⚠️ Start debugger", "⚠️ 启动调试器"),
|
|
88
|
+
"dbg_exit": ("⚠️ Exit debugger", "⚠️ 退出调试器"),
|
|
89
|
+
"dbg_continue": ("⚠️ Continue execution", "⚠️ 继续执行"),
|
|
90
|
+
"dbg_run_to": ("⚠️ Run to address", "⚠️ 运行到地址"),
|
|
91
|
+
"dbg_step_into": ("⚠️ Step into instruction", "⚠️ 步入指令"),
|
|
92
|
+
"dbg_step_over": ("⚠️ Step over instruction", "⚠️ 步过指令"),
|
|
93
|
+
"dbg_list_bps": ("⚠️ List breakpoints", "⚠️ 列出断点"),
|
|
94
|
+
"dbg_add_bp": ("⚠️ Add breakpoint", "⚠️ 添加断点"),
|
|
95
|
+
"dbg_delete_bp": ("⚠️ Delete breakpoint", "⚠️ 删除断点"),
|
|
96
|
+
"dbg_enable_bp": ("⚠️ Enable/disable breakpoint", "⚠️ 启用/禁用断点"),
|
|
97
|
+
"dbg_regs": ("⚠️ Get all registers", "⚠️ 获取所有寄存器"),
|
|
98
|
+
"dbg_regs_thread": ("⚠️ Get thread registers", "⚠️ 获取线程寄存器"),
|
|
99
|
+
"dbg_regs_cur": ("⚠️ Get current thread registers", "⚠️ 获取当前线程寄存器"),
|
|
100
|
+
"dbg_gpregs_thread": ("⚠️ Get GP registers for thread", "⚠️ 获取线程通用寄存器"),
|
|
101
|
+
"dbg_current_gpregs": ("⚠️ Get current GP registers", "⚠️ 获取当前通用寄存器"),
|
|
102
|
+
"dbg_regs_for_thread": ("⚠️ Get specific thread registers", "⚠️ 获取特定线程寄存器"),
|
|
103
|
+
"dbg_current_regs": ("⚠️ Get specific current registers", "⚠️ 获取特定当前寄存器"),
|
|
104
|
+
"dbg_callstack": ("⚠️ Get call stack", "⚠️ 获取调用栈"),
|
|
105
|
+
"dbg_read_mem": ("⚠️ Read debug memory", "⚠️ 读取调试内存"),
|
|
106
|
+
"dbg_write_mem": ("⚠️ Write debug memory", "⚠️ 写入调试内存"),
|
|
107
|
+
|
|
108
|
+
# Python
|
|
109
|
+
"py_eval": ("⚠️ Execute Python code in IDA", "⚠️ 在 IDA 中执行 Python 代码"),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# 国际化文本 / Internationalization texts
|
|
113
|
+
I18N = {
|
|
114
|
+
"en": {
|
|
115
|
+
"title": "IDA Pro MCP Config",
|
|
116
|
+
"server_config": "Server Configuration",
|
|
117
|
+
"host": "Host",
|
|
118
|
+
"host_hint": "0.0.0.0 = all interfaces, 127.0.0.1 = localhost only",
|
|
119
|
+
"port": "Port",
|
|
120
|
+
"enabled_tools": "Enabled Tools",
|
|
121
|
+
"select": "Select",
|
|
122
|
+
"all": "All",
|
|
123
|
+
"none": "None",
|
|
124
|
+
"disable_unsafe": "Disable unsafe",
|
|
125
|
+
"save": "Save",
|
|
126
|
+
"save_restart": "Save & Restart Server",
|
|
127
|
+
"language": "Language",
|
|
128
|
+
"server_will_restart": "Server will restart after saving configuration changes.",
|
|
129
|
+
"current_status": "Current Status",
|
|
130
|
+
"running": "Running",
|
|
131
|
+
"stopped": "Stopped",
|
|
132
|
+
"listening_on": "Listening on",
|
|
133
|
+
"config_saved": "Configuration saved. Server restarting...",
|
|
134
|
+
"auth_config": "Authentication",
|
|
135
|
+
"auth_enabled": "Enable API Key Authentication",
|
|
136
|
+
"api_key": "API Key",
|
|
137
|
+
"api_key_tip": "Leave empty to disable. Use ${ENV_VAR} for environment variable.",
|
|
138
|
+
"tools_count": "tools enabled",
|
|
139
|
+
"unsafe_warning": "⚠️ = Unsafe tool (debugger/code execution)",
|
|
140
|
+
},
|
|
141
|
+
"zh": {
|
|
142
|
+
"title": "IDA Pro MCP 配置",
|
|
143
|
+
"server_config": "服务器配置",
|
|
144
|
+
"host": "监听地址",
|
|
145
|
+
"host_hint": "0.0.0.0 = 所有接口,127.0.0.1 = 仅本地",
|
|
146
|
+
"port": "端口",
|
|
147
|
+
"enabled_tools": "已启用工具",
|
|
148
|
+
"select": "选择",
|
|
149
|
+
"all": "全部",
|
|
150
|
+
"none": "无",
|
|
151
|
+
"disable_unsafe": "禁用不安全工具",
|
|
152
|
+
"save": "保存",
|
|
153
|
+
"save_restart": "保存并重启服务器",
|
|
154
|
+
"language": "语言",
|
|
155
|
+
"server_will_restart": "保存配置更改后服务器将重启。",
|
|
156
|
+
"current_status": "当前状态",
|
|
157
|
+
"running": "运行中",
|
|
158
|
+
"stopped": "已停止",
|
|
159
|
+
"listening_on": "监听地址",
|
|
160
|
+
"config_saved": "配置已保存。服务器正在重启...",
|
|
161
|
+
"auth_config": "认证设置",
|
|
162
|
+
"auth_enabled": "启用 API Key 认证",
|
|
163
|
+
"api_key": "API Key",
|
|
164
|
+
"api_key_tip": "留空禁用。使用 ${环境变量} 引用环境变量。",
|
|
165
|
+
"tools_count": "个工具已启用",
|
|
166
|
+
"unsafe_warning": "⚠️ = 不安全工具(调试器/代码执行)",
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@idasync
|
|
172
|
+
def config_json_get(key: str, default: T) -> T:
|
|
173
|
+
node = ida_netnode.netnode(f"$ ida_mcp.{key}")
|
|
174
|
+
json_blob: bytes | None = node.getblob(0, "C")
|
|
175
|
+
if json_blob is None:
|
|
176
|
+
return default
|
|
177
|
+
try:
|
|
178
|
+
return json.loads(json_blob)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print(
|
|
181
|
+
f"[WARNING] Invalid JSON stored in netnode '{key}': '{json_blob}' from netnode: {e}"
|
|
182
|
+
)
|
|
183
|
+
return default
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@idasync
|
|
187
|
+
def config_json_set(key: str, value):
|
|
188
|
+
node = ida_netnode.netnode(f"$ ida_mcp.{key}", 0, True)
|
|
189
|
+
json_blob = json.dumps(value).encode("utf-8")
|
|
190
|
+
node.setblob(json_blob, 0, "C")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def handle_enabled_tools(registry: McpRpcRegistry, config_key: str):
|
|
194
|
+
"""Changed to registry to enable configured tools, returns original tools."""
|
|
195
|
+
original_tools = registry.methods.copy()
|
|
196
|
+
enabled_tools = config_json_get(
|
|
197
|
+
config_key, {name: True for name in original_tools.keys()}
|
|
198
|
+
)
|
|
199
|
+
new_tools = [name for name in original_tools if name not in enabled_tools]
|
|
200
|
+
|
|
201
|
+
removed_tools = [name for name in enabled_tools if name not in original_tools]
|
|
202
|
+
if removed_tools:
|
|
203
|
+
for name in removed_tools:
|
|
204
|
+
enabled_tools.pop(name)
|
|
205
|
+
|
|
206
|
+
if new_tools:
|
|
207
|
+
enabled_tools.update({name: True for name in new_tools})
|
|
208
|
+
config_json_set(config_key, enabled_tools)
|
|
209
|
+
|
|
210
|
+
registry.methods = {
|
|
211
|
+
name: func for name, func in original_tools.items() if enabled_tools.get(name)
|
|
212
|
+
}
|
|
213
|
+
return original_tools
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
217
|
+
DEFAULT_PORT = 13337
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_server_config() -> dict:
|
|
221
|
+
"""Get server configuration from IDA database."""
|
|
222
|
+
return config_json_get(
|
|
223
|
+
"server_config",
|
|
224
|
+
{
|
|
225
|
+
"host": DEFAULT_HOST,
|
|
226
|
+
"port": DEFAULT_PORT,
|
|
227
|
+
"auth_enabled": False,
|
|
228
|
+
"api_key": None,
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def set_server_config(config: dict):
|
|
234
|
+
"""Save server configuration to IDA database."""
|
|
235
|
+
config_json_set("server_config", config)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_language() -> str:
|
|
239
|
+
"""Get current language setting."""
|
|
240
|
+
return config_json_get("language", "en")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def set_language(lang: str):
|
|
244
|
+
"""Set language preference."""
|
|
245
|
+
if lang in I18N:
|
|
246
|
+
config_json_set("language", lang)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def t(key: str, lang: str = None) -> str:
|
|
250
|
+
"""Get translated text for the given key."""
|
|
251
|
+
if lang is None:
|
|
252
|
+
lang = get_language()
|
|
253
|
+
return I18N.get(lang, I18N["en"]).get(key, key)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_tool_description(name: str, lang: str) -> str:
|
|
257
|
+
"""Get tool description in specified language."""
|
|
258
|
+
if name in TOOL_DESCRIPTIONS:
|
|
259
|
+
en_desc, zh_desc = TOOL_DESCRIPTIONS[name]
|
|
260
|
+
return zh_desc if lang == "zh" else en_desc
|
|
261
|
+
return name
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
ORIGINAL_TOOLS = handle_enabled_tools(MCP_SERVER.tools, "enabled_tools")
|
|
265
|
+
|
|
266
|
+
# Global reference to trigger server restart
|
|
267
|
+
_server_restart_callback = None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def set_server_restart_callback(callback):
|
|
271
|
+
"""Set callback function to restart the server."""
|
|
272
|
+
global _server_restart_callback
|
|
273
|
+
_server_restart_callback = callback
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class IdaMcpHttpRequestHandler(McpHttpRequestHandler):
|
|
277
|
+
def __init__(self, request, client_address, server):
|
|
278
|
+
super().__init__(request, client_address, server)
|
|
279
|
+
|
|
280
|
+
def do_POST(self):
|
|
281
|
+
"""Handles POST requests."""
|
|
282
|
+
if urlparse(self.path).path == "/config":
|
|
283
|
+
if not self._check_origin():
|
|
284
|
+
return
|
|
285
|
+
self._handle_config_post()
|
|
286
|
+
else:
|
|
287
|
+
super().do_POST()
|
|
288
|
+
|
|
289
|
+
def do_GET(self):
|
|
290
|
+
"""Handles GET requests."""
|
|
291
|
+
parsed = urlparse(self.path)
|
|
292
|
+
path = parsed.path
|
|
293
|
+
|
|
294
|
+
if path == "/config.html":
|
|
295
|
+
if not self._check_host():
|
|
296
|
+
return
|
|
297
|
+
self._handle_config_get()
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
# Handle output download requests
|
|
301
|
+
output_match = re.match(r"^/output/([a-f0-9-]+)\.(\w+)$", path)
|
|
302
|
+
if output_match:
|
|
303
|
+
self._handle_output_download(output_match.group(1), output_match.group(2))
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
super().do_GET()
|
|
307
|
+
|
|
308
|
+
def _handle_output_download(self, output_id: str, extension: str):
|
|
309
|
+
"""Handle download of cached output data."""
|
|
310
|
+
data = get_cached_output(output_id)
|
|
311
|
+
if data is None:
|
|
312
|
+
self.send_error(404, "Output not found or expired")
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
if extension == "json":
|
|
316
|
+
content = json.dumps(data, indent=2)
|
|
317
|
+
elif isinstance(data, dict) and "code" in data:
|
|
318
|
+
content = str(data["code"])
|
|
319
|
+
elif isinstance(data, list) and data and isinstance(data[0], dict):
|
|
320
|
+
content = "\n\n".join(
|
|
321
|
+
str(item.get("code", item.get("asm", item.get("lines", ""))))
|
|
322
|
+
for item in data
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
content = json.dumps(data, indent=2)
|
|
326
|
+
|
|
327
|
+
body = content.encode("utf-8")
|
|
328
|
+
self.send_response(200)
|
|
329
|
+
content_type = "application/json" if extension == "json" else "text/plain"
|
|
330
|
+
self.send_header("Content-Type", f"{content_type}; charset=utf-8")
|
|
331
|
+
self.send_header("Content-Length", str(len(body)))
|
|
332
|
+
self.send_header(
|
|
333
|
+
"Content-Disposition", f'attachment; filename="{output_id}.{extension}"'
|
|
334
|
+
)
|
|
335
|
+
self.end_headers()
|
|
336
|
+
self.wfile.write(body)
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def server_port(self) -> int:
|
|
340
|
+
return cast(HTTPServer, self.server).server_port
|
|
341
|
+
|
|
342
|
+
def _check_origin(self) -> bool:
|
|
343
|
+
"""
|
|
344
|
+
Prevents CSRF and DNS rebinding attacks by ensuring POST requests
|
|
345
|
+
originate from pages served by this server, not external websites.
|
|
346
|
+
"""
|
|
347
|
+
origin = self.headers.get("Origin")
|
|
348
|
+
port = self.server_port
|
|
349
|
+
if origin not in (f"http://127.0.0.1:{port}", f"http://localhost:{port}"):
|
|
350
|
+
self.send_error(403, "Invalid Origin")
|
|
351
|
+
return False
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
def _check_host(self) -> bool:
|
|
355
|
+
"""
|
|
356
|
+
Prevents DNS rebinding attacks where an attacker's domain (e.g., evil.com)
|
|
357
|
+
resolves to 127.0.0.1, allowing their page to read localhost resources.
|
|
358
|
+
"""
|
|
359
|
+
host = self.headers.get("Host")
|
|
360
|
+
port = self.server_port
|
|
361
|
+
if host not in (f"127.0.0.1:{port}", f"localhost:{port}"):
|
|
362
|
+
self.send_error(403, "Invalid Host")
|
|
363
|
+
return False
|
|
364
|
+
return True
|
|
365
|
+
|
|
366
|
+
def _send_html(self, status: int, text: str):
|
|
367
|
+
"""
|
|
368
|
+
Prevents clickjacking by blocking iframes (X-Frame-Options for older
|
|
369
|
+
browsers, frame-ancestors for modern ones). Other CSP directives
|
|
370
|
+
provide defense-in-depth against content injection attacks.
|
|
371
|
+
"""
|
|
372
|
+
body = text.encode("utf-8")
|
|
373
|
+
self.send_response(status)
|
|
374
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
375
|
+
self.send_header("Content-Length", str(len(body)))
|
|
376
|
+
self.send_header("X-Frame-Options", "DENY")
|
|
377
|
+
self.send_header(
|
|
378
|
+
"Content-Security-Policy",
|
|
379
|
+
"; ".join(
|
|
380
|
+
[
|
|
381
|
+
"frame-ancestors 'none'",
|
|
382
|
+
"script-src 'self' 'unsafe-inline'",
|
|
383
|
+
"style-src 'self' 'unsafe-inline'",
|
|
384
|
+
"default-src 'self'",
|
|
385
|
+
"form-action 'self'",
|
|
386
|
+
]
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
self.end_headers()
|
|
390
|
+
self.wfile.write(body)
|
|
391
|
+
|
|
392
|
+
def _handle_config_get(self):
|
|
393
|
+
"""Sends the configuration page with checkboxes."""
|
|
394
|
+
# Get current settings
|
|
395
|
+
server_config = get_server_config()
|
|
396
|
+
lang = get_language()
|
|
397
|
+
|
|
398
|
+
# Get query parameter for language switch
|
|
399
|
+
parsed = urlparse(self.path)
|
|
400
|
+
query = parse_qs(parsed.query)
|
|
401
|
+
if "lang" in query:
|
|
402
|
+
new_lang = query["lang"][0]
|
|
403
|
+
if new_lang in I18N:
|
|
404
|
+
set_language(new_lang)
|
|
405
|
+
lang = new_lang
|
|
406
|
+
|
|
407
|
+
# Count enabled tools
|
|
408
|
+
enabled_count = len(self.mcp_server.tools.methods)
|
|
409
|
+
total_count = len(ORIGINAL_TOOLS)
|
|
410
|
+
|
|
411
|
+
# Build HTML
|
|
412
|
+
body = f"""<!DOCTYPE html>
|
|
413
|
+
<html lang="{lang}">
|
|
414
|
+
<head>
|
|
415
|
+
<meta charset="UTF-8">
|
|
416
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
417
|
+
<title>{t("title", lang)}</title>
|
|
418
|
+
<style>
|
|
419
|
+
:root {{
|
|
420
|
+
--bg: #ffffff;
|
|
421
|
+
--text: #1a1a1a;
|
|
422
|
+
--border: #e0e0e0;
|
|
423
|
+
--accent: #0066cc;
|
|
424
|
+
--hover: #f5f5f5;
|
|
425
|
+
--success: #28a745;
|
|
426
|
+
--warning: #ffc107;
|
|
427
|
+
--card-bg: #f8f9fa;
|
|
428
|
+
}}
|
|
429
|
+
|
|
430
|
+
@media (prefers-color-scheme: dark) {{
|
|
431
|
+
:root {{
|
|
432
|
+
--bg: #1a1a1a;
|
|
433
|
+
--text: #e0e0e0;
|
|
434
|
+
--border: #333333;
|
|
435
|
+
--accent: #4da6ff;
|
|
436
|
+
--hover: #2a2a2a;
|
|
437
|
+
--success: #48bb78;
|
|
438
|
+
--warning: #ecc94b;
|
|
439
|
+
--card-bg: #242424;
|
|
440
|
+
}}
|
|
441
|
+
}}
|
|
442
|
+
|
|
443
|
+
* {{
|
|
444
|
+
box-sizing: border-box;
|
|
445
|
+
}}
|
|
446
|
+
|
|
447
|
+
body {{
|
|
448
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
449
|
+
background: var(--bg);
|
|
450
|
+
color: var(--text);
|
|
451
|
+
max-width: 900px;
|
|
452
|
+
margin: 2rem auto;
|
|
453
|
+
padding: 1rem;
|
|
454
|
+
line-height: 1.5;
|
|
455
|
+
}}
|
|
456
|
+
|
|
457
|
+
h1 {{
|
|
458
|
+
font-size: 1.5rem;
|
|
459
|
+
margin-bottom: 0.5rem;
|
|
460
|
+
display: flex;
|
|
461
|
+
justify-content: space-between;
|
|
462
|
+
align-items: center;
|
|
463
|
+
}}
|
|
464
|
+
|
|
465
|
+
h2 {{
|
|
466
|
+
font-size: 1.1rem;
|
|
467
|
+
margin-top: 1.5rem;
|
|
468
|
+
margin-bottom: 0.75rem;
|
|
469
|
+
padding-bottom: 0.25rem;
|
|
470
|
+
border-bottom: 1px solid var(--border);
|
|
471
|
+
}}
|
|
472
|
+
|
|
473
|
+
.lang-switch {{
|
|
474
|
+
font-size: 0.9rem;
|
|
475
|
+
font-weight: normal;
|
|
476
|
+
}}
|
|
477
|
+
|
|
478
|
+
.lang-switch a {{
|
|
479
|
+
color: var(--accent);
|
|
480
|
+
text-decoration: none;
|
|
481
|
+
padding: 0.25rem 0.5rem;
|
|
482
|
+
border-radius: 4px;
|
|
483
|
+
}}
|
|
484
|
+
|
|
485
|
+
.lang-switch a:hover {{
|
|
486
|
+
background: var(--hover);
|
|
487
|
+
}}
|
|
488
|
+
|
|
489
|
+
.lang-switch a.active {{
|
|
490
|
+
background: var(--accent);
|
|
491
|
+
color: white;
|
|
492
|
+
}}
|
|
493
|
+
|
|
494
|
+
.card {{
|
|
495
|
+
background: var(--card-bg);
|
|
496
|
+
border: 1px solid var(--border);
|
|
497
|
+
border-radius: 8px;
|
|
498
|
+
padding: 1rem;
|
|
499
|
+
margin-bottom: 1rem;
|
|
500
|
+
}}
|
|
501
|
+
|
|
502
|
+
.status-bar {{
|
|
503
|
+
display: flex;
|
|
504
|
+
gap: 1rem;
|
|
505
|
+
align-items: center;
|
|
506
|
+
flex-wrap: wrap;
|
|
507
|
+
padding: 0.75rem 1rem;
|
|
508
|
+
background: var(--card-bg);
|
|
509
|
+
border-radius: 8px;
|
|
510
|
+
margin-bottom: 1rem;
|
|
511
|
+
border: 1px solid var(--border);
|
|
512
|
+
}}
|
|
513
|
+
|
|
514
|
+
.status-indicator {{
|
|
515
|
+
display: inline-block;
|
|
516
|
+
width: 10px;
|
|
517
|
+
height: 10px;
|
|
518
|
+
border-radius: 50%;
|
|
519
|
+
margin-right: 0.5rem;
|
|
520
|
+
background: var(--success);
|
|
521
|
+
box-shadow: 0 0 6px var(--success);
|
|
522
|
+
}}
|
|
523
|
+
|
|
524
|
+
.form-group {{
|
|
525
|
+
margin-bottom: 1rem;
|
|
526
|
+
}}
|
|
527
|
+
|
|
528
|
+
.form-row {{
|
|
529
|
+
display: grid;
|
|
530
|
+
grid-template-columns: 1fr 1fr;
|
|
531
|
+
gap: 1rem;
|
|
532
|
+
}}
|
|
533
|
+
|
|
534
|
+
@media (max-width: 600px) {{
|
|
535
|
+
.form-row {{
|
|
536
|
+
grid-template-columns: 1fr;
|
|
537
|
+
}}
|
|
538
|
+
}}
|
|
539
|
+
|
|
540
|
+
label {{
|
|
541
|
+
display: block;
|
|
542
|
+
padding: 0.25rem 0.5rem;
|
|
543
|
+
border-radius: 4px;
|
|
544
|
+
cursor: pointer;
|
|
545
|
+
}}
|
|
546
|
+
|
|
547
|
+
label:hover {{
|
|
548
|
+
background: var(--hover);
|
|
549
|
+
}}
|
|
550
|
+
|
|
551
|
+
label.form-label {{
|
|
552
|
+
font-weight: 500;
|
|
553
|
+
margin-bottom: 0.25rem;
|
|
554
|
+
padding: 0;
|
|
555
|
+
}}
|
|
556
|
+
|
|
557
|
+
label.form-label:hover {{
|
|
558
|
+
background: transparent;
|
|
559
|
+
}}
|
|
560
|
+
|
|
561
|
+
input[type="text"],
|
|
562
|
+
input[type="number"] {{
|
|
563
|
+
width: 100%;
|
|
564
|
+
padding: 0.5rem;
|
|
565
|
+
border: 1px solid var(--border);
|
|
566
|
+
border-radius: 4px;
|
|
567
|
+
background: var(--bg);
|
|
568
|
+
color: var(--text);
|
|
569
|
+
font-size: 1rem;
|
|
570
|
+
}}
|
|
571
|
+
|
|
572
|
+
input[type="text"]:focus,
|
|
573
|
+
input[type="number"]:focus {{
|
|
574
|
+
outline: none;
|
|
575
|
+
border-color: var(--accent);
|
|
576
|
+
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
|
|
577
|
+
}}
|
|
578
|
+
|
|
579
|
+
input[type="checkbox"] {{
|
|
580
|
+
margin-right: 0.5rem;
|
|
581
|
+
accent-color: var(--accent);
|
|
582
|
+
}}
|
|
583
|
+
|
|
584
|
+
.btn {{
|
|
585
|
+
padding: 0.6rem 1.5rem;
|
|
586
|
+
border: none;
|
|
587
|
+
border-radius: 4px;
|
|
588
|
+
cursor: pointer;
|
|
589
|
+
font-size: 1rem;
|
|
590
|
+
margin-right: 0.5rem;
|
|
591
|
+
margin-top: 0.5rem;
|
|
592
|
+
}}
|
|
593
|
+
|
|
594
|
+
.btn-success {{
|
|
595
|
+
background: var(--success);
|
|
596
|
+
color: white;
|
|
597
|
+
}}
|
|
598
|
+
|
|
599
|
+
.btn-success:hover {{
|
|
600
|
+
opacity: 0.9;
|
|
601
|
+
}}
|
|
602
|
+
|
|
603
|
+
.hint {{
|
|
604
|
+
font-size: 0.85rem;
|
|
605
|
+
color: #666;
|
|
606
|
+
margin-top: 0.25rem;
|
|
607
|
+
}}
|
|
608
|
+
|
|
609
|
+
@media (prefers-color-scheme: dark) {{
|
|
610
|
+
.hint {{
|
|
611
|
+
color: #999;
|
|
612
|
+
}}
|
|
613
|
+
}}
|
|
614
|
+
|
|
615
|
+
.tools-container {{
|
|
616
|
+
max-height: 500px;
|
|
617
|
+
overflow-y: auto;
|
|
618
|
+
border: 1px solid var(--border);
|
|
619
|
+
border-radius: 4px;
|
|
620
|
+
padding: 0.5rem;
|
|
621
|
+
}}
|
|
622
|
+
|
|
623
|
+
.tool-item {{
|
|
624
|
+
display: flex;
|
|
625
|
+
align-items: flex-start;
|
|
626
|
+
padding: 0.4rem 0.5rem;
|
|
627
|
+
border-radius: 4px;
|
|
628
|
+
}}
|
|
629
|
+
|
|
630
|
+
.tool-item:hover {{
|
|
631
|
+
background: var(--hover);
|
|
632
|
+
}}
|
|
633
|
+
|
|
634
|
+
.tool-name {{
|
|
635
|
+
font-family: monospace;
|
|
636
|
+
font-weight: 500;
|
|
637
|
+
min-width: 180px;
|
|
638
|
+
}}
|
|
639
|
+
|
|
640
|
+
.tool-desc {{
|
|
641
|
+
color: #666;
|
|
642
|
+
font-size: 0.9rem;
|
|
643
|
+
}}
|
|
644
|
+
|
|
645
|
+
@media (prefers-color-scheme: dark) {{
|
|
646
|
+
.tool-desc {{
|
|
647
|
+
color: #999;
|
|
648
|
+
}}
|
|
649
|
+
}}
|
|
650
|
+
|
|
651
|
+
.quick-select {{
|
|
652
|
+
font-size: 0.9rem;
|
|
653
|
+
margin: 0.5rem 0;
|
|
654
|
+
display: flex;
|
|
655
|
+
gap: 0.5rem;
|
|
656
|
+
align-items: center;
|
|
657
|
+
}}
|
|
658
|
+
|
|
659
|
+
.quick-select a {{
|
|
660
|
+
color: var(--accent);
|
|
661
|
+
text-decoration: none;
|
|
662
|
+
}}
|
|
663
|
+
|
|
664
|
+
.quick-select a:hover {{
|
|
665
|
+
text-decoration: underline;
|
|
666
|
+
}}
|
|
667
|
+
|
|
668
|
+
.notice {{
|
|
669
|
+
padding: 0.75rem 1rem;
|
|
670
|
+
background: rgba(255, 193, 7, 0.1);
|
|
671
|
+
border: 1px solid var(--warning);
|
|
672
|
+
border-radius: 4px;
|
|
673
|
+
margin-bottom: 1rem;
|
|
674
|
+
font-size: 0.9rem;
|
|
675
|
+
}}
|
|
676
|
+
|
|
677
|
+
.tools-header {{
|
|
678
|
+
display: flex;
|
|
679
|
+
justify-content: space-between;
|
|
680
|
+
align-items: center;
|
|
681
|
+
}}
|
|
682
|
+
|
|
683
|
+
.tools-count {{
|
|
684
|
+
font-size: 0.9rem;
|
|
685
|
+
color: #666;
|
|
686
|
+
}}
|
|
687
|
+
</style>
|
|
688
|
+
<script defer>
|
|
689
|
+
function setTools(mode) {{
|
|
690
|
+
document.querySelectorAll('input[data-tool]').forEach(cb => {{
|
|
691
|
+
if (mode === 'all') cb.checked = true;
|
|
692
|
+
else if (mode === 'none') cb.checked = false;
|
|
693
|
+
else if (mode === 'disable-unsafe' && cb.hasAttribute('data-unsafe')) cb.checked = false;
|
|
694
|
+
}});
|
|
695
|
+
updateCount();
|
|
696
|
+
}}
|
|
697
|
+
function updateCount() {{
|
|
698
|
+
const checked = document.querySelectorAll('input[data-tool]:checked').length;
|
|
699
|
+
const total = document.querySelectorAll('input[data-tool]').length;
|
|
700
|
+
document.getElementById('tools-count').textContent = checked + '/' + total;
|
|
701
|
+
}}
|
|
702
|
+
</script>
|
|
703
|
+
</head>
|
|
704
|
+
<body>
|
|
705
|
+
<h1>
|
|
706
|
+
{t("title", lang)}
|
|
707
|
+
<span class="lang-switch">
|
|
708
|
+
<a href="?lang=en" class="{'active' if lang == 'en' else ''}">EN</a>
|
|
709
|
+
<a href="?lang=zh" class="{'active' if lang == 'zh' else ''}">中文</a>
|
|
710
|
+
</span>
|
|
711
|
+
</h1>
|
|
712
|
+
|
|
713
|
+
<div class="status-bar">
|
|
714
|
+
<span>
|
|
715
|
+
<span class="status-indicator"></span>
|
|
716
|
+
<strong>{t("current_status", lang)}:</strong> {t("running", lang)}
|
|
717
|
+
</span>
|
|
718
|
+
<span>
|
|
719
|
+
<strong>{t("listening_on", lang)}:</strong> {server_config.get("host", DEFAULT_HOST)}:{server_config.get("port", DEFAULT_PORT)}
|
|
720
|
+
</span>
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<form method="post" action="/config">
|
|
724
|
+
|
|
725
|
+
<div class="notice">
|
|
726
|
+
⚠️ {t("server_will_restart", lang)}
|
|
727
|
+
</div>
|
|
728
|
+
|
|
729
|
+
<h2>{t("server_config", lang)}</h2>
|
|
730
|
+
<div class="card">
|
|
731
|
+
<div class="form-row">
|
|
732
|
+
<div class="form-group">
|
|
733
|
+
<label class="form-label">{t("host", lang)}</label>
|
|
734
|
+
<input type="text" name="host" value="{html.escape(str(server_config.get('host', DEFAULT_HOST)))}" placeholder="127.0.0.1">
|
|
735
|
+
<div class="hint">{t("host_hint", lang)}</div>
|
|
736
|
+
</div>
|
|
737
|
+
<div class="form-group">
|
|
738
|
+
<label class="form-label">{t("port", lang)}</label>
|
|
739
|
+
<input type="number" name="port" value="{server_config.get('port', DEFAULT_PORT)}" min="1" max="65535">
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
</div>
|
|
743
|
+
|
|
744
|
+
<h2>{t("auth_config", lang)}</h2>
|
|
745
|
+
<div class="card">
|
|
746
|
+
<div class="form-group">
|
|
747
|
+
<label>
|
|
748
|
+
<input type="checkbox" name="auth_enabled" value="1" {'checked' if server_config.get('auth_enabled') else ''}>
|
|
749
|
+
{t("auth_enabled", lang)}
|
|
750
|
+
</label>
|
|
751
|
+
</div>
|
|
752
|
+
<div class="form-group">
|
|
753
|
+
<label class="form-label">{t("api_key", lang)}</label>
|
|
754
|
+
<input type="text" name="api_key" value="{html.escape(str(server_config.get('api_key') or ''))}" placeholder="${{IDA_MCP_API_KEY}}">
|
|
755
|
+
<div class="hint">{t("api_key_tip", lang)}</div>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
|
|
759
|
+
<div class="tools-header">
|
|
760
|
+
<h2>{t("enabled_tools", lang)}</h2>
|
|
761
|
+
<span class="tools-count"><span id="tools-count">{enabled_count}/{total_count}</span> {t("tools_count", lang)}</span>
|
|
762
|
+
</div>
|
|
763
|
+
|
|
764
|
+
<div class="quick-select">
|
|
765
|
+
{t("select", lang)}:
|
|
766
|
+
<a href="#" onclick="setTools('all'); return false;">{t("all", lang)}</a> ·
|
|
767
|
+
<a href="#" onclick="setTools('none'); return false;">{t("none", lang)}</a> ·
|
|
768
|
+
<a href="#" onclick="setTools('disable-unsafe'); return false;">{t("disable_unsafe", lang)}</a>
|
|
769
|
+
<span style="margin-left: 1rem; color: #666; font-size: 0.85rem;">{t("unsafe_warning", lang)}</span>
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
<div class="tools-container">
|
|
773
|
+
"""
|
|
774
|
+
for name, func in ORIGINAL_TOOLS.items():
|
|
775
|
+
checked = " checked" if name in self.mcp_server.tools.methods else ""
|
|
776
|
+
unsafe_attr = " data-unsafe" if name in MCP_UNSAFE else ""
|
|
777
|
+
description = get_tool_description(name, lang)
|
|
778
|
+
|
|
779
|
+
body += f"""<label class="tool-item">
|
|
780
|
+
<input type="checkbox" name="{html.escape(name)}" value="{html.escape(name)}"{checked}{unsafe_attr} data-tool onchange="updateCount()">
|
|
781
|
+
<span class="tool-name">{html.escape(name)}</span>
|
|
782
|
+
<span class="tool-desc">{html.escape(description)}</span>
|
|
783
|
+
</label>
|
|
784
|
+
"""
|
|
785
|
+
body += "</div>"
|
|
786
|
+
|
|
787
|
+
body += f"""
|
|
788
|
+
<div style="margin-top: 1.5rem;">
|
|
789
|
+
<button type="submit" class="btn btn-success">{t("save_restart", lang)}</button>
|
|
790
|
+
</div>
|
|
791
|
+
|
|
792
|
+
</form>
|
|
793
|
+
</body>
|
|
794
|
+
</html>"""
|
|
795
|
+
self._send_html(200, body)
|
|
796
|
+
|
|
797
|
+
def _handle_config_post(self):
|
|
798
|
+
"""Handles the configuration form submission."""
|
|
799
|
+
# Validate Content-Type
|
|
800
|
+
content_type = self.headers.get("content-type", "").split(";")[0].strip()
|
|
801
|
+
if content_type != "application/x-www-form-urlencoded":
|
|
802
|
+
self.send_error(400, f"Unsupported Content-Type: {content_type}")
|
|
803
|
+
return
|
|
804
|
+
|
|
805
|
+
# Parse the form data
|
|
806
|
+
length = int(self.headers.get("content-length", "0"))
|
|
807
|
+
postvars = parse_qs(self.rfile.read(length).decode("utf-8"))
|
|
808
|
+
|
|
809
|
+
# Update server configuration
|
|
810
|
+
new_host = postvars.get("host", [DEFAULT_HOST])[0].strip() or DEFAULT_HOST
|
|
811
|
+
try:
|
|
812
|
+
new_port = int(postvars.get("port", [DEFAULT_PORT])[0])
|
|
813
|
+
if not (1 <= new_port <= 65535):
|
|
814
|
+
new_port = DEFAULT_PORT
|
|
815
|
+
except (ValueError, TypeError):
|
|
816
|
+
new_port = DEFAULT_PORT
|
|
817
|
+
|
|
818
|
+
auth_enabled = "auth_enabled" in postvars
|
|
819
|
+
api_key = postvars.get("api_key", [None])[0]
|
|
820
|
+
if api_key:
|
|
821
|
+
api_key = api_key.strip() or None
|
|
822
|
+
|
|
823
|
+
server_config = {
|
|
824
|
+
"host": new_host,
|
|
825
|
+
"port": new_port,
|
|
826
|
+
"auth_enabled": auth_enabled,
|
|
827
|
+
"api_key": api_key,
|
|
828
|
+
}
|
|
829
|
+
set_server_config(server_config)
|
|
830
|
+
|
|
831
|
+
# Update the server's tools
|
|
832
|
+
enabled_tools = {name: name in postvars for name in ORIGINAL_TOOLS.keys()}
|
|
833
|
+
self.mcp_server.tools.methods = {
|
|
834
|
+
name: func
|
|
835
|
+
for name, func in ORIGINAL_TOOLS.items()
|
|
836
|
+
if enabled_tools.get(name)
|
|
837
|
+
}
|
|
838
|
+
config_json_set("enabled_tools", enabled_tools)
|
|
839
|
+
|
|
840
|
+
# Trigger server restart if callback is set
|
|
841
|
+
if _server_restart_callback:
|
|
842
|
+
try:
|
|
843
|
+
# Schedule restart after response is sent
|
|
844
|
+
import threading
|
|
845
|
+
|
|
846
|
+
def delayed_restart():
|
|
847
|
+
import time
|
|
848
|
+
|
|
849
|
+
time.sleep(0.5) # Wait for response to be sent
|
|
850
|
+
_server_restart_callback(new_host, new_port)
|
|
851
|
+
|
|
852
|
+
threading.Thread(target=delayed_restart, daemon=True).start()
|
|
853
|
+
except Exception as e:
|
|
854
|
+
print(f"[MCP] Failed to schedule server restart: {e}")
|
|
855
|
+
|
|
856
|
+
# Redirect back to the config page (will use new port after restart)
|
|
857
|
+
self.send_response(302)
|
|
858
|
+
self.send_header("Location", f"http://{new_host}:{new_port}/config.html")
|
|
859
|
+
self.end_headers()
|