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/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()