Skip to content
Closed
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
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@ pip install brainctl

Requires Python 3.11+. SQLite is built-in. No other mandatory dependencies.

Optional features are pulled in via [extras](https://peps.python.org/pep-0508/#extras). Put them in the **same** install command (they are part of the `brainctl` package, not a separate step) — e.g. `pip install 'brainctl[mcp]'`. **To run the `brainctl-mcp` server you must install the `[mcp]` extra**; a plain `pip install brainctl` does not include it.

```bash
pip install brainctl[mcp] # MCP server — 100 visible tools for Claude Desktop, Cursor, VS Code
pip install brainctl[vec] # vector similarity search (sqlite-vec + Ollama)
pip install brainctl[signing] # Ed25519-signed memory exports + optional Solana on-chain pinning
pip install brainctl[all] # everything
pip install 'brainctl[mcp]' # MCP server — 100 visible tools for Claude Desktop, Cursor, VS Code
pip install 'brainctl[vec]' # vector similarity search (sqlite-vec + Ollama)
pip install 'brainctl[signing]' # Ed25519-signed memory exports + optional Solana on-chain pinning
pip install 'brainctl[all]' # everything

# combine extras in one install:
pip install 'brainctl[mcp,vec]'
```

## 5-line example
Expand Down
19 changes: 18 additions & 1 deletion src/agentmemory/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,24 @@
# below at the former _surprise_score_mcp callsite.
from agentmemory._impl import _surprise_score
logger = logging.getLogger(__name__)
from mcp.server import Server
try:
from mcp.server import Server
except ModuleNotFoundError as _mcp_import_err:
# The MCP SDK ships via the optional `[mcp]` extra. A plain
# `pip install brainctl` (or `pipx install brainctl`) doesn't pull it,
# so without this guard `brainctl-mcp` dies with a bare traceback and
# the MCP client only sees "failed to connect". Surface the actual fix.
if (_mcp_import_err.name or "").split(".")[0] != "mcp":
raise
sys.stderr.write(
"brainctl-mcp requires the MCP server extra, which is not installed.\n"
"Install it with:\n\n"
" pip install 'brainctl[mcp]'\n\n"
"Or, if you installed brainctl with pipx:\n\n"
" pipx inject brainctl 'mcp>=1.27.0' starlette 'uvicorn[standard]' "
"python-json-logger\n"
)
raise SystemExit(1)

# Extension tool modules — each exports TOOLS: list[Tool] and DISPATCH: dict
try:
Expand Down
60 changes: 60 additions & 0 deletions tests/test_mcp_missing_extra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Regression test for the `[mcp]` extra not being installed.

`brainctl-mcp` (entry point `agentmemory.mcp_server:run`) used to die with a
bare `ModuleNotFoundError: No module named 'mcp'` traceback when the optional
`[mcp]` extra was absent — the MCP client only saw "failed to connect". The
import is now guarded to exit with an actionable install hint instead.

See issue #159.
"""
from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path

SRC = Path(__file__).resolve().parent.parent / "src"

# A subprocess that hides the `mcp` package (it IS installed in the test env,
# so we block it at import time) and then imports the MCP server module.
_HIDE_MCP_AND_IMPORT = """
import sys

class _BlockMcp:
def find_spec(self, name, path, target=None):
if name == "mcp" or name.startswith("mcp."):
raise ModuleNotFoundError("No module named 'mcp'", name="mcp")
return None

# Drop anything already cached, then refuse future imports of `mcp`.
for _m in [m for m in sys.modules if m == "mcp" or m.startswith("mcp.")]:
del sys.modules[_m]
sys.meta_path.insert(0, _BlockMcp())

import agentmemory.mcp_server # noqa: F401 (import is the thing under test)
"""


def _run_with_mcp_hidden() -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, "-c", _HIDE_MCP_AND_IMPORT],
capture_output=True,
text=True,
timeout=60,
env={**os.environ, "PYTHONPATH": str(SRC)},
)


def test_missing_mcp_extra_exits_with_actionable_message():
proc = _run_with_mcp_hidden()

# Clean exit code, not an unhandled traceback (which would be 1 *with* a
# Traceback dump on stderr).
assert proc.returncode == 1, proc.stderr
assert "Traceback (most recent call last)" not in proc.stderr, proc.stderr

# The message must tell the user exactly how to fix it.
assert "brainctl[mcp]" in proc.stderr
assert "pip install" in proc.stderr
assert "MCP server extra" in proc.stderr