@memoryprocess/mcp 0.0.0-stage → 0.0.1

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.
Files changed (3) hide show
  1. package/README.md +73 -2
  2. package/dist/server.mjs +765 -0
  3. package/package.json +53 -4
package/README.md CHANGED
@@ -1,3 +1,74 @@
1
- # Temporary Holding Version
1
+ # @memoryprocess/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ MCP server exposing the memoryprocess toolkit to LLM agents — the primary
4
+ agentic surface. Stdio transport; the agent's MCP client owns the server
5
+ process, so open process handles die with the session.
6
+
7
+ ## Usage
8
+
9
+ Point your MCP client at the bin (works under bun or node):
10
+
11
+ ```jsonc
12
+ // claude_desktop_config.json
13
+ {
14
+ "mcpServers": {
15
+ "memoryprocess": {
16
+ "command": "bun",
17
+ "args": ["C:/path/to/memoryprocess/packages/mcp/src/server.ts"],
18
+ },
19
+ },
20
+ }
21
+ ```
22
+
23
+ ```
24
+ memoryprocess-mcp [--no-eval] [--help] [--version]
25
+ MEMP_NO_EVAL=1 # same as --no-eval
26
+ ```
27
+
28
+ ## Session model
29
+
30
+ There is no implicit "current process". `proc_open` returns a session handle
31
+ (`p1`, `p2`, …); every `mem_*` tool takes it as `session`, so an agent can
32
+ hold several targets at once. `proc_close` releases the handle and any
33
+ watches/freezes bound to it.
34
+
35
+ ## Tools
36
+
37
+ | Tool | Description |
38
+ | --------------------- | -------------------------------------------------------------------------------------------------------- |
39
+ | `proc_list` | List running processes `{pid,name}[]` |
40
+ | `proc_open` | Open by `{pid}` or `{name}` → `{session, describe}`. `access:"ro"` = read-only |
41
+ | `proc_close` | Close a session |
42
+ | `mem_describe` | `Process.describe()`: arch, modules, writable/exec regions, threads |
43
+ | `mem_read` | Typed read: `i8…i64/u8…u64`, `f32/f64`, `ptr`, `str`, `wstr`, `bytes[:N]` (+`len`) |
44
+ | `mem_write` | Typed write. `dryRun:true` → `{addr,current,would,bytes}` preview. Journaled |
45
+ | `mem_scan` | CE value scan → `{hits:[{addr,value}],count,truncated}` (`module`/`start`/`end`/`limit`) |
46
+ | `mem_scan_next` | Refine hits: `exact`/`changed`/`unchanged`/`increased`/`decreased`/`increasedBy`/`decreasedBy`/`between` |
47
+ | `mem_pointer_resolve` | `module`+`rva`+`offsets[]` → resolved address (CE pointer syntax) |
48
+ | `mem_watch` | Poll an i32 every `ms` → `{watchId}`; changes accumulate |
49
+ | `mem_watch_poll` | Drain recorded changes `[{value,ts}]` for a `watchId` |
50
+ | `mem_unwatch` | Stop a watch |
51
+ | `mem_freeze` | Re-write a value every `ms` → `{freezeId}` |
52
+ | `mem_unfreeze` | Stop a freeze |
53
+ | `mem_undo` | Revert last `n` journaled writes (self-correction primitive) |
54
+ | `mem_journal` | Write-log snapshot `[{index,addr,bytes}]` |
55
+ | `eval_js` | Run JS with `proc` (`Process`) in scope — the escape hatch |
56
+
57
+ ## Wire format (docs/json-conventions.md)
58
+
59
+ - Every result is `{ok:true,data}` or `{ok:false,error:{code,message,win32code?}}`
60
+ — `error.code` is the typed code from `@memoryprocess/core`
61
+ (`READ_FAILED`, `PROCESS_NOT_FOUND`, …) plus `UNKNOWN_SESSION`,
62
+ `UNKNOWN_WATCH`, `UNKNOWN_FREEZE`, `MODULE_NOT_FOUND`, `INVALID_ARGS`,
63
+ `EVAL_ERROR`.
64
+ - Addresses/pointers/64-bit values are `"0x…"` hex strings; values that fit
65
+ in a JS number stay numbers; raw bytes are hex strings.
66
+ - Address args accept `"0x…"`, decimal strings, or `"module+rva"`.
67
+
68
+ ## Safety
69
+
70
+ All writes are journaled → `mem_journal` + `mem_undo` let an agent preview
71
+ (`dryRun`), commit, inspect, and revert. `eval_js` runs arbitrary JS on the
72
+ host — it's on by default (agents already have code execution) and can be
73
+ disabled with `--no-eval` / `MEMP_NO_EVAL=1`, which omits it from
74
+ `tools/list` entirely.
@@ -0,0 +1,765 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ import { MemoryProcessError, Process } from "@memoryprocess/core";
9
+ // src/tools.ts
10
+ /**
11
+ * Session-handle tool layer for @memoryprocess/mcp.
12
+ *
13
+ * The MCP wire layer (server.ts) is a thin adapter over the definitions in
14
+ * this file so the handlers are unit-testable without a stdio transport:
15
+ * `createSessions()` + `callTool(sessions, name, args)`.
16
+ *
17
+ * Wire format follows docs/json-conventions.md:
18
+ * - every result is `{ok:true,data} | {ok:false,error:{code,message,win32code?}}`
19
+ * - bigints (addresses, pointers, 64-bit values) -> '0x…' strings
20
+ * - Buffers -> lowercase hex strings
21
+ * - errors carry the typed `.code` from @memoryprocess/core when available
22
+ */
23
+ const hex = (v) => `0x${BigInt(v).toString(16)}`;
24
+ /** Recursively convert a JS value into the JSON-safe wire shape. */
25
+ function jsonSafe(v) {
26
+ if (typeof v === "bigint") return hex(v);
27
+ if (v === null || v === void 0) return v;
28
+ if (typeof v === "number" || typeof v === "string" || typeof v === "boolean") return v;
29
+ if (Buffer.isBuffer(v)) return v.toString("hex");
30
+ if (ArrayBuffer.isView(v)) {
31
+ if (v instanceof Float32Array || v instanceof Float64Array) return Array.from(v);
32
+ return Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString("hex");
33
+ }
34
+ if (Array.isArray(v)) return v.map(jsonSafe);
35
+ if (typeof v === "object") {
36
+ const out = {};
37
+ for (const [k, val] of Object.entries(v)) out[k] = jsonSafe(val);
38
+ return out;
39
+ }
40
+ return String(v);
41
+ }
42
+ const fail = (code, message) => {
43
+ const e = new Error(message);
44
+ e.code = code;
45
+ return e;
46
+ };
47
+ function errorEnvelope(e) {
48
+ const err = e;
49
+ const out = {
50
+ code: typeof err?.code === "string" ? err.code : err instanceof MemoryProcessError ? err.code : "INTERNAL_ERROR",
51
+ message: err?.message ?? String(e)
52
+ };
53
+ if (typeof err?.win32code === "number") out.win32code = err.win32code;
54
+ return {
55
+ ok: false,
56
+ error: out
57
+ };
58
+ }
59
+ /**
60
+ * Parse an address argument: `0x…` hex string, decimal string, or
61
+ * `"module+rva"` (resolved against the session's module table — the rva is
62
+ * optional). Returns the absolute address as bigint.
63
+ */
64
+ function parseAddress(proc, v, name = "addr") {
65
+ if (typeof v === "bigint") return v;
66
+ if (typeof v === "number") {
67
+ if (!Number.isFinite(v) || v < 0) throw fail("INVALID_ARGS", `${name} invalid`);
68
+ return BigInt(v);
69
+ }
70
+ if (typeof v === "string") {
71
+ const s = v.trim();
72
+ const plus = s.lastIndexOf("+");
73
+ if (plus > 0) {
74
+ const mod = proc.module(s.slice(0, plus));
75
+ if (!mod) throw fail("MODULE_NOT_FOUND", `module not found: ${s.slice(0, plus)} (in "${s}")`);
76
+ const rva = s.slice(plus + 1);
77
+ let off;
78
+ try {
79
+ off = BigInt(rva);
80
+ } catch {
81
+ throw fail("INVALID_ARGS", `bad rva in "${s}"`);
82
+ }
83
+ return BigInt(mod.base) + off;
84
+ }
85
+ if (/\.(exe|dll)$/i.test(s)) {
86
+ const mod = proc.module(s);
87
+ if (!mod) throw fail("MODULE_NOT_FOUND", `module not found: ${s}`);
88
+ return BigInt(mod.base);
89
+ }
90
+ try {
91
+ return BigInt(s);
92
+ } catch {
93
+ throw fail("INVALID_ARGS", `${name} must be an address ("${v}")`);
94
+ }
95
+ }
96
+ throw fail("INVALID_ARGS", `${name} must be an address`);
97
+ }
98
+ /** Parse a numeric value argument (accepts numbers, bigints, dec/hex strings). */
99
+ function parseNumeric(v, wide, name = "value") {
100
+ if (typeof v === "bigint") return wide ? v : Number(v);
101
+ if (typeof v === "number") {
102
+ if (!Number.isFinite(v)) throw fail("INVALID_ARGS", `${name} must be finite`);
103
+ return wide ? BigInt(Math.trunc(v)) : v;
104
+ }
105
+ if (typeof v === "string") {
106
+ const s = v.trim();
107
+ if (wide) try {
108
+ return BigInt(s);
109
+ } catch {
110
+ throw fail("INVALID_ARGS", `${name} must be an integer ("${v}")`);
111
+ }
112
+ const n = /^0x[0-9a-f]+$/i.test(s) ? Number(BigInt(s)) : Number(s);
113
+ if (!Number.isFinite(n)) throw fail("INVALID_ARGS", `${name} must be a number ("${v}")`);
114
+ return n;
115
+ }
116
+ throw fail("INVALID_ARGS", `${name} must be a number`);
117
+ }
118
+ /** Agent-facing type aliases -> core WriteValueType/ScanValueType. */
119
+ const NUMERIC_TYPES = {
120
+ i8: "uint8",
121
+ u8: "uint8",
122
+ i16: "uint16",
123
+ u16: "uint16",
124
+ i32: "int32",
125
+ u32: "uint32",
126
+ i64: "int64",
127
+ u64: "uint64",
128
+ f32: "float",
129
+ f64: "double",
130
+ ptr: "pointer",
131
+ int8: "int8",
132
+ uint8: "uint8",
133
+ int16: "int16",
134
+ uint16: "uint16",
135
+ int32: "int32",
136
+ uint32: "uint32",
137
+ int64: "int64",
138
+ uint64: "uint64",
139
+ float: "float",
140
+ double: "double",
141
+ pointer: "pointer"
142
+ };
143
+ const SCAN_TYPES = /* @__PURE__ */ new Set([
144
+ "int32",
145
+ "uint32",
146
+ "int64",
147
+ "uint64",
148
+ "float",
149
+ "double"
150
+ ]);
151
+ const WIDE_TYPES = /* @__PURE__ */ new Set([
152
+ "int64",
153
+ "uint64",
154
+ "pointer"
155
+ ]);
156
+ /**
157
+ * Normalise an agent-facing type name to the core name.
158
+ * Accepts `bytes`/`bytes:N` (needs `len`), `str`/`wstr`, and numeric types.
159
+ */
160
+ function normType(t, name = "type") {
161
+ if (typeof t !== "string" || !t) throw fail("INVALID_ARGS", `${name} must be a string`);
162
+ const lower = t.toLowerCase();
163
+ const hit = NUMERIC_TYPES[lower];
164
+ if (hit) return hit;
165
+ if (lower === "str" || lower === "wstr") return lower;
166
+ if (lower === "bytes" || /^bytes:\d+$/.test(lower)) return "bytes";
167
+ throw fail("INVALID_ARGS", `unknown type "${t}"`);
168
+ }
169
+ var SessionStore = class {
170
+ procs = /* @__PURE__ */ new Map();
171
+ watches = /* @__PURE__ */ new Map();
172
+ freezes = /* @__PURE__ */ new Map();
173
+ seq = 0;
174
+ wseq = 0;
175
+ fseq = 0;
176
+ open(nameOrPid, access) {
177
+ const proc = access === "ro" ? Process.openReadOnly(nameOrPid) : Process.open(nameOrPid);
178
+ const id = `p${++this.seq}`;
179
+ this.procs.set(id, proc);
180
+ return id;
181
+ }
182
+ get(id) {
183
+ if (typeof id !== "string") throw fail("INVALID_ARGS", "session must be a string");
184
+ const p = this.procs.get(id);
185
+ if (!p) throw fail("UNKNOWN_SESSION", `unknown session "${id}"`);
186
+ return p;
187
+ }
188
+ close(id) {
189
+ if (typeof id !== "string") throw fail("INVALID_ARGS", "session must be a string");
190
+ const p = this.procs.get(id);
191
+ if (!p) throw fail("UNKNOWN_SESSION", `unknown session "${id}"`);
192
+ for (const k of [...this.watches.keys()]) if (k.startsWith(`${id}:`)) {
193
+ this.watches.get(k).stop();
194
+ this.watches.delete(k);
195
+ }
196
+ for (const k of [...this.freezes.keys()]) if (k.startsWith(`${id}:`)) {
197
+ this.freezes.get(k)();
198
+ this.freezes.delete(k);
199
+ }
200
+ this.procs.delete(id);
201
+ p.close();
202
+ }
203
+ addWatch(session, rec) {
204
+ const id = `${session}:w${++this.wseq}`;
205
+ this.watches.set(id, rec);
206
+ return id;
207
+ }
208
+ getWatch(id) {
209
+ if (typeof id !== "string") throw fail("INVALID_ARGS", "watchId must be a string");
210
+ const w = this.watches.get(id);
211
+ if (!w) throw fail("UNKNOWN_WATCH", `unknown watchId "${id}"`);
212
+ return w;
213
+ }
214
+ removeWatch(id) {
215
+ this.getWatch(id).stop();
216
+ this.watches.delete(id);
217
+ }
218
+ addFreeze(session, stop) {
219
+ const id = `${session}:f${++this.fseq}`;
220
+ this.freezes.set(id, stop);
221
+ return id;
222
+ }
223
+ removeFreeze(id) {
224
+ if (typeof id !== "string") throw fail("INVALID_ARGS", "freezeId must be a string");
225
+ const stop = this.freezes.get(id);
226
+ if (!stop) throw fail("UNKNOWN_FREEZE", `unknown freezeId "${id}"`);
227
+ stop();
228
+ this.freezes.delete(id);
229
+ }
230
+ closeAll() {
231
+ for (const [id] of this.procs) try {
232
+ this.close(id);
233
+ } catch {}
234
+ for (const [, w] of this.watches) w.stop();
235
+ for (const [, f] of this.freezes) f();
236
+ this.watches.clear();
237
+ this.freezes.clear();
238
+ }
239
+ get size() {
240
+ return this.procs.size;
241
+ }
242
+ };
243
+ function createSessions() {
244
+ return new SessionStore();
245
+ }
246
+ function readTyped(proc, addr, args) {
247
+ switch (normType(args.type)) {
248
+ case "str": return proc.readString(addr, typeof args.len === "number" ? args.len : void 0);
249
+ case "wstr": return proc.readWideString(addr, typeof args.len === "number" ? args.len : void 0);
250
+ case "bytes": {
251
+ const m = typeof args.type === "string" && /^bytes:(\d+)$/.exec(args.type);
252
+ const len = typeof args.len === "number" ? args.len : m ? Number(m[1]) : void 0;
253
+ if (len === void 0 || !Number.isInteger(len) || len < 1) throw fail("INVALID_ARGS", "bytes read needs len (or bytes:N)");
254
+ return proc.readBuffer(addr, len);
255
+ }
256
+ case "int8": return proc.readUInt8(addr) << 24 >> 24;
257
+ case "int16": return proc.readUInt16(addr) << 16 >> 16;
258
+ case "uint8": return proc.readUInt8(addr);
259
+ case "uint16": return proc.readUInt16(addr);
260
+ case "int32": return proc.readInt32(addr);
261
+ case "uint32": return proc.readUInt32(addr);
262
+ case "int64": return proc.readInt64(addr);
263
+ case "uint64": return proc.readUInt64(addr);
264
+ case "float": return proc.readFloat(addr);
265
+ case "double": return proc.readDouble(addr);
266
+ case "pointer": return proc.readPointer(addr);
267
+ default: throw fail("INVALID_ARGS", `unsupported read type "${args.type}"`);
268
+ }
269
+ }
270
+ function serializePreview(p) {
271
+ return {
272
+ addr: hex(p.addr),
273
+ current: p.current.toString("hex"),
274
+ would: p.would.toString("hex"),
275
+ bytes: p.bytes
276
+ };
277
+ }
278
+ function writeTyped(proc, addr, args) {
279
+ const type = normType(args.type);
280
+ const opts = { dryRun: args.dryRun === true };
281
+ const fin = (r) => r === true ? true : serializePreview(r);
282
+ switch (type) {
283
+ case "str":
284
+ case "wstr":
285
+ if (typeof args.value !== "string") throw fail("INVALID_ARGS", `${type} write needs a string value`);
286
+ return fin(type === "str" ? proc.writeString(addr, args.value, "utf8", opts) : proc.writeWideString(addr, args.value, opts));
287
+ case "bytes": {
288
+ const v = args.value;
289
+ let buf;
290
+ if (typeof v === "string" && /^(0x)?[0-9a-fA-F]*$/.test(v)) buf = Buffer.from(v.replace(/^0x/, ""), "hex");
291
+ else if (Array.isArray(v)) buf = Buffer.from(v.map((b) => Number(b) & 255));
292
+ else throw fail("INVALID_ARGS", "bytes write needs a hex string or byte array");
293
+ if (buf.length === 0) throw fail("INVALID_ARGS", "bytes write needs at least 1 byte");
294
+ return fin(proc.writeBuffer(addr, buf, opts));
295
+ }
296
+ case "int8":
297
+ case "int16":
298
+ case "uint8":
299
+ case "uint16": {
300
+ const v = parseNumeric(args.value, false);
301
+ return fin(type === "int8" || type === "uint8" ? proc.writeUInt8(addr, v, opts) : proc.writeUInt16(addr, v, opts));
302
+ }
303
+ case "int32": return fin(proc.writeInt32(addr, parseNumeric(args.value, false), opts));
304
+ case "uint32": return fin(proc.writeUInt32(addr, parseNumeric(args.value, false), opts));
305
+ case "int64": return fin(proc.writeInt64(addr, parseNumeric(args.value, true), opts));
306
+ case "uint64": return fin(proc.writeUInt64(addr, parseNumeric(args.value, true), opts));
307
+ case "float": return fin(proc.writeFloat(addr, parseNumeric(args.value, false), opts));
308
+ case "double": return fin(proc.writeDouble(addr, parseNumeric(args.value, false), opts));
309
+ case "pointer": return fin(proc.writePointer(addr, parseNumeric(args.value, true), opts));
310
+ default: throw fail("INVALID_ARGS", `unsupported write type "${args.type}"`);
311
+ }
312
+ }
313
+ function serializeHit(h) {
314
+ return {
315
+ addr: hex(h.addr),
316
+ value: jsonSafe(h.value)
317
+ };
318
+ }
319
+ function parseHits(raw, type) {
320
+ if (!Array.isArray(raw)) throw fail("INVALID_ARGS", "hits must be an array of {addr,value}");
321
+ const wide = WIDE_TYPES.has(type);
322
+ return raw.map((h, i) => {
323
+ if (typeof h !== "object" || h === null) throw fail("INVALID_ARGS", `hits[${i}] must be {addr,value}`);
324
+ const rec = h;
325
+ let addr;
326
+ try {
327
+ addr = BigInt(rec.addr);
328
+ } catch {
329
+ throw fail("INVALID_ARGS", `hits[${i}].addr is not an address`);
330
+ }
331
+ let value;
332
+ if (wide) try {
333
+ value = BigInt(rec.value);
334
+ } catch {
335
+ throw fail("INVALID_ARGS", `hits[${i}].value is not an integer`);
336
+ }
337
+ else {
338
+ value = Number(rec.value);
339
+ if (!Number.isFinite(value)) throw fail("INVALID_ARGS", `hits[${i}].value is not a number`);
340
+ }
341
+ return {
342
+ addr,
343
+ value
344
+ };
345
+ });
346
+ }
347
+ const sessionArg = { session: z.string().describe("session handle from proc_open") };
348
+ const addrArg = z.union([z.string(), z.number()]).describe("address: \"0x…\" hex, decimal, or \"module+rva\"");
349
+ const typeArg = z.string().describe("i8 u8 i16 u16 i32 u32 i64 u64 f32 f64 ptr str wstr bytes[:N]");
350
+ const toolDefs = {
351
+ proc_list: {
352
+ description: "List running processes as {pid,name}[]",
353
+ schema: {},
354
+ handler: () => Process.list()
355
+ },
356
+ proc_open: {
357
+ description: "Open a process and return a session handle plus a describe() snapshot. access:\"ro\" opens read-only.",
358
+ schema: {
359
+ pid: z.number().int().optional().describe("process id (use pid OR name)"),
360
+ name: z.string().optional().describe("process name e.g. \"victim.exe\""),
361
+ access: z.enum(["rw", "ro"]).optional().describe("\"ro\" = openReadOnly")
362
+ },
363
+ handler: (s, a) => {
364
+ if (a.pid === void 0 && a.name === void 0) throw fail("INVALID_ARGS", "proc_open needs pid or name");
365
+ const target = a.pid !== void 0 ? a.pid : a.name;
366
+ const session = s.open(target, a.access);
367
+ return {
368
+ session,
369
+ describe: s.get(session).describe()
370
+ };
371
+ }
372
+ },
373
+ proc_close: {
374
+ description: "Close a session and release its process handle",
375
+ schema: sessionArg,
376
+ handler: (s, a) => {
377
+ s.close(a.session);
378
+ return { closed: true };
379
+ }
380
+ },
381
+ mem_describe: {
382
+ description: "Machine-readable snapshot of the target: pid, arch, modules, writable/executable regions, threads",
383
+ schema: sessionArg,
384
+ handler: (s, a) => s.get(a.session).describe()
385
+ },
386
+ mem_read: {
387
+ description: "Read a typed value. Integers/pointers come back as hex strings, floats as numbers, str/wstr as strings, bytes as hex.",
388
+ schema: {
389
+ ...sessionArg,
390
+ addr: addrArg,
391
+ type: typeArg,
392
+ len: z.number().int().positive().optional().describe("byte/char count for bytes/str/wstr")
393
+ },
394
+ handler: (s, a) => {
395
+ const proc = s.get(a.session);
396
+ const addr = parseAddress(proc, a.addr);
397
+ return {
398
+ addr: hex(addr),
399
+ value: readTyped(proc, addr, a)
400
+ };
401
+ }
402
+ },
403
+ mem_write: {
404
+ description: "Write a typed value (journaled — reversible via mem_undo). dryRun:true returns {addr,current,would,bytes} WITHOUT writing.",
405
+ schema: {
406
+ ...sessionArg,
407
+ addr: addrArg,
408
+ type: typeArg,
409
+ value: z.union([
410
+ z.string(),
411
+ z.number(),
412
+ z.array(z.number())
413
+ ]).describe("value (hex/dec strings ok; bytes takes hex string or byte array)"),
414
+ dryRun: z.boolean().optional().describe("preview only, do not write")
415
+ },
416
+ destructive: true,
417
+ handler: (s, a) => {
418
+ const proc = s.get(a.session);
419
+ const r = writeTyped(proc, parseAddress(proc, a.addr), a);
420
+ return r === true ? { written: true } : { preview: r };
421
+ }
422
+ },
423
+ mem_scan: {
424
+ description: "CE-style value scan: find all occurrences of `value` in committed (writable by default) memory. Returns {hits:[{addr,value}],count,truncated}.",
425
+ schema: {
426
+ ...sessionArg,
427
+ value: z.union([z.string(), z.number()]).describe("value to find"),
428
+ type: z.string().optional().describe("i32 u32 i64 u64 f32 f64 (default i32)"),
429
+ module: z.string().optional().describe("restrict to a module"),
430
+ start: addrArg.optional().describe("range start"),
431
+ end: addrArg.optional().describe("range end"),
432
+ onlyWritable: z.boolean().optional(),
433
+ limit: z.number().int().positive().optional().describe("max hits returned (default 1000)")
434
+ },
435
+ handler: async (s, a) => {
436
+ const proc = s.get(a.session);
437
+ const type = normType(a.type ?? "i32");
438
+ if (!SCAN_TYPES.has(type)) throw fail("INVALID_ARGS", `mem_scan type must be a scan type, got "${a.type ?? "i32"}"`);
439
+ const value = parseNumeric(a.value, WIDE_TYPES.has(type));
440
+ const hits = await proc.scanValue(value, {
441
+ type,
442
+ module: a.module,
443
+ start: a.start !== void 0 ? parseAddress(proc, a.start, "start") : void 0,
444
+ end: a.end !== void 0 ? parseAddress(proc, a.end, "end") : void 0,
445
+ onlyWritable: a.onlyWritable
446
+ });
447
+ const limit = a.limit ?? 1e3;
448
+ return {
449
+ count: hits.length,
450
+ truncated: hits.length > limit,
451
+ hits: hits.slice(0, limit).map(serializeHit)
452
+ };
453
+ }
454
+ },
455
+ mem_scan_next: {
456
+ description: "Refine a previous mem_scan hit list (CE \"next scan\"). mode: exact|changed|unchanged|increased|decreased|increasedBy|decreasedBy|between; relative modes compare against each hit's stored value.",
457
+ schema: {
458
+ ...sessionArg,
459
+ hits: z.array(z.object({
460
+ addr: z.union([z.string(), z.number()]),
461
+ value: z.union([z.string(), z.number()])
462
+ })).describe("hits from mem_scan / a previous mem_scan_next"),
463
+ mode: z.enum([
464
+ "exact",
465
+ "changed",
466
+ "unchanged",
467
+ "increased",
468
+ "decreased",
469
+ "increasedBy",
470
+ "decreasedBy",
471
+ "between"
472
+ ]).describe("refine mode"),
473
+ value: z.union([z.string(), z.number()]).optional().describe("target for exact/increasedBy/decreasedBy"),
474
+ lo: z.union([z.string(), z.number()]).optional().describe("between low"),
475
+ hi: z.union([z.string(), z.number()]).optional().describe("between high"),
476
+ type: z.string().optional().describe("same type as the original scan"),
477
+ limit: z.number().int().positive().optional()
478
+ },
479
+ handler: async (s, a) => {
480
+ const proc = s.get(a.session);
481
+ const type = normType(a.type ?? "i32");
482
+ if (!SCAN_TYPES.has(type)) throw fail("INVALID_ARGS", `mem_scan_next type must be a scan type`);
483
+ const wide = WIDE_TYPES.has(type);
484
+ const mode = a.mode;
485
+ let m;
486
+ switch (mode) {
487
+ case "exact":
488
+ m = {
489
+ type: "exact",
490
+ value: parseNumeric(a.value, wide)
491
+ };
492
+ break;
493
+ case "increasedBy":
494
+ case "decreasedBy":
495
+ m = {
496
+ type: mode,
497
+ value: parseNumeric(a.value, wide)
498
+ };
499
+ break;
500
+ case "between":
501
+ m = {
502
+ type: "between",
503
+ lo: parseNumeric(a.lo, wide, "lo"),
504
+ hi: parseNumeric(a.hi, wide, "hi")
505
+ };
506
+ break;
507
+ case "changed":
508
+ case "unchanged":
509
+ case "increased":
510
+ case "decreased":
511
+ m = { type: mode };
512
+ break;
513
+ default: throw fail("INVALID_ARGS", `unknown mode "${mode}"`);
514
+ }
515
+ const hits = await proc.nextScan(parseHits(a.hits, type), m, { type });
516
+ const limit = a.limit ?? 1e3;
517
+ return {
518
+ count: hits.length,
519
+ truncated: hits.length > limit,
520
+ hits: hits.slice(0, limit).map(serializeHit)
521
+ };
522
+ }
523
+ },
524
+ mem_pointer_resolve: {
525
+ description: "Resolve module+rva plus a pointer chain: base = module base + rva, then dereference each offset (CE \"module.exe+RVA, offsets\" style).",
526
+ schema: {
527
+ ...sessionArg,
528
+ module: z.string().describe("module basename e.g. \"victim.exe\""),
529
+ rva: z.union([z.string(), z.number()]).describe("offset from module base (hex ok)"),
530
+ offsets: z.array(z.union([z.string(), z.number()])).describe("pointer-chain offsets; all but the last dereference")
531
+ },
532
+ handler: (s, a) => {
533
+ const proc = s.get(a.session);
534
+ const mod = proc.module(a.module);
535
+ if (!mod) throw fail("MODULE_NOT_FOUND", `module not found: ${a.module}`);
536
+ let rva;
537
+ try {
538
+ rva = BigInt(a.rva);
539
+ } catch {
540
+ throw fail("INVALID_ARGS", `bad rva "${a.rva}"`);
541
+ }
542
+ const offsets = a.offsets.map((o, i) => {
543
+ const n = typeof o === "string" ? Number(BigInt(o)) : Number(o);
544
+ if (!Number.isFinite(n)) throw fail("INVALID_ARGS", `offsets[${i}] invalid`);
545
+ return n;
546
+ });
547
+ const base = BigInt(mod.base) + rva;
548
+ const addr = offsets.length === 0 ? base : proc.resolvePointer(base, offsets);
549
+ return {
550
+ base: hex(base),
551
+ addr: hex(addr)
552
+ };
553
+ }
554
+ },
555
+ mem_watch: {
556
+ description: "Poll an i32 at addr every ms (default 50); changes are recorded and drained by mem_watch_poll.",
557
+ schema: {
558
+ ...sessionArg,
559
+ addr: addrArg,
560
+ ms: z.number().int().positive().optional().describe("poll interval ms")
561
+ },
562
+ handler: (s, a) => {
563
+ const proc = s.get(a.session);
564
+ const addr = parseAddress(proc, a.addr);
565
+ const events = [];
566
+ const stop = proc.watch(addr, (v) => events.push({
567
+ value: v,
568
+ ts: Date.now()
569
+ }), a.ms ?? 50);
570
+ return { watchId: s.addWatch(a.session, {
571
+ events,
572
+ stop
573
+ }) };
574
+ }
575
+ },
576
+ mem_watch_poll: {
577
+ description: "Drain the recorded changes for a watchId: returns [{value,ts}] since the last poll.",
578
+ schema: { watchId: z.string() },
579
+ handler: (s, a) => {
580
+ const w = s.getWatch(a.watchId);
581
+ const events = w.events;
582
+ w.events = [];
583
+ return {
584
+ events,
585
+ count: events.length
586
+ };
587
+ }
588
+ },
589
+ mem_unwatch: {
590
+ description: "Stop a watch created by mem_watch",
591
+ schema: { watchId: z.string() },
592
+ handler: (s, a) => {
593
+ s.removeWatch(a.watchId);
594
+ return { stopped: true };
595
+ }
596
+ },
597
+ mem_freeze: {
598
+ description: "Freeze a value: rewrites `value` at addr every ms (default 50) until mem_unfreeze.",
599
+ schema: {
600
+ ...sessionArg,
601
+ addr: addrArg,
602
+ type: z.string().describe("i8 u8 i16 u16 i32 u32 i64 u64 f32 f64 ptr"),
603
+ value: z.union([z.string(), z.number()]),
604
+ ms: z.number().int().positive().optional()
605
+ },
606
+ destructive: true,
607
+ handler: (s, a) => {
608
+ const proc = s.get(a.session);
609
+ const addr = parseAddress(proc, a.addr);
610
+ const type = normType(a.type);
611
+ if (type === "str" || type === "wstr" || type === "bytes") throw fail("INVALID_ARGS", `freeze needs a numeric type, got "${a.type}"`);
612
+ const coreType = type === "int8" || type === "int16" ? type === "int8" ? "uint8" : "uint16" : type;
613
+ const stop = proc.freeze(addr, coreType, parseNumeric(a.value, WIDE_TYPES.has(type)), a.ms ?? 50);
614
+ return { freezeId: s.addFreeze(a.session, stop) };
615
+ }
616
+ },
617
+ mem_unfreeze: {
618
+ description: "Stop a freeze created by mem_freeze",
619
+ schema: { freezeId: z.string() },
620
+ handler: (s, a) => {
621
+ s.removeFreeze(a.freezeId);
622
+ return { stopped: true };
623
+ }
624
+ },
625
+ mem_undo: {
626
+ description: "Revert the last n journaled writes (default 1), most-recent first — the self-correction primitive.",
627
+ schema: {
628
+ ...sessionArg,
629
+ n: z.number().int().positive().optional()
630
+ },
631
+ destructive: true,
632
+ handler: (s, a) => {
633
+ return { reverted: s.get(a.session).undo(a.n ?? 1) };
634
+ }
635
+ },
636
+ mem_journal: {
637
+ description: "Snapshot of the write journal for this session: [{index,addr,bytes|null}] — bytes is the pre-write hex that undo() restores.",
638
+ schema: sessionArg,
639
+ handler: (s, a) => s.get(a.session).writeLog
640
+ },
641
+ eval_js: {
642
+ description: "Escape hatch: run JS with the bound Process in scope as `proc`. Code is wrapped in an async body — use `return` for a result, `await` is allowed.",
643
+ schema: {
644
+ ...sessionArg,
645
+ code: z.string().describe("JS body; `proc` is the open Process")
646
+ },
647
+ destructive: true,
648
+ handler: async (s, a) => {
649
+ const proc = s.get(a.session);
650
+ if (typeof a.code !== "string" || !a.code) throw fail("INVALID_ARGS", "eval_js needs a code string");
651
+ try {
652
+ return { result: await new Function("proc", "Process", `return (async () => { ${a.code} })()`)(proc, Process) };
653
+ } catch (e) {
654
+ const err = e;
655
+ throw fail(err?.code ?? "EVAL_ERROR", err?.message ?? String(e));
656
+ }
657
+ }
658
+ }
659
+ };
660
+ const TOOL_DEFS = toolDefs;
661
+ function toolNames(opts = {}) {
662
+ const evalEnabled = opts.evalEnabled ?? true;
663
+ return Object.keys(toolDefs).filter((n) => evalEnabled || n !== "eval_js");
664
+ }
665
+ /** Direct dispatcher used by both the stdio server and the tests. */
666
+ async function callTool(sessions, name, args, opts = {}) {
667
+ try {
668
+ const evalEnabled = opts.evalEnabled ?? true;
669
+ const def = toolDefs[name];
670
+ if (!def || name === "eval_js" && !evalEnabled) throw fail("UNKNOWN_TOOL", `unknown tool "${name}"`);
671
+ const parsed = z.object(def.schema).safeParse(args ?? {});
672
+ if (!parsed.success) {
673
+ const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
674
+ throw fail("INVALID_ARGS", msg);
675
+ }
676
+ return {
677
+ ok: true,
678
+ data: jsonSafe(await def.handler(sessions, parsed.data))
679
+ };
680
+ } catch (e) {
681
+ return errorEnvelope(e);
682
+ }
683
+ }
684
+ // src/server.ts
685
+ /**
686
+ * @memoryprocess/mcp — MCP server exposing the memoryprocess toolkit to
687
+ * LLM agents over stdio (the primary agentic surface).
688
+ *
689
+ * Usage:
690
+ * memoryprocess-mcp # serve MCP over stdio
691
+ * memoryprocess-mcp --no-eval # omit the eval_js tool (restricted mode)
692
+ * memoryprocess-mcp --help # list tools + exit
693
+ * memoryprocess-mcp --version
694
+ *
695
+ * Env: MEMP_NO_EVAL=1 is equivalent to --no-eval.
696
+ */
697
+ const dir = dirname(fileURLToPath(import.meta.url));
698
+ const pkg = JSON.parse(readFileSync(join(dir, "..", "package.json"), "utf8"));
699
+ const argv = process.argv.slice(2);
700
+ const evalEnabled = !argv.includes("--no-eval") && process.env.MEMP_NO_EVAL !== "1";
701
+ if (argv.includes("--version") || argv.includes("-v")) {
702
+ console.log(`${pkg.name} ${pkg.version}`);
703
+ process.exit(0);
704
+ }
705
+ if (argv.includes("--help") || argv.includes("-h")) {
706
+ console.log(`${pkg.name} ${pkg.version}`);
707
+ console.log("MCP server exposing the memoryprocess toolkit to LLM agents (stdio).");
708
+ console.log("");
709
+ console.log("Usage: memoryprocess-mcp [--no-eval] [--help] [--version]");
710
+ console.log(" MEMP_NO_EVAL=1 is equivalent to --no-eval");
711
+ console.log("");
712
+ console.log("Session model: proc_open returns a handle; every mem_* tool");
713
+ console.log("takes it as `session`. Writes are journaled (mem_journal) and");
714
+ console.log("reversible (mem_undo); mem_write supports dryRun previews.");
715
+ console.log("");
716
+ console.log("Tools:");
717
+ const names = toolNames({ evalEnabled });
718
+ const width = Math.max(...names.map((n) => n.length));
719
+ for (const n of names) {
720
+ const def = TOOL_DEFS[n];
721
+ console.log(` ${n.padEnd(width)} ${def.description}`);
722
+ }
723
+ if (!evalEnabled) console.log("\n(eval_js is disabled by --no-eval / MEMP_NO_EVAL=1)");
724
+ process.exit(0);
725
+ }
726
+ const sessions = createSessions();
727
+ const server = new McpServer({
728
+ name: pkg.name,
729
+ version: pkg.version
730
+ }, { instructions: "Process-memory toolkit. proc_open a target to get a session handle; all mem_* tools take it. Writes are journaled: mem_write with dryRun:true previews, mem_undo reverts. Prefer the coarse tools; eval_js is the escape hatch for anything else." });
731
+ for (const name of toolNames({ evalEnabled })) {
732
+ const def = TOOL_DEFS[name];
733
+ server.registerTool(name, {
734
+ description: def.description,
735
+ inputSchema: def.schema,
736
+ annotations: {
737
+ readOnlyHint: !def.destructive,
738
+ destructiveHint: def.destructive === true
739
+ }
740
+ }, async (args) => {
741
+ const result = await callTool(sessions, name, args, { evalEnabled });
742
+ const data = result.ok === true ? result.data !== null && typeof result.data === "object" && !Array.isArray(result.data) ? result.data : { result: result.data } : void 0;
743
+ return {
744
+ content: [{
745
+ type: "text",
746
+ text: JSON.stringify(result)
747
+ }],
748
+ structuredContent: data,
749
+ isError: result.ok !== true
750
+ };
751
+ });
752
+ }
753
+ const shutdown = () => {
754
+ try {
755
+ sessions.closeAll();
756
+ } finally {
757
+ process.exit(0);
758
+ }
759
+ };
760
+ process.on("SIGINT", shutdown);
761
+ process.on("SIGTERM", shutdown);
762
+ const transport = new StdioServerTransport();
763
+ await server.connect(transport);
764
+ process.stderr.write(`${pkg.name} ${pkg.version} ready on stdio (${toolNames({ evalEnabled }).length} tools${evalEnabled ? "" : ", eval_js disabled"})\n`);
765
+ export {};
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@memoryprocess/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "MCP server exposing the memoryprocess toolkit to LLM agents — the primary agentic surface.",
5
+ "type": "module",
6
+ "packageManager": "bun@1.4.4",
7
+ "bin": {
8
+ "memoryprocess-mcp": "./dist/server.mjs"
9
+ },
10
+ "exports": {
11
+ ".": "./dist/server.mjs"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "os": [
17
+ "win32"
18
+ ],
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "scripts": {
23
+ "build": "tsdown",
24
+ "check": "bun check",
25
+ "typecheck": "tsc --noEmit"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/memoryprocess/memoryprocess.git",
33
+ "directory": "packages/mcp"
34
+ },
35
+ "author": "JoShMiQueL",
36
+ "license": "MIT",
37
+ "keywords": [
38
+ "mcp",
39
+ "model-context-protocol",
40
+ "memory",
41
+ "process",
42
+ "windows",
43
+ "reverse-engineering",
44
+ "llm",
45
+ "agent"
46
+ ],
47
+ "dependencies": {
48
+ "@memoryprocess/core": "0.0.1",
49
+ "@modelcontextprotocol/sdk": "^1.32.2",
50
+ "zod": "^4.6.7"
51
+ },
52
+ "devDependencies": {
53
+ "@types/bun": "^1.4.4"
54
+ }
55
+ }