cheatengine 5.8.13 → 5.8.15
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/bin/cheatengine +30 -40
- package/ce_mcp_server.js +367 -0
- package/package.json +9 -7
- package/src/base.js +235 -0
- package/src/pipe-client.js +316 -0
- package/src/tool-registry.js +887 -0
- package/ce_mcp_server.py +0 -2509
package/ce_mcp_server.py
DELETED
|
@@ -1,2509 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""Cheat Engine MCP Server - Named pipe bridge to Cheat Engine"""
|
|
4
|
-
|
|
5
|
-
import concurrent.futures
|
|
6
|
-
import io
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
import struct
|
|
10
|
-
import sys
|
|
11
|
-
import threading
|
|
12
|
-
import time
|
|
13
|
-
import traceback
|
|
14
|
-
from dataclasses import dataclass, field
|
|
15
|
-
from enum import Enum
|
|
16
|
-
from typing import Any, Dict, List, Optional
|
|
17
|
-
|
|
18
|
-
try:
|
|
19
|
-
import pywintypes
|
|
20
|
-
import win32file
|
|
21
|
-
import win32pipe
|
|
22
|
-
import winerror
|
|
23
|
-
except ImportError:
|
|
24
|
-
print(
|
|
25
|
-
"Error: 'pywin32' module is missing. Please run: pip install pywin32",
|
|
26
|
-
file=sys.stderr,
|
|
27
|
-
)
|
|
28
|
-
sys.exit(1)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# ============ Timeout Error ============
|
|
32
|
-
class TimeoutError(Exception):
|
|
33
|
-
"""Tool execution timeout exception"""
|
|
34
|
-
|
|
35
|
-
def __init__(self, tool_name: str, params: dict, elapsed_time: float, timeout: int):
|
|
36
|
-
self.tool_name = tool_name
|
|
37
|
-
self.params = params
|
|
38
|
-
self.elapsed_time = elapsed_time
|
|
39
|
-
self.timeout = timeout
|
|
40
|
-
super().__init__(
|
|
41
|
-
f"Tool '{tool_name}' timed out after {elapsed_time:.2f}s (timeout: {timeout}s)"
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# ============ Connection Health Monitoring ============
|
|
46
|
-
@dataclass
|
|
47
|
-
class ConnectionHealth:
|
|
48
|
-
"""Connection health status"""
|
|
49
|
-
|
|
50
|
-
last_success_time: float = 0.0
|
|
51
|
-
connection_attempts: int = 0
|
|
52
|
-
consecutive_errors: int = 0
|
|
53
|
-
total_errors: int = 0
|
|
54
|
-
last_error: Optional[str] = None
|
|
55
|
-
is_connected: bool = False
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class HealthMonitor:
|
|
59
|
-
"""Connection health monitor
|
|
60
|
-
|
|
61
|
-
Tracks connection state, records success/failure, triggers reconnect on consecutive errors.
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
def __init__(self, error_threshold: int = 5):
|
|
65
|
-
"""Initialize health monitor
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
error_threshold: Consecutive error threshold for reconnect (default: 5)
|
|
69
|
-
"""
|
|
70
|
-
self.health = ConnectionHealth()
|
|
71
|
-
self.error_threshold = error_threshold
|
|
72
|
-
|
|
73
|
-
def record_success(self) -> None:
|
|
74
|
-
"""Record successful communication"""
|
|
75
|
-
self.health.last_success_time = time.time()
|
|
76
|
-
self.health.consecutive_errors = 0
|
|
77
|
-
self.health.is_connected = True
|
|
78
|
-
|
|
79
|
-
def record_error(self, error: str) -> bool:
|
|
80
|
-
"""Record communication error
|
|
81
|
-
|
|
82
|
-
Args:
|
|
83
|
-
error: Error message
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
bool: Whether reconnect is needed (consecutive_errors >= error_threshold)
|
|
87
|
-
"""
|
|
88
|
-
self.health.consecutive_errors += 1
|
|
89
|
-
self.health.total_errors += 1
|
|
90
|
-
self.health.last_error = error
|
|
91
|
-
self.health.is_connected = False
|
|
92
|
-
return self.health.consecutive_errors >= self.error_threshold
|
|
93
|
-
|
|
94
|
-
def record_connection_attempt(self) -> None:
|
|
95
|
-
"""Record connection attempt"""
|
|
96
|
-
self.health.connection_attempts += 1
|
|
97
|
-
|
|
98
|
-
def get_metrics(self) -> dict:
|
|
99
|
-
"""Get health metrics"""
|
|
100
|
-
return {
|
|
101
|
-
"last_success_time": self.health.last_success_time,
|
|
102
|
-
"connection_attempts": self.health.connection_attempts,
|
|
103
|
-
"consecutive_errors": self.health.consecutive_errors,
|
|
104
|
-
"total_errors": self.health.total_errors,
|
|
105
|
-
"last_error": self.health.last_error,
|
|
106
|
-
"is_connected": self.health.is_connected,
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
def reset(self) -> None:
|
|
110
|
-
"""Reset health state"""
|
|
111
|
-
self.health = ConnectionHealth()
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
# ============ Timeout Manager ============
|
|
115
|
-
class TimeoutManager:
|
|
116
|
-
"""Tool execution timeout manager
|
|
117
|
-
|
|
118
|
-
Manages timeout configuration for different tools.
|
|
119
|
-
|
|
120
|
-
**Validates: Requirements 2.1, 2.4**
|
|
121
|
-
"""
|
|
122
|
-
|
|
123
|
-
# Default timeout (seconds)
|
|
124
|
-
DEFAULT_TIMEOUT = 30
|
|
125
|
-
|
|
126
|
-
# Tool-specific timeouts (seconds) for long-running operations
|
|
127
|
-
TOOL_TIMEOUTS = {
|
|
128
|
-
"ce_find_pointer_path": 60,
|
|
129
|
-
"ce_break_and_trace": 60,
|
|
130
|
-
"ce_find_what_accesses": 30,
|
|
131
|
-
"ce_find_what_writes": 30,
|
|
132
|
-
"ce_aob_scan": 30,
|
|
133
|
-
"ce_value_scan": 30,
|
|
134
|
-
"ce_scan_new": 30,
|
|
135
|
-
"ce_scan_next": 30,
|
|
136
|
-
"ce_build_cfg": 45,
|
|
137
|
-
"ce_symbolic_trace": 45,
|
|
138
|
-
"ce_call_function": 30,
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
def __init__(self, default_timeout: int = None, tool_timeouts: dict = None):
|
|
142
|
-
"""Initialize timeout manager
|
|
143
|
-
|
|
144
|
-
Args:
|
|
145
|
-
default_timeout: Custom default timeout (seconds), None uses DEFAULT_TIMEOUT
|
|
146
|
-
tool_timeouts: Custom tool timeouts, merged with defaults
|
|
147
|
-
"""
|
|
148
|
-
self.default_timeout = (
|
|
149
|
-
default_timeout if default_timeout is not None else self.DEFAULT_TIMEOUT
|
|
150
|
-
)
|
|
151
|
-
self.tool_timeouts = dict(self.TOOL_TIMEOUTS)
|
|
152
|
-
if tool_timeouts:
|
|
153
|
-
self.tool_timeouts.update(tool_timeouts)
|
|
154
|
-
|
|
155
|
-
def get_timeout(self, tool_name: str) -> int:
|
|
156
|
-
"""Get tool timeout
|
|
157
|
-
|
|
158
|
-
Args:
|
|
159
|
-
tool_name: Tool name (e.g., 'ce_find_pointer_path')
|
|
160
|
-
|
|
161
|
-
Returns:
|
|
162
|
-
int: Timeout in seconds
|
|
163
|
-
"""
|
|
164
|
-
return self.tool_timeouts.get(tool_name, self.default_timeout)
|
|
165
|
-
|
|
166
|
-
def set_timeout(self, tool_name: str, timeout: int) -> None:
|
|
167
|
-
"""Set tool-specific timeout
|
|
168
|
-
|
|
169
|
-
Args:
|
|
170
|
-
tool_name: Tool name
|
|
171
|
-
timeout: Timeout in seconds
|
|
172
|
-
"""
|
|
173
|
-
self.tool_timeouts[tool_name] = timeout
|
|
174
|
-
|
|
175
|
-
def get_all_timeouts(self) -> dict:
|
|
176
|
-
"""Get all timeout configurations"""
|
|
177
|
-
return {
|
|
178
|
-
"default_timeout": self.default_timeout,
|
|
179
|
-
"tool_timeouts": dict(self.tool_timeouts),
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# ============ Metrics Collector ============
|
|
184
|
-
@dataclass
|
|
185
|
-
class ToolMetrics:
|
|
186
|
-
"""Tool execution metrics
|
|
187
|
-
|
|
188
|
-
**Validates: Requirements 4.4**
|
|
189
|
-
"""
|
|
190
|
-
|
|
191
|
-
call_count: int = 0
|
|
192
|
-
total_time: float = 0.0
|
|
193
|
-
error_count: int = 0
|
|
194
|
-
last_call_time: float = 0.0
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
class MetricsCollector:
|
|
198
|
-
"""Metrics collector for tracking tool calls, execution time and error rates.
|
|
199
|
-
|
|
200
|
-
**Validates: Requirements 4.4**
|
|
201
|
-
"""
|
|
202
|
-
|
|
203
|
-
def __init__(self):
|
|
204
|
-
"""Initialize metrics collector"""
|
|
205
|
-
self.tool_metrics: Dict[str, ToolMetrics] = {}
|
|
206
|
-
self.start_time = time.time()
|
|
207
|
-
|
|
208
|
-
def record_call(
|
|
209
|
-
self, tool_name: str, duration: float, is_error: bool = False
|
|
210
|
-
) -> None:
|
|
211
|
-
"""Record tool call
|
|
212
|
-
|
|
213
|
-
Args:
|
|
214
|
-
tool_name: Tool name
|
|
215
|
-
duration: Execution time (seconds)
|
|
216
|
-
is_error: Whether call resulted in error
|
|
217
|
-
"""
|
|
218
|
-
if tool_name not in self.tool_metrics:
|
|
219
|
-
self.tool_metrics[tool_name] = ToolMetrics()
|
|
220
|
-
|
|
221
|
-
metrics = self.tool_metrics[tool_name]
|
|
222
|
-
metrics.call_count += 1
|
|
223
|
-
metrics.total_time += duration
|
|
224
|
-
metrics.last_call_time = time.time()
|
|
225
|
-
if is_error:
|
|
226
|
-
metrics.error_count += 1
|
|
227
|
-
|
|
228
|
-
def get_tool_metrics(self, tool_name: str) -> Optional[dict]:
|
|
229
|
-
"""Get metrics for specific tool"""
|
|
230
|
-
metrics = self.tool_metrics.get(tool_name)
|
|
231
|
-
if not metrics:
|
|
232
|
-
return None
|
|
233
|
-
return {
|
|
234
|
-
"calls": metrics.call_count,
|
|
235
|
-
"total_time": metrics.total_time,
|
|
236
|
-
"avg_time": metrics.total_time / metrics.call_count
|
|
237
|
-
if metrics.call_count > 0
|
|
238
|
-
else 0,
|
|
239
|
-
"errors": metrics.error_count,
|
|
240
|
-
"error_rate": metrics.error_count / metrics.call_count
|
|
241
|
-
if metrics.call_count > 0
|
|
242
|
-
else 0,
|
|
243
|
-
"last_call_time": metrics.last_call_time,
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
def get_summary(self) -> dict:
|
|
247
|
-
"""Get metrics summary"""
|
|
248
|
-
total_calls = sum(m.call_count for m in self.tool_metrics.values())
|
|
249
|
-
total_errors = sum(m.error_count for m in self.tool_metrics.values())
|
|
250
|
-
|
|
251
|
-
return {
|
|
252
|
-
"uptime": time.time() - self.start_time,
|
|
253
|
-
"total_calls": total_calls,
|
|
254
|
-
"total_errors": total_errors,
|
|
255
|
-
"error_rate": total_errors / total_calls if total_calls > 0 else 0,
|
|
256
|
-
"tools": {
|
|
257
|
-
name: {
|
|
258
|
-
"calls": m.call_count,
|
|
259
|
-
"avg_time": m.total_time / m.call_count if m.call_count > 0 else 0,
|
|
260
|
-
"errors": m.error_count,
|
|
261
|
-
"error_rate": m.error_count / m.call_count
|
|
262
|
-
if m.call_count > 0
|
|
263
|
-
else 0,
|
|
264
|
-
}
|
|
265
|
-
for name, m in self.tool_metrics.items()
|
|
266
|
-
},
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
def reset(self) -> None:
|
|
270
|
-
"""Reset all metrics"""
|
|
271
|
-
self.tool_metrics.clear()
|
|
272
|
-
self.start_time = time.time()
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
# ============ Config ============
|
|
276
|
-
class Config:
|
|
277
|
-
PIPE_NAME = r"\\.\pipe\{}".format(
|
|
278
|
-
os.environ.get("CE_MCP_PIPE_NAME", "ce_mcp_bridge")
|
|
279
|
-
)
|
|
280
|
-
AUTH_TOKEN = os.environ.get("CE_MCP_AUTH_TOKEN")
|
|
281
|
-
MAX_RETRIES = 3
|
|
282
|
-
CHUNK_SIZE = 1024 * 1024
|
|
283
|
-
MAX_RESPONSE_SIZE = 10 * 1024 * 1024
|
|
284
|
-
DEBUG = False
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8")
|
|
288
|
-
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", newline="\n")
|
|
289
|
-
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
# ============ Logger ============
|
|
293
|
-
class Logger:
|
|
294
|
-
@staticmethod
|
|
295
|
-
def log(msg: str, level: str = "INFO"):
|
|
296
|
-
if level == "DEBUG" and not Config.DEBUG:
|
|
297
|
-
return
|
|
298
|
-
sys.stderr.write(f"[CheatEngine-MCP-{level}] {msg}\n")
|
|
299
|
-
sys.stderr.flush()
|
|
300
|
-
|
|
301
|
-
@staticmethod
|
|
302
|
-
def info(msg: str):
|
|
303
|
-
Logger.log(msg, "INFO")
|
|
304
|
-
|
|
305
|
-
@staticmethod
|
|
306
|
-
def debug(msg: str):
|
|
307
|
-
Logger.log(msg, "DEBUG")
|
|
308
|
-
|
|
309
|
-
@staticmethod
|
|
310
|
-
def error(msg: str):
|
|
311
|
-
Logger.log(msg, "ERROR")
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
log = Logger()
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
# ============ Pipe Client ============
|
|
318
|
-
class PipeClient:
|
|
319
|
-
"""Named pipe client with persistent connection and background reconnection"""
|
|
320
|
-
|
|
321
|
-
def __init__(self, pipe_name: str = Config.PIPE_NAME, error_threshold: int = 5):
|
|
322
|
-
self.pipe_name = pipe_name
|
|
323
|
-
self.pipe = None
|
|
324
|
-
self.health_monitor = HealthMonitor(error_threshold=error_threshold)
|
|
325
|
-
self._lock = threading.Lock()
|
|
326
|
-
self._reconnect_thread = None
|
|
327
|
-
self._stop_reconnect = threading.Event()
|
|
328
|
-
self._connected = threading.Event()
|
|
329
|
-
# Legacy attributes
|
|
330
|
-
self.connection_attempts = 0
|
|
331
|
-
self.last_error: Optional[str] = None
|
|
332
|
-
self._last_connect_error_code = None
|
|
333
|
-
self.last_success_time = 0
|
|
334
|
-
|
|
335
|
-
def is_valid(self) -> bool:
|
|
336
|
-
if not self.pipe:
|
|
337
|
-
return False
|
|
338
|
-
try:
|
|
339
|
-
win32pipe.GetNamedPipeInfo(self.pipe)
|
|
340
|
-
return True
|
|
341
|
-
except:
|
|
342
|
-
return False
|
|
343
|
-
|
|
344
|
-
def _try_connect_once(self) -> bool:
|
|
345
|
-
"""Single connection attempt, returns True on success"""
|
|
346
|
-
try:
|
|
347
|
-
pipe = win32file.CreateFile(
|
|
348
|
-
self.pipe_name,
|
|
349
|
-
win32file.GENERIC_READ | win32file.GENERIC_WRITE,
|
|
350
|
-
0,
|
|
351
|
-
None,
|
|
352
|
-
win32file.OPEN_EXISTING,
|
|
353
|
-
0,
|
|
354
|
-
None,
|
|
355
|
-
)
|
|
356
|
-
win32pipe.SetNamedPipeHandleState(
|
|
357
|
-
pipe, win32pipe.PIPE_READMODE_BYTE, None, None
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
with self._lock:
|
|
361
|
-
self._close_unlocked()
|
|
362
|
-
self.pipe = pipe
|
|
363
|
-
self.last_error = None
|
|
364
|
-
self._last_connect_error_code = None
|
|
365
|
-
self.last_success_time = time.time()
|
|
366
|
-
self._connected.set()
|
|
367
|
-
|
|
368
|
-
log.info("Connected to Cheat Engine Pipe")
|
|
369
|
-
self.health_monitor.record_success()
|
|
370
|
-
return True
|
|
371
|
-
|
|
372
|
-
except pywintypes.error as e:
|
|
373
|
-
self._last_connect_error_code = e.winerror
|
|
374
|
-
self.last_error = str(e)
|
|
375
|
-
|
|
376
|
-
# If pipe busy, wait and retry once
|
|
377
|
-
if e.winerror == winerror.ERROR_PIPE_BUSY:
|
|
378
|
-
try:
|
|
379
|
-
win32pipe.WaitNamedPipe(self.pipe_name, 2000)
|
|
380
|
-
return self._try_connect_once()
|
|
381
|
-
except:
|
|
382
|
-
pass
|
|
383
|
-
return False
|
|
384
|
-
|
|
385
|
-
def _reconnect_loop(self):
|
|
386
|
-
"""Background reconnection loop"""
|
|
387
|
-
backoff = 0.5
|
|
388
|
-
max_backoff = 10.0
|
|
389
|
-
|
|
390
|
-
while not self._stop_reconnect.is_set():
|
|
391
|
-
if not self.is_valid():
|
|
392
|
-
self.connection_attempts += 1
|
|
393
|
-
self.health_monitor.record_connection_attempt()
|
|
394
|
-
|
|
395
|
-
if self._try_connect_once():
|
|
396
|
-
backoff = 0.5
|
|
397
|
-
else:
|
|
398
|
-
backoff = min(backoff * 1.5, max_backoff)
|
|
399
|
-
else:
|
|
400
|
-
backoff = 0.5
|
|
401
|
-
|
|
402
|
-
self._stop_reconnect.wait(backoff)
|
|
403
|
-
|
|
404
|
-
def start_background_reconnect(self):
|
|
405
|
-
"""Start background reconnection thread"""
|
|
406
|
-
if self._reconnect_thread and self._reconnect_thread.is_alive():
|
|
407
|
-
return
|
|
408
|
-
self._stop_reconnect.clear()
|
|
409
|
-
self._reconnect_thread = threading.Thread(
|
|
410
|
-
target=self._reconnect_loop, daemon=True
|
|
411
|
-
)
|
|
412
|
-
self._reconnect_thread.start()
|
|
413
|
-
|
|
414
|
-
def connect(self, force: bool = False, timeout: float = 3.0) -> bool:
|
|
415
|
-
"""Connect to pipe with optional wait for background reconnect"""
|
|
416
|
-
if not force and self.is_valid():
|
|
417
|
-
return True
|
|
418
|
-
|
|
419
|
-
# Try immediate connection
|
|
420
|
-
self.connection_attempts += 1
|
|
421
|
-
self.health_monitor.record_connection_attempt()
|
|
422
|
-
if self._try_connect_once():
|
|
423
|
-
return True
|
|
424
|
-
|
|
425
|
-
# Start background reconnect and wait
|
|
426
|
-
self.start_background_reconnect()
|
|
427
|
-
if timeout > 0:
|
|
428
|
-
return self._connected.wait(timeout)
|
|
429
|
-
return False
|
|
430
|
-
|
|
431
|
-
def _close_unlocked(self):
|
|
432
|
-
"""Close pipe without lock"""
|
|
433
|
-
if self.pipe:
|
|
434
|
-
try:
|
|
435
|
-
win32file.CloseHandle(self.pipe)
|
|
436
|
-
except:
|
|
437
|
-
pass
|
|
438
|
-
self.pipe = None
|
|
439
|
-
self._connected.clear()
|
|
440
|
-
|
|
441
|
-
def _close(self):
|
|
442
|
-
with self._lock:
|
|
443
|
-
self._close_unlocked()
|
|
444
|
-
|
|
445
|
-
def _get_connection_error_message(self, winerror_code: int, attempt: int) -> str:
|
|
446
|
-
"""Get user-friendly error message based on Windows error code"""
|
|
447
|
-
messages = {
|
|
448
|
-
winerror.ERROR_FILE_NOT_FOUND: (
|
|
449
|
-
"Cheat Engine MCP Bridge not running. "
|
|
450
|
-
"Load 'ce_mcp_bridge.lua' in CE (Table -> Show Cheat Table Lua Script -> Execute)"
|
|
451
|
-
),
|
|
452
|
-
winerror.ERROR_PIPE_BUSY: "Pipe busy - another client may be connected",
|
|
453
|
-
winerror.ERROR_ACCESS_DENIED: "Access denied - try running as administrator",
|
|
454
|
-
winerror.ERROR_BROKEN_PIPE: "Connection lost - restart bridge script in CE",
|
|
455
|
-
}
|
|
456
|
-
base_msg = messages.get(
|
|
457
|
-
winerror_code, f"Connection failed (error {winerror_code})"
|
|
458
|
-
)
|
|
459
|
-
return f"{base_msg} [attempt {attempt}/{Config.MAX_RETRIES}]"
|
|
460
|
-
|
|
461
|
-
def _do_send_receive(self, data: dict) -> dict:
|
|
462
|
-
"""Execute send/receive operation with auto-reconnect"""
|
|
463
|
-
# Add authentication token if configured
|
|
464
|
-
if Config.AUTH_TOKEN:
|
|
465
|
-
data = {**data, "auth_token": Config.AUTH_TOKEN}
|
|
466
|
-
|
|
467
|
-
for retry in range(Config.MAX_RETRIES):
|
|
468
|
-
if not self.connect(force=retry > 0):
|
|
469
|
-
if self._last_connect_error_code:
|
|
470
|
-
return {
|
|
471
|
-
"error": self._get_connection_error_message(
|
|
472
|
-
self._last_connect_error_code, retry + 1
|
|
473
|
-
)
|
|
474
|
-
}
|
|
475
|
-
return {
|
|
476
|
-
"error": f"Cannot connect to CE (attempt {retry + 1}/{Config.MAX_RETRIES})"
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
try:
|
|
480
|
-
with self._lock:
|
|
481
|
-
if not self.pipe:
|
|
482
|
-
continue
|
|
483
|
-
|
|
484
|
-
json_bytes = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
|
485
|
-
win32file.WriteFile(self.pipe, struct.pack("<I", len(json_bytes)))
|
|
486
|
-
win32file.WriteFile(self.pipe, json_bytes)
|
|
487
|
-
|
|
488
|
-
_, header = win32file.ReadFile(self.pipe, 4)
|
|
489
|
-
if not header or len(header) < 4:
|
|
490
|
-
raise Exception("Empty response header")
|
|
491
|
-
|
|
492
|
-
resp_len = struct.unpack("<I", header)[0]
|
|
493
|
-
if resp_len == 0 or resp_len > Config.MAX_RESPONSE_SIZE:
|
|
494
|
-
raise Exception(f"Invalid response size: {resp_len}")
|
|
495
|
-
|
|
496
|
-
resp_data = self._read_chunked(resp_len)
|
|
497
|
-
|
|
498
|
-
decoded = self._decode_response(resp_data)
|
|
499
|
-
self.last_success_time = time.time()
|
|
500
|
-
self.health_monitor.record_success()
|
|
501
|
-
return json.loads(decoded)
|
|
502
|
-
|
|
503
|
-
except pywintypes.error as e:
|
|
504
|
-
log.debug(f"Pipe error (attempt {retry + 1}): {e}")
|
|
505
|
-
self.health_monitor.record_error(str(e))
|
|
506
|
-
self._close()
|
|
507
|
-
time.sleep(0.1 * (retry + 1))
|
|
508
|
-
|
|
509
|
-
except Exception as e:
|
|
510
|
-
log.debug(f"Error (attempt {retry + 1}): {e}")
|
|
511
|
-
self.health_monitor.record_error(str(e))
|
|
512
|
-
self._close()
|
|
513
|
-
|
|
514
|
-
return {"error": self.last_error or "Connection failed after retries"}
|
|
515
|
-
|
|
516
|
-
def send_receive(
|
|
517
|
-
self, data: dict, timeout: int = 30000, tool_name: str = None
|
|
518
|
-
) -> dict:
|
|
519
|
-
"""Send request and receive response with timeout support
|
|
520
|
-
|
|
521
|
-
Args:
|
|
522
|
-
data: Data dictionary to send
|
|
523
|
-
timeout: Timeout in milliseconds, default 30000ms (30s)
|
|
524
|
-
tool_name: Tool name for timeout error reporting
|
|
525
|
-
|
|
526
|
-
Returns:
|
|
527
|
-
dict: Response data, or error dict on timeout
|
|
528
|
-
|
|
529
|
-
**Validates: Requirements 2.2, 2.3**
|
|
530
|
-
"""
|
|
531
|
-
timeout_seconds = timeout / 1000.0
|
|
532
|
-
start_time = time.time()
|
|
533
|
-
|
|
534
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
535
|
-
future = executor.submit(self._do_send_receive, data)
|
|
536
|
-
try:
|
|
537
|
-
result = future.result(timeout=timeout_seconds)
|
|
538
|
-
return result
|
|
539
|
-
except concurrent.futures.TimeoutError:
|
|
540
|
-
elapsed_time = time.time() - start_time
|
|
541
|
-
error_msg = f"Tool execution timed out after {elapsed_time:.2f}s (timeout: {timeout_seconds}s)"
|
|
542
|
-
log.error(
|
|
543
|
-
f"Timeout: tool={tool_name or 'unknown'}, params={data.get('params', {})}, elapsed={elapsed_time:.2f}s"
|
|
544
|
-
)
|
|
545
|
-
self.health_monitor.record_error(error_msg)
|
|
546
|
-
return {
|
|
547
|
-
"error": error_msg,
|
|
548
|
-
"timeout_info": {
|
|
549
|
-
"tool_name": tool_name or data.get("command", "unknown"),
|
|
550
|
-
"params": data.get("params", {}),
|
|
551
|
-
"elapsed_time": elapsed_time,
|
|
552
|
-
"timeout_seconds": timeout_seconds,
|
|
553
|
-
},
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
def get_health_metrics(self) -> dict:
|
|
557
|
-
"""Get connection health metrics"""
|
|
558
|
-
return self.health_monitor.get_metrics()
|
|
559
|
-
|
|
560
|
-
def _read_chunked(self, total_size: int) -> bytes:
|
|
561
|
-
buffer = bytearray(total_size)
|
|
562
|
-
offset = 0
|
|
563
|
-
while offset < total_size:
|
|
564
|
-
chunk_size = min(total_size - offset, Config.CHUNK_SIZE)
|
|
565
|
-
_, chunk = win32file.ReadFile(self.pipe, chunk_size)
|
|
566
|
-
if not chunk:
|
|
567
|
-
raise Exception("Connection closed while reading response")
|
|
568
|
-
buffer[offset : offset + len(chunk)] = chunk
|
|
569
|
-
offset += len(chunk)
|
|
570
|
-
return bytes(buffer)
|
|
571
|
-
|
|
572
|
-
def _decode_response(self, data: bytes) -> str:
|
|
573
|
-
for encoding in ["utf-8", "gbk", "gb2312", "latin-1"]:
|
|
574
|
-
try:
|
|
575
|
-
return data.decode(encoding)
|
|
576
|
-
except UnicodeDecodeError:
|
|
577
|
-
continue
|
|
578
|
-
return data.decode("utf-8", errors="replace")
|
|
579
|
-
|
|
580
|
-
def stop(self):
|
|
581
|
-
"""Stop the client and background threads"""
|
|
582
|
-
self._stop_reconnect.set()
|
|
583
|
-
if self._reconnect_thread:
|
|
584
|
-
self._reconnect_thread.join(timeout=1)
|
|
585
|
-
self._close()
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
# ============ Tool Registry ============
|
|
589
|
-
class ToolCategory(Enum):
|
|
590
|
-
SYSTEM = "system"
|
|
591
|
-
MEMORY = "memory"
|
|
592
|
-
SCANNING = "scanning"
|
|
593
|
-
SYMBOLS = "symbols"
|
|
594
|
-
DEBUG = "debug"
|
|
595
|
-
ANALYSIS = "analysis"
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
@dataclass
|
|
599
|
-
class ToolParam:
|
|
600
|
-
"""Tool parameter definition"""
|
|
601
|
-
|
|
602
|
-
name: str
|
|
603
|
-
type: str
|
|
604
|
-
description: str = ""
|
|
605
|
-
required: bool = False
|
|
606
|
-
default: Any = None
|
|
607
|
-
enum: List[str] = field(default_factory=list)
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
@dataclass
|
|
611
|
-
class Tool:
|
|
612
|
-
"""Tool definition"""
|
|
613
|
-
|
|
614
|
-
name: str
|
|
615
|
-
description: str
|
|
616
|
-
lua_command: str
|
|
617
|
-
category: ToolCategory
|
|
618
|
-
params: List[ToolParam] = field(default_factory=list)
|
|
619
|
-
|
|
620
|
-
def to_mcp_schema(self) -> dict:
|
|
621
|
-
"""Convert to MCP tool schema format"""
|
|
622
|
-
properties = {}
|
|
623
|
-
required = []
|
|
624
|
-
|
|
625
|
-
for p in self.params:
|
|
626
|
-
prop = {"type": p.type}
|
|
627
|
-
if p.description:
|
|
628
|
-
prop["description"] = p.description
|
|
629
|
-
if p.default is not None:
|
|
630
|
-
prop["default"] = p.default
|
|
631
|
-
if p.enum:
|
|
632
|
-
prop["enum"] = p.enum
|
|
633
|
-
properties[p.name] = prop
|
|
634
|
-
|
|
635
|
-
if p.required:
|
|
636
|
-
required.append(p.name)
|
|
637
|
-
|
|
638
|
-
return {
|
|
639
|
-
"name": self.name,
|
|
640
|
-
"description": self.description,
|
|
641
|
-
"inputSchema": {
|
|
642
|
-
"type": "object",
|
|
643
|
-
"properties": properties,
|
|
644
|
-
"required": required,
|
|
645
|
-
},
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
class ToolRegistry:
|
|
650
|
-
"""Registry for all MCP tools"""
|
|
651
|
-
|
|
652
|
-
# Common parameter definitions
|
|
653
|
-
ADDR_PARAM = ToolParam(
|
|
654
|
-
"address",
|
|
655
|
-
"string",
|
|
656
|
-
"Address expression (e.g. 'game.exe+123' or '0x123456')",
|
|
657
|
-
True,
|
|
658
|
-
)
|
|
659
|
-
|
|
660
|
-
def __init__(self):
|
|
661
|
-
self.tools: Dict[str, Tool] = {}
|
|
662
|
-
self._register_all_tools()
|
|
663
|
-
|
|
664
|
-
def _register_all_tools(self):
|
|
665
|
-
"""Register all available tools"""
|
|
666
|
-
self._register_system_tools()
|
|
667
|
-
self._register_memory_tools()
|
|
668
|
-
self._register_scanning_tools()
|
|
669
|
-
self._register_symbol_tools()
|
|
670
|
-
self._register_debug_tools()
|
|
671
|
-
self._register_analysis_tools()
|
|
672
|
-
|
|
673
|
-
def _register(self, tool: Tool):
|
|
674
|
-
"""Register a single tool"""
|
|
675
|
-
self.tools[tool.name] = tool
|
|
676
|
-
|
|
677
|
-
def _register_system_tools(self):
|
|
678
|
-
"""Register system/info tools"""
|
|
679
|
-
self._register(
|
|
680
|
-
Tool(
|
|
681
|
-
"ce_ping",
|
|
682
|
-
"Test connection to Cheat Engine and get bridge status. "
|
|
683
|
-
"If connection fails, automatically returns diagnostic info with troubleshooting suggestions. "
|
|
684
|
-
"Returns connection health metrics (last_success_time, connection_attempts, consecutive_errors, total_errors, is_connected) "
|
|
685
|
-
"and server metrics (uptime, total_calls, error_rate, per-tool statistics).",
|
|
686
|
-
"ping",
|
|
687
|
-
ToolCategory.SYSTEM,
|
|
688
|
-
)
|
|
689
|
-
)
|
|
690
|
-
|
|
691
|
-
self._register(
|
|
692
|
-
Tool(
|
|
693
|
-
"ce_get_process_info",
|
|
694
|
-
"[INIT] Get current attached process info and refresh symbols. CALL THIS FIRST in any session. "
|
|
695
|
-
"Returns: {name, pid, is64bit, ce_version, debugger: {active, interfaceName, canBreak}, "
|
|
696
|
-
"last_symbol_refresh (timestamp), symbol_refresh_count, module_count}. "
|
|
697
|
-
"Also triggers symbol handler refresh to ensure symbols are up-to-date after module loading.",
|
|
698
|
-
"get_process_info",
|
|
699
|
-
ToolCategory.SYSTEM,
|
|
700
|
-
)
|
|
701
|
-
)
|
|
702
|
-
|
|
703
|
-
self._register(
|
|
704
|
-
Tool(
|
|
705
|
-
"ce_execute_lua",
|
|
706
|
-
"Execute raw Lua code in CE",
|
|
707
|
-
"execute_lua",
|
|
708
|
-
ToolCategory.SYSTEM,
|
|
709
|
-
[ToolParam("code", "string", "Lua code to execute", True)],
|
|
710
|
-
)
|
|
711
|
-
)
|
|
712
|
-
|
|
713
|
-
self._register(
|
|
714
|
-
Tool(
|
|
715
|
-
"ce_get_selected_address",
|
|
716
|
-
"Get the currently selected address in CE Memory View/Disassembler",
|
|
717
|
-
"get_selected_address",
|
|
718
|
-
ToolCategory.SYSTEM,
|
|
719
|
-
)
|
|
720
|
-
)
|
|
721
|
-
|
|
722
|
-
# ============ Logging Tools ============
|
|
723
|
-
self._register(
|
|
724
|
-
Tool(
|
|
725
|
-
"ce_get_logs",
|
|
726
|
-
"Get log entries from the Cheat Engine MCP Bridge Lua side. "
|
|
727
|
-
"Returns: {entries: [{timestamp, level, category, message, data}], count}. "
|
|
728
|
-
"Useful for debugging and monitoring bridge operations.",
|
|
729
|
-
"get_logs",
|
|
730
|
-
ToolCategory.SYSTEM,
|
|
731
|
-
[
|
|
732
|
-
ToolParam(
|
|
733
|
-
"count",
|
|
734
|
-
"integer",
|
|
735
|
-
"Maximum number of log entries to return (default: 100)",
|
|
736
|
-
False,
|
|
737
|
-
100,
|
|
738
|
-
),
|
|
739
|
-
ToolParam(
|
|
740
|
-
"min_level",
|
|
741
|
-
"integer",
|
|
742
|
-
"Minimum log level to include: 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR (default: 1)",
|
|
743
|
-
False,
|
|
744
|
-
1,
|
|
745
|
-
),
|
|
746
|
-
],
|
|
747
|
-
)
|
|
748
|
-
)
|
|
749
|
-
|
|
750
|
-
# ============ Process Automation Tools (NEW) ============
|
|
751
|
-
self._register(
|
|
752
|
-
Tool(
|
|
753
|
-
"ce_list_processes",
|
|
754
|
-
"List running processes. Use before ce_attach_process to find available targets. "
|
|
755
|
-
"Returns: {count, processes: [{pid, name}]}.",
|
|
756
|
-
"list_processes",
|
|
757
|
-
ToolCategory.SYSTEM,
|
|
758
|
-
[
|
|
759
|
-
ToolParam(
|
|
760
|
-
"filter",
|
|
761
|
-
"string",
|
|
762
|
-
"Filter processes by name (case-insensitive substring match)",
|
|
763
|
-
),
|
|
764
|
-
ToolParam(
|
|
765
|
-
"max_results",
|
|
766
|
-
"integer",
|
|
767
|
-
"Maximum results to return (default: 100)",
|
|
768
|
-
False,
|
|
769
|
-
100,
|
|
770
|
-
),
|
|
771
|
-
],
|
|
772
|
-
)
|
|
773
|
-
)
|
|
774
|
-
|
|
775
|
-
self._register(
|
|
776
|
-
Tool(
|
|
777
|
-
"ce_attach_process",
|
|
778
|
-
"Attach to a process by PID or name. Clears caches and scan sessions after attaching. "
|
|
779
|
-
"Returns: {success, pid, name, is64bit}.",
|
|
780
|
-
"attach_process",
|
|
781
|
-
ToolCategory.SYSTEM,
|
|
782
|
-
[
|
|
783
|
-
ToolParam(
|
|
784
|
-
"target",
|
|
785
|
-
"string",
|
|
786
|
-
"Process ID (number) or process name (e.g. 'game.exe')",
|
|
787
|
-
True,
|
|
788
|
-
)
|
|
789
|
-
],
|
|
790
|
-
)
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
self._register(
|
|
794
|
-
Tool(
|
|
795
|
-
"ce_auto_assemble",
|
|
796
|
-
"Execute an Auto Assembler script. Supports enable/disable scripts, code injection, etc. "
|
|
797
|
-
"Returns: {success, target_self}.",
|
|
798
|
-
"auto_assemble",
|
|
799
|
-
ToolCategory.SYSTEM,
|
|
800
|
-
[
|
|
801
|
-
ToolParam(
|
|
802
|
-
"script", "string", "Auto Assembler script content", True
|
|
803
|
-
),
|
|
804
|
-
ToolParam(
|
|
805
|
-
"target_self",
|
|
806
|
-
"boolean",
|
|
807
|
-
"Target CE process itself (for internal scripts)",
|
|
808
|
-
False,
|
|
809
|
-
False,
|
|
810
|
-
),
|
|
811
|
-
],
|
|
812
|
-
)
|
|
813
|
-
)
|
|
814
|
-
|
|
815
|
-
def _register_memory_tools(self):
|
|
816
|
-
"""Register memory operation tools"""
|
|
817
|
-
mem_types = [
|
|
818
|
-
"byte",
|
|
819
|
-
"word",
|
|
820
|
-
"dword",
|
|
821
|
-
"qword",
|
|
822
|
-
"float",
|
|
823
|
-
"double",
|
|
824
|
-
"string",
|
|
825
|
-
"bytes",
|
|
826
|
-
]
|
|
827
|
-
|
|
828
|
-
self._register(
|
|
829
|
-
Tool(
|
|
830
|
-
"ce_read_memory",
|
|
831
|
-
"Read a single memory value. Returns: {address, type, value}. "
|
|
832
|
-
"For reading multiple addresses, use ce_read_memory_batch instead for better performance.",
|
|
833
|
-
"read_memory",
|
|
834
|
-
ToolCategory.MEMORY,
|
|
835
|
-
[
|
|
836
|
-
self.ADDR_PARAM,
|
|
837
|
-
ToolParam("type", "string", "Value type", True, enum=mem_types),
|
|
838
|
-
ToolParam("size", "integer", "Size for string/bytes", False, 100),
|
|
839
|
-
],
|
|
840
|
-
)
|
|
841
|
-
)
|
|
842
|
-
|
|
843
|
-
self._register(
|
|
844
|
-
Tool(
|
|
845
|
-
"ce_read_memory_batch",
|
|
846
|
-
"[PERFORMANCE] Read multiple memory addresses in ONE request. "
|
|
847
|
-
"ALWAYS prefer this over multiple ce_read_memory calls for better performance. "
|
|
848
|
-
'Example: [{"address": "game.exe+100", "type": "dword", "id": "hp"}, {"address": "game.exe+104", "type": "float", "id": "mp"}]. '
|
|
849
|
-
"Returns: {results: [{id, address, type, value, error?}]}.",
|
|
850
|
-
"read_memory_batch",
|
|
851
|
-
ToolCategory.MEMORY,
|
|
852
|
-
[
|
|
853
|
-
ToolParam(
|
|
854
|
-
"requests",
|
|
855
|
-
"array",
|
|
856
|
-
"Array of {address, type, id?, size?} objects",
|
|
857
|
-
True,
|
|
858
|
-
)
|
|
859
|
-
],
|
|
860
|
-
)
|
|
861
|
-
)
|
|
862
|
-
|
|
863
|
-
self._register(
|
|
864
|
-
Tool(
|
|
865
|
-
"ce_write_memory",
|
|
866
|
-
"Write memory value. Returns: {success: true, address, bytes_written}.",
|
|
867
|
-
"write_memory",
|
|
868
|
-
ToolCategory.MEMORY,
|
|
869
|
-
[
|
|
870
|
-
self.ADDR_PARAM,
|
|
871
|
-
ToolParam("type", "string", "Value type", True, enum=mem_types),
|
|
872
|
-
ToolParam(
|
|
873
|
-
"value",
|
|
874
|
-
"string",
|
|
875
|
-
"Value to write (number, string, or byte array)",
|
|
876
|
-
True,
|
|
877
|
-
),
|
|
878
|
-
],
|
|
879
|
-
)
|
|
880
|
-
)
|
|
881
|
-
|
|
882
|
-
def _register_scanning_tools(self):
|
|
883
|
-
"""Register scanning tools"""
|
|
884
|
-
self._register(
|
|
885
|
-
Tool(
|
|
886
|
-
"ce_aob_scan",
|
|
887
|
-
"Scan for byte pattern (AOB/signature) in memory. Supports ?? wildcards for variable bytes. "
|
|
888
|
-
"USE WHEN: Finding code by signature, locating functions, making version-independent scripts. "
|
|
889
|
-
"NOT FOR: Finding data values (use ce_scan_new or ce_value_scan). "
|
|
890
|
-
"Returns: {count, results: [addresses]}.",
|
|
891
|
-
"aob_scan",
|
|
892
|
-
ToolCategory.SCANNING,
|
|
893
|
-
[
|
|
894
|
-
ToolParam(
|
|
895
|
-
"aob_string",
|
|
896
|
-
"string",
|
|
897
|
-
"e.g. '48 89 5C 24 ?? 48 83 EC 20'",
|
|
898
|
-
True,
|
|
899
|
-
),
|
|
900
|
-
ToolParam(
|
|
901
|
-
"module",
|
|
902
|
-
"string",
|
|
903
|
-
"Limit scan to specific module for better performance (e.g. 'game.exe', 'UnityPlayer.dll')",
|
|
904
|
-
),
|
|
905
|
-
ToolParam("protection", "string", "Flags like -C+X", False, "-C+X"),
|
|
906
|
-
ToolParam(
|
|
907
|
-
"start",
|
|
908
|
-
"string",
|
|
909
|
-
"Start address (optional, use module instead for better performance)",
|
|
910
|
-
),
|
|
911
|
-
ToolParam("stop", "string", "Stop address (optional)"),
|
|
912
|
-
ToolParam("max_results", "integer", "Maximum results", False, 100),
|
|
913
|
-
],
|
|
914
|
-
)
|
|
915
|
-
)
|
|
916
|
-
|
|
917
|
-
self._register(
|
|
918
|
-
Tool(
|
|
919
|
-
"ce_value_scan",
|
|
920
|
-
"[MANUAL POINTER TRACING - STEP 2] Scan memory for a specific value. "
|
|
921
|
-
"USE WHEN: After ce_find_what_accesses returns a register value, search for that value to find pointer storage. "
|
|
922
|
-
"Example: RBX=0x12345678 accessed your address -> scan for 0x12345678 (qword) -> find where pointer is stored. "
|
|
923
|
-
"NOT FOR: Finding game values like health/gold (use ce_scan_new for value hunting). "
|
|
924
|
-
"Returns: {count, results: [{address, symbol, isStatic}], value_searched, type, module}. "
|
|
925
|
-
"TIP: isStatic=true means the address is in a module (potential static base found!).",
|
|
926
|
-
"value_scan",
|
|
927
|
-
ToolCategory.SCANNING,
|
|
928
|
-
[
|
|
929
|
-
ToolParam(
|
|
930
|
-
"value",
|
|
931
|
-
"string",
|
|
932
|
-
"Value to search for (e.g. '0x255D5E758' or '12345')",
|
|
933
|
-
True,
|
|
934
|
-
),
|
|
935
|
-
ToolParam(
|
|
936
|
-
"type",
|
|
937
|
-
"string",
|
|
938
|
-
"Value type",
|
|
939
|
-
True,
|
|
940
|
-
enum=[
|
|
941
|
-
"byte",
|
|
942
|
-
"word",
|
|
943
|
-
"dword",
|
|
944
|
-
"qword",
|
|
945
|
-
"float",
|
|
946
|
-
"double",
|
|
947
|
-
"string",
|
|
948
|
-
],
|
|
949
|
-
),
|
|
950
|
-
ToolParam(
|
|
951
|
-
"module",
|
|
952
|
-
"string",
|
|
953
|
-
"Limit scan to specific module (e.g. 'game.exe')",
|
|
954
|
-
),
|
|
955
|
-
ToolParam(
|
|
956
|
-
"protection",
|
|
957
|
-
"string",
|
|
958
|
-
"Memory protection flags (default: '+W-C' for writable memory)",
|
|
959
|
-
False,
|
|
960
|
-
"+W-C",
|
|
961
|
-
),
|
|
962
|
-
ToolParam("start", "string", "Start address (optional)"),
|
|
963
|
-
ToolParam("stop", "string", "Stop address (optional)"),
|
|
964
|
-
ToolParam("max_results", "integer", "Maximum results", False, 100),
|
|
965
|
-
ToolParam(
|
|
966
|
-
"is_hex",
|
|
967
|
-
"boolean",
|
|
968
|
-
"Treat value as hexadecimal (auto-detected if omitted)",
|
|
969
|
-
),
|
|
970
|
-
],
|
|
971
|
-
)
|
|
972
|
-
)
|
|
973
|
-
|
|
974
|
-
# ============ Scan Session Tools (NEW) ============
|
|
975
|
-
self._register(
|
|
976
|
-
Tool(
|
|
977
|
-
"ce_scan_new",
|
|
978
|
-
"[VALUE HUNTING] Start scan session to find unknown addresses by observing value changes. "
|
|
979
|
-
"USE WHEN: You don't know the address but can observe changes (health 100->95). "
|
|
980
|
-
"WORKFLOW: ce_scan_new -> change value in game -> ce_scan_next('decreased') -> repeat until few results. "
|
|
981
|
-
"NOT FOR: Pointer tracing (use ce_value_scan). NOT FOR: Code patterns (use ce_aob_scan). "
|
|
982
|
-
"Returns: {session_id, count}. Use ce_scan_next to filter, ce_scan_results to get addresses.",
|
|
983
|
-
"scan_new",
|
|
984
|
-
ToolCategory.SCANNING,
|
|
985
|
-
[
|
|
986
|
-
ToolParam(
|
|
987
|
-
"value",
|
|
988
|
-
"string",
|
|
989
|
-
"Value to search for (e.g. '100' or '0x64')",
|
|
990
|
-
True,
|
|
991
|
-
),
|
|
992
|
-
ToolParam(
|
|
993
|
-
"type",
|
|
994
|
-
"string",
|
|
995
|
-
"Value type",
|
|
996
|
-
True,
|
|
997
|
-
enum=[
|
|
998
|
-
"byte",
|
|
999
|
-
"word",
|
|
1000
|
-
"dword",
|
|
1001
|
-
"qword",
|
|
1002
|
-
"float",
|
|
1003
|
-
"double",
|
|
1004
|
-
"string",
|
|
1005
|
-
],
|
|
1006
|
-
),
|
|
1007
|
-
ToolParam(
|
|
1008
|
-
"module",
|
|
1009
|
-
"string",
|
|
1010
|
-
"Limit scan to specific module (e.g. 'game.exe')",
|
|
1011
|
-
),
|
|
1012
|
-
ToolParam(
|
|
1013
|
-
"protection", "string", "Memory protection flags", False, "+W-C"
|
|
1014
|
-
),
|
|
1015
|
-
ToolParam("start", "string", "Start address (optional)"),
|
|
1016
|
-
ToolParam("stop", "string", "Stop address (optional)"),
|
|
1017
|
-
ToolParam(
|
|
1018
|
-
"is_hex",
|
|
1019
|
-
"boolean",
|
|
1020
|
-
"Treat value as hexadecimal (auto-detected if omitted)",
|
|
1021
|
-
),
|
|
1022
|
-
],
|
|
1023
|
-
)
|
|
1024
|
-
)
|
|
1025
|
-
|
|
1026
|
-
self._register(
|
|
1027
|
-
Tool(
|
|
1028
|
-
"ce_scan_next",
|
|
1029
|
-
"Continue scanning (filter) an existing session. Supports various scan types. "
|
|
1030
|
-
"Returns: {session_id, count, scan_number, scan_type}.",
|
|
1031
|
-
"scan_next",
|
|
1032
|
-
ToolCategory.SCANNING,
|
|
1033
|
-
[
|
|
1034
|
-
ToolParam(
|
|
1035
|
-
"session_id", "string", "Session ID from ce_scan_new", True
|
|
1036
|
-
),
|
|
1037
|
-
ToolParam("value", "string", "Value to search for", True),
|
|
1038
|
-
ToolParam(
|
|
1039
|
-
"scan_type",
|
|
1040
|
-
"string",
|
|
1041
|
-
"Scan type for filtering",
|
|
1042
|
-
False,
|
|
1043
|
-
"exact",
|
|
1044
|
-
enum=[
|
|
1045
|
-
"exact",
|
|
1046
|
-
"increased",
|
|
1047
|
-
"decreased",
|
|
1048
|
-
"changed",
|
|
1049
|
-
"unchanged",
|
|
1050
|
-
"increased_by",
|
|
1051
|
-
"decreased_by",
|
|
1052
|
-
"bigger_than",
|
|
1053
|
-
"smaller_than",
|
|
1054
|
-
"between",
|
|
1055
|
-
],
|
|
1056
|
-
),
|
|
1057
|
-
ToolParam(
|
|
1058
|
-
"value2", "string", "Second value for 'between' scan type"
|
|
1059
|
-
),
|
|
1060
|
-
ToolParam("is_hex", "boolean", "Treat value as hexadecimal"),
|
|
1061
|
-
],
|
|
1062
|
-
)
|
|
1063
|
-
)
|
|
1064
|
-
|
|
1065
|
-
self._register(
|
|
1066
|
-
Tool(
|
|
1067
|
-
"ce_scan_results",
|
|
1068
|
-
"Get paginated results from a scan session. "
|
|
1069
|
-
"Returns: {session_id, total_count, start_index, returned_count, has_more, results: [{index, address, symbol, value, isStatic}]}.",
|
|
1070
|
-
"scan_results",
|
|
1071
|
-
ToolCategory.SCANNING,
|
|
1072
|
-
[
|
|
1073
|
-
ToolParam(
|
|
1074
|
-
"session_id", "string", "Session ID from ce_scan_new", True
|
|
1075
|
-
),
|
|
1076
|
-
ToolParam(
|
|
1077
|
-
"start_index",
|
|
1078
|
-
"integer",
|
|
1079
|
-
"Starting index for pagination",
|
|
1080
|
-
False,
|
|
1081
|
-
0,
|
|
1082
|
-
),
|
|
1083
|
-
ToolParam(
|
|
1084
|
-
"limit",
|
|
1085
|
-
"integer",
|
|
1086
|
-
"Maximum results to return (max 1000)",
|
|
1087
|
-
False,
|
|
1088
|
-
100,
|
|
1089
|
-
),
|
|
1090
|
-
],
|
|
1091
|
-
)
|
|
1092
|
-
)
|
|
1093
|
-
|
|
1094
|
-
self._register(
|
|
1095
|
-
Tool(
|
|
1096
|
-
"ce_scan_close",
|
|
1097
|
-
"Close a scan session and release resources. "
|
|
1098
|
-
"Returns: {session_id, closed, active_sessions}.",
|
|
1099
|
-
"scan_close",
|
|
1100
|
-
ToolCategory.SCANNING,
|
|
1101
|
-
[ToolParam("session_id", "string", "Session ID to close", True)],
|
|
1102
|
-
)
|
|
1103
|
-
)
|
|
1104
|
-
|
|
1105
|
-
self._register(
|
|
1106
|
-
Tool(
|
|
1107
|
-
"ce_scan_list",
|
|
1108
|
-
"List all active scan sessions. "
|
|
1109
|
-
"Returns: {active_sessions, max_sessions, sessions: [{session_id, count, scan_count, value_type, ...}]}.",
|
|
1110
|
-
"scan_list",
|
|
1111
|
-
ToolCategory.SCANNING,
|
|
1112
|
-
)
|
|
1113
|
-
)
|
|
1114
|
-
|
|
1115
|
-
self._register(
|
|
1116
|
-
Tool(
|
|
1117
|
-
"ce_enum_modules",
|
|
1118
|
-
"List all loaded modules (DLLs). Returns: {count, modules: [{name, base, size}]}.",
|
|
1119
|
-
"enum_modules",
|
|
1120
|
-
ToolCategory.SCANNING,
|
|
1121
|
-
)
|
|
1122
|
-
)
|
|
1123
|
-
|
|
1124
|
-
self._register(
|
|
1125
|
-
Tool(
|
|
1126
|
-
"ce_get_address_list",
|
|
1127
|
-
"Get all records from Cheat Table (address list)",
|
|
1128
|
-
"get_address_list",
|
|
1129
|
-
ToolCategory.SCANNING,
|
|
1130
|
-
[
|
|
1131
|
-
ToolParam(
|
|
1132
|
-
"include_script",
|
|
1133
|
-
"boolean",
|
|
1134
|
-
"Include script content for AA script entries",
|
|
1135
|
-
False,
|
|
1136
|
-
False,
|
|
1137
|
-
)
|
|
1138
|
-
],
|
|
1139
|
-
)
|
|
1140
|
-
)
|
|
1141
|
-
|
|
1142
|
-
self._register(
|
|
1143
|
-
Tool(
|
|
1144
|
-
"ce_add_address_record",
|
|
1145
|
-
"Add a new record to Cheat Table",
|
|
1146
|
-
"add_address_record",
|
|
1147
|
-
ToolCategory.SCANNING,
|
|
1148
|
-
[
|
|
1149
|
-
ToolParam("description", "string", "Record description/name", True),
|
|
1150
|
-
ToolParam("address", "string", "Address expression", True),
|
|
1151
|
-
ToolParam(
|
|
1152
|
-
"value_type",
|
|
1153
|
-
"string",
|
|
1154
|
-
"Value type",
|
|
1155
|
-
False,
|
|
1156
|
-
"dword",
|
|
1157
|
-
enum=[
|
|
1158
|
-
"byte",
|
|
1159
|
-
"word",
|
|
1160
|
-
"dword",
|
|
1161
|
-
"qword",
|
|
1162
|
-
"float",
|
|
1163
|
-
"double",
|
|
1164
|
-
"string",
|
|
1165
|
-
"bytes",
|
|
1166
|
-
"script",
|
|
1167
|
-
],
|
|
1168
|
-
),
|
|
1169
|
-
ToolParam(
|
|
1170
|
-
"script", "string", "AA script content (only for script type)"
|
|
1171
|
-
),
|
|
1172
|
-
],
|
|
1173
|
-
)
|
|
1174
|
-
)
|
|
1175
|
-
|
|
1176
|
-
def _register_symbol_tools(self):
|
|
1177
|
-
"""Register symbol management tools"""
|
|
1178
|
-
self._register(
|
|
1179
|
-
Tool(
|
|
1180
|
-
"ce_get_address",
|
|
1181
|
-
"Resolve expression to address. Returns: {expression, address}. "
|
|
1182
|
-
"Supports: 'game.exe+0x1234', '[[base]+10]+20', symbol names.",
|
|
1183
|
-
"get_address",
|
|
1184
|
-
ToolCategory.SYMBOLS,
|
|
1185
|
-
[
|
|
1186
|
-
ToolParam(
|
|
1187
|
-
"expression", "string", "Address expression to resolve", True
|
|
1188
|
-
)
|
|
1189
|
-
],
|
|
1190
|
-
)
|
|
1191
|
-
)
|
|
1192
|
-
|
|
1193
|
-
self._register(
|
|
1194
|
-
Tool(
|
|
1195
|
-
"ce_get_symbol",
|
|
1196
|
-
"[SYMBOL LOOKUP] Get symbol name, RTTI class info, and module details for an address. "
|
|
1197
|
-
"USE WHEN: Identifying what code/data an address belongs to, checking if address is in a module. "
|
|
1198
|
-
"Returns: {address, symbol, hasSymbol, rttiClassName, hasRTTI, inModule, inSystemModule, "
|
|
1199
|
-
"moduleInfo: {name, base, size, offset, is64bit, path}}. "
|
|
1200
|
-
"TIP: inModule=true with symbol containing '.exe+' or '.dll+' indicates a static address.",
|
|
1201
|
-
"get_symbol",
|
|
1202
|
-
ToolCategory.SYMBOLS,
|
|
1203
|
-
[
|
|
1204
|
-
self.ADDR_PARAM,
|
|
1205
|
-
ToolParam(
|
|
1206
|
-
"include_module",
|
|
1207
|
-
"boolean",
|
|
1208
|
-
"Include module name in symbol",
|
|
1209
|
-
False,
|
|
1210
|
-
True,
|
|
1211
|
-
),
|
|
1212
|
-
],
|
|
1213
|
-
)
|
|
1214
|
-
)
|
|
1215
|
-
|
|
1216
|
-
self._register(
|
|
1217
|
-
Tool(
|
|
1218
|
-
"ce_get_region_info",
|
|
1219
|
-
"Get memory region info (base, size, protection)",
|
|
1220
|
-
"get_region_info",
|
|
1221
|
-
ToolCategory.SYMBOLS,
|
|
1222
|
-
[self.ADDR_PARAM],
|
|
1223
|
-
)
|
|
1224
|
-
)
|
|
1225
|
-
|
|
1226
|
-
self._register(
|
|
1227
|
-
Tool(
|
|
1228
|
-
"ce_auto_guess",
|
|
1229
|
-
"Guess the value type at an address (byte/word/dword/qword/float/double/string/pointer). "
|
|
1230
|
-
"USE WHEN: Analyzing unknown memory, determining how to read a value. "
|
|
1231
|
-
"Returns: {type_id, type_name, value}. Useful for structure field analysis.",
|
|
1232
|
-
"auto_guess",
|
|
1233
|
-
ToolCategory.SYMBOLS,
|
|
1234
|
-
[self.ADDR_PARAM],
|
|
1235
|
-
)
|
|
1236
|
-
)
|
|
1237
|
-
|
|
1238
|
-
self._register(
|
|
1239
|
-
Tool(
|
|
1240
|
-
"ce_resolve_pointer",
|
|
1241
|
-
"[VERIFICATION] Resolve a KNOWN pointer chain to get final address and optionally read value. "
|
|
1242
|
-
"USE WHEN: You already have base+offsets and want to verify the chain works or read the value. "
|
|
1243
|
-
"Returns CE notation like '[[game.exe+123]+10]+20' for easy copy to CE. "
|
|
1244
|
-
"NOT FOR: Discovering pointer paths (use ce_find_pointer_path for discovery). "
|
|
1245
|
-
"Returns: {base, offsets, final_address, ceNotation, chain: [{level, address, value, symbol}]}.",
|
|
1246
|
-
"resolve_pointer",
|
|
1247
|
-
ToolCategory.SYMBOLS,
|
|
1248
|
-
[
|
|
1249
|
-
ToolParam("base", "string", "Base address or symbol", True),
|
|
1250
|
-
ToolParam(
|
|
1251
|
-
"offsets",
|
|
1252
|
-
"array",
|
|
1253
|
-
"Array of offsets, e.g. [0x100, 0x20, 0x8]",
|
|
1254
|
-
True,
|
|
1255
|
-
),
|
|
1256
|
-
ToolParam(
|
|
1257
|
-
"read_value",
|
|
1258
|
-
"boolean",
|
|
1259
|
-
"Read value at final address",
|
|
1260
|
-
False,
|
|
1261
|
-
False,
|
|
1262
|
-
),
|
|
1263
|
-
ToolParam(
|
|
1264
|
-
"value_type",
|
|
1265
|
-
"string",
|
|
1266
|
-
"Value type to read if read_value=true",
|
|
1267
|
-
False,
|
|
1268
|
-
"dword",
|
|
1269
|
-
enum=[
|
|
1270
|
-
"byte",
|
|
1271
|
-
"word",
|
|
1272
|
-
"dword",
|
|
1273
|
-
"qword",
|
|
1274
|
-
"float",
|
|
1275
|
-
"double",
|
|
1276
|
-
"pointer",
|
|
1277
|
-
],
|
|
1278
|
-
),
|
|
1279
|
-
],
|
|
1280
|
-
)
|
|
1281
|
-
)
|
|
1282
|
-
|
|
1283
|
-
def _register_debug_tools(self):
|
|
1284
|
-
"""Register debugging tools"""
|
|
1285
|
-
self._register(
|
|
1286
|
-
Tool(
|
|
1287
|
-
"ce_disassemble",
|
|
1288
|
-
"Basic disassembly - get raw instructions at an address. "
|
|
1289
|
-
"USE WHEN: You just need to see assembly code (opcodes, bytes). "
|
|
1290
|
-
"FOR DEEPER ANALYSIS: Use ce_analyze_code (extracts calls/jumps) or ce_build_cfg (function-level CFG). "
|
|
1291
|
-
"Returns: {instructions: [{address, opcode, bytes, size}], nextAddress}.",
|
|
1292
|
-
"disassemble",
|
|
1293
|
-
ToolCategory.DEBUG,
|
|
1294
|
-
[
|
|
1295
|
-
self.ADDR_PARAM,
|
|
1296
|
-
ToolParam("count", "integer", "Number of instructions", False, 10),
|
|
1297
|
-
ToolParam(
|
|
1298
|
-
"direction",
|
|
1299
|
-
"string",
|
|
1300
|
-
"Disassembly direction: forward (default) or backward using getPreviousOpcode",
|
|
1301
|
-
False,
|
|
1302
|
-
"forward",
|
|
1303
|
-
enum=["forward", "backward"],
|
|
1304
|
-
),
|
|
1305
|
-
],
|
|
1306
|
-
)
|
|
1307
|
-
)
|
|
1308
|
-
|
|
1309
|
-
self._register(
|
|
1310
|
-
Tool(
|
|
1311
|
-
"ce_get_instruction_info",
|
|
1312
|
-
"Get detailed instruction info. Returns: {address, opcode, parameters, bytes, size, isCall, isJump, isRet, isConditionalJump, readsMemory, writesMemory}.",
|
|
1313
|
-
"get_instruction_info",
|
|
1314
|
-
ToolCategory.DEBUG,
|
|
1315
|
-
[self.ADDR_PARAM],
|
|
1316
|
-
)
|
|
1317
|
-
)
|
|
1318
|
-
|
|
1319
|
-
self._register(
|
|
1320
|
-
Tool(
|
|
1321
|
-
"ce_set_breakpoint",
|
|
1322
|
-
"Set a hardware breakpoint. Returns: {success, address, type}. Use ce_remove_breakpoint to remove.",
|
|
1323
|
-
"set_breakpoint",
|
|
1324
|
-
ToolCategory.DEBUG,
|
|
1325
|
-
[
|
|
1326
|
-
self.ADDR_PARAM,
|
|
1327
|
-
ToolParam(
|
|
1328
|
-
"type",
|
|
1329
|
-
"string",
|
|
1330
|
-
"Breakpoint type",
|
|
1331
|
-
False,
|
|
1332
|
-
"execute",
|
|
1333
|
-
enum=["execute", "write", "access"],
|
|
1334
|
-
),
|
|
1335
|
-
ToolParam(
|
|
1336
|
-
"size",
|
|
1337
|
-
"integer",
|
|
1338
|
-
"Size in bytes for write/access breakpoints",
|
|
1339
|
-
False,
|
|
1340
|
-
1,
|
|
1341
|
-
),
|
|
1342
|
-
],
|
|
1343
|
-
)
|
|
1344
|
-
)
|
|
1345
|
-
|
|
1346
|
-
self._register(
|
|
1347
|
-
Tool(
|
|
1348
|
-
"ce_remove_breakpoint",
|
|
1349
|
-
"Remove a debug breakpoint",
|
|
1350
|
-
"remove_breakpoint",
|
|
1351
|
-
ToolCategory.DEBUG,
|
|
1352
|
-
[self.ADDR_PARAM],
|
|
1353
|
-
)
|
|
1354
|
-
)
|
|
1355
|
-
|
|
1356
|
-
self._register(
|
|
1357
|
-
Tool(
|
|
1358
|
-
"ce_get_breakpoints",
|
|
1359
|
-
"List all active breakpoints",
|
|
1360
|
-
"get_breakpoints",
|
|
1361
|
-
ToolCategory.DEBUG,
|
|
1362
|
-
)
|
|
1363
|
-
)
|
|
1364
|
-
|
|
1365
|
-
self._register(
|
|
1366
|
-
Tool(
|
|
1367
|
-
"ce_break_and_get_regs",
|
|
1368
|
-
"[SINGLE CAPTURE] Set breakpoint and capture registers ONCE when hit. "
|
|
1369
|
-
"USE WHEN: You need register values at ONE specific point (function args, pointer values). "
|
|
1370
|
-
"Returns all registers + call stack at breakpoint hit. "
|
|
1371
|
-
"FOR MULTI-STEP: Use ce_break_and_trace to trace execution flow. "
|
|
1372
|
-
"Returns: {triggered, registers: {rax-r15, rip, rflags}, callStack, returnAddress}.",
|
|
1373
|
-
"break_and_get_regs",
|
|
1374
|
-
ToolCategory.DEBUG,
|
|
1375
|
-
[
|
|
1376
|
-
self.ADDR_PARAM,
|
|
1377
|
-
ToolParam("timeout", "integer", "Timeout in ms", False, 5000),
|
|
1378
|
-
ToolParam(
|
|
1379
|
-
"stack_depth",
|
|
1380
|
-
"integer",
|
|
1381
|
-
"Number of stack entries to read",
|
|
1382
|
-
False,
|
|
1383
|
-
16,
|
|
1384
|
-
),
|
|
1385
|
-
ToolParam(
|
|
1386
|
-
"include_xmm",
|
|
1387
|
-
"boolean",
|
|
1388
|
-
"Include XMM registers (SSE/AVX) for floating point analysis",
|
|
1389
|
-
False,
|
|
1390
|
-
False,
|
|
1391
|
-
),
|
|
1392
|
-
],
|
|
1393
|
-
)
|
|
1394
|
-
)
|
|
1395
|
-
|
|
1396
|
-
self._register(
|
|
1397
|
-
Tool(
|
|
1398
|
-
"ce_break_and_trace",
|
|
1399
|
-
"[EXECUTION TRACE] Trace code execution step-by-step from breakpoint. Most powerful debugging tool. "
|
|
1400
|
-
"USE WHEN: Understanding algorithm logic, following data transformations, debugging execution flow. "
|
|
1401
|
-
"Captures FULL register state at EACH instruction. "
|
|
1402
|
-
"FOR SINGLE SNAPSHOT: Use ce_break_and_get_regs instead. "
|
|
1403
|
-
"Returns: {steps, stop_reason, trace: [{step, address, instruction, registers}]}. "
|
|
1404
|
-
"Stop reasons: 'ret', 'end_address', 'max_steps', 'timeout'.",
|
|
1405
|
-
"break_and_trace",
|
|
1406
|
-
ToolCategory.DEBUG,
|
|
1407
|
-
[
|
|
1408
|
-
self.ADDR_PARAM,
|
|
1409
|
-
ToolParam(
|
|
1410
|
-
"max_steps",
|
|
1411
|
-
"integer",
|
|
1412
|
-
"Maximum instructions to trace",
|
|
1413
|
-
False,
|
|
1414
|
-
100,
|
|
1415
|
-
),
|
|
1416
|
-
ToolParam("timeout", "integer", "Timeout in ms", False, 10000),
|
|
1417
|
-
ToolParam(
|
|
1418
|
-
"stop_on_ret",
|
|
1419
|
-
"boolean",
|
|
1420
|
-
"Stop tracing when ret is encountered",
|
|
1421
|
-
False,
|
|
1422
|
-
True,
|
|
1423
|
-
),
|
|
1424
|
-
ToolParam(
|
|
1425
|
-
"trace_into_call",
|
|
1426
|
-
"boolean",
|
|
1427
|
-
"Trace into call instructions (step into vs step over)",
|
|
1428
|
-
False,
|
|
1429
|
-
False,
|
|
1430
|
-
),
|
|
1431
|
-
ToolParam(
|
|
1432
|
-
"end_address",
|
|
1433
|
-
"string",
|
|
1434
|
-
"Stop tracing when this address is reached",
|
|
1435
|
-
False,
|
|
1436
|
-
),
|
|
1437
|
-
ToolParam(
|
|
1438
|
-
"initial_regs",
|
|
1439
|
-
"object",
|
|
1440
|
-
"Set register values at first hit. "
|
|
1441
|
-
'Example: {"rcx": "0x12345", "rdx": 100}',
|
|
1442
|
-
False,
|
|
1443
|
-
{},
|
|
1444
|
-
),
|
|
1445
|
-
],
|
|
1446
|
-
)
|
|
1447
|
-
)
|
|
1448
|
-
|
|
1449
|
-
def _register_analysis_tools(self):
|
|
1450
|
-
"""Register analysis tools"""
|
|
1451
|
-
self._register(
|
|
1452
|
-
Tool(
|
|
1453
|
-
"ce_find_what_accesses",
|
|
1454
|
-
"[MANUAL POINTER TRACING - STEP 1] Find code that reads/writes an address (like CE's F5 key). "
|
|
1455
|
-
"This tool monitors for 10 seconds - user must trigger memory access during this time. "
|
|
1456
|
-
"USE WHEN: ce_find_pointer_path failed and you need manual pointer tracing. "
|
|
1457
|
-
"Returns register values (e.g., RBX=0x12345678) showing what pointer was used to access the address. "
|
|
1458
|
-
"WORKFLOW: Take register value -> ce_value_scan to find where that pointer is stored -> repeat until static base.",
|
|
1459
|
-
"find_what_accesses",
|
|
1460
|
-
ToolCategory.ANALYSIS,
|
|
1461
|
-
[
|
|
1462
|
-
self.ADDR_PARAM,
|
|
1463
|
-
ToolParam(
|
|
1464
|
-
"user_prompted",
|
|
1465
|
-
"boolean",
|
|
1466
|
-
"REQUIRED: Set true ONLY after you told user 'I will monitor for 10 seconds, please change the value in game NOW'. You must prompt user BEFORE calling.",
|
|
1467
|
-
True,
|
|
1468
|
-
),
|
|
1469
|
-
ToolParam(
|
|
1470
|
-
"size", "integer", "Size to monitor (1/2/4/8 bytes)", False, 4
|
|
1471
|
-
),
|
|
1472
|
-
ToolParam(
|
|
1473
|
-
"duration_ms",
|
|
1474
|
-
"integer",
|
|
1475
|
-
"Monitoring duration in milliseconds (default 10000 = 10 seconds)",
|
|
1476
|
-
False,
|
|
1477
|
-
10000,
|
|
1478
|
-
),
|
|
1479
|
-
ToolParam(
|
|
1480
|
-
"max_records",
|
|
1481
|
-
"integer",
|
|
1482
|
-
"Maximum hit records before stopping",
|
|
1483
|
-
False,
|
|
1484
|
-
1000,
|
|
1485
|
-
),
|
|
1486
|
-
],
|
|
1487
|
-
)
|
|
1488
|
-
)
|
|
1489
|
-
|
|
1490
|
-
self._register(
|
|
1491
|
-
Tool(
|
|
1492
|
-
"ce_find_what_writes",
|
|
1493
|
-
"Find code that WRITES to an address (like CE's F6 key). Monitors writes only, ignores reads. "
|
|
1494
|
-
"This tool monitors for 10 seconds - user must trigger memory write during this time. "
|
|
1495
|
-
"USE WHEN: Finding what MODIFIES a value (e.g., what decreases player health). "
|
|
1496
|
-
"USE ce_find_what_accesses instead if you need both reads and writes for pointer tracing.",
|
|
1497
|
-
"find_what_writes",
|
|
1498
|
-
ToolCategory.ANALYSIS,
|
|
1499
|
-
[
|
|
1500
|
-
self.ADDR_PARAM,
|
|
1501
|
-
ToolParam(
|
|
1502
|
-
"user_prompted",
|
|
1503
|
-
"boolean",
|
|
1504
|
-
"REQUIRED: Set true ONLY after you told user 'I will monitor for 10 seconds, please trigger a write in game NOW'. You must prompt user BEFORE calling.",
|
|
1505
|
-
True,
|
|
1506
|
-
),
|
|
1507
|
-
ToolParam(
|
|
1508
|
-
"size", "integer", "Size to monitor (1/2/4/8 bytes)", False, 4
|
|
1509
|
-
),
|
|
1510
|
-
ToolParam(
|
|
1511
|
-
"duration_ms",
|
|
1512
|
-
"integer",
|
|
1513
|
-
"Monitoring duration in milliseconds (default 10000 = 10 seconds)",
|
|
1514
|
-
False,
|
|
1515
|
-
10000,
|
|
1516
|
-
),
|
|
1517
|
-
ToolParam(
|
|
1518
|
-
"max_records",
|
|
1519
|
-
"integer",
|
|
1520
|
-
"Maximum hit records before stopping",
|
|
1521
|
-
False,
|
|
1522
|
-
1000,
|
|
1523
|
-
),
|
|
1524
|
-
],
|
|
1525
|
-
)
|
|
1526
|
-
)
|
|
1527
|
-
|
|
1528
|
-
self._register(
|
|
1529
|
-
Tool(
|
|
1530
|
-
"ce_analyze_code",
|
|
1531
|
-
"Static analysis of code block - disassembly PLUS extracted calls, jumps, memory references. "
|
|
1532
|
-
"USE WHEN: Understanding what a code section does without execution. "
|
|
1533
|
-
"Returns call targets, jump destinations, memory access patterns. "
|
|
1534
|
-
"FOR FUNCTION-LEVEL: Use ce_build_cfg. FOR DYNAMIC ANALYSIS: Use ce_break_and_trace. "
|
|
1535
|
-
"Returns: {instructions, calls, jumps, memory_refs}.",
|
|
1536
|
-
"analyze_code",
|
|
1537
|
-
ToolCategory.ANALYSIS,
|
|
1538
|
-
[
|
|
1539
|
-
self.ADDR_PARAM,
|
|
1540
|
-
ToolParam(
|
|
1541
|
-
"count",
|
|
1542
|
-
"integer",
|
|
1543
|
-
"Number of instructions to analyze",
|
|
1544
|
-
False,
|
|
1545
|
-
20,
|
|
1546
|
-
),
|
|
1547
|
-
],
|
|
1548
|
-
)
|
|
1549
|
-
)
|
|
1550
|
-
|
|
1551
|
-
self._register(
|
|
1552
|
-
Tool(
|
|
1553
|
-
"ce_build_cfg",
|
|
1554
|
-
"Build Control Flow Graph for an entire function. "
|
|
1555
|
-
"USE WHEN: Analyzing function structure, finding loops, understanding complex branching. "
|
|
1556
|
-
"Returns basic blocks, edges, loop detection, cyclomatic complexity. "
|
|
1557
|
-
"TIP: Use ce_find_function_boundaries first if you don't know function start address. "
|
|
1558
|
-
"Returns: {entry, blocks: [{id, start, end, successors, predecessors, isLoopHeader}], edges, loops, complexity}.",
|
|
1559
|
-
"build_cfg",
|
|
1560
|
-
ToolCategory.ANALYSIS,
|
|
1561
|
-
[
|
|
1562
|
-
self.ADDR_PARAM,
|
|
1563
|
-
ToolParam(
|
|
1564
|
-
"max_instructions",
|
|
1565
|
-
"integer",
|
|
1566
|
-
"Maximum instructions to analyze (default 500)",
|
|
1567
|
-
False,
|
|
1568
|
-
500,
|
|
1569
|
-
),
|
|
1570
|
-
ToolParam(
|
|
1571
|
-
"max_blocks",
|
|
1572
|
-
"integer",
|
|
1573
|
-
"Maximum basic blocks (default 100)",
|
|
1574
|
-
False,
|
|
1575
|
-
100,
|
|
1576
|
-
),
|
|
1577
|
-
ToolParam(
|
|
1578
|
-
"detect_loops",
|
|
1579
|
-
"boolean",
|
|
1580
|
-
"Detect and annotate loop structures",
|
|
1581
|
-
False,
|
|
1582
|
-
True,
|
|
1583
|
-
),
|
|
1584
|
-
ToolParam(
|
|
1585
|
-
"include_disasm",
|
|
1586
|
-
"boolean",
|
|
1587
|
-
"Include disassembly in each block",
|
|
1588
|
-
False,
|
|
1589
|
-
True,
|
|
1590
|
-
),
|
|
1591
|
-
],
|
|
1592
|
-
)
|
|
1593
|
-
)
|
|
1594
|
-
|
|
1595
|
-
self._register(
|
|
1596
|
-
Tool(
|
|
1597
|
-
"ce_detect_patterns",
|
|
1598
|
-
"[PATTERN RECOGNITION] Detect common code patterns in a function. "
|
|
1599
|
-
"USE WHEN: Quick function classification, finding crypto code, detecting anti-debug, locating string usage. "
|
|
1600
|
-
"Detects: switch_tables (jump tables), virtual_calls (vtable calls), string_refs (string literals), "
|
|
1601
|
-
"crypto_constants (MD5/SHA/TEA/Blowfish magic numbers), anti_debug (IsDebuggerPresent/rdtsc), "
|
|
1602
|
-
"comparisons (cmp/test), memory_patterns (struct field access). "
|
|
1603
|
-
"Returns: {patterns: {switch_tables, virtual_calls, string_refs, crypto_constants, anti_debug, comparisons, memory_patterns}, "
|
|
1604
|
-
"summary: {has_switch, has_virtual_calls, has_strings, has_crypto, has_anti_debug, unique_offsets}}.",
|
|
1605
|
-
"detect_patterns",
|
|
1606
|
-
ToolCategory.ANALYSIS,
|
|
1607
|
-
[
|
|
1608
|
-
self.ADDR_PARAM,
|
|
1609
|
-
ToolParam(
|
|
1610
|
-
"max_instructions",
|
|
1611
|
-
"integer",
|
|
1612
|
-
"Maximum instructions to scan",
|
|
1613
|
-
False,
|
|
1614
|
-
200,
|
|
1615
|
-
),
|
|
1616
|
-
ToolParam(
|
|
1617
|
-
"patterns",
|
|
1618
|
-
"array",
|
|
1619
|
-
"Specific patterns to detect (omit for all)",
|
|
1620
|
-
False,
|
|
1621
|
-
),
|
|
1622
|
-
],
|
|
1623
|
-
)
|
|
1624
|
-
)
|
|
1625
|
-
|
|
1626
|
-
self._register(
|
|
1627
|
-
Tool(
|
|
1628
|
-
"ce_compare_functions",
|
|
1629
|
-
"Compare two functions for similarity. Returns matching blocks, differing instructions, "
|
|
1630
|
-
"and similarity score. Useful for patch analysis and finding similar code.",
|
|
1631
|
-
"compare_functions",
|
|
1632
|
-
ToolCategory.ANALYSIS,
|
|
1633
|
-
[
|
|
1634
|
-
ToolParam("address1", "string", "First function address", True),
|
|
1635
|
-
ToolParam("address2", "string", "Second function address", True),
|
|
1636
|
-
ToolParam(
|
|
1637
|
-
"max_instructions",
|
|
1638
|
-
"integer",
|
|
1639
|
-
"Max instructions per function",
|
|
1640
|
-
False,
|
|
1641
|
-
200,
|
|
1642
|
-
),
|
|
1643
|
-
],
|
|
1644
|
-
)
|
|
1645
|
-
)
|
|
1646
|
-
|
|
1647
|
-
self._register(
|
|
1648
|
-
Tool(
|
|
1649
|
-
"ce_trace_dataflow",
|
|
1650
|
-
"Trace how a SINGLE register's value flows through code. "
|
|
1651
|
-
"USE WHEN: 'Where does RAX get its value?' or 'Where is RCX used after this?'. "
|
|
1652
|
-
"Tracks ONE register only. FOR CROSS-REGISTER: Use ce_program_slice which follows data across mov/xchg. "
|
|
1653
|
-
"Returns: {definitions: [where value comes from], uses: [where value goes]}.",
|
|
1654
|
-
"trace_dataflow",
|
|
1655
|
-
ToolCategory.ANALYSIS,
|
|
1656
|
-
[
|
|
1657
|
-
self.ADDR_PARAM,
|
|
1658
|
-
ToolParam(
|
|
1659
|
-
"register",
|
|
1660
|
-
"string",
|
|
1661
|
-
"Register to trace (e.g. 'rax', 'rcx')",
|
|
1662
|
-
True,
|
|
1663
|
-
),
|
|
1664
|
-
ToolParam(
|
|
1665
|
-
"max_instructions",
|
|
1666
|
-
"integer",
|
|
1667
|
-
"Max instructions to analyze",
|
|
1668
|
-
False,
|
|
1669
|
-
100,
|
|
1670
|
-
),
|
|
1671
|
-
ToolParam(
|
|
1672
|
-
"direction",
|
|
1673
|
-
"string",
|
|
1674
|
-
"Trace direction: 'forward' (uses) or 'backward' (definitions)",
|
|
1675
|
-
False,
|
|
1676
|
-
"both",
|
|
1677
|
-
enum=["forward", "backward", "both"],
|
|
1678
|
-
),
|
|
1679
|
-
],
|
|
1680
|
-
)
|
|
1681
|
-
)
|
|
1682
|
-
|
|
1683
|
-
self._register(
|
|
1684
|
-
Tool(
|
|
1685
|
-
"ce_program_slice",
|
|
1686
|
-
"[ADVANCED] Compute program slice - find ALL instructions affecting a value (backward) or affected by it (forward). "
|
|
1687
|
-
"USE WHEN: Understanding 'how is this value computed' across multiple registers and memory operations. "
|
|
1688
|
-
"Unlike ce_trace_dataflow, follows data through register transfers (mov rax,rbx). "
|
|
1689
|
-
"Essential for complex algorithm reverse engineering. "
|
|
1690
|
-
"Returns: {slice_instructions, dependencies}.",
|
|
1691
|
-
"program_slice",
|
|
1692
|
-
ToolCategory.ANALYSIS,
|
|
1693
|
-
[
|
|
1694
|
-
self.ADDR_PARAM,
|
|
1695
|
-
ToolParam(
|
|
1696
|
-
"criterion",
|
|
1697
|
-
"string",
|
|
1698
|
-
"Slicing criterion: register name (e.g. 'rax') or memory pattern (e.g. '[rbx+10]')",
|
|
1699
|
-
True,
|
|
1700
|
-
),
|
|
1701
|
-
ToolParam(
|
|
1702
|
-
"direction",
|
|
1703
|
-
"string",
|
|
1704
|
-
"Slice direction: 'backward' (what affects this) or 'forward' (what this affects)",
|
|
1705
|
-
False,
|
|
1706
|
-
"backward",
|
|
1707
|
-
enum=["backward", "forward"],
|
|
1708
|
-
),
|
|
1709
|
-
ToolParam(
|
|
1710
|
-
"max_instructions",
|
|
1711
|
-
"integer",
|
|
1712
|
-
"Max instructions to analyze",
|
|
1713
|
-
False,
|
|
1714
|
-
200,
|
|
1715
|
-
),
|
|
1716
|
-
ToolParam(
|
|
1717
|
-
"follow_calls",
|
|
1718
|
-
"boolean",
|
|
1719
|
-
"Follow into called functions (increases depth but slower)",
|
|
1720
|
-
False,
|
|
1721
|
-
False,
|
|
1722
|
-
),
|
|
1723
|
-
],
|
|
1724
|
-
)
|
|
1725
|
-
)
|
|
1726
|
-
|
|
1727
|
-
self._register(
|
|
1728
|
-
Tool(
|
|
1729
|
-
"ce_analyze_struct_access",
|
|
1730
|
-
"Infer structure fields by scanning memory values at an address. "
|
|
1731
|
-
"USE WHEN: Analyzing object/struct layout, finding field offsets. "
|
|
1732
|
-
"Scans memory range and guesses field types. "
|
|
1733
|
-
"FOR DYNAMIC ANALYSIS: Use ce_trace_struct_access to see what code accesses the struct.",
|
|
1734
|
-
"analyze_struct_access",
|
|
1735
|
-
ToolCategory.ANALYSIS,
|
|
1736
|
-
[
|
|
1737
|
-
ToolParam(
|
|
1738
|
-
"base_address", "string", self.ADDR_PARAM.description, True
|
|
1739
|
-
),
|
|
1740
|
-
ToolParam(
|
|
1741
|
-
"scan_range", "integer", "Range to scan in bytes", False, 512
|
|
1742
|
-
),
|
|
1743
|
-
],
|
|
1744
|
-
)
|
|
1745
|
-
)
|
|
1746
|
-
|
|
1747
|
-
self._register(
|
|
1748
|
-
Tool(
|
|
1749
|
-
"ce_trace_struct_access",
|
|
1750
|
-
"Dynamic trace: Monitor what code accesses a memory region. "
|
|
1751
|
-
"USE WHEN: Finding which code reads/writes struct fields, understanding object usage patterns. "
|
|
1752
|
-
"FOR STATIC ANALYSIS: Use ce_analyze_struct_access to infer field types from values.",
|
|
1753
|
-
"trace_struct_access",
|
|
1754
|
-
ToolCategory.ANALYSIS,
|
|
1755
|
-
[
|
|
1756
|
-
self.ADDR_PARAM,
|
|
1757
|
-
ToolParam("size", "integer", "Size to monitor", False, 4),
|
|
1758
|
-
ToolParam(
|
|
1759
|
-
"mode",
|
|
1760
|
-
"string",
|
|
1761
|
-
"Trace mode",
|
|
1762
|
-
False,
|
|
1763
|
-
"read_write",
|
|
1764
|
-
enum=["read_write", "write"],
|
|
1765
|
-
),
|
|
1766
|
-
ToolParam(
|
|
1767
|
-
"duration_ms",
|
|
1768
|
-
"integer",
|
|
1769
|
-
"Duration in milliseconds",
|
|
1770
|
-
False,
|
|
1771
|
-
1000,
|
|
1772
|
-
),
|
|
1773
|
-
],
|
|
1774
|
-
)
|
|
1775
|
-
)
|
|
1776
|
-
|
|
1777
|
-
self._register(
|
|
1778
|
-
Tool(
|
|
1779
|
-
"ce_cleanup",
|
|
1780
|
-
"Force remove ALL breakpoints and traces set by MCP tools. "
|
|
1781
|
-
"USE WHEN: Game frozen due to stuck breakpoint, cleaning up after analysis, resetting debug state. "
|
|
1782
|
-
"Safe to call anytime - cleans up zombie resources.",
|
|
1783
|
-
"cleanup_breakpoints",
|
|
1784
|
-
ToolCategory.ANALYSIS,
|
|
1785
|
-
)
|
|
1786
|
-
)
|
|
1787
|
-
|
|
1788
|
-
self._register(
|
|
1789
|
-
Tool(
|
|
1790
|
-
"ce_find_pointer_path",
|
|
1791
|
-
"[RECOMMENDED FIRST] Automatically find static pointer path to a dynamic address. "
|
|
1792
|
-
"This tool monitors memory access internally for ~10 seconds per level - user must trigger access during this time. "
|
|
1793
|
-
"USE WHEN: You have a dynamic address and need a stable pointer chain (base+offsets). "
|
|
1794
|
-
"Returns: {success, base_address, offsets, ce_pointer_notation, steps, suggestions}. "
|
|
1795
|
-
"Strategies: 'hybrid' (recommended), 'f5', 'value_scan'. "
|
|
1796
|
-
"NOT FOR: Addresses already static (module+offset format).",
|
|
1797
|
-
"find_pointer_path",
|
|
1798
|
-
ToolCategory.ANALYSIS,
|
|
1799
|
-
[
|
|
1800
|
-
self.ADDR_PARAM,
|
|
1801
|
-
ToolParam(
|
|
1802
|
-
"user_prompted",
|
|
1803
|
-
"boolean",
|
|
1804
|
-
"REQUIRED: Set true ONLY after you told user 'I will trace pointers, please interact with the value in game NOW (change it, take damage, use feature)'. You must prompt user BEFORE calling.",
|
|
1805
|
-
True,
|
|
1806
|
-
),
|
|
1807
|
-
ToolParam(
|
|
1808
|
-
"max_depth",
|
|
1809
|
-
"integer",
|
|
1810
|
-
"Maximum pointer chain depth (1-10)",
|
|
1811
|
-
False,
|
|
1812
|
-
7,
|
|
1813
|
-
),
|
|
1814
|
-
ToolParam(
|
|
1815
|
-
"duration_ms",
|
|
1816
|
-
"integer",
|
|
1817
|
-
"Monitoring duration per level in ms (default 10000)",
|
|
1818
|
-
False,
|
|
1819
|
-
10000,
|
|
1820
|
-
),
|
|
1821
|
-
ToolParam(
|
|
1822
|
-
"max_results",
|
|
1823
|
-
"integer",
|
|
1824
|
-
"Maximum candidate pointers to evaluate per level",
|
|
1825
|
-
False,
|
|
1826
|
-
10,
|
|
1827
|
-
),
|
|
1828
|
-
ToolParam(
|
|
1829
|
-
"strategy",
|
|
1830
|
-
"string",
|
|
1831
|
-
"Search strategy: 'hybrid' (recommended, F5 + value_scan + region-based scoring), 'f5' (pure F5 method), 'value_scan' (pure pointer search)",
|
|
1832
|
-
False,
|
|
1833
|
-
"hybrid",
|
|
1834
|
-
enum=["hybrid", "f5", "value_scan"],
|
|
1835
|
-
),
|
|
1836
|
-
],
|
|
1837
|
-
)
|
|
1838
|
-
)
|
|
1839
|
-
|
|
1840
|
-
# New reference analysis tools
|
|
1841
|
-
self._register(
|
|
1842
|
-
Tool(
|
|
1843
|
-
"ce_find_references",
|
|
1844
|
-
"Find all code locations referencing an address. Returns: {address, count, references: [{address, instruction}]}.",
|
|
1845
|
-
"find_references",
|
|
1846
|
-
ToolCategory.ANALYSIS,
|
|
1847
|
-
[
|
|
1848
|
-
self.ADDR_PARAM,
|
|
1849
|
-
ToolParam(
|
|
1850
|
-
"limit", "integer", "Maximum results to return", False, 50
|
|
1851
|
-
),
|
|
1852
|
-
],
|
|
1853
|
-
)
|
|
1854
|
-
)
|
|
1855
|
-
|
|
1856
|
-
self._register(
|
|
1857
|
-
Tool(
|
|
1858
|
-
"ce_find_call_references",
|
|
1859
|
-
"Find all CALL instructions targeting a function (who calls this function). "
|
|
1860
|
-
"USE WHEN: Understanding function usage, finding entry points, tracing call hierarchy. "
|
|
1861
|
-
"Auto-detects module from target address for better performance. "
|
|
1862
|
-
"Returns: {count, callers: [{address, instruction, symbol}]}.",
|
|
1863
|
-
"find_call_references",
|
|
1864
|
-
ToolCategory.ANALYSIS,
|
|
1865
|
-
[
|
|
1866
|
-
self.ADDR_PARAM,
|
|
1867
|
-
ToolParam(
|
|
1868
|
-
"module",
|
|
1869
|
-
"string",
|
|
1870
|
-
"Limit scan to specific module (e.g. 'game.exe'). If omitted, auto-detects from target address.",
|
|
1871
|
-
),
|
|
1872
|
-
ToolParam(
|
|
1873
|
-
"limit", "integer", "Maximum results to return", False, 100
|
|
1874
|
-
),
|
|
1875
|
-
],
|
|
1876
|
-
)
|
|
1877
|
-
)
|
|
1878
|
-
|
|
1879
|
-
self._register(
|
|
1880
|
-
Tool(
|
|
1881
|
-
"ce_find_function_boundaries",
|
|
1882
|
-
"Find function start/end by prologue/epilogue patterns. "
|
|
1883
|
-
"USE WHEN: You have an address inside a function and need to find where the function starts/ends. "
|
|
1884
|
-
"WORKFLOW: Use this first, then ce_build_cfg for full function analysis. "
|
|
1885
|
-
"Returns: {function_start, function_end, size}.",
|
|
1886
|
-
"find_function_boundaries",
|
|
1887
|
-
ToolCategory.ANALYSIS,
|
|
1888
|
-
[
|
|
1889
|
-
self.ADDR_PARAM,
|
|
1890
|
-
ToolParam(
|
|
1891
|
-
"max_search",
|
|
1892
|
-
"integer",
|
|
1893
|
-
"Maximum bytes to search for boundaries",
|
|
1894
|
-
False,
|
|
1895
|
-
4096,
|
|
1896
|
-
),
|
|
1897
|
-
],
|
|
1898
|
-
)
|
|
1899
|
-
)
|
|
1900
|
-
|
|
1901
|
-
self._register(
|
|
1902
|
-
Tool(
|
|
1903
|
-
"ce_checksum_memory",
|
|
1904
|
-
"Calculate MD5 hash of a memory region. Useful for detecting code modifications.",
|
|
1905
|
-
"checksum_memory",
|
|
1906
|
-
ToolCategory.ANALYSIS,
|
|
1907
|
-
[
|
|
1908
|
-
self.ADDR_PARAM,
|
|
1909
|
-
ToolParam("size", "integer", "Number of bytes to hash", False, 256),
|
|
1910
|
-
],
|
|
1911
|
-
)
|
|
1912
|
-
)
|
|
1913
|
-
|
|
1914
|
-
self._register(
|
|
1915
|
-
Tool(
|
|
1916
|
-
"ce_generate_signature",
|
|
1917
|
-
"Generate unique AOB signature for code at an address. "
|
|
1918
|
-
"USE WHEN: Creating version-independent scripts, documenting code locations for future game updates. "
|
|
1919
|
-
"WORKFLOW: Generate signature here -> after game update, use ce_aob_scan to relocate. "
|
|
1920
|
-
"Returns: {address, signature, mask, unique}.",
|
|
1921
|
-
"generate_signature",
|
|
1922
|
-
ToolCategory.ANALYSIS,
|
|
1923
|
-
[self.ADDR_PARAM],
|
|
1924
|
-
)
|
|
1925
|
-
)
|
|
1926
|
-
|
|
1927
|
-
self._register_hook_tools()
|
|
1928
|
-
|
|
1929
|
-
def _register_hook_tools(self):
|
|
1930
|
-
"""Register function hooking tools"""
|
|
1931
|
-
self._register(
|
|
1932
|
-
Tool(
|
|
1933
|
-
"ce_hook_function",
|
|
1934
|
-
"[NON-BLOCKING] Hook a function to intercept calls and capture arguments automatically. "
|
|
1935
|
-
"USE WHEN: Monitoring function calls without stopping execution (e.g., logging all damage events). "
|
|
1936
|
-
"Captures first 4 args: x64 uses RCX/RDX/R8/R9, x32 uses stack. "
|
|
1937
|
-
"WORKFLOW: ce_hook_function -> let game run -> ce_get_hook_log to retrieve captured data. "
|
|
1938
|
-
"FOR SINGLE CAPTURE: Use ce_break_and_get_regs instead.",
|
|
1939
|
-
"hook_function",
|
|
1940
|
-
ToolCategory.ANALYSIS,
|
|
1941
|
-
[
|
|
1942
|
-
self.ADDR_PARAM,
|
|
1943
|
-
ToolParam("name", "string", "Hook identifier name", True),
|
|
1944
|
-
ToolParam(
|
|
1945
|
-
"capture_args",
|
|
1946
|
-
"integer",
|
|
1947
|
-
"Number of arguments to capture (0-4, default 4)",
|
|
1948
|
-
False,
|
|
1949
|
-
4,
|
|
1950
|
-
),
|
|
1951
|
-
ToolParam(
|
|
1952
|
-
"capture_return",
|
|
1953
|
-
"boolean",
|
|
1954
|
-
"Reserved for future use (not implemented)",
|
|
1955
|
-
False,
|
|
1956
|
-
True,
|
|
1957
|
-
),
|
|
1958
|
-
ToolParam(
|
|
1959
|
-
"max_records",
|
|
1960
|
-
"integer",
|
|
1961
|
-
"Fixed at 64 (circular buffer)",
|
|
1962
|
-
False,
|
|
1963
|
-
64,
|
|
1964
|
-
),
|
|
1965
|
-
ToolParam(
|
|
1966
|
-
"calling_convention",
|
|
1967
|
-
"string",
|
|
1968
|
-
"Calling convention hint",
|
|
1969
|
-
False,
|
|
1970
|
-
"auto",
|
|
1971
|
-
enum=["auto", "fastcall", "stdcall", "cdecl"],
|
|
1972
|
-
),
|
|
1973
|
-
],
|
|
1974
|
-
)
|
|
1975
|
-
)
|
|
1976
|
-
|
|
1977
|
-
self._register(
|
|
1978
|
-
Tool(
|
|
1979
|
-
"ce_unhook_function",
|
|
1980
|
-
"Remove a function hook by name and restore original code. "
|
|
1981
|
-
"Call this when finished analyzing to clean up resources and free memory.",
|
|
1982
|
-
"unhook_function",
|
|
1983
|
-
ToolCategory.ANALYSIS,
|
|
1984
|
-
[ToolParam("name", "string", "Hook identifier name", True)],
|
|
1985
|
-
)
|
|
1986
|
-
)
|
|
1987
|
-
|
|
1988
|
-
self._register(
|
|
1989
|
-
Tool(
|
|
1990
|
-
"ce_list_hooks",
|
|
1991
|
-
"List all active function hooks with their status and call counts. "
|
|
1992
|
-
"Use to check which hooks are currently installed before adding new ones.",
|
|
1993
|
-
"list_hooks",
|
|
1994
|
-
ToolCategory.ANALYSIS,
|
|
1995
|
-
)
|
|
1996
|
-
)
|
|
1997
|
-
|
|
1998
|
-
self._register(
|
|
1999
|
-
Tool(
|
|
2000
|
-
"ce_get_hook_log",
|
|
2001
|
-
"Get captured function call arguments. Returns entries with args[1-4] containing: "
|
|
2002
|
-
"x64: RCX(this/arg1), RDX(arg2), R8(arg3), R9(arg4); "
|
|
2003
|
-
"x32: stack params [esp+10/14/18/1C]. "
|
|
2004
|
-
"Values are hex addresses/integers. total_calls shows how many times function was called.",
|
|
2005
|
-
"get_hook_log",
|
|
2006
|
-
ToolCategory.ANALYSIS,
|
|
2007
|
-
[
|
|
2008
|
-
ToolParam("name", "string", "Hook identifier name", True),
|
|
2009
|
-
ToolParam(
|
|
2010
|
-
"clear", "boolean", "Clear log after reading", False, False
|
|
2011
|
-
),
|
|
2012
|
-
ToolParam(
|
|
2013
|
-
"limit",
|
|
2014
|
-
"integer",
|
|
2015
|
-
"Max records to return (1-64, default 50)",
|
|
2016
|
-
False,
|
|
2017
|
-
50,
|
|
2018
|
-
),
|
|
2019
|
-
],
|
|
2020
|
-
)
|
|
2021
|
-
)
|
|
2022
|
-
|
|
2023
|
-
self._register(
|
|
2024
|
-
Tool(
|
|
2025
|
-
"ce_clear_hook_log",
|
|
2026
|
-
"Clear the call log and reset total_calls counter for a hook. "
|
|
2027
|
-
"Use when: 1) Starting fresh measurement, 2) Log buffer full (64 entries max), "
|
|
2028
|
-
"3) Want to count calls from a specific point. Omit name to clear all hooks.",
|
|
2029
|
-
"clear_hook_log",
|
|
2030
|
-
ToolCategory.ANALYSIS,
|
|
2031
|
-
[ToolParam("name", "string", "Hook name (omit to clear all)")],
|
|
2032
|
-
)
|
|
2033
|
-
)
|
|
2034
|
-
|
|
2035
|
-
self._register_emulation_tools()
|
|
2036
|
-
|
|
2037
|
-
def _register_emulation_tools(self):
|
|
2038
|
-
"""Register code emulation and symbolic execution tools"""
|
|
2039
|
-
self._register(
|
|
2040
|
-
Tool(
|
|
2041
|
-
"ce_call_function",
|
|
2042
|
-
"[DANGEROUS] Call a function in target process - EXECUTES REAL CODE! "
|
|
2043
|
-
"USE WHEN: Testing function behavior, calling game functions programmatically. "
|
|
2044
|
-
"Uses x64 fastcall (RCX, RDX, R8, R9). May crash game if used incorrectly. "
|
|
2045
|
-
"Returns: {success, return_value, return_type}.",
|
|
2046
|
-
"call_function",
|
|
2047
|
-
ToolCategory.ANALYSIS,
|
|
2048
|
-
[
|
|
2049
|
-
self.ADDR_PARAM,
|
|
2050
|
-
ToolParam(
|
|
2051
|
-
"args",
|
|
2052
|
-
"array",
|
|
2053
|
-
"Function arguments (up to 4 for fastcall). Can be integers or address expressions.",
|
|
2054
|
-
False,
|
|
2055
|
-
[],
|
|
2056
|
-
),
|
|
2057
|
-
ToolParam(
|
|
2058
|
-
"timeout", "integer", "Timeout in milliseconds", False, 5000
|
|
2059
|
-
),
|
|
2060
|
-
ToolParam(
|
|
2061
|
-
"return_type",
|
|
2062
|
-
"string",
|
|
2063
|
-
"How to interpret return value",
|
|
2064
|
-
False,
|
|
2065
|
-
"qword",
|
|
2066
|
-
enum=[
|
|
2067
|
-
"byte",
|
|
2068
|
-
"word",
|
|
2069
|
-
"dword",
|
|
2070
|
-
"qword",
|
|
2071
|
-
"float",
|
|
2072
|
-
"double",
|
|
2073
|
-
"pointer",
|
|
2074
|
-
],
|
|
2075
|
-
),
|
|
2076
|
-
],
|
|
2077
|
-
)
|
|
2078
|
-
)
|
|
2079
|
-
|
|
2080
|
-
self._register(
|
|
2081
|
-
Tool(
|
|
2082
|
-
"ce_symbolic_trace",
|
|
2083
|
-
"[NO EXECUTION] Lightweight symbolic execution - interprets instructions WITHOUT running them. "
|
|
2084
|
-
"USE WHEN: Understanding code logic safely, analyzing potentially dangerous code, static algorithm analysis. "
|
|
2085
|
-
"Produces readable expressions like 'rax = ((arg0 + 5) << 2)'. "
|
|
2086
|
-
"Supports: mov, movzx, movsxd, lea, add, sub, xor, and, or, shl, shr, imul, cmp, test, cmovxx. "
|
|
2087
|
-
"FOR ACTUAL EXECUTION: Use ce_break_and_trace instead. "
|
|
2088
|
-
"Returns: {trace: [{address, instruction, effects}], final_state: {reg: expression}, stop_reason}.",
|
|
2089
|
-
"symbolic_trace",
|
|
2090
|
-
ToolCategory.ANALYSIS,
|
|
2091
|
-
[
|
|
2092
|
-
self.ADDR_PARAM,
|
|
2093
|
-
ToolParam(
|
|
2094
|
-
"count", "integer", "Number of instructions to trace", False, 30
|
|
2095
|
-
),
|
|
2096
|
-
ToolParam(
|
|
2097
|
-
"initial_state",
|
|
2098
|
-
"object",
|
|
2099
|
-
"Initial register values as symbols or concrete values. "
|
|
2100
|
-
'Example: {"rcx": "this_ptr", "rdx": "arg1", "r8": 0}',
|
|
2101
|
-
False,
|
|
2102
|
-
{},
|
|
2103
|
-
),
|
|
2104
|
-
ToolParam(
|
|
2105
|
-
"stop_on_call",
|
|
2106
|
-
"boolean",
|
|
2107
|
-
"Stop tracing when encountering a call instruction",
|
|
2108
|
-
False,
|
|
2109
|
-
True,
|
|
2110
|
-
),
|
|
2111
|
-
ToolParam(
|
|
2112
|
-
"stop_on_ret",
|
|
2113
|
-
"boolean",
|
|
2114
|
-
"Stop tracing when encountering a ret instruction",
|
|
2115
|
-
False,
|
|
2116
|
-
True,
|
|
2117
|
-
),
|
|
2118
|
-
ToolParam(
|
|
2119
|
-
"simplify",
|
|
2120
|
-
"boolean",
|
|
2121
|
-
"Simplify expressions (e.g., x^x=0, x+0=x)",
|
|
2122
|
-
False,
|
|
2123
|
-
True,
|
|
2124
|
-
),
|
|
2125
|
-
],
|
|
2126
|
-
)
|
|
2127
|
-
)
|
|
2128
|
-
|
|
2129
|
-
def get_tool(self, name: str) -> Optional[Tool]:
|
|
2130
|
-
"""Get tool by name"""
|
|
2131
|
-
return self.tools.get(name)
|
|
2132
|
-
|
|
2133
|
-
def get_all_schemas(self) -> List[dict]:
|
|
2134
|
-
"""Get all tool schemas for MCP"""
|
|
2135
|
-
return [tool.to_mcp_schema() for tool in self.tools.values()]
|
|
2136
|
-
|
|
2137
|
-
def get_lua_command(self, tool_name: str) -> Optional[str]:
|
|
2138
|
-
"""Get Lua command for a tool"""
|
|
2139
|
-
tool = self.tools.get(tool_name)
|
|
2140
|
-
return tool.lua_command if tool else None
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
# ============ MCP Server ============
|
|
2144
|
-
class CEMCPServer:
|
|
2145
|
-
"""Main MCP Server implementation"""
|
|
2146
|
-
|
|
2147
|
-
def __init__(self):
|
|
2148
|
-
self.pipe_client = PipeClient()
|
|
2149
|
-
self.pipe_client.start_background_reconnect() # Always run background reconnect
|
|
2150
|
-
self.tool_registry = ToolRegistry()
|
|
2151
|
-
self.timeout_manager = TimeoutManager()
|
|
2152
|
-
self.metrics_collector = MetricsCollector()
|
|
2153
|
-
self.request_count = 0
|
|
2154
|
-
|
|
2155
|
-
def _diagnose_connection(self) -> dict:
|
|
2156
|
-
"""Diagnose connection issues without requiring CE connection"""
|
|
2157
|
-
import subprocess
|
|
2158
|
-
|
|
2159
|
-
checks = []
|
|
2160
|
-
suggestions = []
|
|
2161
|
-
all_passed = True
|
|
2162
|
-
|
|
2163
|
-
# Check 1: Pipe existence
|
|
2164
|
-
pipe_exists = False
|
|
2165
|
-
try:
|
|
2166
|
-
test_pipe = win32file.CreateFile(
|
|
2167
|
-
self.pipe_client.pipe_name,
|
|
2168
|
-
win32file.GENERIC_READ | win32file.GENERIC_WRITE,
|
|
2169
|
-
0,
|
|
2170
|
-
None,
|
|
2171
|
-
win32file.OPEN_EXISTING,
|
|
2172
|
-
0,
|
|
2173
|
-
None,
|
|
2174
|
-
)
|
|
2175
|
-
win32file.CloseHandle(test_pipe)
|
|
2176
|
-
pipe_exists = True
|
|
2177
|
-
checks.append(
|
|
2178
|
-
{
|
|
2179
|
-
"name": "pipe_exists",
|
|
2180
|
-
"passed": True,
|
|
2181
|
-
"message": f"Pipe '{Config.PIPE_NAME}' available",
|
|
2182
|
-
}
|
|
2183
|
-
)
|
|
2184
|
-
except pywintypes.error as e:
|
|
2185
|
-
all_passed = False
|
|
2186
|
-
if e.winerror == winerror.ERROR_FILE_NOT_FOUND:
|
|
2187
|
-
checks.append(
|
|
2188
|
-
{
|
|
2189
|
-
"name": "pipe_exists",
|
|
2190
|
-
"passed": False,
|
|
2191
|
-
"message": "Pipe not found - CE bridge not running",
|
|
2192
|
-
}
|
|
2193
|
-
)
|
|
2194
|
-
suggestions.extend(
|
|
2195
|
-
[
|
|
2196
|
-
"1. Open Cheat Engine",
|
|
2197
|
-
"2. Table -> Show Cheat Table Lua Script",
|
|
2198
|
-
"3. Paste ce_mcp_bridge.lua content and Execute",
|
|
2199
|
-
"4. Look for '[CheatEngine-MCP] Bridge started' message",
|
|
2200
|
-
]
|
|
2201
|
-
)
|
|
2202
|
-
elif e.winerror == winerror.ERROR_PIPE_BUSY:
|
|
2203
|
-
checks.append(
|
|
2204
|
-
{
|
|
2205
|
-
"name": "pipe_exists",
|
|
2206
|
-
"passed": True,
|
|
2207
|
-
"message": "Pipe busy - another client connected",
|
|
2208
|
-
}
|
|
2209
|
-
)
|
|
2210
|
-
suggestions.append("Close other MCP connections first")
|
|
2211
|
-
elif e.winerror == winerror.ERROR_ACCESS_DENIED:
|
|
2212
|
-
checks.append(
|
|
2213
|
-
{"name": "pipe_exists", "passed": False, "message": "Access denied"}
|
|
2214
|
-
)
|
|
2215
|
-
suggestions.append("Try running with administrator privileges")
|
|
2216
|
-
else:
|
|
2217
|
-
checks.append(
|
|
2218
|
-
{
|
|
2219
|
-
"name": "pipe_exists",
|
|
2220
|
-
"passed": False,
|
|
2221
|
-
"message": f"Pipe error: {e}",
|
|
2222
|
-
}
|
|
2223
|
-
)
|
|
2224
|
-
|
|
2225
|
-
# Check 2: CE process
|
|
2226
|
-
try:
|
|
2227
|
-
result = subprocess.run(
|
|
2228
|
-
["tasklist", "/FI", "IMAGENAME eq cheatengine*", "/FO", "CSV", "/NH"],
|
|
2229
|
-
capture_output=True,
|
|
2230
|
-
text=True,
|
|
2231
|
-
timeout=5,
|
|
2232
|
-
)
|
|
2233
|
-
ce_running = "cheatengine" in result.stdout.lower()
|
|
2234
|
-
if ce_running:
|
|
2235
|
-
checks.append(
|
|
2236
|
-
{
|
|
2237
|
-
"name": "ce_process",
|
|
2238
|
-
"passed": True,
|
|
2239
|
-
"message": "Cheat Engine is running",
|
|
2240
|
-
}
|
|
2241
|
-
)
|
|
2242
|
-
else:
|
|
2243
|
-
all_passed = False
|
|
2244
|
-
checks.append(
|
|
2245
|
-
{
|
|
2246
|
-
"name": "ce_process",
|
|
2247
|
-
"passed": False,
|
|
2248
|
-
"message": "Cheat Engine not found",
|
|
2249
|
-
}
|
|
2250
|
-
)
|
|
2251
|
-
suggestions.insert(0, "Start Cheat Engine first!")
|
|
2252
|
-
except Exception as e:
|
|
2253
|
-
checks.append(
|
|
2254
|
-
{
|
|
2255
|
-
"name": "ce_process",
|
|
2256
|
-
"passed": None,
|
|
2257
|
-
"message": f"Could not check: {e}",
|
|
2258
|
-
}
|
|
2259
|
-
)
|
|
2260
|
-
|
|
2261
|
-
# Check 3: Custom pipe name
|
|
2262
|
-
custom_pipe = os.environ.get("CE_MCP_PIPE_NAME")
|
|
2263
|
-
if custom_pipe:
|
|
2264
|
-
checks.append(
|
|
2265
|
-
{
|
|
2266
|
-
"name": "custom_pipe",
|
|
2267
|
-
"passed": None,
|
|
2268
|
-
"message": f"Using custom pipe: {custom_pipe}",
|
|
2269
|
-
}
|
|
2270
|
-
)
|
|
2271
|
-
suggestions.append(f"Ensure CE Lua also uses pipe name: {custom_pipe}")
|
|
2272
|
-
|
|
2273
|
-
return {
|
|
2274
|
-
"status": "ok" if (all_passed and pipe_exists) else "failed",
|
|
2275
|
-
"pipe_name": Config.PIPE_NAME,
|
|
2276
|
-
"checks": checks,
|
|
2277
|
-
"suggestions": suggestions or ["Connection should work - try again"],
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
def execute_tool(self, name: str, args: dict) -> dict:
|
|
2281
|
-
"""Execute a tool by sending command to Lua bridge
|
|
2282
|
-
|
|
2283
|
-
Uses TimeoutManager for tool-specific timeouts.
|
|
2284
|
-
Uses MetricsCollector to record call metrics.
|
|
2285
|
-
|
|
2286
|
-
**Validates: Requirements 2.1, 2.2, 2.3, 2.4, 4.4**
|
|
2287
|
-
"""
|
|
2288
|
-
lua_cmd = self.tool_registry.get_lua_command(name)
|
|
2289
|
-
if not lua_cmd:
|
|
2290
|
-
return {"error": f"Unknown tool: {name}"}
|
|
2291
|
-
|
|
2292
|
-
timeout_seconds = self.timeout_manager.get_timeout(name)
|
|
2293
|
-
timeout_ms = timeout_seconds * 1000
|
|
2294
|
-
start_time = time.time()
|
|
2295
|
-
|
|
2296
|
-
response = self.pipe_client.send_receive(
|
|
2297
|
-
{"command": lua_cmd, "params": args}, timeout=timeout_ms, tool_name=name
|
|
2298
|
-
)
|
|
2299
|
-
|
|
2300
|
-
duration = time.time() - start_time
|
|
2301
|
-
is_error = "error" in response
|
|
2302
|
-
self.metrics_collector.record_call(name, duration, is_error)
|
|
2303
|
-
|
|
2304
|
-
if is_error:
|
|
2305
|
-
# For ce_ping, return diagnostic info on connection failure
|
|
2306
|
-
if name == "ce_ping":
|
|
2307
|
-
diag = self._diagnose_connection()
|
|
2308
|
-
return {"error": response["error"], "diagnostic": diag}
|
|
2309
|
-
if "timeout_info" in response:
|
|
2310
|
-
return {
|
|
2311
|
-
"error": response["error"],
|
|
2312
|
-
"timeout_info": response["timeout_info"],
|
|
2313
|
-
}
|
|
2314
|
-
return {"error": response["error"]}
|
|
2315
|
-
|
|
2316
|
-
result = response.get("result", response)
|
|
2317
|
-
|
|
2318
|
-
# Attach health and server metrics to ce_ping response
|
|
2319
|
-
if name == "ce_ping":
|
|
2320
|
-
health_metrics = self.pipe_client.get_health_metrics()
|
|
2321
|
-
server_metrics = self.metrics_collector.get_summary()
|
|
2322
|
-
if isinstance(result, dict):
|
|
2323
|
-
result["connection_health"] = health_metrics
|
|
2324
|
-
result["server_metrics"] = server_metrics
|
|
2325
|
-
else:
|
|
2326
|
-
result = {
|
|
2327
|
-
"result": result,
|
|
2328
|
-
"connection_health": health_metrics,
|
|
2329
|
-
"server_metrics": server_metrics,
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
return result
|
|
2333
|
-
|
|
2334
|
-
def handle_request(self, req: dict) -> Optional[dict]:
|
|
2335
|
-
"""Handle incoming MCP request"""
|
|
2336
|
-
method = req.get("method", "")
|
|
2337
|
-
req_id = req.get("id")
|
|
2338
|
-
params = req.get("params", {})
|
|
2339
|
-
|
|
2340
|
-
if method == "initialize":
|
|
2341
|
-
return self._handle_initialize(req_id)
|
|
2342
|
-
elif method == "notifications/initialized":
|
|
2343
|
-
return None
|
|
2344
|
-
elif method == "tools/list":
|
|
2345
|
-
return self._handle_tools_list(req_id)
|
|
2346
|
-
elif method == "tools/call":
|
|
2347
|
-
return self._handle_tools_call(req_id, params)
|
|
2348
|
-
else:
|
|
2349
|
-
return self._error_response(req_id, -32601, f"Method not found: {method}")
|
|
2350
|
-
|
|
2351
|
-
def _handle_initialize(self, req_id) -> dict:
|
|
2352
|
-
"""Handle initialize request"""
|
|
2353
|
-
return {
|
|
2354
|
-
"jsonrpc": "2.0",
|
|
2355
|
-
"id": req_id,
|
|
2356
|
-
"result": {
|
|
2357
|
-
"protocolVersion": "2024-11-05",
|
|
2358
|
-
"capabilities": {"tools": {}},
|
|
2359
|
-
"serverInfo": {"name": "cheatengine-mcp-bridge", "version": "5.8.13"},
|
|
2360
|
-
"instructions": (
|
|
2361
|
-
"# Cheat Engine MCP - AI Usage Guide\n\n"
|
|
2362
|
-
"## Tool Selection Decision Tree\n\n"
|
|
2363
|
-
"### Q: Do I know the address?\n"
|
|
2364
|
-
"- NO, but can observe value changes -> ce_scan_new workflow (value hunting)\n"
|
|
2365
|
-
"- YES, need stable pointer -> ce_find_pointer_path (auto) or manual F5+value_scan\n"
|
|
2366
|
-
"- YES, want to read/write -> ce_read_memory / ce_write_memory\n\n"
|
|
2367
|
-
"### Q: What am I searching for?\n"
|
|
2368
|
-
"- Game values (health, gold) -> ce_scan_new (NOT ce_value_scan!)\n"
|
|
2369
|
-
"- Code signatures -> ce_aob_scan (supports ?? wildcards)\n"
|
|
2370
|
-
"- Pointer storage locations -> ce_value_scan (after getting register value from F5)\n\n"
|
|
2371
|
-
"### Q: How to analyze code?\n"
|
|
2372
|
-
"- View execution flow -> ce_break_and_trace (dynamic, real execution)\n"
|
|
2373
|
-
"- Safely understand algorithm -> ce_symbolic_trace (static, no execution)\n"
|
|
2374
|
-
"- Function structure -> ce_build_cfg + ce_detect_patterns\n\n"
|
|
2375
|
-
"## Workflow Templates\n\n"
|
|
2376
|
-
"### 1. Value Hunting (find unknown address)\n"
|
|
2377
|
-
"1. ce_attach_process(target='game.exe')\n"
|
|
2378
|
-
"2. ce_scan_new(value='100', type='dword') -> returns session_id\n"
|
|
2379
|
-
"3. [Take damage in game, health becomes 95]\n"
|
|
2380
|
-
"4. ce_scan_next(session_id, value='95', scan_type='exact') -> count decreases\n"
|
|
2381
|
-
"5. Repeat steps 3-4 until count < 10\n"
|
|
2382
|
-
"6. ce_scan_results(session_id) -> get candidate addresses\n"
|
|
2383
|
-
"7. ce_read_memory(address, type='dword') -> verify correct address\n"
|
|
2384
|
-
"8. ce_scan_close(session_id) -> release resources (max 5 concurrent!)\n\n"
|
|
2385
|
-
"### 2. Pointer Tracing (make address stable)\n"
|
|
2386
|
-
"Method A (auto, try first): ce_find_pointer_path(address='0x12345678')\n"
|
|
2387
|
-
"Method B (manual, if A fails):\n"
|
|
2388
|
-
"1. ce_find_what_accesses(address='0x12345678', duration_ms=10000)\n"
|
|
2389
|
-
"2. Get register value from result (e.g., RBX=0x255D5E758)\n"
|
|
2390
|
-
"3. ce_value_scan(value='0x255D5E758', type='qword')\n"
|
|
2391
|
-
"4. Find result with isStatic=true -> that's your base pointer!\n\n"
|
|
2392
|
-
"### 3. Function Monitoring (non-blocking)\n"
|
|
2393
|
-
"1. ce_hook_function(address='game.exe+12345', name='damageHook')\n"
|
|
2394
|
-
"2. [Let game run, trigger events]\n"
|
|
2395
|
-
"3. ce_get_hook_log(name='damageHook') -> captured args (x64: RCX,RDX,R8,R9)\n"
|
|
2396
|
-
"4. ce_unhook_function(name='damageHook') -> cleanup when done\n\n"
|
|
2397
|
-
"## Tool Comparison (Correct vs Wrong)\n"
|
|
2398
|
-
"| Purpose | Correct | Wrong |\n"
|
|
2399
|
-
"| Find game values | ce_scan_new | ce_value_scan |\n"
|
|
2400
|
-
"| Find code signature | ce_aob_scan | ce_scan_new |\n"
|
|
2401
|
-
"| Batch read memory | ce_read_memory_batch | multiple ce_read_memory |\n"
|
|
2402
|
-
"| Monitor function | ce_hook_function | ce_break_and_get_regs |\n\n"
|
|
2403
|
-
"## System Limits\n"
|
|
2404
|
-
"- Scan sessions: max 5 (auto-expire after 5 min inactivity)\n"
|
|
2405
|
-
"- Hardware breakpoints: max 4 (shared by F5/F6/trace)\n"
|
|
2406
|
-
"- Hook buffer: 64 records (circular, oldest overwritten when full)\n"
|
|
2407
|
-
"- Pointer depth: max 10 levels\n"
|
|
2408
|
-
"- Message size: 10MB per request/response\n\n"
|
|
2409
|
-
"## Common Errors\n"
|
|
2410
|
-
"- 'Process not attached' -> call ce_attach_process first\n"
|
|
2411
|
-
"- 'Too many scan sessions' -> ce_scan_list() then ce_scan_close()\n"
|
|
2412
|
-
"- 'Hardware breakpoint limit' -> ce_cleanup() to clear all\n"
|
|
2413
|
-
"- 'Hook name already exists' -> ce_unhook_function(name) first"
|
|
2414
|
-
),
|
|
2415
|
-
},
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
def _handle_tools_list(self, req_id) -> dict:
|
|
2419
|
-
"""Handle tools/list request"""
|
|
2420
|
-
return {
|
|
2421
|
-
"jsonrpc": "2.0",
|
|
2422
|
-
"id": req_id,
|
|
2423
|
-
"result": {"tools": self.tool_registry.get_all_schemas()},
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
def _handle_tools_call(self, req_id, params: dict) -> dict:
|
|
2427
|
-
"""Handle tools/call request"""
|
|
2428
|
-
tool_name = params.get("name", "")
|
|
2429
|
-
tool_args = params.get("arguments", {})
|
|
2430
|
-
|
|
2431
|
-
try:
|
|
2432
|
-
result = self.execute_tool(tool_name, tool_args)
|
|
2433
|
-
|
|
2434
|
-
# Check if error: has error field, success=false, and no partial results
|
|
2435
|
-
# For tools like resolve_pointer, return partial results even on failure
|
|
2436
|
-
is_error = False
|
|
2437
|
-
if isinstance(result, dict) and "error" in result:
|
|
2438
|
-
has_partial_result = any(
|
|
2439
|
-
k in result
|
|
2440
|
-
for k in ["chain", "path", "partialCENotation", "finalAddress"]
|
|
2441
|
-
)
|
|
2442
|
-
is_error = result.get("success") == False and not has_partial_result
|
|
2443
|
-
|
|
2444
|
-
text = (
|
|
2445
|
-
f"Error: {result['error']}"
|
|
2446
|
-
if is_error
|
|
2447
|
-
else json.dumps(result, ensure_ascii=False, indent=2)
|
|
2448
|
-
)
|
|
2449
|
-
|
|
2450
|
-
return {
|
|
2451
|
-
"jsonrpc": "2.0",
|
|
2452
|
-
"id": req_id,
|
|
2453
|
-
"result": {
|
|
2454
|
-
"content": [{"type": "text", "text": text}],
|
|
2455
|
-
"isError": is_error,
|
|
2456
|
-
},
|
|
2457
|
-
}
|
|
2458
|
-
except Exception as e:
|
|
2459
|
-
return self._error_response(req_id, -32603, str(e))
|
|
2460
|
-
|
|
2461
|
-
def _error_response(self, req_id, code: int, message: str) -> dict:
|
|
2462
|
-
"""Create error response"""
|
|
2463
|
-
return {
|
|
2464
|
-
"jsonrpc": "2.0",
|
|
2465
|
-
"id": req_id,
|
|
2466
|
-
"error": {"code": code, "message": message},
|
|
2467
|
-
}
|
|
2468
|
-
|
|
2469
|
-
def run(self):
|
|
2470
|
-
"""Main server loop"""
|
|
2471
|
-
log.info(f"Cheat Engine MCP Bridge Started. Waiting for input...")
|
|
2472
|
-
log.info(f"Python version: {sys.version}")
|
|
2473
|
-
|
|
2474
|
-
try:
|
|
2475
|
-
while True:
|
|
2476
|
-
line = sys.stdin.readline()
|
|
2477
|
-
if not line:
|
|
2478
|
-
break
|
|
2479
|
-
|
|
2480
|
-
self.request_count += 1
|
|
2481
|
-
|
|
2482
|
-
try:
|
|
2483
|
-
request = json.loads(line)
|
|
2484
|
-
response = self.handle_request(request)
|
|
2485
|
-
if response:
|
|
2486
|
-
sys.stdout.write(json.dumps(response) + "\n")
|
|
2487
|
-
sys.stdout.flush()
|
|
2488
|
-
except json.JSONDecodeError as e:
|
|
2489
|
-
log.error(f"Failed to decode JSON: {e}")
|
|
2490
|
-
except Exception as e:
|
|
2491
|
-
log.error(
|
|
2492
|
-
f"Critical Error (request #{self.request_count}): {traceback.format_exc()}"
|
|
2493
|
-
)
|
|
2494
|
-
|
|
2495
|
-
except KeyboardInterrupt:
|
|
2496
|
-
log.info("Received interrupt signal")
|
|
2497
|
-
finally:
|
|
2498
|
-
self.pipe_client.stop()
|
|
2499
|
-
log.info(f"Server Stopped (processed {self.request_count} requests)")
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
# ============ Entry Point ============
|
|
2503
|
-
def main():
|
|
2504
|
-
server = CEMCPServer()
|
|
2505
|
-
server.run()
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
if __name__ == "__main__":
|
|
2509
|
-
main()
|