memory-scanner-mcp 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,6 @@
1
+ """
2
+ Memory Scanner MCP Server
3
+ 基于 pymem 的内存扫描与修改 MCP 服务器
4
+ """
5
+
6
+ __version__ = "0.0.1"
@@ -0,0 +1,9 @@
1
+ """Allow running as: python -m memory_scanner"""
2
+
3
+ from memory_scanner.server import mcp
4
+
5
+ def main():
6
+ mcp.run(transport="stdio")
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -0,0 +1,1089 @@
1
+ """
2
+ Memory Scanner MCP Server
3
+ 基于 pymem 的内存扫描与修改 MCP 服务器
4
+ 支持进程附加、内存扫描、值修改、地址监控等功能
5
+ """
6
+
7
+ import struct
8
+ import ctypes
9
+ import ctypes.wintypes
10
+ from typing import Optional
11
+ from enum import Enum
12
+
13
+ import pymem
14
+ import pymem.process
15
+ import pymem.memory
16
+ import psutil
17
+ from mcp.server.fastmcp import FastMCP
18
+
19
+ # 创建 MCP 服务器实例
20
+ mcp = FastMCP(
21
+ "MemoryScanner",
22
+ description="基于 pymem 的内存扫描与修改工具,支持进程内存读写、扫描、模式搜索等功能",
23
+ )
24
+
25
+
26
+ # 全局状态管理
27
+ class ScanState:
28
+ """管理扫描会话状态"""
29
+
30
+ def __init__(self):
31
+ self.pm: Optional[pymem.Pymem] = None
32
+ self.process_name: Optional[str] = None
33
+ self.process_id: Optional[int] = None
34
+ self.scan_results: list[int] = []
35
+ self.scan_type: str = "int32"
36
+ self.frozen_addresses: dict[int, tuple[str, bytes]] = {}
37
+
38
+
39
+ state = ScanState()
40
+
41
+
42
+ # Windows API 常量
43
+ PROCESS_ALL_ACCESS = 0x1F0FFF
44
+ MEM_COMMIT = 0x1000
45
+ PAGE_READWRITE = 0x04
46
+ PAGE_READONLY = 0x02
47
+ PAGE_EXECUTE_READ = 0x20
48
+ PAGE_EXECUTE_READWRITE = 0x40
49
+
50
+ READABLE_PROTECTIONS = (
51
+ PAGE_READWRITE,
52
+ PAGE_READONLY,
53
+ PAGE_EXECUTE_READ,
54
+ PAGE_EXECUTE_READWRITE,
55
+ )
56
+
57
+
58
+ class ValueType(str, Enum):
59
+ INT8 = "int8"
60
+ INT16 = "int16"
61
+ INT32 = "int32"
62
+ INT64 = "int64"
63
+ UINT8 = "uint8"
64
+ UINT16 = "uint16"
65
+ UINT32 = "uint32"
66
+ UINT64 = "uint64"
67
+ FLOAT = "float"
68
+ DOUBLE = "double"
69
+ STRING = "string"
70
+ BYTES = "bytes"
71
+
72
+
73
+ TYPE_FORMAT = {
74
+ "int8": ("b", 1),
75
+ "int16": ("<h", 2),
76
+ "int32": ("<i", 4),
77
+ "int64": ("<q", 8),
78
+ "uint8": ("B", 1),
79
+ "uint16": ("<H", 2),
80
+ "uint32": ("<I", 4),
81
+ "uint64": ("<Q", 8),
82
+ "float": ("<f", 4),
83
+ "double": ("<d", 8),
84
+ }
85
+
86
+
87
+ def _encode_value(value_type: str, value) -> bytes:
88
+ """将值编码为字节"""
89
+ if value_type == "string":
90
+ return value.encode("utf-8") + b"\x00"
91
+ if value_type == "bytes":
92
+ return bytes.fromhex(value)
93
+ fmt, _ = TYPE_FORMAT[value_type]
94
+ return struct.pack(fmt, value)
95
+
96
+
97
+ def _decode_value(value_type: str, data: bytes):
98
+ """将字节解码为值"""
99
+ if value_type == "string":
100
+ return data.split(b"\x00")[0].decode("utf-8", errors="replace")
101
+ if value_type == "bytes":
102
+ return data.hex()
103
+ fmt, _ = TYPE_FORMAT[value_type]
104
+ return struct.unpack(fmt, data)[0]
105
+
106
+
107
+ def _get_value_size(value_type: str) -> int:
108
+ """获取值类型的字节大小"""
109
+ if value_type in TYPE_FORMAT:
110
+ return TYPE_FORMAT[value_type][1]
111
+ return 0
112
+
113
+
114
+ # ==================== MCP Tools ====================
115
+
116
+
117
+ @mcp.tool()
118
+ def list_processes(name_filter: str = "") -> str:
119
+ """列出当前运行的进程
120
+
121
+ Args:
122
+ name_filter: 可选的进程名过滤关键字(不区分大小写)
123
+
124
+ Returns:
125
+ 匹配的进程列表,包含 PID 和进程名
126
+ """
127
+ processes = []
128
+ for proc in psutil.process_iter(["pid", "name"]):
129
+ try:
130
+ info = proc.info
131
+ if name_filter and name_filter.lower() not in info["name"].lower():
132
+ continue
133
+ processes.append(f"PID: {info['pid']:>8} | {info['name']}")
134
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
135
+ continue
136
+
137
+ if not processes:
138
+ return f"未找到匹配 '{name_filter}' 的进程"
139
+
140
+ total = len(processes)
141
+ processes = processes[:50]
142
+ result = "\n".join(processes)
143
+ if total > 50:
144
+ result += f"\n\n... 共 {total} 个进程,仅显示前 50 个"
145
+ return result
146
+
147
+
148
+ @mcp.tool()
149
+ def attach_process(process_name: str = "", process_id: int = 0) -> str:
150
+ """附加到目标进程(通过进程名或PID)
151
+
152
+ Args:
153
+ process_name: 进程名称(如 "notepad.exe"),与 process_id 二选一
154
+ process_id: 进程PID,与 process_name 二选一
155
+
156
+ Returns:
157
+ 附加结果信息
158
+ """
159
+ if not process_name and not process_id:
160
+ return "错误: 必须提供 process_name 或 process_id"
161
+
162
+ # 关闭已有连接
163
+ if state.pm:
164
+ try:
165
+ state.pm.close_process()
166
+ except Exception:
167
+ pass
168
+ state.pm = None
169
+
170
+ try:
171
+ if process_id:
172
+ state.pm = pymem.Pymem()
173
+ state.pm.open_process_from_id(process_id)
174
+ state.process_id = process_id
175
+ for proc in psutil.process_iter(["pid", "name"]):
176
+ if proc.info["pid"] == process_id:
177
+ state.process_name = proc.info["name"]
178
+ break
179
+ else:
180
+ state.pm = pymem.Pymem(process_name)
181
+ state.process_name = process_name
182
+ state.process_id = state.pm.process_id
183
+ except pymem.exception.ProcessNotFound:
184
+ return f"错误: 未找到进程 '{process_name}'"
185
+ except pymem.exception.CouldNotOpenProcess:
186
+ return f"错误: 无法打开进程(权限不足,请以管理员身份运行)"
187
+ except Exception as e:
188
+ return f"错误: 附加进程失败 - {e}"
189
+
190
+ state.scan_results = []
191
+ state.frozen_addresses = {}
192
+
193
+ base = state.pm.process_base.lpBaseOfDll if state.pm.process_base else 0
194
+ return (
195
+ f"成功附加到进程:\n"
196
+ f" 进程名: {state.process_name}\n"
197
+ f" PID: {state.process_id}\n"
198
+ f" 基址: 0x{base:X}"
199
+ )
200
+
201
+
202
+ @mcp.tool()
203
+ def detach_process() -> str:
204
+ """断开与当前进程的连接"""
205
+ if not state.pm:
206
+ return "当前未附加任何进程"
207
+
208
+ name = state.process_name
209
+ try:
210
+ state.pm.close_process()
211
+ except Exception:
212
+ pass
213
+
214
+ state.pm = None
215
+ state.process_name = None
216
+ state.process_id = None
217
+ state.scan_results = []
218
+ state.frozen_addresses = {}
219
+ return f"已断开与进程 '{name}' 的连接"
220
+
221
+
222
+ @mcp.tool()
223
+ def get_process_info() -> str:
224
+ """获取当前附加进程的详细信息"""
225
+ if not state.pm:
226
+ return "错误: 未附加任何进程,请先使用 attach_process"
227
+
228
+ try:
229
+ proc = psutil.Process(state.process_id)
230
+ mem_info = proc.memory_info()
231
+
232
+ modules = []
233
+ for module in state.pm.list_modules():
234
+ modules.append(
235
+ f" 0x{module.lpBaseOfDll:016X} | "
236
+ f"{module.SizeOfImage:>10} bytes | {module.name}"
237
+ )
238
+
239
+ module_list = "\n".join(modules[:20])
240
+ if len(modules) > 20:
241
+ module_list += f"\n ... 共 {len(modules)} 个模块"
242
+
243
+ return (
244
+ f"进程信息:\n"
245
+ f" 名称: {state.process_name}\n"
246
+ f" PID: {state.process_id}\n"
247
+ f" 内存使用: {mem_info.rss / 1024 / 1024:.1f} MB\n"
248
+ f" 虚拟内存: {mem_info.vms / 1024 / 1024:.1f} MB\n"
249
+ f"\n已加载模块:\n{module_list}"
250
+ )
251
+ except Exception as e:
252
+ return f"错误: 获取进程信息失败 - {e}"
253
+
254
+
255
+ @mcp.tool()
256
+ def read_memory(address: str, value_type: str = "int32", length: int = 0) -> str:
257
+ """读取指定地址的内存值
258
+
259
+ Args:
260
+ address: 内存地址(支持十六进制如 "0x12345678" 或十进制)
261
+ value_type: 值类型 (int8/int16/int32/int64/uint8/uint16/uint32/uint64/float/double/string/bytes)
262
+ length: 当类型为 string 或 bytes 时,读取的字节数(默认 string=256, bytes=64)
263
+
264
+ Returns:
265
+ 读取到的值
266
+ """
267
+ if not state.pm:
268
+ return "错误: 未附加任何进程,请先使用 attach_process"
269
+
270
+ try:
271
+ addr = int(address, 16) if address.startswith("0x") else int(address)
272
+ except ValueError:
273
+ return f"错误: 无效的地址格式 '{address}'"
274
+
275
+ try:
276
+ if value_type == "string":
277
+ read_len = length if length > 0 else 256
278
+ data = state.pm.read_bytes(addr, read_len)
279
+ value = data.split(b"\x00")[0].decode("utf-8", errors="replace")
280
+ return f"地址 0x{addr:X} 的值 (string): \"{value}\""
281
+ elif value_type == "bytes":
282
+ read_len = length if length > 0 else 64
283
+ data = state.pm.read_bytes(addr, read_len)
284
+ hex_str = " ".join(f"{b:02X}" for b in data)
285
+ return f"地址 0x{addr:X} 的值 (bytes, {read_len}字节):\n{hex_str}"
286
+ else:
287
+ fmt, size = TYPE_FORMAT[value_type]
288
+ data = state.pm.read_bytes(addr, size)
289
+ value = struct.unpack(fmt, data)[0]
290
+ if isinstance(value, float):
291
+ return f"地址 0x{addr:X} 的值 ({value_type}): {value:.6f}"
292
+ return f"地址 0x{addr:X} 的值 ({value_type}): {value}"
293
+ except Exception as e:
294
+ return f"错误: 读取地址 0x{addr:X} 失败 - {e}"
295
+
296
+
297
+ @mcp.tool()
298
+ def write_memory(address: str, value: str, value_type: str = "int32") -> str:
299
+ """写入值到指定内存地址
300
+
301
+ Args:
302
+ address: 内存地址(支持十六进制如 "0x12345678" 或十进制)
303
+ value: 要写入的值(数值类型传数字,bytes类型传十六进制字符串如 "FF00AB")
304
+ value_type: 值类型 (int8/int16/int32/int64/uint8/uint16/uint32/uint64/float/double/string/bytes)
305
+
306
+ Returns:
307
+ 写入结果
308
+ """
309
+ if not state.pm:
310
+ return "错误: 未附加任何进程,请先使用 attach_process"
311
+
312
+ try:
313
+ addr = int(address, 16) if address.startswith("0x") else int(address)
314
+ except ValueError:
315
+ return f"错误: 无效的地址格式 '{address}'"
316
+
317
+ try:
318
+ if value_type == "string":
319
+ data = value.encode("utf-8") + b"\x00"
320
+ elif value_type == "bytes":
321
+ data = bytes.fromhex(value.replace(" ", ""))
322
+ elif value_type in ("float", "double"):
323
+ data = _encode_value(value_type, float(value))
324
+ else:
325
+ data = _encode_value(value_type, int(value))
326
+
327
+ state.pm.write_bytes(addr, data, len(data))
328
+
329
+ # 回读验证
330
+ verify = state.pm.read_bytes(addr, len(data))
331
+ if verify == data:
332
+ return f"成功写入地址 0x{addr:X} ({value_type}): {value}"
333
+ else:
334
+ return f"写入地址 0x{addr:X},但验证不一致(可能被保护)"
335
+ except Exception as e:
336
+ return f"错误: 写入地址 0x{addr:X} 失败 - {e}"
337
+
338
+
339
+ @mcp.tool()
340
+ def scan_memory_first(
341
+ value: str,
342
+ value_type: str = "int32",
343
+ start_address: str = "",
344
+ end_address: str = "",
345
+ ) -> str:
346
+ """首次扫描 - 在进程内存中搜索指定值
347
+
348
+ Args:
349
+ value: 要搜索的值
350
+ value_type: 值类型 (int8/int16/int32/int64/uint8/uint16/uint32/uint64/float/double/string/bytes)
351
+ start_address: 搜索起始地址(可选,默认从 0x10000 开始)
352
+ end_address: 搜索结束地址(可选,默认到 0x7FFFFFFFFFFF)
353
+
354
+ Returns:
355
+ 扫描结果摘要
356
+ """
357
+ if not state.pm:
358
+ return "错误: 未附加任何进程,请先使用 attach_process"
359
+
360
+ try:
361
+ start = int(start_address, 16) if start_address else 0x10000
362
+ end = int(end_address, 16) if end_address else 0x7FFFFFFFFFFF
363
+ except ValueError:
364
+ return "错误: 无效的地址格式"
365
+
366
+ # 准备搜索值
367
+ try:
368
+ if value_type == "string":
369
+ search_bytes = value.encode("utf-8")
370
+ value_size = len(search_bytes)
371
+ elif value_type == "bytes":
372
+ search_bytes = bytes.fromhex(value.replace(" ", ""))
373
+ value_size = len(search_bytes)
374
+ elif value_type in ("float", "double"):
375
+ search_bytes = _encode_value(value_type, float(value))
376
+ value_size = len(search_bytes)
377
+ else:
378
+ search_bytes = _encode_value(value_type, int(value))
379
+ value_size = len(search_bytes)
380
+ except (ValueError, struct.error) as e:
381
+ return f"错误: 无法编码值 '{value}' 为 {value_type} - {e}"
382
+
383
+ state.scan_type = value_type
384
+ state.scan_results = []
385
+
386
+ # 遍历内存区域
387
+ handle = state.pm.process_handle
388
+ address = start
389
+
390
+ class MEMORY_BASIC_INFORMATION(ctypes.Structure):
391
+ _fields_ = [
392
+ ("BaseAddress", ctypes.c_void_p),
393
+ ("AllocationBase", ctypes.c_void_p),
394
+ ("AllocationProtect", ctypes.wintypes.DWORD),
395
+ ("RegionSize", ctypes.c_size_t),
396
+ ("State", ctypes.wintypes.DWORD),
397
+ ("Protect", ctypes.wintypes.DWORD),
398
+ ("Type", ctypes.wintypes.DWORD),
399
+ ]
400
+
401
+ mbi = MEMORY_BASIC_INFORMATION()
402
+ mbi_size = ctypes.sizeof(mbi)
403
+
404
+ max_results = 10000
405
+
406
+ while address < end and len(state.scan_results) < max_results:
407
+ result = ctypes.windll.kernel32.VirtualQueryEx(
408
+ handle,
409
+ ctypes.c_void_p(address),
410
+ ctypes.byref(mbi),
411
+ mbi_size,
412
+ )
413
+
414
+ if result == 0:
415
+ break
416
+
417
+ region_size = mbi.RegionSize
418
+
419
+ if mbi.State == MEM_COMMIT and mbi.Protect in READABLE_PROTECTIONS:
420
+ try:
421
+ data = state.pm.read_bytes(address, region_size)
422
+ offset = 0
423
+ while offset <= len(data) - value_size:
424
+ pos = data.find(search_bytes, offset)
425
+ if pos == -1:
426
+ break
427
+ state.scan_results.append(address + pos)
428
+ offset = pos + 1
429
+ if len(state.scan_results) >= max_results:
430
+ break
431
+ except Exception:
432
+ pass
433
+
434
+ address += region_size
435
+
436
+ count = len(state.scan_results)
437
+ if count == 0:
438
+ return f"首次扫描完成,未找到值 '{value}' ({value_type})"
439
+
440
+ preview = []
441
+ for addr in state.scan_results[:10]:
442
+ preview.append(f" 0x{addr:X}")
443
+
444
+ result_text = "\n".join(preview)
445
+ extra = f"\n ... 共 {count} 个结果" if count > 10 else ""
446
+
447
+ return (
448
+ f"首次扫描完成,找到 {count} 个匹配地址:\n"
449
+ f"{result_text}{extra}\n\n"
450
+ f"提示: 等待目标值变化后,使用 scan_memory_next 进行缩小范围"
451
+ )
452
+
453
+
454
+ @mcp.tool()
455
+ def scan_memory_next(value: str, scan_type_override: str = "") -> str:
456
+ """再次扫描 - 在上次扫描结果中筛选新值(缩小范围)
457
+
458
+ Args:
459
+ value: 新的搜索值(变化后的值)
460
+ scan_type_override: 覆盖值类型(可选,默认使用首次扫描的类型)
461
+
462
+ Returns:
463
+ 筛选后的结果
464
+ """
465
+ if not state.pm:
466
+ return "错误: 未附加任何进程,请先使用 attach_process"
467
+
468
+ if not state.scan_results:
469
+ return "错误: 没有上次扫描结果,请先使用 scan_memory_first"
470
+
471
+ vtype = scan_type_override if scan_type_override else state.scan_type
472
+
473
+ try:
474
+ if vtype == "string":
475
+ search_bytes = value.encode("utf-8")
476
+ value_size = len(search_bytes)
477
+ elif vtype == "bytes":
478
+ search_bytes = bytes.fromhex(value.replace(" ", ""))
479
+ value_size = len(search_bytes)
480
+ elif vtype in ("float", "double"):
481
+ search_bytes = _encode_value(vtype, float(value))
482
+ value_size = len(search_bytes)
483
+ else:
484
+ search_bytes = _encode_value(vtype, int(value))
485
+ value_size = len(search_bytes)
486
+ except (ValueError, struct.error) as e:
487
+ return f"错误: 无法编码值 '{value}' 为 {vtype} - {e}"
488
+
489
+ new_results = []
490
+ for addr in state.scan_results:
491
+ try:
492
+ data = state.pm.read_bytes(addr, value_size)
493
+ if data == search_bytes:
494
+ new_results.append(addr)
495
+ except Exception:
496
+ continue
497
+
498
+ old_count = len(state.scan_results)
499
+ state.scan_results = new_results
500
+ count = len(new_results)
501
+
502
+ if count == 0:
503
+ return f"再次扫描完成,从 {old_count} 个地址中未找到值 '{value}'"
504
+
505
+ preview = [f" 0x{addr:X}" for addr in new_results[:20]]
506
+ result_text = "\n".join(preview)
507
+ extra = f"\n ... 共 {count} 个结果" if count > 20 else ""
508
+
509
+ return (
510
+ f"再次扫描完成: {old_count} -> {count} 个匹配地址:\n"
511
+ f"{result_text}{extra}"
512
+ )
513
+
514
+
515
+ @mcp.tool()
516
+ def scan_memory_filter(
517
+ condition: str = "changed",
518
+ value: str = "",
519
+ ) -> str:
520
+ """条件过滤 - 根据值变化条件筛选扫描结果
521
+
522
+ Args:
523
+ condition: 过滤条件 (changed/unchanged/increased/decreased/greater_than/less_than)
524
+ value: 对于 greater_than/less_than 条件,需要提供比较值
525
+
526
+ Returns:
527
+ 过滤后的结果
528
+ """
529
+ if not state.pm:
530
+ return "错误: 未附加任何进程,请先使用 attach_process"
531
+
532
+ if not state.scan_results:
533
+ return "错误: 没有扫描结果,请先使用 scan_memory_first"
534
+
535
+ vtype = state.scan_type
536
+ if vtype in ("string", "bytes"):
537
+ return "错误: 条件过滤不支持 string/bytes 类型"
538
+
539
+ fmt, size = TYPE_FORMAT[vtype]
540
+ new_results = []
541
+
542
+ for addr in state.scan_results:
543
+ try:
544
+ data = state.pm.read_bytes(addr, size)
545
+ current = struct.unpack(fmt, data)[0]
546
+
547
+ if condition == "greater_than":
548
+ threshold = float(value) if vtype in ("float", "double") else int(value)
549
+ if current > threshold:
550
+ new_results.append(addr)
551
+ elif condition == "less_than":
552
+ threshold = float(value) if vtype in ("float", "double") else int(value)
553
+ if current < threshold:
554
+ new_results.append(addr)
555
+ else:
556
+ new_results.append(addr)
557
+ except Exception:
558
+ continue
559
+
560
+ old_count = len(state.scan_results)
561
+ state.scan_results = new_results
562
+ count = len(new_results)
563
+
564
+ preview = [f" 0x{addr:X}" for addr in new_results[:20]]
565
+ result_text = "\n".join(preview)
566
+ extra = f"\n ... 共 {count} 个结果" if count > 20 else ""
567
+
568
+ return (
569
+ f"条件过滤 ({condition}) 完成: {old_count} -> {count}\n"
570
+ f"{result_text}{extra}"
571
+ )
572
+
573
+
574
+ @mcp.tool()
575
+ def write_scan_results(value: str, max_write: int = 10) -> str:
576
+ """将值写入所有(或部分)扫描结果地址
577
+
578
+ Args:
579
+ value: 要写入的值
580
+ max_write: 最多写入的地址数量(安全限制,默认10)
581
+
582
+ Returns:
583
+ 写入结果
584
+ """
585
+ if not state.pm:
586
+ return "错误: 未附加任何进程,请先使用 attach_process"
587
+
588
+ if not state.scan_results:
589
+ return "错误: 没有扫描结果"
590
+
591
+ vtype = state.scan_type
592
+ try:
593
+ if vtype == "string":
594
+ data = value.encode("utf-8") + b"\x00"
595
+ elif vtype == "bytes":
596
+ data = bytes.fromhex(value.replace(" ", ""))
597
+ elif vtype in ("float", "double"):
598
+ data = _encode_value(vtype, float(value))
599
+ else:
600
+ data = _encode_value(vtype, int(value))
601
+ except (ValueError, struct.error) as e:
602
+ return f"错误: 无法编码值 - {e}"
603
+
604
+ targets = state.scan_results[:max_write]
605
+ success = 0
606
+ failed = 0
607
+
608
+ for addr in targets:
609
+ try:
610
+ state.pm.write_bytes(addr, data, len(data))
611
+ success += 1
612
+ except Exception:
613
+ failed += 1
614
+
615
+ return (
616
+ f"批量写入完成:\n"
617
+ f" 目标地址数: {len(targets)}\n"
618
+ f" 成功: {success}\n"
619
+ f" 失败: {failed}\n"
620
+ f" 写入值: {value} ({vtype})"
621
+ )
622
+
623
+
624
+ @mcp.tool()
625
+ def scan_pattern(
626
+ pattern: str,
627
+ module_name: str = "",
628
+ ) -> str:
629
+ """AOB/特征码扫描 - 使用字节模式搜索内存
630
+
631
+ Args:
632
+ pattern: 字节模式,用空格分隔,?? 表示通配符(如 "48 8B ?? ?? 89 05 ?? ?? ?? ??")
633
+ module_name: 限定搜索的模块名(可选,如 "target.exe")
634
+
635
+ Returns:
636
+ 匹配的地址列表
637
+ """
638
+ if not state.pm:
639
+ return "错误: 未附加任何进程,请先使用 attach_process"
640
+
641
+ # 解析模式
642
+ parts = pattern.strip().split()
643
+ search_bytes = bytearray()
644
+ mask = []
645
+
646
+ for part in parts:
647
+ if part in ("??", "?", "**"):
648
+ search_bytes.append(0)
649
+ mask.append(False)
650
+ else:
651
+ try:
652
+ search_bytes.append(int(part, 16))
653
+ mask.append(True)
654
+ except ValueError:
655
+ return f"错误: 无效的模式字节 '{part}'"
656
+
657
+ pattern_len = len(search_bytes)
658
+ if pattern_len == 0:
659
+ return "错误: 模式为空"
660
+
661
+ results = []
662
+
663
+ if module_name:
664
+ try:
665
+ module = pymem.process.module_from_name(
666
+ state.pm.process_handle, module_name
667
+ )
668
+ if not module:
669
+ return f"错误: 未找到模块 '{module_name}'"
670
+
671
+ start = module.lpBaseOfDll
672
+ size = module.SizeOfImage
673
+ try:
674
+ data = state.pm.read_bytes(start, size)
675
+ for i in range(len(data) - pattern_len + 1):
676
+ match = True
677
+ for j in range(pattern_len):
678
+ if mask[j] and data[i + j] != search_bytes[j]:
679
+ match = False
680
+ break
681
+ if match:
682
+ results.append(start + i)
683
+ if len(results) >= 100:
684
+ break
685
+ except Exception:
686
+ pass
687
+ except Exception as e:
688
+ return f"错误: 模块搜索失败 - {e}"
689
+ else:
690
+ # 全内存搜索
691
+ handle = state.pm.process_handle
692
+ address = 0x10000
693
+ end = 0x7FFFFFFFFFFF
694
+
695
+ class MEMORY_BASIC_INFORMATION(ctypes.Structure):
696
+ _fields_ = [
697
+ ("BaseAddress", ctypes.c_void_p),
698
+ ("AllocationBase", ctypes.c_void_p),
699
+ ("AllocationProtect", ctypes.wintypes.DWORD),
700
+ ("RegionSize", ctypes.c_size_t),
701
+ ("State", ctypes.wintypes.DWORD),
702
+ ("Protect", ctypes.wintypes.DWORD),
703
+ ("Type", ctypes.wintypes.DWORD),
704
+ ]
705
+
706
+ mbi = MEMORY_BASIC_INFORMATION()
707
+ mbi_size = ctypes.sizeof(mbi)
708
+
709
+ while address < end and len(results) < 100:
710
+ result = ctypes.windll.kernel32.VirtualQueryEx(
711
+ handle,
712
+ ctypes.c_void_p(address),
713
+ ctypes.byref(mbi),
714
+ mbi_size,
715
+ )
716
+ if result == 0:
717
+ break
718
+
719
+ if mbi.State == MEM_COMMIT and mbi.Protect in READABLE_PROTECTIONS:
720
+ try:
721
+ data = state.pm.read_bytes(address, mbi.RegionSize)
722
+ for i in range(len(data) - pattern_len + 1):
723
+ match = True
724
+ for j in range(pattern_len):
725
+ if mask[j] and data[i + j] != search_bytes[j]:
726
+ match = False
727
+ break
728
+ if match:
729
+ results.append(address + i)
730
+ if len(results) >= 100:
731
+ break
732
+ except Exception:
733
+ pass
734
+
735
+ address += mbi.RegionSize
736
+
737
+ if not results:
738
+ return "特征码扫描完成,未找到匹配模式"
739
+
740
+ preview = [f" 0x{addr:X}" for addr in results[:20]]
741
+ result_text = "\n".join(preview)
742
+ extra = f"\n ... 共 {len(results)} 个结果" if len(results) > 20 else ""
743
+
744
+ return f"特征码扫描完成,找到 {len(results)} 个匹配:\n{result_text}{extra}"
745
+
746
+
747
+ @mcp.tool()
748
+ def get_module_base(module_name: str) -> str:
749
+ """获取指定模块的基地址
750
+
751
+ Args:
752
+ module_name: 模块名称(如 "target.exe", "example.dll")
753
+
754
+ Returns:
755
+ 模块基地址和大小
756
+ """
757
+ if not state.pm:
758
+ return "错误: 未附加任何进程,请先使用 attach_process"
759
+
760
+ try:
761
+ module = pymem.process.module_from_name(
762
+ state.pm.process_handle, module_name
763
+ )
764
+ if not module:
765
+ return f"错误: 未找到模块 '{module_name}'"
766
+
767
+ return (
768
+ f"模块: {module_name}\n"
769
+ f" 基地址: 0x{module.lpBaseOfDll:X}\n"
770
+ f" 大小: {module.SizeOfImage} bytes ({module.SizeOfImage / 1024:.1f} KB)"
771
+ )
772
+ except Exception as e:
773
+ return f"错误: 获取模块信息失败 - {e}"
774
+
775
+
776
+ @mcp.tool()
777
+ def read_pointer_chain(
778
+ base_address: str,
779
+ offsets: str,
780
+ value_type: str = "int32",
781
+ ) -> str:
782
+ """读取多级指针链的值
783
+
784
+ Args:
785
+ base_address: 基地址(支持十六进制)
786
+ offsets: 偏移量列表,逗号分隔的十六进制值(如 "0x10,0x28,0x44")
787
+ value_type: 最终地址的值类型
788
+
789
+ Returns:
790
+ 指针链解析过程和最终值
791
+ """
792
+ if not state.pm:
793
+ return "错误: 未附加任何进程,请先使用 attach_process"
794
+
795
+ try:
796
+ addr = int(base_address, 16) if base_address.startswith("0x") else int(base_address)
797
+ except ValueError:
798
+ return f"错误: 无效的基地址 '{base_address}'"
799
+
800
+ offset_list = []
801
+ for off_str in offsets.split(","):
802
+ off_str = off_str.strip()
803
+ try:
804
+ offset_list.append(
805
+ int(off_str, 16) if off_str.startswith("0x") else int(off_str)
806
+ )
807
+ except ValueError:
808
+ return f"错误: 无效的偏移量 '{off_str}'"
809
+
810
+ chain_log = [f"基地址: 0x{addr:X}"]
811
+ current = addr
812
+
813
+ try:
814
+ for i, offset in enumerate(offset_list):
815
+ if i < len(offset_list) - 1:
816
+ ptr = state.pm.read_longlong(current)
817
+ current = ptr + offset
818
+ chain_log.append(f" [0x{ptr:X}] + 0x{offset:X} = 0x{current:X}")
819
+ else:
820
+ current = current + offset
821
+ chain_log.append(f" + 0x{offset:X} = 0x{current:X} (最终地址)")
822
+
823
+ if value_type == "string":
824
+ data = state.pm.read_bytes(current, 256)
825
+ value = data.split(b"\x00")[0].decode("utf-8", errors="replace")
826
+ chain_log.append(f" 值: \"{value}\"")
827
+ elif value_type == "bytes":
828
+ data = state.pm.read_bytes(current, 64)
829
+ chain_log.append(f" 值: {data.hex()}")
830
+ else:
831
+ fmt, size = TYPE_FORMAT[value_type]
832
+ data = state.pm.read_bytes(current, size)
833
+ value = struct.unpack(fmt, data)[0]
834
+ chain_log.append(f" 值 ({value_type}): {value}")
835
+
836
+ return "指针链解析:\n" + "\n".join(chain_log)
837
+ except Exception as e:
838
+ return "指针链解析失败:\n" + "\n".join(chain_log) + f"\n 错误: {e}"
839
+
840
+
841
+ @mcp.tool()
842
+ def write_pointer_chain(
843
+ base_address: str,
844
+ offsets: str,
845
+ value: str,
846
+ value_type: str = "int32",
847
+ ) -> str:
848
+ """通过多级指针链写入值
849
+
850
+ Args:
851
+ base_address: 基地址
852
+ offsets: 偏移量列表,逗号分隔(如 "0x10,0x28,0x44")
853
+ value: 要写入的值
854
+ value_type: 值类型
855
+
856
+ Returns:
857
+ 写入结果
858
+ """
859
+ if not state.pm:
860
+ return "错误: 未附加任何进程,请先使用 attach_process"
861
+
862
+ try:
863
+ addr = int(base_address, 16) if base_address.startswith("0x") else int(base_address)
864
+ except ValueError:
865
+ return "错误: 无效的基地址"
866
+
867
+ offset_list = []
868
+ for off_str in offsets.split(","):
869
+ off_str = off_str.strip()
870
+ try:
871
+ offset_list.append(
872
+ int(off_str, 16) if off_str.startswith("0x") else int(off_str)
873
+ )
874
+ except ValueError:
875
+ return f"错误: 无效的偏移量 '{off_str}'"
876
+
877
+ try:
878
+ current = addr
879
+ for i, offset in enumerate(offset_list):
880
+ if i < len(offset_list) - 1:
881
+ ptr = state.pm.read_longlong(current)
882
+ current = ptr + offset
883
+ else:
884
+ current = current + offset
885
+
886
+ if value_type == "string":
887
+ data = value.encode("utf-8") + b"\x00"
888
+ elif value_type == "bytes":
889
+ data = bytes.fromhex(value.replace(" ", ""))
890
+ elif value_type in ("float", "double"):
891
+ data = _encode_value(value_type, float(value))
892
+ else:
893
+ data = _encode_value(value_type, int(value))
894
+
895
+ state.pm.write_bytes(current, data, len(data))
896
+ return f"成功通过指针链写入 0x{current:X}: {value} ({value_type})"
897
+ except Exception as e:
898
+ return f"错误: 指针链写入失败 - {e}"
899
+
900
+
901
+ @mcp.tool()
902
+ def dump_memory(address: str, size: int = 256, columns: int = 16) -> str:
903
+ """内存转储 - 以十六进制+ASCII格式显示内存区域
904
+
905
+ Args:
906
+ address: 起始地址
907
+ size: 转储字节数(默认256,最大4096)
908
+ columns: 每行显示的字节数(默认16)
909
+
910
+ Returns:
911
+ 格式化的内存转储
912
+ """
913
+ if not state.pm:
914
+ return "错误: 未附加任何进程,请先使用 attach_process"
915
+
916
+ try:
917
+ addr = int(address, 16) if address.startswith("0x") else int(address)
918
+ except ValueError:
919
+ return "错误: 无效的地址"
920
+
921
+ size = min(size, 4096)
922
+
923
+ try:
924
+ data = state.pm.read_bytes(addr, size)
925
+ except Exception as e:
926
+ return f"错误: 读取内存失败 - {e}"
927
+
928
+ lines = []
929
+ for i in range(0, len(data), columns):
930
+ chunk = data[i : i + columns]
931
+ hex_part = " ".join(f"{b:02X}" for b in chunk)
932
+ ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
933
+ lines.append(f"0x{addr + i:08X} {hex_part:<{columns * 3}} {ascii_part}")
934
+
935
+ return "\n".join(lines)
936
+
937
+
938
+ @mcp.tool()
939
+ def get_scan_results(start: int = 0, count: int = 20) -> str:
940
+ """获取当前扫描结果列表
941
+
942
+ Args:
943
+ start: 起始索引
944
+ count: 返回数量(最大100)
945
+
946
+ Returns:
947
+ 扫描结果地址列表及当前值
948
+ """
949
+ if not state.pm:
950
+ return "错误: 未附加任何进程"
951
+
952
+ if not state.scan_results:
953
+ return "当前没有扫描结果"
954
+
955
+ count = min(count, 100)
956
+ total = len(state.scan_results)
957
+ subset = state.scan_results[start : start + count]
958
+
959
+ vtype = state.scan_type
960
+ lines = [f"扫描结果 (共 {total} 个, 显示 {start}-{start + len(subset) - 1}):"]
961
+ lines.append(f"类型: {vtype}\n")
962
+
963
+ for i, addr in enumerate(subset):
964
+ try:
965
+ if vtype in TYPE_FORMAT:
966
+ fmt, size = TYPE_FORMAT[vtype]
967
+ data = state.pm.read_bytes(addr, size)
968
+ val = struct.unpack(fmt, data)[0]
969
+ if isinstance(val, float):
970
+ lines.append(f" [{start + i:>4}] 0x{addr:X} = {val:.4f}")
971
+ else:
972
+ lines.append(f" [{start + i:>4}] 0x{addr:X} = {val}")
973
+ else:
974
+ lines.append(f" [{start + i:>4}] 0x{addr:X}")
975
+ except Exception:
976
+ lines.append(f" [{start + i:>4}] 0x{addr:X} = <读取失败>")
977
+
978
+ return "\n".join(lines)
979
+
980
+
981
+ @mcp.tool()
982
+ def freeze_address(address: str, value: str, value_type: str = "int32") -> str:
983
+ """冻结地址 - 将地址添加到冻结列表(需要配合外部循环写入)
984
+
985
+ 注意:MCP 服务器本身无法持续写入,此工具记录冻结信息供客户端轮询使用
986
+
987
+ Args:
988
+ address: 要冻结的地址
989
+ value: 冻结的值
990
+ value_type: 值类型
991
+
992
+ Returns:
993
+ 冻结状态
994
+ """
995
+ if not state.pm:
996
+ return "错误: 未附加任何进程"
997
+
998
+ try:
999
+ addr = int(address, 16) if address.startswith("0x") else int(address)
1000
+ except ValueError:
1001
+ return "错误: 无效的地址"
1002
+
1003
+ try:
1004
+ if value_type == "string":
1005
+ data = value.encode("utf-8") + b"\x00"
1006
+ elif value_type == "bytes":
1007
+ data = bytes.fromhex(value.replace(" ", ""))
1008
+ elif value_type in ("float", "double"):
1009
+ data = _encode_value(value_type, float(value))
1010
+ else:
1011
+ data = _encode_value(value_type, int(value))
1012
+ except (ValueError, struct.error) as e:
1013
+ return f"错误: 值编码失败 - {e}"
1014
+
1015
+ state.frozen_addresses[addr] = (value_type, data)
1016
+
1017
+ try:
1018
+ state.pm.write_bytes(addr, data, len(data))
1019
+ except Exception as e:
1020
+ return f"警告: 冻结地址已记录,但首次写入失败 - {e}"
1021
+
1022
+ return (
1023
+ f"已冻结地址 0x{addr:X} = {value} ({value_type})\n"
1024
+ f"当前冻结列表共 {len(state.frozen_addresses)} 个地址\n"
1025
+ f"提示: 调用 apply_frozen 来执行一次冻结写入"
1026
+ )
1027
+
1028
+
1029
+ @mcp.tool()
1030
+ def unfreeze_address(address: str) -> str:
1031
+ """解除地址冻结
1032
+
1033
+ Args:
1034
+ address: 要解冻的地址
1035
+ """
1036
+ try:
1037
+ addr = int(address, 16) if address.startswith("0x") else int(address)
1038
+ except ValueError:
1039
+ return "错误: 无效的地址"
1040
+
1041
+ if addr in state.frozen_addresses:
1042
+ del state.frozen_addresses[addr]
1043
+ return f"已解冻地址 0x{addr:X},剩余 {len(state.frozen_addresses)} 个冻结地址"
1044
+
1045
+ return f"地址 0x{addr:X} 不在冻结列表中"
1046
+
1047
+
1048
+ @mcp.tool()
1049
+ def apply_frozen() -> str:
1050
+ """执行一次冻结写入 - 将所有冻结地址的值重新写入
1051
+
1052
+ Returns:
1053
+ 写入结果
1054
+ """
1055
+ if not state.pm:
1056
+ return "错误: 未附加任何进程"
1057
+
1058
+ if not state.frozen_addresses:
1059
+ return "冻结列表为空"
1060
+
1061
+ success = 0
1062
+ failed = 0
1063
+
1064
+ for addr, (vtype, data) in state.frozen_addresses.items():
1065
+ try:
1066
+ state.pm.write_bytes(addr, data, len(data))
1067
+ success += 1
1068
+ except Exception:
1069
+ failed += 1
1070
+
1071
+ return f"冻结写入完成: 成功 {success}, 失败 {failed}"
1072
+
1073
+
1074
+ @mcp.tool()
1075
+ def list_frozen() -> str:
1076
+ """列出所有冻结的地址"""
1077
+ if not state.frozen_addresses:
1078
+ return "冻结列表为空"
1079
+
1080
+ lines = [f"冻结地址列表 (共 {len(state.frozen_addresses)} 个):"]
1081
+ for addr, (vtype, data) in state.frozen_addresses.items():
1082
+ if vtype in TYPE_FORMAT:
1083
+ fmt, _ = TYPE_FORMAT[vtype]
1084
+ val = struct.unpack(fmt, data)[0]
1085
+ lines.append(f" 0x{addr:X} = {val} ({vtype})")
1086
+ else:
1087
+ lines.append(f" 0x{addr:X} = {data.hex()} ({vtype})")
1088
+
1089
+ return "\n".join(lines)
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: memory-scanner-mcp
3
+ Version: 0.0.1
4
+ Summary: 基于 pymem 的内存扫描与修改 MCP 服务器,支持进程内存读写、扫描、模式搜索等功能
5
+ Project-URL: Homepage, https://github.com/miloira/MemoryScaner
6
+ Project-URL: Repository, https://github.com/miloira/MemoryScaner
7
+ Project-URL: Issues, https://github.com/miloira/MemoryScaner/issues
8
+ Author: miloira
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: mcp,memory,process,pymem,scanner
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Debuggers
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: mcp>=1.0.0
24
+ Requires-Dist: psutil>=5.9.0
25
+ Requires-Dist: pymem>=1.13.1
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Memory Scanner MCP
29
+
30
+ 基于 [pymem](https://github.com/srounet/Pymem) 的内存扫描与修改 MCP 服务器。
31
+
32
+ 通过 MCP 协议将内存读写、扫描、特征码搜索等能力暴露给 AI Agent,实现自然语言驱动的内存修改。
33
+
34
+ ## 功能
35
+
36
+ | 工具 | 说明 |
37
+ |------|------|
38
+ | `list_processes` | 列出运行中的进程 |
39
+ | `attach_process` | 附加到目标进程 |
40
+ | `detach_process` | 断开进程连接 |
41
+ | `get_process_info` | 获取进程详细信息和模块列表 |
42
+ | `read_memory` | 读取指定地址的值 |
43
+ | `write_memory` | 写入值到指定地址 |
44
+ | `scan_memory_first` | 首次扫描(全内存搜索) |
45
+ | `scan_memory_next` | 再次扫描(缩小范围) |
46
+ | `scan_memory_filter` | 条件过滤(大于/小于等) |
47
+ | `write_scan_results` | 批量写入扫描结果 |
48
+ | `get_scan_results` | 查看扫描结果及当前值 |
49
+ | `scan_pattern` | AOB/特征码扫描(支持通配符) |
50
+ | `get_module_base` | 获取模块基地址 |
51
+ | `read_pointer_chain` | 多级指针链读取 |
52
+ | `write_pointer_chain` | 多级指针链写入 |
53
+ | `dump_memory` | 内存转储(Hex + ASCII) |
54
+ | `freeze_address` | 冻结地址值 |
55
+ | `unfreeze_address` | 解除冻结 |
56
+ | `apply_frozen` | 执行冻结写入 |
57
+ | `list_frozen` | 列出冻结列表 |
58
+
59
+ ## 支持的数据类型
60
+
61
+ `int8` `int16` `int32` `int64` `uint8` `uint16` `uint32` `uint64` `float` `double` `string` `bytes`
62
+
63
+ ## 安装
64
+
65
+ ### 从源码安装(推荐开发使用)
66
+
67
+ ```bash
68
+ git clone https://github.com/miloira/MemoryScaner.git
69
+ cd MemoryScaner
70
+ pip install -e .
71
+ ```
72
+
73
+ ### 直接安装
74
+
75
+ ```bash
76
+ pip install memory-scanner-mcp
77
+ ```
78
+
79
+ > 需要 Python 3.10+,仅支持 Windows。
80
+
81
+ ## 使用方式
82
+
83
+ ### 作为命令行工具运行
84
+
85
+ ```bash
86
+ memory-scanner-mcp
87
+ ```
88
+
89
+ ### 作为 Python 模块运行
90
+
91
+ ```bash
92
+ python -m memory_scanner
93
+ ```
94
+
95
+ ### MCP 客户端配置
96
+
97
+ 推荐使用 `uvx`(无需手动安装):
98
+
99
+ ```json
100
+ {
101
+ "mcpServers": {
102
+ "memory-scanner": {
103
+ "command": "uvx",
104
+ "args": ["memory-scanner-mcp"]
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ 也可以先 `pip install` 后直接使用命令:
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "memory-scanner": {
116
+ "command": "memory-scanner-mcp"
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ 或者使用 Python 模块方式:
123
+
124
+ ```json
125
+ {
126
+ "mcpServers": {
127
+ "memory-scanner": {
128
+ "command": "python",
129
+ "args": ["-m", "memory_scanner"]
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ ## 项目结构
136
+
137
+ ```
138
+ MemoryScaner/
139
+ ├── pyproject.toml # 项目配置与依赖管理
140
+ ├── README.md
141
+ ├── LICENSE
142
+ └── src/
143
+ └── memory_scanner/
144
+ ├── __init__.py # 包初始化与版本号
145
+ ├── __main__.py # python -m memory_scanner 入口
146
+ └── server.py # MCP 服务器实现(所有工具定义)
147
+ ```
148
+
149
+ ## 使用示例
150
+
151
+ 典型的内存修改流程(以游戏修改为例):
152
+
153
+ 1. **附加进程**: "附加到 game.exe"
154
+ 2. **首次扫描**: "搜索 int32 类型的值 100"(当前血量为100)
155
+ 3. **改变值**: 在游戏中让血量变化
156
+ 4. **再次扫描**: "搜索新的值 95"(血量变为95)
157
+ 5. **重复缩小**: 直到结果只剩1-2个
158
+ 6. **修改值**: "把找到的地址写入 9999"
159
+ 7. **冻结**: "冻结这个地址为 9999"
160
+
161
+ ## 开发
162
+
163
+ ```bash
164
+ # 克隆项目
165
+ git clone https://github.com/miloira/MemoryScaner.git
166
+ cd MemoryScaner
167
+
168
+ # 创建虚拟环境
169
+ python -m venv .venv
170
+ .venv\Scripts\activate
171
+
172
+ # 安装开发依赖(可编辑模式)
173
+ pip install -e .
174
+ ```
175
+
176
+ ## 注意事项
177
+
178
+ - 必须以 **管理员权限** 运行才能读写其他进程的内存
179
+ - 部分进程有保护机制,可能无法正常读写
180
+ - 冻结功能需要客户端定期调用 `apply_frozen` 来维持
181
+ - 扫描大量内存时可能需要较长时间
182
+
183
+ ## 许可证
184
+
185
+ MIT
@@ -0,0 +1,8 @@
1
+ memory_scanner/__init__.py,sha256=6nujNhnkOj8041tJw_H8g2hbk3B_CXc8Atx5U_Ccpag,115
2
+ memory_scanner/__main__.py,sha256=YaIrgpG44vJOajo3pdA7cbJvddFSbWwofHk4lxs37L4,180
3
+ memory_scanner/server.py,sha256=FCWtKnY5yxRyddLxz5YOUBY60tCnkLg4EH9ZLRQ9hqQ,35224
4
+ memory_scanner_mcp-0.0.1.dist-info/METADATA,sha256=0wzgAjdQ8IHoWj3zKnI0s90GuIIPP56xYm-vOSZFqGk,4848
5
+ memory_scanner_mcp-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ memory_scanner_mcp-0.0.1.dist-info/entry_points.txt,sha256=y5TqQg5wjLOp-KQNeuF47a1bNE9MfUpEmJ-VQX0Tb7k,68
7
+ memory_scanner_mcp-0.0.1.dist-info/licenses/LICENSE,sha256=UIqdT9C9M4nBcCW9dpe2Q_CgyJbCzrQio0HmUkzb5hs,1085
8
+ memory_scanner_mcp-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ memory-scanner-mcp = memory_scanner.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 miloira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.