Files
Tiago Valente 814e5bdfc5 CheatSpecter 0.4.0
- Find: text search, grouped search, undo, went up/down by, where to search, before/after compare
- Debug tab: breakpoints, stepping, registers, stack, threads, what code touches, structure compare, pointer re-check
- Engine tab: Unreal, Unity, Godot, GameMaker and C++ class names; hidden settings and switches
- Code cheats with labels, variables, references and shared code; rule builder; script commands
- Cheats kept on by the PS5 without the browser
- Learn section with twelve lessons and an in-page practice game
- Section (master code) community cheats; SHN and MC4 export
- Groups, dropdowns, hotkeys, first-cheat guide, re-find after update, restore everything
- MCP server: 25 tools
- Quick start, wiki, contributor and security guides, changelog
2026-10-10 12:06:50 -07:00

135 lines
5.5 KiB
JavaScript

// Builds the MCP server as a desktop extension: mcp/cheatspecter-<version>.mcpb
//
// An .mcpb file is a zip with a manifest and the server inside. Opening it in
// the Claude desktop app installs it; nothing has to be typed or configured
// beyond the PS5's address.
import fs from 'node:fs';
import path from 'node:path';
import zlib from 'node:zlib';
import { fileURLToPath } from 'node:url';
import { rolldown } from 'rolldown';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const { version } = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
const out = path.join(root, 'mcp/build');
fs.rmSync(out, { recursive: true, force: true });
fs.mkdirSync(path.join(out, 'server'), { recursive: true });
// One file with everything in it except the disassembler, which loads a
// WebAssembly file from beside itself and so is shipped as it is.
const bundle = await rolldown({
input: path.join(root, 'mcp/server.ts'),
platform: 'node',
external: ['iced-x86'],
transform: { define: { __VERSION__: JSON.stringify(version) } },
logLevel: 'warn',
});
await bundle.write({ file: path.join(out, 'server/index.mjs'), format: 'esm', codeSplitting: false });
await bundle.close();
const iced = path.join(root, 'node_modules/iced-x86');
const icedOut = path.join(out, 'server/node_modules/iced-x86');
fs.mkdirSync(icedOut, { recursive: true });
for (const f of ['package.json', 'iced_x86.js', 'iced_x86_bg.wasm', 'LICENSE.txt']) fs.copyFileSync(path.join(iced, f), path.join(icedOut, f));
const manifest = {
manifest_version: '0.3',
name: 'cheatspecter',
display_name: 'CheatSpecter',
version,
description: 'Find and make game cheats on a jailbroken PS5 running the CheatSpecter payload.',
long_description:
'Lets Claude work with a PS5 that has ps5debug-NG and the CheatSpecter payload loaded: see what game is running, search its memory for values, find the code that changes them, and save the result as a switch in the CheatSpecter app. For single-player games you own.',
author: { name: 'CheatSpecter' },
license: 'GPL-3.0-only',
server: {
type: 'node',
entry_point: 'server/index.mjs',
mcp_config: {
command: 'node',
args: ['${__dirname}/server/index.mjs'],
env: { CHEATSPECTER_HOST: '${user_config.host}' },
},
},
user_config: {
host: {
type: 'string',
title: 'PS5 address',
description: "Your PS5's IP address, as shown in the notification when the CheatSpecter payload starts (for example 192.168.1.50). Can be left empty and set later by asking Claude.",
required: false,
default: '',
},
},
tools: [],
compatibility: { runtimes: { node: '>=18.0.0' } },
};
// The tool list in the manifest is what the app shows before the server runs; take it from the source so the two cannot drift.
const source = fs.readFileSync(path.join(root, 'mcp/server.ts'), 'utf8');
for (const m of source.matchAll(/\n \{\n\s+name: '([a-z_]+)',\n\s+description:\s*\n?\s*(['"`])((?:\\.|(?!\2).)*)\2/g)) {
manifest.tools.push({ name: m[1], description: m[3].replace(/\\(['"`n])/g, (_, c) => (c === 'n' ? ' ' : c)).split('\n')[0].slice(0, 300) });
}
fs.writeFileSync(path.join(out, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
// ---- a zip, written by hand (deflate; no dependencies) ----
const crcTable = new Uint32Array(256).map((_, n) => {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
return c >>> 0;
});
const crc32 = (buf) => {
let c = 0xffffffff;
for (const b of buf) c = crcTable[(c ^ b) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
};
const files = [];
const walk = (dir, rel = '') => {
for (const name of fs.readdirSync(dir).sort()) {
const full = path.join(dir, name);
const r = rel ? `${rel}/${name}` : name;
if (fs.statSync(full).isDirectory()) walk(full, r);
else files.push({ name: r, data: fs.readFileSync(full) });
}
};
walk(out);
const parts = [];
const central = [];
let offset = 0;
for (const f of files) {
const name = Buffer.from(f.name, 'utf8');
const packed = zlib.deflateRawSync(f.data, { level: 9 });
const head = Buffer.alloc(30);
head.writeUInt32LE(0x04034b50, 0);
head.writeUInt16LE(20, 4);
head.writeUInt16LE(0x0800, 6); // names are UTF-8
head.writeUInt16LE(8, 8); // deflate
head.writeUInt16LE(0, 10);
head.writeUInt16LE(0x21, 12); // 1980-01-01, so the same input always gives the same file
head.writeUInt32LE(crc32(f.data), 14);
head.writeUInt32LE(packed.length, 18);
head.writeUInt32LE(f.data.length, 22);
head.writeUInt16LE(name.length, 26);
parts.push(head, name, packed);
const cd = Buffer.alloc(46);
cd.writeUInt32LE(0x02014b50, 0);
cd.writeUInt16LE(20, 4);
cd.writeUInt16LE(20, 6);
head.copy(cd, 8, 6, 30); // flags .. name length
cd.writeUInt32LE(offset, 42);
central.push(cd, name);
offset += head.length + name.length + packed.length;
}
const cdBuf = Buffer.concat(central);
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0);
end.writeUInt16LE(files.length, 8);
end.writeUInt16LE(files.length, 10);
end.writeUInt32LE(cdBuf.length, 12);
end.writeUInt32LE(offset, 16);
for (const f of fs.readdirSync(path.join(root, 'mcp'))) if (/^cheatspecter-.*\.mcpb$/.test(f)) fs.rmSync(path.join(root, 'mcp', f));
const target = path.join(root, `mcp/cheatspecter-${version}.mcpb`);
fs.writeFileSync(target, Buffer.concat([...parts, cdBuf, end]));
console.log(`Built ${path.relative(root, target)} (${files.length} files, ${(fs.statSync(target).size / 1024).toFixed(0)} KB, ${manifest.tools.length} tools)`);