@memoryprocess/mcp 0.0.0-stage → 0.0.3-beta.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.
- package/README.md +73 -2
- package/dist/server.mjs +832 -0
- package/package.json +53 -4
package/README.md
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @memoryprocess/mcp
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
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: "int8",
|
|
121
|
+
u8: "uint8",
|
|
122
|
+
i16: "int16",
|
|
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_scan_pattern: {
|
|
525
|
+
description: "AOB signature scan: find all matches of a byte pattern like \"48 8B ?? 10\" (\"??\" wildcards). Returns {hits:[hexaddr],count,truncated}.",
|
|
526
|
+
schema: {
|
|
527
|
+
...sessionArg,
|
|
528
|
+
pattern: z.string().describe("byte pattern, hex bytes separated by spaces, \"??\" wildcards"),
|
|
529
|
+
module: z.string().optional().describe("restrict to a module"),
|
|
530
|
+
section: z.string().optional().describe("restrict to a PE section of `module` (e.g. \".text\")"),
|
|
531
|
+
start: addrArg.optional().describe("range start"),
|
|
532
|
+
end: addrArg.optional().describe("range end"),
|
|
533
|
+
onlyReadable: z.boolean().optional().describe("only scan readable regions (default true)"),
|
|
534
|
+
limit: z.number().int().positive().optional().describe("max hits returned (default 1000)")
|
|
535
|
+
},
|
|
536
|
+
handler: async (s, a) => {
|
|
537
|
+
const proc = s.get(a.session);
|
|
538
|
+
if (typeof a.pattern !== "string" || !a.pattern.trim()) throw fail("INVALID_ARGS", "mem_scan_pattern needs a pattern string");
|
|
539
|
+
const hits = await proc.scan(a.pattern, {
|
|
540
|
+
module: a.module,
|
|
541
|
+
section: a.section,
|
|
542
|
+
start: a.start !== void 0 ? parseAddress(proc, a.start, "start") : void 0,
|
|
543
|
+
end: a.end !== void 0 ? parseAddress(proc, a.end, "end") : void 0,
|
|
544
|
+
onlyReadable: a.onlyReadable
|
|
545
|
+
});
|
|
546
|
+
const limit = a.limit ?? 1e3;
|
|
547
|
+
return {
|
|
548
|
+
count: hits.length,
|
|
549
|
+
truncated: hits.length > limit,
|
|
550
|
+
hits: hits.slice(0, limit).map((h) => hex(h))
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
mem_regions: {
|
|
555
|
+
description: "VirtualQueryEx map of the target's memory regions: [{base,size,protect,state,type}] -- base/size are hex, protect/state/type are hex flag values. writableOnly/executableOnly filter on protection.",
|
|
556
|
+
schema: {
|
|
557
|
+
...sessionArg,
|
|
558
|
+
writableOnly: z.boolean().optional().describe("only regions whose protection allows writes"),
|
|
559
|
+
executableOnly: z.boolean().optional().describe("only regions whose protection allows execution")
|
|
560
|
+
},
|
|
561
|
+
handler: (s, a) => {
|
|
562
|
+
const proc = s.get(a.session);
|
|
563
|
+
const WRITABLE = 204;
|
|
564
|
+
const EXECUTABLE = 240;
|
|
565
|
+
let regions = proc.regions();
|
|
566
|
+
if (a.writableOnly === true) regions = regions.filter((r) => (r.protection & WRITABLE) !== 0);
|
|
567
|
+
if (a.executableOnly === true) regions = regions.filter((r) => (r.protection & EXECUTABLE) !== 0);
|
|
568
|
+
return {
|
|
569
|
+
count: regions.length,
|
|
570
|
+
regions: regions.map((r) => ({
|
|
571
|
+
base: hex(r.baseAddress),
|
|
572
|
+
size: hex(r.size),
|
|
573
|
+
protect: hex(r.protection),
|
|
574
|
+
state: hex(4096),
|
|
575
|
+
type: hex(r.type)
|
|
576
|
+
}))
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
mem_pointer_resolve: {
|
|
581
|
+
description: "Resolve module+rva plus a pointer chain: base = module base + rva, then dereference each offset (CE \"module.exe+RVA, offsets\" style).",
|
|
582
|
+
schema: {
|
|
583
|
+
...sessionArg,
|
|
584
|
+
module: z.string().describe("module basename e.g. \"victim.exe\""),
|
|
585
|
+
rva: z.union([z.string(), z.number()]).describe("offset from module base (hex ok)"),
|
|
586
|
+
offsets: z.array(z.union([z.string(), z.number()])).describe("pointer-chain offsets; all but the last dereference")
|
|
587
|
+
},
|
|
588
|
+
handler: (s, a) => {
|
|
589
|
+
const proc = s.get(a.session);
|
|
590
|
+
const mod = proc.module(a.module);
|
|
591
|
+
if (!mod) throw fail("MODULE_NOT_FOUND", `module not found: ${a.module}`);
|
|
592
|
+
let rva;
|
|
593
|
+
try {
|
|
594
|
+
rva = BigInt(a.rva);
|
|
595
|
+
} catch {
|
|
596
|
+
throw fail("INVALID_ARGS", `bad rva "${a.rva}"`);
|
|
597
|
+
}
|
|
598
|
+
const offsets = a.offsets.map((o, i) => {
|
|
599
|
+
const n = typeof o === "string" ? Number(BigInt(o)) : Number(o);
|
|
600
|
+
if (!Number.isFinite(n)) throw fail("INVALID_ARGS", `offsets[${i}] invalid`);
|
|
601
|
+
return n;
|
|
602
|
+
});
|
|
603
|
+
const base = BigInt(mod.base) + rva;
|
|
604
|
+
const addr = offsets.length === 0 ? base : proc.resolvePointer(base, offsets);
|
|
605
|
+
return {
|
|
606
|
+
base: hex(base),
|
|
607
|
+
addr: hex(addr)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
mem_watch: {
|
|
612
|
+
description: "Poll an i32 at addr every ms (default 50); changes are recorded and drained by mem_watch_poll.",
|
|
613
|
+
schema: {
|
|
614
|
+
...sessionArg,
|
|
615
|
+
addr: addrArg,
|
|
616
|
+
ms: z.number().int().positive().optional().describe("poll interval ms")
|
|
617
|
+
},
|
|
618
|
+
handler: (s, a) => {
|
|
619
|
+
const proc = s.get(a.session);
|
|
620
|
+
const addr = parseAddress(proc, a.addr);
|
|
621
|
+
const events = [];
|
|
622
|
+
const stop = proc.watch(addr, (v) => events.push({
|
|
623
|
+
value: v,
|
|
624
|
+
ts: Date.now()
|
|
625
|
+
}), a.ms ?? 50);
|
|
626
|
+
return { watchId: s.addWatch(a.session, {
|
|
627
|
+
events,
|
|
628
|
+
stop
|
|
629
|
+
}) };
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
mem_watch_poll: {
|
|
633
|
+
description: "Drain the recorded changes for a watchId: returns [{value,ts}] since the last poll.",
|
|
634
|
+
schema: { watchId: z.string() },
|
|
635
|
+
handler: (s, a) => {
|
|
636
|
+
const w = s.getWatch(a.watchId);
|
|
637
|
+
const events = w.events;
|
|
638
|
+
w.events = [];
|
|
639
|
+
return {
|
|
640
|
+
events,
|
|
641
|
+
count: events.length
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
mem_unwatch: {
|
|
646
|
+
description: "Stop a watch created by mem_watch",
|
|
647
|
+
schema: { watchId: z.string() },
|
|
648
|
+
handler: (s, a) => {
|
|
649
|
+
s.removeWatch(a.watchId);
|
|
650
|
+
return { stopped: true };
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
mem_freeze: {
|
|
654
|
+
description: "Freeze a value: rewrites `value` at addr every ms (default 50) until mem_unfreeze.",
|
|
655
|
+
schema: {
|
|
656
|
+
...sessionArg,
|
|
657
|
+
addr: addrArg,
|
|
658
|
+
type: z.string().describe("i8 u8 i16 u16 i32 u32 i64 u64 f32 f64 ptr"),
|
|
659
|
+
value: z.union([z.string(), z.number()]),
|
|
660
|
+
ms: z.number().int().positive().optional()
|
|
661
|
+
},
|
|
662
|
+
destructive: true,
|
|
663
|
+
handler: (s, a) => {
|
|
664
|
+
const proc = s.get(a.session);
|
|
665
|
+
const addr = parseAddress(proc, a.addr);
|
|
666
|
+
const type = normType(a.type);
|
|
667
|
+
if (type === "str" || type === "wstr" || type === "bytes") throw fail("INVALID_ARGS", `freeze needs a numeric type, got "${a.type}"`);
|
|
668
|
+
const ms = a.ms ?? 50;
|
|
669
|
+
let stop;
|
|
670
|
+
if (type === "int8" || type === "uint8" || type === "int16" || type === "uint16") {
|
|
671
|
+
const v = Number(parseNumeric(a.value, false));
|
|
672
|
+
const write = type === "int8" || type === "uint8" ? () => proc.writeUInt8(addr, v & 255) : () => proc.writeUInt16(addr, v & 65535);
|
|
673
|
+
const timer = setInterval(() => {
|
|
674
|
+
try {
|
|
675
|
+
write();
|
|
676
|
+
} catch {}
|
|
677
|
+
}, ms);
|
|
678
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
679
|
+
stop = () => clearInterval(timer);
|
|
680
|
+
} else stop = proc.freeze(addr, type, parseNumeric(a.value, WIDE_TYPES.has(type)), ms);
|
|
681
|
+
return { freezeId: s.addFreeze(a.session, stop) };
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
mem_unfreeze: {
|
|
685
|
+
description: "Stop a freeze created by mem_freeze",
|
|
686
|
+
schema: { freezeId: z.string() },
|
|
687
|
+
handler: (s, a) => {
|
|
688
|
+
s.removeFreeze(a.freezeId);
|
|
689
|
+
return { stopped: true };
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
mem_undo: {
|
|
693
|
+
description: "Revert the last n journaled writes (default 1), most-recent first -- the self-correction primitive.",
|
|
694
|
+
schema: {
|
|
695
|
+
...sessionArg,
|
|
696
|
+
n: z.number().int().positive().optional()
|
|
697
|
+
},
|
|
698
|
+
destructive: true,
|
|
699
|
+
handler: (s, a) => {
|
|
700
|
+
return { reverted: s.get(a.session).undo(a.n ?? 1) };
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
mem_journal: {
|
|
704
|
+
description: "Snapshot of the write journal for this session: [{index,addr,bytes|null}] -- bytes is the pre-write hex that undo() restores.",
|
|
705
|
+
schema: sessionArg,
|
|
706
|
+
handler: (s, a) => s.get(a.session).writeLog
|
|
707
|
+
},
|
|
708
|
+
eval_js: {
|
|
709
|
+
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.",
|
|
710
|
+
schema: {
|
|
711
|
+
...sessionArg,
|
|
712
|
+
code: z.string().describe("JS body; `proc` is the open Process")
|
|
713
|
+
},
|
|
714
|
+
destructive: true,
|
|
715
|
+
handler: async (s, a) => {
|
|
716
|
+
const proc = s.get(a.session);
|
|
717
|
+
if (typeof a.code !== "string" || !a.code) throw fail("INVALID_ARGS", "eval_js needs a code string");
|
|
718
|
+
try {
|
|
719
|
+
return { result: await new Function("proc", "Process", `return (async () => { ${a.code} })()`)(proc, Process) };
|
|
720
|
+
} catch (e) {
|
|
721
|
+
const err = e;
|
|
722
|
+
throw fail(err?.code ?? "EVAL_ERROR", err?.message ?? String(e));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const TOOL_DEFS = toolDefs;
|
|
728
|
+
function toolNames(opts = {}) {
|
|
729
|
+
const evalEnabled = opts.evalEnabled ?? true;
|
|
730
|
+
return Object.keys(toolDefs).filter((n) => evalEnabled || n !== "eval_js");
|
|
731
|
+
}
|
|
732
|
+
/** Direct dispatcher used by both the stdio server and the tests. */
|
|
733
|
+
async function callTool(sessions, name, args, opts = {}) {
|
|
734
|
+
try {
|
|
735
|
+
const evalEnabled = opts.evalEnabled ?? true;
|
|
736
|
+
const def = toolDefs[name];
|
|
737
|
+
if (!def || name === "eval_js" && !evalEnabled) throw fail("UNKNOWN_TOOL", `unknown tool "${name}"`);
|
|
738
|
+
const parsed = z.object(def.schema).safeParse(args ?? {});
|
|
739
|
+
if (!parsed.success) {
|
|
740
|
+
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
741
|
+
throw fail("INVALID_ARGS", msg);
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
ok: true,
|
|
745
|
+
data: jsonSafe(await def.handler(sessions, parsed.data))
|
|
746
|
+
};
|
|
747
|
+
} catch (e) {
|
|
748
|
+
return errorEnvelope(e);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// src/server.ts
|
|
752
|
+
/**
|
|
753
|
+
* @memoryprocess/mcp -- MCP server exposing the memoryprocess toolkit to
|
|
754
|
+
* LLM agents over stdio (the primary agentic surface).
|
|
755
|
+
*
|
|
756
|
+
* Usage:
|
|
757
|
+
* memoryprocess-mcp # serve MCP over stdio
|
|
758
|
+
* memoryprocess-mcp --no-eval # omit the eval_js tool (restricted mode)
|
|
759
|
+
* memoryprocess-mcp --help # list tools + exit
|
|
760
|
+
* memoryprocess-mcp --version
|
|
761
|
+
*
|
|
762
|
+
* Env: MEMP_NO_EVAL=1 is equivalent to --no-eval.
|
|
763
|
+
*/
|
|
764
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
765
|
+
const pkg = JSON.parse(readFileSync(join(dir, "..", "package.json"), "utf8"));
|
|
766
|
+
const argv = process.argv.slice(2);
|
|
767
|
+
const evalEnabled = !argv.includes("--no-eval") && process.env.MEMP_NO_EVAL !== "1";
|
|
768
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
769
|
+
console.log(`${pkg.name} ${pkg.version}`);
|
|
770
|
+
process.exit(0);
|
|
771
|
+
}
|
|
772
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
773
|
+
console.log(`${pkg.name} ${pkg.version}`);
|
|
774
|
+
console.log("MCP server exposing the memoryprocess toolkit to LLM agents (stdio).");
|
|
775
|
+
console.log("");
|
|
776
|
+
console.log("Usage: memoryprocess-mcp [--no-eval] [--help] [--version]");
|
|
777
|
+
console.log(" MEMP_NO_EVAL=1 is equivalent to --no-eval");
|
|
778
|
+
console.log("");
|
|
779
|
+
console.log("Session model: proc_open returns a handle; every mem_* tool");
|
|
780
|
+
console.log("takes it as `session`. Writes are journaled (mem_journal) and");
|
|
781
|
+
console.log("reversible (mem_undo); mem_write supports dryRun previews.");
|
|
782
|
+
console.log("");
|
|
783
|
+
console.log("Tools:");
|
|
784
|
+
const names = toolNames({ evalEnabled });
|
|
785
|
+
const width = Math.max(...names.map((n) => n.length));
|
|
786
|
+
for (const n of names) {
|
|
787
|
+
const def = TOOL_DEFS[n];
|
|
788
|
+
console.log(` ${n.padEnd(width)} ${def.description}`);
|
|
789
|
+
}
|
|
790
|
+
if (!evalEnabled) console.log("\n(eval_js is disabled by --no-eval / MEMP_NO_EVAL=1)");
|
|
791
|
+
process.exit(0);
|
|
792
|
+
}
|
|
793
|
+
const sessions = createSessions();
|
|
794
|
+
const server = new McpServer({
|
|
795
|
+
name: pkg.name,
|
|
796
|
+
version: pkg.version
|
|
797
|
+
}, { 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." });
|
|
798
|
+
for (const name of toolNames({ evalEnabled })) {
|
|
799
|
+
const def = TOOL_DEFS[name];
|
|
800
|
+
server.registerTool(name, {
|
|
801
|
+
description: def.description,
|
|
802
|
+
inputSchema: def.schema,
|
|
803
|
+
annotations: {
|
|
804
|
+
readOnlyHint: !def.destructive,
|
|
805
|
+
destructiveHint: def.destructive === true
|
|
806
|
+
}
|
|
807
|
+
}, async (args) => {
|
|
808
|
+
const result = await callTool(sessions, name, args, { evalEnabled });
|
|
809
|
+
const data = result.ok === true ? result.data !== null && typeof result.data === "object" && !Array.isArray(result.data) ? result.data : { result: result.data } : void 0;
|
|
810
|
+
return {
|
|
811
|
+
content: [{
|
|
812
|
+
type: "text",
|
|
813
|
+
text: JSON.stringify(result)
|
|
814
|
+
}],
|
|
815
|
+
structuredContent: data,
|
|
816
|
+
isError: result.ok !== true
|
|
817
|
+
};
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
const shutdown = () => {
|
|
821
|
+
try {
|
|
822
|
+
sessions.closeAll();
|
|
823
|
+
} finally {
|
|
824
|
+
process.exit(0);
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
process.on("SIGINT", shutdown);
|
|
828
|
+
process.on("SIGTERM", shutdown);
|
|
829
|
+
const transport = new StdioServerTransport();
|
|
830
|
+
await server.connect(transport);
|
|
831
|
+
process.stderr.write(`${pkg.name} ${pkg.version} ready on stdio (${toolNames({ evalEnabled }).length} tools${evalEnabled ? "" : ", eval_js disabled"})\n`);
|
|
832
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memoryprocess/mcp",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.3-beta.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": "workspace:*",
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
50
|
+
"zod": "^4.6.7"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/bun": "^1.4.4"
|
|
54
|
+
}
|
|
55
|
+
}
|