PyMemoryEditor 2.2.1__tar.gz → 3.0.0__tar.gz
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.
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PKG-INFO +56 -13
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/__init__.py +1 -1
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/linux/functions.py +18 -2
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/macos/functions.py +4 -0
- pymemoryeditor-3.0.0/PyMemoryEditor/mcp/__init__.py +68 -0
- pymemoryeditor-3.0.0/PyMemoryEditor/mcp/__main__.py +17 -0
- pymemoryeditor-3.0.0/PyMemoryEditor/mcp/config.py +301 -0
- pymemoryeditor-3.0.0/PyMemoryEditor/mcp/policy.py +311 -0
- pymemoryeditor-3.0.0/PyMemoryEditor/mcp/server.py +513 -0
- pymemoryeditor-3.0.0/PyMemoryEditor/mcp/session.py +545 -0
- pymemoryeditor-3.0.0/PyMemoryEditor/mcp/toolset.py +2231 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/process/abstract.py +24 -6
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/process/scanning.py +17 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/process/util.py +19 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/util/__init__.py +3 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/util/convert.py +154 -10
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/util/scan.py +20 -10
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/win32/functions.py +4 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/README.md +51 -11
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/api/utilities.md +39 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/guide/searching.md +8 -3
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/index.md +1 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/installation.md +44 -0
- pymemoryeditor-3.0.0/docs/mcp.md +323 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/pyproject.toml +37 -3
- pymemoryeditor-3.0.0/tests/conftest.py +33 -0
- pymemoryeditor-3.0.0/tests/mcp_server/__init__.py +1 -0
- pymemoryeditor-3.0.0/tests/mcp_server/conftest.py +416 -0
- pymemoryeditor-3.0.0/tests/mcp_server/test_argument_fuzz.py +451 -0
- pymemoryeditor-3.0.0/tests/mcp_server/test_fake_fidelity.py +739 -0
- pymemoryeditor-3.0.0/tests/mcp_server/test_live.py +232 -0
- pymemoryeditor-3.0.0/tests/mcp_server/test_parsing.py +739 -0
- pymemoryeditor-3.0.0/tests/mcp_server/test_policy.py +228 -0
- pymemoryeditor-3.0.0/tests/mcp_server/test_server.py +736 -0
- pymemoryeditor-3.0.0/tests/mcp_server/test_session.py +590 -0
- pymemoryeditor-3.0.0/tests/mcp_server/test_toolset.py +1720 -0
- pymemoryeditor-3.0.0/tests/memory/test_narrow_width_consistency.py +222 -0
- pymemoryeditor-3.0.0/tests/memory/test_write_span.py +115 -0
- pymemoryeditor-2.2.1/tests/conftest.py +0 -5
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/.flake8 +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/.gitignore +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/.readthedocs.yaml +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/CODE_OF_CONDUCT.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/CONTRIBUTING.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/LICENSE +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/Makefile +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/__main__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/__init__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/_auto_refresh_dialog.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/_icon.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/_widgets.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/application.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/assets/icon.svg +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/cheat_entry.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/cheat_poll_worker.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/cheat_table.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/log_console_dialog.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/main_window.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/memory_map_dialog.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/memory_viewer_dialog.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/modules_dialog.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/open_process_dialog.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/pointer_chain_dialog.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/pointer_scan_dialog.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/results_view.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/scan_types.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/scan_worker.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/scanner_panel.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/threads_dialog.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/app/value_types.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/enums.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/linux/libc.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/linux/process.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/linux/types.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/macos/__init__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/macos/libsystem.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/macos/process.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/macos/types.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/process/__init__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/process/errors.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/process/info.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/process/module_info.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/process/pointer_scan.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/process/region.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/process/remote_pointer.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/process/thread_info.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/py.typed +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/util/pattern.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/util/scan_numpy.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/win32/enums/__init__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/win32/enums/memory_types.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/win32/enums/process_operations.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/win32/process.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/PyMemoryEditor/win32/types.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/SECURITY.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/assets/screenshots/app.png +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/assets/social-preview.html +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/assets/social-preview.png +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/_static/.gitkeep +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/_static/custom.css +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/_templates/layout.html +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/api/enums.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/api/errors.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/api/index.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/api/memory-region.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/api/module-info.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/api/openprocess.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/api/pointer-path.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/api/remote-pointer.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/api/thread-info.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/app.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/conf.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/contributing.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/funding.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/glossary.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/guide/allocate-free.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/guide/index.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/guide/logging.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/guide/memory-regions.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/guide/modules-threads.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/guide/opening-process.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/guide/pattern-scan.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/guide/pointer-scan.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/guide/pointers.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/guide/read-write.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/license.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/platform-notes.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/quickstart.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/requirements.txt +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/troubleshooting.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/docs/why.md +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/app/__init__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/app/test_app_cheat_entry.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/app/test_app_pointer_parsing.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/app/test_app_scan_request.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/app/test_app_smoke.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/app/test_app_value_types.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/app/test_app_widgets.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/app/test_auto_refresh_dialog.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/app/test_cheat_poll_worker.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/app/test_open_process_dialog.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/app/test_refine_scan_worker.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/core/__init__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/core/test_errors.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/core/test_logger.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/memory/__init__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/memory/test_allocate_free.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/memory/test_bufflength_inference.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/memory/test_editor.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/memory/test_partial_io.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/memory/test_read_into.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/memory/test_str_boundary.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/memory/test_str_decode_consistency.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/memory/test_typed_accessors.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/memory/test_write_safety.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/memory/test_write_str_bytes_width.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/platforms/__init__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/platforms/test_bitness.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/platforms/test_bitness_fallback_warns.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/platforms/test_linux_types.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/platforms/test_macos_protect.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/platforms/test_macos_transient_reads.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/platforms/test_win32_permissions.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/pointer/__init__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/pointer/test_pointer_chain.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/pointer/test_pointer_scan.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/pointer/test_remote_pointer.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/process/__init__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/process/test_memory_region.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/process/test_module_enumeration.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/process/test_partial_name_match.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/process/test_process_enumeration.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/process/test_process_lookup.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/process/test_thread_enumeration.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/scan/__init__.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/scan/test_chunking_integration.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/scan/test_pattern_compile.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/scan/test_pattern_scan.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/scan/test_region_snapshot.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/scan/test_scan.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/scan/test_scan_numpy.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/scan/test_scan_properties.py +0 -0
- {pymemoryeditor-2.2.1 → pymemoryeditor-3.0.0}/tests/scan/test_scanning_helper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: PyMemoryEditor
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Read, write and scan process memory in a few lines of Python — Cheat Engine-style scans, pointer chains and AOB search on Windows, Linux and macOS.
|
|
5
5
|
Project-URL: Homepage, https://github.com/JeanExtreme002/PyMemoryEditor
|
|
6
6
|
Project-URL: Documentation, https://pymemoryeditor.readthedocs.io
|
|
@@ -12,7 +12,7 @@ Author-email: Jean Loui Bernard Silva de Jesus <contact@jeanloui.dev>
|
|
|
12
12
|
Maintainer-email: Jean Loui Bernard Silva de Jesus <contact@jeanloui.dev>
|
|
13
13
|
License-Expression: MIT
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Keywords: aob-scan,cheat-engine,ctypes,debugging,game-hacking,introspection,memory-editor,memory-scanner,pattern-scan,pointer-scan,process-memory,reverse-engineering
|
|
15
|
+
Keywords: ai-agent,aob-scan,cheat-engine,ctypes,debugging,game-hacking,introspection,llm-tools,mcp,memory-editor,memory-scanner,model-context-protocol,pattern-scan,pointer-scan,process-memory,reverse-engineering
|
|
16
16
|
Classifier: Development Status :: 5 - Production/Stable
|
|
17
17
|
Classifier: Intended Audience :: Developers
|
|
18
18
|
Classifier: Intended Audience :: Information Technology
|
|
@@ -41,6 +41,7 @@ Provides-Extra: dev
|
|
|
41
41
|
Requires-Dist: build; extra == 'dev'
|
|
42
42
|
Requires-Dist: flake8; extra == 'dev'
|
|
43
43
|
Requires-Dist: hypothesis; extra == 'dev'
|
|
44
|
+
Requires-Dist: mcp<3,>=2; extra == 'dev'
|
|
44
45
|
Requires-Dist: mypy; extra == 'dev'
|
|
45
46
|
Requires-Dist: psutil<7,>=5.9; extra == 'dev'
|
|
46
47
|
Requires-Dist: pyside6>=6.5; extra == 'dev'
|
|
@@ -49,6 +50,8 @@ Requires-Dist: pytest-cov; extra == 'dev'
|
|
|
49
50
|
Requires-Dist: pytest-qt; extra == 'dev'
|
|
50
51
|
Requires-Dist: pytest-xdist; extra == 'dev'
|
|
51
52
|
Requires-Dist: twine; extra == 'dev'
|
|
53
|
+
Provides-Extra: mcp
|
|
54
|
+
Requires-Dist: mcp<3,>=2; extra == 'mcp'
|
|
52
55
|
Provides-Extra: speed
|
|
53
56
|
Requires-Dist: numpy>=1.24; extra == 'speed'
|
|
54
57
|
Provides-Extra: tests
|
|
@@ -115,6 +118,13 @@ selective scans:
|
|
|
115
118
|
```bash
|
|
116
119
|
pip install "PyMemoryEditor[speed]"
|
|
117
120
|
```
|
|
121
|
+
|
|
122
|
+
To let an AI assistant drive the library over the Model Context Protocol, use
|
|
123
|
+
the `mcp` extra (see [below](#let-an-ai-assistant-do-it-mcp)):
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install "PyMemoryEditor[mcp]"
|
|
127
|
+
```
|
|
118
128
|
📖 Full guide at **[Read the Docs](https://pymemoryeditor.readthedocs.io)**.
|
|
119
129
|
|
|
120
130
|
---
|
|
@@ -139,6 +149,38 @@ That's it — read, write or scan another process in three lines, the same way o
|
|
|
139
149
|
|
|
140
150
|
---
|
|
141
151
|
|
|
152
|
+
## Let an AI assistant do it (MCP)
|
|
153
|
+
|
|
154
|
+
PyMemoryEditor ships a **Model Context Protocol server**, so an AI assistant can run the whole loop itself:
|
|
155
|
+
|
|
156
|
+
> *"Find the health value in my game. It's 100 right now."*
|
|
157
|
+
>
|
|
158
|
+
> It scans, asks you to take damage, refines the matches, and hands you the address.
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
pip install "PyMemoryEditor[mcp]"
|
|
162
|
+
claude mcp add pymemoryeditor -- pymemoryeditor-mcp
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Or in any client that reads `mcpServers` JSON (Claude Desktop, editors, …):
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"mcpServers": {
|
|
170
|
+
"pymemoryeditor": {
|
|
171
|
+
"command": "pymemoryeditor-mcp",
|
|
172
|
+
"args": []
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Fourteen tools cover the full workflow, with scan results kept server-side behind a handle, so a 40 000-hit first scan costs a few tokens instead of your whole context.
|
|
179
|
+
|
|
180
|
+
Read the [MCP guide](https://pymemoryeditor.readthedocs.io/en/latest/mcp.html) to learn more.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
142
184
|
## 📖 Documentation
|
|
143
185
|
|
|
144
186
|
Full documentation lives at **[pymemoryeditor.readthedocs.io](https://pymemoryeditor.readthedocs.io)** — installation, the Cheat Engine workflow, every method and parameter, the GUI app guide, platform notes and troubleshooting.
|
|
@@ -146,15 +188,16 @@ Full documentation lives at **[pymemoryeditor.readthedocs.io](https://pymemoryed
|
|
|
146
188
|
A quick map of where to go:
|
|
147
189
|
|
|
148
190
|
<table>
|
|
149
|
-
<tr><td><a href="
|
|
150
|
-
<tr><td><a href="
|
|
151
|
-
<tr><td><a href="
|
|
152
|
-
<tr><td><a href="
|
|
153
|
-
<tr><td><a href="
|
|
154
|
-
<tr><td><a href="
|
|
155
|
-
<tr><td><a href="
|
|
156
|
-
<tr><td><a href="
|
|
157
|
-
<tr><td><a href="
|
|
191
|
+
<tr><td><a href="https://pymemoryeditor.readthedocs.io/en/latest/quickstart.html"><b>Quick Start</b></a></td><td>Open a process, read, write and run your first scan.</td></tr>
|
|
192
|
+
<tr><td><a href="https://pymemoryeditor.readthedocs.io/en/latest/guide/searching.html"><b>Searching memory</b></a></td><td>Value scans, ranges, refining results, the Cheat Engine loop.</td></tr>
|
|
193
|
+
<tr><td><a href="https://pymemoryeditor.readthedocs.io/en/latest/guide/pattern-scan.html"><b>Pattern scan</b></a></td><td>Find code/data with byte signatures (AOB) and regex.</td></tr>
|
|
194
|
+
<tr><td><a href="https://pymemoryeditor.readthedocs.io/en/latest/guide/pointers.html"><b>Pointers</b></a></td><td>Multi-level pointer chains and the live <code>RemotePointer</code>.</td></tr>
|
|
195
|
+
<tr><td><a href="https://pymemoryeditor.readthedocs.io/en/latest/guide/pointer-scan.html"><b>Pointer scan</b></a></td><td>Find static pointers that survive ASLR.</td></tr>
|
|
196
|
+
<tr><td><a href="https://pymemoryeditor.readthedocs.io/en/latest/app.html"><b>The GUI app</b></a></td><td>The bundled Cheat Engine-style scanner.</td></tr>
|
|
197
|
+
<tr><td><a href="https://pymemoryeditor.readthedocs.io/en/latest/mcp.html"><b>The MCP server</b></a></td><td>Let an AI assistant drive the scan/refine loop.</td></tr>
|
|
198
|
+
<tr><td><a href="https://pymemoryeditor.readthedocs.io/en/latest/api/openprocess.html"><b>API reference</b></a></td><td>Every public class, method and parameter.</td></tr>
|
|
199
|
+
<tr><td><a href="https://pymemoryeditor.readthedocs.io/en/latest/platform-notes.html"><b>Platform notes</b></a></td><td>Permissions and quirks on Windows, Linux and macOS.</td></tr>
|
|
200
|
+
<tr><td><a href="https://pymemoryeditor.readthedocs.io/en/latest/troubleshooting.html"><b>Troubleshooting</b></a></td><td>Common errors and how to fix them.</td></tr>
|
|
158
201
|
</table>
|
|
159
202
|
|
|
160
203
|
---
|
|
@@ -176,7 +219,7 @@ A quick map of where to go:
|
|
|
176
219
|
## 🤝 Contributing
|
|
177
220
|
|
|
178
221
|
Pull requests, bug reports and feature ideas are very welcome. Read
|
|
179
|
-
[`CONTRIBUTING.md`](CONTRIBUTING.md) for the development setup, test layout and
|
|
222
|
+
[`CONTRIBUTING.md`](https://github.com/JeanExtreme002/PyMemoryEditor/blob/main/CONTRIBUTING.md) for the development setup, test layout and
|
|
180
223
|
the small set of platform-specific quirks to be aware of.
|
|
181
224
|
|
|
182
225
|
If PyMemoryEditor helped your project, please ⭐ the repo — it's the easiest way to
|
|
@@ -186,4 +229,4 @@ support the work and to help others discover the library.
|
|
|
186
229
|
|
|
187
230
|
## License
|
|
188
231
|
|
|
189
|
-
Released under the [MIT License](LICENSE) — free for personal and commercial use.
|
|
232
|
+
Released under the [MIT License](https://github.com/JeanExtreme002/PyMemoryEditor/blob/main/LICENSE) — free for personal and commercial use.
|
|
@@ -32,6 +32,7 @@ from ..util import (
|
|
|
32
32
|
_validate_pytype,
|
|
33
33
|
as_writable_c_buffer,
|
|
34
34
|
get_c_type_of,
|
|
35
|
+
sign_extend_narrow_int,
|
|
35
36
|
values_to_bytes,
|
|
36
37
|
)
|
|
37
38
|
from ..util.pattern import PatternLike, compile_pattern
|
|
@@ -369,13 +370,23 @@ def read_process_memory(pid: int, address: int, pytype: Type[T], bufflength: int
|
|
|
369
370
|
_validate_pytype(pytype)
|
|
370
371
|
|
|
371
372
|
data = get_c_type_of(pytype, bufflength)
|
|
372
|
-
|
|
373
|
+
# `bufflength`, not `sizeof(data)`. The two differ whenever the width
|
|
374
|
+
# rounds up to a wider C type — `int` at 3 bytes gets a `c_int32` — and
|
|
375
|
+
# passing the buffer size made this backend read 4 bytes where Windows and
|
|
376
|
+
# macOS read 3, so the same call returned a different value per platform
|
|
377
|
+
# (0x11223344 here against 0x223344 there). `get_c_type_of` guarantees
|
|
378
|
+
# `sizeof(data) >= bufflength`, so reading into the front of a larger,
|
|
379
|
+
# zero-initialised buffer is safe and matches the other two backends.
|
|
380
|
+
_process_vm_readv(pid, addressof(data), address, bufflength)
|
|
373
381
|
|
|
374
382
|
if pytype is str:
|
|
375
383
|
return bytes(data).decode("utf-8", errors="replace")
|
|
376
384
|
elif pytype is bytes:
|
|
377
385
|
return bytes(data)
|
|
378
386
|
else:
|
|
387
|
+
# Narrow widths pad with zeroes, which reads a signed value as
|
|
388
|
+
# unsigned -- and a scan for the same bytes is signed.
|
|
389
|
+
sign_extend_narrow_int(data, pytype, bufflength)
|
|
379
390
|
return data.value
|
|
380
391
|
|
|
381
392
|
|
|
@@ -525,7 +536,12 @@ def write_process_memory(
|
|
|
525
536
|
data = get_c_type_of(pytype, bufflength)
|
|
526
537
|
data.value = value.encode() if isinstance(value, str) else value
|
|
527
538
|
|
|
528
|
-
|
|
539
|
+
# `bufflength`, not `sizeof(data)` — this one corrupted memory rather than
|
|
540
|
+
# merely disagreeing. A write of 3 bytes sized a 4-byte `c_int32` and then
|
|
541
|
+
# wrote all four, destroying a byte the caller never asked to touch, while
|
|
542
|
+
# Windows and macOS wrote exactly three. Verified on Linux: `w=3` touched 4
|
|
543
|
+
# bytes and `w=5` touched 8.
|
|
544
|
+
_process_vm_writev(pid, addressof(data), address, bufflength)
|
|
529
545
|
return value
|
|
530
546
|
|
|
531
547
|
|
|
@@ -30,6 +30,7 @@ from ..util import (
|
|
|
30
30
|
_validate_pytype,
|
|
31
31
|
as_writable_c_buffer,
|
|
32
32
|
get_c_type_of,
|
|
33
|
+
sign_extend_narrow_int,
|
|
33
34
|
values_to_bytes,
|
|
34
35
|
)
|
|
35
36
|
from ..util.pattern import PatternLike, compile_pattern
|
|
@@ -587,6 +588,9 @@ def read_process_memory(
|
|
|
587
588
|
elif pytype is bytes:
|
|
588
589
|
return bytes(data)
|
|
589
590
|
else:
|
|
591
|
+
# Narrow widths pad with zeroes, which reads a signed value as
|
|
592
|
+
# unsigned -- and a scan for the same bytes is signed.
|
|
593
|
+
sign_extend_narrow_int(data, pytype, bufflength)
|
|
590
594
|
return data.value
|
|
591
595
|
|
|
592
596
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
A Model Context Protocol server for PyMemoryEditor.
|
|
5
|
+
|
|
6
|
+
Exposes the library's process-memory tools over MCP so an AI assistant can run
|
|
7
|
+
the Cheat Engine loop itself — enumerate processes, attach, scan for a value,
|
|
8
|
+
narrow the matches as the value changes, then read (and optionally write) the
|
|
9
|
+
address it converged on.
|
|
10
|
+
|
|
11
|
+
Install and run::
|
|
12
|
+
|
|
13
|
+
pip install "PyMemoryEditor[mcp]"
|
|
14
|
+
pymemoryeditor-mcp # asks before attaching; writes allowed
|
|
15
|
+
pymemoryeditor-mcp --read-only # never modifies a target
|
|
16
|
+
|
|
17
|
+
Or register it with a client — Claude Code::
|
|
18
|
+
|
|
19
|
+
claude mcp add pymemoryeditor -- pymemoryeditor-mcp
|
|
20
|
+
|
|
21
|
+
...and any client that reads ``mcpServers`` JSON::
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"pymemoryeditor": {
|
|
26
|
+
"command": "pymemoryeditor-mcp",
|
|
27
|
+
"args": []
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
No flags are needed, because consent is asked for where it can actually be
|
|
33
|
+
given: attaching to a process the operator did not pre-approve **asks the
|
|
34
|
+
user**, and every ``write_value`` call is confirmed by their client.
|
|
35
|
+
``--read-only`` drops the write tool entirely, ``--allow-process NAME``
|
|
36
|
+
pre-approves a target so it stops prompting, and ``--allow-any-process`` turns
|
|
37
|
+
prompting off for scripted runs. Read :doc:`the MCP guide </mcp>` before
|
|
38
|
+
pointing it at anything you care about — it runs with your privileges and it is
|
|
39
|
+
**not a sandbox**.
|
|
40
|
+
|
|
41
|
+
Layout: :mod:`~PyMemoryEditor.mcp.toolset` holds the tools (and imports no MCP
|
|
42
|
+
SDK, so it is testable on its own), :mod:`~PyMemoryEditor.mcp.session` the
|
|
43
|
+
process/scan handles, :mod:`~PyMemoryEditor.mcp.policy` the access rules, and
|
|
44
|
+
:mod:`~PyMemoryEditor.mcp.server` the protocol wiring.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from .config import ServerConfig, build_parser, parse_args
|
|
48
|
+
from .policy import AccessDecision, ProcessPolicy
|
|
49
|
+
from .server import INSTRUCTIONS, build_server, main
|
|
50
|
+
from .session import ScanResult, Session, SessionError, SessionStore
|
|
51
|
+
from .toolset import MemoryToolset, ToolError
|
|
52
|
+
|
|
53
|
+
__all__ = (
|
|
54
|
+
"INSTRUCTIONS",
|
|
55
|
+
"AccessDecision",
|
|
56
|
+
"MemoryToolset",
|
|
57
|
+
"ProcessPolicy",
|
|
58
|
+
"ScanResult",
|
|
59
|
+
"ServerConfig",
|
|
60
|
+
"Session",
|
|
61
|
+
"SessionError",
|
|
62
|
+
"SessionStore",
|
|
63
|
+
"ToolError",
|
|
64
|
+
"build_parser",
|
|
65
|
+
"build_server",
|
|
66
|
+
"main",
|
|
67
|
+
"parse_args",
|
|
68
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""``python -m PyMemoryEditor.mcp`` — the same entry point as the console script.
|
|
4
|
+
|
|
5
|
+
Useful when the console script is not on PATH, which is the usual situation
|
|
6
|
+
when an MCP client launches the server from a virtualenv it did not activate::
|
|
7
|
+
|
|
8
|
+
{"command": "/path/to/venv/bin/python",
|
|
9
|
+
"args": ["-m", "PyMemoryEditor.mcp", "--allow-process", "game.exe"]}
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from .server import main
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
sys.exit(main())
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Command-line configuration for the MCP server.
|
|
5
|
+
|
|
6
|
+
The server is launched by an MCP *client* (Claude Code, Claude Desktop, an
|
|
7
|
+
editor plugin) from a JSON config file, not by a human at a prompt. The
|
|
8
|
+
operator writes these flags once, months before the model ever calls a tool,
|
|
9
|
+
and nobody is watching stderr when it does — so the defaults are what actually
|
|
10
|
+
governs the server, and they need to be the ones that make it useful *and*
|
|
11
|
+
keep a human in the loop.
|
|
12
|
+
|
|
13
|
+
Both halves are load-bearing here. The server is a memory **editor**: a
|
|
14
|
+
read-only default made the headline workflow — find a value, change it —
|
|
15
|
+
require a flag nobody discovers, which is a bad trade when the protections that
|
|
16
|
+
matter are elsewhere. Consent is enforced at the two points where it can
|
|
17
|
+
actually be given:
|
|
18
|
+
|
|
19
|
+
* **which process** — attaching to a target the operator did not pre-approve
|
|
20
|
+
asks the user, live, and the prompt says whether writes are possible
|
|
21
|
+
(:mod:`PyMemoryEditor.mcp.policy`);
|
|
22
|
+
* **each write** — ``write_value`` is tagged so a client prompts on every call,
|
|
23
|
+
even in its most permissive auto-approve mode
|
|
24
|
+
(see ``_ALWAYS_ASK`` in :mod:`PyMemoryEditor.mcp.server`).
|
|
25
|
+
|
|
26
|
+
``--read-only`` is there for when you want the guarantee enforced below this
|
|
27
|
+
process rather than promised by it: the write tool is not registered at all, so
|
|
28
|
+
the model never sees it, and on Windows the target is opened with a handle that
|
|
29
|
+
carries no write rights, leaving the kernel to enforce it.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import argparse
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from typing import List, Literal, Optional, Sequence, Tuple, cast
|
|
35
|
+
|
|
36
|
+
from .policy import ProcessPolicy
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
#: The transports the MCP SDK can serve this server over. Spelled as a
|
|
40
|
+
#: ``Literal`` because the SDK's ``run()`` is overloaded per transport — a plain
|
|
41
|
+
#: ``str`` matches none of the overloads and fails type checking at the call
|
|
42
|
+
#: site.
|
|
43
|
+
Transport = Literal["stdio", "sse", "streamable-http"]
|
|
44
|
+
|
|
45
|
+
#: Hard ceiling on the addresses one scan keeps. A first scan for a common
|
|
46
|
+
#: value (``int`` ``0``, ``100``) legitimately matches millions of addresses;
|
|
47
|
+
#: keeping them all would blow out the server's memory for a result set no
|
|
48
|
+
#: refine loop can use anyway. At the cap the scan stops early and says so.
|
|
49
|
+
#:
|
|
50
|
+
#: 100 000 costs 4.2 MB per result set against 2.1 MB, and 21 ms to sort
|
|
51
|
+
#: against 10 ms. The figure that matters is not per set, though: a session
|
|
52
|
+
#: keeps ``MAX_SCANS_PER_SESSION`` (20) of them and a server keeps
|
|
53
|
+
#: ``MAX_OPEN_SESSIONS`` (8) sessions, so the ceiling on retained addresses
|
|
54
|
+
#: goes from ~336 MB to ~672 MB. Reaching it needs 160 capped result sets,
|
|
55
|
+
#: which a long refine chain across several targets can do.
|
|
56
|
+
#:
|
|
57
|
+
#: What it buys: a scan whose true hit count falls between the two ceilings
|
|
58
|
+
#: stops being flagged ``partial``, and refining a truncated set can converge
|
|
59
|
+
#: on an address that was never in it. That case is real but narrow — above
|
|
60
|
+
#: the new ceiling nothing changes, and a common value like ``int 0`` matches
|
|
61
|
+
#: millions and is out of reach at any sane cap.
|
|
62
|
+
#:
|
|
63
|
+
#: What it costs in time depends on the value's density, and an earlier
|
|
64
|
+
#: version of this comment got that wrong. Measured on ``int 0``, reaching
|
|
65
|
+
#: either ceiling took 0.02s against 0.03s — but that is the dense case, where
|
|
66
|
+
#: hits arrive faster than the clock can spend. For a sparser value the scan
|
|
67
|
+
#: has to walk further to collect twice as many hits, so the time roughly
|
|
68
|
+
#: doubles up to the 30-second budget. "Wall clock is unchanged" was true of
|
|
69
|
+
#: one measurement, not of the change.
|
|
70
|
+
DEFAULT_MAX_SCAN_RESULTS = 100_000
|
|
71
|
+
|
|
72
|
+
#: Wall-clock budget for one scan, in seconds. A full address-space scan of a
|
|
73
|
+
#: large process takes minutes — long past the point where an MCP client gives
|
|
74
|
+
#: up on the request and the model starts retrying. Scans check the budget
|
|
75
|
+
#: between region batches and return a partial, explicitly-flagged result
|
|
76
|
+
#: instead of hanging.
|
|
77
|
+
DEFAULT_MAX_SCAN_SECONDS = 30.0
|
|
78
|
+
|
|
79
|
+
#: Bytes of target memory one scan batch covers before the deadline is checked.
|
|
80
|
+
#: Value scans only yield on a *hit*, so a rare value produces no yields for
|
|
81
|
+
#: minutes; the only way to stay interruptible is to drive the scan region
|
|
82
|
+
#: batch by region batch and check the clock between them.
|
|
83
|
+
DEFAULT_SCAN_BATCH_BYTES = 64 * 1024 * 1024
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ServerConfig:
|
|
88
|
+
"""Everything the server's behaviour depends on.
|
|
89
|
+
|
|
90
|
+
:param allow_write: register the memory-mutating tools (``write_value``).
|
|
91
|
+
**On by default** — editing memory is what the library is for, and each
|
|
92
|
+
write is confirmed by the client. Set it off with ``--read-only`` when
|
|
93
|
+
you want the guarantee enforced below this process: the tool then does
|
|
94
|
+
not exist, and on Windows the process handle carries no write rights.
|
|
95
|
+
:param allowed_processes: process names pre-approved so they open without
|
|
96
|
+
prompting — see :class:`~PyMemoryEditor.mcp.policy.ProcessPolicy`.
|
|
97
|
+
Leaving it empty does **not** mean "anything goes": unlisted targets
|
|
98
|
+
prompt the user instead.
|
|
99
|
+
:param allow_any_process: never prompt — open any target the denylist
|
|
100
|
+
permits. For scripted use, where there is nobody to ask.
|
|
101
|
+
:param allow_system_processes: lift the system-process denylist.
|
|
102
|
+
:param max_scan_results: per-scan cap on stored addresses.
|
|
103
|
+
:param max_scan_seconds: per-scan wall-clock budget.
|
|
104
|
+
:param scan_batch_bytes: memory covered per deadline check.
|
|
105
|
+
|
|
106
|
+
:raises ValueError: if ``max_scan_results``, ``max_scan_seconds`` or
|
|
107
|
+
``scan_batch_bytes`` is not positive.
|
|
108
|
+
|
|
109
|
+
.. note::
|
|
110
|
+
The three numeric bounds are validated here as well as in
|
|
111
|
+
``parse_args``, so an embedder constructing this in code gets the same
|
|
112
|
+
answer the CLI operator gets. They used to be checked only at the CLI,
|
|
113
|
+
which meant a config built in Python failed quietly instead of loudly:
|
|
114
|
+
``scan_batch_bytes=0`` made every region its own scan batch (thousands
|
|
115
|
+
of generator setups per scan on a desktop target), and
|
|
116
|
+
``max_scan_results=0`` made every scan return nothing while flagging
|
|
117
|
+
itself partial. Neither is unsafe — that is why the process allowlist,
|
|
118
|
+
where a bad value silently disables the consent prompt, was fixed first
|
|
119
|
+
— but both are indistinguishable from a broken server.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
allow_write: bool = True
|
|
123
|
+
allowed_processes: Tuple[str, ...] = field(default=())
|
|
124
|
+
allow_any_process: bool = False
|
|
125
|
+
allow_system_processes: bool = False
|
|
126
|
+
max_scan_results: int = DEFAULT_MAX_SCAN_RESULTS
|
|
127
|
+
max_scan_seconds: float = DEFAULT_MAX_SCAN_SECONDS
|
|
128
|
+
scan_batch_bytes: int = DEFAULT_SCAN_BATCH_BYTES
|
|
129
|
+
|
|
130
|
+
def __post_init__(self) -> None:
|
|
131
|
+
# Mirrors the three checks in `parse_args`. Kept as ValueError rather
|
|
132
|
+
# than `parser.error`: this constructor is library API, and the CLI
|
|
133
|
+
# already reports its own violations before ever reaching here.
|
|
134
|
+
if self.max_scan_results < 1:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
"max_scan_results must be at least 1 (got %r)." % (self.max_scan_results,)
|
|
137
|
+
)
|
|
138
|
+
if self.max_scan_seconds <= 0:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
"max_scan_seconds must be positive (got %r)." % (self.max_scan_seconds,)
|
|
141
|
+
)
|
|
142
|
+
if self.scan_batch_bytes < 1:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"scan_batch_bytes must be at least 1 (got %r)." % (self.scan_batch_bytes,)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def policy(self) -> ProcessPolicy:
|
|
148
|
+
"""Build the :class:`ProcessPolicy` this configuration describes."""
|
|
149
|
+
return ProcessPolicy(
|
|
150
|
+
allowed_names=self.allowed_processes,
|
|
151
|
+
allow_system=self.allow_system_processes,
|
|
152
|
+
allow_any=self.allow_any_process,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
157
|
+
"""The ``pymemoryeditor-mcp`` argument parser."""
|
|
158
|
+
parser = argparse.ArgumentParser(
|
|
159
|
+
prog="pymemoryeditor-mcp",
|
|
160
|
+
description=(
|
|
161
|
+
"Expose PyMemoryEditor's process-memory tools over the Model "
|
|
162
|
+
"Context Protocol, so an AI assistant can run the Cheat "
|
|
163
|
+
"Engine scan/refine/read loop against a live process."
|
|
164
|
+
),
|
|
165
|
+
epilog=(
|
|
166
|
+
"Needs no flags: it asks you before attaching to a process, and "
|
|
167
|
+
"your client confirms every write. Pass --read-only to remove the "
|
|
168
|
+
"write tool entirely, or --allow-process NAME to skip the attach "
|
|
169
|
+
"prompt for a target you already trust."
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument(
|
|
173
|
+
"--read-only",
|
|
174
|
+
action="store_true",
|
|
175
|
+
dest="read_only",
|
|
176
|
+
help=(
|
|
177
|
+
"do not register the write tool, so the server can read and scan "
|
|
178
|
+
"but never modify a target. On Windows the process handle is also "
|
|
179
|
+
"opened without write rights, so the kernel enforces it."
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
parser.add_argument(
|
|
183
|
+
"--allow-process",
|
|
184
|
+
action="append",
|
|
185
|
+
default=[],
|
|
186
|
+
metavar="NAME",
|
|
187
|
+
dest="allowed_processes",
|
|
188
|
+
help=(
|
|
189
|
+
"pre-approve processes whose name contains NAME (case-insensitive), "
|
|
190
|
+
"so attaching to them never prompts. Repeatable. Omitting it does "
|
|
191
|
+
"not open the server up: unlisted targets ask for your approval at "
|
|
192
|
+
"the moment they are needed."
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
parser.add_argument(
|
|
196
|
+
"--allow-any-process",
|
|
197
|
+
action="store_true",
|
|
198
|
+
help=(
|
|
199
|
+
"never ask — open any target the system denylist permits. Use it "
|
|
200
|
+
"for scripted or non-interactive runs, where no one is there to "
|
|
201
|
+
"answer a prompt. Interactively, prefer the default: it asks once "
|
|
202
|
+
"per process and can remember your answer."
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
parser.add_argument(
|
|
206
|
+
"--allow-system-processes",
|
|
207
|
+
action="store_true",
|
|
208
|
+
help=(
|
|
209
|
+
"lift the built-in denylist of OS/credential processes "
|
|
210
|
+
"(lsass.exe, launchd, systemd, ...). Rarely what you want."
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
parser.add_argument(
|
|
214
|
+
"--scan-batch-bytes",
|
|
215
|
+
type=int,
|
|
216
|
+
default=DEFAULT_SCAN_BATCH_BYTES,
|
|
217
|
+
metavar="BYTES",
|
|
218
|
+
help=(
|
|
219
|
+
"target memory covered between deadline checks during a scan "
|
|
220
|
+
"(default: %(default)s). Smaller means a scan gives up closer to "
|
|
221
|
+
"its time budget, at the cost of more per-batch overhead. The "
|
|
222
|
+
"field existed and was honoured, but had no flag — so it could "
|
|
223
|
+
"only be set from Python."
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
parser.add_argument(
|
|
227
|
+
"--max-scan-results",
|
|
228
|
+
type=int,
|
|
229
|
+
default=DEFAULT_MAX_SCAN_RESULTS,
|
|
230
|
+
metavar="N",
|
|
231
|
+
help="addresses one scan may keep (default: %(default)s).",
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"--max-scan-seconds",
|
|
235
|
+
type=float,
|
|
236
|
+
default=DEFAULT_MAX_SCAN_SECONDS,
|
|
237
|
+
metavar="SECONDS",
|
|
238
|
+
help="wall-clock budget for one scan (default: %(default)s).",
|
|
239
|
+
)
|
|
240
|
+
parser.add_argument(
|
|
241
|
+
"--transport",
|
|
242
|
+
choices=("stdio", "sse", "streamable-http"),
|
|
243
|
+
default="stdio",
|
|
244
|
+
help=(
|
|
245
|
+
"MCP transport (default: %(default)s). stdio is what desktop "
|
|
246
|
+
"clients launch; the HTTP transports are for remote hosting and "
|
|
247
|
+
"expose this machine's memory to whoever can reach the port."
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
return parser
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def parse_args(
|
|
254
|
+
argv: Optional[Sequence[str]] = None,
|
|
255
|
+
) -> Tuple[ServerConfig, Transport]:
|
|
256
|
+
"""Parse ``argv`` into a :class:`ServerConfig` and a transport name."""
|
|
257
|
+
args = build_parser().parse_args(argv)
|
|
258
|
+
|
|
259
|
+
if args.max_scan_results < 1:
|
|
260
|
+
build_parser().error("--max-scan-results must be at least 1.")
|
|
261
|
+
if args.max_scan_seconds <= 0:
|
|
262
|
+
build_parser().error("--max-scan-seconds must be positive.")
|
|
263
|
+
if args.scan_batch_bytes < 1:
|
|
264
|
+
# 0 or negative makes every region its own batch, so a desktop target
|
|
265
|
+
# with thousands of regions pays thousands of generator setups per
|
|
266
|
+
# scan — and the operator would get no error at launch.
|
|
267
|
+
build_parser().error("--scan-batch-bytes must be at least 1.")
|
|
268
|
+
|
|
269
|
+
# Stripped, not merely tested for blankness: a JSON args array carrying
|
|
270
|
+
# "notepad.exe " (trailing space) used to be stored verbatim, so the
|
|
271
|
+
# substring matched no process at all — the pre-approval silently did
|
|
272
|
+
# nothing while the operator kept being prompted.
|
|
273
|
+
allowed: List[str] = [
|
|
274
|
+
stripped
|
|
275
|
+
for stripped in (name.strip() for name in args.allowed_processes)
|
|
276
|
+
if stripped
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
config = ServerConfig(
|
|
280
|
+
allow_write=not args.read_only,
|
|
281
|
+
allowed_processes=tuple(allowed),
|
|
282
|
+
allow_any_process=args.allow_any_process,
|
|
283
|
+
allow_system_processes=args.allow_system_processes,
|
|
284
|
+
max_scan_results=args.max_scan_results,
|
|
285
|
+
max_scan_seconds=args.max_scan_seconds,
|
|
286
|
+
scan_batch_bytes=args.scan_batch_bytes,
|
|
287
|
+
)
|
|
288
|
+
# argparse's `choices` already constrains this to the three names; the cast
|
|
289
|
+
# just carries that guarantee into the type system.
|
|
290
|
+
return config, cast(Transport, args.transport)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
__all__ = (
|
|
294
|
+
"DEFAULT_MAX_SCAN_RESULTS",
|
|
295
|
+
"Transport",
|
|
296
|
+
"DEFAULT_MAX_SCAN_SECONDS",
|
|
297
|
+
"DEFAULT_SCAN_BATCH_BYTES",
|
|
298
|
+
"ServerConfig",
|
|
299
|
+
"build_parser",
|
|
300
|
+
"parse_args",
|
|
301
|
+
)
|