diff --git a/PyMemoryEditor/app/application.py b/PyMemoryEditor/app/application.py
index bcc65ba..a67a19e 100644
--- a/PyMemoryEditor/app/application.py
+++ b/PyMemoryEditor/app/application.py
@@ -559,7 +559,20 @@ def main_cli(argv=None):
Kept separate from :func:`main` so that process-wide change only happens
when the app *is* the process, never when it is embedded in someone else's.
+
+ ``pymemoryeditor mcp [flags]`` runs the MCP server instead, the same as
+ ``pymemoryeditor-mcp [flags]``. MCP registries launch a PyPI package by its
+ project name (``uvx --from "PyMemoryEditor[mcp]" pymemoryeditor mcp``), so
+ this is the only command they can reach. It never touches Qt, and the
+ server keeps its own signal handling.
"""
+ if argv is None:
+ argv = sys.argv
+ if len(argv) > 1 and argv[1].strip() == "mcp":
+ from PyMemoryEditor.mcp.server import main as mcp_main
+
+ return mcp_main(argv[2:])
+
with _scoped_signal_handler(signal.SIGINT, signal.SIG_DFL):
return main(argv)
diff --git a/README.md b/README.md
index b20fd69..1d32b52 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# PyMemoryEditor
+
+
A pure-Python library (built on [ctypes](https://docs.python.org/3/library/ctypes.html)) that lets you **inspect, modify and search the memory of any running process in a few lines of Python** — Cheat Engine workflows on Windows, Linux and macOS!
---
diff --git a/docs/mcp.md b/docs/mcp.md
index b8d025f..a25194e 100644
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -85,6 +85,9 @@ pymemoryeditor-mcp --allow-process game.exe # never asks for game.exe
pymemoryeditor-mcp --allow-any-process # never asks at all (scripts/CI)
```
+`pymemoryeditor mcp` is the same command, flags included: it is how clients
+that install from the MCP Registry start the server.
+
## Flags
diff --git a/pyproject.toml b/pyproject.toml
index 9ac1b90..a043d7f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -166,6 +166,7 @@ exclude = [
"/.coveragerc-lib",
"/.coveragerc-mcp",
"/scripts",
+ "/server.json",
]
# hatchling>=1.27 is required for PEP 639 metadata: the SPDX ``license = "MIT"``
diff --git a/server.json b/server.json
new file mode 100644
index 0000000..75fa304
--- /dev/null
+++ b/server.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
+ "name": "io.github.JeanExtreme002/PyMemoryEditor",
+ "title": "PyMemoryEditor",
+ "description": "Read, write and scan process memory, Cheat Engine style, on Windows, Linux and macOS.",
+ "repository": {
+ "url": "https://github.com/JeanExtreme002/PyMemoryEditor",
+ "source": "github"
+ },
+ "websiteUrl": "https://pymemoryeditor.readthedocs.io/en/latest/mcp.html",
+ "version": "3.0.1",
+ "packages": [
+ {
+ "registryType": "pypi",
+ "registryBaseUrl": "https://pypi.org",
+ "identifier": "pymemoryeditor",
+ "version": "3.0.1",
+ "runtimeHint": "uvx",
+ "runtimeArguments": [
+ {
+ "type": "named",
+ "name": "--from",
+ "value": "PyMemoryEditor[mcp]==3.0.1",
+ "description": "Install the MCP extra, pinned to this release."
+ }
+ ],
+ "packageArguments": [
+ {
+ "type": "positional",
+ "value": "mcp"
+ }
+ ],
+ "transport": {
+ "type": "stdio"
+ }
+ }
+ ]
+}
diff --git a/tests/mcp_server/test_registry.py b/tests/mcp_server/test_registry.py
new file mode 100644
index 0000000..7867443
--- /dev/null
+++ b/tests/mcp_server/test_registry.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+
+"""
+The MCP Registry listing (server.json) and the command it makes clients run.
+
+A registry client starts a PyPI server as ``uvx
+``, with the identifier being the PyPI project. So the listing
+only works while four things agree: the server name in server.json and the
+``mcp-name`` marker in the README that PyPI shows (the registry's ownership
+check), the versions in server.json and the package, and the console script
+the identifier names, which must start the server when given ``mcp``.
+"""
+
+import json
+import re
+import sys
+from pathlib import Path
+
+import pytest
+
+import PyMemoryEditor
+
+ROOT = Path(__file__).resolve().parents[2]
+
+
+@pytest.fixture(scope="module")
+def server():
+ return json.loads((ROOT / "server.json").read_text(encoding="utf-8"))
+
+
+@pytest.fixture(scope="module")
+def package(server):
+ (package,) = server["packages"]
+ return package
+
+
+def test_readme_carries_the_registry_name(server):
+ readme = (ROOT / "README.md").read_text(encoding="utf-8")
+
+ assert re.findall(r"mcp-name: (\S+) -->", readme) == [server["name"]]
+
+
+def test_versions_follow_the_package(server, package):
+ version = PyMemoryEditor.__version__
+
+ assert server["version"] == package["version"] == version
+ assert {"type": "named", "name": "--from"}.items() <= package["runtimeArguments"][0].items()
+ assert package["runtimeArguments"][0]["value"] == "PyMemoryEditor[mcp]==" + version
+
+
+def test_the_command_a_client_builds_starts_the_server(package):
+ pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
+ scripts = re.search(r"\[project\.scripts\](.*?)\n\[", pyproject, re.S).group(1)
+
+ # uvx looks the executable up by its exact name, and the console scripts
+ # are lowercase: "PyMemoryEditor" would not be found on Linux.
+ assert re.search(r'^{} = "PyMemoryEditor\.app\.application:main_cli"$'.format(package["identifier"]), scripts, re.M)
+ assert package["runtimeHint"] == "uvx"
+ assert package["packageArguments"] == [{"type": "positional", "value": "mcp"}]
+ assert package["transport"] == {"type": "stdio"}
+
+
+def test_description_fits_the_registry_limit(server):
+ # The registry rejects a description over 100 characters at publish time.
+ assert len(server["description"]) <= 100
+
+
+def test_mcp_subcommand_runs_the_server_without_qt(monkeypatch):
+ from PyMemoryEditor.app import application
+ from PyMemoryEditor.mcp import server as mcp_server
+
+ received = []
+ monkeypatch.setattr(mcp_server, "main", lambda argv: received.append(list(argv)) or 0)
+ monkeypatch.setattr(application, "main", lambda argv: pytest.fail("the Qt app started"))
+ monkeypatch.setitem(sys.modules, "PySide6", None) # any Qt import would raise
+
+ assert application.main_cli(["pymemoryeditor", "mcp", "--read-only", "--allow-process", "game.exe"]) == 0
+ assert received == [["--read-only", "--allow-process", "game.exe"]]
+
+
+def test_other_arguments_still_reach_the_app(monkeypatch):
+ from PyMemoryEditor.app import application
+
+ received = []
+ monkeypatch.setattr(application, "main", lambda argv: received.append(argv))
+
+ application.main_cli(["pymemoryeditor", "--version"])
+ assert received == [["pymemoryeditor", "--version"]]