cheatengine 5.8.14 → 5.8.16

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 CHANGED
@@ -1,89 +1,45 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Cheat Engine MCP Server - NPX wrapper
4
- * 调用Python运行ce_mcp_server.py
3
+ * Cheat Engine MCP Server - CLI Entry Point
4
+ * Works with: npm install -g, npx, and local installs
5
5
  */
6
6
 
7
- const { spawn, execSync } = require('child_process');
8
7
  const path = require('path');
9
8
  const fs = require('fs');
10
9
 
11
- // 调试模式
10
+ // Debug mode
12
11
  const DEBUG = process.env.DEBUG === '1';
13
12
  function log(...args) {
14
13
  if (DEBUG) console.error('[cheatengine]', ...args);
15
14
  }
16
15
 
17
- log('Starting cheatengine wrapper...');
16
+ log('Starting cheatengine MCP server...');
18
17
 
19
- // 找到包根目录(兼容 npx/npm install 各种情况)
20
- function findPackageRoot() {
21
- // 方法1: 通过 package.json 定位
22
- try {
23
- const pkgPath = require.resolve('cheatengine/package.json');
24
- return path.dirname(pkgPath);
25
- } catch (e) {
26
- // 方法2: 通过当前文件相对路径
27
- return path.join(__dirname, '..');
28
- }
29
- }
30
-
31
- const packageRoot = findPackageRoot();
18
+ // Get package root directory (__dirname is bin/ in the package)
19
+ const packageRoot = path.join(__dirname, '..');
32
20
  log('Package root:', packageRoot);
33
21
 
34
- const pythonScript = path.join(packageRoot, 'ce_mcp_server.py');
35
- log('Python script path:', pythonScript);
22
+ const serverScript = path.join(packageRoot, 'ce_mcp_server.js');
23
+ log('Server script:', serverScript);
36
24
 
37
- // 验证Python脚本存在
38
- if (!fs.existsSync(pythonScript)) {
39
- console.error('[cheatengine] 错误: 找不到 ce_mcp_server.py');
40
- console.error('查找路径:', pythonScript);
41
- console.error('包根目录:', packageRoot);
25
+ // Verify script exists
26
+ if (!fs.existsSync(serverScript)) {
27
+ console.error('[cheatengine] Error: Cannot find ce_mcp_server.js');
28
+ console.error('Expected at:', serverScript);
42
29
  process.exit(1);
43
30
  }
44
31
 
45
- // 检测Python命令 - Windows上尝试多个选项
46
- let pythonCmd = 'python';
47
- if (process.platform === 'win32') {
48
- // Windows: 尝试 python, python3, py
49
- for (const cmd of ['python', 'python3', 'py']) {
50
- try {
51
- execSync(`${cmd} --version`, { stdio: 'ignore' });
52
- pythonCmd = cmd;
53
- log('Found Python:', cmd);
54
- break;
55
- } catch (e) {
56
- // 尝试下一个
57
- }
58
- }
32
+ // Ensure stdin/stdout are in proper mode for MCP
33
+ if (process.stdin.isTTY) {
34
+ process.stdin.setRawMode(false);
59
35
  }
60
36
 
61
- log('Using Python command:', pythonCmd);
62
-
63
- const proc = spawn(pythonCmd, [pythonScript], {
64
- stdio: ['pipe', 'pipe', 'pipe'],
65
- windowsHide: true
66
- });
67
-
68
- log('Python process spawned, PID:', proc.pid);
69
-
70
- // 透传stdin/stdout/stderr
71
- process.stdin.pipe(proc.stdin);
72
- proc.stdout.pipe(process.stdout);
73
- proc.stderr.pipe(process.stderr);
74
-
75
- proc.on('exit', (code, signal) => {
76
- log('Python process exited, code:', code, 'signal:', signal);
77
- process.exit(code ?? 0);
78
- });
79
-
80
- proc.on('error', (err) => {
81
- console.error('[cheatengine] 启动失败:', err.message);
82
- console.error('请确保已安装Python和pywin32: pip install pywin32');
37
+ // Run the server
38
+ log('Loading MCP server...');
39
+ try {
40
+ require(serverScript);
41
+ } catch (err) {
42
+ console.error('[cheatengine] Failed to start:', err.message);
43
+ console.error(err.stack);
83
44
  process.exit(1);
84
- });
85
-
86
- // 如果3秒内没有退出,说明启动成功
87
- setTimeout(() => {
88
- log('Python process still running after 3s');
89
- }, 3000);
45
+ }
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env node
2
+ // -*- coding: utf-8 -*-
3
+ /**
4
+ * Cheat Engine MCP Server - Main Server Implementation
5
+ * JavaScript/Node.js version
6
+ */
7
+
8
+ const { Config, TimeoutManager, MetricsCollector, log } = require('./src/base');
9
+ const { PipeClient } = require('./src/pipe-client');
10
+ const { ToolRegistry } = require('./src/tool-registry');
11
+
12
+ // ============ MCP Server ============
13
+ class CEMCPServer {
14
+ constructor() {
15
+ this.pipeClient = new PipeClient();
16
+ this.pipeClient.startBackgroundReconnect();
17
+ this.toolRegistry = new ToolRegistry();
18
+ this.timeoutManager = new TimeoutManager();
19
+ this.metricsCollector = new MetricsCollector();
20
+ this.requestCount = 0;
21
+ }
22
+
23
+ async _diagnoseConnection() {
24
+ const checks = [];
25
+ const suggestions = [];
26
+ let allPassed = true;
27
+
28
+ // Check 1: Pipe existence
29
+ let pipeExists = false;
30
+ try {
31
+ const fs = require('fs');
32
+ // Try to check if pipe exists (Windows specific)
33
+ const testClient = new PipeClient();
34
+ const connected = await testClient.connect(false, 1000);
35
+ if (connected) {
36
+ pipeExists = true;
37
+ testClient.stop();
38
+ checks.push({
39
+ name: 'pipe_exists',
40
+ passed: true,
41
+ message: `Pipe '${Config.PIPE_NAME}' available`,
42
+ });
43
+ } else {
44
+ throw new Error('Connection failed');
45
+ }
46
+ } catch (err) {
47
+ allPassed = false;
48
+ checks.push({
49
+ name: 'pipe_exists',
50
+ passed: false,
51
+ message: 'Pipe not found - CE bridge not running',
52
+ });
53
+ suggestions.push(
54
+ '1. Open Cheat Engine',
55
+ "2. Table -> Show Cheat Table Lua Script",
56
+ '3. Paste ce_mcp_bridge.lua content and Execute',
57
+ "4. Look for '[CheatEngine-MCP] Bridge started' message"
58
+ );
59
+ }
60
+
61
+ // Check 2: CE process
62
+ try {
63
+ const { execSync } = require('child_process');
64
+ const result = execSync('tasklist /FI "IMAGENAME eq cheatengine*" /FO CSV /NH', {
65
+ encoding: 'utf-8',
66
+ timeout: 5000
67
+ });
68
+ const ceRunning = result.toLowerCase().includes('cheatengine');
69
+ if (ceRunning) {
70
+ checks.push({
71
+ name: 'ce_process',
72
+ passed: true,
73
+ message: 'Cheat Engine is running',
74
+ });
75
+ } else {
76
+ allPassed = false;
77
+ checks.push({
78
+ name: 'ce_process',
79
+ passed: false,
80
+ message: 'Cheat Engine not found',
81
+ });
82
+ suggestions.unshift('Start Cheat Engine first!');
83
+ }
84
+ } catch (err) {
85
+ checks.push({
86
+ name: 'ce_process',
87
+ passed: null,
88
+ message: `Could not check: ${err.message}`,
89
+ });
90
+ }
91
+
92
+ // Check 3: Custom pipe name
93
+ const customPipe = process.env.CE_MCP_PIPE_NAME;
94
+ if (customPipe) {
95
+ checks.push({
96
+ name: 'custom_pipe',
97
+ passed: null,
98
+ message: `Using custom pipe: ${customPipe}`,
99
+ });
100
+ suggestions.push(`Ensure CE Lua also uses pipe name: ${customPipe}`);
101
+ }
102
+
103
+ return {
104
+ status: (allPassed && pipeExists) ? 'ok' : 'failed',
105
+ pipe_name: Config.PIPE_NAME,
106
+ checks,
107
+ suggestions: suggestions.length > 0 ? suggestions : ['Connection should work - try again'],
108
+ };
109
+ }
110
+
111
+ async executeTool(name, args) {
112
+ const luaCmd = this.toolRegistry.getLuaCommand(name);
113
+ if (!luaCmd) {
114
+ return { error: `Unknown tool: ${name}` };
115
+ }
116
+
117
+ const timeoutSeconds = this.timeoutManager.getTimeout(name);
118
+ const timeoutMs = timeoutSeconds * 1000;
119
+ const startTime = Date.now();
120
+
121
+ const response = await this.pipeClient.sendReceive(
122
+ { command: luaCmd, params: args },
123
+ timeoutMs,
124
+ name
125
+ );
126
+
127
+ const duration = (Date.now() - startTime) / 1000;
128
+ const isError = response.error !== undefined;
129
+ this.metricsCollector.recordCall(name, duration, isError);
130
+
131
+ if (isError) {
132
+ // For ce_ping, return diagnostic info on connection failure
133
+ if (name === 'ce_ping') {
134
+ const diag = await this._diagnoseConnection();
135
+ return { error: response.error, diagnostic: diag };
136
+ }
137
+ if (response.timeout_info) {
138
+ return {
139
+ error: response.error,
140
+ timeout_info: response.timeout_info,
141
+ };
142
+ }
143
+ return { error: response.error };
144
+ }
145
+
146
+ const result = response.result || response;
147
+
148
+ // Attach health and server metrics to ce_ping response
149
+ if (name === 'ce_ping') {
150
+ const healthMetrics = this.pipeClient.getHealthMetrics();
151
+ const serverMetrics = this.metricsCollector.getSummary();
152
+ if (typeof result === 'object' && result !== null) {
153
+ result.connection_health = healthMetrics;
154
+ result.server_metrics = serverMetrics;
155
+ } else {
156
+ return {
157
+ result,
158
+ connection_health: healthMetrics,
159
+ server_metrics: serverMetrics,
160
+ };
161
+ }
162
+ }
163
+
164
+ return result;
165
+ }
166
+
167
+ handleRequest(req) {
168
+ const method = req.method || '';
169
+ const reqId = req.id;
170
+ const params = req.params || {};
171
+
172
+ if (method === 'initialize') {
173
+ return this._handleInitialize(reqId);
174
+ } else if (method === 'notifications/initialized') {
175
+ return null;
176
+ } else if (method === 'tools/list') {
177
+ return this._handleToolsList(reqId);
178
+ } else if (method === 'tools/call') {
179
+ return this._handleToolsCall(reqId, params);
180
+ } else {
181
+ return this._errorResponse(reqId, -32601, `Method not found: ${method}`);
182
+ }
183
+ }
184
+
185
+ _handleInitialize(reqId) {
186
+ return {
187
+ jsonrpc: '2.0',
188
+ id: reqId,
189
+ result: {
190
+ protocolVersion: '2024-11-05',
191
+ capabilities: { tools: {} },
192
+ serverInfo: { name: 'cheatengine-mcp-bridge', version: '5.8.14' },
193
+ instructions: (
194
+ '# Cheat Engine MCP - AI Usage Guide\n\n' +
195
+ '## Tool Selection Decision Tree\n\n' +
196
+ '### Q: Do I know the address?\n' +
197
+ '- NO, but can observe value changes -> ce_scan_new workflow (value hunting)\n' +
198
+ '- YES, need stable pointer -> ce_find_pointer_path (auto) or manual F5+value_scan\n' +
199
+ '- YES, want to read/write -> ce_read_memory / ce_write_memory\n\n' +
200
+ '### Q: What am I searching for?\n' +
201
+ '- Game values (health, gold) -> ce_scan_new (NOT ce_value_scan!)\n' +
202
+ '- Code signatures -> ce_aob_scan (supports ?? wildcards)\n' +
203
+ '- Pointer storage locations -> ce_value_scan (after getting register value from F5)\n\n' +
204
+ '### Q: How to analyze code?\n' +
205
+ '- View execution flow -> ce_break_and_trace (dynamic, real execution)\n' +
206
+ '- Safely understand algorithm -> ce_symbolic_trace (static, no execution)\n' +
207
+ '- Function structure -> ce_build_cfg + ce_detect_patterns\n\n' +
208
+ '## Workflow Templates\n\n' +
209
+ '### 1. Value Hunting (find unknown address)\n' +
210
+ "1. ce_attach_process(target='game.exe')\n" +
211
+ "2. ce_scan_new(value='100', type='dword') -> returns session_id\n" +
212
+ '3. [Take damage in game, health becomes 95]\n' +
213
+ "4. ce_scan_next(session_id, value='95', scan_type='exact') -> count decreases\n" +
214
+ '5. Repeat steps 3-4 until count < 10\n' +
215
+ '6. ce_scan_results(session_id) -> get candidate addresses\n' +
216
+ "7. ce_read_memory(address, type='dword') -> verify correct address\n" +
217
+ '8. ce_scan_close(session_id) -> release resources (max 5 concurrent!)\n\n' +
218
+ '### 2. Pointer Tracing (make address stable)\n' +
219
+ "Method A (auto, try first): ce_find_pointer_path(address='0x12345678')\n" +
220
+ 'Method B (manual, if A fails):\n' +
221
+ "1. ce_find_what_accesses(address='0x12345678', duration_ms=10000)\n" +
222
+ '2. Get register value from result (e.g., RBX=0x255D5E758)\n' +
223
+ "3. ce_value_scan(value='0x255D5E758', type='qword')\n" +
224
+ '4. Find result with isStatic=true -> that\'s your base pointer!\n\n' +
225
+ '### 3. Function Monitoring (non-blocking)\n' +
226
+ "1. ce_hook_function(address='game.exe+12345', name='damageHook')\n" +
227
+ '2. [Let game run, trigger events]\n' +
228
+ "3. ce_get_hook_log(name='damageHook') -> captured args (x64: RCX,RDX,R8,R9)\n" +
229
+ "4. ce_unhook_function(name='damageHook') -> cleanup when done\n\n" +
230
+ '## Tool Comparison (Correct vs Wrong)\n' +
231
+ '| Purpose | Correct | Wrong |\n' +
232
+ '| Find game values | ce_scan_new | ce_value_scan |\n' +
233
+ '| Find code signature | ce_aob_scan | ce_scan_new |\n' +
234
+ '| Batch read memory | ce_read_memory_batch | multiple ce_read_memory |\n' +
235
+ '| Monitor function | ce_hook_function | ce_break_and_get_regs |\n\n' +
236
+ '## System Limits\n' +
237
+ '- Scan sessions: max 5 (auto-expire after 5 min inactivity)\n' +
238
+ '- Hardware breakpoints: max 4 (shared by F5/F6/trace)\n' +
239
+ '- Hook buffer: 64 records (circular, oldest overwritten when full)\n' +
240
+ '- Pointer depth: max 10 levels\n' +
241
+ '- Message size: 10MB per request/response\n\n' +
242
+ '## Common Errors\n' +
243
+ "- 'Process not attached' -> call ce_attach_process first\n" +
244
+ "- 'Too many scan sessions' -> ce_scan_list() then ce_scan_close()\n" +
245
+ "- 'Hardware breakpoint limit' -> ce_cleanup() to clear all\n" +
246
+ "- 'Hook name already exists' -> ce_unhook_function(name) first"
247
+ ),
248
+ },
249
+ };
250
+ }
251
+
252
+ _handleToolsList(reqId) {
253
+ return {
254
+ jsonrpc: '2.0',
255
+ id: reqId,
256
+ result: { tools: this.toolRegistry.getAllSchemas() },
257
+ };
258
+ }
259
+
260
+ async _handleToolsCall(reqId, params) {
261
+ const toolName = params.name || '';
262
+ const toolArgs = params.arguments || {};
263
+
264
+ try {
265
+ const result = await this.executeTool(toolName, toolArgs);
266
+
267
+ // Check if error: has error field, success=false, and no partial results
268
+ let isError = false;
269
+ if (typeof result === 'object' && result !== null && result.error) {
270
+ const hasPartialResult = ['chain', 'path', 'partialCENotation', 'finalAddress'].some(k => k in result);
271
+ isError = result.success === false && !hasPartialResult;
272
+ }
273
+
274
+ const text = isError
275
+ ? `Error: ${result.error}`
276
+ : JSON.stringify(result, null, 2);
277
+
278
+ return {
279
+ jsonrpc: '2.0',
280
+ id: reqId,
281
+ result: {
282
+ content: [{ type: 'text', text }],
283
+ isError: isError,
284
+ },
285
+ };
286
+ } catch (err) {
287
+ return this._errorResponse(reqId, -32603, err.message);
288
+ }
289
+ }
290
+
291
+ _errorResponse(reqId, code, message) {
292
+ return {
293
+ jsonrpc: '2.0',
294
+ id: reqId,
295
+ error: { code, message },
296
+ };
297
+ }
298
+
299
+ async run() {
300
+ log.info('Cheat Engine MCP Bridge Started. Waiting for input...');
301
+ log.info(`Node.js version: ${process.version}`);
302
+
303
+ // Use raw stdin/stdout for MCP protocol - no readline to avoid buffering issues
304
+ process.stdin.setEncoding('utf8');
305
+
306
+ let buffer = '';
307
+
308
+ process.stdin.on('data', async (chunk) => {
309
+ buffer += chunk;
310
+
311
+ // Process complete lines
312
+ let lines = buffer.split('\n');
313
+ buffer = lines.pop(); // Keep incomplete line in buffer
314
+
315
+ for (const line of lines) {
316
+ if (!line.trim()) continue;
317
+
318
+ this.requestCount++;
319
+
320
+ try {
321
+ const request = JSON.parse(line);
322
+ const response = this.handleRequest(request);
323
+
324
+ if (response) {
325
+ // Handle async tool calls
326
+ if (response instanceof Promise) {
327
+ const result = await response;
328
+ process.stdout.write(JSON.stringify(result) + '\n');
329
+ } else {
330
+ process.stdout.write(JSON.stringify(response) + '\n');
331
+ }
332
+ }
333
+ } catch (err) {
334
+ if (err instanceof SyntaxError) {
335
+ log.error(`Failed to decode JSON: ${err.message}`);
336
+ } else {
337
+ log.error(`Critical Error (request #${this.requestCount}): ${err.stack}`);
338
+ }
339
+ }
340
+ }
341
+ });
342
+
343
+ process.stdin.on('end', () => {
344
+ log.info('Received end of input');
345
+ this.pipeClient.stop();
346
+ log.info(`Server Stopped (processed ${this.requestCount} requests)`);
347
+ process.exit(0);
348
+ });
349
+
350
+ // Handle graceful shutdown
351
+ process.on('SIGINT', () => {
352
+ log.info('Received interrupt signal');
353
+ this.pipeClient.stop();
354
+ log.info(`Server Stopped (processed ${this.requestCount} requests)`);
355
+ process.exit(0);
356
+ });
357
+
358
+ process.on('SIGTERM', () => {
359
+ log.info('Received terminate signal');
360
+ this.pipeClient.stop();
361
+ log.info(`Server Stopped (processed ${this.requestCount} requests)`);
362
+ process.exit(0);
363
+ });
364
+ }
365
+ }
366
+
367
+ // ============ Entry Point ============
368
+ function main() {
369
+ const server = new CEMCPServer();
370
+ server.run();
371
+ }
372
+
373
+ main();
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "cheatengine",
3
- "version": "5.8.14",
3
+ "version": "5.8.16",
4
4
  "description": "Cheat Engine MCP Server - AI-assisted reverse engineering bridge",
5
- "main": "bin/cheatengine",
5
+ "main": "ce_mcp_server.js",
6
6
  "bin": {
7
7
  "cheatengine": "bin/cheatengine"
8
8
  },
9
9
  "scripts": {
10
- "start": "node bin/cheatengine"
10
+ "start": "node ce_mcp_server.js",
11
+ "test": "node ce_mcp_server.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=14.0.0"
11
15
  },
12
16
  "keywords": [
13
17
  "mcp",
@@ -19,12 +23,10 @@
19
23
  ],
20
24
  "author": "",
21
25
  "license": "MIT",
22
- "engines": {
23
- "node": ">=14.0.0"
24
- },
25
26
  "files": [
26
27
  "bin/",
27
- "ce_mcp_server.py",
28
+ "src/",
29
+ "ce_mcp_server.js",
28
30
  "README.md",
29
31
  "README_CN.md"
30
32
  ]