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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ on:

jobs:
test:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.13"]

steps:
Expand All @@ -29,7 +31,7 @@ jobs:
run: uv run ruff check .

- name: Run type checking
run: uv run mypy api_types.py errors.py server.py
run: uv run mypy code_memory

- name: Run tests
run: uv run pytest tests/ -v --tb=short
Expand Down
32 changes: 17 additions & 15 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ uv sync --all-extras

```bash
# Run the MCP server
uv run mcp run server.py
uv run mcp run code_memory/server.py

# Run with MCP Inspector for debugging
uv run mcp dev server.py
uv run mcp dev code_memory/server.py
```

## Development Workflow
Expand Down Expand Up @@ -73,18 +73,20 @@ uv build

```
code-memory/
├── server.py # MCP server entry point
├── db.py # SQLite database layer
├── parser.py # Tree-sitter code parser
├── doc_parser.py # Markdown documentation parser
├── queries.py # Hybrid retrieval query layer
├── git_search.py # Git history search module
├── errors.py # Custom exception hierarchy
├── validation.py # Input validation functions
├── logging_config.py # Structured logging configuration
├── tests/ # Test suite
├── prompts/ # Milestone prompt files
└── pyproject.toml # Project configuration
├── code_memory/ # Package source
│ ├── server.py # MCP server entry point
│ ├── db.py # SQLite database layer
│ ├── parser.py # Tree-sitter code parser
│ ├── doc_parser.py # Markdown documentation parser
│ ├── queries.py # Hybrid retrieval query layer
│ ├── git_search.py # Git history search module
│ ├── errors.py # Custom exception hierarchy
│ ├── validation.py # Input validation functions
│ ├── logging_config.py # Structured logging configuration
│ └── api_types.py # MCP response TypedDicts
├── tests/ # Test suite
├── prompts/ # Milestone prompt files
└── pyproject.toml # Project configuration
```

## Pull Request Process
Expand All @@ -109,7 +111,7 @@ code-memory/

### Adding a New Tool

1. Add the tool function in `server.py` with the `@mcp.tool()` decorator
1. Add the tool function in `code_memory/server.py` with the `@mcp.tool()` decorator
2. Add input validation using functions from `validation.py`
3. Wrap the implementation in error handling
4. Add logging using `ToolLogger`
Expand Down
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ cd code-memory
uv sync

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

### Pre-built Binaries (Standalone)
Expand Down Expand Up @@ -119,7 +119,7 @@ uvx code-memory

```bash
# Run with the MCP Inspector for interactive debugging
uv run mcp dev server.py
uv run mcp dev code_memory/server.py

# Run tests
uv run pytest tests/ -v
Expand Down Expand Up @@ -382,18 +382,20 @@ search_history(query="server.py", search_type="blame", target_file="server.py",

```
code-memory/
├── server.py # MCP server entry point (FastMCP)
├── db.py # SQLite database layer with sqlite-vec
├── parser.py # Tree-sitter-based code parser
├── doc_parser.py # Markdown documentation parser
├── queries.py # Hybrid retrieval query layer
├── git_search.py # Git history search module
├── errors.py # Custom exception hierarchy
├── validation.py # Input validation functions
├── logging_config.py # Structured logging configuration
├── tests/ # Test suite
├── pyproject.toml # Project metadata & dependencies
└── prompts/ # Milestone prompt engineering files
├── code_memory/ # Package source
│ ├── server.py # MCP server entry point (FastMCP)
│ ├── db.py # SQLite database layer with sqlite-vec
│ ├── parser.py # Tree-sitter-based code parser
│ ├── doc_parser.py # Markdown documentation parser
│ ├── queries.py # Hybrid retrieval query layer
│ ├── git_search.py # Git history search module
│ ├── errors.py # Custom exception hierarchy
│ ├── validation.py # Input validation functions
│ ├── logging_config.py # Structured logging configuration
│ └── api_types.py # MCP response TypedDicts
├── tests/ # Test suite
├── pyproject.toml # Project metadata & dependencies
└── prompts/ # Milestone prompt engineering files
```

## Troubleshooting
Expand Down
22 changes: 12 additions & 10 deletions code-memory.spec
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,17 @@ hidden_imports = [
# Tree-sitter core
'tree_sitter',
# Local modules
'server',
'db',
'parser',
'doc_parser',
'queries',
'git_search',
'errors',
'validation',
'logging_config',
'code_memory',
'code_memory.server',
'code_memory.db',
'code_memory.parser',
'code_memory.doc_parser',
'code_memory.queries',
'code_memory.git_search',
'code_memory.errors',
'code_memory.validation',
'code_memory.logging_config',
'code_memory.api_types',
# GitPython dependencies
'git',
'gitdb',
Expand Down Expand Up @@ -101,7 +103,7 @@ except ImportError:
pass

a = Analysis(
['server.py'],
['code_memory/server.py'],
pathex=[str(PROJECT_ROOT)],
binaries=[],
datas=datas,
Expand Down
1 change: 1 addition & 0 deletions code_memory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""code-memory: a deterministic, high-precision code intelligence MCP server."""
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions doc_parser.py → code_memory/doc_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

from markdown_it import MarkdownIt

import db as db_mod
from parser import SKIP_DIRS, GitignoreMatcher
from . import db as db_mod
from .parser import SKIP_DIRS, GitignoreMatcher

logger = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion errors.py → code_memory/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
import api_types
from . import api_types


class CodeMemoryError(Exception):
Expand Down
2 changes: 1 addition & 1 deletion git_search.py → code_memory/git_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import git
from git.exc import InvalidGitRepositoryError, NoSuchPathError

import errors
from . import errors

# ---------------------------------------------------------------------------
# Helpers
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion parser.py → code_memory/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import pathspec
from tree_sitter import Language, Node, Parser

import db as db_mod
from . import db as db_mod

logger = logging.getLogger(__name__)

Expand Down
4 changes: 2 additions & 2 deletions queries.py → code_memory/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
import logging
import struct

import db as db_mod
import validation as val
from . import db as db_mod
from . import validation as val

logger = logging.getLogger(__name__)

Expand Down
62 changes: 30 additions & 32 deletions server.py → code_memory/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,37 @@

import argparse
import asyncio
from typing import Literal, cast
from typing import Any, Literal, cast

from mcp.server.fastmcp import Context, FastMCP

import api_types
import db as db_mod
import doc_parser as doc_parser_mod
import errors
import logging_config
import parser as parser_mod
import queries
import validation as val
from . import api_types, errors, logging_config, queries
from . import db as db_mod
from . import doc_parser as doc_parser_mod
from . import parser as parser_mod
from . import validation as val

# ── Initialize logging ───────────────────────────────────────────────────
logger = logging_config.setup_logging()
tool_logger = logging_config.get_logger("tools")

# ── Hint messages shown when a tool returns no results because nothing is indexed yet ──
_HINT_CODE_NOT_INDEXED = (
"No results. Codebase may not be indexed. Call index_codebase(directory) first."
)
_HINT_DOCS_NOT_INDEXED = (
"No results. Documentation may not be indexed. Call index_codebase(directory) first."
)


def _hint_if_unindexed(
response: Any, db: Any, *, table: Literal["symbols", "doc_chunks"], hint: str
) -> None:
"""Attach `hint` to `response` if the given table has zero rows."""
if db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] == 0:
response["hint"] = hint


# ── Lazy warmup state ────────────────────────────────────────────────────
_warmup_done = False

Expand Down Expand Up @@ -306,9 +320,7 @@ def search_code(
"results": results,
})
if not results:
symbols_count = database.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
if symbols_count == 0:
topic_response["hint"] = "No results. Codebase may not be indexed. Call index_codebase(directory) first." # type: ignore[typeddict-unknown-key]
_hint_if_unindexed(topic_response, database, table="symbols", hint=_HINT_CODE_NOT_INDEXED)
return topic_response

elif validated_search_type == "definition":
Expand All @@ -321,9 +333,7 @@ def search_code(
"results": results,
})
if not results:
symbols_count = database.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
if symbols_count == 0:
def_response["hint"] = "No results. Codebase may not be indexed. Call index_codebase(directory) first." # type: ignore[typeddict-unknown-key]
_hint_if_unindexed(def_response, database, table="symbols", hint=_HINT_CODE_NOT_INDEXED)
return def_response

elif validated_search_type == "references":
Expand All @@ -336,9 +346,7 @@ def search_code(
"results": results,
})
if not results:
symbols_count = database.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
if symbols_count == 0:
ref_response["hint"] = "No results. Codebase may not be indexed. Call index_codebase(directory) first." # type: ignore[typeddict-unknown-key]
_hint_if_unindexed(ref_response, database, table="symbols", hint=_HINT_CODE_NOT_INDEXED)
return ref_response

elif validated_search_type == "file_structure":
Expand All @@ -351,9 +359,7 @@ def search_code(
"results": results,
})
if not results:
symbols_count = database.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
if symbols_count == 0:
struct_response["hint"] = "No results. Codebase may not be indexed. Call index_codebase(directory) first." # type: ignore[typeddict-unknown-key]
_hint_if_unindexed(struct_response, database, table="symbols", hint=_HINT_CODE_NOT_INDEXED)
return struct_response

return errors.format_error(errors.ValidationError(f"Unknown search_type: {search_type}"))
Expand Down Expand Up @@ -627,9 +633,7 @@ def search_docs(query: str, directory: str, top_k: int = 10) -> api_types.Search
})

if not results:
doc_chunks_count = database.execute("SELECT COUNT(*) FROM doc_chunks").fetchone()[0]
if doc_chunks_count == 0:
response["hint"] = "No results. Documentation may not be indexed. Call index_codebase(directory) first." # type: ignore[typeddict-unknown-key]
_hint_if_unindexed(response, database, table="doc_chunks", hint=_HINT_DOCS_NOT_INDEXED)

return response

Expand Down Expand Up @@ -707,7 +711,7 @@ def search_history(
try:
from git.exc import InvalidGitRepositoryError, NoSuchPathError

import git_search as gs
from . import git_search as gs

# Validate inputs
validated_search_type = val.validate_search_type(
Expand Down Expand Up @@ -908,13 +912,7 @@ def find_dead_code(
})

if result["total_symbols"] == 0:
symbols_count = database.execute(
"SELECT COUNT(*) FROM symbols"
).fetchone()[0]
if symbols_count == 0:
response["hint"] = ( # type: ignore[typeddict-unknown-key]
"Codebase may not be indexed. Call index_codebase(directory) first."
)
_hint_if_unindexed(response, database, table="symbols", hint=_HINT_CODE_NOT_INDEXED)

return response

Expand Down
2 changes: 1 addition & 1 deletion validation.py → code_memory/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import re
from pathlib import Path

from errors import ValidationError
from .errors import ValidationError


def validate_directory(path: str, must_exist: bool = True) -> Path:
Expand Down
Loading
Loading