Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
## Version 2 - MARM Protocol to Universal MCP Server Evolution

<details>
<summary><strong>Unreleased: Concept Store Module Split (v2.29.1)</strong></summary>
<summary><strong>Unreleased: Product CLI Module Split (v2.29.1)</strong></summary>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Synchronize the v2.29.1 version surfaces

With this unreleased section as the first changelog entry, python scripts/find-versions.py treats 2.29.1 as the target, while pyproject.toml, runtime constants, server.json, Docker metadata, READMEs, and installation docs all remain at 2.29.0. Consequently, builds from this commit still identify themselves as 2.29.0 and the repository's version audit reports conflicting versions; either leave the unreleased entry unversioned or update every required version surface.

AGENTS.md reference: AGENTS.md:L39-L49

Useful? React with 👍 / 👎.


### Internal

- Split `cli.py`'s remaining output-formatting and argparse-construction concerns into `services/cli_output.py` and `services/cli_parser.py`, continuing the v2.28.0 CLI service-module split. No behavior change; `_dispatch_product`, `main`, and runtime-preset application stay in `cli.py` as the orchestration owner. `cli.py` drops from 786 to 509 lines.
<summary><strong>Unreleased: Concept Store Module Split (v2.29.1)</strong></summary>

- Split MARM Console's `concept_store.py` graph-atlas and single-entity-neighborhood queries into their own modules: `console/concept_graph_overview.py` (`graph_overview`, the full-vs-sampled visual atlas with its degree-ranked BFS tree-sampling) and `console/concept_neighborhood.py` (`neighborhood`, the bounded single-entity BFS traversal). No behavior change; shared low-level helpers (`_connect`, `_schema_status`, `_entity`) and the smaller query functions (`summary`, `search`, `get_entity`, `build_runs`, `get_build_run`, `duplicates`) stay in `concept_store.py`. `concept_store.py` drops from 676 to 334 lines.

</details>
Expand Down
291 changes: 7 additions & 284 deletions marm-mcp-server/marm_mcp_server/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import argparse
import asyncio
import json
import os
import sys
import urllib.error
Expand All @@ -26,6 +25,13 @@
SERVER_VERSION,
)
from .core.rate_limiter import rate_limiter
from .services.cli_output import (
_print_doctor,
_print_maintenance,
_print_payload,
_print_status,
)
from .services.cli_parser import _compatibility_parser, _product_parser
from .utils.dependency_check import check_dependencies
from .utils.security import generate_api_key

Expand Down Expand Up @@ -128,182 +134,6 @@ def apply_runtime_preset(
}


def _add_profile_arguments(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--profile",
choices=("standard", "swarm", "swarm-max", "trusted"),
default="standard",
)
parser.add_argument(
"--rate-limit-rpm",
type=int,
help="Override HTTP rate limit RPM; 0 disables rate limiting",
)


def _add_docker_run_arguments(parser: argparse.ArgumentParser) -> None:
_add_profile_arguments(parser)
parser.add_argument("--port", type=int, default=8001)
parser.add_argument("--data-dir", type=Path, default=Path.home() / ".marm")
parser.add_argument("--name", default="marm-mcp-server")
parser.add_argument("--tag", default="latest")
parser.add_argument("--repo", type=Path, action="append", default=[])
parser.add_argument("--pull", action="store_true")
parser.add_argument("--expose-network", action="store_true")
parser.add_argument("--env-file", type=Path)
parser.add_argument("--memory")
parser.add_argument("--cpus")


def _product_help() -> str:
"""Return the stable, human-oriented root help for the product CLI."""
from .services.product_help import render_product_help

return render_product_help(SERVER_VERSION)


class _ProductArgumentParser(argparse.ArgumentParser):
"""Keep root help stable while retaining argparse for all subcommands."""

def format_help(self) -> str:
return _product_help()


def _product_parser() -> argparse.ArgumentParser:
parser = _ProductArgumentParser(
prog="marm-memory",
description="Run and manage marm-memory locally",
add_help=False,
)
parser.add_argument("-h", "--help", action="help", help="Show help")
parser.add_argument(
"-V",
"--version",
action="version",
version=SERVER_VERSION,
help="Show installed version",
)
subparsers = parser.add_subparsers(
dest="command", required=True, parser_class=argparse.ArgumentParser
)

start = subparsers.add_parser("start", help="Start the local MARM runtime")
_add_profile_arguments(start)
start.add_argument("--foreground", action="store_true")
start.add_argument("--runtime-id", help=argparse.SUPPRESS)

fast_start = subparsers.add_parser(
"fast-start-http", help="Start HTTP, Console, and optional client setup"
)
_add_profile_arguments(fast_start)
fast_start.add_argument("--client", help="Configure a supported MCP client")
fast_start.add_argument("--no-console", action="store_true")
fast_start.add_argument("--no-browser", action="store_true")

http = subparsers.add_parser(
"http", help="Run the HTTP transport in the foreground"
)
_add_profile_arguments(http)
http.add_argument("--runtime-id", help=argparse.SUPPRESS)
http.set_defaults(foreground=True)

subparsers.add_parser("stdio", help="Run the MCP STDIO transport")

stop = subparsers.add_parser("stop", help="Stop the managed MARM runtime")
stop.add_argument("--force", action="store_true")
restart = subparsers.add_parser("restart", help="Restart the managed runtime")
restart.add_argument("--force", action="store_true")
status = subparsers.add_parser("status", help="Show local MARM status")
status.add_argument("--json", action="store_true", dest="as_json")
console = subparsers.add_parser("console", help="Launch MARM Console")
browser = console.add_mutually_exclusive_group()
browser.add_argument("--open", action="store_true", dest="open_browser")
browser.add_argument("--no-open", action="store_false", dest="open_browser")
console.set_defaults(open_browser=True)
console.add_argument("--foreground", action="store_true")
console.add_argument(
"--import-key",
action="store_true",
help="Create a managed authenticated Console browser session",
)
logs = subparsers.add_parser("logs", help="Read managed runtime logs")
logs.add_argument("--follow", action="store_true")
logs.add_argument("--lines", type=int, default=100)
doctor = subparsers.add_parser("doctor", help="Diagnose the local install")
doctor.add_argument("--json", action="store_true", dest="as_json")

knowledge = subparsers.add_parser("knowledge", help="Manage concept extraction")
knowledge_sub = knowledge.add_subparsers(dest="knowledge_command", required=True)
knowledge_sub.add_parser("status")
build = knowledge_sub.add_parser("build")
scope = build.add_mutually_exclusive_group(required=True)
scope.add_argument("--all", action="store_true", dest="search_all")
scope.add_argument("--session")
scope.add_argument("--project")

projects = subparsers.add_parser("projects", help="Manage code indexes")
projects_sub = projects.add_subparsers(dest="projects_command", required=True)
projects_sub.add_parser("list")
index = projects_sub.add_parser("index")
index.add_argument("path")
index.add_argument(
"--mode", choices=("fast", "moderate", "full"), default="moderate"
)
project_status = projects_sub.add_parser("status")
project_status.add_argument("project", nargs="?")
remove = projects_sub.add_parser("remove")
remove.add_argument("project")
remove.add_argument("--confirm", required=True)

maintenance = subparsers.add_parser("maintenance")
maintenance_sub = maintenance.add_subparsers(
dest="maintenance_command", required=True
)
maintenance_status = maintenance_sub.add_parser("status")
maintenance_status.add_argument("--json", action="store_true", dest="as_json")
embeddings = maintenance_sub.add_parser("embeddings")
embeddings_sub = embeddings.add_subparsers(dest="embeddings_command", required=True)
embeddings_sub.add_parser("migrate")

key = subparsers.add_parser("key", help="Manage local bearer authentication")
key_sub = key.add_subparsers(dest="key_command", required=True)
key_sub.add_parser("generate", help="Generate and display an ephemeral key")
key_sub.add_parser("init", help="Create or reuse the managed local key file")
key_sub.add_parser("path", help="Print the managed local key-file path")
key_sub.add_parser("reveal", help="Display the managed local key")

from .services.docker_cli import add_docker_commands

add_docker_commands(subparsers, _add_docker_run_arguments)
upgrade = subparsers.add_parser(
"upgrade", aliases=["update"], help="Check for and install a newer MARM release"
)
upgrade.add_argument("--check", action="store_true")
upgrade.add_argument("--version")
upgrade.add_argument("--yes", action="store_true")
upgrade.add_argument("--json", action="store_true", dest="as_json")

uninstall = subparsers.add_parser(
"uninstall", help="Remove MARM while preserving user data"
)
uninstall.add_argument("--yes", action="store_true")

subparsers.add_parser("version", help="Show installed version")
return parser


def _compatibility_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="MARM MCP Server")
parser.add_argument("--check-deps", action="store_true")
parser.add_argument("--generate-key", action="store_true")
parser.add_argument("--swarm", action="store_true")
parser.add_argument("--swarm-max", action="store_true")
parser.add_argument("--trusted", action="store_true")
parser.add_argument("--rate-limit-rpm", type=int)
parser.add_argument("--migrate-embeddings", action="store_true")
return parser


def _profile_flags(profile: str) -> dict[str, bool]:
return {
"swarm": profile == "swarm",
Expand Down Expand Up @@ -363,113 +193,6 @@ def _log_startup(runtime_config: dict) -> None:
)


def _print_payload(payload: dict[str, Any], *, as_json: bool = False) -> None:
if as_json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print(json.dumps(payload, indent=2))


def _format_size(size_bytes: int | None) -> str:
if size_bytes is None:
return "unknown"
size = float(size_bytes)
for unit in ("B", "KB", "MB", "GB"):
if size < 1024 or unit == "GB":
return f"{size:.1f} {unit}"
size /= 1024
return f"{size_bytes} B"


def _queue_state(queue: dict[str, Any]) -> str:
if not queue.get("enabled", True):
return "disabled"
if queue.get("stopping"):
return "stopping"
return "healthy" if queue.get("running") else "starting"


def _print_status(payload: dict[str, Any]) -> None:
runtime = payload.get("runtime", {})
metadata = runtime.get("metadata", {})
mcp = payload.get("mcp", {})
console = payload.get("console", {})
memory = payload.get("memory", {})
knowledge = payload.get("knowledge", {})
projects = payload.get("projects", {})
queue = payload.get("write_queue")
print(f"MARM Memory {payload.get('version', SERVER_VERSION)}")
print(
f"Runtime: {runtime.get('state', 'unknown')}"
f" | profile: {metadata.get('profile', 'standard')}"
)
print(
f"MCP: {mcp.get('state', 'unknown')}"
f" | http://127.0.0.1:{mcp.get('port', SERVER_PORT)}/mcp"
)
print(
f"Console: {console.get('state', 'unknown')}"
f" | http://127.0.0.1:{console.get('port', 8002)}"
)
if memory.get("error"):
print(f"Memory: unavailable | {memory['error']}")
elif memory.get("exists"):
print(
f"Memory: {memory.get('memories', 0)} records"
f" | {memory.get('sessions', 0)} sessions"
f" | WAL: {memory.get('wal_mode', 'unknown')}"
f" | {_format_size(memory.get('size_bytes'))}"
)
else:
print(f"Memory: no database at {memory.get('path', DEFAULT_DB_PATH)}")
if isinstance(queue, dict):
print(
f"Write queue: {_queue_state(queue)}"
f" | depth: {queue.get('depth', queue.get('queue_depth', 0))}"
)
else:
print("Write queue: runtime stopped")
print(
f"Knowledge: {knowledge.get('state', 'unknown')}"
f" | schema: {knowledge.get('schema', 'unknown')}"
)
print(f"Projects: {projects.get('state', projects.get('status', 'unknown'))}")


def _print_doctor(payload: dict[str, Any]) -> None:
print("MARM Doctor")
for check in payload.get("checks", []):
marker = (
"OK" if check.get("ok") else "WARN" if check.get("optional") else "FAIL"
)
print(f"[{marker}] {check.get('name')}: {check.get('detail')}")
print()
_print_status(payload.get("status", {}))


def _print_maintenance(payload: dict[str, Any]) -> None:
runtime = payload.get("runtime", {})
memory = payload.get("memory_database", {})
embedding = payload.get("embedding", {})
print(f"MARM Maintenance {payload.get('version', SERVER_VERSION)}")
print(f"Runtime: {runtime.get('state', 'unknown')}")
queue = runtime.get("write_queue")
print(
"Write queue: runtime stopped"
if not isinstance(queue, dict)
else f"Write queue: {_queue_state(queue)}"
)
print(
f"Memory DB: {memory.get('path', DEFAULT_DB_PATH)}"
f" | WAL: {memory.get('wal_mode', 'unknown')}"
f" | {_format_size(memory.get('size_bytes'))}"
)
print(
f"Embeddings: {'compatible' if embedding.get('compatible') else 'migration required'}"
f" | {embedding.get('model', 'unknown')}"
)


def _ensure_runtime() -> dict[str, Any]:
from .core.runtime_manager import inspect_runtime, start_background

Expand Down
Loading
Loading