Skip to content
Open
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
82 changes: 81 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,18 @@ uv run ruff check .
uv run ruff format .

# Build package
uv build
uv build# Clone the repo
git clone https://github.com/kapillamba4/code-memory.git
cd code-memory

# Install dependencies
uv sync

# Run the MCP server (stdio transport)
uv run mcp run server.py

# Run the MCP server (SSE transport for shared usage)
uv run python server.py --transport sse

Comment on lines +132 to 144
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

The development code block is malformed: uv build# Clone the repo concatenates two commands, and the subsequent clone/install/run instructions duplicate the existing “From Source” section earlier in the README. This breaks the rendered docs and makes the quickstart instructions confusing; split uv build onto its own line and move/remove the duplicated clone/setup steps (keep each set of instructions in one place).

Suggested change
uv build# Clone the repo
git clone https://github.com/kapillamba4/code-memory.git
cd code-memory
# Install dependencies
uv sync
# Run the MCP server (stdio transport)
uv run mcp run server.py
# Run the MCP server (SSE transport for shared usage)
uv run python server.py --transport sse
uv build

Copilot uses AI. Check for mistakes.
# Build standalone binary (requires pyinstaller)
pip install pyinstaller
Expand Down Expand Up @@ -228,8 +239,77 @@ For Windows:
}
```

## Shared SSE Server (Reduce Memory Usage)

By default, each MCP host project launches its own `code-memory` process, which loads the embedding model (~1–2 GB) once per project. To avoid this, you can run a **single shared instance** over SSE (Server-Sent Events) and point all your MCP hosts at it.

### Start the shared server

```bash
# Using uvx (recommended)
uvx code-memory --transport sse

# Custom port and host
uvx code-memory --transport sse --port 8765 --host 127.0.0.1

# Using standalone binary
./code-memory-linux-x86_64 --transport sse
```

The server listens on `http://127.0.0.1:8765/sse` by default.

### Configure MCP hosts to use the shared server

Instead of launching a new process, point your MCP host at the running SSE endpoint.

#### Claude Desktop

```json
{
"mcpServers": {
"code-memory": {
"url": "http://127.0.0.1:8765/sse"
}
}
}
```

#### VS Code (Copilot / Continue)

```json
{
"servers": {
"code-memory": {
"url": "http://127.0.0.1:8765/sse"
}
}
}
```

#### Claude Code (CLI) — `.mcp.json`

```json
{
"mcpServers": {
"code-memory": {
"url": "http://127.0.0.1:8765/sse"
}
}
}
```

> **Tip:** Configure `uvx code-memory --transport sse` to start via a single-instance service manager (e.g. systemd user service, launchd agent, or another one-time login/startup mechanism) so the shared server starts automatically.

## Configuration

### CLI Options

| Option | Description | Default |
|--------|-------------|---------|
| `--transport` | Transport protocol: `stdio` or `sse` | `stdio` |
| `--port` | Port for SSE transport (only when `--transport sse` is used) | `8765` |
| `--host` | Host/bind address for SSE transport (only when `--transport sse` is used) | `127.0.0.1` |

### Environment Variables

| Variable | Description | Default |
Expand Down
36 changes: 35 additions & 1 deletion server.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,10 +772,44 @@ def search_history(


# ── Entrypoint ────────────────────────────────────────────────────────────
def build_arg_parser() -> "argparse.ArgumentParser":
"""Build and return the CLI argument parser for code-memory."""
import argparse

parser = argparse.ArgumentParser(
prog="code-memory",
description="code-memory MCP server",
)
parser.add_argument(
"--transport",
choices=["stdio", "sse"],
default="stdio",
help="Transport protocol to use (default: stdio). Use 'sse' to run a shared HTTP server.",
)
parser.add_argument(
"--port",
type=int,
default=8765,
help="Port for SSE transport (default: 8765). Only used when --transport=sse.",
)
parser.add_argument(
"--host",
default="127.0.0.1",
help="Host for SSE transport (default: 127.0.0.1). Only used when --transport=sse.",
)
return parser


def main():
"""Entry point for the MCP server when installed as a package."""
args = build_arg_parser().parse_args()

if args.transport == "sse":
mcp.settings.host = args.host
mcp.settings.port = args.port

# Warmup is now done lazily on first index_codebase call
mcp.run()
mcp.run(transport=args.transport)


if __name__ == "__main__":
Expand Down
107 changes: 107 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Tests for the main() entrypoint CLI argument parsing."""

from __future__ import annotations

import argparse
from unittest.mock import MagicMock, patch

import pytest

from server import build_arg_parser


class TestMainArgParsing:
"""Tests for CLI argument parsing in main()."""

def _parse_args(self, argv: list[str]) -> argparse.Namespace:
"""Parse args using the real CLI parser from server.py."""
Comment on lines +10 to +17
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

from server import build_arg_parser imports server.py at test module import time. Since server.py imports db.py, which imports sqlite_vec at module import time, these arg-parsing tests will error (not skip) in environments where sqlite_vec isn't installed. To make the tests robust, either import server lazily inside the tests after pytest.importorskip(...), or move build_arg_parser() into a lightweight module that doesn’t import the full server stack.

Suggested change
from server import build_arg_parser
class TestMainArgParsing:
"""Tests for CLI argument parsing in main()."""
def _parse_args(self, argv: list[str]) -> argparse.Namespace:
"""Parse args using the real CLI parser from server.py."""
class TestMainArgParsing:
"""Tests for CLI argument parsing in main()."""
def _parse_args(self, argv: list[str]) -> argparse.Namespace:
"""Parse args using the real CLI parser from server.py."""
pytest.importorskip("sqlite_vec", reason="sqlite_vec not installed")
from server import build_arg_parser

Copilot uses AI. Check for mistakes.
return build_arg_parser().parse_args(argv)

def test_default_transport_is_stdio(self):
"""Default transport should be stdio."""
args = self._parse_args([])
assert args.transport == "stdio"

def test_default_port_is_8765(self):
"""Default SSE port should be 8765."""
args = self._parse_args([])
assert args.port == 8765

def test_default_host_is_localhost(self):
"""Default SSE host should be 127.0.0.1."""
args = self._parse_args([])
assert args.host == "127.0.0.1"

def test_sse_transport_flag(self):
"""--transport sse should set transport to sse."""
args = self._parse_args(["--transport", "sse"])
assert args.transport == "sse"

def test_custom_port(self):
"""--port should accept a custom port number."""
args = self._parse_args(["--transport", "sse", "--port", "9000"])
assert args.port == 9000

def test_custom_host(self):
"""--host should accept a custom host."""
args = self._parse_args(["--transport", "sse", "--host", "0.0.0.0"])
assert args.host == "0.0.0.0"

def test_invalid_transport_raises_error(self):
"""Invalid transport should raise SystemExit."""
with pytest.raises(SystemExit):
self._parse_args(["--transport", "invalid"])


class TestMainRunsBehavior:
"""Tests that main() calls mcp.run() with the correct arguments."""

@pytest.fixture(autouse=True)
def require_server(self):
"""Skip tests when server module dependencies are unavailable."""
pytest.importorskip("sqlite_vec", reason="sqlite_vec not installed")

@pytest.fixture
def server_main(self):
"""Import and return the server.main function."""
import importlib

import server as server_mod
importlib.reload(server_mod)
return server_mod.main

def test_stdio_calls_run_with_stdio(self, server_main):
"""main() with no args should call mcp.run(transport='stdio')."""
import server as server_mod
with patch("sys.argv", ["code-memory"]):
with patch.object(server_mod, "mcp") as mock_mcp:
mock_mcp.settings = MagicMock()
server_main()
mock_mcp.run.assert_called_once_with(transport="stdio")

def test_sse_calls_run_with_sse(self, server_main):
"""main() with --transport sse should call mcp.run(transport='sse')."""
import server as server_mod
with patch("sys.argv", ["code-memory", "--transport", "sse"]):
with patch.object(server_mod, "mcp") as mock_mcp:
mock_mcp.settings = MagicMock()
server_main()
mock_mcp.run.assert_called_once_with(transport="sse")

def test_sse_sets_port_on_settings(self, server_main):
"""main() with --transport sse --port 9000 should set mcp.settings.port."""
import server as server_mod
with patch("sys.argv", ["code-memory", "--transport", "sse", "--port", "9000"]):
with patch.object(server_mod, "mcp") as mock_mcp:
mock_mcp.settings = MagicMock()
server_main()
assert mock_mcp.settings.port == 9000

def test_sse_sets_host_on_settings(self, server_main):
"""main() with --transport sse --host 0.0.0.0 should set mcp.settings.host."""
import server as server_mod
with patch("sys.argv", ["code-memory", "--transport", "sse", "--host", "0.0.0.0"]):
with patch.object(server_mod, "mcp") as mock_mcp:
mock_mcp.settings = MagicMock()
server_main()
assert mock_mcp.settings.host == "0.0.0.0"
6 changes: 5 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading